diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 518823b42f..8a44e995a6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -96,6 +96,9 @@ jobs: rpcimportable: runs-on: ubuntu-20.04 timeout-minutes: 15 + # do not run this on forks as they are not installable + # it will still be check in the merge queue in this case + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'zeta-chain/node' steps: - uses: actions/checkout@v4 - name: Set up Go @@ -105,8 +108,9 @@ jobs: - name: go get node working-directory: contrib/rpcimportable run: go get github.com/zeta-chain/node@${{github.event.pull_request.head.sha || github.sha}} - env: + env: GOPROXY: direct + GOSUMDB: off - name: go mod tidy working-directory: contrib/rpcimportable run: go mod tidy diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index e6de47d1a5..a8b324de6b 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -48,9 +48,9 @@ jobs: runs-on: ${{ vars.RELEASE_RUNNER }} steps: - uses: actions/checkout@v4 - - name: Release build dry-run + - name: Build release snapshot run: | - make release-dry-run + make release-snapshot check-changelog: needs: diff --git a/.github/workflows/reusable-e2e.yml b/.github/workflows/reusable-e2e.yml index aaac140960..88c42466a5 100644 --- a/.github/workflows/reusable-e2e.yml +++ b/.github/workflows/reusable-e2e.yml @@ -40,7 +40,7 @@ jobs: - name: Login to Docker Hub registry uses: docker/login-action@v3 - if: (github.event_name == 'push' && github.repository == 'zeta-chain/node') || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == 'zeta-chain/node') + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'zeta-chain/node' with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_READ_ONLY }} diff --git a/.github/workflows/sast-linters.yml b/.github/workflows/sast-linters.yml index 26c8fb895d..7b09472298 100644 --- a/.github/workflows/sast-linters.yml +++ b/.github/workflows/sast-linters.yml @@ -25,7 +25,7 @@ jobs: fetch-depth: 0 - name: Run Gosec Security Scanner - uses: zeta-chain/gosec@v2.21.0-zeta + uses: zeta-chain/gosec@v2.21.4-zeta2 with: args: -exclude-generated -exclude-dir testutil ./... diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml index fcffcf7855..bcefc3da55 100644 --- a/.github/workflows/semgrep.yml +++ b/.github/workflows/semgrep.yml @@ -17,7 +17,9 @@ jobs: container: image: ghcr.io/zeta-chain/semgrep-semgrep:1.90.0 - if: (github.actor != 'dependabot[bot]') + if: | + github.actor != 'dependabot[bot]' && + (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'zeta-chain/node') steps: - uses: actions/checkout@v4 - name: Checkout semgrep-utilities repo diff --git a/.github/workflows/sim.yaml b/.github/workflows/sim.yaml new file mode 100644 index 0000000000..11e6858a81 --- /dev/null +++ b/.github/workflows/sim.yaml @@ -0,0 +1,111 @@ +name: sim + +on: + push: + branches: + - develop + pull_request: + types: [opened, synchronize, labeled] + schedule: + - cron: "0 6 * * *" + workflow_dispatch: + inputs: + make-targets: + description: 'Comma separated list of make targets to run (e.g., test-sim-nondeterminism, test-sim-fullappsimulation)' + required: true + default: 'test-sim-nondeterminism' + +concurrency: + group: simulation-${{ github.head_ref || github.sha }} + cancel-in-progress: true + +jobs: + changed-files: + runs-on: ubuntu-latest + outputs: + modified_files: ${{ steps.changes.outputs.modified_files }} + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Get changed files in x directory + id: changes + run: | + echo "::set-output name=modified_files::$(git diff --name-only --diff-filter=ACMRT ${{ github.event.pull_request.base.sha }} ${{ github.sha }} | grep '^x/' | xargs)" + + matrix-conditionals: + needs: changed-files + if: | + contains(github.event.pull_request.labels.*.name, 'SIM_TESTS') || needs.changed-files.outputs.modified_files + runs-on: ubuntu-22.04 + outputs: + SIM_TEST_NOND: ${{ steps.matrix-conditionals.outputs.SIM_TEST_NOND }} + SIM_TEST_FULL: ${{ steps.matrix-conditionals.outputs.SIM_TEST_FULL }} + SIM_TEST_IMPORT_EXPORT: ${{ steps.matrix-conditionals.outputs.SIM_TEST_IMPORT_EXPORT }} + SIM_TEST_AFTER_IMPORT: ${{ steps.matrix-conditionals.outputs.SIM_TEST_AFTER_IMPORT }} + steps: + - id: matrix-conditionals + uses: actions/github-script@v7 + with: + script: | + const makeTargetsInput = context.payload.inputs ? context.payload.inputs['make-targets'] : null; + const defaultTargets = ['test-sim-nondeterminism', 'test-sim-fullappsimulation', 'test-sim-import-export', 'test-sim-after-import']; + + const makeTargets = makeTargetsInput ? makeTargetsInput.split(',') : defaultTargets; + + core.setOutput('SIM_TEST_NOND', makeTargets.includes('test-sim-nondeterminism')); + core.setOutput('SIM_TEST_FULL', makeTargets.includes('test-sim-fullappsimulation')); + core.setOutput('SIM_TEST_IMPORT_EXPORT', makeTargets.includes('test-sim-import-export')); + core.setOutput('SIM_TEST_AFTER_IMPORT', makeTargets.includes('test-sim-after-import')); + + simulation-tests: + needs: + - matrix-conditionals + if: | + contains(github.event.pull_request.labels.*.name, 'SIM_TESTS') || needs.changed-files.outputs.modified_files + runs-on: ubuntu-22.04 + strategy: + fail-fast: false + matrix: + include: + - make-target: "test-sim-nondeterminism" + condition: ${{ needs.matrix-conditionals.outputs.SIM_TEST_NOND == 'true' }} + - make-target: "test-sim-fullappsimulation" + condition: ${{ needs.matrix-conditionals.outputs.SIM_TEST_FULL == 'true' }} + - make-target: "test-sim-import-export" + condition: ${{ needs.matrix-conditionals.outputs.SIM_TEST_IMPORT_EXPORT == 'true' }} + - make-target: "test-sim-after-import" + condition: ${{ needs.matrix-conditionals.outputs.SIM_TEST_AFTER_IMPORT == 'true' }} + name: ${{ matrix.make-target }} + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.22' + + - name: Install dependencies + run: make runsim + + - name: Run ${{ matrix.make-target }} + if: ${{ matrix.condition }} + run: | + make ${{ matrix.make-target }} + + sim-ok: + needs: + - simulation-tests + if: | + contains(github.event.pull_request.labels.*.name, 'SIM_TESTS') || needs.changed-files.outputs.modified_files + runs-on: ubuntu-22.04 + steps: + - name: Aggregate Results + run: | + result="${{ needs.simulation-tests.result }}" + if [[ $result == "success" || $result == "skipped" ]]; then + exit 0 + else + exit 1 + fi diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 37cdbcb8c2..56cab7608f 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -54,11 +54,16 @@ builds: - -X github.com/cosmos/cosmos-sdk/version.ClientName=zetaclientd - -X github.com/cosmos/cosmos-sdk/version.Version={{ .Version }} - -X github.com/cosmos/cosmos-sdk/version.Commit={{ .FullCommit }} + - -X github.com/cosmos/cosmos-sdk/types.DBBackend=pebbledb - -X github.com/zeta-chain/node/pkg/constant.Name=zetacored - -X github.com/zeta-chain/node/pkg/constant.Version={{ .Version }} - -X github.com/zeta-chain/node/pkg/constant.CommitHash={{ .FullCommit }} - - -X github.com/zeta-chain/node/pkg/constant.BuildTime={{ .Env.BUILDTIME }} - - -X github.com/cosmos/cosmos-sdk/types.DBBackend=pebbledb + - -X github.com/zeta-chain/node/pkg/constant.BuildTime={{ .CommitDate }} + - -X main.version={{ .Version }} + - -X main.commit={{ .Commit }} + - -X main.date={{ .CommitDate }} + - -buildid= + - -s -w - id: "zetaclientd" main: ./cmd/zetaclientd diff --git a/Dockerfile-localnet b/Dockerfile-localnet index 0ec2408120..49247d6be4 100644 --- a/Dockerfile-localnet +++ b/Dockerfile-localnet @@ -1,6 +1,6 @@ # syntax=ghcr.io/zeta-chain/docker-dockerfile:1.9-labs # check=error=true -FROM ghcr.io/zeta-chain/golang:1.22.5-bookworm AS base-build +FROM ghcr.io/zeta-chain/golang:1.22.7-bookworm AS base-build ENV GOPATH=/go ENV GOOS=linux @@ -27,10 +27,10 @@ RUN --mount=type=cache,target="/root/.cache/go-build" \ NODE_COMMIT=${NODE_COMMIT} \ make install install-zetae2e -FROM ghcr.io/zeta-chain/golang:1.22.5-bookworm AS cosmovisor-build +FROM ghcr.io/zeta-chain/golang:1.22.7-bookworm AS cosmovisor-build RUN go install cosmossdk.io/tools/cosmovisor/cmd/cosmovisor@v1.6.0 -FROM ghcr.io/zeta-chain/golang:1.22.5-bookworm AS base-runtime +FROM ghcr.io/zeta-chain/golang:1.22.7-bookworm AS base-runtime RUN apt update && \ apt install -yq jq yq curl tmux python3 openssh-server iputils-ping iproute2 bind9-host && \ diff --git a/Makefile b/Makefile index 1e87c984e9..f39e4f27ff 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,6 @@ PACKAGE_NAME := github.com/zeta-chain/node NODE_VERSION := $(shell ./version.sh) NODE_COMMIT := $(shell [ -z "${NODE_COMMIT}" ] && git log -1 --format='%H' || echo ${NODE_COMMIT} ) -BUILDTIME := $(shell date -u +"%Y%m%d.%H%M%S" ) DOCKER ?= docker # allow setting of NODE_COMPOSE_ARGS to pass additional args to docker compose # useful for setting profiles and/ort optional overlays @@ -11,19 +10,31 @@ DOCKER ?= docker DOCKER_COMPOSE ?= $(DOCKER) compose -f docker-compose.yml $(NODE_COMPOSE_ARGS) DOCKER_BUF := $(DOCKER) run --rm -v $(CURDIR):/workspace --workdir /workspace bufbuild/buf GOFLAGS := "" -GOLANG_CROSS_VERSION ?= v1.22.4 GOPATH ?= '$(HOME)/go' +# common goreaser command definition +GOLANG_CROSS_VERSION ?= v1.22.7@sha256:24b2d75007f0ec8e35d01f3a8efa40c197235b200a1a91422d78b851f67ecce4 +GORELEASER := $(DOCKER) run \ + --rm \ + --privileged \ + -e CGO_ENABLED=1 \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v `pwd`:/go/src/$(PACKAGE_NAME) \ + -w /go/src/$(PACKAGE_NAME) \ + -e "GITHUB_TOKEN=${GITHUB_TOKEN}" \ + ghcr.io/goreleaser/goreleaser-cross:${GOLANG_CROSS_VERSION} + ldflags = -X github.com/cosmos/cosmos-sdk/version.Name=zetacore \ -X github.com/cosmos/cosmos-sdk/version.ServerName=zetacored \ -X github.com/cosmos/cosmos-sdk/version.ClientName=zetaclientd \ -X github.com/cosmos/cosmos-sdk/version.Version=$(NODE_VERSION) \ -X github.com/cosmos/cosmos-sdk/version.Commit=$(NODE_COMMIT) \ + -X github.com/cosmos/cosmos-sdk/types.DBBackend=pebbledb \ -X github.com/zeta-chain/node/pkg/constant.Name=zetacored \ -X github.com/zeta-chain/node/pkg/constant.Version=$(NODE_VERSION) \ -X github.com/zeta-chain/node/pkg/constant.CommitHash=$(NODE_COMMIT) \ - -X github.com/zeta-chain/node/pkg/constant.BuildTime=$(BUILDTIME) \ - -X github.com/cosmos/cosmos-sdk/types.DBBackend=pebbledb + -buildid= \ + -s -w BUILD_FLAGS := -ldflags '$(ldflags)' -tags pebbledb,ledger @@ -301,7 +312,7 @@ ifdef UPGRADE_TEST_FROM_SOURCE zetanode-upgrade: zetanode @echo "Building zetanode-upgrade from source" $(DOCKER) build -t zetanode:old -f Dockerfile-localnet --target old-runtime-source \ - --build-arg OLD_VERSION='release/v20' \ + --build-arg OLD_VERSION='release/v21' \ --build-arg NODE_VERSION=$(NODE_VERSION) \ --build-arg NODE_COMMIT=$(NODE_COMMIT) . @@ -310,7 +321,7 @@ else zetanode-upgrade: zetanode @echo "Building zetanode-upgrade from binaries" $(DOCKER) build -t zetanode:old -f Dockerfile-localnet --target old-runtime \ - --build-arg OLD_VERSION='https://github.com/zeta-chain/node/releases/download/v20.0.2' \ + --build-arg OLD_VERSION='https://github.com/zeta-chain/node/releases/download/v21.0.0' \ --build-arg NODE_VERSION=$(NODE_VERSION) \ --build-arg NODE_COMMIT=$(NODE_COMMIT) \ . @@ -360,7 +371,7 @@ start-upgrade-import-mainnet-test: zetanode-upgrade ############################################################################### BINDIR ?= $(GOPATH)/bin -SIMAPP = ./tests/simulation +SIMAPP = ./simulation # Run sim is a cosmos tool which helps us to run multiple simulations in parallel. @@ -381,16 +392,22 @@ $(BINDIR)/runsim: # Period: Invariant check period # Timeout: Timeout for the simulation test define run-sim-test - @echo "Running $(1)..." + @echo "Running $(1)" @go test -mod=readonly $(SIMAPP) -run $(2) -Enabled=true \ -NumBlocks=$(3) -BlockSize=$(4) -Commit=true -Period=0 -v -timeout $(5) endef test-sim-nondeterminism: - $(call run-sim-test,"non-determinism test",TestAppStateDeterminism,100,200,2h) + $(call run-sim-test,"non-determinism test",TestAppStateDeterminism,100,200,30m) test-sim-fullappsimulation: - $(call run-sim-test,"TestFullAppSimulation",TestFullAppSimulation,100,200,2h) + $(call run-sim-test,"TestFullAppSimulation",TestFullAppSimulation,100,200,30m) + +test-sim-import-export: + $(call run-sim-test,"test-import-export",TestAppImportExport,100,200,30m) + +test-sim-after-import: + $(call run-sim-test,"test-sim-after-import",TestAppSimulationAfterImport,100,200,30m) test-sim-multi-seed-long: runsim @echo "Running long multi-seed application simulation." @@ -400,46 +417,41 @@ test-sim-multi-seed-short: runsim @echo "Running short multi-seed application simulation." @$(BINDIR)/runsim -Jobs=4 -SimAppPkg=$(SIMAPP) -ExitOnFail 50 10 TestFullAppSimulation +test-sim-import-export-long: runsim + @echo "Running application import/export simulation. This may take several minutes" + @$(BINDIR)/runsim -Jobs=4 -SimAppPkg=$(SIMAPP) -ExitOnFail 500 50 TestAppImportExport +test-sim-after-import-long: runsim + @echo "Running application simulation-after-import. This may take several minute" + @$(BINDIR)/runsim -Jobs=4 -SimAppPkg=$(SIMAPP) -ExitOnFail 500 50 TestAppSimulationAfterImport .PHONY: \ test-sim-nondeterminism \ test-sim-fullappsimulation \ test-sim-multi-seed-long \ -test-sim-multi-seed-short +test-sim-multi-seed-short \ +test-sim-import-export \ +test-sim-after-import \ +test-sim-import-export-long \ +test-sim-after-import-long ############################################################################### ### GoReleaser ### ############################################################################### -release-dry-run: - docker run \ - --rm \ - --privileged \ - -e CGO_ENABLED=1 \ - -v /var/run/docker.sock:/var/run/docker.sock \ - -v `pwd`:/go/src/$(PACKAGE_NAME) \ - -v ${GOPATH}/pkg:/go/pkg \ - -w /go/src/$(PACKAGE_NAME) \ - ghcr.io/goreleaser/goreleaser-cross:${GOLANG_CROSS_VERSION} \ - --clean --skip=validate --skip=publish --snapshot +release-snapshot: + $(GORELEASER) --clean --skip=validate --skip=publish --snapshot + +release-build-only: + $(GORELEASER) --clean --skip=validate --skip=publish release: @if [ ! -f ".release-env" ]; then \ echo "\033[91m.release-env is required for release\033[0m";\ exit 1;\ fi - docker run \ - --rm \ - --privileged \ - -e CGO_ENABLED=1 \ - -e "GITHUB_TOKEN=${GITHUB_TOKEN}" \ - -v /var/run/docker.sock:/var/run/docker.sock \ - -v `pwd`:/go/src/$(PACKAGE_NAME) \ - -w /go/src/$(PACKAGE_NAME) \ - ghcr.io/goreleaser/goreleaser-cross:${GOLANG_CROSS_VERSION} \ - release --clean --skip=validate + $(GORELEASER) --clean --skip=validate ############################################################################### ### Local Mainnet Development ### diff --git a/app/ante/interfaces.go b/app/ante/interfaces.go index ffe1090121..d0afe126a3 100644 --- a/app/ante/interfaces.go +++ b/app/ante/interfaces.go @@ -41,7 +41,7 @@ type EVMKeeper interface { statedb.Keeper DynamicFeeEVMKeeper - NewEVM(ctx sdk.Context, msg core.Message, cfg *statedb.EVMConfig, tracer vm.EVMLogger, stateDB vm.StateDB) *vm.EVM + NewEVM(ctx sdk.Context, msg *core.Message, cfg *statedb.EVMConfig, tracer vm.EVMLogger, stateDB vm.StateDB) *vm.EVM DeductTxCostsFromUserBalance(ctx sdk.Context, fees sdk.Coins, from common.Address) error GetBalance(ctx sdk.Context, addr common.Address) *big.Int ResetTransientGasUsed(ctx sdk.Context) diff --git a/app/app.go b/app/app.go index eb4a59b774..2a369ae7d1 100644 --- a/app/app.go +++ b/app/app.go @@ -372,8 +372,6 @@ func New( authAddr, ) - logger.Info("bank keeper blocklist addresses", "addresses", app.BlockedAddrs()) - app.BankKeeper = bankkeeper.NewBaseKeeper( appCodec, keys[banktypes.StoreKey], @@ -1062,6 +1060,10 @@ func (app *App) BasicManager() module.BasicManager { return app.mb } +func (app *App) ModuleManager() *module.Manager { + return app.mm +} + func (app *App) BlockedAddrs() map[string]bool { blockList := make(map[string]bool) diff --git a/app/export.go b/app/export.go index 1f1ae26a96..5ccb887c5d 100644 --- a/app/export.go +++ b/app/export.go @@ -2,11 +2,13 @@ package app import ( "encoding/json" + "errors" "log" tmproto "github.com/cometbft/cometbft/proto/tendermint/types" servertypes "github.com/cosmos/cosmos-sdk/server/types" sdk "github.com/cosmos/cosmos-sdk/types" + distributiontypes "github.com/cosmos/cosmos-sdk/x/distribution/types" slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types" "github.com/cosmos/cosmos-sdk/x/staking" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" @@ -17,7 +19,6 @@ import ( func (app *App) ExportAppStateAndValidators( forZeroHeight bool, jailAllowedAddrs []string, modulesToExport []string, ) (servertypes.ExportedApp, error) { - // as if they could withdraw from the start of the next block ctx := app.NewContext(true, tmproto.Header{Height: app.LastBlockHeight()}) // We export at last height + 1, because that's the height at which @@ -76,7 +77,7 @@ func (app *App) prepForZeroHeightGenesis(ctx sdk.Context, jailAllowedAddrs []str // withdraw all validator commission app.StakingKeeper.IterateValidators(ctx, func(_ int64, val stakingtypes.ValidatorI) (stop bool) { _, err := app.DistrKeeper.WithdrawValidatorCommission(ctx, val.GetOperator()) - if err != nil { + if !errors.Is(err, distributiontypes.ErrNoValidatorCommission) && err != nil { panic(err) } return false @@ -162,7 +163,13 @@ func (app *App) prepForZeroHeightGenesis(ctx sdk.Context, jailAllowedAddrs []str counter := int16(0) for ; iter.Valid(); iter.Next() { - addr := sdk.ValAddress(iter.Key()[1:]) + key := iter.Key() + keyPrefixLength := 2 + if len(key) <= keyPrefixLength { + app.Logger().Error("unexpected key in staking store", "key", key) + continue + } + addr := sdk.ValAddress(key[keyPrefixLength:]) validator, found := app.StakingKeeper.GetValidator(ctx, addr) if !found { panic("expected validator, not found") diff --git a/changelog.md b/changelog.md index babe5d10d8..d60a7fdfba 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,23 @@ ## Unreleased +### Features +* [2984](https://github.com/zeta-chain/node/pull/2984) - add Whitelist message ability to whitelist SPL tokens on Solana +* [3091](https://github.com/zeta-chain/node/pull/3091) - improve build reproducability. `make release{,-build-only}` checksums should now be stable. + +### Tests +* [3075](https://github.com/zeta-chain/node/pull/3075) - ton: withdraw concurrent, deposit & revert. + +### Refactor + +### Tests + +### Fixes +* [3041](https://github.com/zeta-chain/node/pull/3041) - replace libp2p public DHT with private gossip peer discovery and connection gater for inbound connections + + +## v21.0.0 + ### Features * [2633](https://github.com/zeta-chain/node/pull/2633) - support for stateful precompiled contracts @@ -21,6 +38,10 @@ * [2987](https://github.com/zeta-chain/node/pull/2987) - add non-EVM standard inbound memo package * [2979](https://github.com/zeta-chain/node/pull/2979) - add fungible keeper ability to lock/unlock ZRC20 tokens * [3012](https://github.com/zeta-chain/node/pull/3012) - integrate authenticated calls erc20 smart contract functionality into protocol +* [3025](https://github.com/zeta-chain/node/pull/3025) - standard memo for Bitcoin inbound +* [3028](https://github.com/zeta-chain/node/pull/3028) - whitelist connection gater +* [3019](https://github.com/zeta-chain/node/pull/3019) - add ditribute functions to staking precompile +* [3020](https://github.com/zeta-chain/node/pull/3020) - add support for TON withdrawals ### Refactor @@ -31,6 +52,8 @@ * [2890](https://github.com/zeta-chain/node/pull/2890) - refactor `MsgUpdateChainInfo` to accept a single chain, and add `MsgRemoveChainInfo` to remove a chain * [2899](https://github.com/zeta-chain/node/pull/2899) - remove btc deposit fee v1 and improve unit tests * [2952](https://github.com/zeta-chain/node/pull/2952) - add error_message to cctx.status +* [3039](https://github.com/zeta-chain/node/pull/3039) - use `btcd` native APIs to handle Bitcoin Taproot address +* [3082](https://github.com/zeta-chain/node/pull/3082) - replace docker-based bitcoin sidecar inscription build with Golang implementation ### Tests @@ -44,6 +67,7 @@ * [2894](https://github.com/zeta-chain/node/pull/2894) - increase gas limit for TSS vote tx * [2932](https://github.com/zeta-chain/node/pull/2932) - add gateway upgrade as part of the upgrade test * [2947](https://github.com/zeta-chain/node/pull/2947) - initialize simulation tests +* [3033](https://github.com/zeta-chain/node/pull/3033) - initialize simulation tests for import and export ### Fixes @@ -57,6 +81,7 @@ * [2909](https://github.com/zeta-chain/node/pull/2909) - add legacy messages back to codec for querier backward compatibility * [3018](https://github.com/zeta-chain/node/pull/3018) - support `DepositAndCall` and `WithdrawAndCall` with empty payload * [3030](https://github.com/zeta-chain/node/pull/3030) - Avoid storing invalid Solana gateway address in the `SetGatewayAddress` +* [3047](https://github.com/zeta-chain/node/pull/3047) - wrong block hash in subscribe new heads ## v20.0.0 @@ -132,7 +157,7 @@ * [2518](https://github.com/zeta-chain/node/pull/2518) - add support for Solana address in zetacore * [2483](https://github.com/zeta-chain/node/pull/2483) - add priorityFee (gasTipCap) gas to the state * [2567](https://github.com/zeta-chain/node/pull/2567) - add sign latency metric to zetaclient (zetaclient_sign_latency) -* [2524](https://github.com/zeta-chain/node/pull/2524) - add inscription envelop parsing +* [2524](https://github.com/zeta-chain/node/pull/2524) - add inscription envelop parsing * [2560](https://github.com/zeta-chain/node/pull/2560) - add support for Solana SOL token withdraw * [2533](https://github.com/zeta-chain/node/pull/2533) - parse memo from both OP_RETURN and inscription * [2765](https://github.com/zeta-chain/node/pull/2765) - bitcoin depositor fee improvement @@ -214,7 +239,7 @@ ### CI -* [2388](https://github.com/zeta-chain/node/pull/2388) - added GitHub attestations of binaries produced in the release workflow. +* [2388](https://github.com/zeta-chain/node/pull/2388) - added GitHub attestations of binaries produced in the release workflow. * [2285](https://github.com/zeta-chain/node/pull/2285) - added nightly EVM performance testing pipeline, modified localnet testing docker image to utilize debian:bookworm, removed build-jet runners where applicable, removed deprecated/removed upgrade path testing pipeline * [2268](https://github.com/zeta-chain/node/pull/2268) - updated the publish-release pipeline to utilize the Github Actions Ubuntu 20.04 Runners * [2070](https://github.com/zeta-chain/node/pull/2070) - Added commands to build binaries from the working branch as a live full node rpc to test non-governance changes @@ -626,7 +651,7 @@ Getting the correct TSS address for Bitcoin now requires providing the Bitcoin c ### Tests -* Add unit tests for adding votes to a ballot +* Add unit tests for adding votes to a ballot ### CI @@ -666,7 +691,7 @@ Getting the correct TSS address for Bitcoin now requires providing the Bitcoin c ### Refactoring * [1226](https://github.com/zeta-chain/node/pull/1226) - call `onCrossChainCall` when depositing to a contract -* [1238](https://github.com/zeta-chain/node/pull/1238) - change default mempool version in config +* [1238](https://github.com/zeta-chain/node/pull/1238) - change default mempool version in config * [1279](https://github.com/zeta-chain/node/pull/1279) - remove duplicate funtion name IsEthereum * [1289](https://github.com/zeta-chain/node/pull/1289) - skip gas stability pool funding when gasLimit is equal gasUsed diff --git a/cmd/zetaclientd/p2p_diagnostics.go b/cmd/zetaclientd/p2p_diagnostics.go deleted file mode 100644 index 201b91504b..0000000000 --- a/cmd/zetaclientd/p2p_diagnostics.go +++ /dev/null @@ -1,229 +0,0 @@ -package main - -import ( - "context" - "fmt" - "os" - "sync" - "time" - - "github.com/cometbft/cometbft/crypto/secp256k1" - cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" - libp2p "github.com/libp2p/go-libp2p" - dht "github.com/libp2p/go-libp2p-kad-dht" - "github.com/libp2p/go-libp2p/core/crypto" - "github.com/libp2p/go-libp2p/core/network" - "github.com/libp2p/go-libp2p/core/peer" - "github.com/libp2p/go-libp2p/core/protocol" - drouting "github.com/libp2p/go-libp2p/p2p/discovery/routing" - dutil "github.com/libp2p/go-libp2p/p2p/discovery/util" - maddr "github.com/multiformats/go-multiaddr" - "github.com/rs/zerolog" - - "github.com/zeta-chain/node/pkg/cosmos" - "github.com/zeta-chain/node/zetaclient/config" - "github.com/zeta-chain/node/zetaclient/metrics" -) - -func RunDiagnostics( - startLogger zerolog.Logger, - peers []maddr.Multiaddr, - hotkeyPk cryptotypes.PrivKey, - cfg config.Config, -) error { - startLogger.Warn().Msg("P2P Diagnostic mode enabled") - startLogger.Warn().Msgf("seed peer: %s", peers) - priKey := secp256k1.PrivKey(hotkeyPk.Bytes()[:32]) - pubkeyBech32, err := cosmos.Bech32ifyPubKey(cosmos.Bech32PubKeyTypeAccPub, hotkeyPk.PubKey()) - if err != nil { - startLogger.Error().Err(err).Msg("Bech32ifyPubKey error") - return err - } - startLogger.Warn().Msgf("my pubkey %s", pubkeyBech32) - - var s *metrics.TelemetryServer - if len(peers) == 0 { - startLogger.Warn().Msg("No seed peer specified; assuming I'm the host") - } - p2pPriKey, err := crypto.UnmarshalSecp256k1PrivateKey(priKey[:]) - if err != nil { - startLogger.Error().Err(err).Msg("UnmarshalSecp256k1PrivateKey error") - return err - } - listenAddress, err := maddr.NewMultiaddr(fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", 6668)) - if err != nil { - startLogger.Error().Err(err).Msg("NewMultiaddr error") - return err - } - IP := os.Getenv("MYIP") - if len(IP) == 0 { - startLogger.Warn().Msg("empty env MYIP") - } - var externalAddr maddr.Multiaddr - if len(IP) != 0 { - externalAddr, err = maddr.NewMultiaddr(fmt.Sprintf("/ip4/%s/tcp/%d", IP, 6668)) - if err != nil { - startLogger.Error().Err(err).Msg("NewMultiaddr error") - return err - } - } - - host, err := libp2p.New( - libp2p.ListenAddrs(listenAddress), - libp2p.Identity(p2pPriKey), - libp2p.AddrsFactory(func(addrs []maddr.Multiaddr) []maddr.Multiaddr { - if externalAddr != nil { - return []maddr.Multiaddr{externalAddr} - } - return addrs - }), - libp2p.DisableRelay(), - ) - if err != nil { - startLogger.Error().Err(err).Msg("fail to create host") - return err - } - startLogger.Info().Msgf("host created: ID %s", host.ID().String()) - if len(peers) == 0 { - s = metrics.NewTelemetryServer() - s.SetP2PID(host.ID().String()) - go func() { - startLogger.Info().Msg("Starting TSS HTTP Server...") - if err := s.Start(); err != nil { - fmt.Println(err) - } - }() - } - - // create stream handler - handleStream := func(s network.Stream) { - defer s.Close() - - // read the message - buf := make([]byte, 1024) - n, err := s.Read(buf) - if err != nil { - startLogger.Error().Err(err).Msg("read stream error") - return - } - // send the message back - if _, err := s.Write(buf[:n]); err != nil { - startLogger.Error().Err(err).Msg("write stream error") - return - } - } - ProtocolID := "/echo/0.3.0" - host.SetStreamHandler(protocol.ID(ProtocolID), handleStream) - - kademliaDHT, err := dht.New(context.Background(), host, dht.Mode(dht.ModeServer)) - if err != nil { - return fmt.Errorf("fail to create DHT: %w", err) - } - startLogger.Info().Msg("Bootstrapping the DHT") - if err = kademliaDHT.Bootstrap(context.Background()); err != nil { - return fmt.Errorf("fail to bootstrap DHT: %w", err) - } - - var wg sync.WaitGroup - for _, peerAddr := range peers { - peerinfo, err := peer.AddrInfoFromP2pAddr(peerAddr) - if err != nil { - startLogger.Error().Err(err).Msgf("fail to parse peer address %s", peerAddr) - continue - } - wg.Add(1) - go func() { - defer wg.Done() - if err := host.Connect(context.Background(), *peerinfo); err != nil { - startLogger.Warn().Msgf("Connection failed with bootstrap node: %s", *peerinfo) - } else { - startLogger.Info().Msgf("Connection established with bootstrap node: %s", *peerinfo) - } - }() - } - wg.Wait() - - // We use a rendezvous point "meet me here" to announce our location. - // This is like telling your friends to meet you at the Eiffel Tower. - startLogger.Info().Msgf("Announcing ourselves...") - routingDiscovery := drouting.NewRoutingDiscovery(kademliaDHT) - dutil.Advertise(context.Background(), routingDiscovery, "ZetaZetaOpenTheDoor") - startLogger.Info().Msgf("Successfully announced!") - - // every 1min, print out the p2p diagnostic - ticker := time.NewTicker(time.Duration(cfg.P2PDiagnosticTicker) * time.Second) - round := 0 - - for range ticker.C { - round++ - // Now, look for others who have announced - // This is like your friend telling you the location to meet you. - startLogger.Info().Msgf("Searching for other peers...") - peerChan, err := routingDiscovery.FindPeers(context.Background(), "ZetaZetaOpenTheDoor") - if err != nil { - return err - } - - peerCount := 0 - okPingPongCount := 0 - for peer := range peerChan { - peerCount++ - if peer.ID == host.ID() { - startLogger.Info().Msgf("Found myself #(%d): %s", peerCount, peer) - continue - } - startLogger.Info().Msgf("Found peer #(%d): %s; pinging the peer...", peerCount, peer) - stream, err := host.NewStream(context.Background(), peer.ID, protocol.ID(ProtocolID)) - if err != nil { - startLogger.Error().Err(err).Msgf("fail to create stream to peer %s", peer) - continue - } - - // write a message to the stream - message := fmt.Sprintf( - "round %d %s => %s", - round, - host.ID().String()[len(host.ID().String())-5:], - peer.ID.String()[len(peer.ID.String())-5:], - ) - _, err = stream.Write([]byte(message)) - if err != nil { - startLogger.Error().Err(err).Msgf("fail to write to stream to peer %s", peer) - err = stream.Close() - if err != nil { - startLogger.Warn().Err(err).Msgf("fail to close stream to peer %s", peer) - } - continue - } - - // read the echoed message - buf := make([]byte, 1024) - nr, err := stream.Read(buf) - if err != nil { - startLogger.Error().Err(err).Msgf("fail to read from stream to peer %s", peer) - err = stream.Close() - if err != nil { - startLogger.Warn().Err(err).Msgf("fail to close stream to peer %s", peer) - } - continue - } - startLogger.Debug().Msgf("echoed message: %s", string(buf[:nr])) - err = stream.Close() - if err != nil { - startLogger.Warn().Err(err).Msgf("fail to close stream to peer %s", peer) - } - - // check if the message is echoed correctly - if string(buf[:nr]) != message { - startLogger.Error(). - Msgf("ping-pong failed with peer #(%d): %s; want %s got %s", peerCount, peer, message, string(buf[:nr])) - continue - } - startLogger.Info().Msgf("ping-pong success with peer #(%d): %s;", peerCount, peer) - okPingPongCount++ - } - startLogger.Info(). - Msgf("Expect %d peers in total; successful pings (%d/%d)", peerCount, okPingPongCount, peerCount-1) - } - return nil -} diff --git a/cmd/zetaclientd/start.go b/cmd/zetaclientd/start.go index f00abdcde1..f86a5fa2ee 100644 --- a/cmd/zetaclientd/start.go +++ b/cmd/zetaclientd/start.go @@ -9,14 +9,18 @@ import ( "os/signal" "path/filepath" "strings" + "sync" "syscall" "time" "github.com/cometbft/cometbft/crypto/secp256k1" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/libp2p/go-libp2p/p2p/protocol/ping" maddr "github.com/multiformats/go-multiaddr" "github.com/pkg/errors" "github.com/rs/zerolog/log" "github.com/spf13/cobra" + "gitlab.com/thorchain/tss/go-tss/conversion" "github.com/zeta-chain/node/pkg/authz" "github.com/zeta-chain/node/pkg/chains" @@ -179,13 +183,6 @@ func start(_ *cobra.Command, _ []string) error { log.Error().Err(err).Msg("peer address error") } initPreParams(cfg.PreParamsPath) - if cfg.P2PDiagnostic { - err := RunDiagnostics(startLogger, peers, hotkeyPk, cfg) - if err != nil { - startLogger.Error().Err(err).Msg("RunDiagnostics error") - return err - } - } m, err := metrics.NewMetrics() if err != nil { @@ -204,8 +201,19 @@ func start(_ *cobra.Command, _ []string) error { } telemetryServer.SetIPAddress(cfg.PublicIP) + + keygen := appContext.GetKeygen() + whitelistedPeers := []peer.ID{} + for _, pk := range keygen.GranteePubkeys { + pid, err := conversion.Bech32PubkeyToPeerID(pk) + if err != nil { + return err + } + whitelistedPeers = append(whitelistedPeers, pid) + } + // Create TSS server - server, err := mc.SetupTSSServer(peers, priKey, preParams, appContext.Config(), tssKeyPass, true) + server, err := mc.SetupTSSServer(peers, priKey, preParams, appContext.Config(), tssKeyPass, true, whitelistedPeers) if err != nil { return fmt.Errorf("SetupTSSServer error: %w", err) } @@ -222,6 +230,40 @@ func start(_ *cobra.Command, _ []string) error { signalChannel <- syscall.SIGTERM }) + go func() { + for { + time.Sleep(30 * time.Second) + ps := server.GetKnownPeers() + metrics.NumConnectedPeers.Set(float64(len(ps))) + telemetryServer.SetConnectedPeers(ps) + } + }() + go func() { + host := server.GetP2PHost() + pingRTT := make(map[peer.ID]int64) + for { + var wg sync.WaitGroup + for _, p := range whitelistedPeers { + wg.Add(1) + go func(p peer.ID) { + defer wg.Done() + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + result := <-ping.Ping(ctx, host, p) + if result.Error != nil { + masterLogger.Error().Err(result.Error).Msg("ping error") + pingRTT[p] = -1 // RTT -1 indicate ping error + return + } + pingRTT[p] = result.RTT.Nanoseconds() + }(p) + } + wg.Wait() + telemetryServer.SetPingRTT(pingRTT) + time.Sleep(30 * time.Second) + } + }() + // Generate a new TSS if keygen is set and add it into the tss server // If TSS has already been generated, and keygen was successful ; we use the existing TSS err = GenerateTSS(ctx, masterLogger, zetacoreClient, server) diff --git a/cmd/zetacored/config/prefixes.go b/cmd/zetacored/config/prefixes.go index a96a4c57bc..5fcd9218a5 100644 --- a/cmd/zetacored/config/prefixes.go +++ b/cmd/zetacored/config/prefixes.go @@ -6,6 +6,9 @@ const ( // Bech32Prefix defines the Bech32 prefix used for Cronos Accounts Bech32Prefix = "zeta" + // ZRC20DenomPrefix defines the prefix for ZRC20 tokens when converted to sdk.Coin. + ZRC20DenomPrefix = "zrc20/" + // Bech32PrefixAccAddr defines the Bech32 prefix of an account's address Bech32PrefixAccAddr = Bech32Prefix // Bech32PrefixAccPub defines the Bech32 prefix of an account's public key diff --git a/cmd/zetacored/root.go b/cmd/zetacored/root.go index 440b921bc2..3880b12108 100644 --- a/cmd/zetacored/root.go +++ b/cmd/zetacored/root.go @@ -268,48 +268,43 @@ func (ac appCreator) newApp( ) } -// appExport creates a new simapp (optionally at a given height) +// appExport is used to export the state of the application for a genesis file. func (ac appCreator) appExport( logger log.Logger, db dbm.DB, traceStore io.Writer, height int64, forZeroHeight bool, jailAllowedAddrs []string, appOpts servertypes.AppOptions, modulesToExport []string, ) (servertypes.ExportedApp, error) { - var anApp *app.App + var zetaApp *app.App homePath, ok := appOpts.Get(flags.FlagHome).(string) if !ok || homePath == "" { return servertypes.ExportedApp{}, errors.New("application home not set") } - if height != -1 { - anApp = app.New( - logger, - db, - traceStore, - false, - map[int64]bool{}, - homePath, - uint(1), - ac.encCfg, - appOpts, - ) - - if err := anApp.LoadHeight(height); err != nil { + loadLatest := false + if height == -1 { + loadLatest = true + } + + zetaApp = app.New( + logger, + db, + traceStore, + loadLatest, + map[int64]bool{}, + homePath, + uint(1), + ac.encCfg, + appOpts, + ) + + // If height is -1, it means we are using the latest height. + // For all other cases, we load the specified height from the Store + if !loadLatest { + if err := zetaApp.LoadHeight(height); err != nil { return servertypes.ExportedApp{}, err } - } else { - anApp = app.New( - logger, - db, - traceStore, - true, - map[int64]bool{}, - homePath, - uint(1), - ac.encCfg, - appOpts, - ) } - return anApp.ExportAppStateAndValidators(forZeroHeight, jailAllowedAddrs, modulesToExport) + return zetaApp.ExportAppStateAndValidators(forZeroHeight, jailAllowedAddrs, modulesToExport) } diff --git a/cmd/zetae2e/config/contracts.go b/cmd/zetae2e/config/contracts.go index 9af3ccd812..6f2dff72c6 100644 --- a/cmd/zetae2e/config/contracts.go +++ b/cmd/zetae2e/config/contracts.go @@ -277,7 +277,7 @@ func setContractsFromConfig(r *runner.E2ERunner, conf config.Config) error { if err != nil { return fmt.Errorf("invalid TestDAppV2Addr: %w", err) } - r.TestDAppV2ZEVM, err = testdappv2.NewTestDAppV2(r.TestDAppV2ZEVMAddr, r.EVMClient) + r.TestDAppV2ZEVM, err = testdappv2.NewTestDAppV2(r.TestDAppV2ZEVMAddr, r.ZEVMClient) if err != nil { return err } diff --git a/cmd/zetae2e/init.go b/cmd/zetae2e/init.go index d8dbfd379f..6f98c24102 100644 --- a/cmd/zetae2e/init.go +++ b/cmd/zetae2e/init.go @@ -9,7 +9,7 @@ import ( "github.com/zeta-chain/node/e2e/config" ) -var initConf = config.Config{} +var initConf = config.DefaultConfig() var configFile = "" func NewInitCmd() *cobra.Command { @@ -19,18 +19,20 @@ func NewInitCmd() *cobra.Command { RunE: initConfig, } - InitCmd.Flags().StringVar(&initConf.RPCs.EVM, "ethURL", "http://eth:8545", "--ethURL http://eth:8545") - InitCmd.Flags().StringVar(&initConf.RPCs.ZetaCoreGRPC, "grpcURL", "zetacore0:9090", "--grpcURL zetacore0:9090") + InitCmd.Flags().StringVar(&initConf.RPCs.EVM, "ethURL", initConf.RPCs.EVM, "--ethURL http://eth:8545") InitCmd.Flags(). - StringVar(&initConf.RPCs.ZetaCoreRPC, "rpcURL", "http://zetacore0:26657", "--rpcURL http://zetacore0:26657") + StringVar(&initConf.RPCs.ZetaCoreGRPC, "grpcURL", initConf.RPCs.ZetaCoreGRPC, "--grpcURL zetacore0:9090") InitCmd.Flags(). - StringVar(&initConf.RPCs.Zevm, "zevmURL", "http://zetacore0:8545", "--zevmURL http://zetacore0:8545") - InitCmd.Flags().StringVar(&initConf.RPCs.Bitcoin.Host, "btcURL", "bitcoin:18443", "--grpcURL bitcoin:18443") + StringVar(&initConf.RPCs.ZetaCoreRPC, "rpcURL", initConf.RPCs.ZetaCoreRPC, "--rpcURL http://zetacore0:26657") InitCmd.Flags(). - StringVar(&initConf.RPCs.Solana, "solanaURL", "http://solana:8899", "--solanaURL http://solana:8899") + StringVar(&initConf.RPCs.Zevm, "zevmURL", initConf.RPCs.Zevm, "--zevmURL http://zetacore0:8545") InitCmd.Flags(). - StringVar(&initConf.RPCs.TONSidecarURL, "tonSidecarURL", "http://ton:8000", "--tonSidecarURL http://ton:8000") - InitCmd.Flags().StringVar(&initConf.ZetaChainID, "chainID", "athens_101-1", "--chainID athens_101-1") + StringVar(&initConf.RPCs.Bitcoin.Host, "btcURL", initConf.RPCs.Bitcoin.Host, "--btcURL bitcoin:18443") + InitCmd.Flags(). + StringVar(&initConf.RPCs.Solana, "solanaURL", initConf.RPCs.Solana, "--solanaURL http://solana:8899") + InitCmd.Flags(). + StringVar(&initConf.RPCs.TONSidecarURL, "tonSidecarURL", initConf.RPCs.TONSidecarURL, "--tonSidecarURL http://ton:8000") + InitCmd.Flags().StringVar(&initConf.ZetaChainID, "chainID", initConf.ZetaChainID, "--chainID athens_101-1") InitCmd.Flags().StringVar(&configFile, local.FlagConfigFile, "e2e.config", "--cfg ./e2e.config") return InitCmd diff --git a/cmd/zetae2e/local/local.go b/cmd/zetae2e/local/local.go index de46edf450..81779faef9 100644 --- a/cmd/zetae2e/local/local.go +++ b/cmd/zetae2e/local/local.go @@ -50,7 +50,7 @@ const ( ) var ( - TestTimeout = 15 * time.Minute + TestTimeout = 20 * time.Minute ) var noError = testutil.NoError @@ -185,6 +185,11 @@ func localE2ETest(cmd *cobra.Command, _ []string) { // set the authority client to the zeta tx server to be able to query message permissions deployerRunner.ZetaTxServer.SetAuthorityClient(deployerRunner.AuthorityClient) + // run setup steps that do not require tss + if !skipSetup { + noError(deployerRunner.FundEmissionsPool()) + } + // wait for keygen to be completed // if setup is skipped, we assume that the keygen is already completed if !skipSetup { @@ -229,7 +234,6 @@ func localE2ETest(cmd *cobra.Command, _ []string) { if testSolana { deployerRunner.SetupSolana(conf.AdditionalAccounts.UserSolana.SolanaPrivateKey.String()) } - noError(deployerRunner.FundEmissionsPool()) deployerRunner.MintERC20OnEvm(1000000) @@ -298,9 +302,15 @@ func localE2ETest(cmd *cobra.Command, _ []string) { } bitcoinTests := []string{ + e2etests.TestBitcoinDonationName, e2etests.TestBitcoinDepositName, e2etests.TestBitcoinDepositAndCallName, - e2etests.TestBitcoinDepositRefundName, + e2etests.TestBitcoinDepositAndCallRevertName, + e2etests.TestBitcoinStdMemoDepositName, + e2etests.TestBitcoinStdMemoDepositAndCallName, + e2etests.TestBitcoinStdMemoDepositAndCallRevertName, + e2etests.TestBitcoinStdMemoDepositAndCallRevertOtherAddressName, + e2etests.TestBitcoinStdMemoInscribedDepositAndCallName, e2etests.TestBitcoinWithdrawSegWitName, e2etests.TestBitcoinWithdrawInvalidAddressName, e2etests.TestZetaWithdrawBTCRevertName, @@ -327,14 +337,17 @@ func localE2ETest(cmd *cobra.Command, _ []string) { if !skipPrecompiles { precompiledContractTests = []string{ - e2etests.TestPrecompilesPrototypeName, - e2etests.TestPrecompilesPrototypeThroughContractName, - e2etests.TestPrecompilesStakingName, - // Disabled until further notice, check https://github.com/zeta-chain/node/issues/3005. - // e2etests.TestPrecompilesStakingThroughContractName, - e2etests.TestPrecompilesBankName, - e2etests.TestPrecompilesBankFailName, - e2etests.TestPrecompilesBankThroughContractName, + // e2etests.TestPrecompilesPrototypeName, + // e2etests.TestPrecompilesPrototypeThroughContractName, + // e2etests.TestPrecompilesStakingName, + // // Disabled until further notice, check https://github.com/zeta-chain/node/issues/3005. + // // e2etests.TestPrecompilesStakingThroughContractName, + // e2etests.TestPrecompilesBankName, + // e2etests.TestPrecompilesBankFailName, + // e2etests.TestPrecompilesBankThroughContractName, + e2etests.TestPrecompilesDistributeName, + e2etests.TestPrecompilesDistributeNonZRC20Name, + e2etests.TestPrecompilesDistributeThroughContractName, } } @@ -397,6 +410,9 @@ func localE2ETest(cmd *cobra.Command, _ []string) { e2etests.TestSolanaDepositAndCallRefundName, e2etests.TestSolanaDepositRestrictedName, e2etests.TestSolanaWithdrawRestrictedName, + // TODO move under admin tests + // https://github.com/zeta-chain/node/issues/3085 + e2etests.TestSolanaWhitelistSPLName, } eg.Go(solanaTestRoutine(conf, deployerRunner, verbose, solanaTests...)) } @@ -410,6 +426,9 @@ func localE2ETest(cmd *cobra.Command, _ []string) { tonTests := []string{ e2etests.TestTONDepositName, e2etests.TestTONDepositAndCallName, + e2etests.TestTONDepositAndCallRefundName, + e2etests.TestTONWithdrawName, + e2etests.TestTONWithdrawConcurrentName, } eg.Go(tonTestRoutine(conf, deployerRunner, verbose, tonTests...)) diff --git a/cmd/zetae2e/local/solana.go b/cmd/zetae2e/local/solana.go index 135615051a..3e58418cc4 100644 --- a/cmd/zetae2e/local/solana.go +++ b/cmd/zetae2e/local/solana.go @@ -26,6 +26,7 @@ func solanaTestRoutine( deployerRunner, conf.AdditionalAccounts.UserSolana, runner.NewLogger(verbose, color.FgCyan, "solana"), + runner.WithZetaTxServer(deployerRunner.ZetaTxServer), ) if err != nil { return err diff --git a/cmd/zetae2e/run.go b/cmd/zetae2e/run.go index f58e5b51cf..fe52f056d5 100644 --- a/cmd/zetae2e/run.go +++ b/cmd/zetae2e/run.go @@ -155,20 +155,22 @@ func runE2ETest(cmd *cobra.Command, args []string) error { // parseCmdArgsToE2ETestRunConfig parses command-line arguments into a slice of E2ETestRunConfig structs. func parseCmdArgsToE2ETestRunConfig(args []string) ([]runner.E2ETestRunConfig, error) { - tests := []runner.E2ETestRunConfig{} + tests := make([]runner.E2ETestRunConfig, 0, len(args)) + for _, arg := range args { parts := strings.SplitN(arg, ":", 2) - if len(parts) != 2 { - return nil, errors.New("command arguments should be in format: testName:testArgs") - } - if parts[0] == "" { + testName := parts[0] + if testName == "" { return nil, errors.New("missing testName") } - testName := parts[0] - testArgs := []string{} - if parts[1] != "" { - testArgs = strings.Split(parts[1], ",") + + var testArgs []string + if len(parts) > 1 { + if parts[1] != "" { + testArgs = strings.Split(parts[1], ",") + } } + tests = append(tests, runner.E2ETestRunConfig{ Name: testName, Args: testArgs, diff --git a/codecov.yml b/codecov.yml index fee85c9c04..da90e44bd9 100644 --- a/codecov.yml +++ b/codecov.yml @@ -81,3 +81,4 @@ ignore: - "precompiles/**/*.json" - "precompiles/**/*.sol" - "precompiles/**/*.gen.go" + - "simulation/*.go" diff --git a/contrib/localnet/docker-compose.yml b/contrib/localnet/docker-compose.yml index 363f5e1f6d..4f36583d91 100644 --- a/contrib/localnet/docker-compose.yml +++ b/contrib/localnet/docker-compose.yml @@ -227,18 +227,6 @@ services: -rpcauth=smoketest:63acf9b8dccecce914d85ff8c044b78b$$5892f9bbc84f4364e79f0970039f88bdd823f168d4acc76099ab97b14a766a99 -txindex=1 - bitcoin-node-sidecar: - image: ghcr.io/zeta-chain/node-localnet-bitcoin-sidecar:e0205d7 - container_name: bitcoin-node-sidecar - hostname: bitcoin-node-sidecar - networks: - mynetwork: - ipv4_address: 172.20.0.111 - environment: - - PORT=8000 - ports: - - "8000:8000" - solana: image: solana-local:latest container_name: solana @@ -254,6 +242,8 @@ services: entrypoint: ["/usr/bin/start-solana.sh"] ton: + # figure out why E2E fail with MyLocalTon v124 @ deposit: deployer.CreateWallet(..) + # image: ghcr.io/zeta-chain/ton-docker:4f08c1d image: ghcr.io/zeta-chain/ton-docker:a69ea0f container_name: ton hostname: ton diff --git a/contrib/localnet/orchestrator/Dockerfile.fastbuild b/contrib/localnet/orchestrator/Dockerfile.fastbuild index c776d96c06..cbab881a65 100644 --- a/contrib/localnet/orchestrator/Dockerfile.fastbuild +++ b/contrib/localnet/orchestrator/Dockerfile.fastbuild @@ -3,7 +3,7 @@ FROM zetanode:latest AS zeta FROM ghcr.io/zeta-chain/ethereum-client-go:v1.10.26 AS geth FROM ghcr.io/zeta-chain/solana-docker:1.18.15 AS solana -FROM ghcr.io/zeta-chain/golang:1.22.5-bookworm AS orchestrator +FROM ghcr.io/zeta-chain/golang:1.22.7-bookworm AS orchestrator RUN apt update && \ apt install -yq jq yq curl tmux python3 openssh-server iputils-ping iproute2 bind9-host && \ diff --git a/contrib/localnet/scripts/start-zetacored.sh b/contrib/localnet/scripts/start-zetacored.sh index 1f5e2fca12..c0e8424738 100755 --- a/contrib/localnet/scripts/start-zetacored.sh +++ b/contrib/localnet/scripts/start-zetacored.sh @@ -292,8 +292,13 @@ then echo "Importing data" zetacored parse-genesis-file /root/genesis_data/exported-genesis.json fi -# Update governance voting period to 100s , to ignore the voting period imported from mainnet. - cat $HOME/.zetacored/config/genesis.json | jq '.app_state["gov"]["voting_params"]["voting_period"]="100s"' > $HOME/.zetacored/config/tmp_genesis.json && mv $HOME/.zetacored/config/tmp_genesis.json $HOME/.zetacored/config/genesis.json + +# Update governance voting parameters for localnet +# this allows for quick upgrades and using more than two nodes + jq '.app_state["gov"]["params"]["voting_period"]="100s" | + .app_state["gov"]["params"]["quorum"]="0.1" | + .app_state["gov"]["params"]["threshold"]="0.1"' \ + $HOME/.zetacored/config/genesis.json > tmp.json && mv tmp.json $HOME/.zetacored/config/genesis.json # 4. Collect all the gentx files in zetacore0 and create the final genesis.json zetacored collect-gentxs diff --git a/contrib/localnet/solana/gateway.so b/contrib/localnet/solana/gateway.so index 185b938c3c..0fe82f24f2 100755 Binary files a/contrib/localnet/solana/gateway.so and b/contrib/localnet/solana/gateway.so differ diff --git a/contrib/rpcimportable/go.mod b/contrib/rpcimportable/go.mod index 32fe6d9e3d..044f51ae8a 100644 --- a/contrib/rpcimportable/go.mod +++ b/contrib/rpcimportable/go.mod @@ -11,5 +11,10 @@ replace ( github.com/gogo/protobuf => github.com/regen-network/protobuf v1.3.3-alpha.regen.1 ) +// go-ethereum fork must be used as it removes incompatible pebbledb version +replace ( + github.com/ethereum/go-ethereum => github.com/zeta-chain/go-ethereum v1.13.16-0.20241022183758-422c6ef93ccc +) + // uncomment this for local development/testing/debugging // replace github.com/zeta-chain/node => ../.. \ No newline at end of file diff --git a/docs/development/SIMULATION_TESTING.md b/docs/development/SIMULATION_TESTING.md index 1f5232911a..e6bb2f2ca7 100644 --- a/docs/development/SIMULATION_TESTING.md +++ b/docs/development/SIMULATION_TESTING.md @@ -1,34 +1,78 @@ # Zetachain simulation testing ## Overview The blockchain simulation tests how the blockchain application would behave under real life circumstances by generating -and sending randomized messages.The goal of this is to detect and debug failures that could halt a live chain,by providing -logs and statistics about the operations run by the simulator as well as exporting the latest application state. - +and sending randomized messages.The goal of this is to detect and debug failures that could halt a live chain by +providing logs and statistics about the operations run by the simulator as well as exporting the latest application +state. ## Simulation tests ### Nondeterminism test Nondeterminism test runs a full application simulation , and produces multiple blocks as per the config It checks the determinism of the application by comparing the apphash at the end of each run to other runs -The test certifies that , for the same set of operations ( irrespective of what the operations are ), we would reach the same final state if the initial state is the same +The test certifies that, for the same set of operations (regardless of what the operations are), we +would reach the same final state if the initial state is the same +Approximate run time is 2 minutes. ```bash make test-sim-nondeterminism ``` + ### Full application simulation test Full application runs a full app simulation test with the provided configuration. -At the end of the run it tries to export the genesis state to make sure the export works. +At the end of the run, it tries to export the genesis state to make sure the export works. +Approximate run time is 2 minutes. ```bash make test-sim-full-app ``` +### Import Export simulation test +The import export simulation test runs a full application simulation +and exports the application state at the end of the run. +This state is then imported into a new simulation. +At the end of the run, we compare the keys for the application state for both the simulations +to make sure they are the same. +Approximate run time is 2 minutes. +```bash +make test-sim-import-export +``` + +### Import and run simulation test +This simulation test exports the application state at the end of the run and imports it into a new simulation. +Approximate run time is 2 minutes. +```bash +make test-sim-after-import +``` + ### Multi seed long test -Multi seed long test runs a full application simulation with multiple seeds and multiple blocks.This runs the test for a longer duration compared to the multi seed short test +Multi seed long test runs a full application simulation with multiple seeds and multiple blocks. +It uses the `runsim` tool to run the same test in parallel threads. +Approximate run time is 30 minutes. ```bash make test-sim-multi-seed-long ``` ### Multi seed short test -Multi seed short test runs a full application simulation with multiple seeds and multiple blocks. This runs the test for a longer duration compared to the multi seed long test +Multi seed short test runs a full application simulation with multiple seeds and multiple blocks. +It uses the `runsim` tool to run the same test in parallel threads. +This test is a shorter version of the Multi seed long test. +Approximate run time is 10 minutes. ```bash make test-sim-multi-seed-short -``` \ No newline at end of file +``` + +### Import Export long test +This test runs the import export simulation test for a longer duration. +It uses the `runsim` tool to run the same test in parallel threads. +Approximate run time is 30 minutes. +```bash +make test-sim-import-export-long +``` + +### Import and run simulation test long +This test runs the import and run simulation test for a longer duration. +It uses the `runsim` tool to run the same test in parallel threads. +Approximate run time is 30 minutes. +```bash +make test-sim-after-import-long +``` + diff --git a/e2e/config/config.go b/e2e/config/config.go index 32a799c027..67685b0dd3 100644 --- a/e2e/config/config.go +++ b/e2e/config/config.go @@ -208,13 +208,20 @@ func WriteConfig(file string, config Config) error { return errors.New("file name cannot be empty") } - b, err := yaml.Marshal(config) + // #nosec G304 -- the variable is expected to be controlled by the user + fHandle, err := os.OpenFile(file, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) if err != nil { - return err + return fmt.Errorf("open file: %w", err) } - err = os.WriteFile(file, b, 0600) + defer fHandle.Close() + + // use a custom encoder so we can set the indentation level + encoder := yaml.NewEncoder(fHandle) + defer encoder.Close() + encoder.SetIndent(2) + err = encoder.Encode(config) if err != nil { - return err + return fmt.Errorf("encode config: %w", err) } return nil } diff --git a/e2e/contracts/testdistribute/TestDistribute.abi b/e2e/contracts/testdistribute/TestDistribute.abi new file mode 100644 index 0000000000..26aa07af4e --- /dev/null +++ b/e2e/contracts/testdistribute/TestDistribute.abi @@ -0,0 +1,64 @@ +[ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "zrc20_distributor", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "zrc20_token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Distributed", + "type": "event" + }, + { + "stateMutability": "payable", + "type": "fallback" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "zrc20", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "distributeThroughContract", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } +] diff --git a/e2e/contracts/testdistribute/TestDistribute.bin b/e2e/contracts/testdistribute/TestDistribute.bin new file mode 100644 index 0000000000..5840bf96e5 --- /dev/null +++ b/e2e/contracts/testdistribute/TestDistribute.bin @@ -0,0 +1 @@ +60a060405260666000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555034801561005157600080fd5b503373ffffffffffffffffffffffffffffffffffffffff1660808173ffffffffffffffffffffffffffffffffffffffff168152505060805161034d6100a06000396000606c015261034d6000f3fe6080604052600436106100225760003560e01c806350b54e841461002b57610029565b3661002957005b005b34801561003757600080fd5b50610052600480360381019061004d9190610201565b610068565b60405161005f919061025c565b60405180910390f35b60007f000000000000000000000000000000000000000000000000000000000000000073ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16146100c257600080fd5b60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663fb93210884846040518363ffffffff1660e01b815260040161011d929190610295565b6020604051808303816000875af115801561013c573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061016091906102ea565b905092915050565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006101988261016d565b9050919050565b6101a88161018d565b81146101b357600080fd5b50565b6000813590506101c58161019f565b92915050565b6000819050919050565b6101de816101cb565b81146101e957600080fd5b50565b6000813590506101fb816101d5565b92915050565b6000806040838503121561021857610217610168565b5b6000610226858286016101b6565b9250506020610237858286016101ec565b9150509250929050565b60008115159050919050565b61025681610241565b82525050565b6000602082019050610271600083018461024d565b92915050565b6102808161018d565b82525050565b61028f816101cb565b82525050565b60006040820190506102aa6000830185610277565b6102b76020830184610286565b9392505050565b6102c781610241565b81146102d257600080fd5b50565b6000815190506102e4816102be565b92915050565b600060208284031215610300576102ff610168565b5b600061030e848285016102d5565b9150509291505056fea26469706673582212205443ec313ecb8c2e08ca8a30687daed4c3b666f9318ae72ccbe9033479c8b8be64736f6c634300080a0033 diff --git a/e2e/contracts/testdistribute/TestDistribute.go b/e2e/contracts/testdistribute/TestDistribute.go new file mode 100644 index 0000000000..18b4201b1a --- /dev/null +++ b/e2e/contracts/testdistribute/TestDistribute.go @@ -0,0 +1,420 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package testdistribute + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType +) + +// TestDistributeMetaData contains all meta data concerning the TestDistribute contract. +var TestDistributeMetaData = &bind.MetaData{ + ABI: "[{\"inputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"zrc20_distributor\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"zrc20_token\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"Distributed\",\"type\":\"event\"},{\"stateMutability\":\"payable\",\"type\":\"fallback\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"zrc20\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"distributeThroughContract\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"stateMutability\":\"payable\",\"type\":\"receive\"}]", + Bin: "0x60a060405260666000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555034801561005157600080fd5b503373ffffffffffffffffffffffffffffffffffffffff1660808173ffffffffffffffffffffffffffffffffffffffff168152505060805161034d6100a06000396000606c015261034d6000f3fe6080604052600436106100225760003560e01c806350b54e841461002b57610029565b3661002957005b005b34801561003757600080fd5b50610052600480360381019061004d9190610201565b610068565b60405161005f919061025c565b60405180910390f35b60007f000000000000000000000000000000000000000000000000000000000000000073ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16146100c257600080fd5b60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663fb93210884846040518363ffffffff1660e01b815260040161011d929190610295565b6020604051808303816000875af115801561013c573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061016091906102ea565b905092915050565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006101988261016d565b9050919050565b6101a88161018d565b81146101b357600080fd5b50565b6000813590506101c58161019f565b92915050565b6000819050919050565b6101de816101cb565b81146101e957600080fd5b50565b6000813590506101fb816101d5565b92915050565b6000806040838503121561021857610217610168565b5b6000610226858286016101b6565b9250506020610237858286016101ec565b9150509250929050565b60008115159050919050565b61025681610241565b82525050565b6000602082019050610271600083018461024d565b92915050565b6102808161018d565b82525050565b61028f816101cb565b82525050565b60006040820190506102aa6000830185610277565b6102b76020830184610286565b9392505050565b6102c781610241565b81146102d257600080fd5b50565b6000815190506102e4816102be565b92915050565b600060208284031215610300576102ff610168565b5b600061030e848285016102d5565b9150509291505056fea26469706673582212205443ec313ecb8c2e08ca8a30687daed4c3b666f9318ae72ccbe9033479c8b8be64736f6c634300080a0033", +} + +// TestDistributeABI is the input ABI used to generate the binding from. +// Deprecated: Use TestDistributeMetaData.ABI instead. +var TestDistributeABI = TestDistributeMetaData.ABI + +// TestDistributeBin is the compiled bytecode used for deploying new contracts. +// Deprecated: Use TestDistributeMetaData.Bin instead. +var TestDistributeBin = TestDistributeMetaData.Bin + +// DeployTestDistribute deploys a new Ethereum contract, binding an instance of TestDistribute to it. +func DeployTestDistribute(auth *bind.TransactOpts, backend bind.ContractBackend) (common.Address, *types.Transaction, *TestDistribute, error) { + parsed, err := TestDistributeMetaData.GetAbi() + if err != nil { + return common.Address{}, nil, nil, err + } + if parsed == nil { + return common.Address{}, nil, nil, errors.New("GetABI returned nil") + } + + address, tx, contract, err := bind.DeployContract(auth, *parsed, common.FromHex(TestDistributeBin), backend) + if err != nil { + return common.Address{}, nil, nil, err + } + return address, tx, &TestDistribute{TestDistributeCaller: TestDistributeCaller{contract: contract}, TestDistributeTransactor: TestDistributeTransactor{contract: contract}, TestDistributeFilterer: TestDistributeFilterer{contract: contract}}, nil +} + +// TestDistribute is an auto generated Go binding around an Ethereum contract. +type TestDistribute struct { + TestDistributeCaller // Read-only binding to the contract + TestDistributeTransactor // Write-only binding to the contract + TestDistributeFilterer // Log filterer for contract events +} + +// TestDistributeCaller is an auto generated read-only Go binding around an Ethereum contract. +type TestDistributeCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// TestDistributeTransactor is an auto generated write-only Go binding around an Ethereum contract. +type TestDistributeTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// TestDistributeFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type TestDistributeFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// TestDistributeSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type TestDistributeSession struct { + Contract *TestDistribute // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// TestDistributeCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type TestDistributeCallerSession struct { + Contract *TestDistributeCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// TestDistributeTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type TestDistributeTransactorSession struct { + Contract *TestDistributeTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// TestDistributeRaw is an auto generated low-level Go binding around an Ethereum contract. +type TestDistributeRaw struct { + Contract *TestDistribute // Generic contract binding to access the raw methods on +} + +// TestDistributeCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type TestDistributeCallerRaw struct { + Contract *TestDistributeCaller // Generic read-only contract binding to access the raw methods on +} + +// TestDistributeTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type TestDistributeTransactorRaw struct { + Contract *TestDistributeTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewTestDistribute creates a new instance of TestDistribute, bound to a specific deployed contract. +func NewTestDistribute(address common.Address, backend bind.ContractBackend) (*TestDistribute, error) { + contract, err := bindTestDistribute(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &TestDistribute{TestDistributeCaller: TestDistributeCaller{contract: contract}, TestDistributeTransactor: TestDistributeTransactor{contract: contract}, TestDistributeFilterer: TestDistributeFilterer{contract: contract}}, nil +} + +// NewTestDistributeCaller creates a new read-only instance of TestDistribute, bound to a specific deployed contract. +func NewTestDistributeCaller(address common.Address, caller bind.ContractCaller) (*TestDistributeCaller, error) { + contract, err := bindTestDistribute(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &TestDistributeCaller{contract: contract}, nil +} + +// NewTestDistributeTransactor creates a new write-only instance of TestDistribute, bound to a specific deployed contract. +func NewTestDistributeTransactor(address common.Address, transactor bind.ContractTransactor) (*TestDistributeTransactor, error) { + contract, err := bindTestDistribute(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &TestDistributeTransactor{contract: contract}, nil +} + +// NewTestDistributeFilterer creates a new log filterer instance of TestDistribute, bound to a specific deployed contract. +func NewTestDistributeFilterer(address common.Address, filterer bind.ContractFilterer) (*TestDistributeFilterer, error) { + contract, err := bindTestDistribute(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &TestDistributeFilterer{contract: contract}, nil +} + +// bindTestDistribute binds a generic wrapper to an already deployed contract. +func bindTestDistribute(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := TestDistributeMetaData.GetAbi() + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_TestDistribute *TestDistributeRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _TestDistribute.Contract.TestDistributeCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_TestDistribute *TestDistributeRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _TestDistribute.Contract.TestDistributeTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_TestDistribute *TestDistributeRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _TestDistribute.Contract.TestDistributeTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_TestDistribute *TestDistributeCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _TestDistribute.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_TestDistribute *TestDistributeTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _TestDistribute.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_TestDistribute *TestDistributeTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _TestDistribute.Contract.contract.Transact(opts, method, params...) +} + +// DistributeThroughContract is a paid mutator transaction binding the contract method 0x50b54e84. +// +// Solidity: function distributeThroughContract(address zrc20, uint256 amount) returns(bool) +func (_TestDistribute *TestDistributeTransactor) DistributeThroughContract(opts *bind.TransactOpts, zrc20 common.Address, amount *big.Int) (*types.Transaction, error) { + return _TestDistribute.contract.Transact(opts, "distributeThroughContract", zrc20, amount) +} + +// DistributeThroughContract is a paid mutator transaction binding the contract method 0x50b54e84. +// +// Solidity: function distributeThroughContract(address zrc20, uint256 amount) returns(bool) +func (_TestDistribute *TestDistributeSession) DistributeThroughContract(zrc20 common.Address, amount *big.Int) (*types.Transaction, error) { + return _TestDistribute.Contract.DistributeThroughContract(&_TestDistribute.TransactOpts, zrc20, amount) +} + +// DistributeThroughContract is a paid mutator transaction binding the contract method 0x50b54e84. +// +// Solidity: function distributeThroughContract(address zrc20, uint256 amount) returns(bool) +func (_TestDistribute *TestDistributeTransactorSession) DistributeThroughContract(zrc20 common.Address, amount *big.Int) (*types.Transaction, error) { + return _TestDistribute.Contract.DistributeThroughContract(&_TestDistribute.TransactOpts, zrc20, amount) +} + +// Fallback is a paid mutator transaction binding the contract fallback function. +// +// Solidity: fallback() payable returns() +func (_TestDistribute *TestDistributeTransactor) Fallback(opts *bind.TransactOpts, calldata []byte) (*types.Transaction, error) { + return _TestDistribute.contract.RawTransact(opts, calldata) +} + +// Fallback is a paid mutator transaction binding the contract fallback function. +// +// Solidity: fallback() payable returns() +func (_TestDistribute *TestDistributeSession) Fallback(calldata []byte) (*types.Transaction, error) { + return _TestDistribute.Contract.Fallback(&_TestDistribute.TransactOpts, calldata) +} + +// Fallback is a paid mutator transaction binding the contract fallback function. +// +// Solidity: fallback() payable returns() +func (_TestDistribute *TestDistributeTransactorSession) Fallback(calldata []byte) (*types.Transaction, error) { + return _TestDistribute.Contract.Fallback(&_TestDistribute.TransactOpts, calldata) +} + +// Receive is a paid mutator transaction binding the contract receive function. +// +// Solidity: receive() payable returns() +func (_TestDistribute *TestDistributeTransactor) Receive(opts *bind.TransactOpts) (*types.Transaction, error) { + return _TestDistribute.contract.RawTransact(opts, nil) // calldata is disallowed for receive function +} + +// Receive is a paid mutator transaction binding the contract receive function. +// +// Solidity: receive() payable returns() +func (_TestDistribute *TestDistributeSession) Receive() (*types.Transaction, error) { + return _TestDistribute.Contract.Receive(&_TestDistribute.TransactOpts) +} + +// Receive is a paid mutator transaction binding the contract receive function. +// +// Solidity: receive() payable returns() +func (_TestDistribute *TestDistributeTransactorSession) Receive() (*types.Transaction, error) { + return _TestDistribute.Contract.Receive(&_TestDistribute.TransactOpts) +} + +// TestDistributeDistributedIterator is returned from FilterDistributed and is used to iterate over the raw logs and unpacked data for Distributed events raised by the TestDistribute contract. +type TestDistributeDistributedIterator struct { + Event *TestDistributeDistributed // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *TestDistributeDistributedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(TestDistributeDistributed) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(TestDistributeDistributed) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *TestDistributeDistributedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *TestDistributeDistributedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// TestDistributeDistributed represents a Distributed event raised by the TestDistribute contract. +type TestDistributeDistributed struct { + Zrc20Distributor common.Address + Zrc20Token common.Address + Amount *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterDistributed is a free log retrieval operation binding the contract event 0xad4a9acf26d8bba7a8cf1a41160d59be042ee554578e256c98d2ab74cdd43542. +// +// Solidity: event Distributed(address indexed zrc20_distributor, address indexed zrc20_token, uint256 amount) +func (_TestDistribute *TestDistributeFilterer) FilterDistributed(opts *bind.FilterOpts, zrc20_distributor []common.Address, zrc20_token []common.Address) (*TestDistributeDistributedIterator, error) { + + var zrc20_distributorRule []interface{} + for _, zrc20_distributorItem := range zrc20_distributor { + zrc20_distributorRule = append(zrc20_distributorRule, zrc20_distributorItem) + } + var zrc20_tokenRule []interface{} + for _, zrc20_tokenItem := range zrc20_token { + zrc20_tokenRule = append(zrc20_tokenRule, zrc20_tokenItem) + } + + logs, sub, err := _TestDistribute.contract.FilterLogs(opts, "Distributed", zrc20_distributorRule, zrc20_tokenRule) + if err != nil { + return nil, err + } + return &TestDistributeDistributedIterator{contract: _TestDistribute.contract, event: "Distributed", logs: logs, sub: sub}, nil +} + +// WatchDistributed is a free log subscription operation binding the contract event 0xad4a9acf26d8bba7a8cf1a41160d59be042ee554578e256c98d2ab74cdd43542. +// +// Solidity: event Distributed(address indexed zrc20_distributor, address indexed zrc20_token, uint256 amount) +func (_TestDistribute *TestDistributeFilterer) WatchDistributed(opts *bind.WatchOpts, sink chan<- *TestDistributeDistributed, zrc20_distributor []common.Address, zrc20_token []common.Address) (event.Subscription, error) { + + var zrc20_distributorRule []interface{} + for _, zrc20_distributorItem := range zrc20_distributor { + zrc20_distributorRule = append(zrc20_distributorRule, zrc20_distributorItem) + } + var zrc20_tokenRule []interface{} + for _, zrc20_tokenItem := range zrc20_token { + zrc20_tokenRule = append(zrc20_tokenRule, zrc20_tokenItem) + } + + logs, sub, err := _TestDistribute.contract.WatchLogs(opts, "Distributed", zrc20_distributorRule, zrc20_tokenRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(TestDistributeDistributed) + if err := _TestDistribute.contract.UnpackLog(event, "Distributed", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseDistributed is a log parse operation binding the contract event 0xad4a9acf26d8bba7a8cf1a41160d59be042ee554578e256c98d2ab74cdd43542. +// +// Solidity: event Distributed(address indexed zrc20_distributor, address indexed zrc20_token, uint256 amount) +func (_TestDistribute *TestDistributeFilterer) ParseDistributed(log types.Log) (*TestDistributeDistributed, error) { + event := new(TestDistributeDistributed) + if err := _TestDistribute.contract.UnpackLog(event, "Distributed", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} diff --git a/e2e/contracts/testdistribute/TestDistribute.json b/e2e/contracts/testdistribute/TestDistribute.json new file mode 100644 index 0000000000..05ee369e1c --- /dev/null +++ b/e2e/contracts/testdistribute/TestDistribute.json @@ -0,0 +1,67 @@ +{ + "abi": [ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "zrc20_distributor", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "zrc20_token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Distributed", + "type": "event" + }, + { + "stateMutability": "payable", + "type": "fallback" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "zrc20", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "distributeThroughContract", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } + ], + "bin": "60a060405260666000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555034801561005157600080fd5b503373ffffffffffffffffffffffffffffffffffffffff1660808173ffffffffffffffffffffffffffffffffffffffff168152505060805161034d6100a06000396000606c015261034d6000f3fe6080604052600436106100225760003560e01c806350b54e841461002b57610029565b3661002957005b005b34801561003757600080fd5b50610052600480360381019061004d9190610201565b610068565b60405161005f919061025c565b60405180910390f35b60007f000000000000000000000000000000000000000000000000000000000000000073ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16146100c257600080fd5b60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663fb93210884846040518363ffffffff1660e01b815260040161011d929190610295565b6020604051808303816000875af115801561013c573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061016091906102ea565b905092915050565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006101988261016d565b9050919050565b6101a88161018d565b81146101b357600080fd5b50565b6000813590506101c58161019f565b92915050565b6000819050919050565b6101de816101cb565b81146101e957600080fd5b50565b6000813590506101fb816101d5565b92915050565b6000806040838503121561021857610217610168565b5b6000610226858286016101b6565b9250506020610237858286016101ec565b9150509250929050565b60008115159050919050565b61025681610241565b82525050565b6000602082019050610271600083018461024d565b92915050565b6102808161018d565b82525050565b61028f816101cb565b82525050565b60006040820190506102aa6000830185610277565b6102b76020830184610286565b9392505050565b6102c781610241565b81146102d257600080fd5b50565b6000815190506102e4816102be565b92915050565b600060208284031215610300576102ff610168565b5b600061030e848285016102d5565b9150509291505056fea26469706673582212205443ec313ecb8c2e08ca8a30687daed4c3b666f9318ae72ccbe9033479c8b8be64736f6c634300080a0033" +} diff --git a/e2e/contracts/testdistribute/TestDistribute.sol b/e2e/contracts/testdistribute/TestDistribute.sol new file mode 100644 index 0000000000..5cf2277b88 --- /dev/null +++ b/e2e/contracts/testdistribute/TestDistribute.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.10; + +// @dev Interface to interact with distribute. +interface IDistribute { + function distribute( + address zrc20, + uint256 amount + ) external returns (bool success); +} + +// @dev Call IBank contract functions +contract TestDistribute { + event Distributed( + address indexed zrc20_distributor, + address indexed zrc20_token, + uint256 amount + ); + + IDistribute distr = IDistribute(0x0000000000000000000000000000000000000066); + + address immutable owner; + + constructor() { + owner = msg.sender; + } + + modifier onlyOwner() { + require(msg.sender == owner); + _; + } + + function distributeThroughContract( + address zrc20, + uint256 amount + ) external onlyOwner returns (bool) { + return distr.distribute(zrc20, amount); + } + + fallback() external payable {} + + receive() external payable {} +} diff --git a/e2e/contracts/testdistribute/bindings.go b/e2e/contracts/testdistribute/bindings.go new file mode 100644 index 0000000000..765dfb5a8a --- /dev/null +++ b/e2e/contracts/testdistribute/bindings.go @@ -0,0 +1,8 @@ +//go:generate sh -c "solc TestDistribute.sol --combined-json abi,bin | jq '.contracts.\"TestDistribute.sol:TestDistribute\"' > TestDistribute.json" +//go:generate sh -c "cat TestDistribute.json | jq .abi > TestDistribute.abi" +//go:generate sh -c "cat TestDistribute.json | jq .bin | tr -d '\"' > TestDistribute.bin" +//go:generate sh -c "abigen --abi TestDistribute.abi --bin TestDistribute.bin --pkg testdistribute --type TestDistribute --out TestDistribute.go" + +package testdistribute + +var _ TestDistribute diff --git a/e2e/contracts/teststaking/TestStaking.sol b/e2e/contracts/teststaking/TestStaking.sol index 48a3837100..b6235ae658 100644 --- a/e2e/contracts/teststaking/TestStaking.sol +++ b/e2e/contracts/teststaking/TestStaking.sol @@ -36,13 +36,20 @@ interface IStaking { uint256 amount ) external returns (int64 completionTime); - function getAllValidators() external view returns (Validator[] calldata validators); + function getAllValidators() + external + view + returns (Validator[] calldata validators); - function getShares(address staker, string memory validator) external view returns (uint256 shares); + function getShares( + address staker, + string memory validator + ) external view returns (uint256 shares); } interface WZETA { function deposit() external payable; + function withdraw(uint256 wad) external; } @@ -94,18 +101,30 @@ contract TestStaking { wzeta.withdraw(wad); } - function stake(address staker, string memory validator, uint256 amount) external onlyOwner returns (bool) { + function stake( + address staker, + string memory validator, + uint256 amount + ) external onlyOwner returns (bool) { return staking.stake(staker, validator, amount); } - function stakeWithStateUpdate(address staker, string memory validator, uint256 amount) external onlyOwner returns (bool) { + function stakeWithStateUpdate( + address staker, + string memory validator, + uint256 amount + ) external onlyOwner returns (bool) { counter = counter + 1; bool success = staking.stake(staker, validator, amount); counter = counter + 1; return success; } - function stakeAndRevert(address staker, string memory validator, uint256 amount) external onlyOwner returns (bool) { + function stakeAndRevert( + address staker, + string memory validator, + uint256 amount + ) external onlyOwner returns (bool) { counter = counter + 1; staking.stake(staker, validator, amount); counter = counter + 1; @@ -129,15 +148,22 @@ contract TestStaking { return staking.moveStake(staker, validatorSrc, validatorDst, amount); } - function getShares(address staker, string memory validator) external view returns(uint256 shares) { + function getShares( + address staker, + string memory validator + ) external view returns (uint256 shares) { return staking.getShares(staker, validator); } - function getAllValidators() external view returns (Validator[] memory validators) { + function getAllValidators() + external + view + returns (Validator[] memory validators) + { return staking.getAllValidators(); } fallback() external payable {} receive() external payable {} -} \ No newline at end of file +} diff --git a/e2e/e2etests/e2etests.go b/e2e/e2etests/e2etests.go index 979876b352..82764cb537 100644 --- a/e2e/e2etests/e2etests.go +++ b/e2e/e2etests/e2etests.go @@ -65,25 +65,33 @@ const ( /** * TON tests */ - TestTONDepositName = "ton_deposit" - TestTONDepositAndCallName = "ton_deposit_and_call" + TestTONDepositName = "ton_deposit" + TestTONDepositAndCallName = "ton_deposit_and_call" + TestTONDepositAndCallRefundName = "ton_deposit_refund" + TestTONWithdrawName = "ton_withdraw" + TestTONWithdrawConcurrentName = "ton_withdraw_concurrent" /* Bitcoin tests Test transfer of Bitcoin asset across chains */ - TestBitcoinDepositName = "bitcoin_deposit" - TestBitcoinDepositRefundName = "bitcoin_deposit_refund" - TestBitcoinDepositAndCallName = "bitcoin_deposit_and_call" - TestBitcoinWithdrawSegWitName = "bitcoin_withdraw_segwit" - TestBitcoinWithdrawTaprootName = "bitcoin_withdraw_taproot" - TestBitcoinWithdrawMultipleName = "bitcoin_withdraw_multiple" - TestBitcoinWithdrawLegacyName = "bitcoin_withdraw_legacy" - TestBitcoinWithdrawP2WSHName = "bitcoin_withdraw_p2wsh" - TestBitcoinWithdrawP2SHName = "bitcoin_withdraw_p2sh" - TestBitcoinWithdrawInvalidAddressName = "bitcoin_withdraw_invalid" - TestBitcoinWithdrawRestrictedName = "bitcoin_withdraw_restricted" - TestExtractBitcoinInscriptionMemoName = "bitcoin_memo_from_inscription" + TestBitcoinDepositName = "bitcoin_deposit" + TestBitcoinDepositAndCallName = "bitcoin_deposit_and_call" + TestBitcoinDepositAndCallRevertName = "bitcoin_deposit_and_call_revert" + TestBitcoinDonationName = "bitcoin_donation" + TestBitcoinStdMemoDepositName = "bitcoin_std_memo_deposit" + TestBitcoinStdMemoDepositAndCallName = "bitcoin_std_memo_deposit_and_call" + TestBitcoinStdMemoDepositAndCallRevertName = "bitcoin_std_memo_deposit_and_call_revert" + TestBitcoinStdMemoDepositAndCallRevertOtherAddressName = "bitcoin_std_memo_deposit_and_call_revert_other_address" + TestBitcoinStdMemoInscribedDepositAndCallName = "bitcoin_std_memo_inscribed_deposit_and_call" + TestBitcoinWithdrawSegWitName = "bitcoin_withdraw_segwit" + TestBitcoinWithdrawTaprootName = "bitcoin_withdraw_taproot" + TestBitcoinWithdrawMultipleName = "bitcoin_withdraw_multiple" + TestBitcoinWithdrawLegacyName = "bitcoin_withdraw_legacy" + TestBitcoinWithdrawP2WSHName = "bitcoin_withdraw_p2wsh" + TestBitcoinWithdrawP2SHName = "bitcoin_withdraw_p2sh" + TestBitcoinWithdrawInvalidAddressName = "bitcoin_withdraw_invalid" + TestBitcoinWithdrawRestrictedName = "bitcoin_withdraw_restricted" /* Application tests @@ -124,6 +132,7 @@ const ( TestPauseERC20CustodyName = "pause_erc20_custody" TestMigrateERC20CustodyFundsName = "migrate_erc20_custody_funds" TestMigrateTSSName = "migrate_TSS" + TestSolanaWhitelistSPLName = "solana_whitelist_spl" /* V2 smart contract tests @@ -167,13 +176,16 @@ const ( /* Stateful precompiled contracts tests */ - TestPrecompilesPrototypeName = "precompile_contracts_prototype" - TestPrecompilesPrototypeThroughContractName = "precompile_contracts_prototype_through_contract" - TestPrecompilesStakingName = "precompile_contracts_staking" - TestPrecompilesStakingThroughContractName = "precompile_contracts_staking_through_contract" - TestPrecompilesBankName = "precompile_contracts_bank" - TestPrecompilesBankFailName = "precompile_contracts_bank_fail" - TestPrecompilesBankThroughContractName = "precompile_contracts_bank_through_contract" + TestPrecompilesPrototypeName = "precompile_contracts_prototype" + TestPrecompilesPrototypeThroughContractName = "precompile_contracts_prototype_through_contract" + TestPrecompilesStakingName = "precompile_contracts_staking" + TestPrecompilesStakingThroughContractName = "precompile_contracts_staking_through_contract" + TestPrecompilesBankName = "precompile_contracts_bank" + TestPrecompilesBankFailName = "precompile_contracts_bank_fail" + TestPrecompilesBankThroughContractName = "precompile_contracts_bank_through_contract" + TestPrecompilesDistributeName = "precompile_contracts_distribute" + TestPrecompilesDistributeNonZRC20Name = "precompile_contracts_distribute_non_zrc20" + TestPrecompilesDistributeThroughContractName = "precompile_contracts_distribute_through_contract" ) // AllE2ETests is an ordered list of all e2e tests @@ -444,6 +456,12 @@ var AllE2ETests = []runner.E2ETest{ }, TestSolanaWithdrawRestricted, ), + runner.NewE2ETest( + TestSolanaWhitelistSPLName, + "whitelist SPL", + []runner.ArgDefinition{}, + TestSolanaWhitelistSPL, + ), /* TON tests */ @@ -463,15 +481,37 @@ var AllE2ETests = []runner.E2ETest{ }, TestTONDepositAndCall, ), + runner.NewE2ETest( + TestTONDepositAndCallRefundName, + "deposit TON into ZEVM and call a smart contract that reverts; expect refund", + []runner.ArgDefinition{ + {Description: "amount in nano tons", DefaultValue: "1000000000"}, // 1.0 TON + }, + TestTONDepositAndCallRefund, + ), + runner.NewE2ETest( + TestTONWithdrawName, + "withdraw TON from ZEVM", + []runner.ArgDefinition{ + {Description: "amount in nano tons", DefaultValue: "2000000000"}, // 2.0 TON + }, + TestTONWithdraw, + ), + runner.NewE2ETest( + TestTONWithdrawConcurrentName, + "withdraw TON from ZEVM for several recipients simultaneously", + []runner.ArgDefinition{}, + TestTONWithdrawConcurrent, + ), /* Bitcoin tests */ runner.NewE2ETest( - TestExtractBitcoinInscriptionMemoName, - "extract memo from BTC inscription", []runner.ArgDefinition{ + TestBitcoinDonationName, + "donate Bitcoin to TSS address", []runner.ArgDefinition{ {Description: "amount in btc", DefaultValue: "0.1"}, }, - TestExtractBitcoinInscriptionMemo, + TestBitcoinDonation, ), runner.NewE2ETest( TestBitcoinDepositName, @@ -490,11 +530,52 @@ var AllE2ETests = []runner.E2ETest{ TestBitcoinDepositAndCall, ), runner.NewE2ETest( - TestBitcoinDepositRefundName, + TestBitcoinDepositAndCallRevertName, "deposit Bitcoin into ZEVM; expect refund", []runner.ArgDefinition{ {Description: "amount in btc", DefaultValue: "0.1"}, }, - TestBitcoinDepositRefund, + TestBitcoinDepositAndCallRevert, + ), + runner.NewE2ETest( + TestBitcoinStdMemoDepositName, + "deposit Bitcoin into ZEVM with standard memo", + []runner.ArgDefinition{ + {Description: "amount in btc", DefaultValue: "0.2"}, + }, + TestBitcoinStdMemoDeposit, + ), + runner.NewE2ETest( + TestBitcoinStdMemoDepositAndCallName, + "deposit Bitcoin into ZEVM and call a contract with standard memo", + []runner.ArgDefinition{ + {Description: "amount in btc", DefaultValue: "0.5"}, + }, + TestBitcoinStdMemoDepositAndCall, + ), + runner.NewE2ETest( + TestBitcoinStdMemoDepositAndCallRevertName, + "deposit Bitcoin into ZEVM and call a contract with standard memo; expect revert", + []runner.ArgDefinition{ + {Description: "amount in btc", DefaultValue: "0.1"}, + }, + TestBitcoinStdMemoDepositAndCallRevert, + ), + runner.NewE2ETest( + TestBitcoinStdMemoDepositAndCallRevertOtherAddressName, + "deposit Bitcoin into ZEVM and call a contract with standard memo; expect revert to other address", + []runner.ArgDefinition{ + {Description: "amount in btc", DefaultValue: "0.1"}, + }, + TestBitcoinStdMemoDepositAndCallRevertOtherAddress, + ), + runner.NewE2ETest( + TestBitcoinStdMemoInscribedDepositAndCallName, + "deposit Bitcoin into ZEVM and call a contract with inscribed standard memo", + []runner.ArgDefinition{ + {Description: "amount in btc", DefaultValue: "0.1"}, + {Description: "fee rate", DefaultValue: "10"}, + }, + TestBitcoinStdMemoInscribedDepositAndCall, ), runner.NewE2ETest( TestBitcoinWithdrawSegWitName, @@ -997,4 +1078,22 @@ var AllE2ETests = []runner.E2ETest{ []runner.ArgDefinition{}, TestPrecompilesBankThroughContract, ), + runner.NewE2ETest( + TestPrecompilesDistributeName, + "test stateful precompiled contracts distribute", + []runner.ArgDefinition{}, + TestPrecompilesDistribute, + ), + runner.NewE2ETest( + TestPrecompilesDistributeNonZRC20Name, + "test stateful precompiled contracts distribute with non ZRC20 tokens", + []runner.ArgDefinition{}, + TestPrecompilesDistributeNonZRC20, + ), + runner.NewE2ETest( + TestPrecompilesDistributeThroughContractName, + "test stateful precompiled contracts distribute through contract", + []runner.ArgDefinition{}, + TestPrecompilesDistributeThroughContract, + ), } diff --git a/e2e/e2etests/helpers.go b/e2e/e2etests/helpers.go index 64f0920c2a..a7a997d70b 100644 --- a/e2e/e2etests/helpers.go +++ b/e2e/e2etests/helpers.go @@ -1,6 +1,8 @@ package e2etests import ( + "crypto/rand" + "encoding/hex" "math/big" "strconv" @@ -20,6 +22,15 @@ import ( crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" ) +// randomPayload generates a random payload to be used in gateway calls for testing purposes +func randomPayload(r *runner.E2ERunner) string { + bytes := make([]byte, 50) + _, err := rand.Read(bytes) + require.NoError(r, err) + + return hex.EncodeToString(bytes) +} + func withdrawBTCZRC20(r *runner.E2ERunner, to btcutil.Address, amount *big.Int) *btcjson.TxRawResult { tx, err := r.BTCZRC20.Approve( r.ZEVMAuth, diff --git a/e2e/e2etests/test_bitcoin_deposit.go b/e2e/e2etests/test_bitcoin_deposit.go index c9c6fdbf45..590a5c81d8 100644 --- a/e2e/e2etests/test_bitcoin_deposit.go +++ b/e2e/e2etests/test_bitcoin_deposit.go @@ -15,7 +15,7 @@ func TestBitcoinDeposit(r *runner.E2ERunner, args []string) { r.SetBtcAddress(r.Name, false) - txHash := r.DepositBTCWithAmount(depositAmount) + txHash := r.DepositBTCWithAmount(depositAmount, nil) // wait for the cctx to be mined cctx := utils.WaitCctxMinedByInboundHash(r.Ctx, txHash.String(), r.CctxClient, r.Logger, r.CctxTimeout) diff --git a/e2e/e2etests/test_bitcoin_deposit_and_call_revert.go b/e2e/e2etests/test_bitcoin_deposit_and_call_revert.go new file mode 100644 index 0000000000..eed10485bf --- /dev/null +++ b/e2e/e2etests/test_bitcoin_deposit_and_call_revert.go @@ -0,0 +1,51 @@ +package e2etests + +import ( + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zeta-chain/node/e2e/runner" + "github.com/zeta-chain/node/e2e/utils" + "github.com/zeta-chain/node/testutil/sample" + zetabitcoin "github.com/zeta-chain/node/zetaclient/chains/bitcoin" +) + +func TestBitcoinDepositAndCallRevert(r *runner.E2ERunner, args []string) { + // ARRANGE + // Given BTC address + r.SetBtcAddress(r.Name, false) + + // Given "Live" BTC network + stop := r.MineBlocksIfLocalBitcoin() + defer stop() + + // Given amount to send + require.Len(r, args, 1) + amount := parseFloat(r, args[0]) + amount += zetabitcoin.DefaultDepositorFee + + // Given a list of UTXOs + utxos, err := r.ListDeployerUTXOs() + require.NoError(r, err) + require.NotEmpty(r, utxos) + + // ACT + // Send BTC to TSS address with a dummy memo + // zetacore should revert cctx if call is made on a non-existing address + nonExistReceiver := sample.EthAddress() + badMemo := append(nonExistReceiver.Bytes(), []byte("gibberish-memo")...) + txHash, err := r.SendToTSSFromDeployerWithMemo(amount, utxos, badMemo) + require.NoError(r, err) + require.NotEmpty(r, txHash) + + // ASSERT + // Now we want to make sure refund TX is completed. + cctx := utils.WaitCctxRevertedByInboundHash(r.Ctx, r, txHash.String(), r.CctxClient) + + // Check revert tx receiver address and amount + receiver, value := r.QueryOutboundReceiverAndAmount(cctx.OutboundParams[1].Hash) + assert.Equal(r, r.BTCDeployerAddress.EncodeAddress(), receiver) + assert.Positive(r, value) + + r.Logger.Info("Sent %f BTC to TSS with invalid memo, got refund of %d satoshis", amount, value) +} diff --git a/e2e/e2etests/test_bitcoin_deposit_call.go b/e2e/e2etests/test_bitcoin_deposit_call.go index c79ca9c1b8..d3d6917c59 100644 --- a/e2e/e2etests/test_bitcoin_deposit_call.go +++ b/e2e/e2etests/test_bitcoin_deposit_call.go @@ -49,7 +49,7 @@ func TestBitcoinDepositAndCall(r *runner.E2ERunner, args []string) { utils.RequireCCTXStatus(r, cctx, crosschaintypes.CctxStatus_OutboundMined) // check if example contract has been called, 'bar' value should be set to amount - amoutSats, err := zetabitcoin.GetSatoshis(amount) + amountSats, err := zetabitcoin.GetSatoshis(amount) require.NoError(r, err) - utils.MustHaveCalledExampleContract(r, contract, big.NewInt(amoutSats)) + utils.MustHaveCalledExampleContract(r, contract, big.NewInt(amountSats)) } diff --git a/e2e/e2etests/test_bitcoin_deposit_refund.go b/e2e/e2etests/test_bitcoin_deposit_refund.go deleted file mode 100644 index b0189d4b69..0000000000 --- a/e2e/e2etests/test_bitcoin_deposit_refund.go +++ /dev/null @@ -1,66 +0,0 @@ -package e2etests - -import ( - "github.com/btcsuite/btcd/chaincfg/chainhash" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/zeta-chain/node/e2e/runner" - "github.com/zeta-chain/node/e2e/utils" - "github.com/zeta-chain/node/x/crosschain/types" - zetabitcoin "github.com/zeta-chain/node/zetaclient/chains/bitcoin" -) - -func TestBitcoinDepositRefund(r *runner.E2ERunner, args []string) { - // ARRANGE - // Given BTC address - r.SetBtcAddress(r.Name, false) - - // Given "Live" BTC network - stop := r.MineBlocksIfLocalBitcoin() - defer stop() - - // Given amount to send - require.Len(r, args, 1) - amount := parseFloat(r, args[0]) - amount += zetabitcoin.DefaultDepositorFee - - // Given a list of UTXOs - utxos, err := r.ListDeployerUTXOs() - require.NoError(r, err) - require.NotEmpty(r, utxos) - - // ACT - // Send BTC to TSS address with a dummy memo - txHash, err := r.SendToTSSFromDeployerWithMemo(amount, utxos, []byte("gibberish-memo")) - require.NoError(r, err) - require.NotEmpty(r, txHash) - - // ASSERT - // Now we want to make sure refund TX is completed. - // Let's check that zetaclient issued a refund on BTC - searchForCrossChainWithBtcRefund := utils.Matches(func(tx types.CrossChainTx) bool { - return tx.GetCctxStatus().Status == types.CctxStatus_Reverted && - len(tx.OutboundParams) == 2 && - tx.OutboundParams[1].Hash != "" - }) - - cctxs := utils.WaitCctxByInboundHash(r.Ctx, r, txHash.String(), r.CctxClient, searchForCrossChainWithBtcRefund) - require.Len(r, cctxs, 1) - - // Pick btc tx hash from the cctx - btcTxHash, err := chainhash.NewHashFromStr(cctxs[0].OutboundParams[1].Hash) - require.NoError(r, err) - - // Query the BTC network to check the refund transaction - refundTx, err := r.BtcRPCClient.GetTransaction(btcTxHash) - require.NoError(r, err, refundTx) - - // Finally, check the refund transaction details - refundTxDetails := refundTx.Details[0] - assert.Equal(r, "receive", refundTxDetails.Category) - assert.Equal(r, r.BTCDeployerAddress.EncodeAddress(), refundTxDetails.Address) - assert.NotEmpty(r, refundTxDetails.Amount) - - r.Logger.Info("Sent %f BTC to TSS with invalid memo, got refund of %f BTC", amount, refundTxDetails.Amount) -} diff --git a/e2e/e2etests/test_bitcoin_donation.go b/e2e/e2etests/test_bitcoin_donation.go new file mode 100644 index 0000000000..1dd5a34859 --- /dev/null +++ b/e2e/e2etests/test_bitcoin_donation.go @@ -0,0 +1,44 @@ +package e2etests + +import ( + "time" + + "github.com/stretchr/testify/require" + + "github.com/zeta-chain/node/e2e/runner" + "github.com/zeta-chain/node/pkg/constant" + crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" + zetabitcoin "github.com/zeta-chain/node/zetaclient/chains/bitcoin" +) + +func TestBitcoinDonation(r *runner.E2ERunner, args []string) { + // ARRANGE + // Given BTC address + r.SetBtcAddress(r.Name, false) + + // Given "Live" BTC network + stop := r.MineBlocksIfLocalBitcoin() + defer stop() + + // Given amount to send + require.Len(r, args, 1) + amount := parseFloat(r, args[0]) + amountTotal := amount + zetabitcoin.DefaultDepositorFee + + // Given a list of UTXOs + utxos, err := r.ListDeployerUTXOs() + require.NoError(r, err) + require.NotEmpty(r, utxos) + + // ACT + // Send BTC to TSS address with donation message + memo := []byte(constant.DonationMessage) + txHash, err := r.SendToTSSFromDeployerWithMemo(amountTotal, utxos, memo) + require.NoError(r, err) + + // ASSERT after 4 Zeta blocks + time.Sleep(constant.ZetaBlockTime * 4) + req := &crosschaintypes.QueryInboundHashToCctxDataRequest{InboundHash: txHash.String()} + _, err = r.CctxClient.InTxHashToCctxData(r.Ctx, req) + require.Error(r, err) +} diff --git a/e2e/e2etests/test_bitcoin_std_deposit.go b/e2e/e2etests/test_bitcoin_std_deposit.go new file mode 100644 index 0000000000..e90b23d64a --- /dev/null +++ b/e2e/e2etests/test_bitcoin_std_deposit.go @@ -0,0 +1,65 @@ +package e2etests + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/stretchr/testify/require" + + "github.com/zeta-chain/node/e2e/runner" + "github.com/zeta-chain/node/e2e/utils" + "github.com/zeta-chain/node/pkg/memo" + crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" + "github.com/zeta-chain/node/zetaclient/chains/bitcoin" +) + +func TestBitcoinStdMemoDeposit(r *runner.E2ERunner, args []string) { + // setup deployer BTC address + r.SetBtcAddress(r.Name, false) + + // start mining blocks if local bitcoin + stop := r.MineBlocksIfLocalBitcoin() + defer stop() + + // parse amount to deposit + require.Len(r, args, 1) + amount := parseFloat(r, args[0]) + + // get ERC20 BTC balance before deposit + balanceBefore, err := r.BTCZRC20.BalanceOf(&bind.CallOpts{}, r.EVMAddress()) + require.NoError(r, err) + r.Logger.Info("runner balance of BTC before deposit: %d satoshis", balanceBefore) + + // create standard memo with receiver address + memo := &memo.InboundMemo{ + Header: memo.Header{ + Version: 0, + EncodingFmt: memo.EncodingFmtCompactShort, + OpCode: memo.OpCodeDeposit, + }, + FieldsV0: memo.FieldsV0{ + Receiver: r.EVMAddress(), // to deployer self + }, + } + + // deposit BTC with standard memo + txHash := r.DepositBTCWithAmount(amount, memo) + + // wait for the cctx to be mined + cctx := utils.WaitCctxMinedByInboundHash(r.Ctx, txHash.String(), r.CctxClient, r.Logger, r.CctxTimeout) + r.Logger.CCTX(*cctx, "bitcoin_std_memo_deposit") + utils.RequireCCTXStatus(r, cctx, crosschaintypes.CctxStatus_OutboundMined) + + // get ERC20 BTC balance after deposit + balanceAfter, err := r.BTCZRC20.BalanceOf(&bind.CallOpts{}, r.EVMAddress()) + require.NoError(r, err) + r.Logger.Info("runner balance of BTC after deposit: %d satoshis", balanceAfter) + + // the runner balance should be increased by the deposit amount + amountIncreased := new(big.Int).Sub(balanceAfter, balanceBefore) + amountSatoshis, err := bitcoin.GetSatoshis(amount) + require.NoError(r, err) + require.Positive(r, amountSatoshis) + // #nosec G115 always positive + require.Equal(r, uint64(amountSatoshis), amountIncreased.Uint64()) +} diff --git a/e2e/e2etests/test_bitcoin_std_deposit_and_call.go b/e2e/e2etests/test_bitcoin_std_deposit_and_call.go new file mode 100644 index 0000000000..7a9c6ca255 --- /dev/null +++ b/e2e/e2etests/test_bitcoin_std_deposit_and_call.go @@ -0,0 +1,57 @@ +package e2etests + +import ( + "math/big" + + "github.com/stretchr/testify/require" + + "github.com/zeta-chain/node/e2e/runner" + "github.com/zeta-chain/node/e2e/utils" + "github.com/zeta-chain/node/pkg/memo" + testcontract "github.com/zeta-chain/node/testutil/contracts" + crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" + zetabitcoin "github.com/zeta-chain/node/zetaclient/chains/bitcoin" +) + +func TestBitcoinStdMemoDepositAndCall(r *runner.E2ERunner, args []string) { + // setup deployer BTC address + r.SetBtcAddress(r.Name, false) + + // start mining blocks if local bitcoin + stop := r.MineBlocksIfLocalBitcoin() + defer stop() + + // parse amount to deposit + require.Len(r, args, 1) + amount := parseFloat(r, args[0]) + + // deploy an example contract in ZEVM + contractAddr, _, contract, err := testcontract.DeployExample(r.ZEVMAuth, r.ZEVMClient) + require.NoError(r, err) + + // create standard memo with [receiver, payload] + memo := &memo.InboundMemo{ + Header: memo.Header{ + Version: 0, + EncodingFmt: memo.EncodingFmtCompactShort, + OpCode: memo.OpCodeDepositAndCall, + }, + FieldsV0: memo.FieldsV0{ + Receiver: contractAddr, + Payload: []byte("hello satoshi"), + }, + } + + // deposit BTC with standard memo + txHash := r.DepositBTCWithAmount(amount, memo) + + // wait for the cctx to be mined + cctx := utils.WaitCctxMinedByInboundHash(r.Ctx, txHash.String(), r.CctxClient, r.Logger, r.CctxTimeout) + r.Logger.CCTX(*cctx, "bitcoin_std_memo_deposit_and_call") + utils.RequireCCTXStatus(r, cctx, crosschaintypes.CctxStatus_OutboundMined) + + // check if example contract has been called, 'bar' value should be set to amount + amountSats, err := zetabitcoin.GetSatoshis(amount) + require.NoError(r, err) + utils.MustHaveCalledExampleContract(r, contract, big.NewInt(amountSats)) +} diff --git a/e2e/e2etests/test_bitcoin_std_deposit_and_call_revert.go b/e2e/e2etests/test_bitcoin_std_deposit_and_call_revert.go new file mode 100644 index 0000000000..76bf128aad --- /dev/null +++ b/e2e/e2etests/test_bitcoin_std_deposit_and_call_revert.go @@ -0,0 +1,53 @@ +package e2etests + +import ( + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zeta-chain/node/e2e/runner" + "github.com/zeta-chain/node/e2e/utils" + "github.com/zeta-chain/node/pkg/memo" + "github.com/zeta-chain/node/testutil/sample" +) + +func TestBitcoinStdMemoDepositAndCallRevert(r *runner.E2ERunner, args []string) { + // ARRANGE + // Given BTC address + r.SetBtcAddress(r.Name, false) + + // Start mining blocks + stop := r.MineBlocksIfLocalBitcoin() + defer stop() + + // Parse amount to send + require.Len(r, args, 1) + amount := parseFloat(r, args[0]) + + // Create a memo to call non-existing contract + memo := &memo.InboundMemo{ + Header: memo.Header{ + Version: 0, + EncodingFmt: memo.EncodingFmtCompactShort, + OpCode: memo.OpCodeDepositAndCall, + }, + FieldsV0: memo.FieldsV0{ + Receiver: sample.EthAddress(), // non-existing contract + Payload: []byte("a payload"), + }, + } + + // ACT + // Deposit + txHash := r.DepositBTCWithAmount(amount, memo) + + // ASSERT + // Now we want to make sure revert TX is completed. + cctx := utils.WaitCctxRevertedByInboundHash(r.Ctx, r, txHash.String(), r.CctxClient) + + // Check revert tx receiver address and amount + receiver, value := r.QueryOutboundReceiverAndAmount(cctx.OutboundParams[1].Hash) + assert.Equal(r, r.BTCDeployerAddress.EncodeAddress(), receiver) + assert.Positive(r, value) + + r.Logger.Info("Sent %f BTC to TSS to call non-existing contract, got refund of %d satoshis", amount, value) +} diff --git a/e2e/e2etests/test_bitcoin_std_deposit_and_call_revert_other_address.go b/e2e/e2etests/test_bitcoin_std_deposit_and_call_revert_other_address.go new file mode 100644 index 0000000000..c6da1b1696 --- /dev/null +++ b/e2e/e2etests/test_bitcoin_std_deposit_and_call_revert_other_address.go @@ -0,0 +1,62 @@ +package e2etests + +import ( + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zeta-chain/node/e2e/runner" + "github.com/zeta-chain/node/e2e/utils" + "github.com/zeta-chain/node/pkg/memo" + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/x/crosschain/types" +) + +func TestBitcoinStdMemoDepositAndCallRevertOtherAddress(r *runner.E2ERunner, args []string) { + // ARRANGE + // Given BTC address + r.SetBtcAddress(r.Name, false) + + // Start mining blocks + stop := r.MineBlocksIfLocalBitcoin() + defer stop() + + // Parse amount to send + require.Len(r, args, 1) + amount := parseFloat(r, args[0]) + + // Create a memo to call non-existing contract + revertAddress := "bcrt1qy9pqmk2pd9sv63g27jt8r657wy0d9uee4x2dt2" + memo := &memo.InboundMemo{ + Header: memo.Header{ + Version: 0, + EncodingFmt: memo.EncodingFmtCompactShort, + OpCode: memo.OpCodeDepositAndCall, + }, + FieldsV0: memo.FieldsV0{ + Receiver: sample.EthAddress(), // non-existing contract + Payload: []byte("a payload"), + RevertOptions: types.RevertOptions{ + RevertAddress: revertAddress, + }, + }, + } + + // ACT + // Deposit + txHash := r.DepositBTCWithAmount(amount, memo) + + // ASSERT + // Now we want to make sure revert TX is completed. + cctx := utils.WaitCctxRevertedByInboundHash(r.Ctx, r, txHash.String(), r.CctxClient) + + // Check revert tx receiver address and amount + receiver, value := r.QueryOutboundReceiverAndAmount(cctx.OutboundParams[1].Hash) + assert.Equal(r, revertAddress, receiver) + assert.Positive(r, value) + + r.Logger.Info( + "Sent %f BTC to TSS to call non-existing contract, got refund of %d satoshis to other address", + amount, + value, + ) +} diff --git a/e2e/e2etests/test_bitcoin_std_memo_inscribed_deposit_and_call.go b/e2e/e2etests/test_bitcoin_std_memo_inscribed_deposit_and_call.go new file mode 100644 index 0000000000..c9a5d7af31 --- /dev/null +++ b/e2e/e2etests/test_bitcoin_std_memo_inscribed_deposit_and_call.go @@ -0,0 +1,64 @@ +package e2etests + +import ( + "math/big" + + "github.com/stretchr/testify/require" + + "github.com/zeta-chain/node/e2e/runner" + "github.com/zeta-chain/node/e2e/utils" + "github.com/zeta-chain/node/pkg/memo" + testcontract "github.com/zeta-chain/node/testutil/contracts" + crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" + zetabitcoin "github.com/zeta-chain/node/zetaclient/chains/bitcoin" +) + +func TestBitcoinStdMemoInscribedDepositAndCall(r *runner.E2ERunner, args []string) { + // ARRANGE + // Given BTC address + r.SetBtcAddress(r.Name, false) + + // Start mining blocks + stop := r.MineBlocksIfLocalBitcoin() + defer stop() + + // Given amount to send and fee rate + require.Len(r, args, 2) + amount := parseFloat(r, args[0]) + feeRate := parseInt(r, args[1]) + + // deploy an example contract in ZEVM + contractAddr, _, contract, err := testcontract.DeployExample(r.ZEVMAuth, r.ZEVMClient) + require.NoError(r, err) + + // create a standard memo > 80 bytes + memo := &memo.InboundMemo{ + Header: memo.Header{ + Version: 0, + EncodingFmt: memo.EncodingFmtCompactShort, + OpCode: memo.OpCodeDepositAndCall, + }, + FieldsV0: memo.FieldsV0{ + Receiver: contractAddr, + Payload: []byte("for use case that passes a large memo > 80 bytes, inscripting the memo is the way to go"), + }, + } + memoBytes, err := memo.EncodeToBytes() + require.NoError(r, err) + + // ACT + // Send BTC to TSS address with memo + txHash, depositAmount := r.InscribeToTSSFromDeployerWithMemo(amount, memoBytes, int64(feeRate)) + + // ASSERT + // wait for the cctx to be mined + cctx := utils.WaitCctxMinedByInboundHash(r.Ctx, txHash.String(), r.CctxClient, r.Logger, r.CctxTimeout) + r.Logger.CCTX(*cctx, "bitcoin_std_memo_inscribed_deposit_and_call") + utils.RequireCCTXStatus(r, cctx, crosschaintypes.CctxStatus_OutboundMined) + + // check if example contract has been called, 'bar' value should be set to correct amount + depositFeeSats, err := zetabitcoin.GetSatoshis(zetabitcoin.DefaultDepositorFee) + require.NoError(r, err) + receiveAmount := depositAmount - depositFeeSats + utils.MustHaveCalledExampleContract(r, contract, big.NewInt(receiveAmount)) +} diff --git a/e2e/e2etests/test_bitcoin_withdraw_taproot.go b/e2e/e2etests/test_bitcoin_withdraw_taproot.go index d9d0b4cf48..f675a88c43 100644 --- a/e2e/e2etests/test_bitcoin_withdraw_taproot.go +++ b/e2e/e2etests/test_bitcoin_withdraw_taproot.go @@ -1,10 +1,10 @@ package e2etests import ( + "github.com/btcsuite/btcd/btcutil" "github.com/stretchr/testify/require" "github.com/zeta-chain/node/e2e/runner" - "github.com/zeta-chain/node/pkg/chains" ) func TestBitcoinWithdrawTaproot(r *runner.E2ERunner, args []string) { @@ -15,7 +15,7 @@ func TestBitcoinWithdrawTaproot(r *runner.E2ERunner, args []string) { // parse arguments and withdraw BTC defaultReceiver := "bcrt1pqqqsyqcyq5rqwzqfpg9scrgwpugpzysnzs23v9ccrydpk8qarc0sj9hjuh" receiver, amount := parseBitcoinWithdrawArgs(r, args, defaultReceiver) - _, ok := receiver.(*chains.AddressTaproot) + _, ok := receiver.(*btcutil.AddressTaproot) require.True(r, ok, "Invalid receiver address specified for TestBitcoinWithdrawTaproot.") withdrawBTCZRC20(r, receiver, amount) diff --git a/e2e/e2etests/test_deploy_contract.go b/e2e/e2etests/test_deploy_contract.go index bddef5b95d..e2623a1381 100644 --- a/e2e/e2etests/test_deploy_contract.go +++ b/e2e/e2etests/test_deploy_contract.go @@ -6,9 +6,9 @@ import ( ethcommon "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" - "github.com/zeta-chain/node/e2e/contracts/testdapp" "github.com/zeta-chain/node/e2e/runner" "github.com/zeta-chain/node/e2e/utils" + "github.com/zeta-chain/node/pkg/contracts/testdappv2" ) // deployFunc is a function that deploys a contract @@ -42,11 +42,9 @@ func TestDeployContract(r *runner.E2ERunner, args []string) { // deployZEVMTestDApp deploys the TestDApp contract on ZetaChain func deployZEVMTestDApp(r *runner.E2ERunner) (ethcommon.Address, error) { - addr, tx, _, err := testdapp.DeployTestDApp( + addr, tx, _, err := testdappv2.DeployTestDAppV2( r.ZEVMAuth, r.ZEVMClient, - r.ConnectorZEVMAddr, - r.WZetaAddr, ) if err != nil { return addr, err @@ -63,11 +61,9 @@ func deployZEVMTestDApp(r *runner.E2ERunner) (ethcommon.Address, error) { // deployEVMTestDApp deploys the TestDApp contract on Ethereum func deployEVMTestDApp(r *runner.E2ERunner) (ethcommon.Address, error) { - addr, tx, _, err := testdapp.DeployTestDApp( + addr, tx, _, err := testdappv2.DeployTestDAppV2( r.EVMAuth, r.EVMClient, - r.ConnectorEthAddr, - r.ZetaEthAddr, ) if err != nil { return addr, err diff --git a/e2e/e2etests/test_eth_withdraw.go b/e2e/e2etests/test_eth_withdraw.go index 0a9140578b..0ba15299e1 100644 --- a/e2e/e2etests/test_eth_withdraw.go +++ b/e2e/e2etests/test_eth_withdraw.go @@ -39,12 +39,8 @@ func TestEtherWithdraw(r *runner.E2ERunner, args []string) { utils.RequireCCTXStatus(r, cctx, crosschaintypes.CctxStatus_OutboundMined) - // Previous binary doesn't take EIP-1559 into account, so this will fail. - // Thus, we need to skip this check for upgrade tests - if !r.IsRunningUpgrade() { - withdrawalReceipt := mustFetchEthReceipt(r, cctx) - require.Equal(r, uint8(ethtypes.DynamicFeeTxType), withdrawalReceipt.Type, "receipt type mismatch") - } + withdrawalReceipt := mustFetchEthReceipt(r, cctx) + require.Equal(r, uint8(ethtypes.DynamicFeeTxType), withdrawalReceipt.Type, "receipt type mismatch") r.Logger.Info("TestEtherWithdraw completed") } diff --git a/e2e/e2etests/test_extract_bitcoin_inscription_memo.go b/e2e/e2etests/test_extract_bitcoin_inscription_memo.go deleted file mode 100644 index eedc24b577..0000000000 --- a/e2e/e2etests/test_extract_bitcoin_inscription_memo.go +++ /dev/null @@ -1,57 +0,0 @@ -package e2etests - -import ( - "encoding/hex" - - "github.com/btcsuite/btcd/btcjson" - "github.com/rs/zerolog/log" - "github.com/stretchr/testify/require" - - "github.com/zeta-chain/node/e2e/runner" - btcobserver "github.com/zeta-chain/node/zetaclient/chains/bitcoin/observer" -) - -func TestExtractBitcoinInscriptionMemo(r *runner.E2ERunner, args []string) { - r.SetBtcAddress(r.Name, false) - - // obtain some initial fund - stop := r.MineBlocksIfLocalBitcoin() - defer stop() - r.Logger.Info("Mined blocks") - - // list deployer utxos - utxos, err := r.ListDeployerUTXOs() - require.NoError(r, err) - - amount := parseFloat(r, args[0]) - // this is just some random test memo for inscription - memo, err := hex.DecodeString( - "72f080c854647755d0d9e6f6821f6931f855b9acffd53d87433395672756d58822fd143360762109ab898626556b1c3b8d3096d2361f1297df4a41c1b429471a9aa2fc9be5f27c13b3863d6ac269e4b587d8389f8fd9649859935b0d48dea88cdb40f20c", - ) - require.NoError(r, err) - - txid := r.InscribeToTSSFromDeployerWithMemo(amount, utxos, memo) - - _, err = r.GenerateToAddressIfLocalBitcoin(6, r.BTCDeployerAddress) - require.NoError(r, err) - - rawtx, err := r.BtcRPCClient.GetRawTransactionVerbose(txid) - require.NoError(r, err) - r.Logger.Info("obtained reveal txn id %s", txid) - - dummyCoinbaseTxn := rawtx - events, err := btcobserver.FilterAndParseIncomingTx( - r.BtcRPCClient, - []btcjson.TxRawResult{*dummyCoinbaseTxn, *rawtx}, - 0, - r.BTCTSSAddress.String(), - log.Logger, - r.BitcoinParams, - ) - require.NoError(r, err) - - require.Equal(r, 1, len(events)) - event := events[0] - - require.Equal(r, event.MemoBytes, memo) -} diff --git a/e2e/e2etests/test_migrate_chain_support.go b/e2e/e2etests/test_migrate_chain_support.go index 9916c076c2..5c6a53ec13 100644 --- a/e2e/e2etests/test_migrate_chain_support.go +++ b/e2e/e2etests/test_migrate_chain_support.go @@ -167,12 +167,10 @@ func TestMigrateChainSupport(r *runner.E2ERunner, _ []string) { )) require.NoError(r, err) - // retrieve zrc20 and cctx from event - whitelistCCTXIndex, err := txserver.FetchAttributeFromTxResponse(res, "whitelist_cctx_index") - require.NoError(r, err) - - erc20zrc20Addr, err := txserver.FetchAttributeFromTxResponse(res, "zrc20_address") - require.NoError(r, err) + event, ok := txserver.EventOfType[*crosschaintypes.EventERC20Whitelist](res.Events) + require.True(r, ok, "no EventERC20Whitelist in %s", res.TxHash) + erc20zrc20Addr := event.Zrc20Address + whitelistCCTXIndex := event.WhitelistCctxIndex // wait for the whitelist cctx to be mined newRunner.WaitForMinedCCTXFromIndex(whitelistCCTXIndex) diff --git a/e2e/e2etests/test_migrate_erc20_custody_funds.go b/e2e/e2etests/test_migrate_erc20_custody_funds.go index 4ddb0da4e7..8ff5be6327 100644 --- a/e2e/e2etests/test_migrate_erc20_custody_funds.go +++ b/e2e/e2etests/test_migrate_erc20_custody_funds.go @@ -35,18 +35,17 @@ func TestMigrateERC20CustodyFunds(r *runner.E2ERunner, _ []string) { res, err := r.ZetaTxServer.BroadcastTx(utils.AdminPolicyName, msg) require.NoError(r, err) - // fetch cctx index from tx response - cctxIndex, err := txserver.FetchAttributeFromTxResponse(res, "cctx_index") - require.NoError(r, err) + event, ok := txserver.EventOfType[*crosschaintypes.EventERC20CustodyFundsMigration](res.Events) + require.True(r, ok, "no EventERC20CustodyFundsMigration in %s", res.TxHash) - cctxRes, err := r.CctxClient.Cctx(r.Ctx, &crosschaintypes.QueryGetCctxRequest{Index: cctxIndex}) + cctxRes, err := r.CctxClient.Cctx(r.Ctx, &crosschaintypes.QueryGetCctxRequest{Index: event.CctxIndex}) require.NoError(r, err) cctx := cctxRes.CrossChainTx r.Logger.CCTX(*cctx, "migration") // wait for the cctx to be mined - r.WaitForMinedCCTXFromIndex(cctxIndex) + r.WaitForMinedCCTXFromIndex(event.CctxIndex) // check ERC20 balance on new address newAddrBalance, err := r.ERC20.BalanceOf(&bind.CallOpts{}, newAddr) diff --git a/e2e/e2etests/test_pause_erc20_custody.go b/e2e/e2etests/test_pause_erc20_custody.go index a1b0319c76..0c999d91d6 100644 --- a/e2e/e2etests/test_pause_erc20_custody.go +++ b/e2e/e2etests/test_pause_erc20_custody.go @@ -32,18 +32,19 @@ func TestPauseERC20Custody(r *runner.E2ERunner, _ []string) { res, err := r.ZetaTxServer.BroadcastTx(utils.AdminPolicyName, msg) require.NoError(r, err) - // fetch cctx index from tx response - cctxIndex, err := txserver.FetchAttributeFromTxResponse(res, "cctx_index") - require.NoError(r, err) + event, ok := txserver.EventOfType[*crosschaintypes.EventERC20CustodyPausing](res.Events) + require.True(r, ok, "no EventERC20CustodyPausing in %s", res.TxHash) + + require.True(r, event.Pause, "should be paused") - cctxRes, err := r.CctxClient.Cctx(r.Ctx, &crosschaintypes.QueryGetCctxRequest{Index: cctxIndex}) + cctxRes, err := r.CctxClient.Cctx(r.Ctx, &crosschaintypes.QueryGetCctxRequest{Index: event.CctxIndex}) require.NoError(r, err) cctx := cctxRes.CrossChainTx r.Logger.CCTX(*cctx, "pausing") // wait for the cctx to be mined - r.WaitForMinedCCTXFromIndex(cctxIndex) + r.WaitForMinedCCTXFromIndex(event.CctxIndex) // check ERC20 custody contract is paused paused, err = r.ERC20Custody.Paused(&bind.CallOpts{}) @@ -61,18 +62,19 @@ func TestPauseERC20Custody(r *runner.E2ERunner, _ []string) { res, err = r.ZetaTxServer.BroadcastTx(utils.AdminPolicyName, msg) require.NoError(r, err) - // fetch cctx index from tx response - cctxIndex, err = txserver.FetchAttributeFromTxResponse(res, "cctx_index") - require.NoError(r, err) + event, ok = txserver.EventOfType[*crosschaintypes.EventERC20CustodyPausing](res.Events) + require.True(r, ok, "no EventERC20CustodyPausing in %s", res.TxHash) + + require.False(r, event.Pause, "should be unpaused") - cctxRes, err = r.CctxClient.Cctx(r.Ctx, &crosschaintypes.QueryGetCctxRequest{Index: cctxIndex}) + cctxRes, err = r.CctxClient.Cctx(r.Ctx, &crosschaintypes.QueryGetCctxRequest{Index: event.CctxIndex}) require.NoError(r, err) cctx = cctxRes.CrossChainTx r.Logger.CCTX(*cctx, "unpausing") // wait for the cctx to be mined - r.WaitForMinedCCTXFromIndex(cctxIndex) + r.WaitForMinedCCTXFromIndex(event.CctxIndex) // check ERC20 custody contract is unpaused paused, err = r.ERC20Custody.Paused(&bind.CallOpts{}) diff --git a/e2e/e2etests/test_precompiles_bank_through_contract.go b/e2e/e2etests/test_precompiles_bank_through_contract.go index 6d6384fd9e..4baa6fefb5 100644 --- a/e2e/e2etests/test_precompiles_bank_through_contract.go +++ b/e2e/e2etests/test_precompiles_bank_through_contract.go @@ -58,7 +58,7 @@ func TestPrecompilesBankThroughContract(r *runner.E2ERunner, args []string) { }() // Check initial balances. - balanceShouldBe(r, 0, checkCosmosBalance(r, testBank, zrc20Address, spender)) + balanceShouldBe(r, 0, checkCosmosBalanceThroughBank(r, testBank, zrc20Address, spender)) balanceShouldBe(r, 1000, checkZRC20Balance(r, spender)) balanceShouldBe(r, 0, checkZRC20Balance(r, bankAddress)) @@ -67,7 +67,7 @@ func TestPrecompilesBankThroughContract(r *runner.E2ERunner, args []string) { utils.RequiredTxFailed(r, receipt, "Deposit ERC20ZRC20 without allowance should fail") // Check balances, should be the same. - balanceShouldBe(r, 0, checkCosmosBalance(r, testBank, zrc20Address, spender)) + balanceShouldBe(r, 0, checkCosmosBalanceThroughBank(r, testBank, zrc20Address, spender)) balanceShouldBe(r, 1000, checkZRC20Balance(r, spender)) balanceShouldBe(r, 0, checkZRC20Balance(r, bankAddress)) @@ -80,7 +80,7 @@ func TestPrecompilesBankThroughContract(r *runner.E2ERunner, args []string) { utils.RequiredTxFailed(r, receipt, "Depositting an amount higher than allowed should fail") // Balances shouldn't change. - balanceShouldBe(r, 0, checkCosmosBalance(r, testBank, zrc20Address, spender)) + balanceShouldBe(r, 0, checkCosmosBalanceThroughBank(r, testBank, zrc20Address, spender)) balanceShouldBe(r, 1000, checkZRC20Balance(r, spender)) balanceShouldBe(r, 0, checkZRC20Balance(r, bankAddress)) @@ -93,7 +93,7 @@ func TestPrecompilesBankThroughContract(r *runner.E2ERunner, args []string) { utils.RequiredTxFailed(r, receipt, "Depositting an amount higher than balance should fail") // Balances shouldn't change. - balanceShouldBe(r, 0, checkCosmosBalance(r, testBank, zrc20Address, spender)) + balanceShouldBe(r, 0, checkCosmosBalanceThroughBank(r, testBank, zrc20Address, spender)) balanceShouldBe(r, 1000, checkZRC20Balance(r, spender)) balanceShouldBe(r, 0, checkZRC20Balance(r, bankAddress)) @@ -102,7 +102,7 @@ func TestPrecompilesBankThroughContract(r *runner.E2ERunner, args []string) { utils.RequireTxSuccessful(r, receipt, "Depositting a correct amount should pass") // Balances should be transferred. Bank now locks 500 ZRC20 tokens. - balanceShouldBe(r, 500, checkCosmosBalance(r, testBank, zrc20Address, spender)) + balanceShouldBe(r, 500, checkCosmosBalanceThroughBank(r, testBank, zrc20Address, spender)) balanceShouldBe(r, 500, checkZRC20Balance(r, spender)) balanceShouldBe(r, 500, checkZRC20Balance(r, bankAddress)) @@ -118,7 +118,7 @@ func TestPrecompilesBankThroughContract(r *runner.E2ERunner, args []string) { utils.RequiredTxFailed(r, receipt, "Withdrawing an amount higher than balance should fail") // Balances shouldn't change. - balanceShouldBe(r, 500, checkCosmosBalance(r, testBank, zrc20Address, spender)) + balanceShouldBe(r, 500, checkCosmosBalanceThroughBank(r, testBank, zrc20Address, spender)) balanceShouldBe(r, 500, checkZRC20Balance(r, spender)) balanceShouldBe(r, 500, checkZRC20Balance(r, bankAddress)) @@ -127,7 +127,7 @@ func TestPrecompilesBankThroughContract(r *runner.E2ERunner, args []string) { utils.RequireTxSuccessful(r, receipt, "Withdraw correct amount should pass") // Balances should be reverted to initial state. - balanceShouldBe(r, 0, checkCosmosBalance(r, testBank, zrc20Address, spender)) + balanceShouldBe(r, 0, checkCosmosBalanceThroughBank(r, testBank, zrc20Address, spender)) balanceShouldBe(r, 1000, checkZRC20Balance(r, spender)) balanceShouldBe(r, 0, checkZRC20Balance(r, bankAddress)) @@ -156,7 +156,11 @@ func checkZRC20Balance(r *runner.E2ERunner, target common.Address) *big.Int { return bankZRC20Balance } -func checkCosmosBalance(r *runner.E2ERunner, bank *testbank.TestBank, zrc20, target common.Address) *big.Int { +func checkCosmosBalanceThroughBank( + r *runner.E2ERunner, + bank *testbank.TestBank, + zrc20, target common.Address, +) *big.Int { balance, err := bank.BalanceOf(&bind.CallOpts{Context: r.Ctx, From: r.ZEVMAuth.From}, zrc20, target) require.NoError(r, err) return balance diff --git a/e2e/e2etests/test_precompiles_distribute.go b/e2e/e2etests/test_precompiles_distribute.go new file mode 100644 index 0000000000..36870e090c --- /dev/null +++ b/e2e/e2etests/test_precompiles_distribute.go @@ -0,0 +1,239 @@ +package e2etests + +import ( + "math/big" + + "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + + "github.com/zeta-chain/node/e2e/runner" + "github.com/zeta-chain/node/e2e/utils" + "github.com/zeta-chain/node/precompiles/bank" + "github.com/zeta-chain/node/precompiles/staking" + precompiletypes "github.com/zeta-chain/node/precompiles/types" +) + +func TestPrecompilesDistribute(r *runner.E2ERunner, args []string) { + require.Len(r, args, 0, "No arguments expected") + + var ( + spenderAddress = r.EVMAddress() + distributeContractAddress = staking.ContractAddress + lockerAddress = bank.ContractAddress + + zrc20Address = r.ERC20ZRC20Addr + zrc20Denom = precompiletypes.ZRC20ToCosmosDenom(zrc20Address) + + oneThousand = big.NewInt(1e3) + oneThousandOne = big.NewInt(1001) + fiveHundred = big.NewInt(500) + fiveHundredOne = big.NewInt(501) + + previousGasLimit = r.ZEVMAuth.GasLimit + ) + + // Set new gas limit to avoid out of gas errors. + r.ZEVMAuth.GasLimit = 10_000_000 + + // Set the test to reset the state after it finishes. + defer resetDistributionTest(r, lockerAddress, previousGasLimit, fiveHundred) + + // Get ERC20ZRC20. + txHash := r.DepositERC20WithAmountAndMessage(spenderAddress, oneThousand, []byte{}) + utils.WaitCctxMinedByInboundHash(r.Ctx, txHash.Hex(), r.CctxClient, r.Logger, r.CctxTimeout) + + dstrContract, err := staking.NewIStaking(distributeContractAddress, r.ZEVMClient) + require.NoError(r, err, "failed to create distribute contract caller") + + // DO NOT REMOVE - will be used in a subsequent PR when the ability to withdraw delegator rewards is introduced. + // Get validators through staking contract. + // validators, err := dstrContract.GetAllValidators(&bind.CallOpts{}) + // require.NoError(r, err) + + // Check initial balances. + balanceShouldBe(r, 1000, checkZRC20Balance(r, spenderAddress)) + balanceShouldBe(r, 0, checkZRC20Balance(r, lockerAddress)) + balanceShouldBe(r, 0, checkCosmosBalance(r, r.FeeCollectorAddress, zrc20Denom)) + + tx, err := dstrContract.Distribute(r.ZEVMAuth, zrc20Address, oneThousand) + require.NoError(r, err) + receipt := utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequiredTxFailed(r, receipt, "distribute should fail when there's no allowance") + + // Balances shouldn't change after a failed attempt. + balanceShouldBe(r, 1000, checkZRC20Balance(r, spenderAddress)) + balanceShouldBe(r, 0, checkZRC20Balance(r, lockerAddress)) + balanceShouldBe(r, 0, checkCosmosBalance(r, r.FeeCollectorAddress, zrc20Denom)) + + // Allow 500. + approveAllowance(r, distributeContractAddress, fiveHundred) + + // Shouldn't be able to distribute more than allowed. + tx, err = dstrContract.Distribute(r.ZEVMAuth, zrc20Address, fiveHundredOne) + require.NoError(r, err) + receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequiredTxFailed(r, receipt, "distribute should fail trying to distribute more than allowed") + + // Balances shouldn't change after a failed attempt. + balanceShouldBe(r, 1000, checkZRC20Balance(r, spenderAddress)) + balanceShouldBe(r, 0, checkZRC20Balance(r, lockerAddress)) + balanceShouldBe(r, 0, checkCosmosBalance(r, r.FeeCollectorAddress, zrc20Denom)) + + // Raise the allowance to 1000. + approveAllowance(r, distributeContractAddress, oneThousand) + + // Shouldn't be able to distribute more than owned balance. + tx, err = dstrContract.Distribute(r.ZEVMAuth, zrc20Address, oneThousandOne) + require.NoError(r, err) + receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequiredTxFailed(r, receipt, "distribute should fail trying to distribute more than owned balance") + + // Balances shouldn't change after a failed attempt. + balanceShouldBe(r, 1000, checkZRC20Balance(r, spenderAddress)) + balanceShouldBe(r, 0, checkZRC20Balance(r, lockerAddress)) + balanceShouldBe(r, 0, checkCosmosBalance(r, r.FeeCollectorAddress, zrc20Denom)) + + // Should be able to distribute 500, which is within balance and allowance. + tx, err = dstrContract.Distribute(r.ZEVMAuth, zrc20Address, fiveHundred) + require.NoError(r, err) + receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequireTxSuccessful(r, receipt, "distribute should succeed when distributing within balance and allowance") + + balanceShouldBe(r, 500, checkZRC20Balance(r, spenderAddress)) + balanceShouldBe(r, 500, checkZRC20Balance(r, lockerAddress)) + balanceShouldBe(r, 500, checkCosmosBalance(r, r.FeeCollectorAddress, zrc20Denom)) + + eventDitributed, err := dstrContract.ParseDistributed(*receipt.Logs[0]) + require.NoError(r, err) + require.Equal(r, zrc20Address, eventDitributed.Zrc20Token) + require.Equal(r, spenderAddress, eventDitributed.Zrc20Distributor) + require.Equal(r, fiveHundred.Uint64(), eventDitributed.Amount.Uint64()) + + // After one block the rewards should have been distributed and fee collector should have 0 ZRC20 balance. + r.WaitForBlocks(1) + balanceShouldBe(r, 0, checkCosmosBalance(r, r.FeeCollectorAddress, zrc20Denom)) + + // DO NOT REMOVE THE FOLLOWING CODE + // This section is commented until a following PR introduces the ability to withdraw delegator rewards. + // This validator checks will be used then to complete the whole e2e. + + // res, err := r.DistributionClient.ValidatorDistributionInfo( + // r.Ctx, + // &distributiontypes.QueryValidatorDistributionInfoRequest{ + // ValidatorAddress: validators[0].OperatorAddress, + // }, + // ) + // require.NoError(r, err) + // fmt.Printf("Validator 0 distribution info: %+v\n", res) + + // res2, err := r.DistributionClient.ValidatorOutstandingRewards(r.Ctx, &distributiontypes.QueryValidatorOutstandingRewardsRequest{ + // ValidatorAddress: validators[0].OperatorAddress, + // }) + // require.NoError(r, err) + // fmt.Printf("Validator 0 outstanding rewards: %+v\n", res2) + + // res3, err := r.DistributionClient.ValidatorCommission(r.Ctx, &distributiontypes.QueryValidatorCommissionRequest{ + // ValidatorAddress: validators[0].OperatorAddress, + // }) + // require.NoError(r, err) + // fmt.Printf("Validator 0 commission: %+v\n", res3) + + // // Validator 1 + // res, err = r.DistributionClient.ValidatorDistributionInfo( + // r.Ctx, + // &distributiontypes.QueryValidatorDistributionInfoRequest{ + // ValidatorAddress: validators[1].OperatorAddress, + // }, + // ) + // require.NoError(r, err) + // fmt.Printf("Validator 1 distribution info: %+v\n", res) + + // res2, err = r.DistributionClient.ValidatorOutstandingRewards(r.Ctx, &distributiontypes.QueryValidatorOutstandingRewardsRequest{ + // ValidatorAddress: validators[1].OperatorAddress, + // }) + // require.NoError(r, err) + // fmt.Printf("Validator 1 outstanding rewards: %+v\n", res2) + + // res3, err = r.DistributionClient.ValidatorCommission(r.Ctx, &distributiontypes.QueryValidatorCommissionRequest{ + // ValidatorAddress: validators[1].OperatorAddress, + // }) + // require.NoError(r, err) + // fmt.Printf("Validator 1 commission: %+v\n", res3) +} + +func TestPrecompilesDistributeNonZRC20(r *runner.E2ERunner, args []string) { + require.Len(r, args, 0, "No arguments expected") + + // Increase the gasLimit. It's required because of the gas consumed by precompiled functions. + previousGasLimit := r.ZEVMAuth.GasLimit + r.ZEVMAuth.GasLimit = 10_000_000 + defer func() { + r.ZEVMAuth.GasLimit = previousGasLimit + }() + + spender, dstrAddress := r.EVMAddress(), staking.ContractAddress + + // Create a staking contract caller. + dstrContract, err := staking.NewIStaking(dstrAddress, r.ZEVMClient) + require.NoError(r, err, "Failed to create staking contract caller") + + // Deposit and approve 50 WZETA for the test. + approveAmount := big.NewInt(0).Mul(big.NewInt(1e18), big.NewInt(50)) + r.DepositAndApproveWZeta(approveAmount) + + // Allow the staking contract to spend 25 WZeta tokens. + tx, err := r.WZeta.Approve(r.ZEVMAuth, dstrAddress, big.NewInt(25)) + require.NoError(r, err, "Error approving allowance for staking contract") + receipt := utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + require.EqualValues(r, uint64(1), receipt.Status, "approve allowance tx failed") + + // Check the allowance of the staking in WZeta tokens. Should be 25. + allowance, err := r.WZeta.Allowance(&bind.CallOpts{Context: r.Ctx}, spender, dstrAddress) + require.NoError(r, err, "Error retrieving staking allowance") + require.EqualValues(r, uint64(25), allowance.Uint64(), "Error allowance for staking contract") + + // Call Distribute with 25 Non ZRC20 tokens. Should fail. + tx, err = dstrContract.Distribute(r.ZEVMAuth, r.WZetaAddr, big.NewInt(25)) + require.NoError(r, err, "Error calling staking.distribute()") + receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + require.Equal(r, uint64(0), receipt.Status, "Non ZRC20 deposit should fail") +} + +// checkCosmosBalance checks the cosmos coin balance for an address. The coin is specified by its denom. +func checkCosmosBalance(r *runner.E2ERunner, address types.AccAddress, denom string) *big.Int { + bal, err := r.BankClient.Balance( + r.Ctx, + &banktypes.QueryBalanceRequest{Address: address.String(), Denom: denom}, + ) + require.NoError(r, err) + + return bal.Balance.Amount.BigInt() +} + +func resetDistributionTest( + r *runner.E2ERunner, + lockerAddress common.Address, + previousGasLimit uint64, + amount *big.Int, +) { + r.ZEVMAuth.GasLimit = previousGasLimit + + // Reset the allowance to 0; this is needed when running upgrade tests where this test runs twice. + tx, err := r.ERC20ZRC20.Approve(r.ZEVMAuth, lockerAddress, big.NewInt(0)) + require.NoError(r, err) + receipt := utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequireTxSuccessful(r, receipt, "Resetting allowance failed") + + // Reset balance to 0 for spender; this is needed when running upgrade tests where this test runs twice. + tx, err = r.ERC20ZRC20.Transfer( + r.ZEVMAuth, + common.HexToAddress("0x000000000000000000000000000000000000dEaD"), + amount, + ) + require.NoError(r, err) + receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequireTxSuccessful(r, receipt, "Resetting balance failed") +} diff --git a/e2e/e2etests/test_precompiles_distribute_through_contract.go b/e2e/e2etests/test_precompiles_distribute_through_contract.go new file mode 100644 index 0000000000..444d8e6a59 --- /dev/null +++ b/e2e/e2etests/test_precompiles_distribute_through_contract.go @@ -0,0 +1,120 @@ +package e2etests + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/stretchr/testify/require" + + "github.com/zeta-chain/node/e2e/contracts/testdistribute" + "github.com/zeta-chain/node/e2e/runner" + "github.com/zeta-chain/node/e2e/utils" + "github.com/zeta-chain/node/precompiles/bank" + "github.com/zeta-chain/node/precompiles/staking" + precompiletypes "github.com/zeta-chain/node/precompiles/types" +) + +func TestPrecompilesDistributeThroughContract(r *runner.E2ERunner, args []string) { + require.Len(r, args, 0, "No arguments expected") + + var ( + spenderAddress = r.EVMAddress() + distributeContractAddress = staking.ContractAddress + lockerAddress = bank.ContractAddress + + zrc20Address = r.ERC20ZRC20Addr + zrc20Denom = precompiletypes.ZRC20ToCosmosDenom(zrc20Address) + + oneThousand = big.NewInt(1e3) + oneThousandOne = big.NewInt(1001) + fiveHundred = big.NewInt(500) + fiveHundredOne = big.NewInt(501) + + previousGasLimit = r.ZEVMAuth.GasLimit + ) + + // Get ERC20ZRC20. + txHash := r.DepositERC20WithAmountAndMessage(spenderAddress, oneThousand, []byte{}) + utils.WaitCctxMinedByInboundHash(r.Ctx, txHash.Hex(), r.CctxClient, r.Logger, r.CctxTimeout) + + dstrContract, err := staking.NewIStaking(distributeContractAddress, r.ZEVMClient) + require.NoError(r, err, "failed to create distribute contract caller") + + _, tx, testDstrContract, err := testdistribute.DeployTestDistribute(r.ZEVMAuth, r.ZEVMClient) + require.NoError(r, err) + receipt := utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequireTxSuccessful(r, receipt, "deployment of disitributor caller contract failed") + + // Set new gas limit to avoid out of gas errors. + r.ZEVMAuth.GasLimit = 10_000_000 + + // Set the test to reset the state after it finishes. + defer resetDistributionTest(r, lockerAddress, previousGasLimit, fiveHundred) + + // Check initial balances. + balanceShouldBe(r, 1000, checkZRC20Balance(r, spenderAddress)) + balanceShouldBe(r, 500, checkZRC20Balance(r, lockerAddress)) // Carries 500 from distribute e2e. + balanceShouldBe(r, 0, checkCosmosBalance(r, r.FeeCollectorAddress, zrc20Denom)) + + receipt = distributeThroughContract(r, testDstrContract, zrc20Address, oneThousand) + utils.RequiredTxFailed(r, receipt, "distribute should fail when there's no allowance") + + // Balances shouldn't change after a failed attempt. + balanceShouldBe(r, 1000, checkZRC20Balance(r, spenderAddress)) + balanceShouldBe(r, 500, checkZRC20Balance(r, lockerAddress)) // Carries 500 from distribute e2e. + balanceShouldBe(r, 0, checkCosmosBalance(r, r.FeeCollectorAddress, zrc20Denom)) + + // Allow 500. + approveAllowance(r, distributeContractAddress, fiveHundred) + + receipt = distributeThroughContract(r, testDstrContract, zrc20Address, fiveHundredOne) + utils.RequiredTxFailed(r, receipt, "distribute should fail trying to distribute more than allowed") + + // Balances shouldn't change after a failed attempt. + balanceShouldBe(r, 1000, checkZRC20Balance(r, spenderAddress)) + balanceShouldBe(r, 500, checkZRC20Balance(r, lockerAddress)) // Carries 500 from distribute e2e. + balanceShouldBe(r, 0, checkCosmosBalance(r, r.FeeCollectorAddress, zrc20Denom)) + + // Raise the allowance to 1000. + approveAllowance(r, distributeContractAddress, oneThousand) + + // Shouldn't be able to distribute more than owned balance. + receipt = distributeThroughContract(r, testDstrContract, zrc20Address, oneThousandOne) + utils.RequiredTxFailed(r, receipt, "distribute should fail trying to distribute more than owned balance") + + // Balances shouldn't change after a failed attempt. + balanceShouldBe(r, 1000, checkZRC20Balance(r, spenderAddress)) + balanceShouldBe(r, 500, checkZRC20Balance(r, lockerAddress)) // Carries 500 from distribute e2e. + balanceShouldBe(r, 0, checkCosmosBalance(r, r.FeeCollectorAddress, zrc20Denom)) + + // Should be able to distribute 500, which is within balance and allowance. + receipt = distributeThroughContract(r, testDstrContract, zrc20Address, fiveHundred) + utils.RequireTxSuccessful(r, receipt, "distribute should succeed when distributing within balance and allowance") + + balanceShouldBe(r, 500, checkZRC20Balance(r, spenderAddress)) + balanceShouldBe(r, 1000, checkZRC20Balance(r, lockerAddress)) // Carries 500 from distribute e2e. + balanceShouldBe(r, 500, checkCosmosBalance(r, r.FeeCollectorAddress, zrc20Denom)) + + eventDitributed, err := dstrContract.ParseDistributed(*receipt.Logs[0]) + require.NoError(r, err) + require.Equal(r, zrc20Address, eventDitributed.Zrc20Token) + require.Equal(r, spenderAddress, eventDitributed.Zrc20Distributor) + require.Equal(r, fiveHundred.Uint64(), eventDitributed.Amount.Uint64()) + + // After one block the rewards should have been distributed and fee collector should have 0 ZRC20 balance. + r.WaitForBlocks(1) + balanceShouldBe(r, 0, checkCosmosBalance(r, r.FeeCollectorAddress, zrc20Denom)) +} + +func distributeThroughContract( + r *runner.E2ERunner, + dstr *testdistribute.TestDistribute, + zrc20Address common.Address, + amount *big.Int, +) *types.Receipt { + tx, err := dstr.DistributeThroughContract(r.ZEVMAuth, zrc20Address, amount) + require.NoError(r, err) + receipt := utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + return receipt +} diff --git a/e2e/e2etests/test_solana_whitelist_spl.go b/e2e/e2etests/test_solana_whitelist_spl.go new file mode 100644 index 0000000000..259657f72b --- /dev/null +++ b/e2e/e2etests/test_solana_whitelist_spl.go @@ -0,0 +1,68 @@ +package e2etests + +import ( + "github.com/gagliardetto/solana-go" + "github.com/stretchr/testify/require" + + "github.com/zeta-chain/node/e2e/runner" + "github.com/zeta-chain/node/e2e/txserver" + "github.com/zeta-chain/node/e2e/utils" + "github.com/zeta-chain/node/pkg/chains" + crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" +) + +func TestSolanaWhitelistSPL(r *runner.E2ERunner, _ []string) { + // Deploy a new SPL + r.Logger.Info("Deploying new SPL") + + // load deployer private key + privkey, err := solana.PrivateKeyFromBase58(r.Account.SolanaPrivateKey.String()) + require.NoError(r, err) + + spl := r.DeploySPL(&privkey) + + // check that whitelist entry doesn't exist for this spl + seed := [][]byte{[]byte("whitelist"), spl.PublicKey().Bytes()} + whitelistEntryPDA, _, err := solana.FindProgramAddress(seed, r.GatewayProgram) + require.NoError(r, err) + + whitelistEntryInfo, err := r.SolanaClient.GetAccountInfo(r.Ctx, whitelistEntryPDA) + require.Error(r, err) + require.Nil(r, whitelistEntryInfo) + + // whitelist sol zrc20 + r.Logger.Info("whitelisting spl on new network") + res, err := r.ZetaTxServer.BroadcastTx(utils.AdminPolicyName, crosschaintypes.NewMsgWhitelistERC20( + r.ZetaTxServer.MustGetAccountAddressFromName(utils.AdminPolicyName), + spl.PublicKey().String(), + chains.SolanaLocalnet.ChainId, + "TESTSPL", + "TESTSPL", + 6, + 100000, + )) + require.NoError(r, err) + + event, ok := txserver.EventOfType[*crosschaintypes.EventERC20Whitelist](res.Events) + require.True(r, ok, "no EventERC20Whitelist in %s", res.TxHash) + erc20zrc20Addr := event.Zrc20Address + whitelistCCTXIndex := event.WhitelistCctxIndex + + err = r.ZetaTxServer.InitializeLiquidityCaps(erc20zrc20Addr) + require.NoError(r, err) + + // ensure CCTX created + resCCTX, err := r.CctxClient.Cctx(r.Ctx, &crosschaintypes.QueryGetCctxRequest{Index: whitelistCCTXIndex}) + require.NoError(r, err) + + cctx := resCCTX.CrossChainTx + r.Logger.CCTX(*cctx, "whitelist_cctx") + + // wait for the whitelist cctx to be mined + r.WaitForMinedCCTXFromIndex(whitelistCCTXIndex) + + // check that whitelist entry exists for this spl + whitelistEntryInfo, err = r.SolanaClient.GetAccountInfo(r.Ctx, whitelistEntryPDA) + require.NoError(r, err) + require.NotNil(r, whitelistEntryInfo) +} diff --git a/e2e/e2etests/test_stress_btc_deposit.go b/e2e/e2etests/test_stress_btc_deposit.go index 53caea09df..bedf004bdf 100644 --- a/e2e/e2etests/test_stress_btc_deposit.go +++ b/e2e/e2etests/test_stress_btc_deposit.go @@ -30,7 +30,7 @@ func TestStressBTCDeposit(r *runner.E2ERunner, args []string) { // send the deposits for i := 0; i < numDeposits; i++ { i := i - txHash := r.DepositBTCWithAmount(depositAmount) + txHash := r.DepositBTCWithAmount(depositAmount, nil) r.Logger.Print("index %d: starting deposit, tx hash: %s", i, txHash.String()) eg.Go(func() error { return monitorBTCDeposit(r, txHash, i, time.Now()) }) diff --git a/e2e/e2etests/test_stress_eth_withdraw.go b/e2e/e2etests/test_stress_eth_withdraw.go index 337a4d416d..864890d6cf 100644 --- a/e2e/e2etests/test_stress_eth_withdraw.go +++ b/e2e/e2etests/test_stress_eth_withdraw.go @@ -4,9 +4,10 @@ import ( "fmt" "math/big" "strconv" + "sync" "time" - ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/montanaflynn/stats" "github.com/stretchr/testify/require" "golang.org/x/sync/errgroup" @@ -36,6 +37,10 @@ func TestStressEtherWithdraw(r *runner.E2ERunner, args []string) { // create a wait group to wait for all the withdraws to complete var eg errgroup.Group + // store durations as float64 seconds like prometheus + withdrawDurations := []float64{} + withdrawDurationsLock := sync.Mutex{} + // send the withdraws for i := 0; i < numWithdraws; i++ { i := i @@ -49,29 +54,45 @@ func TestStressEtherWithdraw(r *runner.E2ERunner, args []string) { r.Logger.Print("index %d: starting withdraw, tx hash: %s", i, tx.Hash().Hex()) eg.Go(func() error { - return monitorEtherWithdraw(r, tx, i, time.Now()) + startTime := time.Now() + cctx := utils.WaitCctxMinedByInboundHash(r.Ctx, tx.Hash().Hex(), r.CctxClient, r.Logger, r.ReceiptTimeout) + if cctx.CctxStatus.Status != crosschaintypes.CctxStatus_OutboundMined { + return fmt.Errorf( + "index %d: withdraw cctx failed with status %s, message %s, cctx index %s", + i, + cctx.CctxStatus.Status, + cctx.CctxStatus.StatusMessage, + cctx.Index, + ) + } + timeToComplete := time.Since(startTime) + r.Logger.Print("index %d: withdraw cctx success in %s", i, timeToComplete.String()) + + withdrawDurationsLock.Lock() + withdrawDurations = append(withdrawDurations, timeToComplete.Seconds()) + withdrawDurationsLock.Unlock() + + return nil }) } - require.NoError(r, eg.Wait()) + err = eg.Wait() - r.Logger.Print("all withdraws completed") -} + desc, descErr := stats.Describe(withdrawDurations, false, &[]float64{50.0, 75.0, 90.0, 95.0}) + if descErr != nil { + r.Logger.Print("❌ failed to calculate latency report: %v", descErr) + } -// monitorEtherWithdraw monitors the withdraw of ether, returns once the withdraw is complete -func monitorEtherWithdraw(r *runner.E2ERunner, tx *ethtypes.Transaction, index int, startTime time.Time) error { - cctx := utils.WaitCctxMinedByInboundHash(r.Ctx, tx.Hash().Hex(), r.CctxClient, r.Logger, r.ReceiptTimeout) - if cctx.CctxStatus.Status != crosschaintypes.CctxStatus_OutboundMined { - return fmt.Errorf( - "index %d: withdraw cctx failed with status %s, message %s, cctx index %s", - index, - cctx.CctxStatus.Status, - cctx.CctxStatus.StatusMessage, - cctx.Index, - ) + r.Logger.Print("Latency report:") + r.Logger.Print("min: %.2f", desc.Min) + r.Logger.Print("max: %.2f", desc.Max) + r.Logger.Print("mean: %.2f", desc.Mean) + r.Logger.Print("std: %.2f", desc.Std) + for _, p := range desc.DescriptionPercentiles { + r.Logger.Print("p%.0f: %.2f", p.Percentile, p.Value) } - timeToComplete := time.Since(startTime) - r.Logger.Print("index %d: withdraw cctx success in %s", index, timeToComplete.String()) - return nil + require.NoError(r, err) + + r.Logger.Print("all withdraws completed") } diff --git a/e2e/e2etests/test_ton_deposit.go b/e2e/e2etests/test_ton_deposit.go index 73860629df..4ee6b4a6fb 100644 --- a/e2e/e2etests/test_ton_deposit.go +++ b/e2e/e2etests/test_ton_deposit.go @@ -1,53 +1,40 @@ package e2etests import ( - "time" - - "cosmossdk.io/math" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/stretchr/testify/require" "github.com/zeta-chain/node/e2e/runner" - "github.com/zeta-chain/node/e2e/runner/ton" - "github.com/zeta-chain/node/pkg/chains" + toncontracts "github.com/zeta-chain/node/pkg/contracts/ton" "github.com/zeta-chain/node/testutil/sample" - cctypes "github.com/zeta-chain/node/x/crosschain/types" ) func TestTONDeposit(r *runner.E2ERunner, args []string) { require.Len(r, args, 1) // Given deployer - ctx, deployer, chain := r.Ctx, r.TONDeployer, chains.TONLocalnet + ctx, deployer := r.Ctx, r.TONDeployer // Given amount amount := parseUint(r, args[0]) - // https://github.com/zeta-chain/protocol-contracts-ton/blob/main/contracts/gateway.fc#L28 - // (will be optimized & dynamic in the future) - depositFee := math.NewUint(10_000_000) + // Given approx deposit fee + depositFee, err := r.TONGateway.GetTxFee(ctx, r.Clients.TON, toncontracts.OpDeposit) + require.NoError(r, err) // Given sample wallet with a balance of 50 TON - sender, err := deployer.CreateWallet(ctx, ton.TONCoins(50)) + sender, err := deployer.CreateWallet(ctx, toncontracts.Coins(50)) require.NoError(r, err) // Given sample EVM address recipient := sample.EthAddress() // ACT - err = r.TONDeposit(sender, amount, recipient) + cctx, err := r.TONDeposit(sender, amount, recipient) // ASSERT require.NoError(r, err) - // Wait for CCTX mining - filter := func(cctx *cctypes.CrossChainTx) bool { - return cctx.InboundParams.SenderChainId == chain.ChainId && - cctx.InboundParams.Sender == sender.GetAddress().ToRaw() - } - - cctx := r.WaitForSpecificCCTX(filter, time.Minute) - // Check CCTX expectedDeposit := amount.Sub(depositFee) diff --git a/e2e/e2etests/test_ton_deposit_and_call.go b/e2e/e2etests/test_ton_deposit_and_call.go index 43e5dcc4e0..d0b2cac9e0 100644 --- a/e2e/e2etests/test_ton_deposit_and_call.go +++ b/e2e/e2etests/test_ton_deposit_and_call.go @@ -1,35 +1,30 @@ package e2etests import ( - "time" - - "cosmossdk.io/math" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/stretchr/testify/require" "github.com/zeta-chain/node/e2e/runner" - "github.com/zeta-chain/node/e2e/runner/ton" "github.com/zeta-chain/node/e2e/utils" - "github.com/zeta-chain/node/pkg/chains" + toncontracts "github.com/zeta-chain/node/pkg/contracts/ton" testcontract "github.com/zeta-chain/node/testutil/contracts" - cctypes "github.com/zeta-chain/node/x/crosschain/types" ) func TestTONDepositAndCall(r *runner.E2ERunner, args []string) { require.Len(r, args, 1) // Given deployer - ctx, deployer, chain := r.Ctx, r.TONDeployer, chains.TONLocalnet + ctx, deployer := r.Ctx, r.TONDeployer // Given amount amount := parseUint(r, args[0]) - // https://github.com/zeta-chain/protocol-contracts-ton/blob/main/contracts/gateway.fc#L28 - // (will be optimized & dynamic in the future) - depositFee := math.NewUint(10_000_000) + // Given approx depositAndCall fee + depositFee, err := r.TONGateway.GetTxFee(ctx, r.Clients.TON, toncontracts.OpDepositAndCall) + require.NoError(r, err) // Given sample wallet with a balance of 50 TON - sender, err := deployer.CreateWallet(ctx, ton.TONCoins(50)) + sender, err := deployer.CreateWallet(ctx, toncontracts.Coins(50)) require.NoError(r, err) // Given sample zEVM contract @@ -41,19 +36,11 @@ func TestTONDepositAndCall(r *runner.E2ERunner, args []string) { callData := []byte("hello from TON!") // ACT - err = r.TONDepositAndCall(sender, amount, contractAddr, callData) + _, err = r.TONDepositAndCall(sender, amount, contractAddr, callData) // ASSERT require.NoError(r, err) - // Wait for CCTX mining - filter := func(cctx *cctypes.CrossChainTx) bool { - return cctx.InboundParams.SenderChainId == chain.ChainId && - cctx.InboundParams.Sender == sender.GetAddress().ToRaw() - } - - r.WaitForSpecificCCTX(filter, time.Minute) - expectedDeposit := amount.Sub(depositFee) // check if example contract has been called, bar value should be set to amount diff --git a/e2e/e2etests/test_ton_deposit_refund.go b/e2e/e2etests/test_ton_deposit_refund.go new file mode 100644 index 0000000000..3259e1d1d6 --- /dev/null +++ b/e2e/e2etests/test_ton_deposit_refund.go @@ -0,0 +1,50 @@ +package e2etests + +import ( + "github.com/stretchr/testify/require" + + "github.com/zeta-chain/node/e2e/runner" + "github.com/zeta-chain/node/e2e/utils" + testcontract "github.com/zeta-chain/node/testutil/contracts" + cctypes "github.com/zeta-chain/node/x/crosschain/types" +) + +func TestTONDepositAndCallRefund(r *runner.E2ERunner, args []string) { + require.Len(r, args, 1) + + // Given amount and arbitrary call data + var ( + amount = parseUint(r, args[0]) + data = []byte("hello reverter") + ) + + // Given deployer mock revert contract + // deploy a reverter contract in ZEVM + reverterAddr, _, _, err := testcontract.DeployReverter(r.ZEVMAuth, r.ZEVMClient) + require.NoError(r, err) + r.Logger.Info("Reverter contract deployed at: %s", reverterAddr.String()) + + // ACT + // Send a deposit and call transaction from the deployer (faucet) + // to the reverter contract + cctx, err := r.TONDepositAndCall( + &r.TONDeployer.Wallet, + amount, + reverterAddr, + data, + runner.TONExpectStatus(cctypes.CctxStatus_Reverted), + ) + + // ASSERT + require.NoError(r, err) + r.Logger.CCTX(*cctx, "ton_deposit_and_refund") + + // Check the error carries the revert executed. + // tolerate the error in both the ErrorMessage field and the StatusMessage field + if cctx.CctxStatus.ErrorMessage != "" { + require.Contains(r, cctx.CctxStatus.ErrorMessage, "revert executed") + return + } + + require.Contains(r, cctx.CctxStatus.StatusMessage, utils.ErrHashRevertFoo) +} diff --git a/e2e/e2etests/test_ton_withdrawal.go b/e2e/e2etests/test_ton_withdrawal.go new file mode 100644 index 0000000000..db75fa4d28 --- /dev/null +++ b/e2e/e2etests/test_ton_withdrawal.go @@ -0,0 +1,90 @@ +package e2etests + +import ( + "math/big" + + "cosmossdk.io/math" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/stretchr/testify/require" + + "github.com/zeta-chain/node/e2e/runner" + toncontracts "github.com/zeta-chain/node/pkg/contracts/ton" + "github.com/zeta-chain/node/zetaclient/chains/ton/liteapi" +) + +func TestTONWithdraw(r *runner.E2ERunner, args []string) { + // ARRANGE + require.Len(r, args, 1) + + // Given a deployer + _, deployer := r.Ctx, r.TONDeployer + + // And zEVM sender + zevmSender := r.ZEVMAuth.From + + // Given his ZRC-20 balance + senderZRC20BalanceBefore, err := r.TONZRC20.BalanceOf(&bind.CallOpts{}, zevmSender) + require.NoError(r, err) + r.Logger.Info("zEVM sender's ZRC20 TON balance before withdraw: %d", senderZRC20BalanceBefore) + + // Given another TON wallet + tonRecipient, err := deployer.CreateWallet(r.Ctx, toncontracts.Coins(1)) + require.NoError(r, err) + + tonRecipientBalanceBefore, err := deployer.GetBalanceOf(r.Ctx, tonRecipient.GetAddress(), true) + require.NoError(r, err) + + r.Logger.Info("Recipient's TON balance before withdrawal: %s", toncontracts.FormatCoins(tonRecipientBalanceBefore)) + + // Given amount to withdraw (and approved amount in TON ZRC20 to cover the gas fee) + amount := parseUint(r, args[0]) + approvedAmount := amount.Add(toncontracts.Coins(1)) + + // ACT + cctx := r.WithdrawTONZRC20(tonRecipient.GetAddress(), amount.BigInt(), approvedAmount.BigInt()) + + // ASSERT + r.Logger.Info( + "Withdraw TON ZRC20 transaction (with %s) sent: %+v", + toncontracts.FormatCoins(amount), + map[string]any{ + "zevm_sender": zevmSender.Hex(), + "ton_recipient": tonRecipient.GetAddress().ToRaw(), + "ton_amount": toncontracts.FormatCoins(amount), + "cctx_index": cctx.Index, + "ton_hash": cctx.GetCurrentOutboundParam().Hash, + "zevm_hash": cctx.InboundParams.ObservedHash, + }, + ) + + // Make sure that recipient's TON balance has increased + tonRecipientBalanceAfter, err := deployer.GetBalanceOf(r.Ctx, tonRecipient.GetAddress(), true) + require.NoError(r, err) + + r.Logger.Info("Recipient's balance after withdrawal: %s", toncontracts.FormatCoins(tonRecipientBalanceAfter)) + + // Make sure that sender's ZRC20 balance has decreased + senderZRC20BalanceAfter, err := r.TONZRC20.BalanceOf(&bind.CallOpts{}, zevmSender) + require.NoError(r, err) + r.Logger.Info("zEVM sender's ZRC20 TON balance after withdraw: %d", senderZRC20BalanceAfter) + r.Logger.Info( + "zEVM sender's ZRC20 TON balance diff: %d", + big.NewInt(0).Sub(senderZRC20BalanceBefore, senderZRC20BalanceAfter), + ) + + // Make sure that TON withdrawal CCTX contain outgoing message with exact withdrawal amount + lt, hash, err := liteapi.TransactionHashFromString(cctx.GetCurrentOutboundParam().Hash) + require.NoError(r, err) + + txs, err := r.Clients.TON.GetTransactions(r.Ctx, 1, r.TONGateway.AccountID(), lt, hash) + require.NoError(r, err) + require.Len(r, txs, 1) + + // TON coins that were withdrawn from GW to the recipient + inMsgAmount := math.NewUint( + uint64(txs[0].Msgs.OutMsgs.Values()[0].Value.Info.IntMsgInfo.Value.Grams), + ) + + // #nosec G115 always in range + require.Equal(r, int(amount.Uint64()), int(inMsgAmount.Uint64())) +} diff --git a/e2e/e2etests/test_ton_withdrawal_concurrent.go b/e2e/e2etests/test_ton_withdrawal_concurrent.go new file mode 100644 index 0000000000..fb4217013d --- /dev/null +++ b/e2e/e2etests/test_ton_withdrawal_concurrent.go @@ -0,0 +1,74 @@ +package e2etests + +import ( + "math/rand" + "sync" + + "cosmossdk.io/math" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/stretchr/testify/require" + "github.com/tonkeeper/tongo/ton" + + "github.com/zeta-chain/node/e2e/runner" + "github.com/zeta-chain/node/e2e/utils" + toncontracts "github.com/zeta-chain/node/pkg/contracts/ton" + "github.com/zeta-chain/node/testutil/sample" + cc "github.com/zeta-chain/node/x/crosschain/types" +) + +// TestTONWithdrawConcurrent makes sure that multiple concurrent +// withdrawals will be eventually processed by sequentially increasing Gateway nonce +// and that zetaclient tolerates "invalid nonce" error from RPC. +func TestTONWithdrawConcurrent(r *runner.E2ERunner, _ []string) { + // ARRANGE + // Given a deployer + _, deployer := r.Ctx, r.TONDeployer + + const recipientsCount = 10 + + // Fire withdrawals. Note that zevm sender is r.ZEVMAuth + var wg sync.WaitGroup + for i := 0; i < recipientsCount; i++ { + // ARRANGE + // Given multiple recipients WITHOUT deployed wallet-contracts + // and withdrawal amounts between 1 and 5 TON + var ( + // #nosec G404: it's a test + amountCoins = 1 + rand.Intn(5) + // #nosec G115 test - always in range + amount = toncontracts.Coins(uint64(amountCoins)) + recipient = sample.GenerateTONAccountID() + ) + + // ACT + r.Logger.Info( + "Withdrawal #%d: sending %s to %s", + i+1, + toncontracts.FormatCoins(amount), + recipient.ToRaw(), + ) + + approvedAmount := amount.Add(toncontracts.Coins(1)) + tx := r.SendWithdrawTONZRC20(recipient, amount.BigInt(), approvedAmount.BigInt()) + + wg.Add(1) + + go func(number int, recipient ton.AccountID, amount math.Uint, tx *ethtypes.Transaction) { + defer wg.Done() + + // wait for the cctx to be mined + cctx := utils.WaitCctxMinedByInboundHash(r.Ctx, tx.Hash().Hex(), r.CctxClient, r.Logger, r.CctxTimeout) + + // ASSERT + utils.RequireCCTXStatus(r, cctx, cc.CctxStatus_OutboundMined) + r.Logger.Info("Withdrawal #%d complete! cctx index: %s", number, cctx.Index) + + // Check recipient's balance ON TON + balance, err := deployer.GetBalanceOf(r.Ctx, recipient, false) + require.NoError(r, err, "failed to get balance of %s", recipient.ToRaw()) + require.Equal(r, amount.Uint64(), balance.Uint64()) + }(i+1, recipient, amount, tx) + } + + wg.Wait() +} diff --git a/e2e/e2etests/test_v2_erc20_deposit_and_call.go b/e2e/e2etests/test_v2_erc20_deposit_and_call.go index 332f2d6009..7b8215162a 100644 --- a/e2e/e2etests/test_v2_erc20_deposit_and_call.go +++ b/e2e/e2etests/test_v2_erc20_deposit_and_call.go @@ -12,8 +12,6 @@ import ( crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" ) -const payloadMessageDepositERC20 = "this is a test ERC20 deposit and call payload" - func TestV2ERC20DepositAndCall(r *runner.E2ERunner, args []string) { require.Len(r, args, 1) @@ -22,7 +20,9 @@ func TestV2ERC20DepositAndCall(r *runner.E2ERunner, args []string) { r.ApproveERC20OnEVM(r.GatewayEVMAddr) - r.AssertTestDAppZEVMCalled(false, payloadMessageDepositERC20, amount) + payload := randomPayload(r) + + r.AssertTestDAppZEVMCalled(false, payload, amount) oldBalance, err := r.ERC20ZRC20.BalanceOf(&bind.CallOpts{}, r.TestDAppV2ZEVMAddr) require.NoError(r, err) @@ -31,7 +31,7 @@ func TestV2ERC20DepositAndCall(r *runner.E2ERunner, args []string) { tx := r.V2ERC20DepositAndCall( r.TestDAppV2ZEVMAddr, amount, - []byte(payloadMessageDepositERC20), + []byte(payload), gatewayevm.RevertOptions{OnRevertGasLimit: big.NewInt(0)}, ) @@ -41,7 +41,7 @@ func TestV2ERC20DepositAndCall(r *runner.E2ERunner, args []string) { require.Equal(r, crosschaintypes.CctxStatus_OutboundMined, cctx.CctxStatus.Status) // check the payload was received on the contract - r.AssertTestDAppZEVMCalled(true, payloadMessageDepositERC20, amount) + r.AssertTestDAppZEVMCalled(true, payload, amount) // check the balance was updated newBalance, err := r.ERC20ZRC20.BalanceOf(&bind.CallOpts{}, r.TestDAppV2ZEVMAddr) diff --git a/e2e/e2etests/test_v2_erc20_deposit_and_call_revert_with_call.go b/e2e/e2etests/test_v2_erc20_deposit_and_call_revert_with_call.go index 0bee6ff837..7f52c9e70f 100644 --- a/e2e/e2etests/test_v2_erc20_deposit_and_call_revert_with_call.go +++ b/e2e/e2etests/test_v2_erc20_deposit_and_call_revert_with_call.go @@ -12,8 +12,6 @@ import ( crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" ) -const payloadMessageDepositOnRevertERC20 = "this is a test ERC20 deposit and call on revert" - func TestV2ERC20DepositAndCallRevertWithCall(r *runner.E2ERunner, args []string) { require.Len(r, args, 1) @@ -22,13 +20,15 @@ func TestV2ERC20DepositAndCallRevertWithCall(r *runner.E2ERunner, args []string) r.ApproveERC20OnEVM(r.GatewayEVMAddr) - r.AssertTestDAppEVMCalled(false, payloadMessageDepositOnRevertERC20, amount) + payload := randomPayload(r) + + r.AssertTestDAppEVMCalled(false, payload, amount) // perform the deposit tx := r.V2ERC20DepositAndCall(r.TestDAppV2ZEVMAddr, amount, []byte("revert"), gatewayevm.RevertOptions{ RevertAddress: r.TestDAppV2EVMAddr, CallOnRevert: true, - RevertMessage: []byte(payloadMessageDepositOnRevertERC20), + RevertMessage: []byte(payload), OnRevertGasLimit: big.NewInt(200000), }) @@ -38,12 +38,12 @@ func TestV2ERC20DepositAndCallRevertWithCall(r *runner.E2ERunner, args []string) require.Equal(r, crosschaintypes.CctxStatus_Reverted, cctx.CctxStatus.Status) // check the payload was received on the contract - r.AssertTestDAppEVMCalled(true, payloadMessageDepositOnRevertERC20, big.NewInt(0)) + r.AssertTestDAppEVMCalled(true, payload, big.NewInt(0)) // check expected sender was used senderForMsg, err := r.TestDAppV2EVM.SenderWithMessage( &bind.CallOpts{}, - []byte(payloadMessageDepositOnRevertERC20), + []byte(payload), ) require.NoError(r, err) require.Equal(r, r.EVMAuth.From, senderForMsg) diff --git a/e2e/e2etests/test_v2_erc20_withdraw_and_arbitrary_call.go b/e2e/e2etests/test_v2_erc20_withdraw_and_arbitrary_call.go index ca48f4be8b..6a73774f18 100644 --- a/e2e/e2etests/test_v2_erc20_withdraw_and_arbitrary_call.go +++ b/e2e/e2etests/test_v2_erc20_withdraw_and_arbitrary_call.go @@ -11,15 +11,15 @@ import ( crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" ) -const payloadMessageWithdrawERC20 = "this is a test ERC20 withdraw and call payload" - func TestV2ERC20WithdrawAndArbitraryCall(r *runner.E2ERunner, args []string) { require.Len(r, args, 1) amount, ok := big.NewInt(0).SetString(args[0], 10) require.True(r, ok, "Invalid amount specified for TestV2ERC20WithdrawAndCall") - r.AssertTestDAppEVMCalled(false, payloadMessageWithdrawERC20, amount) + payload := randomPayload(r) + + r.AssertTestDAppEVMCalled(false, payload, amount) r.ApproveERC20ZRC20(r.GatewayZEVMAddr) r.ApproveETHZRC20(r.GatewayZEVMAddr) @@ -28,7 +28,7 @@ func TestV2ERC20WithdrawAndArbitraryCall(r *runner.E2ERunner, args []string) { tx := r.V2ERC20WithdrawAndArbitraryCall( r.TestDAppV2EVMAddr, amount, - r.EncodeERC20Call(r.ERC20Addr, amount, payloadMessageWithdrawERC20), + r.EncodeERC20Call(r.ERC20Addr, amount, payload), gatewayzevm.RevertOptions{OnRevertGasLimit: big.NewInt(0)}, ) @@ -37,5 +37,5 @@ func TestV2ERC20WithdrawAndArbitraryCall(r *runner.E2ERunner, args []string) { r.Logger.CCTX(*cctx, "withdraw") require.Equal(r, crosschaintypes.CctxStatus_OutboundMined, cctx.CctxStatus.Status) - r.AssertTestDAppEVMCalled(true, payloadMessageWithdrawERC20, amount) + r.AssertTestDAppEVMCalled(true, payload, amount) } diff --git a/e2e/e2etests/test_v2_erc20_withdraw_and_call.go b/e2e/e2etests/test_v2_erc20_withdraw_and_call.go index 4596ba12da..fbc9e2ae7a 100644 --- a/e2e/e2etests/test_v2_erc20_withdraw_and_call.go +++ b/e2e/e2etests/test_v2_erc20_withdraw_and_call.go @@ -12,8 +12,6 @@ import ( crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" ) -const payloadMessageWithdrawAuthenticatedCallERC20 = "this is a test ERC20 withdraw and authenticated call payload" - func TestV2ERC20WithdrawAndCall(r *runner.E2ERunner, _ []string) { previousGasLimit := r.ZEVMAuth.GasLimit r.ZEVMAuth.GasLimit = 10000000 @@ -25,7 +23,9 @@ func TestV2ERC20WithdrawAndCall(r *runner.E2ERunner, _ []string) { // without decoding the payload and amount handling for erc20, purpose of test is to verify correct sender and payload are used amount := big.NewInt(10000) - r.AssertTestDAppEVMCalled(false, payloadMessageWithdrawAuthenticatedCallERC20, amount) + payload := randomPayload(r) + + r.AssertTestDAppEVMCalled(false, payload, amount) r.ApproveERC20ZRC20(r.GatewayZEVMAddr) r.ApproveETHZRC20(r.GatewayZEVMAddr) @@ -34,7 +34,7 @@ func TestV2ERC20WithdrawAndCall(r *runner.E2ERunner, _ []string) { tx := r.V2ERC20WithdrawAndCall( r.TestDAppV2EVMAddr, amount, - []byte(payloadMessageWithdrawAuthenticatedCallERC20), + []byte(payload), gatewayzevm.RevertOptions{OnRevertGasLimit: big.NewInt(0)}, ) @@ -43,12 +43,12 @@ func TestV2ERC20WithdrawAndCall(r *runner.E2ERunner, _ []string) { r.Logger.CCTX(*cctx, "withdraw") require.Equal(r, crosschaintypes.CctxStatus_OutboundMined, cctx.CctxStatus.Status) - r.AssertTestDAppEVMCalled(true, payloadMessageWithdrawAuthenticatedCallERC20, big.NewInt(0)) + r.AssertTestDAppEVMCalled(true, payload, big.NewInt(0)) // check expected sender was used senderForMsg, err := r.TestDAppV2EVM.SenderWithMessage( &bind.CallOpts{}, - []byte(payloadMessageWithdrawAuthenticatedCallERC20), + []byte(payload), ) require.NoError(r, err) require.Equal(r, r.ZEVMAuth.From, senderForMsg) diff --git a/e2e/e2etests/test_v2_erc20_withdraw_and_call_revert_with_call.go b/e2e/e2etests/test_v2_erc20_withdraw_and_call_revert_with_call.go index fb3b3201ff..b3d331f296 100644 --- a/e2e/e2etests/test_v2_erc20_withdraw_and_call_revert_with_call.go +++ b/e2e/e2etests/test_v2_erc20_withdraw_and_call_revert_with_call.go @@ -12,15 +12,15 @@ import ( crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" ) -const payloadMessageWithdrawOnRevertERC20 = "this is a test ERC20 withdraw and call on revert" - func TestV2ERC20WithdrawAndCallRevertWithCall(r *runner.E2ERunner, args []string) { require.Len(r, args, 1) amount, ok := big.NewInt(0).SetString(args[0], 10) require.True(r, ok, "Invalid amount specified for TestV2ERC20WithdrawAndCallRevertWithCall") - r.AssertTestDAppZEVMCalled(false, payloadMessageWithdrawOnRevertERC20, amount) + payload := randomPayload(r) + + r.AssertTestDAppZEVMCalled(false, payload, amount) r.ApproveERC20ZRC20(r.GatewayZEVMAddr) r.ApproveETHZRC20(r.GatewayZEVMAddr) @@ -33,7 +33,7 @@ func TestV2ERC20WithdrawAndCallRevertWithCall(r *runner.E2ERunner, args []string gatewayzevm.RevertOptions{ RevertAddress: r.TestDAppV2ZEVMAddr, CallOnRevert: true, - RevertMessage: []byte(payloadMessageWithdrawOnRevertERC20), + RevertMessage: []byte(payload), OnRevertGasLimit: big.NewInt(0), }, ) @@ -43,12 +43,12 @@ func TestV2ERC20WithdrawAndCallRevertWithCall(r *runner.E2ERunner, args []string r.Logger.CCTX(*cctx, "withdraw") require.Equal(r, crosschaintypes.CctxStatus_Reverted, cctx.CctxStatus.Status) - r.AssertTestDAppZEVMCalled(true, payloadMessageWithdrawOnRevertERC20, big.NewInt(0)) + r.AssertTestDAppZEVMCalled(true, payload, big.NewInt(0)) // check expected sender was used senderForMsg, err := r.TestDAppV2ZEVM.SenderWithMessage( &bind.CallOpts{}, - []byte(payloadMessageWithdrawOnRevertERC20), + []byte(payload), ) require.NoError(r, err) require.Equal(r, r.ZEVMAuth.From, senderForMsg) diff --git a/e2e/e2etests/test_v2_eth_deposit_and_call.go b/e2e/e2etests/test_v2_eth_deposit_and_call.go index a7b2a3a8be..67fbe73846 100644 --- a/e2e/e2etests/test_v2_eth_deposit_and_call.go +++ b/e2e/e2etests/test_v2_eth_deposit_and_call.go @@ -12,15 +12,15 @@ import ( crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" ) -const payloadMessageDepositETH = "this is a test ETH deposit and call payload" - func TestV2ETHDepositAndCall(r *runner.E2ERunner, args []string) { require.Len(r, args, 1) amount, ok := big.NewInt(0).SetString(args[0], 10) require.True(r, ok, "Invalid amount specified for TestV2ETHDepositAndCall") - r.AssertTestDAppZEVMCalled(false, payloadMessageDepositETH, amount) + payload := randomPayload(r) + + r.AssertTestDAppZEVMCalled(false, payload, amount) oldBalance, err := r.ETHZRC20.BalanceOf(&bind.CallOpts{}, r.TestDAppV2ZEVMAddr) require.NoError(r, err) @@ -29,7 +29,7 @@ func TestV2ETHDepositAndCall(r *runner.E2ERunner, args []string) { tx := r.V2ETHDepositAndCall( r.TestDAppV2ZEVMAddr, amount, - []byte(payloadMessageDepositETH), + []byte(payload), gatewayevm.RevertOptions{OnRevertGasLimit: big.NewInt(0)}, ) @@ -39,7 +39,7 @@ func TestV2ETHDepositAndCall(r *runner.E2ERunner, args []string) { require.Equal(r, crosschaintypes.CctxStatus_OutboundMined, cctx.CctxStatus.Status) // check the payload was received on the contract - r.AssertTestDAppZEVMCalled(true, payloadMessageDepositETH, amount) + r.AssertTestDAppZEVMCalled(true, payload, amount) // check the balance was updated newBalance, err := r.ETHZRC20.BalanceOf(&bind.CallOpts{}, r.TestDAppV2ZEVMAddr) diff --git a/e2e/e2etests/test_v2_eth_deposit_and_call_revert_with_call.go b/e2e/e2etests/test_v2_eth_deposit_and_call_revert_with_call.go index 245aa3e636..f01f2ce6e8 100644 --- a/e2e/e2etests/test_v2_eth_deposit_and_call_revert_with_call.go +++ b/e2e/e2etests/test_v2_eth_deposit_and_call_revert_with_call.go @@ -12,8 +12,6 @@ import ( crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" ) -const payloadMessageDepositOnRevertETH = "this is a test ETH deposit and call on revert" - func TestV2ETHDepositAndCallRevertWithCall(r *runner.E2ERunner, args []string) { require.Len(r, args, 1) @@ -22,13 +20,15 @@ func TestV2ETHDepositAndCallRevertWithCall(r *runner.E2ERunner, args []string) { r.ApproveERC20OnEVM(r.GatewayEVMAddr) - r.AssertTestDAppEVMCalled(false, payloadMessageDepositOnRevertETH, amount) + payload := randomPayload(r) + + r.AssertTestDAppEVMCalled(false, payload, amount) // perform the deposit tx := r.V2ETHDepositAndCall(r.TestDAppV2ZEVMAddr, amount, []byte("revert"), gatewayevm.RevertOptions{ RevertAddress: r.TestDAppV2EVMAddr, CallOnRevert: true, - RevertMessage: []byte(payloadMessageDepositOnRevertETH), + RevertMessage: []byte(payload), OnRevertGasLimit: big.NewInt(200000), }) @@ -38,12 +38,12 @@ func TestV2ETHDepositAndCallRevertWithCall(r *runner.E2ERunner, args []string) { require.Equal(r, crosschaintypes.CctxStatus_Reverted, cctx.CctxStatus.Status) // check the payload was received on the contract - r.AssertTestDAppEVMCalled(true, payloadMessageDepositOnRevertETH, big.NewInt(0)) + r.AssertTestDAppEVMCalled(true, payload, big.NewInt(0)) // check expected sender was used senderForMsg, err := r.TestDAppV2EVM.SenderWithMessage( &bind.CallOpts{}, - []byte(payloadMessageDepositOnRevertETH), + []byte(payload), ) require.NoError(r, err) require.Equal(r, r.EVMAuth.From, senderForMsg) diff --git a/e2e/e2etests/test_v2_eth_withdraw_and_arbitrary_call.go b/e2e/e2etests/test_v2_eth_withdraw_and_arbitrary_call.go index b290e33fcb..c3bedecb02 100644 --- a/e2e/e2etests/test_v2_eth_withdraw_and_arbitrary_call.go +++ b/e2e/e2etests/test_v2_eth_withdraw_and_arbitrary_call.go @@ -11,15 +11,15 @@ import ( crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" ) -const payloadMessageWithdrawETH = "this is a test ETH withdraw and call payload" - func TestV2ETHWithdrawAndArbitraryCall(r *runner.E2ERunner, args []string) { require.Len(r, args, 1) amount, ok := big.NewInt(0).SetString(args[0], 10) require.True(r, ok, "Invalid amount specified for TestV2ETHWithdrawAndCall") - r.AssertTestDAppEVMCalled(false, payloadMessageWithdrawETH, amount) + payload := randomPayload(r) + + r.AssertTestDAppEVMCalled(false, payload, amount) r.ApproveETHZRC20(r.GatewayZEVMAddr) @@ -27,7 +27,7 @@ func TestV2ETHWithdrawAndArbitraryCall(r *runner.E2ERunner, args []string) { tx := r.V2ETHWithdrawAndArbitraryCall( r.TestDAppV2EVMAddr, amount, - r.EncodeGasCall(payloadMessageWithdrawETH), + r.EncodeGasCall(payload), gatewayzevm.RevertOptions{OnRevertGasLimit: big.NewInt(0)}, ) @@ -36,5 +36,5 @@ func TestV2ETHWithdrawAndArbitraryCall(r *runner.E2ERunner, args []string) { r.Logger.CCTX(*cctx, "withdraw") require.Equal(r, crosschaintypes.CctxStatus_OutboundMined, cctx.CctxStatus.Status) - r.AssertTestDAppEVMCalled(true, payloadMessageWithdrawETH, amount) + r.AssertTestDAppEVMCalled(true, payload, amount) } diff --git a/e2e/e2etests/test_v2_eth_withdraw_and_call.go b/e2e/e2etests/test_v2_eth_withdraw_and_call.go index bffd037e72..9962bfe462 100644 --- a/e2e/e2etests/test_v2_eth_withdraw_and_call.go +++ b/e2e/e2etests/test_v2_eth_withdraw_and_call.go @@ -12,8 +12,6 @@ import ( crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" ) -const payloadMessageAuthenticatedWithdrawETH = "this is a test ETH withdraw and authenticated call payload" - func TestV2ETHWithdrawAndCall(r *runner.E2ERunner, args []string) { require.Len(r, args, 1) @@ -26,7 +24,9 @@ func TestV2ETHWithdrawAndCall(r *runner.E2ERunner, args []string) { amount, ok := big.NewInt(0).SetString(args[0], 10) require.True(r, ok, "Invalid amount specified for TestV2ETHWithdrawAndCall") - r.AssertTestDAppEVMCalled(false, payloadMessageAuthenticatedWithdrawETH, amount) + payload := randomPayload(r) + + r.AssertTestDAppEVMCalled(false, payload, amount) r.ApproveETHZRC20(r.GatewayZEVMAddr) @@ -34,7 +34,7 @@ func TestV2ETHWithdrawAndCall(r *runner.E2ERunner, args []string) { tx := r.V2ETHWithdrawAndCall( r.TestDAppV2EVMAddr, amount, - []byte(payloadMessageAuthenticatedWithdrawETH), + []byte(payload), gatewayzevm.RevertOptions{OnRevertGasLimit: big.NewInt(0)}, ) @@ -43,12 +43,12 @@ func TestV2ETHWithdrawAndCall(r *runner.E2ERunner, args []string) { r.Logger.CCTX(*cctx, "withdraw") require.Equal(r, crosschaintypes.CctxStatus_OutboundMined, cctx.CctxStatus.Status) - r.AssertTestDAppEVMCalled(true, payloadMessageAuthenticatedWithdrawETH, amount) + r.AssertTestDAppEVMCalled(true, payload, amount) // check expected sender was used senderForMsg, err := r.TestDAppV2EVM.SenderWithMessage( &bind.CallOpts{}, - []byte(payloadMessageAuthenticatedWithdrawETH), + []byte(payload), ) require.NoError(r, err) require.Equal(r, r.ZEVMAuth.From, senderForMsg) diff --git a/e2e/e2etests/test_v2_eth_withdraw_and_call_revert_with_call.go b/e2e/e2etests/test_v2_eth_withdraw_and_call_revert_with_call.go index f613ebffc2..8c7ea40f9e 100644 --- a/e2e/e2etests/test_v2_eth_withdraw_and_call_revert_with_call.go +++ b/e2e/e2etests/test_v2_eth_withdraw_and_call_revert_with_call.go @@ -12,15 +12,15 @@ import ( crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" ) -const payloadMessageWithdrawOnRevertETH = "this is a test ETH withdraw and call on revert" - func TestV2ETHWithdrawAndCallRevertWithCall(r *runner.E2ERunner, args []string) { require.Len(r, args, 1) amount, ok := big.NewInt(0).SetString(args[0], 10) require.True(r, ok, "Invalid amount specified for TestV2ETHWithdrawAndCallRevertWithCall") - r.AssertTestDAppZEVMCalled(false, payloadMessageWithdrawOnRevertETH, amount) + payload := randomPayload(r) + + r.AssertTestDAppZEVMCalled(false, payload, amount) r.ApproveETHZRC20(r.GatewayZEVMAddr) @@ -32,7 +32,7 @@ func TestV2ETHWithdrawAndCallRevertWithCall(r *runner.E2ERunner, args []string) gatewayzevm.RevertOptions{ RevertAddress: r.TestDAppV2ZEVMAddr, CallOnRevert: true, - RevertMessage: []byte(payloadMessageWithdrawOnRevertETH), + RevertMessage: []byte(payload), OnRevertGasLimit: big.NewInt(0), }, ) @@ -42,12 +42,12 @@ func TestV2ETHWithdrawAndCallRevertWithCall(r *runner.E2ERunner, args []string) r.Logger.CCTX(*cctx, "withdraw") require.Equal(r, crosschaintypes.CctxStatus_Reverted, cctx.CctxStatus.Status) - r.AssertTestDAppZEVMCalled(true, payloadMessageWithdrawOnRevertETH, big.NewInt(0)) + r.AssertTestDAppZEVMCalled(true, payload, big.NewInt(0)) // check expected sender was used senderForMsg, err := r.TestDAppV2ZEVM.SenderWithMessage( &bind.CallOpts{}, - []byte(payloadMessageWithdrawOnRevertETH), + []byte(payload), ) require.NoError(r, err) require.Equal(r, r.ZEVMAuth.From, senderForMsg) diff --git a/e2e/e2etests/test_v2_eth_withdraw_and_call_through_contract.go b/e2e/e2etests/test_v2_eth_withdraw_and_call_through_contract.go index 28c36ee69d..ce82f95829 100644 --- a/e2e/e2etests/test_v2_eth_withdraw_and_call_through_contract.go +++ b/e2e/e2etests/test_v2_eth_withdraw_and_call_through_contract.go @@ -12,8 +12,6 @@ import ( crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" ) -const payloadMessageAuthenticatedWithdrawETHThroughContract = "this is a test ETH withdraw and authenticated call payload through contract" - func TestV2ETHWithdrawAndCallThroughContract(r *runner.E2ERunner, args []string) { require.Len(r, args, 1) @@ -40,10 +38,12 @@ func TestV2ETHWithdrawAndCallThroughContract(r *runner.E2ERunner, args []string) require.NoError(r, err) utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + payload := randomPayload(r) + // perform the authenticated call tx = r.V2ETHWithdrawAndCallThroughContract(gatewayCaller, r.TestDAppV2EVMAddr, amount, - []byte(payloadMessageAuthenticatedWithdrawETHThroughContract), + []byte(payload), gatewayzevmcaller.RevertOptions{OnRevertGasLimit: big.NewInt(0)}) utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) @@ -51,12 +51,12 @@ func TestV2ETHWithdrawAndCallThroughContract(r *runner.E2ERunner, args []string) r.Logger.CCTX(*cctx, "withdraw") require.Equal(r, crosschaintypes.CctxStatus_OutboundMined, cctx.CctxStatus.Status) - r.AssertTestDAppEVMCalled(true, payloadMessageAuthenticatedWithdrawETHThroughContract, amount) + r.AssertTestDAppEVMCalled(true, payload, amount) // check expected sender was used senderForMsg, err := r.TestDAppV2EVM.SenderWithMessage( &bind.CallOpts{}, - []byte(payloadMessageAuthenticatedWithdrawETHThroughContract), + []byte(payload), ) require.NoError(r, err) require.Equal(r, gatewayCallerAddr, senderForMsg) diff --git a/e2e/e2etests/test_v2_evm_to_zevm_call.go b/e2e/e2etests/test_v2_evm_to_zevm_call.go index 9fb5502e7a..305f279164 100644 --- a/e2e/e2etests/test_v2_evm_to_zevm_call.go +++ b/e2e/e2etests/test_v2_evm_to_zevm_call.go @@ -11,17 +11,17 @@ import ( crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" ) -const payloadMessageZEVMCall = "this is a test ZEVM call payload" - func TestV2EVMToZEVMCall(r *runner.E2ERunner, args []string) { require.Len(r, args, 0) - r.AssertTestDAppZEVMCalled(false, payloadMessageZEVMCall, big.NewInt(0)) + payload := randomPayload(r) + + r.AssertTestDAppZEVMCalled(false, payload, big.NewInt(0)) // perform the withdraw tx := r.V2EVMToZEMVCall( r.TestDAppV2ZEVMAddr, - []byte(payloadMessageZEVMCall), + []byte(payload), gatewayevm.RevertOptions{OnRevertGasLimit: big.NewInt(0)}, ) @@ -31,5 +31,5 @@ func TestV2EVMToZEVMCall(r *runner.E2ERunner, args []string) { require.Equal(r, crosschaintypes.CctxStatus_OutboundMined, cctx.CctxStatus.Status) // check the payload was received on the contract - r.AssertTestDAppZEVMCalled(true, payloadMessageZEVMCall, big.NewInt(0)) + r.AssertTestDAppZEVMCalled(true, payload, big.NewInt(0)) } diff --git a/e2e/e2etests/test_v2_zevm_to_evm_arbitrary_call.go b/e2e/e2etests/test_v2_zevm_to_evm_arbitrary_call.go index 4b722fc4ae..429fbea714 100644 --- a/e2e/e2etests/test_v2_zevm_to_evm_arbitrary_call.go +++ b/e2e/e2etests/test_v2_zevm_to_evm_arbitrary_call.go @@ -11,12 +11,12 @@ import ( crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" ) -const payloadMessageEVMCall = "this is a test EVM call payload" - func TestV2ZEVMToEVMArbitraryCall(r *runner.E2ERunner, args []string) { require.Len(r, args, 0) - r.AssertTestDAppEVMCalled(false, payloadMessageEVMCall, big.NewInt(0)) + payload := randomPayload(r) + + r.AssertTestDAppEVMCalled(false, payload, big.NewInt(0)) // Necessary approval for fee payment r.ApproveETHZRC20(r.GatewayZEVMAddr) @@ -24,7 +24,7 @@ func TestV2ZEVMToEVMArbitraryCall(r *runner.E2ERunner, args []string) { // perform the call tx := r.V2ZEVMToEMVArbitraryCall( r.TestDAppV2EVMAddr, - r.EncodeSimpleCall(payloadMessageEVMCall), + r.EncodeSimpleCall(payload), gatewayzevm.RevertOptions{ OnRevertGasLimit: big.NewInt(0), }, @@ -36,5 +36,5 @@ func TestV2ZEVMToEVMArbitraryCall(r *runner.E2ERunner, args []string) { require.Equal(r, crosschaintypes.CctxStatus_OutboundMined, cctx.CctxStatus.Status) // check the payload was received on the contract - r.AssertTestDAppEVMCalled(true, payloadMessageEVMCall, big.NewInt(0)) + r.AssertTestDAppEVMCalled(true, payload, big.NewInt(0)) } diff --git a/e2e/e2etests/test_v2_zevm_to_evm_call.go b/e2e/e2etests/test_v2_zevm_to_evm_call.go index 9641778e3c..2f1ed19d1b 100644 --- a/e2e/e2etests/test_v2_zevm_to_evm_call.go +++ b/e2e/e2etests/test_v2_zevm_to_evm_call.go @@ -12,12 +12,12 @@ import ( crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" ) -const payloadMessageEVMAuthenticatedCall = "this is a test EVM authenticated call payload" - func TestV2ZEVMToEVMCall(r *runner.E2ERunner, args []string) { require.Len(r, args, 0) - r.AssertTestDAppEVMCalled(false, payloadMessageEVMAuthenticatedCall, big.NewInt(0)) + payload := randomPayload(r) + + r.AssertTestDAppEVMCalled(false, payload, big.NewInt(0)) // necessary approval for fee payment r.ApproveETHZRC20(r.GatewayZEVMAddr) @@ -25,7 +25,7 @@ func TestV2ZEVMToEVMCall(r *runner.E2ERunner, args []string) { // perform the authenticated call tx := r.V2ZEVMToEMVCall( r.TestDAppV2EVMAddr, - []byte(payloadMessageEVMAuthenticatedCall), + []byte(payload), gatewayzevm.RevertOptions{ OnRevertGasLimit: big.NewInt(0), }, @@ -37,10 +37,10 @@ func TestV2ZEVMToEVMCall(r *runner.E2ERunner, args []string) { require.Equal(r, crosschaintypes.CctxStatus_OutboundMined, cctx.CctxStatus.Status) // check the payload was received on the contract - r.AssertTestDAppEVMCalled(true, payloadMessageEVMAuthenticatedCall, big.NewInt(0)) + r.AssertTestDAppEVMCalled(true, payload, big.NewInt(0)) // check expected sender was used - senderForMsg, err := r.TestDAppV2EVM.SenderWithMessage(&bind.CallOpts{}, []byte(payloadMessageEVMAuthenticatedCall)) + senderForMsg, err := r.TestDAppV2EVM.SenderWithMessage(&bind.CallOpts{}, []byte(payload)) require.NoError(r, err) require.Equal(r, r.ZEVMAuth.From, senderForMsg) } diff --git a/e2e/e2etests/test_v2_zevm_to_evm_call_through_contract.go b/e2e/e2etests/test_v2_zevm_to_evm_call_through_contract.go index 7ff9158365..fa54ca34f6 100644 --- a/e2e/e2etests/test_v2_zevm_to_evm_call_through_contract.go +++ b/e2e/e2etests/test_v2_zevm_to_evm_call_through_contract.go @@ -12,12 +12,12 @@ import ( crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" ) -const payloadMessageEVMAuthenticatedCallThroughContract = "this is a test EVM authenticated call payload through contract" - func TestV2ZEVMToEVMCallThroughContract(r *runner.E2ERunner, args []string) { require.Len(r, args, 0) - r.AssertTestDAppEVMCalled(false, payloadMessageEVMAuthenticatedCallThroughContract, big.NewInt(0)) + payload := randomPayload(r) + + r.AssertTestDAppEVMCalled(false, payload, big.NewInt(0)) // deploy caller contract and send it gas zrc20 to pay gas fee gatewayCallerAddr, tx, gatewayCaller, err := gatewayzevmcaller.DeployGatewayZEVMCaller( @@ -37,7 +37,7 @@ func TestV2ZEVMToEVMCallThroughContract(r *runner.E2ERunner, args []string) { tx = r.V2ZEVMToEMVCallThroughContract( gatewayCaller, r.TestDAppV2EVMAddr, - []byte(payloadMessageEVMAuthenticatedCallThroughContract), + []byte(payload), gatewayzevmcaller.RevertOptions{ OnRevertGasLimit: big.NewInt(0), }, @@ -47,12 +47,12 @@ func TestV2ZEVMToEVMCallThroughContract(r *runner.E2ERunner, args []string) { r.Logger.CCTX(*cctx, "call") require.Equal(r, crosschaintypes.CctxStatus_OutboundMined, cctx.CctxStatus.Status) - r.AssertTestDAppEVMCalled(true, payloadMessageEVMAuthenticatedCallThroughContract, big.NewInt(0)) + r.AssertTestDAppEVMCalled(true, payload, big.NewInt(0)) // check expected sender was used senderForMsg, err := r.TestDAppV2EVM.SenderWithMessage( &bind.CallOpts{}, - []byte(payloadMessageEVMAuthenticatedCallThroughContract), + []byte(payload), ) require.NoError(r, err) require.Equal(r, gatewayCallerAddr, senderForMsg) diff --git a/e2e/e2etests/test_whitelist_erc20.go b/e2e/e2etests/test_whitelist_erc20.go index df34b05597..1823947b98 100644 --- a/e2e/e2etests/test_whitelist_erc20.go +++ b/e2e/e2etests/test_whitelist_erc20.go @@ -43,14 +43,12 @@ func TestWhitelistERC20(r *runner.E2ERunner, _ []string) { )) require.NoError(r, err) - // retrieve zrc20 and cctx from event - whitelistCCTXIndex, err := txserver.FetchAttributeFromTxResponse(res, "whitelist_cctx_index") - require.NoError(r, err) - - erc20zrc20Addr, err := txserver.FetchAttributeFromTxResponse(res, "zrc20_address") - require.NoError(r, err) + event, ok := txserver.EventOfType[*crosschaintypes.EventERC20Whitelist](res.Events) + require.True(r, ok, "no EventERC20Whitelist in %s", res.TxHash) + erc20zrc20Addr := event.Zrc20Address + whitelistCCTXIndex := event.WhitelistCctxIndex - err = r.ZetaTxServer.InitializeLiquidityCap(erc20zrc20Addr) + err = r.ZetaTxServer.InitializeLiquidityCaps(erc20zrc20Addr) require.NoError(r, err) // ensure CCTX created diff --git a/e2e/runner/accounting.go b/e2e/runner/accounting.go index 92e120b7fd..c47883c3f0 100644 --- a/e2e/runner/accounting.go +++ b/e2e/runner/accounting.go @@ -122,7 +122,7 @@ func (r *E2ERunner) CheckBtcTSSBalance() error { ) } // #nosec G115 test - always in range - r.Logger.Print( + r.Logger.Info( "BTC: Balance (%d) >= ZRC20 TotalSupply (%d)", int64(tssTotalBalance*1e8), zrc20Supply.Int64()-10000000, diff --git a/e2e/runner/bitcoin.go b/e2e/runner/bitcoin.go index d2c04fccf0..047e97139b 100644 --- a/e2e/runner/bitcoin.go +++ b/e2e/runner/bitcoin.go @@ -4,7 +4,6 @@ import ( "bytes" "encoding/hex" "fmt" - "net/http" "sort" "time" @@ -21,6 +20,7 @@ import ( "github.com/zeta-chain/node/e2e/utils" "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/pkg/constant" + "github.com/zeta-chain/node/pkg/memo" crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" zetabitcoin "github.com/zeta-chain/node/zetaclient/chains/bitcoin" btcobserver "github.com/zeta-chain/node/zetaclient/chains/bitcoin/observer" @@ -76,10 +76,8 @@ func (r *E2ERunner) GetTop20UTXOsForTssAddress() ([]btcjson.ListUnspentResult, e return utxos, nil } -// DepositBTCWithAmount deposits BTC on ZetaChain with a specific amount -func (r *E2ERunner) DepositBTCWithAmount(amount float64) *chainhash.Hash { - r.Logger.Print("⏳ depositing BTC into ZEVM") - +// DepositBTCWithAmount deposits BTC into ZetaChain with a specific amount and memo +func (r *E2ERunner) DepositBTCWithAmount(amount float64, memo *memo.InboundMemo) *chainhash.Hash { // list deployer utxos utxos, err := r.ListDeployerUTXOs() require.NoError(r, err) @@ -100,8 +98,16 @@ func (r *E2ERunner) DepositBTCWithAmount(amount float64) *chainhash.Hash { r.Logger.Info(" spendableUTXOs: %d", spendableUTXOs) r.Logger.Info("Now sending two txs to TSS address...") + // add depositor fee so that receiver gets the exact given 'amount' in ZetaChain amount += zetabitcoin.DefaultDepositorFee - txHash, err := r.SendToTSSFromDeployerToDeposit(amount, utxos) + + // deposit to TSS address + var txHash *chainhash.Hash + if memo != nil { + txHash, err = r.DepositBTCWithStandardMemo(amount, utxos, memo) + } else { + txHash, err = r.DepositBTCWithLegacyMemo(amount, utxos) + } require.NoError(r, err) r.Logger.Info("send BTC to TSS txHash: %s", txHash.String()) @@ -140,11 +146,11 @@ func (r *E2ERunner) DepositBTC() { // send two transactions to the TSS address amount1 := 1.1 + zetabitcoin.DefaultDepositorFee - _, err = r.SendToTSSFromDeployerToDeposit(amount1, utxos[:2]) + _, err = r.DepositBTCWithLegacyMemo(amount1, utxos[:2]) require.NoError(r, err) amount2 := 0.05 + zetabitcoin.DefaultDepositorFee - txHash2, err := r.SendToTSSFromDeployerToDeposit(amount2, utxos[2:4]) + txHash2, err := r.DepositBTCWithLegacyMemo(amount2, utxos[2:4]) require.NoError(r, err) // send a donation to the TSS address to compensate for the funds minted automatically during pool creation @@ -168,11 +174,34 @@ func (r *E2ERunner) DepositBTC() { require.Equal(r, 1, balance.Sign(), "balance should be positive") } -func (r *E2ERunner) SendToTSSFromDeployerToDeposit(amount float64, inputUTXOs []btcjson.ListUnspentResult) ( - *chainhash.Hash, - error, -) { - return r.SendToTSSFromDeployerWithMemo(amount, inputUTXOs, r.EVMAddress().Bytes()) +// DepositBTCWithLegacyMemo deposits BTC from the deployer address to the TSS using legacy memo +// +// The legacy memo layout: [20-byte receiver] + [payload] +func (r *E2ERunner) DepositBTCWithLegacyMemo( + amount float64, + inputUTXOs []btcjson.ListUnspentResult, +) (*chainhash.Hash, error) { + r.Logger.Info("⏳ depositing BTC into ZEVM with legacy memo") + + // payload is not needed for pure deposit + memoBytes := r.EVMAddress().Bytes() + + return r.SendToTSSFromDeployerWithMemo(amount, inputUTXOs, memoBytes) +} + +// DepositBTCWithStandardMemo deposits BTC from the deployer address to the TSS using standard `InboundMemo` struct +func (r *E2ERunner) DepositBTCWithStandardMemo( + amount float64, + inputUTXOs []btcjson.ListUnspentResult, + memoStd *memo.InboundMemo, +) (*chainhash.Hash, error) { + r.Logger.Info("⏳ depositing BTC into ZEVM with standard memo") + + // encode memo to bytes + memoBytes, err := memoStd.EncodeToBytes() + require.NoError(r, err) + + return r.SendToTSSFromDeployerWithMemo(amount, inputUTXOs, memoBytes) } func (r *E2ERunner) SendToTSSFromDeployerWithMemo( @@ -301,44 +330,47 @@ func (r *E2ERunner) sendToAddrFromDeployerWithMemo( // InscribeToTSSFromDeployerWithMemo creates an inscription that is sent to the tss address with the corresponding memo func (r *E2ERunner) InscribeToTSSFromDeployerWithMemo( amount float64, - inputUTXOs []btcjson.ListUnspentResult, memo []byte, -) *chainhash.Hash { - // TODO: replace builder with Go function to enable instructions - // https://github.com/zeta-chain/node/issues/2759 - builder := InscriptionBuilder{sidecarURL: "http://bitcoin-node-sidecar:8000", client: http.Client{}} - - address, err := builder.GenerateCommitAddress(memo) - require.NoError(r, err) - r.Logger.Info("received inscription commit address %s", address) - - receiver, err := chains.DecodeBtcAddress(address, r.GetBitcoinChainID()) + feeRate int64, +) (*chainhash.Hash, int64) { + // list deployer utxos + utxos, err := r.ListDeployerUTXOs() require.NoError(r, err) - txnHash, err := r.sendToAddrFromDeployerWithMemo(amount, receiver, inputUTXOs, []byte(constant.DonationMessage)) + // generate commit address + builder := NewTapscriptSpender(r.BitcoinParams) + receiver, err := builder.GenerateCommitAddress(memo) require.NoError(r, err) - r.Logger.Info("obtained inscription commit txn hash %s", txnHash.String()) + r.Logger.Info("received inscription commit address: %s", receiver) - // sendToAddrFromDeployerWithMemo makes sure index is 0 - outpointIdx := 0 - hexTx, err := builder.GenerateRevealTxn(r.BTCTSSAddress.String(), txnHash.String(), outpointIdx, amount) + // send funds to the commit address + commitTxHash, err := r.sendToAddrFromDeployerWithMemo(amount, receiver, utxos, nil) require.NoError(r, err) + r.Logger.Info("obtained inscription commit txn hash: %s", commitTxHash.String()) - // Decode the hex string into raw bytes - rawTxBytes, err := hex.DecodeString(hexTx) + // parameters to build the reveal transaction + commitOutputIdx := uint32(0) + commitAmount, err := zetabitcoin.GetSatoshis(amount) require.NoError(r, err) - // Deserialize the raw bytes into a wire.MsgTx structure - msgTx := wire.NewMsgTx(wire.TxVersion) - err = msgTx.Deserialize(bytes.NewReader(rawTxBytes)) + // build the reveal transaction to spend above funds + revealTx, err := builder.BuildRevealTxn( + r.BTCTSSAddress, + wire.OutPoint{ + Hash: *commitTxHash, + Index: commitOutputIdx, + }, + commitAmount, + feeRate, + ) require.NoError(r, err) - r.Logger.Info("recovered inscription reveal txn %s", hexTx) - txid, err := r.BtcRPCClient.SendRawTransaction(msgTx, true) + // submit the reveal transaction + txid, err := r.BtcRPCClient.SendRawTransaction(revealTx, true) require.NoError(r, err) - r.Logger.Info("txid: %+v", txid) + r.Logger.Info("reveal txid: %s", txid.String()) - return txid + return txid, revealTx.TxOut[0].Value } // GetBitcoinChainID gets the bitcoin chain ID from the network params @@ -366,6 +398,25 @@ func (r *E2ERunner) GenerateToAddressIfLocalBitcoin( return nil, nil } +// QueryOutboundReceiverAndAmount queries the outbound receiver and amount (in satoshis) from the given txid +func (r *E2ERunner) QueryOutboundReceiverAndAmount(txid string) (string, int64) { + txHash, err := chainhash.NewHashFromStr(txid) + require.NoError(r, err) + + // query outbound raw transaction + revertTx, err := r.BtcRPCClient.GetRawTransaction(txHash) + require.NoError(r, err, revertTx) + require.True(r, len(revertTx.MsgTx().TxOut) >= 2, "bitcoin outbound must have at least two outputs") + + // parse receiver address from pkScript + txOutput := revertTx.MsgTx().TxOut[1] + pkScript := txOutput.PkScript + receiver, err := zetabitcoin.DecodeScriptP2WPKH(hex.EncodeToString(pkScript), r.BitcoinParams) + require.NoError(r, err) + + return receiver, txOutput.Value +} + // MineBlocksIfLocalBitcoin mines blocks on the local BTC chain at a rate of 1 blocks every 5 seconds // and returns a channel that can be used to stop the mining // If the chain is not local, the function does nothing diff --git a/e2e/runner/bitcoin_inscription.go b/e2e/runner/bitcoin_inscription.go index 6f90068905..5ff237391a 100644 --- a/e2e/runner/bitcoin_inscription.go +++ b/e2e/runner/bitcoin_inscription.go @@ -1,119 +1,234 @@ package runner import ( - "bytes" - "encoding/hex" - "encoding/json" "fmt" - "io" - "net/http" + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/mempool" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" "github.com/pkg/errors" ) -type commitResponse struct { - Address string `json:"address"` -} +// TapscriptSpender is a utility struct that helps create Taproot address and reveal transaction +type TapscriptSpender struct { + // internalKey is a local-generated private key used for signing the Taproot script path. + internalKey *btcec.PrivateKey -type revealResponse struct { - RawHex string `json:"rawHex"` -} + // taprootOutputKey is the Taproot output key derived from the internal key and the merkle root. + // It is used to create Taproot addresses that can be funded. + taprootOutputKey *btcec.PublicKey + + // taprootOutputAddr is the Taproot address derived from the taprootOutputKey. + taprootOutputAddr *btcutil.AddressTaproot + + // tapLeaf represents the Taproot leaf node script (tapscript) that contains the embedded inscription data. + tapLeaf txscript.TapLeaf + + // ctrlBlockBytes contains the control block data required for spending the Taproot output via the script path. + // This includes the internal key and proof for the tapLeaf used to authenticate spending. + ctrlBlockBytes []byte -type revealRequest struct { - Txn string `json:"txn"` - Idx int `json:"idx"` - Amount int `json:"amount"` - FeeRate int `json:"feeRate"` - To string `json:"to"` + net *chaincfg.Params } -// InscriptionBuilder is a util struct that help create inscription commit and reveal transactions -type InscriptionBuilder struct { - sidecarURL string - client http.Client +// NewTapscriptSpender creates a new NewTapscriptSpender instance +func NewTapscriptSpender(net *chaincfg.Params) *TapscriptSpender { + return &TapscriptSpender{ + net: net, + } } -// GenerateCommitAddress generates a commit p2tr address that one can send funds to this address -func (r *InscriptionBuilder) GenerateCommitAddress(memo []byte) (string, error) { - // Create the payload - postData := map[string]string{ - "memo": hex.EncodeToString(memo), +// GenerateCommitAddress generates a Taproot commit address for the given receiver and payload +func (s *TapscriptSpender) GenerateCommitAddress(memo []byte) (*btcutil.AddressTaproot, error) { + // OP_RETURN is a better choice for memo <= 80 bytes + if len(memo) <= txscript.MaxDataCarrierSize { + return nil, fmt.Errorf("OP_RETURN is a better choice for memo <= 80 bytes") } - // Convert the payload to JSON - jsonData, err := json.Marshal(postData) + // generate internal private key, leaf script and Taproot output key + err := s.genTaprootLeafAndKeys(memo) if err != nil { - return "", err + return nil, errors.Wrap(err, "genTaprootLeafAndKeys failed") } - postURL := r.sidecarURL + "/commit" - req, err := http.NewRequest("POST", postURL, bytes.NewBuffer(jsonData)) + return s.taprootOutputAddr, nil +} + +// BuildRevealTxn returns a signed reveal transaction that spends the commit transaction +func (s *TapscriptSpender) BuildRevealTxn( + to btcutil.Address, + commitTxn wire.OutPoint, + commitAmount int64, + feeRate int64, +) (*wire.MsgTx, error) { + // Step 1: create tx message + revealTx := wire.NewMsgTx(2) + + // Step 2: add input (the commit tx) + outpoint := wire.NewOutPoint(&commitTxn.Hash, commitTxn.Index) + revealTx.AddTxIn(wire.NewTxIn(outpoint, nil, nil)) + + // Step 3: add output (to TSS) + pkScript, err := txscript.PayToAddrScript(to) if err != nil { - return "", errors.Wrap(err, "cannot create commit request") + return nil, errors.Wrap(err, "failed to create receiver pkScript") } - req.Header.Set("Content-Type", "application/json") - - // Send the request - resp, err := r.client.Do(req) + fee, err := s.estimateFee(revealTx, to, commitAmount, feeRate) if err != nil { - return "", errors.Wrap(err, "cannot send to sidecar") + return nil, errors.Wrap(err, "failed to estimate fee for reveal txn") } - defer resp.Body.Close() + revealTx.AddTxOut(wire.NewTxOut(commitAmount-fee, pkScript)) - // Read the response body - var response commitResponse - err = json.NewDecoder(resp.Body).Decode(&response) + // Step 4: compute the sighash for the P2TR input to be spent using script path + commitScript, err := txscript.PayToAddrScript(s.taprootOutputAddr) if err != nil { - return "", err + return nil, errors.Wrap(err, "failed to create commit pkScript") + } + prevOutFetcher := txscript.NewCannedPrevOutputFetcher(commitScript, commitAmount) + sigHashes := txscript.NewTxSigHashes(revealTx, prevOutFetcher) + sigHash, err := txscript.CalcTapscriptSignaturehash( + sigHashes, + txscript.SigHashDefault, + revealTx, + int(commitTxn.Index), + prevOutFetcher, + s.tapLeaf, + ) + if err != nil { + return nil, errors.Wrap(err, "failed to calculate tapscript sighash") } - fmt.Print("raw commit response ", response.Address) + // Step 5: sign the sighash with the internal key + sig, err := schnorr.Sign(s.internalKey, sigHash) + if err != nil { + return nil, errors.Wrap(err, "failed to sign sighash") + } + revealTx.TxIn[0].Witness = wire.TxWitness{sig.Serialize(), s.tapLeaf.Script, s.ctrlBlockBytes} - return response.Address, nil + return revealTx, nil } -// GenerateRevealTxn creates the corresponding reveal txn to the commit txn. -func (r *InscriptionBuilder) GenerateRevealTxn(to string, txnHash string, idx int, amount float64) (string, error) { - postData := revealRequest{ - Txn: txnHash, - Idx: idx, - Amount: int(amount * 100000000), - FeeRate: 10, - To: to, +// genTaprootLeafAndKeys generates internal private key, leaf script and Taproot output key +func (s *TapscriptSpender) genTaprootLeafAndKeys(data []byte) error { + // generate an internal private key + internalKey, err := btcec.NewPrivateKey() + if err != nil { + return errors.Wrap(err, "failed to generate internal private key") } - // Convert the payload to JSON - jsonData, err := json.Marshal(postData) + // generate the leaf script + leafScript, err := genLeafScript(internalKey.PubKey(), data) if err != nil { - return "", err + return errors.Wrap(err, "failed to generate leaf script") } - postURL := r.sidecarURL + "/reveal" - req, err := http.NewRequest("POST", postURL, bytes.NewBuffer(jsonData)) + // assemble Taproot tree + tapLeaf := txscript.NewBaseTapLeaf(leafScript) + tapScriptTree := txscript.AssembleTaprootScriptTree(tapLeaf) + + // compute the Taproot output key and address + tapScriptRoot := tapScriptTree.RootNode.TapHash() + taprootOutputKey := txscript.ComputeTaprootOutputKey(internalKey.PubKey(), tapScriptRoot[:]) + taprootOutputAddr, err := btcutil.NewAddressTaproot(schnorr.SerializePubKey(taprootOutputKey), s.net) if err != nil { - return "", errors.Wrap(err, "cannot create reveal request") + return errors.Wrap(err, "failed to create Taproot address") } - req.Header.Set("Content-Type", "application/json") - // Send the request - resp, err := r.client.Do(req) + // construct the control block for the Taproot leaf script. + ctrlBlock := tapScriptTree.LeafMerkleProofs[0].ToControlBlock(internalKey.PubKey()) + ctrlBlockBytes, err := ctrlBlock.ToBytes() if err != nil { - return "", errors.Wrap(err, "cannot send reveal to sidecar") + return errors.Wrap(err, "failed to serialize control block") } - defer resp.Body.Close() - // Read the response body - body, err := io.ReadAll(resp.Body) + // save generated keys, script and control block for later use + s.internalKey = internalKey + s.taprootOutputKey = taprootOutputKey + s.taprootOutputAddr = taprootOutputAddr + s.tapLeaf = tapLeaf + s.ctrlBlockBytes = ctrlBlockBytes + + return nil +} + +// estimateFee estimates the tx fee based given fee rate and estimated tx virtual size +func (s *TapscriptSpender) estimateFee( + tx *wire.MsgTx, + to btcutil.Address, + amount int64, + feeRate int64, +) (int64, error) { + txCopy := tx.Copy() + + // add output to the copied transaction + pkScript, err := txscript.PayToAddrScript(to) if err != nil { - return "", errors.Wrap(err, "cannot read reveal response body") + return 0, err } + txCopy.AddTxOut(wire.NewTxOut(amount, pkScript)) + + // create 64-byte fake Schnorr signature + sigBytes := make([]byte, 64) + + // set the witness for the first input + txWitness := wire.TxWitness{sigBytes, s.tapLeaf.Script, s.ctrlBlockBytes} + txCopy.TxIn[0].Witness = txWitness + + // calculate the fee based on the estimated virtual size + fee := mempool.GetTxVirtualSize(btcutil.NewTx(txCopy)) * feeRate + + return fee, nil +} + +//================================================================================================= +//================================================================================================= + +// LeafScriptBuilder represents a builder for Taproot leaf scripts +type LeafScriptBuilder struct { + script txscript.ScriptBuilder +} + +// NewLeafScriptBuilder initializes a new LeafScriptBuilder with a public key and `OP_CHECKSIG` +func NewLeafScriptBuilder(pubKey *btcec.PublicKey) *LeafScriptBuilder { + builder := txscript.NewScriptBuilder() + builder.AddData(schnorr.SerializePubKey(pubKey)) + builder.AddOp(txscript.OP_CHECKSIG) + + return &LeafScriptBuilder{script: *builder} +} - // Parse the JSON response - var response revealResponse - if err := json.Unmarshal(body, &response); err != nil { - return "", errors.Wrap(err, "cannot parse reveal response body") +// PushData adds a large data to the Taproot leaf script following OP_FALSE and OP_IF structure +func (b *LeafScriptBuilder) PushData(data []byte) { + // start the inscription envelope + b.script.AddOp(txscript.OP_FALSE) + b.script.AddOp(txscript.OP_IF) + + // break data into chunks and push each one + dataLen := len(data) + for i := 0; i < dataLen; i += txscript.MaxScriptElementSize { + if dataLen-i >= txscript.MaxScriptElementSize { + b.script.AddData(data[i : i+txscript.MaxScriptElementSize]) + } else { + b.script.AddData(data[i:]) + } } - // Access the "address" field - return response.RawHex, nil + // end the inscription envelope + b.script.AddOp(txscript.OP_ENDIF) +} + +// Script returns the current script +func (b *LeafScriptBuilder) Script() ([]byte, error) { + return b.script.Script() +} + +// genLeafScript creates a Taproot leaf script using provided pubkey and data +func genLeafScript(pubKey *btcec.PublicKey, data []byte) ([]byte, error) { + builder := NewLeafScriptBuilder(pubKey) + builder.PushData(data) + return builder.Script() } diff --git a/e2e/runner/runner.go b/e2e/runner/runner.go index 03cafb6fc4..cf7a8e6f02 100644 --- a/e2e/runner/runner.go +++ b/e2e/runner/runner.go @@ -9,8 +9,10 @@ import ( "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/rpcclient" + "github.com/cosmos/cosmos-sdk/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + distributiontypes "github.com/cosmos/cosmos-sdk/x/distribution/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" "github.com/ethereum/go-ethereum/accounts/abi/bind" ethcommon "github.com/ethereum/go-ethereum/common" @@ -74,6 +76,7 @@ type E2ERunner struct { SolanaDeployerAddress solana.PublicKey TONDeployer *tonrunner.Deployer TONGateway *toncontracts.Gateway + FeeCollectorAddress types.AccAddress // all clients. // a reference to this type is required to enable creating a new E2ERunner. @@ -86,14 +89,15 @@ type E2ERunner struct { SolanaClient *rpc.Client // zetacored grpc clients - AuthorityClient authoritytypes.QueryClient - CctxClient crosschaintypes.QueryClient - FungibleClient fungibletypes.QueryClient - AuthClient authtypes.QueryClient - BankClient banktypes.QueryClient - StakingClient stakingtypes.QueryClient - ObserverClient observertypes.QueryClient - LightclientClient lightclienttypes.QueryClient + AuthorityClient authoritytypes.QueryClient + CctxClient crosschaintypes.QueryClient + FungibleClient fungibletypes.QueryClient + AuthClient authtypes.QueryClient + BankClient banktypes.QueryClient + StakingClient stakingtypes.QueryClient + ObserverClient observertypes.QueryClient + LightclientClient lightclienttypes.QueryClient + DistributionClient distributiontypes.QueryClient // optional zeta (cosmos) client // typically only in test runners that need it @@ -187,18 +191,21 @@ func NewE2ERunner( Account: account, + FeeCollectorAddress: authtypes.NewModuleAddress(authtypes.FeeCollectorName), + Clients: clients, - ZEVMClient: clients.Zevm, - EVMClient: clients.Evm, - AuthorityClient: clients.Zetacore.Authority, - CctxClient: clients.Zetacore.Crosschain, - FungibleClient: clients.Zetacore.Fungible, - AuthClient: clients.Zetacore.Auth, - BankClient: clients.Zetacore.Bank, - StakingClient: clients.Zetacore.Staking, - ObserverClient: clients.Zetacore.Observer, - LightclientClient: clients.Zetacore.Lightclient, + ZEVMClient: clients.Zevm, + EVMClient: clients.Evm, + AuthorityClient: clients.Zetacore.Authority, + CctxClient: clients.Zetacore.Crosschain, + FungibleClient: clients.Zetacore.Fungible, + AuthClient: clients.Zetacore.Auth, + BankClient: clients.Zetacore.Bank, + StakingClient: clients.Zetacore.Staking, + ObserverClient: clients.Zetacore.Observer, + LightclientClient: clients.Zetacore.Lightclient, + DistributionClient: clients.Zetacore.Distribution, EVMAuth: clients.EvmAuth, ZEVMAuth: clients.ZevmAuth, diff --git a/e2e/runner/setup_evm.go b/e2e/runner/setup_evm.go index 733109fbf6..f14383356b 100644 --- a/e2e/runner/setup_evm.go +++ b/e2e/runner/setup_evm.go @@ -76,7 +76,7 @@ func (r *E2ERunner) SetupEVM(contractsDeployed bool, whitelistERC20 bool) { r.ZetaEth = ZetaEth r.ZetaEthAddr = zetaEthAddr conf.Contracts.EVM.ZetaEthAddr = config.DoubleQuotedString(zetaEthAddr.String()) - r.Logger.Info("ZetaEth contract address: %s, tx hash: %s", zetaEthAddr.Hex(), zetaEthAddr.Hash().Hex()) + r.Logger.Info("ZetaEth contract address: %s, tx hash: %s", zetaEthAddr.Hex(), txZetaEth.Hash()) r.Logger.Info("Deploying ZetaConnectorEth contract") connectorEthAddr, txConnector, ConnectorEth, err := zetaconnectoreth.DeployZetaConnectorEth( diff --git a/e2e/runner/setup_solana.go b/e2e/runner/setup_solana.go index 17bf3149dd..73a571b2be 100644 --- a/e2e/runner/setup_solana.go +++ b/e2e/runner/setup_solana.go @@ -49,19 +49,19 @@ func (r *E2ERunner) SetupSolana(deployerPrivateKey string) { accountSlice = append(accountSlice, solana.Meta(privkey.PublicKey()).WRITE().SIGNER()) accountSlice = append(accountSlice, solana.Meta(pdaComputed).WRITE()) accountSlice = append(accountSlice, solana.Meta(solana.SystemProgramID)) - accountSlice = append(accountSlice, solana.Meta(r.GatewayProgram)) inst.ProgID = r.GatewayProgram inst.AccountValues = accountSlice inst.DataBytes, err = borsh.Serialize(solanacontracts.InitializeParams{ - Discriminator: solanacontracts.DiscriminatorInitialize(), + Discriminator: solanacontracts.DiscriminatorInitialize, TssAddress: r.TSSAddress, - ChainID: uint64(chains.SolanaLocalnet.ChainId), + // #nosec G115 chain id always positive + ChainID: uint64(chains.SolanaLocalnet.ChainId), }) require.NoError(r, err) // create and sign the transaction - signedTx := r.CreateSignedTransaction([]solana.Instruction{&inst}, privkey) + signedTx := r.CreateSignedTransaction([]solana.Instruction{&inst}, privkey, []solana.PrivateKey{}) // broadcast the transaction and wait for finalization _, out := r.BroadcastTxSync(signedTx) diff --git a/e2e/runner/setup_ton.go b/e2e/runner/setup_ton.go index 62d89c3329..10954b5f43 100644 --- a/e2e/runner/setup_ton.go +++ b/e2e/runner/setup_ton.go @@ -11,6 +11,7 @@ import ( "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/pkg/constant" toncontracts "github.com/zeta-chain/node/pkg/contracts/ton" + cctxtypes "github.com/zeta-chain/node/x/crosschain/types" observertypes "github.com/zeta-chain/node/x/observer/types" ) @@ -36,15 +37,13 @@ func (r *E2ERunner) SetupTON() error { depAddr := deployer.GetAddress() r.Logger.Print("💎TON Deployer %s (%s)", depAddr.ToRaw(), depAddr.ToHuman(false, true)) - gwAccount, err := ton.ConstructGatewayAccount(r.TSSAddress) + // 2. Deploy Gateway + gwAccount, err := ton.ConstructGatewayAccount(depAddr, r.TSSAddress) if err != nil { return errors.Wrap(err, "unable to initialize TON gateway") } - // 2. Deploy Gateway - initStateAmount := ton.TONCoins(10) - - if err := deployer.Deploy(ctx, gwAccount, initStateAmount); err != nil { + if err = deployer.Deploy(ctx, gwAccount, toncontracts.Coins(1)); err != nil { return errors.Wrapf(err, "unable to deploy TON gateway") } @@ -56,19 +55,35 @@ func (r *E2ERunner) SetupTON() error { ) // 3. Check that the gateway indeed was deployed and has desired TON balance. - gwBalance, err := deployer.GetBalanceOf(ctx, gwAccount.ID) - if err != nil { + gwBalance, err := deployer.GetBalanceOf(ctx, gwAccount.ID, true) + switch { + case err != nil: return errors.Wrap(err, "unable to get balance of TON gateway") + case gwBalance.IsZero(): + return fmt.Errorf("TON gateway balance is zero") } - if gwBalance.IsZero() { - return fmt.Errorf("TON gateway balance is zero") + // 4. Set chain params & chain nonce + if err := r.ensureTONChainParams(gwAccount); err != nil { + return errors.Wrap(err, "unable to ensure TON chain params") } r.TONDeployer = deployer r.TONGateway = toncontracts.NewGateway(gwAccount.ID) - return r.ensureTONChainParams(gwAccount) + // 5. Deposit 10000 TON deployer to Zevm Auth + veryFirstDeposit := toncontracts.Coins(10000) + zevmRecipient := r.ZEVMAuth.From + + gwDeposit, err := r.TONDeposit(&deployer.Wallet, veryFirstDeposit, zevmRecipient) + switch { + case err != nil: + return errors.Wrap(err, "unable to deposit TON to Zevm Auth") + case gwDeposit.CctxStatus.Status != cctxtypes.CctxStatus_OutboundMined: + return errors.New("gateway deposit CCTX is not mined") + } + + return nil } func (r *E2ERunner) ensureTONChainParams(gw *ton.AccountInit) error { @@ -103,6 +118,11 @@ func (r *E2ERunner) ensureTONChainParams(gw *ton.AccountInit) error { return errors.Wrap(err, "unable to broadcast TON chain params tx") } + resetMsg := observertypes.NewMsgResetChainNonces(creator, chainID, 0, 0) + if _, err := r.ZetaTxServer.BroadcastTx(utils.OperationalPolicyName, resetMsg); err != nil { + return errors.Wrap(err, "unable to broadcast TON chain nonce reset tx") + } + r.Logger.Print("💎Voted for adding TON chain params (localnet). Waiting for confirmation") query := &observertypes.QueryGetChainParamsForChainRequest{ChainId: chainID} diff --git a/e2e/runner/solana.go b/e2e/runner/solana.go index 4d5e6a9b9d..24ea3c3b2f 100644 --- a/e2e/runner/solana.go +++ b/e2e/runner/solana.go @@ -6,6 +6,8 @@ import ( ethcommon "github.com/ethereum/go-ethereum/common" "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/programs/system" + "github.com/gagliardetto/solana-go/programs/token" "github.com/gagliardetto/solana-go/rpc" "github.com/near/borsh-go" "github.com/stretchr/testify/require" @@ -49,7 +51,7 @@ func (r *E2ERunner) CreateDepositInstruction( var err error inst.DataBytes, err = borsh.Serialize(solanacontract.DepositInstructionParams{ - Discriminator: solanacontract.DiscriminatorDeposit(), + Discriminator: solanacontract.DiscriminatorDeposit, Amount: amount, Memo: append(receiver.Bytes(), data...), }) @@ -62,6 +64,7 @@ func (r *E2ERunner) CreateDepositInstruction( func (r *E2ERunner) CreateSignedTransaction( instructions []solana.Instruction, privateKey solana.PrivateKey, + additionalPrivateKeys []solana.PrivateKey, ) *solana.Transaction { // get a recent blockhash recent, err := r.SolanaClient.GetLatestBlockhash(r.Ctx, rpc.CommitmentFinalized) @@ -81,6 +84,11 @@ func (r *E2ERunner) CreateSignedTransaction( if privateKey.PublicKey().Equals(key) { return &privateKey } + for _, apk := range additionalPrivateKeys { + if apk.PublicKey().Equals(key) { + return &apk + } + } return nil }, ) @@ -89,6 +97,40 @@ func (r *E2ERunner) CreateSignedTransaction( return tx } +func (r *E2ERunner) DeploySPL(privateKey *solana.PrivateKey) *solana.Wallet { + lamport, err := r.SolanaClient.GetMinimumBalanceForRentExemption(r.Ctx, token.MINT_SIZE, rpc.CommitmentFinalized) + require.NoError(r, err) + + // to deploy new spl token, create account instruction and initialize mint instruction have to be in the same transaction + tokenAccount := solana.NewWallet() + createAccountInstruction := system.NewCreateAccountInstruction( + lamport, + token.MINT_SIZE, + solana.TokenProgramID, + privateKey.PublicKey(), + tokenAccount.PublicKey(), + ).Build() + + initializeMintInstruction := token.NewInitializeMint2Instruction( + 6, + privateKey.PublicKey(), + privateKey.PublicKey(), + tokenAccount.PublicKey(), + ).Build() + + signedTx := r.CreateSignedTransaction( + []solana.Instruction{createAccountInstruction, initializeMintInstruction}, + *privateKey, + []solana.PrivateKey{tokenAccount.PrivateKey}, + ) + + // broadcast the transaction and wait for finalization + _, out := r.BroadcastTxSync(signedTx) + r.Logger.Info("create spl logs: %v", out.Meta.LogMessages) + + return tokenAccount +} + // BroadcastTxSync broadcasts a transaction and waits for it to be finalized func (r *E2ERunner) BroadcastTxSync(tx *solana.Transaction) (solana.Signature, *rpc.GetTransactionResult) { // broadcast the transaction @@ -134,7 +176,7 @@ func (r *E2ERunner) SOLDepositAndCall( instruction := r.CreateDepositInstruction(signerPrivKey.PublicKey(), receiver, data, amount.Uint64()) // create and sign the transaction - signedTx := r.CreateSignedTransaction([]solana.Instruction{instruction}, *signerPrivKey) + signedTx := r.CreateSignedTransaction([]solana.Instruction{instruction}, *signerPrivKey, []solana.PrivateKey{}) // broadcast the transaction and wait for finalization sig, out := r.BroadcastTxSync(signedTx) diff --git a/e2e/runner/ton.go b/e2e/runner/ton.go index 8746e25977..369bfa0df3 100644 --- a/e2e/runner/ton.go +++ b/e2e/runner/ton.go @@ -1,12 +1,22 @@ package runner import ( + "encoding/hex" + "math/big" + "time" + "cosmossdk.io/math" eth "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/pkg/errors" "github.com/stretchr/testify/require" + "github.com/tonkeeper/tongo/ton" "github.com/tonkeeper/tongo/wallet" + "github.com/zeta-chain/node/e2e/utils" + "github.com/zeta-chain/node/pkg/chains" toncontracts "github.com/zeta-chain/node/pkg/contracts/ton" + cctypes "github.com/zeta-chain/node/x/crosschain/types" ) // we need to use this send mode due to how wallet V5 works @@ -15,8 +25,28 @@ import ( // https://docs.ton.org/develop/smart-contracts/guidelines/message-modes-cookbook const tonDepositSendCode = toncontracts.SendFlagSeparateFees + toncontracts.SendFlagIgnoreErrors +// currently implemented only for DepositAndCall, +// can be adopted for all TON ops +type tonOpts struct { + expectedStatus cctypes.CctxStatus +} + +type TONOpt func(t *tonOpts) + +func TONExpectStatus(status cctypes.CctxStatus) TONOpt { + return func(t *tonOpts) { + t.expectedStatus = status + } +} + // TONDeposit deposit TON to Gateway contract -func (r *E2ERunner) TONDeposit(sender *wallet.Wallet, amount math.Uint, zevmRecipient eth.Address) error { +func (r *E2ERunner) TONDeposit( + sender *wallet.Wallet, + amount math.Uint, + zevmRecipient eth.Address, +) (*cctypes.CrossChainTx, error) { + chain := chains.TONLocalnet + require.NotNil(r, r.TONGateway, "TON Gateway is not initialized") require.NotNil(r, sender, "Sender wallet is nil") @@ -30,7 +60,21 @@ func (r *E2ERunner) TONDeposit(sender *wallet.Wallet, amount math.Uint, zevmReci zevmRecipient.Hex(), ) - return r.TONGateway.SendDeposit(r.Ctx, sender, amount, zevmRecipient, tonDepositSendCode) + // Send TX + err := r.TONGateway.SendDeposit(r.Ctx, sender, amount, zevmRecipient, tonDepositSendCode) + if err != nil { + return nil, errors.Wrap(err, "failed to send TON deposit") + } + + filter := func(cctx *cctypes.CrossChainTx) bool { + return cctx.InboundParams.SenderChainId == chain.ChainId && + cctx.InboundParams.Sender == sender.GetAddress().ToRaw() + } + + // Wait for cctx + cctx := r.WaitForSpecificCCTX(filter, cctypes.CctxStatus_OutboundMined, time.Minute) + + return cctx, nil } // TONDepositAndCall deposit TON to Gateway contract with call data. @@ -39,7 +83,15 @@ func (r *E2ERunner) TONDepositAndCall( amount math.Uint, zevmRecipient eth.Address, callData []byte, -) error { + opts ...TONOpt, +) (*cctypes.CrossChainTx, error) { + cfg := &tonOpts{expectedStatus: cctypes.CctxStatus_OutboundMined} + for _, opt := range opts { + opt(cfg) + } + + chain := chains.TONLocalnet + require.NotNil(r, r.TONGateway, "TON Gateway is not initialized") require.NotNil(r, sender, "Sender wallet is nil") @@ -55,5 +107,58 @@ func (r *E2ERunner) TONDepositAndCall( string(callData), ) - return r.TONGateway.SendDepositAndCall(r.Ctx, sender, amount, zevmRecipient, callData, tonDepositSendCode) + err := r.TONGateway.SendDepositAndCall(r.Ctx, sender, amount, zevmRecipient, callData, tonDepositSendCode) + if err != nil { + return nil, errors.Wrap(err, "failed to send TON deposit and call") + } + + filter := func(cctx *cctypes.CrossChainTx) bool { + memo := zevmRecipient.Bytes() + memo = append(memo, callData...) + + return cctx.InboundParams.SenderChainId == chain.ChainId && + cctx.InboundParams.Sender == sender.GetAddress().ToRaw() && + cctx.RelayedMessage == hex.EncodeToString(memo) + } + + // Wait for cctx + cctx := r.WaitForSpecificCCTX(filter, cfg.expectedStatus, time.Minute) + + return cctx, nil +} + +// SendWithdrawTONZRC20 sends withdraw tx of TON ZRC20 tokens +func (r *E2ERunner) SendWithdrawTONZRC20( + to ton.AccountID, + amount *big.Int, + approveAmount *big.Int, +) *ethtypes.Transaction { + // approve + tx, err := r.TONZRC20.Approve(r.ZEVMAuth, r.TONZRC20Addr, approveAmount) + require.NoError(r, err) + receipt := utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequireTxSuccessful(r, receipt, "approve") + + // withdraw + tx, err = r.TONZRC20.Withdraw(r.ZEVMAuth, []byte(to.ToRaw()), amount) + require.NoError(r, err) + r.Logger.EVMTransaction(*tx, "withdraw") + + // wait for tx receipt + receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequireTxSuccessful(r, receipt, "withdraw") + r.Logger.Info("Receipt txhash %s status %d", receipt.TxHash, receipt.Status) + + return tx +} + +// WithdrawTONZRC20 withdraws an amount of ZRC20 TON tokens and waits for the cctx to be mined +func (r *E2ERunner) WithdrawTONZRC20(to ton.AccountID, amount *big.Int, approveAmount *big.Int) *cctypes.CrossChainTx { + tx := r.SendWithdrawTONZRC20(to, amount, approveAmount) + + // wait for the cctx to be mined + cctx := utils.WaitCctxMinedByInboundHash(r.Ctx, tx.Hash().Hex(), r.CctxClient, r.Logger, r.CctxTimeout) + utils.RequireCCTXStatus(r, cctx, cctypes.CctxStatus_OutboundMined) + + return cctx } diff --git a/e2e/runner/ton/accounts.go b/e2e/runner/ton/accounts.go index 3ffd8c0907..bed0ba7c21 100644 --- a/e2e/runner/ton/accounts.go +++ b/e2e/runner/ton/accounts.go @@ -1,8 +1,6 @@ package ton import ( - _ "embed" - "encoding/json" "fmt" eth "github.com/ethereum/go-ethereum/common" @@ -18,12 +16,6 @@ import ( const workchainID = 0 -// https://github.com/zeta-chain/protocol-contracts-ton -// `make compile` -// -//go:embed gateway.compiled.json -var tonGatewayCodeJSON []byte - type AccountInit struct { Code *boc.Cell Data *boc.Cell @@ -90,69 +82,15 @@ func ConstructAccount(code, data *boc.Cell) (*AccountInit, error) { }, nil } -func ConstructGatewayAccount(tss eth.Address) (*AccountInit, error) { - code, err := getGatewayCode() - if err != nil { - return nil, errors.Wrap(err, "unable to get TON Gateway code") - } - - data, err := buildGatewayData(tss) - if err != nil { - return nil, errors.Wrap(err, "unable to build TON Gateway data") - } - - return ConstructAccount(code, data) -} - -func getGatewayCode() (*boc.Cell, error) { - var code struct { - Hex string `json:"hex"` - } - - if err := json.Unmarshal(tonGatewayCodeJSON, &code); err != nil { - return nil, errors.Wrap(err, "unable to unmarshal TON Gateway code") - } - - cells, err := boc.DeserializeBocHex(code.Hex) - if err != nil { - return nil, errors.Wrap(err, "unable to deserialize TON Gateway code") - } - - if len(cells) != 1 { - return nil, errors.New("invalid cells count") - } - - return cells[0], nil -} - -// buildGatewayState returns TON Gateway initial state data cell -func buildGatewayData(tss eth.Address) (*boc.Cell, error) { - const evmAddressBits = 20 * 8 - - tssSlice := boc.NewBitString(evmAddressBits) - if err := tssSlice.WriteBytes(tss.Bytes()); err != nil { - return nil, errors.Wrap(err, "unable to convert TSS address to ton slice") - } - - var ( - zeroCoins = tlb.Coins(0) - enc = &tlb.Encoder{} - cell = boc.NewCell() - ) - - err := toncontracts.ErrCollect( - cell.WriteBit(true), // deposits_enabled - zeroCoins.MarshalTLB(cell, enc), // total_locked - zeroCoins.MarshalTLB(cell, enc), // fees - cell.WriteUint(0, 32), // seqno - cell.WriteBitString(tssSlice), // tss_address +// ConstructGatewayAccount constructs gateway AccountInit. +// - Authority is the address of the gateway "admin". +// - TSS is the EVM address of TSS. +// - Deposits are enabled by default. +func ConstructGatewayAccount(authority ton.AccountID, tss eth.Address) (*AccountInit, error) { + return ConstructAccount( + toncontracts.GatewayCode(), + toncontracts.GatewayStateInit(authority, tss, true), ) - - if err != nil { - return nil, errors.Wrap(err, "unable to write TON Gateway state cell") - } - - return cell, nil } // copied from tongo wallets_common.go diff --git a/e2e/runner/ton/accounts_test.go b/e2e/runner/ton/accounts_test.go index 50ec07ee79..df1668c3af 100644 --- a/e2e/runner/ton/accounts_test.go +++ b/e2e/runner/ton/accounts_test.go @@ -1,57 +1,12 @@ package ton import ( - "crypto/ecdsa" - "encoding/hex" "testing" - "github.com/ethereum/go-ethereum/crypto" "github.com/stretchr/testify/require" "github.com/tonkeeper/tongo/wallet" ) -func TestGateway(t *testing.T) { - // ARRANGE - // Given TSS address - const sampleTSSPrivateKey = "0xb984cd65727cfd03081fc7bf33bf5c208bca697ce16139b5ded275887e81395a" - - pkBytes, err := hex.DecodeString(sampleTSSPrivateKey[2:]) - require.NoError(t, err) - - privateKey, err := crypto.ToECDSA(pkBytes) - require.NoError(t, err) - - publicKey := privateKey.Public() - publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) - require.True(t, ok) - - tss := crypto.PubkeyToAddress(*publicKeyECDSA) - - t.Run("Build gateway deployment payload", func(t *testing.T) { - // ACT - codeCell, errCode := getGatewayCode() - stateCell, errState := buildGatewayData(tss) - - // ASSERT - require.NoError(t, errCode) - require.NoError(t, errState) - - codeString, err := codeCell.ToBocStringCustom(false, true, false, 0) - require.NoError(t, err) - - stateString, err := stateCell.ToBocStringCustom(false, true, false, 0) - require.NoError(t, err) - - t.Logf("Gateway code: %s", codeString) - t.Logf("Gateway state: %s", stateString) - - // Taken from jest tests in protocol-contracts-ton (using the same TSS address private key) - const expectedState = "b5ee9c7241010101001c0000338000000000124d38a790fdf1d9311fae87d4b21aeffd77bc26c0776433f3" - - require.Equal(t, expectedState, stateString) - }) -} - func TestWalletConstruction(t *testing.T) { // ARRANGE seed := wallet.RandomSeed() diff --git a/e2e/runner/ton/deployer.go b/e2e/runner/ton/deployer.go index 858cc8af79..3f12f784da 100644 --- a/e2e/runner/ton/deployer.go +++ b/e2e/runner/ton/deployer.go @@ -10,6 +10,8 @@ import ( "github.com/tonkeeper/tongo/tlb" "github.com/tonkeeper/tongo/ton" "github.com/tonkeeper/tongo/wallet" + + toncontracts "github.com/zeta-chain/node/pkg/contracts/ton" ) // Deployer represents a wrapper around ton Wallet with some helpful methods. @@ -55,10 +57,13 @@ func (d *Deployer) Seqno(ctx context.Context) (uint32, error) { return d.blockchain.GetSeqno(ctx, d.GetAddress()) } -// GetBalanceOf returns the balance of the given account. -func (d *Deployer) GetBalanceOf(ctx context.Context, id ton.AccountID) (math.Uint, error) { - if err := d.waitForAccountActivation(ctx, id); err != nil { - return math.Uint{}, errors.Wrap(err, "failed to wait for account activation") +// GetBalanceOf returns the balance of a given account. +// wait=true waits for account activation. +func (d *Deployer) GetBalanceOf(ctx context.Context, id ton.AccountID, wait bool) (math.Uint, error) { + if wait { + if err := d.waitForAccountActivation(ctx, id); err != nil { + return math.Uint{}, errors.Wrap(err, "failed to wait for account activation") + } } state, err := d.blockchain.GetAccountState(ctx, id) @@ -74,7 +79,7 @@ func (d *Deployer) GetBalanceOf(ctx context.Context, id ton.AccountID) (math.Uin // Fund sends the given amount of coins to the recipient. Returns tx hash and error. func (d *Deployer) Fund(ctx context.Context, recipient ton.AccountID, amount math.Uint) (ton.Bits256, error) { msg := wallet.SimpleTransfer{ - Amount: UintToCoins(amount), + Amount: toncontracts.UintToCoins(amount), Address: recipient, } @@ -84,7 +89,7 @@ func (d *Deployer) Fund(ctx context.Context, recipient ton.AccountID, amount mat // Deploy deploys AccountInit with the given amount of coins. Returns tx hash and error. func (d *Deployer) Deploy(ctx context.Context, account *AccountInit, amount math.Uint) error { msg := wallet.Message{ - Amount: UintToCoins(amount), + Amount: toncontracts.UintToCoins(amount), Address: account.ID, Code: account.Code, Data: account.Data, diff --git a/e2e/runner/ton/gateway.compiled.json b/e2e/runner/ton/gateway.compiled.json deleted file mode 100644 index 5177dd0d6c..0000000000 --- a/e2e/runner/ton/gateway.compiled.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "hash": "a675833643f352d5d40e08efc5f093b6a0e6c246531ff1ba44374a30d8366d18", - "hashBase64": "pnWDNkPzUtXUDgjvxfCTtqDmwkZTH/G6RDdKMNg2bRg=", - "hex": "b5ee9c724102130100039b000114ff00f4a413f4bcf2c80b01020120020b020148030802b8d033d0d303fa40300171b0f2d06420fa4430c000f2e06922d749c160f2d06502d31fd33f593020c064925f04e020c065e302c0668ea0218208989680b9f2d06a20d7498100a0b9f2d067d39f20c702f2d068d430db3ce05f03f2c066040503ec30218208989680b9f2d06a20d7498100a0b9f2d067d39f30db3cdb3c018208989680a1f84221a0f862f8438208989680a0f863db3c708065c8cb1fcb3f5003cf1658fa02cb9fc98d041cd95b9917db1bd9d7db595cdcd859d960fe14307170530073c8cb01f828cf16cb01cb5fcb00cb00ccc970fb000f061204f4db3cdb3c20830d8068218409a904a413f94130315301bc8e2f8d0858d95b1b081cda5e99481a5cc81d1bdbc8189a59ce8816d9dbdd0b081dd85b9d1760fe1430fe2030fe2030f2f0925f03e2028208989680a1f84221a0f862f8438208989680a0f863db3c708066c8cb1fcb3f5004cf165003fa0212cb9fccc90f061207000ef841c000f2d06e005e8d041cd95b9917db1bd9d7db595cdcd859d960fe14307170530073c8cb01f828cf16cb01cb5fcb00cb00ccc970fb00020120090a010dbe64bed9e7c2240f0115bfda36d9e7c20fc217c22c0f04f8f2d31f218100c8ba8ff031db3cdb3cd0fa40fa00d31f3002fa4401c000f2e06921c000f2d06af844a413bdf2d06df80071c8c922103402318d04dcd95b9917dcda5b5c1b1957db595cdcd859d960fe1430708018c8cb05542005745003cb02cb07cbff58fa0212cb6ac901fb00f84201a1f862f844a4f864db3ce0210f10120c04528100c9ba8f9d31db3cdb3cd0d300d31f30f844a4bdf2d06df800f861f844a4f864db3ce0218100caba0f10120d047a8fb531db3cdb3cd0d69fd31f30f844a4bdf2d06df80020f865f844a4f8648bf6e65775f7473735f616464726573738fe1430fe2030db3ce0018100cbba0f10120e033a8f96db3cdb3cd0d4d31f30f844a4bdf2d06df800fb04db3ce030f2c0660f10120038ed44d0d30001f861fa0001f862fa0001f863d31f01f864d69f30f8650170810208d718d3ffd43020f90022bdf2d06bf84513db3c8d0558da1958dad7d958d91cd857dcda59db985d1d5c9960fe1430fe20c3fff2d06c11008e01d3070120c21e92a6e19720c21a92a6e5e0e201d3ffd3ff301034f912c3ff935f0471e002c304935f0372e0c8cbffcbff71f90403c8cbffc9d08100a0d722c705c0009173e07f0030f844f841c8cb00f842fa02f843fa02cb1ff845cf16c9ed54375cf328" -} \ No newline at end of file diff --git a/e2e/runner/v2_migration.go b/e2e/runner/v2_migration.go index 1419701887..b150263c6a 100644 --- a/e2e/runner/v2_migration.go +++ b/e2e/runner/v2_migration.go @@ -149,9 +149,9 @@ func (r *E2ERunner) migrateERC20CustodyFunds() { res, err := r.ZetaTxServer.BroadcastTx(utils.AdminPolicyName, msgPausing) require.NoError(r, err) - // fetch cctx index from tx response - cctxIndex, err := txserver.FetchAttributeFromTxResponse(res, "cctx_index") - require.NoError(r, err) + migrationEvent, ok := txserver.EventOfType[*crosschaintypes.EventERC20CustodyFundsMigration](res.Events) + require.True(r, ok, "no EventERC20CustodyFundsMigration in %s", res.TxHash) + cctxIndex := migrationEvent.CctxIndex cctxRes, err := r.CctxClient.Cctx(r.Ctx, &crosschaintypes.QueryGetCctxRequest{Index: cctxIndex}) require.NoError(r, err) @@ -188,9 +188,9 @@ func (r *E2ERunner) migrateERC20CustodyFunds() { res, err = r.ZetaTxServer.BroadcastTx(utils.AdminPolicyName, msgMigration) require.NoError(r, err) - // fetch cctx index from tx response - cctxIndex, err = txserver.FetchAttributeFromTxResponse(res, "cctx_index") - require.NoError(r, err) + migrationEvent, ok = txserver.EventOfType[*crosschaintypes.EventERC20CustodyFundsMigration](res.Events) + require.True(r, ok, "no EventERC20CustodyFundsMigration in %s", res.TxHash) + cctxIndex = migrationEvent.CctxIndex cctxRes, err = r.CctxClient.Cctx(r.Ctx, &crosschaintypes.QueryGetCctxRequest{Index: cctxIndex}) require.NoError(r, err) diff --git a/e2e/runner/v2_zevm.go b/e2e/runner/v2_zevm.go index 45dd8c6761..4f6f299db2 100644 --- a/e2e/runner/v2_zevm.go +++ b/e2e/runner/v2_zevm.go @@ -11,7 +11,7 @@ import ( gatewayzevmcaller "github.com/zeta-chain/node/pkg/contracts/gatewayzevmcaller" ) -var gasLimit = big.NewInt(1000000) +var gasLimit = big.NewInt(250000) // V2ETHWithdraw calls Withdraw of Gateway with gas token on ZEVM func (r *E2ERunner) V2ETHWithdraw( @@ -31,7 +31,7 @@ func (r *E2ERunner) V2ETHWithdraw( return tx } -// V2ETHWithdrawAndCall calls WithdrawAndCall of Gateway with gas token on ZEVM using arbitrary call +// V2ETHWithdrawAndArbitraryCall calls WithdrawAndCall of Gateway with gas token on ZEVM using arbitrary call func (r *E2ERunner) V2ETHWithdrawAndArbitraryCall( receiver ethcommon.Address, amount *big.Int, diff --git a/e2e/runner/zeta.go b/e2e/runner/zeta.go index 1df7e676e3..d59ef85645 100644 --- a/e2e/runner/zeta.go +++ b/e2e/runner/zeta.go @@ -99,11 +99,15 @@ func (r *E2ERunner) WaitForMinedCCTX(txHash ethcommon.Hash) { // WaitForMinedCCTXFromIndex waits for a cctx to be mined from its index func (r *E2ERunner) WaitForMinedCCTXFromIndex(index string) *types.CrossChainTx { + return r.waitForMinedCCTXFromIndex(index, types.CctxStatus_OutboundMined) +} + +func (r *E2ERunner) waitForMinedCCTXFromIndex(index string, status types.CctxStatus) *types.CrossChainTx { r.Lock() defer r.Unlock() cctx := utils.WaitCCTXMinedByIndex(r.Ctx, index, r.CctxClient, r.Logger, r.CctxTimeout) - utils.RequireCCTXStatus(r, cctx, types.CctxStatus_OutboundMined) + utils.RequireCCTXStatus(r, cctx, status) return cctx } @@ -111,6 +115,7 @@ func (r *E2ERunner) WaitForMinedCCTXFromIndex(index string) *types.CrossChainTx // WaitForSpecificCCTX scans for cctx by filters and ensures it's mined func (r *E2ERunner) WaitForSpecificCCTX( filter func(*types.CrossChainTx) bool, + status types.CctxStatus, timeout time.Duration, ) *types.CrossChainTx { var ( @@ -128,7 +133,7 @@ func (r *E2ERunner) WaitForSpecificCCTX( for i := range res.CrossChainTx { tx := res.CrossChainTx[i] if filter(tx) { - return r.WaitForMinedCCTXFromIndex(tx.Index) + return r.waitForMinedCCTXFromIndex(tx.Index, status) } } diff --git a/e2e/txserver/zeta_tx_server.go b/e2e/txserver/zeta_tx_server.go index a79c5af098..9b6e2d0b65 100644 --- a/e2e/txserver/zeta_tx_server.go +++ b/e2e/txserver/zeta_tx_server.go @@ -3,7 +3,6 @@ package txserver import ( "context" "encoding/hex" - "encoding/json" "errors" "fmt" "math/big" @@ -11,6 +10,7 @@ import ( "strings" "time" + abci "github.com/cometbft/cometbft/abci/types" rpchttp "github.com/cometbft/cometbft/rpc/client/http" coretypes "github.com/cometbft/cometbft/rpc/core/types" "github.com/cosmos/cosmos-sdk/client" @@ -32,7 +32,8 @@ import ( slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" - ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/cosmos/gogoproto/proto" + "github.com/samber/lo" "github.com/zeta-chain/ethermint/crypto/hd" etherminttypes "github.com/zeta-chain/ethermint/types" evmtypes "github.com/zeta-chain/ethermint/x/evm/types" @@ -192,7 +193,7 @@ func (zts ZetaTxServer) GetAccountMnemonic(index int) string { // BroadcastTx broadcasts a tx to ZetaChain with the provided msg from the account // and waiting for blockTime for tx to be included in the block -func (zts ZetaTxServer) BroadcastTx(account string, msg sdktypes.Msg) (*sdktypes.TxResponse, error) { +func (zts ZetaTxServer) BroadcastTx(account string, msgs ...sdktypes.Msg) (*sdktypes.TxResponse, error) { // Find number and sequence and set it acc, err := zts.clientCtx.Keyring.Key(account) if err != nil { @@ -208,10 +209,13 @@ func (zts ZetaTxServer) BroadcastTx(account string, msg sdktypes.Msg) (*sdktypes } zts.txFactory = zts.txFactory.WithAccountNumber(accountNumber).WithSequence(accountSeq) - txBuilder, err := zts.txFactory.BuildUnsignedTx(msg) + txBuilder, err := zts.txFactory.BuildUnsignedTx(msgs...) if err != nil { return nil, err } + // increase gas and fees if multiple messages are provided + txBuilder.SetGasLimit(zts.txFactory.Gas() * uint64(len(msgs))) + txBuilder.SetFeeAmount(zts.txFactory.Fees().MulInt(sdktypes.NewInt(int64(len(msgs))))) // Sign tx err = tx.Sign(zts.txFactory, account, txBuilder, true) @@ -237,6 +241,9 @@ func broadcastWithBlockTimeout(zts ZetaTxServer, txBytes []byte) (*sdktypes.TxRe TxHash: res.TxHash, }, err } + if res.Code != 0 { + return res, fmt.Errorf("broadcast failed: %s", res.RawLog) + } exitAfter := time.After(zts.blockTimeout) hash, err := hex.DecodeString(res.TxHash) @@ -261,6 +268,9 @@ func mkTxResult( clientCtx client.Context, resTx *coretypes.ResultTx, ) (*sdktypes.TxResponse, error) { + if resTx.TxResult.Code != 0 { + return nil, fmt.Errorf("tx failed: %s", resTx.TxResult.Log) + } txb, err := clientCtx.TxConfig.TxDecoder()(resTx.Tx) if err != nil { return nil, err @@ -348,59 +358,25 @@ func (zts ZetaTxServer) DeploySystemContracts( return SystemContractAddresses{}, fmt.Errorf("failed to deploy system contracts: %s", err.Error()) } - systemContractAddress, err := FetchAttributeFromTxResponse(res, "system_contract") - if err != nil { - return SystemContractAddresses{}, fmt.Errorf( - "failed to fetch system contract address: %s; rawlog %s", - err.Error(), - res.RawLog, - ) + deployedEvent, ok := EventOfType[*fungibletypes.EventSystemContractsDeployed](res.Events) + if !ok { + return SystemContractAddresses{}, fmt.Errorf("no EventSystemContractsDeployed in %s", res.TxHash) } // get system contract _, err = zts.BroadcastTx( accountAdmin, - fungibletypes.NewMsgUpdateSystemContract(addrAdmin.String(), systemContractAddress), + fungibletypes.NewMsgUpdateSystemContract(addrAdmin.String(), deployedEvent.SystemContract), ) if err != nil { return SystemContractAddresses{}, fmt.Errorf("failed to set system contract: %s", err.Error()) } - // get uniswap contract addresses - uniswapV2FactoryAddr, err := FetchAttributeFromTxResponse(res, "uniswap_v2_factory") - if err != nil { - return SystemContractAddresses{}, fmt.Errorf("failed to fetch uniswap v2 factory address: %s", err.Error()) - } - uniswapV2RouterAddr, err := FetchAttributeFromTxResponse(res, "uniswap_v2_router") - if err != nil { - return SystemContractAddresses{}, fmt.Errorf("failed to fetch uniswap v2 router address: %s", err.Error()) - } - - // get zevm connector address - zevmConnectorAddr, err := FetchAttributeFromTxResponse(res, "connector_zevm") - if err != nil { - return SystemContractAddresses{}, fmt.Errorf( - "failed to fetch zevm connector address: %s, txResponse: %s", - err.Error(), - res.String(), - ) - } - - // get wzeta address - wzetaAddr, err := FetchAttributeFromTxResponse(res, "wzeta") - if err != nil { - return SystemContractAddresses{}, fmt.Errorf( - "failed to fetch wzeta address: %s, txResponse: %s", - err.Error(), - res.String(), - ) - } - return SystemContractAddresses{ - UniswapV2FactoryAddr: uniswapV2FactoryAddr, - UniswapV2RouterAddr: uniswapV2RouterAddr, - ZEVMConnectorAddr: zevmConnectorAddr, - WZETAAddr: wzetaAddr, + UniswapV2FactoryAddr: deployedEvent.UniswapV2Factory, + UniswapV2RouterAddr: deployedEvent.UniswapV2Router, + ZEVMConnectorAddr: deployedEvent.ConnectorZevm, + WZETAAddr: deployedEvent.Wzeta, }, nil } @@ -442,105 +418,98 @@ func (zts ZetaTxServer) DeployZRC20s( deployerAddr = addrOperational.String() } - deploy := func(msg *fungibletypes.MsgDeployFungibleCoinZRC20) (string, error) { - // noop - if skipChain(msg.ForeignChainId) { - return "", nil - } - - res, err := zts.BroadcastTx(deployerAccount, msg) - if err != nil { - return "", fmt.Errorf("failed to deploy eth zrc20: %w", err) - } - - addr, err := fetchZRC20FromDeployResponse(res) - if err != nil { - return "", fmt.Errorf("unable to fetch zrc20 from deploy response: %w", err) - } - - if err := zts.InitializeLiquidityCap(addr); err != nil { - return "", fmt.Errorf("unable to initialize liquidity cap: %w", err) - } - - return addr, nil - } + deployMsgs := []*fungibletypes.MsgDeployFungibleCoinZRC20{ + fungibletypes.NewMsgDeployFungibleCoinZRC20( + deployerAddr, + "", + chains.GoerliLocalnet.ChainId, + 18, + "ETH", + "gETH", + coin.CoinType_Gas, + 100000, + ), + fungibletypes.NewMsgDeployFungibleCoinZRC20( + deployerAddr, + "", + chains.BitcoinRegtest.ChainId, + 8, + "BTC", + "tBTC", + coin.CoinType_Gas, + 100000, + ), + fungibletypes.NewMsgDeployFungibleCoinZRC20( + deployerAddr, + "", + chains.SolanaLocalnet.ChainId, + 9, + "Solana", + "SOL", + coin.CoinType_Gas, + 100000, + ), + fungibletypes.NewMsgDeployFungibleCoinZRC20( + deployerAddr, + "", + chains.TONLocalnet.ChainId, + 9, + "TON", + "TON", + coin.CoinType_Gas, + 100_000, + ), + fungibletypes.NewMsgDeployFungibleCoinZRC20( + deployerAddr, + erc20Addr, + chains.GoerliLocalnet.ChainId, + 6, + "USDT", + "USDT", + coin.CoinType_ERC20, + 100000, + ), + } + + // apply skipChain filter and convert to sdk.Msg + deployMsgsIface := lo.FilterMap( + deployMsgs, + func(msg *fungibletypes.MsgDeployFungibleCoinZRC20, _ int) (sdktypes.Msg, bool) { + if skipChain(msg.ForeignChainId) { + return nil, false + } + return msg, true + }, + ) - // deploy eth zrc20 - _, err = deploy(fungibletypes.NewMsgDeployFungibleCoinZRC20( - deployerAddr, - "", - chains.GoerliLocalnet.ChainId, - 18, - "ETH", - "gETH", - coin.CoinType_Gas, - 100000, - )) + res, err := zts.BroadcastTx(deployerAccount, deployMsgsIface...) if err != nil { - return "", fmt.Errorf("failed to deploy eth zrc20: %s", err.Error()) + return "", fmt.Errorf("deploy zrc20s: %w", err) } - // deploy btc zrc20 - _, err = deploy(fungibletypes.NewMsgDeployFungibleCoinZRC20( - deployerAddr, - "", - chains.BitcoinRegtest.ChainId, - 8, - "BTC", - "tBTC", - coin.CoinType_Gas, - 100000, - )) - if err != nil { - return "", fmt.Errorf("failed to deploy btc zrc20: %s", err.Error()) + deployedEvents, ok := EventsOfType[*fungibletypes.EventZRC20Deployed](res.Events) + if !ok { + return "", fmt.Errorf("no EventZRC20Deployed in %s", res.TxHash) } - // deploy sol zrc20 - _, err = deploy(fungibletypes.NewMsgDeployFungibleCoinZRC20( - deployerAddr, - "", - chains.SolanaLocalnet.ChainId, - 9, - "Solana", - "SOL", - coin.CoinType_Gas, - 100000, - )) - if err != nil { - return "", fmt.Errorf("failed to deploy sol zrc20: %s", err.Error()) - } + zrc20Addrs := lo.Map(deployedEvents, func(ev *fungibletypes.EventZRC20Deployed, _ int) string { + return ev.Contract + }) - // deploy ton zrc20 - _, err = deploy(fungibletypes.NewMsgDeployFungibleCoinZRC20( - deployerAddr, - "", - chains.TONLocalnet.ChainId, - 9, - "TON", - "TON", - coin.CoinType_Gas, - 100_000, - )) + err = zts.InitializeLiquidityCaps(zrc20Addrs...) if err != nil { - return "", fmt.Errorf("failed to deploy ton zrc20: %w", err) + return "", fmt.Errorf("initialize liquidity cap: %w", err) } - // deploy erc20 zrc20 - erc20zrc20Addr, err := deploy(fungibletypes.NewMsgDeployFungibleCoinZRC20( - deployerAddr, - erc20Addr, - chains.GoerliLocalnet.ChainId, - 6, - "USDT", - "USDT", - coin.CoinType_ERC20, - 100000, - )) - if err != nil { - return "", fmt.Errorf("failed to deploy erc20 zrc20: %s", err.Error()) + // find erc20 zrc20 + erc20zrc20, ok := lo.Find(deployedEvents, func(ev *fungibletypes.EventZRC20Deployed) bool { + return ev.ChainId == chains.GoerliLocalnet.ChainId && ev.CoinType == coin.CoinType_ERC20 + }) + if !ok { + return "", fmt.Errorf("unable to find erc20 zrc20") } - return erc20zrc20Addr, nil + return erc20zrc20.Contract, nil } // FundEmissionsPool funds the emissions pool with the given amount @@ -588,31 +557,20 @@ func (zts *ZetaTxServer) SetAuthorityClient(authorityClient authoritytypes.Query zts.authorityClient = authorityClient } -// InitializeLiquidityCap initializes the liquidity cap for the given coin with a large value -func (zts ZetaTxServer) InitializeLiquidityCap(zrc20 string) error { +// InitializeLiquidityCaps initializes the liquidity cap for the given coin with a large value +func (zts ZetaTxServer) InitializeLiquidityCaps(zrc20s ...string) error { liquidityCap := sdktypes.NewUint(1e18).MulUint64(1e12) - msg := fungibletypes.NewMsgUpdateZRC20LiquidityCap( - zts.MustGetAccountAddressFromName(utils.OperationalPolicyName), - zrc20, - liquidityCap, - ) - _, err := zts.BroadcastTx(utils.OperationalPolicyName, msg) - return err -} - -// fetchZRC20FromDeployResponse fetches the zrc20 address from the response -func fetchZRC20FromDeployResponse(res *sdktypes.TxResponse) (string, error) { - // fetch the erc20 zrc20 contract address and remove the quotes - zrc20Addr, err := FetchAttributeFromTxResponse(res, "Contract") - if err != nil { - return "", fmt.Errorf("failed to fetch zrc20 contract address: %s, %s", err.Error(), res.String()) - } - if !ethcommon.IsHexAddress(zrc20Addr) { - return "", fmt.Errorf("invalid address in event: %s", zrc20Addr) + msgs := make([]sdktypes.Msg, len(zrc20s)) + for i, zrc20 := range zrc20s { + msgs[i] = fungibletypes.NewMsgUpdateZRC20LiquidityCap( + zts.MustGetAccountAddressFromName(utils.OperationalPolicyName), + zrc20, + liquidityCap, + ) } - - return zrc20Addr, nil + _, err := zts.BroadcastTx(utils.OperationalPolicyName, msgs...) + return err } // fetchMessagePermissions fetches the message permissions for a given message @@ -703,48 +661,32 @@ func newFactory(clientCtx client.Context) tx.Factory { WithFees("100000000000000000azeta") } -type messageLog struct { - Events []event `json:"events"` -} - -type event struct { - Type string `json:"type"` - Attributes []attribute `json:"attributes"` -} - -type attribute struct { - Key string `json:"key"` - Value string `json:"value"` -} - -// FetchAttributeFromTxResponse fetches the attribute from the tx response -func FetchAttributeFromTxResponse(res *sdktypes.TxResponse, key string) (string, error) { - var logs []messageLog - err := json.Unmarshal([]byte(res.RawLog), &logs) - if err != nil { - return "", fmt.Errorf("failed to unmarshal logs: %s, logs content: %s", err.Error(), res.RawLog) +// EventsOfType gets events of a specified type +func EventsOfType[T proto.Message](events []abci.Event) ([]T, bool) { + var filteredEvents []T + for _, ev := range events { + pEvent, err := sdktypes.ParseTypedEvent(ev) + if err != nil { + continue + } + if typedEvent, ok := pEvent.(T); ok { + filteredEvents = append(filteredEvents, typedEvent) + } } + return filteredEvents, len(filteredEvents) > 0 +} - var attributes []string - for _, log := range logs { - for _, event := range log.Events { - for _, attr := range event.Attributes { - attributes = append(attributes, attr.Key) - if strings.EqualFold(attr.Key, key) { - address := attr.Value - - if len(address) < 2 { - return "", fmt.Errorf("invalid address: %s", address) - } - - // trim the quotes - address = address[1 : len(address)-1] - - return address, nil - } - } +// EventOfType gets one event of a specific type +func EventOfType[T proto.Message](events []abci.Event) (T, bool) { + var event T + for _, ev := range events { + pEvent, err := sdktypes.ParseTypedEvent(ev) + if err != nil { + continue + } + if typedEvent, ok := pEvent.(T); ok { + return typedEvent, true } } - - return "", fmt.Errorf("attribute %s not found, attributes: %+v", key, attributes) + return event, false } diff --git a/e2e/utils/require.go b/e2e/utils/require.go index 3dedb7dd26..8bf9c5f5d0 100644 --- a/e2e/utils/require.go +++ b/e2e/utils/require.go @@ -38,7 +38,7 @@ func RequireTxSuccessful(t require.TestingT, receipt *ethtypes.Receipt, msgAndAr // RequiredTxFailed checks if the receipt status is failed. // Currently, it accepts eth receipt, but we can make this more generic by using type assertion. func RequiredTxFailed(t require.TestingT, receipt *ethtypes.Receipt, msgAndArgs ...any) { - msg := "receipt status is not successful: %s" + msg := "receipt status is not failed: %s" require.Equal( t, ethtypes.ReceiptStatusFailed, diff --git a/e2e/utils/zetacore.go b/e2e/utils/zetacore.go index 6d50be10da..0b4d5217ed 100644 --- a/e2e/utils/zetacore.go +++ b/e2e/utils/zetacore.go @@ -81,7 +81,7 @@ func WaitCctxsMinedByInboundHash( timedOut := time.Since(startTime) > timeout require.False(t, timedOut, "waiting cctx timeout, cctx not mined, inbound hash: %s", inboundHash) - time.Sleep(1 * time.Second) + time.Sleep(500 * time.Millisecond) // We use InTxHashToCctxData instead of InboundTrackerAllByChain to able to run these tests with the previous version // for the update tests @@ -90,7 +90,7 @@ func WaitCctxsMinedByInboundHash( res, err := client.InTxHashToCctxData(ctx, in) if err != nil { // prevent spamming logs - if i%10 == 0 { + if i%20 == 0 { logger.Info("Error getting cctx by inboundHash: %s", err.Error()) } continue @@ -113,7 +113,7 @@ func WaitCctxsMinedByInboundHash( cctx := cctx if !IsTerminalStatus(cctx.CctxStatus.Status) { // prevent spamming logs - if i%10 == 0 { + if i%20 == 0 { logger.Info( "waiting for cctx index %d to be mined by inboundHash: %s, cctx status: %s, message: %s", j, @@ -154,7 +154,7 @@ func WaitCCTXMinedByIndex( require.False(t, time.Since(startTime) > timeout, "waiting cctx timeout, cctx not mined, cctx: %s", cctxIndex) if i > 0 { - time.Sleep(1 * time.Second) + time.Sleep(500 * time.Millisecond) } // fetch cctx by index @@ -170,7 +170,7 @@ func WaitCCTXMinedByIndex( cctx := res.CrossChainTx if !IsTerminalStatus(cctx.CctxStatus.Status) { // prevent spamming logs - if i%10 == 0 { + if i%20 == 0 { logger.Info( "waiting for cctx to be mined from index: %s, cctx status: %s, message: %s", cctxIndex, @@ -187,13 +187,22 @@ func WaitCCTXMinedByIndex( type WaitOpts func(c *waitConfig) -// MatchStatus waits for a specific CCTX status. +// MatchStatus is the WaitOpts that matches CCTX with the given status. func MatchStatus(s crosschaintypes.CctxStatus) WaitOpts { return Matches(func(tx crosschaintypes.CrossChainTx) bool { return tx.CctxStatus != nil && tx.CctxStatus.Status == s }) } +// MatchReverted is the WaitOpts that matches reverted CCTX. +func MatchReverted() WaitOpts { + return Matches(func(tx crosschaintypes.CrossChainTx) bool { + return tx.GetCctxStatus().Status == crosschaintypes.CctxStatus_Reverted && + len(tx.OutboundParams) == 2 && + tx.OutboundParams[1].Hash != "" + }) +} + // Matches adds a filter to WaitCctxByInboundHash that checks cctxs match provided callback. // ALL cctxs should match this filter. func Matches(fn func(tx crosschaintypes.CrossChainTx) bool) WaitOpts { @@ -204,6 +213,20 @@ type waitConfig struct { matchFunction func(tx crosschaintypes.CrossChainTx) bool } +// WaitCctxRevertedByInboundHash waits until cctx is reverted by inbound hash. +func WaitCctxRevertedByInboundHash( + ctx context.Context, + t require.TestingT, + hash string, + c CCTXClient, +) crosschaintypes.CrossChainTx { + // wait for cctx to be reverted + cctxs := WaitCctxByInboundHash(ctx, t, hash, c, MatchReverted()) + require.Len(t, cctxs, 1) + + return cctxs[0] +} + // WaitCctxByInboundHash waits until cctx appears by inbound hash. func WaitCctxByInboundHash( ctx context.Context, diff --git a/go.mod b/go.mod index 524a63d8be..55b0377e8d 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,8 @@ module github.com/zeta-chain/node -go 1.22.2 +go 1.22.7 -toolchain go1.22.5 +toolchain go1.22.8 require ( cosmossdk.io/errors v1.0.1 @@ -24,7 +24,7 @@ require ( github.com/cosmos/ibc-go/v7 v7.4.0 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/emicklei/proto v1.11.1 - github.com/ethereum/go-ethereum v1.10.26 + github.com/ethereum/go-ethereum v1.13.15 github.com/fatih/color v1.14.1 github.com/frumioj/crypto11 v1.2.5-0.20210823151709-946ce662cc0e github.com/gagliardetto/solana-go v1.10.0 @@ -39,7 +39,6 @@ require ( github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/libp2p/go-libp2p v0.27.8 - github.com/libp2p/go-libp2p-kad-dht v0.24.2 github.com/mattn/go-sqlite3 v1.14.19 // indirect github.com/multiformats/go-multiaddr v0.9.0 github.com/nanmu42/etherscan-api v1.10.0 @@ -57,13 +56,13 @@ require ( github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.18.2 github.com/stretchr/testify v1.9.0 - github.com/zeta-chain/ethermint v0.0.0-20241010181243-044e22bdb7e7 + github.com/zeta-chain/ethermint v0.0.0-20241105191054-1ebf85a354a0 github.com/zeta-chain/keystone/keys v0.0.0-20240826165841-3874f358c138 github.com/zeta-chain/protocol-contracts v1.0.2-athens3.0.20241021075719-d40d2e28467c gitlab.com/thorchain/tss/go-tss v1.6.5 go.nhat.io/grpcmock v0.25.0 golang.org/x/crypto v0.23.0 - golang.org/x/exp v0.0.0-20230905200255-921286631fa9 + golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa golang.org/x/net v0.25.0 golang.org/x/sync v0.7.0 google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80 @@ -91,7 +90,7 @@ require ( github.com/ChainSafe/go-schnorrkel v1.0.0 // indirect github.com/DataDog/zstd v1.5.0 // indirect github.com/StackExchange/wmi v1.2.1 // indirect - github.com/VictoriaMetrics/fastcache v1.6.0 // indirect + github.com/VictoriaMetrics/fastcache v1.12.1 // indirect github.com/agl/ed25519 v0.0.0-20200225211852-fd4d107ace12 // indirect github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect github.com/armon/go-metrics v0.4.1 // indirect @@ -135,10 +134,9 @@ require ( github.com/dgraph-io/ristretto v0.1.1 // indirect github.com/dlclark/regexp2 v1.7.0 // indirect github.com/docker/go-units v0.5.0 // indirect - github.com/dop251/goja v0.0.0-20230122112309-96b1610dd4f7 // indirect + github.com/dop251/goja v0.0.0-20230806174421-c933cf95e127 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/dvsekhvalnov/jose2go v1.6.0 // indirect - github.com/edsrzf/mmap-go v1.0.0 // indirect github.com/elastic/gosigar v0.14.2 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/flynn/noise v1.0.0 // indirect @@ -152,16 +150,15 @@ require ( github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/go-logr/logr v1.3.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-ole/go-ole v1.2.6 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect - github.com/go-stack/stack v1.8.1 // indirect github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/gogo/googleapis v1.4.1 // indirect github.com/gogo/protobuf v1.3.3 // indirect github.com/golang/glog v1.2.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/snappy v0.0.4 // indirect + github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect github.com/google/btree v1.1.2 // indirect github.com/google/flatbuffers v2.0.8+incompatible // indirect github.com/google/go-cmp v0.6.0 // indirect @@ -186,19 +183,15 @@ require ( github.com/hashicorp/hcl v1.0.0 // indirect github.com/hdevalence/ed25519consensus v0.1.0 // indirect github.com/holiman/bloomfilter/v2 v2.0.3 // indirect - github.com/holiman/uint256 v1.2.3 // indirect - github.com/huin/goupnp v1.2.0 // indirect + github.com/holiman/uint256 v1.2.4 + github.com/huin/goupnp v1.3.0 // indirect github.com/iancoleman/orderedmap v0.3.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/ipfs/boxo v0.10.0 // indirect github.com/ipfs/go-cid v0.4.1 // indirect - github.com/ipfs/go-datastore v0.6.0 // indirect github.com/ipfs/go-log v1.0.5 // indirect github.com/ipfs/go-log/v2 v2.5.1 // indirect - github.com/ipld/go-ipld-prime v0.20.0 // indirect github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect - github.com/jbenet/goprocess v0.1.4 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jmhodges/levigo v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect @@ -212,8 +205,6 @@ require ( github.com/libp2p/go-cidranger v1.1.0 // indirect github.com/libp2p/go-flow-metrics v0.1.0 // indirect github.com/libp2p/go-libp2p-asn-util v0.3.0 // indirect - github.com/libp2p/go-libp2p-kbucket v0.6.3 // indirect - github.com/libp2p/go-libp2p-record v0.2.0 // indirect github.com/libp2p/go-msgio v0.3.0 // indirect github.com/libp2p/go-nat v0.1.0 // indirect github.com/libp2p/go-netroute v0.2.1 // indirect @@ -226,7 +217,7 @@ require ( github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.9 // indirect + github.com/mattn/go-runewidth v0.0.13 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/miekg/dns v1.1.54 // indirect github.com/miekg/pkcs11 v1.1.1 // indirect @@ -261,14 +252,11 @@ require ( github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/petermattis/goid v0.0.0-20230317030725-371a4b8eda08 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/polydawn/refmt v0.89.0 // indirect github.com/prometheus/client_model v0.4.0 // indirect github.com/prometheus/common v0.42.0 // indirect github.com/prometheus/procfs v0.9.0 // indirect - github.com/prometheus/tsdb v0.7.1 // indirect github.com/raulk/go-watchdog v1.3.0 // indirect github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect - github.com/rjeczalik/notify v0.9.1 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sasha-s/go-deadlock v0.3.1 // indirect @@ -293,7 +281,6 @@ require ( github.com/tklauser/numcpus v0.6.1 // indirect github.com/tyler-smith/go-bip39 v1.1.0 // indirect github.com/ulikunitz/xz v0.5.11 // indirect - github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 // indirect github.com/yudai/gojsondiff v1.0.0 // indirect github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect github.com/zondax/hid v0.9.2 // indirect @@ -319,13 +306,11 @@ require ( golang.org/x/text v0.16.0 // indirect golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect - gonum.org/v1/gonum v0.13.0 // indirect google.golang.org/api v0.155.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 // indirect gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect lukechampine.com/blake3 v1.2.1 // indirect nhooyr.io/websocket v1.8.7 // indirect pgregory.net/rapid v1.1.0 // indirect @@ -334,19 +319,39 @@ require ( require ( github.com/bnb-chain/tss-lib v1.5.0 + github.com/montanaflynn/stats v0.7.1 github.com/showa-93/go-mask v0.6.2 github.com/tonkeeper/tongo v1.9.3 + github.com/zeta-chain/protocol-contracts-solana/go-idl v0.0.0-20241025181051-d8d49e4fc85b ) require ( + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/aead/siphash v1.0.1 // indirect + github.com/bits-and-blooms/bitset v1.10.0 // indirect + github.com/consensys/bavard v0.1.13 // indirect + github.com/consensys/gnark-crypto v0.12.1 // indirect + github.com/crate-crypto/go-ipa v0.0.0-20231025140028-3c0104f4b233 // indirect + github.com/crate-crypto/go-kzg-4844 v0.7.0 // indirect + github.com/deckarep/golang-set/v2 v2.1.0 // indirect github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect + github.com/ethereum/c-kzg-4844 v0.4.0 // indirect + github.com/gballet/go-verkle v0.1.1-0.20231031103413-a67434b50f46 // indirect + github.com/gofrs/flock v0.8.1 // indirect + github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 // indirect + github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23 // indirect + github.com/mmcloughlin/addchain v0.4.0 // indirect github.com/oasisprotocol/curve25519-voi v0.0.0-20220328075252-7dd334e3daae // indirect + github.com/onsi/ginkgo/v2 v2.9.5 // indirect + github.com/rivo/uniseg v0.2.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/snksoft/crc v1.1.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect + github.com/supranational/blst v0.3.11 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect + rsc.io/tmplfunc v0.0.3 // indirect ) replace ( @@ -365,7 +370,7 @@ replace ( // https://github.com/zeta-chain/tss-lib/tree/threshold-dep-updates // which is a fork of https://github.com/threshold-network/tss-lib github.com/bnb-chain/tss-lib => github.com/zeta-chain/tss-lib v0.0.0-20240916163010-2e6b438bd901 - github.com/ethereum/go-ethereum => github.com/zeta-chain/go-ethereum v1.10.26-spc + github.com/ethereum/go-ethereum => github.com/zeta-chain/go-ethereum v1.13.16-0.20241022183758-422c6ef93ccc github.com/libp2p/go-libp2p => github.com/zeta-chain/go-libp2p v0.0.0-20240710192637-567fbaacc2b4 - gitlab.com/thorchain/tss/go-tss => github.com/zeta-chain/go-tss v0.0.0-20240916173049-89fee4b0ae7f + gitlab.com/thorchain/tss/go-tss => github.com/zeta-chain/go-tss v0.0.0-20241031223543-18765295f992 ) diff --git a/go.sum b/go.sum index 04150048bc..6878385b68 100644 --- a/go.sum +++ b/go.sum @@ -11,7 +11,6 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.37.2/go.mod h1:H8IAquKe2L30IxoupDgqTaQvKSwF/c8prYHynGIWQbA= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.39.0/go.mod h1:rVLT6fkc8chs9sfPtFc1SBH6em7n+ZoXaG+87tDISts= -cloud.google.com/go v0.43.0/go.mod h1:BOSR3VbTLkk6FDC/TcffxP4NF/FFBGA5ku+jvKOP7pg= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= @@ -233,7 +232,6 @@ cloud.google.com/go/bigquery v1.53.0/go.mod h1:3b/iXjRQGU4nKa87cXeg6/gogLjO8C6Pm cloud.google.com/go/bigquery v1.55.0/go.mod h1:9Y5I3PN9kQWuid6183JFhOGOW3GcirA5LpsKCUn+2ec= cloud.google.com/go/bigquery v1.56.0/go.mod h1:KDcsploXTEY7XT3fDQzMUZlpQLHzE4itubHrnmhUrZA= cloud.google.com/go/bigquery v1.57.1/go.mod h1:iYzC0tGVWt1jqSzBHqCr3lrRn0u13E8e+AqowBsDgug= -cloud.google.com/go/bigtable v1.2.0/go.mod h1:JcVAOl45lrTmQfLj7T6TxyMzIN/3FGGcFm+2xVAli2o= cloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY= cloud.google.com/go/billing v1.5.0/go.mod h1:mztb1tBc3QekhjSgmpf/CV4LzWXLzCArwpLmP2Gm88s= cloud.google.com/go/billing v1.6.0/go.mod h1:WoXzguj+BeHXPbKfNWkqVtDdzORazmCjraY+vrxcyvI= @@ -1192,7 +1190,6 @@ cloud.google.com/go/workflows v1.12.1/go.mod h1:5A95OhD/edtOhQd/O741NSfIMezNTbCw cloud.google.com/go/workflows v1.12.2/go.mod h1:+OmBIgNqYJPVggnMo9nqmizW0qEXHhmnAzK/CnBqsHc= cloud.google.com/go/workflows v1.12.3/go.mod h1:fmOUeeqEwPzIU81foMjTRQIdwQHADi/vEr1cx9R1m5g= code.gitea.io/sdk/gitea v0.12.0/go.mod h1:z3uwDV/b9Ls47NGukYM9XhnHtqPh/J+t40lsUrR6JDY= -collectd.org v0.3.0/go.mod h1:A/8DzQBkF6abtvrT2j/AU/4tiBgJWYyh0y/oB/4MlWE= contrib.go.opencensus.io/exporter/aws v0.0.0-20181029163544-2befc13012d0/go.mod h1:uu1P0UCM/6RbsMrgPa98ll8ZcHM858i/AD06a9aLRCA= contrib.go.opencensus.io/exporter/ocagent v0.5.0/go.mod h1:ImxhfLRpxoYiSq891pBrLVhN+qmP8BTVvdH2YLs7Gl0= contrib.go.opencensus.io/exporter/stackdriver v0.12.1/go.mod h1:iwB6wGarfphGGe/e5CWqyUk/cLzKnWsOKPVW3no6OTw= @@ -1259,11 +1256,19 @@ github.com/Azure/azure-sdk-for-go v35.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9mo github.com/Azure/azure-sdk-for-go v38.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go v42.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw= -github.com/Azure/azure-sdk-for-go/sdk/azcore v0.21.1/go.mod h1:fBF9PQNqB8scdgpZ3ufzaLntG0AG7C1WjPMsiFOmfHM= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.0.0/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.0.0/go.mod h1:+6sju8gk8FRmSajX3Oz4G5Gm7P+mbqE9FVaXXFYTkCM= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0/go.mod h1:OQeznEEkTZ9OrhHJoDD8ZDq51FHgXjqtP9z6bEwBq9U= github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8= -github.com/Azure/azure-sdk-for-go/sdk/internal v0.8.3/go.mod h1:KLF4gFr6DcKFZwSuH8w8yEK6DpFl3LP5rhdvAb7Yz5I= -github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.3.0/go.mod h1:tPaiy8S5bQ+S5sOiDlINkp7+Ef339+Nz5L5XO+cnOHo= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal v1.0.0/go.mod h1:ceIuwmxDWptoW3eCqSXlnPsZFKh4X+R38dWPv7GS9Vs= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.0.0/go.mod h1:s1tW/At+xHqjNFvWU4G0c0Qv33KOhvbGNj0RCTQDV8s= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.2.0/go.mod h1:c+Lifp3EDEamAkPVzMooRNOK6CZjNSdEnf1A7jsI9u4= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.2.0/go.mod h1:+6KLcKIVgxoBDMqMO/Nvy7bZ9a0nbU3I1DtFQK3YvB4= github.com/Azure/azure-service-bus-go v0.9.1/go.mod h1:yzBx6/BUGfjfeqbRZny9AQIbIe3AcV9WZbAdpkoXOa0= github.com/Azure/azure-storage-blob-go v0.8.0/go.mod h1:lPI3aLPpuLTeUwh1sViKXFxwl2B6teiRqI0deQUvsw0= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= @@ -1308,11 +1313,14 @@ github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZ github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0/go.mod h1:Vt9sXTKwMyGcOxSmLDMnGPgqsUg7m8pe215qMLrDXw4= +github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0/go.mod h1:kgDmCTgBzIEPFElEF+FK0SdjAor06dRq2Go927dnQ6o= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.2.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/ChainSafe/go-schnorrkel v0.0.0-20200405005733-88cbf1b4c40d/go.mod h1:URdX5+vg25ts3aCh8H5IFZybJYKWhJHYMTnf+ULtoC4= github.com/ChainSafe/go-schnorrkel v1.0.0 h1:3aDA67lAykLaG1y3AOjs88dMxC88PgUuHRrLeDnvGIM= @@ -1323,7 +1331,6 @@ github.com/CloudyKit/jet v2.1.3-0.20180809161101-62edd43e4f88+incompatible/go.mo github.com/CloudyKit/jet/v3 v3.0.0/go.mod h1:HKQPgSJmdK8hdoAbKUUWajkHyHo4RaU5rMdUywE7VMo= github.com/CloudyKit/jet/v6 v6.1.0/go.mod h1:d3ypHeIRNo2+XyqnGA8s+aphtcVpjP5hPwP/Lzo7Ro4= github.com/CloudyKit/jet/v6 v6.2.0/go.mod h1:d3ypHeIRNo2+XyqnGA8s+aphtcVpjP5hPwP/Lzo7Ro4= -github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= @@ -1406,8 +1413,8 @@ github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMx github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= -github.com/VictoriaMetrics/fastcache v1.6.0 h1:C/3Oi3EiBCqufydp1neRZkqcwmEiuRT9c3fqvvgKm5o= -github.com/VictoriaMetrics/fastcache v1.6.0/go.mod h1:0qHz5QP0GMX4pfmMA/zt5RgfNuXJrTP0zS7DqpHGGTw= +github.com/VictoriaMetrics/fastcache v1.12.1 h1:i0mICQuojGDL3KblA7wUNlY5lOK6a4bwt3uRKnkZU40= +github.com/VictoriaMetrics/fastcache v1.12.1/go.mod h1:tX04vaqcNoQeGLD+ra5pU5sWkuxnzWhEzLwhP9w653o= github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/Workiva/go-datastructures v1.0.52/go.mod h1:Z+F2Rca0qCsVYDS8z7bAGm8f3UkzuWYS/oBZz5a7VVA= @@ -1417,6 +1424,7 @@ github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ github.com/adlio/schema v1.1.13/go.mod h1:L5Z7tw+7lRK1Fnpi/LT/ooCP1elkXn0krMWBQHUhEDE= github.com/adlio/schema v1.3.3 h1:oBJn8I02PyTB466pZO1UZEn1TV5XLlifBSyMrmHl/1I= github.com/adlio/schema v1.3.3/go.mod h1:1EsRssiv9/Ce2CMzq5DoL7RiMshhuigQxrR4DMV9fHg= +github.com/aead/siphash v1.0.1 h1:FwHfE/T45KPKYuuSAKyyvE+oPWcaQ+CUmFW0bPlM+kg= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= @@ -1444,7 +1452,6 @@ github.com/allegro/bigcache v1.2.1 h1:hg1sY1raCwic3Vnsvje6TT7/pnZba83LeFck5NrFKS github.com/allegro/bigcache v1.2.1/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 h1:MzBOUgng9orim59UnfUTLRjMpd09C5uEVQ6RPGeCaVI= github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129/go.mod h1:rFgpPQZYZ8vdbc+48xibu8ALc3yeyd64IhHS+PU6Yyg= -github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/andybalholm/brotli v1.0.3/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= @@ -1454,7 +1461,6 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuW github.com/antihax/optional v0.0.0-20180407024304-ca021399b1a6/go.mod h1:V8iCPQYkqmusNa815XgQio277wI47sdRh1dUOLdyC6Q= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/aokoli/goutils v1.0.1/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ= -github.com/apache/arrow/go/arrow v0.0.0-20191024131854-af6fa24be0db/go.mod h1:VTxUBvSJ3s3eHAg65PNgrsn5BtqCRPdmyXh6rAfdxN0= github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0= github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4xei5aX110hRiI= github.com/apache/arrow/go/v12 v12.0.0/go.mod h1:d+tV/eHZZ7Dz7RPrFKtPK02tpr+c9/PEd/zm8mDS9Vg= @@ -1503,18 +1509,22 @@ github.com/aws/aws-sdk-go v1.44.122/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX github.com/aws/aws-sdk-go v1.44.203 h1:pcsP805b9acL3wUqa4JR2vg1k2wnItkDYNvfmcy6F+U= github.com/aws/aws-sdk-go v1.44.203/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= -github.com/aws/aws-sdk-go-v2 v1.2.0/go.mod h1:zEQs02YRBw1DjK0PoJv3ygDYOFTre1ejlJWl8FwAuQo= github.com/aws/aws-sdk-go-v2 v1.9.1/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4= -github.com/aws/aws-sdk-go-v2/config v1.1.1/go.mod h1:0XsVy9lBI/BCXm+2Tuvt39YmdHwS5unDQmxZOYe8F5Y= -github.com/aws/aws-sdk-go-v2/credentials v1.1.1/go.mod h1:mM2iIjwl7LULWtS6JCACyInboHirisUUdkBPoTHMOUo= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.0.2/go.mod h1:3hGg3PpiEjHnrkrlasTfxFqUsZ2GCk/fMUn4CbKgSkM= +github.com/aws/aws-sdk-go-v2 v1.21.2/go.mod h1:ErQhvNuEMhJjweavOYhxVkn2RUx7kQXVATHrjKtxIpM= +github.com/aws/aws-sdk-go-v2/config v1.18.45/go.mod h1:ZwDUgFnQgsazQTnWfeLWk5GjeqTQTL8lMkoE1UXzxdE= +github.com/aws/aws-sdk-go-v2/credentials v1.13.43/go.mod h1:zWJBz1Yf1ZtX5NGax9ZdNjhhI4rgjfgsyk6vTY1yfVg= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.13/go.mod h1:f/Ib/qYjhV2/qdsf79H3QP/eRE4AkVyEf6sk7XfZ1tg= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.43/go.mod h1:auo+PiyLl0n1l8A0e8RIeR8tOzYPfZZH/JNlrJ8igTQ= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.37/go.mod h1:Qe+2KtKml+FEsQF/DHmDV+xjtche/hwoF75EG4UlHW8= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.45/go.mod h1:lD5M20o09/LCuQ2mE62Mb/iSdSlCNuj6H5ci7tW7OsE= github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.8.1/go.mod h1:CM+19rL1+4dFWnOQKwDc7H1KwXTz+h61oUSHyhV0b3o= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.0.2/go.mod h1:45MfaXZ0cNbeuT0KQ1XJylq8A6+OpVV2E5kvY/Kq+u8= -github.com/aws/aws-sdk-go-v2/service/route53 v1.1.1/go.mod h1:rLiOUrPLW/Er5kRcQ7NkwbjlijluLsrIbu/iyl35RO4= -github.com/aws/aws-sdk-go-v2/service/sso v1.1.1/go.mod h1:SuZJxklHxLAXgLTc1iFXbEWkXs7QRTQpCLGaKIprQW0= -github.com/aws/aws-sdk-go-v2/service/sts v1.1.1/go.mod h1:Wi0EBZwiz/K44YliU0EKxqTCJGUfYTWXrrBwkq736bM= -github.com/aws/smithy-go v1.1.0/go.mod h1:EzMw8dbp/YJL4A5/sbhGddag+NPT7q084agLbB9LgIw= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.37/go.mod h1:vBmDnwWXWxNPFRMmG2m/3MKOe+xEcMDo1tanpaWCcck= +github.com/aws/aws-sdk-go-v2/service/route53 v1.30.2/go.mod h1:TQZBt/WaQy+zTHoW++rnl8JBrmZ0VO6EUbVua1+foCA= +github.com/aws/aws-sdk-go-v2/service/sso v1.15.2/go.mod h1:gsL4keucRCgW+xA85ALBpRFfdSLH4kHOVSnLMSuBECo= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.17.3/go.mod h1:a7bHA82fyUXOm+ZSWKU6PIoBxrjSprdLoM8xPYvzYVg= +github.com/aws/aws-sdk-go-v2/service/sts v1.23.2/go.mod h1:Eows6e1uQEsc4ZaHANmsPRzAKcVDrcmjjWiih2+HUUQ= github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= +github.com/aws/smithy-go v1.15.0/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= @@ -1537,6 +1547,10 @@ github.com/binance-chain/edwards25519 v0.0.0-20200305024217-f36fc4b53d43 h1:Vkf7 github.com/binance-chain/edwards25519 v0.0.0-20200305024217-f36fc4b53d43/go.mod h1:TnVqVdGEK8b6erOMkcyYGWzCQMw7HEMCOw3BgFYCFWs= github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= +github.com/bits-and-blooms/bitset v1.5.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= +github.com/bits-and-blooms/bitset v1.7.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= +github.com/bits-and-blooms/bitset v1.10.0 h1:ePXTeiPEazB5+opbv5fr8umg2R/1NlzgDsyepwsSr88= +github.com/bits-and-blooms/bitset v1.10.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= github.com/bkielbasa/cyclop v1.2.0/go.mod h1:qOI0yy6A7dYC4Zgsa72Ppm9kONl0RoIlPbzot9mhmeI= @@ -1550,8 +1564,6 @@ github.com/blendle/zapdriver v1.3.1 h1:C3dydBOWYRiOk+B8X9IVZ5IOe+7cl+tGOexN4QqHf github.com/blendle/zapdriver v1.3.1/go.mod h1:mdXfREi6u5MArG4j9fewC+FGnXaBR+T4Ox4J2u4eHCc= github.com/blizzy78/varnamelen v0.8.0/go.mod h1:V9TzQZ4fLJ1DSrjVDfl89H7aMnTvKkApdHeyESmyR7k= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= -github.com/bmizerany/pat v0.0.0-20170815010413-6226ea591a40/go.mod h1:8rLXio+WjiTceGBHIoTvn60HIbs7Hm7bcHjyrSqYB9c= -github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= github.com/bombsimon/wsl/v2 v2.0.0/go.mod h1:mf25kr/SqFEPhhcxW1+7pxzGlW+hIl/hYTKY95VwV8U= github.com/bombsimon/wsl/v2 v2.2.0/go.mod h1:Azh8c3XGEJl9LyX0/sFC+CKMc7Ssgua0g+6abzXN4Pg= github.com/bombsimon/wsl/v3 v3.0.0/go.mod h1:st10JtZYLE4D5sC7b8xV4zTKZwAQjCH/Hy2Pm1FNZIc= @@ -1625,7 +1637,6 @@ github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3k github.com/butuzov/ireturn v0.1.1/go.mod h1:Wh6Zl3IMtTpaIKbmwzqi6olnM9ptYQxxVacMsOEFPoc= github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/bwesterb/go-ristretto v1.2.2/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= -github.com/c-bata/go-prompt v0.2.2/go.mod h1:VzqtzE2ksDBcdln8G7mk2RX9QyGjH+OVqOCSiVIqS34= github.com/caarlos0/ctrlc v1.0.0/go.mod h1:CdXpj4rmq0q/1Eb44M9zi2nKB0QraNKuRGYGrrHhcQw= github.com/campoy/unique v0.0.0-20180121183637-88950e537e7e/go.mod h1:9IOqJGCPMSc6E5ydlp5NIonxObaeu/Iub/X03EKPVYo= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= @@ -1660,12 +1671,15 @@ github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAc github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927/go.mod h1:h/aW8ynjgkuj+NQRlZcDbAbM1ORAbXjXX77sX7T289U= github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY= github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic= github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg= @@ -1681,7 +1695,7 @@ github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= github.com/cloudflare/circl v1.3.1/go.mod h1:+CauBF6R70Jqcyl8N2hC8pAXYbWkGIezuSbuGLtRhnw= -github.com/cloudflare/cloudflare-go v0.14.0/go.mod h1:EnwdgGMaFOruiPZRFSgn+TsQ3hQ7C/YWzIGLeu5c304= +github.com/cloudflare/cloudflare-go v0.79.0/go.mod h1:gkHQf9xEubaQPEuerBuoinR9P8bf8a05Lq0X6WKy1Oc= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= @@ -1744,10 +1758,13 @@ github.com/cometbft/cometbft v0.37.5/go.mod h1:QC+mU0lBhKn8r9qvmnq53Dmf3DWBt4Vtk github.com/cometbft/cometbft-db v0.7.0/go.mod h1:yiKJIm2WKrt6x8Cyxtq9YTEcIMPcEe4XPxhgX59Fzf0= github.com/cometbft/cometbft-db v0.12.0 h1:v77/z0VyfSU7k682IzZeZPFZrQAKiQwkqGN0QzAjMi0= github.com/cometbft/cometbft-db v0.12.0/go.mod h1:aX2NbCrjNVd2ZajYxt1BsiFf/Z+TQ2MN0VxdicheYuw= -github.com/consensys/bavard v0.1.8-0.20210406032232-f3452dc9b572/go.mod h1:Bpd0/3mZuaj6Sj+PqrmIquiOKy397AKGThQPaGzNXAQ= github.com/consensys/bavard v0.1.8-0.20210915155054-088da2f7f54a/go.mod h1:9ItSMtA/dXMAiL7BG6bqW2m3NdSEObYWoH223nGHukI= -github.com/consensys/gnark-crypto v0.4.1-0.20210426202927-39ac3d4b3f1f/go.mod h1:815PAHg3wvysy0SyIqanF8gZ0Y1wjk/hrDHD/iT88+Q= +github.com/consensys/bavard v0.1.13 h1:oLhMLOFGTLdlda/kma4VOJazblc7IM5y5QPd2A/YjhQ= +github.com/consensys/bavard v0.1.13/go.mod h1:9ItSMtA/dXMAiL7BG6bqW2m3NdSEObYWoH223nGHukI= github.com/consensys/gnark-crypto v0.5.3/go.mod h1:hOdPlWQV1gDLp7faZVeg8Y0iEPFaOUnCc4XeCCk96p0= +github.com/consensys/gnark-crypto v0.10.0/go.mod h1:Iq/P3HHl0ElSjsg2E1gsMwhAyxnxoKK5nVyZKd+/KhU= +github.com/consensys/gnark-crypto v0.12.1 h1:lHH39WuuFgVHONRl3J0LRBtuYdQTumFSDtJF7HpyG8M= +github.com/consensys/gnark-crypto v0.12.1/go.mod h1:v2Gy7L/4ZRosZ7Ivs+9SfUDr0f5UlG+EM5t7MPHiLuY= github.com/containerd/aufs v0.0.0-20200908144142-dab0cbea06f4/go.mod h1:nukgQABAEopAHvB6j7cnP5zJ+/3aVcE7hCYqvIwAHyE= github.com/containerd/aufs v0.0.0-20201003224125-76a6863f2989/go.mod h1:AkGGQs9NM2vtYHaUen+NljV0/baGCAPELGm2q9ZXpWU= github.com/containerd/aufs v0.0.0-20210316121734-20793ff83c97/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU= @@ -1942,6 +1959,10 @@ github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/crate-crypto/go-ipa v0.0.0-20231025140028-3c0104f4b233 h1:d28BXYi+wUpz1KBmiF9bWrjEMacUEREV6MBi2ODnrfQ= +github.com/crate-crypto/go-ipa v0.0.0-20231025140028-3c0104f4b233/go.mod h1:geZJZH3SzKCqnz5VT0q/DyIG/tvu/dZk+VIfXicupJs= +github.com/crate-crypto/go-kzg-4844 v0.7.0 h1:C0vgZRk4q4EZ/JgPfzuSoxdCq3C3mOZMBShovmncxvA= +github.com/crate-crypto/go-kzg-4844 v0.7.0/go.mod h1:1kMhvPgI0Ky3yIa+9lFySEBUBXkYxeOi8ZF1sYioxhc= github.com/creachadair/taskgroup v0.3.2/go.mod h1:wieWwecHVzsidg2CsUnFinW1faVN4+kq+TDlRJQ0Wbk= github.com/creachadair/taskgroup v0.4.2 h1:jsBLdAJE42asreGss2xZGZ8fJra7WtwnHWeJFxv2Li8= github.com/creachadair/taskgroup v0.4.2/go.mod h1:qiXUOSrbwAY3u0JPGTzObbE3yf9hcXHDKBZ2ZjpCbgM= @@ -1967,7 +1988,6 @@ github.com/daixiang0/gci v0.8.1/go.mod h1:EpVfrztufwVgQRXjnX4zuNinEpLj5OmMjtu/+M github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg= github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= -github.com/dave/jennifer v1.2.0/go.mod h1:fIb+770HOpJ2fmN9EPPKOqm1vMGhB+TwXKMZhrIygKg= github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -1979,6 +1999,8 @@ github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c h1:pFUpOrbxDR github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c/go.mod h1:6UhI8N9EjYm1c2odKpFpAYeR8dsBeM7PtzQhRgxRr9U= github.com/deckarep/golang-set v1.8.0 h1:sk9/l/KqpunDwP7pSjUg0keiOOLEnOBHzykLrsPppp4= github.com/deckarep/golang-set v1.8.0/go.mod h1:5nI87KwE7wgsBU1F4GKAw2Qod7p5kyS383rP6+o6qqo= +github.com/deckarep/golang-set/v2 v2.1.0 h1:g47V4Or+DUdzbs8FxCCmgb6VYd+ptPAngjM6dtGktsI= +github.com/deckarep/golang-set/v2 v2.1.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= @@ -1990,7 +2012,6 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etly github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= github.com/deepmap/oapi-codegen v1.6.0/go.mod h1:ryDa9AgbELGeB+YEXE1dR53yAjHwFvE9iAUlWl9Al3M= -github.com/deepmap/oapi-codegen v1.8.2/go.mod h1:YLgSKSDv/bZQB7N4ws6luhozi3cEdRktEqrX88CvjIw= github.com/denis-tingaikin/go-header v0.4.3/go.mod h1:0wOCWuN71D5qIgE2nz9KrKmuYBAC2Mra5RassOIQ2/c= github.com/denisenkom/go-mssqldb v0.12.0/go.mod h1:iiK0YP1ZeepvmBQk/QpLEhhTNJgfzrpArPY/aFvc9yU= github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0= @@ -2010,7 +2031,6 @@ github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWa github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dgryski/go-bitstream v0.0.0-20180413035011-3522498ce2c8/go.mod h1:VMaSuZ+SZcx/wljOQKvp5srsbCiKDEb6K2wC4+PiBmQ= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= @@ -2041,7 +2061,6 @@ github.com/docker/docker v0.0.0-20200511152416-a93e9eb0e95c/go.mod h1:eEKB0N0r5N github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v1.4.2-0.20180531152204-71cd53e4a197/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker v1.6.2/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v17.12.0-ce-rc1.0.20200730172259-9f28837c1d93+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v20.10.0-beta1.0.20201110211921-af34b94a78a1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v20.10.3-0.20211208011758-87521affb077+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= @@ -2065,9 +2084,8 @@ github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNE github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= -github.com/dop251/goja v0.0.0-20220405120441-9037c2b61cbf/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= -github.com/dop251/goja v0.0.0-20230122112309-96b1610dd4f7 h1:kgvzE5wLsLa7XKfV85VZl40QXaMCaeFtHpPwJ8fhotY= -github.com/dop251/goja v0.0.0-20230122112309-96b1610dd4f7/go.mod h1:yRkwfj0CBpOGre+TwBsqPV0IH0Pk73e4PXJOeNDboGs= +github.com/dop251/goja v0.0.0-20230806174421-c933cf95e127 h1:qwcF+vdFrvPSEUDSX5RVoRccG8a5DhOdWdQ4zN62zzo= +github.com/dop251/goja v0.0.0-20230806174421-c933cf95e127/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4= github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= @@ -2080,8 +2098,6 @@ github.com/dvsekhvalnov/jose2go v1.6.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= -github.com/eclipse/paho.mqtt.golang v1.2.0/go.mod h1:H9keYFcgq3Qr5OUJm/JZI/i6U7joQ8SYLhZwfeOo6Ts= -github.com/edsrzf/mmap-go v1.0.0 h1:CEBF7HpRnUCSJgGUb5h1Gm7e3VkmVDrR8lvWVLtrOFw= github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= github.com/elastic/gosigar v0.12.0/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= @@ -2122,6 +2138,8 @@ github.com/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew= github.com/esimonov/ifshort v1.0.4/go.mod h1:Pe8zjlRrJ80+q2CxHLfEOfTwxCZ4O+MuhcHcfgNWTk0= github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= +github.com/ethereum/c-kzg-4844 v0.4.0 h1:3MS1s4JtA868KpJxroZoepdV0ZKBp3u/O5HcZ7R3nlY= +github.com/ethereum/c-kzg-4844 v0.4.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0= github.com/ettle/strcase v0.1.1/go.mod h1:hzDLsPC7/lwKyBOywSHEP89nt2pDgdy+No1NBA9o9VY= github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= @@ -2146,11 +2164,12 @@ github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSw github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/ferranbt/fastssz v0.1.2/go.mod h1:X5UPrE2u1UJjxHA8X54u04SBwdAQjG2sFtWs39YxyWs= github.com/firefart/nonamedreturns v1.0.1/go.mod h1:D3dpIBojGGNh5UfElmwPu73SwDCm+VKhHYqwlNOk2uQ= github.com/firefart/nonamedreturns v1.0.4/go.mod h1:TDhe/tjI1BXo48CmYbUduTV7BdIga8MAO/xbKdcVsGI= -github.com/fjl/gencodec v0.0.0-20220412091415-8bb9e558978c/go.mod h1:AzA8Lj6YtixmJWL+wkKoBGsLWy9gFrAzi4g+5bCKwpY= -github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5 h1:FtmdgXiUlNeRsoNMFlKLDt+S+6hbjVMEW6RGQ7aUf7c= -github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0= +github.com/fjl/gencodec v0.0.0-20230517082657-f9840df7b83e/go.mod h1:AzA8Lj6YtixmJWL+wkKoBGsLWy9gFrAzi4g+5bCKwpY= +github.com/fjl/memsize v0.0.2 h1:27txuSD9or+NZlnOWdKUxeBzTAUkWCVh+4Gf2dWFOzA= +github.com/fjl/memsize v0.0.2/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0= github.com/flosch/pongo2 v0.0.0-20190707114632-bbf5a6c351f4/go.mod h1:T9YF2M40nIgbVgp3rreNmTged+9HrbNTIQf1PsaIiTA= github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= @@ -2186,6 +2205,8 @@ github.com/fzipp/gocyclo v0.5.1/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlya github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA= github.com/gagliardetto/binary v0.8.0 h1:U9ahc45v9HW0d15LoN++vIXSJyqR/pWw8DDlhd7zvxg= github.com/gagliardetto/binary v0.8.0/go.mod h1:2tfj51g5o9dnvsc+fL3Jxr22MuWzYXwx9wEoN0XQ7/c= +github.com/gagliardetto/gofuzz v1.2.2 h1:XL/8qDMzcgvR4+CyRQW9UGdwPRPMHVJfqQ/uMvSUuQw= +github.com/gagliardetto/gofuzz v1.2.2/go.mod h1:bkH/3hYLZrMLbfYWA0pWzXmi5TTRZnu4pMGZBkqMKvY= github.com/gagliardetto/solana-go v1.10.0 h1:lDuHGC+XLxw9j8fCHBZM9tv4trI0PVhev1m9NAMaIdM= github.com/gagliardetto/solana-go v1.10.0/go.mod h1:afBEcIRrDLJst3lvAahTr63m6W2Ns6dajZxe2irF7Jg= github.com/gagliardetto/treeout v0.1.4 h1:ozeYerrLCmCubo1TcIjFiOWTTGteOOHND1twdFpgwaw= @@ -2195,8 +2216,9 @@ github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYis github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff h1:tY80oXqGNY4FhTFhk+o9oFHGINQ/+vhlm8HFzi6znCI= github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= +github.com/gballet/go-verkle v0.1.1-0.20231031103413-a67434b50f46 h1:BAIP2GihuqhwdILrV+7GJel5lyPV3u1+PgzrWLc0TkE= +github.com/gballet/go-verkle v0.1.1-0.20231031103413-a67434b50f46/go.mod h1:QNpY22eby74jVhqH4WhDLDwxc/vqsern6pW+u2kbkpc= github.com/getkin/kin-openapi v0.53.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4= -github.com/getkin/kin-openapi v0.61.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4= github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/getsentry/sentry-go v0.12.0/go.mod h1:NSap0JBYWzHND8oMbyi0+XZhUalc1TBdRL1M71JZW2c= @@ -2217,8 +2239,6 @@ github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8= github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4= -github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= -github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs= github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= @@ -2279,8 +2299,9 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= -github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= @@ -2317,7 +2338,6 @@ github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= @@ -2343,7 +2363,6 @@ github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslW github.com/go-toolsmith/typep v1.0.0/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU= github.com/go-toolsmith/typep v1.0.2/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU= github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= -github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= github.com/go-zookeeper/zk v1.0.2/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= @@ -2356,8 +2375,9 @@ github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/E github.com/gobwas/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA= github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0= github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk= github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= @@ -2370,8 +2390,8 @@ github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/flock v0.0.0-20190320160742-5135e617513b/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gofrs/flock v0.7.3/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= -github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= @@ -2384,16 +2404,18 @@ github.com/gogo/googleapis v1.4.1-0.20201022092350-68b0159b7869/go.mod h1:5YRNX2 github.com/gogo/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0= github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4= github.com/gogo/status v1.1.0/go.mod h1:BFv9nrluPLmrS0EmGVvLaPNmRosr9KapBYd5/hpY1WM= +github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= -github.com/golang-jwt/jwt/v4 v4.3.0 h1:kHL1vqdqWNfATmA0FNMdmZNMyZI1U6O31X4rlIPoBog= -github.com/golang-jwt/jwt/v4 v4.3.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/sqlexp v0.0.0-20170517235910-f1bb20e5a188/go.mod h1:vXjM/+wXQnTPR4KqTKDgJukSZ6amVRtWMPEjE6sQoK8= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= -github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ= @@ -2444,8 +2466,9 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6 github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk= +github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4= github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk= github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6/go.mod h1:DbHgvLiFKX1Sh2T1w8Q/h4NAI8MHIpzCdnBUDTXU3I0= @@ -2483,7 +2506,6 @@ github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl76 github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= github.com/google/certificate-transparency-go v1.1.1/go.mod h1:FDKqPvSXawb2ecErVRrD+nfy23RCzyl7eqVCEmlT1Zs= github.com/google/crfs v0.0.0-20191108021818-71d77da419c9/go.mod h1:etGhoOqfwPkooV6aqoX3eBGQOJblqdoc9XvWOeuxpPw= -github.com/google/flatbuffers v1.11.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/flatbuffers v2.0.0+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/flatbuffers v2.0.8+incompatible h1:ivUb1cGomAB101ZM1T0nOiWz9pSrTMoa9+EiY7igmkM= @@ -2520,7 +2542,6 @@ github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSN github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.1.1-0.20200604201612-c04b05f3adfa/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gopacket v1.1.17/go.mod h1:UdDNZ1OO62aGYVnPhxT1U6aI7ukYtA/kB8vaU0diBUM= @@ -2553,6 +2574,7 @@ github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg= github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 h1:hR7/MlvK23p6+lIw9SN1TigNLn9ZnF3W4SYRKq2gAHs= github.com/google/pprof v0.0.0-20230602150820-91b7bce49751/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= @@ -2564,6 +2586,7 @@ github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= +github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/trillian v1.3.11/go.mod h1:0tPraVHrSDkA3BO6vKX67zgLXs6SsOAbHEivX+9mPgw= github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -2616,7 +2639,6 @@ github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl github.com/gookit/color v1.5.1/go.mod h1:wZFzea4X8qN6vHOSP2apMb4/+w/orMznEzYsIHPaqKM= github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= github.com/gordonklaus/ineffassign v0.0.0-20200309095847-7953dde2c7bf/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU= github.com/gordonklaus/ineffassign v0.0.0-20210914165742-4cc7213b9bc8/go.mod h1:Qcp2HIAYhR7mNUVSIxZww3Guk4it82ghYcEXIAk+QT0= @@ -2734,6 +2756,7 @@ github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es github.com/hashicorp/go-retryablehttp v0.6.4/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= github.com/hashicorp/go-retryablehttp v0.7.0/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= +github.com/hashicorp/go-retryablehttp v0.7.4/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= @@ -2778,11 +2801,12 @@ github.com/hdevalence/ed25519consensus v0.0.0-20220222234857-c00d1f31bab3/go.mod github.com/hdevalence/ed25519consensus v0.1.0 h1:jtBwzzcHuTmFrQN6xQZn6CQEO/V9f7HsjsjeEZ6auqU= github.com/hdevalence/ed25519consensus v0.1.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4 h1:X4egAf/gcS1zATw6wn4Ej8vjuVGxeHdan+bRb2ebyv4= +github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4/go.mod h1:5GuXa7vkL8u9FkFuWdVvfR5ix8hRB7DbOAaYULamFpc= github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= -github.com/holiman/uint256 v1.2.0/go.mod h1:y4ga/t+u+Xwd7CpDgZESaRcWy0I7XMlTMA25ApIH5Jw= -github.com/holiman/uint256 v1.2.3 h1:K8UWO1HUJpRMXBxbmaY1Y8IAMZC/RsKB+ArEnnK4l5o= -github.com/holiman/uint256 v1.2.3/go.mod h1:SC8Ryt4n+UBbPbIBKaG9zbbDlp4jOru9xFZmPzLUTxw= +github.com/holiman/uint256 v1.2.4 h1:jUc4Nk8fm9jZabQuqr2JzednajVmBpC+oiTiXZJEApU= +github.com/holiman/uint256 v1.2.4/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/go-assert v1.1.5 h1:fjemmA7sSfYHJD7CUqs9qTwwfdNAx7/j2/ZlHXzNB3c= github.com/huandu/go-assert v1.1.5/go.mod h1:yOLvuqZwmcHIC5rIzrBhT7D3Q9c3GFnd0JrPVhn/06U= @@ -2793,9 +2817,8 @@ github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63 github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= github.com/hudl/fargo v1.4.0/go.mod h1:9Ai6uvFy5fQNq6VPKtg+Ceq1+eTY4nKUlR2JElEOcDo= github.com/huin/goupnp v1.0.0/go.mod h1:n9v9KO1tAxYH82qOn+UTIFQDmx5n1Zxd/ClZDMX7Bnc= -github.com/huin/goupnp v1.0.3/go.mod h1:ZxNlw5WqJj6wSsRK5+YfflQGXYfccj5VgQsMNixHM7Y= -github.com/huin/goupnp v1.2.0 h1:uOKW26NG1hsSSbXIZ1IR7XP9Gjd1U8pnLaCMgntmkmY= -github.com/huin/goupnp v1.2.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150/go.mod h1:PpLOETDnJ0o3iZrZfqZzyLl6l7F3c6L1oWn7OICBi6o= github.com/hydrogen18/memlistener v0.0.0-20141126152155-54553eb933fb/go.mod h1:qEIFzExnS6016fRpRfxrExeVn2gbClQA99gQhnIcdhE= github.com/hydrogen18/memlistener v0.0.0-20200120041712-dcc25e7acd91/go.mod h1:qEIFzExnS6016fRpRfxrExeVn2gbClQA99gQhnIcdhE= @@ -2805,6 +2828,7 @@ github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJ github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= github.com/imdario/mergo v0.3.4/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= @@ -2820,40 +2844,24 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/influxdata/flux v0.65.1/go.mod h1:J754/zds0vvpfwuq7Gc2wRdVwEodfpCFM7mYlOw2LqY= -github.com/influxdata/influxdb v1.8.3/go.mod h1:JugdFhsvvI8gadxOI6noqNeeBHvWNTbfYGtiAn+2jhI= github.com/influxdata/influxdb-client-go/v2 v2.4.0/go.mod h1:vLNHdxTJkIf2mSLvGrpj8TCcISApPoXkaxP8g9uRlW8= github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= -github.com/influxdata/influxql v1.1.1-0.20200828144457-65d3ef77d385/go.mod h1:gHp9y86a/pxhjJ+zMjNXiQAA197Xk9wLxaz+fGG+kWk= -github.com/influxdata/line-protocol v0.0.0-20180522152040-32c6aa80de5e/go.mod h1:4kt73NQhadE3daL3WhR5EJ/J2ocX0PZzwxQ0gXJ7oFE= +github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= -github.com/influxdata/line-protocol v0.0.0-20210311194329-9aa0e372d097/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= -github.com/influxdata/promql/v2 v2.12.0/go.mod h1:fxOPu+DY0bqCTCECchSRtWfc+0X19ybifQhZoQNF5D8= -github.com/influxdata/roaring v0.4.13-0.20180809181101-fc520f41fab6/go.mod h1:bSgUQ7q5ZLSO+bKBGqJiCBGAl+9DxyW63zLTujjUlOE= -github.com/influxdata/tdigest v0.0.0-20181121200506-bf2b5ad3c0a9/go.mod h1:Js0mqiSBE6Ffsg94weZZ2c+v/ciT8QRHFOap7EKDrR0= -github.com/influxdata/usage-client v0.0.0-20160829180054-6d3895376368/go.mod h1:Wbbw6tYNvwa5dlB6304Sd+82Z3f7PmVZHVKU637d4po= github.com/informalsystems/tm-load-test v1.0.0/go.mod h1:WVaSKaQdfZK3v0C74EMzn7//+3aeCZF8wkIKBz2/M74= github.com/informalsystems/tm-load-test v1.3.0/go.mod h1:OQ5AQ9TbT5hKWBNIwsMjn6Bf4O0U4b1kRc+0qZlQJKw= github.com/intel/goresctrl v0.2.0/go.mod h1:+CZdzouYFn5EsxgqAQTEzMfwKwuc0fVdMrT9FCCAVRQ= -github.com/ipfs/boxo v0.10.0 h1:tdDAxq8jrsbRkYoF+5Rcqyeb91hgWe2hp7iLu7ORZLY= -github.com/ipfs/boxo v0.10.0/go.mod h1:Fg+BnfxZ0RPzR0nOodzdIq3A7KgoWAOWsEIImrIQdBM= github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s= github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk= -github.com/ipfs/go-datastore v0.6.0 h1:JKyz+Gvz1QEZw0LsX1IBn+JFCJQH4SJVFtM4uWU0Myk= -github.com/ipfs/go-datastore v0.6.0/go.mod h1:rt5M3nNbSO/8q1t4LNkLyUwRs8HupMeN/8O4Vn9YAT8= github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk= github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps= -github.com/ipfs/go-ipfs-util v0.0.2 h1:59Sswnk1MFaiq+VcaknX7aYEyGyGDAA73ilhEK2POp8= -github.com/ipfs/go-ipfs-util v0.0.2/go.mod h1:CbPtkWJzjLdEcezDns2XYaehFVNXG9zrdrtMecczcsQ= github.com/ipfs/go-log v0.0.1/go.mod h1:kL1d2/hzSpI0thNYjiKfjanbVNU+IIGA/WnNESY9leM= github.com/ipfs/go-log v1.0.5 h1:2dOuUCB1Z7uoczMWgAyDck5JLb72zHzrMnGnCNNbvY8= github.com/ipfs/go-log v1.0.5/go.mod h1:j0b8ZoR+7+R99LD9jZ6+AJsrzkPbSXbZfGakb5JPtIo= github.com/ipfs/go-log/v2 v2.1.3/go.mod h1:/8d0SH3Su5Ooc31QlL1WysJhvyOTDCjcCZ9Axpmri6g= github.com/ipfs/go-log/v2 v2.5.1 h1:1XdUzF7048prq4aBjDQQ4SL5RxftpRGdXhNRwKSAlcY= github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI= -github.com/ipld/go-ipld-prime v0.20.0 h1:Ud3VwE9ClxpO2LkCYP7vWPc0Fo+dYdYzgxUJZ3uRG4g= -github.com/ipld/go-ipld-prime v0.20.0/go.mod h1:PzqZ/ZR981eKbgdr3y2DJYeD/8bgMawdGVlJDE8kK+M= github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI= github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0= github.com/iris-contrib/httpexpect/v2 v2.3.1/go.mod h1:ICTf89VBKSD3KB0fsyyHviKF8G8hyepP0dOXJPWz3T0= @@ -2874,14 +2882,11 @@ github.com/jaguilar/vt100 v0.0.0-20150826170717-2703a27b14ea/go.mod h1:QMdK4dGB3 github.com/jarcoal/httpmock v1.0.5/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc= github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= -github.com/jbenet/go-cienv v0.1.0/go.mod h1:TqNnHUmJgXau0nCzC7kXWeotg3J9W34CUv5Djy1+FlA= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk= github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPwbGVtZVWC34vc5WLsDk= -github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0o= -github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4= github.com/jdxcode/netrc v0.0.0-20210204082910-926c7f70242a/go.mod h1:Zi/ZFkEqFHTm7qkjyNJjaWH4LQA9LQhGJyF0lTYGpxw= -github.com/jedisct1/go-minisign v0.0.0-20190909160543-45766022959e/go.mod h1:G1CVv03EnqU1wYL2dFwXxW2An0az9JTl/ZsqXQeBlkU= +github.com/jedisct1/go-minisign v0.0.0-20230811132847-661be99b8267/go.mod h1:h1nSAbGFqGVzn6Jyl1R/iCcBUHN4g+gW1u9CoBTrb9E= github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= @@ -2941,8 +2946,6 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/jsternberg/zap-logfmt v1.0.0/go.mod h1:uvPs/4X51zdkcm5jXl5SYoN+4RK21K8mysFmDaM/h+o= -github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/juju/errors v0.0.0-20181118221551-089d3ea4e4d5/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= github.com/juju/loggo v0.0.0-20180524022052-584905176618/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= @@ -2953,7 +2956,6 @@ github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8 github.com/julz/importas v0.1.0/go.mod h1:oSFU2R4XK/P7kNBrnL/FEQlDGN1/6WoxXEjSSXO0DV0= github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= -github.com/jwilder/encoding v0.0.0-20170811194829-b4e1701a28ef/go.mod h1:Ct9fl0F6iIOGgxJ5npU/IUOhOhqlVrGjyIZc8/MagT0= github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= github.com/karalabe/usb v0.0.2/go.mod h1:Od972xHfMJowv7NGVDiWVxk2zxnWgjLlJzE+F4F7AGU= github.com/kataras/blocks v0.0.6/go.mod h1:UK+Iwk0Oxpc0GdoJja7sEildotAUKK1LYeYcVF0COWc= @@ -2980,11 +2982,13 @@ github.com/kataras/sitemap v0.0.6/go.mod h1:dW4dOCNs896OR1HmG+dMLdT7JjDk7mYBzoIR github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwfnHGpYw= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kilic/bls12-381 v0.1.0/go.mod h1:vDTTHJONJ6G+P2R74EhnyotQDTliQDnFEwhdmfzw1ig= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/errcheck v1.6.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/errcheck v1.6.2/go.mod h1:nXw/i/MfnvRHqXa7XXmQMUB0oNFGuBrNI8d8NLy0LPw= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kkHAIKE/contextcheck v1.1.3/go.mod h1:PG/cwd6c0705/LM0KTr1acO2gORUxkSVWyLJOFW5qoo= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23 h1:FOOIBWrEkLgmlgGfMuZT83xIwfPDxEI2OHu6xUmJMFE= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= @@ -3014,15 +3018,13 @@ github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQs github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= -github.com/klauspost/cpuid v0.0.0-20170728055534-ae7887de9fa5/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid v0.0.0-20180405133222-e7e905edc00e/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= -github.com/klauspost/crc32 v0.0.0-20161016154125-cb6bfca970f6/go.mod h1:+ZoRqAPRLkC4NPOvfYeR5KNOrY6TD+/sAC3HXPZgDYg= -github.com/klauspost/pgzip v1.0.2-0.20170402124221-0bf5dcad4ada/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -3051,6 +3053,7 @@ github.com/kunwardeep/paralleltest v1.0.3/go.mod h1:vLydzomDFpk7yu5UX02RmP0H8QfR github.com/kunwardeep/paralleltest v1.0.6/go.mod h1:Y0Y0XISdZM5IKm3TREQMZ6iteqn1YuwCsJO/0kL9Zes= github.com/kylelemons/godebug v0.0.0-20170224010052-a616ab194758/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/kyoh86/exportloopref v0.1.8/go.mod h1:1tUcJeiioIs7VWe5gcOObrux3lb66+sBqGZrRkMwPgg= github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g= @@ -3063,6 +3066,7 @@ github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3 github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= github.com/ldez/gomoddirectives v0.2.3/go.mod h1:cpgBogWITnCfRq2qGoDkKMEVSaarhdBr6g8G04uz6d0= github.com/ldez/tagliatelle v0.3.1/go.mod h1:8s6WJQwEYHbKZDsp/LjArytKOG8qaMrKQQ3mFukHs88= +github.com/leanovate/gopter v0.2.9 h1:fQjYxZaynp97ozCzfOyOuAGOU4aU/z37zf/tOujFk7c= github.com/leanovate/gopter v0.2.9/go.mod h1:U2L/78B+KVFIx2VmW6onHJQzXtFb+p5y3y2Sh+Jxxv8= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= @@ -3088,12 +3092,6 @@ github.com/libp2p/go-flow-metrics v0.1.0 h1:0iPhMI8PskQwzh57jB9WxIuIOQ0r+15PChFG github.com/libp2p/go-flow-metrics v0.1.0/go.mod h1:4Xi8MX8wj5aWNDAZttg6UPmc0ZrnFNsMtpsYUClFtro= github.com/libp2p/go-libp2p-asn-util v0.3.0 h1:gMDcMyYiZKkocGXDQ5nsUQyquC9+H+iLEQHwOCZ7s8s= github.com/libp2p/go-libp2p-asn-util v0.3.0/go.mod h1:B1mcOrKUE35Xq/ASTmQ4tN3LNzVVaMNmq2NACuqyB9w= -github.com/libp2p/go-libp2p-kad-dht v0.24.2 h1:zd7myKBKCmtZBhI3I0zm8xBkb28v3gmSEtQfBdAdFwc= -github.com/libp2p/go-libp2p-kad-dht v0.24.2/go.mod h1:BShPzRbK6+fN3hk8a0WGAYKpb8m4k+DtchkqouGTrSg= -github.com/libp2p/go-libp2p-kbucket v0.6.3 h1:p507271wWzpy2f1XxPzCQG9NiN6R6lHL9GiSErbQQo0= -github.com/libp2p/go-libp2p-kbucket v0.6.3/go.mod h1:RCseT7AH6eJWxxk2ol03xtP9pEHetYSPXOaJnOiD8i0= -github.com/libp2p/go-libp2p-record v0.2.0 h1:oiNUOCWno2BFuxt3my4i1frNrt7PerzB3queqa1NkQ0= -github.com/libp2p/go-libp2p-record v0.2.0/go.mod h1:I+3zMkvvg5m2OcSdoL0KPljyJyvNDFGKX7QdlpYUcwk= github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA= github.com/libp2p/go-libp2p-testing v0.12.0/go.mod h1:KcGDRXyN7sQCllucn1cOOS+Dmm7ujhfEyXQL5lvkcPg= github.com/libp2p/go-msgio v0.3.0 h1:mf3Z8B1xcFN314sWX+2vOTShIE0Mmn2TXn3YCUQGNj0= @@ -3190,20 +3188,19 @@ github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzp github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= -github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI= github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= -github.com/mattn/go-tty v0.0.0-20180907095812-13ff1204f104/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE= github.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo= github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= @@ -3254,6 +3251,7 @@ github.com/minio/highwayhash v1.0.1/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLT github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g= github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= +github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4= @@ -3277,6 +3275,7 @@ github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0Qu github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.3.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= @@ -3287,6 +3286,9 @@ github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjU github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY= +github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU= +github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU= github.com/moby/buildkit v0.8.1/go.mod h1:/kyU1hKy/aYCuP39GZA9MaKioovHku57N6cqlKZIaiQ= github.com/moby/buildkit v0.10.3/go.mod h1:jxeOuly98l9gWHai0Ojrbnczrk/rf+o9/JqNhY+UCSo= github.com/moby/buildkit v0.10.4/go.mod h1:Yajz9vt1Zw5q9Pp4pdb3TCSUXJBIroIQGQ3TTs/sLug= @@ -3321,6 +3323,10 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= +github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/moricho/tparallel v0.2.1/go.mod h1:fXEIZxG2vdfl0ZF8b42f5a78EhjjD5mX8qUplsoSU4k= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 h1:mPMvm6X6tf4w8y7j9YIt6V9jfWhL6QlbEc7CCmeQlWk= @@ -3335,7 +3341,6 @@ github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/mrunalp/fileutils v0.0.0-20200520151820-abd8a0e76976/go.mod h1:x8F1gnqOkIEiO4rqoeEEEqQbo7HjGMTvyoq3gej4iT0= github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= -github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg= github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= @@ -3457,8 +3462,8 @@ github.com/onsi/ginkgo/v2 v2.4.0/go.mod h1:iHkDK1fKGcBoEHT5W7YBq4RFWaQulw+caOMkA github.com/onsi/ginkgo/v2 v2.5.0/go.mod h1:Luc4sArBICYCS8THh8v3i3i5CuSZO+RaQRaJoeNwomw= github.com/onsi/ginkgo/v2 v2.7.0/go.mod h1:yjiuMwPokqY1XauOgju45q3sJt6VzQ/Fict1LFVcsAo= github.com/onsi/ginkgo/v2 v2.8.1/go.mod h1:N1/NbDngAFcSLdyZ+/aYTYGSlq9qMCS/cNKGJjy+csc= -github.com/onsi/ginkgo/v2 v2.9.7 h1:06xGQy5www2oN160RtEZoTvnP2sPhEfePYmCDc2szss= -github.com/onsi/ginkgo/v2 v2.9.7/go.mod h1:cxrmXWykAwTwhQsJOPfdIDiJ+l2RYq7U8hFU+M/1uw0= +github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= +github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= github.com/onsi/gomega v0.0.0-20151007035656-2152b45fa28a/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= @@ -3530,7 +3535,6 @@ github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go github.com/opentracing-contrib/go-stdlib v1.0.0/go.mod h1:qtI1ogk+2JhVPIXVc6q+NHziSmy2W5GbdQZFUHADCBU= github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/opentracing/opentracing-go v1.0.3-0.20180606204148-bd9c31933947/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= @@ -3557,7 +3561,6 @@ github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIw github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/paulbellamy/ratecounter v0.2.0/go.mod h1:Hfx1hDpSGoqxkVVpBi/IlYD7kChlfo5C6hzIHwPqfFE= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= @@ -3579,13 +3582,11 @@ github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdU github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= github.com/performancecopilot/speed/v4 v4.0.0/go.mod h1:qxrSyuDGrTOWfV+uKRFhfxw6h/4HXRGUiZiufxo49BM= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= -github.com/peterh/liner v1.0.1-0.20180619022028-8c1271fcf47f/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc= github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o= github.com/petermattis/goid v0.0.0-20230317030725-371a4b8eda08 h1:hDSdbBuw3Lefr6R18ax0tZ2BJeNB3NehB3trOwYBsdU= github.com/petermattis/goid v0.0.0-20230317030725-371a4b8eda08/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/phayes/checkstyle v0.0.0-20170904204023-bfd46e6a821d/go.mod h1:3OzsM7FXDQlpCiw2j81fOmAwQLnZnLGXVKUzeKQXIAw= -github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= @@ -3597,6 +3598,7 @@ github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4 github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pjbgf/sha1cd v0.2.3/go.mod h1:HOK9QrgzdHpbc2Kzip0Q1yi3M2MFGPADtR6HjG65m5M= github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= +github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7/go.mod h1:zO8QMzTeZd5cpnIkz/Gn6iK0jDfGicM1nynOkkPIl28= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= @@ -3610,7 +3612,6 @@ github.com/pkg/profile v1.5.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdL github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= -github.com/pkg/term v0.0.0-20180730021639-bffc007b7fd5/go.mod h1:eCbImbZ95eXtAUIbLAuAVnBnwf83mjf6QIVH8SHYwqQ= github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= @@ -3618,8 +3619,6 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH github.com/pointlander/compress v1.1.1-0.20190518213731-ff44bd196cc3/go.mod h1:q5NXNGzqj5uPnVuhGkZfmgHqNUhf15VLi6L9kW0VEc0= github.com/pointlander/jetset v1.0.1-0.20190518214125-eee7eff80bd4/go.mod h1:RdR1j20Aj5pB6+fw6Y9Ur7lMHpegTEjY1vc19hEZL40= github.com/pointlander/peg v1.0.1/go.mod h1:5hsGDQR2oZI4QoWz0/Kdg3VSVEC31iJw/b7WjqCBGRI= -github.com/polydawn/refmt v0.89.0 h1:ADJTApkvkeBZsN0tBTx8QjpD9JkmxbKp0cxfr9qszm4= -github.com/polydawn/refmt v0.89.0/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw= github.com/polyfloyd/go-errorlint v1.0.0/go.mod h1:KZy4xxPJyy88/gldCe5OdW6OQRtNO3EZE7hXzmnebgA= github.com/polyfloyd/go-errorlint v1.0.2/go.mod h1:APVvOesVSAnne5SClsPxPdfvZTVDojXh1/G3qb5wjGI= github.com/polyfloyd/go-errorlint v1.0.5/go.mod h1:APVvOesVSAnne5SClsPxPdfvZTVDojXh1/G3qb5wjGI= @@ -3693,8 +3692,9 @@ github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1 github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= -github.com/prometheus/tsdb v0.7.1 h1:YZcsG11NqnK4czYLrWd9mpEuAJIHVQLwdrleYfszMAA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/protolambda/bls12-381-util v0.0.0-20220416220906-d8552aa452c7/go.mod h1:IToEjHuttnUzwZI5KBSM/LOOW3qLbbrHOEfp3SbECGY= +github.com/prysmaticlabs/gohashtree v0.0.1-alpha.0.20220714111606-acbb2962fb48/go.mod h1:4pWaT30XoEx1j8KNJf3TV+E3mQkaufn7mf+jRNb/Fuk= github.com/pseudomuto/protoc-gen-doc v1.3.2/go.mod h1:y5+P6n3iGrbKG+9O04V5ld71in3v/bX88wUwgt+U8EA= github.com/pseudomuto/protokit v0.2.0/go.mod h1:2PdH30hxVHsup8KpBTOXTBeMVhJZVio3Q8ViKSAXT0Q= github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI= @@ -3730,9 +3730,8 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qq github.com/remyoudompheng/go-dbus v0.0.0-20121104212943-b7232d34b1d5/go.mod h1:+u151txRmLpwxBmpYn9z3d1sdJdjRPQpsXuYeY9jNls= github.com/remyoudompheng/go-liblzma v0.0.0-20190506200333-81bf2d431b96/go.mod h1:90HvCY7+oHHUKkbeMCiHt1WuFR2/hPJ9QrljDG+v6ls= github.com/remyoudompheng/go-misc v0.0.0-20190427085024-2d6ac652a50e/go.mod h1:80FQABjoFzZ2M5uEa6FUaJYEmqU2UOKojlFVak1UAwI= -github.com/retailnext/hllpp v1.0.1-0.20180308014038-101a6d2f8b52/go.mod h1:RDpi1RftBQPUCDRw6SmxeaREsAaRKnOclghuzp/WRzc= -github.com/rjeczalik/notify v0.9.3 h1:6rJAzHTGKXGj76sbRgDiDcYj/HniypXmSJo1SWakZeY= -github.com/rjeczalik/notify v0.9.3/go.mod h1:gF3zSOrafR9DQEWSE8TjfI9NkooDxbyT4UgRGKZA0lc= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= @@ -3809,8 +3808,6 @@ github.com/securego/gosec/v2 v2.3.0/go.mod h1:UzeVyUXbxukhLeHKV3VVqo7HdoQR9MrRfF github.com/securego/gosec/v2 v2.11.0/go.mod h1:SX8bptShuG8reGC0XS09+a4H2BoWSJi+fscA+Pulbpo= github.com/securego/gosec/v2 v2.13.1/go.mod h1:EO1sImBMBWFjOTFzMWfTRrZW6M15gm60ljzrmy/wtHo= github.com/segmentio/fasthash v1.0.3/go.mod h1:waKX8l2N8yckOgmSsXJi7x1ZfdKZ4x7KRMzBtS3oedY= -github.com/segmentio/kafka-go v0.1.0/go.mod h1:X6itGqS9L4jDletMsxZ7Dz+JFWxM6JHfPOCvTvk+EJo= -github.com/segmentio/kafka-go v0.2.0/go.mod h1:X6itGqS9L4jDletMsxZ7Dz+JFWxM6JHfPOCvTvk+EJo= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= @@ -3855,12 +3852,10 @@ github.com/skeema/knownhosts v1.1.0/go.mod h1:sKFq3RD6/TKZkSWn8boUbDC7Qkgcv+8XXi github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= -github.com/smartystreets/assertions v1.13.0 h1:Dx1kYM01xsSqKPno3aqLnrwac2LetPvN23diwyr69Qs= github.com/smartystreets/assertions v1.13.0/go.mod h1:wDmR7qL282YbGsPy6H/yAsesrxfxaaSlJazyFLYVFx8= github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg= github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM= github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs= github.com/snikch/goodman v0.0.0-20171125024755-10e37e294daa/go.mod h1:oJyF+mSPHbB5mVY2iO9KV3pTt/QbIkGaO8gQ2WrDbP4= @@ -3928,7 +3923,6 @@ github.com/spf13/viper v1.14.0/go.mod h1:WT//axPky3FdvXHzGw33dNdXXXfFQqmEalje+eg github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I= -github.com/status-im/keycard-go v0.0.0-20190316090335-8537d3370df4/go.mod h1:RZLeN1LMWmRsyYjvAu+I6Dm9QmlDaIIt+Y+4Kd7Tp+Q= github.com/status-im/keycard-go v0.2.0 h1:QDLFswOQu1r5jsycloeQh3bVU8n/NatHHaZobtDnDzA= github.com/status-im/keycard-go v0.2.0/go.mod h1:wlp8ZLbsmrF6g6WjugPAx+IzoLrkdf9+mHxBEeo3Hbg= github.com/stbenjam/no-sprintf-host-port v0.1.1/go.mod h1:TLhvtIvONRzdmkFiio4O8LHsN9N74I+PhRquPsxpL0I= @@ -3955,7 +3949,6 @@ github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRci github.com/stretchr/testify v0.0.0-20170130113145-4d4bfba8f1d1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v0.0.0-20180303142811-b89eecf5ca5d/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.1.4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.2.0/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= @@ -3979,7 +3972,8 @@ github.com/subosito/gotenv v1.4.0/go.mod h1:mZd6rFysKEcUhUHXJk0C/08wAgyDBFuwEYL7 github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/supranational/blst v0.3.8-0.20220526154634-513d2456b344/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/supranational/blst v0.3.11 h1:LyU6FolezeWAhvQk0k6O/d49jqgO52MSDDfYgbeoEm4= +github.com/supranational/blst v0.3.11/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= github.com/swaggest/assertjson v1.9.0 h1:dKu0BfJkIxv/xe//mkCrK5yZbs79jL7OVf9Ija7o2xQ= github.com/swaggest/assertjson v1.9.0/go.mod h1:b+ZKX2VRiUjxfUIal0HDN85W0nHPAYUbYH5WkkSsFsU= github.com/sylvia7788/contextcheck v1.0.4/go.mod h1:vuPKJMQ7MQ91ZTqfdyreNKwZjyUg6KO+IebVyQDedZQ= @@ -4041,18 +4035,15 @@ github.com/timakin/bodyclose v0.0.0-20200424151742-cb6215831a94/go.mod h1:Qimiff github.com/timakin/bodyclose v0.0.0-20210704033933-f49887972144/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk= github.com/timonwong/loggercheck v0.9.3/go.mod h1:wUqnk9yAOIKtGA39l1KLE9Iz0QiTocu/YZoOf+OzFdw= github.com/timonwong/logrlint v0.1.0/go.mod h1:Zleg4Gw+kRxNej+Ra7o+tEaW5k1qthTaYKU7rSD39LU= -github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= github.com/tinylib/msgp v1.1.5/go.mod h1:eQsjooMTnV42mHu917E26IogZ2930nFyBQdofk10Udg= github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0= github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0= github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao= github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4= -github.com/tklauser/go-sysconf v0.3.5/go.mod h1:MkWzOF4RMCshBAMXuhXJs64Rte09mITnppBXY/rYEFI= github.com/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk= github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI= github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= -github.com/tklauser/numcpus v0.2.2/go.mod h1:x3qojaO3uyYt0i56EW/VUYs7uBvdl2fkfZFu0T9wgjM= github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ= github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= @@ -4080,7 +4071,6 @@ github.com/tonkeeper/tongo v1.9.3/go.mod h1:MjgIgAytFarjCoVjMLjYEtpZNN1f2G/pnZhK github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31/go.mod h1:onvgF043R+lC5RZ8IT9rBXDaEDnpnw/Cl+HFiw+v/7Q= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c/go.mod h1:hzIxponao9Kjc7aWznkXaL4U4TWaDSs8zcsY4Ka08nM= -github.com/tyler-smith/go-bip39 v1.0.1-0.20181017060643-dbb3b84ba2ef/go.mod h1:sJ5fKU0s6JVwZjjcUEX2zFOnvq0ASQ2K9Zr6cf67kNs= github.com/tyler-smith/go-bip39 v1.0.2/go.mod h1:sJ5fKU0s6JVwZjjcUEX2zFOnvq0ASQ2K9Zr6cf67kNs= github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8= github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U= @@ -4108,12 +4098,11 @@ github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli v1.22.4 h1:u7tSpNPPswAFymm8IehJhy4uJMlUuU/GmqSkvJ1InXA= github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/urfave/cli v1.22.10 h1:p8Fspmz3iTctJstry1PYS3HVdllxnEzTEsgIgtxTrCk= -github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= -github.com/urfave/cli/v2 v2.10.2 h1:x3p8awjp/2arX+Nl/G2040AZpOCHS/eMJJ1/a+mye4Y= -github.com/urfave/cli/v2 v2.10.2/go.mod h1:f8iq5LtQ/bLxafbdBSLPPNsgaW0l/2fYYEHhAyPlwvo= +github.com/urfave/cli/v2 v2.24.1/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= +github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= +github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= github.com/uudashr/gocognit v1.0.1/go.mod h1:j44Ayx2KW4+oB6SWMv8KsmHzZrOInQav7D3cQMJ5JUM= github.com/uudashr/gocognit v1.0.5/go.mod h1:wgYz0mitoKOTysqxTDMOUXg+Jb5SvtihkfmugIZYpEA= @@ -4145,12 +4134,7 @@ github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1 github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/vmware/govmomi v0.20.3/go.mod h1:URlwyTFZX72RmxtxuaFL2Uj3fD1JTvZdx59bHWk6aFU= -github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ= -github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw= -github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 h1:EKhdznlJHPMoKr0XTrX+IlJs1LH3lyx2nfr1dOlZ79k= -github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1/go.mod h1:8UvriyWtv5Q5EOgjHaSseUEdkQfvwFv1I/In/O2M9gc= github.com/whyrusleeping/go-logging v0.0.0-20170515211332-0457bb6b88fc/go.mod h1:bopw91TMyo8J3tvftk8xmU2kPmlrt4nScJQZU2hE5EM= -github.com/willf/bitset v1.1.3/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI= github.com/xanzy/go-gitlab v0.31.0/go.mod h1:sPLojNBn68fMUWSxIJtdVVIP8uSBYqesTfDUseX11Ug= @@ -4167,7 +4151,6 @@ github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQ github.com/xhit/go-str2duration v1.2.0/go.mod h1:3cPSlfZlUHVlneIVfePFWcJZsuwf+P1v2SRTV4cUmp4= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -github.com/xlab/treeprint v0.0.0-20180616005107-d6fb6747feb6/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= @@ -4198,18 +4181,20 @@ github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPS github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= -github.com/zeta-chain/ethermint v0.0.0-20241010181243-044e22bdb7e7 h1:eW5aAW9Ag4GDMa7qzsQm6EWC6SENQUokHUpCdS+WSSg= -github.com/zeta-chain/ethermint v0.0.0-20241010181243-044e22bdb7e7/go.mod h1:bY9wUmkSjTJ65U7LF3e9Pc2737NqxCXGN+b/U2Rm5rU= -github.com/zeta-chain/go-ethereum v1.10.26-spc h1:NvY4rR9yw52wfxWt7YoFsWbaIwVMyOtTsWKqGAXk+sE= -github.com/zeta-chain/go-ethereum v1.10.26-spc/go.mod h1:/6CsT5Ceen2WPLI/oCA3xMcZ5sWMF/D46SjM/ayY0Oo= +github.com/zeta-chain/ethermint v0.0.0-20241105191054-1ebf85a354a0 h1:Mr6EEv9H0Ac9kpG/OnYz3nt0Uh48JiVwdDu1HLJlPBs= +github.com/zeta-chain/ethermint v0.0.0-20241105191054-1ebf85a354a0/go.mod h1:e1G1pfDM9is8ZrskMPw2oSuITBU7+vSdfxTZyHbzy8A= +github.com/zeta-chain/go-ethereum v1.13.16-0.20241022183758-422c6ef93ccc h1:FVOttT/f7QCZMkOLssLTY1cbX8pT+HS/kg81zgUAmYE= +github.com/zeta-chain/go-ethereum v1.13.16-0.20241022183758-422c6ef93ccc/go.mod h1:MgO2/CmxFnj6W7v/5hrz3ypco3kHkb8856pRnFkY4xQ= github.com/zeta-chain/go-libp2p v0.0.0-20240710192637-567fbaacc2b4 h1:FmO3HfVdZ7LzxBUfg6sVzV7ilKElQU2DZm8PxJ7KcYI= github.com/zeta-chain/go-libp2p v0.0.0-20240710192637-567fbaacc2b4/go.mod h1:TBv5NY/CqWYIfUstXO1fDWrt4bDoqgCw79yihqBspg8= -github.com/zeta-chain/go-tss v0.0.0-20240916173049-89fee4b0ae7f h1:XqUvw9a3EnDa271r5/tjRy90U2l1E8thdWzlrkbrEGE= -github.com/zeta-chain/go-tss v0.0.0-20240916173049-89fee4b0ae7f/go.mod h1:B1FDE6kHs8hozKSX1/iXgCdvlFbS6+FeAupoBHDK0Cc= +github.com/zeta-chain/go-tss v0.0.0-20241031223543-18765295f992 h1:jpfOoQGHQo29CKZaAPLCjguj35ikpV4UHFI99nL3fVA= +github.com/zeta-chain/go-tss v0.0.0-20241031223543-18765295f992/go.mod h1:nqelgf4HKkqlXaVg8X38a61WfyYB+ivCt6nnjoTIgCc= github.com/zeta-chain/keystone/keys v0.0.0-20240826165841-3874f358c138 h1:vck/FcIIpFOvpBUm0NO17jbEtmSz/W/a5Y4jRuSJl6I= github.com/zeta-chain/keystone/keys v0.0.0-20240826165841-3874f358c138/go.mod h1:U494OsZTWsU75hqoriZgMdSsgSGP1mUL1jX+wN/Aez8= github.com/zeta-chain/protocol-contracts v1.0.2-athens3.0.20241021075719-d40d2e28467c h1:ZoFxMMZtivRLquXVq1sEVlT45UnTPMO1MSXtc88nDv4= github.com/zeta-chain/protocol-contracts v1.0.2-athens3.0.20241021075719-d40d2e28467c/go.mod h1:SjT7QirtJE8stnAe1SlNOanxtfSfijJm3MGJ+Ax7w7w= +github.com/zeta-chain/protocol-contracts-solana/go-idl v0.0.0-20241025181051-d8d49e4fc85b h1:w4YVBbWxk9TI+7HM8hTvK66IgOo5XvEFsmH7n6WgW50= +github.com/zeta-chain/protocol-contracts-solana/go-idl v0.0.0-20241025181051-d8d49e4fc85b/go.mod h1:DcDY828o773soiU/h0XpC+naxitrIMFVZqEvq/EJxMA= github.com/zeta-chain/tss-lib v0.0.0-20240916163010-2e6b438bd901 h1:9whtN5fjYHfk4yXIuAsYP2EHxImwDWDVUOnZJ2pfL3w= github.com/zeta-chain/tss-lib v0.0.0-20240916163010-2e6b438bd901/go.mod h1:d2iTC62s9JwKiCMPhcDDXbIZmuzAyJ4lwso0H5QyRbk= github.com/zondax/hid v0.9.1/go.mod h1:l5wttcP0jwtdLjqjMMWFVEE7d1zO0jvSPA9OPZxWpEM= @@ -4352,6 +4337,7 @@ go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0 go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/automaxprocs v1.5.1/go.mod h1:BF4eumQw0P9GtnuxxovUd06vwm1o18oMzFtK66vU6XU= +go.uber.org/automaxprocs v1.5.2/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0= go.uber.org/dig v1.17.0 h1:5Chju+tUvcC+N7N6EV08BJz41UZuO3BmHcN4A287ZLI= go.uber.org/dig v1.17.0/go.mod h1:rTxpf7l5I0eBTlE6/9RL+lDybC7WFwY2QH55ZSjy1mU= go.uber.org/fx v1.19.2 h1:SyFgYQFr1Wl0AYstE8vyYIzP4bFz2URrScjwC4cwUvY= @@ -4373,7 +4359,6 @@ go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN8 go.uber.org/ratelimit v0.2.0 h1:UQE2Bgi7p2B85uP5dC2bbRtig0C+OeNRnNEafLjsLPA= go.uber.org/ratelimit v0.2.0/go.mod h1:YYBV4e4naJvhpitQrWJu1vCpgB7CboMe0qhltKt6mUg= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= -go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= @@ -4404,7 +4389,6 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190909091759-094676da4a83/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -4440,6 +4424,7 @@ golang.org/x/crypto v0.0.0-20220313003712-b769efc7c000/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= @@ -4459,7 +4444,9 @@ golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIi golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -4487,8 +4474,8 @@ golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZ golang.org/x/exp v0.0.0-20230131160201-f062dba9d201/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/exp v0.0.0-20230711153332-06a737ee72cb/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= +golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/exp/typeparams v0.0.0-20220613132600-b0d781184e0d/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= @@ -4542,6 +4529,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -4607,7 +4596,6 @@ golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= @@ -4661,8 +4649,10 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= @@ -4731,6 +4721,7 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -4739,7 +4730,6 @@ golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -4789,7 +4779,6 @@ golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200107162124-548cf772de50/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -4827,6 +4816,7 @@ golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201013081832-0aaa2718063a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201117170446-d9b008d0a637/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -4845,7 +4835,6 @@ golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210313202042-bd2e13477e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -4938,6 +4927,7 @@ golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= @@ -4964,6 +4954,7 @@ golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= @@ -5069,7 +5060,6 @@ golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200102140908-9497f49d5709/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200108203644-89082a384178/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200117220505-0cba7a3a9ee9/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= @@ -5154,6 +5144,8 @@ golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -5166,15 +5158,10 @@ golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNq golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= -gonum.org/v1/gonum v0.0.0-20181121035319-3f7ecaa7e8ca/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0= -gonum.org/v1/gonum v0.6.0/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU= gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0= gonum.org/v1/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA= -gonum.org/v1/gonum v0.13.0 h1:a0T3bh+7fhRyqeNbiC3qVHYmkiQgit3wnNan/2c0HMM= -gonum.org/v1/gonum v0.13.0/go.mod h1:/WPYRckkfWrhWefxyYTfrTtQR0KH4iyHNuzxqXAKyAU= -gonum.org/v1/netlib v0.0.0-20181029234149-ec6d1f5cefe6/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= gonum.org/v1/netlib v0.0.0-20190331212654-76723241ea4e/go.mod h1:kS+toOQn6AQKjmKJ7gzohV1XkqsFehRA2FbsbkopSuQ= gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= @@ -5291,7 +5278,6 @@ google.golang.org/genproto v0.0.0-20190508193815-b515fa19cec8/go.mod h1:VzzqZJRn google.golang.org/genproto v0.0.0-20190522204451-c2c4e71fbf69/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= google.golang.org/genproto v0.0.0-20190620144150-6af8c5fc6601/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= -google.golang.org/genproto v0.0.0-20190716160619-c506a9f90610/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= @@ -5300,7 +5286,6 @@ google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvx google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200108215221-bd8f9a0ef82f/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200117163144-32f20d992d24/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= @@ -5638,9 +5623,8 @@ gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= +gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= -gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU= -gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= @@ -5855,6 +5839,7 @@ rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8 rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU= rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.15/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= diff --git a/pkg/chains/address.go b/pkg/chains/address.go index 1f2ddddad8..8c5ba45db3 100644 --- a/pkg/chains/address.go +++ b/pkg/chains/address.go @@ -54,6 +54,7 @@ func ConvertRecoverToError(r interface{}) error { // DecodeBtcAddress decodes a BTC address from a given string and chainID func DecodeBtcAddress(inputAddress string, chainID int64) (address btcutil.Address, err error) { + // prevent potential panic from 'btcutil.DecodeAddress' defer func() { if r := recover(); r != nil { err = ConvertRecoverToError(r) @@ -68,19 +69,15 @@ func DecodeBtcAddress(inputAddress string, chainID int64) (address btcutil.Addre if chainParams == nil { return nil, fmt.Errorf("chain params not found") } - // test taproot address type - address, err = DecodeTaprootAddress(inputAddress) - if err == nil { - if address.IsForNet(chainParams) { - return address, nil - } - return nil, fmt.Errorf("address %s is not for network %s", inputAddress, chainParams.Name) - } - // test taproot address failed; continue testing other types: P2WSH, P2WPKH, P2SH, P2PKH + + // try decoding input address as a Bitcoin address. + // this will decode all types of Bitcoin addresses: P2PKH, P2SH, P2WPKH, P2WSH, P2TR, etc. address, err = btcutil.DecodeAddress(inputAddress, chainParams) if err != nil { return nil, fmt.Errorf("decode address failed: %s, for input address %s", err.Error(), inputAddress) } + + // address must match the network ok := address.IsForNet(chainParams) if !ok { return nil, fmt.Errorf("address %s is not for network %s", inputAddress, chainParams.Name) @@ -109,7 +106,7 @@ func DecodeSolanaWalletAddress(inputAddress string) (pk solana.PublicKey, err er func IsBtcAddressSupported(addr btcutil.Address) bool { switch addr.(type) { // P2TR address - case *AddressTaproot, + case *btcutil.AddressTaproot, // P2WSH address *btcutil.AddressWitnessScriptHash, // P2WPKH address diff --git a/pkg/chains/address_taproot.go b/pkg/chains/address_taproot.go deleted file mode 100644 index 796c3fde85..0000000000 --- a/pkg/chains/address_taproot.go +++ /dev/null @@ -1,217 +0,0 @@ -package chains - -import ( - "bytes" - "errors" - "fmt" - "strings" - - "github.com/btcsuite/btcd/btcutil" - "github.com/btcsuite/btcd/btcutil/bech32" - "github.com/btcsuite/btcd/chaincfg" - "github.com/btcsuite/btcd/txscript" -) - -// taproot address type - -type AddressSegWit struct { - hrp string - witnessVersion byte - witnessProgram []byte -} - -type AddressTaproot struct { - AddressSegWit -} - -var _ btcutil.Address = &AddressTaproot{} - -// NewAddressTaproot returns a new AddressTaproot. -func NewAddressTaproot(witnessProg []byte, - net *chaincfg.Params) (*AddressTaproot, error) { - return newAddressTaproot(net.Bech32HRPSegwit, witnessProg) -} - -// newAddressTaproot is an internal helper function to create an -// AddressWitnessScriptHash with a known human-readable part, rather than -// looking it up through its parameters. -func newAddressTaproot(hrp string, witnessProg []byte) (*AddressTaproot, error) { - // Check for valid program length for witness version 1, which is 32 - // for P2TR. - if len(witnessProg) != 32 { - return nil, errors.New("witness program must be 32 bytes for " + - "p2tr") - } - - addr := &AddressTaproot{ - AddressSegWit{ - hrp: strings.ToLower(hrp), - witnessVersion: 0x01, - witnessProgram: witnessProg, - }, - } - - return addr, nil -} - -// EncodeAddress returns the bech32 (or bech32m for SegWit v1) string encoding -// of an AddressSegWit. -// -// NOTE: This method is part of the Address interface. -func (a AddressSegWit) EncodeAddress() string { - str, err := encodeSegWitAddress( - a.hrp, a.witnessVersion, a.witnessProgram[:], - ) - if err != nil { - return "" - } - return str -} - -// encodeSegWitAddress creates a bech32 (or bech32m for SegWit v1) encoded -// address string representation from witness version and witness program. -func encodeSegWitAddress(hrp string, witnessVersion byte, witnessProgram []byte) (string, error) { - // Group the address bytes into 5 bit groups, as this is what is used to - // encode each character in the address string. - converted, err := bech32.ConvertBits(witnessProgram, 8, 5, true) - if err != nil { - return "", err - } - - // Concatenate the witness version and program, and encode the resulting - // bytes using bech32 encoding. - combined := make([]byte, len(converted)+1) - combined[0] = witnessVersion - copy(combined[1:], converted) - - var bech string - switch witnessVersion { - case 0: - bech, err = bech32.Encode(hrp, combined) - - case 1: - bech, err = bech32.EncodeM(hrp, combined) - - default: - return "", fmt.Errorf("unsupported witness version %d", - witnessVersion) - } - if err != nil { - return "", err - } - - // Check validity by decoding the created address. - _, version, program, err := decodeSegWitAddress(bech) - if err != nil { - return "", fmt.Errorf("invalid segwit address: %v", err) - } - - if version != witnessVersion || !bytes.Equal(program, witnessProgram) { - return "", fmt.Errorf("invalid segwit address") - } - - return bech, nil -} - -// decodeSegWitAddress parses a bech32 encoded segwit address string and -// returns the witness version and witness program byte representation. -func decodeSegWitAddress(address string) (string, byte, []byte, error) { - // Decode the bech32 encoded address. - hrp, data, bech32version, err := bech32.DecodeGeneric(address) - if err != nil { - return "", 0, nil, err - } - - // The first byte of the decoded address is the witness version, it must - // exist. - if len(data) < 1 { - return "", 0, nil, fmt.Errorf("no witness version") - } - - // ...and be <= 16. - version := data[0] - if version > 16 { - return "", 0, nil, fmt.Errorf("invalid witness version: %v", version) - } - - // The remaining characters of the address returned are grouped into - // words of 5 bits. In order to restore the original witness program - // bytes, we'll need to regroup into 8 bit words. - regrouped, err := bech32.ConvertBits(data[1:], 5, 8, false) - if err != nil { - return "", 0, nil, err - } - - // The regrouped data must be between 2 and 40 bytes. - if len(regrouped) < 2 || len(regrouped) > 40 { - return "", 0, nil, fmt.Errorf("invalid data length") - } - - // For witness version 0, address MUST be exactly 20 or 32 bytes. - if version == 0 && len(regrouped) != 20 && len(regrouped) != 32 { - return "", 0, nil, fmt.Errorf("invalid data length for witness "+ - "version 0: %v", len(regrouped)) - } - - // For witness version 0, the bech32 encoding must be used. - if version == 0 && bech32version != bech32.Version0 { - return "", 0, nil, fmt.Errorf("invalid checksum expected bech32 " + - "encoding for address with witness version 0") - } - - // For witness version 1, the bech32m encoding must be used. - if version == 1 && bech32version != bech32.VersionM { - return "", 0, nil, fmt.Errorf("invalid checksum expected bech32m " + - "encoding for address with witness version 1") - } - - return hrp, version, regrouped, nil -} - -// ScriptAddress returns the witness program for this address. -// -// NOTE: This method is part of the Address interface. -func (a *AddressSegWit) ScriptAddress() []byte { - return a.witnessProgram[:] -} - -// IsForNet returns whether the AddressSegWit is associated with the passed -// bitcoin network. -// -// NOTE: This method is part of the Address interface. -func (a *AddressSegWit) IsForNet(net *chaincfg.Params) bool { - return a.hrp == net.Bech32HRPSegwit -} - -// String returns a human-readable string for the AddressWitnessPubKeyHash. -// This is equivalent to calling EncodeAddress, but is provided so the type -// can be used as a fmt.Stringer. -// -// NOTE: This method is part of the Address interface. -func (a *AddressSegWit) String() string { - return a.EncodeAddress() -} - -// DecodeTaprootAddress decodes taproot address only and returns error on non-taproot address -func DecodeTaprootAddress(addr string) (*AddressTaproot, error) { - hrp, version, program, err := decodeSegWitAddress(addr) - if err != nil { - return nil, err - } - if version != 1 { - return nil, errors.New("invalid witness version; taproot address must be version 1") - } - return &AddressTaproot{ - AddressSegWit{ - hrp: hrp, - witnessVersion: version, - witnessProgram: program, - }, - }, nil -} - -// PayToWitnessTaprootScript creates a new script to pay to a version 1 -// (taproot) witness program. The passed hash is expected to be valid. -func PayToWitnessTaprootScript(rawKey []byte) ([]byte, error) { - return txscript.NewScriptBuilder().AddOp(txscript.OP_1).AddData(rawKey).Script() -} diff --git a/pkg/chains/address_taproot_test.go b/pkg/chains/address_taproot_test.go deleted file mode 100644 index c3742dcefc..0000000000 --- a/pkg/chains/address_taproot_test.go +++ /dev/null @@ -1,79 +0,0 @@ -package chains - -import ( - "encoding/hex" - "testing" - - "github.com/btcsuite/btcd/chaincfg" - "github.com/stretchr/testify/require" -) - -func TestAddressTaproot(t *testing.T) { - { - // should parse mainnet taproot address - addrStr := "bc1p4ur084x8y63mj5hj7eydscuc4awals7ly749x8vhyquc0twcmvhquspa5c" - addr, err := DecodeTaprootAddress(addrStr) - require.NoError(t, err) - require.Equal(t, addrStr, addr.String()) - require.Equal(t, addrStr, addr.EncodeAddress()) - require.True(t, addr.IsForNet(&chaincfg.MainNetParams)) - } - { - // should parse testnet taproot address - addrStr := "tb1pzeclkt6upu8xwuksjcz36y4q56dd6jw5r543eu8j8238yaxpvcvq7t8f33" - addr, err := DecodeTaprootAddress(addrStr) - require.NoError(t, err) - require.Equal(t, addrStr, addr.String()) - require.Equal(t, addrStr, addr.EncodeAddress()) - require.True(t, addr.IsForNet(&chaincfg.TestNet3Params)) - } - { - // should parse regtest taproot address - addrStr := "bcrt1pqqqsyqcyq5rqwzqfpg9scrgwpugpzysnzs23v9ccrydpk8qarc0sj9hjuh" - addr, err := DecodeTaprootAddress(addrStr) - require.NoError(t, err) - require.Equal(t, addrStr, addr.String()) - require.Equal(t, addrStr, addr.EncodeAddress()) - require.True(t, addr.IsForNet(&chaincfg.RegressionNetParams)) - } - - { - // should fail to parse invalid taproot address - // should parse mainnet taproot address - addrStr := "bc1qysd4sp9q8my59ul9wsf5rvs9p387hf8vfwatzu" - _, err := DecodeTaprootAddress(addrStr) - require.Error(t, err) - } - { - var witnessProg [32]byte - for i := 0; i < 32; i++ { - witnessProg[i] = byte(i) - } - _, err := newAddressTaproot("bcrt", witnessProg[:]) - require.NoError(t, err) - //t.Logf("addr: %v", addr) - } - { - // should create correct taproot address from given witness program - // these hex string comes from link - // https://mempool.space/tx/41f7cbaaf9a8d378d09ee86de32eebef455225520cb71015cc9a7318fb42e326 - witnessProg, err := hex.DecodeString("af06f3d4c726a3b952f2f648d86398af5ddfc3df27aa531d97203987add8db2e") - require.NoError(t, err) - addr, err := NewAddressTaproot(witnessProg[:], &chaincfg.MainNetParams) - require.NoError(t, err) - require.Equal(t, addr.EncodeAddress(), "bc1p4ur084x8y63mj5hj7eydscuc4awals7ly749x8vhyquc0twcmvhquspa5c") - } - { - // should give correct ScriptAddress for taproot address - // example comes from - // https://blockstream.info/tx/09298a2f32f5267f419aeaf8a58c4807dcf6cac3edb59815a3b129cd8f1219b0?expand - addrStr := "bc1p6pls9gpm24g8ntl37pajpjtuhd3y08hs5rnf9a4n0wq595hwdh9suw7m2h" - addr, err := DecodeTaprootAddress(addrStr) - require.NoError(t, err) - require.Equal( - t, - "d07f02a03b555079aff1f07b20c97cbb62479ef0a0e692f6b37b8142d2ee6dcb", - hex.EncodeToString(addr.ScriptAddress()), - ) - } -} diff --git a/pkg/chains/address_test.go b/pkg/chains/address_test.go index 38991bc7ab..bba2db0bc9 100644 --- a/pkg/chains/address_test.go +++ b/pkg/chains/address_test.go @@ -179,7 +179,7 @@ func Test_IsBtcAddressSupported_P2TR(t *testing.T) { // it should be a taproot address addr, err := DecodeBtcAddress(tt.addr, tt.chainId) require.NoError(t, err) - _, ok := addr.(*AddressTaproot) + _, ok := addr.(*btcutil.AddressTaproot) require.True(t, ok) // it should be supported diff --git a/pkg/chains/chain.go b/pkg/chains/chain.go index 665251eb39..6731a70959 100644 --- a/pkg/chains/chain.go +++ b/pkg/chains/chain.go @@ -81,6 +81,12 @@ func (chain Chain) EncodeAddress(b []byte) (string, error) { return "", err } return pk.String(), nil + case Consensus_catchain_consensus: + acc, err := ton.ParseAccountID(string(b)) + if err != nil { + return "", err + } + return acc.ToRaw(), nil default: return "", fmt.Errorf("chain id %d not supported", chain.ChainId) } @@ -90,6 +96,10 @@ func (chain Chain) IsEVMChain() bool { return chain.Vm == Vm_evm } +func (chain Chain) IsSolanaChain() bool { + return chain.Consensus == Consensus_solana_consensus +} + func (chain Chain) IsBitcoinChain() bool { return chain.Consensus == Consensus_bitcoin } diff --git a/pkg/contracts/solana/gateway.go b/pkg/contracts/solana/gateway.go index a8f0c571e5..a3adcf5eae 100644 --- a/pkg/contracts/solana/gateway.go +++ b/pkg/contracts/solana/gateway.go @@ -4,6 +4,7 @@ package solana import ( "github.com/gagliardetto/solana-go" "github.com/pkg/errors" + idlgateway "github.com/zeta-chain/protocol-contracts-solana/go-idl/generated" ) const ( @@ -18,30 +19,20 @@ const ( AccountsNumDeposit = 3 ) -// DiscriminatorInitialize returns the discriminator for Solana gateway 'initialize' instruction -func DiscriminatorInitialize() [8]byte { - return [8]byte{175, 175, 109, 31, 13, 152, 155, 237} -} - -// DiscriminatorDeposit returns the discriminator for Solana gateway 'deposit' instruction -func DiscriminatorDeposit() [8]byte { - return [8]byte{242, 35, 198, 137, 82, 225, 242, 182} -} - -// DiscriminatorDepositSPL returns the discriminator for Solana gateway 'deposit_spl_token' instruction -func DiscriminatorDepositSPL() [8]byte { - return [8]byte{86, 172, 212, 121, 63, 233, 96, 144} -} - -// DiscriminatorWithdraw returns the discriminator for Solana gateway 'withdraw' instruction -func DiscriminatorWithdraw() [8]byte { - return [8]byte{183, 18, 70, 156, 148, 109, 161, 34} -} - -// DiscriminatorWithdrawSPL returns the discriminator for Solana gateway 'withdraw_spl_token' instruction -func DiscriminatorWithdrawSPL() [8]byte { - return [8]byte{156, 234, 11, 89, 235, 246, 32} -} +var ( + // DiscriminatorInitialize returns the discriminator for Solana gateway 'initialize' instruction + DiscriminatorInitialize = idlgateway.IDLGateway.GetDiscriminator("initialize") + // DiscriminatorDeposit returns the discriminator for Solana gateway 'deposit' instruction + DiscriminatorDeposit = idlgateway.IDLGateway.GetDiscriminator("deposit") + // DiscriminatorDepositSPL returns the discriminator for Solana gateway 'deposit_spl_token' instruction + DiscriminatorDepositSPL = idlgateway.IDLGateway.GetDiscriminator("deposit_spl_token") + // DiscriminatorWithdraw returns the discriminator for Solana gateway 'withdraw' instruction + DiscriminatorWithdraw = idlgateway.IDLGateway.GetDiscriminator("withdraw") + // DiscriminatorWithdrawSPL returns the discriminator for Solana gateway 'withdraw_spl_token' instruction + DiscriminatorWithdrawSPL = idlgateway.IDLGateway.GetDiscriminator("withdraw_spl_token") + // DiscriminatorWhitelist returns the discriminator for Solana gateway 'whitelist_spl_mint' instruction + DiscriminatorWhitelistSplMint = idlgateway.IDLGateway.GetDiscriminator("whitelist_spl_mint") +) // ParseGatewayAddressAndPda parses the gateway id and program derived address from the given string func ParseGatewayIDAndPda(address string) (solana.PublicKey, solana.PublicKey, error) { diff --git a/pkg/contracts/solana/gateway.json b/pkg/contracts/solana/gateway.json index 8747c2ca0f..b42f29779e 100644 --- a/pkg/contracts/solana/gateway.json +++ b/pkg/contracts/solana/gateway.json @@ -27,7 +27,76 @@ }, { "name": "pda", - "writable": true + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 109, + 101, + 116, + 97 + ] + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + }, + { + "name": "receiver", + "type": { + "array": [ + "u8", + 20 + ] + } + } + ] + }, + { + "name": "deposit_and_call", + "discriminator": [ + 65, + 33, + 186, + 198, + 114, + 223, + 133, + 57 + ], + "accounts": [ + { + "name": "signer", + "writable": true, + "signer": true + }, + { + "name": "pda", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 109, + 101, + 116, + 97 + ] + } + ] + } }, { "name": "system_program", @@ -40,7 +109,16 @@ "type": "u64" }, { - "name": "memo", + "name": "receiver", + "type": { + "array": [ + "u8", + 20 + ] + } + }, + { + "name": "message", "type": "bytes" } ] @@ -65,7 +143,97 @@ }, { "name": "pda", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 109, + 101, + 116, + 97 + ] + } + ] + } + }, + { + "name": "whitelist_entry", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 119, + 104, + 105, + 116, + 101, + 108, + 105, + 115, + 116 + ] + }, + { + "kind": "account", + "path": "mint_account" + } + ] + } + }, + { + "name": "mint_account" + }, + { + "name": "token_program", + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "name": "from", + "writable": true + }, + { + "name": "to", + "writable": true + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + }, + { + "name": "receiver", + "type": { + "array": [ + "u8", + 20 + ] + } + } + ] + }, + { + "name": "deposit_spl_token_and_call", + "discriminator": [ + 14, + 181, + 27, + 187, + 171, + 61, + 237, + 147 + ], + "accounts": [ + { + "name": "signer", "writable": true, + "signer": true + }, + { + "name": "pda", "pda": { "seeds": [ { @@ -80,6 +248,34 @@ ] } }, + { + "name": "whitelist_entry", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 119, + 104, + 105, + 116, + 101, + 108, + 105, + 115, + 116 + ] + }, + { + "kind": "account", + "path": "mint_account" + } + ] + } + }, + { + "name": "mint_account" + }, { "name": "token_program", "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" @@ -99,7 +295,16 @@ "type": "u64" }, { - "name": "memo", + "name": "receiver", + "type": { + "array": [ + "u8", + 20 + ] + } + }, + { + "name": "message", "type": "bytes" } ] @@ -153,6 +358,242 @@ 20 ] } + }, + { + "name": "chain_id", + "type": "u64" + } + ] + }, + { + "name": "initialize_rent_payer", + "discriminator": [ + 225, + 73, + 166, + 180, + 25, + 245, + 183, + 96 + ], + "accounts": [ + { + "name": "rent_payer_pda", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 114, + 101, + 110, + 116, + 45, + 112, + 97, + 121, + 101, + 114 + ] + } + ] + } + }, + { + "name": "authority", + "writable": true, + "signer": true + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [] + }, + { + "name": "set_deposit_paused", + "discriminator": [ + 98, + 179, + 141, + 24, + 246, + 120, + 164, + 143 + ], + "accounts": [ + { + "name": "pda", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 109, + 101, + 116, + 97 + ] + } + ] + } + }, + { + "name": "signer", + "writable": true, + "signer": true + } + ], + "args": [ + { + "name": "deposit_paused", + "type": "bool" + } + ] + }, + { + "name": "unwhitelist_spl_mint", + "discriminator": [ + 73, + 142, + 63, + 191, + 233, + 238, + 170, + 104 + ], + "accounts": [ + { + "name": "whitelist_entry", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 119, + 104, + 105, + 116, + 101, + 108, + 105, + 115, + 116 + ] + }, + { + "kind": "account", + "path": "whitelist_candidate" + } + ] + } + }, + { + "name": "whitelist_candidate" + }, + { + "name": "pda", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 109, + 101, + 116, + 97 + ] + } + ] + } + }, + { + "name": "authority", + "writable": true, + "signer": true + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "signature", + "type": { + "array": [ + "u8", + 64 + ] + } + }, + { + "name": "recovery_id", + "type": "u8" + }, + { + "name": "message_hash", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "nonce", + "type": "u64" + } + ] + }, + { + "name": "update_authority", + "discriminator": [ + 32, + 46, + 64, + 28, + 149, + 75, + 243, + 88 + ], + "accounts": [ + { + "name": "pda", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 109, + 101, + 116, + 97 + ] + } + ] + } + }, + { + "name": "signer", + "writable": true, + "signer": true + } + ], + "args": [ + { + "name": "new_authority_address", + "type": "pubkey" } ] }, @@ -171,7 +612,20 @@ "accounts": [ { "name": "pda", - "writable": true + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 109, + 101, + 116, + 97 + ] + } + ] + } }, { "name": "signer", @@ -191,6 +645,104 @@ } ] }, + { + "name": "whitelist_spl_mint", + "discriminator": [ + 30, + 110, + 162, + 42, + 208, + 147, + 254, + 219 + ], + "accounts": [ + { + "name": "whitelist_entry", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 119, + 104, + 105, + 116, + 101, + 108, + 105, + 115, + 116 + ] + }, + { + "kind": "account", + "path": "whitelist_candidate" + } + ] + } + }, + { + "name": "whitelist_candidate" + }, + { + "name": "pda", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 109, + 101, + 116, + 97 + ] + } + ] + } + }, + { + "name": "authority", + "writable": true, + "signer": true + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "signature", + "type": { + "array": [ + "u8", + 64 + ] + } + }, + { + "name": "recovery_id", + "type": "u8" + }, + { + "name": "message_hash", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "nonce", + "type": "u64" + } + ] + }, { "name": "withdraw", "discriminator": [ @@ -211,7 +763,20 @@ }, { "name": "pda", - "writable": true + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 109, + 101, + 116, + 97 + ] + } + ] + } }, { "name": "to", @@ -287,19 +852,63 @@ } }, { - "name": "from", + "name": "pda_ata", "writable": true }, { - "name": "to", + "name": "mint_account" + }, + { + "name": "recipient" + }, + { + "name": "recipient_ata", + "docs": [ + "the validation will be done in the instruction processor." + ], "writable": true }, + { + "name": "rent_payer_pda", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 114, + 101, + 110, + 116, + 45, + 112, + 97, + 121, + 101, + 114 + ] + } + ] + } + }, { "name": "token_program", "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "name": "associated_token_program", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" } ], "args": [ + { + "name": "decimals", + "type": "u8" + }, { "name": "amount", "type": "u64" @@ -346,6 +955,32 @@ 43, 94 ] + }, + { + "name": "RentPayerPda", + "discriminator": [ + 48, + 247, + 192, + 150, + 46, + 218, + 14, + 121 + ] + }, + { + "name": "WhitelistEntry", + "discriminator": [ + 51, + 70, + 173, + 81, + 219, + 192, + 234, + 62 + ] } ], "errors": [ @@ -388,6 +1023,16 @@ "code": 6007, "name": "MemoLengthTooShort", "msg": "MemoLengthTooShort" + }, + { + "code": 6008, + "name": "DepositPaused", + "msg": "DepositPaused" + }, + { + "code": 6009, + "name": "SPLAtaAndMintAddressMismatch", + "msg": "SPLAtaAndMintAddressMismatch" } ], "types": [ @@ -412,9 +1057,31 @@ { "name": "authority", "type": "pubkey" + }, + { + "name": "chain_id", + "type": "u64" + }, + { + "name": "deposit_paused", + "type": "bool" } ] } + }, + { + "name": "RentPayerPda", + "type": { + "kind": "struct", + "fields": [] + } + }, + { + "name": "WhitelistEntry", + "type": { + "kind": "struct", + "fields": [] + } } ] } \ No newline at end of file diff --git a/pkg/contracts/solana/gateway_message.go b/pkg/contracts/solana/gateway_message.go index 021af3cf1f..1c8abaca23 100644 --- a/pkg/contracts/solana/gateway_message.go +++ b/pkg/contracts/solana/gateway_message.go @@ -61,6 +61,8 @@ func (msg *MsgWithdraw) Hash() [32]byte { var message []byte buff := make([]byte, 8) + message = append(message, []byte("withdraw")...) + binary.BigEndian.PutUint64(buff, msg.chainID) message = append(message, buff...) @@ -105,3 +107,103 @@ func (msg *MsgWithdraw) Signer() (common.Address, error) { return RecoverSigner(msgHash[:], msgSig[:]) } + +// MsgWhitelist is the message for the Solana gateway whitelist_spl_mint instruction +type MsgWhitelist struct { + // whitelistCandidate is the SPL token to be whitelisted in gateway program + whitelistCandidate solana.PublicKey + + // whitelistEntry is the entry in gateway program representing whitelisted SPL token + whitelistEntry solana.PublicKey + + // chainID is the chain ID of Solana chain + chainID uint64 + + // Nonce is the nonce for the withdraw/withdraw_spl + nonce uint64 + + // signature is the signature of the message + signature [65]byte +} + +// NewMsgWhitelist returns a new whitelist_spl_mint message +func NewMsgWhitelist( + whitelistCandidate solana.PublicKey, + whitelistEntry solana.PublicKey, + chainID, nonce uint64, +) *MsgWhitelist { + return &MsgWhitelist{ + whitelistCandidate: whitelistCandidate, + whitelistEntry: whitelistEntry, + chainID: chainID, + nonce: nonce, + } +} + +// To returns the recipient address of the message +func (msg *MsgWhitelist) WhitelistCandidate() solana.PublicKey { + return msg.whitelistCandidate +} + +func (msg *MsgWhitelist) WhitelistEntry() solana.PublicKey { + return msg.whitelistEntry +} + +// ChainID returns the chain ID of the message +func (msg *MsgWhitelist) ChainID() uint64 { + return msg.chainID +} + +// Nonce returns the nonce of the message +func (msg *MsgWhitelist) Nonce() uint64 { + return msg.nonce +} + +// Hash packs the whitelist message and computes the hash +func (msg *MsgWhitelist) Hash() [32]byte { + var message []byte + buff := make([]byte, 8) + + message = append(message, []byte("whitelist_spl_mint")...) + + binary.BigEndian.PutUint64(buff, msg.chainID) + message = append(message, buff...) + + message = append(message, msg.whitelistCandidate.Bytes()...) + + binary.BigEndian.PutUint64(buff, msg.nonce) + message = append(message, buff...) + + return crypto.Keccak256Hash(message) +} + +// SetSignature attaches the signature to the message +func (msg *MsgWhitelist) SetSignature(signature [65]byte) *MsgWhitelist { + msg.signature = signature + return msg +} + +// SigRSV returns the full 65-byte [R+S+V] signature +func (msg *MsgWhitelist) SigRSV() [65]byte { + return msg.signature +} + +// SigRS returns the 64-byte [R+S] core part of the signature +func (msg *MsgWhitelist) SigRS() [64]byte { + var sig [64]byte + copy(sig[:], msg.signature[:64]) + return sig +} + +// SigV returns the V part (recovery ID) of the signature +func (msg *MsgWhitelist) SigV() uint8 { + return msg.signature[64] +} + +// Signer returns the signer of the message +func (msg *MsgWhitelist) Signer() (common.Address, error) { + msgHash := msg.Hash() + msgSig := msg.SigRSV() + + return RecoverSigner(msgHash[:], msgSig[:]) +} diff --git a/pkg/contracts/solana/gateway_message_test.go b/pkg/contracts/solana/gateway_message_test.go index 20c4d84ef9..68af93e859 100644 --- a/pkg/contracts/solana/gateway_message_test.go +++ b/pkg/contracts/solana/gateway_message_test.go @@ -20,7 +20,7 @@ func Test_MsgWithdrawHash(t *testing.T) { amount := uint64(1336000) to := solana.MustPublicKeyFromBase58("37yGiHAnLvWZUNVwu9esp74YQFqxU1qHCbABkDvRddUQ") - wantHash := "a20cddb3f888f4064ced892a477101f45469a8c50f783b966d3fec2455887c05" + wantHash := "aa609ef9480303e8d743f6e36fe1bea0cc56b8d27dcbd8220846125c1181b681" wantHashBytes, err := hex.DecodeString(wantHash) require.NoError(t, err) @@ -29,3 +29,21 @@ func Test_MsgWithdrawHash(t *testing.T) { require.True(t, bytes.Equal(hash[:], wantHashBytes)) }) } + +func Test_MsgWhitelistHash(t *testing.T) { + t.Run("should pass for archived inbound, receipt and cctx", func(t *testing.T) { + // #nosec G115 always positive + chainID := uint64(chains.SolanaLocalnet.ChainId) + nonce := uint64(0) + whitelistCandidate := solana.MustPublicKeyFromBase58("37yGiHAnLvWZUNVwu9esp74YQFqxU1qHCbABkDvRddUQ") + whitelistEntry := solana.MustPublicKeyFromBase58("2kJndCL9NBR36ySiQ4bmArs4YgWQu67LmCDfLzk5Gb7s") + + wantHash := "cde8fa3ab24b50320db1c47f30492e789177d28e76208176f0a52b8ed54ce2dd" + wantHashBytes, err := hex.DecodeString(wantHash) + require.NoError(t, err) + + // create new withdraw message + hash := contracts.NewMsgWhitelist(whitelistCandidate, whitelistEntry, chainID, nonce).Hash() + require.True(t, bytes.Equal(hash[:], wantHashBytes)) + }) +} diff --git a/pkg/contracts/solana/instruction.go b/pkg/contracts/solana/instruction.go index f338129c9b..df5db0416b 100644 --- a/pkg/contracts/solana/instruction.go +++ b/pkg/contracts/solana/instruction.go @@ -99,7 +99,7 @@ func ParseInstructionWithdraw(instruction solana.CompiledInstruction) (*Withdraw } // check the discriminator to ensure it's a 'withdraw' instruction - if inst.Discriminator != DiscriminatorWithdraw() { + if inst.Discriminator != DiscriminatorWithdraw { return nil, fmt.Errorf("not a withdraw instruction: %v", inst.Discriminator) } @@ -116,3 +116,60 @@ func RecoverSigner(msgHash []byte, msgSig []byte) (signer common.Address, err er return crypto.PubkeyToAddress(*pubKey), nil } + +var _ OutboundInstruction = (*WhitelistInstructionParams)(nil) + +// WhitelistInstructionParams contains the parameters for a gateway whitelist_spl_mint instruction +type WhitelistInstructionParams struct { + // Discriminator is the unique identifier for the whitelist instruction + Discriminator [8]byte + + // Signature is the ECDSA signature (by TSS) for the whitelist + Signature [64]byte + + // RecoveryID is the recovery ID used to recover the public key from ECDSA signature + RecoveryID uint8 + + // MessageHash is the hash of the message signed by TSS + MessageHash [32]byte + + // Nonce is the nonce for the whitelist + Nonce uint64 +} + +// Signer returns the signer of the signature contained +func (inst *WhitelistInstructionParams) Signer() (signer common.Address, err error) { + var signature [65]byte + copy(signature[:], inst.Signature[:64]) + signature[64] = inst.RecoveryID + + return RecoverSigner(inst.MessageHash[:], signature[:]) +} + +// GatewayNonce returns the nonce of the instruction +func (inst *WhitelistInstructionParams) GatewayNonce() uint64 { + return inst.Nonce +} + +// TokenAmount returns the amount of the instruction +func (inst *WhitelistInstructionParams) TokenAmount() uint64 { + return 0 +} + +// ParseInstructionWhitelist tries to parse the instruction as a 'whitelist_spl_mint'. +// It returns nil if the instruction can't be parsed as a 'whitelist_spl_mint'. +func ParseInstructionWhitelist(instruction solana.CompiledInstruction) (*WhitelistInstructionParams, error) { + // try deserializing instruction as a 'whitelist_spl_mint' + inst := &WhitelistInstructionParams{} + err := borsh.Deserialize(inst, instruction.Data) + if err != nil { + return nil, errors.Wrap(err, "error deserializing instruction") + } + + // check the discriminator to ensure it's a 'whitelist_spl_mint' instruction + if inst.Discriminator != DiscriminatorWhitelistSplMint { + return nil, fmt.Errorf("not a whitelist_spl_mint instruction: %v", inst.Discriminator) + } + + return inst, nil +} diff --git a/e2e/runner/ton/coin.go b/pkg/contracts/ton/coin.go similarity index 77% rename from e2e/runner/ton/coin.go rename to pkg/contracts/ton/coin.go index dcd1c86009..10921facfc 100644 --- a/e2e/runner/ton/coin.go +++ b/pkg/contracts/ton/coin.go @@ -6,11 +6,12 @@ import ( "github.com/tonkeeper/tongo/utils" ) -// TONCoins takes amount in nano tons and returns it in tons. +// Coins takes amount in TON and returns it in nano tons. +// Example Coins(5) return math.Uint(5 * 10^9) nano tons. // //nolint:revive // in this context TON means 10^9 nano tons. //goland:noinspection GoNameStartsWithPackageName -func TONCoins(amount uint64) math.Uint { +func Coins(amount uint64) math.Uint { // 1 ton = 10^9 nano tons const mul = 1_000_000_000 diff --git a/pkg/contracts/ton/gateway.compiled.json b/pkg/contracts/ton/gateway.compiled.json new file mode 100644 index 0000000000..2f7d5444cb --- /dev/null +++ b/pkg/contracts/ton/gateway.compiled.json @@ -0,0 +1,5 @@ +{ + "hash": "7c82b775cb06df2b7b0329eac2db90cf6c9845425ad1ffe1f9fe11fa3eafcb4b", + "hashBase64": "fIK3dcsG3yt7AynqwtuQz2yYRUJa0f/h+f4R+j6vy0s=", + "hex": "b5ee9c7241021c010004d5000114ff00f4a413f4bcf2c80b010201200215020148030f0202cc040e0499d99e86981fd201800b8d8492f81f0107d22186000797034916ba4e0b079683281698fe99fac98106032492f8270106032f18110603347429836096d9e7040a7106d9e09dcf9683510c08064dd40506180903ee306c12ed44d0d30001f861fa0001f862d31f01f863d69f01f864fa4030f865db3c20d749318100a0b9f2d067812710db3c5cbbf2d06a66a1f84221a0f862db3cc801fa0201fa02c98d041cd95b9917db1bd9d7db595cdcd859d960fe14307170530073c8cb01f828cf16cb01cb5fcb00cb00ccc970fb0007181a04eced44d0d30001f861fa0001f862d31f01f863d69f01f864fa4030f865db3c20d7498100a0b9f2d067d39f3120c702f2d068d430830d8068218409a904a413f9406fa56fa1316c12c0009322f2f0de20c00002bcb192f2f09130e28132c8db3c5cbbf2d06a66a1f84221a0f862db3cc801fa0201fa02c907181a08000ef841c000f2d06e005e8d041cd95b9917db1bd9d7db595cdcd859d960fe14307170530073c8cb01f828cf16cb01cb5fcb00cb00ccc970fb00046c8f2731ed44d0d30001f861fa0001f862d31f01f863d69f01f864fa4030f86501db3cd30030f861db3ce0218100cabae302218100cbba0d1a0a0b024e31ed44d0d30001f861fa0001f862d31f01f863d69f01f864fa4030f86501db3cd69f30f864db3c0d1a03688f2631ed44d0d30001f861fa0001f862d31f01f863d69f01f864fa4030f86501db3cd430fb04db3ce0018100ccbae3025bf2c0660d1a0c025eed44d0d30001f861fa0001f862d31f01f863d69f01f864fa4030f86501db3cfa403020fa4430c000f2e069f865db3c0d1a000ef845c705f2e06f0101b919020120101402015811120041b592fda89a1a60003f0c3f40003f0c5a63e03f0c7ad3e03f0c9f48061f0cbf0870047db72644180cb1d0c61024e21b679c04180cd1d0c61026591b679c041020193744302019575624302019775624302019975631d0c61029c41b679c10201917501818181301188e8581445cdb3ce0f2c0667018004dbfda376a268698000fc30fd0000fc31698f80fc31eb4f80fc327d20187c32fc20fc217c227c22c02d6f2ed44d0d30001f861fa0001f862d31f01f863d69f01f864fa4030f865f84401810208d71820c702f2d06bd43020f9004003db3c20c3ff8e218d0558da1958dad7d958d91cd857dcda59db985d1d5c9960fe1430fe2030f2c06c9130e2d0d31f018100c8bae30230f2c0661617008e01d3070120c21e92a6e19720c21a92a6e5e0e201d3ffd3ff301034f912c3ff935f0471e002c304935f0372e0c8cbffcbff71f90403c8cbffc9d08100a0d722c705c0009173e07f0396fa40fa00d31f3022fa4401c000f2e069f82814c705f2d07021c000f2d06af843bdf2d06d81445cdb3cf8425321a0b9f2d06af800f8425222a0a1f862f843a4f863db3cf80f71801001db3c181a1b014470c0ff948014f833948015f833e2d0db3c6c135db993135f03975aa101ab0fa8a0e2190058d307218100d1ba9c31d33fd33f5902f00c6c2113e0218100deba028100ddba12b196d33f01705202e07053000030f843f841c8cb00f842fa02cb1ff844cf16f845cf16c9ed5400668d04dcd95b9917dcda5b5c1b1957db595cdcd859d960fe14307002c8cb05542025745003cb02cb07cbff58fa02cb6ac901fb005bd61845" +} diff --git a/pkg/contracts/ton/gateway.go b/pkg/contracts/ton/gateway.go index 33ca2c7977..31f1fe1d6d 100644 --- a/pkg/contracts/ton/gateway.go +++ b/pkg/contracts/ton/gateway.go @@ -2,6 +2,8 @@ package ton import ( + "context" + "cosmossdk.io/math" "github.com/pkg/errors" "github.com/tonkeeper/tongo/boc" @@ -24,9 +26,16 @@ type Gateway struct { accountID ton.AccountID } +type MethodRunner interface { + RunSmcMethod(ctx context.Context, acc ton.AccountID, method string, params tlb.VmStack) (uint32, tlb.VmStack, error) +} + +type Filter func(*Transaction) bool + const ( sizeOpCode = 32 sizeQueryID = 64 + sizeSeqno = 32 ) var ( @@ -47,31 +56,16 @@ func (gw *Gateway) AccountID() ton.AccountID { // ParseTransaction parses transaction to Transaction func (gw *Gateway) ParseTransaction(tx ton.Transaction) (*Transaction, error) { - if !tx.IsSuccess() { - exitCode := tx.Description.TransOrd.ComputePh.TrPhaseComputeVm.Vm.ExitCode - return nil, errors.Wrapf(ErrParse, "tx %s is not successful (exit code %d)", tx.Hash().Hex(), exitCode) - } - - if tx.Msgs.InMsg.Exists { - inbound, err := gw.parseInbound(tx) - if err != nil { - return nil, errors.Wrapf(err, "unable to parse inbound tx %s", tx.Hash().Hex()) - } - - return inbound, nil - } - - outbound, err := gw.parseOutbound(tx) - if err != nil { - return nil, errors.Wrapf(err, "unable to parse outbound tx %s", tx.Hash().Hex()) + if isOutbound(tx) { + return gw.parseOutbound(tx) } - return outbound, nil + return gw.parseInbound(tx) } // ParseAndFilter parses transaction and applies filter to it. Returns (tx, skip?, error) // If parse fails due to known error, skip is set to true -func (gw *Gateway) ParseAndFilter(tx ton.Transaction, filter func(*Transaction) bool) (*Transaction, bool, error) { +func (gw *Gateway) ParseAndFilter(tx ton.Transaction, filter Filter) (*Transaction, bool, error) { parsedTX, err := gw.ParseTransaction(tx) switch { case errors.Is(err, ErrParse): @@ -83,166 +77,31 @@ func (gw *Gateway) ParseAndFilter(tx ton.Transaction, filter func(*Transaction) } if !filter(parsedTX) { - return nil, true, nil + return parsedTX, true, nil } return parsedTX, false, nil } -// FilterInbounds filters transactions with deposit operations -func FilterInbounds(tx *Transaction) bool { return tx.IsInbound() } - -func (gw *Gateway) parseInbound(tx ton.Transaction) (*Transaction, error) { - body, err := parseInternalMessageBody(tx) - if err != nil { - return nil, errors.Wrap(err, "unable to parse body") - } - - intMsgInfo := tx.Msgs.InMsg.Value.Value.Info.IntMsgInfo - if intMsgInfo == nil { - return nil, errors.Wrap(ErrParse, "no internal message info") - } - - sourceID, err := ton.AccountIDFromTlb(intMsgInfo.Src) - if err != nil { - return nil, errors.Wrap(err, "unable to parse source account") - } - - destinationID, err := ton.AccountIDFromTlb(intMsgInfo.Dest) - if err != nil { - return nil, errors.Wrap(err, "unable to parse destination account") - } - - if gw.accountID != *destinationID { - return nil, errors.Wrap(ErrParse, "destination account is not gateway") - } - - op, err := body.ReadUint(sizeOpCode) - if err != nil { - return nil, errors.Wrap(err, "unable to read op code") - } - - var ( - sender = *sourceID - opCode = Op(op) - - content any - errContent error - ) - - switch opCode { - case OpDonate: - amount := intMsgInfo.Value.Grams - tx.TotalFees.Grams - content = Donation{Sender: sender, Amount: GramsToUint(amount)} - case OpDeposit: - content, errContent = parseDeposit(tx, sender, body) - case OpDepositAndCall: - content, errContent = parseDepositAndCall(tx, sender, body) - default: - // #nosec G115 always in range - return nil, errors.Wrapf(ErrUnknownOp, "op code %d", int64(op)) - } - - if errContent != nil { - // #nosec G115 always in range - return nil, errors.Wrapf(ErrParse, "unable to parse content for op code %d: %s", int64(op), errContent.Error()) - } - - return &Transaction{ - Transaction: tx, - Operation: opCode, - - content: content, - inbound: true, - }, nil -} - -func parseDeposit(tx ton.Transaction, sender ton.AccountID, body *boc.Cell) (Deposit, error) { - // skip query id - if err := body.Skip(sizeQueryID); err != nil { - return Deposit{}, err - } +// ParseAndFilterMany parses and filters many txs. +func (gw *Gateway) ParseAndFilterMany(txs []ton.Transaction, filter Filter) []*Transaction { + //goland:noinspection GoPreferNilSlice + out := []*Transaction{} - recipient, err := UnmarshalEVMAddress(body) - if err != nil { - return Deposit{}, errors.Wrap(err, "unable to read recipient") - } - - dl, err := parseDepositLog(tx) - if err != nil { - return Deposit{}, errors.Wrap(err, "unable to parse deposit log") - } - - return Deposit{ - Sender: sender, - Amount: dl.Amount, - Recipient: recipient, - }, nil -} - -type depositLog struct { - Amount math.Uint -} - -func parseDepositLog(tx ton.Transaction) (depositLog, error) { - messages := tx.Msgs.OutMsgs.Values() - if len(messages) == 0 { - return depositLog{}, errors.Wrap(ErrParse, "no out messages") - } - - // stored as ref - // cell log = begin_cell() - // .store_uint(op::internal::deposit, size::op_code_size) - // .store_uint(0, size::query_id_size) - // .store_slice(sender) - // .store_coins(deposit_amount) - // .store_uint(evm_recipient, size::evm_address) - // .end_cell(); - - var ( - bodyValue = boc.Cell(messages[0].Value.Body.Value) - body = &bodyValue - ) - - if err := body.Skip(sizeOpCode + sizeQueryID); err != nil { - return depositLog{}, errors.Wrap(err, "unable to skip bits") - } - - // skip msg address (ton sender) - if err := UnmarshalTLB(&tlb.MsgAddress{}, body); err != nil { - return depositLog{}, errors.Wrap(err, "unable to read sender address") - } - - var deposited tlb.Grams - if err := UnmarshalTLB(&deposited, body); err != nil { - return depositLog{}, errors.Wrap(err, "unable to read deposited amount") - } - - return depositLog{Amount: GramsToUint(deposited)}, nil -} - -func parseDepositAndCall(tx ton.Transaction, sender ton.AccountID, body *boc.Cell) (DepositAndCall, error) { - deposit, err := parseDeposit(tx, sender, body) - if err != nil { - return DepositAndCall{}, err - } - - callDataCell, err := body.NextRef() - if err != nil { - return DepositAndCall{}, errors.Wrap(err, "unable to read call data cell") - } + for i := range txs { + tx, skip, err := gw.ParseAndFilter(txs[i], filter) + if skip || err != nil { + continue + } - callData, err := UnmarshalSnakeCell(callDataCell) - if err != nil { - return DepositAndCall{}, errors.Wrap(err, "unable to unmarshal call data") + out = append(out, tx) } - return DepositAndCall{Deposit: deposit, CallData: callData}, nil + return out } -func (gw *Gateway) parseOutbound(_ ton.Transaction) (*Transaction, error) { - return nil, errors.New("not implemented") -} +// FilterInbounds filters transactions with deposit operations +func FilterInbounds(tx *Transaction) bool { return tx.IsInbound() } func parseInternalMessageBody(tx ton.Transaction) (*boc.Cell, error) { if !tx.Msgs.InMsg.Exists { @@ -256,3 +115,33 @@ func parseInternalMessageBody(tx ton.Transaction) (*boc.Cell, error) { return &body, nil } + +var zero = math.NewUint(0) + +// GetTxFee returns maximum transaction fee for the given operation. +// Real fee may be lower. +func (gw *Gateway) GetTxFee(ctx context.Context, client MethodRunner, op Op) (math.Uint, error) { + const ( + method = "calculate_gas_fee" + sumType = "VmStkTinyInt" + ) + + query := tlb.VmStack{{SumType: sumType, VmStkTinyInt: int64(op)}} + + exitCode, res, err := client.RunSmcMethod(ctx, gw.accountID, method, query) + switch { + case err != nil: + return zero, err + case exitCode != 0: + return zero, errors.Errorf("calculate_gas_fee failed with exit code %d", exitCode) + case len(res) == 0: + return zero, errors.New("empty result") + case res[0].SumType != sumType: + return zero, errors.Errorf("res is not %s (got %s)", sumType, res[0].SumType) + case res[0].VmStkTinyInt <= 0: + return zero, errors.New("fee is zero or negative") + } + + // #nosec G115 positive + return math.NewUint(uint64(res[0].VmStkTinyInt)), nil +} diff --git a/pkg/contracts/ton/gateway_deploy.go b/pkg/contracts/ton/gateway_deploy.go new file mode 100644 index 0000000000..33ec6a422b --- /dev/null +++ b/pkg/contracts/ton/gateway_deploy.go @@ -0,0 +1,74 @@ +package ton + +import ( + _ "embed" + "encoding/json" + + eth "github.com/ethereum/go-ethereum/common" + "github.com/pkg/errors" + "github.com/tonkeeper/tongo/boc" + "github.com/tonkeeper/tongo/tlb" + "github.com/tonkeeper/tongo/ton" +) + +//go:embed gateway.compiled.json +var gatewayCode []byte + +// GatewayCode returns Gateway's code as cell +func GatewayCode() *boc.Cell { + c, err := getGatewayCode() + if err != nil { + panic(err) + } + + return c +} + +// GatewayStateInit returns Gateway's stateInit as cell +func GatewayStateInit(authority ton.AccountID, tss eth.Address, depositsEnabled bool) *boc.Cell { + c, err := buildGatewayStateInit(authority, tss, depositsEnabled) + if err != nil { + panic(err) + } + + return c +} + +func getGatewayCode() (*boc.Cell, error) { + var code struct { + Hex string `json:"hex"` + } + + if err := json.Unmarshal(gatewayCode, &code); err != nil { + return nil, errors.Wrap(err, "unable to unmarshal TON Gateway code") + } + + cells, err := boc.DeserializeBocHex(code.Hex) + if err != nil { + return nil, errors.Wrap(err, "unable to deserialize TON Gateway code") + } + + if len(cells) != 1 { + return nil, errors.New("invalid cells count") + } + + return cells[0], nil +} + +func buildGatewayStateInit(authority ton.AccountID, tss eth.Address, depositsEnabled bool) (*boc.Cell, error) { + cell := boc.NewCell() + + err := ErrCollect( + cell.WriteBit(depositsEnabled), // deposits_enabled + tlb.Marshal(cell, tlb.Coins(0)), // total_locked + cell.WriteUint(0, 32), // seqno + cell.WriteBytes(tss.Bytes()), // tss_address + tlb.Marshal(cell, authority.ToMsgAddress()), // authority_address (TON) + ) + + if err != nil { + return nil, errors.Wrap(err, "unable to write TON Gateway state cell") + } + + return cell, nil +} diff --git a/pkg/contracts/ton/gateway_msg.go b/pkg/contracts/ton/gateway_msg.go new file mode 100644 index 0000000000..3a4716e0b4 --- /dev/null +++ b/pkg/contracts/ton/gateway_msg.go @@ -0,0 +1,224 @@ +package ton + +import ( + "errors" + + "cosmossdk.io/math" + eth "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/tonkeeper/tongo/boc" + "github.com/tonkeeper/tongo/tlb" + "github.com/tonkeeper/tongo/ton" +) + +// Op operation code +type Op uint32 + +// github.com/zeta-chain/protocol-contracts-ton/blob/main/contracts/gateway.fc +// Inbound operations +const ( + OpDonate Op = 100 + iota + OpDeposit + OpDepositAndCall +) + +const OpWithdraw Op = 200 + +// ExitCode represents an error code. Might be TVM or custom. +// TVM: https://docs.ton.org/v3/documentation/tvm/tvm-exit-codes +// Zeta: https://github.com/zeta-chain/protocol-contracts-ton/blob/main/contracts/common/errors.fc +type ExitCode uint32 + +const ( + ExitCodeInvalidSeqno ExitCode = 109 +) + +// Donation represents a donation operation +type Donation struct { + Sender ton.AccountID + Amount math.Uint +} + +// AsBody casts struct as internal message body. +func (d Donation) AsBody() (*boc.Cell, error) { + b := boc.NewCell() + err := ErrCollect( + b.WriteUint(uint64(OpDonate), sizeOpCode), + b.WriteUint(0, sizeQueryID), + ) + + return b, err +} + +// Deposit represents a deposit operation +type Deposit struct { + Sender ton.AccountID + Amount math.Uint + Recipient eth.Address +} + +// Memo casts deposit to memo bytes +func (d Deposit) Memo() []byte { + return d.Recipient.Bytes() +} + +// AsBody casts struct as internal message body. +func (d Deposit) AsBody() (*boc.Cell, error) { + b := boc.NewCell() + + return b, writeDepositBody(b, d.Recipient) +} + +// DepositAndCall represents a deposit and call operation +type DepositAndCall struct { + Deposit + CallData []byte +} + +// Memo casts deposit to call to memo bytes +func (d DepositAndCall) Memo() []byte { + recipient := d.Recipient.Bytes() + out := make([]byte, 0, len(recipient)+len(d.CallData)) + + out = append(out, recipient...) + out = append(out, d.CallData...) + + return out +} + +// AsBody casts struct to internal message body. +func (d DepositAndCall) AsBody() (*boc.Cell, error) { + b := boc.NewCell() + + return b, writeDepositAndCallBody(b, d.Recipient, d.CallData) +} + +func writeDepositBody(b *boc.Cell, recipient eth.Address) error { + return ErrCollect( + b.WriteUint(uint64(OpDeposit), sizeOpCode), + b.WriteUint(0, sizeQueryID), + b.WriteBytes(recipient.Bytes()), + ) +} + +func writeDepositAndCallBody(b *boc.Cell, recipient eth.Address, callData []byte) error { + if len(callData) == 0 { + return errors.New("call data is empty") + } + + callDataCell, err := MarshalSnakeCell(callData) + if err != nil { + return err + } + + return ErrCollect( + b.WriteUint(uint64(OpDepositAndCall), sizeOpCode), + b.WriteUint(0, sizeQueryID), + b.WriteBytes(recipient.Bytes()), + b.AddRef(callDataCell), + ) +} + +// Withdrawal represents a withdrawal external message +type Withdrawal struct { + Recipient ton.AccountID + Amount math.Uint + Seqno uint32 + Sig [65]byte +} + +func (w *Withdrawal) emptySig() bool { + return w.Sig == [65]byte{} +} + +// Hash returns hash of the withdrawal message. (used for signing) +func (w *Withdrawal) Hash() ([32]byte, error) { + payload, err := w.payload() + if err != nil { + return [32]byte{}, err + } + + return payload.Hash256() +} + +// SetSignature sets signature to the withdrawal message. +// Note that signature has the following order: [R, S, V (recovery ID)] +func (w *Withdrawal) SetSignature(sig [65]byte) { + copy(w.Sig[:], sig[:]) +} + +// Signer returns EVM address of the signer (e.g. TSS) +func (w *Withdrawal) Signer() (eth.Address, error) { + hash, err := w.Hash() + if err != nil { + return eth.Address{}, err + } + + var sig [65]byte + copy(sig[:], w.Sig[:]) + + // recovery id + // https://bitcoin.stackexchange.com/questions/38351/ecdsa-v-r-s-what-is-v + if sig[64] >= 27 { + sig[64] -= 27 + } + + pub, err := crypto.SigToPub(hash[:], sig[:]) + if err != nil { + return eth.Address{}, err + } + + return crypto.PubkeyToAddress(*pub), nil +} + +func (w *Withdrawal) AsBody() (*boc.Cell, error) { + payload, err := w.payload() + if err != nil { + return nil, err + } + + var ( + body = boc.NewCell() + v, r, s = splitSignature(w.Sig) + ) + + // note that in TVM, the order of signature is different (v, r, s) + err = ErrCollect( + body.WriteUint(uint64(v), 8), + body.WriteBytes(r[:]), + body.WriteBytes(s[:]), + body.AddRef(payload), + ) + if err != nil { + return nil, err + } + + return body, nil +} + +func (w *Withdrawal) payload() (*boc.Cell, error) { + payload := boc.NewCell() + + err := ErrCollect( + payload.WriteUint(uint64(OpWithdraw), sizeOpCode), + tlb.Marshal(payload, w.Recipient.ToMsgAddress()), + tlb.Marshal(payload, tlb.Coins(w.Amount.Uint64())), + payload.WriteUint(uint64(w.Seqno), sizeSeqno), + ) + + if err != nil { + return nil, errors.New("unable to marshal payload as cell") + } + + return payload, nil +} + +// Ton Virtual Machine (TVM) uses different order of signature params (v,r,s) instead of (r,s,v); +// Let's split them as required. +func splitSignature(sig [65]byte) (v byte, r [32]byte, s [32]byte) { + copy(r[:], sig[:32]) + copy(s[:], sig[32:64]) + v = sig[64] + + return v, r, s +} diff --git a/pkg/contracts/ton/gateway_op.go b/pkg/contracts/ton/gateway_op.go deleted file mode 100644 index 7d711ab89c..0000000000 --- a/pkg/contracts/ton/gateway_op.go +++ /dev/null @@ -1,115 +0,0 @@ -package ton - -import ( - "errors" - - "cosmossdk.io/math" - eth "github.com/ethereum/go-ethereum/common" - "github.com/tonkeeper/tongo/boc" - "github.com/tonkeeper/tongo/ton" -) - -// Op operation code -type Op uint32 - -// github.com/zeta-chain/protocol-contracts-ton/blob/main/contracts/gateway.fc -// Inbound operations -const ( - OpDonate Op = 100 + iota - OpDeposit - OpDepositAndCall -) - -// Outbound operations -const ( - OpWithdraw Op = 200 + iota - SetDepositsEnabled - UpdateTSS - UpdateCode -) - -// Donation represents a donation operation -type Donation struct { - Sender ton.AccountID - Amount math.Uint -} - -// AsBody casts struct as internal message body. -func (d Donation) AsBody() (*boc.Cell, error) { - b := boc.NewCell() - err := ErrCollect( - b.WriteUint(uint64(OpDonate), sizeOpCode), - b.WriteUint(0, sizeQueryID), - ) - - return b, err -} - -// Deposit represents a deposit operation -type Deposit struct { - Sender ton.AccountID - Amount math.Uint - Recipient eth.Address -} - -// Memo casts deposit to memo bytes -func (d Deposit) Memo() []byte { - return d.Recipient.Bytes() -} - -// AsBody casts struct as internal message body. -func (d Deposit) AsBody() (*boc.Cell, error) { - b := boc.NewCell() - - return b, writeDepositBody(b, d.Recipient) -} - -// DepositAndCall represents a deposit and call operation -type DepositAndCall struct { - Deposit - CallData []byte -} - -// Memo casts deposit to call to memo bytes -func (d DepositAndCall) Memo() []byte { - recipient := d.Recipient.Bytes() - out := make([]byte, 0, len(recipient)+len(d.CallData)) - - out = append(out, recipient...) - out = append(out, d.CallData...) - - return out -} - -// AsBody casts struct to internal message body. -func (d DepositAndCall) AsBody() (*boc.Cell, error) { - b := boc.NewCell() - - return b, writeDepositAndCallBody(b, d.Recipient, d.CallData) -} - -func writeDepositBody(b *boc.Cell, recipient eth.Address) error { - return ErrCollect( - b.WriteUint(uint64(OpDeposit), sizeOpCode), - b.WriteUint(0, sizeQueryID), - b.WriteBytes(recipient.Bytes()), - ) -} - -func writeDepositAndCallBody(b *boc.Cell, recipient eth.Address, callData []byte) error { - if len(callData) == 0 { - return errors.New("call data is empty") - } - - callDataCell, err := MarshalSnakeCell(callData) - if err != nil { - return err - } - - return ErrCollect( - b.WriteUint(uint64(OpDepositAndCall), sizeOpCode), - b.WriteUint(0, sizeQueryID), - b.WriteBytes(recipient.Bytes()), - b.AddRef(callDataCell), - ) -} diff --git a/pkg/contracts/ton/gateway_parse.go b/pkg/contracts/ton/gateway_parse.go new file mode 100644 index 0000000000..4d6206f1ed --- /dev/null +++ b/pkg/contracts/ton/gateway_parse.go @@ -0,0 +1,293 @@ +package ton + +import ( + "cosmossdk.io/math" + "github.com/pkg/errors" + "github.com/tonkeeper/tongo/boc" + "github.com/tonkeeper/tongo/tlb" + "github.com/tonkeeper/tongo/ton" +) + +func (gw *Gateway) parseInbound(tx ton.Transaction) (*Transaction, error) { + body, err := parseInternalMessageBody(tx) + if err != nil { + return nil, errors.Wrap(err, "unable to parse body") + } + + intMsgInfo := tx.Msgs.InMsg.Value.Value.Info.IntMsgInfo + if intMsgInfo == nil { + return nil, errors.Wrap(ErrParse, "no internal message info") + } + + sourceID, err := ton.AccountIDFromTlb(intMsgInfo.Src) + if err != nil { + return nil, errors.Wrap(err, "unable to parse source account") + } + + destinationID, err := ton.AccountIDFromTlb(intMsgInfo.Dest) + if err != nil { + return nil, errors.Wrap(err, "unable to parse destination account") + } + + if gw.accountID != *destinationID { + return nil, errors.Wrap(ErrParse, "destination account is not gateway") + } + + op, err := body.ReadUint(sizeOpCode) + if err != nil { + return nil, errParse(err, "unable to read op code") + } + + var ( + // #nosec G115 always in range + opCode = Op(op) + sender = *sourceID + + content any + errContent error + ) + + switch opCode { + case OpDonate: + amount := intMsgInfo.Value.Grams - tx.TotalFees.Grams + content = Donation{Sender: sender, Amount: GramsToUint(amount)} + case OpDeposit: + content, errContent = parseDeposit(tx, sender, body) + case OpDepositAndCall: + content, errContent = parseDepositAndCall(tx, sender, body) + default: + // #nosec G115 always in range + return nil, errors.Wrapf(ErrUnknownOp, "op code %d", int64(op)) + } + + if errContent != nil { + // #nosec G115 always in range + return nil, errors.Wrapf(ErrParse, "unable to parse content for op code %d: %s", int64(op), errContent.Error()) + } + + return &Transaction{ + Transaction: tx, + Operation: opCode, + ExitCode: exitCodeFromTx(tx), + + content: content, + inbound: true, + }, nil +} + +func parseDeposit(tx ton.Transaction, sender ton.AccountID, body *boc.Cell) (Deposit, error) { + // skip query id + if err := body.Skip(sizeQueryID); err != nil { + return Deposit{}, err + } + + recipient, err := UnmarshalEVMAddress(body) + if err != nil { + return Deposit{}, errors.Wrap(err, "unable to read recipient") + } + + dl, err := parseDepositLog(tx) + if err != nil { + return Deposit{}, errors.Wrap(err, "unable to parse deposit log") + } + + return Deposit{ + Sender: sender, + Amount: dl.Amount, + Recipient: recipient, + }, nil +} + +type depositLog struct { + Amount math.Uint + DepositFee math.Uint +} + +func parseDepositLog(tx ton.Transaction) (depositLog, error) { + messages := tx.Msgs.OutMsgs.Values() + if len(messages) == 0 { + return depositLog{}, errors.Wrap(ErrParse, "no out messages") + } + + // stored as ref + // cell log = begin_cell() + // .store_coins(deposit_amount) + // .store_coins(tx_fee) + // .end_cell(); + + var ( + bodyValue = boc.Cell(messages[0].Value.Body.Value) + body = &bodyValue + ) + + var deposited tlb.Grams + if err := UnmarshalTLB(&deposited, body); err != nil { + return depositLog{}, errors.Wrap(err, "unable to read deposited amount") + } + + var depositFee tlb.Grams + if err := UnmarshalTLB(&depositFee, body); err != nil { + return depositLog{}, errors.Wrap(err, "unable to read deposit fee") + } + + return depositLog{ + Amount: GramsToUint(deposited), + DepositFee: GramsToUint(depositFee), + }, nil +} + +func parseDepositAndCall(tx ton.Transaction, sender ton.AccountID, body *boc.Cell) (DepositAndCall, error) { + deposit, err := parseDeposit(tx, sender, body) + if err != nil { + return DepositAndCall{}, err + } + + callDataCell, err := body.NextRef() + if err != nil { + return DepositAndCall{}, errors.Wrap(err, "unable to read call data cell") + } + + callData, err := UnmarshalSnakeCell(callDataCell) + if err != nil { + return DepositAndCall{}, errors.Wrap(err, "unable to unmarshal call data") + } + + return DepositAndCall{Deposit: deposit, CallData: callData}, nil +} + +// an outbound is a tx that was initiated by TSS signature with external message +func isOutbound(tx ton.Transaction) bool { + return tx.Msgs.InMsg.Exists && + tx.Msgs.InMsg.Value.Value.Info.SumType == "ExtInMsgInfo" +} + +func (gw *Gateway) parseOutbound(tx ton.Transaction) (*Transaction, error) { + if !isOutbound(tx) { + return nil, errors.Wrap(ErrParse, "not an outbound transaction") + } + + extMsgBodyCell := boc.Cell(tx.Msgs.InMsg.Value.Value.Body.Value) + + sig, payload, err := parseExternalMessage(&extMsgBodyCell) + if err != nil { + return nil, errParse(err, "unable to parse external message") + } + + op, err := payload.ReadUint(sizeOpCode) + if err != nil { + return nil, errParse(err, "unable to read op code") + } + + // #nosec G115 always in range + opCode := Op(op) + + if opCode != OpWithdraw { + return nil, errors.Wrapf(ErrUnknownOp, "op code %d", op) + } + + withdrawal, err := parseWithdrawal(tx, sig, payload) + if err != nil { + return nil, errParse(err, "unable to parse withdrawal") + } + + return &Transaction{ + Transaction: tx, + Operation: opCode, + ExitCode: exitCodeFromTx(tx), + content: withdrawal, + }, nil +} + +// external message is essentially a cell with 65 bytes of ECDSA sig + cell_ref to payload +func parseExternalMessage(b *boc.Cell) ([65]byte, *boc.Cell, error) { + sig, err := b.ReadBytes(65) + if err != nil { + return [65]byte{}, nil, err + } + + var sigArray [65]byte + copy(sigArray[:], sig) + + payload, err := b.NextRef() + + return sigArray, payload, err +} + +func parseWithdrawal(tx ton.Transaction, sig [65]byte, payload *boc.Cell) (Withdrawal, error) { + // Note that ECDSA sig has the following order: (v, r, s) but in EVM we have (r, s, v) + var sigFlipped [65]byte + + copy(sigFlipped[:64], sig[1:]) + sigFlipped[64] = sig[0] + + var ( + recipient tlb.MsgAddress + amount tlb.Coins + seqno uint32 + ) + + err := ErrCollect( + tlb.Unmarshal(payload, &recipient), + tlb.Unmarshal(payload, &amount), + tlb.Unmarshal(payload, &seqno), + ) + if err != nil { + return Withdrawal{}, errors.Wrap(err, "unable to unmarshal payload") + } + + recipientAddr, err := parseAccount(recipient) + if err != nil { + return Withdrawal{}, errors.Wrap(err, "unable to parse recipient from payload") + } + + // ensure a single outgoing message for the withdrawal + if tx.OutMsgCnt != 1 { + return Withdrawal{}, errors.Wrap(ErrParse, "invalid out messages count") + } + + // tlb.Message{} + outMsg := tx.Msgs.OutMsgs.Values()[0].Value + if outMsg.Info.SumType != "IntMsgInfo" || outMsg.Info.IntMsgInfo == nil { + return Withdrawal{}, errors.Wrap(ErrParse, "invalid out message") + } + + msgRecipientAddr, err := parseAccount(outMsg.Info.IntMsgInfo.Dest) + + switch { + case err != nil: + return Withdrawal{}, errors.Wrap(err, "unable to parse recipient from out msg") + case recipientAddr != msgRecipientAddr: + // should not happen + return Withdrawal{}, errors.Wrap(ErrParse, "recipient mismatch") + case amount != outMsg.Info.IntMsgInfo.Value.Grams: + // should not happen + return Withdrawal{}, errors.Wrap(ErrParse, "amount mismatch") + } + + return Withdrawal{ + Recipient: recipientAddr, + Amount: math.NewUint(uint64(amount)), + Seqno: seqno, + Sig: sigFlipped, + }, nil +} + +func parseAccount(raw tlb.MsgAddress) (ton.AccountID, error) { + if raw.SumType != "AddrStd" { + return ton.AccountID{}, errors.Wrapf(ErrParse, "invalid address type %s", raw.SumType) + } + + return ton.AccountID{ + // #nosec G115 always in range + Workchain: int32(raw.AddrStd.WorkchainId), + Address: raw.AddrStd.Address, + }, nil +} + +func errParse(err error, msg string) error { + return errors.Wrapf(ErrParse, "%s (%s)", msg, err.Error()) +} + +func exitCodeFromTx(tx ton.Transaction) int32 { + return tx.Description.TransOrd.ComputePh.TrPhaseComputeVm.Vm.ExitCode +} diff --git a/pkg/contracts/ton/gateway_send.go b/pkg/contracts/ton/gateway_send.go index 5dd9c21340..a17bf9ef26 100644 --- a/pkg/contracts/ton/gateway_send.go +++ b/pkg/contracts/ton/gateway_send.go @@ -8,14 +8,25 @@ import ( "github.com/pkg/errors" "github.com/tonkeeper/tongo/boc" "github.com/tonkeeper/tongo/tlb" + "github.com/tonkeeper/tongo/ton" "github.com/tonkeeper/tongo/wallet" ) -// Sender TON tx sender. +// Sender TON tx sender. Usually an interface to a wallet. type Sender interface { Send(ctx context.Context, messages ...wallet.Sendable) error } +// Client represents a sender that allows sending an arbitrary external message to the network. +type Client interface { + SendMessage(ctx context.Context, payload []byte) (uint32, error) +} + +type ExternalMsg interface { + emptySig() bool + AsBody() (*boc.Cell, error) +} + // see https://docs.ton.org/develop/smart-contracts/messages#message-modes const ( SendFlagSeparateFees = uint8(1) @@ -70,3 +81,38 @@ func (gw *Gateway) send(ctx context.Context, s Sender, amount math.Uint, body *b Mode: sendMode, }) } + +// SendExternalMessage sends an external message to the Gateway. +func (gw *Gateway) SendExternalMessage(ctx context.Context, s Client, msg ExternalMsg) (uint32, error) { + return sendExternalMessage(ctx, s, gw.accountID, msg) +} + +// inspired by tongo's wallet.Wallet{}.RawSendV2() +func sendExternalMessage(ctx context.Context, via Client, to ton.AccountID, msg ExternalMsg) (uint32, error) { + if msg.emptySig() { + return 0, errors.New("empty signature") + } + + body, err := msg.AsBody() + if err != nil { + return 0, errors.Wrap(err, "unable to encode msg as cell") + } + + extMsg, err := ton.CreateExternalMessage(to, body, nil, tlb.VarUInteger16{}) + if err != nil { + return 0, errors.Wrap(err, "unable to create external message") + } + + extMsgCell := boc.NewCell() + err = tlb.Marshal(extMsgCell, extMsg) + if err != nil { + return 0, errors.Wrap(err, "can not marshal wallet external message") + } + + payload, err := extMsgCell.ToBocCustom(false, false, false, 0) + if err != nil { + return 0, errors.Wrap(err, "can not serialize external message cell") + } + + return via.SendMessage(ctx, payload) +} diff --git a/pkg/contracts/ton/gateway_test.go b/pkg/contracts/ton/gateway_test.go index dc680761ff..6698147e7c 100644 --- a/pkg/contracts/ton/gateway_test.go +++ b/pkg/contracts/ton/gateway_test.go @@ -1,12 +1,17 @@ package ton import ( + "crypto/ecdsa" "embed" + "encoding/hex" "encoding/json" "fmt" + "math/big" "strings" "testing" + "cosmossdk.io/math" + "github.com/ethereum/go-ethereum/crypto" "github.com/samber/lo" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -16,7 +21,8 @@ import ( ) func TestParsing(t *testing.T) { - swapBodyAndParse := func(gw *Gateway, tx ton.Transaction, body *boc.Cell) *Transaction { + // small helpers that allows to alter inMsg body and parse it again + alterBodyAndParse := func(gw *Gateway, tx ton.Transaction, body *boc.Cell) *Transaction { tx.Msgs.InMsg.Value.Value.Body.Value = tlb.Any(*body) parsed, err := gw.ParseTransaction(tx) @@ -44,7 +50,7 @@ func TestParsing(t *testing.T) { const ( expectedSender = "0:9594c719ec4c95f66683b2fb1ca0b09de4a41f6fb087ba4c8d265b96a4cce50f" - expectedDonation = 1_499_432_947 // 1.49... TON + expectedDonation = 599_509_877 // ~0.6 TON ) donation, err := parsedTX.Donation() @@ -54,7 +60,7 @@ func TestParsing(t *testing.T) { // Check that AsBody works var ( - parsedTX2 = swapBodyAndParse(gw, tx, lo.Must(donation.AsBody())) + parsedTX2 = alterBodyAndParse(gw, tx, lo.Must(donation.AsBody())) donation2 = lo.Must(parsedTX2.Donation()) ) @@ -77,6 +83,7 @@ func TestParsing(t *testing.T) { // Check tx props assert.Equal(t, int(OpDeposit), int(parsedTX.Operation)) + assert.Zero(t, parsedTX.ExitCode) // Check deposit deposit, err := parsedTX.Deposit() @@ -84,13 +91,13 @@ func TestParsing(t *testing.T) { const ( expectedSender = "0:9594c719ec4c95f66683b2fb1ca0b09de4a41f6fb087ba4c8d265b96a4cce50f" - vitalikDotETH = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" - expectedDeposit = 990_000_000 // 0.99 TON + zevmRecipient = "0xA1eb8D65b765D259E7520B791bc4783AdeFDd998" + expectedDeposit = 996_000_000 // 0.996 TON ) assert.Equal(t, expectedSender, deposit.Sender.ToRaw()) assert.Equal(t, expectedDeposit, int(deposit.Amount.Uint64())) - assert.Equal(t, vitalikDotETH, deposit.Recipient.Hex()) + assert.Equal(t, zevmRecipient, deposit.Recipient.Hex()) // Check that other casting fails _, err = parsedTX.Donation() @@ -98,7 +105,7 @@ func TestParsing(t *testing.T) { // Check that AsBody works var ( - parsedTX2 = swapBodyAndParse(gw, tx, lo.Must(deposit.AsBody())) + parsedTX2 = alterBodyAndParse(gw, tx, lo.Must(deposit.AsBody())) deposit2 = lo.Must(parsedTX2.Deposit()) ) @@ -127,25 +134,73 @@ func TestParsing(t *testing.T) { assert.NoError(t, err) const ( - expectedSender = "0:9594c719ec4c95f66683b2fb1ca0b09de4a41f6fb087ba4c8d265b96a4cce50f" - vitalikDotETH = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" - expectedDeposit = 490_000_000 // 0.49 TON + expectedSender = "0:9594c719ec4c95f66683b2fb1ca0b09de4a41f6fb087ba4c8d265b96a4cce50f" + zevmRecipient = "0xA1eb8D65b765D259E7520B791bc4783AdeFDd998" + expectedDeposit = 394_800_000 // 0.4 TON - tx fee + expectedCallData = `These "NFTs" - are they in the room with us right now?` ) - expectedCallData := readFixtureFile(t, "testdata/long-call-data.txt") - assert.Equal(t, expectedSender, depositAndCall.Sender.ToRaw()) assert.Equal(t, expectedDeposit, int(depositAndCall.Amount.Uint64())) - assert.Equal(t, vitalikDotETH, depositAndCall.Recipient.Hex()) - assert.Equal(t, expectedCallData, depositAndCall.CallData) + assert.Equal(t, zevmRecipient, depositAndCall.Recipient.Hex()) + assert.Equal(t, []byte(expectedCallData), depositAndCall.CallData) - // Check that AsBody works - var ( - parsedTX2 = swapBodyAndParse(gw, tx, lo.Must(depositAndCall.AsBody())) - depositAndCall2 = lo.Must(parsedTX2.DepositAndCall()) - ) + t.Run("Long call data", func(t *testing.T) { + // ARRANGE + longCallData := readFixtureFile(t, "testdata/long-call-data.txt") + depositAndCall.CallData = longCallData + + // ACT + parsedTX = alterBodyAndParse(gw, tx, lo.Must(depositAndCall.AsBody())) + + depositAndCall2, err := parsedTX.DepositAndCall() + + // ASSERT + require.NoError(t, err) + + assert.Equal(t, longCallData, depositAndCall2.CallData) + }) + + }) + + t.Run("Withdrawal", func(t *testing.T) { + // ARRANGE + // Given a tx + tx, fx := getFixtureTX(t, "06-withdrawal") + + // Given a gateway contract + gw := NewGateway(ton.MustParseAccountID(fx.Account)) + + // ACT + parsedTX, err := gw.ParseTransaction(tx) + + // ASSERT + require.NoError(t, err) + assert.Equal(t, OpWithdraw, parsedTX.Operation) + assert.Zero(t, parsedTX.ExitCode) + + // 0.01 TON + const expectedAmount = 10_000_000 + + actualAmount := parsedTX.Msgs.OutMsgs.Values()[0].Value.Info.IntMsgInfo.Value.Grams + assert.Equal(t, tlb.Coins(expectedAmount), actualAmount) + + // Check withdrawal + w, err := parsedTX.Withdrawal() + require.NoError(t, err) + + expectedRecipient := ton.MustParseAccountID("0QDQ51yWafHKgOjMF4ZwOfGsxnQ2yx_InZQG1NGwMfs2y62E") + + assert.Equal(t, expectedRecipient, w.Recipient) + assert.Equal(t, uint32(1), w.Seqno) + assert.Equal(t, math.NewUint(expectedAmount), w.Amount) - assert.Equal(t, depositAndCall, depositAndCall2) + // Expect TSS signer address + signer, err := w.Signer() + require.NoError(t, err) + + const expectedTSS = "0xFA033cebd2EB4A800F74d70C10dfc8710fF0d148" + assert.Equal(t, expectedTSS, signer.Hex()) }) t.Run("Irrelevant tx", func(t *testing.T) { @@ -160,14 +215,14 @@ func TestParsing(t *testing.T) { // ACT _, err := gw.ParseTransaction(tx) - assert.ErrorIs(t, err, ErrParse) + assert.ErrorIs(t, err, ErrUnknownOp) // 102 is 'unknown op' // https://github.com/zeta-chain/protocol-contracts-ton/blob/main/contracts/common/errors.fc - assert.ErrorContains(t, err, "is not successful (exit code 102)") + assert.ErrorContains(t, err, "unknown op") }) - t.Run("not a deposit nor withdrawal", func(t *testing.T) { + t.Run("not a deposit nor a withdrawal", func(t *testing.T) { // actually, it's a bounce of the previous tx // ARRANGE @@ -184,6 +239,72 @@ func TestParsing(t *testing.T) { }) } +func TestWithdrawal(t *testing.T) { + // FYI: asserts are checks with protocol-contracts-ton tests (Typescript lib) + + // ARRANGE + // Given a withdrawal msg + withdrawal := &Withdrawal{ + Recipient: ton.MustParseAccountID("0:552f6db5da0cae7f0b3ab4ab58d85927f6beb962cda426a6a6ee751c82cead1f"), + Amount: Coins(5), + Seqno: 2, + } + + const expectedHash = "e8eddf26276c747bd14d5161d18bc235c5c1a050187ab468996572d34e2f8f30" + + // ACT + hash, err := withdrawal.Hash() + + // ASSERT + require.NoError(t, err) + require.Equal(t, expectedHash, fmt.Sprintf("%x", hash[:])) + + t.Run("Sign", func(t *testing.T) { + // ARRANGE + // Given a sample EVM wallet (simulates TSS) + privateKey := evmWallet(t, "0xb984cd65727cfd03081fc7bf33bf5c208bca697ce16139b5ded275887e81395a") + + // Given ECDSA signature ... + sig, err := crypto.Sign(hash[:], privateKey) + require.NoError(t, err) + + var sigArray [65]byte + copy(sigArray[:], sig) + + // That is set in withdrawal + withdrawal.SetSignature(sigArray) + + // ACT + // Convert withdrawal to external-message's body + body, err := withdrawal.AsBody() + require.NoError(t, err) + + // ASSERT + // Check that sig is not empty + require.False(t, withdrawal.emptySig()) + + // Ensure that signature has the right format when decoding + var ( + r = big.NewInt(0).SetBytes(sig[:32]) + s = big.NewInt(0).SetBytes(sig[32:64]) + v = sig[64] + ) + + actualV, err := body.ReadUint(8) + require.NoError(t, err) + + actualR, err := body.ReadBigUint(256) + require.NoError(t, err) + + actualS, err := body.ReadBigUint(256) + require.NoError(t, err) + + assert.Equal(t, 0, r.Cmp(actualR)) + assert.Equal(t, 0, s.Cmp(actualS)) + assert.Equal(t, uint64(v), actualV) + }) +} + func TestFiltering(t *testing.T) { t.Run("Inbound", func(t *testing.T) { for _, tt := range []struct { @@ -237,8 +358,8 @@ func TestFixtures(t *testing.T) { tx, _ := getFixtureTX(t, "01-deposit") // ASSERT - require.Equal(t, uint64(26023788000003), tx.Lt) - require.Equal(t, "cbd6e2261334d08120e2fef428ecbb4e7773606ced878d0e6da204f2b4bf42bf", tx.Hash().Hex()) + require.Equal(t, uint64(27040013000003), tx.Lt) + require.Equal(t, "f893d7ed7fc3d73aedb44ca7c350026a5d27e679cf85c0c8df9e69db28387b06", tx.Hash().Hex()) } func TestSnakeData(t *testing.T) { @@ -262,6 +383,38 @@ func TestSnakeData(t *testing.T) { } } +func TestDeployment(t *testing.T) { + // ARRANGE + // Given TSS address & Authority address + const ( + sampleTSSPrivateKey = "0xb984cd65727cfd03081fc7bf33bf5c208bca697ce16139b5ded275887e81395a" + sampleAuthority = "0:4686a2c066c784a915f3e01c853d3195ed254c948e21adbb3e4a9b3f5f3c74d7" + ) + + privateKey := evmWallet(t, sampleTSSPrivateKey) + publicKey := privateKey.Public() + publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) + require.True(t, ok) + + tss := crypto.PubkeyToAddress(*publicKeyECDSA) + + // ACT + code := GatewayCode() + stateInit := GatewayStateInit(ton.MustParseAccountID(sampleAuthority), tss, true) + + // ASSERT + _, err := code.ToBocStringCustom(false, true, false, 0) + require.NoError(t, err) + + stateString, err := stateInit.ToBocStringCustom(false, true, false, 0) + require.NoError(t, err) + + // Taken from jest tests in protocol-contracts-ton (using the same vars for initState config) + const expectedState = "b5ee9c7241010101003c000074800000000124d38a790fdf1d9311fae87d4b21aeffd77bc26c004686a2c066c784a915f3e01c853d3195ed254c948e21adbb3e4a9b3f5f3c74d746f17671" + + require.Equal(t, expectedState, stateString) +} + //go:embed testdata var fixtures embed.FS @@ -311,3 +464,13 @@ func readFixtureFile(t *testing.T, filename string) []byte { return b } + +func evmWallet(t *testing.T, privateKey string) *ecdsa.PrivateKey { + pkBytes, err := hex.DecodeString(privateKey[2:]) + require.NoError(t, err) + + pk, err := crypto.ToECDSA(pkBytes) + require.NoError(t, err) + + return pk +} diff --git a/pkg/contracts/ton/gateway_tx.go b/pkg/contracts/ton/gateway_tx.go index 75c12c8eff..e25872d241 100644 --- a/pkg/contracts/ton/gateway_tx.go +++ b/pkg/contracts/ton/gateway_tx.go @@ -10,6 +10,7 @@ import ( type Transaction struct { ton.Transaction Operation Op + ExitCode int32 content any inbound bool @@ -20,6 +21,11 @@ func (tx *Transaction) IsInbound() bool { return tx.inbound } +// IsOutbound returns true if the transaction is outbound. +func (tx *Transaction) IsOutbound() bool { + return !tx.inbound +} + // GasUsed returns the amount of gas used by the transaction. func (tx *Transaction) GasUsed() math.Uint { return math.NewUint(uint64(tx.TotalFees.Grams)) @@ -40,6 +46,11 @@ func (tx *Transaction) DepositAndCall() (DepositAndCall, error) { return retrieveContent[DepositAndCall](tx) } +// Withdrawal casts the transaction content to a Withdrawal. +func (tx *Transaction) Withdrawal() (Withdrawal, error) { + return retrieveContent[Withdrawal](tx) +} + func retrieveContent[T any](tx *Transaction) (T, error) { typed, ok := tx.content.(T) if !ok { diff --git a/pkg/contracts/ton/testdata/00-donation.json b/pkg/contracts/ton/testdata/00-donation.json index 867f096a90..5e9ab7312d 100644 --- a/pkg/contracts/ton/testdata/00-donation.json +++ b/pkg/contracts/ton/testdata/00-donation.json @@ -1,8 +1,8 @@ { - "account": "0:997d889c815aeac21c47f86ae0e38383efc3c3463067582f6263ad48c5a1485b", - "boc": "b5ee9c72010207010001a10003b57997d889c815aeac21c47f86ae0e38383efc3c3463067582f6263ad48c5a1485b000017d46c458143cbd6e2261334d08120e2fef428ecbb4e7773606ced878d0e6da204f2b4bf42bf000017ab22a3b30366f17efd000146114e1a80102030101a00400827213c7e41677dcade29f2b424cdad8712132fbd5465b37df2e1763369e2fb12da0a7b38b0f8722a81351a279e9d229e4f5d92a5d91aed6c197ab4b5ef756b042cc021b04c0731749165a0bc01860db5611050600c968012b298e33d8992beccd0765f63941613bc9483edf610f74991a4cb72d4999ca1f00265f62272056bab08711fe1ab838e0e0fbf0f0d18c19d60bd898eb5231685216d165a0bc000608235a00002fa8d88b0284cde2fdfa00000032000000000000000040009e408c6c3d090000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005bc00000000000000000000000012d452da449e50b8cf7dd27861f146122afe1b546bb8b70fc8216f0c614139f8e04", - "description": "Sample donation to gw contract. https://testnet.tonviewer.com/transaction/d9339c9e78a55ee9ea0cd46cab798926c139db2a7c17a002041c3db90a80d5ea", - "hash": "d9339c9e78a55ee9ea0cd46cab798926c139db2a7c17a002041c3db90a80d5ea", - "logicalTime": 26201117000003, + "account": "0:8bcc0c665d07e58e33b56ed6aab02195071dbf08c3f6e00bf5f240cdb0a5df99", + "boc": "b5ee9c720102070100019f0003b578bcc0c665d07e58e33b56ed6aab02195071dbf08c3f6e00bf5f240cdb0a5df9900001897c5b37203f893d7ed7fc3d73aedb44ca7c350026a5d27e679cf85c0c8df9e69db28387b0600001897be5e9d4367110c250001460ef51680102030101a0040082728ae60f54fbd7891d1dc0cce8ca7e5dba50607abb642c57079270694d7b4fcd199318244a0da46af5b8f09ec45994a6520b84fb68e915d568c80bf9485d03e5b40217045ec908f0d1801860ef4211050600c968012b298e33d8992beccd0765f63941613bc9483edf610f74991a4cb72d4999ca1f0022f303199741f9638ced5bb5aaac086541c76fc230fdb802fd7c90336c2977e6508f0d18000608235a0000312f8b66e404ce22184a00000032000000000000000040009e40992c3d090000000000000000002100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005bc00000000000000000000000012d452da449e50b8cf7dd27861f146122afe1b546bb8b70fc8216f0c614139f8e04", + "description": "Donate ~0.6 TON; https://testnet.tonviewer.com/transaction/924a0996125ed491157b1398d5192bd371a2677df4b60a36ac94d8370c23c94d", + "hash": "924a0996125ed491157b1398d5192bd371a2677df4b60a36ac94d8370c23c94d", + "logicalTime": 27040136000003, "test": true } \ No newline at end of file diff --git a/pkg/contracts/ton/testdata/01-deposit.json b/pkg/contracts/ton/testdata/01-deposit.json index d22170ddf0..78f1b53f94 100644 --- a/pkg/contracts/ton/testdata/01-deposit.json +++ b/pkg/contracts/ton/testdata/01-deposit.json @@ -1,8 +1,8 @@ { - "account": "0:997d889c815aeac21c47f86ae0e38383efc3c3463067582f6263ad48c5a1485b", - "boc": "b5ee9c7201020a0100023d0003b57997d889c815aeac21c47f86ae0e38383efc3c3463067582f6263ad48c5a1485b000017ab22a3b3031b48df020aa3647a59163a25772d81991916a2bf523b771d89deec9e5be15d58000017ab20f8740366ead1e30003465b1d0080102030201e004050082723e346a6461f48fab691e87d9fd5954595eb15351fa1c536486a863d9708b79c313c7e41677dcade29f2b424cdad8712132fbd5465b37df2e1763369e2fb12da0021904222490ee6b28018646dca110080900f168012b298e33d8992beccd0765f63941613bc9483edf610f74991a4cb72d4999ca1f00265f62272056bab08711fe1ab838e0e0fbf0f0d18c19d60bd898eb5231685216d0ee6b28000608235a00002f5645476604cdd5a3c60000003280000000000000006c6d35f934b257cebf76cf01f29a0ae9bd54b022c00101df06015de004cbec44e40ad75610e23fc357071c1c1f7e1e1a31833ac17b131d6a462d0a42d800002f5645476608cdd5a3c6c007008b0000006500000000000000008012b298e33d8992beccd0765f63941613bc9483edf610f74991a4cb72d4999ca1e876046701b1b4d7e4d2c95f3afddb3c07ca682ba6f552c08b009e42d5ac3d090000000000000000007e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006fc98510184c2880c0000000000002000000000003da7f47a5d1898330bd18801617ae23a388cbaf527c312921718ef36ce9cf8c4e40901d04", - "description": "Sample deposit to gw contract. https://testnet.tonviewer.com/transaction/cbd6e2261334d08120e2fef428ecbb4e7773606ced878d0e6da204f2b4bf42bf", - "hash": "cbd6e2261334d08120e2fef428ecbb4e7773606ced878d0e6da204f2b4bf42bf", - "logicalTime": 26023788000003, + "account": "0:8bcc0c665d07e58e33b56ed6aab02195071dbf08c3f6e00bf5f240cdb0a5df99", + "boc": "b5ee9c7201020a010001ff0003b578bcc0c665d07e58e33b56ed6aab02195071dbf08c3f6e00bf5f240cdb0a5df9900001897be5e9d437fd807ff43c2da1f30cf6ebda98e5de8d54d6b5b644405d34501772896bd915b00001897bd98400367110b0000034672e48080102030201e00405008272a3610b9408c63364d64dccd4b442629796edf1843c0c2279d3d219a10a2ea4028ae60f54fbd7891d1dc0cce8ca7e5dba50607abb642c57079270694d7b4fcd19021904221a90ee6b28018664af0110080900f168012b298e33d8992beccd0765f63941613bc9483edf610f74991a4cb72d4999ca1f0022f303199741f9638ced5bb5aaac086541c76fc230fdb802fd7c90336c2977e650ee6b28000608235a0000312f7cbd3a84ce22160000000032800000000000000050f5c6b2dbb2e92cf3a905bc8de23c1d6f7eeccc400101df06015de0045e606332e83f2c719dab76b555810ca838edf8461fb7005faf92066d852efcc80000312f7cbd3a88ce221600c007001043b5dc10033d0900009e44070c3d09000000000000000000b400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006fc9838d604c1c6b0000000000000200000000000360161b93272c05ec52632c491b53e07b47cbbc930d19187ffb87a6961e0bf72240900d8c", + "description": "Deposit 1 TON; https://testnet.tonviewer.com/transaction/f893d7ed7fc3d73aedb44ca7c350026a5d27e679cf85c0c8df9e69db28387b06", + "hash": "f893d7ed7fc3d73aedb44ca7c350026a5d27e679cf85c0c8df9e69db28387b06", + "logicalTime": 27040013000003, "test": true -} +} \ No newline at end of file diff --git a/pkg/contracts/ton/testdata/02-deposit-and-call.json b/pkg/contracts/ton/testdata/02-deposit-and-call.json index 60f824a828..0e73f845a0 100644 --- a/pkg/contracts/ton/testdata/02-deposit-and-call.json +++ b/pkg/contracts/ton/testdata/02-deposit-and-call.json @@ -1,8 +1,8 @@ { - "account": "0:997d889c815aeac21c47f86ae0e38383efc3c3463067582f6263ad48c5a1485b", - "boc": "b5ee9c7201021a01000a040003b57997d889c815aeac21c47f86ae0e38383efc3c3463067582f6263ad48c5a1485b000017d4acf14a83d9339c9e78a55ee9ea0cd46cab798926c139db2a7c17a002041c3db90a80d5ea000017d46c45814366f1896b000347487d2080102030201e00405008272a7b38b0f8722a81351a279e9d229e4f5d92a5d91aed6c197ab4b5ef756b042ccdb7075fb2e3f1dc397aa3c93d8dd352cbe54af9fb033df63082875f03a70947102190480b409077359401866309211181901f168012b298e33d8992beccd0765f63941613bc9483edf610f74991a4cb72d4999ca1f00265f62272056bab08711fe1ab838e0e0fbf0f0d18c19d60bd898eb5231685216d077359400069397a000002fa959e29504cde312d60000003300000000000000006c6d35f934b257cebf76cf01f29a0ae9bd54b022c0080101df06015de004cbec44e40ad75610e23fc357071c1c1f7e1e1a31833ac17b131d6a462d0a42d800002fa959e29508cde312d6c007018b0000006600000000000000008012b298e33d8992beccd0765f63941613bc9483edf610f74991a4cb72d4999ca1e83a699d01b1b4d7e4d2c95f3afddb3c07ca682ba6f552c08b0801fe57686174206973204c6f72656d20497073756d3f2028617070726f782032204b696c6f6279746573290a0a4c6f72656d20497073756d2069732073696d706c792064756d6d792074657874206f6620746865207072696e74696e6720616e64207479706573657474696e6720696e6475737472792e0a4c6f72656d204970730901fe756d20686173206265656e2074686520696e6475737472792773207374616e646172642064756d6d79207465787420657665722073696e6365207468652031353030732c207768656e20616e20756e6b6e6f776e207072696e74657220746f6f6b0a612067616c6c6579206f66207479706520616e6420736372616d626c650a01fe6420697420746f206d616b65206120747970652073706563696d656e20626f6f6b2e20497420686173207375727669766564206e6f74206f6e6c7920666976652063656e7475726965732c0a62757420616c736f20746865206c65617020696e746f20656c656374726f6e6963207479706573657474696e672c2072656d610b01fe696e696e6720657373656e7469616c6c7920756e6368616e6765642e0a0a49742077617320706f70756c61726973656420696e207468652031393630732077697468207468652072656c65617365206f66204c657472617365742073686565747320636f6e7461696e696e67204c6f72656d20497073756d207061737361670c01fe65732c0a616e64206d6f726520726563656e746c792077697468206465736b746f70207075626c697368696e6720736f667477617265206c696b6520416c64757320506167654d616b657220696e636c7564696e672076657273696f6e73206f66204c6f72656d20497073756d2e0a0a57687920646f2077652075736520690d01fe743f0a0a49742069732061206c6f6e672d65737461626c6973686564206661637420746861742061207265616465722077696c6c206265206469737472616374656420627920746865207265616461626c6520636f6e74656e74206f6620612070616765207768656e0a6c6f6f6b696e6720617420697473206c61796f75740e01fe2e2054686520706f696e74206f66207573696e67204c6f72656d20497073756d2069732074686174206974206861732061206d6f72652d6f722d6c657373206e6f726d616c20646973747269627574696f6e206f66206c6574746572732c0a6173206f70706f73656420746f207573696e672027436f6e74656e74206865720f01fe652c20636f6e74656e742068657265272c206d616b696e67206974206c6f6f6b206c696b65207265616461626c6520456e676c6973682e204d616e79206465736b746f70207075626c697368696e670a7061636b6167657320616e6420776562207061676520656469746f7273206e6f7720757365204c6f72656d204970731001fe756d2061732074686569722064656661756c74206d6f64656c20746578742c20616e6420612073656172636820666f7220276c6f72656d20697073756d270a77696c6c20756e636f766572206d616e7920776562207369746573207374696c6c20696e20746865697220696e66616e63792e20566172696f757320766572731101fe696f6e7320686176652065766f6c766564206f766572207468652079656172732c20736f6d6574696d65730a6279206163636964656e742c20736f6d6574696d6573206f6e20707572706f73652028696e6a65637465642068756d6f757220616e6420746865206c696b65292e0a0a576865726520646f657320697420636f1201fe6d652066726f6d3f0a0a436f6e747261727920746f20706f70756c61722062656c6965662c204c6f72656d20497073756d206973206e6f742073696d706c792072616e646f6d20746578742e2049742068617320726f6f747320696e2061207069656365206f6620636c6173736963616c0a4c6174696e206c6974657261741301fe7572652066726f6d2034352042432c206d616b696e67206974206f7665722032303030207965617273206f6c642e2052696368617264204d63436c696e746f636b2c2061204c6174696e2070726f666573736f720a61742048616d7064656e2d5379646e657920436f6c6c65676520696e2056697267696e69612c206c6f6f1401fe6b6564207570206f6e65206f6620746865206d6f7265206f627363757265204c6174696e20776f7264732c20636f6e73656374657475722c0a66726f6d2061204c6f72656d20497073756d20706173736167652c20616e6420676f696e67207468726f75676820746865206369746573206f662074686520776f726420696e1501fe20636c6173736963616c206c6974657261747572652c20646973636f76657265640a74686520756e646f75627461626c6520736f757263652e204c6f72656d20497073756d20636f6d65732066726f6d2073656374696f6e7320312e31302e333220616e6420312e31302e3333206f66202264652046696e6962757320426f1601fe6e6f72756d206574204d616c6f72756d220a285468652045787472656d6573206f6620476f6f6420616e64204576696c292062792043696365726f2c207772697474656e20696e2034352042432e205468697320626f6f6b2069732061207472656174697365206f6e20746865207468656f7279206f66206574686963732c17004a0a7665727920706f70756c617220647572696e67207468652052656e61697373616e63652e009e43f62c3d090000000000000000009b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006fc9b95b984dcadcc0000000000002000000000003fa4c79ea2219e757506c681b3ab938299662dccbcef3f334edcddee1cd13bade44920284", - "description": "Sample deposit-and-call to gw contract. https://testnet.tonviewer.com/transaction/3647f17cc28e4a70404a10c62ad6262fbf67aa72579acde449d66cc0d0fd7ca8", - "hash": "3647f17cc28e4a70404a10c62ad6262fbf67aa72579acde449d66cc0d0fd7ca8", - "logicalTime": 26202202000003, + "account": "0:8bcc0c665d07e58e33b56ed6aab02195071dbf08c3f6e00bf5f240cdb0a5df99", + "boc": "b5ee9c7201020b010002380003b578bcc0c665d07e58e33b56ed6aab02195071dbf08c3f6e00bf5f240cdb0a5df9900001897f1271a43924a0996125ed491157b1398d5192bd371a2677df4b60a36ac94d8370c23c94d00001897c5b37203671113570003467ef32680102030201e004050082729318244a0da46af5b8f09ec45994a6520b84fb68e915d568c80bf9485d03e5b4dfb5d92afc4cc4b8a9ee7bd8e44b1cb4e83f8ae846b86103d1b13a885bf6d99302190480acc905f5e10018670b8411090a01f168012b298e33d8992beccd0765f63941613bc9483edf610f74991a4cb72d4999ca1f0022f303199741f9638ced5bb5aaac086541c76fc230fdb802fd7c90336c2977e6505f5e1000060c77b20000312fe24e3484ce2226ae00000033000000000000000050f5c6b2dbb2e92cf3a905bc8de23c1d6f7eeccc40060101df07006c546865736520224e46547322202d20617265207468657920696e2074686520726f6f6d2077697468207573207269676874206e6f773f015de0045e606332e83f2c719dab76b555810ca838edf8461fb7005faf92066d852efcc80000312fe24e3488ce2226aec0080010417882b8034f5880009e44824c3d09000000000000000000da00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006fc9838d604c1c6b000000000000020000000000029cbe8eaac8bfc6c3c14f724ae49966bef85efe4d41702b512100dd68514496de40900d8c", + "description": "Deposit 0.4 TON and add some call data; https://testnet.tonviewer.com/transaction/b6e26611d75a96a67d39a61958b81a940c2b2e6cb7e51798bb9cccc247eda6d7", + "hash": "b6e26611d75a96a67d39a61958b81a940c2b2e6cb7e51798bb9cccc247eda6d7", + "logicalTime": 27040865000003, "test": true } \ No newline at end of file diff --git a/pkg/contracts/ton/testdata/04-bounced-msg.json b/pkg/contracts/ton/testdata/04-bounced-msg.json index 9887db3123..afbef3199a 100644 --- a/pkg/contracts/ton/testdata/04-bounced-msg.json +++ b/pkg/contracts/ton/testdata/04-bounced-msg.json @@ -1,8 +1,8 @@ { - "account": "0:9594c719ec4c95f66683b2fb1ca0b09de4a41f6fb087ba4c8d265b96a4cce50f", - "boc": "b5ee9c72010207010001a30003b579594c719ec4c95f66683b2fb1ca0b09de4a41f6fb087ba4c8d265b96a4cce50f000017d5af81eb05b4269279feefdc3ea4220465ec2bce20c6e7f0a8c09b51dac55b90aef3c342c6000017d5af81eb0166f1b2b0000146097f4080102030101a0040082727682d6ce21cbeaec70573e8d6ab7dc6357b875cbffc8c50608035fda3fe3bc378db05a075336855e0ae2db0067bf7de2341a87b9d555e968d64b216fe5491aba02150c090179e568186097f411050600d3580132fb113902b5d584388ff0d5c1c70707df87868c60ceb05ec4c75a918b4290b700256531c67b13257d99a0ecbec7282c27792907dbec21ee93234996e5a9333943d0179e56800608235a00002fab5f03d608cde365607fffffffa432b6363796102bb7b9363210c0009e40614c0f1da800000000000000001700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005bc00000000000000000000000012d452da449e50b8cf7dd27861f146122afe1b546bb8b70fc8216f0c614139f8e04", - "description": "Sample bounced message. This address is not even a gw. https://testnet.tonviewer.com/transaction/b3c46f5faf8aee7348083e7adbbc9a60ab1c8e0eac09133d64e2c4eb831e607b", - "hash": "b3c46f5faf8aee7348083e7adbbc9a60ab1c8e0eac09133d64e2c4eb831e607b", - "logicalTime": 26206540000005, + "account": "0:8bcc0c665d07e58e33b56ed6aab02195071dbf08c3f6e00bf5f240cdb0a5df99", + "boc": "b5ee9c72010208010001ce0003b578bcc0c665d07e58e33b56ed6aab02195071dbf08c3f6e00bf5f240cdb0a5df9900001897fbd26d03b6e26611d75a96a67d39a61958b81a940c2b2e6cb7e51798bb9cccc247eda6d700001897f1271a436711152300034610fd4080102030201e00405008272dfb5d92afc4cc4b8a9ee7bd8e44b1cb4e83f8ae846b86103d1b13a885bf6d99374f73160896d4a44a6716af0cbffd5188134aad0f5ad71e85b3d997e93d2f2b30127046b49077359401060cea40e0181046998208d6a0700b168012b298e33d8992beccd0765f63941613bc9483edf610f74991a4cb72d4999ca1f0022f303199741f9638ced5bb5aaac086541c76fc230fdb802fd7c90336c2977e650773594000608235a0000312ff7a4da04ce222a46400101df0600b95801179818ccba0fcb1c676addad5560432a0e3b7e1187edc017ebe4819b614bbf3300256531c67b13257d99a0ecbec7282c27792907dbec21ee93234996e5a9333943d0770355800608235a0000312ff7a4da08ce222a467fffffffc0009e40844c3d090000000000ca0000001600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "description": "Sent & bounce 0.5 TON w/o op code; https://testnet.tonviewer.com/transaction/45c1d3af95531f21f89e7ae358cdb8676be6d115d1452cfecc0afba504b2865c", + "hash": "45c1d3af95531f21f89e7ae358cdb8676be6d115d1452cfecc0afba504b2865c", + "logicalTime": 27041044000003, "test": true -} +} \ No newline at end of file diff --git a/pkg/contracts/ton/testdata/06-withdrawal.json b/pkg/contracts/ton/testdata/06-withdrawal.json new file mode 100644 index 0000000000..4a1c439049 --- /dev/null +++ b/pkg/contracts/ton/testdata/06-withdrawal.json @@ -0,0 +1,8 @@ +{ + "account": "0:8bcc0c665d07e58e33b56ed6aab02195071dbf08c3f6e00bf5f240cdb0a5df99", + "boc": "b5ee9c7201020a010002330003b578bcc0c665d07e58e33b56ed6aab02195071dbf08c3f6e00bf5f240cdb0a5df99000018c1045da241a4c5249c3137d2a34959c24e17559ce9ac2db1ab50bf56ff90c51c699be1a101000018c1044e60016717d9b20003469b312880102030201e0040500827297289c46e054cc4185bbc479cec317dd3f434b6ad3c944d3d03498edd9f292cfb5c9d220a628308d45ba04ca3933aa8af3980597f6bde7cb357e24dca0cbe019020f0c40461a15408440080901c78801179818ccba0fcb1c676addad5560432a0e3b7e1187edc017ebe4819b614bbf3200d9a3d1d692aeaa822e70fbb03323f2547486ab0de48411a5e254b57a01007695383cfc2b0a381d566e8a16ff1f59507eaf4600cf5b3d642f0845f8635b9c82432c060101df070059000000c8801a1ceb92cd3e39501d1982f0ce073e3598ce86d963f913b280da9a36063f66d967312d000000000300af4801179818ccba0fcb1c676addad5560432a0e3b7e1187edc017ebe4819b614bbf33003439d7259a7c72a03a3305e19c0e7c6b319d0db2c7f2276501b5346c0c7ecdb2ce625a000608235a0000318208bb4484ce2fb36440009d45552313880000000000000000384000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020006fc9830d404c08234c0000000000020000000000039c9b8959c7d6f61f0cdcae7c604fb1eec3ce2c80c6e17335d9b1c705edf34ba4405015cc", + "description": "Withdrawal of 0.01 TON; https://testnet.tonviewer.com/transaction/ea60180151e4449af44438d93e13f2451820245cac5a9e2fa44ff1f2d78118fa", + "hash": "ea60180151e4449af44438d93e13f2451820245cac5a9e2fa44ff1f2d78118fa", + "logicalTime": 27217281000001, + "test": true +} \ No newline at end of file diff --git a/pkg/crypto/aes256_gcm.go b/pkg/crypto/aes256_gcm.go index e4fba7de7c..a7538363da 100644 --- a/pkg/crypto/aes256_gcm.go +++ b/pkg/crypto/aes256_gcm.go @@ -103,6 +103,7 @@ func DecryptAES256GCM(ciphertext []byte, password string) ([]byte, error) { nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:] // decrypt the ciphertext + // #nosec G407 false positive https://github.com/securego/gosec/issues/1211 plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) if err != nil { return nil, err diff --git a/pkg/memo/codec_compact.go b/pkg/memo/codec_compact.go index bcb49d3f92..817a12066a 100644 --- a/pkg/memo/codec_compact.go +++ b/pkg/memo/codec_compact.go @@ -120,6 +120,7 @@ func (c *CodecCompact) packLength(length int) ([]byte, error) { if length > math.MaxUint8 { return nil, fmt.Errorf("data length %d exceeds %d bytes", length, math.MaxUint8) } + // #nosec G115 range checked data[0] = uint8(length) case LenBytesLong: if length > math.MaxUint16 { diff --git a/pkg/memo/fields.go b/pkg/memo/fields.go index fff853f955..e0415e0636 100644 --- a/pkg/memo/fields.go +++ b/pkg/memo/fields.go @@ -6,7 +6,7 @@ type Fields interface { Pack(opCode OpCode, encodingFmt EncodingFormat, dataFlags uint8) ([]byte, error) // Unpack decodes the memo fields - Unpack(opCode OpCode, encodingFmt EncodingFormat, dataFlags uint8, data []byte) error + Unpack(encodingFmt EncodingFormat, dataFlags uint8, data []byte) error // Validate checks if the fields are valid Validate(opCode OpCode, dataFlags uint8) error diff --git a/pkg/memo/fields_v0.go b/pkg/memo/fields_v0.go index a8f79d99ba..d30745b6a3 100644 --- a/pkg/memo/fields_v0.go +++ b/pkg/memo/fields_v0.go @@ -54,18 +54,13 @@ func (f *FieldsV0) Pack(opCode OpCode, encodingFmt EncodingFormat, dataFlags uin } // Unpack decodes the memo fields -func (f *FieldsV0) Unpack(opCode OpCode, encodingFmt EncodingFormat, dataFlags uint8, data []byte) error { +func (f *FieldsV0) Unpack(encodingFmt EncodingFormat, dataFlags uint8, data []byte) error { codec, err := GetCodec(encodingFmt) if err != nil { return errors.Wrap(err, "unable to get codec") } - err = f.unpackFields(codec, dataFlags, data) - if err != nil { - return err - } - - return f.Validate(opCode, dataFlags) + return f.unpackFields(codec, dataFlags, data) } // Validate checks if the fields are valid diff --git a/pkg/memo/fields_v0_test.go b/pkg/memo/fields_v0_test.go index 13742422c2..11cff3e1bd 100644 --- a/pkg/memo/fields_v0_test.go +++ b/pkg/memo/fields_v0_test.go @@ -125,7 +125,6 @@ func Test_V0_Unpack(t *testing.T) { tests := []struct { name string - opCode memo.OpCode encodeFmt memo.EncodingFormat dataFlags byte data []byte @@ -134,7 +133,6 @@ func Test_V0_Unpack(t *testing.T) { }{ { name: "unpack all fields with ABI encoding", - opCode: memo.OpCodeDepositAndCall, encodeFmt: memo.EncodingFmtABI, dataFlags: flagsAllFieldsSet, // all fields are set data: ABIPack(t, @@ -156,7 +154,6 @@ func Test_V0_Unpack(t *testing.T) { }, { name: "unpack all fields with compact encoding", - opCode: memo.OpCodeDepositAndCall, encodeFmt: memo.EncodingFmtCompactShort, dataFlags: flagsAllFieldsSet, // all fields are set data: CompactPack( @@ -179,7 +176,6 @@ func Test_V0_Unpack(t *testing.T) { }, { name: "unpack empty ABI encoded payload if flag is set", - opCode: memo.OpCodeDepositAndCall, encodeFmt: memo.EncodingFmtABI, dataFlags: 0b00000010, // payload flags are set data: ABIPack(t, @@ -188,7 +184,6 @@ func Test_V0_Unpack(t *testing.T) { }, { name: "unpack empty compact encoded payload if flag is set", - opCode: memo.OpCodeDepositAndCall, encodeFmt: memo.EncodingFmtCompactShort, dataFlags: 0b00000010, // payload flag is set data: CompactPack( @@ -198,7 +193,6 @@ func Test_V0_Unpack(t *testing.T) { }, { name: "unable to get codec on invalid encoding format", - opCode: memo.OpCodeDepositAndCall, encodeFmt: 0x0F, dataFlags: 0b00000001, data: []byte{}, @@ -206,7 +200,6 @@ func Test_V0_Unpack(t *testing.T) { }, { name: "failed to unpack ABI encoded data with compact encoding format", - opCode: memo.OpCodeDepositAndCall, encodeFmt: memo.EncodingFmtCompactShort, dataFlags: 0b00000011, // receiver and payload flags are set data: ABIPack(t, @@ -214,23 +207,13 @@ func Test_V0_Unpack(t *testing.T) { memo.ArgPayload(fBytes)), errMsg: "failed to unpack arguments", }, - { - name: "fields validation failed due to empty receiver address", - opCode: memo.OpCodeDepositAndCall, - encodeFmt: memo.EncodingFmtABI, - dataFlags: 0b00000011, // receiver and payload flags are set - data: ABIPack(t, - memo.ArgReceiver(common.Address{}), - memo.ArgPayload(fBytes)), - errMsg: "receiver address is empty", - }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // unpack the fields fields := memo.FieldsV0{} - err := fields.Unpack(tc.opCode, tc.encodeFmt, tc.dataFlags, tc.data) + err := fields.Unpack(tc.encodeFmt, tc.dataFlags, tc.data) // validate the error message if tc.errMsg != "" { diff --git a/pkg/memo/memo.go b/pkg/memo/memo.go index 30670952b0..ca429a5f38 100644 --- a/pkg/memo/memo.go +++ b/pkg/memo/memo.go @@ -8,6 +8,11 @@ import ( "github.com/pkg/errors" ) +const ( + // version0 is the latest version of the memo + version0 uint8 = 0 +) + // InboundMemo represents the inbound memo structure for non-EVM chains type InboundMemo struct { // Header contains the memo header @@ -37,7 +42,7 @@ func (m *InboundMemo) EncodeToBytes() ([]byte, error) { // encode fields based on version var data []byte switch m.Version { - case 0: + case version0: data, err = m.FieldsV0.Pack(m.OpCode, m.EncodingFmt, dataFlags) default: return nil, fmt.Errorf("invalid memo version: %d", m.Version) @@ -51,28 +56,41 @@ func (m *InboundMemo) EncodeToBytes() ([]byte, error) { // DecodeFromBytes decodes a InboundMemo struct from raw bytes // -// Returns an error if given data is not a valid memo -func DecodeFromBytes(data []byte) (*InboundMemo, error) { +// Returns: +// - [memo, true, nil] if given data is successfully decoded as a memo. +// - [nil, true, err] if given data is successfully decoded as a memo but contains improper field values. +// - [nil, false, err] if given data can't be decoded as a memo. +// +// Note: we won't have to differentiate between the two 'true' cases if legacy memo phase out is completed. +func DecodeFromBytes(data []byte) (*InboundMemo, bool, error) { memo := &InboundMemo{} // decode header err := memo.Header.DecodeFromBytes(data) if err != nil { - return nil, errors.Wrap(err, "failed to decode memo header") + return nil, false, errors.Wrap(err, "failed to decode memo header") } // decode fields based on version switch memo.Version { - case 0: - err = memo.FieldsV0.Unpack(memo.OpCode, memo.EncodingFmt, memo.Header.DataFlags, data[HeaderSize:]) + case version0: + // unpack fields + err = memo.FieldsV0.Unpack(memo.EncodingFmt, memo.Header.DataFlags, data[HeaderSize:]) + if err != nil { + return nil, false, errors.Wrap(err, "failed to unpack memo FieldsV0") + } + + // validate fields + // Note: a well-formatted memo may still contain improper field values + err = memo.FieldsV0.Validate(memo.OpCode, memo.Header.DataFlags) + if err != nil { + return nil, true, errors.Wrap(err, "failed to validate memo FieldsV0") + } default: - return nil, fmt.Errorf("invalid memo version: %d", memo.Version) - } - if err != nil { - return nil, errors.Wrapf(err, "failed to unpack memo fields version: %d", memo.Version) + return nil, false, fmt.Errorf("invalid memo version: %d", memo.Version) } - return memo, nil + return memo, true, nil } // DecodeLegacyMemoHex decodes hex encoded memo message into address and calldata diff --git a/pkg/memo/memo_test.go b/pkg/memo/memo_test.go index e6cb067793..4eabd6a18f 100644 --- a/pkg/memo/memo_test.go +++ b/pkg/memo/memo_test.go @@ -137,7 +137,8 @@ func Test_Memo_EncodeToBytes(t *testing.T) { require.Equal(t, append(tt.expectedHead, tt.expectedData...), data) // decode the memo and compare with the original - decodedMemo, err := memo.DecodeFromBytes(data) + decodedMemo, isMemo, err := memo.DecodeFromBytes(data) + require.True(t, isMemo) require.NoError(t, err) require.Equal(t, tt.memo, decodedMemo) }) @@ -154,6 +155,7 @@ func Test_Memo_DecodeFromBytes(t *testing.T) { name string head []byte data []byte + isMemo bool expectedMemo memo.InboundMemo errMsg string }{ @@ -172,6 +174,7 @@ func Test_Memo_DecodeFromBytes(t *testing.T) { memo.ArgRevertAddress(fString), memo.ArgAbortAddress(fAddress), memo.ArgRevertMessage(fBytes)), + isMemo: true, expectedMemo: memo.InboundMemo{ Header: memo.Header{ Version: 0, @@ -207,6 +210,7 @@ func Test_Memo_DecodeFromBytes(t *testing.T) { memo.ArgRevertAddress(fString), memo.ArgAbortAddress(fAddress), memo.ArgRevertMessage(fBytes)), + isMemo: true, expectedMemo: memo.InboundMemo{ Header: memo.Header{ Version: 0, @@ -251,20 +255,51 @@ func Test_Memo_DecodeFromBytes(t *testing.T) { memo.EncodingFmtCompactShort, memo.ArgReceiver(fAddress), ), // but data is compact encoded - errMsg: "failed to unpack memo fields", + errMsg: "failed to unpack memo FieldsV0", + }, + { + name: "should return [nil, true, err] if fields validation fails", + head: MakeHead( + 0, + uint8(memo.EncodingFmtABI), + uint8(memo.OpCodeDepositAndCall), + 0, + 0b00000011, // receiver flag is set + ), + data: ABIPack(t, + memo.ArgReceiver(common.Address{}), // empty receiver address provided + memo.ArgPayload(fBytes)), + isMemo: true, // it's still a memo, but with invalid field values + errMsg: "failed to validate memo FieldsV0", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { data := append(tt.head, tt.data...) - memo, err := memo.DecodeFromBytes(data) + memo, isMemo, err := memo.DecodeFromBytes(data) + + // check error message if tt.errMsg != "" { + require.Nil(t, memo) require.ErrorContains(t, err, tt.errMsg) return } - require.NoError(t, err) - require.Equal(t, tt.expectedMemo, *memo) + + // a standard memo or not + require.Equal(t, tt.isMemo, isMemo) + if !isMemo { + require.Nil(t, memo) + return + } + + // if it's a standard memo, depending on validation result + if err != nil { + require.Nil(t, memo) + } else { + require.NotNil(t, memo) + require.Equal(t, tt.expectedMemo, *memo) + } }) } } diff --git a/pkg/proofs/ethereum/proof.go b/pkg/proofs/ethereum/proof.go index 8f7d2976b9..96a31a1957 100644 --- a/pkg/proofs/ethereum/proof.go +++ b/pkg/proofs/ethereum/proof.go @@ -136,7 +136,7 @@ func (t *Trie) GenerateProof(txIndex int) (*Proof, error) { // #nosec G115 checked as non-negative indexBuf = rlp.AppendUint64(indexBuf[:0], uint64(txIndex)) proof := NewProof() - err := t.Prove(indexBuf, 0, proof) + err := t.Prove(indexBuf, proof) if err != nil { return nil, err } @@ -146,8 +146,7 @@ func (t *Trie) GenerateProof(txIndex int) (*Proof, error) { // NewTrie builds a trie from a DerivableList. The DerivableList must be types.Transactions // or types.Receipts. func NewTrie(list types.DerivableList) Trie { - hasher := new(trie.Trie) - hasher.Reset() + hasher := trie.NewEmpty(nil) valueBuf := encodeBufferPool.Get().(*bytes.Buffer) defer encodeBufferPool.Put(valueBuf) @@ -160,18 +159,18 @@ func NewTrie(list types.DerivableList) Trie { // #nosec G115 iterator indexBuf = rlp.AppendUint64(indexBuf[:0], uint64(i)) value := encodeForDerive(list, i, valueBuf) - hasher.Update(indexBuf, value) + _ = hasher.Update(indexBuf, value) } if list.Len() > 0 { indexBuf = rlp.AppendUint64(indexBuf[:0], 0) value := encodeForDerive(list, 0, valueBuf) - hasher.Update(indexBuf, value) + _ = hasher.Update(indexBuf, value) } for i := 0x80; i < list.Len(); i++ { // #nosec G115 iterator indexBuf = rlp.AppendUint64(indexBuf[:0], uint64(i)) value := encodeForDerive(list, i, valueBuf) - hasher.Update(indexBuf, value) + _ = hasher.Update(indexBuf, value) } return Trie{hasher} } diff --git a/pkg/rpc/clients.go b/pkg/rpc/clients.go index 26ecbe729f..1dc0d7314e 100644 --- a/pkg/rpc/clients.go +++ b/pkg/rpc/clients.go @@ -8,6 +8,7 @@ import ( "github.com/cosmos/cosmos-sdk/client/grpc/tmservice" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + distributiontypes "github.com/cosmos/cosmos-sdk/x/distribution/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" feemarkettypes "github.com/zeta-chain/ethermint/x/feemarket/types" @@ -35,6 +36,8 @@ type Clients struct { Staking stakingtypes.QueryClient // Upgrade is a github.com/cosmos/cosmos-sdk/x/upgrade/types QueryClient Upgrade upgradetypes.QueryClient + // Distribution is a "github.com/cosmos/cosmos-sdk/x/distribution/types" QueryClient + Distribution distributiontypes.QueryClient // ZetaCore specific clients @@ -65,11 +68,12 @@ type Clients struct { func newClients(ctx client.Context) (Clients, error) { return Clients{ // Cosmos SDK clients - Auth: authtypes.NewQueryClient(ctx), - Bank: banktypes.NewQueryClient(ctx), - Staking: stakingtypes.NewQueryClient(ctx), - Upgrade: upgradetypes.NewQueryClient(ctx), - Authority: authoritytypes.NewQueryClient(ctx), + Auth: authtypes.NewQueryClient(ctx), + Bank: banktypes.NewQueryClient(ctx), + Staking: stakingtypes.NewQueryClient(ctx), + Upgrade: upgradetypes.NewQueryClient(ctx), + Authority: authoritytypes.NewQueryClient(ctx), + Distribution: distributiontypes.NewQueryClient(ctx), // ZetaCore specific clients Crosschain: crosschaintypes.NewQueryClient(ctx), Fungible: fungibletypes.NewQueryClient(ctx), diff --git a/pkg/ticker/ticker.go b/pkg/ticker/ticker.go index 2a5a7edff1..ebac3e1a96 100644 --- a/pkg/ticker/ticker.go +++ b/pkg/ticker/ticker.go @@ -66,7 +66,7 @@ type Opt func(*Ticker) // WithLogger sets the logger for the Ticker. func WithLogger(log zerolog.Logger, name string) Opt { return func(t *Ticker) { - t.logger = log.With().Str("ticker_name", name).Logger() + t.logger = log.With().Str("ticker.name", name).Logger() } } @@ -82,6 +82,7 @@ func New(interval time.Duration, task Task, opts ...Opt) *Ticker { t := &Ticker{ interval: interval, task: task, + logger: zerolog.Nop(), } for _, opt := range opts { @@ -162,6 +163,11 @@ func (t *Ticker) SetInterval(interval time.Duration) { return } + t.logger.Info(). + Dur("ticker.old_interval", t.interval). + Dur("ticker.new_interval", interval). + Msg("Changing interval") + t.interval = interval t.ticker.Reset(interval) } @@ -183,7 +189,8 @@ func (t *Ticker) Stop() { t.logger.Info().Msgf("Ticker stopped") } -// SecondsFromUint64 converts uint64 to time.Duration in seconds. -func SecondsFromUint64(d uint64) time.Duration { - return time.Duration(d) * time.Second +// DurationFromUint64Seconds converts uint64 of seconds to time.Duration. +func DurationFromUint64Seconds(seconds uint64) time.Duration { + // #nosec G115 seconds should be in range and is not user controlled + return time.Duration(seconds) * time.Second } diff --git a/pkg/ticker/ticker_test.go b/pkg/ticker/ticker_test.go index 60d6c74dc8..4e03a28d4f 100644 --- a/pkg/ticker/ticker_test.go +++ b/pkg/ticker/ticker_test.go @@ -245,6 +245,6 @@ func TestTicker(t *testing.T) { // ARRANGE require.ErrorContains(t, err, "hey") - require.Contains(t, out.String(), `{"level":"info","ticker_name":"my-task","message":"Ticker stopped"}`) + require.Contains(t, out.String(), `{"level":"info","ticker.name":"my-task","message":"Ticker stopped"}`) }) } diff --git a/precompiles/bank/bank.go b/precompiles/bank/bank.go index 51a559edd3..63311ca8e7 100644 --- a/precompiles/bank/bank.go +++ b/precompiles/bank/bank.go @@ -9,9 +9,8 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/vm" - "github.com/zeta-chain/protocol-contracts/v2/pkg/zrc20.sol" - ptypes "github.com/zeta-chain/node/precompiles/types" + precompiletypes "github.com/zeta-chain/node/precompiles/types" fungiblekeeper "github.com/zeta-chain/node/x/fungible/keeper" ) @@ -49,11 +48,10 @@ func initABI() { } type Contract struct { - ptypes.BaseContract + precompiletypes.BaseContract bankKeeper bank.Keeper fungibleKeeper fungiblekeeper.Keeper - zrc20ABI *abi.ABI cdc codec.Codec kvGasConfig storetypes.GasConfig } @@ -70,18 +68,10 @@ func NewIBankContract( fungibleKeeper.GetAuthKeeper().SetAccount(ctx, authtypes.NewBaseAccount(accAddress, nil, 0, 0)) } - // Instantiate the ZRC20 ABI only one time. - // This avoids instantiating it every time deposit or withdraw are called. - zrc20ABI, err := zrc20.ZRC20MetaData.GetAbi() - if err != nil { - return nil - } - return &Contract{ - BaseContract: ptypes.NewBaseContract(ContractAddress), + BaseContract: precompiletypes.NewBaseContract(ContractAddress), bankKeeper: bankKeeper, fungibleKeeper: fungibleKeeper, - zrc20ABI: zrc20ABI, cdc: cdc, kvGasConfig: kvGasConfig, } @@ -130,13 +120,13 @@ func (c *Contract) Run(evm *vm.EVM, contract *vm.Contract, readOnly bool) ([]byt return nil, err } - stateDB := evm.StateDB.(ptypes.ExtStateDB) + stateDB := evm.StateDB.(precompiletypes.ExtStateDB) switch method.Name { // Deposit and Withdraw methods are both not allowed in read-only mode. case DepositMethodName, WithdrawMethodName: if readOnly { - return nil, ptypes.ErrWriteMethod{ + return nil, precompiletypes.ErrWriteMethod{ Method: method.Name, } } @@ -175,42 +165,8 @@ func (c *Contract) Run(evm *vm.EVM, contract *vm.Contract, readOnly bool) ([]byt return res, nil default: - return nil, ptypes.ErrInvalidMethod{ + return nil, precompiletypes.ErrInvalidMethod{ Method: method.Name, } } } - -// getEVMCallerAddress returns the caller address. -// Usually the caller is the contract.CallerAddress, which is the address of the contract that called the precompiled contract. -// If contract.CallerAddress != evm.Origin is true, it means the call was made through a contract, -// on which case there is a need to set the caller to the evm.Origin. -func getEVMCallerAddress(evm *vm.EVM, contract *vm.Contract) (common.Address, error) { - caller := contract.CallerAddress - if contract.CallerAddress != evm.Origin { - caller = evm.Origin - } - - return caller, nil -} - -// getCosmosAddress returns the counterpart cosmos address of the given ethereum address. -// It checks if the address is empty or blocked by the bank keeper. -func getCosmosAddress(bankKeeper bank.Keeper, addr common.Address) (sdk.AccAddress, error) { - toAddr := sdk.AccAddress(addr.Bytes()) - if toAddr.Empty() { - return nil, &ptypes.ErrInvalidAddr{ - Got: toAddr.String(), - Reason: "empty address", - } - } - - if bankKeeper.BlockedAddr(toAddr) { - return nil, &ptypes.ErrInvalidAddr{ - Got: toAddr.String(), - Reason: "destination address blocked by bank keeper", - } - } - - return toAddr, nil -} diff --git a/precompiles/bank/bank_test.go b/precompiles/bank/bank_test.go index 518c330a7f..a42bdeb88a 100644 --- a/precompiles/bank/bank_test.go +++ b/precompiles/bank/bank_test.go @@ -2,16 +2,12 @@ package bank import ( "encoding/json" - "math/big" "testing" storetypes "github.com/cosmos/cosmos-sdk/store/types" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/vm" "github.com/stretchr/testify/require" ethermint "github.com/zeta-chain/ethermint/types" "github.com/zeta-chain/node/testutil/keeper" - "github.com/zeta-chain/node/testutil/sample" ) func Test_IBankContract(t *testing.T) { @@ -128,29 +124,3 @@ func Test_InvalidABI(t *testing.T) { initABI() } - -func Test_getEVMCallerAddress(t *testing.T) { - mockEVM := vm.EVM{ - TxContext: vm.TxContext{ - Origin: common.Address{}, - }, - } - - mockVMContract := vm.NewContract( - contractRef{address: common.Address{}}, - contractRef{address: ContractAddress}, - big.NewInt(0), - 0, - ) - - // When contract.CallerAddress == evm.Origin, caller is set to contract.CallerAddress. - caller, err := getEVMCallerAddress(&mockEVM, mockVMContract) - require.NoError(t, err) - require.Equal(t, common.Address{}, caller, "address shouldn be the same") - - // When contract.CallerAddress != evm.Origin, caller should be set to evm.Origin. - mockEVM.Origin = sample.EthAddress() - caller, err = getEVMCallerAddress(&mockEVM, mockVMContract) - require.NoError(t, err) - require.Equal(t, mockEVM.Origin, caller, "address should be evm.Origin") -} diff --git a/precompiles/bank/logs.go b/precompiles/bank/logs.go index 36b877dfa5..9043cc63f9 100644 --- a/precompiles/bank/logs.go +++ b/precompiles/bank/logs.go @@ -28,8 +28,8 @@ func (c *Contract) addEventLog( topics, err := logs.MakeTopics( event, - []interface{}{common.BytesToAddress(eventData.zrc20Addr.Bytes())}, - []interface{}{common.BytesToAddress(eventData.zrc20Token.Bytes())}, + []interface{}{eventData.zrc20Addr}, + []interface{}{eventData.zrc20Token}, []interface{}{eventData.cosmosCoin}, ) if err != nil { diff --git a/precompiles/bank/method_balance_of.go b/precompiles/bank/method_balance_of.go index e4bc644a2b..85765d2ab8 100644 --- a/precompiles/bank/method_balance_of.go +++ b/precompiles/bank/method_balance_of.go @@ -5,7 +5,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" - ptypes "github.com/zeta-chain/node/precompiles/types" + precompiletypes "github.com/zeta-chain/node/precompiles/types" ) // balanceOf returns the balance of cosmos coins minted by the bank's deposit function, @@ -19,7 +19,7 @@ func (c *Contract) balanceOf( args []interface{}, ) (result []byte, err error) { if len(args) != 2 { - return nil, &(ptypes.ErrInvalidNumberOfArgs{ + return nil, &(precompiletypes.ErrInvalidNumberOfArgs{ Got: len(args), Expect: 2, }) @@ -32,7 +32,7 @@ func (c *Contract) balanceOf( } // Get the counterpart cosmos address. - toAddr, err := getCosmosAddress(c.bankKeeper, addr) + toAddr, err := precompiletypes.GetCosmosAddress(c.bankKeeper, addr) if err != nil { return nil, err } @@ -41,7 +41,7 @@ func (c *Contract) balanceOf( // Do not check for t.Paused, as the balance is read only the EOA won't be able to operate. _, found := c.fungibleKeeper.GetForeignCoins(ctx, zrc20Addr.String()) if !found { - return nil, &ptypes.ErrInvalidToken{ + return nil, &precompiletypes.ErrInvalidToken{ Got: zrc20Addr.String(), Reason: "token is not a whitelisted ZRC20", } @@ -49,9 +49,9 @@ func (c *Contract) balanceOf( // Bank Keeper GetBalance returns the specified Cosmos coin balance for a given address. // Check explicitly the balance is a non-negative non-nil value. - coin := c.bankKeeper.GetBalance(ctx, toAddr, ZRC20ToCosmosDenom(zrc20Addr)) + coin := c.bankKeeper.GetBalance(ctx, toAddr, precompiletypes.ZRC20ToCosmosDenom(zrc20Addr)) if !coin.IsValid() { - return nil, &ptypes.ErrInvalidCoin{ + return nil, &precompiletypes.ErrInvalidCoin{ Got: coin.GetDenom(), Negative: coin.IsNegative(), Nil: coin.IsNil(), @@ -64,14 +64,14 @@ func (c *Contract) balanceOf( func unpackBalanceOfArgs(args []interface{}) (zrc20Addr common.Address, addr common.Address, err error) { zrc20Addr, ok := args[0].(common.Address) if !ok { - return common.Address{}, common.Address{}, &ptypes.ErrInvalidAddr{ + return common.Address{}, common.Address{}, &precompiletypes.ErrInvalidAddr{ Got: zrc20Addr.String(), } } addr, ok = args[1].(common.Address) if !ok { - return common.Address{}, common.Address{}, &ptypes.ErrInvalidAddr{ + return common.Address{}, common.Address{}, &precompiletypes.ErrInvalidAddr{ Got: addr.String(), } } diff --git a/precompiles/bank/method_deposit.go b/precompiles/bank/method_deposit.go index 1d5792d8a3..43ba2a492f 100644 --- a/precompiles/bank/method_deposit.go +++ b/precompiles/bank/method_deposit.go @@ -8,7 +8,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/vm" - ptypes "github.com/zeta-chain/node/precompiles/types" + precompiletypes "github.com/zeta-chain/node/precompiles/types" "github.com/zeta-chain/node/x/fungible/types" ) @@ -34,7 +34,7 @@ func (c *Contract) deposit( // This function is developed using the Check - Effects - Interactions pattern: // 1. Check everything is correct. if len(args) != 2 { - return nil, &(ptypes.ErrInvalidNumberOfArgs{ + return nil, &(precompiletypes.ErrInvalidNumberOfArgs{ Got: len(args), Expect: 2, }) @@ -48,43 +48,29 @@ func (c *Contract) deposit( } // Get the correct caller address. - caller, err := getEVMCallerAddress(evm, contract) + caller, err := precompiletypes.GetEVMCallerAddress(evm, contract) if err != nil { return nil, err } // Get the cosmos address of the caller. - toAddr, err := getCosmosAddress(c.bankKeeper, caller) + toAddr, err := precompiletypes.GetCosmosAddress(c.bankKeeper, caller) if err != nil { return nil, err } // Check for enough balance. // function balanceOf(address account) public view virtual override returns (uint256) - resBalanceOf, err := c.CallContract( - ctx, - &c.fungibleKeeper, - c.zrc20ABI, - zrc20Addr, - "balanceOf", - []interface{}{caller}, - ) + balance, err := c.fungibleKeeper.ZRC20BalanceOf(ctx, zrc20Addr, caller) if err != nil { - return nil, &ptypes.ErrUnexpected{ + return nil, &precompiletypes.ErrUnexpected{ When: "balanceOf", Got: err.Error(), } } - balance, ok := resBalanceOf[0].(*big.Int) - if !ok { - return nil, &ptypes.ErrUnexpected{ - Got: "ZRC20 balanceOf returned an unexpected type", - } - } - if balance.Cmp(amount) < 0 || balance.Cmp(big.NewInt(0)) <= 0 { - return nil, &ptypes.ErrInvalidAmount{ + return nil, &precompiletypes.ErrInvalidAmount{ Got: balance.String(), } } @@ -94,14 +80,14 @@ func (c *Contract) deposit( // this way we map ZRC20 addresses to cosmos denoms "zevm/0x12345". // - Mint coins to the fungible module. // - Send coins from fungible to the caller. - coinSet, err := createCoinSet(ZRC20ToCosmosDenom(zrc20Addr), amount) + coinSet, err := precompiletypes.CreateCoinSet(zrc20Addr, amount) if err != nil { return nil, err } // 2. Effect: subtract balance. - if err := c.fungibleKeeper.LockZRC20(ctx, c.zrc20ABI, zrc20Addr, c.Address(), caller, c.Address(), amount); err != nil { - return nil, &ptypes.ErrUnexpected{ + if err := c.fungibleKeeper.LockZRC20(ctx, zrc20Addr, c.Address(), caller, c.Address(), amount); err != nil { + return nil, &precompiletypes.ErrUnexpected{ When: "LockZRC20InBank", Got: err.Error(), } @@ -109,7 +95,7 @@ func (c *Contract) deposit( // 3. Interactions: create cosmos coin and send. if err := c.bankKeeper.MintCoins(ctx, types.ModuleName, coinSet); err != nil { - return nil, &ptypes.ErrUnexpected{ + return nil, &precompiletypes.ErrUnexpected{ When: "MintCoins", Got: err.Error(), } @@ -117,14 +103,14 @@ func (c *Contract) deposit( err = c.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, toAddr, coinSet) if err != nil { - return nil, &ptypes.ErrUnexpected{ + return nil, &precompiletypes.ErrUnexpected{ When: "SendCoinsFromModuleToAccount", Got: err.Error(), } } if err := c.addEventLog(ctx, evm.StateDB, DepositEventName, eventData{caller, zrc20Addr, toAddr.String(), coinSet.Denoms()[0], amount}); err != nil { - return nil, &ptypes.ErrUnexpected{ + return nil, &precompiletypes.ErrUnexpected{ When: "AddDepositLog", Got: err.Error(), } @@ -136,14 +122,14 @@ func (c *Contract) deposit( func unpackDepositArgs(args []interface{}) (zrc20Addr common.Address, amount *big.Int, err error) { zrc20Addr, ok := args[0].(common.Address) if !ok { - return common.Address{}, nil, &ptypes.ErrInvalidAddr{ + return common.Address{}, nil, &precompiletypes.ErrInvalidAddr{ Got: zrc20Addr.String(), } } amount, ok = args[1].(*big.Int) if !ok || amount == nil || amount.Sign() <= 0 { - return common.Address{}, nil, &ptypes.ErrInvalidAmount{ + return common.Address{}, nil, &precompiletypes.ErrInvalidAmount{ Got: amount.String(), } } diff --git a/precompiles/bank/method_test.go b/precompiles/bank/method_test.go index e416187490..d820d38f71 100644 --- a/precompiles/bank/method_test.go +++ b/precompiles/bank/method_test.go @@ -10,13 +10,14 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/params" + "github.com/holiman/uint256" "github.com/stretchr/testify/require" ethermint "github.com/zeta-chain/ethermint/types" evmkeeper "github.com/zeta-chain/ethermint/x/evm/keeper" "github.com/zeta-chain/ethermint/x/evm/statedb" "github.com/zeta-chain/node/pkg/chains" erc1967proxy "github.com/zeta-chain/node/pkg/contracts/erc1967proxy" - ptypes "github.com/zeta-chain/node/precompiles/types" + precompiletypes "github.com/zeta-chain/node/precompiles/types" "github.com/zeta-chain/node/testutil/keeper" "github.com/zeta-chain/node/testutil/sample" fungiblekeeper "github.com/zeta-chain/node/x/fungible/keeper" @@ -46,7 +47,7 @@ func Test_Methods(t *testing.T) { success, err := ts.bankContract.Run(ts.mockEVM, ts.mockVMContract, true) require.ErrorIs( t, - ptypes.ErrWriteMethod{ + precompiletypes.ErrWriteMethod{ Method: "deposit", }, err) @@ -73,7 +74,7 @@ func Test_Methods(t *testing.T) { success, err := ts.bankContract.Run(ts.mockEVM, ts.mockVMContract, true) require.ErrorIs( t, - ptypes.ErrWriteMethod{ + precompiletypes.ErrWriteMethod{ Method: "withdraw", }, err) @@ -101,7 +102,7 @@ func Test_Methods(t *testing.T) { require.Error(t, err) require.ErrorAs( t, - ptypes.ErrInvalidAmount{ + precompiletypes.ErrInvalidAmount{ Got: "0", }, err, @@ -248,7 +249,7 @@ func Test_Methods(t *testing.T) { require.Error(t, err) require.ErrorAs( t, - ptypes.ErrInvalidAmount{ + precompiletypes.ErrInvalidAmount{ Got: "1000", }, err, @@ -459,7 +460,7 @@ func Test_Methods(t *testing.T) { require.Error(t, err) require.ErrorAs( t, - ptypes.ErrInsufficientBalance{ + precompiletypes.ErrInsufficientBalance{ Requested: "501", Got: "500", }, @@ -550,7 +551,7 @@ func setupChain(t *testing.T) testSuite { mockVMContract := vm.NewContract( contractRef{address: common.Address{}}, contractRef{address: ContractAddress}, - big.NewInt(0), + uint256.NewInt(0), 0, ) diff --git a/precompiles/bank/method_withdraw.go b/precompiles/bank/method_withdraw.go index e071ed04cd..24f83bf5e4 100644 --- a/precompiles/bank/method_withdraw.go +++ b/precompiles/bank/method_withdraw.go @@ -9,7 +9,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/vm" - ptypes "github.com/zeta-chain/node/precompiles/types" + precompiletypes "github.com/zeta-chain/node/precompiles/types" "github.com/zeta-chain/node/x/fungible/types" ) @@ -29,7 +29,7 @@ func (c *Contract) withdraw( ) (result []byte, err error) { // 1. Check everything is correct. if len(args) != 2 { - return nil, &(ptypes.ErrInvalidNumberOfArgs{ + return nil, &(precompiletypes.ErrInvalidNumberOfArgs{ Got: len(args), Expect: 2, }) @@ -43,50 +43,50 @@ func (c *Contract) withdraw( } // Get the correct caller address. - caller, err := getEVMCallerAddress(evm, contract) + caller, err := precompiletypes.GetEVMCallerAddress(evm, contract) if err != nil { return nil, err } // Get the cosmos address of the caller. // This address should have enough cosmos coin balance as the requested amount. - fromAddr, err := getCosmosAddress(c.bankKeeper, caller) + fromAddr, err := precompiletypes.GetCosmosAddress(c.bankKeeper, caller) if err != nil { return nil, err } // Safety check: token has to be a non-paused whitelisted ZRC20. if err := c.fungibleKeeper.IsValidZRC20(ctx, zrc20Addr); err != nil { - return nil, &ptypes.ErrInvalidToken{ + return nil, &precompiletypes.ErrInvalidToken{ Got: zrc20Addr.String(), Reason: err.Error(), } } // Caller has to have enough cosmos coin balance to withdraw the requested amount. - coin := c.bankKeeper.GetBalance(ctx, fromAddr, ZRC20ToCosmosDenom(zrc20Addr)) + coin := c.bankKeeper.GetBalance(ctx, fromAddr, precompiletypes.ZRC20ToCosmosDenom(zrc20Addr)) if !coin.IsValid() { - return nil, &ptypes.ErrInsufficientBalance{ + return nil, &precompiletypes.ErrInsufficientBalance{ Requested: amount.String(), Got: "invalid coin", } } if coin.Amount.LT(math.NewIntFromBigInt(amount)) { - return nil, &ptypes.ErrInsufficientBalance{ + return nil, &precompiletypes.ErrInsufficientBalance{ Requested: amount.String(), Got: coin.Amount.String(), } } - coinSet, err := createCoinSet(ZRC20ToCosmosDenom(zrc20Addr), amount) + coinSet, err := precompiletypes.CreateCoinSet(zrc20Addr, amount) if err != nil { return nil, err } // Check if bank address has enough ZRC20 balance. - if err := c.fungibleKeeper.CheckZRC20Balance(ctx, c.zrc20ABI, zrc20Addr, c.Address(), amount); err != nil { - return nil, &ptypes.ErrInsufficientBalance{ + if err := c.fungibleKeeper.CheckZRC20Balance(ctx, zrc20Addr, c.Address(), amount); err != nil { + return nil, &precompiletypes.ErrInsufficientBalance{ Requested: amount.String(), Got: err.Error(), } @@ -94,29 +94,29 @@ func (c *Contract) withdraw( // 2. Effect: burn cosmos coin balance. if err := c.bankKeeper.SendCoinsFromAccountToModule(ctx, fromAddr, types.ModuleName, coinSet); err != nil { - return nil, &ptypes.ErrUnexpected{ + return nil, &precompiletypes.ErrUnexpected{ When: "SendCoinsFromAccountToModule", Got: err.Error(), } } if err := c.bankKeeper.BurnCoins(ctx, types.ModuleName, coinSet); err != nil { - return nil, &ptypes.ErrUnexpected{ + return nil, &precompiletypes.ErrUnexpected{ When: "BurnCoins", Got: err.Error(), } } // 3. Interactions: send ZRC20. - if err := c.fungibleKeeper.UnlockZRC20(ctx, c.zrc20ABI, zrc20Addr, caller, c.Address(), amount); err != nil { - return nil, &ptypes.ErrUnexpected{ + if err := c.fungibleKeeper.UnlockZRC20(ctx, zrc20Addr, caller, c.Address(), amount); err != nil { + return nil, &precompiletypes.ErrUnexpected{ When: "UnlockZRC20InBank", Got: err.Error(), } } if err := c.addEventLog(ctx, evm.StateDB, WithdrawEventName, eventData{caller, zrc20Addr, fromAddr.String(), coinSet.Denoms()[0], amount}); err != nil { - return nil, &ptypes.ErrUnexpected{ + return nil, &precompiletypes.ErrUnexpected{ When: "AddWithdrawLog", Got: err.Error(), } @@ -128,14 +128,14 @@ func (c *Contract) withdraw( func unpackWithdrawArgs(args []interface{}) (zrc20Addr common.Address, amount *big.Int, err error) { zrc20Addr, ok := args[0].(common.Address) if !ok { - return common.Address{}, nil, &ptypes.ErrInvalidAddr{ + return common.Address{}, nil, &precompiletypes.ErrInvalidAddr{ Got: zrc20Addr.String(), } } amount, ok = args[1].(*big.Int) if !ok || amount == nil || amount.Sign() <= 0 { - return common.Address{}, nil, &ptypes.ErrInvalidAmount{ + return common.Address{}, nil, &precompiletypes.ErrInvalidAmount{ Got: amount.String(), } } diff --git a/precompiles/logs/logs.go b/precompiles/logs/logs.go index 07960a442e..6848cbc9a0 100644 --- a/precompiles/logs/logs.go +++ b/precompiles/logs/logs.go @@ -16,9 +16,10 @@ type Argument struct { // AddLog adds log to stateDB func AddLog(ctx sdk.Context, precompileAddr common.Address, stateDB vm.StateDB, topics []common.Hash, data []byte) { stateDB.AddLog(&types.Log{ - Address: precompileAddr, - Topics: topics, - Data: data, + Address: precompileAddr, + Topics: topics, + Data: data, + // #nosec G115 block height always positive BlockNumber: uint64(ctx.BlockHeight()), }) } diff --git a/precompiles/precompiles.go b/precompiles/precompiles.go index cdd5e2ff74..24d574695d 100644 --- a/precompiles/precompiles.go +++ b/precompiles/precompiles.go @@ -39,7 +39,7 @@ func StatefulContracts( // Define the prototype contract function. if EnabledStatefulContracts[prototype.ContractAddress] { - prototypeContract := func(_ sdktypes.Context, _ ethparams.Rules) vm.PrecompiledContract { + prototypeContract := func(_ sdktypes.Context, _ ethparams.Rules) vm.StatefulPrecompiledContract { return prototype.NewIPrototypeContract(fungibleKeeper, cdc, gasConfig) } @@ -49,8 +49,8 @@ func StatefulContracts( // Define the staking contract function. if EnabledStatefulContracts[staking.ContractAddress] { - stakingContract := func(_ sdktypes.Context, _ ethparams.Rules) vm.PrecompiledContract { - return staking.NewIStakingContract(stakingKeeper, cdc, gasConfig) + stakingContract := func(ctx sdktypes.Context, _ ethparams.Rules) vm.StatefulPrecompiledContract { + return staking.NewIStakingContract(ctx, stakingKeeper, *fungibleKeeper, bankKeeper, cdc, gasConfig) } // Append the staking contract to the precompiledContracts slice. @@ -58,7 +58,7 @@ func StatefulContracts( } if EnabledStatefulContracts[bank.ContractAddress] { - bankContract := func(ctx sdktypes.Context, _ ethparams.Rules) vm.PrecompiledContract { + bankContract := func(ctx sdktypes.Context, _ ethparams.Rules) vm.StatefulPrecompiledContract { return bank.NewIBankContract(ctx, bankKeeper, *fungibleKeeper, cdc, gasConfig) } diff --git a/precompiles/prototype/prototype.go b/precompiles/prototype/prototype.go index 123885ccb4..31f1bcaaec 100644 --- a/precompiles/prototype/prototype.go +++ b/precompiles/prototype/prototype.go @@ -11,7 +11,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/vm" - ptypes "github.com/zeta-chain/node/precompiles/types" + precompiletypes "github.com/zeta-chain/node/precompiles/types" fungiblekeeper "github.com/zeta-chain/node/x/fungible/keeper" ) @@ -55,7 +55,7 @@ func initABI() { } type Contract struct { - ptypes.BaseContract + precompiletypes.BaseContract fungibleKeeper fungiblekeeper.Keeper cdc codec.Codec @@ -68,7 +68,7 @@ func NewIPrototypeContract( kvGasConfig storetypes.GasConfig, ) *Contract { return &Contract{ - BaseContract: ptypes.NewBaseContract(ContractAddress), + BaseContract: precompiletypes.NewBaseContract(ContractAddress), fungibleKeeper: *fungibleKeeper, cdc: cdc, kvGasConfig: kvGasConfig, @@ -106,7 +106,7 @@ func (c *Contract) RequiredGas(input []byte) uint64 { // Bech32ToHexAddr converts a bech32 address to a hex address. func (c *Contract) Bech32ToHexAddr(method *abi.Method, args []interface{}) ([]byte, error) { if len(args) != 1 { - return nil, &ptypes.ErrInvalidNumberOfArgs{ + return nil, &precompiletypes.ErrInvalidNumberOfArgs{ Got: len(args), Expect: 1, } @@ -144,7 +144,7 @@ func (c *Contract) Bech32ToHexAddr(method *abi.Method, args []interface{}) ([]by // Bech32ify converts a hex address to a bech32 address. func (c *Contract) Bech32ify(method *abi.Method, args []interface{}) ([]byte, error) { if len(args) != 2 { - return nil, &ptypes.ErrInvalidNumberOfArgs{ + return nil, &precompiletypes.ErrInvalidNumberOfArgs{ Got: len(args), Expect: 2, } @@ -198,7 +198,7 @@ func (c *Contract) GetGasStabilityPoolBalance( args []interface{}, ) ([]byte, error) { if len(args) != 1 { - return nil, &(ptypes.ErrInvalidNumberOfArgs{ + return nil, &(precompiletypes.ErrInvalidNumberOfArgs{ Got: len(args), Expect: 1, }) @@ -207,7 +207,7 @@ func (c *Contract) GetGasStabilityPoolBalance( // Unwrap arguments. The chainID is the first and unique argument. chainID, ok := args[0].(int64) if !ok { - return nil, ptypes.ErrInvalidArgument{ + return nil, precompiletypes.ErrInvalidArgument{ Got: chainID, } } @@ -233,7 +233,7 @@ func (c *Contract) Run(evm *vm.EVM, contract *vm.Contract, _ bool) ([]byte, erro return nil, err } - stateDB := evm.StateDB.(ptypes.ExtStateDB) + stateDB := evm.StateDB.(precompiletypes.ExtStateDB) switch method.Name { case GetGasStabilityPoolBalanceName: @@ -252,7 +252,7 @@ func (c *Contract) Run(evm *vm.EVM, contract *vm.Contract, _ bool) ([]byte, erro case Bech32ifyMethodName: return c.Bech32ify(method, args) default: - return nil, ptypes.ErrInvalidMethod{ + return nil, precompiletypes.ErrInvalidMethod{ Method: method.Name, } } diff --git a/precompiles/staking/IStaking.abi b/precompiles/staking/IStaking.abi index fceba4d682..da1a9e6ffc 100644 --- a/precompiles/staking/IStaking.abi +++ b/precompiles/staking/IStaking.abi @@ -1,4 +1,29 @@ [ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "zrc20_distributor", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "zrc20_token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Distributed", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -80,6 +105,30 @@ "name": "Unstake", "type": "event" }, + { + "inputs": [ + { + "internalType": "address", + "name": "zrc20", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "distribute", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "getAllValidators", diff --git a/precompiles/staking/IStaking.gen.go b/precompiles/staking/IStaking.gen.go index 01226d4ecf..d4f7495d37 100644 --- a/precompiles/staking/IStaking.gen.go +++ b/precompiles/staking/IStaking.gen.go @@ -39,7 +39,7 @@ type Validator struct { // IStakingMetaData contains all meta data concerning the IStaking contract. var IStakingMetaData = &bind.MetaData{ - ABI: "[{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"validatorSrc\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"validatorDst\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"MoveStake\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"validator\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"Stake\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"validator\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"Unstake\",\"type\":\"event\"},{\"inputs\":[],\"name\":\"getAllValidators\",\"outputs\":[{\"components\":[{\"internalType\":\"string\",\"name\":\"operatorAddress\",\"type\":\"string\"},{\"internalType\":\"string\",\"name\":\"consensusPubKey\",\"type\":\"string\"},{\"internalType\":\"bool\",\"name\":\"jailed\",\"type\":\"bool\"},{\"internalType\":\"enumBondStatus\",\"name\":\"bondStatus\",\"type\":\"uint8\"}],\"internalType\":\"structValidator[]\",\"name\":\"validators\",\"type\":\"tuple[]\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"},{\"internalType\":\"string\",\"name\":\"validator\",\"type\":\"string\"}],\"name\":\"getShares\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"shares\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"},{\"internalType\":\"string\",\"name\":\"validatorSrc\",\"type\":\"string\"},{\"internalType\":\"string\",\"name\":\"validatorDst\",\"type\":\"string\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"moveStake\",\"outputs\":[{\"internalType\":\"int64\",\"name\":\"completionTime\",\"type\":\"int64\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"},{\"internalType\":\"string\",\"name\":\"validator\",\"type\":\"string\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"stake\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"},{\"internalType\":\"string\",\"name\":\"validator\",\"type\":\"string\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"unstake\",\"outputs\":[{\"internalType\":\"int64\",\"name\":\"completionTime\",\"type\":\"int64\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]", + ABI: "[{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"zrc20_distributor\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"zrc20_token\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"Distributed\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"validatorSrc\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"validatorDst\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"MoveStake\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"validator\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"Stake\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"validator\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"Unstake\",\"type\":\"event\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"zrc20\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"distribute\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getAllValidators\",\"outputs\":[{\"components\":[{\"internalType\":\"string\",\"name\":\"operatorAddress\",\"type\":\"string\"},{\"internalType\":\"string\",\"name\":\"consensusPubKey\",\"type\":\"string\"},{\"internalType\":\"bool\",\"name\":\"jailed\",\"type\":\"bool\"},{\"internalType\":\"enumBondStatus\",\"name\":\"bondStatus\",\"type\":\"uint8\"}],\"internalType\":\"structValidator[]\",\"name\":\"validators\",\"type\":\"tuple[]\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"},{\"internalType\":\"string\",\"name\":\"validator\",\"type\":\"string\"}],\"name\":\"getShares\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"shares\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"},{\"internalType\":\"string\",\"name\":\"validatorSrc\",\"type\":\"string\"},{\"internalType\":\"string\",\"name\":\"validatorDst\",\"type\":\"string\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"moveStake\",\"outputs\":[{\"internalType\":\"int64\",\"name\":\"completionTime\",\"type\":\"int64\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"},{\"internalType\":\"string\",\"name\":\"validator\",\"type\":\"string\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"stake\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"staker\",\"type\":\"address\"},{\"internalType\":\"string\",\"name\":\"validator\",\"type\":\"string\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"unstake\",\"outputs\":[{\"internalType\":\"int64\",\"name\":\"completionTime\",\"type\":\"int64\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]", } // IStakingABI is the input ABI used to generate the binding from. @@ -250,6 +250,27 @@ func (_IStaking *IStakingCallerSession) GetShares(staker common.Address, validat return _IStaking.Contract.GetShares(&_IStaking.CallOpts, staker, validator) } +// Distribute is a paid mutator transaction binding the contract method 0xfb932108. +// +// Solidity: function distribute(address zrc20, uint256 amount) returns(bool success) +func (_IStaking *IStakingTransactor) Distribute(opts *bind.TransactOpts, zrc20 common.Address, amount *big.Int) (*types.Transaction, error) { + return _IStaking.contract.Transact(opts, "distribute", zrc20, amount) +} + +// Distribute is a paid mutator transaction binding the contract method 0xfb932108. +// +// Solidity: function distribute(address zrc20, uint256 amount) returns(bool success) +func (_IStaking *IStakingSession) Distribute(zrc20 common.Address, amount *big.Int) (*types.Transaction, error) { + return _IStaking.Contract.Distribute(&_IStaking.TransactOpts, zrc20, amount) +} + +// Distribute is a paid mutator transaction binding the contract method 0xfb932108. +// +// Solidity: function distribute(address zrc20, uint256 amount) returns(bool success) +func (_IStaking *IStakingTransactorSession) Distribute(zrc20 common.Address, amount *big.Int) (*types.Transaction, error) { + return _IStaking.Contract.Distribute(&_IStaking.TransactOpts, zrc20, amount) +} + // MoveStake is a paid mutator transaction binding the contract method 0xd11a93d0. // // Solidity: function moveStake(address staker, string validatorSrc, string validatorDst, uint256 amount) returns(int64 completionTime) @@ -313,6 +334,160 @@ func (_IStaking *IStakingTransactorSession) Unstake(staker common.Address, valid return _IStaking.Contract.Unstake(&_IStaking.TransactOpts, staker, validator, amount) } +// IStakingDistributedIterator is returned from FilterDistributed and is used to iterate over the raw logs and unpacked data for Distributed events raised by the IStaking contract. +type IStakingDistributedIterator struct { + Event *IStakingDistributed // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *IStakingDistributedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(IStakingDistributed) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(IStakingDistributed) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *IStakingDistributedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *IStakingDistributedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// IStakingDistributed represents a Distributed event raised by the IStaking contract. +type IStakingDistributed struct { + Zrc20Distributor common.Address + Zrc20Token common.Address + Amount *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterDistributed is a free log retrieval operation binding the contract event 0xad4a9acf26d8bba7a8cf1a41160d59be042ee554578e256c98d2ab74cdd43542. +// +// Solidity: event Distributed(address indexed zrc20_distributor, address indexed zrc20_token, uint256 amount) +func (_IStaking *IStakingFilterer) FilterDistributed(opts *bind.FilterOpts, zrc20_distributor []common.Address, zrc20_token []common.Address) (*IStakingDistributedIterator, error) { + + var zrc20_distributorRule []interface{} + for _, zrc20_distributorItem := range zrc20_distributor { + zrc20_distributorRule = append(zrc20_distributorRule, zrc20_distributorItem) + } + var zrc20_tokenRule []interface{} + for _, zrc20_tokenItem := range zrc20_token { + zrc20_tokenRule = append(zrc20_tokenRule, zrc20_tokenItem) + } + + logs, sub, err := _IStaking.contract.FilterLogs(opts, "Distributed", zrc20_distributorRule, zrc20_tokenRule) + if err != nil { + return nil, err + } + return &IStakingDistributedIterator{contract: _IStaking.contract, event: "Distributed", logs: logs, sub: sub}, nil +} + +// WatchDistributed is a free log subscription operation binding the contract event 0xad4a9acf26d8bba7a8cf1a41160d59be042ee554578e256c98d2ab74cdd43542. +// +// Solidity: event Distributed(address indexed zrc20_distributor, address indexed zrc20_token, uint256 amount) +func (_IStaking *IStakingFilterer) WatchDistributed(opts *bind.WatchOpts, sink chan<- *IStakingDistributed, zrc20_distributor []common.Address, zrc20_token []common.Address) (event.Subscription, error) { + + var zrc20_distributorRule []interface{} + for _, zrc20_distributorItem := range zrc20_distributor { + zrc20_distributorRule = append(zrc20_distributorRule, zrc20_distributorItem) + } + var zrc20_tokenRule []interface{} + for _, zrc20_tokenItem := range zrc20_token { + zrc20_tokenRule = append(zrc20_tokenRule, zrc20_tokenItem) + } + + logs, sub, err := _IStaking.contract.WatchLogs(opts, "Distributed", zrc20_distributorRule, zrc20_tokenRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(IStakingDistributed) + if err := _IStaking.contract.UnpackLog(event, "Distributed", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseDistributed is a log parse operation binding the contract event 0xad4a9acf26d8bba7a8cf1a41160d59be042ee554578e256c98d2ab74cdd43542. +// +// Solidity: event Distributed(address indexed zrc20_distributor, address indexed zrc20_token, uint256 amount) +func (_IStaking *IStakingFilterer) ParseDistributed(log types.Log) (*IStakingDistributed, error) { + event := new(IStakingDistributed) + if err := _IStaking.contract.UnpackLog(event, "Distributed", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + // IStakingMoveStakeIterator is returned from FilterMoveStake and is used to iterate over the raw logs and unpacked data for MoveStake events raised by the IStaking contract. type IStakingMoveStakeIterator struct { Event *IStakingMoveStake // Event containing the contract specifics and raw log diff --git a/precompiles/staking/IStaking.json b/precompiles/staking/IStaking.json index cff6a7a365..d4e0bb75f0 100644 --- a/precompiles/staking/IStaking.json +++ b/precompiles/staking/IStaking.json @@ -1,5 +1,30 @@ { "abi": [ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "zrc20_distributor", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "zrc20_token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Distributed", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -81,6 +106,30 @@ "name": "Unstake", "type": "event" }, + { + "inputs": [ + { + "internalType": "address", + "name": "zrc20", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "distribute", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "getAllValidators", diff --git a/precompiles/staking/IStaking.sol b/precompiles/staking/IStaking.sol index c6b2642a4c..dece711d71 100644 --- a/precompiles/staking/IStaking.sol +++ b/precompiles/staking/IStaking.sol @@ -5,9 +5,7 @@ pragma solidity ^0.8.26; address constant ISTAKING_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000066; // 102 /// @dev The IStaking contract's instance. -IStaking constant ISTAKING_CONTRACT = IStaking( - ISTAKING_PRECOMPILE_ADDRESS -); +IStaking constant ISTAKING_CONTRACT = IStaking(ISTAKING_PRECOMPILE_ADDRESS); /// @notice Bond status for validator enum BondStatus { @@ -58,6 +56,16 @@ interface IStaking { uint256 amount ); + /// @notice Distributed event is emitted when distribute function is called successfully. + /// @param zrc20_distributor Distributor address. + /// @param zrc20_token ZRC20 token address. + /// @param amount Distributed amount. + event Distributed( + address indexed zrc20_distributor, + address indexed zrc20_token, + uint256 amount + ); + /// @notice Stake coins to validator /// @param staker Staker address /// @param validator Validator address @@ -95,9 +103,24 @@ interface IStaking { /// @notice Get all validators /// @return validators All validators - function getAllValidators() external view returns (Validator[] calldata validators); + function getAllValidators() + external + view + returns (Validator[] calldata validators); /// @notice Get shares for staker in validator /// @return shares Staker shares in validator - function getShares(address staker, string memory validator) external view returns (uint256 shares); + function getShares( + address staker, + string memory validator + ) external view returns (uint256 shares); + + /// @notice Distribute a ZRC20 token as staking rewards. + /// @param zrc20 The ZRC20 token address to be distributed. + /// @param amount The amount of ZRC20 tokens to distribute. + /// @return success Boolean indicating whether the distribution was successful. + function distribute( + address zrc20, + uint256 amount + ) external returns (bool success); } diff --git a/precompiles/staking/const.go b/precompiles/staking/const.go new file mode 100644 index 0000000000..8500e723f4 --- /dev/null +++ b/precompiles/staking/const.go @@ -0,0 +1,22 @@ +package staking + +const ( + DistributeMethodName = "distribute" + DistributeEventName = "Distributed" + DistributeMethodGas = 10000 + + GetAllValidatorsMethodName = "getAllValidators" + GetSharesMethodName = "getShares" + + MoveStakeMethodName = "moveStake" + MoveStakeEventName = "MoveStake" + MoveStakeMethodGas = 10000 + + StakeMethodName = "stake" + StakeEventName = "Stake" + StakeMethodGas = 10000 + + UnstakeMethodName = "unstake" + UnstakeEventName = "Unstake" + UnstakeMethodGas = 1000 +) diff --git a/precompiles/staking/logs.go b/precompiles/staking/logs.go index ea8e51274c..c8d1db24e2 100644 --- a/precompiles/staking/logs.go +++ b/precompiles/staking/logs.go @@ -10,13 +10,7 @@ import ( "github.com/zeta-chain/node/precompiles/logs" ) -const ( - StakeEventName = "Stake" - UnstakeEventName = "Unstake" - MoveStakeEventName = "MoveStake" -) - -func (c *Contract) AddStakeLog( +func (c *Contract) addStakeLog( ctx sdk.Context, stateDB vm.StateDB, staker common.Address, @@ -49,7 +43,7 @@ func (c *Contract) AddStakeLog( return nil } -func (c *Contract) AddUnstakeLog( +func (c *Contract) addUnstakeLog( ctx sdk.Context, stateDB vm.StateDB, staker common.Address, @@ -81,7 +75,7 @@ func (c *Contract) AddUnstakeLog( return nil } -func (c *Contract) AddMoveStakeLog( +func (c *Contract) addMoveStakeLog( ctx sdk.Context, stateDB vm.StateDB, staker common.Address, @@ -123,3 +117,33 @@ func (c *Contract) AddMoveStakeLog( return nil } + +func (c *Contract) addDistributeLog( + ctx sdk.Context, + stateDB vm.StateDB, + distributor common.Address, + zrc20Token common.Address, + amount *big.Int, +) error { + event := c.Abi().Events[DistributeEventName] + + topics, err := logs.MakeTopics( + event, + []interface{}{distributor}, + []interface{}{zrc20Token}, + ) + if err != nil { + return err + } + + data, err := logs.PackArguments([]logs.Argument{ + {Type: "uint256", Value: amount}, + }) + if err != nil { + return err + } + + logs.AddLog(ctx, c.Address(), stateDB, topics, data) + + return nil +} diff --git a/precompiles/staking/method_distribute.go b/precompiles/staking/method_distribute.go new file mode 100644 index 0000000000..c173517aeb --- /dev/null +++ b/precompiles/staking/method_distribute.go @@ -0,0 +1,112 @@ +package staking + +import ( + "math/big" + + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + + "github.com/zeta-chain/node/precompiles/bank" + precompiletypes "github.com/zeta-chain/node/precompiles/types" + fungibletypes "github.com/zeta-chain/node/x/fungible/types" +) + +// function distribute(address zrc20, uint256 amount) external returns (bool success) +func (c *Contract) distribute( + ctx sdk.Context, + evm *vm.EVM, + contract *vm.Contract, + method *abi.Method, + args []interface{}, +) ([]byte, error) { + if len(args) != 2 { + return nil, &precompiletypes.ErrInvalidNumberOfArgs{ + Got: len(args), + Expect: 2, + } + } + + // Unpack arguments and check if they are valid. + zrc20Addr, amount, err := unpackDistributeArgs(args) + if err != nil { + return nil, err + } + + // Get the original caller address. Necessary for LockZRC20 to work. + caller, err := precompiletypes.GetEVMCallerAddress(evm, contract) + if err != nil { + return nil, err + } + + // Create the coinSet in advance, if this step fails do not lock ZRC20. + coinSet, err := precompiletypes.CreateCoinSet(zrc20Addr, amount) + if err != nil { + return nil, err + } + + // LockZRC20 locks the ZRC20 under the locker address. + // It performs all the necessary checks such as allowance in order to execute a transferFrom. + // - spender is the staking contract address (c.Address()). + // - owner is the caller address. + // - locker is the bank address. Assets are locked under this address to prevent liquidity fragmentation. + if err := c.fungibleKeeper.LockZRC20(ctx, zrc20Addr, c.Address(), caller, bank.ContractAddress, amount); err != nil { + return nil, &precompiletypes.ErrUnexpected{ + When: "LockZRC20InBank", + Got: err.Error(), + } + } + + // With the ZRC20 locked, proceed to mint the cosmos coins. + if err := c.bankKeeper.MintCoins(ctx, fungibletypes.ModuleName, coinSet); err != nil { + return nil, &precompiletypes.ErrUnexpected{ + When: "MintCoins", + Got: err.Error(), + } + } + + // Send the coins to the FeePool. + if err := c.bankKeeper.SendCoinsFromModuleToModule(ctx, fungibletypes.ModuleName, authtypes.FeeCollectorName, coinSet); err != nil { + return nil, &precompiletypes.ErrUnexpected{ + When: "SendCoinsFromModuleToModule", + Got: err.Error(), + } + } + + // Log similar message as in abci DistributeValidatorRewards function. + ctx.Logger().Info( + "Distributing ZRC20 Validator Rewards", + "Total", amount.String(), + "Fee_collector", authtypes.FeeCollectorName, + "Denom", precompiletypes.ZRC20ToCosmosDenom(zrc20Addr), + ) + + if err := c.addDistributeLog(ctx, evm.StateDB, caller, zrc20Addr, amount); err != nil { + return nil, &precompiletypes.ErrUnexpected{ + When: "AddDistributeLog", + Got: err.Error(), + } + } + + return method.Outputs.Pack(true) +} + +func unpackDistributeArgs(args []interface{}) (zrc20Addr common.Address, amount *big.Int, err error) { + zrc20Addr, ok := args[0].(common.Address) + if !ok { + return common.Address{}, nil, &precompiletypes.ErrInvalidAddr{ + Got: zrc20Addr.String(), + } + } + + amount, ok = args[1].(*big.Int) + if !ok || amount == nil || amount.Sign() <= 0 { + return common.Address{}, nil, &precompiletypes.ErrInvalidAmount{ + Got: amount.String(), + } + } + + return zrc20Addr, amount, nil +} diff --git a/precompiles/staking/method_distribute_test.go b/precompiles/staking/method_distribute_test.go new file mode 100644 index 0000000000..dddb467b74 --- /dev/null +++ b/precompiles/staking/method_distribute_test.go @@ -0,0 +1,283 @@ +package staking + +import ( + "math/big" + "testing" + + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/stretchr/testify/require" + precompiletypes "github.com/zeta-chain/node/precompiles/types" +) + +func Test_Distribute(t *testing.T) { + feeCollectorAddress := authtypes.NewModuleAddress(authtypes.FeeCollectorName).String() + + t.Run("should fail to run distribute as read only method", func(t *testing.T) { + // Setup test. + s := newTestSuite(t) + zrc20Denom := precompiletypes.ZRC20ToCosmosDenom(s.zrc20Address) + + // Setup method input. + s.mockVMContract.Input = packInputArgs( + t, + s.methodID, + []interface{}{s.zrc20Address, big.NewInt(0)}..., + ) + + // Call method as read only. + result, err := s.contract.Run(s.mockEVM, s.mockVMContract, true) + + // Check error and result. + require.ErrorIs(t, err, precompiletypes.ErrWriteMethod{ + Method: DistributeMethodName, + }) + + // Result is empty as the write check is done before executing distribute() function. + // On-chain this would look like reverting, so staticcall is properly reverted. + require.Empty(t, result) + + // End fee collector balance should be 0. + balance, err := s.sdkKeepers.BankKeeper.Balance(s.ctx, &banktypes.QueryBalanceRequest{ + Address: feeCollectorAddress, + Denom: zrc20Denom, + }) + require.NoError(t, err) + require.Equal(t, uint64(0), balance.Balance.Amount.Uint64()) + }) + + t.Run("should fail to distribute with 0 token balance", func(t *testing.T) { + // Setup test. + s := newTestSuite(t) + zrc20Denom := precompiletypes.ZRC20ToCosmosDenom(s.zrc20Address) + + // Setup method input. + s.mockVMContract.Input = packInputArgs( + t, + s.methodID, + []interface{}{s.zrc20Address, big.NewInt(0)}..., + ) + + // Call method. + success, err := s.contract.Run(s.mockEVM, s.mockVMContract, false) + + // Check error. + require.ErrorAs( + t, + precompiletypes.ErrInvalidAmount{ + Got: "0", + }, + err, + ) + + // Unpack and check result boolean. + res, err := s.methodID.Outputs.Unpack(success) + require.NoError(t, err) + + ok := res[0].(bool) + require.False(t, ok) + + // End fee collector balance should be 0. + balance, err := s.sdkKeepers.BankKeeper.Balance(s.ctx, &banktypes.QueryBalanceRequest{ + Address: feeCollectorAddress, + Denom: zrc20Denom, + }) + require.NoError(t, err) + require.Equal(t, uint64(0), balance.Balance.Amount.Uint64()) + }) + + t.Run("should fail to distribute with 0 allowance", func(t *testing.T) { + // Setup test. + s := newTestSuite(t) + zrc20Denom := precompiletypes.ZRC20ToCosmosDenom(s.zrc20Address) + + // Set caller balance. + _, err := s.fungibleKeeper.DepositZRC20(s.ctx, s.zrc20Address, s.defaultCaller, big.NewInt(1000)) + require.NoError(t, err) + + // Setup method input. + s.mockVMContract.Input = packInputArgs( + t, + s.methodID, + []interface{}{s.zrc20Address, big.NewInt(1000)}..., + ) + + // Call method. + success, err := s.contract.Run(s.mockEVM, s.mockVMContract, false) + + // Check error. + require.Error(t, err) + require.Contains(t, err.Error(), "invalid allowance, got 0") + + // Unpack and check result boolean. + res, err := s.methodID.Outputs.Unpack(success) + require.NoError(t, err) + + ok := res[0].(bool) + require.False(t, ok) + + // End fee collector balance should be 0. + balance, err := s.sdkKeepers.BankKeeper.Balance(s.ctx, &banktypes.QueryBalanceRequest{ + Address: feeCollectorAddress, + Denom: zrc20Denom, + }) + require.NoError(t, err) + require.Equal(t, uint64(0), balance.Balance.Amount.Uint64()) + }) + + t.Run("should fail to distribute 0 token", func(t *testing.T) { + // Setup test. + s := newTestSuite(t) + zrc20Denom := precompiletypes.ZRC20ToCosmosDenom(s.zrc20Address) + + // Set caller balance. + _, err := s.fungibleKeeper.DepositZRC20(s.ctx, s.zrc20Address, s.defaultCaller, big.NewInt(1000)) + require.NoError(t, err) + + // Allow staking to spend ZRC20 tokens. + allowStaking(t, s, big.NewInt(1000)) + + // Setup method input. + s.mockVMContract.Input = packInputArgs( + t, + s.methodID, + []interface{}{s.zrc20Address, big.NewInt(0)}..., + ) + + // Call method. + success, err := s.contract.Run(s.mockEVM, s.mockVMContract, false) + + // Check error. + require.Error(t, err) + require.Contains(t, err.Error(), "invalid token amount: 0") + + // Unpack and check result boolean. + res, err := s.methodID.Outputs.Unpack(success) + require.NoError(t, err) + + ok := res[0].(bool) + require.False(t, ok) + + // End fee collector balance should be 0. + balance, err := s.sdkKeepers.BankKeeper.Balance(s.ctx, &banktypes.QueryBalanceRequest{ + Address: feeCollectorAddress, + Denom: zrc20Denom, + }) + require.NoError(t, err) + require.Equal(t, uint64(0), balance.Balance.Amount.Uint64()) + }) + + t.Run("should fail to distribute more than allowed to staking", func(t *testing.T) { + // Setup test. + s := newTestSuite(t) + zrc20Denom := precompiletypes.ZRC20ToCosmosDenom(s.zrc20Address) + + // Set caller balance. + _, err := s.fungibleKeeper.DepositZRC20(s.ctx, s.zrc20Address, s.defaultCaller, big.NewInt(1000)) + require.NoError(t, err) + + // Allow staking to spend ZRC20 tokens. + allowStaking(t, s, big.NewInt(999)) + + // Setup method input. + s.mockVMContract.Input = packInputArgs( + t, + s.methodID, + []interface{}{s.zrc20Address, big.NewInt(1000)}..., + ) + + // Call method. + success, err := s.contract.Run(s.mockEVM, s.mockVMContract, false) + + // Check error. + require.Error(t, err) + require.Contains(t, err.Error(), "invalid allowance, got 999, wanted 1000") + + // Unpack and check result boolean. + res, err := s.methodID.Outputs.Unpack(success) + require.NoError(t, err) + + ok := res[0].(bool) + require.False(t, ok) + + // End fee collector balance should be 0. + balance, err := s.sdkKeepers.BankKeeper.Balance(s.ctx, &banktypes.QueryBalanceRequest{ + Address: feeCollectorAddress, + Denom: zrc20Denom, + }) + require.NoError(t, err) + require.Equal(t, uint64(0), balance.Balance.Amount.Uint64()) + }) + + t.Run("should fail to distribute more than user balance", func(t *testing.T) { + // Setup test. + s := newTestSuite(t) + zrc20Denom := precompiletypes.ZRC20ToCosmosDenom(s.zrc20Address) + + // Set caller balance. + _, err := s.fungibleKeeper.DepositZRC20(s.ctx, s.zrc20Address, s.defaultCaller, big.NewInt(1000)) + require.NoError(t, err) + + // Allow staking to spend ZRC20 tokens. + allowStaking(t, s, big.NewInt(100000)) + + // Setup method input. + s.mockVMContract.Input = packInputArgs( + t, + s.methodID, + []interface{}{s.zrc20Address, big.NewInt(1001)}..., + ) + + success, err := s.contract.Run(s.mockEVM, s.mockVMContract, false) + + // Check error. + require.Error(t, err) + require.Contains(t, err.Error(), "execution reverted") + + // Unpack and check result boolean. + res, err := s.methodID.Outputs.Unpack(success) + require.NoError(t, err) + + ok := res[0].(bool) + require.False(t, ok) + + // End fee collector balance should be 0. + balance, err := s.sdkKeepers.BankKeeper.Balance(s.ctx, &banktypes.QueryBalanceRequest{ + Address: feeCollectorAddress, + Denom: zrc20Denom, + }) + require.NoError(t, err) + require.Equal(t, uint64(0), balance.Balance.Amount.Uint64()) + }) + + t.Run("should distribute and lock ZRC20", func(t *testing.T) { + // Setup test. + s := newTestSuite(t) + + // Set caller balance. + _, err := s.fungibleKeeper.DepositZRC20(s.ctx, s.zrc20Address, s.defaultCaller, big.NewInt(1000)) + require.NoError(t, err) + + // Allow staking to spend ZRC20 tokens. + allowStaking(t, s, big.NewInt(1000)) + + // Setup method input. + s.mockVMContract.Input = packInputArgs( + t, + s.methodID, + []interface{}{s.zrc20Address, big.NewInt(1000)}..., + ) + + success, err := s.contract.Run(s.mockEVM, s.mockVMContract, false) + + // Check error. + require.NoError(t, err) + + // Unpack and check result boolean. + res, err := s.methodID.Outputs.Unpack(success) + require.NoError(t, err) + + ok := res[0].(bool) + require.True(t, ok) + }) +} diff --git a/precompiles/staking/method_get_all_validators.go b/precompiles/staking/method_get_all_validators.go new file mode 100644 index 0000000000..2187f2858a --- /dev/null +++ b/precompiles/staking/method_get_all_validators.go @@ -0,0 +1,27 @@ +package staking + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/accounts/abi" +) + +func (c *Contract) GetAllValidators( + ctx sdk.Context, + method *abi.Method, +) ([]byte, error) { + validators := c.stakingKeeper.GetAllValidators(ctx) + + validatorsRes := make([]Validator, len(validators)) + for i, v := range validators { + validatorsRes[i] = Validator{ + OperatorAddress: v.OperatorAddress, + ConsensusPubKey: v.ConsensusPubkey.String(), + // Safe casting from int32 to uint8, as BondStatus is an enum. + // #nosec G115 always in range + BondStatus: uint8(v.Status), + Jailed: v.Jailed, + } + } + + return method.Outputs.Pack(validatorsRes) +} diff --git a/precompiles/staking/method_get_all_validators_test.go b/precompiles/staking/method_get_all_validators_test.go new file mode 100644 index 0000000000..8a80793de3 --- /dev/null +++ b/precompiles/staking/method_get_all_validators_test.go @@ -0,0 +1,57 @@ +package staking + +import ( + "math/rand" + "testing" + + "github.com/stretchr/testify/require" + "github.com/zeta-chain/node/testutil/sample" +) + +func Test_GetAllValidators(t *testing.T) { + t.Run("should return empty array if validators not set", func(t *testing.T) { + // ARRANGE + s := newTestSuite(t) + + // Clean all validators. + validatorsList := s.sdkKeepers.StakingKeeper.GetAllValidators(s.ctx) + for _, v := range validatorsList { + s.sdkKeepers.StakingKeeper.RemoveValidator(s.ctx, v.GetOperator()) + } + + methodID := s.contractABI.Methods[GetAllValidatorsMethodName] + s.mockVMContract.Input = methodID.ID + + // ACT + validators, err := s.contract.Run(s.mockEVM, s.mockVMContract, false) + + // ASSERT + require.NoError(t, err) + + res, err := methodID.Outputs.Unpack(validators) + require.NoError(t, err) + + require.Empty(t, res[0]) + }) + + t.Run("should return validators if set", func(t *testing.T) { + // ARRANGE + s := newTestSuite(t) + methodID := s.contractABI.Methods[GetAllValidatorsMethodName] + s.mockVMContract.Input = methodID.ID + r := rand.New(rand.NewSource(42)) + validator := sample.Validator(t, r) + s.sdkKeepers.StakingKeeper.SetValidator(s.ctx, validator) + + // ACT + validators, err := s.contract.Run(s.mockEVM, s.mockVMContract, false) + + // ASSERT + require.NoError(t, err) + + res, err := methodID.Outputs.Unpack(validators) + require.NoError(t, err) + + require.NotEmpty(t, res[0]) + }) +} diff --git a/precompiles/staking/method_get_shares.go b/precompiles/staking/method_get_shares.go new file mode 100644 index 0000000000..fcf6ce7d1f --- /dev/null +++ b/precompiles/staking/method_get_shares.go @@ -0,0 +1,50 @@ +package staking + +import ( + "math/big" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + + precompiletypes "github.com/zeta-chain/node/precompiles/types" +) + +func (c *Contract) GetShares( + ctx sdk.Context, + method *abi.Method, + args []interface{}, +) ([]byte, error) { + if len(args) != 2 { + return nil, &(precompiletypes.ErrInvalidNumberOfArgs{ + Got: len(args), + Expect: 2, + }) + } + stakerAddress, ok := args[0].(common.Address) + if !ok { + return nil, precompiletypes.ErrInvalidArgument{ + Got: args[0], + } + } + + validatorAddress, ok := args[1].(string) + if !ok { + return nil, precompiletypes.ErrInvalidArgument{ + Got: args[1], + } + } + + validator, err := sdk.ValAddressFromBech32(validatorAddress) + if err != nil { + return nil, err + } + + delegation := c.stakingKeeper.Delegation(ctx, sdk.AccAddress(stakerAddress.Bytes()), validator) + shares := big.NewInt(0) + if delegation != nil { + shares = delegation.GetShares().BigInt() + } + + return method.Outputs.Pack(shares) +} diff --git a/precompiles/staking/method_get_shares_test.go b/precompiles/staking/method_get_shares_test.go new file mode 100644 index 0000000000..d0038886f4 --- /dev/null +++ b/precompiles/staking/method_get_shares_test.go @@ -0,0 +1,115 @@ +package staking + +import ( + "math/rand" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + "github.com/zeta-chain/node/testutil/sample" +) + +func Test_GetShares(t *testing.T) { + // Disabled temporarily because the staking functions were disabled. + // Issue: https://github.com/zeta-chain/node/issues/3009 + // t.Run("should return stakes", func(t *testing.T) { + // // ARRANGE + // s := newTestSuite(t) + // methodID := s.contractABI.Methods[GetSharesMethodName] + // r := rand.New(rand.NewSource(42)) + // validator := sample.Validator(t, r) + // s.sdkKeepers.StakingKeeper.SetValidator(s.ctx, validator) + + // staker := sample.Bech32AccAddress() + // stakerEthAddr := common.BytesToAddress(staker.Bytes()) + // coins := sample.Coins() + // err := s.sdkKeepers.BankKeeper.MintCoins(s.ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = s.sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(s.ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + + // stakeArgs := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} + + // stakeMethodID := s.contractABI.Methods[StakeMethodName] + + // // ACT + // _, err = s.contract.Stake(s.ctx, s.mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &stakeMethodID, stakeArgs) + // require.NoError(t, err) + + // // ASSERT + // args := []interface{}{stakerEthAddr, validator.OperatorAddress} + // s.mockVMContract.Input = packInputArgs(t, methodID, args...) + // stakes, err := s.contract.Run(s.mockEVM, s.mockVMContract, false) + // require.NoError(t, err) + + // res, err := methodID.Outputs.Unpack(stakes) + // require.NoError(t, err) + // require.Equal( + // t, + // fmt.Sprintf("%d000000000000000000", coins.AmountOf(config.BaseDenom).BigInt().Int64()), + // res[0].(*big.Int).String(), + // ) + // }) + + t.Run("should fail if wrong args amount", func(t *testing.T) { + // ARRANGE + s := newTestSuite(t) + methodID := s.contractABI.Methods[GetSharesMethodName] + staker := sample.Bech32AccAddress() + stakerEthAddr := common.BytesToAddress(staker.Bytes()) + args := []interface{}{stakerEthAddr} + + // ACT + _, err := s.contract.GetShares(s.ctx, &methodID, args) + + // ASSERT + require.Error(t, err) + }) + + t.Run("should fail if invalid staker arg", func(t *testing.T) { + // ARRANGE + s := newTestSuite(t) + methodID := s.contractABI.Methods[GetSharesMethodName] + r := rand.New(rand.NewSource(42)) + validator := sample.Validator(t, r) + args := []interface{}{42, validator.OperatorAddress} + + // ACT + _, err := s.contract.GetShares(s.ctx, &methodID, args) + + // ASSERT + require.Error(t, err) + }) + + t.Run("should fail if invalid val address", func(t *testing.T) { + // ARRANGE + s := newTestSuite(t) + methodID := s.contractABI.Methods[GetSharesMethodName] + staker := sample.Bech32AccAddress() + stakerEthAddr := common.BytesToAddress(staker.Bytes()) + args := []interface{}{stakerEthAddr, staker.String()} + + // ACT + _, err := s.contract.GetShares(s.ctx, &methodID, args) + + // ASSERT + require.Error(t, err) + }) + + t.Run("should fail if invalid val address format", func(t *testing.T) { + // ARRANGE + s := newTestSuite(t) + methodID := s.contractABI.Methods[GetSharesMethodName] + staker := sample.Bech32AccAddress() + stakerEthAddr := common.BytesToAddress(staker.Bytes()) + args := []interface{}{stakerEthAddr, 42} + + // ACT + _, err := s.contract.GetShares(s.ctx, &methodID, args) + + // ASSERT + require.Error(t, err) + }) +} diff --git a/precompiles/staking/method_move_stake.go b/precompiles/staking/method_move_stake.go new file mode 100644 index 0000000000..71c43bdd7d --- /dev/null +++ b/precompiles/staking/method_move_stake.go @@ -0,0 +1,85 @@ +package staking + +import ( + "fmt" + "math/big" + + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + + precompiletypes "github.com/zeta-chain/node/precompiles/types" +) + +func (c *Contract) MoveStake( + ctx sdk.Context, + evm *vm.EVM, + contract *vm.Contract, + method *abi.Method, + args []interface{}, +) ([]byte, error) { + if len(args) != 4 { + return nil, &(precompiletypes.ErrInvalidNumberOfArgs{ + Got: len(args), + Expect: 4, + }) + } + + stakerAddress, ok := args[0].(common.Address) + if !ok { + return nil, precompiletypes.ErrInvalidArgument{ + Got: args[0], + } + } + + if contract.CallerAddress != stakerAddress { + return nil, fmt.Errorf("caller is not staker address") + } + + validatorSrcAddress, ok := args[1].(string) + if !ok { + return nil, precompiletypes.ErrInvalidArgument{ + Got: args[1], + } + } + + validatorDstAddress, ok := args[2].(string) + if !ok { + return nil, precompiletypes.ErrInvalidArgument{ + Got: args[2], + } + } + + amount, ok := args[3].(*big.Int) + if !ok { + return nil, precompiletypes.ErrInvalidArgument{ + Got: args[3], + } + } + + msgServer := stakingkeeper.NewMsgServerImpl(&c.stakingKeeper) + res, err := msgServer.BeginRedelegate(ctx, &stakingtypes.MsgBeginRedelegate{ + DelegatorAddress: sdk.AccAddress(stakerAddress.Bytes()).String(), + ValidatorSrcAddress: validatorSrcAddress, + ValidatorDstAddress: validatorDstAddress, + Amount: sdk.Coin{ + Denom: c.stakingKeeper.BondDenom(ctx), + Amount: math.NewIntFromBigInt(amount), + }, + }) + if err != nil { + return nil, err + } + + stateDB := evm.StateDB.(precompiletypes.ExtStateDB) + err = c.addMoveStakeLog(ctx, stateDB, stakerAddress, validatorSrcAddress, validatorDstAddress, amount) + if err != nil { + return nil, err + } + + return method.Outputs.Pack(res.GetCompletionTime().UTC().Unix()) +} diff --git a/precompiles/staking/method_move_stake_test.go b/precompiles/staking/method_move_stake_test.go new file mode 100644 index 0000000000..8882442069 --- /dev/null +++ b/precompiles/staking/method_move_stake_test.go @@ -0,0 +1,481 @@ +package staking + +import ( + "math/rand" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + "github.com/zeta-chain/node/cmd/zetacored/config" + precompiletypes "github.com/zeta-chain/node/precompiles/types" + "github.com/zeta-chain/node/testutil/sample" + fungibletypes "github.com/zeta-chain/node/x/fungible/types" +) + +func Test_MoveStake(t *testing.T) { + // Disabled until further notice, check https://github.com/zeta-chain/node/issues/3005. + t.Run("should fail with error disabled", func(t *testing.T) { + // ARRANGE + s := newTestSuite(t) + methodID := s.contractABI.Methods[MoveStakeMethodName] + r := rand.New(rand.NewSource(42)) + validatorSrc := sample.Validator(t, r) + s.sdkKeepers.StakingKeeper.SetValidator(s.ctx, validatorSrc) + validatorDest := sample.Validator(t, r) + + staker := sample.Bech32AccAddress() + stakerEthAddr := common.BytesToAddress(staker.Bytes()) + coins := sample.Coins() + err := s.sdkKeepers.BankKeeper.MintCoins(s.ctx, fungibletypes.ModuleName, sample.Coins()) + require.NoError(t, err) + err = s.sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(s.ctx, fungibletypes.ModuleName, staker, coins) + require.NoError(t, err) + + stakerAddr := common.BytesToAddress(staker.Bytes()) + s.mockVMContract.CallerAddress = stakerAddr + + argsStake := []interface{}{ + stakerEthAddr, + validatorSrc.OperatorAddress, + coins.AmountOf(config.BaseDenom).BigInt(), + } + + // stake to validator src + stakeMethodID := s.contractABI.Methods[StakeMethodName] + s.mockVMContract.Input = packInputArgs(t, stakeMethodID, argsStake...) + _, err = s.contract.Run(s.mockEVM, s.mockVMContract, false) + require.Error(t, err) + require.ErrorIs(t, err, precompiletypes.ErrDisabledMethod{ + Method: StakeMethodName, + }) + + argsMoveStake := []interface{}{ + stakerEthAddr, + validatorSrc.OperatorAddress, + validatorDest.OperatorAddress, + coins.AmountOf(config.BaseDenom).BigInt(), + } + s.mockVMContract.Input = packInputArgs(t, methodID, argsMoveStake...) + + // ACT + _, err = s.contract.Run(s.mockEVM, s.mockVMContract, false) + + // ASSERT + require.Error(t, err) + require.ErrorIs(t, err, precompiletypes.ErrDisabledMethod{ + Method: MoveStakeMethodName, + }) + }) + + // t.Run("should fail in read only method", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) + // methodID := abi.Methods[MoveStakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validatorSrc := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorSrc) + // validatorDest := sample.Validator(t, r) + + // staker := sample.Bech32AccAddress() + // stakerEthAddr := common.BytesToAddress(staker.Bytes()) + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + // mockVMContract.CallerAddress = stakerAddr + + // argsStake := []interface{}{ + // stakerEthAddr, + // validatorSrc.OperatorAddress, + // coins.AmountOf(config.BaseDenom).BigInt(), + // } + + // // stake to validator src + // stakeMethodID := abi.Methods[StakeMethodName] + // mockVMContract.Input = packInputArgs(t, stakeMethodID, argsStake...) + // _, err = contract.Run(mockEVM, mockVMContract, false) + // require.NoError(t, err) + + // argsMoveStake := []interface{}{ + // stakerEthAddr, + // validatorSrc.OperatorAddress, + // validatorDest.OperatorAddress, + // coins.AmountOf(config.BaseDenom).BigInt(), + // } + // mockVMContract.Input = packInputArgs(t, methodID, argsMoveStake...) + + // // ACT + // _, err = contract.Run(mockEVM, mockVMContract, true) + + // // ASSERT + // require.ErrorIs(t, err, ptypes.ErrWriteMethod{Method: MoveStakeMethodName}) + // }) + + // t.Run("should fail if validator dest doesn't exist", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) + // methodID := abi.Methods[MoveStakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validatorSrc := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorSrc) + // validatorDest := sample.Validator(t, r) + + // staker := sample.Bech32AccAddress() + // stakerEthAddr := common.BytesToAddress(staker.Bytes()) + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + // mockVMContract.CallerAddress = stakerAddr + + // argsStake := []interface{}{ + // stakerEthAddr, + // validatorSrc.OperatorAddress, + // coins.AmountOf(config.BaseDenom).BigInt(), + // } + + // // stake to validator src + // stakeMethodID := abi.Methods[StakeMethodName] + // mockVMContract.Input = packInputArgs(t, stakeMethodID, argsStake...) + // _, err = contract.Run(mockEVM, mockVMContract, false) + // require.NoError(t, err) + + // argsMoveStake := []interface{}{ + // stakerEthAddr, + // validatorSrc.OperatorAddress, + // validatorDest.OperatorAddress, + // coins.AmountOf(config.BaseDenom).BigInt(), + // } + // mockVMContract.Input = packInputArgs(t, methodID, argsMoveStake...) + + // // ACT + // _, err = contract.Run(mockEVM, mockVMContract, false) + + // // ASSERT + // require.Error(t, err) + // }) + + // t.Run("should move stake", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) + // methodID := abi.Methods[MoveStakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validatorSrc := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorSrc) + // validatorDest := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorDest) + + // staker := sample.Bech32AccAddress() + // stakerEthAddr := common.BytesToAddress(staker.Bytes()) + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + // mockVMContract.CallerAddress = stakerAddr + // argsStake := []interface{}{ + // stakerEthAddr, + // validatorSrc.OperatorAddress, + // coins.AmountOf(config.BaseDenom).BigInt(), + // } + + // // stake to validator src + // stakeMethodID := abi.Methods[StakeMethodName] + // mockVMContract.Input = packInputArgs(t, stakeMethodID, argsStake...) + // _, err = contract.Run(mockEVM, mockVMContract, false) + // require.NoError(t, err) + + // argsMoveStake := []interface{}{ + // stakerEthAddr, + // validatorSrc.OperatorAddress, + // validatorDest.OperatorAddress, + // coins.AmountOf(config.BaseDenom).BigInt(), + // } + // mockVMContract.Input = packInputArgs(t, methodID, argsMoveStake...) + + // // ACT + // // move stake to validator dest + // _, err = contract.Run(mockEVM, mockVMContract, false) + + // // ASSERT + // require.NoError(t, err) + // }) + + // t.Run("should fail if staker is invalid arg", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, _ := setup(t) + // methodID := abi.Methods[MoveStakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validatorSrc := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorSrc) + // validatorDest := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorDest) + + // staker := sample.Bech32AccAddress() + // stakerEthAddr := common.BytesToAddress(staker.Bytes()) + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + + // argsStake := []interface{}{ + // stakerEthAddr, + // validatorSrc.OperatorAddress, + // coins.AmountOf(config.BaseDenom).BigInt(), + // } + + // // stake to validator src + // stakeMethodID := abi.Methods[StakeMethodName] + // _, err = contract.Stake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &stakeMethodID, argsStake) + // require.NoError(t, err) + + // argsMoveStake := []interface{}{ + // 42, + // validatorSrc.OperatorAddress, + // validatorDest.OperatorAddress, + // coins.AmountOf(config.BaseDenom).BigInt(), + // } + + // // ACT + // _, err = contract.MoveStake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &methodID, argsMoveStake) + + // // ASSERT + // require.Error(t, err) + // }) + + // t.Run("should fail if validator src is invalid arg", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, _ := setup(t) + // methodID := abi.Methods[MoveStakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validatorSrc := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorSrc) + // validatorDest := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorDest) + + // staker := sample.Bech32AccAddress() + // stakerEthAddr := common.BytesToAddress(staker.Bytes()) + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + + // argsStake := []interface{}{ + // stakerEthAddr, + // validatorSrc.OperatorAddress, + // coins.AmountOf(config.BaseDenom).BigInt(), + // } + + // // stake to validator src + // stakeMethodID := abi.Methods[StakeMethodName] + // _, err = contract.Stake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &stakeMethodID, argsStake) + // require.NoError(t, err) + + // argsMoveStake := []interface{}{ + // stakerEthAddr, + // 42, + // validatorDest.OperatorAddress, + // coins.AmountOf(config.BaseDenom).BigInt(), + // } + + // // ACT + // _, err = contract.MoveStake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &methodID, argsMoveStake) + + // // ASSERT + // require.Error(t, err) + // }) + + // t.Run("should fail if validator dest is invalid arg", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, _ := setup(t) + // methodID := abi.Methods[MoveStakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validatorSrc := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorSrc) + // validatorDest := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorDest) + + // staker := sample.Bech32AccAddress() + // stakerEthAddr := common.BytesToAddress(staker.Bytes()) + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + + // argsStake := []interface{}{ + // stakerEthAddr, + // validatorSrc.OperatorAddress, + // coins.AmountOf(config.BaseDenom).BigInt(), + // } + + // // stake to validator src + // stakeMethodID := abi.Methods[StakeMethodName] + // _, err = contract.Stake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &stakeMethodID, argsStake) + // require.NoError(t, err) + + // argsMoveStake := []interface{}{ + // stakerEthAddr, + // validatorSrc.OperatorAddress, + // 42, + // coins.AmountOf(config.BaseDenom).BigInt(), + // } + + // // ACT + // _, err = contract.MoveStake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &methodID, argsMoveStake) + + // // ASSERT + // require.Error(t, err) + // }) + + // t.Run("should fail if amount is invalid arg", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, _ := setup(t) + // methodID := abi.Methods[MoveStakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validatorSrc := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorSrc) + // validatorDest := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorDest) + + // staker := sample.Bech32AccAddress() + // stakerEthAddr := common.BytesToAddress(staker.Bytes()) + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + + // argsStake := []interface{}{ + // stakerEthAddr, + // validatorSrc.OperatorAddress, + // coins.AmountOf(config.BaseDenom).BigInt(), + // } + + // // stake to validator src + // stakeMethodID := abi.Methods[StakeMethodName] + // _, err = contract.Stake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &stakeMethodID, argsStake) + // require.NoError(t, err) + + // argsMoveStake := []interface{}{ + // stakerEthAddr, + // validatorSrc.OperatorAddress, + // validatorDest.OperatorAddress, + // coins.AmountOf(config.BaseDenom).Uint64(), + // } + + // // ACT + // _, err = contract.MoveStake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &methodID, argsMoveStake) + + // // ASSERT + // require.Error(t, err) + // }) + + // t.Run("should fail if wrong args amount", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, _ := setup(t) + // methodID := abi.Methods[MoveStakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validatorSrc := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorSrc) + // validatorDest := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorDest) + + // staker := sample.Bech32AccAddress() + // stakerEthAddr := common.BytesToAddress(staker.Bytes()) + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + + // argsStake := []interface{}{ + // stakerEthAddr, + // validatorSrc.OperatorAddress, + // coins.AmountOf(config.BaseDenom).BigInt(), + // } + + // // stake to validator src + // stakeMethodID := abi.Methods[StakeMethodName] + // _, err = contract.Stake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &stakeMethodID, argsStake) + // require.NoError(t, err) + + // argsMoveStake := []interface{}{stakerEthAddr, validatorSrc.OperatorAddress, validatorDest.OperatorAddress} + + // // ACT + // _, err = contract.MoveStake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &methodID, argsMoveStake) + + // // ASSERT + // require.Error(t, err) + // }) + + // t.Run("should fail if caller is not staker", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) + // methodID := abi.Methods[MoveStakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validatorSrc := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorSrc) + // validatorDest := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorDest) + + // staker := sample.Bech32AccAddress() + // stakerEthAddr := common.BytesToAddress(staker.Bytes()) + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + // mockVMContract.CallerAddress = stakerAddr + // argsStake := []interface{}{ + // stakerEthAddr, + // validatorSrc.OperatorAddress, + // coins.AmountOf(config.BaseDenom).BigInt(), + // } + + // // stake to validator src + // stakeMethodID := abi.Methods[StakeMethodName] + // mockVMContract.Input = packInputArgs(t, stakeMethodID, argsStake...) + // _, err = contract.Run(mockEVM, mockVMContract, false) + // require.NoError(t, err) + + // argsMoveStake := []interface{}{ + // stakerEthAddr, + // validatorSrc.OperatorAddress, + // validatorDest.OperatorAddress, + // coins.AmountOf(config.BaseDenom).BigInt(), + // } + // mockVMContract.Input = packInputArgs(t, methodID, argsMoveStake...) + + // callerEthAddr := common.BytesToAddress(sample.Bech32AccAddress().Bytes()) + // mockVMContract.CallerAddress = callerEthAddr + + // // ACT + // _, err = contract.Run(mockEVM, mockVMContract, false) + + // // ASSERT + // require.ErrorContains(t, err, "caller is not staker") + // }) +} diff --git a/precompiles/staking/method_stake.go b/precompiles/staking/method_stake.go new file mode 100644 index 0000000000..90a0affb11 --- /dev/null +++ b/precompiles/staking/method_stake.go @@ -0,0 +1,92 @@ +package staking + +import ( + "fmt" + "math/big" + + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/holiman/uint256" + + precompiletypes "github.com/zeta-chain/node/precompiles/types" +) + +func (c *Contract) Stake( + ctx sdk.Context, + evm *vm.EVM, + contract *vm.Contract, + method *abi.Method, + args []interface{}, +) ([]byte, error) { + if len(args) != 3 { + return nil, &(precompiletypes.ErrInvalidNumberOfArgs{ + Got: len(args), + Expect: 3, + }) + } + + stakerAddress, ok := args[0].(common.Address) + if !ok { + return nil, precompiletypes.ErrInvalidArgument{ + Got: args[0], + } + } + + if contract.CallerAddress != stakerAddress { + return nil, fmt.Errorf("caller is not staker address") + } + + validatorAddress, ok := args[1].(string) + if !ok { + return nil, precompiletypes.ErrInvalidArgument{ + Got: args[1], + } + } + + amount, ok := args[2].(*big.Int) + if !ok { + return nil, precompiletypes.ErrInvalidArgument{ + Got: args[2], + } + } + + msgServer := stakingkeeper.NewMsgServerImpl(&c.stakingKeeper) + _, err := msgServer.Delegate(ctx, &stakingtypes.MsgDelegate{ + DelegatorAddress: sdk.AccAddress(stakerAddress.Bytes()).String(), + ValidatorAddress: validatorAddress, + Amount: sdk.Coin{ + Denom: c.stakingKeeper.BondDenom(ctx), + Amount: math.NewIntFromBigInt(amount), + }, + }) + if err != nil { + return nil, err + } + + amountUint256, overflowed := uint256.FromBig(amount) + if overflowed { + return nil, precompiletypes.ErrInvalidArgument{ + Got: args[2], + } + } + + // if caller is not the same as origin it means call is coming through smart contract, + // and because state of smart contract calling precompile might be updated as well + // manually reduce amount in stateDB, so it is properly reflected in bank module + stateDB := evm.StateDB.(precompiletypes.ExtStateDB) + if contract.CallerAddress != evm.Origin { + stateDB.SubBalance(stakerAddress, amountUint256) + } + + err = c.addStakeLog(ctx, stateDB, stakerAddress, validatorAddress, amount) + if err != nil { + return nil, err + } + + return method.Outputs.Pack(true) +} diff --git a/precompiles/staking/method_stake_test.go b/precompiles/staking/method_stake_test.go new file mode 100644 index 0000000000..bd5558abdb --- /dev/null +++ b/precompiles/staking/method_stake_test.go @@ -0,0 +1,316 @@ +package staking + +import ( + "math/rand" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + "github.com/zeta-chain/node/cmd/zetacored/config" + precompiletypes "github.com/zeta-chain/node/precompiles/types" + "github.com/zeta-chain/node/testutil/sample" + fungibletypes "github.com/zeta-chain/node/x/fungible/types" +) + +func Test_Stake(t *testing.T) { + // Disabled until further notice, check https://github.com/zeta-chain/node/issues/3005. + t.Run("should fail with error disabled", func(t *testing.T) { + // ARRANGE + s := newTestSuite(t) + methodID := s.contractABI.Methods[StakeMethodName] + r := rand.New(rand.NewSource(42)) + validator := sample.Validator(t, r) + + staker := sample.Bech32AccAddress() + stakerEthAddr := common.BytesToAddress(staker.Bytes()) + coins := sample.Coins() + err := s.sdkKeepers.BankKeeper.MintCoins(s.ctx, fungibletypes.ModuleName, sample.Coins()) + require.NoError(t, err) + err = s.sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(s.ctx, fungibletypes.ModuleName, staker, coins) + require.NoError(t, err) + + stakerAddr := common.BytesToAddress(staker.Bytes()) + s.mockVMContract.CallerAddress = stakerAddr + args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} + s.mockVMContract.Input = packInputArgs(t, methodID, args...) + + // ACT + _, err = s.contract.Run(s.mockEVM, s.mockVMContract, false) + + // ASSERT + require.Error(t, err) + require.ErrorIs(t, err, precompiletypes.ErrDisabledMethod{ + Method: StakeMethodName, + }) + }) + + // t.Run("should fail in read only mode", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) + // methodID := abi.Methods[StakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validator := sample.Validator(t, r) + + // staker := sample.Bech32AccAddress() + // stakerEthAddr := common.BytesToAddress(staker.Bytes()) + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + // mockVMContract.CallerAddress = stakerAddr + // args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} + // mockVMContract.Input = packInputArgs(t, methodID, args...) + + // // ACT + // _, err = contract.Run(mockEVM, mockVMContract, true) + + // // ASSERT + // require.ErrorIs(t, err, ptypes.ErrWriteMethod{Method: StakeMethodName}) + // }) + + // t.Run("should fail if validator doesn't exist", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) + // methodID := abi.Methods[StakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validator := sample.Validator(t, r) + + // staker := sample.Bech32AccAddress() + // stakerEthAddr := common.BytesToAddress(staker.Bytes()) + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + // mockVMContract.CallerAddress = stakerAddr + // args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} + // mockVMContract.Input = packInputArgs(t, methodID, args...) + + // // ACT + // _, err = contract.Run(mockEVM, mockVMContract, false) + + // // ASSERT + // require.Error(t, err) + // }) + + // t.Run("should stake", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) + // methodID := abi.Methods[StakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validator := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) + + // staker := sample.Bech32AccAddress() + // stakerEthAddr := common.BytesToAddress(staker.Bytes()) + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + // mockVMContract.CallerAddress = stakerAddr + // args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} + // mockVMContract.Input = packInputArgs(t, methodID, args...) + + // // ACT + // _, err = contract.Run(mockEVM, mockVMContract, false) + + // // ASSERT + // require.NoError(t, err) + // }) + + // t.Run("should fail if no input args", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) + // methodID := abi.Methods[StakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validator := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) + + // staker := sample.Bech32AccAddress() + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + // mockVMContract.CallerAddress = stakerAddr + // mockVMContract.Input = methodID.ID + + // // ACT + // _, err = contract.Run(mockEVM, mockVMContract, false) + + // // ASSERT + // require.Error(t, err) + // }) + + // t.Run("should fail if caller is not staker", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) + // methodID := abi.Methods[StakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validator := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) + + // staker := sample.Bech32AccAddress() + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + // mockVMContract.CallerAddress = stakerAddr + + // nonStakerAddr := common.BytesToAddress(sample.Bech32AccAddress().Bytes()) + // args := []interface{}{nonStakerAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} + // mockVMContract.Input = packInputArgs(t, methodID, args...) + + // // ACT + // _, err = contract.Run(mockEVM, mockVMContract, false) + + // // ASSERT + // require.ErrorContains(t, err, "caller is not staker address") + // }) + + // t.Run("should fail if staking fails", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) + // methodID := abi.Methods[StakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validator := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) + + // // staker without funds + // staker := sample.Bech32AccAddress() + // stakerEthAddr := common.BytesToAddress(staker.Bytes()) + // coins := sample.Coins() + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + // mockVMContract.CallerAddress = stakerAddr + + // args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} + // mockVMContract.Input = packInputArgs(t, methodID, args...) + + // // ACT + // _, err := contract.Run(mockEVM, mockVMContract, false) + + // // ASSERT + // require.Error(t, err) + // }) + + // t.Run("should fail if wrong args amount", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, _ := setup(t) + // methodID := abi.Methods[StakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validator := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) + + // staker := sample.Bech32AccAddress() + // stakerEthAddr := common.BytesToAddress(staker.Bytes()) + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + + // args := []interface{}{stakerEthAddr, validator.OperatorAddress} + + // // ACT + // _, err = contract.Stake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &methodID, args) + + // // ASSERT + // require.Error(t, err) + // }) + + // t.Run("should fail if staker is not eth addr", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, _ := setup(t) + // methodID := abi.Methods[StakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validator := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) + + // staker := sample.Bech32AccAddress() + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + + // args := []interface{}{staker, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} + + // // ACT + // _, err = contract.Stake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &methodID, args) + + // // ASSERT + // require.Error(t, err) + // }) + + // t.Run("should fail if validator is not valid string", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, _ := setup(t) + // methodID := abi.Methods[StakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validator := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) + + // staker := sample.Bech32AccAddress() + // stakerEthAddr := common.BytesToAddress(staker.Bytes()) + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + + // args := []interface{}{stakerEthAddr, 42, coins.AmountOf(config.BaseDenom).BigInt()} + + // // ACT + // _, err = contract.Stake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &methodID, args) + + // // ASSERT + // require.Error(t, err) + // }) + + // t.Run("should fail if amount is not int64", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, _ := setup(t) + // methodID := abi.Methods[StakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validator := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) + + // staker := sample.Bech32AccAddress() + // stakerEthAddr := common.BytesToAddress(staker.Bytes()) + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + + // args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).Uint64()} + + // // ACT + // _, err = contract.Stake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &methodID, args) + + // // ASSERT + // require.Error(t, err) + // }) +} diff --git a/precompiles/staking/method_unstake.go b/precompiles/staking/method_unstake.go new file mode 100644 index 0000000000..4efb41993b --- /dev/null +++ b/precompiles/staking/method_unstake.go @@ -0,0 +1,77 @@ +package staking + +import ( + "fmt" + "math/big" + + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + + precompiletypes "github.com/zeta-chain/node/precompiles/types" +) + +func (c *Contract) Unstake( + ctx sdk.Context, + evm *vm.EVM, + contract *vm.Contract, + method *abi.Method, + args []interface{}, +) ([]byte, error) { + if len(args) != 3 { + return nil, &(precompiletypes.ErrInvalidNumberOfArgs{ + Got: len(args), + Expect: 3, + }) + } + + stakerAddress, ok := args[0].(common.Address) + if !ok { + return nil, precompiletypes.ErrInvalidArgument{ + Got: args[0], + } + } + + if contract.CallerAddress != stakerAddress { + return nil, fmt.Errorf("caller is not staker address") + } + + validatorAddress, ok := args[1].(string) + if !ok { + return nil, precompiletypes.ErrInvalidArgument{ + Got: args[1], + } + } + + amount, ok := args[2].(*big.Int) + if !ok { + return nil, precompiletypes.ErrInvalidArgument{ + Got: args[2], + } + } + + msgServer := stakingkeeper.NewMsgServerImpl(&c.stakingKeeper) + res, err := msgServer.Undelegate(ctx, &stakingtypes.MsgUndelegate{ + DelegatorAddress: sdk.AccAddress(stakerAddress.Bytes()).String(), + ValidatorAddress: validatorAddress, + Amount: sdk.Coin{ + Denom: c.stakingKeeper.BondDenom(ctx), + Amount: math.NewIntFromBigInt(amount), + }, + }) + if err != nil { + return nil, err + } + + stateDB := evm.StateDB.(precompiletypes.ExtStateDB) + err = c.addUnstakeLog(ctx, stateDB, stakerAddress, validatorAddress, amount) + if err != nil { + return nil, err + } + + return method.Outputs.Pack(res.GetCompletionTime().UTC().Unix()) +} diff --git a/precompiles/staking/method_unstake_test.go b/precompiles/staking/method_unstake_test.go new file mode 100644 index 0000000000..e770020946 --- /dev/null +++ b/precompiles/staking/method_unstake_test.go @@ -0,0 +1,311 @@ +package staking + +import ( + "math/rand" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + "github.com/zeta-chain/node/cmd/zetacored/config" + precompiletypes "github.com/zeta-chain/node/precompiles/types" + "github.com/zeta-chain/node/testutil/sample" + fungibletypes "github.com/zeta-chain/node/x/fungible/types" +) + +func Test_Unstake(t *testing.T) { + // Disabled until further notice, check https://github.com/zeta-chain/node/issues/3005. + t.Run("should fail with error disabled", func(t *testing.T) { + // ARRANGE + s := newTestSuite(t) + methodID := s.contractABI.Methods[UnstakeMethodName] + r := rand.New(rand.NewSource(42)) + validator := sample.Validator(t, r) + + staker := sample.Bech32AccAddress() + stakerEthAddr := common.BytesToAddress(staker.Bytes()) + coins := sample.Coins() + err := s.sdkKeepers.BankKeeper.MintCoins(s.ctx, fungibletypes.ModuleName, sample.Coins()) + require.NoError(t, err) + err = s.sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(s.ctx, fungibletypes.ModuleName, staker, coins) + require.NoError(t, err) + + stakerAddr := common.BytesToAddress(staker.Bytes()) + s.mockVMContract.CallerAddress = stakerAddr + + args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} + s.mockVMContract.Input = packInputArgs(t, methodID, args...) + + // ACT + _, err = s.contract.Run(s.mockEVM, s.mockVMContract, false) + + // ASSERT + require.Error(t, err) + require.ErrorIs(t, err, precompiletypes.ErrDisabledMethod{ + Method: UnstakeMethodName, + }) + }) + + // t.Run("should fail in read only method", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) + // methodID := abi.Methods[UnstakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validator := sample.Validator(t, r) + + // staker := sample.Bech32AccAddress() + // stakerEthAddr := common.BytesToAddress(staker.Bytes()) + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + // mockVMContract.CallerAddress = stakerAddr + + // args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} + // mockVMContract.Input = packInputArgs(t, methodID, args...) + + // // ACT + // _, err = contract.Run(mockEVM, mockVMContract, true) + + // // ASSERT + // require.ErrorIs(t, err, ptypes.ErrWriteMethod{Method: UnstakeMethodName}) + // }) + + // t.Run("should fail if validator doesn't exist", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) + // methodID := abi.Methods[UnstakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validator := sample.Validator(t, r) + + // staker := sample.Bech32AccAddress() + // stakerEthAddr := common.BytesToAddress(staker.Bytes()) + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + // mockVMContract.CallerAddress = stakerAddr + + // args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} + // mockVMContract.Input = packInputArgs(t, methodID, args...) + + // // ACT + // _, err = contract.Run(mockEVM, mockVMContract, false) + + // // ASSERT + // require.Error(t, err) + // }) + + // t.Run("should unstake", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) + // methodID := abi.Methods[UnstakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validator := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) + + // staker := sample.Bech32AccAddress() + // stakerEthAddr := common.BytesToAddress(staker.Bytes()) + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + // mockVMContract.CallerAddress = stakerAddr + + // args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} + + // // stake first + // stakeMethodID := abi.Methods[StakeMethodName] + // mockVMContract.Input = packInputArgs(t, stakeMethodID, args...) + // _, err = contract.Run(mockEVM, mockVMContract, false) + // require.NoError(t, err) + + // // ACT + // mockVMContract.Input = packInputArgs(t, methodID, args...) + // _, err = contract.Run(mockEVM, mockVMContract, false) + + // // ASSERT + // require.NoError(t, err) + // }) + + // t.Run("should fail if caller is not staker", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) + // methodID := abi.Methods[UnstakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validator := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) + + // staker := sample.Bech32AccAddress() + // stakerEthAddr := common.BytesToAddress(staker.Bytes()) + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + // mockVMContract.CallerAddress = stakerAddr + + // args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} + // // stake first + // stakeMethodID := abi.Methods[StakeMethodName] + // mockVMContract.Input = packInputArgs(t, stakeMethodID, args...) + // _, err = contract.Run(mockEVM, mockVMContract, false) + // require.NoError(t, err) + + // callerEthAddr := common.BytesToAddress(sample.Bech32AccAddress().Bytes()) + // mockVMContract.CallerAddress = callerEthAddr + // mockVMContract.Input = packInputArgs(t, methodID, args...) + + // // ACT + // _, err = contract.Run(mockEVM, mockVMContract, false) + + // // ASSERT + // require.ErrorContains(t, err, "caller is not staker address") + // }) + + // t.Run("should fail if no previous staking", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) + // methodID := abi.Methods[UnstakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validator := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) + + // staker := sample.Bech32AccAddress() + // stakerEthAddr := common.BytesToAddress(staker.Bytes()) + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + // mockVMContract.CallerAddress = stakerAddr + + // args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} + // mockVMContract.Input = packInputArgs(t, methodID, args...) + + // // ACT + // _, err = contract.Run(mockEVM, mockVMContract, false) + + // // ASSERT + // require.Error(t, err) + // }) + + // t.Run("should fail if wrong args amount", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, _ := setup(t) + // methodID := abi.Methods[UnstakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validator := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) + + // staker := sample.Bech32AccAddress() + // stakerEthAddr := common.BytesToAddress(staker.Bytes()) + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + + // args := []interface{}{stakerEthAddr, validator.OperatorAddress} + + // // ACT + // _, err = contract.Unstake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &methodID, args) + + // // ASSERT + // require.Error(t, err) + // }) + + // t.Run("should fail if staker is not eth addr", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, _ := setup(t) + // methodID := abi.Methods[UnstakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validator := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) + + // staker := sample.Bech32AccAddress() + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + + // args := []interface{}{staker, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} + + // // ACT + // _, err = contract.Unstake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &methodID, args) + + // // ASSERT + // require.Error(t, err) + // }) + + // t.Run("should fail if validator is not valid string", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, _ := setup(t) + // methodID := abi.Methods[UnstakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validator := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) + + // staker := sample.Bech32AccAddress() + // stakerEthAddr := common.BytesToAddress(staker.Bytes()) + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + + // args := []interface{}{stakerEthAddr, 42, coins.AmountOf(config.BaseDenom).BigInt()} + + // // ACT + // _, err = contract.Unstake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &methodID, args) + + // // ASSERT + // require.Error(t, err) + // }) + + // t.Run("should fail if amount is not int64", func(t *testing.T) { + // // ARRANGE + // ctx, contract, abi, sdkKeepers, mockEVM, _ := setup(t) + // methodID := abi.Methods[UnstakeMethodName] + // r := rand.New(rand.NewSource(42)) + // validator := sample.Validator(t, r) + // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) + + // staker := sample.Bech32AccAddress() + // stakerEthAddr := common.BytesToAddress(staker.Bytes()) + // coins := sample.Coins() + // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) + // require.NoError(t, err) + // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) + // require.NoError(t, err) + + // stakerAddr := common.BytesToAddress(staker.Bytes()) + + // args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).Uint64()} + + // // ACT + // _, err = contract.Unstake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &methodID, args) + + // // ASSERT + // require.Error(t, err) + // }) +} diff --git a/precompiles/staking/staking.go b/precompiles/staking/staking.go index fea0d0f9f2..4d8115336a 100644 --- a/precompiles/staking/staking.go +++ b/precompiles/staking/staking.go @@ -1,32 +1,18 @@ package staking import ( - "fmt" - "math/big" - - "cosmossdk.io/math" "github.com/cosmos/cosmos-sdk/codec" storetypes "github.com/cosmos/cosmos-sdk/store/types" sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" - stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/vm" - ptypes "github.com/zeta-chain/node/precompiles/types" -) - -// method names -const ( - // write - StakeMethodName = "stake" - UnstakeMethodName = "unstake" - MoveStakeMethodName = "moveStake" - - // read - GetAllValidatorsMethodName = "getAllValidators" - GetSharesMethodName = "getShares" + precompiletypes "github.com/zeta-chain/node/precompiles/types" + fungiblekeeper "github.com/zeta-chain/node/x/fungible/keeper" ) var ( @@ -54,11 +40,13 @@ func initABI() { // just temporary flat values, double check these flat values // can we just use WriteCostFlat/ReadCostFlat from gas config for flat values? case StakeMethodName: - GasRequiredByMethod[methodID] = 10000 + GasRequiredByMethod[methodID] = StakeMethodGas case UnstakeMethodName: - GasRequiredByMethod[methodID] = 10000 + GasRequiredByMethod[methodID] = UnstakeMethodGas case MoveStakeMethodName: - GasRequiredByMethod[methodID] = 10000 + GasRequiredByMethod[methodID] = MoveStakeMethodGas + case DistributeMethodName: + GasRequiredByMethod[methodID] = DistributeMethodGas case GetAllValidatorsMethodName: GasRequiredByMethod[methodID] = 0 ViewMethod[methodID] = true @@ -72,23 +60,35 @@ func initABI() { } type Contract struct { - ptypes.BaseContract + precompiletypes.BaseContract - stakingKeeper stakingkeeper.Keeper - cdc codec.Codec - kvGasConfig storetypes.GasConfig + stakingKeeper stakingkeeper.Keeper + fungibleKeeper fungiblekeeper.Keeper + bankKeeper bankkeeper.Keeper + cdc codec.Codec + kvGasConfig storetypes.GasConfig } func NewIStakingContract( + ctx sdk.Context, stakingKeeper *stakingkeeper.Keeper, + fungibleKeeper fungiblekeeper.Keeper, + bankKeeper bankkeeper.Keeper, cdc codec.Codec, kvGasConfig storetypes.GasConfig, ) *Contract { + accAddress := sdk.AccAddress(ContractAddress.Bytes()) + if fungibleKeeper.GetAuthKeeper().GetAccount(ctx, accAddress) == nil { + fungibleKeeper.GetAuthKeeper().SetAccount(ctx, authtypes.NewBaseAccount(accAddress, nil, 0, 0)) + } + return &Contract{ - BaseContract: ptypes.NewBaseContract(ContractAddress), - stakingKeeper: *stakingKeeper, - cdc: cdc, - kvGasConfig: kvGasConfig, + BaseContract: precompiletypes.NewBaseContract(ContractAddress), + stakingKeeper: *stakingKeeper, + fungibleKeeper: fungibleKeeper, + bankKeeper: bankKeeper, + cdc: cdc, + kvGasConfig: kvGasConfig, } } @@ -122,262 +122,6 @@ func (c *Contract) RequiredGas(input []byte) uint64 { return 0 } -func (c *Contract) GetAllValidators( - ctx sdk.Context, - method *abi.Method, -) ([]byte, error) { - validators := c.stakingKeeper.GetAllValidators(ctx) - - validatorsRes := make([]Validator, len(validators)) - for i, v := range validators { - validatorsRes[i] = Validator{ - OperatorAddress: v.OperatorAddress, - ConsensusPubKey: v.ConsensusPubkey.String(), - BondStatus: uint8(v.Status), - Jailed: v.Jailed, - } - } - - return method.Outputs.Pack(validatorsRes) -} - -func (c *Contract) GetShares( - ctx sdk.Context, - method *abi.Method, - args []interface{}, -) ([]byte, error) { - if len(args) != 2 { - return nil, &(ptypes.ErrInvalidNumberOfArgs{ - Got: len(args), - Expect: 2, - }) - } - stakerAddress, ok := args[0].(common.Address) - if !ok { - return nil, ptypes.ErrInvalidArgument{ - Got: args[0], - } - } - - validatorAddress, ok := args[1].(string) - if !ok { - return nil, ptypes.ErrInvalidArgument{ - Got: args[1], - } - } - - validator, err := sdk.ValAddressFromBech32(validatorAddress) - if err != nil { - return nil, err - } - - delegation := c.stakingKeeper.Delegation(ctx, sdk.AccAddress(stakerAddress.Bytes()), validator) - shares := big.NewInt(0) - if delegation != nil { - shares = delegation.GetShares().BigInt() - } - - return method.Outputs.Pack(shares) -} - -func (c *Contract) Stake( - ctx sdk.Context, - evm *vm.EVM, - contract *vm.Contract, - method *abi.Method, - args []interface{}, -) ([]byte, error) { - if len(args) != 3 { - return nil, &(ptypes.ErrInvalidNumberOfArgs{ - Got: len(args), - Expect: 3, - }) - } - - stakerAddress, ok := args[0].(common.Address) - if !ok { - return nil, ptypes.ErrInvalidArgument{ - Got: args[0], - } - } - - if contract.CallerAddress != stakerAddress { - return nil, fmt.Errorf("caller is not staker address") - } - - validatorAddress, ok := args[1].(string) - if !ok { - return nil, ptypes.ErrInvalidArgument{ - Got: args[1], - } - } - - amount, ok := args[2].(*big.Int) - if !ok { - return nil, ptypes.ErrInvalidArgument{ - Got: args[2], - } - } - - msgServer := stakingkeeper.NewMsgServerImpl(&c.stakingKeeper) - _, err := msgServer.Delegate(ctx, &stakingtypes.MsgDelegate{ - DelegatorAddress: sdk.AccAddress(stakerAddress.Bytes()).String(), - ValidatorAddress: validatorAddress, - Amount: sdk.Coin{ - Denom: c.stakingKeeper.BondDenom(ctx), - Amount: math.NewIntFromBigInt(amount), - }, - }) - if err != nil { - return nil, err - } - - // if caller is not the same as origin it means call is coming through smart contract, - // and because state of smart contract calling precompile might be updated as well - // manually reduce amount in stateDB, so it is properly reflected in bank module - stateDB := evm.StateDB.(ptypes.ExtStateDB) - if contract.CallerAddress != evm.Origin { - stateDB.SubBalance(stakerAddress, amount) - } - - err = c.AddStakeLog(ctx, stateDB, stakerAddress, validatorAddress, amount) - if err != nil { - return nil, err - } - - return method.Outputs.Pack(true) -} - -func (c *Contract) Unstake( - ctx sdk.Context, - evm *vm.EVM, - contract *vm.Contract, - method *abi.Method, - args []interface{}, -) ([]byte, error) { - if len(args) != 3 { - return nil, &(ptypes.ErrInvalidNumberOfArgs{ - Got: len(args), - Expect: 3, - }) - } - - stakerAddress, ok := args[0].(common.Address) - if !ok { - return nil, ptypes.ErrInvalidArgument{ - Got: args[0], - } - } - - if contract.CallerAddress != stakerAddress { - return nil, fmt.Errorf("caller is not staker address") - } - - validatorAddress, ok := args[1].(string) - if !ok { - return nil, ptypes.ErrInvalidArgument{ - Got: args[1], - } - } - - amount, ok := args[2].(*big.Int) - if !ok { - return nil, ptypes.ErrInvalidArgument{ - Got: args[2], - } - } - - msgServer := stakingkeeper.NewMsgServerImpl(&c.stakingKeeper) - res, err := msgServer.Undelegate(ctx, &stakingtypes.MsgUndelegate{ - DelegatorAddress: sdk.AccAddress(stakerAddress.Bytes()).String(), - ValidatorAddress: validatorAddress, - Amount: sdk.Coin{ - Denom: c.stakingKeeper.BondDenom(ctx), - Amount: math.NewIntFromBigInt(amount), - }, - }) - if err != nil { - return nil, err - } - - stateDB := evm.StateDB.(ptypes.ExtStateDB) - err = c.AddUnstakeLog(ctx, stateDB, stakerAddress, validatorAddress, amount) - if err != nil { - return nil, err - } - - return method.Outputs.Pack(res.GetCompletionTime().UTC().Unix()) -} - -func (c *Contract) MoveStake( - ctx sdk.Context, - evm *vm.EVM, - contract *vm.Contract, - method *abi.Method, - args []interface{}, -) ([]byte, error) { - if len(args) != 4 { - return nil, &(ptypes.ErrInvalidNumberOfArgs{ - Got: len(args), - Expect: 4, - }) - } - - stakerAddress, ok := args[0].(common.Address) - if !ok { - return nil, ptypes.ErrInvalidArgument{ - Got: args[0], - } - } - - if contract.CallerAddress != stakerAddress { - return nil, fmt.Errorf("caller is not staker address") - } - - validatorSrcAddress, ok := args[1].(string) - if !ok { - return nil, ptypes.ErrInvalidArgument{ - Got: args[1], - } - } - - validatorDstAddress, ok := args[2].(string) - if !ok { - return nil, ptypes.ErrInvalidArgument{ - Got: args[2], - } - } - - amount, ok := args[3].(*big.Int) - if !ok { - return nil, ptypes.ErrInvalidArgument{ - Got: args[3], - } - } - - msgServer := stakingkeeper.NewMsgServerImpl(&c.stakingKeeper) - res, err := msgServer.BeginRedelegate(ctx, &stakingtypes.MsgBeginRedelegate{ - DelegatorAddress: sdk.AccAddress(stakerAddress.Bytes()).String(), - ValidatorSrcAddress: validatorSrcAddress, - ValidatorDstAddress: validatorDstAddress, - Amount: sdk.Coin{ - Denom: c.stakingKeeper.BondDenom(ctx), - Amount: math.NewIntFromBigInt(amount), - }, - }) - if err != nil { - return nil, err - } - - stateDB := evm.StateDB.(ptypes.ExtStateDB) - err = c.AddMoveStakeLog(ctx, stateDB, stakerAddress, validatorSrcAddress, validatorDstAddress, amount) - if err != nil { - return nil, err - } - - return method.Outputs.Pack(res.GetCompletionTime().UTC().Unix()) -} - // Run is the entrypoint of the precompiled contract, it switches over the input method, // and execute them accordingly. func (c *Contract) Run(evm *vm.EVM, contract *vm.Contract, readOnly bool) ([]byte, error) { @@ -391,7 +135,14 @@ func (c *Contract) Run(evm *vm.EVM, contract *vm.Contract, readOnly bool) ([]byt return nil, err } - stateDB := evm.StateDB.(ptypes.ExtStateDB) + stateDB := evm.StateDB.(precompiletypes.ExtStateDB) + + // If the method is not a view method, it should not be executed in read-only mode. + if _, isViewMethod := ViewMethod[[4]byte(method.ID)]; !isViewMethod && readOnly { + return nil, precompiletypes.ErrWriteMethod{ + Method: method.Name, + } + } switch method.Name { case GetAllValidatorsMethodName: @@ -416,17 +167,11 @@ func (c *Contract) Run(evm *vm.EVM, contract *vm.Contract, readOnly bool) ([]byt return res, nil case StakeMethodName: // Disabled until further notice, check https://github.com/zeta-chain/node/issues/3005. - return nil, ptypes.ErrDisabledMethod{ + return nil, precompiletypes.ErrDisabledMethod{ Method: method.Name, } //nolint:govet - if readOnly { - return nil, ptypes.ErrWriteMethod{ - Method: method.Name, - } - } - var res []byte execErr := stateDB.ExecuteNativeAction(contract.Address(), nil, func(ctx sdk.Context) error { res, err = c.Stake(ctx, evm, contract, method, args) @@ -438,17 +183,11 @@ func (c *Contract) Run(evm *vm.EVM, contract *vm.Contract, readOnly bool) ([]byt return res, nil case UnstakeMethodName: // Disabled until further notice, check https://github.com/zeta-chain/node/issues/3005. - return nil, ptypes.ErrDisabledMethod{ + return nil, precompiletypes.ErrDisabledMethod{ Method: method.Name, } //nolint:govet - if readOnly { - return nil, ptypes.ErrWriteMethod{ - Method: method.Name, - } - } - var res []byte execErr := stateDB.ExecuteNativeAction(contract.Address(), nil, func(ctx sdk.Context) error { res, err = c.Unstake(ctx, evm, contract, method, args) @@ -460,17 +199,11 @@ func (c *Contract) Run(evm *vm.EVM, contract *vm.Contract, readOnly bool) ([]byt return res, nil case MoveStakeMethodName: // Disabled until further notice, check https://github.com/zeta-chain/node/issues/3005. - return nil, ptypes.ErrDisabledMethod{ + return nil, precompiletypes.ErrDisabledMethod{ Method: method.Name, } //nolint:govet - if readOnly { - return nil, ptypes.ErrWriteMethod{ - Method: method.Name, - } - } - var res []byte execErr := stateDB.ExecuteNativeAction(contract.Address(), nil, func(ctx sdk.Context) error { res, err = c.MoveStake(ctx, evm, contract, method, args) @@ -480,9 +213,23 @@ func (c *Contract) Run(evm *vm.EVM, contract *vm.Contract, readOnly bool) ([]byt return nil, err } return res, nil + case DistributeMethodName: + var res []byte + execErr := stateDB.ExecuteNativeAction(contract.Address(), nil, func(ctx sdk.Context) error { + res, err = c.distribute(ctx, evm, contract, method, args) + return err + }) + if execErr != nil { + res, errPack := method.Outputs.Pack(false) + if errPack != nil { + return nil, errPack + } + return res, err + } + return res, nil default: - return nil, ptypes.ErrInvalidMethod{ + return nil, precompiletypes.ErrInvalidMethod{ Method: method.Name, } } diff --git a/precompiles/staking/staking_test.go b/precompiles/staking/staking_test.go index aaea87f94d..977090e4dd 100644 --- a/precompiles/staking/staking_test.go +++ b/precompiles/staking/staking_test.go @@ -2,17 +2,17 @@ package staking import ( "encoding/json" - "fmt" "testing" "math/big" - "math/rand" tmdb "github.com/cometbft/cometbft-db" "github.com/cosmos/cosmos-sdk/store" + "github.com/holiman/uint256" storetypes "github.com/cosmos/cosmos-sdk/store/types" sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" @@ -20,103 +20,39 @@ import ( "github.com/ethereum/go-ethereum/params" "github.com/stretchr/testify/require" ethermint "github.com/zeta-chain/ethermint/types" + evmkeeper "github.com/zeta-chain/ethermint/x/evm/keeper" "github.com/zeta-chain/ethermint/x/evm/statedb" "github.com/zeta-chain/node/cmd/zetacored/config" + "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/pkg/contracts/erc1967proxy" "github.com/zeta-chain/node/precompiles/prototype" - ptypes "github.com/zeta-chain/node/precompiles/types" "github.com/zeta-chain/node/testutil/keeper" - "github.com/zeta-chain/node/testutil/sample" + fungiblekeeper "github.com/zeta-chain/node/x/fungible/keeper" fungibletypes "github.com/zeta-chain/node/x/fungible/types" + "github.com/zeta-chain/protocol-contracts/v2/pkg/gatewayzevm.sol" + "github.com/zeta-chain/protocol-contracts/v2/pkg/zrc20.sol" ) -func setup(t *testing.T) (sdk.Context, *Contract, abi.ABI, keeper.SDKKeepers, *vm.EVM, *vm.Contract) { - var encoding ethermint.EncodingConfig - appCodec := encoding.Codec - - cdc := keeper.NewCodec() - - db := tmdb.NewMemDB() - stateStore := store.NewCommitMultiStore(db) - keys, memKeys, tkeys, allKeys := keeper.StoreKeys() - sdkKeepers := keeper.NewSDKKeepersWithKeys(cdc, keys, memKeys, tkeys, allKeys) - for _, key := range keys { - stateStore.MountStoreWithDB(key, storetypes.StoreTypeIAVL, db) - } - for _, key := range tkeys { - stateStore.MountStoreWithDB(key, storetypes.StoreTypeTransient, nil) - } - for _, key := range memKeys { - stateStore.MountStoreWithDB(key, storetypes.StoreTypeMemory, nil) - } - - gasConfig := storetypes.TransientGasConfig() - ctx := keeper.NewContext(stateStore) - - require.NoError(t, stateStore.LoadLatestVersion()) - - stakingGenesisState := stakingtypes.DefaultGenesisState() - stakingGenesisState.Params.BondDenom = config.BaseDenom - sdkKeepers.StakingKeeper.InitGenesis(ctx, stakingGenesisState) - - contract := NewIStakingContract(&sdkKeepers.StakingKeeper, appCodec, gasConfig) - require.NotNil(t, contract, "NewIStakingContract() should not return a nil contract") - - abi := contract.Abi() - require.NotNil(t, abi, "contract ABI should not be nil") - - address := contract.Address() - require.NotNil(t, address, "contract address should not be nil") - - mockEVM := vm.NewEVM( - vm.BlockContext{}, - vm.TxContext{}, - statedb.New(ctx, sdkKeepers.EvmKeeper, statedb.TxConfig{}), - ¶ms.ChainConfig{}, - vm.Config{}, - ) - mockVMContract := vm.NewContract( - contractRef{address: common.Address{}}, - contractRef{address: ContractAddress}, - big.NewInt(0), - 0, - ) - return ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract -} - -func packInputArgs(t *testing.T, methodID abi.Method, args ...interface{}) []byte { - input, err := methodID.Inputs.Pack(args...) - require.NoError(t, err) - return append(methodID.ID, input...) -} - -type contractRef struct { - address common.Address -} - -func (c contractRef) Address() common.Address { - return c.address -} - func Test_IStakingContract(t *testing.T) { - _, contract, abi, _, _, _ := setup(t) + s := newTestSuite(t) gasConfig := storetypes.TransientGasConfig() t.Run("should check methods are present in ABI", func(t *testing.T) { - require.NotNil(t, abi.Methods[StakeMethodName], "stake method should be present in the ABI") - require.NotNil(t, abi.Methods[UnstakeMethodName], "unstake method should be present in the ABI") + require.NotNil(t, s.contractABI.Methods[StakeMethodName], "stake method should be present in the ABI") + require.NotNil(t, s.contractABI.Methods[UnstakeMethodName], "unstake method should be present in the ABI") require.NotNil( t, - abi.Methods[MoveStakeMethodName], + s.contractABI.Methods[MoveStakeMethodName], "moveStake method should be present in the ABI", ) require.NotNil( t, - abi.Methods[GetAllValidatorsMethodName], + s.contractABI.Methods[GetAllValidatorsMethodName], "getAllValidators method should be present in the ABI", ) - require.NotNil(t, abi.Methods[GetSharesMethodName], "getShares method should be present in the ABI") + require.NotNil(t, s.contractABI.Methods[GetSharesMethodName], "getShares method should be present in the ABI") }) t.Run("should check gas requirements for methods", func(t *testing.T) { @@ -124,9 +60,9 @@ func Test_IStakingContract(t *testing.T) { t.Run("stake", func(t *testing.T) { // ACT - stake := contract.RequiredGas(abi.Methods[StakeMethodName].ID) + stake := s.contract.RequiredGas(s.contractABI.Methods[StakeMethodName].ID) // ASSERT - copy(method[:], abi.Methods[StakeMethodName].ID[:4]) + copy(method[:], s.contractABI.Methods[StakeMethodName].ID[:4]) baseCost := uint64(len(method)) * gasConfig.WriteCostPerByte require.Equal( t, @@ -140,9 +76,9 @@ func Test_IStakingContract(t *testing.T) { t.Run("unstake", func(t *testing.T) { // ACT - unstake := contract.RequiredGas(abi.Methods[UnstakeMethodName].ID) + unstake := s.contract.RequiredGas(s.contractABI.Methods[UnstakeMethodName].ID) // ASSERT - copy(method[:], abi.Methods[UnstakeMethodName].ID[:4]) + copy(method[:], s.contractABI.Methods[UnstakeMethodName].ID[:4]) baseCost := uint64(len(method)) * gasConfig.WriteCostPerByte require.Equal( t, @@ -156,9 +92,9 @@ func Test_IStakingContract(t *testing.T) { t.Run("moveStake", func(t *testing.T) { // ACT - moveStake := contract.RequiredGas(abi.Methods[MoveStakeMethodName].ID) + moveStake := s.contract.RequiredGas(s.contractABI.Methods[MoveStakeMethodName].ID) // ASSERT - copy(method[:], abi.Methods[MoveStakeMethodName].ID[:4]) + copy(method[:], s.contractABI.Methods[MoveStakeMethodName].ID[:4]) baseCost := uint64(len(method)) * gasConfig.WriteCostPerByte require.Equal( t, @@ -172,9 +108,9 @@ func Test_IStakingContract(t *testing.T) { t.Run("getAllValidators", func(t *testing.T) { // ACT - getAllValidators := contract.RequiredGas(abi.Methods[GetAllValidatorsMethodName].ID) + getAllValidators := s.contract.RequiredGas(s.contractABI.Methods[GetAllValidatorsMethodName].ID) // ASSERT - copy(method[:], abi.Methods[GetAllValidatorsMethodName].ID[:4]) + copy(method[:], s.contractABI.Methods[GetAllValidatorsMethodName].ID[:4]) baseCost := uint64(len(method)) * gasConfig.ReadCostPerByte require.Equal( t, @@ -188,9 +124,9 @@ func Test_IStakingContract(t *testing.T) { t.Run("getShares", func(t *testing.T) { // ACT - getShares := contract.RequiredGas(abi.Methods[GetSharesMethodName].ID) + getShares := s.contract.RequiredGas(s.contractABI.Methods[GetSharesMethodName].ID) // ASSERT - copy(method[:], abi.Methods[GetSharesMethodName].ID[:4]) + copy(method[:], s.contractABI.Methods[GetSharesMethodName].ID[:4]) baseCost := uint64(len(method)) * gasConfig.ReadCostPerByte require.Equal( t, @@ -206,7 +142,7 @@ func Test_IStakingContract(t *testing.T) { // ARRANGE invalidMethodBytes := []byte("invalidMethod") // ACT - gasInvalidMethod := contract.RequiredGas(invalidMethodBytes) + gasInvalidMethod := s.contract.RequiredGas(invalidMethodBytes) // ASSERT require.Equal( t, @@ -221,9 +157,9 @@ func Test_IStakingContract(t *testing.T) { } func Test_InvalidMethod(t *testing.T) { - _, _, abi, _, _, _ := setup(t) + s := newTestSuite(t) - _, doNotExist := abi.Methods["invalidMethod"] + _, doNotExist := s.contractABI.Methods["invalidMethod"] require.False(t, doNotExist, "invalidMethod should not be present in the ABI") } @@ -238,1238 +174,379 @@ func Test_InvalidABI(t *testing.T) { initABI() } -func Test_Stake(t *testing.T) { - // Disabled until further notice, check https://github.com/zeta-chain/node/issues/3005. - t.Run("should fail with error disabled", func(t *testing.T) { - // ARRANGE - ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) - methodID := abi.Methods[StakeMethodName] - r := rand.New(rand.NewSource(42)) - validator := sample.Validator(t, r) - - staker := sample.Bech32AccAddress() - stakerEthAddr := common.BytesToAddress(staker.Bytes()) - coins := sample.Coins() - err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - require.NoError(t, err) - err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - require.NoError(t, err) - - stakerAddr := common.BytesToAddress(staker.Bytes()) - mockVMContract.CallerAddress = stakerAddr - args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} - mockVMContract.Input = packInputArgs(t, methodID, args...) - - // ACT - _, err = contract.Run(mockEVM, mockVMContract, false) - - // ASSERT - require.Error(t, err) - require.ErrorIs(t, err, ptypes.ErrDisabledMethod{ - Method: StakeMethodName, - }) - }) +func Test_RunInvalidMethod(t *testing.T) { + // ARRANGE + s := newTestSuite(t) - // t.Run("should fail in read only mode", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) - // methodID := abi.Methods[StakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validator := sample.Validator(t, r) - - // staker := sample.Bech32AccAddress() - // stakerEthAddr := common.BytesToAddress(staker.Bytes()) - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - // mockVMContract.CallerAddress = stakerAddr - // args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} - // mockVMContract.Input = packInputArgs(t, methodID, args...) - - // // ACT - // _, err = contract.Run(mockEVM, mockVMContract, true) - - // // ASSERT - // require.ErrorIs(t, err, ptypes.ErrWriteMethod{Method: StakeMethodName}) - // }) - - // t.Run("should fail if validator doesn't exist", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) - // methodID := abi.Methods[StakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validator := sample.Validator(t, r) - - // staker := sample.Bech32AccAddress() - // stakerEthAddr := common.BytesToAddress(staker.Bytes()) - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - // mockVMContract.CallerAddress = stakerAddr - // args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} - // mockVMContract.Input = packInputArgs(t, methodID, args...) - - // // ACT - // _, err = contract.Run(mockEVM, mockVMContract, false) - - // // ASSERT - // require.Error(t, err) - // }) - - // t.Run("should stake", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) - // methodID := abi.Methods[StakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validator := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) - - // staker := sample.Bech32AccAddress() - // stakerEthAddr := common.BytesToAddress(staker.Bytes()) - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - // mockVMContract.CallerAddress = stakerAddr - // args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} - // mockVMContract.Input = packInputArgs(t, methodID, args...) - - // // ACT - // _, err = contract.Run(mockEVM, mockVMContract, false) - - // // ASSERT - // require.NoError(t, err) - // }) - - // t.Run("should fail if no input args", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) - // methodID := abi.Methods[StakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validator := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) - - // staker := sample.Bech32AccAddress() - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - // mockVMContract.CallerAddress = stakerAddr - // mockVMContract.Input = methodID.ID - - // // ACT - // _, err = contract.Run(mockEVM, mockVMContract, false) - - // // ASSERT - // require.Error(t, err) - // }) - - // t.Run("should fail if caller is not staker", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) - // methodID := abi.Methods[StakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validator := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) - - // staker := sample.Bech32AccAddress() - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - // mockVMContract.CallerAddress = stakerAddr - - // nonStakerAddr := common.BytesToAddress(sample.Bech32AccAddress().Bytes()) - // args := []interface{}{nonStakerAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} - // mockVMContract.Input = packInputArgs(t, methodID, args...) - - // // ACT - // _, err = contract.Run(mockEVM, mockVMContract, false) - - // // ASSERT - // require.ErrorContains(t, err, "caller is not staker address") - // }) - - // t.Run("should fail if staking fails", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) - // methodID := abi.Methods[StakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validator := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) - - // // staker without funds - // staker := sample.Bech32AccAddress() - // stakerEthAddr := common.BytesToAddress(staker.Bytes()) - // coins := sample.Coins() - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - // mockVMContract.CallerAddress = stakerAddr - - // args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} - // mockVMContract.Input = packInputArgs(t, methodID, args...) - - // // ACT - // _, err := contract.Run(mockEVM, mockVMContract, false) - - // // ASSERT - // require.Error(t, err) - // }) - - // t.Run("should fail if wrong args amount", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, _ := setup(t) - // methodID := abi.Methods[StakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validator := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) - - // staker := sample.Bech32AccAddress() - // stakerEthAddr := common.BytesToAddress(staker.Bytes()) - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - - // args := []interface{}{stakerEthAddr, validator.OperatorAddress} - - // // ACT - // _, err = contract.Stake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &methodID, args) - - // // ASSERT - // require.Error(t, err) - // }) - - // t.Run("should fail if staker is not eth addr", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, _ := setup(t) - // methodID := abi.Methods[StakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validator := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) - - // staker := sample.Bech32AccAddress() - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - - // args := []interface{}{staker, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} - - // // ACT - // _, err = contract.Stake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &methodID, args) - - // // ASSERT - // require.Error(t, err) - // }) - - // t.Run("should fail if validator is not valid string", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, _ := setup(t) - // methodID := abi.Methods[StakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validator := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) - - // staker := sample.Bech32AccAddress() - // stakerEthAddr := common.BytesToAddress(staker.Bytes()) - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - - // args := []interface{}{stakerEthAddr, 42, coins.AmountOf(config.BaseDenom).BigInt()} - - // // ACT - // _, err = contract.Stake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &methodID, args) - - // // ASSERT - // require.Error(t, err) - // }) - - // t.Run("should fail if amount is not int64", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, _ := setup(t) - // methodID := abi.Methods[StakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validator := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) - - // staker := sample.Bech32AccAddress() - // stakerEthAddr := common.BytesToAddress(staker.Bytes()) - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - - // args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).Uint64()} - - // // ACT - // _, err = contract.Stake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &methodID, args) - - // // ASSERT - // require.Error(t, err) - // }) -} + var encoding ethermint.EncodingConfig + appCodec := encoding.Codec + gasConfig := storetypes.TransientGasConfig() -func Test_Unstake(t *testing.T) { - // Disabled until further notice, check https://github.com/zeta-chain/node/issues/3005. - t.Run("should fail with error disabled", func(t *testing.T) { - // ARRANGE - ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) - methodID := abi.Methods[UnstakeMethodName] - r := rand.New(rand.NewSource(42)) - validator := sample.Validator(t, r) - - staker := sample.Bech32AccAddress() - stakerEthAddr := common.BytesToAddress(staker.Bytes()) - coins := sample.Coins() - err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - require.NoError(t, err) - err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - require.NoError(t, err) - - stakerAddr := common.BytesToAddress(staker.Bytes()) - mockVMContract.CallerAddress = stakerAddr - - args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} - mockVMContract.Input = packInputArgs(t, methodID, args...) - - // ACT - _, err = contract.Run(mockEVM, mockVMContract, false) - - // ASSERT - require.Error(t, err) - require.ErrorIs(t, err, ptypes.ErrDisabledMethod{ - Method: UnstakeMethodName, - }) - }) + prototype := prototype.NewIPrototypeContract(s.fungibleKeeper, appCodec, gasConfig) - // t.Run("should fail in read only method", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) - // methodID := abi.Methods[UnstakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validator := sample.Validator(t, r) - - // staker := sample.Bech32AccAddress() - // stakerEthAddr := common.BytesToAddress(staker.Bytes()) - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - // mockVMContract.CallerAddress = stakerAddr - - // args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} - // mockVMContract.Input = packInputArgs(t, methodID, args...) - - // // ACT - // _, err = contract.Run(mockEVM, mockVMContract, true) - - // // ASSERT - // require.ErrorIs(t, err, ptypes.ErrWriteMethod{Method: UnstakeMethodName}) - // }) - - // t.Run("should fail if validator doesn't exist", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) - // methodID := abi.Methods[UnstakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validator := sample.Validator(t, r) - - // staker := sample.Bech32AccAddress() - // stakerEthAddr := common.BytesToAddress(staker.Bytes()) - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - // mockVMContract.CallerAddress = stakerAddr - - // args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} - // mockVMContract.Input = packInputArgs(t, methodID, args...) - - // // ACT - // _, err = contract.Run(mockEVM, mockVMContract, false) - - // // ASSERT - // require.Error(t, err) - // }) - - // t.Run("should unstake", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) - // methodID := abi.Methods[UnstakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validator := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) - - // staker := sample.Bech32AccAddress() - // stakerEthAddr := common.BytesToAddress(staker.Bytes()) - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - // mockVMContract.CallerAddress = stakerAddr - - // args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} - - // // stake first - // stakeMethodID := abi.Methods[StakeMethodName] - // mockVMContract.Input = packInputArgs(t, stakeMethodID, args...) - // _, err = contract.Run(mockEVM, mockVMContract, false) - // require.NoError(t, err) - - // // ACT - // mockVMContract.Input = packInputArgs(t, methodID, args...) - // _, err = contract.Run(mockEVM, mockVMContract, false) - - // // ASSERT - // require.NoError(t, err) - // }) - - // t.Run("should fail if caller is not staker", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) - // methodID := abi.Methods[UnstakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validator := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) - - // staker := sample.Bech32AccAddress() - // stakerEthAddr := common.BytesToAddress(staker.Bytes()) - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - // mockVMContract.CallerAddress = stakerAddr - - // args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} - // // stake first - // stakeMethodID := abi.Methods[StakeMethodName] - // mockVMContract.Input = packInputArgs(t, stakeMethodID, args...) - // _, err = contract.Run(mockEVM, mockVMContract, false) - // require.NoError(t, err) - - // callerEthAddr := common.BytesToAddress(sample.Bech32AccAddress().Bytes()) - // mockVMContract.CallerAddress = callerEthAddr - // mockVMContract.Input = packInputArgs(t, methodID, args...) - - // // ACT - // _, err = contract.Run(mockEVM, mockVMContract, false) - - // // ASSERT - // require.ErrorContains(t, err, "caller is not staker address") - // }) - - // t.Run("should fail if no previous staking", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) - // methodID := abi.Methods[UnstakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validator := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) - - // staker := sample.Bech32AccAddress() - // stakerEthAddr := common.BytesToAddress(staker.Bytes()) - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - // mockVMContract.CallerAddress = stakerAddr - - // args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} - // mockVMContract.Input = packInputArgs(t, methodID, args...) - - // // ACT - // _, err = contract.Run(mockEVM, mockVMContract, false) - - // // ASSERT - // require.Error(t, err) - // }) - - // t.Run("should fail if wrong args amount", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, _ := setup(t) - // methodID := abi.Methods[UnstakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validator := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) - - // staker := sample.Bech32AccAddress() - // stakerEthAddr := common.BytesToAddress(staker.Bytes()) - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - - // args := []interface{}{stakerEthAddr, validator.OperatorAddress} - - // // ACT - // _, err = contract.Unstake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &methodID, args) - - // // ASSERT - // require.Error(t, err) - // }) - - // t.Run("should fail if staker is not eth addr", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, _ := setup(t) - // methodID := abi.Methods[UnstakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validator := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) - - // staker := sample.Bech32AccAddress() - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - - // args := []interface{}{staker, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} - - // // ACT - // _, err = contract.Unstake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &methodID, args) - - // // ASSERT - // require.Error(t, err) - // }) - - // t.Run("should fail if validator is not valid string", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, _ := setup(t) - // methodID := abi.Methods[UnstakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validator := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) - - // staker := sample.Bech32AccAddress() - // stakerEthAddr := common.BytesToAddress(staker.Bytes()) - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - - // args := []interface{}{stakerEthAddr, 42, coins.AmountOf(config.BaseDenom).BigInt()} - - // // ACT - // _, err = contract.Unstake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &methodID, args) - - // // ASSERT - // require.Error(t, err) - // }) - - // t.Run("should fail if amount is not int64", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, _ := setup(t) - // methodID := abi.Methods[UnstakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validator := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validator) - - // staker := sample.Bech32AccAddress() - // stakerEthAddr := common.BytesToAddress(staker.Bytes()) - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - - // args := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).Uint64()} - - // // ACT - // _, err = contract.Unstake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &methodID, args) - - // // ASSERT - // require.Error(t, err) - // }) + prototypeAbi := prototype.Abi() + methodID := prototypeAbi.Methods["bech32ToHexAddr"] + args := []interface{}{"123"} + s.mockVMContract.Input = packInputArgs(t, methodID, args...) + + // ACT + _, err := s.contract.Run(s.mockEVM, s.mockVMContract, false) + + // ASSERT + require.Error(t, err) } -func Test_MoveStake(t *testing.T) { - // Disabled until further notice, check https://github.com/zeta-chain/node/issues/3005. - t.Run("should fail with error disabled", func(t *testing.T) { - // ARRANGE - ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) - methodID := abi.Methods[MoveStakeMethodName] - r := rand.New(rand.NewSource(42)) - validatorSrc := sample.Validator(t, r) - sdkKeepers.StakingKeeper.SetValidator(ctx, validatorSrc) - validatorDest := sample.Validator(t, r) - - staker := sample.Bech32AccAddress() - stakerEthAddr := common.BytesToAddress(staker.Bytes()) - coins := sample.Coins() - err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - require.NoError(t, err) - err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - require.NoError(t, err) - - stakerAddr := common.BytesToAddress(staker.Bytes()) - mockVMContract.CallerAddress = stakerAddr - - argsStake := []interface{}{ - stakerEthAddr, - validatorSrc.OperatorAddress, - coins.AmountOf(config.BaseDenom).BigInt(), - } +func setup(t *testing.T) (sdk.Context, *Contract, abi.ABI, keeper.SDKKeepers, *vm.EVM, *vm.Contract) { + // Initialize state. + // Get sdk keepers initialized with this state and the context. + cdc := keeper.NewCodec() + db := tmdb.NewMemDB() + stateStore := store.NewCommitMultiStore(db) + keys, memKeys, tkeys, allKeys := keeper.StoreKeys() - // stake to validator src - stakeMethodID := abi.Methods[StakeMethodName] - mockVMContract.Input = packInputArgs(t, stakeMethodID, argsStake...) - _, err = contract.Run(mockEVM, mockVMContract, false) - require.Error(t, err) - require.ErrorIs(t, err, ptypes.ErrDisabledMethod{ - Method: StakeMethodName, - }) + sdkKeepers := keeper.NewSDKKeepersWithKeys(cdc, keys, memKeys, tkeys, allKeys) - argsMoveStake := []interface{}{ - stakerEthAddr, - validatorSrc.OperatorAddress, - validatorDest.OperatorAddress, - coins.AmountOf(config.BaseDenom).BigInt(), - } - mockVMContract.Input = packInputArgs(t, methodID, argsMoveStake...) + for _, key := range keys { + stateStore.MountStoreWithDB(key, storetypes.StoreTypeIAVL, db) + } + for _, key := range tkeys { + stateStore.MountStoreWithDB(key, storetypes.StoreTypeTransient, nil) + } + for _, key := range memKeys { + stateStore.MountStoreWithDB(key, storetypes.StoreTypeMemory, nil) + } - // ACT - _, err = contract.Run(mockEVM, mockVMContract, false) + require.NoError(t, stateStore.LoadLatestVersion()) - // ASSERT - require.Error(t, err) - require.ErrorIs(t, err, ptypes.ErrDisabledMethod{ - Method: MoveStakeMethodName, - }) - }) + ctx := keeper.NewContext(stateStore) + + // Intiliaze codecs and gas config. + var encoding ethermint.EncodingConfig + appCodec := encoding.Codec + gasConfig := storetypes.TransientGasConfig() + + stakingGenesisState := stakingtypes.DefaultGenesisState() + stakingGenesisState.Params.BondDenom = config.BaseDenom + sdkKeepers.StakingKeeper.InitGenesis(ctx, stakingGenesisState) + + // Get the fungible keeper. + fungibleKeeper, _, _, _ := keeper.FungibleKeeper(t) + + accAddress := sdk.AccAddress(ContractAddress.Bytes()) + fungibleKeeper.GetAuthKeeper().SetAccount(ctx, authtypes.NewBaseAccount(accAddress, nil, 0, 0)) + + // Initialize staking contract. + contract := NewIStakingContract( + ctx, + &sdkKeepers.StakingKeeper, + *fungibleKeeper, + sdkKeepers.BankKeeper, + appCodec, + gasConfig, + ) + require.NotNil(t, contract, "NewIStakingContract() should not return a nil contract") + + abi := contract.Abi() + require.NotNil(t, abi, "contract ABI should not be nil") + + address := contract.Address() + require.NotNil(t, address, "contract address should not be nil") + + mockEVM := vm.NewEVM( + vm.BlockContext{}, + vm.TxContext{}, + statedb.New(ctx, sdkKeepers.EvmKeeper, statedb.TxConfig{}), + ¶ms.ChainConfig{}, + vm.Config{}, + ) + + mockVMContract := vm.NewContract( + contractRef{address: common.Address{}}, + contractRef{address: ContractAddress}, + uint256.NewInt(0), + 0, + ) - // t.Run("should fail in read only method", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) - // methodID := abi.Methods[MoveStakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validatorSrc := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorSrc) - // validatorDest := sample.Validator(t, r) - - // staker := sample.Bech32AccAddress() - // stakerEthAddr := common.BytesToAddress(staker.Bytes()) - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - // mockVMContract.CallerAddress = stakerAddr - - // argsStake := []interface{}{ - // stakerEthAddr, - // validatorSrc.OperatorAddress, - // coins.AmountOf(config.BaseDenom).BigInt(), - // } - - // // stake to validator src - // stakeMethodID := abi.Methods[StakeMethodName] - // mockVMContract.Input = packInputArgs(t, stakeMethodID, argsStake...) - // _, err = contract.Run(mockEVM, mockVMContract, false) - // require.NoError(t, err) - - // argsMoveStake := []interface{}{ - // stakerEthAddr, - // validatorSrc.OperatorAddress, - // validatorDest.OperatorAddress, - // coins.AmountOf(config.BaseDenom).BigInt(), - // } - // mockVMContract.Input = packInputArgs(t, methodID, argsMoveStake...) - - // // ACT - // _, err = contract.Run(mockEVM, mockVMContract, true) - - // // ASSERT - // require.ErrorIs(t, err, ptypes.ErrWriteMethod{Method: MoveStakeMethodName}) - // }) - - // t.Run("should fail if validator dest doesn't exist", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) - // methodID := abi.Methods[MoveStakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validatorSrc := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorSrc) - // validatorDest := sample.Validator(t, r) - - // staker := sample.Bech32AccAddress() - // stakerEthAddr := common.BytesToAddress(staker.Bytes()) - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - // mockVMContract.CallerAddress = stakerAddr - - // argsStake := []interface{}{ - // stakerEthAddr, - // validatorSrc.OperatorAddress, - // coins.AmountOf(config.BaseDenom).BigInt(), - // } - - // // stake to validator src - // stakeMethodID := abi.Methods[StakeMethodName] - // mockVMContract.Input = packInputArgs(t, stakeMethodID, argsStake...) - // _, err = contract.Run(mockEVM, mockVMContract, false) - // require.NoError(t, err) - - // argsMoveStake := []interface{}{ - // stakerEthAddr, - // validatorSrc.OperatorAddress, - // validatorDest.OperatorAddress, - // coins.AmountOf(config.BaseDenom).BigInt(), - // } - // mockVMContract.Input = packInputArgs(t, methodID, argsMoveStake...) - - // // ACT - // _, err = contract.Run(mockEVM, mockVMContract, false) - - // // ASSERT - // require.Error(t, err) - // }) - - // t.Run("should move stake", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) - // methodID := abi.Methods[MoveStakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validatorSrc := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorSrc) - // validatorDest := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorDest) - - // staker := sample.Bech32AccAddress() - // stakerEthAddr := common.BytesToAddress(staker.Bytes()) - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - // mockVMContract.CallerAddress = stakerAddr - // argsStake := []interface{}{ - // stakerEthAddr, - // validatorSrc.OperatorAddress, - // coins.AmountOf(config.BaseDenom).BigInt(), - // } - - // // stake to validator src - // stakeMethodID := abi.Methods[StakeMethodName] - // mockVMContract.Input = packInputArgs(t, stakeMethodID, argsStake...) - // _, err = contract.Run(mockEVM, mockVMContract, false) - // require.NoError(t, err) - - // argsMoveStake := []interface{}{ - // stakerEthAddr, - // validatorSrc.OperatorAddress, - // validatorDest.OperatorAddress, - // coins.AmountOf(config.BaseDenom).BigInt(), - // } - // mockVMContract.Input = packInputArgs(t, methodID, argsMoveStake...) - - // // ACT - // // move stake to validator dest - // _, err = contract.Run(mockEVM, mockVMContract, false) - - // // ASSERT - // require.NoError(t, err) - // }) - - // t.Run("should fail if staker is invalid arg", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, _ := setup(t) - // methodID := abi.Methods[MoveStakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validatorSrc := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorSrc) - // validatorDest := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorDest) - - // staker := sample.Bech32AccAddress() - // stakerEthAddr := common.BytesToAddress(staker.Bytes()) - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - - // argsStake := []interface{}{ - // stakerEthAddr, - // validatorSrc.OperatorAddress, - // coins.AmountOf(config.BaseDenom).BigInt(), - // } - - // // stake to validator src - // stakeMethodID := abi.Methods[StakeMethodName] - // _, err = contract.Stake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &stakeMethodID, argsStake) - // require.NoError(t, err) - - // argsMoveStake := []interface{}{ - // 42, - // validatorSrc.OperatorAddress, - // validatorDest.OperatorAddress, - // coins.AmountOf(config.BaseDenom).BigInt(), - // } - - // // ACT - // _, err = contract.MoveStake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &methodID, argsMoveStake) - - // // ASSERT - // require.Error(t, err) - // }) - - // t.Run("should fail if validator src is invalid arg", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, _ := setup(t) - // methodID := abi.Methods[MoveStakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validatorSrc := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorSrc) - // validatorDest := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorDest) - - // staker := sample.Bech32AccAddress() - // stakerEthAddr := common.BytesToAddress(staker.Bytes()) - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - - // argsStake := []interface{}{ - // stakerEthAddr, - // validatorSrc.OperatorAddress, - // coins.AmountOf(config.BaseDenom).BigInt(), - // } - - // // stake to validator src - // stakeMethodID := abi.Methods[StakeMethodName] - // _, err = contract.Stake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &stakeMethodID, argsStake) - // require.NoError(t, err) - - // argsMoveStake := []interface{}{ - // stakerEthAddr, - // 42, - // validatorDest.OperatorAddress, - // coins.AmountOf(config.BaseDenom).BigInt(), - // } - - // // ACT - // _, err = contract.MoveStake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &methodID, argsMoveStake) - - // // ASSERT - // require.Error(t, err) - // }) - - // t.Run("should fail if validator dest is invalid arg", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, _ := setup(t) - // methodID := abi.Methods[MoveStakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validatorSrc := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorSrc) - // validatorDest := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorDest) - - // staker := sample.Bech32AccAddress() - // stakerEthAddr := common.BytesToAddress(staker.Bytes()) - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - - // argsStake := []interface{}{ - // stakerEthAddr, - // validatorSrc.OperatorAddress, - // coins.AmountOf(config.BaseDenom).BigInt(), - // } - - // // stake to validator src - // stakeMethodID := abi.Methods[StakeMethodName] - // _, err = contract.Stake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &stakeMethodID, argsStake) - // require.NoError(t, err) - - // argsMoveStake := []interface{}{ - // stakerEthAddr, - // validatorSrc.OperatorAddress, - // 42, - // coins.AmountOf(config.BaseDenom).BigInt(), - // } - - // // ACT - // _, err = contract.MoveStake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &methodID, argsMoveStake) - - // // ASSERT - // require.Error(t, err) - // }) - - // t.Run("should fail if amount is invalid arg", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, _ := setup(t) - // methodID := abi.Methods[MoveStakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validatorSrc := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorSrc) - // validatorDest := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorDest) - - // staker := sample.Bech32AccAddress() - // stakerEthAddr := common.BytesToAddress(staker.Bytes()) - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - - // argsStake := []interface{}{ - // stakerEthAddr, - // validatorSrc.OperatorAddress, - // coins.AmountOf(config.BaseDenom).BigInt(), - // } - - // // stake to validator src - // stakeMethodID := abi.Methods[StakeMethodName] - // _, err = contract.Stake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &stakeMethodID, argsStake) - // require.NoError(t, err) - - // argsMoveStake := []interface{}{ - // stakerEthAddr, - // validatorSrc.OperatorAddress, - // validatorDest.OperatorAddress, - // coins.AmountOf(config.BaseDenom).Uint64(), - // } - - // // ACT - // _, err = contract.MoveStake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &methodID, argsMoveStake) - - // // ASSERT - // require.Error(t, err) - // }) - - // t.Run("should fail if wrong args amount", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, _ := setup(t) - // methodID := abi.Methods[MoveStakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validatorSrc := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorSrc) - // validatorDest := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorDest) - - // staker := sample.Bech32AccAddress() - // stakerEthAddr := common.BytesToAddress(staker.Bytes()) - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - - // argsStake := []interface{}{ - // stakerEthAddr, - // validatorSrc.OperatorAddress, - // coins.AmountOf(config.BaseDenom).BigInt(), - // } - - // // stake to validator src - // stakeMethodID := abi.Methods[StakeMethodName] - // _, err = contract.Stake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &stakeMethodID, argsStake) - // require.NoError(t, err) - - // argsMoveStake := []interface{}{stakerEthAddr, validatorSrc.OperatorAddress, validatorDest.OperatorAddress} - - // // ACT - // _, err = contract.MoveStake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &methodID, argsMoveStake) - - // // ASSERT - // require.Error(t, err) - // }) - - // t.Run("should fail if caller is not staker", func(t *testing.T) { - // // ARRANGE - // ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) - // methodID := abi.Methods[MoveStakeMethodName] - // r := rand.New(rand.NewSource(42)) - // validatorSrc := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorSrc) - // validatorDest := sample.Validator(t, r) - // sdkKeepers.StakingKeeper.SetValidator(ctx, validatorDest) - - // staker := sample.Bech32AccAddress() - // stakerEthAddr := common.BytesToAddress(staker.Bytes()) - // coins := sample.Coins() - // err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - // require.NoError(t, err) - // err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - // require.NoError(t, err) - - // stakerAddr := common.BytesToAddress(staker.Bytes()) - // mockVMContract.CallerAddress = stakerAddr - // argsStake := []interface{}{ - // stakerEthAddr, - // validatorSrc.OperatorAddress, - // coins.AmountOf(config.BaseDenom).BigInt(), - // } - - // // stake to validator src - // stakeMethodID := abi.Methods[StakeMethodName] - // mockVMContract.Input = packInputArgs(t, stakeMethodID, argsStake...) - // _, err = contract.Run(mockEVM, mockVMContract, false) - // require.NoError(t, err) - - // argsMoveStake := []interface{}{ - // stakerEthAddr, - // validatorSrc.OperatorAddress, - // validatorDest.OperatorAddress, - // coins.AmountOf(config.BaseDenom).BigInt(), - // } - // mockVMContract.Input = packInputArgs(t, methodID, argsMoveStake...) - - // callerEthAddr := common.BytesToAddress(sample.Bech32AccAddress().Bytes()) - // mockVMContract.CallerAddress = callerEthAddr - - // // ACT - // _, err = contract.Run(mockEVM, mockVMContract, false) - - // // ASSERT - // require.ErrorContains(t, err, "caller is not staker") - // }) + return ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract } -func Test_GetAllValidators(t *testing.T) { - t.Run("should return empty array if validators not set", func(t *testing.T) { - // ARRANGE - _, contract, abi, _, mockEVM, mockVMContract := setup(t) - methodID := abi.Methods[GetAllValidatorsMethodName] - mockVMContract.Input = methodID.ID +/* + Complete Test Suite + TODO: Migrate all staking tests to this suite. +*/ + +type testSuite struct { + ctx sdk.Context + contract *Contract + contractABI *abi.ABI + fungibleKeeper *fungiblekeeper.Keeper + sdkKeepers keeper.SDKKeepers + mockEVM *vm.EVM + mockVMContract *vm.Contract + methodID abi.Method + defaultCaller common.Address + defaultLocker common.Address + zrc20Address common.Address + zrc20ABI *abi.ABI +} - // ACT - validators, err := contract.Run(mockEVM, mockVMContract, false) +func newTestSuite(t *testing.T) testSuite { + // Initialize basic parameters to mock the chain. + fungibleKeeper, ctx, sdkKeepers, _ := keeper.FungibleKeeper(t) + chainID := getValidChainID(t) - // ASSERT - require.NoError(t, err) + // Make sure the account store is initialized. + // This is completely needed for accounts to be created in the state. + fungibleKeeper.GetAuthKeeper().GetModuleAccount(ctx, fungibletypes.ModuleName) - res, err := methodID.Outputs.Unpack(validators) - require.NoError(t, err) + // Deploy system contracts in order to deploy a ZRC20 token. + deploySystemContracts(t, ctx, fungibleKeeper, *sdkKeepers.EvmKeeper) + zrc20Address := setupGasCoin(t, ctx, fungibleKeeper, sdkKeepers.EvmKeeper, chainID, "ZRC20", "ZRC20") - require.Empty(t, res[0]) - }) + // Keepers and chain configuration. + var encoding ethermint.EncodingConfig + appCodec := encoding.Codec + gasConfig := storetypes.TransientGasConfig() - t.Run("should return validators if set", func(t *testing.T) { - // ARRANGE - ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) - methodID := abi.Methods[GetAllValidatorsMethodName] - mockVMContract.Input = methodID.ID - r := rand.New(rand.NewSource(42)) - validator := sample.Validator(t, r) - sdkKeepers.StakingKeeper.SetValidator(ctx, validator) + // Create the staking contract. + contract := NewIStakingContract( + ctx, + &sdkKeepers.StakingKeeper, + *fungibleKeeper, + sdkKeepers.BankKeeper, + appCodec, + gasConfig, + ) + require.NotNil(t, contract, "NewIStakingContract() should not return a nil contract") - // ACT - validators, err := contract.Run(mockEVM, mockVMContract, false) + accAddress := sdk.AccAddress(ContractAddress.Bytes()) + fungibleKeeper.GetAuthKeeper().SetAccount(ctx, authtypes.NewBaseAccount(accAddress, nil, 0, 0)) - // ASSERT - require.NoError(t, err) + abi := contract.Abi() + require.NotNil(t, abi, "contract ABI should not be nil") - res, err := methodID.Outputs.Unpack(validators) - require.NoError(t, err) + address := contract.Address() + require.NotNil(t, address, "contract address should not be nil") - require.NotEmpty(t, res[0]) - }) + mockEVM := vm.NewEVM( + vm.BlockContext{}, + vm.TxContext{}, + statedb.New(ctx, sdkKeepers.EvmKeeper, statedb.TxConfig{}), + ¶ms.ChainConfig{}, + vm.Config{}, + ) + + mockVMContract := vm.NewContract( + contractRef{address: common.Address{}}, + contractRef{address: ContractAddress}, + uint256.NewInt(0), + 0, + ) + + zrc20ABI, err := zrc20.ZRC20MetaData.GetAbi() + require.NoError(t, err) + + // Default locker is the bank address. + locker := common.HexToAddress("0x0000000000000000000000000000000000000067") + + // Set default caller. + caller := fungibletypes.ModuleAddressEVM + mockVMContract.CallerAddress = caller + mockEVM.Origin = caller + + return testSuite{ + ctx, + contract, + &abi, + fungibleKeeper, + sdkKeepers, + mockEVM, + mockVMContract, + abi.Methods[DistributeMethodName], + caller, + locker, + zrc20Address, + zrc20ABI, + } } -func Test_GetShares(t *testing.T) { - t.Run("should return stakes", func(t *testing.T) { - // ARRANGE - ctx, contract, abi, sdkKeepers, mockEVM, mockVMContract := setup(t) - methodID := abi.Methods[GetSharesMethodName] - r := rand.New(rand.NewSource(42)) - validator := sample.Validator(t, r) - sdkKeepers.StakingKeeper.SetValidator(ctx, validator) - - staker := sample.Bech32AccAddress() - stakerEthAddr := common.BytesToAddress(staker.Bytes()) - coins := sample.Coins() - err := sdkKeepers.BankKeeper.MintCoins(ctx, fungibletypes.ModuleName, sample.Coins()) - require.NoError(t, err) - err = sdkKeepers.BankKeeper.SendCoinsFromModuleToAccount(ctx, fungibletypes.ModuleName, staker, coins) - require.NoError(t, err) - - stakerAddr := common.BytesToAddress(staker.Bytes()) - - stakeArgs := []interface{}{stakerEthAddr, validator.OperatorAddress, coins.AmountOf(config.BaseDenom).BigInt()} - - stakeMethodID := abi.Methods[StakeMethodName] - - // ACT - _, err = contract.Stake(ctx, mockEVM, &vm.Contract{CallerAddress: stakerAddr}, &stakeMethodID, stakeArgs) - require.NoError(t, err) - - // ASSERT - args := []interface{}{stakerEthAddr, validator.OperatorAddress} - mockVMContract.Input = packInputArgs(t, methodID, args...) - stakes, err := contract.Run(mockEVM, mockVMContract, false) - require.NoError(t, err) - - res, err := methodID.Outputs.Unpack(stakes) - require.NoError(t, err) - require.Equal( - t, - fmt.Sprintf("%d000000000000000000", coins.AmountOf(config.BaseDenom).BigInt().Int64()), - res[0].(*big.Int).String(), - ) - }) +func packInputArgs(t *testing.T, methodID abi.Method, args ...interface{}) []byte { + input, err := methodID.Inputs.Pack(args...) + require.NoError(t, err) + return append(methodID.ID, input...) +} - t.Run("should fail if wrong args amount", func(t *testing.T) { - // ARRANGE - ctx, contract, abi, _, _, _ := setup(t) - methodID := abi.Methods[GetSharesMethodName] - staker := sample.Bech32AccAddress() - stakerEthAddr := common.BytesToAddress(staker.Bytes()) - args := []interface{}{stakerEthAddr} +func allowStaking(t *testing.T, ts testSuite, amount *big.Int) { + resAllowance, err := callEVM( + t, + ts.ctx, + ts.fungibleKeeper, + ts.zrc20ABI, + fungibletypes.ModuleAddressEVM, + ts.zrc20Address, + "approve", + []interface{}{ts.contract.Address(), amount}, + ) + require.NoError(t, err, "error allowing staking to spend ZRC20 tokens") - // ACT - _, err := contract.GetShares(ctx, &methodID, args) + allowed, ok := resAllowance[0].(bool) + require.True(t, ok) + require.True(t, allowed) +} - // ASSERT - require.Error(t, err) - }) +func callEVM( + t *testing.T, + ctx sdk.Context, + fungibleKeeper *fungiblekeeper.Keeper, + abi *abi.ABI, + from common.Address, + dst common.Address, + method string, + args []interface{}, +) ([]interface{}, error) { + res, err := fungibleKeeper.CallEVM( + ctx, // ctx + *abi, // abi + from, // from + dst, // to + big.NewInt(0), // value + nil, // gasLimit + true, // commit + true, // noEthereumTxEvent + method, // method + args..., // args + ) + require.NoError(t, err, "CallEVM error") + require.Equal(t, "", res.VmError, "res.VmError should be empty") - t.Run("should fail if invalid staker arg", func(t *testing.T) { - // ARRANGE - ctx, contract, abi, _, _, _ := setup(t) - methodID := abi.Methods[GetSharesMethodName] - r := rand.New(rand.NewSource(42)) - validator := sample.Validator(t, r) - args := []interface{}{42, validator.OperatorAddress} + ret, err := abi.Methods[method].Outputs.Unpack(res.Ret) + require.NoError(t, err, "Unpack error") - // ACT - _, err := contract.GetShares(ctx, &methodID, args) + return ret, nil +} - // ASSERT - require.Error(t, err) - }) +// setupGasCoin is a helper function to setup the gas coin for testing +func setupGasCoin( + t *testing.T, + ctx sdk.Context, + k *fungiblekeeper.Keeper, + evmk *evmkeeper.Keeper, + chainID int64, + assetName string, + symbol string, +) (zrc20 common.Address) { + addr, err := k.SetupChainGasCoinAndPool( + ctx, + chainID, + assetName, + symbol, + 8, + nil, + ) + require.NoError(t, err) + assertContractDeployment(t, *evmk, ctx, addr) + return addr +} + +// get a valid chain id independently of the build flag +func getValidChainID(t *testing.T) int64 { + list := chains.DefaultChainsList() + require.NotEmpty(t, list) + require.NotNil(t, list[0]) + return list[0].ChainId +} - t.Run("should fail if invalid val address", func(t *testing.T) { - // ARRANGE - ctx, contract, abi, _, _, _ := setup(t) - methodID := abi.Methods[GetSharesMethodName] - staker := sample.Bech32AccAddress() - stakerEthAddr := common.BytesToAddress(staker.Bytes()) - args := []interface{}{stakerEthAddr, staker.String()} +// require that a contract has been deployed by checking stored code is non-empty. +func assertContractDeployment(t *testing.T, k evmkeeper.Keeper, ctx sdk.Context, contractAddress common.Address) { + acc := k.GetAccount(ctx, contractAddress) + require.NotNil(t, acc) + code := k.GetCode(ctx, common.BytesToHash(acc.CodeHash)) + require.NotEmpty(t, code) +} - // ACT - _, err := contract.GetShares(ctx, &methodID, args) +// deploySystemContracts deploys the system contracts and returns their addresses. +func deploySystemContracts( + t *testing.T, + ctx sdk.Context, + k *fungiblekeeper.Keeper, + evmk evmkeeper.Keeper, +) (wzeta, uniswapV2Factory, uniswapV2Router, connector, systemContract common.Address) { + var err error - // ASSERT - require.Error(t, err) - }) + wzeta, err = k.DeployWZETA(ctx) + require.NoError(t, err) + require.NotEmpty(t, wzeta) + assertContractDeployment(t, evmk, ctx, wzeta) - t.Run("should fail if invalid val address format", func(t *testing.T) { - // ARRANGE - ctx, contract, abi, _, _, _ := setup(t) - methodID := abi.Methods[GetSharesMethodName] - staker := sample.Bech32AccAddress() - stakerEthAddr := common.BytesToAddress(staker.Bytes()) - args := []interface{}{stakerEthAddr, 42} + uniswapV2Factory, err = k.DeployUniswapV2Factory(ctx) + require.NoError(t, err) + require.NotEmpty(t, uniswapV2Factory) + assertContractDeployment(t, evmk, ctx, uniswapV2Factory) - // ACT - _, err := contract.GetShares(ctx, &methodID, args) + uniswapV2Router, err = k.DeployUniswapV2Router02(ctx, uniswapV2Factory, wzeta) + require.NoError(t, err) + require.NotEmpty(t, uniswapV2Router) + assertContractDeployment(t, evmk, ctx, uniswapV2Router) - // ASSERT - require.Error(t, err) - }) + connector, err = k.DeployConnectorZEVM(ctx, wzeta) + require.NoError(t, err) + require.NotEmpty(t, connector) + assertContractDeployment(t, evmk, ctx, connector) + + systemContract, err = k.DeploySystemContract(ctx, wzeta, uniswapV2Factory, uniswapV2Router) + require.NoError(t, err) + require.NotEmpty(t, systemContract) + assertContractDeployment(t, evmk, ctx, systemContract) + + // deploy the gateway contract + contract := deployGatewayContract(t, ctx, k, &evmk, wzeta, sample.EthAddress()) + require.NotEmpty(t, contract) + + return } -func Test_RunInvalidMethod(t *testing.T) { - // ARRANGE - _, contract, _, _, mockEVM, mockVMContract := setup(t) - k, _, _, _ := keeper.FungibleKeeper(t) +// deploy upgradable gateway contract and return its address +func deployGatewayContract( + t *testing.T, + ctx sdk.Context, + k *fungiblekeeper.Keeper, + evmk *evmkeeper.Keeper, + wzeta, admin common.Address, +) common.Address { + // Deploy the gateway contract + implAddr, err := k.DeployContract(ctx, gatewayzevm.GatewayZEVMMetaData) + require.NoError(t, err) + require.NotEmpty(t, implAddr) + assertContractDeployment(t, *evmk, ctx, implAddr) - var encoding ethermint.EncodingConfig - appCodec := encoding.Codec - gasConfig := storetypes.TransientGasConfig() + // Deploy the proxy contract + gatewayABI, err := gatewayzevm.GatewayZEVMMetaData.GetAbi() + require.NoError(t, err) - prototype := prototype.NewIPrototypeContract(k, appCodec, gasConfig) + // Encode the initializer data + initializerData, err := gatewayABI.Pack("initialize", wzeta, admin) + require.NoError(t, err) - prototypeAbi := prototype.Abi() - methodID := prototypeAbi.Methods["bech32ToHexAddr"] - args := []interface{}{"123"} - mockVMContract.Input = packInputArgs(t, methodID, args...) + gatewayContract, err := k.DeployContract(ctx, erc1967proxy.ERC1967ProxyMetaData, implAddr, initializerData) + require.NoError(t, err) + require.NotEmpty(t, gatewayContract) + assertContractDeployment(t, *evmk, ctx, gatewayContract) - // ACT - _, err := contract.Run(mockEVM, mockVMContract, false) + // store the gateway in the system contract object + sys, found := k.GetSystemContract(ctx) + if !found { + sys = fungibletypes.SystemContract{} + } + sys.Gateway = gatewayContract.Hex() + k.SetSystemContract(ctx, sys) - // ASSERT - require.Error(t, err) + return gatewayContract +} + +type contractRef struct { + address common.Address +} + +func (c contractRef) Address() common.Address { + return c.address } diff --git a/precompiles/types/address.go b/precompiles/types/address.go new file mode 100644 index 0000000000..5e6654cc5b --- /dev/null +++ b/precompiles/types/address.go @@ -0,0 +1,48 @@ +package types + +import ( + "errors" + + sdk "github.com/cosmos/cosmos-sdk/types" + bank "github.com/cosmos/cosmos-sdk/x/bank/keeper" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" +) + +// GetEVMCallerAddress returns the caller address. +// Usually the caller is the contract.CallerAddress, which is the address of the contract that called the precompiled contract. +// If contract.CallerAddress != evm.Origin is true, it means the call was made through a contract, +// on which case there is a need to set the caller to the evm.Origin. +func GetEVMCallerAddress(evm *vm.EVM, contract *vm.Contract) (common.Address, error) { + if evm == nil || contract == nil { + return common.Address{}, errors.New("invalid input: evm or contract is nil") + } + + caller := contract.CallerAddress + if contract.CallerAddress != evm.Origin { + caller = evm.Origin + } + + return caller, nil +} + +// GetCosmosAddress returns the counterpart cosmos address of the given ethereum address. +// It checks if the address is empty or blocked by the bank keeper. +func GetCosmosAddress(bankKeeper bank.Keeper, addr common.Address) (sdk.AccAddress, error) { + toAddr := sdk.AccAddress(addr.Bytes()) + if toAddr.Empty() { + return nil, &ErrInvalidAddr{ + Got: toAddr.String(), + Reason: "empty address", + } + } + + if bankKeeper.BlockedAddr(toAddr) { + return nil, &ErrInvalidAddr{ + Got: toAddr.String(), + Reason: "destination address blocked by bank keeper", + } + } + + return toAddr, nil +} diff --git a/precompiles/types/address_test.go b/precompiles/types/address_test.go new file mode 100644 index 0000000000..81dd15b1bd --- /dev/null +++ b/precompiles/types/address_test.go @@ -0,0 +1,69 @@ +package types + +import ( + "testing" + + "github.com/holiman/uint256" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/stretchr/testify/require" + "github.com/zeta-chain/node/testutil/sample" +) + +func Test_GetEVMCallerAddress(t *testing.T) { + t.Run("should raise error when evm is nil", func(t *testing.T) { + _, mockVMContract := setupMockEVMAndContract(common.Address{}) + caller, err := GetEVMCallerAddress(nil, &mockVMContract) + require.Error(t, err) + require.Equal(t, common.Address{}, caller, "address should be zeroed") + }) + + t.Run("should raise error when contract is nil", func(t *testing.T) { + mockEVM, _ := setupMockEVMAndContract(common.Address{}) + caller, err := GetEVMCallerAddress(&mockEVM, nil) + require.Error(t, err) + require.Equal(t, common.Address{}, caller, "address should be zeroed") + }) + + // When contract.CallerAddress == evm.Origin, caller is set to contract.CallerAddress. + t.Run("when caller address equals origin", func(t *testing.T) { + mockEVM, mockVMContract := setupMockEVMAndContract(common.Address{}) + caller, err := GetEVMCallerAddress(&mockEVM, &mockVMContract) + require.NoError(t, err) + require.Equal(t, common.Address{}, caller, "address should be the same") + }) + + // When contract.CallerAddress != evm.Origin, caller should be set to evm.Origin. + t.Run("when caller address equals origin", func(t *testing.T) { + mockEVM, mockVMContract := setupMockEVMAndContract(sample.EthAddress()) + caller, err := GetEVMCallerAddress(&mockEVM, &mockVMContract) + require.NoError(t, err) + require.Equal(t, mockEVM.Origin, caller, "address should be evm.Origin") + }) +} + +func setupMockEVMAndContract(address common.Address) (vm.EVM, vm.Contract) { + mockEVM := vm.EVM{ + TxContext: vm.TxContext{ + Origin: address, + }, + } + + mockVMContract := vm.NewContract( + contractRef{address: common.Address{}}, + contractRef{address: common.Address{}}, + uint256.NewInt(0), + 0, + ) + + return mockEVM, *mockVMContract +} + +type contractRef struct { + address common.Address +} + +func (c contractRef) Address() common.Address { + return c.address +} diff --git a/precompiles/bank/coin.go b/precompiles/types/coin.go similarity index 59% rename from precompiles/bank/coin.go rename to precompiles/types/coin.go index 121ecdc1ee..4219040d42 100644 --- a/precompiles/bank/coin.go +++ b/precompiles/types/coin.go @@ -1,4 +1,4 @@ -package bank +package types import ( "math/big" @@ -7,19 +7,27 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/ethereum/go-ethereum/common" - ptypes "github.com/zeta-chain/node/precompiles/types" + "github.com/zeta-chain/node/cmd/zetacored/config" ) // ZRC20ToCosmosDenom returns the cosmos coin address for a given ZRC20 address. -// This is converted to "zevm/{ZRC20Address}". +// This is converted to "zrc20/{ZRC20Address}". func ZRC20ToCosmosDenom(ZRC20Address common.Address) string { - return ZRC20DenomPrefix + ZRC20Address.String() + return config.ZRC20DenomPrefix + ZRC20Address.String() } -func createCoinSet(tokenDenom string, amount *big.Int) (sdk.Coins, error) { - coin := sdk.NewCoin(tokenDenom, math.NewIntFromBigInt(amount)) +func CreateCoinSet(zrc20address common.Address, amount *big.Int) (sdk.Coins, error) { + defer func() { + if r := recover(); r != nil { + return + } + }() + + denom := ZRC20ToCosmosDenom(zrc20address) + + coin := sdk.NewCoin(denom, math.NewIntFromBigInt(amount)) if !coin.IsValid() { - return nil, &ptypes.ErrInvalidCoin{ + return nil, &ErrInvalidCoin{ Got: coin.GetDenom(), Negative: coin.IsNegative(), Nil: coin.IsNil(), @@ -28,10 +36,10 @@ func createCoinSet(tokenDenom string, amount *big.Int) (sdk.Coins, error) { // A sdk.Coins (type []sdk.Coin) has to be created because it's the type expected by MintCoins // and SendCoinsFromModuleToAccount. - // But sdk.Coins will only contain one coin, always. + // But coinSet will only contain one coin, always. coinSet := sdk.NewCoins(coin) if !coinSet.IsValid() || coinSet.Empty() || coinSet.IsAnyNil() || coinSet == nil { - return nil, &ptypes.ErrInvalidCoin{ + return nil, &ErrInvalidCoin{ Got: coinSet.String(), Negative: coinSet.IsAnyNegative(), Nil: coinSet.IsAnyNil(), diff --git a/precompiles/bank/coin_test.go b/precompiles/types/coin_test.go similarity index 81% rename from precompiles/bank/coin_test.go rename to precompiles/types/coin_test.go index 0a9669e0a6..6a8aac7d46 100644 --- a/precompiles/bank/coin_test.go +++ b/precompiles/types/coin_test.go @@ -1,4 +1,4 @@ -package bank +package types import ( "math/big" @@ -16,10 +16,11 @@ func Test_ZRC20ToCosmosDenom(t *testing.T) { } func Test_createCoinSet(t *testing.T) { - tokenDenom := "zrc20/0x0000000000000000000000000000000000003039" + tokenAddr := common.HexToAddress("0x0000000000000000000000000000000000003039") + tokenDenom := ZRC20ToCosmosDenom(tokenAddr) amount := big.NewInt(100) - coinSet, err := createCoinSet(tokenDenom, amount) + coinSet, err := CreateCoinSet(tokenAddr, amount) require.NoError(t, err, "createCoinSet should not return an error") require.NotNil(t, coinSet, "coinSet should not be nil") diff --git a/proto/zetachain/zetacore/crosschain/tx.proto b/proto/zetachain/zetacore/crosschain/tx.proto index cb76e53d0a..ed8b6b6f59 100644 --- a/proto/zetachain/zetacore/crosschain/tx.proto +++ b/proto/zetachain/zetacore/crosschain/tx.proto @@ -71,6 +71,7 @@ message MsgAddInboundTracker { } message MsgAddInboundTrackerResponse {} +// TODO: https://github.com/zeta-chain/node/issues/3083 message MsgWhitelistERC20 { string creator = 1; string erc20_address = 2; diff --git a/readme.md b/readme.md index 5e7230fe00..dd4b98b87f 100644 --- a/readme.md +++ b/readme.md @@ -87,6 +87,6 @@ Find below further documentation for development and running your own ZetaChain ## Community -[Twitter](https://twitter.com/zetablockchain) | +[X (formerly Twitter)](https://x.com/zetablockchain) | [Discord](https://discord.com/invite/zetachain) | [Telegram](https://t.me/zetachainofficial) | [Website](https://zetachain.com) diff --git a/rpc/backend/backend.go b/rpc/backend/backend.go index 80d33e0cd5..06252a90c8 100644 --- a/rpc/backend/backend.go +++ b/rpc/backend/backend.go @@ -28,6 +28,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/common/math" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rpc" @@ -131,7 +132,7 @@ type EVMBackend interface { PendingTransactions() ([]*sdk.Tx, error) GetCoinbase() (sdk.AccAddress, error) FeeHistory( - blockCount rpc.DecimalOrHex, + blockCount math.HexOrDecimal64, lastBlock rpc.BlockNumber, rewardPercentiles []float64, ) (*rpctypes.FeeHistoryResult, error) diff --git a/rpc/backend/blocks.go b/rpc/backend/blocks.go index a91118168a..60759d7e0a 100644 --- a/rpc/backend/blocks.go +++ b/rpc/backend/blocks.go @@ -404,8 +404,12 @@ func (b *Backend) HeaderByNumber(blockNum rpctypes.BlockNumber) (*ethtypes.Heade ) } - ethHeader := rpctypes.EthHeaderFromTendermint(resBlock.Block.Header, bloom, baseFee) - return ethHeader, nil + validatorAccount, err := GetValidatorAccount(&resBlock.Block.Header, b.queryClient) + if err != nil { + return nil, err + } + + return rpctypes.EthHeaderFromTendermint(resBlock.Block.Header, bloom, baseFee, validatorAccount), nil } // HeaderByHash returns the block header identified by hash. @@ -440,8 +444,12 @@ func (b *Backend) HeaderByHash(blockHash common.Hash) (*ethtypes.Header, error) ) } - ethHeader := rpctypes.EthHeaderFromTendermint(resBlock.Block.Header, bloom, baseFee) - return ethHeader, nil + validatorAccount, err := GetValidatorAccount(&resBlock.Block.Header, b.queryClient) + if err != nil { + return nil, err + } + + return rpctypes.EthHeaderFromTendermint(resBlock.Block.Header, bloom, baseFee, validatorAccount), nil } // BlockBloom query block bloom filter from block results @@ -613,7 +621,12 @@ func (b *Backend) EthBlockFromTendermintBlock( ) } - ethHeader := rpctypes.EthHeaderFromTendermint(block.Header, bloom, baseFee) + validatorAccount, err := GetValidatorAccount(&resBlock.Block.Header, b.queryClient) + if err != nil { + return nil, err + } + + ethHeader := rpctypes.EthHeaderFromTendermint(block.Header, bloom, baseFee, validatorAccount) msgs, additionals := b.EthMsgsFromTendermintBlock(resBlock, blockRes) txs := []*ethtypes.Transaction{} diff --git a/rpc/backend/blocks_test.go b/rpc/backend/blocks_test.go index 5da81d03c9..8fdea01533 100644 --- a/rpc/backend/blocks_test.go +++ b/rpc/backend/blocks_test.go @@ -1191,6 +1191,7 @@ func (suite *BackendTestSuite) TestHeaderByNumber() { var expResultBlock *tmrpctypes.ResultBlock _, bz := suite.buildEthereumTx() + validator := sdk.AccAddress(tests.GenerateAddress().Bytes()) testCases := []struct { name string @@ -1218,6 +1219,7 @@ func (suite *BackendTestSuite) TestHeaderByNumber() { height := blockNum.Int64() client := suite.backend.clientCtx.Client.(*mocks.Client) RegisterBlockNotFound(client, height) + }, false, }, @@ -1245,6 +1247,7 @@ func (suite *BackendTestSuite) TestHeaderByNumber() { queryClient := suite.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) RegisterBaseFeeError(queryClient) + RegisterValidatorAccount(queryClient, validator) }, true, }, @@ -1260,6 +1263,7 @@ func (suite *BackendTestSuite) TestHeaderByNumber() { queryClient := suite.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) RegisterBaseFee(queryClient, baseFee) + RegisterValidatorAccount(queryClient, validator) }, true, }, @@ -1275,6 +1279,7 @@ func (suite *BackendTestSuite) TestHeaderByNumber() { queryClient := suite.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) RegisterBaseFee(queryClient, baseFee) + RegisterValidatorAccount(queryClient, validator) }, true, }, @@ -1287,7 +1292,12 @@ func (suite *BackendTestSuite) TestHeaderByNumber() { header, err := suite.backend.HeaderByNumber(tc.blockNumber) if tc.expPass { - expHeader := ethrpc.EthHeaderFromTendermint(expResultBlock.Block.Header, ethtypes.Bloom{}, tc.baseFee) + expHeader := ethrpc.EthHeaderFromTendermint( + expResultBlock.Block.Header, + ethtypes.Bloom{}, + tc.baseFee, + validator, + ) suite.Require().NoError(err) suite.Require().Equal(expHeader, header) } else { @@ -1303,6 +1313,7 @@ func (suite *BackendTestSuite) TestHeaderByHash() { _, bz := suite.buildEthereumTx() block := tmtypes.MakeBlock(1, []tmtypes.Tx{bz}, nil, nil) emptyBlock := tmtypes.MakeBlock(1, []tmtypes.Tx{}, nil, nil) + validator := sdk.AccAddress(tests.GenerateAddress().Bytes()) testCases := []struct { name string @@ -1355,6 +1366,7 @@ func (suite *BackendTestSuite) TestHeaderByHash() { queryClient := suite.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) RegisterBaseFeeError(queryClient) + RegisterValidatorAccount(queryClient, validator) }, true, }, @@ -1370,6 +1382,7 @@ func (suite *BackendTestSuite) TestHeaderByHash() { queryClient := suite.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) RegisterBaseFee(queryClient, baseFee) + RegisterValidatorAccount(queryClient, validator) }, true, }, @@ -1385,6 +1398,7 @@ func (suite *BackendTestSuite) TestHeaderByHash() { queryClient := suite.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) RegisterBaseFee(queryClient, baseFee) + RegisterValidatorAccount(queryClient, validator) }, true, }, @@ -1397,7 +1411,12 @@ func (suite *BackendTestSuite) TestHeaderByHash() { header, err := suite.backend.HeaderByHash(tc.hash) if tc.expPass { - expHeader := ethrpc.EthHeaderFromTendermint(expResultBlock.Block.Header, ethtypes.Bloom{}, tc.baseFee) + expHeader := ethrpc.EthHeaderFromTendermint( + expResultBlock.Block.Header, + ethtypes.Bloom{}, + tc.baseFee, + validator, + ) suite.Require().NoError(err) suite.Require().Equal(expHeader, header) } else { @@ -1410,6 +1429,7 @@ func (suite *BackendTestSuite) TestHeaderByHash() { func (suite *BackendTestSuite) TestEthBlockByNumber() { msgEthereumTx, bz := suite.buildEthereumTx() emptyBlock := tmtypes.MakeBlock(1, []tmtypes.Tx{}, nil, nil) + validator := sdk.AccAddress(tests.GenerateAddress().Bytes()) testCases := []struct { name string @@ -1453,12 +1473,14 @@ func (suite *BackendTestSuite) TestEthBlockByNumber() { queryClient := suite.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) baseFee := sdk.NewInt(1) RegisterBaseFee(queryClient, baseFee) + RegisterValidatorAccount(queryClient, validator) }, ethtypes.NewBlock( ethrpc.EthHeaderFromTendermint( emptyBlock.Header, ethtypes.Bloom{}, sdk.NewInt(1).BigInt(), + validator, ), []*ethtypes.Transaction{}, nil, @@ -1479,12 +1501,14 @@ func (suite *BackendTestSuite) TestEthBlockByNumber() { queryClient := suite.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) baseFee := sdk.NewInt(1) RegisterBaseFee(queryClient, baseFee) + RegisterValidatorAccount(queryClient, validator) }, ethtypes.NewBlock( ethrpc.EthHeaderFromTendermint( emptyBlock.Header, ethtypes.Bloom{}, sdk.NewInt(1).BigInt(), + validator, ), []*ethtypes.Transaction{msgEthereumTx.AsTransaction()}, nil, @@ -1520,6 +1544,7 @@ func (suite *BackendTestSuite) TestEthBlockByNumber() { func (suite *BackendTestSuite) TestEthBlockFromTendermintBlock() { msgEthereumTx, bz := suite.buildEthereumTx() emptyBlock := tmtypes.MakeBlock(1, []tmtypes.Tx{}, nil, nil) + validator := sdk.AccAddress(tests.GenerateAddress().Bytes()) testCases := []struct { name string @@ -1543,12 +1568,14 @@ func (suite *BackendTestSuite) TestEthBlockFromTendermintBlock() { func(baseFee sdkmath.Int, blockNum int64) { queryClient := suite.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) RegisterBaseFee(queryClient, baseFee) + RegisterValidatorAccount(queryClient, validator) }, ethtypes.NewBlock( ethrpc.EthHeaderFromTendermint( emptyBlock.Header, ethtypes.Bloom{}, sdk.NewInt(1).BigInt(), + validator, ), []*ethtypes.Transaction{}, nil, @@ -1578,12 +1605,14 @@ func (suite *BackendTestSuite) TestEthBlockFromTendermintBlock() { func(baseFee sdkmath.Int, blockNum int64) { queryClient := suite.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) RegisterBaseFee(queryClient, baseFee) + RegisterValidatorAccount(queryClient, validator) }, ethtypes.NewBlock( ethrpc.EthHeaderFromTendermint( emptyBlock.Header, ethtypes.Bloom{}, sdk.NewInt(1).BigInt(), + validator, ), []*ethtypes.Transaction{msgEthereumTx.AsTransaction()}, nil, @@ -1657,6 +1686,8 @@ func (suite *BackendTestSuite) TestEthAndSyntheticEthBlockByNumber() { msgEthereumTx, _ := suite.buildEthereumTx() realTx := suite.signAndEncodeEthTx(msgEthereumTx) + validator := sdk.AccAddress(tests.GenerateAddress().Bytes()) + suite.backend.indexer = nil client := suite.backend.clientCtx.Client.(*mocks.Client) queryClient := suite.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) @@ -1664,6 +1695,7 @@ func (suite *BackendTestSuite) TestEthAndSyntheticEthBlockByNumber() { RegisterBlock(client, 1, []tmtypes.Tx{realTx, tx}) RegisterBlockResultsWithTxResults(client, 1, []*types.ResponseDeliverTx{{}, &txRes}) RegisterBaseFee(queryClient, sdk.NewInt(1)) + RegisterValidatorAccount(queryClient, validator) // only real should be returned block, err := suite.backend.EthBlockByNumber(1) diff --git a/rpc/backend/call_tx_test.go b/rpc/backend/call_tx_test.go index ec92db089b..7d8d95275e 100644 --- a/rpc/backend/call_tx_test.go +++ b/rpc/backend/call_tx_test.go @@ -24,6 +24,7 @@ func (suite *BackendTestSuite) TestResend() { gasPrice := new(hexutil.Big) toAddr := tests.GenerateAddress() chainID := (*hexutil.Big)(suite.backend.chainID) + validator := sdk.AccAddress(tests.GenerateAddress().Bytes()) callArgs := evmtypes.TransactionArgs{ From: nil, To: &toAddr, @@ -69,6 +70,7 @@ func (suite *BackendTestSuite) TestResend() { RegisterBlock(client, 1, nil) RegisterBlockResults(client, 1) RegisterBaseFeeDisabled(queryClient) + RegisterValidatorAccount(queryClient, validator) }, evmtypes.TransactionArgs{ Nonce: &txNonce, @@ -91,6 +93,7 @@ func (suite *BackendTestSuite) TestResend() { RegisterBlock(client, 1, nil) RegisterBlockResults(client, 1) RegisterBaseFee(queryClient, baseFee) + RegisterValidatorAccount(queryClient, validator) }, evmtypes.TransactionArgs{ Nonce: &txNonce, @@ -110,6 +113,7 @@ func (suite *BackendTestSuite) TestResend() { RegisterBlock(client, 1, nil) RegisterBlockResults(client, 1) RegisterBaseFeeDisabled(queryClient) + RegisterValidatorAccount(queryClient, validator) }, evmtypes.TransactionArgs{ Nonce: &txNonce, @@ -163,6 +167,7 @@ func (suite *BackendTestSuite) TestResend() { RegisterBlock(client, 1, nil) RegisterBlockResults(client, 1) RegisterBaseFee(queryClient, baseFee) + RegisterValidatorAccount(queryClient, validator) }, evmtypes.TransactionArgs{ Nonce: &txNonce, @@ -186,6 +191,7 @@ func (suite *BackendTestSuite) TestResend() { RegisterBlock(client, 1, nil) RegisterBlockResults(client, 1) RegisterBaseFee(queryClient, baseFee) + RegisterValidatorAccount(queryClient, validator) }, evmtypes.TransactionArgs{ Nonce: &txNonce, @@ -210,6 +216,7 @@ func (suite *BackendTestSuite) TestResend() { RegisterParams(queryClient, &header, 1) RegisterParamsWithoutHeader(queryClient, 1) RegisterUnconfirmedTxsError(client, nil) + RegisterValidatorAccount(queryClient, validator) }, evmtypes.TransactionArgs{ Nonce: &txNonce, @@ -238,6 +245,7 @@ func (suite *BackendTestSuite) TestResend() { RegisterParams(queryClient, &header, 1) RegisterParamsWithoutHeader(queryClient, 1) RegisterUnconfirmedTxsEmpty(client, nil) + RegisterValidatorAccount(queryClient, validator) }, evmtypes.TransactionArgs{ Nonce: &txNonce, @@ -448,6 +456,7 @@ func (suite *BackendTestSuite) TestDoCall() { func (suite *BackendTestSuite) TestGasPrice() { defaultGasPrice := (*hexutil.Big)(big.NewInt(1)) + validator := sdk.AccAddress(tests.GenerateAddress().Bytes()) testCases := []struct { name string @@ -467,6 +476,7 @@ func (suite *BackendTestSuite) TestGasPrice() { RegisterBlock(client, 1, nil) RegisterBlockResults(client, 1) RegisterBaseFee(queryClient, sdk.NewInt(1)) + RegisterValidatorAccount(queryClient, validator) }, defaultGasPrice, true, @@ -483,6 +493,7 @@ func (suite *BackendTestSuite) TestGasPrice() { RegisterBlock(client, 1, nil) RegisterBlockResults(client, 1) RegisterBaseFee(queryClient, sdk.NewInt(1)) + RegisterValidatorAccount(queryClient, validator) }, defaultGasPrice, false, diff --git a/rpc/backend/chain_info.go b/rpc/backend/chain_info.go index 7ace99cf34..6945cf0e28 100644 --- a/rpc/backend/chain_info.go +++ b/rpc/backend/chain_info.go @@ -25,6 +25,7 @@ import ( tmrpctypes "github.com/cometbft/cometbft/rpc/core/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/ethereum/go-ethereum/common/hexutil" + ethmath "github.com/ethereum/go-ethereum/common/math" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rpc" @@ -170,7 +171,7 @@ func (b *Backend) GetCoinbase() (sdk.AccAddress, error) { // FeeHistory returns data relevant for fee estimation based on the specified range of blocks. func (b *Backend) FeeHistory( - userBlockCount rpc.DecimalOrHex, // number blocks to fetch, maximum is 100 + userBlockCount ethmath.HexOrDecimal64, // number blocks to fetch, maximum is 100 lastBlock rpc.BlockNumber, // the block to start search , to oldest rewardPercentiles []float64, // percentiles to fetch reward ) (*rpctypes.FeeHistoryResult, error) { diff --git a/rpc/backend/chain_info_test.go b/rpc/backend/chain_info_test.go index b59e5c3461..da00f4dee6 100644 --- a/rpc/backend/chain_info_test.go +++ b/rpc/backend/chain_info_test.go @@ -14,6 +14,7 @@ import ( feemarkettypes "github.com/zeta-chain/ethermint/x/feemarket/types" "google.golang.org/grpc/metadata" + ethmath "github.com/ethereum/go-ethereum/common/math" "github.com/zeta-chain/node/rpc/backend/mocks" rpc "github.com/zeta-chain/node/rpc/types" ) @@ -321,7 +322,7 @@ func (suite *BackendTestSuite) TestFeeHistory() { testCases := []struct { name string registerMock func(validator sdk.AccAddress) - userBlockCount ethrpc.DecimalOrHex + userBlockCount ethmath.HexOrDecimal64 latestBlock ethrpc.BlockNumber expFeeHistory *rpc.FeeHistoryResult validator sdk.AccAddress diff --git a/rpc/backend/node_info.go b/rpc/backend/node_info.go index 0a5c9c0b0b..180389298e 100644 --- a/rpc/backend/node_info.go +++ b/rpc/backend/node_info.go @@ -81,8 +81,10 @@ func (b *Backend) Syncing() (interface{}, error) { } return map[string]interface{}{ + // #nosec G115 block height always positive "startingBlock": hexutil.Uint64(status.SyncInfo.EarliestBlockHeight), - "currentBlock": hexutil.Uint64(status.SyncInfo.LatestBlockHeight), + // #nosec G115 block height always positive + "currentBlock": hexutil.Uint64(status.SyncInfo.LatestBlockHeight), // "highestBlock": nil, // NA // "pulledStates": nil, // NA // "knownStates": nil, // NA diff --git a/rpc/backend/sign_tx.go b/rpc/backend/sign_tx.go index 105f161976..ad3175e41b 100644 --- a/rpc/backend/sign_tx.go +++ b/rpc/backend/sign_tx.go @@ -26,9 +26,9 @@ import ( "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" - ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/signer/core/apitypes" + ethermint "github.com/zeta-chain/ethermint/types" evmtypes "github.com/zeta-chain/ethermint/x/evm/types" ) @@ -70,7 +70,7 @@ func (b *Backend) SendTransaction(args evmtypes.TransactionArgs) (common.Hash, e return common.Hash{}, err } - signer := ethtypes.MakeSigner(b.ChainConfig(), new(big.Int).SetUint64(uint64(bn))) + signer := ethermint.MakeSigner(b.ChainConfig(), new(big.Int).SetUint64(uint64(bn))) // Sign transaction if err := msg.Sign(signer, b.clientCtx.Keyring); err != nil { diff --git a/rpc/backend/sign_tx_test.go b/rpc/backend/sign_tx_test.go index 1483b0b5e0..ee82303b0e 100644 --- a/rpc/backend/sign_tx_test.go +++ b/rpc/backend/sign_tx_test.go @@ -31,6 +31,7 @@ func (suite *BackendTestSuite) TestSendTransaction() { from := common.BytesToAddress(priv.PubKey().Address().Bytes()) nonce := hexutil.Uint64(1) baseFee := sdk.NewInt(1) + validator := sdk.AccAddress(tests.GenerateAddress().Bytes()) callArgsDefault := evmtypes.TransactionArgs{ From: &from, To: &toAddr, @@ -82,6 +83,7 @@ func (suite *BackendTestSuite) TestSendTransaction() { RegisterBlock(client, 1, nil) RegisterBlockResults(client, 1) RegisterBaseFee(queryClient, baseFee) + RegisterValidatorAccount(queryClient, validator) }, evmtypes.TransactionArgs{ From: &from, @@ -115,6 +117,7 @@ func (suite *BackendTestSuite) TestSendTransaction() { txBytes, err := txEncoder(tx) suite.Require().NoError(err) RegisterBroadcastTxError(client, txBytes) + RegisterValidatorAccount(queryClient, validator) }, callArgsDefault, common.Hash{}, @@ -142,6 +145,7 @@ func (suite *BackendTestSuite) TestSendTransaction() { txBytes, err := txEncoder(tx) suite.Require().NoError(err) RegisterBroadcastTx(client, txBytes) + RegisterValidatorAccount(queryClient, validator) }, callArgsDefault, hash, diff --git a/rpc/backend/tx_info.go b/rpc/backend/tx_info.go index 3d2810331d..3f69ec86f9 100644 --- a/rpc/backend/tx_info.go +++ b/rpc/backend/tx_info.go @@ -304,8 +304,10 @@ func (b *Backend) GetTransactionReceipt(hash common.Hash) (map[string]interface{ // Inclusion information: These fields provide information about the inclusion of the // transaction corresponding to this receipt. - "blockHash": common.BytesToHash(resBlock.Block.Header.Hash()).Hex(), - "blockNumber": hexutil.Uint64(res.Height), + "blockHash": common.BytesToHash(resBlock.Block.Header.Hash()).Hex(), + // #nosec G115 height always positive + "blockNumber": hexutil.Uint64(res.Height), + // #nosec G115 tx index always positive "transactionIndex": hexutil.Uint64(res.EthTxIndex), // sender and receiver (contract or EOA) addreses diff --git a/rpc/backend/utils.go b/rpc/backend/utils.go index bada8750c6..5112737b36 100644 --- a/rpc/backend/utils.go +++ b/rpc/backend/utils.go @@ -26,11 +26,12 @@ import ( "github.com/cometbft/cometbft/libs/log" "github.com/cometbft/cometbft/proto/tendermint/crypto" tmrpctypes "github.com/cometbft/cometbft/rpc/core/types" + tmtypes "github.com/cometbft/cometbft/types" sdk "github.com/cosmos/cosmos-sdk/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/consensus/misc" + "github.com/ethereum/go-ethereum/consensus/misc/eip1559" ethtypes "github.com/ethereum/go-ethereum/core/types" evmtypes "github.com/zeta-chain/ethermint/x/evm/types" "google.golang.org/grpc/codes" @@ -137,7 +138,7 @@ func (b *Backend) processBlock( targetOneFeeHistory.BaseFee = blockBaseFee cfg := b.ChainConfig() if cfg.IsLondon(big.NewInt(blockHeight + 1)) { - targetOneFeeHistory.NextBaseFee = misc.CalcBaseFee(cfg, b.CurrentHeader()) + targetOneFeeHistory.NextBaseFee = eip1559.CalcBaseFee(cfg, b.CurrentHeader()) } else { targetOneFeeHistory.NextBaseFee = new(big.Int) } @@ -318,3 +319,16 @@ func GetHexProofs(proof *crypto.ProofOps) []string { } return proofs } + +func GetValidatorAccount(header *tmtypes.Header, qc *types.QueryClient) (sdk.AccAddress, error) { + res, err := qc.ValidatorAccount( + types.ContextWithHeight(header.Height), + &evmtypes.QueryValidatorAccountRequest{ + ConsAddress: sdk.ConsAddress(header.ProposerAddress).String(), + }, + ) + if err != nil { + return nil, fmt.Errorf("failed to get validator account %w", err) + } + return sdk.AccAddressFromBech32(res.AccountAddress) +} diff --git a/rpc/namespaces/ethereum/debug/api.go b/rpc/namespaces/ethereum/debug/api.go index 27c4ec5500..1b70bcc2f1 100644 --- a/rpc/namespaces/ethereum/debug/api.go +++ b/rpc/namespaces/ethereum/debug/api.go @@ -18,7 +18,6 @@ package debug import ( "bytes" "errors" - "fmt" "io" "os" "runtime" @@ -32,7 +31,6 @@ import ( "github.com/davecgh/go-spew/spew" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/consensus/ethash" "github.com/ethereum/go-ethereum/rlp" stderrors "github.com/pkg/errors" evmtypes "github.com/zeta-chain/ethermint/x/evm/types" @@ -126,6 +124,7 @@ func (a *API) BlockProfile(file string, nsec uint) error { runtime.SetBlockProfileRate(1) defer runtime.SetBlockProfileRate(0) + // #nosec G115 uint always in int64 range time.Sleep(time.Duration(nsec) * time.Second) return writeProfile("block", file, a.logger) } @@ -137,6 +136,7 @@ func (a *API) CpuProfile(file string, nsec uint) error { //nolint: golint, style if err := a.StartCPUProfile(file); err != nil { return err } + // #nosec G115 uint always in int64 range time.Sleep(time.Duration(nsec) * time.Second) return a.StopCPUProfile() } @@ -156,6 +156,7 @@ func (a *API) GoTrace(file string, nsec uint) error { if err := a.StartGoTrace(file); err != nil { return err } + // #nosec G115 uint always in int64 range time.Sleep(time.Duration(nsec) * time.Second) return a.StopGoTrace() } @@ -273,6 +274,7 @@ func (a *API) WriteMemProfile(file string) error { func (a *API) MutexProfile(file string, nsec uint) error { a.logger.Debug("debug_mutexProfile", "file", file, "nsec", nsec) runtime.SetMutexProfileFraction(1) + // #nosec G115 uint always in int64 range time.Sleep(time.Duration(nsec) * time.Second) defer runtime.SetMutexProfileFraction(0) return writeProfile("mutex", file, a.logger) @@ -305,6 +307,7 @@ func (a *API) SetGCPercent(v int) int { // GetHeaderRlp retrieves the RLP encoded for of a single header. func (a *API) GetHeaderRlp(number uint64) (hexutil.Bytes, error) { + // #nosec G115 number always in int64 range header, err := a.backend.HeaderByNumber(rpctypes.BlockNumber(number)) if err != nil { return nil, err @@ -315,6 +318,7 @@ func (a *API) GetHeaderRlp(number uint64) (hexutil.Bytes, error) { // GetBlockRlp retrieves the RLP encoded for of a single block. func (a *API) GetBlockRlp(number uint64) (hexutil.Bytes, error) { + // #nosec G115 number always in int64 range block, err := a.backend.EthBlockByNumber(rpctypes.BlockNumber(number)) if err != nil { return nil, err @@ -325,6 +329,7 @@ func (a *API) GetBlockRlp(number uint64) (hexutil.Bytes, error) { // PrintBlock retrieves a block and returns its pretty printed form. func (a *API) PrintBlock(number uint64) (string, error) { + // #nosec G115 number always in int64 range block, err := a.backend.EthBlockByNumber(rpctypes.BlockNumber(number)) if err != nil { return "", err @@ -333,16 +338,6 @@ func (a *API) PrintBlock(number uint64) (string, error) { return spew.Sdump(block), nil } -// SeedHash retrieves the seed hash of a block. -func (a *API) SeedHash(number uint64) (string, error) { - _, err := a.backend.HeaderByNumber(rpctypes.BlockNumber(number)) - if err != nil { - return "", err - } - - return fmt.Sprintf("0x%x", ethash.SeedHash(number)), nil -} - // IntermediateRoots executes a block, and returns a list // of intermediate roots: the stateroot after each transaction. func (a *API) IntermediateRoots(hash common.Hash, _ *evmtypes.TraceConfig) ([]common.Hash, error) { diff --git a/rpc/namespaces/ethereum/eth/api.go b/rpc/namespaces/ethereum/eth/api.go index da8474d963..1bd68e5bed 100644 --- a/rpc/namespaces/ethereum/eth/api.go +++ b/rpc/namespaces/ethereum/eth/api.go @@ -21,6 +21,7 @@ import ( "github.com/cometbft/cometbft/libs/log" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + ethmath "github.com/ethereum/go-ethereum/common/math" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/rpc" ethermint "github.com/zeta-chain/ethermint/types" @@ -98,7 +99,7 @@ type EthereumAPI interface { GasPrice() (*hexutil.Big, error) EstimateGas(args evmtypes.TransactionArgs, blockNrOptional *rpctypes.BlockNumber) (hexutil.Uint64, error) FeeHistory( - blockCount rpc.DecimalOrHex, + blockCount ethmath.HexOrDecimal64, lastBlock rpc.BlockNumber, rewardPercentiles []float64, ) (*rpctypes.FeeHistoryResult, error) @@ -350,7 +351,7 @@ func (e *PublicAPI) EstimateGas( return e.backend.EstimateGas(args, blockNrOptional) } -func (e *PublicAPI) FeeHistory(blockCount rpc.DecimalOrHex, +func (e *PublicAPI) FeeHistory(blockCount ethmath.HexOrDecimal64, lastBlock rpc.BlockNumber, rewardPercentiles []float64, ) (*rpctypes.FeeHistoryResult, error) { diff --git a/rpc/namespaces/ethereum/eth/filters/api.go b/rpc/namespaces/ethereum/eth/filters/api.go index 7b6d3d9ff3..83a262b01f 100644 --- a/rpc/namespaces/ethereum/eth/filters/api.go +++ b/rpc/namespaces/ethereum/eth/filters/api.go @@ -27,11 +27,13 @@ import ( tmtypes "github.com/cometbft/cometbft/types" "github.com/cosmos/cosmos-sdk/client" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/eth/filters" "github.com/ethereum/go-ethereum/rpc" evmtypes "github.com/zeta-chain/ethermint/x/evm/types" + "github.com/zeta-chain/node/rpc/backend" "github.com/zeta-chain/node/rpc/types" ) @@ -81,12 +83,13 @@ type filter struct { // PublicFilterAPI offers support to create and manage filters. This will allow external clients to retrieve various // information related to the Ethereum protocol such as blocks, transactions and logs. type PublicFilterAPI struct { - logger log.Logger - clientCtx client.Context - backend Backend - events *EventSystem - filtersMu sync.Mutex - filters map[rpc.ID]*filter + logger log.Logger + clientCtx client.Context + backend Backend + events *EventSystem + filtersMu sync.Mutex + filters map[rpc.ID]*filter + queryClient *types.QueryClient } // NewPublicAPI returns a new PublicFilterAPI instance. @@ -98,11 +101,12 @@ func NewPublicAPI( ) *PublicFilterAPI { logger = logger.With("api", "filter") api := &PublicFilterAPI{ - logger: logger, - clientCtx: clientCtx, - backend: backend, - filters: make(map[rpc.ID]*filter), - events: NewEventSystem(logger, tmWSClient), + logger: logger, + clientCtx: clientCtx, + backend: backend, + filters: make(map[rpc.ID]*filter), + events: NewEventSystem(logger, tmWSClient), + queryClient: types.NewQueryClient(clientCtx), } go api.timeoutLoop() @@ -368,9 +372,35 @@ func (api *PublicFilterAPI) NewHeads(ctx context.Context) (*rpc.Subscription, er baseFee := types.BaseFeeFromEvents(data.ResultBeginBlock.Events) + validatorAccount, err := backend.GetValidatorAccount(&data.Header, api.queryClient) + if err != nil { + api.logger.Error("failed to get validator account", "err", err) + continue + } + // TODO: fetch bloom from events - header := types.EthHeaderFromTendermint(data.Header, ethtypes.Bloom{}, baseFee) - err = notifier.Notify(rpcSub.ID, header) + header := types.EthHeaderFromTendermint(data.Header, ethtypes.Bloom{}, baseFee, validatorAccount) + + var enc types.Header + enc.ParentHash = header.ParentHash + enc.UncleHash = header.UncleHash + enc.Coinbase = header.Coinbase.Hex() + enc.Root = header.Root + enc.TxHash = header.TxHash + enc.ReceiptHash = header.ReceiptHash + enc.Bloom = header.Bloom + enc.Difficulty = (*hexutil.Big)(header.Difficulty) + enc.Number = (*hexutil.Big)(header.Number) + enc.GasLimit = hexutil.Uint64(header.GasLimit) + enc.GasUsed = hexutil.Uint64(header.GasUsed) + enc.Time = hexutil.Uint64(header.Time) + enc.Extra = header.Extra + enc.MixDigest = header.MixDigest + enc.Nonce = header.Nonce + enc.BaseFee = (*hexutil.Big)(header.BaseFee) + enc.Hash = common.BytesToHash(data.Header.Hash()) + + err = notifier.Notify(rpcSub.ID, enc) if err != nil { api.logger.Debug("failed to notify", "error", err.Error()) } diff --git a/rpc/types/block.go b/rpc/types/block.go index cb2c873709..ea1b872bcc 100644 --- a/rpc/types/block.go +++ b/rpc/types/block.go @@ -27,6 +27,7 @@ import ( grpctypes "github.com/cosmos/cosmos-sdk/types/grpc" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/spf13/cast" ethermint "github.com/zeta-chain/ethermint/types" "google.golang.org/grpc/metadata" @@ -210,3 +211,26 @@ func (bnh *BlockNumberOrHash) decodeFromString(input string) error { } return nil } + +// https://github.com/ethereum/go-ethereum/blob/release/1.11/core/types/gen_header_json.go#L18 +type Header struct { + ParentHash common.Hash `json:"parentHash" gencodec:"required"` + UncleHash common.Hash `json:"sha3Uncles" gencodec:"required"` + // update string avoid lost checksumed miner after MarshalText + Coinbase string `json:"miner"` + Root common.Hash `json:"stateRoot" gencodec:"required"` + TxHash common.Hash `json:"transactionsRoot" gencodec:"required"` + ReceiptHash common.Hash `json:"receiptsRoot" gencodec:"required"` + Bloom ethtypes.Bloom `json:"logsBloom" gencodec:"required"` + Difficulty *hexutil.Big `json:"difficulty" gencodec:"required"` + Number *hexutil.Big `json:"number" gencodec:"required"` + GasLimit hexutil.Uint64 `json:"gasLimit" gencodec:"required"` + GasUsed hexutil.Uint64 `json:"gasUsed" gencodec:"required"` + Time hexutil.Uint64 `json:"timestamp" gencodec:"required"` + Extra hexutil.Bytes `json:"extraData" gencodec:"required"` + MixDigest common.Hash `json:"mixHash"` + Nonce ethtypes.BlockNonce `json:"nonce"` + BaseFee *hexutil.Big `json:"baseFeePerGas" rlp:"optional"` + // overwrite rlpHash + Hash common.Hash `json:"hash"` +} diff --git a/rpc/types/utils.go b/rpc/types/utils.go index addce9521a..a17f3af2a2 100644 --- a/rpc/types/utils.go +++ b/rpc/types/utils.go @@ -26,6 +26,7 @@ import ( tmrpcclient "github.com/cometbft/cometbft/rpc/client" tmtypes "github.com/cometbft/cometbft/types" "github.com/cosmos/cosmos-sdk/client" + sdk "github.com/cosmos/cosmos-sdk/types" errortypes "github.com/cosmos/cosmos-sdk/types/errors" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" @@ -61,7 +62,12 @@ func RawTxToEthTx(clientCtx client.Context, txBz tmtypes.Tx) ([]*evmtypes.MsgEth // EthHeaderFromTendermint is an util function that returns an Ethereum Header // from a tendermint Header. -func EthHeaderFromTendermint(header tmtypes.Header, bloom ethtypes.Bloom, baseFee *big.Int) *ethtypes.Header { +func EthHeaderFromTendermint( + header tmtypes.Header, + bloom ethtypes.Bloom, + baseFee *big.Int, + miner sdk.AccAddress, +) *ethtypes.Header { txHash := ethtypes.EmptyRootHash if len(header.DataHash) == 0 { txHash = common.BytesToHash(header.DataHash) @@ -70,7 +76,7 @@ func EthHeaderFromTendermint(header tmtypes.Header, bloom ethtypes.Bloom, baseFe return ðtypes.Header{ ParentHash: common.BytesToHash(header.LastBlockID.Hash.Bytes()), UncleHash: ethtypes.EmptyUncleHash, - Coinbase: common.BytesToAddress(header.ProposerAddress), + Coinbase: common.BytesToAddress(miner), Root: common.BytesToHash(header.AppHash), TxHash: txHash, ReceiptHash: ethtypes.EmptyRootHash, @@ -127,20 +133,24 @@ func FormatBlock( } result := map[string]interface{}{ - "number": hexutil.Uint64(header.Height), - "hash": hexutil.Bytes(header.Hash()), - "parentHash": common.BytesToHash(header.LastBlockID.Hash.Bytes()), - "nonce": ethtypes.BlockNonce{}, // PoW specific - "sha3Uncles": ethtypes.EmptyUncleHash, // No uncles in Tendermint - "logsBloom": bloom, - "stateRoot": hexutil.Bytes(header.AppHash), - "miner": validatorAddr, - "mixHash": common.Hash{}, - "difficulty": (*hexutil.Big)(big.NewInt(0)), - "extraData": "0x", - "size": hexutil.Uint64(size), - "gasLimit": hexutil.Uint64(gasLimit), // Static gas limit - "gasUsed": (*hexutil.Big)(gasUsed), + // #nosec G115 block height always positive + "number": hexutil.Uint64(header.Height), + "hash": hexutil.Bytes(header.Hash()), + "parentHash": common.BytesToHash(header.LastBlockID.Hash.Bytes()), + "nonce": ethtypes.BlockNonce{}, // PoW specific + "sha3Uncles": ethtypes.EmptyUncleHash, // No uncles in Tendermint + "logsBloom": bloom, + "stateRoot": hexutil.Bytes(header.AppHash), + "miner": validatorAddr, + "mixHash": common.Hash{}, + "difficulty": (*hexutil.Big)(big.NewInt(0)), + "extraData": "0x", + // #nosec G115 size always positive + "size": hexutil.Uint64(size), + // #nosec G115 gasLimit always positive + "gasLimit": hexutil.Uint64(gasLimit), // Static gas limit + "gasUsed": (*hexutil.Big)(gasUsed), + // #nosec G115 timestamp always positive "timestamp": hexutil.Uint64(header.Time.Unix()), "transactionsRoot": transactionsRoot, "receiptsRoot": ethtypes.EmptyRootHash, diff --git a/rpc/websockets.go b/rpc/websockets.go index c977c41acc..7d60ae62a3 100644 --- a/rpc/websockets.go +++ b/rpc/websockets.go @@ -32,15 +32,16 @@ import ( tmtypes "github.com/cometbft/cometbft/types" "github.com/cosmos/cosmos-sdk/client" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/eth/filters" - "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rpc" "github.com/gorilla/mux" "github.com/gorilla/websocket" "github.com/pkg/errors" evmtypes "github.com/zeta-chain/ethermint/x/evm/types" + "github.com/zeta-chain/node/rpc/backend" "github.com/zeta-chain/node/rpc/ethereum/pubsub" rpcfilters "github.com/zeta-chain/node/rpc/namespaces/ethereum/eth/filters" "github.com/zeta-chain/node/rpc/types" @@ -378,18 +379,21 @@ func (s *websocketsServer) tcpGetAndSendResponse(wsConn *wsConn, mb []byte) erro // pubSubAPI is the eth_ prefixed set of APIs in the Web3 JSON-RPC spec type pubSubAPI struct { - events *rpcfilters.EventSystem - logger log.Logger - clientCtx client.Context + events *rpcfilters.EventSystem + logger log.Logger + clientCtx client.Context + queryClient *types.QueryClient } // newPubSubAPI creates an instance of the ethereum PubSub API. func newPubSubAPI(clientCtx client.Context, logger log.Logger, tmWSClient *rpcclient.WSClient) *pubSubAPI { logger = logger.With("module", "websocket-client") + types.NewQueryClient(clientCtx) return &pubSubAPI{ - events: rpcfilters.NewEventSystem(logger, tmWSClient), - logger: logger, - clientCtx: clientCtx, + events: rpcfilters.NewEventSystem(logger, tmWSClient), + logger: logger, + clientCtx: clientCtx, + queryClient: types.NewQueryClient(clientCtx), } } @@ -423,9 +427,6 @@ func (api *pubSubAPI) subscribeNewHeads(wsConn *wsConn, subID rpc.ID) (pubsub.Un return nil, errors.Wrap(err, "error creating block filter") } - // TODO: use events - baseFee := big.NewInt(params.InitialBaseFee) - go func() { headersCh := sub.Event() errCh := sub.Err() @@ -442,7 +443,34 @@ func (api *pubSubAPI) subscribeNewHeads(wsConn *wsConn, subID rpc.ID) (pubsub.Un continue } - header := types.EthHeaderFromTendermint(data.Header, ethtypes.Bloom{}, baseFee) + validatorAccount, err := backend.GetValidatorAccount(&data.Header, api.queryClient) + if err != nil { + api.logger.Error("failed to get validator account", "err", err) + continue + } + + baseFee := types.BaseFeeFromEvents(data.ResultBeginBlock.Events) + + header := types.EthHeaderFromTendermint(data.Header, ethtypes.Bloom{}, baseFee, validatorAccount) + + var enc types.Header + enc.ParentHash = header.ParentHash + enc.UncleHash = header.UncleHash + enc.Coinbase = header.Coinbase.Hex() + enc.Root = header.Root + enc.TxHash = header.TxHash + enc.ReceiptHash = header.ReceiptHash + enc.Bloom = header.Bloom + enc.Difficulty = (*hexutil.Big)(header.Difficulty) + enc.Number = (*hexutil.Big)(header.Number) + enc.GasLimit = hexutil.Uint64(header.GasLimit) + enc.GasUsed = hexutil.Uint64(header.GasUsed) + enc.Time = hexutil.Uint64(header.Time) + enc.Extra = header.Extra + enc.MixDigest = header.MixDigest + enc.Nonce = header.Nonce + enc.BaseFee = (*hexutil.Big)(header.BaseFee) + enc.Hash = common.BytesToHash(data.Header.Hash()) // write to ws conn res := &SubscriptionNotification{ @@ -450,7 +478,7 @@ func (api *pubSubAPI) subscribeNewHeads(wsConn *wsConn, subID rpc.ID) (pubsub.Un Method: "eth_subscription", Params: &SubscriptionResult{ Subscription: subID, - Result: header, + Result: enc, }, } diff --git a/scripts/gosec.sh b/scripts/gosec.sh index 2b831a35e9..cfc2d603df 100644 --- a/scripts/gosec.sh +++ b/scripts/gosec.sh @@ -1,3 +1,3 @@ #!/usr/bin/env bash -docker run -it --rm -w /node -v "$(pwd):/node" ghcr.io/zeta-chain/gosec:2.21.0-zeta -exclude-generated -exclude-dir testutil ./... \ No newline at end of file +docker run -it --rm -w /node -v "$(pwd):/node" ghcr.io/zeta-chain/gosec:2.21.4-zeta2 -exclude-generated -exclude-dir testutil ./... diff --git a/server/json_rpc.go b/server/json_rpc.go index 47315892cd..9b780017b2 100644 --- a/server/json_rpc.go +++ b/server/json_rpc.go @@ -16,9 +16,11 @@ package server import ( + "context" "net/http" "time" + tmlog "github.com/cometbft/cometbft/libs/log" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/server" "github.com/cosmos/cosmos-sdk/server/types" @@ -27,11 +29,51 @@ import ( "github.com/gorilla/mux" "github.com/rs/cors" ethermint "github.com/zeta-chain/ethermint/types" + "golang.org/x/exp/slog" "github.com/zeta-chain/node/rpc" "github.com/zeta-chain/node/server/config" ) +type gethLogsToTm struct { + logger tmlog.Logger + attrs []slog.Attr +} + +func (g *gethLogsToTm) Enabled(_ context.Context, _ slog.Level) bool { + return true +} + +func (g *gethLogsToTm) Handle(_ context.Context, record slog.Record) error { + attrs := g.attrs + record.Attrs(func(attr slog.Attr) bool { + attrs = append(attrs, attr) + return true + }) + switch record.Level { + case slog.LevelDebug: + g.logger.Debug(record.Message, attrs) + case slog.LevelInfo: + g.logger.Info(record.Message, attrs) + case slog.LevelWarn: + g.logger.Info(record.Message, attrs) + case slog.LevelError: + g.logger.Error(record.Message, attrs) + } + return nil +} + +func (g *gethLogsToTm) WithAttrs(attrs []slog.Attr) slog.Handler { + return &gethLogsToTm{ + logger: g.logger, + attrs: append(g.attrs, attrs...), + } +} + +func (g *gethLogsToTm) WithGroup(_ string) slog.Handler { + return g +} + // StartJSONRPC starts the JSON-RPC server func StartJSONRPC(ctx *server.Context, clientCtx client.Context, @@ -43,17 +85,7 @@ func StartJSONRPC(ctx *server.Context, tmWsClient := ConnectTmWS(tmRPCAddr, tmEndpoint, ctx.Logger) logger := ctx.Logger.With("module", "geth") - ethlog.Root().SetHandler(ethlog.FuncHandler(func(r *ethlog.Record) error { - switch r.Lvl { - case ethlog.LvlTrace, ethlog.LvlDebug: - logger.Debug(r.Msg, r.Ctx...) - case ethlog.LvlInfo, ethlog.LvlWarn: - logger.Info(r.Msg, r.Ctx...) - case ethlog.LvlError, ethlog.LvlCrit: - logger.Error(r.Msg, r.Ctx...) - } - return nil - })) + ethlog.SetDefault(ethlog.NewLogger(&gethLogsToTm{logger: logger})) rpcServer := ethrpc.NewServer() diff --git a/tests/simulation/sim/sim_config.go b/simulation/config.go similarity index 99% rename from tests/simulation/sim/sim_config.go rename to simulation/config.go index 8a6c281db8..254365c856 100644 --- a/tests/simulation/sim/sim_config.go +++ b/simulation/config.go @@ -1,4 +1,4 @@ -package sim +package simulation import ( "flag" diff --git a/tests/simulation/sim/sim_utils.go b/simulation/simulation.go similarity index 62% rename from tests/simulation/sim/sim_utils.go rename to simulation/simulation.go index b310d7cae2..faa727c65f 100644 --- a/tests/simulation/sim/sim_utils.go +++ b/simulation/simulation.go @@ -1,12 +1,16 @@ -package sim +package simulation import ( + "encoding/json" "fmt" + "os" dbm "github.com/cometbft/cometbft-db" "github.com/cometbft/cometbft/libs/log" "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/runtime" servertypes "github.com/cosmos/cosmos-sdk/server/types" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" "github.com/zeta-chain/ethermint/app" evmante "github.com/zeta-chain/ethermint/app/ante" @@ -66,3 +70,30 @@ func PrintStats(db dbm.DB) { fmt.Println(db.Stats()["leveldb.stats"]) fmt.Println("GoLevelDB cached block size", db.Stats()["leveldb.cachedblock"]) } + +// CheckExportSimulation exports the app state and simulation parameters to JSON +// if the export paths are defined. +func CheckExportSimulation(app runtime.AppI, config simtypes.Config, params simtypes.Params) error { + if config.ExportStatePath != "" { + exported, err := app.ExportAppStateAndValidators(false, nil, nil) + if err != nil { + return fmt.Errorf("failed to export app state: %w", err) + } + + if err := os.WriteFile(config.ExportStatePath, exported.AppState, 0o600); err != nil { + return err + } + } + + if config.ExportParamsPath != "" { + paramsBz, err := json.MarshalIndent(params, "", " ") + if err != nil { + return fmt.Errorf("failed to write app state to %s: %w", config.ExportStatePath, err) + } + + if err := os.WriteFile(config.ExportParamsPath, paramsBz, 0o600); err != nil { + return err + } + } + return nil +} diff --git a/simulation/simulation_test.go b/simulation/simulation_test.go new file mode 100644 index 0000000000..3f39c77fa1 --- /dev/null +++ b/simulation/simulation_test.go @@ -0,0 +1,541 @@ +package simulation_test + +import ( + "encoding/json" + "fmt" + "math/rand" + "os" + "runtime/debug" + "testing" + + abci "github.com/cometbft/cometbft/abci/types" + tmproto "github.com/cometbft/cometbft/proto/tendermint/types" + "github.com/cosmos/cosmos-sdk/store" + storetypes "github.com/cosmos/cosmos-sdk/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + evidencetypes "github.com/cosmos/cosmos-sdk/x/evidence/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" + slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/stretchr/testify/require" + evmtypes "github.com/zeta-chain/ethermint/x/evm/types" + "github.com/zeta-chain/node/app" + zetasimulation "github.com/zeta-chain/node/simulation" + + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/server" + cosmossimutils "github.com/cosmos/cosmos-sdk/testutil/sims" + cosmossim "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + cosmossimcli "github.com/cosmos/cosmos-sdk/x/simulation/client/cli" +) + +// AppChainID hardcoded chainID for simulation + +func init() { + zetasimulation.GetSimulatorFlags() +} + +type StoreKeysPrefixes struct { + A storetypes.StoreKey + B storetypes.StoreKey + Prefixes [][]byte +} + +const ( + SimAppChainID = "simulation_777-1" + SimBlockMaxGas = 815000000 + //github.com/zeta-chain/node/issues/3004 + // TODO : Support pebbleDB for simulation tests + SimDBBackend = "goleveldb" + SimDBName = "simulation" +) + +// interBlockCacheOpt returns a BaseApp option function that sets the persistent +// inter-block write-through cache. +func interBlockCacheOpt() func(*baseapp.BaseApp) { + return baseapp.SetInterBlockCache(store.NewCommitKVStoreCacheManager()) +} + +// TestAppStateDeterminism runs a full application simulation , and produces multiple blocks as per the config +// It checks the determinism of the application by comparing the apphash at the end of each run to other runs +// The following test certifies that , for the same set of operations ( irrespective of what the operations are ) , +// we would reach the same final state if the initial state is the same +func TestAppStateDeterminism(t *testing.T) { + if !zetasimulation.FlagEnabledValue { + t.Skip("skipping application simulation") + } + + config := zetasimulation.NewConfigFromFlags() + + config.InitialBlockHeight = 1 + config.ExportParamsPath = "" + config.OnOperation = false + config.AllInvariants = false + config.ChainID = SimAppChainID + config.DBBackend = SimDBBackend + config.BlockMaxGas = SimBlockMaxGas + + numSeeds := 3 + numTimesToRunPerSeed := 5 + + // We will be overriding the random seed and just run a single simulation on the provided seed value + if config.Seed != cosmossimcli.DefaultSeedValue { + numSeeds = 1 + } + + appHashList := make([]json.RawMessage, numTimesToRunPerSeed) + + appOptions := make(cosmossimutils.AppOptionsMap, 0) + appOptions[server.FlagInvCheckPeriod] = zetasimulation.FlagPeriodValue + + t.Log("Running tests for numSeeds: ", numSeeds, " numTimesToRunPerSeed: ", numTimesToRunPerSeed) + + for i := 0; i < numSeeds; i++ { + if config.Seed == cosmossimcli.DefaultSeedValue { + config.Seed = rand.Int63() + } + // For the same seed, the simApp hash produced at the end of each run should be the same + for j := 0; j < numTimesToRunPerSeed; j++ { + db, dir, logger, _, err := cosmossimutils.SetupSimulation( + config, + SimDBBackend, + SimDBName, + zetasimulation.FlagVerboseValue, + zetasimulation.FlagEnabledValue, + ) + require.NoError(t, err) + appOptions[flags.FlagHome] = dir + + simApp, err := zetasimulation.NewSimApp( + logger, + db, + appOptions, + interBlockCacheOpt(), + baseapp.SetChainID(SimAppChainID), + ) + + t.Logf( + "running non-determinism simulation; seed %d: %d/%d, attempt: %d/%d\n", + config.Seed, i+1, numSeeds, j+1, numTimesToRunPerSeed, + ) + + blockedAddresses := simApp.ModuleAccountAddrs() + + // Random seed is used to produce a random initial state for the simulation + _, _, err = simulation.SimulateFromSeed( + t, + os.Stdout, + simApp.BaseApp, + zetasimulation.AppStateFn( + simApp.AppCodec(), + simApp.SimulationManager(), + simApp.BasicManager().DefaultGenesis(simApp.AppCodec()), + ), + cosmossim.RandomAccounts, + cosmossimutils.SimulationOperations(simApp, simApp.AppCodec(), config), + blockedAddresses, + config, + simApp.AppCodec(), + ) + require.NoError(t, err) + + zetasimulation.PrintStats(db) + + appHash := simApp.LastCommitID().Hash + appHashList[j] = appHash + + // Clean up resources + t.Cleanup(func() { + require.NoError(t, db.Close()) + require.NoError(t, os.RemoveAll(dir)) + }) + + if j != 0 { + require.Equal( + t, + string(appHashList[0]), + string(appHashList[j]), + "non-determinism in seed %d: %d/%d, attempt: %d/%d\n", + config.Seed, + i+1, + numSeeds, + j+1, + numTimesToRunPerSeed, + ) + } + } + } +} + +// TestFullAppSimulation runs a full simApp simulation with the provided configuration. +// At the end of the run it tries to export the genesis state to make sure the export works. +func TestFullAppSimulation(t *testing.T) { + + config := zetasimulation.NewConfigFromFlags() + + config.ChainID = SimAppChainID + config.BlockMaxGas = SimBlockMaxGas + config.DBBackend = SimDBBackend + + db, dir, logger, skip, err := cosmossimutils.SetupSimulation( + config, + SimDBBackend, + SimDBName, + zetasimulation.FlagVerboseValue, + zetasimulation.FlagEnabledValue, + ) + if skip { + t.Skip("skipping application simulation") + } + require.NoError(t, err, "simulation setup failed") + + t.Cleanup(func() { + if err := db.Close(); err != nil { + require.NoError(t, err, "Error closing new database") + } + if err := os.RemoveAll(dir); err != nil { + require.NoError(t, err, "Error removing directory") + } + }) + appOptions := make(cosmossimutils.AppOptionsMap, 0) + appOptions[server.FlagInvCheckPeriod] = zetasimulation.FlagPeriodValue + appOptions[flags.FlagHome] = dir + + simApp, err := zetasimulation.NewSimApp( + logger, + db, + appOptions, + interBlockCacheOpt(), + baseapp.SetChainID(SimAppChainID), + ) + require.NoError(t, err) + + blockedAddresses := simApp.ModuleAccountAddrs() + _, _, simErr := simulation.SimulateFromSeed( + t, + os.Stdout, + simApp.BaseApp, + zetasimulation.AppStateFn( + simApp.AppCodec(), + simApp.SimulationManager(), + simApp.BasicManager().DefaultGenesis(simApp.AppCodec()), + ), + cosmossim.RandomAccounts, + cosmossimutils.SimulationOperations(simApp, simApp.AppCodec(), config), + blockedAddresses, + config, + simApp.AppCodec(), + ) + require.NoError(t, simErr) + + // check export works as expected + exported, err := simApp.ExportAppStateAndValidators(false, nil, nil) + require.NoError(t, err) + if config.ExportStatePath != "" { + err := os.WriteFile(config.ExportStatePath, exported.AppState, 0o600) + require.NoError(t, err) + } + + zetasimulation.PrintStats(db) +} + +func TestAppImportExport(t *testing.T) { + config := zetasimulation.NewConfigFromFlags() + + config.ChainID = SimAppChainID + config.BlockMaxGas = SimBlockMaxGas + config.DBBackend = SimDBBackend + + db, dir, logger, skip, err := cosmossimutils.SetupSimulation( + config, + SimDBBackend, + SimDBName, + zetasimulation.FlagVerboseValue, + zetasimulation.FlagEnabledValue, + ) + if skip { + t.Skip("skipping application simulation") + } + require.NoError(t, err, "simulation setup failed") + + t.Cleanup(func() { + if err := db.Close(); err != nil { + require.NoError(t, err, "Error closing new database") + } + if err := os.RemoveAll(dir); err != nil { + require.NoError(t, err, "Error removing directory") + } + }) + + appOptions := make(cosmossimutils.AppOptionsMap, 0) + appOptions[server.FlagInvCheckPeriod] = zetasimulation.FlagPeriodValue + appOptions[flags.FlagHome] = dir + simApp, err := zetasimulation.NewSimApp( + logger, + db, + appOptions, + interBlockCacheOpt(), + baseapp.SetChainID(SimAppChainID), + ) + require.NoError(t, err) + + // Run randomized simulation + blockedAddresses := simApp.ModuleAccountAddrs() + _, simParams, simErr := simulation.SimulateFromSeed( + t, + os.Stdout, + simApp.BaseApp, + zetasimulation.AppStateFn( + simApp.AppCodec(), + simApp.SimulationManager(), + simApp.BasicManager().DefaultGenesis(simApp.AppCodec()), + ), + cosmossim.RandomAccounts, + cosmossimutils.SimulationOperations(simApp, simApp.AppCodec(), config), + blockedAddresses, + config, + simApp.AppCodec(), + ) + require.NoError(t, simErr) + + err = zetasimulation.CheckExportSimulation(simApp, config, simParams) + require.NoError(t, err) + + zetasimulation.PrintStats(db) + + t.Log("exporting genesis") + // export state and simParams + exported, err := simApp.ExportAppStateAndValidators(false, []string{}, []string{}) + require.NoError(t, err) + + t.Log("importing genesis") + newDB, newDir, _, _, err := cosmossimutils.SetupSimulation( + config, + SimDBBackend+"_new", + SimDBName+"_new", + zetasimulation.FlagVerboseValue, + zetasimulation.FlagEnabledValue, + ) + + require.NoError(t, err, "simulation setup failed") + + t.Cleanup(func() { + if err := newDB.Close(); err != nil { + require.NoError(t, err, "Error closing new database") + } + if err := os.RemoveAll(newDir); err != nil { + require.NoError(t, err, "Error removing directory") + } + }) + + newSimApp, err := zetasimulation.NewSimApp( + logger, + newDB, + appOptions, + interBlockCacheOpt(), + baseapp.SetChainID(SimAppChainID), + ) + require.NoError(t, err) + + var genesisState app.GenesisState + err = json.Unmarshal(exported.AppState, &genesisState) + require.NoError(t, err) + + defer func() { + if r := recover(); r != nil { + err := fmt.Sprintf("%v", r) + require.Contains(t, err, "validator set is empty after InitGenesis", "unexpected error: %v", r) + t.Log("Skipping simulation as all validators have been unbonded") + t.Log("err", err, "stacktrace", string(debug.Stack())) + } + }() + + // Create context for the old and the new sim app, which can be used to compare keys + ctxSimApp := simApp.NewContext(true, tmproto.Header{ + Height: simApp.LastBlockHeight(), + ChainID: SimAppChainID, + }).WithChainID(SimAppChainID) + ctxNewSimApp := newSimApp.NewContext(true, tmproto.Header{ + Height: simApp.LastBlockHeight(), + ChainID: SimAppChainID, + }).WithChainID(SimAppChainID) + + // Use genesis state from the first app to initialize the second app + newSimApp.ModuleManager().InitGenesis(ctxNewSimApp, newSimApp.AppCodec(), genesisState) + newSimApp.StoreConsensusParams(ctxNewSimApp, exported.ConsensusParams) + + t.Log("comparing stores") + // The ordering of the keys is not important, we compare the same prefix for both simulations + storeKeysPrefixes := []StoreKeysPrefixes{ + {simApp.GetKey(authtypes.StoreKey), newSimApp.GetKey(authtypes.StoreKey), [][]byte{}}, + { + simApp.GetKey(stakingtypes.StoreKey), newSimApp.GetKey(stakingtypes.StoreKey), + [][]byte{ + stakingtypes.UnbondingQueueKey, stakingtypes.RedelegationQueueKey, stakingtypes.ValidatorQueueKey, + stakingtypes.HistoricalInfoKey, stakingtypes.UnbondingIDKey, stakingtypes.UnbondingIndexKey, stakingtypes.UnbondingTypeKey, stakingtypes.ValidatorUpdatesKey, + }, + }, + {simApp.GetKey(slashingtypes.StoreKey), newSimApp.GetKey(slashingtypes.StoreKey), [][]byte{}}, + {simApp.GetKey(distrtypes.StoreKey), newSimApp.GetKey(distrtypes.StoreKey), [][]byte{}}, + {simApp.GetKey(banktypes.StoreKey), newSimApp.GetKey(banktypes.StoreKey), [][]byte{banktypes.BalancesPrefix}}, + {simApp.GetKey(paramtypes.StoreKey), newSimApp.GetKey(paramtypes.StoreKey), [][]byte{}}, + {simApp.GetKey(govtypes.StoreKey), newSimApp.GetKey(govtypes.StoreKey), [][]byte{}}, + {simApp.GetKey(evidencetypes.StoreKey), newSimApp.GetKey(evidencetypes.StoreKey), [][]byte{}}, + {simApp.GetKey(evmtypes.StoreKey), newSimApp.GetKey(evmtypes.StoreKey), [][]byte{}}, + } + + for _, skp := range storeKeysPrefixes { + storeA := ctxSimApp.KVStore(skp.A) + storeB := ctxNewSimApp.KVStore(skp.B) + + failedKVAs, failedKVBs := sdk.DiffKVStores(storeA, storeB, skp.Prefixes) + require.Equal(t, len(failedKVAs), len(failedKVBs), "unequal sets of key-values to compare") + + t.Logf("compared %d different key/value pairs between %s and %s\n", len(failedKVAs), skp.A, skp.B) + require.Equal( + t, + 0, + len(failedKVAs), + cosmossimutils.GetSimulationLog( + skp.A.Name(), + simApp.SimulationManager().StoreDecoders, + failedKVAs, + failedKVBs, + ), + ) + } +} + +func TestAppSimulationAfterImport(t *testing.T) { + config := zetasimulation.NewConfigFromFlags() + + config.ChainID = SimAppChainID + config.BlockMaxGas = SimBlockMaxGas + config.DBBackend = SimDBBackend + + db, dir, logger, skip, err := cosmossimutils.SetupSimulation( + config, + SimDBBackend, + SimDBName, + zetasimulation.FlagVerboseValue, + zetasimulation.FlagEnabledValue, + ) + if skip { + t.Skip("skipping application simulation") + } + require.NoError(t, err, "simulation setup failed") + + t.Cleanup(func() { + if err := db.Close(); err != nil { + require.NoError(t, err, "Error closing new database") + } + if err := os.RemoveAll(dir); err != nil { + require.NoError(t, err, "Error removing directory") + } + }) + + appOptions := make(cosmossimutils.AppOptionsMap, 0) + appOptions[server.FlagInvCheckPeriod] = zetasimulation.FlagPeriodValue + appOptions[flags.FlagHome] = dir + simApp, err := zetasimulation.NewSimApp( + logger, + db, + appOptions, + interBlockCacheOpt(), + baseapp.SetChainID(SimAppChainID), + ) + require.NoError(t, err) + + // Run randomized simulation + blockedAddresses := simApp.ModuleAccountAddrs() + stopEarly, simParams, simErr := simulation.SimulateFromSeed( + t, + os.Stdout, + simApp.BaseApp, + zetasimulation.AppStateFn( + simApp.AppCodec(), + simApp.SimulationManager(), + simApp.BasicManager().DefaultGenesis(simApp.AppCodec()), + ), + cosmossim.RandomAccounts, + cosmossimutils.SimulationOperations(simApp, simApp.AppCodec(), config), + blockedAddresses, + config, + simApp.AppCodec(), + ) + require.NoError(t, simErr) + + err = zetasimulation.CheckExportSimulation(simApp, config, simParams) + require.NoError(t, err) + + zetasimulation.PrintStats(db) + + if stopEarly { + t.Log("can't export or import a zero-validator genesis, exiting test") + return + } + + t.Log("exporting genesis") + + // export state and simParams + exported, err := simApp.ExportAppStateAndValidators(true, []string{}, []string{}) + require.NoError(t, err) + + t.Log("importing genesis") + + newDB, newDir, _, _, err := cosmossimutils.SetupSimulation( + config, + SimDBBackend+"_new", + SimDBName+"_new", + zetasimulation.FlagVerboseValue, + zetasimulation.FlagEnabledValue, + ) + + require.NoError(t, err, "simulation setup failed") + + t.Cleanup(func() { + if err := newDB.Close(); err != nil { + require.NoError(t, err, "Error closing new database") + } + if err := os.RemoveAll(newDir); err != nil { + require.NoError(t, err, "Error removing directory") + } + }) + + newSimApp, err := zetasimulation.NewSimApp( + logger, + newDB, + appOptions, + interBlockCacheOpt(), + baseapp.SetChainID(SimAppChainID), + ) + require.NoError(t, err) + + newSimApp.InitChain(abci.RequestInitChain{ + ChainId: SimAppChainID, + AppStateBytes: exported.AppState, + }) + + stopEarly, simParams, simErr = simulation.SimulateFromSeed( + t, + os.Stdout, + newSimApp.BaseApp, + zetasimulation.AppStateFn( + simApp.AppCodec(), + simApp.SimulationManager(), + simApp.BasicManager().DefaultGenesis(simApp.AppCodec()), + ), + cosmossim.RandomAccounts, + cosmossimutils.SimulationOperations(newSimApp, newSimApp.AppCodec(), config), + blockedAddresses, + config, + simApp.AppCodec(), + ) + require.NoError(t, err) +} diff --git a/tests/simulation/sim/sim_state.go b/simulation/state.go similarity index 99% rename from tests/simulation/sim/sim_state.go rename to simulation/state.go index 499b988aee..b80cdf5570 100644 --- a/tests/simulation/sim/sim_state.go +++ b/simulation/state.go @@ -1,4 +1,4 @@ -package sim +package simulation import ( "encoding/json" diff --git a/tests/simulation/sim_test.go b/tests/simulation/sim_test.go deleted file mode 100644 index 18d803cc5f..0000000000 --- a/tests/simulation/sim_test.go +++ /dev/null @@ -1,214 +0,0 @@ -package simulation_test - -import ( - "encoding/json" - "math/rand" - "os" - "testing" - - "github.com/stretchr/testify/require" - simutils "github.com/zeta-chain/node/tests/simulation/sim" - - "github.com/cosmos/cosmos-sdk/store" - - "github.com/cosmos/cosmos-sdk/baseapp" - "github.com/cosmos/cosmos-sdk/client/flags" - "github.com/cosmos/cosmos-sdk/server" - cosmossimutils "github.com/cosmos/cosmos-sdk/testutil/sims" - cosmossim "github.com/cosmos/cosmos-sdk/types/simulation" - "github.com/cosmos/cosmos-sdk/x/simulation" - cosmossimcli "github.com/cosmos/cosmos-sdk/x/simulation/client/cli" -) - -// AppChainID hardcoded chainID for simulation - -func init() { - simutils.GetSimulatorFlags() -} - -const ( - SimAppChainID = "simulation_777-1" - SimBlockMaxGas = 815000000 - //github.com/zeta-chain/node/issues/3004 - // TODO : Support pebbleDB for simulation tests - SimDBBackend = "goleveldb" - SimDBName = "simulation" -) - -// interBlockCacheOpt returns a BaseApp option function that sets the persistent -// inter-block write-through cache. -func interBlockCacheOpt() func(*baseapp.BaseApp) { - return baseapp.SetInterBlockCache(store.NewCommitKVStoreCacheManager()) -} - -// TestAppStateDeterminism runs a full application simulation , and produces multiple blocks as per the config -// It checks the determinism of the application by comparing the apphash at the end of each run to other runs -// The following test certifies that , for the same set of operations ( irrespective of what the operations are ) , -// we would reach the same final state if the initial state is the same -func TestAppStateDeterminism(t *testing.T) { - if !simutils.FlagEnabledValue { - t.Skip("skipping application simulation") - } - - config := simutils.NewConfigFromFlags() - - config.InitialBlockHeight = 1 - config.ExportParamsPath = "" - config.OnOperation = false - config.AllInvariants = false - config.ChainID = SimAppChainID - config.DBBackend = SimDBBackend - config.BlockMaxGas = SimBlockMaxGas - - numSeeds := 3 - numTimesToRunPerSeed := 5 - - // We will be overriding the random seed and just run a single simulation on the provided seed value - if config.Seed != cosmossimcli.DefaultSeedValue { - numSeeds = 1 - } - - appHashList := make([]json.RawMessage, numTimesToRunPerSeed) - - appOptions := make(cosmossimutils.AppOptionsMap, 0) - appOptions[server.FlagInvCheckPeriod] = simutils.FlagPeriodValue - - t.Log("Running tests for numSeeds: ", numSeeds, " numTimesToRunPerSeed: ", numTimesToRunPerSeed) - - for i := 0; i < numSeeds; i++ { - if config.Seed == cosmossimcli.DefaultSeedValue { - config.Seed = rand.Int63() - } - // For the same seed, the app hash produced at the end of each run should be the same - for j := 0; j < numTimesToRunPerSeed; j++ { - db, dir, logger, _, err := cosmossimutils.SetupSimulation( - config, - SimDBBackend, - SimDBName, - simutils.FlagVerboseValue, - simutils.FlagEnabledValue, - ) - require.NoError(t, err) - appOptions[flags.FlagHome] = dir - - simApp, err := simutils.NewSimApp( - logger, - db, - appOptions, - interBlockCacheOpt(), - baseapp.SetChainID(SimAppChainID), - ) - - t.Logf( - "running non-determinism simulation; seed %d: %d/%d, attempt: %d/%d\n", - config.Seed, i+1, numSeeds, j+1, numTimesToRunPerSeed, - ) - - blockedAddresses := simApp.ModuleAccountAddrs() - - // Random seed is used to produce a random initial state for the simulation - _, _, err = simulation.SimulateFromSeed( - t, - os.Stdout, - simApp.BaseApp, - simutils.AppStateFn( - simApp.AppCodec(), - simApp.SimulationManager(), - simApp.BasicManager().DefaultGenesis(simApp.AppCodec()), - ), - cosmossim.RandomAccounts, - cosmossimutils.SimulationOperations(simApp, simApp.AppCodec(), config), - blockedAddresses, - config, - simApp.AppCodec(), - ) - require.NoError(t, err) - - simutils.PrintStats(db) - - appHash := simApp.LastCommitID().Hash - appHashList[j] = appHash - - // Clean up resources - require.NoError(t, db.Close()) - require.NoError(t, os.RemoveAll(dir)) - - if j != 0 { - require.Equal( - t, - string(appHashList[0]), - string(appHashList[j]), - "non-determinism in seed %d: %d/%d, attempt: %d/%d\n", - config.Seed, - i+1, - numSeeds, - j+1, - numTimesToRunPerSeed, - ) - } - } - } -} - -// TestFullAppSimulation runs a full app simulation with the provided configuration. -// At the end of the run it tries to export the genesis state to make sure the export works. -func TestFullAppSimulation(t *testing.T) { - - config := simutils.NewConfigFromFlags() - - config.ChainID = SimAppChainID - config.BlockMaxGas = SimBlockMaxGas - config.DBBackend = SimDBBackend - - db, dir, logger, skip, err := cosmossimutils.SetupSimulation( - config, - SimDBBackend, - SimDBName, - simutils.FlagVerboseValue, - simutils.FlagEnabledValue, - ) - if skip { - t.Skip("skipping application simulation") - } - require.NoError(t, err, "simulation setup failed") - - defer func() { - require.NoError(t, db.Close()) - require.NoError(t, os.RemoveAll(dir)) - }() - appOptions := make(cosmossimutils.AppOptionsMap, 0) - appOptions[server.FlagInvCheckPeriod] = simutils.FlagPeriodValue - appOptions[flags.FlagHome] = dir - - simApp, err := simutils.NewSimApp(logger, db, appOptions, interBlockCacheOpt(), baseapp.SetChainID(SimAppChainID)) - require.NoError(t, err) - - // Run randomized simulation - blockedAddresses := simApp.ModuleAccountAddrs() - _, _, simerr := simulation.SimulateFromSeed( - t, - os.Stdout, - simApp.BaseApp, - simutils.AppStateFn( - simApp.AppCodec(), - simApp.SimulationManager(), - simApp.BasicManager().DefaultGenesis(simApp.AppCodec()), - ), - cosmossim.RandomAccounts, - cosmossimutils.SimulationOperations(simApp, simApp.AppCodec(), config), - blockedAddresses, - config, - simApp.AppCodec(), - ) - require.NoError(t, simerr) - - // check export works as expected - exported, err := simApp.ExportAppStateAndValidators(false, nil, nil) - require.NoError(t, err) - if config.ExportStatePath != "" { - err := os.WriteFile(config.ExportStatePath, exported.AppState, 0o600) - require.NoError(t, err) - } - - simutils.PrintStats(db) -} diff --git a/testutil/helpers.go b/testutil/helpers.go index dc49a0b024..f39c4931fb 100644 --- a/testutil/helpers.go +++ b/testutil/helpers.go @@ -1,11 +1,14 @@ package testutil import ( + "encoding/hex" "fmt" "os" "strings" + "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) const helpersFile = "testutil/helpers.go" @@ -36,3 +39,10 @@ func exit(err error) { os.Exit(1) } + +// HexToBytes convert hex string to bytes +func HexToBytes(t *testing.T, hexStr string) []byte { + bytes, err := hex.DecodeString(hexStr) + require.NoError(t, err) + return bytes +} diff --git a/testutil/keeper/mocks/fungible/bank.go b/testutil/keeper/mocks/fungible/bank.go index d83a49e876..26c07bb9ad 100644 --- a/testutil/keeper/mocks/fungible/bank.go +++ b/testutil/keeper/mocks/fungible/bank.go @@ -13,6 +13,24 @@ type FungibleBankKeeper struct { mock.Mock } +// GetSupply provides a mock function with given fields: ctx, denom +func (_m *FungibleBankKeeper) GetSupply(ctx types.Context, denom string) types.Coin { + ret := _m.Called(ctx, denom) + + if len(ret) == 0 { + panic("no return value specified for GetSupply") + } + + var r0 types.Coin + if rf, ok := ret.Get(0).(func(types.Context, string) types.Coin); ok { + r0 = rf(ctx, denom) + } else { + r0 = ret.Get(0).(types.Coin) + } + + return r0 +} + // MintCoins provides a mock function with given fields: ctx, moduleName, amt func (_m *FungibleBankKeeper) MintCoins(ctx types.Context, moduleName string, amt types.Coins) error { ret := _m.Called(ctx, moduleName, amt) diff --git a/testutil/keeper/mocks/fungible/evm.go b/testutil/keeper/mocks/fungible/evm.go index 79b2cc1985..68ebfa7443 100644 --- a/testutil/keeper/mocks/fungible/evm.go +++ b/testutil/keeper/mocks/fungible/evm.go @@ -27,7 +27,7 @@ type FungibleEVMKeeper struct { } // ApplyMessage provides a mock function with given fields: ctx, msg, tracer, commit -func (_m *FungibleEVMKeeper) ApplyMessage(ctx types.Context, msg core.Message, tracer vm.EVMLogger, commit bool) (*evmtypes.MsgEthereumTxResponse, error) { +func (_m *FungibleEVMKeeper) ApplyMessage(ctx types.Context, msg *core.Message, tracer vm.EVMLogger, commit bool) (*evmtypes.MsgEthereumTxResponse, error) { ret := _m.Called(ctx, msg, tracer, commit) if len(ret) == 0 { @@ -36,10 +36,10 @@ func (_m *FungibleEVMKeeper) ApplyMessage(ctx types.Context, msg core.Message, t var r0 *evmtypes.MsgEthereumTxResponse var r1 error - if rf, ok := ret.Get(0).(func(types.Context, core.Message, vm.EVMLogger, bool) (*evmtypes.MsgEthereumTxResponse, error)); ok { + if rf, ok := ret.Get(0).(func(types.Context, *core.Message, vm.EVMLogger, bool) (*evmtypes.MsgEthereumTxResponse, error)); ok { return rf(ctx, msg, tracer, commit) } - if rf, ok := ret.Get(0).(func(types.Context, core.Message, vm.EVMLogger, bool) *evmtypes.MsgEthereumTxResponse); ok { + if rf, ok := ret.Get(0).(func(types.Context, *core.Message, vm.EVMLogger, bool) *evmtypes.MsgEthereumTxResponse); ok { r0 = rf(ctx, msg, tracer, commit) } else { if ret.Get(0) != nil { @@ -47,7 +47,7 @@ func (_m *FungibleEVMKeeper) ApplyMessage(ctx types.Context, msg core.Message, t } } - if rf, ok := ret.Get(1).(func(types.Context, core.Message, vm.EVMLogger, bool) error); ok { + if rf, ok := ret.Get(1).(func(types.Context, *core.Message, vm.EVMLogger, bool) error); ok { r1 = rf(ctx, msg, tracer, commit) } else { r1 = ret.Error(1) diff --git a/testutil/sample/crypto.go b/testutil/sample/crypto.go index e14b64f967..7cc565936a 100644 --- a/testutil/sample/crypto.go +++ b/testutil/sample/crypto.go @@ -7,6 +7,9 @@ import ( "strconv" "testing" + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/cometbft/cometbft/crypto/secp256k1" "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" @@ -57,6 +60,18 @@ func EthAddress() ethcommon.Address { return ethcommon.BytesToAddress(sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address()).Bytes()) } +// BtcAddressP2WPKH returns a sample btc P2WPKH address +func BtcAddressP2WPKH(t *testing.T, net *chaincfg.Params) string { + privateKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + pubKeyHash := btcutil.Hash160(privateKey.PubKey().SerializeCompressed()) + addr, err := btcutil.NewAddressWitnessPubKeyHash(pubKeyHash, net) + require.NoError(t, err) + + return addr.String() +} + // SolanaPrivateKey returns a sample solana private key func SolanaPrivateKey(t *testing.T) solana.PrivateKey { privKey, err := solana.NewRandomPrivateKey() @@ -91,7 +106,7 @@ func SolanaSignature(t *testing.T) solana.Signature { // Hash returns a sample hash func Hash() ethcommon.Hash { - return EthAddress().Hash() + return ethcommon.BytesToHash(EthAddress().Bytes()) } // BtcHash returns a sample btc hash diff --git a/testutil/sample/sample_ton.go b/testutil/sample/ton.go similarity index 71% rename from testutil/sample/sample_ton.go rename to testutil/sample/ton.go index 01ce8724c5..4e1627f394 100644 --- a/testutil/sample/sample_ton.go +++ b/testutil/sample/ton.go @@ -8,7 +8,6 @@ import ( "unsafe" "cosmossdk.io/math" - eth "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" "github.com/tonkeeper/tongo/boc" "github.com/tonkeeper/tongo/tlb" @@ -18,16 +17,17 @@ import ( ) const ( - tonWorkchainID = 0 - tonShardID = 123 - tonDepositFee = 10_000_000 // 0.01 TON - tonSampleGasUsage = 50_000_000 // 0.05 TON + tonWorkchainID = 0 + tonShardID = 123 + tonSampleTxFee = 006_500_000 // 0.0065 TON + tonSampleGasUsed = 8500 ) type TONTransactionProps struct { - Account ton.AccountID - GasUsed uint64 - BlockID ton.BlockIDExt + Account ton.AccountID + GasUsed uint64 + TotalTONFees uint64 + BlockID ton.BlockIDExt // For simplicity let's have only one input // and one output (both optional) @@ -56,7 +56,7 @@ func TONDonateProps(t *testing.T, acc ton.AccountID, d toncontracts.Donation) TO body, err := d.AsBody() require.NoError(t, err) - deposited := tonSampleGasUsage + d.Amount.Uint64() + tonSent := tonSampleTxFee + d.Amount.Uint64() return TONTransactionProps{ Account: acc, @@ -65,7 +65,7 @@ func TONDonateProps(t *testing.T, acc ton.AccountID, d toncontracts.Donation) TO Bounce: true, Src: d.Sender.ToMsgAddress(), Dest: acc.ToMsgAddress(), - Value: tlb.CurrencyCollection{Grams: tlb.Grams(deposited)}, + Value: tlb.CurrencyCollection{Grams: tlb.Grams(tonSent)}, }), Body: tlb.EitherRef[tlb.Any]{Value: tlb.Any(*body)}, }, @@ -80,7 +80,7 @@ func TONDepositProps(t *testing.T, acc ton.AccountID, d toncontracts.Deposit) TO body, err := d.AsBody() require.NoError(t, err) - logBody := depositLogMock(t, d.Sender, d.Amount.Uint64(), d.Recipient, nil) + logBody := depositLogMock(t, d.Amount.Uint64(), tonSampleTxFee) return TONTransactionProps{ Account: acc, @@ -107,7 +107,7 @@ func TONDepositAndCallProps(t *testing.T, acc ton.AccountID, d toncontracts.Depo body, err := d.AsBody() require.NoError(t, err) - logBody := depositLogMock(t, d.Sender, d.Amount.Uint64(), d.Recipient, d.CallData) + logBody := depositLogMock(t, d.Amount.Uint64(), tonSampleTxFee) return TONTransactionProps{ Account: acc, @@ -126,6 +126,31 @@ func TONDepositAndCallProps(t *testing.T, acc ton.AccountID, d toncontracts.Depo } } +func TONWithdrawal(t *testing.T, acc ton.AccountID, w toncontracts.Withdrawal) ton.Transaction { + return TONTransaction(t, TONWithdrawalProps(t, acc, w)) +} + +func TONWithdrawalProps(t *testing.T, acc ton.AccountID, w toncontracts.Withdrawal) TONTransactionProps { + body, err := w.AsBody() + require.NoError(t, err) + + return TONTransactionProps{ + Account: acc, + Input: &tlb.Message{ + Info: externalMessageInfo(acc), + Body: tlb.EitherRef[tlb.Any]{Value: tlb.Any(*body)}, + }, + Output: &tlb.Message{ + Info: internalMessageInfo(&intMsgInfo{ + IhrDisabled: true, + Src: acc.ToMsgAddress(), + Dest: w.Recipient.ToMsgAddress(), + Value: tlb.CurrencyCollection{Grams: tlb.Coins(w.Amount.Uint64())}, + }), + }, + } +} + // TONTransaction creates a sample TON transaction. func TONTransaction(t *testing.T, p TONTransactionProps) ton.Transaction { require.False(t, p.Account.IsZero(), "account address is empty") @@ -134,7 +159,11 @@ func TONTransaction(t *testing.T, p TONTransactionProps) ton.Transaction { now := time.Now().UTC() if p.GasUsed == 0 { - p.GasUsed = tonSampleGasUsage + p.GasUsed = tonSampleGasUsed + } + + if p.TotalTONFees == 0 { + p.TotalTONFees = tonSampleTxFee } if p.BlockID.BlockID.Seqno == 0 { @@ -170,7 +199,7 @@ func TONTransaction(t *testing.T, p TONTransactionProps) ton.Transaction { Lt: lt, Now: uint32(now.Unix()), OutMsgCnt: tlb.Uint15(len(outputs.Keys())), - TotalFees: tlb.CurrencyCollection{Grams: tlb.Grams(p.GasUsed)}, + TotalFees: tlb.CurrencyCollection{Grams: tlb.Grams(p.TotalTONFees)}, Msgs: messages{InMsg: input, OutMsgs: outputs}, }, } @@ -207,6 +236,20 @@ func internalMessageInfo(info *intMsgInfo) tlb.CommonMsgInfo { } } +func externalMessageInfo(dest ton.AccountID) tlb.CommonMsgInfo { + ext := struct { + Src tlb.MsgAddress + Dest tlb.MsgAddress + ImportFee tlb.VarUInteger16 + }{ + Src: tlb.MsgAddress{SumType: "AddrNone"}, + Dest: dest.ToMsgAddress(), + ImportFee: tlb.VarUInteger16{}, + } + + return tlb.CommonMsgInfo{SumType: "ExtInMsgInfo", ExtInMsgInfo: &ext} +} + func tonBlockID(now time.Time) ton.BlockIDExt { // simulate shard seqno as unix timestamp seqno := uint32(now.Unix()) @@ -221,42 +264,19 @@ func tonBlockID(now time.Time) ton.BlockIDExt { } func fakeDepositAmount(v math.Uint) tlb.Grams { - return tlb.Grams(v.Uint64() + tonDepositFee) + return tlb.Grams(v.Uint64() + tonSampleTxFee) } -func depositLogMock( - t *testing.T, - sender ton.AccountID, - amount uint64, - recipient eth.Address, - callData []byte, -) *boc.Cell { - // cell log = begin_cell() - // .store_uint(op::internal::deposit_and_call, size::op_code_size) - // .store_uint(0, size::query_id_size) - // .store_slice(sender) - // .store_coins(deposit_amount) - // .store_uint(evm_recipient, size::evm_address) - // .store_ref(call_data) // only for DepositAndCall - // .end_cell(); +func depositLogMock(t *testing.T, depositAmount, txFee uint64) *boc.Cell { + // cell log = begin_cell() + // .store_coins(deposit_amount) + // .store_coins(tx_fee) + // .end_cell(); b := boc.NewCell() - require.NoError(t, b.WriteUint(0, 32+64)) - - // skip - msgAddr := sender.ToMsgAddress() - require.NoError(t, tlb.Marshal(b, msgAddr)) - coins := tlb.Grams(amount) - require.NoError(t, coins.MarshalTLB(b, nil)) - - require.NoError(t, b.WriteBytes(recipient.Bytes())) - - if callData != nil { - callDataCell, err := toncontracts.MarshalSnakeCell(callData) - require.NoError(t, err) - require.NoError(t, b.AddRef(callDataCell)) - } + require.NoError(t, tlb.Grams(depositAmount).MarshalTLB(b, nil)) + require.NoError(t, tlb.Grams(txFee).MarshalTLB(b, nil)) return b } diff --git a/testutil/sample/sample_ton_test.go b/testutil/sample/ton_test.go similarity index 100% rename from testutil/sample/sample_ton_test.go rename to testutil/sample/ton_test.go diff --git a/typescript/zetachain/zetacore/crosschain/tx_pb.d.ts b/typescript/zetachain/zetacore/crosschain/tx_pb.d.ts index f830e00a48..db3e7073ea 100644 --- a/typescript/zetachain/zetacore/crosschain/tx_pb.d.ts +++ b/typescript/zetachain/zetacore/crosschain/tx_pb.d.ts @@ -186,6 +186,8 @@ export declare class MsgAddInboundTrackerResponse extends Message { diff --git a/x/crosschain/keeper/evm_hooks.go b/x/crosschain/keeper/evm_hooks.go index c07cba213a..38726c287a 100644 --- a/x/crosschain/keeper/evm_hooks.go +++ b/x/crosschain/keeper/evm_hooks.go @@ -40,21 +40,21 @@ func (k Keeper) Hooks() Hooks { // PostTxProcessing is a wrapper for calling the EVM PostTxProcessing hook on // the module keeper -func (h Hooks) PostTxProcessing(ctx sdk.Context, msg core.Message, receipt *ethtypes.Receipt) error { +func (h Hooks) PostTxProcessing(ctx sdk.Context, msg *core.Message, receipt *ethtypes.Receipt) error { return h.k.PostTxProcessing(ctx, msg, receipt) } // PostTxProcessing implements EvmHooks.PostTxProcessing. func (k Keeper) PostTxProcessing( ctx sdk.Context, - msg core.Message, + msg *core.Message, receipt *ethtypes.Receipt, ) error { var emittingContract ethcommon.Address - if msg.To() != nil { - emittingContract = *msg.To() + if msg.To != nil { + emittingContract = *msg.To } - return k.ProcessLogs(ctx, receipt.Logs, emittingContract, msg.From().Hex()) + return k.ProcessLogs(ctx, receipt.Logs, emittingContract, msg.From.Hex()) } // ProcessLogs post-processes logs emitted by a zEVM contract; if the log contains Withdrawal event diff --git a/x/crosschain/keeper/msg_server_vote_inbound_tx_test.go b/x/crosschain/keeper/msg_server_vote_inbound_tx_test.go index 4f12536a8d..6ee95004f4 100644 --- a/x/crosschain/keeper/msg_server_vote_inbound_tx_test.go +++ b/x/crosschain/keeper/msg_server_vote_inbound_tx_test.go @@ -11,6 +11,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ethcommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" + "github.com/holiman/uint256" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/zeta-chain/ethermint/x/evm/statedb" @@ -68,7 +69,7 @@ func TestKeeper_VoteInbound(t *testing.T) { err := sdkk.EvmKeeper.SetAccount(ctx, ethcommon.HexToAddress(msg.Receiver), statedb.Account{ Nonce: 0, - Balance: big.NewInt(0), + Balance: uint256.NewInt(0), CodeHash: crypto.Keccak256(nil), }) require.NoError(t, err) diff --git a/x/crosschain/keeper/msg_server_whitelist_erc20.go b/x/crosschain/keeper/msg_server_whitelist_erc20.go index 4ae98a85b5..197310e16c 100644 --- a/x/crosschain/keeper/msg_server_whitelist_erc20.go +++ b/x/crosschain/keeper/msg_server_whitelist_erc20.go @@ -8,6 +8,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/gagliardetto/solana-go" "github.com/zeta-chain/node/pkg/coin" authoritytypes "github.com/zeta-chain/node/x/authority/types" @@ -31,20 +32,44 @@ func (k msgServer) WhitelistERC20( return nil, errorsmod.Wrap(authoritytypes.ErrUnauthorized, err.Error()) } - erc20Addr := ethcommon.HexToAddress(msg.Erc20Address) - if erc20Addr == (ethcommon.Address{}) { + chain, found := k.zetaObserverKeeper.GetSupportedChainFromChainID(ctx, msg.ChainId) + if !found { + return nil, errorsmod.Wrapf(types.ErrInvalidChainID, "chain id (%d) not supported", msg.ChainId) + } + + switch { + case chain.IsEVMChain(): + erc20Addr := ethcommon.HexToAddress(msg.Erc20Address) + if erc20Addr == (ethcommon.Address{}) { + return nil, errorsmod.Wrapf( + sdkerrors.ErrInvalidAddress, + "invalid ERC20 contract address (%s)", + msg.Erc20Address, + ) + } + + case chain.IsSolanaChain(): + _, err := solana.PublicKeyFromBase58(msg.Erc20Address) + if err != nil { + return nil, errorsmod.Wrapf( + sdkerrors.ErrInvalidAddress, + "invalid solana contract address (%s)", + msg.Erc20Address, + ) + } + + default: return nil, errorsmod.Wrapf( - sdkerrors.ErrInvalidAddress, - "invalid ERC20 contract address (%s)", - msg.Erc20Address, + sdkerrors.ErrInvalidChainID, + "whitelist for chain id (%d) not supported", + msg.ChainId, ) } - // check if the erc20 is already whitelisted + // check if the asset is already whitelisted foreignCoins := k.fungibleKeeper.GetAllForeignCoins(ctx) for _, fCoin := range foreignCoins { - assetAddr := ethcommon.HexToAddress(fCoin.Asset) - if assetAddr == erc20Addr && fCoin.ForeignChainId == msg.ChainId { + if fCoin.Asset == msg.Erc20Address && fCoin.ForeignChainId == msg.ChainId { return nil, errorsmod.Wrapf( fungibletypes.ErrForeignCoinAlreadyExist, "ERC20 contract address (%s) already whitelisted on chain (%d)", @@ -59,11 +84,6 @@ func (k msgServer) WhitelistERC20( return nil, errorsmod.Wrapf(types.ErrCannotFindTSSKeys, "Cannot create new admin cmd of type whitelistERC20") } - chain, found := k.zetaObserverKeeper.GetSupportedChainFromChainID(ctx, msg.ChainId) - if !found { - return nil, errorsmod.Wrapf(types.ErrInvalidChainID, "chain id (%d) not supported", msg.ChainId) - } - // use a temporary context for the zrc20 deployment tmpCtx, commit := ctx.CacheContext() diff --git a/x/crosschain/keeper/msg_server_whitelist_erc20_test.go b/x/crosschain/keeper/msg_server_whitelist_erc20_test.go index 3eb18b9931..c82261bd05 100644 --- a/x/crosschain/keeper/msg_server_whitelist_erc20_test.go +++ b/x/crosschain/keeper/msg_server_whitelist_erc20_test.go @@ -18,81 +18,156 @@ import ( ) func TestKeeper_WhitelistERC20(t *testing.T) { - t.Run("can deploy and whitelist an erc20", func(t *testing.T) { - k, ctx, sdkk, zk := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ + tests := []struct { + name string + tokenAddress string + secondTokenAddress string + chainID int64 + }{ + { + name: "can deploy and whitelist an erc20", + tokenAddress: sample.EthAddress().Hex(), + secondTokenAddress: sample.EthAddress().Hex(), + chainID: getValidEthChainID(), + }, + { + name: "can deploy and whitelist a spl", + tokenAddress: sample.SolanaAddress(t), + secondTokenAddress: sample.SolanaAddress(t), + chainID: getValidSolanaChainID(), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + k, ctx, sdkk, zk := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ + UseAuthorityMock: true, + }) + + msgServer := crosschainkeeper.NewMsgServerImpl(*k) + k.GetAuthKeeper().GetModuleAccount(ctx, fungibletypes.ModuleName) + + setSupportedChain(ctx, zk, tt.chainID) + + admin := sample.AccAddress() + authorityMock := keepertest.GetCrosschainAuthorityMock(t, k) + + deploySystemContracts(t, ctx, zk.FungibleKeeper, sdkk.EvmKeeper) + setupGasCoin(t, ctx, zk.FungibleKeeper, sdkk.EvmKeeper, tt.chainID, "foobar", "FOOBAR") + k.GetObserverKeeper().SetTssAndUpdateNonce(ctx, sample.Tss()) + k.SetGasPrice(ctx, types.GasPrice{ + ChainId: tt.chainID, + MedianIndex: 0, + Prices: []uint64{1}, + }) + + msg := types.MsgWhitelistERC20{ + Creator: admin, + Erc20Address: tt.tokenAddress, + ChainId: tt.chainID, + Name: "foo", + Symbol: "FOO", + Decimals: 18, + GasLimit: 100000, + } + keepertest.MockCheckAuthorization(&authorityMock.Mock, &msg, nil) + res, err := msgServer.WhitelistERC20(ctx, &msg) + require.NoError(t, err) + require.NotNil(t, res) + zrc20 := res.Zrc20Address + cctxIndex := res.CctxIndex + + // check zrc20 and cctx created + assertContractDeployment(t, sdkk.EvmKeeper, ctx, ethcommon.HexToAddress(zrc20)) + fc, found := zk.FungibleKeeper.GetForeignCoins(ctx, zrc20) + require.True(t, found) + require.EqualValues(t, "foo", fc.Name) + require.EqualValues(t, tt.tokenAddress, fc.Asset) + cctx, found := k.GetCrossChainTx(ctx, cctxIndex) + require.True(t, found) + require.EqualValues( + t, + fmt.Sprintf("%s:%s", constant.CmdWhitelistERC20, tt.tokenAddress), + cctx.RelayedMessage, + ) + + // check gas limit is set + gasLimit, err := zk.FungibleKeeper.QueryGasLimit(ctx, ethcommon.HexToAddress(zrc20)) + require.NoError(t, err) + require.Equal(t, uint64(100000), gasLimit.Uint64()) + + msgNew := types.MsgWhitelistERC20{ + Creator: admin, + Erc20Address: tt.secondTokenAddress, + ChainId: tt.chainID, + Name: "bar", + Symbol: "BAR", + Decimals: 18, + GasLimit: 100000, + } + keepertest.MockCheckAuthorization(&authorityMock.Mock, &msgNew, nil) + + // Ensure that whitelist a new erc20 create a cctx with a different index + res, err = msgServer.WhitelistERC20(ctx, &msgNew) + require.NoError(t, err) + require.NotNil(t, res) + require.NotEqual(t, cctxIndex, res.CctxIndex) + }) + } + + t.Run("should fail if not authorized", func(t *testing.T) { + k, ctx, _, _ := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ UseAuthorityMock: true, }) msgServer := crosschainkeeper.NewMsgServerImpl(*k) k.GetAuthKeeper().GetModuleAccount(ctx, fungibletypes.ModuleName) - chainID := getValidEthChainID() - setSupportedChain(ctx, zk, chainID) - admin := sample.AccAddress() - erc20Address := sample.EthAddress().Hex() authorityMock := keepertest.GetCrosschainAuthorityMock(t, k) - deploySystemContracts(t, ctx, zk.FungibleKeeper, sdkk.EvmKeeper) - setupGasCoin(t, ctx, zk.FungibleKeeper, sdkk.EvmKeeper, chainID, "foobar", "FOOBAR") - k.GetObserverKeeper().SetTssAndUpdateNonce(ctx, sample.Tss()) - k.SetGasPrice(ctx, types.GasPrice{ - ChainId: chainID, - MedianIndex: 0, - Prices: []uint64{1}, - }) - msg := types.MsgWhitelistERC20{ Creator: admin, - Erc20Address: erc20Address, - ChainId: chainID, + Erc20Address: sample.EthAddress().Hex(), + ChainId: getValidEthChainID(), Name: "foo", Symbol: "FOO", Decimals: 18, GasLimit: 100000, } - keepertest.MockCheckAuthorization(&authorityMock.Mock, &msg, nil) - res, err := msgServer.WhitelistERC20(ctx, &msg) - require.NoError(t, err) - require.NotNil(t, res) - zrc20 := res.Zrc20Address - cctxIndex := res.CctxIndex - - // check zrc20 and cctx created - assertContractDeployment(t, sdkk.EvmKeeper, ctx, ethcommon.HexToAddress(zrc20)) - fc, found := zk.FungibleKeeper.GetForeignCoins(ctx, zrc20) - require.True(t, found) - require.EqualValues(t, "foo", fc.Name) - require.EqualValues(t, erc20Address, fc.Asset) - cctx, found := k.GetCrossChainTx(ctx, cctxIndex) - require.True(t, found) - require.EqualValues(t, fmt.Sprintf("%s:%s", constant.CmdWhitelistERC20, erc20Address), cctx.RelayedMessage) - - // check gas limit is set - gasLimit, err := zk.FungibleKeeper.QueryGasLimit(ctx, ethcommon.HexToAddress(zrc20)) - require.NoError(t, err) - require.Equal(t, uint64(100000), gasLimit.Uint64()) - - msgNew := types.MsgWhitelistERC20{ + keepertest.MockCheckAuthorization(&authorityMock.Mock, &msg, authoritytypes.ErrUnauthorized) + _, err := msgServer.WhitelistERC20(ctx, &msg) + require.ErrorIs(t, err, authoritytypes.ErrUnauthorized) + }) + + t.Run("should fail if invalid erc20 address", func(t *testing.T) { + k, ctx, _, _ := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ + UseAuthorityMock: true, + }) + + msgServer := crosschainkeeper.NewMsgServerImpl(*k) + k.GetAuthKeeper().GetModuleAccount(ctx, fungibletypes.ModuleName) + + admin := sample.AccAddress() + authorityMock := keepertest.GetCrosschainAuthorityMock(t, k) + + msg := types.MsgWhitelistERC20{ Creator: admin, - Erc20Address: sample.EthAddress().Hex(), - ChainId: chainID, - Name: "bar", - Symbol: "BAR", + Erc20Address: "invalid", + ChainId: getValidEthChainID(), + Name: "foo", + Symbol: "FOO", Decimals: 18, GasLimit: 100000, } - keepertest.MockCheckAuthorization(&authorityMock.Mock, &msgNew, nil) + keepertest.MockCheckAuthorization(&authorityMock.Mock, &msg, nil) - // Ensure that whitelist a new erc20 create a cctx with a different index - res, err = msgServer.WhitelistERC20(ctx, &msgNew) - require.NoError(t, err) - require.NotNil(t, res) - require.NotEqual(t, cctxIndex, res.CctxIndex) + _, err := msgServer.WhitelistERC20(ctx, &msg) + require.ErrorIs(t, err, sdkerrors.ErrInvalidAddress) }) - t.Run("should fail if not authorized", func(t *testing.T) { - k, ctx, _, _ := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ + t.Run("should fail if invalid spl address", func(t *testing.T) { + k, ctx, _, zk := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ UseAuthorityMock: true, }) @@ -102,22 +177,26 @@ func TestKeeper_WhitelistERC20(t *testing.T) { admin := sample.AccAddress() authorityMock := keepertest.GetCrosschainAuthorityMock(t, k) + chainID := getValidSolanaChainID() + setSupportedChain(ctx, zk, chainID) + msg := types.MsgWhitelistERC20{ Creator: admin, - Erc20Address: sample.EthAddress().Hex(), - ChainId: getValidEthChainID(), + Erc20Address: "invalid", + ChainId: chainID, Name: "foo", Symbol: "FOO", Decimals: 18, GasLimit: 100000, } - keepertest.MockCheckAuthorization(&authorityMock.Mock, &msg, authoritytypes.ErrUnauthorized) + keepertest.MockCheckAuthorization(&authorityMock.Mock, &msg, nil) + _, err := msgServer.WhitelistERC20(ctx, &msg) - require.ErrorIs(t, err, authoritytypes.ErrUnauthorized) + require.ErrorIs(t, err, sdkerrors.ErrInvalidAddress) }) - t.Run("should fail if invalid erc20 address", func(t *testing.T) { - k, ctx, _, _ := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ + t.Run("should fail if whitelisting not supported for chain", func(t *testing.T) { + k, ctx, _, zk := keepertest.CrosschainKeeperWithMocks(t, keepertest.CrosschainMockOptions{ UseAuthorityMock: true, }) @@ -127,10 +206,13 @@ func TestKeeper_WhitelistERC20(t *testing.T) { admin := sample.AccAddress() authorityMock := keepertest.GetCrosschainAuthorityMock(t, k) + chainID := getValidBtcChainID() + setSupportedChain(ctx, zk, chainID) + msg := types.MsgWhitelistERC20{ Creator: admin, Erc20Address: "invalid", - ChainId: getValidEthChainID(), + ChainId: chainID, Name: "foo", Symbol: "FOO", Decimals: 18, @@ -139,7 +221,7 @@ func TestKeeper_WhitelistERC20(t *testing.T) { keepertest.MockCheckAuthorization(&authorityMock.Mock, &msg, nil) _, err := msgServer.WhitelistERC20(ctx, &msg) - require.ErrorIs(t, err, sdkerrors.ErrInvalidAddress) + require.ErrorIs(t, err, sdkerrors.ErrInvalidChainID) }) t.Run("should fail if foreign coin already exists for the asset", func(t *testing.T) { diff --git a/x/crosschain/simulation/operations.go b/x/crosschain/simulation/operations.go index f5648efdc8..c13354d3dd 100644 --- a/x/crosschain/simulation/operations.go +++ b/x/crosschain/simulation/operations.go @@ -315,12 +315,6 @@ func SimulateVoteInbound(k keeper.Keeper) simtypes.Operation { func SimulateMsgVoteGasPrice(k keeper.Keeper) simtypes.Operation { return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, chainID string, ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { - - // System contracts are deployed on the first block , so we cannot vote on gas prices before that - if ctx.BlockHeight() <= 1 { - return simtypes.NoOpMsg(types.ModuleName, authz.GasPriceVoter.String(), "block height less than 1"), nil, nil - } - // Get a random account and observer simAccount, randomObserver, err := GetRandomAccountAndObserver(r, ctx, k, accounts) if err != nil { @@ -346,6 +340,11 @@ func SimulateMsgVoteGasPrice(k keeper.Keeper) simtypes.Operation { Supply: fmt.Sprintf("%d", r.Int63()), } + // System contracts are deployed on the first block, so we cannot vote on gas prices before that + if ctx.BlockHeight() <= 1 { + return simtypes.NewOperationMsg(&msg, true, "block height less than 1", nil), nil, nil + } + err = msg.ValidateBasic() if err != nil { return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to validate vote gas price msg"), nil, err diff --git a/x/crosschain/types/message_vote_inbound.go b/x/crosschain/types/message_vote_inbound.go index 34afb68be2..3db9fdde0f 100644 --- a/x/crosschain/types/message_vote_inbound.go +++ b/x/crosschain/types/message_vote_inbound.go @@ -22,6 +22,13 @@ const MaxMessageLength = 10240 // InboundVoteOption is a function that sets some option on the inbound vote message type InboundVoteOption func(*MsgVoteInbound) +// WithMemoRevertOptions sets the revert options for inbound vote message +func WithRevertOptions(revertOptions RevertOptions) InboundVoteOption { + return func(msg *MsgVoteInbound) { + msg.RevertOptions = revertOptions + } +} + // WithZEVMRevertOptions sets the revert options for the inbound vote message (ZEVM format) // the function convert the type from abigen to type defined in proto func WithZEVMRevertOptions(revertOptions gatewayzevm.RevertOptions) InboundVoteOption { diff --git a/x/crosschain/types/message_vote_inbound_test.go b/x/crosschain/types/message_vote_inbound_test.go index e3e9bc7fce..2c30b2a343 100644 --- a/x/crosschain/types/message_vote_inbound_test.go +++ b/x/crosschain/types/message_vote_inbound_test.go @@ -42,6 +42,45 @@ func TestNewMsgVoteInbound(t *testing.T) { require.EqualValues(t, types.NewEmptyRevertOptions(), msg.RevertOptions) }) + t.Run("can set revert options", func(t *testing.T) { + revertAddress := sample.EthAddress() + abortAddress := sample.EthAddress() + revertMessage := sample.Bytes() + + msg := types.NewMsgVoteInbound( + sample.AccAddress(), + sample.AccAddress(), + 31, + sample.String(), + sample.String(), + 31, + math.NewUint(31), + sample.String(), + sample.String(), + 31, + 31, + coin.CoinType_Gas, + sample.String(), + 31, + types.ProtocolContractVersion_V2, + true, + types.WithRevertOptions(types.RevertOptions{ + RevertAddress: revertAddress.Hex(), + CallOnRevert: true, + AbortAddress: abortAddress.Hex(), + RevertMessage: revertMessage, + RevertGasLimit: math.NewUint(21000), + }), + ) + require.EqualValues(t, types.RevertOptions{ + RevertAddress: revertAddress.Hex(), + CallOnRevert: true, + AbortAddress: abortAddress.Hex(), + RevertMessage: revertMessage, + RevertGasLimit: math.NewUint(21000), + }, msg.RevertOptions) + }) + t.Run("can set ZEVM revert options", func(t *testing.T) { revertAddress := sample.EthAddress() abortAddress := sample.EthAddress() diff --git a/x/crosschain/types/message_whitelist_erc20.go b/x/crosschain/types/message_whitelist_erc20.go index 3267581662..d27492d7a5 100644 --- a/x/crosschain/types/message_whitelist_erc20.go +++ b/x/crosschain/types/message_whitelist_erc20.go @@ -4,7 +4,6 @@ import ( cosmoserrors "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" - ethcommon "github.com/ethereum/go-ethereum/common" "github.com/zeta-chain/node/x/fungible/types" ) @@ -53,9 +52,8 @@ func (msg *MsgWhitelistERC20) ValidateBasic() error { if err != nil { return cosmoserrors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid creator address (%s)", err) } - // check if the system contract address is valid - if ethcommon.HexToAddress(msg.Erc20Address) == (ethcommon.Address{}) { - return cosmoserrors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid ERC20 contract address (%s)", msg.Erc20Address) + if msg.Erc20Address == "" { + return cosmoserrors.Wrapf(sdkerrors.ErrInvalidAddress, "empty asset address") } if msg.Decimals > 128 { return cosmoserrors.Wrapf(types.ErrInvalidDecimals, "invalid decimals (%d)", msg.Decimals) diff --git a/x/crosschain/types/message_whitelist_erc20_test.go b/x/crosschain/types/message_whitelist_erc20_test.go index 8219140fd9..d5bf845272 100644 --- a/x/crosschain/types/message_whitelist_erc20_test.go +++ b/x/crosschain/types/message_whitelist_erc20_test.go @@ -32,10 +32,10 @@ func TestMsgWhitelistERC20_ValidateBasic(t *testing.T) { error: true, }, { - name: "invalid erc20", + name: "invalid asset", msg: types.NewMsgWhitelistERC20( sample.AccAddress(), - "0x0", + "", 1, "name", "symbol", diff --git a/x/crosschain/types/tx.pb.go b/x/crosschain/types/tx.pb.go index ffe5c8cec7..52facc862d 100644 --- a/x/crosschain/types/tx.pb.go +++ b/x/crosschain/types/tx.pb.go @@ -337,6 +337,7 @@ func (m *MsgAddInboundTrackerResponse) XXX_DiscardUnknown() { var xxx_messageInfo_MsgAddInboundTrackerResponse proto.InternalMessageInfo +// TODO: https://github.com/zeta-chain/node/issues/3083 type MsgWhitelistERC20 struct { Creator string `protobuf:"bytes,1,opt,name=creator,proto3" json:"creator,omitempty"` Erc20Address string `protobuf:"bytes,2,opt,name=erc20_address,json=erc20Address,proto3" json:"erc20_address,omitempty"` diff --git a/x/fungible/keeper/deposits_test.go b/x/fungible/keeper/deposits_test.go index 3958ae191e..836ce0b61a 100644 --- a/x/fungible/keeper/deposits_test.go +++ b/x/fungible/keeper/deposits_test.go @@ -437,6 +437,10 @@ func TestKeeper_DepositCoinZeta(t *testing.T) { b := sdkk.BankKeeper.GetBalance(ctx, zetaToAddress, config.BaseDenom) require.Equal(t, int64(0), b.Amount.Int64()) errorMint := errors.New("", 1, "error minting coins") + + bankMock.On("GetSupply", ctx, mock.Anything, mock.Anything). + Return(sdk.NewCoin(config.BaseDenom, sdk.NewInt(0))). + Once() bankMock.On("MintCoins", ctx, types.ModuleName, mock.Anything).Return(errorMint).Once() err := k.DepositCoinZeta(ctx, to, amount) require.ErrorIs(t, err, errorMint) diff --git a/x/fungible/keeper/evm.go b/x/fungible/keeper/evm.go index ab0332dfe3..95b1b05dae 100644 --- a/x/fungible/keeper/evm.go +++ b/x/fungible/keeper/evm.go @@ -17,6 +17,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" evmtypes "github.com/zeta-chain/ethermint/x/evm/types" @@ -735,19 +736,19 @@ func (k Keeper) CallEVMWithData( if gasLimit != nil { gasCap = gasLimit.Uint64() } - msg := ethtypes.NewMessage( - from, - contract, - nonce, - value, // amount - gasCap, // gasLimit - big.NewInt(0), // gasFeeCap - big.NewInt(0), // gasTipCap - big.NewInt(0), // gasPrice - data, - ethtypes.AccessList{}, // AccessList - !commit, // isFake - ) + msg := &core.Message{ + From: from, + To: contract, + Nonce: nonce, + Value: value, // amount + GasLimit: gasCap, // gasLimit + GasFeeCap: big.NewInt(0), // gasFeeCap + GasTipCap: big.NewInt(0), // gasTipCap + GasPrice: big.NewInt(0), // gasPrice + Data: data, + AccessList: ethtypes.AccessList{}, // AccessList + SkipAccountChecks: !commit, // isFake + } k.evmKeeper.WithChainID(ctx) //FIXME: set chainID for signer; should not need to do this; but seems necessary. Why? k.Logger(ctx).Debug("call evm", "gasCap", gasCap, "chainid", k.evmKeeper.ChainID(), "ctx.chainid", ctx.ChainID()) res, err := k.evmKeeper.ApplyMessage(ctx, msg, evmtypes.NewNoOpTracer(), commit) @@ -799,7 +800,7 @@ func (k Keeper) CallEVMWithData( if !noEthereumTxEvent { // adding txData for more info in rpc methods in order to parse synthetic txs - attrs = append(attrs, sdk.NewAttribute(evmtypes.AttributeKeyTxData, hexutil.Encode(msg.Data()))) + attrs = append(attrs, sdk.NewAttribute(evmtypes.AttributeKeyTxData, hexutil.Encode(msg.Data))) // adding nonce for more info in rpc methods in order to parse synthetic txs attrs = append(attrs, sdk.NewAttribute(evmtypes.AttributeKeyTxNonce, fmt.Sprint(nonce))) ctx.EventManager().EmitEvents(sdk.Events{ diff --git a/x/fungible/keeper/evm_hooks.go b/x/fungible/keeper/evm_hooks.go index 03f1ecf6cd..04bba3ccbe 100644 --- a/x/fungible/keeper/evm_hooks.go +++ b/x/fungible/keeper/evm_hooks.go @@ -22,7 +22,7 @@ func (k Keeper) EVMHooks() EVMHooks { } // PostTxProcessing is a wrapper for calling the EVM PostTxProcessing hook on the module keeper -func (h EVMHooks) PostTxProcessing(ctx sdk.Context, _ core.Message, receipt *ethtypes.Receipt) error { +func (h EVMHooks) PostTxProcessing(ctx sdk.Context, _ *core.Message, receipt *ethtypes.Receipt) error { return h.k.checkPausedZRC20(ctx, receipt) } diff --git a/x/fungible/keeper/zeta.go b/x/fungible/keeper/zeta.go index bc15a06e44..cd4acfcaa7 100644 --- a/x/fungible/keeper/zeta.go +++ b/x/fungible/keeper/zeta.go @@ -1,6 +1,7 @@ package keeper import ( + "fmt" "math/big" sdk "github.com/cosmos/cosmos-sdk/types" @@ -9,9 +10,17 @@ import ( "github.com/zeta-chain/node/x/fungible/types" ) +// ZETAMaxSupplyStr is the maximum mintable ZETA in the fungible module +// 1.85 billion ZETA +const ZETAMaxSupplyStr = "1850000000000000000000000000" + // MintZetaToEVMAccount mints ZETA (gas token) to the given address // NOTE: this method should be used with a temporary context, and it should not be committed if the method returns an error func (k *Keeper) MintZetaToEVMAccount(ctx sdk.Context, to sdk.AccAddress, amount *big.Int) error { + if err := k.validateZetaSupply(ctx, amount); err != nil { + return err + } + coins := sdk.NewCoins(sdk.NewCoin(config.BaseDenom, sdk.NewIntFromBigInt(amount))) // Mint coins if err := k.bankKeeper.MintCoins(ctx, types.ModuleName, coins); err != nil { @@ -23,7 +32,25 @@ func (k *Keeper) MintZetaToEVMAccount(ctx sdk.Context, to sdk.AccAddress, amount } func (k *Keeper) MintZetaToFungibleModule(ctx sdk.Context, amount *big.Int) error { + if err := k.validateZetaSupply(ctx, amount); err != nil { + return err + } + coins := sdk.NewCoins(sdk.NewCoin(config.BaseDenom, sdk.NewIntFromBigInt(amount))) // Mint coins return k.bankKeeper.MintCoins(ctx, types.ModuleName, coins) } + +// validateZetaSupply checks if the minted ZETA amount exceeds the maximum supply +func (k *Keeper) validateZetaSupply(ctx sdk.Context, amount *big.Int) error { + zetaMaxSupply, ok := sdk.NewIntFromString(ZETAMaxSupplyStr) + if !ok { + return fmt.Errorf("failed to parse ZETA max supply: %s", ZETAMaxSupplyStr) + } + + supply := k.bankKeeper.GetSupply(ctx, config.BaseDenom) + if supply.Amount.Add(sdk.NewIntFromBigInt(amount)).GT(zetaMaxSupply) { + return types.ErrMaxSupplyReached + } + return nil +} diff --git a/x/fungible/keeper/zeta_test.go b/x/fungible/keeper/zeta_test.go index 62e41700c1..51e5fe279c 100644 --- a/x/fungible/keeper/zeta_test.go +++ b/x/fungible/keeper/zeta_test.go @@ -2,6 +2,7 @@ package keeper_test import ( "errors" + "github.com/stretchr/testify/mock" "math/big" "testing" @@ -11,6 +12,7 @@ import ( "github.com/zeta-chain/node/cmd/zetacored/config" testkeeper "github.com/zeta-chain/node/testutil/keeper" "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/x/fungible/keeper" "github.com/zeta-chain/node/x/fungible/types" ) @@ -29,6 +31,46 @@ func TestKeeper_MintZetaToEVMAccount(t *testing.T) { require.True(t, bal.Amount.Equal(sdk.NewInt(42))) }) + t.Run("mint the token to reach max supply", func(t *testing.T) { + k, ctx, sdkk, _ := testkeeper.FungibleKeeper(t) + k.GetAuthKeeper().GetModuleAccount(ctx, types.ModuleName) + + acc := sample.Bech32AccAddress() + bal := sdkk.BankKeeper.GetBalance(ctx, acc, config.BaseDenom) + require.True(t, bal.IsZero()) + + zetaMaxSupply, ok := sdk.NewIntFromString(keeper.ZETAMaxSupplyStr) + require.True(t, ok) + + supply := sdkk.BankKeeper.GetSupply(ctx, config.BaseDenom).Amount + + newAmount := zetaMaxSupply.Sub(supply) + + err := k.MintZetaToEVMAccount(ctx, acc, newAmount.BigInt()) + require.NoError(t, err) + bal = sdkk.BankKeeper.GetBalance(ctx, acc, config.BaseDenom) + require.True(t, bal.Amount.Equal(newAmount)) + }) + + t.Run("can't mint more than max supply", func(t *testing.T) { + k, ctx, sdkk, _ := testkeeper.FungibleKeeper(t) + k.GetAuthKeeper().GetModuleAccount(ctx, types.ModuleName) + + acc := sample.Bech32AccAddress() + bal := sdkk.BankKeeper.GetBalance(ctx, acc, config.BaseDenom) + require.True(t, bal.IsZero()) + + zetaMaxSupply, ok := sdk.NewIntFromString(keeper.ZETAMaxSupplyStr) + require.True(t, ok) + + supply := sdkk.BankKeeper.GetSupply(ctx, config.BaseDenom).Amount + + newAmount := zetaMaxSupply.Sub(supply).Add(sdk.NewInt(1)) + + err := k.MintZetaToEVMAccount(ctx, acc, newAmount.BigInt()) + require.ErrorIs(t, err, types.ErrMaxSupplyReached) + }) + coins42 := sdk.NewCoins(sdk.NewCoin(config.BaseDenom, sdk.NewInt(42))) t.Run("should fail if minting fail", func(t *testing.T) { @@ -36,6 +78,9 @@ func TestKeeper_MintZetaToEVMAccount(t *testing.T) { mockBankKeeper := testkeeper.GetFungibleBankMock(t, k) + mockBankKeeper.On("GetSupply", ctx, mock.Anything, mock.Anything). + Return(sdk.NewCoin(config.BaseDenom, sdk.NewInt(0))). + Once() mockBankKeeper.On( "MintCoins", ctx, @@ -55,6 +100,9 @@ func TestKeeper_MintZetaToEVMAccount(t *testing.T) { mockBankKeeper := testkeeper.GetFungibleBankMock(t, k) + mockBankKeeper.On("GetSupply", ctx, mock.Anything, mock.Anything). + Return(sdk.NewCoin(config.BaseDenom, sdk.NewInt(0))). + Once() mockBankKeeper.On( "MintCoins", ctx, @@ -76,3 +124,33 @@ func TestKeeper_MintZetaToEVMAccount(t *testing.T) { mockBankKeeper.AssertExpectations(t) }) } + +func TestKeeper_MintZetaToFungibleModule(t *testing.T) { + t.Run("should mint the token in the specified balance", func(t *testing.T) { + k, ctx, sdkk, _ := testkeeper.FungibleKeeper(t) + acc := k.GetAuthKeeper().GetModuleAccount(ctx, types.ModuleName).GetAddress() + + bal := sdkk.BankKeeper.GetBalance(ctx, acc, config.BaseDenom) + require.True(t, bal.IsZero()) + + err := k.MintZetaToEVMAccount(ctx, acc, big.NewInt(42)) + require.NoError(t, err) + bal = sdkk.BankKeeper.GetBalance(ctx, acc, config.BaseDenom) + require.True(t, bal.Amount.Equal(sdk.NewInt(42))) + }) + + t.Run("can't mint more than max supply", func(t *testing.T) { + k, ctx, sdkk, _ := testkeeper.FungibleKeeper(t) + k.GetAuthKeeper().GetModuleAccount(ctx, types.ModuleName) + + zetaMaxSupply, ok := sdk.NewIntFromString(keeper.ZETAMaxSupplyStr) + require.True(t, ok) + + supply := sdkk.BankKeeper.GetSupply(ctx, config.BaseDenom).Amount + + newAmount := zetaMaxSupply.Sub(supply).Add(sdk.NewInt(1)) + + err := k.MintZetaToFungibleModule(ctx, newAmount.BigInt()) + require.ErrorIs(t, err, types.ErrMaxSupplyReached) + }) +} diff --git a/x/fungible/keeper/zevm_message_passing_test.go b/x/fungible/keeper/zevm_message_passing_test.go index f68fec1f62..f633773e8e 100644 --- a/x/fungible/keeper/zevm_message_passing_test.go +++ b/x/fungible/keeper/zevm_message_passing_test.go @@ -7,6 +7,7 @@ import ( "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/ethereum/go-ethereum/crypto" + "github.com/holiman/uint256" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/zeta-chain/ethermint/x/evm/statedb" @@ -82,7 +83,7 @@ func TestKeeper_ZEVMDepositAndCallContract(t *testing.T) { err := sdkk.EvmKeeper.SetAccount(ctx, zetaTxReceiver, statedb.Account{ Nonce: 0, - Balance: big.NewInt(0), + Balance: uint256.NewInt(0), CodeHash: crypto.Keccak256(nil), }) require.NoError(t, err) @@ -141,11 +142,14 @@ func TestKeeper_ZEVMDepositAndCallContract(t *testing.T) { err := sdkk.EvmKeeper.SetAccount(ctx, zetaTxReceiver, statedb.Account{ Nonce: 0, - Balance: big.NewInt(0), + Balance: uint256.NewInt(0), CodeHash: crypto.Keccak256(nil), }) require.NoError(t, err) errorMint := errors.New("", 10, "error minting coins") + bankMock.On("GetSupply", ctx, mock.Anything, mock.Anything). + Return(sdk.NewCoin(config.BaseDenom, sdk.NewInt(0))). + Once() bankMock.On("MintCoins", ctx, types.ModuleName, mock.Anything).Return(errorMint).Once() _, err = k.ZETADepositAndCallContract( @@ -228,7 +232,7 @@ func TestKeeper_ZEVMRevertAndCallContract(t *testing.T) { err := sdkk.EvmKeeper.SetAccount(ctx, zetaTxSender, statedb.Account{ Nonce: 0, - Balance: big.NewInt(0), + Balance: uint256.NewInt(0), CodeHash: crypto.Keccak256(nil), }) require.NoError(t, err) @@ -291,11 +295,14 @@ func TestKeeper_ZEVMRevertAndCallContract(t *testing.T) { err := sdkk.EvmKeeper.SetAccount(ctx, zetaTxSender, statedb.Account{ Nonce: 0, - Balance: big.NewInt(0), + Balance: uint256.NewInt(0), CodeHash: crypto.Keccak256(nil), }) require.NoError(t, err) errorMint := errors.New("", 101, "error minting coins") + bankMock.On("GetSupply", ctx, mock.Anything, mock.Anything). + Return(sdk.NewCoin(config.BaseDenom, sdk.NewInt(0))). + Once() bankMock.On("MintCoins", ctx, types.ModuleName, mock.Anything).Return(errorMint).Once() _, err = k.ZETARevertAndCallContract( diff --git a/x/fungible/keeper/zrc20_cosmos_coin_mapping_test.go b/x/fungible/keeper/zrc20_cosmos_coin_mapping_test.go index 16803a917c..c19866e217 100644 --- a/x/fungible/keeper/zrc20_cosmos_coin_mapping_test.go +++ b/x/fungible/keeper/zrc20_cosmos_coin_mapping_test.go @@ -38,28 +38,21 @@ func Test_LockZRC20(t *testing.T) { t.Run("should fail when trying to lock zero amount", func(t *testing.T) { // Check lock with zero amount. - err = ts.fungibleKeeper.LockZRC20(ts.ctx, zrc20ABI, ts.zrc20Address, locker, owner, locker, big.NewInt(0)) + err = ts.fungibleKeeper.LockZRC20(ts.ctx, ts.zrc20Address, locker, owner, locker, big.NewInt(0)) require.Error(t, err) require.ErrorIs(t, err, fungibletypes.ErrInvalidAmount) }) - t.Run("should fail when ZRC20 ABI is not properly initialized", func(t *testing.T) { - // Check lock with nil ABI. - err = ts.fungibleKeeper.LockZRC20(ts.ctx, nil, ts.zrc20Address, locker, owner, locker, big.NewInt(10)) - require.Error(t, err) - require.ErrorIs(t, err, fungibletypes.ErrZRC20NilABI) - }) - t.Run("should fail when trying to lock a zero address ZRC20", func(t *testing.T) { // Check lock with ZRC20 zero address. - err = ts.fungibleKeeper.LockZRC20(ts.ctx, zrc20ABI, common.Address{}, locker, owner, locker, big.NewInt(10)) + err = ts.fungibleKeeper.LockZRC20(ts.ctx, common.Address{}, locker, owner, locker, big.NewInt(10)) require.Error(t, err) require.ErrorIs(t, err, fungibletypes.ErrZRC20ZeroAddress) }) t.Run("should fail when trying to lock a non whitelisted ZRC20", func(t *testing.T) { // Check lock with non whitelisted ZRC20. - err = ts.fungibleKeeper.LockZRC20(ts.ctx, zrc20ABI, sample.EthAddress(), locker, owner, locker, big.NewInt(10)) + err = ts.fungibleKeeper.LockZRC20(ts.ctx, sample.EthAddress(), locker, owner, locker, big.NewInt(10)) require.Error(t, err) require.ErrorIs(t, err, fungibletypes.ErrZRC20NotWhiteListed) }) @@ -70,7 +63,6 @@ func Test_LockZRC20(t *testing.T) { // Check lock with higher amount than totalSupply. err = ts.fungibleKeeper.LockZRC20( ts.ctx, - zrc20ABI, ts.zrc20Address, locker, owner, @@ -85,7 +77,7 @@ func Test_LockZRC20(t *testing.T) { approveAllowance(t, ts, zrc20ABI, owner, locker, big.NewInt(1001)) // Check allowance smaller, equal and bigger than the amount. - err = ts.fungibleKeeper.LockZRC20(ts.ctx, zrc20ABI, ts.zrc20Address, locker, owner, locker, big.NewInt(1001)) + err = ts.fungibleKeeper.LockZRC20(ts.ctx, ts.zrc20Address, locker, owner, locker, big.NewInt(1001)) require.Error(t, err) // We do not check in LockZRC20 explicitly if the amount is bigger than the balance. @@ -97,7 +89,7 @@ func Test_LockZRC20(t *testing.T) { approveAllowance(t, ts, zrc20ABI, owner, locker, allowanceTotal) // Check allowance smaller, equal and bigger than the amount. - err = ts.fungibleKeeper.LockZRC20(ts.ctx, zrc20ABI, ts.zrc20Address, locker, owner, locker, higherThanAllowance) + err = ts.fungibleKeeper.LockZRC20(ts.ctx, ts.zrc20Address, locker, owner, locker, higherThanAllowance) require.Error(t, err) require.Contains(t, err.Error(), "invalid allowance, got 100") }) @@ -105,14 +97,14 @@ func Test_LockZRC20(t *testing.T) { t.Run("should pass when trying to lock a valid approved amount", func(t *testing.T) { approveAllowance(t, ts, zrc20ABI, owner, locker, allowanceTotal) - err = ts.fungibleKeeper.LockZRC20(ts.ctx, zrc20ABI, ts.zrc20Address, locker, owner, locker, allowanceTotal) + err = ts.fungibleKeeper.LockZRC20(ts.ctx, ts.zrc20Address, locker, owner, locker, allowanceTotal) require.NoError(t, err) - ownerBalance, err := ts.fungibleKeeper.ZRC20BalanceOf(ts.ctx, zrc20ABI, ts.zrc20Address, owner) + ownerBalance, err := ts.fungibleKeeper.ZRC20BalanceOf(ts.ctx, ts.zrc20Address, owner) require.NoError(t, err) require.Equal(t, uint64(900), ownerBalance.Uint64()) - lockerBalance, err := ts.fungibleKeeper.ZRC20BalanceOf(ts.ctx, zrc20ABI, ts.zrc20Address, locker) + lockerBalance, err := ts.fungibleKeeper.ZRC20BalanceOf(ts.ctx, ts.zrc20Address, locker) require.NoError(t, err) require.Equal(t, uint64(100), lockerBalance.Uint64()) }) @@ -122,7 +114,6 @@ func Test_LockZRC20(t *testing.T) { err = ts.fungibleKeeper.LockZRC20( ts.ctx, - zrc20ABI, ts.zrc20Address, locker, owner, @@ -132,11 +123,11 @@ func Test_LockZRC20(t *testing.T) { require.NoError(t, err) // Note that balances are cumulative for all tests. That's why we check 801 and 199 here. - ownerBalance, err := ts.fungibleKeeper.ZRC20BalanceOf(ts.ctx, zrc20ABI, ts.zrc20Address, owner) + ownerBalance, err := ts.fungibleKeeper.ZRC20BalanceOf(ts.ctx, ts.zrc20Address, owner) require.NoError(t, err) require.Equal(t, uint64(801), ownerBalance.Uint64()) - lockerBalance, err := ts.fungibleKeeper.ZRC20BalanceOf(ts.ctx, zrc20ABI, ts.zrc20Address, locker) + lockerBalance, err := ts.fungibleKeeper.ZRC20BalanceOf(ts.ctx, ts.zrc20Address, locker) require.NoError(t, err) require.Equal(t, uint64(199), lockerBalance.Uint64()) }) @@ -164,48 +155,42 @@ func Test_UnlockZRC20(t *testing.T) { approveAllowance(t, ts, zrc20ABI, owner, locker, allowanceTotal) // Lock 100 ZRC20. - err = ts.fungibleKeeper.LockZRC20(ts.ctx, zrc20ABI, ts.zrc20Address, locker, owner, locker, allowanceTotal) + err = ts.fungibleKeeper.LockZRC20(ts.ctx, ts.zrc20Address, locker, owner, locker, allowanceTotal) require.NoError(t, err) t.Run("should fail when trying to unlock zero amount", func(t *testing.T) { - err = ts.fungibleKeeper.UnlockZRC20(ts.ctx, zrc20ABI, ts.zrc20Address, owner, locker, big.NewInt(0)) + err = ts.fungibleKeeper.UnlockZRC20(ts.ctx, ts.zrc20Address, owner, locker, big.NewInt(0)) require.Error(t, err) require.ErrorIs(t, err, fungibletypes.ErrInvalidAmount) }) - t.Run("should fail when ZRC20 ABI is not properly initialized", func(t *testing.T) { - err = ts.fungibleKeeper.UnlockZRC20(ts.ctx, nil, ts.zrc20Address, owner, locker, big.NewInt(10)) - require.Error(t, err) - require.ErrorIs(t, err, fungibletypes.ErrZRC20NilABI) - }) - t.Run("should fail when trying to unlock a zero address ZRC20", func(t *testing.T) { - err = ts.fungibleKeeper.UnlockZRC20(ts.ctx, zrc20ABI, common.Address{}, owner, locker, big.NewInt(10)) + err = ts.fungibleKeeper.UnlockZRC20(ts.ctx, common.Address{}, owner, locker, big.NewInt(10)) require.Error(t, err) require.ErrorIs(t, err, fungibletypes.ErrZRC20ZeroAddress) }) t.Run("should fail when trying to unlock a non whitelisted ZRC20", func(t *testing.T) { - err = ts.fungibleKeeper.UnlockZRC20(ts.ctx, zrc20ABI, sample.EthAddress(), owner, locker, big.NewInt(10)) + err = ts.fungibleKeeper.UnlockZRC20(ts.ctx, sample.EthAddress(), owner, locker, big.NewInt(10)) require.Error(t, err) require.ErrorIs(t, err, fungibletypes.ErrZRC20NotWhiteListed) }) t.Run("should fail when trying to unlock an amount bigger than locker's balance", func(t *testing.T) { - err = ts.fungibleKeeper.UnlockZRC20(ts.ctx, zrc20ABI, ts.zrc20Address, owner, locker, big.NewInt(1001)) + err = ts.fungibleKeeper.UnlockZRC20(ts.ctx, ts.zrc20Address, owner, locker, big.NewInt(1001)) require.Error(t, err) require.Contains(t, err.Error(), "invalid balance, got 100") }) t.Run("should pass when trying to unlock a correct amount", func(t *testing.T) { - err = ts.fungibleKeeper.UnlockZRC20(ts.ctx, zrc20ABI, ts.zrc20Address, owner, locker, allowanceTotal) + err = ts.fungibleKeeper.UnlockZRC20(ts.ctx, ts.zrc20Address, owner, locker, allowanceTotal) require.NoError(t, err) - ownerBalance, err := ts.fungibleKeeper.ZRC20BalanceOf(ts.ctx, zrc20ABI, ts.zrc20Address, owner) + ownerBalance, err := ts.fungibleKeeper.ZRC20BalanceOf(ts.ctx, ts.zrc20Address, owner) require.NoError(t, err) require.Equal(t, uint64(1000), ownerBalance.Uint64()) - lockerBalance, err := ts.fungibleKeeper.ZRC20BalanceOf(ts.ctx, zrc20ABI, ts.zrc20Address, locker) + lockerBalance, err := ts.fungibleKeeper.ZRC20BalanceOf(ts.ctx, ts.zrc20Address, locker) require.NoError(t, err) require.Equal(t, uint64(0), lockerBalance.Uint64()) }) @@ -232,13 +217,13 @@ func Test_CheckZRC20Allowance(t *testing.T) { ts.fungibleKeeper.DepositZRC20(ts.ctx, ts.zrc20Address, fungibletypes.ModuleAddressEVM, depositTotal) t.Run("should fail when checking zero amount", func(t *testing.T) { - err = ts.fungibleKeeper.CheckZRC20Allowance(ts.ctx, zrc20ABI, owner, spender, ts.zrc20Address, big.NewInt(0)) + err = ts.fungibleKeeper.CheckZRC20Allowance(ts.ctx, owner, spender, ts.zrc20Address, big.NewInt(0)) require.Error(t, err) require.ErrorAs(t, err, &fungibletypes.ErrInvalidAmount) }) t.Run("should fail when allowance is not approved", func(t *testing.T) { - err = ts.fungibleKeeper.CheckZRC20Allowance(ts.ctx, zrc20ABI, owner, spender, ts.zrc20Address, big.NewInt(10)) + err = ts.fungibleKeeper.CheckZRC20Allowance(ts.ctx, owner, spender, ts.zrc20Address, big.NewInt(10)) require.Error(t, err) require.Contains(t, err.Error(), "invalid allowance, got 0") }) @@ -248,7 +233,6 @@ func Test_CheckZRC20Allowance(t *testing.T) { err = ts.fungibleKeeper.CheckZRC20Allowance( ts.ctx, - zrc20ABI, owner, spender, ts.zrc20Address, @@ -261,7 +245,7 @@ func Test_CheckZRC20Allowance(t *testing.T) { t.Run("should pass when checking the same amount as approved", func(t *testing.T) { approveAllowance(t, ts, zrc20ABI, owner, spender, allowanceTotal) - err = ts.fungibleKeeper.CheckZRC20Allowance(ts.ctx, zrc20ABI, owner, spender, ts.zrc20Address, allowanceTotal) + err = ts.fungibleKeeper.CheckZRC20Allowance(ts.ctx, owner, spender, ts.zrc20Address, allowanceTotal) require.NoError(t, err) }) @@ -270,7 +254,6 @@ func Test_CheckZRC20Allowance(t *testing.T) { err = ts.fungibleKeeper.CheckZRC20Allowance( ts.ctx, - zrc20ABI, owner, spender, ts.zrc20Address, diff --git a/x/fungible/keeper/zrc20_cosmos_coins_mapping.go b/x/fungible/keeper/zrc20_cosmos_coins_mapping.go index f7d152f749..ddf86c7531 100644 --- a/x/fungible/keeper/zrc20_cosmos_coins_mapping.go +++ b/x/fungible/keeper/zrc20_cosmos_coins_mapping.go @@ -6,7 +6,6 @@ import ( "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/zeta-chain/node/pkg/crypto" @@ -19,26 +18,26 @@ import ( // it has to be implemented by the caller of this function. func (k Keeper) LockZRC20( ctx sdk.Context, - zrc20ABI *abi.ABI, zrc20Address, spender, owner, locker common.Address, amount *big.Int, ) error { // owner is the EOA owner of the ZRC20 tokens. + // spender is the EOA allowed to spend ZRC20 on owner's behalf. // locker is the address that will lock the ZRC20 tokens, i.e: bank precompile. - if err := k.CheckZRC20Allowance(ctx, zrc20ABI, owner, locker, zrc20Address, amount); err != nil { + if err := k.CheckZRC20Allowance(ctx, owner, spender, zrc20Address, amount); err != nil { return errors.Wrap(err, "failed allowance check") } // Check amount_to_be_locked <= total_erc20_balance - already_locked // Max amount of ZRC20 tokens that exists in zEVM are the total supply. - totalSupply, err := k.ZRC20TotalSupply(ctx, zrc20ABI, zrc20Address) + totalSupply, err := k.ZRC20TotalSupply(ctx, zrc20Address) if err != nil { return errors.Wrap(err, "failed totalSupply check") } // The alreadyLocked amount is the amount of ZRC20 tokens that have been locked by the locker. // TODO: Implement list of whitelisted locker addresses (https://github.com/zeta-chain/node/issues/2991) - alreadyLocked, err := k.ZRC20BalanceOf(ctx, zrc20ABI, zrc20Address, locker) + alreadyLocked, err := k.ZRC20BalanceOf(ctx, zrc20Address, locker) if err != nil { return errors.Wrap(err, "failed getting the ZRC20 already locked amount") } @@ -49,7 +48,7 @@ func (k Keeper) LockZRC20( // Initiate a transferFrom the owner to the locker. This will lock the ZRC20 tokens. // locker has to initiate the transaction and have enough allowance from owner. - transferred, err := k.ZRC20TransferFrom(ctx, zrc20ABI, zrc20Address, spender, owner, locker, amount) + transferred, err := k.ZRC20TransferFrom(ctx, zrc20Address, spender, owner, locker, amount) if err != nil { return errors.Wrap(err, "failed executing transferFrom") } @@ -66,17 +65,16 @@ func (k Keeper) LockZRC20( // the owner has enough collateral (cosmos coins) to be exchanged (burnt) for the ZRC20 tokens. func (k Keeper) UnlockZRC20( ctx sdk.Context, - zrc20ABI *abi.ABI, zrc20Address, owner, locker common.Address, amount *big.Int, ) error { // Check if the account locking the ZRC20 tokens has enough balance. - if err := k.CheckZRC20Balance(ctx, zrc20ABI, zrc20Address, locker, amount); err != nil { + if err := k.CheckZRC20Balance(ctx, zrc20Address, locker, amount); err != nil { return errors.Wrap(err, "failed balance check") } // transfer from the EOA locking the assets to the owner. - transferred, err := k.ZRC20Transfer(ctx, zrc20ABI, zrc20Address, locker, owner, amount) + transferred, err := k.ZRC20Transfer(ctx, zrc20Address, locker, owner, amount) if err != nil { return errors.Wrap(err, "failed executing transfer") } @@ -92,14 +90,9 @@ func (k Keeper) UnlockZRC20( // is equal or greater than the provided amount. func (k Keeper) CheckZRC20Allowance( ctx sdk.Context, - zrc20ABI *abi.ABI, owner, spender, zrc20Address common.Address, amount *big.Int, ) error { - if zrc20ABI == nil { - return fungibletypes.ErrZRC20NilABI - } - if amount.Sign() <= 0 || amount == nil { return fungibletypes.ErrInvalidAmount } @@ -112,7 +105,7 @@ func (k Keeper) CheckZRC20Allowance( return errors.Wrap(err, "ZRC20 is not valid") } - allowanceValue, err := k.ZRC20Allowance(ctx, zrc20ABI, zrc20Address, owner, spender) + allowanceValue, err := k.ZRC20Allowance(ctx, zrc20Address, owner, spender) if err != nil { return errors.Wrap(err, "failed while checking spender's allowance") } @@ -128,14 +121,9 @@ func (k Keeper) CheckZRC20Allowance( // is equal or greater than the provided amount. func (k Keeper) CheckZRC20Balance( ctx sdk.Context, - zrc20ABI *abi.ABI, zrc20Address, owner common.Address, amount *big.Int, ) error { - if zrc20ABI == nil { - return fungibletypes.ErrZRC20NilABI - } - if amount.Sign() <= 0 || amount == nil { return fungibletypes.ErrInvalidAmount } @@ -150,7 +138,7 @@ func (k Keeper) CheckZRC20Balance( // Check the ZRC20 balance of a given account. // function balanceOf(address account) - balance, err := k.ZRC20BalanceOf(ctx, zrc20ABI, zrc20Address, owner) + balance, err := k.ZRC20BalanceOf(ctx, zrc20Address, owner) if err != nil { return errors.Wrap(err, "failed getting owner's ZRC20 balance") } diff --git a/x/fungible/keeper/zrc20_methods.go b/x/fungible/keeper/zrc20_methods.go index 5c9f2d4645..4d7c7f5174 100644 --- a/x/fungible/keeper/zrc20_methods.go +++ b/x/fungible/keeper/zrc20_methods.go @@ -6,8 +6,8 @@ import ( "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" + "github.com/zeta-chain/protocol-contracts/v2/pkg/zrc20.sol" "github.com/zeta-chain/node/pkg/crypto" fungibletypes "github.com/zeta-chain/node/x/fungible/types" @@ -25,11 +25,11 @@ const ( // The allowance has to be previously approved by the ZRC20 tokens owner. func (k Keeper) ZRC20Allowance( ctx sdk.Context, - zrc20ABI *abi.ABI, zrc20Address, owner, spender common.Address, ) (*big.Int, error) { - if zrc20ABI == nil { - return nil, fungibletypes.ErrZRC20NilABI + zrc20ABI, err := zrc20.ZRC20MetaData.GetAbi() + if err != nil { + return nil, err } if crypto.IsEmptyAddress(owner) || crypto.IsEmptyAddress(spender) { @@ -82,11 +82,11 @@ func (k Keeper) ZRC20Allowance( // ZRC20BalanceOf checks the ZRC20 balance of a given EOA. func (k Keeper) ZRC20BalanceOf( ctx sdk.Context, - zrc20ABI *abi.ABI, zrc20Address, owner common.Address, ) (*big.Int, error) { - if zrc20ABI == nil { - return nil, fungibletypes.ErrZRC20NilABI + zrc20ABI, err := zrc20.ZRC20MetaData.GetAbi() + if err != nil { + return nil, err } if crypto.IsEmptyAddress(owner) { @@ -138,11 +138,11 @@ func (k Keeper) ZRC20BalanceOf( // ZRC20TotalSupply returns the total supply of a ZRC20 token. func (k Keeper) ZRC20TotalSupply( ctx sdk.Context, - zrc20ABI *abi.ABI, zrc20Address common.Address, ) (*big.Int, error) { - if zrc20ABI == nil { - return nil, fungibletypes.ErrZRC20NilABI + zrc20ABI, err := zrc20.ZRC20MetaData.GetAbi() + if err != nil { + return nil, err } if err := k.IsValidZRC20(ctx, zrc20Address); err != nil { @@ -189,12 +189,12 @@ func (k Keeper) ZRC20TotalSupply( // ZRC20Transfer transfers ZRC20 tokens from the sender to the recipient. func (k Keeper) ZRC20Transfer( ctx sdk.Context, - zrc20ABI *abi.ABI, zrc20Address, from, to common.Address, amount *big.Int, ) (bool, error) { - if zrc20ABI == nil { - return false, fungibletypes.ErrZRC20NilABI + zrc20ABI, err := zrc20.ZRC20MetaData.GetAbi() + if err != nil { + return false, err } if crypto.IsEmptyAddress(from) || crypto.IsEmptyAddress(to) { @@ -249,12 +249,12 @@ func (k Keeper) ZRC20Transfer( // Requisite: the original EOA must have approved the spender to spend the tokens. func (k Keeper) ZRC20TransferFrom( ctx sdk.Context, - zrc20ABI *abi.ABI, zrc20Address, spender, from, to common.Address, amount *big.Int, ) (bool, error) { - if zrc20ABI == nil { - return false, fungibletypes.ErrZRC20NilABI + zrc20ABI, err := zrc20.ZRC20MetaData.GetAbi() + if err != nil { + return false, err } if crypto.IsEmptyAddress(from) || crypto.IsEmptyAddress(to) || crypto.IsEmptyAddress(spender) { diff --git a/x/fungible/keeper/zrc20_methods_test.go b/x/fungible/keeper/zrc20_methods_test.go index 7b124f3050..3d64a05928 100644 --- a/x/fungible/keeper/zrc20_methods_test.go +++ b/x/fungible/keeper/zrc20_methods_test.go @@ -14,23 +14,11 @@ import ( ) func Test_ZRC20Allowance(t *testing.T) { - // Instantiate the ZRC20 ABI only one time. - // This avoids instantiating it every time deposit or withdraw are called. - zrc20ABI, err := zrc20.ZRC20MetaData.GetAbi() - require.NoError(t, err) - ts := setupChain(t) - t.Run("should fail when ZRC20ABI is nil", func(t *testing.T) { - _, err := ts.fungibleKeeper.ZRC20Allowance(ts.ctx, nil, ts.zrc20Address, common.Address{}, common.Address{}) - require.Error(t, err) - require.ErrorAs(t, err, &fungibletypes.ErrZRC20NilABI) - }) - t.Run("should fail when owner is zero address", func(t *testing.T) { _, err := ts.fungibleKeeper.ZRC20Allowance( ts.ctx, - zrc20ABI, ts.zrc20Address, common.Address{}, sample.EthAddress(), @@ -42,7 +30,6 @@ func Test_ZRC20Allowance(t *testing.T) { t.Run("should fail when spender is zero address", func(t *testing.T) { _, err := ts.fungibleKeeper.ZRC20Allowance( ts.ctx, - zrc20ABI, ts.zrc20Address, sample.EthAddress(), common.Address{}, @@ -54,7 +41,6 @@ func Test_ZRC20Allowance(t *testing.T) { t.Run("should fail when zrc20 address is zero address", func(t *testing.T) { _, err := ts.fungibleKeeper.ZRC20Allowance( ts.ctx, - zrc20ABI, common.Address{}, sample.EthAddress(), fungibletypes.ModuleAddressEVM, @@ -66,7 +52,6 @@ func Test_ZRC20Allowance(t *testing.T) { t.Run("should pass with correct input", func(t *testing.T) { allowance, err := ts.fungibleKeeper.ZRC20Allowance( ts.ctx, - zrc20ABI, ts.zrc20Address, fungibletypes.ModuleAddressEVM, sample.EthAddress(), @@ -77,27 +62,16 @@ func Test_ZRC20Allowance(t *testing.T) { } func Test_ZRC20BalanceOf(t *testing.T) { - // Instantiate the ZRC20 ABI only one time. - // This avoids instantiating it every time deposit or withdraw are called. - zrc20ABI, err := zrc20.ZRC20MetaData.GetAbi() - require.NoError(t, err) - ts := setupChain(t) - t.Run("should fail when ZRC20ABI is nil", func(t *testing.T) { - _, err := ts.fungibleKeeper.ZRC20BalanceOf(ts.ctx, nil, ts.zrc20Address, common.Address{}) - require.Error(t, err) - require.ErrorAs(t, err, &fungibletypes.ErrZRC20NilABI) - }) - t.Run("should fail when owner is zero address", func(t *testing.T) { - _, err := ts.fungibleKeeper.ZRC20BalanceOf(ts.ctx, zrc20ABI, ts.zrc20Address, common.Address{}) + _, err := ts.fungibleKeeper.ZRC20BalanceOf(ts.ctx, ts.zrc20Address, common.Address{}) require.Error(t, err) require.ErrorAs(t, err, &fungibletypes.ErrZeroAddress) }) t.Run("should fail when zrc20 address is zero address", func(t *testing.T) { - _, err := ts.fungibleKeeper.ZRC20BalanceOf(ts.ctx, zrc20ABI, common.Address{}, sample.EthAddress()) + _, err := ts.fungibleKeeper.ZRC20BalanceOf(ts.ctx, common.Address{}, sample.EthAddress()) require.Error(t, err) require.ErrorAs(t, err, &fungibletypes.ErrZRC20ZeroAddress) }) @@ -105,7 +79,6 @@ func Test_ZRC20BalanceOf(t *testing.T) { t.Run("should pass with correct input", func(t *testing.T) { balance, err := ts.fungibleKeeper.ZRC20BalanceOf( ts.ctx, - zrc20ABI, ts.zrc20Address, fungibletypes.ModuleAddressEVM, ) @@ -115,61 +88,31 @@ func Test_ZRC20BalanceOf(t *testing.T) { } func Test_ZRC20TotalSupply(t *testing.T) { - // Instantiate the ZRC20 ABI only one time. - // This avoids instantiating it every time deposit or withdraw are called. - zrc20ABI, err := zrc20.ZRC20MetaData.GetAbi() - require.NoError(t, err) - ts := setupChain(t) - t.Run("should fail when ZRC20ABI is nil", func(t *testing.T) { - _, err := ts.fungibleKeeper.ZRC20TotalSupply(ts.ctx, nil, ts.zrc20Address) - require.Error(t, err) - require.ErrorAs(t, err, &fungibletypes.ErrZRC20NilABI) - }) - t.Run("should fail when zrc20 address is zero address", func(t *testing.T) { - _, err := ts.fungibleKeeper.ZRC20TotalSupply(ts.ctx, zrc20ABI, common.Address{}) + _, err := ts.fungibleKeeper.ZRC20TotalSupply(ts.ctx, common.Address{}) require.Error(t, err) require.ErrorAs(t, err, &fungibletypes.ErrZRC20ZeroAddress) }) t.Run("should pass with correct input", func(t *testing.T) { - totalSupply, err := ts.fungibleKeeper.ZRC20TotalSupply(ts.ctx, zrc20ABI, ts.zrc20Address) + totalSupply, err := ts.fungibleKeeper.ZRC20TotalSupply(ts.ctx, ts.zrc20Address) require.NoError(t, err) require.Equal(t, uint64(10000000), totalSupply.Uint64()) }) } func Test_ZRC20Transfer(t *testing.T) { - // Instantiate the ZRC20 ABI only one time. - // This avoids instantiating it every time deposit or withdraw are called. - zrc20ABI, err := zrc20.ZRC20MetaData.GetAbi() - require.NoError(t, err) - ts := setupChain(t) // Make sure sample.EthAddress() exists as an ethermint account in state. accAddress := sdk.AccAddress(sample.EthAddress().Bytes()) ts.fungibleKeeper.GetAuthKeeper().SetAccount(ts.ctx, authtypes.NewBaseAccount(accAddress, nil, 0, 0)) - t.Run("should fail when ZRC20ABI is nil", func(t *testing.T) { - _, err := ts.fungibleKeeper.ZRC20Transfer( - ts.ctx, - nil, - ts.zrc20Address, - common.Address{}, - common.Address{}, - big.NewInt(0), - ) - require.Error(t, err) - require.ErrorAs(t, err, &fungibletypes.ErrZRC20NilABI) - }) - t.Run("should fail when owner is zero address", func(t *testing.T) { _, err := ts.fungibleKeeper.ZRC20Transfer( ts.ctx, - zrc20ABI, ts.zrc20Address, common.Address{}, sample.EthAddress(), @@ -182,7 +125,6 @@ func Test_ZRC20Transfer(t *testing.T) { t.Run("should fail when spender is zero address", func(t *testing.T) { _, err := ts.fungibleKeeper.ZRC20Transfer( ts.ctx, - zrc20ABI, ts.zrc20Address, sample.EthAddress(), common.Address{}, @@ -195,7 +137,6 @@ func Test_ZRC20Transfer(t *testing.T) { t.Run("should fail when zrc20 address is zero address", func(t *testing.T) { _, err := ts.fungibleKeeper.ZRC20Transfer( ts.ctx, - zrc20ABI, common.Address{}, sample.EthAddress(), fungibletypes.ModuleAddressEVM, @@ -209,7 +150,6 @@ func Test_ZRC20Transfer(t *testing.T) { ts.fungibleKeeper.DepositZRC20(ts.ctx, ts.zrc20Address, fungibletypes.ModuleAddressEVM, big.NewInt(10)) transferred, err := ts.fungibleKeeper.ZRC20Transfer( ts.ctx, - zrc20ABI, ts.zrc20Address, fungibletypes.ModuleAddressEVM, sample.EthAddress(), @@ -232,24 +172,9 @@ func Test_ZRC20TransferFrom(t *testing.T) { accAddress := sdk.AccAddress(sample.EthAddress().Bytes()) ts.fungibleKeeper.GetAuthKeeper().SetAccount(ts.ctx, authtypes.NewBaseAccount(accAddress, nil, 0, 0)) - t.Run("should fail when ZRC20ABI is nil", func(t *testing.T) { - _, err := ts.fungibleKeeper.ZRC20TransferFrom( - ts.ctx, - nil, - ts.zrc20Address, - common.Address{}, - common.Address{}, - common.Address{}, - big.NewInt(0), - ) - require.Error(t, err) - require.ErrorAs(t, err, &fungibletypes.ErrZRC20NilABI) - }) - t.Run("should fail when from is zero address", func(t *testing.T) { _, err := ts.fungibleKeeper.ZRC20TransferFrom( ts.ctx, - zrc20ABI, ts.zrc20Address, sample.EthAddress(), common.Address{}, @@ -263,7 +188,6 @@ func Test_ZRC20TransferFrom(t *testing.T) { t.Run("should fail when to is zero address", func(t *testing.T) { _, err := ts.fungibleKeeper.ZRC20TransferFrom( ts.ctx, - zrc20ABI, ts.zrc20Address, sample.EthAddress(), sample.EthAddress(), @@ -277,7 +201,6 @@ func Test_ZRC20TransferFrom(t *testing.T) { t.Run("should fail when spender is zero address", func(t *testing.T) { _, err := ts.fungibleKeeper.ZRC20TransferFrom( ts.ctx, - zrc20ABI, ts.zrc20Address, common.Address{}, sample.EthAddress(), @@ -291,7 +214,6 @@ func Test_ZRC20TransferFrom(t *testing.T) { t.Run("should fail when zrc20 address is zero address", func(t *testing.T) { _, err := ts.fungibleKeeper.ZRC20TransferFrom( ts.ctx, - zrc20ABI, common.Address{}, sample.EthAddress(), sample.EthAddress(), @@ -309,7 +231,6 @@ func Test_ZRC20TransferFrom(t *testing.T) { // Transferring the tokens with transferFrom without approval should fail. _, err = ts.fungibleKeeper.ZRC20TransferFrom( ts.ctx, - zrc20ABI, ts.zrc20Address, fungibletypes.ModuleAddressEVM, sample.EthAddress(), @@ -329,7 +250,6 @@ func Test_ZRC20TransferFrom(t *testing.T) { // Transferring the tokens with transferFrom without approval should fail. _, err = ts.fungibleKeeper.ZRC20TransferFrom( ts.ctx, - zrc20ABI, ts.zrc20Address, fungibletypes.ModuleAddressEVM, sample.EthAddress(), diff --git a/x/fungible/types/errors.go b/x/fungible/types/errors.go index cf333c9545..cb152f7ffe 100644 --- a/x/fungible/types/errors.go +++ b/x/fungible/types/errors.go @@ -34,4 +34,5 @@ var ( ErrZRC20NilABI = cosmoserrors.Register(ModuleName, 1132, "ZRC20 ABI is nil") ErrZeroAddress = cosmoserrors.Register(ModuleName, 1133, "address cannot be zero") ErrInvalidAmount = cosmoserrors.Register(ModuleName, 1134, "invalid amount") + ErrMaxSupplyReached = cosmoserrors.Register(ModuleName, 1135, "max supply reached") ) diff --git a/x/fungible/types/expected_keepers.go b/x/fungible/types/expected_keepers.go index fe8edf21ce..dd5d5d30ec 100644 --- a/x/fungible/types/expected_keepers.go +++ b/x/fungible/types/expected_keepers.go @@ -35,6 +35,7 @@ type BankKeeper interface { ) error MintCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error SpendableCoins(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins + GetSupply(ctx sdk.Context, denom string) sdk.Coin } type ObserverKeeper interface { @@ -51,7 +52,7 @@ type EVMKeeper interface { EstimateGas(c context.Context, req *evmtypes.EthCallRequest) (*evmtypes.EstimateGasResponse, error) ApplyMessage( ctx sdk.Context, - msg core.Message, + msg *core.Message, tracer vm.EVMLogger, commit bool, ) (*evmtypes.MsgEthereumTxResponse, error) diff --git a/zetaclient/chains/base/observer.go b/zetaclient/chains/base/observer.go index f089f26815..67f38e627a 100644 --- a/zetaclient/chains/base/observer.go +++ b/zetaclient/chains/base/observer.go @@ -506,21 +506,25 @@ func (ob *Observer) PostVoteInbound( // AlertOnRPCLatency prints an alert if the RPC latency exceeds the threshold. // Returns true if the RPC latency is too high. func (ob *Observer) AlertOnRPCLatency(latestBlockTime time.Time, defaultAlertLatency time.Duration) bool { - // use configured alert latency if set - alertLatency := defaultAlertLatency - if ob.rpcAlertLatency > 0 { - alertLatency = ob.rpcAlertLatency + elapsedTime := time.Since(latestBlockTime) + + alertLatency := ob.rpcAlertLatency + if alertLatency == 0 { + alertLatency = defaultAlertLatency + } + + lf := map[string]any{ + "rpc_latency_alert_ms": alertLatency.Milliseconds(), + "rpc_latency_real_ms": elapsedTime.Milliseconds(), } - // latest block should not be too old - elapsedTime := time.Since(latestBlockTime) if elapsedTime > alertLatency { - ob.logger.Chain.Error(). - Msgf("RPC is stale: latest block is %.0f seconds old, RPC down or chain stuck (check explorer)?", elapsedTime.Seconds()) + ob.logger.Chain.Error().Fields(lf).Msg("RPC latency is too high, please check the node or explorer") return true } - ob.logger.Chain.Info().Msgf("RPC is OK: latest block is %.0f seconds old", elapsedTime.Seconds()) + ob.logger.Chain.Info().Fields(lf).Msg("RPC latency is OK") + return false } diff --git a/zetaclient/chains/bitcoin/fee.go b/zetaclient/chains/bitcoin/fee.go index 58297f37d6..f56a479364 100644 --- a/zetaclient/chains/bitcoin/fee.go +++ b/zetaclient/chains/bitcoin/fee.go @@ -13,7 +13,6 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/pkg/errors" - "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/zetaclient/chains/bitcoin/rpc" "github.com/zeta-chain/node/zetaclient/chains/interfaces" clientcommon "github.com/zeta-chain/node/zetaclient/common" @@ -104,7 +103,7 @@ func EstimateOutboundSize(numInputs uint64, payees []btcutil.Address) (uint64, e // GetOutputSizeByAddress returns the size of a tx output in bytes by the given address func GetOutputSizeByAddress(to btcutil.Address) (uint64, error) { switch addr := to.(type) { - case *chains.AddressTaproot: + case *btcutil.AddressTaproot: if addr == nil { return 0, nil } diff --git a/zetaclient/chains/bitcoin/fee_test.go b/zetaclient/chains/bitcoin/fee_test.go index 8b1f54e5d5..82f60ff0ef 100644 --- a/zetaclient/chains/bitcoin/fee_test.go +++ b/zetaclient/chains/bitcoin/fee_test.go @@ -66,7 +66,7 @@ func generateKeyPair(t *testing.T, net *chaincfg.Params) (*btcec.PrivateKey, btc addr, err := btcutil.NewAddressWitnessPubKeyHash(pubKeyHash, net) require.NoError(t, err) //fmt.Printf("New address: %s\n", addr.EncodeAddress()) - pkScript, err := PayToAddrScript(addr) + pkScript, err := txscript.PayToAddrScript(addr) require.NoError(t, err) return privateKey, addr, pkScript } @@ -83,7 +83,7 @@ func getTestAddrScript(t *testing.T, scriptType string) btcutil.Address { // createPkScripts creates 10 random amount of scripts to the given address 'to' func createPkScripts(t *testing.T, to btcutil.Address, repeat int) ([]btcutil.Address, [][]byte) { - pkScript, err := PayToAddrScript(to) + pkScript, err := txscript.PayToAddrScript(to) require.NoError(t, err) addrs := []btcutil.Address{} @@ -261,7 +261,7 @@ func TestOutboundSizeXIn3Out(t *testing.T) { func TestGetOutputSizeByAddress(t *testing.T) { // test nil P2TR address and non-nil P2TR address - nilP2TR := (*chains.AddressTaproot)(nil) + nilP2TR := (*btcutil.AddressTaproot)(nil) sizeNilP2TR, err := GetOutputSizeByAddress(nilP2TR) require.NoError(t, err) require.Equal(t, uint64(0), sizeNilP2TR) diff --git a/zetaclient/chains/bitcoin/observer/event.go b/zetaclient/chains/bitcoin/observer/event.go new file mode 100644 index 0000000000..69657d29f1 --- /dev/null +++ b/zetaclient/chains/bitcoin/observer/event.go @@ -0,0 +1,253 @@ +package observer + +import ( + "bytes" + "encoding/hex" + "fmt" + "math/big" + + cosmosmath "cosmossdk.io/math" + ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/pkg/errors" + + "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/pkg/coin" + "github.com/zeta-chain/node/pkg/constant" + "github.com/zeta-chain/node/pkg/crypto" + "github.com/zeta-chain/node/pkg/memo" + crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" + "github.com/zeta-chain/node/zetaclient/compliance" + "github.com/zeta-chain/node/zetaclient/config" + "github.com/zeta-chain/node/zetaclient/logs" +) + +// InboundProcessability is an enum representing the processability of an inbound +type InboundProcessability int + +const ( + // InboundProcessabilityGood represents a processable inbound + InboundProcessabilityGood InboundProcessability = iota + + // InboundProcessabilityDonation represents a donation inbound + InboundProcessabilityDonation + + // InboundProcessabilityComplianceViolation represents a compliance violation + InboundProcessabilityComplianceViolation +) + +// BTCInboundEvent represents an incoming transaction event +type BTCInboundEvent struct { + // FromAddress is the first input address + FromAddress string + + // ToAddress is the ZEVM receiver address + ToAddress string + + // Value is the amount of BTC + Value float64 + + // DepositorFee is the deposit fee + DepositorFee float64 + + // MemoBytes is the memo of inbound + MemoBytes []byte + + // MemoStd is the standard inbound memo if it can be decoded + MemoStd *memo.InboundMemo + + // BlockNumber is the block number of the inbound + BlockNumber uint64 + + // TxHash is the hash of the inbound + TxHash string +} + +// Processability returns the processability of the inbound event +func (event *BTCInboundEvent) Processability() InboundProcessability { + // compliance check on sender and receiver addresses + if config.ContainRestrictedAddress(event.FromAddress, event.ToAddress) { + return InboundProcessabilityComplianceViolation + } + + // compliance check on receiver, revert/abort addresses in standard memo + if event.MemoStd != nil { + if config.ContainRestrictedAddress( + event.MemoStd.Receiver.Hex(), + event.MemoStd.RevertOptions.RevertAddress, + event.MemoStd.RevertOptions.AbortAddress, + ) { + return InboundProcessabilityComplianceViolation + } + } + + // donation check + if bytes.Equal(event.MemoBytes, []byte(constant.DonationMessage)) { + return InboundProcessabilityDonation + } + + return InboundProcessabilityGood +} + +// DecodeMemoBytes decodes the contained memo bytes as either standard or legacy memo +func (event *BTCInboundEvent) DecodeMemoBytes(chainID int64) error { + var ( + err error + isStandardMemo bool + memoStd *memo.InboundMemo + receiver ethcommon.Address + ) + + // skip decoding donation tx as it won't go through zetacore + if bytes.Equal(event.MemoBytes, []byte(constant.DonationMessage)) { + return nil + } + + // try to decode the standard memo as the preferred format + // the standard memo is NOT enabled for Bitcoin mainnet + + if chainID != chains.BitcoinMainnet.ChainId { + memoStd, isStandardMemo, err = memo.DecodeFromBytes(event.MemoBytes) + } + + // process standard memo or fallback to legacy memo + if isStandardMemo { + // skip standard memo that carries improper data + if err != nil { + return errors.Wrap(err, "standard memo contains improper data") + } + + // validate the content of the standard memo + err = ValidateStandardMemo(*memoStd, chainID) + if err != nil { + return errors.Wrap(err, "invalid standard memo for bitcoin") + } + + event.MemoStd = memoStd + receiver = memoStd.Receiver + } else { + parsedAddress, _, err := memo.DecodeLegacyMemoHex(hex.EncodeToString(event.MemoBytes)) + if err != nil { // unreachable code + return errors.Wrap(err, "invalid legacy memo") + } + receiver = parsedAddress + } + + // ensure the receiver is valid + if crypto.IsEmptyAddress(receiver) { + return errors.New("got empty receiver address from memo") + } + event.ToAddress = receiver.Hex() + + return nil +} + +// ValidateStandardMemo validates the standard memo in Bitcoin context +func ValidateStandardMemo(memoStd memo.InboundMemo, chainID int64) error { + // NoAssetCall will be disabled for Bitcoin until full V2 support + // https://github.com/zeta-chain/node/issues/2711 + if memoStd.OpCode == memo.OpCodeCall { + return errors.New("NoAssetCall is disabled for Bitcoin") + } + + // ensure the revert address is a valid and supported BTC address + revertAddress := memoStd.RevertOptions.RevertAddress + if revertAddress != "" { + btcAddress, err := chains.DecodeBtcAddress(revertAddress, chainID) + if err != nil { + return errors.Wrapf(err, "invalid revert address in memo: %s", revertAddress) + } + if !chains.IsBtcAddressSupported(btcAddress) { + return fmt.Errorf("unsupported revert address in memo: %s", revertAddress) + } + } + + return nil +} + +// CheckEventProcessability checks if the inbound event is processable +func (ob *Observer) CheckEventProcessability(event BTCInboundEvent) bool { + // check if the event is processable + switch result := event.Processability(); result { + case InboundProcessabilityGood: + return true + case InboundProcessabilityDonation: + logFields := map[string]any{ + logs.FieldChain: ob.Chain().ChainId, + logs.FieldTx: event.TxHash, + } + ob.Logger().Inbound.Info().Fields(logFields).Msgf("thank you rich folk for your donation!") + return false + case InboundProcessabilityComplianceViolation: + compliance.PrintComplianceLog(ob.logger.Inbound, ob.logger.Compliance, + false, ob.Chain().ChainId, event.TxHash, event.FromAddress, event.ToAddress, "BTC") + return false + default: + ob.Logger().Inbound.Error().Msgf("unreachable code got InboundProcessability: %v", result) + return false + } +} + +// NewInboundVoteFromLegacyMemo creates a MsgVoteInbound message for inbound that uses legacy memo +func (ob *Observer) NewInboundVoteFromLegacyMemo( + event *BTCInboundEvent, + amountSats *big.Int, +) *crosschaintypes.MsgVoteInbound { + message := hex.EncodeToString(event.MemoBytes) + + return crosschaintypes.NewMsgVoteInbound( + ob.ZetacoreClient().GetKeys().GetOperatorAddress().String(), + event.FromAddress, + ob.Chain().ChainId, + event.FromAddress, + event.ToAddress, + ob.ZetacoreClient().Chain().ChainId, + cosmosmath.NewUintFromBigInt(amountSats), + message, + event.TxHash, + event.BlockNumber, + 0, + coin.CoinType_Gas, + "", + 0, + crosschaintypes.ProtocolContractVersion_V1, + false, // not relevant for v1 + ) +} + +// NewInboundVoteFromStdMemo creates a MsgVoteInbound message for inbound that uses standard memo +// TODO: upgrade to ProtocolContractVersion_V2 and enable more options +// https://github.com/zeta-chain/node/issues/2711 +func (ob *Observer) NewInboundVoteFromStdMemo( + event *BTCInboundEvent, + amountSats *big.Int, +) *crosschaintypes.MsgVoteInbound { + // replace 'sender' with 'revertAddress' if specified in the memo, so that + // zetacore will refund to the address specified by the user in the revert options. + sender := event.FromAddress + if event.MemoStd.RevertOptions.RevertAddress != "" { + sender = event.MemoStd.RevertOptions.RevertAddress + } + + // make a legacy message so that zetacore can process it as V1 + msgBytes := append(event.MemoStd.Receiver.Bytes(), event.MemoStd.Payload...) + message := hex.EncodeToString(msgBytes) + + return crosschaintypes.NewMsgVoteInbound( + ob.ZetacoreClient().GetKeys().GetOperatorAddress().String(), + sender, + ob.Chain().ChainId, + event.FromAddress, + event.ToAddress, + ob.ZetacoreClient().Chain().ChainId, + cosmosmath.NewUintFromBigInt(amountSats), + message, + event.TxHash, + event.BlockNumber, + 0, + coin.CoinType_Gas, + "", + 0, + crosschaintypes.ProtocolContractVersion_V1, + false, // not relevant for v1 + ) +} diff --git a/zetaclient/chains/bitcoin/observer/event_test.go b/zetaclient/chains/bitcoin/observer/event_test.go new file mode 100644 index 0000000000..5ed8e9b103 --- /dev/null +++ b/zetaclient/chains/bitcoin/observer/event_test.go @@ -0,0 +1,447 @@ +package observer_test + +import ( + "encoding/hex" + "math/big" + "testing" + + cosmosmath "cosmossdk.io/math" + "github.com/btcsuite/btcd/chaincfg" + "github.com/zeta-chain/node/testutil" + crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/pkg/coin" + "github.com/zeta-chain/node/pkg/constant" + "github.com/zeta-chain/node/pkg/memo" + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/zetaclient/chains/bitcoin/observer" + "github.com/zeta-chain/node/zetaclient/config" + "github.com/zeta-chain/node/zetaclient/keys" + "github.com/zeta-chain/node/zetaclient/testutils" + "github.com/zeta-chain/node/zetaclient/testutils/mocks" +) + +// createTestBtcEvent creates a test BTC inbound event +func createTestBtcEvent( + t *testing.T, + net *chaincfg.Params, + memo []byte, + memoStd *memo.InboundMemo, +) observer.BTCInboundEvent { + return observer.BTCInboundEvent{ + FromAddress: sample.BtcAddressP2WPKH(t, net), + ToAddress: sample.EthAddress().Hex(), + MemoBytes: memo, + MemoStd: memoStd, + TxHash: sample.Hash().Hex(), + BlockNumber: 123456, + } +} + +func Test_CheckProcessability(t *testing.T) { + // setup compliance config + cfg := config.Config{ + ComplianceConfig: sample.ComplianceConfig(), + } + config.LoadComplianceConfig(cfg) + + // test cases + tests := []struct { + name string + event *observer.BTCInboundEvent + expected observer.InboundProcessability + }{ + { + name: "should return InboundProcessabilityGood for a processable inbound event", + event: &observer.BTCInboundEvent{ + FromAddress: "tb1quhassyrlj43qar0mn0k5sufyp6mazmh2q85lr6ex8ehqfhxpzsksllwrsu", + ToAddress: testutils.TSSAddressBTCAthens3, + }, + expected: observer.InboundProcessabilityGood, + }, + { + name: "should return InboundProcessabilityComplianceViolation for a restricted sender address", + event: &observer.BTCInboundEvent{ + FromAddress: sample.RestrictedBtcAddressTest, + ToAddress: testutils.TSSAddressBTCAthens3, + }, + expected: observer.InboundProcessabilityComplianceViolation, + }, + { + name: "should return InboundProcessabilityComplianceViolation for a restricted receiver address in standard memo", + event: &observer.BTCInboundEvent{ + FromAddress: "tb1quhassyrlj43qar0mn0k5sufyp6mazmh2q85lr6ex8ehqfhxpzsksllwrsu", + ToAddress: testutils.TSSAddressBTCAthens3, + MemoStd: &memo.InboundMemo{ + FieldsV0: memo.FieldsV0{ + Receiver: common.HexToAddress(sample.RestrictedEVMAddressTest), + }, + }, + }, + expected: observer.InboundProcessabilityComplianceViolation, + }, + { + name: "should return InboundProcessabilityComplianceViolation for a restricted revert address in standard memo", + event: &observer.BTCInboundEvent{ + FromAddress: "tb1quhassyrlj43qar0mn0k5sufyp6mazmh2q85lr6ex8ehqfhxpzsksllwrsu", + ToAddress: testutils.TSSAddressBTCAthens3, + MemoStd: &memo.InboundMemo{ + FieldsV0: memo.FieldsV0{ + RevertOptions: crosschaintypes.RevertOptions{ + RevertAddress: sample.RestrictedBtcAddressTest, + }, + }, + }, + }, + expected: observer.InboundProcessabilityComplianceViolation, + }, + { + name: "should return InboundProcessabilityDonation for a donation inbound event", + event: &observer.BTCInboundEvent{ + FromAddress: "tb1quhassyrlj43qar0mn0k5sufyp6mazmh2q85lr6ex8ehqfhxpzsksllwrsu", + ToAddress: testutils.TSSAddressBTCAthens3, + MemoBytes: []byte(constant.DonationMessage), + }, + expected: observer.InboundProcessabilityDonation, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.event.Processability() + require.Equal(t, tt.expected, result) + }) + } +} + +func Test_DecodeEventMemoBytes(t *testing.T) { + // test cases + tests := []struct { + name string + chainID int64 + event *observer.BTCInboundEvent + expectedMemoStd *memo.InboundMemo + expectedReceiver common.Address + donation bool + errMsg string + }{ + { + name: "should decode standard memo bytes successfully", + chainID: chains.BitcoinTestnet.ChainId, + event: &observer.BTCInboundEvent{ + // a deposit and call + MemoBytes: testutil.HexToBytes( + t, + "5a0110032d07a9cbd57dcca3e2cf966c88bc874445b6e3b60d68656c6c6f207361746f736869", + ), + }, + expectedMemoStd: &memo.InboundMemo{ + Header: memo.Header{ + Version: 0, + EncodingFmt: memo.EncodingFmtCompactShort, + OpCode: memo.OpCodeDepositAndCall, + DataFlags: 3, // reciever + payload + }, + FieldsV0: memo.FieldsV0{ + Receiver: common.HexToAddress("0x2D07A9CBd57DCca3E2cF966C88Bc874445b6E3B6"), + Payload: []byte("hello satoshi"), + }, + }, + }, + { + name: "should fall back to legacy memo successfully", + chainID: chains.BitcoinTestnet.ChainId, + event: &observer.BTCInboundEvent{ + // raw address + payload + MemoBytes: testutil.HexToBytes(t, "2d07a9cbd57dcca3e2cf966c88bc874445b6e3b668656c6c6f207361746f736869"), + }, + expectedReceiver: common.HexToAddress("0x2D07A9CBd57DCca3E2cF966C88Bc874445b6E3B6"), + }, + { + name: "should disable standard memo for Bitcoin mainnet", + chainID: chains.BitcoinMainnet.ChainId, + event: &observer.BTCInboundEvent{ + // a deposit and call + MemoBytes: testutil.HexToBytes( + t, + "5a0110032d07a9cbd57dcca3e2cf966c88bc874445b6e3b60d68656c6c6f207361746f736869", + ), + }, + expectedReceiver: common.HexToAddress("0x5A0110032d07A9cbd57dcCa3e2Cf966c88bC8744"), + }, + { + name: "should do nothing for donation message", + chainID: chains.BitcoinTestnet.ChainId, + event: &observer.BTCInboundEvent{ + MemoBytes: []byte(constant.DonationMessage), + }, + donation: true, + }, + { + name: "should return error if standard memo contains improper data", + chainID: chains.BitcoinTestnet.ChainId, + event: &observer.BTCInboundEvent{ + // a deposit and call, receiver is empty ZEVM address + MemoBytes: testutil.HexToBytes( + t, + "5a01100300000000000000000000000000000000000000000d68656c6c6f207361746f736869", + ), + }, + errMsg: "standard memo contains improper data", + }, + { + name: "should return error if standard memo validation failed", + chainID: chains.BitcoinTestnet.ChainId, + event: &observer.BTCInboundEvent{ + // a no asset call opCode passed, not supported at the moment + MemoBytes: testutil.HexToBytes( + t, + "5a0120032d07a9cbd57dcca3e2cf966c88bc874445b6e3b60d68656c6c6f207361746f736869", + ), + }, + errMsg: "invalid standard memo for bitcoin", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.event.DecodeMemoBytes(tt.chainID) + if tt.errMsg != "" { + require.Contains(t, err.Error(), tt.errMsg) + return + } + require.NoError(t, err) + + // donation message will skip decoding, so ToAddress will be left empty + if tt.donation { + require.Empty(t, tt.event.ToAddress) + return + } + + // if it's a standard memo + if tt.expectedMemoStd != nil { + require.NotNil(t, tt.event.MemoStd) + require.Equal(t, tt.expectedMemoStd.Receiver.Hex(), tt.event.ToAddress) + require.Equal(t, tt.expectedMemoStd, tt.event.MemoStd) + } else { + // if it's a legacy memo, check receiver address only + require.Equal(t, tt.expectedReceiver.Hex(), tt.event.ToAddress) + } + }) + } +} + +func Test_ValidateStandardMemo(t *testing.T) { + // test cases + tests := []struct { + name string + memo memo.InboundMemo + errMsg string + }{ + { + name: "validation should pass for a valid standard memo", + memo: memo.InboundMemo{ + Header: memo.Header{ + OpCode: memo.OpCodeDepositAndCall, + }, + FieldsV0: memo.FieldsV0{ + RevertOptions: crosschaintypes.RevertOptions{ + RevertAddress: sample.BtcAddressP2WPKH(t, &chaincfg.TestNet3Params), + }, + }, + }, + }, + { + name: "NoAssetCall is disabled for Bitcoin", + memo: memo.InboundMemo{ + Header: memo.Header{ + OpCode: memo.OpCodeCall, + }, + }, + errMsg: "NoAssetCall is disabled for Bitcoin", + }, + { + name: "should return error on invalid revert address", + memo: memo.InboundMemo{ + FieldsV0: memo.FieldsV0{ + RevertOptions: crosschaintypes.RevertOptions{ + // not a BTC address + RevertAddress: "0x2D07A9CBd57DCca3E2cF966C88Bc874445b6E3B6", + }, + }, + }, + errMsg: "invalid revert address in memo", + }, + { + name: "should return error if revert address is not a supported address type", + memo: memo.InboundMemo{ + FieldsV0: memo.FieldsV0{ + RevertOptions: crosschaintypes.RevertOptions{ + // address not supported + RevertAddress: "035e4ae279bd416b5da724972c9061ec6298dac020d1e3ca3f06eae715135cdbec", + }, + }, + }, + errMsg: "unsupported revert address in memo", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := observer.ValidateStandardMemo(tt.memo, chains.BitcoinTestnet.ChainId) + if tt.errMsg != "" { + require.Contains(t, err.Error(), tt.errMsg) + return + } + require.NoError(t, err) + }) + } +} + +func Test_CheckEventProcessability(t *testing.T) { + // can use any bitcoin chain for testing + chain := chains.BitcoinMainnet + params := mocks.MockChainParams(chain.ChainId, 10) + + // create test observer + ob := MockBTCObserver(t, chain, params, nil) + + // setup compliance config + cfg := config.Config{ + ComplianceConfig: sample.ComplianceConfig(), + } + config.LoadComplianceConfig(cfg) + + // test cases + tests := []struct { + name string + event observer.BTCInboundEvent + result bool + }{ + { + name: "should return true for processable event", + event: createTestBtcEvent(t, &chaincfg.MainNetParams, []byte("a memo"), nil), + result: true, + }, + { + name: "should return false on donation message", + event: createTestBtcEvent(t, &chaincfg.MainNetParams, []byte(constant.DonationMessage), nil), + result: false, + }, + { + name: "should return false on compliance violation", + event: createTestBtcEvent(t, &chaincfg.MainNetParams, []byte("a memo"), &memo.InboundMemo{ + FieldsV0: memo.FieldsV0{ + Receiver: common.HexToAddress(sample.RestrictedEVMAddressTest), + }, + }), + result: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ob.CheckEventProcessability(tt.event) + require.Equal(t, tt.result, result) + }) + } +} + +func Test_NewInboundVoteFromLegacyMemo(t *testing.T) { + // can use any bitcoin chain for testing + chain := chains.BitcoinMainnet + params := mocks.MockChainParams(chain.ChainId, 10) + + // create test observer + ob := MockBTCObserver(t, chain, params, nil) + zetacoreClient := mocks.NewZetacoreClient(t).WithKeys(&keys.Keys{}).WithZetaChain() + ob.WithZetacoreClient(zetacoreClient) + + t.Run("should create new inbound vote msg V1", func(t *testing.T) { + // create test event + event := createTestBtcEvent(t, &chaincfg.MainNetParams, []byte("dummy memo"), nil) + + // test amount + amountSats := big.NewInt(1000) + + // expected vote + expectedVote := crosschaintypes.MsgVoteInbound{ + Sender: event.FromAddress, + SenderChainId: chain.ChainId, + TxOrigin: event.FromAddress, + Receiver: event.ToAddress, + ReceiverChain: ob.ZetacoreClient().Chain().ChainId, + Amount: cosmosmath.NewUint(amountSats.Uint64()), + Message: hex.EncodeToString(event.MemoBytes), + InboundHash: event.TxHash, + InboundBlockHeight: event.BlockNumber, + CallOptions: &crosschaintypes.CallOptions{ + GasLimit: 0, + }, + CoinType: coin.CoinType_Gas, + ProtocolContractVersion: crosschaintypes.ProtocolContractVersion_V1, + RevertOptions: crosschaintypes.NewEmptyRevertOptions(), // ignored by V1 + } + + // create new inbound vote V1 + vote := ob.NewInboundVoteFromLegacyMemo(&event, amountSats) + require.Equal(t, expectedVote, *vote) + }) +} + +func Test_NewInboundVoteFromStdMemo(t *testing.T) { + // can use any bitcoin chain for testing + chain := chains.BitcoinMainnet + params := mocks.MockChainParams(chain.ChainId, 10) + + // create test observer + ob := MockBTCObserver(t, chain, params, nil) + zetacoreClient := mocks.NewZetacoreClient(t).WithKeys(&keys.Keys{}).WithZetaChain() + ob.WithZetacoreClient(zetacoreClient) + + t.Run("should create new inbound vote msg with standard memo", func(t *testing.T) { + // create revert options + revertOptions := crosschaintypes.NewEmptyRevertOptions() + revertOptions.RevertAddress = sample.BtcAddressP2WPKH(t, &chaincfg.MainNetParams) + + // create test event + receiver := sample.EthAddress() + event := createTestBtcEvent(t, &chaincfg.MainNetParams, []byte("dymmy"), &memo.InboundMemo{ + FieldsV0: memo.FieldsV0{ + Receiver: receiver, + Payload: []byte("some payload"), + RevertOptions: revertOptions, + }, + }) + + // test amount + amountSats := big.NewInt(1000) + + // expected vote + memoBytesExpected := append(event.MemoStd.Receiver.Bytes(), event.MemoStd.Payload...) + expectedVote := crosschaintypes.MsgVoteInbound{ + Sender: revertOptions.RevertAddress, // should be overridden by revert address + SenderChainId: chain.ChainId, + TxOrigin: event.FromAddress, + Receiver: event.ToAddress, + ReceiverChain: ob.ZetacoreClient().Chain().ChainId, + Amount: cosmosmath.NewUint(amountSats.Uint64()), + Message: hex.EncodeToString(memoBytesExpected), // a simulated legacy memo + InboundHash: event.TxHash, + InboundBlockHeight: event.BlockNumber, + CallOptions: &crosschaintypes.CallOptions{ + GasLimit: 0, + }, + CoinType: coin.CoinType_Gas, + ProtocolContractVersion: crosschaintypes.ProtocolContractVersion_V1, + RevertOptions: crosschaintypes.NewEmptyRevertOptions(), // ignored by V1 + } + + // create new inbound vote V1 with standard memo + vote := ob.NewInboundVoteFromStdMemo(&event, amountSats) + require.Equal(t, expectedVote, *vote) + }) +} diff --git a/zetaclient/chains/bitcoin/observer/inbound.go b/zetaclient/chains/bitcoin/observer/inbound.go index 1461096763..3cd1ab3945 100644 --- a/zetaclient/chains/bitcoin/observer/inbound.go +++ b/zetaclient/chains/bitcoin/observer/inbound.go @@ -6,50 +6,22 @@ import ( "fmt" "math/big" - cosmosmath "cosmossdk.io/math" "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" - ethcommon "github.com/ethereum/go-ethereum/common" "github.com/pkg/errors" "github.com/rs/zerolog" "github.com/zeta-chain/node/pkg/coin" - "github.com/zeta-chain/node/pkg/memo" crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" "github.com/zeta-chain/node/zetaclient/chains/bitcoin" "github.com/zeta-chain/node/zetaclient/chains/interfaces" - "github.com/zeta-chain/node/zetaclient/compliance" - "github.com/zeta-chain/node/zetaclient/config" zctx "github.com/zeta-chain/node/zetaclient/context" + "github.com/zeta-chain/node/zetaclient/logs" "github.com/zeta-chain/node/zetaclient/types" "github.com/zeta-chain/node/zetaclient/zetacore" ) -// BTCInboundEvent represents an incoming transaction event -type BTCInboundEvent struct { - // FromAddress is the first input address - FromAddress string - - // ToAddress is the TSS address - ToAddress string - - // Value is the amount of BTC - Value float64 - - // DepositorFee is the deposit fee - DepositorFee float64 - - // MemoBytes is the memo of inbound - MemoBytes []byte - - // BlockNumber is the block number of the inbound - BlockNumber uint64 - - // TxHash is the hash of the inbound - TxHash string -} - // WatchInbound watches Bitcoin chain for inbounds on a ticker // It starts a ticker and run ObserveInbound // TODO(revamp): move all ticker related methods in the same file @@ -100,8 +72,6 @@ func (ob *Observer) WatchInbound(ctx context.Context) error { // ObserveInbound observes the Bitcoin chain for inbounds and post votes to zetacore // TODO(revamp): simplify this function into smaller functions func (ob *Observer) ObserveInbound(ctx context.Context) error { - zetaCoreClient := ob.ZetacoreClient() - // get and update latest block height currentBlock, err := ob.btcClient.GetBlockCount() if err != nil { @@ -166,22 +136,11 @@ func (ob *Observer) ObserveInbound(ctx context.Context) error { // post inbound vote message to zetacore for _, event := range events { - msg := ob.GetInboundVoteMessageFromBtcEvent(event) + msg := ob.GetInboundVoteFromBtcEvent(event) if msg != nil { - zetaHash, ballot, err := zetaCoreClient.PostVoteInbound( - ctx, - zetacore.PostVoteInboundGasLimit, - zetacore.PostVoteInboundExecutionGasLimit, - msg, - ) + _, err = ob.PostVoteInbound(ctx, msg, zetacore.PostVoteInboundExecutionGasLimit) if err != nil { - ob.logger.Inbound.Error(). - Err(err). - Msgf("observeInboundBTC: error posting to zetacore for tx %s", event.TxHash) - return err // we have to re-scan this block next time - } else if zetaHash != "" { - ob.logger.Inbound.Info().Msgf("observeInboundBTC: PostVoteInbound zeta tx hash: %s inbound %s ballot %s fee %v", - zetaHash, event.TxHash, ballot, event.DepositorFee) + return errors.Wrapf(err, "error PostVoteInbound") // we have to re-scan this block next time } } } @@ -291,6 +250,7 @@ func (ob *Observer) CheckReceiptForBtcTxHash(ctx context.Context, txHash string, } // check confirmation + // #nosec G115 block height always positive if !ob.IsBlockConfirmed(uint64(blockVb.Height)) { return "", fmt.Errorf("block %d is not confirmed yet", blockVb.Height) } @@ -319,7 +279,7 @@ func (ob *Observer) CheckReceiptForBtcTxHash(ctx context.Context, txHash string, return "", errors.New("no btc deposit event found") } - msg := ob.GetInboundVoteMessageFromBtcEvent(event) + msg := ob.GetInboundVoteFromBtcEvent(event) if msg == nil { return "", errors.New("no message built for btc sent to TSS") } @@ -383,52 +343,43 @@ func FilterAndParseIncomingTx( return events, nil } -// GetInboundVoteMessageFromBtcEvent converts a BTCInboundEvent to a MsgVoteInbound to enable voting on the inbound on zetacore -func (ob *Observer) GetInboundVoteMessageFromBtcEvent(inbound *BTCInboundEvent) *crosschaintypes.MsgVoteInbound { - ob.logger.Inbound.Debug().Msgf("Processing inbound: %s", inbound.TxHash) - amount := big.NewFloat(inbound.Value) - amount = amount.Mul(amount, big.NewFloat(1e8)) - amountInt, _ := amount.Int(nil) - message := hex.EncodeToString(inbound.MemoBytes) - - // compliance check - // if the inbound contains restricted addresses, return nil - if ob.DoesInboundContainsRestrictedAddress(inbound) { +// GetInboundVoteFromBtcEvent converts a BTCInboundEvent to a MsgVoteInbound to enable voting on the inbound on zetacore +func (ob *Observer) GetInboundVoteFromBtcEvent(event *BTCInboundEvent) *crosschaintypes.MsgVoteInbound { + // prepare logger fields + lf := map[string]any{ + logs.FieldModule: logs.ModNameInbound, + logs.FieldMethod: "GetInboundVoteFromBtcEvent", + logs.FieldChain: ob.Chain().ChainId, + logs.FieldTx: event.TxHash, + } + + // decode event memo bytes + err := event.DecodeMemoBytes(ob.Chain().ChainId) + if err != nil { + ob.Logger().Inbound.Info().Fields(lf).Msgf("invalid memo bytes: %s", hex.EncodeToString(event.MemoBytes)) return nil } - return zetacore.GetInboundVoteMessage( - inbound.FromAddress, - ob.Chain().ChainId, - inbound.FromAddress, - inbound.FromAddress, - ob.ZetacoreClient().Chain().ChainId, - cosmosmath.NewUintFromBigInt(amountInt), - message, - inbound.TxHash, - inbound.BlockNumber, - 0, - coin.CoinType_Gas, - "", - ob.ZetacoreClient().GetKeys().GetOperatorAddress().String(), - 0, - ) -} + // check if the event is processable + if !ob.CheckEventProcessability(*event) { + return nil + } -// DoesInboundContainsRestrictedAddress returns true if the inbound contains restricted addresses -// TODO(revamp): move all compliance related functions in a specific file -func (ob *Observer) DoesInboundContainsRestrictedAddress(inTx *BTCInboundEvent) bool { - receiver := "" - parsedAddress, _, err := memo.DecodeLegacyMemoHex(hex.EncodeToString(inTx.MemoBytes)) - if err == nil && parsedAddress != (ethcommon.Address{}) { - receiver = parsedAddress.Hex() + // convert the amount to integer (satoshis) + amountSats, err := bitcoin.GetSatoshis(event.Value) + if err != nil { + ob.Logger().Inbound.Error().Err(err).Fields(lf).Msgf("can't convert value %f to satoshis", event.Value) + return nil } - if config.ContainRestrictedAddress(inTx.FromAddress, receiver) { - compliance.PrintComplianceLog(ob.logger.Inbound, ob.logger.Compliance, - false, ob.Chain().ChainId, inTx.TxHash, inTx.FromAddress, receiver, "BTC") - return true + amountInt := big.NewInt(amountSats) + + // create inbound vote message contract V1 for legacy memo + if event.MemoStd == nil { + return ob.NewInboundVoteFromLegacyMemo(event, amountInt) } - return false + + // create inbound vote message for standard memo + return ob.NewInboundVoteFromStdMemo(event, amountInt) } // GetBtcEvent returns a valid BTCInboundEvent or nil @@ -489,7 +440,7 @@ func GetBtcEventWithoutWitness( // 2nd vout must be a valid OP_RETURN memo vout1 := tx.Vout[1] - memo, found, err = bitcoin.DecodeOpReturnMemo(vout1.ScriptPubKey.Hex, tx.Txid) + memo, found, err = bitcoin.DecodeOpReturnMemo(vout1.ScriptPubKey.Hex) if err != nil { logger.Error().Err(err).Msgf("GetBtcEvent: error decoding OP_RETURN memo: %s", vout1.ScriptPubKey.Hex) return nil, nil diff --git a/zetaclient/chains/bitcoin/observer/inbound_test.go b/zetaclient/chains/bitcoin/observer/inbound_test.go index 8b01e222a1..838315b7b8 100644 --- a/zetaclient/chains/bitcoin/observer/inbound_test.go +++ b/zetaclient/chains/bitcoin/observer/inbound_test.go @@ -18,9 +18,13 @@ import ( "github.com/stretchr/testify/require" "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/pkg/constant" + "github.com/zeta-chain/node/testutil" + "github.com/zeta-chain/node/testutil/sample" "github.com/zeta-chain/node/zetaclient/chains/bitcoin" "github.com/zeta-chain/node/zetaclient/chains/bitcoin/observer" clientcommon "github.com/zeta-chain/node/zetaclient/common" + "github.com/zeta-chain/node/zetaclient/keys" "github.com/zeta-chain/node/zetaclient/testutils" "github.com/zeta-chain/node/zetaclient/testutils/mocks" "github.com/zeta-chain/node/zetaclient/testutils/testrpc" @@ -138,6 +142,77 @@ func TestAvgFeeRateBlock828440Errors(t *testing.T) { }) } +func Test_GetInboundVoteFromBtcEvent(t *testing.T) { + // can use any bitcoin chain for testing + chain := chains.BitcoinMainnet + params := mocks.MockChainParams(chain.ChainId, 10) + + // create test observer + ob := MockBTCObserver(t, chain, params, nil) + zetacoreClient := mocks.NewZetacoreClient(t).WithKeys(&keys.Keys{}).WithZetaChain() + ob.WithZetacoreClient(zetacoreClient) + + // test cases + tests := []struct { + name string + event *observer.BTCInboundEvent + nilVote bool + }{ + { + name: "should return vote for standard memo", + event: &observer.BTCInboundEvent{ + FromAddress: sample.BtcAddressP2WPKH(t, &chaincfg.MainNetParams), + // a deposit and call + MemoBytes: testutil.HexToBytes( + t, + "5a0110032d07a9cbd57dcca3e2cf966c88bc874445b6e3b60d68656c6c6f207361746f736869", + ), + }, + }, + { + name: "should return vote for legacy memo", + event: &observer.BTCInboundEvent{ + // raw address + payload + MemoBytes: testutil.HexToBytes(t, "2d07a9cbd57dcca3e2cf966c88bc874445b6e3b668656c6c6f207361746f736869"), + }, + }, + { + name: "should return nil if unable to decode memo", + event: &observer.BTCInboundEvent{ + // standard memo that carries payload only, receiver address is empty + MemoBytes: testutil.HexToBytes(t, "5a0110020d68656c6c6f207361746f736869"), + }, + nilVote: true, + }, + { + name: "should return nil on donation message", + event: &observer.BTCInboundEvent{ + MemoBytes: []byte(constant.DonationMessage), + }, + nilVote: true, + }, + { + name: "should return nil on invalid deposit value", + event: &observer.BTCInboundEvent{ + Value: -1, // invalid value + MemoBytes: testutil.HexToBytes(t, "2d07a9cbd57dcca3e2cf966c88bc874445b6e3b668656c6c6f207361746f736869"), + }, + nilVote: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + msg := ob.GetInboundVoteFromBtcEvent(tt.event) + if tt.nilVote { + require.Nil(t, msg) + } else { + require.NotNil(t, msg) + } + }) + } +} + func TestGetSenderAddressByVin(t *testing.T) { // https://mempool.space/tx/3618e869f9e87863c0f1cc46dbbaa8b767b4a5d6d60b143c2c50af52b257e867 txHash := "3618e869f9e87863c0f1cc46dbbaa8b767b4a5d6d60b143c2c50af52b257e867" diff --git a/zetaclient/chains/bitcoin/observer/witness.go b/zetaclient/chains/bitcoin/observer/witness.go index 86b22f95cf..9625ad3caa 100644 --- a/zetaclient/chains/bitcoin/observer/witness.go +++ b/zetaclient/chains/bitcoin/observer/witness.go @@ -6,6 +6,7 @@ import ( "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/txscript" "github.com/pkg/errors" "github.com/rs/zerolog" @@ -104,7 +105,7 @@ func ParseScriptFromWitness(witness []string, logger zerolog.Logger) []byte { // If there are at least two witness elements, and the first byte of // the last element is 0x50, this last element is called annex a // and is removed from the witness stack. - if length >= 2 && len(lastElement) > 0 && lastElement[0] == 0x50 { + if length >= 2 && len(lastElement) > 0 && lastElement[0] == txscript.TaprootAnnexTag { // account for the extra item removed from the end witness = witness[:length-1] } @@ -130,7 +131,7 @@ func tryExtractOpRet(tx btcjson.TxRawResult, logger zerolog.Logger) []byte { return nil } - memo, found, err := bitcoin.DecodeOpReturnMemo(tx.Vout[1].ScriptPubKey.Hex, tx.Txid) + memo, found, err := bitcoin.DecodeOpReturnMemo(tx.Vout[1].ScriptPubKey.Hex) if err != nil { logger.Error().Err(err).Msgf("tryExtractOpRet: error decoding OP_RETURN memo: %s", tx.Vout[1].ScriptPubKey.Hex) return nil diff --git a/zetaclient/chains/bitcoin/signer/signer.go b/zetaclient/chains/bitcoin/signer/signer.go index 7e49d4d675..90257e019e 100644 --- a/zetaclient/chains/bitcoin/signer/signer.go +++ b/zetaclient/chains/bitcoin/signer/signer.go @@ -158,7 +158,7 @@ func (signer *Signer) AddWithdrawTxOutputs( if err != nil { return err } - payToSelfScript, err := bitcoin.PayToAddrScript(tssAddrP2WPKH) + payToSelfScript, err := txscript.PayToAddrScript(tssAddrP2WPKH) if err != nil { return err } @@ -167,7 +167,7 @@ func (signer *Signer) AddWithdrawTxOutputs( // 2nd output: the payment to the recipient if !cancelTx { - pkScript, err := bitcoin.PayToAddrScript(to) + pkScript, err := txscript.PayToAddrScript(to) if err != nil { return err } diff --git a/zetaclient/chains/bitcoin/signer/signer_keysign_test.go b/zetaclient/chains/bitcoin/signer/signer_keysign_test.go index 1ad50f0af5..a339b051dd 100644 --- a/zetaclient/chains/bitcoin/signer/signer_keysign_test.go +++ b/zetaclient/chains/bitcoin/signer/signer_keysign_test.go @@ -17,7 +17,6 @@ import ( "github.com/stretchr/testify/suite" "github.com/zeta-chain/node/pkg/chains" - "github.com/zeta-chain/node/zetaclient/chains/bitcoin" "github.com/zeta-chain/node/zetaclient/chains/interfaces" "github.com/zeta-chain/node/zetaclient/testutils/mocks" ) @@ -96,7 +95,7 @@ func buildTX() (*wire.MsgTx, *txscript.TxSigHashes, int, int64, []byte, *btcec.P txIn := wire.NewTxIn(outpoint, nil, nil) tx.AddTxIn(txIn) - pkScript, err := bitcoin.PayToAddrScript(addr) + pkScript, err := txscript.PayToAddrScript(addr) if err != nil { return nil, nil, 0, 0, nil, nil, false, err } diff --git a/zetaclient/chains/bitcoin/signer/signer_test.go b/zetaclient/chains/bitcoin/signer/signer_test.go index b9decf25b1..17fb2dc3de 100644 --- a/zetaclient/chains/bitcoin/signer/signer_test.go +++ b/zetaclient/chains/bitcoin/signer/signer_test.go @@ -20,7 +20,6 @@ import ( "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/zetaclient/chains/base" - "github.com/zeta-chain/node/zetaclient/chains/bitcoin" "github.com/zeta-chain/node/zetaclient/config" "github.com/zeta-chain/node/zetaclient/testutils/mocks" ) @@ -76,7 +75,7 @@ func (s *BTCSignerSuite) TestP2PH(c *C) { prevOut := wire.NewOutPoint(&chainhash.Hash{}, ^uint32(0)) txIn := wire.NewTxIn(prevOut, []byte{txscript.OP_0, txscript.OP_0}, nil) originTx.AddTxIn(txIn) - pkScript, err := bitcoin.PayToAddrScript(addr) + pkScript, err := txscript.PayToAddrScript(addr) c.Assert(err, IsNil) @@ -148,7 +147,7 @@ func (s *BTCSignerSuite) TestP2WPH(c *C) { prevOut := wire.NewOutPoint(&chainhash.Hash{}, ^uint32(0)) txIn := wire.NewTxIn(prevOut, []byte{txscript.OP_0, txscript.OP_0}, nil) originTx.AddTxIn(txIn) - pkScript, err := bitcoin.PayToAddrScript(addr) + pkScript, err := txscript.PayToAddrScript(addr) c.Assert(err, IsNil) txOut := wire.NewTxOut(100000000, pkScript) originTx.AddTxOut(txOut) @@ -169,7 +168,7 @@ func (s *BTCSignerSuite) TestP2WPH(c *C) { txOut = wire.NewTxOut(0, nil) redeemTx.AddTxOut(txOut) txSigHashes := txscript.NewTxSigHashes(redeemTx, txscript.NewCannedPrevOutputFetcher([]byte{}, 0)) - pkScript, err = bitcoin.PayToAddrScript(addr) + pkScript, err = txscript.PayToAddrScript(addr) c.Assert(err, IsNil) { @@ -240,7 +239,7 @@ func TestAddWithdrawTxOutputs(t *testing.T) { // tss address and script tssAddr, err := signer.TSS().BTCAddress(chains.BitcoinTestnet.ChainId) require.NoError(t, err) - tssScript, err := bitcoin.PayToAddrScript(tssAddr) + tssScript, err := txscript.PayToAddrScript(tssAddr) require.NoError(t, err) fmt.Printf("tss address: %s", tssAddr.EncodeAddress()) @@ -248,7 +247,7 @@ func TestAddWithdrawTxOutputs(t *testing.T) { receiver := "bc1qaxf82vyzy8y80v000e7t64gpten7gawewzu42y" to, err := chains.DecodeBtcAddress(receiver, chains.BitcoinMainnet.ChainId) require.NoError(t, err) - toScript, err := bitcoin.PayToAddrScript(to) + toScript, err := txscript.PayToAddrScript(to) require.NoError(t, err) // test cases diff --git a/zetaclient/chains/bitcoin/tokenizer.go b/zetaclient/chains/bitcoin/tokenizer.go deleted file mode 100644 index 5708bfa250..0000000000 --- a/zetaclient/chains/bitcoin/tokenizer.go +++ /dev/null @@ -1,162 +0,0 @@ -package bitcoin - -import ( - "encoding/binary" - "fmt" - - "github.com/btcsuite/btcd/txscript" -) - -func newScriptTokenizer(script []byte) scriptTokenizer { - return scriptTokenizer{ - script: script, - offset: 0, - } -} - -// scriptTokenizer is supposed to be replaced by txscript.ScriptTokenizer. However, -// it seems currently the btcsuite version does not have ScriptTokenizer. A simplified -// version of that is implemented here. This is fully compatible with txscript.ScriptTokenizer -// one should consider upgrading txscript and remove this implementation -type scriptTokenizer struct { - script []byte - offset int - op byte - data []byte - err error -} - -// Done returns true when either all opcodes have been exhausted or a parse -// failure was encountered and therefore the state has an associated error. -func (t *scriptTokenizer) Done() bool { - return t.err != nil || t.offset >= len(t.script) -} - -// Data returns the data associated with the most recently successfully parsed -// opcode. -func (t *scriptTokenizer) Data() []byte { - return t.data -} - -// Err returns any errors currently associated with the tokenizer. This will -// only be non-nil in the case a parsing error was encountered. -func (t *scriptTokenizer) Err() error { - return t.err -} - -// Opcode returns the current opcode associated with the tokenizer. -func (t *scriptTokenizer) Opcode() byte { - return t.op -} - -// Next attempts to parse the next opcode and returns whether or not it was -// successful. It will not be successful if invoked when already at the end of -// the script, a parse failure is encountered, or an associated error already -// exists due to a previous parse failure. -// -// In the case of a true return, the parsed opcode and data can be obtained with -// the associated functions and the offset into the script will either point to -// the next opcode or the end of the script if the final opcode was parsed. -// -// In the case of a false return, the parsed opcode and data will be the last -// successfully parsed values (if any) and the offset into the script will -// either point to the failing opcode or the end of the script if the function -// was invoked when already at the end of the script. -// -// Invoking this function when already at the end of the script is not -// considered an error and will simply return false. -func (t *scriptTokenizer) Next() bool { - if t.Done() { - return false - } - - op := t.script[t.offset] - - // Only the following op_code will be encountered: - // OP_PUSHDATA*, OP_DATA_*, OP_CHECKSIG, OP_IF, OP_ENDIF, OP_FALSE - switch { - // No additional data. Note that some of the opcodes, notably OP_1NEGATE, - // OP_0, and OP_[1-16] represent the data themselves. - case op == txscript.OP_FALSE || op == txscript.OP_IF || op == txscript.OP_CHECKSIG || op == txscript.OP_ENDIF: - t.offset++ - t.op = op - t.data = nil - return true - - // Data pushes of specific lengths -- OP_DATA_[1-75]. - case op >= txscript.OP_DATA_1 && op <= txscript.OP_DATA_75: - script := t.script[t.offset:] - - // The length should be: int(op) - txscript.OP_DATA_1 + 2, i.e. op is txscript.OP_DATA_10, that means - // the data length should be 10, which is txscript.OP_DATA_10 - txscript.OP_DATA_1 + 1. - // Here, 2 instead of 1 because `script` also includes the opcode which means it contains one more byte. - // Since txscript.OP_DATA_1 is 1, then length is just int(op) - 1 + 2 = int(op) + 1 - length := int(op) + 1 - if len(script) < length { - t.err = fmt.Errorf("opcode %d detected, but script only %d bytes remaining", op, len(script)) - return false - } - - // Move the offset forward and set the opcode and data accordingly. - t.offset += length - t.op = op - t.data = script[1:length] - return true - - case op > txscript.OP_PUSHDATA4: - t.err = fmt.Errorf("unexpected op code %d", op) - return false - - // Data pushes with parsed lengths -- OP_PUSHDATA{1,2,4}. - default: - var length int - switch op { - case txscript.OP_PUSHDATA1: - length = 1 - case txscript.OP_PUSHDATA2: - length = 2 - case txscript.OP_PUSHDATA4: - length = 4 - default: - t.err = fmt.Errorf("unexpected op code %d", op) - return false - } - - script := t.script[t.offset+1:] - if len(script) < length { - t.err = fmt.Errorf("opcode %d requires %d bytes, only %d remaining", op, length, len(script)) - return false - } - - // Next -length bytes are little endian length of data. - var dataLen int - switch length { - case 1: - dataLen = int(script[0]) - case 2: - dataLen = int(binary.LittleEndian.Uint16(script[:length])) - case 4: - dataLen = int(binary.LittleEndian.Uint32(script[:length])) - default: - t.err = fmt.Errorf("invalid opcode length %d", length) - return false - } - - // Move to the beginning of the data. - script = script[length:] - - // Disallow entries that do not fit script or were sign extended. - if dataLen > len(script) || dataLen < 0 { - t.err = fmt.Errorf("opcode %d pushes %d bytes, only %d remaining", op, dataLen, len(script)) - return false - } - - // Move the offset forward and set the opcode and data accordingly. - // 1 is the opcode size, which is just 1 byte. int(op) is the opcode value, - // it should not be mixed with the size. - t.offset += 1 + length + dataLen - t.op = op - t.data = script[:dataLen] - return true - } -} diff --git a/zetaclient/chains/bitcoin/tx_script.go b/zetaclient/chains/bitcoin/tx_script.go index f5bc856d5d..816251024a 100644 --- a/zetaclient/chains/bitcoin/tx_script.go +++ b/zetaclient/chains/bitcoin/tx_script.go @@ -2,10 +2,8 @@ package bitcoin // #nosec G507 ripemd160 required for bitcoin address encoding import ( - "bytes" "encoding/hex" "fmt" - "strconv" "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/btcutil" @@ -16,7 +14,6 @@ import ( "golang.org/x/crypto/ripemd160" "github.com/zeta-chain/node/pkg/chains" - "github.com/zeta-chain/node/pkg/constant" ) const ( @@ -36,17 +33,6 @@ const ( LengthScriptP2PKH = 25 ) -// PayToAddrScript creates a new script to pay a transaction output to a the -// specified address. -func PayToAddrScript(addr btcutil.Address) ([]byte, error) { - switch addr := addr.(type) { - case *chains.AddressTaproot: - return chains.PayToWitnessTaprootScript(addr.ScriptAddress()) - default: - return txscript.PayToAddrScript(addr) - } -} - // IsPkScriptP2TR checks if the given script is a P2TR script func IsPkScriptP2TR(script []byte) bool { return len(script) == LengthScriptP2TR && script[0] == txscript.OP_1 && script[1] == 0x20 @@ -91,7 +77,7 @@ func DecodeScriptP2TR(scriptHex string, net *chaincfg.Params) (string, error) { } witnessProg := script[2:] - receiverAddress, err := chains.NewAddressTaproot(witnessProg, net) + receiverAddress, err := btcutil.NewAddressTaproot(witnessProg, net) if err != nil { // should never happen return "", errors.Wrapf(err, "error getting address from script %s", scriptHex) } @@ -169,27 +155,49 @@ func DecodeScriptP2PKH(scriptHex string, net *chaincfg.Params) (string, error) { // DecodeOpReturnMemo decodes memo from OP_RETURN script // returns (memo, found, error) -func DecodeOpReturnMemo(scriptHex string, txid string) ([]byte, bool, error) { - if len(scriptHex) >= 4 && scriptHex[:2] == "6a" { // OP_RETURN - memoSize, err := strconv.ParseInt(scriptHex[2:4], 16, 32) - if err != nil { - return nil, false, errors.Wrapf(err, "error decoding memo size: %s", scriptHex) +func DecodeOpReturnMemo(scriptHex string) ([]byte, bool, error) { + // decode hex script + scriptBytes, err := hex.DecodeString(scriptHex) + if err != nil { + return nil, false, errors.Wrapf(err, "error decoding script hex: %s", scriptHex) + } + + // skip non-OP_RETURN script + // OP_RETURN script has to be at least 2 bytes: [OP_RETURN + dataLen] + if len(scriptBytes) < 2 || scriptBytes[0] != txscript.OP_RETURN { + return nil, false, nil + } + + // extract appended data in the OP_RETURN script + var memoBytes []byte + var memoSize = scriptBytes[1] + switch { + case memoSize < txscript.OP_PUSHDATA1: + // memo size has to match the actual data + if int(memoSize) != (len(scriptBytes) - 2) { + return nil, false, fmt.Errorf("memo size mismatch: %d != %d", memoSize, (len(scriptBytes) - 2)) } - if int(memoSize) != (len(scriptHex)-4)/2 { - return nil, false, fmt.Errorf("memo size mismatch: %d != %d", memoSize, (len(scriptHex)-4)/2) + memoBytes = scriptBytes[2:] + case memoSize == txscript.OP_PUSHDATA1: + // when data size >= OP_PUSHDATA1 (76), Bitcoin uses 2 bytes to represent the length: [OP_PUSHDATA1 + dataLen] + // see: https://github.com/btcsuite/btcd/blob/master/txscript/scriptbuilder.go#L183 + if len(scriptBytes) < 3 { + return nil, false, fmt.Errorf("script too short: %s", scriptHex) } + memoSize = scriptBytes[2] - memoBytes, err := hex.DecodeString(scriptHex[4:]) - if err != nil { - return nil, false, errors.Wrapf(err, "error hex decoding memo: %s", scriptHex) + // memo size has to match the actual data + if int(memoSize) != (len(scriptBytes) - 3) { + return nil, false, fmt.Errorf("memo size mismatch: %d != %d", memoSize, (len(scriptBytes) - 3)) } - if bytes.Equal(memoBytes, []byte(constant.DonationMessage)) { - return nil, false, fmt.Errorf("donation tx: %s", txid) - } - return memoBytes, true, nil + memoBytes = scriptBytes[3:] + default: + // should never happen + // OP_RETURN script won't carry more than 80 bytes + return nil, false, fmt.Errorf("invalid OP_RETURN script: %s", scriptHex) } - return nil, false, nil + return memoBytes, true, nil } // DecodeScript decodes memo wrapped in an inscription like script in witness @@ -208,7 +216,7 @@ func DecodeOpReturnMemo(scriptHex string, txid string) ([]byte, bool, error) { // OP_ENDIF // There are no content-type or any other attributes, it's just raw bytes. func DecodeScript(script []byte) ([]byte, bool, error) { - t := newScriptTokenizer(script) + t := txscript.MakeScriptTokenizer(0, script) if err := checkInscriptionEnvelope(&t); err != nil { return nil, false, errors.Wrap(err, "checkInscriptionEnvelope: unable to check the envelope") @@ -278,7 +286,7 @@ func DecodeTSSVout(vout btcjson.Vout, receiverExpected string, chain chains.Chai // parse receiver address from vout var receiverVout string switch addr.(type) { - case *chains.AddressTaproot: + case *btcutil.AddressTaproot: receiverVout, err = DecodeScriptP2TR(vout.ScriptPubKey.Hex, chainParams) case *btcutil.AddressWitnessScriptHash: receiverVout, err = DecodeScriptP2WSH(vout.ScriptPubKey.Hex, chainParams) @@ -298,7 +306,7 @@ func DecodeTSSVout(vout btcjson.Vout, receiverExpected string, chain chains.Chai return receiverVout, amount, nil } -func decodeInscriptionPayload(t *scriptTokenizer) ([]byte, error) { +func decodeInscriptionPayload(t *txscript.ScriptTokenizer) ([]byte, error) { if !t.Next() || t.Opcode() != txscript.OP_FALSE { return nil, fmt.Errorf("OP_FALSE not found") } @@ -327,7 +335,7 @@ func decodeInscriptionPayload(t *scriptTokenizer) ([]byte, error) { // checkInscriptionEnvelope decodes the envelope for the script monitoring. The format is // OP_PUSHBYTES_32 <32 bytes> OP_CHECKSIG -func checkInscriptionEnvelope(t *scriptTokenizer) error { +func checkInscriptionEnvelope(t *txscript.ScriptTokenizer) error { if !t.Next() || t.Opcode() != txscript.OP_DATA_32 { return fmt.Errorf("cannot obtain public key bytes op %d or err %s", t.Opcode(), t.Err()) } diff --git a/zetaclient/chains/bitcoin/tx_script_test.go b/zetaclient/chains/bitcoin/tx_script_test.go index cf54b4553f..6c4724eb9a 100644 --- a/zetaclient/chains/bitcoin/tx_script_test.go +++ b/zetaclient/chains/bitcoin/tx_script_test.go @@ -1,6 +1,7 @@ package bitcoin_test import ( + "bytes" "encoding/hex" "path" "strings" @@ -11,7 +12,7 @@ import ( "github.com/stretchr/testify/require" "github.com/zeta-chain/node/pkg/chains" - "github.com/zeta-chain/node/pkg/constant" + "github.com/zeta-chain/node/testutil" "github.com/zeta-chain/node/zetaclient/chains/bitcoin" "github.com/zeta-chain/node/zetaclient/testutils" ) @@ -331,80 +332,95 @@ func TestDecodeVoutP2PKHErrors(t *testing.T) { } func TestDecodeOpReturnMemo(t *testing.T) { - // load archived inbound raw result - // https://mempool.space/tx/847139aa65aa4a5ee896375951cbf7417cfc8a4d6f277ec11f40cd87319f04aa - chain := chains.BitcoinMainnet - txHash := "847139aa65aa4a5ee896375951cbf7417cfc8a4d6f277ec11f40cd87319f04aa" - scriptHex := "6a1467ed0bcc4e1256bc2ce87d22e190d63a120114bf" - rawResult := testutils.LoadBTCInboundRawResult(t, TestDataDir, chain.ChainId, txHash, false) - require.True(t, len(rawResult.Vout) >= 2) - require.Equal(t, scriptHex, rawResult.Vout[1].ScriptPubKey.Hex) - - t.Run("should decode memo from OP_RETURN output", func(t *testing.T) { - memo, found, err := bitcoin.DecodeOpReturnMemo(rawResult.Vout[1].ScriptPubKey.Hex, txHash) - require.NoError(t, err) - require.True(t, found) - // [OP_RETURN, 0x14,<20-byte-hash>] - require.Equal(t, scriptHex[4:], hex.EncodeToString(memo)) - }) - t.Run("should return nil memo non-OP_RETURN output", func(t *testing.T) { - // modify the OP_RETURN to OP_1 - scriptInvalid := strings.Replace(scriptHex, "6a", "51", 1) - memo, found, err := bitcoin.DecodeOpReturnMemo(scriptInvalid, txHash) - require.NoError(t, err) - require.False(t, found) - require.Nil(t, memo) - }) - t.Run("should return nil memo on invalid script", func(t *testing.T) { - // use known short script - scriptInvalid := "00" - memo, found, err := bitcoin.DecodeOpReturnMemo(scriptInvalid, txHash) - require.NoError(t, err) - require.False(t, found) - require.Nil(t, memo) - }) + tests := []struct { + name string + scriptHex string + found bool + expected []byte + }{ + { + name: "should decode memo from OP_RETURN data, size < 76(OP_PUSHDATA1)", + scriptHex: "6a1467ed0bcc4e1256bc2ce87d22e190d63a120114bf", + found: true, + expected: testutil.HexToBytes(t, "67ed0bcc4e1256bc2ce87d22e190d63a120114bf"), + }, + { + name: "should decode memo from OP_RETURN data, size >= 76(OP_PUSHDATA1)", + scriptHex: "6a4c4f" + // 79 bytes memo + "5a0110070a30d55c1031d30dab3b3d85f47b8f1d03df2d480961207061796c6f61642c626372743171793970716d6b32706439737636336732376a7438723635377779306439756565347832647432", + found: true, + expected: testutil.HexToBytes( + t, + "5a0110070a30d55c1031d30dab3b3d85f47b8f1d03df2d480961207061796c6f61642c626372743171793970716d6b32706439737636336732376a7438723635377779306439756565347832647432", + ), + }, + { + name: "should return nil memo for non-OP_RETURN script", + scriptHex: "511467ed0bcc4e1256bc2ce87d22e190d63a120114bf", // 0x51, OP_1 + found: false, + expected: nil, + }, + { + name: "should return nil memo for script less than 2 bytes", + scriptHex: "00", // 1 byte only + found: false, + expected: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + memo, found, err := bitcoin.DecodeOpReturnMemo(tt.scriptHex) + require.NoError(t, err) + require.Equal(t, tt.found, found) + require.True(t, bytes.Equal(tt.expected, memo)) + }) + } } func TestDecodeOpReturnMemoErrors(t *testing.T) { - // https://mempool.space/tx/847139aa65aa4a5ee896375951cbf7417cfc8a4d6f277ec11f40cd87319f04aa - txHash := "847139aa65aa4a5ee896375951cbf7417cfc8a4d6f277ec11f40cd87319f04aa" - scriptHex := "6a1467ed0bcc4e1256bc2ce87d22e190d63a120114bf" - - t.Run("should return error on invalid memo size", func(t *testing.T) { - // use invalid memo size - scriptInvalid := strings.Replace(scriptHex, "6a14", "6axy", 1) - memo, found, err := bitcoin.DecodeOpReturnMemo(scriptInvalid, txHash) - require.ErrorContains(t, err, "error decoding memo size") - require.False(t, found) - require.Nil(t, memo) - }) - - t.Run("should return error on memo size mismatch", func(t *testing.T) { - // use wrong memo size - scriptInvalid := strings.Replace(scriptHex, "6a14", "6a13", 1) - memo, found, err := bitcoin.DecodeOpReturnMemo(scriptInvalid, txHash) - require.ErrorContains(t, err, "memo size mismatch") - require.False(t, found) - require.Nil(t, memo) - }) - - t.Run("should return error on invalid hex", func(t *testing.T) { - // use invalid hex - scriptInvalid := strings.Replace(scriptHex, "6a1467", "6a14xy", 1) - memo, found, err := bitcoin.DecodeOpReturnMemo(scriptInvalid, txHash) - require.ErrorContains(t, err, "error hex decoding memo") - require.False(t, found) - require.Nil(t, memo) - }) + tests := []struct { + name string + scriptHex string + errMsg string + }{ + { + name: "should return error on invalid hex", + scriptHex: "6a14xy", + errMsg: "error decoding script hex", + }, + { + name: "should return error on memo size < 76 (OP_PUSHDATA1) mismatch", + scriptHex: "6a15" + // 20 bytes memo, but length is set to 21(0x15) + "67ed0bcc4e1256bc2ce87d22e190d63a120114bf", + errMsg: "memo size mismatch", + }, + { + name: "should return error when memo size >= 76 (OP_PUSHDATA1) but script is too short", + scriptHex: "6a4c", // 2 bytes only, requires at least 3 bytes + errMsg: "script too short", + }, + { + name: "should return error on memo size >= 76 (OP_PUSHDATA1) mismatch", + scriptHex: "6a4c4e" + // 79 bytes memo, but length is set to 78(0x4e) + "5a0110070a30d55c1031d30dab3b3d85f47b8f1d03df2d480961207061796c6f61642c626372743171793970716d6b32706439737636336732376a7438723635377779306439756565347832647432", + errMsg: "memo size mismatch", + }, + { + name: "should return error on invalid OP_RETURN", + scriptHex: "6a4d0001", // OP_PUSHDATA2, length is set to 256 (0x0001, little-endian) + errMsg: "invalid OP_RETURN script", + }, + } - t.Run("should return nil memo on donation tx", func(t *testing.T) { - // use donation sctipt "6a0a4920616d207269636821" - scriptDonation := "6a0a" + hex.EncodeToString([]byte(constant.DonationMessage)) - memo, found, err := bitcoin.DecodeOpReturnMemo(scriptDonation, txHash) - require.ErrorContains(t, err, "donation tx") - require.False(t, found) - require.Nil(t, memo) - }) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + memo, found, err := bitcoin.DecodeOpReturnMemo(tt.scriptHex) + require.ErrorContains(t, err, tt.errMsg) + require.False(t, found) + require.Nil(t, memo) + }) + } } func TestDecodeSenderFromScript(t *testing.T) { @@ -643,8 +659,8 @@ func TestDecodeScript(t *testing.T) { }) t.Run("decode error due to missing data for public key", func(t *testing.T) { - // missing OP_ENDIF at the end - data := "2001a7bae79bd61c2368fe41a565061d6cf22b4f509fbc1652caea06d98b8fd0" + // require OP_DATA_32 but OP_DATA_31 is given + data := "1f01a7bae79bd61c2368fe41a565061d6cf22b4f509fbc1652caea06d98b8fd0" script, _ := hex.DecodeString(data) memo, isFound, err := bitcoin.DecodeScript(script) diff --git a/zetaclient/chains/evm/observer/inbound.go b/zetaclient/chains/evm/observer/inbound.go index 02ab1a0b6f..cb409f408e 100644 --- a/zetaclient/chains/evm/observer/inbound.go +++ b/zetaclient/chains/evm/observer/inbound.go @@ -38,7 +38,7 @@ import ( // TODO(revamp): move ticker function to a separate file func (ob *Observer) WatchInbound(ctx context.Context) error { sampledLogger := ob.Logger().Inbound.Sample(&zerolog.BasicSampler{N: 10}) - interval := ticker.SecondsFromUint64(ob.ChainParams().InboundTicker) + interval := ticker.DurationFromUint64Seconds(ob.ChainParams().InboundTicker) task := func(ctx context.Context, t *ticker.Ticker) error { return ob.watchInboundOnce(ctx, t, sampledLogger) } @@ -70,7 +70,7 @@ func (ob *Observer) watchInboundOnce(ctx context.Context, t *ticker.Ticker, samp ob.Logger().Inbound.Err(err).Msg("WatchInbound: observeInbound error") } - newInterval := ticker.SecondsFromUint64(ob.ChainParams().InboundTicker) + newInterval := ticker.DurationFromUint64Seconds(ob.ChainParams().InboundTicker) t.SetInterval(newInterval) return nil diff --git a/zetaclient/chains/evm/observer/outbound.go b/zetaclient/chains/evm/observer/outbound.go index 0bab913592..f8ce8f32ba 100644 --- a/zetaclient/chains/evm/observer/outbound.go +++ b/zetaclient/chains/evm/observer/outbound.go @@ -450,6 +450,7 @@ func (ob *Observer) FilterTSSOutboundInBlock(ctx context.Context, blockNumber ui for i := range block.Transactions { tx := block.Transactions[i] if ethcommon.HexToAddress(tx.From) == ob.TSS().EVMAddress() { + // #nosec G115 nonce always positive nonce := uint64(tx.Nonce) if !ob.IsTxConfirmed(nonce) { if receipt, txx, ok := ob.checkConfirmedTx(ctx, tx.Hash, nonce); ok { diff --git a/zetaclient/chains/evm/observer/v2_inbound.go b/zetaclient/chains/evm/observer/v2_inbound.go index b19f0e9f85..9688851af6 100644 --- a/zetaclient/chains/evm/observer/v2_inbound.go +++ b/zetaclient/chains/evm/observer/v2_inbound.go @@ -192,7 +192,7 @@ func (ob *Observer) newDepositInboundVote(event *gatewayevm.GatewayEVMDeposited) hex.EncodeToString(event.Payload), event.Raw.TxHash.Hex(), event.Raw.BlockNumber, - 1_500_000, + zetacore.PostVoteInboundCallOptionsGasLimit, coinType, event.Asset.Hex(), event.Raw.Index, @@ -328,7 +328,7 @@ func (ob *Observer) newCallInboundVote(event *gatewayevm.GatewayEVMCalled) types hex.EncodeToString(event.Payload), event.Raw.TxHash.Hex(), event.Raw.BlockNumber, - 1_500_000, + zetacore.PostVoteInboundCallOptionsGasLimit, coin.CoinType_NoAssetCall, "", event.Raw.Index, diff --git a/zetaclient/chains/solana/observer/inbound.go b/zetaclient/chains/solana/observer/inbound.go index 1441150ada..4c93d95470 100644 --- a/zetaclient/chains/solana/observer/inbound.go +++ b/zetaclient/chains/solana/observer/inbound.go @@ -281,7 +281,7 @@ func (ob *Observer) ParseInboundAsDeposit( } // check if the instruction is a deposit or not - if inst.Discriminator != solanacontracts.DiscriminatorDeposit() { + if inst.Discriminator != solanacontracts.DiscriminatorDeposit { return nil, nil } diff --git a/zetaclient/chains/solana/observer/outbound.go b/zetaclient/chains/solana/observer/outbound.go index e185b1a27d..60bd70bec7 100644 --- a/zetaclient/chains/solana/observer/outbound.go +++ b/zetaclient/chains/solana/observer/outbound.go @@ -157,6 +157,7 @@ func (ob *Observer) VoteOutboundIfConfirmed(ctx context.Context, cctx *crosschai // the amount and status of the outbound outboundAmount := new(big.Int).SetUint64(inst.TokenAmount()) + // status was already verified as successful in CheckFinalizedTx outboundStatus := chains.ReceiveStatus_success @@ -295,6 +296,7 @@ func (ob *Observer) CheckFinalizedTx( logger.Error().Err(err).Msg("ParseGatewayInstruction error") return nil, false } + txNonce := inst.GatewayNonce() // recover ECDSA signer from instruction @@ -352,6 +354,8 @@ func ParseGatewayInstruction( switch coinType { case coin.CoinType_Gas: return contracts.ParseInstructionWithdraw(instruction) + case coin.CoinType_Cmd: + return contracts.ParseInstructionWhitelist(instruction) default: return nil, fmt.Errorf("unsupported outbound coin type %s", coinType) } diff --git a/zetaclient/chains/solana/observer/outbound_test.go b/zetaclient/chains/solana/observer/outbound_test.go index 5cb2b80a5c..73af8da573 100644 --- a/zetaclient/chains/solana/observer/outbound_test.go +++ b/zetaclient/chains/solana/observer/outbound_test.go @@ -35,6 +35,9 @@ const ( // tssAddressTest is the TSS address for testing tssAddressTest = "0x05C7dBdd1954D59c9afaB848dA7d8DD3F35e69Cd" + + // whitelistTxTest is local devnet tx result for testing + whitelistTxTest = "phM9bESbiqojmpkkUxgjed8EABkxvPGNau9q31B8Yk1sXUtsxJvd6G9VbZZQPsEyn6RiTH4YBtqJ89omqfbbNNY" ) // createTestObserver creates a test observer for testing @@ -294,3 +297,63 @@ func Test_ParseInstructionWithdraw(t *testing.T) { require.Nil(t, inst) }) } + +func Test_ParseInstructionWhitelist(t *testing.T) { + // the test chain and transaction hash + chain := chains.SolanaDevnet + txHash := whitelistTxTest + txAmount := uint64(0) + + t.Run("should parse instruction whitelist", func(t *testing.T) { + // tss address used in local devnet + tssAddress := "0x7E8c7bAcd3c6220DDC35A4EA1141BE14F2e1dFEB" + // load and unmarshal archived transaction + txResult := testutils.LoadSolanaOutboundTxResult(t, TestDataDir, chain.ChainId, txHash) + tx, err := txResult.Transaction.GetTransaction() + require.NoError(t, err) + + instruction := tx.Message.Instructions[0] + inst, err := contracts.ParseInstructionWhitelist(instruction) + require.NoError(t, err) + + // check sender, nonce and amount + sender, err := inst.Signer() + require.NoError(t, err) + require.Equal(t, tssAddress, sender.String()) + require.EqualValues(t, inst.GatewayNonce(), 3) + require.EqualValues(t, inst.TokenAmount(), txAmount) + }) + + t.Run("should return error on invalid instruction data", func(t *testing.T) { + // load and unmarshal archived transaction + txResult := testutils.LoadSolanaOutboundTxResult(t, TestDataDir, chain.ChainId, txHash) + txFake, err := txResult.Transaction.GetTransaction() + require.NoError(t, err) + + // set invalid instruction data + instruction := txFake.Message.Instructions[0] + instruction.Data = []byte("invalid instruction data") + + inst, err := contracts.ParseInstructionWhitelist(instruction) + require.ErrorContains(t, err, "error deserializing instruction") + require.Nil(t, inst) + }) + + t.Run("should return error on discriminator mismatch", func(t *testing.T) { + // load and unmarshal archived transaction + txResult := testutils.LoadSolanaOutboundTxResult(t, TestDataDir, chain.ChainId, txHash) + txFake, err := txResult.Transaction.GetTransaction() + require.NoError(t, err) + + // overwrite discriminator (first 8 bytes) + instruction := txFake.Message.Instructions[0] + fakeDiscriminator := "b712469c946da12100980d0000000000" + fakeDiscriminatorBytes, err := hex.DecodeString(fakeDiscriminator) + require.NoError(t, err) + copy(instruction.Data, fakeDiscriminatorBytes) + + inst, err := contracts.ParseInstructionWhitelist(instruction) + require.ErrorContains(t, err, "not a whitelist_spl_mint instruction") + require.Nil(t, inst) + }) +} diff --git a/zetaclient/chains/solana/signer/signer.go b/zetaclient/chains/solana/signer/signer.go index 0d762d9006..8e180f8c7f 100644 --- a/zetaclient/chains/solana/signer/signer.go +++ b/zetaclient/chains/solana/signer/signer.go @@ -2,11 +2,14 @@ package signer import ( "context" + "fmt" + "strings" "cosmossdk.io/errors" ethcommon "github.com/ethereum/go-ethereum/common" "github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go/rpc" + "github.com/rs/zerolog" "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/pkg/coin" @@ -121,12 +124,71 @@ func (signer *Signer) TryProcessOutbound( chainID := signer.Chain().ChainId nonce := params.TssNonce coinType := cctx.InboundParams.CoinType - if coinType != coin.CoinType_Gas { + + // skip relaying the transaction if this signer hasn't set the relayer key + if !signer.HasRelayerKey() { + logger.Warn().Msgf("TryProcessOutbound: no relayer key configured") + return + } + + var tx *solana.Transaction + + switch coinType { + case coin.CoinType_Cmd: + whitelistTx, err := signer.prepareWhitelistTx(ctx, cctx, height) + if err != nil { + logger.Error().Err(err).Msgf("TryProcessOutbound: Fail to sign whitelist outbound") + return + } + + tx = whitelistTx + + case coin.CoinType_Gas: + withdrawTx, err := signer.prepareWithdrawTx(ctx, cctx, height, logger) + if err != nil { + logger.Error().Err(err).Msgf("TryProcessOutbound: Fail to sign withdraw outbound") + return + } + + tx = withdrawTx + default: + logger.Error(). + Msgf("TryProcessOutbound: can only send SOL to the Solana network") + return + } + + // set relayer balance metrics + signer.SetRelayerBalanceMetrics(ctx) + + // broadcast the signed tx to the Solana network with preflight check + txSig, err := signer.client.SendTransactionWithOpts( + ctx, + tx, + // Commitment "finalized" is too conservative for preflight check and + // it results in repeated broadcast attempts that only 1 will succeed. + // Commitment "processed" will simulate tx against more recent state + // thus fails faster once a tx is already broadcasted and processed by the cluster. + // This reduces the number of "failed" txs due to repeated broadcast attempts. + rpc.TransactionOpts{PreflightCommitment: rpc.CommitmentProcessed}, + ) + if err != nil { logger.Error(). - Msgf("TryProcessOutbound: can only send SOL to the Solana network for chain %d nonce %d", chainID, nonce) + Err(err). + Msgf("TryProcessOutbound: broadcast error") return } + // report the outbound to the outbound tracker + signer.reportToOutboundTracker(ctx, zetacoreClient, chainID, nonce, txSig, logger) +} + +func (signer *Signer) prepareWithdrawTx( + ctx context.Context, + cctx *types.CrossChainTx, + height uint64, + logger zerolog.Logger, +) (*solana.Transaction, error) { + params := cctx.GetCurrentOutboundParam() // compliance check cancelTx := compliance.IsCctxRestricted(cctx) if cancelTx { @@ -134,7 +196,7 @@ func (signer *Signer) TryProcessOutbound( logger, signer.Logger().Compliance, true, - chainID, + signer.Chain().ChainId, cctx.Index, cctx.InboundParams.Sender, params.Receiver, @@ -143,48 +205,55 @@ func (signer *Signer) TryProcessOutbound( } // sign gateway withdraw message by TSS - msg, err := signer.SignMsgWithdraw(ctx, params, height, cancelTx) + msg, err := signer.createAndSignMsgWithdraw(ctx, params, height, cancelTx) if err != nil { - logger.Error().Err(err).Msgf("TryProcessOutbound: SignMsgWithdraw error for chain %d nonce %d", chainID, nonce) - return + return nil, err } - // skip relaying the transaction if this signer hasn't set the relayer key - if !signer.HasRelayerKey() { - return + // sign the withdraw transaction by relayer key + tx, err := signer.signWithdrawTx(ctx, *msg) + if err != nil { + return nil, err } - // set relayer balance metrics - signer.SetRelayerBalanceMetrics(ctx) + return tx, nil +} - // sign the withdraw transaction by relayer key - tx, err := signer.SignWithdrawTx(ctx, *msg) +func (signer *Signer) prepareWhitelistTx( + ctx context.Context, + cctx *types.CrossChainTx, + height uint64, +) (*solana.Transaction, error) { + params := cctx.GetCurrentOutboundParam() + relayedMsg := strings.Split(cctx.RelayedMessage, ":") + if len(relayedMsg) != 2 { + return nil, fmt.Errorf("TryProcessOutbound: invalid relayed msg") + } + + pk, err := solana.PublicKeyFromBase58(relayedMsg[1]) if err != nil { - logger.Error().Err(err).Msgf("TryProcessOutbound: SignGasWithdraw error for chain %d nonce %d", chainID, nonce) - return + return nil, err } - // broadcast the signed tx to the Solana network with preflight check - txSig, err := signer.client.SendTransactionWithOpts( - ctx, - tx, - // Commitment "finalized" is too conservative for preflight check and - // it results in repeated broadcast attempts that only 1 will succeed. - // Commitment "processed" will simulate tx against more recent state - // thus fails faster once a tx is already broadcasted and processed by the cluster. - // This reduces the number of "failed" txs due to repeated broadcast attempts. - rpc.TransactionOpts{PreflightCommitment: rpc.CommitmentProcessed}, - ) + seed := [][]byte{[]byte("whitelist"), pk.Bytes()} + whitelistEntryPDA, _, err := solana.FindProgramAddress(seed, signer.gatewayID) if err != nil { - signer.Logger(). - Std.Warn(). - Err(err). - Msgf("TryProcessOutbound: broadcast error for chain %d nonce %d", chainID, nonce) - return + return nil, err } - // report the outbound to the outbound tracker - signer.reportToOutboundTracker(ctx, zetacoreClient, chainID, nonce, txSig, logger) + // sign gateway whitelist message by TSS + msg, err := signer.createAndSignMsgWhitelist(ctx, params, height, pk, whitelistEntryPDA) + if err != nil { + return nil, err + } + + // sign the whitelist transaction by relayer key + tx, err := signer.signWhitelistTx(ctx, msg) + if err != nil { + return nil, err + } + + return tx, nil } // SetGatewayAddress sets the gateway address diff --git a/zetaclient/chains/solana/signer/whitelist.go b/zetaclient/chains/solana/signer/whitelist.go new file mode 100644 index 0000000000..73ee769039 --- /dev/null +++ b/zetaclient/chains/solana/signer/whitelist.go @@ -0,0 +1,125 @@ +package signer + +import ( + "context" + + "cosmossdk.io/errors" + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/rpc" + "github.com/near/borsh-go" + + contracts "github.com/zeta-chain/node/pkg/contracts/solana" + "github.com/zeta-chain/node/x/crosschain/types" +) + +// createAndSignMsgWhitelist creates and signs a whitelist message (for gateway whitelist_spl_mint instruction) with TSS. +func (signer *Signer) createAndSignMsgWhitelist( + ctx context.Context, + params *types.OutboundParams, + height uint64, + whitelistCandidate solana.PublicKey, + whitelistEntry solana.PublicKey, +) (*contracts.MsgWhitelist, error) { + chain := signer.Chain() + // #nosec G115 always positive + chainID := uint64(signer.Chain().ChainId) + nonce := params.TssNonce + + // prepare whitelist msg and compute hash + msg := contracts.NewMsgWhitelist(whitelistCandidate, whitelistEntry, chainID, nonce) + msgHash := msg.Hash() + + // sign the message with TSS to get an ECDSA signature. + // the produced signature is in the [R || S || V] format where V is 0 or 1. + signature, err := signer.TSS().Sign(ctx, msgHash[:], height, nonce, chain.ChainId, "") + if err != nil { + return nil, errors.Wrap(err, "Key-sign failed") + } + signer.Logger().Std.Info().Msgf("Key-sign succeed for chain %d nonce %d", chainID, nonce) + + // attach the signature and return + return msg.SetSignature(signature), nil +} + +// signWhitelistTx wraps the whitelist 'msg' into a Solana transaction and signs it with the relayer key. +func (signer *Signer) signWhitelistTx(ctx context.Context, msg *contracts.MsgWhitelist) (*solana.Transaction, error) { + // create whitelist_spl_mint instruction with program call data + var err error + var inst solana.GenericInstruction + inst.DataBytes, err = borsh.Serialize(contracts.WhitelistInstructionParams{ + Discriminator: contracts.DiscriminatorWhitelistSplMint, + Signature: msg.SigRS(), + RecoveryID: msg.SigV(), + MessageHash: msg.Hash(), + Nonce: msg.Nonce(), + }) + if err != nil { + return nil, errors.Wrap(err, "cannot serialize whitelist_spl_mint instruction") + } + + // attach required accounts to the instruction + privkey := signer.relayerKey + attachWhitelistAccounts( + &inst, + privkey.PublicKey(), + signer.pda, + msg.WhitelistCandidate(), + msg.WhitelistEntry(), + signer.gatewayID, + ) + + // get a recent blockhash + recent, err := signer.client.GetLatestBlockhash(ctx, rpc.CommitmentFinalized) + if err != nil { + return nil, errors.Wrap(err, "GetLatestBlockhash error") + } + + // create a transaction that wraps the instruction + tx, err := solana.NewTransaction( + []solana.Instruction{ + // TODO: outbound now uses 5K lamports as the fixed fee, we could explore priority fee and compute budget + // https://github.com/zeta-chain/node/issues/2599 + // programs.ComputeBudgetSetComputeUnitLimit(computeUnitLimit), + // programs.ComputeBudgetSetComputeUnitPrice(computeUnitPrice), + &inst}, + recent.Value.Blockhash, + solana.TransactionPayer(privkey.PublicKey()), + ) + if err != nil { + return nil, errors.Wrap(err, "NewTransaction error") + } + + // relayer signs the transaction + _, err = tx.Sign(func(key solana.PublicKey) *solana.PrivateKey { + if key.Equals(privkey.PublicKey()) { + return privkey + } + return nil + }) + if err != nil { + return nil, errors.Wrap(err, "signer unable to sign transaction") + } + + return tx, nil +} + +// attachWhitelistAccounts attaches the required accounts for the gateway whitelist instruction. +func attachWhitelistAccounts( + inst *solana.GenericInstruction, + signer solana.PublicKey, + pda solana.PublicKey, + whitelistCandidate solana.PublicKey, + whitelistEntry solana.PublicKey, + gatewayID solana.PublicKey, +) { + // attach required accounts to the instruction + var accountSlice []*solana.AccountMeta + accountSlice = append(accountSlice, solana.Meta(whitelistEntry).WRITE()) + accountSlice = append(accountSlice, solana.Meta(whitelistCandidate)) + accountSlice = append(accountSlice, solana.Meta(pda).WRITE()) + accountSlice = append(accountSlice, solana.Meta(signer).WRITE().SIGNER()) + accountSlice = append(accountSlice, solana.Meta(solana.SystemProgramID)) + inst.ProgID = gatewayID + + inst.AccountValues = accountSlice +} diff --git a/zetaclient/chains/solana/signer/withdraw.go b/zetaclient/chains/solana/signer/withdraw.go index 58411b43bb..51f4cceeea 100644 --- a/zetaclient/chains/solana/signer/withdraw.go +++ b/zetaclient/chains/solana/signer/withdraw.go @@ -13,8 +13,8 @@ import ( "github.com/zeta-chain/node/x/crosschain/types" ) -// SignMsgWithdraw signs a withdraw message (for gateway withdraw/withdraw_spl instruction) with TSS. -func (signer *Signer) SignMsgWithdraw( +// createAndSignMsgWithdraw creates and signs a withdraw message (for gateway withdraw/withdraw_spl instruction) with TSS. +func (signer *Signer) createAndSignMsgWithdraw( ctx context.Context, params *types.OutboundParams, height uint64, @@ -53,13 +53,13 @@ func (signer *Signer) SignMsgWithdraw( return msg.SetSignature(signature), nil } -// SignWithdrawTx wraps the withdraw 'msg' into a Solana transaction and signs it with the relayer key. -func (signer *Signer) SignWithdrawTx(ctx context.Context, msg contracts.MsgWithdraw) (*solana.Transaction, error) { +// signWithdrawTx wraps the withdraw 'msg' into a Solana transaction and signs it with the relayer key. +func (signer *Signer) signWithdrawTx(ctx context.Context, msg contracts.MsgWithdraw) (*solana.Transaction, error) { // create withdraw instruction with program call data var err error var inst solana.GenericInstruction inst.DataBytes, err = borsh.Serialize(contracts.WithdrawInstructionParams{ - Discriminator: contracts.DiscriminatorWithdraw(), + Discriminator: contracts.DiscriminatorWithdraw, Amount: msg.Amount(), Signature: msg.SigRS(), RecoveryID: msg.SigV(), diff --git a/zetaclient/chains/ton/config.go b/zetaclient/chains/ton/config.go index 731287756e..c3c0e3da3e 100644 --- a/zetaclient/chains/ton/config.go +++ b/zetaclient/chains/ton/config.go @@ -7,11 +7,23 @@ import ( "net/url" "time" + "github.com/pkg/errors" "github.com/tonkeeper/tongo/config" + "github.com/tonkeeper/tongo/liteapi" + "github.com/tonkeeper/tongo/tlb" ) type GlobalConfigurationFile = config.GlobalConfigurationFile +// ConfigGetter represents LiteAPI config params getter. +// Don't be confused because config param in this case represent on-chain params, +// not lite-client's ADNL json config to connect to the network. +// +// Read more at https://docs.ton.org/develop/howto/blockchain-configs +type ConfigGetter interface { + GetConfigParams(ctx context.Context, mode liteapi.ConfigMode, params []uint32) (tlb.ConfigParams, error) +} + // ConfigFromURL downloads & parses lite server config. // //nolint:gosec @@ -52,3 +64,67 @@ func ConfigFromSource(ctx context.Context, urlOrPath string) (*GlobalConfigurati return ConfigFromPath(urlOrPath) } + +// FetchGasConfig fetches gas price from the config. +func FetchGasConfig(ctx context.Context, getter ConfigGetter) (tlb.GasLimitsPrices, error) { + // https://docs.ton.org/develop/howto/blockchain-configs + // https://tonviewer.com/config#21 + const configKeyGas = 21 + + response, err := getter.GetConfigParams(ctx, 0, []uint32{configKeyGas}) + if err != nil { + return tlb.GasLimitsPrices{}, errors.Wrap(err, "failed to get config params") + } + + ref, ok := response.Config.Get(configKeyGas) + if !ok { + return tlb.GasLimitsPrices{}, errors.Errorf("config key %d not found", configKeyGas) + } + + var cfg tlb.ConfigParam21 + if err = tlb.Unmarshal(&ref.Value, &cfg); err != nil { + return tlb.GasLimitsPrices{}, errors.Wrap(err, "failed to unmarshal config param") + } + + return cfg.GasLimitsPrices, nil +} + +// ParseGasPrice parses gas price from the config and returns price in tons per 1 gas unit. +// You can take a look at definitions here: +// https://github.com/ton-blockchain/ton/blob/master/crypto/block/block.tlb +// https://docs.ton.org/develop/howto/blockchain-configs#param-20-and-21 +// +// gas_prices#dd gas_price:uint64 gas_limit:uint64 gas_credit:uint64 +// block_gas_limit:uint64 freeze_due_limit:uint64 delete_due_limit:uint64 = GasLimitsPrices; +// +// gas_prices_ext#de gas_price:uint64 gas_limit:uint64 special_gas_limit:uint64 gas_credit:uint64 +// block_gas_limit:uint64 freeze_due_limit:uint64 delete_due_limit:uint64 = GasLimitsPrices; +// +// gas_flat_pfx#d1 flat_gas_limit:uint64 flat_gas_price:uint64 other:GasLimitsPrices = GasLimitsPrices; +func ParseGasPrice(cfg tlb.GasLimitsPrices) (uint64, error) { + // tongo lib uses a concept of "sum types" + // to decode different (sub)type of entities. + // Basically, sumType is a struct property that is not empty (i.e. decoded). + const ( + sumTypeGasPrices = "GasPrices" + sumTypeGasPricesExt = "GasPricesExt" + sumTypeGasFlatPfx = "GasFlatPfx" + ) + + // from TON docs: gas_price: This parameter reflects + // the price of gas in the network, in nano tons per 65536 gas units (2^16). + // We have 3 cases because TON node might return on of these 3 structs. + switch cfg.SumType { + case sumTypeGasPrices: + return cfg.GasPrices.GasPrice >> 16, nil + case sumTypeGasPricesExt: + return cfg.GasPricesExt.GasPrice >> 16, nil + case sumTypeGasFlatPfx: + if cfg.GasFlatPfx.Other == nil { + return 0, errors.New("GasFlatPfx.Other is nil") + } + return ParseGasPrice(*cfg.GasFlatPfx.Other) + default: + return 0, errors.Errorf("unknown SumType: %q", cfg.SumType) + } +} diff --git a/zetaclient/chains/ton/liteapi/client.go b/zetaclient/chains/ton/liteapi/client.go index 25b0efcf39..53f585899f 100644 --- a/zetaclient/chains/ton/liteapi/client.go +++ b/zetaclient/chains/ton/liteapi/client.go @@ -28,6 +28,8 @@ const ( blockCacheSize = 250 ) +var ErrNotFound = errors.New("not found") + // New Client constructor. func New(client *liteapi.Client) *Client { blockCache, _ := lru.New(blockCacheSize) @@ -189,6 +191,25 @@ func (c *Client) GetTransactionsSince( return result, nil } +// GetTransaction returns account's tx by logicalTime and hash or ErrNotFound. +func (c *Client) GetTransaction( + ctx context.Context, + acc ton.AccountID, + lt uint64, + hash ton.Bits256, +) (ton.Transaction, error) { + txs, err := c.GetTransactions(ctx, 1, acc, lt, hash) + if err != nil { + return ton.Transaction{}, err + } + + if len(txs) == 0 { + return ton.Transaction{}, ErrNotFound + } + + return txs[0], nil +} + // getLastTransactionHash returns logical time and hash of the last transaction func (c *Client) getLastTransactionHash(ctx context.Context, acc ton.AccountID) (uint64, tlb.Bits256, error) { state, err := c.GetAccountState(ctx, acc) @@ -203,6 +224,12 @@ func (c *Client) getLastTransactionHash(ctx context.Context, acc ton.AccountID) return state.LastTransLt, state.LastTransHash, nil } +// TransactionToHashString converts transaction's logicalTime and hash to string +// This string is used to store the last scanned hash (e.g. "123:0x123...") +func TransactionToHashString(tx ton.Transaction) string { + return TransactionHashToString(tx.Lt, ton.Bits256(tx.Hash())) +} + // TransactionHashToString converts logicalTime and hash to string func TransactionHashToString(lt uint64, hash ton.Bits256) string { return fmt.Sprintf("%d:%s", lt, hash.Hex()) diff --git a/zetaclient/chains/ton/liteapi/client_live_test.go b/zetaclient/chains/ton/liteapi/client_live_test.go index ed3c850dd8..f77f08420b 100644 --- a/zetaclient/chains/ton/liteapi/client_live_test.go +++ b/zetaclient/chains/ton/liteapi/client_live_test.go @@ -8,12 +8,15 @@ import ( "testing" "time" + "cosmossdk.io/math" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tonkeeper/tongo/config" "github.com/tonkeeper/tongo/liteapi" "github.com/tonkeeper/tongo/tlb" "github.com/tonkeeper/tongo/ton" + toncontracts "github.com/zeta-chain/node/pkg/contracts/ton" + zetaton "github.com/zeta-chain/node/zetaclient/chains/ton" "github.com/zeta-chain/node/zetaclient/common" ) @@ -136,6 +139,55 @@ func TestClient(t *testing.T) { require.NotZero(t, header.MinRefMcSeqno) require.Equal(t, header.MinRefMcSeqno, header.MasterRef.Master.SeqNo) }) + + t.Run("GetMasterchainInfo", func(t *testing.T) { + // ARRANGE + // all bits are 1 (0xFFF...) or also `-1` in TON's notation + const masterChain = uint32(1<<32 - 1) + + // ACT #1 + mc, err := client.GetMasterchainInfo(ctx) + + // ASSERT #1 + require.NoError(t, err) + require.Equal(t, masterChain, mc.Last.Workchain) + + // ACT #2 + block, err := client.GetBlockHeader(ctx, mc.Last.ToBlockIdExt(), 0) + + // ASSERT #2 + require.NoError(t, err) + require.False(t, block.NotMaster) + + // Check that block was generated less than 10 seconds ago + blockTime := time.Unix(int64(block.GenUtime), 0).UTC() + since := time.Since(blockTime) + + assert.LessOrEqual(t, since, 20*time.Second) + + t.Logf("Masterchain block #%d is generated at %q (%s ago)", block.SeqNo, blockTime, since.String()) + }) + + t.Run("GetGasConfig", func(t *testing.T) { + // ACT #1 + gas, err := zetaton.FetchGasConfig(ctx, client) + + // ASSERT #1 + require.NoError(t, err) + require.NotEmpty(t, gas) + + // ACT #2 + gasPrice, err := zetaton.ParseGasPrice(gas) + + // ASSERT #2 + require.NoError(t, err) + require.NotEmpty(t, gasPrice) + + gasPricePer1000 := math.NewUint(1000 * gasPrice) + + t.Logf("Gas cost: %s per 1000 gas", toncontracts.FormatCoins(gasPricePer1000)) + t.Logf("Compare with https://tonwhales.com/explorer/network") + }) } func mustCreateClient(t *testing.T) *liteapi.Client { diff --git a/zetaclient/chains/ton/observer/inbound.go b/zetaclient/chains/ton/observer/inbound.go index 95f9a510d7..092bfa0e9d 100644 --- a/zetaclient/chains/ton/observer/inbound.go +++ b/zetaclient/chains/ton/observer/inbound.go @@ -19,35 +19,43 @@ import ( ) const ( - // MaxTransactionsPerTick is the maximum number of transactions to process on a ticker - MaxTransactionsPerTick = 100 + // maximum number of transactions to process on a ticker + // TODO: move to config + // https://github.com/zeta-chain/node/issues/3086 + maxTransactionsPerTick = 100 + // zero log sample rate for sampled logger (to avoid spamming logs) + logSampleRate = 10 ) +// watchInbound watches for new txs to Gateway's account. func (ob *Observer) watchInbound(ctx context.Context) error { + return ob.inboundTicker(ctx, "WatchInbound", ob.observeGateway) +} + +func (ob *Observer) watchInboundTracker(ctx context.Context) error { + return ob.inboundTicker(ctx, "WatchInboundTracker", ob.processInboundTrackers) +} + +func (ob *Observer) inboundTicker(ctx context.Context, taskName string, taskFunc func(context.Context) error) error { app, err := zctx.FromContext(ctx) if err != nil { return err } - var ( - chainID = ob.Chain().ChainId - initialInterval = ticker.SecondsFromUint64(ob.ChainParams().InboundTicker) - sampledLogger = ob.Logger().Inbound.Sample(&zerolog.BasicSampler{N: 10}) - ) - - ob.Logger().Inbound.Info().Msgf("WatchInbound started for chain %d", chainID) + initialInterval := ticker.DurationFromUint64Seconds(ob.ChainParams().InboundTicker) + sampledLogger := ob.Logger().Inbound.Sample(&zerolog.BasicSampler{N: logSampleRate}) task := func(ctx context.Context, t *ticker.Ticker) error { if !app.IsInboundObservationEnabled() { - sampledLogger.Info().Msgf("WatchInbound: inbound observation is disabled for chain %d", chainID) + sampledLogger.Info().Msgf("%s: inbound observation is disabled", taskName) return nil } - if err := ob.observeInbound(ctx); err != nil { - ob.Logger().Inbound.Err(err).Msg("WatchInbound: observeInbound error") + if err := taskFunc(ctx); err != nil { + ob.Logger().Inbound.Err(err).Msgf("%s failed", taskName) } - newInterval := ticker.SecondsFromUint64(ob.ChainParams().InboundTicker) + newInterval := ticker.DurationFromUint64Seconds(ob.ChainParams().InboundTicker) t.SetInterval(newInterval) return nil @@ -58,11 +66,15 @@ func (ob *Observer) watchInbound(ctx context.Context) error { initialInterval, task, ticker.WithStopChan(ob.StopChannel()), - ticker.WithLogger(ob.Logger().Inbound, "WatchInbound"), + ticker.WithLogger(ob.Logger().Inbound, taskName), ) } -func (ob *Observer) observeInbound(ctx context.Context) error { +// observeGateway observes Gateway's account for new transactions. +// Due to TON architecture we have to scan for all net-new transactions. +// The main purpose is to observe inbounds from TON. +// Note that we might also have *outbounds* here (if a signer broadcasts a tx, it will be observed here). +func (ob *Observer) observeGateway(ctx context.Context) error { if err := ob.ensureLastScannedTX(ctx); err != nil { return errors.Wrap(err, "unable to ensure last scanned tx") } @@ -82,53 +94,138 @@ func (ob *Observer) observeInbound(ctx context.Context) error { case len(txs) == 0: // noop return nil - case len(txs) > MaxTransactionsPerTick: + case len(txs) > maxTransactionsPerTick: ob.Logger().Inbound.Info(). - Msgf("observeInbound: got %d transactions. Taking first %d", len(txs), MaxTransactionsPerTick) + Msgf("observeGateway: got %d transactions. Taking first %d", len(txs), maxTransactionsPerTick) - txs = txs[:MaxTransactionsPerTick] + txs = txs[:maxTransactionsPerTick] default: - ob.Logger().Inbound.Info().Msgf("observeInbound: got %d transactions", len(txs)) + ob.Logger().Inbound.Info().Msgf("observeGateway: got %d transactions", len(txs)) } for i := range txs { - tx := txs[i] - - parsedTX, skip, err := ob.gateway.ParseAndFilter(tx, toncontracts.FilterInbounds) - if err != nil { - return errors.Wrap(err, "unable to parse and filter tx") + var skip bool + + tx, err := ob.gateway.ParseTransaction(txs[i]) + switch { + case errors.Is(err, toncontracts.ErrParse) || errors.Is(err, toncontracts.ErrUnknownOp): + skip = true + case err != nil: + // should not happen + return errors.Wrap(err, "unexpected error") + case tx.ExitCode != 0: + skip = true + ob.Logger().Inbound.Warn().Fields(txLogFields(tx)).Msg("observeGateway: observed a failed tx") } if skip { - ob.Logger().Inbound.Info().Fields(txLogFields(&tx)).Msg("observeInbound: skipping tx") - ob.setLastScannedTX(&tx) + tx = &toncontracts.Transaction{Transaction: txs[i]} + txHash := liteapi.TransactionToHashString(tx.Transaction) + ob.Logger().Inbound.Warn().Str("transaction.hash", txHash).Msg("observeGateway: skipping tx") + ob.setLastScannedTX(tx) + continue + } + + // Should not happen + //goland:noinspection GoDfaConstantCondition + if tx == nil { + return errors.New("tx is nil") + } + // As we might have outbounds here, let's ensure outbound tracker. + // TON signer broadcasts ExtInMsgInfo with `src=null, dest=gateway`, so it will be observed here + if tx.IsOutbound() { + if err = ob.addOutboundTracker(ctx, tx); err != nil { + ob.Logger().Inbound. + Error().Err(err). + Fields(txLogFields(tx)). + Msg("observeGateway: unable to add outbound tracker") + + return errors.Wrap(err, "unable to add outbound tracker") + } + + ob.setLastScannedTX(tx) continue } - if _, err := ob.voteInbound(ctx, parsedTX); err != nil { + // Ok, let's process a new inbound tx + if _, err := ob.voteInbound(ctx, tx); err != nil { ob.Logger().Inbound. Error().Err(err). - Fields(txLogFields(&tx)). - Msg("observeInbound: unable to vote for tx") + Fields(txLogFields(tx)). + Msg("observeGateway: unable to vote for inbound tx") return errors.Wrapf(err, "unable to vote for inbound tx %s", tx.Hash().Hex()) } - ob.setLastScannedTX(&parsedTX.Transaction) + ob.setLastScannedTX(tx) + } + + return nil +} + +// processInboundTrackers handles adhoc trackers that were somehow missed by +func (ob *Observer) processInboundTrackers(ctx context.Context) error { + trackers, err := ob.ZetacoreClient().GetInboundTrackersForChain(ctx, ob.Chain().ChainId) + if err != nil { + return errors.Wrap(err, "unable to get inbound trackers") + } + + // noop + if len(trackers) == 0 { + return nil + } + + gatewayAccountID := ob.gateway.AccountID() + + // a single error should not block other trackers + for _, tracker := range trackers { + txHash := tracker.TxHash + + lt, hash, err := liteapi.TransactionHashFromString(txHash) + if err != nil { + ob.logSkippedTracker(txHash, "unable_to_parse_hash", err) + continue + } + + raw, err := ob.client.GetTransaction(ctx, gatewayAccountID, lt, hash) + if err != nil { + ob.logSkippedTracker(txHash, "unable_to_get_tx", err) + continue + } + + tx, err := ob.gateway.ParseTransaction(raw) + + switch { + case errors.Is(err, toncontracts.ErrParse) || errors.Is(err, toncontracts.ErrUnknownOp): + ob.logSkippedTracker(txHash, "unrelated_tx", err) + continue + case err != nil: + // should not happen + ob.logSkippedTracker(txHash, "unexpected_error", err) + continue + case tx.ExitCode != 0: + ob.logSkippedTracker(txHash, "failed_tx", nil) + continue + case tx.IsOutbound(): + ob.logSkippedTracker(txHash, "outbound_tx", nil) + continue + } + + if _, err := ob.voteInbound(ctx, tx); err != nil { + ob.logSkippedTracker(txHash, "vote_failed", err) + continue + } } return nil } +// Sends PostVoteInbound to zetacore func (ob *Observer) voteInbound(ctx context.Context, tx *toncontracts.Transaction) (string, error) { // noop if tx.Operation == toncontracts.OpDonate { - ob.Logger().Inbound.Info(). - Uint64("tx.lt", tx.Lt). - Str("tx.hash", tx.Hash().Hex()). - Msg("Thank you rich folk for your donation!") - + ob.Logger().Inbound.Info().Fields(txLogFields(tx)).Msg("Thank you rich folk for your donation!") return "", nil } @@ -222,18 +319,18 @@ func (ob *Observer) ensureLastScannedTX(ctx context.Context) error { return nil } - tx, _, err := ob.client.GetFirstTransaction(ctx, ob.gateway.AccountID()) + rawTX, _, err := ob.client.GetFirstTransaction(ctx, ob.gateway.AccountID()) if err != nil { return err } - ob.setLastScannedTX(tx) + ob.setLastScannedTX(&toncontracts.Transaction{Transaction: *rawTX}) return nil } -func (ob *Observer) setLastScannedTX(tx *ton.Transaction) { - txHash := liteapi.TransactionHashToString(tx.Lt, ton.Bits256(tx.Hash())) +func (ob *Observer) setLastScannedTX(tx *toncontracts.Transaction) { + txHash := liteapi.TransactionToHashString(tx.Transaction) ob.WithLastTxScanned(txHash) @@ -251,10 +348,22 @@ func (ob *Observer) setLastScannedTX(tx *ton.Transaction) { Msg("setLastScannedTX: WriteLastTxScannedToDB") } -func txLogFields(tx *ton.Transaction) map[string]any { +func (ob *Observer) logSkippedTracker(hash string, reason string, err error) { + ob.Logger().Inbound.Warn(). + Str("transaction.hash", hash). + Str("skip_reason", reason). + Err(err). + Msg("Skipping tracker") +} + +func txLogFields(tx *toncontracts.Transaction) map[string]any { return map[string]any{ - "inbound.ton.lt": tx.Lt, - "inbound.ton.hash": tx.Hash().Hex(), - "inbound.ton.block_id": tx.BlockID.BlockID.String(), + "transaction.hash": liteapi.TransactionToHashString(tx.Transaction), + "transaction.ton.lt": tx.Lt, + "transaction.ton.hash": tx.Hash().Hex(), + "transaction.ton.block_id": tx.BlockID.BlockID.String(), + "transaction.ton.is_inbound": tx.IsInbound(), + "transaction.ton.op_code": tx.Operation, + "transaction.ton.exit_code": tx.ExitCode, } } diff --git a/zetaclient/chains/ton/observer/inbound_test.go b/zetaclient/chains/ton/observer/inbound_test.go index 281db44f79..e0b7478cfa 100644 --- a/zetaclient/chains/ton/observer/inbound_test.go +++ b/zetaclient/chains/ton/observer/inbound_test.go @@ -2,6 +2,7 @@ package observer import ( "encoding/hex" + "fmt" "testing" "github.com/pkg/errors" @@ -11,14 +12,11 @@ import ( "github.com/tonkeeper/tongo/ton" toncontracts "github.com/zeta-chain/node/pkg/contracts/ton" "github.com/zeta-chain/node/testutil/sample" + cc "github.com/zeta-chain/node/x/crosschain/types" "github.com/zeta-chain/node/zetaclient/chains/ton/liteapi" ) func TestInbound(t *testing.T) { - gw := toncontracts.NewGateway( - ton.MustParseAccountID("0:997d889c815aeac21c47f86ae0e38383efc3c3463067582f6263ad48c5a1485b"), - ) - t.Run("No gateway provided", func(t *testing.T) { ts := newTestSuite(t) @@ -32,15 +30,15 @@ func TestInbound(t *testing.T) { ts := newTestSuite(t) // Given observer - ob, err := New(ts.baseObserver, ts.liteClient, gw) + ob, err := New(ts.baseObserver, ts.liteClient, ts.gateway) require.NoError(t, err) // Given mocked lite client call - ts.OnGetFirstTransaction(gw.AccountID(), nil, 0, errors.New("oops")).Once() + ts.OnGetFirstTransaction(ts.gateway.AccountID(), nil, 0, errors.New("oops")).Once() // ACT // Observe inbounds once - err = ob.observeInbound(ts.ctx) + err = ob.observeGateway(ts.ctx) // ASSERT assert.ErrorContains(t, err, "unable to ensure last scanned tx") @@ -52,21 +50,21 @@ func TestInbound(t *testing.T) { ts := newTestSuite(t) // Given mocked lite client calls - firstTX := sample.TONDonation(t, gw.AccountID(), toncontracts.Donation{ + firstTX := sample.TONDonation(t, ts.gateway.AccountID(), toncontracts.Donation{ Sender: sample.GenerateTONAccountID(), Amount: tonCoins(t, "1"), }) - ts.OnGetFirstTransaction(gw.AccountID(), &firstTX, 0, nil).Once() - ts.OnGetTransactionsSince(gw.AccountID(), firstTX.Lt, txHash(firstTX), nil, nil).Once() + ts.OnGetFirstTransaction(ts.gateway.AccountID(), &firstTX, 0, nil).Once() + ts.OnGetTransactionsSince(ts.gateway.AccountID(), firstTX.Lt, txHash(firstTX), nil, nil).Once() // Given observer - ob, err := New(ts.baseObserver, ts.liteClient, gw) + ob, err := New(ts.baseObserver, ts.liteClient, ts.gateway) require.NoError(t, err) // ACT // Observe inbounds once - err = ob.observeInbound(ts.ctx) + err = ob.observeGateway(ts.ctx) // ASSERT assert.NoError(t, err) @@ -88,13 +86,13 @@ func TestInbound(t *testing.T) { ts := newTestSuite(t) // Given observer - ob, err := New(ts.baseObserver, ts.liteClient, gw) + ob, err := New(ts.baseObserver, ts.liteClient, ts.gateway) require.NoError(t, err) - lastScanned := ts.SetupLastScannedTX(gw.AccountID()) + lastScanned := ts.SetupLastScannedTX(ts.gateway.AccountID()) // Given mocked lite client calls - donation := sample.TONDonation(t, gw.AccountID(), toncontracts.Donation{ + donation := sample.TONDonation(t, ts.gateway.AccountID(), toncontracts.Donation{ Sender: sample.GenerateTONAccountID(), Amount: tonCoins(t, "12"), }) @@ -102,12 +100,12 @@ func TestInbound(t *testing.T) { txs := []ton.Transaction{donation} ts. - OnGetTransactionsSince(gw.AccountID(), lastScanned.Lt, txHash(lastScanned), txs, nil). + OnGetTransactionsSince(ts.gateway.AccountID(), lastScanned.Lt, txHash(lastScanned), txs, nil). Once() // ACT // Observe inbounds once - err = ob.observeInbound(ts.ctx) + err = ob.observeGateway(ts.ctx) // ASSERT assert.NoError(t, err) @@ -124,10 +122,10 @@ func TestInbound(t *testing.T) { ts := newTestSuite(t) // Given observer - ob, err := New(ts.baseObserver, ts.liteClient, gw) + ob, err := New(ts.baseObserver, ts.liteClient, ts.gateway) require.NoError(t, err) - lastScanned := ts.SetupLastScannedTX(gw.AccountID()) + lastScanned := ts.SetupLastScannedTX(ts.gateway.AccountID()) // Given mocked lite client calls deposit := toncontracts.Deposit{ @@ -136,18 +134,18 @@ func TestInbound(t *testing.T) { Recipient: sample.EthAddress(), } - depositTX := sample.TONDeposit(t, gw.AccountID(), deposit) + depositTX := sample.TONDeposit(t, ts.gateway.AccountID(), deposit) txs := []ton.Transaction{depositTX} ts. - OnGetTransactionsSince(gw.AccountID(), lastScanned.Lt, txHash(lastScanned), txs, nil). + OnGetTransactionsSince(ts.gateway.AccountID(), lastScanned.Lt, txHash(lastScanned), txs, nil). Once() ts.MockGetBlockHeader(depositTX.BlockID) // ACT // Observe inbounds once - err = ob.observeInbound(ts.ctx) + err = ob.observeGateway(ts.ctx) // ASSERT assert.NoError(t, err) @@ -182,10 +180,10 @@ func TestInbound(t *testing.T) { ts := newTestSuite(t) // Given observer - ob, err := New(ts.baseObserver, ts.liteClient, gw) + ob, err := New(ts.baseObserver, ts.liteClient, ts.gateway) require.NoError(t, err) - lastScanned := ts.SetupLastScannedTX(gw.AccountID()) + lastScanned := ts.SetupLastScannedTX(ts.gateway.AccountID()) // Given mocked lite client calls const callData = "hey there" @@ -198,18 +196,18 @@ func TestInbound(t *testing.T) { CallData: []byte(callData), } - depositAndCallTX := sample.TONDepositAndCall(t, gw.AccountID(), depositAndCall) + depositAndCallTX := sample.TONDepositAndCall(t, ts.gateway.AccountID(), depositAndCall) txs := []ton.Transaction{depositAndCallTX} ts. - OnGetTransactionsSince(gw.AccountID(), lastScanned.Lt, txHash(lastScanned), txs, nil). + OnGetTransactionsSince(ts.gateway.AccountID(), lastScanned.Lt, txHash(lastScanned), txs, nil). Once() ts.MockGetBlockHeader(depositAndCallTX.BlockID) // ACT // Observe inbounds once - err = ob.observeInbound(ts.ctx) + err = ob.observeGateway(ts.ctx) // ASSERT assert.NoError(t, err) @@ -245,50 +243,108 @@ func TestInbound(t *testing.T) { assert.Equal(t, uint64(blockInfo.MinRefMcSeqno), cctx.InboundBlockHeight) }) + // Yep, it's possible to have withdrawals here because we scroll through all gateway's txs + t.Run("Withdrawal", func(t *testing.T) { + // ARRANGE + ts := newTestSuite(t) + + // Given observer + ob, err := New(ts.baseObserver, ts.liteClient, ts.gateway) + require.NoError(t, err) + + lastScanned := ts.SetupLastScannedTX(ts.gateway.AccountID()) + + // Given mocked lite client calls + withdrawal := toncontracts.Withdrawal{ + Recipient: ton.MustParseAccountID("EQB5A1PJBbnxwf0YrA_bgWKyfuIv8GywEcfIAXrs3oZyqc1_"), + Amount: toncontracts.Coins(5), + Seqno: 0, + } + + ts.sign(&withdrawal) + + withdrawalSigner, err := withdrawal.Signer() + require.NoError(t, err) + require.Equal(t, ob.TSS().EVMAddress().Hex(), withdrawalSigner.Hex()) + + withdrawalTX := sample.TONWithdrawal(t, ts.gateway.AccountID(), withdrawal) + txs := []ton.Transaction{withdrawalTX} + + ts. + OnGetTransactionsSince(ts.gateway.AccountID(), lastScanned.Lt, txHash(lastScanned), txs, nil). + Once() + + // ACT + err = ob.observeGateway(ts.ctx) + + // ASSERT + assert.NoError(t, err) + + // Check that no votes were sent + require.Len(t, ts.votesBag, 0) + + // But an outbound tracker was created + require.Len(t, ts.trackerBag, 1) + + tracker := ts.trackerBag[0] + + assert.Equal(t, uint64(withdrawal.Seqno), tracker.nonce) + assert.Equal(t, liteapi.TransactionToHashString(withdrawalTX), tracker.hash) + }) + t.Run("Multiple transactions", func(t *testing.T) { // ARRANGE ts := newTestSuite(t) // Given observer - ob, err := New(ts.baseObserver, ts.liteClient, gw) + ob, err := New(ts.baseObserver, ts.liteClient, ts.gateway) require.NoError(t, err) - lastScanned := ts.SetupLastScannedTX(gw.AccountID()) + lastScanned := ts.SetupLastScannedTX(ts.gateway.AccountID()) // Given several transactions + withdrawal := toncontracts.Withdrawal{ + Recipient: ton.MustParseAccountID("EQB5A1PJBbnxwf0YrA_bgWKyfuIv8GywEcfIAXrs3oZyqc1_"), + Amount: toncontracts.Coins(5), + Seqno: 1, + } + ts.sign(&withdrawal) + txs := []ton.Transaction{ // should be skipped - sample.TONDonation(t, gw.AccountID(), toncontracts.Donation{ + sample.TONDonation(t, ts.gateway.AccountID(), toncontracts.Donation{ Sender: sample.GenerateTONAccountID(), Amount: tonCoins(t, "1"), }), // should be voted - sample.TONDeposit(t, gw.AccountID(), toncontracts.Deposit{ + sample.TONDeposit(t, ts.gateway.AccountID(), toncontracts.Deposit{ Sender: sample.GenerateTONAccountID(), Amount: tonCoins(t, "3"), Recipient: sample.EthAddress(), }), // should be skipped (invalid inbound message) sample.TONTransaction(t, sample.TONTransactionProps{ - Account: gw.AccountID(), + Account: ts.gateway.AccountID(), Input: &tlb.Message{}, }), // should be voted - sample.TONDeposit(t, gw.AccountID(), toncontracts.Deposit{ + sample.TONDeposit(t, ts.gateway.AccountID(), toncontracts.Deposit{ Sender: sample.GenerateTONAccountID(), Amount: tonCoins(t, "3"), Recipient: sample.EthAddress(), }), + // a tracker should be added + sample.TONWithdrawal(t, ts.gateway.AccountID(), withdrawal), // should be skipped (invalid inbound/outbound messages) sample.TONTransaction(t, sample.TONTransactionProps{ - Account: gw.AccountID(), + Account: ts.gateway.AccountID(), Input: &tlb.Message{}, Output: &tlb.Message{}, }), } ts. - OnGetTransactionsSince(gw.AccountID(), lastScanned.Lt, txHash(lastScanned), txs, nil). + OnGetTransactionsSince(ts.gateway.AccountID(), lastScanned.Lt, txHash(lastScanned), txs, nil). Once() for _, tx := range txs { @@ -297,7 +353,7 @@ func TestInbound(t *testing.T) { // ACT // Observe inbounds once - err = ob.observeInbound(ts.ctx) + err = ob.observeGateway(ts.ctx) // ASSERT assert.NoError(t, err) @@ -323,9 +379,68 @@ func TestInbound(t *testing.T) { assert.NoError(t, err) assert.Equal(t, lastTX.Lt, lastLT) assert.Equal(t, lastTX.Hash().Hex(), lastHash.Hex()) + + // Check that a tracker was added + assert.Len(t, ts.trackerBag, 1) + tracker := ts.trackerBag[0] + + assert.Equal(t, uint64(withdrawal.Seqno), tracker.nonce) + assert.Equal(t, liteapi.TransactionToHashString(txs[4]), tracker.hash) }) } +func TestInboundTracker(t *testing.T) { + // ARRANGE + ts := newTestSuite(t) + + // Given observer + ob, err := New(ts.baseObserver, ts.liteClient, ts.gateway) + require.NoError(t, err) + + // Given TON gateway transactions + // should be voted + deposit := toncontracts.Deposit{ + Sender: sample.GenerateTONAccountID(), + Amount: toncontracts.Coins(5), + Recipient: sample.EthAddress(), + } + + txDeposit := sample.TONDeposit(t, ts.gateway.AccountID(), deposit) + ts.MockGetTransaction(ts.gateway.AccountID(), txDeposit) + ts.MockGetBlockHeader(txDeposit.BlockID) + + // Should be skipped (I doubt anyone would vote for this gov proposal, but let’s still put up rail guards) + txWithdrawal := sample.TONWithdrawal(t, ts.gateway.AccountID(), toncontracts.Withdrawal{ + Recipient: sample.GenerateTONAccountID(), + Amount: toncontracts.Coins(5), + Seqno: 1, + }) + ts.MockGetTransaction(ts.gateway.AccountID(), txWithdrawal) + ts.MockGetBlockHeader(txWithdrawal.BlockID) + + // Given inbound trackers from zetacore + trackers := []cc.InboundTracker{ + ts.TxToInboundTracker(txDeposit), + ts.TxToInboundTracker(txWithdrawal), + } + + ts.OnGetInboundTrackersForChain(trackers).Once() + + // ACT + err = ob.processInboundTrackers(ts.ctx) + + // ARRANGE + require.NoError(t, err) + require.Len(t, ts.votesBag, 1) + + vote := ts.votesBag[0] + assert.Equal(t, deposit.Amount, vote.Amount) + assert.Equal(t, deposit.Sender.ToRaw(), vote.Sender) + + // zevm recipient bytes == memo bytes + assert.Equal(t, fmt.Sprintf("%x", deposit.Recipient), vote.Message) +} + func txHash(tx ton.Transaction) ton.Bits256 { return ton.Bits256(tx.Hash()) } diff --git a/zetaclient/chains/ton/observer/observer.go b/zetaclient/chains/ton/observer/observer.go index e20742116a..565a544073 100644 --- a/zetaclient/chains/ton/observer/observer.go +++ b/zetaclient/chains/ton/observer/observer.go @@ -2,16 +2,22 @@ package observer import ( "context" - "errors" + "time" + lru "github.com/hashicorp/golang-lru" + "github.com/pkg/errors" + "github.com/rs/zerolog" + "github.com/tonkeeper/tongo/liteclient" "github.com/tonkeeper/tongo/tlb" "github.com/tonkeeper/tongo/ton" "github.com/zeta-chain/node/pkg/bg" toncontracts "github.com/zeta-chain/node/pkg/contracts/ton" - "github.com/zeta-chain/node/x/crosschain/types" + "github.com/zeta-chain/node/pkg/ticker" "github.com/zeta-chain/node/zetaclient/chains/base" "github.com/zeta-chain/node/zetaclient/chains/interfaces" + zetaton "github.com/zeta-chain/node/zetaclient/chains/ton" + "github.com/zeta-chain/node/zetaclient/common" ) // Observer is a TON observer. @@ -20,15 +26,25 @@ type Observer struct { client LiteClient gateway *toncontracts.Gateway + + outbounds *lru.Cache } +var _ interfaces.ChainObserver = (*Observer)(nil) + +const outboundsCacheSize = 1024 + // LiteClient represents a TON client +// see https://github.com/ton-blockchain/ton/blob/master/tl/generate/scheme/tonlib_api.tl // //go:generate mockery --name LiteClient --filename ton_liteclient.go --case underscore --output ../../../testutils/mocks type LiteClient interface { + zetaton.ConfigGetter + GetMasterchainInfo(ctx context.Context) (liteclient.LiteServerMasterchainInfoC, error) GetBlockHeader(ctx context.Context, blockID ton.BlockIDExt, mode uint32) (tlb.BlockInfo, error) - GetTransactionsSince(ctx context.Context, acc ton.AccountID, lt uint64, bits ton.Bits256) ([]ton.Transaction, error) - GetFirstTransaction(ctx context.Context, id ton.AccountID) (*ton.Transaction, int, error) + GetTransactionsSince(ctx context.Context, acc ton.AccountID, lt uint64, hash ton.Bits256) ([]ton.Transaction, error) + GetFirstTransaction(ctx context.Context, acc ton.AccountID) (*ton.Transaction, int, error) + GetTransaction(ctx context.Context, acc ton.AccountID, lt uint64, hash ton.Bits256) (ton.Transaction, error) } var _ interfaces.ChainObserver = (*Observer)(nil) @@ -44,37 +60,145 @@ func New(bo *base.Observer, client LiteClient, gateway *toncontracts.Gateway) (* return nil, errors.New("gateway is nil") } + outbounds, err := lru.New(outboundsCacheSize) + if err != nil { + return nil, err + } + bo.LoadLastTxScanned() return &Observer{ - Observer: bo, - client: client, - gateway: gateway, + Observer: bo, + client: client, + gateway: gateway, + outbounds: outbounds, }, nil } // Start starts the observer. This method is NOT blocking. +// Note that each `watch*` method has a ticker that will stop as soon as +// baseObserver.Stop() was called (ticker.WithStopChan) func (ob *Observer) Start(ctx context.Context) { if ok := ob.Observer.Start(); !ok { - ob.Logger().Chain.Info().Msgf("observer is already started for chain %d", ob.Chain().ChainId) + ob.Logger().Chain.Info().Msg("observer is already started") return } - ob.Logger().Chain.Info().Msgf("observer is starting for chain %d", ob.Chain().ChainId) + ob.Logger().Chain.Info().Msg("observer is starting") + + start(ctx, ob.watchInbound, "WatchInbound", ob.Logger().Inbound) + start(ctx, ob.watchInboundTracker, "WatchInboundTracker", ob.Logger().Inbound) + start(ctx, ob.watchOutbound, "WatchOutbound", ob.Logger().Outbound) + start(ctx, ob.watchGasPrice, "WatchGasPrice", ob.Logger().GasPrice) + start(ctx, ob.watchRPCStatus, "WatchRPCStatus", ob.Logger().Chain) +} + +// fire goroutine task +func start(ctx context.Context, task func(ctx context.Context) error, name string, log zerolog.Logger) { + bg.Work(ctx, task, bg.WithName(name), bg.WithLogger(log)) +} + +// watchGasPrice observes TON gas price and votes it to Zetacore. +func (ob *Observer) watchGasPrice(ctx context.Context) error { + task := func(ctx context.Context, t *ticker.Ticker) error { + if err := ob.postGasPrice(ctx); err != nil { + ob.Logger().GasPrice.Err(err).Msg("WatchGasPrice: postGasPrice error") + } + + newInterval := ticker.DurationFromUint64Seconds(ob.ChainParams().GasPriceTicker) + t.SetInterval(newInterval) + + return nil + } + + ob.Logger().GasPrice.Info().Msg("WatchGasPrice started") + + return ticker.Run( + ctx, + ticker.DurationFromUint64Seconds(ob.ChainParams().GasPriceTicker), + task, + ticker.WithStopChan(ob.StopChannel()), + ticker.WithLogger(ob.Logger().GasPrice, "WatchGasPrice"), + ) +} + +// postGasPrice fetches on-chain gas config and reports it to Zetacore. +func (ob *Observer) postGasPrice(ctx context.Context) error { + cfg, err := zetaton.FetchGasConfig(ctx, ob.client) + if err != nil { + return errors.Wrap(err, "failed to fetch gas config") + } + + gasPrice, err := zetaton.ParseGasPrice(cfg) + if err != nil { + return errors.Wrap(err, "failed to parse gas price") + } + + blockID, err := ob.getLatestMasterchainBlock(ctx) + if err != nil { + return errors.Wrap(err, "failed to get latest masterchain block") + } + + // There's no concept of priority fee in TON + const priorityFee = 0 + + _, errVote := ob. + ZetacoreClient(). + PostVoteGasPrice(ctx, ob.Chain(), gasPrice, priorityFee, uint64(blockID.Seqno)) + + return errVote +} + +// watchRPCStatus observes TON RPC status. +func (ob *Observer) watchRPCStatus(ctx context.Context) error { + task := func(ctx context.Context, _ *ticker.Ticker) error { + if err := ob.checkRPCStatus(ctx); err != nil { + ob.Logger().Chain.Err(err).Msg("checkRPCStatus error") + } + + return nil + } - // Note that each `watch*` method has a ticker that will stop as soon as - // baseObserver.Stop() was called (ticker.WithStopChan) + return ticker.Run( + ctx, + common.RPCStatusCheckInterval, + task, + ticker.WithStopChan(ob.StopChannel()), + ticker.WithLogger(ob.Logger().Chain, "WatchRPCStatus"), + ) +} + +// checkRPCStatus checks TON RPC status and alerts if necessary. +func (ob *Observer) checkRPCStatus(ctx context.Context) error { + blockID, err := ob.getLatestMasterchainBlock(ctx) + if err != nil { + return errors.Wrap(err, "failed to get latest masterchain block") + } + + block, err := ob.client.GetBlockHeader(ctx, blockID, 0) + if err != nil { + return errors.Wrap(err, "failed to get masterchain block header") + } - // watch for incoming txs and post votes to zetacore - bg.Work(ctx, ob.watchInbound, bg.WithName("WatchInbound"), bg.WithLogger(ob.Logger().Inbound)) + if block.NotMaster { + return errors.Errorf("block %q is not a master block", blockID.BlockID.String()) + } + + blockTime := time.Unix(int64(block.GenUtime), 0).UTC() + + // will be overridden by chain config + const defaultAlertLatency = 30 * time.Second - // TODO: watchInboundTracker - // https://github.com/zeta-chain/node/issues/2935 + ob.AlertOnRPCLatency(blockTime, defaultAlertLatency) - // TODO: outbounds/withdrawals: (watchOutbound, watchGasPrice, watchRPCStatus) - // https://github.com/zeta-chain/node/issues/2807 + return nil } -func (ob *Observer) VoteOutboundIfConfirmed(_ context.Context, _ *types.CrossChainTx) (bool, error) { - return false, errors.New("not implemented") +func (ob *Observer) getLatestMasterchainBlock(ctx context.Context) (ton.BlockIDExt, error) { + mc, err := ob.client.GetMasterchainInfo(ctx) + if err != nil { + return ton.BlockIDExt{}, errors.Wrap(err, "failed to get masterchain info") + } + + return mc.Last.ToBlockIdExt(), nil } diff --git a/zetaclient/chains/ton/observer/observer_test.go b/zetaclient/chains/ton/observer/observer_test.go index 38c032eb4a..290e34081a 100644 --- a/zetaclient/chains/ton/observer/observer_test.go +++ b/zetaclient/chains/ton/observer/observer_test.go @@ -6,15 +6,17 @@ import ( "testing" "cosmossdk.io/math" + eth "github.com/ethereum/go-ethereum/common" "github.com/rs/zerolog" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/tonkeeper/tongo/tlb" "github.com/tonkeeper/tongo/ton" "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/pkg/coin" toncontracts "github.com/zeta-chain/node/pkg/contracts/ton" "github.com/zeta-chain/node/testutil/sample" - cctxtypes "github.com/zeta-chain/node/x/crosschain/types" + cc "github.com/zeta-chain/node/x/crosschain/types" observertypes "github.com/zeta-chain/node/x/observer/types" "github.com/zeta-chain/node/zetaclient/chains/base" "github.com/zeta-chain/node/zetaclient/chains/ton/liteapi" @@ -30,6 +32,7 @@ type testSuite struct { chain chains.Chain chainParams *observertypes.ChainParams + gateway *toncontracts.Gateway liteClient *mocks.LiteClient zetacore *mocks.ZetacoreClient @@ -38,7 +41,13 @@ type testSuite struct { baseObserver *base.Observer - votesBag []*cctxtypes.MsgVoteInbound + votesBag []*cc.MsgVoteInbound + trackerBag []testTracker +} + +type testTracker struct { + nonce uint64 + hash string } func newTestSuite(t *testing.T) *testSuite { @@ -48,9 +57,13 @@ func newTestSuite(t *testing.T) *testSuite { chain = chains.TONTestnet chainParams = sample.ChainParams(chain.ChainId) + gateway = toncontracts.NewGateway(ton.MustParseAccountID( + "0:997d889c815aeac21c47f86ae0e38383efc3c3463067582f6263ad48c5a1485b", + )) + liteClient = mocks.NewLiteClient(t) - tss = mocks.NewTSSAthens3() + tss = mocks.NewGeneratedTSS(t, chain) zetacore = mocks.NewZetacoreClient(t).WithKeys(&keys.Keys{}) testLogger = zerolog.New(zerolog.NewTestWriter(t)) @@ -83,6 +96,7 @@ func newTestSuite(t *testing.T) *testSuite { chainParams: chainParams, liteClient: liteClient, + gateway: gateway, zetacore: zetacore, tss: tss, @@ -95,6 +109,7 @@ func newTestSuite(t *testing.T) *testSuite { ts.zetacore.On("Chain").Return(chain).Maybe() setupVotesBag(ts) + setupTrackersBag(ts) return ts } @@ -119,6 +134,18 @@ func (ts *testSuite) OnGetFirstTransaction(acc ton.AccountID, tx *ton.Transactio Return(tx, scanned, err) } +func (ts *testSuite) MockGetTransaction(acc ton.AccountID, tx ton.Transaction) *mock.Call { + return ts.liteClient. + On("GetTransaction", mock.Anything, acc, tx.Lt, ton.Bits256(tx.Hash())). + Return(tx, nil) +} + +func (ts *testSuite) MockCCTXByNonce(cctx *cc.CrossChainTx) *mock.Call { + nonce := cctx.GetCurrentOutboundParam().TssNonce + + return ts.zetacore.On("GetCctxByNonce", ts.ctx, ts.chain.ChainId, nonce).Return(cctx, nil) +} + func (ts *testSuite) OnGetTransactionsSince( acc ton.AccountID, lt uint64, @@ -131,6 +158,12 @@ func (ts *testSuite) OnGetTransactionsSince( Return(txs, err) } +func (ts *testSuite) OnGetAllOutboundTrackerByChain(trackers []cc.OutboundTracker) *mock.Call { + return ts.zetacore. + On("GetAllOutboundTrackerByChain", mock.Anything, ts.chain.ChainId, mock.Anything). + Return(trackers, nil) +} + func (ts *testSuite) MockGetBlockHeader(id ton.BlockIDExt) *mock.Call { // let's pretend that block's masterchain ref has the same seqno blockInfo := tlb.BlockInfo{ @@ -142,6 +175,41 @@ func (ts *testSuite) MockGetBlockHeader(id ton.BlockIDExt) *mock.Call { Return(blockInfo, nil) } +func (ts *testSuite) OnGetInboundTrackersForChain(trackers []cc.InboundTracker) *mock.Call { + return ts.zetacore. + On("GetInboundTrackersForChain", mock.Anything, ts.chain.ChainId). + Return(trackers, nil) +} + +func (ts *testSuite) TxToInboundTracker(tx ton.Transaction) cc.InboundTracker { + return cc.InboundTracker{ + ChainId: ts.chain.ChainId, + TxHash: liteapi.TransactionToHashString(tx), + CoinType: coin.CoinType_Gas, + } +} + +type signable interface { + Hash() ([32]byte, error) + SetSignature([65]byte) + Signer() (eth.Address, error) +} + +func (ts *testSuite) sign(msg signable) { + hash, err := msg.Hash() + require.NoError(ts.t, err) + + sig, err := ts.tss.Sign(ts.ctx, hash[:], 0, 0, 0, "") + require.NoError(ts.t, err) + + msg.SetSignature(sig) + + // double check + evmSigner, err := msg.Signer() + require.NoError(ts.t, err) + require.Equal(ts.t, ts.tss.EVMAddress().String(), evmSigner.String()) +} + // parses string to TON func tonCoins(t *testing.T, raw string) math.Uint { t.Helper() @@ -159,7 +227,7 @@ func tonCoins(t *testing.T, raw string) math.Uint { func setupVotesBag(ts *testSuite) { catcher := func(args mock.Arguments) { vote := args.Get(3) - cctx, ok := vote.(*cctxtypes.MsgVoteInbound) + cctx, ok := vote.(*cc.MsgVoteInbound) require.True(ts.t, ok, "unexpected cctx type") ts.votesBag = append(ts.votesBag, cctx) @@ -170,3 +238,26 @@ func setupVotesBag(ts *testSuite) { Run(catcher). Return("", "", nil) // zeta hash, ballot index, error } + +func setupTrackersBag(ts *testSuite) { + catcher := func(args mock.Arguments) { + require.Equal(ts.t, ts.chain.ChainId, args.Get(1).(int64)) + nonce := args.Get(2).(uint64) + txHash := args.Get(3).(string) + + ts.t.Logf("Adding outbound tracker: nonce=%d, hash=%s", nonce, txHash) + + ts.trackerBag = append(ts.trackerBag, testTracker{nonce, txHash}) + } + + ts.zetacore.On( + "AddOutboundTracker", + mock.Anything, + mock.Anything, + mock.Anything, + mock.Anything, + mock.Anything, + mock.Anything, + mock.Anything, + ).Maybe().Run(catcher).Return("", nil) +} diff --git a/zetaclient/chains/ton/observer/outbound.go b/zetaclient/chains/ton/observer/outbound.go new file mode 100644 index 0000000000..b4a466bcf2 --- /dev/null +++ b/zetaclient/chains/ton/observer/outbound.go @@ -0,0 +1,323 @@ +package observer + +import ( + "context" + + "cosmossdk.io/math" + eth "github.com/ethereum/go-ethereum/common" + "github.com/pkg/errors" + "github.com/rs/zerolog" + + "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/pkg/coin" + toncontracts "github.com/zeta-chain/node/pkg/contracts/ton" + "github.com/zeta-chain/node/pkg/ticker" + cc "github.com/zeta-chain/node/x/crosschain/types" + "github.com/zeta-chain/node/zetaclient/chains/interfaces" + "github.com/zeta-chain/node/zetaclient/chains/ton/liteapi" + zctx "github.com/zeta-chain/node/zetaclient/context" + gasconst "github.com/zeta-chain/node/zetaclient/zetacore" +) + +type outbound struct { + tx *toncontracts.Transaction + receiveStatus chains.ReceiveStatus + nonce uint64 +} + +// VoteOutboundIfConfirmed checks outbound status and returns (continueKeysign, error) +func (ob *Observer) VoteOutboundIfConfirmed(ctx context.Context, cctx *cc.CrossChainTx) (bool, error) { + nonce := cctx.GetCurrentOutboundParam().TssNonce + + outboundRes, exists := ob.getOutboundByNonce(nonce) + if !exists { + return true, nil + } + + withdrawal, err := outboundRes.tx.Withdrawal() + if err != nil { + return false, errors.Wrap(err, "unable to get withdrawal") + } + + // TODO: Add compliance check + // https://github.com/zeta-chain/node/issues/2916 + + txHash := liteapi.TransactionToHashString(outboundRes.tx.Transaction) + if err = ob.postVoteOutbound(ctx, cctx, withdrawal, txHash, outboundRes.receiveStatus); err != nil { + return false, errors.Wrap(err, "unable to post vote") + } + + return false, nil +} + +// watchOutbound watches outbound transactions and caches them in-memory so they can be used later in +// VoteOutboundIfConfirmed +func (ob *Observer) watchOutbound(ctx context.Context) error { + app, err := zctx.FromContext(ctx) + if err != nil { + return err + } + + var ( + initialInterval = ticker.DurationFromUint64Seconds(ob.ChainParams().OutboundTicker) + sampledLogger = ob.Logger().Inbound.Sample(&zerolog.BasicSampler{N: 10}) + ) + + task := func(ctx context.Context, t *ticker.Ticker) error { + if !app.IsOutboundObservationEnabled() { + sampledLogger.Info().Msg("WatchOutbound: outbound observation is disabled") + return nil + } + + if err := ob.observeOutboundTrackers(ctx); err != nil { + ob.Logger().Outbound.Err(err).Msg("WatchOutbound: observeOutboundTrackers error") + } + + newInterval := ticker.DurationFromUint64Seconds(ob.ChainParams().OutboundTicker) + t.SetInterval(newInterval) + + return nil + } + + return ticker.Run( + ctx, + initialInterval, + task, + ticker.WithStopChan(ob.StopChannel()), + ticker.WithLogger(ob.Logger().Outbound, "WatchOutbound"), + ) +} + +// observeOutboundTrackers pulls outbounds trackers from zetacore, +// fetches txs from TON and stores them in memory for further use. +func (ob *Observer) observeOutboundTrackers(ctx context.Context) error { + var ( + chainID = ob.Chain().ChainId + zetacore = ob.ZetacoreClient() + ) + + trackers, err := zetacore.GetAllOutboundTrackerByChain(ctx, chainID, interfaces.Ascending) + if err != nil { + return errors.Wrap(err, "unable to get outbound trackers") + } + + for _, tracker := range trackers { + nonce := tracker.Nonce + + // If outbound is already in memory, skip. + if _, ok := ob.getOutboundByNonce(nonce); ok { + continue + } + + // Let's not block other cctxs from being processed + cctx, err := zetacore.GetCctxByNonce(ctx, chainID, nonce) + if err != nil { + ob.Logger().Outbound. + Error().Err(err). + Uint64("outbound.nonce", nonce). + Msg("Unable to get cctx by nonce") + + continue + } + + for _, txHash := range tracker.HashList { + if err := ob.processOutboundTracker(ctx, cctx, txHash.TxHash); err != nil { + ob.Logger().Outbound. + Error().Err(err). + Uint64("outbound.nonce", nonce). + Str("outbound.hash", txHash.TxHash). + Msg("Unable to check transaction by nonce") + } + } + } + + return nil +} + +// processOutboundTracker checks TON tx and stores it in memory for further processing +// by VoteOutboundIfConfirmed. +func (ob *Observer) processOutboundTracker(ctx context.Context, cctx *cc.CrossChainTx, txHash string) error { + if cctx.InboundParams.CoinType != coin.CoinType_Gas { + return errors.New("only gas cctxs are supported") + } + + lt, hash, err := liteapi.TransactionHashFromString(txHash) + if err != nil { + return errors.Wrap(err, "unable to parse tx hash") + } + + rawTX, err := ob.client.GetTransaction(ctx, ob.gateway.AccountID(), lt, hash) + if err != nil { + return errors.Wrap(err, "unable to get transaction form liteapi") + } + + tx, err := ob.gateway.ParseTransaction(rawTX) + if err != nil { + return errors.Wrap(err, "unable to parse transaction") + } + + receiveStatus, err := ob.determineReceiveStatus(tx) + if err != nil { + return errors.Wrap(err, "unable to determine outbound outcome") + } + + // TODO: Add compliance check + // https://github.com/zeta-chain/node/issues/2916 + + nonce := cctx.GetCurrentOutboundParam().TssNonce + ob.setOutboundByNonce(outbound{tx, receiveStatus, nonce}) + + return nil +} + +func (ob *Observer) determineReceiveStatus(tx *toncontracts.Transaction) (chains.ReceiveStatus, error) { + _, evmSigner, err := extractWithdrawal(tx) + switch { + case err != nil: + return 0, err + case evmSigner != ob.TSS().EVMAddress(): + return 0, errors.New("withdrawal signer is not TSS") + case !tx.IsSuccess(): + return chains.ReceiveStatus_failed, nil + default: + return chains.ReceiveStatus_success, nil + } +} + +// addOutboundTracker publishes outbound tracker to Zetacore. +// In most cases will be a noop because the tracker is already published by the signer. +// See Signer{}.trackOutbound(...) for more details. +func (ob *Observer) addOutboundTracker(ctx context.Context, tx *toncontracts.Transaction) error { + w, evmSigner, err := extractWithdrawal(tx) + switch { + case err != nil: + return err + case evmSigner != ob.TSS().EVMAddress(): + ob.Logger().Inbound.Warn(). + Fields(txLogFields(tx)). + Str("transaction.ton.signer", evmSigner.String()). + Msg("observeGateway: addOutboundTracker: withdrawal signer is not TSS. Skipping") + + return nil + } + + var ( + chainID = ob.Chain().ChainId + nonce = uint64(w.Seqno) + hash = liteapi.TransactionToHashString(tx.Transaction) + ) + + // note it has a check for noop + _, err = ob. + ZetacoreClient(). + AddOutboundTracker(ctx, chainID, nonce, hash, nil, "", 0) + + return err +} + +// return withdrawal and tx signer +func extractWithdrawal(tx *toncontracts.Transaction) (toncontracts.Withdrawal, eth.Address, error) { + w, err := tx.Withdrawal() + if err != nil { + return toncontracts.Withdrawal{}, eth.Address{}, errors.Wrap(err, "not a withdrawal") + } + + s, err := w.Signer() + if err != nil { + return toncontracts.Withdrawal{}, eth.Address{}, errors.Wrap(err, "unable to get signer") + } + + return w, s, nil +} + +// getOutboundByNonce returns outbound by nonce +func (ob *Observer) getOutboundByNonce(nonce uint64) (outbound, bool) { + v, ok := ob.outbounds.Get(nonce) + if !ok { + return outbound{}, false + } + + return v.(outbound), true +} + +// setOutboundByNonce stores outbound by nonce +func (ob *Observer) setOutboundByNonce(o outbound) { + ob.outbounds.Add(o.nonce, o) +} + +func (ob *Observer) postVoteOutbound( + ctx context.Context, + cctx *cc.CrossChainTx, + w toncontracts.Withdrawal, + txHash string, + status chains.ReceiveStatus, +) error { + // I. Gas + // TON implements a different tx fee model. Basically, each operation in our Gateway has a + // tx_fee(operation) which is based on hard-coded gas values per operation + // multiplied by the current gas fees on-chain. Each withdrawal tx takes gas directly + // from the Gateway i.e. gw pays tx fees for itself. + // + // - Gas price is stores in zetacore thanks to Observer.postGasPrice() + // - Gas limit should be hardcoded in TON ZRC-20 + // + // II. Block height + // TON doesn't sequential block height because different txs might end up in different shard chains + // tlb.BlockID is essentially a workchain+shard+seqno tuple. We can't use it as a block height. Thus let's use 0. + // Note that for the sake of gas tracking, we use masterchain block height (not applicable here). + const ( + outboundGasUsed = 0 + outboundGasPrice = 0 + outboundGasLimit = 0 + outboundBlockHeight = 0 + ) + + var ( + chainID = ob.Chain().ChainId + nonce = cctx.GetCurrentOutboundParam().TssNonce + signerAddress = ob.ZetacoreClient().GetKeys().GetOperatorAddress() + coinType = cctx.InboundParams.CoinType + ) + + msg := cc.NewMsgVoteOutbound( + signerAddress.String(), + cctx.Index, + txHash, + outboundBlockHeight, + outboundGasUsed, + math.NewInt(outboundGasPrice), + outboundGasLimit, + w.Amount, + status, + chainID, + nonce, + coinType, + ) + + const gasLimit = gasconst.PostVoteOutboundGasLimit + + var retryGasLimit uint64 + if msg.Status == chains.ReceiveStatus_failed { + retryGasLimit = gasconst.PostVoteOutboundRevertGasLimit + } + + log := ob.Logger().Outbound.With(). + Uint64("outbound.nonce", nonce). + Str("outbound.outbound_tx_hash", txHash). + Logger() + + zetaTxHash, ballot, err := ob.ZetacoreClient().PostVoteOutbound(ctx, gasLimit, retryGasLimit, msg) + if err != nil { + log.Error().Err(err).Msg("PostVoteOutbound: error posting vote") + return err + } + + if zetaTxHash != "" { + log.Info(). + Str("outbound.vote_tx_hash", zetaTxHash). + Str("outbound.ballot_id", ballot). + Msg("PostVoteOutbound: posted vote") + } + + return nil +} diff --git a/zetaclient/chains/ton/observer/outbound_test.go b/zetaclient/chains/ton/observer/outbound_test.go new file mode 100644 index 0000000000..4358c353b4 --- /dev/null +++ b/zetaclient/chains/ton/observer/outbound_test.go @@ -0,0 +1,80 @@ +package observer + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tonkeeper/tongo/ton" + "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/pkg/coin" + toncontracts "github.com/zeta-chain/node/pkg/contracts/ton" + "github.com/zeta-chain/node/testutil/sample" + cc "github.com/zeta-chain/node/x/crosschain/types" + "github.com/zeta-chain/node/zetaclient/chains/ton/liteapi" +) + +func TestOutbound(t *testing.T) { + gw := toncontracts.NewGateway( + ton.MustParseAccountID("0:997d889c815aeac21c47f86ae0e38383efc3c3463067582f6263ad48c5a1485b"), + ) + + t.Run("observeOutboundTrackers", func(t *testing.T) { + // ARRANGE + ts := newTestSuite(t) + + ob, err := New(ts.baseObserver, ts.liteClient, gw) + require.NoError(t, err) + + // Given withdrawal + withdrawal := toncontracts.Withdrawal{ + Recipient: ton.MustParseAccountID("0:552f6db5da0cae7f0b3ab4ab58d85927f6beb962cda426a6a6ee751c82cead1f"), + Amount: toncontracts.Coins(2), + Seqno: 3, + } + ts.sign(&withdrawal) + + nonce := uint64(withdrawal.Seqno) + + // Given TON tx + withdrawalTX := sample.TONWithdrawal(t, gw.AccountID(), withdrawal) + + ts.MockGetTransaction(gw.AccountID(), withdrawalTX) + + // Given outbound tracker + tracker := cc.OutboundTracker{ + Index: "index123", + ChainId: ts.chain.ChainId, + Nonce: nonce, + HashList: []*cc.TxHash{{TxHash: liteapi.TransactionToHashString(withdrawalTX)}}, + } + + ts.OnGetAllOutboundTrackerByChain([]cc.OutboundTracker{tracker}) + + // Given cctx + cctx := sample.CrossChainTx(t, "index456") + cctx.InboundParams.CoinType = coin.CoinType_Gas + cctx.GetCurrentOutboundParam().TssNonce = nonce + + ts.MockCCTXByNonce(cctx) + + // ACT + err = ob.observeOutboundTrackers(ts.ctx) + + // ASSERT + require.NoError(t, err) + + // Check that tx exists in outbounds + res, exists := ob.getOutboundByNonce(nonce) + assert.True(t, exists) + + assert.Equal(t, nonce, res.nonce) + assert.Equal(t, chains.ReceiveStatus_success, res.receiveStatus) + assert.Equal(t, true, res.tx.IsSuccess()) + assert.Equal(t, int32(0), res.tx.ExitCode) + + w2, err := res.tx.Withdrawal() + assert.NoError(t, err) + assert.Equal(t, withdrawal, w2) + }) +} diff --git a/zetaclient/chains/ton/signer/signer.go b/zetaclient/chains/ton/signer/signer.go new file mode 100644 index 0000000000..bdd25c0c18 --- /dev/null +++ b/zetaclient/chains/ton/signer/signer.go @@ -0,0 +1,301 @@ +package signer + +import ( + "context" + "regexp" + "strconv" + "strings" + + ethcommon "github.com/ethereum/go-ethereum/common" + lru "github.com/hashicorp/golang-lru" + "github.com/pkg/errors" + "github.com/tonkeeper/tongo/liteclient" + "github.com/tonkeeper/tongo/tlb" + "github.com/tonkeeper/tongo/ton" + + "github.com/zeta-chain/node/pkg/coin" + toncontracts "github.com/zeta-chain/node/pkg/contracts/ton" + cc "github.com/zeta-chain/node/x/crosschain/types" + "github.com/zeta-chain/node/zetaclient/chains/base" + "github.com/zeta-chain/node/zetaclient/chains/interfaces" + "github.com/zeta-chain/node/zetaclient/outboundprocessor" +) + +// LiteClient represents a TON client +// see https://github.com/ton-blockchain/ton/blob/master/tl/generate/scheme/tonlib_api.tl +// +//go:generate mockery --name LiteClient --structname SignerLiteClient --filename ton_signerliteclient.go --case underscore --output ../../../testutils/mocks +type LiteClient interface { + GetTransactionsSince(ctx context.Context, acc ton.AccountID, lt uint64, hash ton.Bits256) ([]ton.Transaction, error) + GetAccountState(ctx context.Context, accountID ton.AccountID) (tlb.ShardAccount, error) + SendMessage(ctx context.Context, payload []byte) (uint32, error) +} + +// Signer represents TON signer. +type Signer struct { + *base.Signer + client LiteClient + gateway *toncontracts.Gateway + signaturesCache *lru.Cache +} + +// Signable represents a message that can be signed. +type Signable interface { + Hash() ([32]byte, error) + SetSignature(sig [65]byte) +} + +// Outcome possible outbound processing outcomes. +type Outcome string + +const ( + Invalid Outcome = "invalid" + Fail Outcome = "fail" + Success Outcome = "success" +) + +const signaturesHashSize = 1024 + +var _ interfaces.ChainSigner = (*Signer)(nil) + +// New Signer constructor. +func New(baseSigner *base.Signer, client LiteClient, gateway *toncontracts.Gateway) *Signer { + sigCache, _ := lru.New(signaturesHashSize) + + return &Signer{ + Signer: baseSigner, + client: client, + gateway: gateway, + signaturesCache: sigCache, + } +} + +// TryProcessOutbound tries to process outbound cctx. +// Note that this API signature will be refactored in orchestrator V2 +func (s *Signer) TryProcessOutbound( + ctx context.Context, + cctx *cc.CrossChainTx, + proc *outboundprocessor.Processor, + outboundID string, + _ interfaces.ChainObserver, + zetacore interfaces.ZetacoreClient, + zetaBlockHeight uint64, +) { + proc.StartTryProcess(outboundID) + + defer func() { + proc.EndTryProcess(outboundID) + }() + + outcome, err := s.ProcessOutbound(ctx, cctx, zetacore, zetaBlockHeight) + + lf := map[string]any{ + "outbound.id": outboundID, + "outbound.nonce": cctx.GetCurrentOutboundParam().TssNonce, + "outbound.outcome": string(outcome), + } + + switch { + case err != nil: + s.Logger().Std.Error().Err(err).Fields(lf).Msg("Unable to ProcessOutbound") + case outcome != Success: + s.Logger().Std.Warn().Fields(lf).Msg("Unsuccessful outcome for ProcessOutbound") + default: + s.Logger().Std.Info().Fields(lf).Msg("Processed outbound") + } +} + +// ProcessOutbound signs and broadcasts an outbound cross-chain transaction. +func (s *Signer) ProcessOutbound( + ctx context.Context, + cctx *cc.CrossChainTx, + zetacore interfaces.ZetacoreClient, + zetaHeight uint64, +) (Outcome, error) { + // TODO: note that *InboundParams* are use used on purpose due to legacy reasons. + // https://github.com/zeta-chain/node/issues/1949 + if cctx.InboundParams.CoinType != coin.CoinType_Gas { + return Invalid, errors.New("only gas coin outbounds are supported") + } + + params := cctx.GetCurrentOutboundParam() + + // TODO: add compliance check + // https://github.com/zeta-chain/node/issues/2916 + + receiver, err := ton.ParseAccountID(params.Receiver) + if err != nil { + return Invalid, errors.Wrapf(err, "unable to parse recipient %q", params.Receiver) + } + + withdrawal := &toncontracts.Withdrawal{ + Recipient: receiver, + Amount: params.Amount, + // #nosec G115 always in range + Seqno: uint32(params.TssNonce), + } + + lf := map[string]any{ + "outbound.recipient": withdrawal.Recipient.ToRaw(), + "outbound.amount": withdrawal.Amount.Uint64(), + "outbound.nonce": withdrawal.Seqno, + } + + s.Logger().Std.Info().Fields(lf).Msg("Signing withdrawal") + + if err = s.SignMessage(ctx, withdrawal, zetaHeight, params.TssNonce); err != nil { + return Fail, errors.Wrap(err, "unable to sign withdrawal message") + } + + gwState, err := s.client.GetAccountState(ctx, s.gateway.AccountID()) + if err != nil { + return Fail, errors.Wrap(err, "unable to get gateway state") + } + + // Publishes signed message to Gateway + // Note that max(tx fee) is hardcoded in the contract. + // + // Example: If a cctx has amount of 5 TON, the recipient will receive 5 TON, + // and gateway's balance will be decreased by 5 TON + txFees. + exitCode, err := s.gateway.SendExternalMessage(ctx, s.client, withdrawal) + if err != nil || exitCode != 0 { + return s.handleSendError(exitCode, err, lf) + } + + // it's okay to run this in the same goroutine + // because TryProcessOutbound method should be called in a goroutine + if err = s.trackOutbound(ctx, zetacore, withdrawal, gwState); err != nil { + return Fail, errors.Wrap(err, "unable to track outbound") + } + + return Success, nil +} + +// SignMessage signs TON external message using TSS +func (s *Signer) SignMessage(ctx context.Context, msg Signable, zetaHeight, nonce uint64) error { + hash, err := msg.Hash() + if err != nil { + return errors.Wrap(err, "unable to hash message") + } + + // cache hit + if sig, ok := s.getSignature(hash); ok { + msg.SetSignature(sig) + return nil + } + + chainID := s.Chain().ChainId + + // sig = [65]byte {R, S, V (recovery ID)} + sig, err := s.TSS().Sign(ctx, hash[:], zetaHeight, nonce, chainID, "") + if err != nil { + return errors.Wrap(err, "unable to sign the message") + } + + msg.SetSignature(sig) + s.setSignature(hash, sig) + + return nil +} + +// because signed msg might fail due to high nonce, +// we need to make sure that signature is cached to avoid redundant TSS calls +func (s *Signer) getSignature(hash [32]byte) ([65]byte, bool) { + sig, ok := s.signaturesCache.Get(hash) + if !ok { + return [65]byte{}, false + } + + return sig.([65]byte), true +} + +// caches signature +func (s *Signer) setSignature(hash [32]byte, sig [65]byte) { + s.signaturesCache.Add(hash, sig) +} + +// Sample (from local ton): +// error code: 0 message: cannot apply external message to current state: +// External message was not accepted Cannot run message on account: +// inbound external message rejected by transaction ...: exitcode=109, steps=108, gas_used=0\ +// VM Log (truncated): ... +var exitCodeErrorRegex = regexp.MustCompile(`exitcode=(\d+)`) + +// handleSendError tries to figure out the reason of the send error. +func (s *Signer) handleSendError(exitCode uint32, err error, logFields map[string]any) (Outcome, error) { + if err != nil { + // Might be possible if 2 concurrent zeta clients + // are trying to broadcast the same message. + if strings.Contains(err.Error(), "duplicate message") { + s.Logger().Std.Warn().Fields(logFields).Msg("Message already sent") + return Invalid, nil + } + + var errLiteClient liteclient.LiteServerErrorC + if errors.As(err, &errLiteClient) { + logFields["outbound.error.message"] = errLiteClient.Message + exitCode = errLiteClient.Code + } + + if code, ok := extractExitCode(err.Error()); ok { + exitCode = code + } + } + + switch { + case exitCode == uint32(toncontracts.ExitCodeInvalidSeqno): + // Might be possible if zeta clients send several seq. numbers concurrently. + // In the current implementation, Gateway supports only 1 nonce per block. + logFields["outbound.error.exit_code"] = exitCode + s.Logger().Std.Warn().Fields(logFields).Msg("Invalid nonce, retry later") + return Invalid, nil + case err != nil: + return Fail, errors.Wrap(err, "unable to send external message") + default: + return Fail, errors.Errorf("unable to send external message: exit code %d", exitCode) + } +} + +func extractExitCode(text string) (uint32, bool) { + match := exitCodeErrorRegex.FindStringSubmatch(text) + if len(match) < 2 { + return 0, false + } + + exitCode, err := strconv.ParseUint(match[1], 10, 32) + if err != nil { + return 0, false + } + + return uint32(exitCode), true +} + +// GetGatewayAddress returns gateway address as raw TON address "0:ABC..." +func (s *Signer) GetGatewayAddress() string { + return s.gateway.AccountID().ToRaw() +} + +// SetGatewayAddress sets gateway address. Has a check for noop. +func (s *Signer) SetGatewayAddress(addr string) { + // noop + if s.gateway.AccountID().ToRaw() == addr { + return + } + + acc, err := ton.ParseAccountID(addr) + if err != nil { + s.Logger().Std.Error().Err(err).Str("addr", addr).Msg("unable to parse gateway address") + return + } + + s.Lock() + s.gateway = toncontracts.NewGateway(acc) + s.Unlock() +} + +// not used + +func (s *Signer) GetZetaConnectorAddress() (_ ethcommon.Address) { return } +func (s *Signer) GetERC20CustodyAddress() (_ ethcommon.Address) { return } +func (s *Signer) SetZetaConnectorAddress(_ ethcommon.Address) {} +func (s *Signer) SetERC20CustodyAddress(_ ethcommon.Address) {} diff --git a/zetaclient/chains/ton/signer/signer_test.go b/zetaclient/chains/ton/signer/signer_test.go new file mode 100644 index 0000000000..dcd78c9817 --- /dev/null +++ b/zetaclient/chains/ton/signer/signer_test.go @@ -0,0 +1,258 @@ +package signer + +import ( + "context" + "encoding/hex" + "strconv" + "testing" + + "cosmossdk.io/math" + "github.com/rs/zerolog" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "github.com/tonkeeper/tongo/tlb" + "github.com/tonkeeper/tongo/ton" + "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/pkg/coin" + toncontracts "github.com/zeta-chain/node/pkg/contracts/ton" + "github.com/zeta-chain/node/testutil/sample" + cc "github.com/zeta-chain/node/x/crosschain/types" + observertypes "github.com/zeta-chain/node/x/observer/types" + "github.com/zeta-chain/node/zetaclient/chains/base" + "github.com/zeta-chain/node/zetaclient/chains/ton/liteapi" + "github.com/zeta-chain/node/zetaclient/keys" + "github.com/zeta-chain/node/zetaclient/outboundprocessor" + "github.com/zeta-chain/node/zetaclient/testutils/mocks" +) + +func TestSigner(t *testing.T) { + // ARRANGE + ts := newTestSuite(t) + + // Given TON signer + signer := New(ts.baseSigner, ts.liteClient, ts.gw) + + // Given a sample TON receiver + receiver := ton.MustParseAccountID("0QAyaVdkvWSuax8luWhDXY_0X9Am1ASWlJz4OI7M-jqcM5wK") + + const ( + zetaHeight = 123 + outboundID = "abc123" + nonce = 2 + ) + + amount := tonCoins(t, "5") + + // Given CCTX + cctx := sample.CrossChainTx(t, "123") + cctx.InboundParams.CoinType = coin.CoinType_Gas + cctx.OutboundParams = []*cc.OutboundParams{{ + Receiver: receiver.ToRaw(), + ReceiverChainId: ts.chain.ChainId, + CoinType: coin.CoinType_Gas, + Amount: amount, + TssNonce: nonce, + }} + + // Given expected withdrawal + withdrawal := toncontracts.Withdrawal{ + Recipient: receiver, + Amount: amount, + Seqno: nonce, + } + + ts.Sign(&withdrawal) + + // Given expected liteapi calls + lt, hash := uint64(400), decodeHash(t, "df8a01053f50a74503dffe6802f357bf0e665bd1f3d082faccfebdea93cddfeb") + ts.OnGetAccountState(ts.gw.AccountID(), tlb.ShardAccount{LastTransLt: lt, LastTransHash: hash}) + + ts.OnSendMessage(0, nil) + + withdrawalTX := sample.TONWithdrawal(t, ts.gw.AccountID(), withdrawal) + ts.OnGetTransactionsSince(ts.gw.AccountID(), lt, ton.Bits256(hash), []ton.Transaction{withdrawalTX}, nil) + + // ACT + signer.TryProcessOutbound(ts.ctx, cctx, ts.proc, outboundID, nil, ts.zetacore, zetaHeight) + + // ASSERT + // Make sure signer send the tx the chain AND published the outbound tracker + require.Len(t, ts.trackerBag, 1) + + tracker := ts.trackerBag[0] + + require.Equal(t, uint64(nonce), tracker.nonce) + require.Equal(t, liteapi.TransactionToHashString(withdrawalTX), tracker.hash) +} + +func TestExitCodeRegex(t *testing.T) { + for _, tt := range []string{ + `unable to send external message: error code: 0 message: + cannot apply external message to current state : + External message was not accepted\nCannot run message on account: inbound external message rejected by + transaction CC8803E21EDA7E6487D191380725A82CD75316E1C131496E1A5636751CE60347: + \nexitcode=109, steps=108, gas_used=0\nVM Log (truncated):\n...INT 0\nexecute THROWIFNOT + 105\nexecute MYADDR\nexecute XCHG s1,s4\nexecute SDEQ\nexecute THROWIF 112\nexecute OVER\nexecute + EQINT 0\nexecute THROWIF 106\nexecute GETGLOB + 3\nexecute NEQ\nexecute THROWIF 109\ndefault exception handler, terminating vm with exit code 109\n`, + + `unable to send external message: error code: 0 message: cannot apply external message to current state : + External message was not accepted\nCannot run message on account: + inbound external message rejected by transaction + 6CCBB83C7D9BFBFDB40541F35AD069714856F18B4850C1273A117DF6BFADE1C6:\nexitcode=109, steps=108, + gas_used=0\nVM Log (truncated):\n...INT 0....`, + } { + require.True(t, exitCodeErrorRegex.MatchString(tt)) + + exitCode, ok := extractExitCode(tt) + require.True(t, ok) + require.Equal(t, uint32(109), exitCode) + } +} + +type testSuite struct { + ctx context.Context + t *testing.T + + chain chains.Chain + chainParams *observertypes.ChainParams + + liteClient *mocks.SignerLiteClient + + zetacore *mocks.ZetacoreClient + tss *mocks.TSS + + gw *toncontracts.Gateway + baseSigner *base.Signer + proc *outboundprocessor.Processor + + trackerBag []testTracker +} + +type testTracker struct { + nonce uint64 + hash string +} + +func newTestSuite(t *testing.T) *testSuite { + var ( + ctx = context.Background() + + chain = chains.TONTestnet + chainParams = sample.ChainParams(chain.ChainId) + + liteClient = mocks.NewSignerLiteClient(t) + + tss = mocks.NewTSSAthens3() + zetacore = mocks.NewZetacoreClient(t).WithKeys(&keys.Keys{}) + + testLogger = zerolog.New(zerolog.NewTestWriter(t)) + logger = base.Logger{Std: testLogger, Compliance: testLogger} + + gwAccountID = ton.MustParseAccountID("0:997d889c815aeac21c47f86ae0e38383efc3c3463067582f6263ad48c5a1485b") + ) + + ts := &testSuite{ + ctx: ctx, + t: t, + + chain: chain, + chainParams: chainParams, + + liteClient: liteClient, + + zetacore: zetacore, + tss: tss, + + proc: outboundprocessor.NewProcessor(logger.Std), + gw: toncontracts.NewGateway(gwAccountID), + baseSigner: base.NewSigner(chain, tss, nil, logger), + } + + // Setup mocks + ts.zetacore.On("Chain").Return(chain).Maybe() + + setupTrackersBag(ts) + + return ts +} + +func (ts *testSuite) OnGetAccountState(acc ton.AccountID, state tlb.ShardAccount) *mock.Call { + return ts.liteClient.On("GetAccountState", mock.Anything, acc).Return(state, nil) +} + +func (ts *testSuite) OnSendMessage(id uint32, err error) *mock.Call { + return ts.liteClient.On("SendMessage", mock.Anything, mock.Anything).Return(id, err) +} + +func (ts *testSuite) OnGetTransactionsSince( + acc ton.AccountID, + lt uint64, + hash ton.Bits256, + txs []ton.Transaction, + err error, +) *mock.Call { + return ts.liteClient. + On("GetTransactionsSince", mock.Anything, acc, lt, hash). + Return(txs, err) +} + +func (ts *testSuite) Sign(msg Signable) { + hash, err := msg.Hash() + require.NoError(ts.t, err) + + sig, err := ts.tss.Sign(ts.ctx, hash[:], 0, 0, 0, "") + require.NoError(ts.t, err) + + msg.SetSignature(sig) +} + +// parses string to TON +func tonCoins(t *testing.T, raw string) math.Uint { + t.Helper() + + const oneTON = 1_000_000_000 + + f, err := strconv.ParseFloat(raw, 64) + require.NoError(t, err) + + f *= oneTON + + return math.NewUint(uint64(f)) +} + +func decodeHash(t *testing.T, raw string) tlb.Bits256 { + t.Helper() + + h, err := hex.DecodeString(raw) + require.NoError(t, err) + + var hash tlb.Bits256 + + copy(hash[:], h) + + return hash +} + +func setupTrackersBag(ts *testSuite) { + catcher := func(args mock.Arguments) { + require.Equal(ts.t, ts.chain.ChainId, args.Get(1).(int64)) + nonce := args.Get(2).(uint64) + txHash := args.Get(3).(string) + + ts.t.Logf("Adding outbound tracker: nonce=%d, hash=%s", nonce, txHash) + + ts.trackerBag = append(ts.trackerBag, testTracker{nonce, txHash}) + } + + ts.zetacore.On( + "AddOutboundTracker", + mock.Anything, + mock.Anything, + mock.Anything, + mock.Anything, + mock.Anything, + mock.Anything, + mock.Anything, + ).Maybe().Run(catcher).Return("", nil) +} diff --git a/zetaclient/chains/ton/signer/signer_tracker.go b/zetaclient/chains/ton/signer/signer_tracker.go new file mode 100644 index 0000000000..065d0f8204 --- /dev/null +++ b/zetaclient/chains/ton/signer/signer_tracker.go @@ -0,0 +1,92 @@ +package signer + +import ( + "context" + "time" + + "github.com/pkg/errors" + "github.com/tonkeeper/tongo/tlb" + "github.com/tonkeeper/tongo/ton" + + toncontracts "github.com/zeta-chain/node/pkg/contracts/ton" + "github.com/zeta-chain/node/zetaclient/chains/interfaces" + "github.com/zeta-chain/node/zetaclient/chains/ton/liteapi" +) + +// trackOutbound tracks sent external message and records it as outboundTracker. +// Explanation: +// Due to TON's nature, it's not possible to get tx hash before it's confirmed on-chain, +// So we need to poll from the latest account state (prevState) up to the most recent tx +// and search for desired tx hash. After it's found, we can record it as outboundTracker. +// +// Note that another zetaclient observers that scrolls Gateway's txs can publish this tracker concurrently. +func (s *Signer) trackOutbound( + ctx context.Context, + zetacore interfaces.ZetacoreClient, + w *toncontracts.Withdrawal, + prevState tlb.ShardAccount, +) error { + const ( + timeout = 60 * time.Second + tick = time.Second + ) + + var ( + start = time.Now() + chainID = s.Chain().ChainId + + acc = s.gateway.AccountID() + lt = prevState.LastTransLt + hash = ton.Bits256(prevState.LastTransHash) + nonce = uint64(w.Seqno) + + filter = withdrawalFilter(w) + ) + + for time.Since(start) <= timeout { + txs, err := s.client.GetTransactionsSince(ctx, acc, lt, hash) + if err != nil { + return errors.Wrapf(err, "unable to get transactions (lt %d, hash %s)", lt, hash.Hex()) + } + + results := s.gateway.ParseAndFilterMany(txs, filter) + if len(results) == 0 { + time.Sleep(tick) + continue + } + + tx := results[0].Transaction + txHash := liteapi.TransactionToHashString(results[0].Transaction) + + if !tx.IsSuccess() { + // should not happen + return errors.Errorf("transaction %q is not successful", txHash) + } + + // Note that this method has a check for noop + _, err = zetacore.AddOutboundTracker(ctx, chainID, nonce, txHash, nil, "", 0) + if err != nil { + return errors.Wrap(err, "unable to add outbound tracker") + } + + return nil + } + + return errors.Errorf("timeout exceeded (%s)", time.Since(start).String()) +} + +// creates a tx filter for this very withdrawal +func withdrawalFilter(w *toncontracts.Withdrawal) func(tx *toncontracts.Transaction) bool { + return func(tx *toncontracts.Transaction) bool { + if !tx.IsOutbound() || tx.Operation != toncontracts.OpWithdraw { + return false + } + + wd, err := tx.Withdrawal() + if err != nil { + return false + } + + return wd.Seqno == w.Seqno && wd.Sig == w.Sig + } +} diff --git a/zetaclient/metrics/metrics.go b/zetaclient/metrics/metrics.go index a0a7341f94..a826da58f2 100644 --- a/zetaclient/metrics/metrics.go +++ b/zetaclient/metrics/metrics.go @@ -148,6 +148,12 @@ var ( }, []string{"host"}, ) + + NumConnectedPeers = promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: ZetaClientNamespace, + Name: "num_connected_peers", + Help: "The number of connected peers (authenticated keygen peers)", + }) ) // NewMetrics creates a new Metrics instance diff --git a/zetaclient/metrics/telemetry.go b/zetaclient/metrics/telemetry.go index d0cd85b538..506945859c 100644 --- a/zetaclient/metrics/telemetry.go +++ b/zetaclient/metrics/telemetry.go @@ -10,6 +10,7 @@ import ( "time" "github.com/gorilla/mux" + "github.com/libp2p/go-libp2p/core/peer" "github.com/rs/zerolog" "github.com/rs/zerolog/log" @@ -30,6 +31,8 @@ type TelemetryServer struct { status types.Status ipAddress string HotKeyBurnRate *BurnRate + connectedPeers []peer.AddrInfo + rtt map[peer.ID]int64 } // NewTelemetryServer should only listen to the loopback @@ -39,6 +42,8 @@ func NewTelemetryServer() *TelemetryServer { lastScannedBlockNumber: make(map[int64]uint64), lastStartTimestamp: time.Now(), HotKeyBurnRate: NewBurnRate(100), + connectedPeers: make([]peer.AddrInfo, 0), + rtt: make(map[peer.ID]int64), } s := &http.Server{ Addr: ":8123", @@ -50,6 +55,30 @@ func NewTelemetryServer() *TelemetryServer { return hs } +func (t *TelemetryServer) SetPingRTT(rtt map[peer.ID]int64) { + t.mu.Lock() + defer t.mu.Unlock() + t.rtt = rtt +} + +func (t *TelemetryServer) GetPingRTT() map[peer.ID]int64 { + t.mu.Lock() + defer t.mu.Unlock() + return t.rtt +} + +func (t *TelemetryServer) SetConnectedPeers(peers []peer.AddrInfo) { + t.mu.Lock() + defer t.mu.Unlock() + t.connectedPeers = peers +} + +func (t *TelemetryServer) GetConnectedPeers() []peer.AddrInfo { + t.mu.Lock() + defer t.mu.Unlock() + return t.connectedPeers +} + // SetP2PID sets p2pid func (t *TelemetryServer) SetP2PID(p2pid string) { t.mu.Lock() @@ -145,7 +174,8 @@ func (t *TelemetryServer) Handlers() http.Handler { router.Handle("/status", http.HandlerFunc(t.statusHandler)).Methods(http.MethodGet) router.Handle("/ip", http.HandlerFunc(t.ipHandler)).Methods(http.MethodGet) router.Handle("/hotkeyburnrate", http.HandlerFunc(t.hotKeyFeeBurnRate)).Methods(http.MethodGet) - + router.Handle("/connectedpeers", http.HandlerFunc(t.connectedPeersHandler)).Methods(http.MethodGet) + router.Handle("/pingrtt", http.HandlerFunc(t.pingRTTHandler)).Methods(http.MethodGet) router.Use(logMiddleware()) return router @@ -184,17 +214,14 @@ func (t *TelemetryServer) pingHandler(w http.ResponseWriter, _ *http.Request) { // p2pHandler returns the p2p id func (t *TelemetryServer) p2pHandler(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) - t.mu.Lock() - defer t.mu.Unlock() - fmt.Fprintf(w, "%s", t.p2pid) + fmt.Fprintf(w, "%s", t.GetP2PID()) } // ipHandler returns the ip address func (t *TelemetryServer) ipHandler(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) - t.mu.Lock() - defer t.mu.Unlock() - fmt.Fprintf(w, "%s", t.ipAddress) + + fmt.Fprintf(w, "%s", t.GetIPAddress()) } func (t *TelemetryServer) lastScannedBlockHandler(w http.ResponseWriter, _ *http.Request) { @@ -251,6 +278,34 @@ func (t *TelemetryServer) hotKeyFeeBurnRate(w http.ResponseWriter, _ *http.Reque fmt.Fprintf(w, "%v", t.HotKeyBurnRate.GetBurnRate()) } +func (t *TelemetryServer) connectedPeersHandler(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + peers := t.GetConnectedPeers() + data, err := json.Marshal(peers) + if err != nil { + t.logger.Error().Err(err).Msg("Failed to marshal connected peers") + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + fmt.Fprintf(w, "%s", string(data)) +} + +func (t *TelemetryServer) pingRTTHandler(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + rtt := t.GetPingRTT() + rtt2 := make(map[string]int64) + for k, v := range rtt { + rtt2[k.String()] = v + } + data, err := json.Marshal(rtt2) + if err != nil { + t.logger.Error().Err(err).Msg("Failed to marshal ping RTT") + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + fmt.Fprintf(w, "%s", string(data)) +} + // logMiddleware logs the incoming HTTP request func logMiddleware() mux.MiddlewareFunc { return func(handler http.Handler) http.Handler { diff --git a/zetaclient/orchestrator/bootstrap.go b/zetaclient/orchestrator/bootstrap.go index 34c94bf77d..e1d89df0fd 100644 --- a/zetaclient/orchestrator/bootstrap.go +++ b/zetaclient/orchestrator/bootstrap.go @@ -23,6 +23,8 @@ import ( solanasigner "github.com/zeta-chain/node/zetaclient/chains/solana/signer" "github.com/zeta-chain/node/zetaclient/chains/ton/liteapi" tonobserver "github.com/zeta-chain/node/zetaclient/chains/ton/observer" + tonsigner "github.com/zeta-chain/node/zetaclient/chains/ton/signer" + "github.com/zeta-chain/node/zetaclient/config" zctx "github.com/zeta-chain/node/zetaclient/context" "github.com/zeta-chain/node/zetaclient/db" "github.com/zeta-chain/node/zetaclient/keys" @@ -187,8 +189,25 @@ func syncSignerMap( addSigner(chainID, signer) case chain.IsTON(): - logger.Std.Error().Err(err).Msgf("TON signer is not implemented yet for chain id %d", chainID) - continue + cfg, found := app.Config().GetTONConfig() + if !found { + logger.Std.Warn().Msgf("Unable to find TON config for chain %d", chainID) + continue + } + + tonClient, gateway, err := makeTONClient(ctx, cfg, chain.Params().GatewayAddress) + if err != nil { + logger.Std.Error().Err(err).Msgf("Unable to create TON client for chain %d", chainID) + continue + } + + tonSigner := tonsigner.New( + base.NewSigner(*rawChain, tss, ts, logger), + tonClient, + gateway, + ) + + addSigner(chainID, tonSigner) default: logger.Std.Warn(). Int64("signer.chain_id", chain.ID()). @@ -435,21 +454,13 @@ func syncObserverMap( continue } - tonClient, err := liteapi.NewFromSource(ctx, cfg.LiteClientConfigURL) - if err != nil { - logger.Std.Error().Err(err).Msgf("Unable to create TON liteapi for chain %d", chainID) - continue - } - - gatewayID, err := ton.ParseAccountID(params.GatewayAddress) + tonClient, gateway, err := makeTONClient(ctx, cfg, chain.Params().GatewayAddress) if err != nil { - logger.Std.Error().Err(err). - Msgf("Unable to parse gateway address %q for chain %d", params.GatewayAddress, chainID) + logger.Std.Error().Err(err).Msgf("Unable to create TON client for chain %d", chainID) continue } - gw := toncontracts.NewGateway(gatewayID) - tonObserver, err := tonobserver.New(baseObserver, tonClient, gw) + tonObserver, err := tonobserver.New(baseObserver, tonClient, gateway) if err != nil { logger.Std.Error().Err(err).Msgf("Unable to create TON observer for chain %d", chainID) continue @@ -469,3 +480,23 @@ func syncObserverMap( return added, removed, nil } + +func makeTONClient( + ctx context.Context, + cfg config.TONConfig, + gatewayAddr string, +) (*liteapi.Client, *toncontracts.Gateway, error) { + client, err := liteapi.NewFromSource(ctx, cfg.LiteClientConfigURL) + if err != nil { + return nil, nil, errors.Wrap(err, "Unable to create TON liteapi") + } + + gatewayID, err := ton.ParseAccountID(gatewayAddr) + if err != nil { + return nil, nil, errors.Wrapf(err, "Unable to parse gateway address %q", gatewayAddr) + } + + gw := toncontracts.NewGateway(gatewayID) + + return client, gw, nil +} diff --git a/zetaclient/orchestrator/orchestrator.go b/zetaclient/orchestrator/orchestrator.go index 698a520fcf..fe1c26b76a 100644 --- a/zetaclient/orchestrator/orchestrator.go +++ b/zetaclient/orchestrator/orchestrator.go @@ -24,7 +24,10 @@ import ( btcobserver "github.com/zeta-chain/node/zetaclient/chains/bitcoin/observer" "github.com/zeta-chain/node/zetaclient/chains/interfaces" solanaobserver "github.com/zeta-chain/node/zetaclient/chains/solana/observer" + tonobserver "github.com/zeta-chain/node/zetaclient/chains/ton/observer" + tonsigner "github.com/zeta-chain/node/zetaclient/chains/ton/signer" zctx "github.com/zeta-chain/node/zetaclient/context" + "github.com/zeta-chain/node/zetaclient/logs" "github.com/zeta-chain/node/zetaclient/metrics" "github.com/zeta-chain/node/zetaclient/outboundprocessor" "github.com/zeta-chain/node/zetaclient/ratelimiter" @@ -92,10 +95,8 @@ func New( return nil, errors.New("signerMap or observerMap is nil") } - log := multiLogger{ - Logger: logger.Std.With().Str("module", "orchestrator").Logger(), - Sampled: logger.Std.With().Str("module", "orchestrator").Logger().Sample(defaultLogSampler), - } + logging := logger.Std.With().Str(logs.FieldModule, "orchestrator").Logger() + multiLog := multiLogger{Logger: logging, Sampled: logging.Sample(defaultLogSampler)} balance, err := client.GetZetaHotKeyBalance(ctx) if err != nil { @@ -116,7 +117,7 @@ func New( dbDirectory: dbDirectory, baseLogger: logger, - logger: log, + logger: multiLog, ts: ts, stop: make(chan struct{}), }, nil @@ -194,6 +195,16 @@ func (oc *Orchestrator) resolveSigner(app *zctx.AppContext, chainID int64) (inte Str("signer.gateway_address", params.GatewayAddress). Msgf("updated gateway address for chain %d", chainID) } + case chain.IsTON(): + newAddress := chain.Params().GatewayAddress + + if newAddress != signer.GetGatewayAddress() { + signer.SetGatewayAddress(newAddress) + oc.logger.Info(). + Str("signer.new_gateway_address", newAddress). + Int64("signer.chain_id", chainID). + Msg("set gateway address") + } } return signer, nil @@ -236,8 +247,9 @@ func (oc *Orchestrator) resolveObserver(app *zctx.AppContext, chainID int64) (in if !observertypes.ChainParamsEqual(curParams, *freshParams) { observer.SetChainParams(*freshParams) oc.logger.Info(). + Int64("observer.chain_id", chainID). Interface("observer.chain_params", *freshParams). - Msgf("updated chain params for chainID %d", chainID) + Msg("updated chain params") } return observer, nil @@ -379,14 +391,16 @@ func (oc *Orchestrator) runScheduler(ctx context.Context) error { signer, err := oc.resolveSigner(app, chainID) if err != nil { oc.logger.Error().Err(err). - Msgf("runScheduler: unable to resolve signer for chain %d", chainID) + Int64(logs.FieldChain, chainID). + Msg("runScheduler: unable to resolve signer") continue } ob, err := oc.resolveObserver(app, chainID) if err != nil { oc.logger.Error().Err(err). - Msgf("runScheduler: resolveObserver failed for chain %d", chainID) + Int64(logs.FieldChain, chainID). + Msg("runScheduler: unable to resolve observer") continue } @@ -415,6 +429,8 @@ func (oc *Orchestrator) runScheduler(ctx context.Context) error { oc.ScheduleCctxBTC(ctx, zetaHeight, chainID, cctxList, ob, signer) case chain.IsSolana(): oc.ScheduleCctxSolana(ctx, zetaHeight, chainID, cctxList, ob, signer) + case chain.IsTON(): + oc.ScheduleCCTXTON(ctx, zetaHeight, chainID, cctxList, ob, signer) default: oc.logger.Error().Msgf("runScheduler: no scheduler found chain %d", chainID) continue @@ -618,7 +634,7 @@ func (oc *Orchestrator) ScheduleCctxSolana( oc.logger.Error().Msgf("ScheduleCctxSolana: chain observer is not a solana observer") return } - // #nosec G701 positive + // #nosec G115 positive interval := uint64(observer.ChainParams().OutboundScheduleInterval) // schedule keysign for each pending cctx @@ -664,6 +680,92 @@ func (oc *Orchestrator) ScheduleCctxSolana( } } +// ScheduleCCTXTON schedules TON outbound keySign on each ZetaChain block +func (oc *Orchestrator) ScheduleCCTXTON( + ctx context.Context, + zetaHeight uint64, + chainID int64, + cctxList []*types.CrossChainTx, + observer interfaces.ChainObserver, + signer interfaces.ChainSigner, +) { + // should never happen + if _, ok := observer.(*tonobserver.Observer); !ok { + oc.logger.Error().Msg("ScheduleCCTXTON: observer is not TON") + return + } + + if _, ok := signer.(*tonsigner.Signer); !ok { + oc.logger.Error().Msg("ScheduleCCTXTON: signer is not TON") + return + } + + // Scheduler interval measured in zeta blocks. + // runScheduler() guarantees that this function is called every zeta block. + // Note that TON blockchain is async and doesn't have a concept of confirmations + // i.e. tx is finalized as soon as it's included in the next block (less than 6 seconds) + // #nosec G115 positive + interval := uint64(observer.ChainParams().OutboundScheduleInterval) + + shouldProcessOutbounds := zetaHeight%interval == 0 + + for i := range cctxList { + var ( + cctx = cctxList[i] + params = cctx.GetCurrentOutboundParam() + nonce = params.TssNonce + outboundID = outboundprocessor.ToOutboundID(cctx.Index, params.ReceiverChainId, nonce) + ) + + if params.ReceiverChainId != chainID { + // should not happen + oc.logger.Error().Msgf("ScheduleCCTXTON: outbound chain id mismatch (got %d)", params.ReceiverChainId) + continue + } + + // vote outbound if it's already confirmed + continueKeySign, err := observer.VoteOutboundIfConfirmed(ctx, cctx) + + switch { + case err != nil: + oc.logger.Error().Err(err).Uint64("outbound.nonce", nonce). + Msg("ScheduleCCTXTON: VoteOutboundIfConfirmed failed") + continue + case !continueKeySign: + oc.logger.Info().Uint64("outbound.nonce", nonce). + Msg("ScheduleCCTXTON: outbound already processed") + continue + case !shouldProcessOutbounds: + // well, let's wait for another block to (probably) trigger the processing + continue + } + + // try to sign and broadcast cctx to TON + task := func(ctx context.Context) error { + signer.TryProcessOutbound( + ctx, + cctx, + oc.outboundProc, + outboundID, + observer, + oc.zetacoreClient, + zetaHeight, + ) + + return nil + } + + // fire async task + taskLogger := oc.logger.Logger.With(). + Int64(logs.FieldChain, chainID). + Str("outbound.id", outboundID). + Uint64("outbound.nonce", cctx.GetCurrentOutboundParam().TssNonce). + Logger() + + bg.Work(ctx, task, bg.WithName("TryProcessOutbound"), bg.WithLogger(taskLogger)) + } +} + // runObserverSignerSync runs a blocking ticker that observes chain changes from zetacore // and optionally (de)provisions respective observers and signers. func (oc *Orchestrator) runObserverSignerSync(ctx context.Context) error { @@ -709,8 +811,8 @@ func (oc *Orchestrator) syncObserverSigner(ctx context.Context) error { if added+removed > 0 { oc.logger.Info(). - Int("signers.added", added). - Int("signers.removed", removed). + Int("signer.added", added). + Int("signer.removed", removed). Msg("synced signers") } diff --git a/zetaclient/orchestrator/orchestrator_test.go b/zetaclient/orchestrator/orchestrator_test.go index 2ab34b900e..d88006920f 100644 --- a/zetaclient/orchestrator/orchestrator_test.go +++ b/zetaclient/orchestrator/orchestrator_test.go @@ -31,12 +31,14 @@ func Test_GetUpdatedSigner(t *testing.T) { evmChain = chains.Ethereum btcChain = chains.BitcoinMainnet solChain = chains.SolanaMainnet + tonChain = chains.TONMainnet ) var ( evmChainParams = mocks.MockChainParams(evmChain.ChainId, 100) btcChainParams = mocks.MockChainParams(btcChain.ChainId, 100) solChainParams = mocks.MockChainParams(solChain.ChainId, 100) + tonChainParams = mocks.MockChainParams(tonChain.ChainId, 100) ) solChainParams.GatewayAddress = solanacontracts.SolanaGatewayProgramID @@ -50,6 +52,9 @@ func Test_GetUpdatedSigner(t *testing.T) { solChainParamsNew := mocks.MockChainParams(solChain.ChainId, 100) solChainParamsNew.GatewayAddress = sample.SolanaAddress(t) + tonChainParamsNew := mocks.MockChainParams(tonChain.ChainId, 100) + tonChainParamsNew.GatewayAddress = sample.GenerateTONAccountID().ToRaw() + t.Run("signer should not be found", func(t *testing.T) { orchestrator := mockOrchestrator(t, nil, evmChain, btcChain, evmChainParams, btcChainParams) appContext := createAppContext(t, evmChain, btcChain, evmChainParamsNew, btcChainParams) @@ -86,6 +91,23 @@ func Test_GetUpdatedSigner(t *testing.T) { require.NoError(t, err) require.Equal(t, solChainParamsNew.GatewayAddress, signer.GetGatewayAddress()) }) + + t.Run("should be able to update ton gateway address", func(t *testing.T) { + orchestrator := mockOrchestrator(t, nil, + evmChain, btcChain, solChain, tonChain, + evmChainParams, btcChainParams, solChainParams, tonChainParams, + ) + + appContext := createAppContext(t, + evmChain, btcChain, solChain, tonChain, + evmChainParams, btcChainParams, solChainParamsNew, tonChainParamsNew, + ) + + // update signer with new gateway address + signer, err := orchestrator.resolveSigner(appContext, tonChain.ChainId) + require.NoError(t, err) + require.Equal(t, tonChainParamsNew.GatewayAddress, signer.GetGatewayAddress()) + }) } func Test_GetUpdatedChainObserver(t *testing.T) { @@ -94,15 +116,18 @@ func Test_GetUpdatedChainObserver(t *testing.T) { evmChain = chains.Ethereum btcChain = chains.BitcoinMainnet solChain = chains.SolanaMainnet + tonChain = chains.TONMainnet ) var ( evmChainParams = mocks.MockChainParams(evmChain.ChainId, 100) btcChainParams = mocks.MockChainParams(btcChain.ChainId, 100) solChainParams = mocks.MockChainParams(solChain.ChainId, 100) + tonChainParams = mocks.MockChainParams(tonChain.ChainId, 100) ) solChainParams.GatewayAddress = solanacontracts.SolanaGatewayProgramID + tonChainParams.GatewayAddress = sample.GenerateTONAccountID().ToRaw() // new chain params in AppContext evmChainParamsNew := &observertypes.ChainParams{ @@ -153,6 +178,22 @@ func Test_GetUpdatedChainObserver(t *testing.T) { MinObserverDelegation: sdk.OneDec(), IsSupported: true, } + tonChainParamsNew := &observertypes.ChainParams{ + ChainId: tonChain.ChainId, + ConfirmationCount: 10, + GasPriceTicker: 5, + InboundTicker: 6, + OutboundTicker: 6, + WatchUtxoTicker: 1, + ZetaTokenContractAddress: "", + ConnectorContractAddress: "", + Erc20CustodyContractAddress: "", + OutboundScheduleInterval: 10, + OutboundScheduleLookahead: 10, + BallotThreshold: sdk.OneDec(), + MinObserverDelegation: sdk.OneDec(), + IsSupported: true, + } t.Run("evm chain observer should not be found", func(t *testing.T) { orchestrator := mockOrchestrator( @@ -284,6 +325,43 @@ func Test_GetUpdatedChainObserver(t *testing.T) { require.NotNil(t, chainOb) require.True(t, observertypes.ChainParamsEqual(*solChainParamsNew, chainOb.ChainParams())) }) + t.Run("ton chain observer should not be found", func(t *testing.T) { + orchestrator := mockOrchestrator( + t, + nil, + evmChain, btcChain, solChain, + evmChainParams, btcChainParams, solChainParams, + ) + + appContext := createAppContext( + t, + evmChain, + btcChain, + solChain, + evmChainParams, + btcChainParams, + solChainParamsNew, + ) + + _, err := orchestrator.resolveObserver(appContext, tonChain.ChainId) + require.ErrorContains(t, err, "observer not found") + }) + t.Run("chain params in ton chain observer should be updated successfully", func(t *testing.T) { + orchestrator := mockOrchestrator(t, nil, + evmChain, btcChain, tonChain, + evmChainParams, btcChainParams, tonChainParams, + ) + appContext := createAppContext(t, + evmChain, btcChain, tonChain, + evmChainParams, btcChainParams, tonChainParamsNew, + ) + + // update solana chain observer with new chain params + chainOb, err := orchestrator.resolveObserver(appContext, tonChain.ChainId) + require.NoError(t, err) + require.NotNil(t, chainOb) + require.True(t, observertypes.ChainParamsEqual(*tonChainParamsNew, chainOb.ChainParams())) + }) } func Test_GetPendingCctxsWithinRateLimit(t *testing.T) { @@ -500,6 +578,9 @@ func mockOrchestrator(t *testing.T, zetaClient interfaces.ZetacoreClient, chains case chains.IsSolanaChain(cp.ChainId, nil): observers[cp.ChainId] = mocks.NewSolanaObserver(cp) signers[cp.ChainId] = mocks.NewSolanaSigner() + case chains.IsTONChain(cp.ChainId, nil): + observers[cp.ChainId] = mocks.NewTONObserver(cp) + signers[cp.ChainId] = mocks.NewTONSigner() default: t.Fatalf("mock orcestrator: unsupported chain %d", cp.ChainId) } @@ -526,6 +607,8 @@ func createAppContext(t *testing.T, chainsOrParams ...any) *zctx.AppContext { cfg.BTCChainConfigs[c.ChainId] = config.BTCConfig{RPCHost: "localhost"} case chains.IsSolanaChain(c.ChainId, nil): cfg.SolanaConfig = config.SolanaConfig{Endpoint: "localhost"} + case chains.IsTONChain(c.ChainId, nil): + cfg.TONConfig = config.TONConfig{LiteClientConfigURL: "localhost"} default: t.Fatalf("create app context: unsupported chain %d", c.ChainId) } diff --git a/zetaclient/testdata/solana/chain_901_inbound_tx_result_MS3MPLN7hkbyCZFwKqXcg8fmEvQMD74fN6Ps2LSWXJoRxPW5ehaxBorK9q1JFVbqnAvu9jXm6ertj7kT7HpYw1j.json b/zetaclient/testdata/solana/chain_901_inbound_tx_result_MS3MPLN7hkbyCZFwKqXcg8fmEvQMD74fN6Ps2LSWXJoRxPW5ehaxBorK9q1JFVbqnAvu9jXm6ertj7kT7HpYw1j.json index 210d639ead..cf7edb3b81 100644 --- a/zetaclient/testdata/solana/chain_901_inbound_tx_result_MS3MPLN7hkbyCZFwKqXcg8fmEvQMD74fN6Ps2LSWXJoRxPW5ehaxBorK9q1JFVbqnAvu9jXm6ertj7kT7HpYw1j.json +++ b/zetaclient/testdata/solana/chain_901_inbound_tx_result_MS3MPLN7hkbyCZFwKqXcg8fmEvQMD74fN6Ps2LSWXJoRxPW5ehaxBorK9q1JFVbqnAvu9jXm6ertj7kT7HpYw1j.json @@ -8,9 +8,9 @@ "message": { "accountKeys": [ "AS48jKNQsDGkEdDvfwu1QpqjtqbCadrAq9nGXjFmdX3Z", - "2f9SLuUNb7TNeM6gzBwT4ZjbL5ZyKzzHg1Ce9yiquEjj", + "9dcAyYG4bawApZocwZSyJBi9Mynf5EuKAJfifXdfkqik", "11111111111111111111111111111111", - "ZETAjseVjuFsxdRxo6MmTCvqFwb3ZHUx56Co3vCmGis" + "94U5AHQMKkV5txNJ17QPXWoh474PheGou6cNP2FEuL1d" ], "header": { "numRequiredSignatures": 1, @@ -47,13 +47,13 @@ "preTokenBalances": [], "postTokenBalances": [], "logMessages": [ - "Program ZETAjseVjuFsxdRxo6MmTCvqFwb3ZHUx56Co3vCmGis invoke [1]", + "Program 94U5AHQMKkV5txNJ17QPXWoh474PheGou6cNP2FEuL1d invoke [1]", "Program log: Instruction: Deposit", "Program 11111111111111111111111111111111 invoke [2]", "Program 11111111111111111111111111111111 success", "Program log: AS48jKNQsDGkEdDvfwu1QpqjtqbCadrAq9nGXjFmdX3Z deposits 100000 lamports to PDA", - "Program ZETAjseVjuFsxdRxo6MmTCvqFwb3ZHUx56Co3vCmGis consumed 17006 of 200000 compute units", - "Program ZETAjseVjuFsxdRxo6MmTCvqFwb3ZHUx56Co3vCmGis success" + "Program 94U5AHQMKkV5txNJ17QPXWoh474PheGou6cNP2FEuL1d consumed 17006 of 200000 compute units", + "Program 94U5AHQMKkV5txNJ17QPXWoh474PheGou6cNP2FEuL1d success" ], "status": { "Ok": null }, "rewards": [], diff --git a/zetaclient/testdata/solana/chain_901_outbound_tx_result_phM9bESbiqojmpkkUxgjed8EABkxvPGNau9q31B8Yk1sXUtsxJvd6G9VbZZQPsEyn6RiTH4YBtqJ89omqfbbNNY.json b/zetaclient/testdata/solana/chain_901_outbound_tx_result_phM9bESbiqojmpkkUxgjed8EABkxvPGNau9q31B8Yk1sXUtsxJvd6G9VbZZQPsEyn6RiTH4YBtqJ89omqfbbNNY.json new file mode 100644 index 0000000000..c488facac3 --- /dev/null +++ b/zetaclient/testdata/solana/chain_901_outbound_tx_result_phM9bESbiqojmpkkUxgjed8EABkxvPGNau9q31B8Yk1sXUtsxJvd6G9VbZZQPsEyn6RiTH4YBtqJ89omqfbbNNY.json @@ -0,0 +1,76 @@ +{ + "slot": 1109, + "blockTime": 1730732052, + "transaction": { + "signatures": [ + "phM9bESbiqojmpkkUxgjed8EABkxvPGNau9q31B8Yk1sXUtsxJvd6G9VbZZQPsEyn6RiTH4YBtqJ89omqfbbNNY" + ], + "message": { + "accountKeys": [ + "2qBVcNBZCubcnSR3NyCnFjCfkCVUB3G7ECPoaW5rxVjx", + "3eXQYW8nC9142kJUHRgZ9RggJaMgpAEtnZPrwPT7CdxH", + "9dcAyYG4bawApZocwZSyJBi9Mynf5EuKAJfifXdfkqik", + "GNQPa92uBDem5ZFH16TkmFwN5EN8LAzkqeRrxsxZt4eD", + "11111111111111111111111111111111", + "94U5AHQMKkV5txNJ17QPXWoh474PheGou6cNP2FEuL1d" + ], + "header": { + "numRequiredSignatures": 1, + "numReadonlySignedAccounts": 0, + "numReadonlyUnsignedAccounts": 3 + }, + "recentBlockhash": "94KjHcf2zDN6VtbFnVU3vkEqJv4d5jWCzBTvg6PtPkdB", + "instructions": [ + { + "programIdIndex": 5, + "accounts": [ + 1, + 3, + 2, + 0, + 4 + ], + "data": "SDhLNtfumZy7dZ96HRWDWWC9NHtnc54NUDt3XAAY8msc42QtH8rF3nYfFcmFjX64KsoMSYNtkWQTv4iVU3Ly36a5ff3nEU5aPbgeBGAPsMbnEiX1bz51dHoyMJjpKxvWJbmCxEG6Z8tA1Tk4EcY39DTDRH" + } + ] + } + }, + "meta": { + "err": null, + "fee": 5000, + "preBalances": [ + 99999985000, + 14947680, + 1461600, + 1, + 1141440 + ], + "postBalances": [ + 99999033440, + 946560, + 14947680, + 1461600, + 1, + 1141440 + ], + "innerInstructions": [], + "preTokenBalances": [], + "postTokenBalances": [], + "logMessages": [ + "Program 94U5AHQMKkV5txNJ17QPXWoh474PheGou6cNP2FEuL1d invoke [1]", + "Program log: Instruction: WhitelistSplMint Program 11111111111111111111111111111111 invoke [2]", + "Program 11111111111111111111111111111111 success", + "Program log: recovered address [126, 140, 123, 172, 211, 198, 34, 13, 220, 53, 164, 234, 17, 65, 190, 20, 242, 225, 223, 235]", + "Program 94U5AHQMKkV5txNJ17QPXWoh474PheGou6cNP2FEuL1d consumed 46731 of 200000 compute units", + "Program 94U5AHQMKkV5txNJ17QPXWoh474PheGou6cNP2FEuL1d success" + ], + "status": { "Ok": null }, + "rewards": [], + "loadedAddresses": { + "readonly": [], + "writable": [] + }, + "computeUnitsConsumed": 274896977280 + }, + "version": 0 +} \ No newline at end of file diff --git a/zetaclient/testutils/constant.go b/zetaclient/testutils/constant.go index e9b8b53563..f776c7019f 100644 --- a/zetaclient/testutils/constant.go +++ b/zetaclient/testutils/constant.go @@ -42,7 +42,9 @@ const ( // GatewayAddresses contains constants gateway addresses for testing var GatewayAddresses = map[int64]string{ // Gateway address on Solana devnet - chains.SolanaDevnet.ChainId: "ZETAjseVjuFsxdRxo6MmTCvqFwb3ZHUx56Co3vCmGis", + // NOTE: currently different deployer key pair is used for development compared to live networks + // as live networks key pair is sensitive information at this point, can be unified once we have deployments completed + chains.SolanaDevnet.ChainId: "94U5AHQMKkV5txNJ17QPXWoh474PheGou6cNP2FEuL1d", } // ConnectorAddresses contains constants ERC20 connector addresses for testing diff --git a/zetaclient/testutils/mocks/chain_clients.go b/zetaclient/testutils/mocks/chain_clients.go index aa5e36889b..14ec720873 100644 --- a/zetaclient/testutils/mocks/chain_clients.go +++ b/zetaclient/testutils/mocks/chain_clients.go @@ -3,11 +3,22 @@ package mocks import ( "context" - crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" + cc "github.com/zeta-chain/node/x/crosschain/types" observertypes "github.com/zeta-chain/node/x/observer/types" "github.com/zeta-chain/node/zetaclient/chains/interfaces" ) +type DummyObserver struct{} + +func (ob *DummyObserver) VoteOutboundIfConfirmed(_ context.Context, _ *cc.CrossChainTx) (bool, error) { + return false, nil +} + +func (ob *DummyObserver) Start(_ context.Context) {} +func (ob *DummyObserver) Stop() {} +func (ob *DummyObserver) SetChainParams(_ observertypes.ChainParams) {} +func (ob *DummyObserver) ChainParams() (_ observertypes.ChainParams) { return } + // ---------------------------------------------------------------------------- // EVMObserver // ---------------------------------------------------------------------------- @@ -15,23 +26,12 @@ var _ interfaces.ChainObserver = (*EVMObserver)(nil) // EVMObserver is a mock of evm chain observer for testing type EVMObserver struct { + DummyObserver chainParams observertypes.ChainParams } func NewEVMObserver(chainParams *observertypes.ChainParams) *EVMObserver { - return &EVMObserver{ - chainParams: *chainParams, - } -} - -func (ob *EVMObserver) Start(_ context.Context) {} -func (ob *EVMObserver) Stop() {} - -func (ob *EVMObserver) VoteOutboundIfConfirmed( - _ context.Context, - _ *crosschaintypes.CrossChainTx, -) (bool, error) { - return false, nil + return &EVMObserver{chainParams: *chainParams} } func (ob *EVMObserver) SetChainParams(chainParams observertypes.ChainParams) { @@ -42,14 +42,6 @@ func (ob *EVMObserver) ChainParams() observertypes.ChainParams { return ob.chainParams } -func (ob *EVMObserver) GetTxID(_ uint64) string { - return "" -} - -func (ob *EVMObserver) WatchInboundTracker(_ context.Context) error { - return nil -} - // ---------------------------------------------------------------------------- // BTCObserver // ---------------------------------------------------------------------------- @@ -57,24 +49,12 @@ var _ interfaces.ChainObserver = (*BTCObserver)(nil) // BTCObserver is a mock of btc chain observer for testing type BTCObserver struct { + DummyObserver chainParams observertypes.ChainParams } func NewBTCObserver(chainParams *observertypes.ChainParams) *BTCObserver { - return &BTCObserver{ - chainParams: *chainParams, - } -} - -func (ob *BTCObserver) Start(_ context.Context) {} - -func (ob *BTCObserver) Stop() {} - -func (ob *BTCObserver) VoteOutboundIfConfirmed( - _ context.Context, - _ *crosschaintypes.CrossChainTx, -) (bool, error) { - return false, nil + return &BTCObserver{chainParams: *chainParams} } func (ob *BTCObserver) SetChainParams(chainParams observertypes.ChainParams) { @@ -85,12 +65,6 @@ func (ob *BTCObserver) ChainParams() observertypes.ChainParams { return ob.chainParams } -func (ob *BTCObserver) GetTxID(_ uint64) string { - return "" -} - -func (ob *BTCObserver) WatchInboundTracker(_ context.Context) error { return nil } - // ---------------------------------------------------------------------------- // SolanaObserver // ---------------------------------------------------------------------------- @@ -98,24 +72,12 @@ var _ interfaces.ChainObserver = (*SolanaObserver)(nil) // SolanaObserver is a mock of solana chain observer for testing type SolanaObserver struct { + DummyObserver chainParams observertypes.ChainParams } func NewSolanaObserver(chainParams *observertypes.ChainParams) *SolanaObserver { - return &SolanaObserver{ - chainParams: *chainParams, - } -} - -func (ob *SolanaObserver) Start(_ context.Context) {} - -func (ob *SolanaObserver) Stop() {} - -func (ob *SolanaObserver) VoteOutboundIfConfirmed( - _ context.Context, - _ *crosschaintypes.CrossChainTx, -) (bool, error) { - return false, nil + return &SolanaObserver{chainParams: *chainParams} } func (ob *SolanaObserver) SetChainParams(chainParams observertypes.ChainParams) { @@ -126,8 +88,25 @@ func (ob *SolanaObserver) ChainParams() observertypes.ChainParams { return ob.chainParams } -func (ob *SolanaObserver) GetTxID(_ uint64) string { - return "" +// ---------------------------------------------------------------------------- +// TONObserver +// ---------------------------------------------------------------------------- +var _ interfaces.ChainObserver = (*TONObserver)(nil) + +// TONObserver is a mock of TON chain observer for testing +type TONObserver struct { + DummyObserver + chainParams observertypes.ChainParams +} + +func NewTONObserver(chainParams *observertypes.ChainParams) *TONObserver { + return &TONObserver{chainParams: *chainParams} } -func (ob *SolanaObserver) WatchInboundTracker(_ context.Context) error { return nil } +func (ob *TONObserver) SetChainParams(chainParams observertypes.ChainParams) { + ob.chainParams = chainParams +} + +func (ob *TONObserver) ChainParams() observertypes.ChainParams { + return ob.chainParams +} diff --git a/zetaclient/testutils/mocks/chain_signer.go b/zetaclient/testutils/mocks/chain_signer.go index cc0c65457f..790ee8fd92 100644 --- a/zetaclient/testutils/mocks/chain_signer.go +++ b/zetaclient/testutils/mocks/chain_signer.go @@ -11,6 +11,26 @@ import ( "github.com/zeta-chain/node/zetaclient/outboundprocessor" ) +type DummySigner struct{} + +func (s *DummySigner) TryProcessOutbound( + _ context.Context, + _ *crosschaintypes.CrossChainTx, + _ *outboundprocessor.Processor, + _ string, + _ interfaces.ChainObserver, + _ interfaces.ZetacoreClient, + _ uint64, +) { +} + +func (s *DummySigner) SetGatewayAddress(_ string) {} +func (s *DummySigner) GetGatewayAddress() (_ string) { return } +func (s *DummySigner) SetZetaConnectorAddress(_ ethcommon.Address) {} +func (s *DummySigner) SetERC20CustodyAddress(_ ethcommon.Address) {} +func (s *DummySigner) GetZetaConnectorAddress() (_ ethcommon.Address) { return } +func (s *DummySigner) GetERC20CustodyAddress() (_ ethcommon.Address) { return } + // ---------------------------------------------------------------------------- // EVMSigner // ---------------------------------------------------------------------------- @@ -18,6 +38,7 @@ var _ interfaces.ChainSigner = (*EVMSigner)(nil) // EVMSigner is a mock of evm chain signer for testing type EVMSigner struct { + DummySigner Chain chains.Chain ZetaConnectorAddress ethcommon.Address ERC20CustodyAddress ethcommon.Address @@ -35,24 +56,6 @@ func NewEVMSigner( } } -func (s *EVMSigner) TryProcessOutbound( - _ context.Context, - _ *crosschaintypes.CrossChainTx, - _ *outboundprocessor.Processor, - _ string, - _ interfaces.ChainObserver, - _ interfaces.ZetacoreClient, - _ uint64, -) { -} - -func (s *EVMSigner) SetGatewayAddress(_ string) { -} - -func (s *EVMSigner) GetGatewayAddress() string { - return "" -} - func (s *EVMSigner) SetZetaConnectorAddress(address ethcommon.Address) { s.ZetaConnectorAddress = address } @@ -75,45 +78,12 @@ func (s *EVMSigner) GetERC20CustodyAddress() ethcommon.Address { var _ interfaces.ChainSigner = (*BTCSigner)(nil) // BTCSigner is a mock of bitcoin chain signer for testing -type BTCSigner struct { -} +type BTCSigner = DummySigner func NewBTCSigner() *BTCSigner { return &BTCSigner{} } -func (s *BTCSigner) TryProcessOutbound( - _ context.Context, - _ *crosschaintypes.CrossChainTx, - _ *outboundprocessor.Processor, - _ string, - _ interfaces.ChainObserver, - _ interfaces.ZetacoreClient, - _ uint64, -) { -} - -func (s *BTCSigner) SetGatewayAddress(_ string) { -} - -func (s *BTCSigner) GetGatewayAddress() string { - return "" -} - -func (s *BTCSigner) SetZetaConnectorAddress(_ ethcommon.Address) { -} - -func (s *BTCSigner) SetERC20CustodyAddress(_ ethcommon.Address) { -} - -func (s *BTCSigner) GetZetaConnectorAddress() ethcommon.Address { - return ethcommon.Address{} -} - -func (s *BTCSigner) GetERC20CustodyAddress() ethcommon.Address { - return ethcommon.Address{} -} - // ---------------------------------------------------------------------------- // SolanaSigner // ---------------------------------------------------------------------------- @@ -121,6 +91,7 @@ var _ interfaces.ChainSigner = (*SolanaSigner)(nil) // SolanaSigner is a mock of solana chain signer for testing type SolanaSigner struct { + DummySigner GatewayAddress string } @@ -128,17 +99,6 @@ func NewSolanaSigner() *SolanaSigner { return &SolanaSigner{} } -func (s *SolanaSigner) TryProcessOutbound( - _ context.Context, - _ *crosschaintypes.CrossChainTx, - _ *outboundprocessor.Processor, - _ string, - _ interfaces.ChainObserver, - _ interfaces.ZetacoreClient, - _ uint64, -) { -} - func (s *SolanaSigner) SetGatewayAddress(address string) { s.GatewayAddress = address } @@ -147,16 +107,25 @@ func (s *SolanaSigner) GetGatewayAddress() string { return s.GatewayAddress } -func (s *SolanaSigner) SetZetaConnectorAddress(_ ethcommon.Address) { +// ---------------------------------------------------------------------------- +// TONSigner +// ---------------------------------------------------------------------------- +var _ interfaces.ChainSigner = (*TONSigner)(nil) + +// TONSigner is a mock of TON chain signer for testing +type TONSigner struct { + DummySigner + GatewayAddress string } -func (s *SolanaSigner) SetERC20CustodyAddress(_ ethcommon.Address) { +func NewTONSigner() *TONSigner { + return &TONSigner{} } -func (s *SolanaSigner) GetZetaConnectorAddress() ethcommon.Address { - return ethcommon.Address{} +func (s *TONSigner) SetGatewayAddress(address string) { + s.GatewayAddress = address } -func (s *SolanaSigner) GetERC20CustodyAddress() ethcommon.Address { - return ethcommon.Address{} +func (s *TONSigner) GetGatewayAddress() string { + return s.GatewayAddress } diff --git a/zetaclient/testutils/mocks/ton_liteclient.go b/zetaclient/testutils/mocks/ton_liteclient.go index f11ccaf24c..0fa56f4ee6 100644 --- a/zetaclient/testutils/mocks/ton_liteclient.go +++ b/zetaclient/testutils/mocks/ton_liteclient.go @@ -5,6 +5,9 @@ package mocks import ( context "context" + liteapi "github.com/tonkeeper/tongo/liteapi" + liteclient "github.com/tonkeeper/tongo/liteclient" + mock "github.com/stretchr/testify/mock" tlb "github.com/tonkeeper/tongo/tlb" @@ -45,9 +48,37 @@ func (_m *LiteClient) GetBlockHeader(ctx context.Context, blockID ton.BlockIDExt return r0, r1 } -// GetFirstTransaction provides a mock function with given fields: ctx, id -func (_m *LiteClient) GetFirstTransaction(ctx context.Context, id ton.AccountID) (*ton.Transaction, int, error) { - ret := _m.Called(ctx, id) +// GetConfigParams provides a mock function with given fields: ctx, mode, params +func (_m *LiteClient) GetConfigParams(ctx context.Context, mode liteapi.ConfigMode, params []uint32) (tlb.ConfigParams, error) { + ret := _m.Called(ctx, mode, params) + + if len(ret) == 0 { + panic("no return value specified for GetConfigParams") + } + + var r0 tlb.ConfigParams + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, liteapi.ConfigMode, []uint32) (tlb.ConfigParams, error)); ok { + return rf(ctx, mode, params) + } + if rf, ok := ret.Get(0).(func(context.Context, liteapi.ConfigMode, []uint32) tlb.ConfigParams); ok { + r0 = rf(ctx, mode, params) + } else { + r0 = ret.Get(0).(tlb.ConfigParams) + } + + if rf, ok := ret.Get(1).(func(context.Context, liteapi.ConfigMode, []uint32) error); ok { + r1 = rf(ctx, mode, params) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetFirstTransaction provides a mock function with given fields: ctx, acc +func (_m *LiteClient) GetFirstTransaction(ctx context.Context, acc ton.AccountID) (*ton.Transaction, int, error) { + ret := _m.Called(ctx, acc) if len(ret) == 0 { panic("no return value specified for GetFirstTransaction") @@ -57,10 +88,10 @@ func (_m *LiteClient) GetFirstTransaction(ctx context.Context, id ton.AccountID) var r1 int var r2 error if rf, ok := ret.Get(0).(func(context.Context, ton.AccountID) (*ton.Transaction, int, error)); ok { - return rf(ctx, id) + return rf(ctx, acc) } if rf, ok := ret.Get(0).(func(context.Context, ton.AccountID) *ton.Transaction); ok { - r0 = rf(ctx, id) + r0 = rf(ctx, acc) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*ton.Transaction) @@ -68,13 +99,13 @@ func (_m *LiteClient) GetFirstTransaction(ctx context.Context, id ton.AccountID) } if rf, ok := ret.Get(1).(func(context.Context, ton.AccountID) int); ok { - r1 = rf(ctx, id) + r1 = rf(ctx, acc) } else { r1 = ret.Get(1).(int) } if rf, ok := ret.Get(2).(func(context.Context, ton.AccountID) error); ok { - r2 = rf(ctx, id) + r2 = rf(ctx, acc) } else { r2 = ret.Error(2) } @@ -82,9 +113,65 @@ func (_m *LiteClient) GetFirstTransaction(ctx context.Context, id ton.AccountID) return r0, r1, r2 } -// GetTransactionsSince provides a mock function with given fields: ctx, acc, lt, bits -func (_m *LiteClient) GetTransactionsSince(ctx context.Context, acc ton.AccountID, lt uint64, bits ton.Bits256) ([]ton.Transaction, error) { - ret := _m.Called(ctx, acc, lt, bits) +// GetMasterchainInfo provides a mock function with given fields: ctx +func (_m *LiteClient) GetMasterchainInfo(ctx context.Context) (liteclient.LiteServerMasterchainInfoC, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetMasterchainInfo") + } + + var r0 liteclient.LiteServerMasterchainInfoC + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (liteclient.LiteServerMasterchainInfoC, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) liteclient.LiteServerMasterchainInfoC); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(liteclient.LiteServerMasterchainInfoC) + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetTransaction provides a mock function with given fields: ctx, acc, lt, hash +func (_m *LiteClient) GetTransaction(ctx context.Context, acc ton.AccountID, lt uint64, hash ton.Bits256) (ton.Transaction, error) { + ret := _m.Called(ctx, acc, lt, hash) + + if len(ret) == 0 { + panic("no return value specified for GetTransaction") + } + + var r0 ton.Transaction + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, ton.AccountID, uint64, ton.Bits256) (ton.Transaction, error)); ok { + return rf(ctx, acc, lt, hash) + } + if rf, ok := ret.Get(0).(func(context.Context, ton.AccountID, uint64, ton.Bits256) ton.Transaction); ok { + r0 = rf(ctx, acc, lt, hash) + } else { + r0 = ret.Get(0).(ton.Transaction) + } + + if rf, ok := ret.Get(1).(func(context.Context, ton.AccountID, uint64, ton.Bits256) error); ok { + r1 = rf(ctx, acc, lt, hash) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetTransactionsSince provides a mock function with given fields: ctx, acc, lt, hash +func (_m *LiteClient) GetTransactionsSince(ctx context.Context, acc ton.AccountID, lt uint64, hash ton.Bits256) ([]ton.Transaction, error) { + ret := _m.Called(ctx, acc, lt, hash) if len(ret) == 0 { panic("no return value specified for GetTransactionsSince") @@ -93,10 +180,10 @@ func (_m *LiteClient) GetTransactionsSince(ctx context.Context, acc ton.AccountI var r0 []ton.Transaction var r1 error if rf, ok := ret.Get(0).(func(context.Context, ton.AccountID, uint64, ton.Bits256) ([]ton.Transaction, error)); ok { - return rf(ctx, acc, lt, bits) + return rf(ctx, acc, lt, hash) } if rf, ok := ret.Get(0).(func(context.Context, ton.AccountID, uint64, ton.Bits256) []ton.Transaction); ok { - r0 = rf(ctx, acc, lt, bits) + r0 = rf(ctx, acc, lt, hash) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]ton.Transaction) @@ -104,7 +191,7 @@ func (_m *LiteClient) GetTransactionsSince(ctx context.Context, acc ton.AccountI } if rf, ok := ret.Get(1).(func(context.Context, ton.AccountID, uint64, ton.Bits256) error); ok { - r1 = rf(ctx, acc, lt, bits) + r1 = rf(ctx, acc, lt, hash) } else { r1 = ret.Error(1) } diff --git a/zetaclient/testutils/mocks/ton_signerliteclient.go b/zetaclient/testutils/mocks/ton_signerliteclient.go new file mode 100644 index 0000000000..4cbd6360eb --- /dev/null +++ b/zetaclient/testutils/mocks/ton_signerliteclient.go @@ -0,0 +1,118 @@ +// Code generated by mockery v2.43.2. DO NOT EDIT. + +package mocks + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + + tlb "github.com/tonkeeper/tongo/tlb" + + ton "github.com/tonkeeper/tongo/ton" +) + +// SignerLiteClient is an autogenerated mock type for the LiteClient type +type SignerLiteClient struct { + mock.Mock +} + +// GetAccountState provides a mock function with given fields: ctx, accountID +func (_m *SignerLiteClient) GetAccountState(ctx context.Context, accountID ton.AccountID) (tlb.ShardAccount, error) { + ret := _m.Called(ctx, accountID) + + if len(ret) == 0 { + panic("no return value specified for GetAccountState") + } + + var r0 tlb.ShardAccount + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, ton.AccountID) (tlb.ShardAccount, error)); ok { + return rf(ctx, accountID) + } + if rf, ok := ret.Get(0).(func(context.Context, ton.AccountID) tlb.ShardAccount); ok { + r0 = rf(ctx, accountID) + } else { + r0 = ret.Get(0).(tlb.ShardAccount) + } + + if rf, ok := ret.Get(1).(func(context.Context, ton.AccountID) error); ok { + r1 = rf(ctx, accountID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetTransactionsSince provides a mock function with given fields: ctx, acc, lt, hash +func (_m *SignerLiteClient) GetTransactionsSince(ctx context.Context, acc ton.AccountID, lt uint64, hash ton.Bits256) ([]ton.Transaction, error) { + ret := _m.Called(ctx, acc, lt, hash) + + if len(ret) == 0 { + panic("no return value specified for GetTransactionsSince") + } + + var r0 []ton.Transaction + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, ton.AccountID, uint64, ton.Bits256) ([]ton.Transaction, error)); ok { + return rf(ctx, acc, lt, hash) + } + if rf, ok := ret.Get(0).(func(context.Context, ton.AccountID, uint64, ton.Bits256) []ton.Transaction); ok { + r0 = rf(ctx, acc, lt, hash) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]ton.Transaction) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, ton.AccountID, uint64, ton.Bits256) error); ok { + r1 = rf(ctx, acc, lt, hash) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SendMessage provides a mock function with given fields: ctx, payload +func (_m *SignerLiteClient) SendMessage(ctx context.Context, payload []byte) (uint32, error) { + ret := _m.Called(ctx, payload) + + if len(ret) == 0 { + panic("no return value specified for SendMessage") + } + + var r0 uint32 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, []byte) (uint32, error)); ok { + return rf(ctx, payload) + } + if rf, ok := ret.Get(0).(func(context.Context, []byte) uint32); ok { + r0 = rf(ctx, payload) + } else { + r0 = ret.Get(0).(uint32) + } + + if rf, ok := ret.Get(1).(func(context.Context, []byte) error); ok { + r1 = rf(ctx, payload) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewSignerLiteClient creates a new instance of SignerLiteClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewSignerLiteClient(t interface { + mock.TestingT + Cleanup(func()) +}) *SignerLiteClient { + mock := &SignerLiteClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/zetaclient/testutils/mocks/tss_signer.go b/zetaclient/testutils/mocks/tss_signer.go index e0d9bd384c..78ce36649a 100644 --- a/zetaclient/testutils/mocks/tss_signer.go +++ b/zetaclient/testutils/mocks/tss_signer.go @@ -4,12 +4,14 @@ import ( "context" "crypto/ecdsa" "fmt" + "testing" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" ethcommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/require" "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/zetaclient/chains/interfaces" @@ -61,6 +63,29 @@ func NewTSSAthens3() *TSS { return NewMockTSS(chains.BscTestnet, testutils.TSSAddressEVMAthens3, testutils.TSSAddressBTCAthens3) } +func NewGeneratedTSS(t *testing.T, chain chains.Chain) *TSS { + pk, err := crypto.GenerateKey() + require.NoError(t, err) + + btcPub, err := btcec.ParsePubKey(crypto.FromECDSAPub(&pk.PublicKey)) + require.NoError(t, err) + + btcAddress, err := btcutil.NewAddressWitnessPubKeyHash( + btcutil.Hash160(btcPub.SerializeCompressed()), + &chaincfg.TestNet3Params, + ) + + require.NoError(t, err) + + return &TSS{ + paused: false, + chain: chain, + evmAddress: crypto.PubkeyToAddress(pk.PublicKey).Hex(), + btcAddress: btcAddress.String(), + PrivKey: pk, + } +} + // WithPrivKey sets the private key for the TSS func (s *TSS) WithPrivKey(privKey *ecdsa.PrivateKey) *TSS { s.PrivKey = privKey diff --git a/zetaclient/tss/tss_signer.go b/zetaclient/tss/tss_signer.go index 0c7daa98e7..e3df81d7ad 100644 --- a/zetaclient/tss/tss_signer.go +++ b/zetaclient/tss/tss_signer.go @@ -148,6 +148,7 @@ func SetupTSSServer( cfg config.Config, tssPassword string, enableMonitor bool, + whitelistedPeers []gopeer.ID, ) (*tss.TssServer, error) { bootstrapPeers := peer log.Info().Msgf("Peers AddrList %v", bootstrapPeers) @@ -173,7 +174,6 @@ func SetupTSSServer( bootstrapPeers, 6668, privkey, - "MetaMetaOpenTheDoor", tsspath, thorcommon.TssConfig{ EnableMonitor: enableMonitor, @@ -185,6 +185,7 @@ func SetupTSSServer( preParams, // use pre-generated pre-params if non-nil IP, // for docker test tssPassword, + whitelistedPeers, ) if err != nil { log.Error().Err(err).Msg("NewTSS error") diff --git a/zetaclient/types/dynamic_ticker.go b/zetaclient/types/dynamic_ticker.go index 103bfffb94..bfbc1e3cd4 100644 --- a/zetaclient/types/dynamic_ticker.go +++ b/zetaclient/types/dynamic_ticker.go @@ -23,7 +23,8 @@ func NewDynamicTicker(name string, interval uint64) (*DynamicTicker, error) { return &DynamicTicker{ name: name, interval: interval, - impl: time.NewTicker(time.Duration(interval) * time.Second), + // #nosec G115 interval is in range and not user controlled + impl: time.NewTicker(time.Duration(interval) * time.Second), }, nil } @@ -38,6 +39,7 @@ func (t *DynamicTicker) UpdateInterval(newInterval uint64, logger zerolog.Logger t.impl.Stop() oldInterval := t.interval t.interval = newInterval + // #nosec G115 interval is in range and not user controlled t.impl = time.NewTicker(time.Duration(t.interval) * time.Second) logger.Info().Msgf("%s ticker interval changed from %d to %d", t.name, oldInterval, newInterval) } diff --git a/zetaclient/zetacore/client_worker.go b/zetaclient/zetacore/client_worker.go index c5dcaa248d..b1fb4a6074 100644 --- a/zetaclient/zetacore/client_worker.go +++ b/zetaclient/zetacore/client_worker.go @@ -21,6 +21,7 @@ func (c *Client) UpdateAppContextWorker(ctx context.Context, app *appcontext.App }() var ( + // #nosec G115 interval is in range and not user controlled updateEvery = time.Duration(app.Config().ConfigUpdateTicker) * time.Second ticker = time.NewTicker(updateEvery) logger = c.logger.Sample(logSampler) diff --git a/zetaclient/zetacore/constant.go b/zetaclient/zetacore/constant.go index 1457dd0c58..ab13e741d0 100644 --- a/zetaclient/zetacore/constant.go +++ b/zetaclient/zetacore/constant.go @@ -24,6 +24,9 @@ const ( // PostVoteInboundMessagePassingExecutionGasLimit is the gas limit for voting on, and executing ,observed inbound tx related to message passing (coin_type == zeta) PostVoteInboundMessagePassingExecutionGasLimit = 4_000_000 + // PostVoteInboundCallOptionsGasLimit is the gas limit for inbound call options + PostVoteInboundCallOptionsGasLimit uint64 = 1_500_000 + // AddOutboundTrackerGasLimit is the gas limit for adding tx hash to out tx tracker AddOutboundTrackerGasLimit = 200_000