From 9d6d0ec0e3acf0b0e71b3bbc4fe44307e65b2ce8 Mon Sep 17 00:00:00 2001 From: Alex Gartner Date: Fri, 16 Aug 2024 09:28:26 -0700 Subject: [PATCH] chore: backport zetaclient v19.1.0 changes (#2721) * feat: parse inscription like witness data (#2524) * parse inscription like witness data * more comment * remove unused code * Update zetaclient/chains/bitcoin/tx_script.go Co-authored-by: Dmitry S <11892559+swift1337@users.noreply.github.com> * Update zetaclient/chains/bitcoin/observer/inbound.go Co-authored-by: Dmitry S <11892559+swift1337@users.noreply.github.com> * Update zetaclient/chains/bitcoin/tx_script.go Co-authored-by: Dmitry S <11892559+swift1337@users.noreply.github.com> * Update zetaclient/chains/bitcoin/tx_script.go Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * pull origin * Update zetaclient/chains/bitcoin/observer/inbound.go Co-authored-by: Dmitry S <11892559+swift1337@users.noreply.github.com> * review feedbacks * update review feedbacks * update make generate * fix linter * remove over flow * Update zetaclient/chains/bitcoin/observer/inbound.go Co-authored-by: Francisco de Borja Aranda Castillejo * Update zetaclient/chains/bitcoin/tokenizer.go Co-authored-by: Francisco de Borja Aranda Castillejo * Update zetaclient/chains/bitcoin/tokenizer.go Co-authored-by: Francisco de Borja Aranda Castillejo * Update zetaclient/chains/bitcoin/tokenizer.go Co-authored-by: Francisco de Borja Aranda Castillejo * Update zetaclient/chains/bitcoin/tokenizer.go Co-authored-by: Francisco de Borja Aranda Castillejo * update review feedback * update code commnet * update comment * more comments * Update changelog.md --------- Co-authored-by: Dmitry S <11892559+swift1337@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Francisco de Borja Aranda Castillejo fix version * feat: detect memo in btc txn from OP_RETURN and inscription (#2533) * parse inscription like witness data * more comment * remove unused code * parse inscription * Update zetaclient/chains/bitcoin/tx_script.go Co-authored-by: Dmitry S <11892559+swift1337@users.noreply.github.com> * Update zetaclient/chains/bitcoin/observer/inbound.go Co-authored-by: Dmitry S <11892559+swift1337@users.noreply.github.com> * Update zetaclient/chains/bitcoin/tx_script.go Co-authored-by: Dmitry S <11892559+swift1337@users.noreply.github.com> * Update zetaclient/chains/bitcoin/tx_script.go Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * pull origin * Update zetaclient/chains/bitcoin/observer/inbound.go Co-authored-by: Dmitry S <11892559+swift1337@users.noreply.github.com> * review feedbacks * update review feedbacks * add mainnet txn * Update zetaclient/chains/bitcoin/tx_script.go Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * parse inscription like witness data * more comment * remove unused code * Update zetaclient/chains/bitcoin/tx_script.go Co-authored-by: Dmitry S <11892559+swift1337@users.noreply.github.com> * Update zetaclient/chains/bitcoin/observer/inbound.go Co-authored-by: Dmitry S <11892559+swift1337@users.noreply.github.com> * Update zetaclient/chains/bitcoin/tx_script.go Co-authored-by: Dmitry S <11892559+swift1337@users.noreply.github.com> * Update zetaclient/chains/bitcoin/tx_script.go Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * pull origin * Update zetaclient/chains/bitcoin/observer/inbound.go Co-authored-by: Dmitry S <11892559+swift1337@users.noreply.github.com> * review feedbacks * update review feedbacks * update make generate * fix linter * remove over flow * Update zetaclient/chains/bitcoin/observer/inbound.go Co-authored-by: Francisco de Borja Aranda Castillejo * Update zetaclient/chains/bitcoin/tokenizer.go Co-authored-by: Francisco de Borja Aranda Castillejo * Update zetaclient/chains/bitcoin/tokenizer.go Co-authored-by: Francisco de Borja Aranda Castillejo * Update zetaclient/chains/bitcoin/tokenizer.go Co-authored-by: Francisco de Borja Aranda Castillejo * Update zetaclient/chains/bitcoin/tokenizer.go Co-authored-by: Francisco de Borja Aranda Castillejo * update review feedback * update code commnet * update comment * more comments * Update changelog.md * Update zetaclient/chains/bitcoin/observer/inbound.go Co-authored-by: Francisco de Borja Aranda Castillejo * Update zetaclient/chains/bitcoin/observer/inbound.go Co-authored-by: Francisco de Borja Aranda Castillejo * clean up * format code --------- Co-authored-by: Dmitry S <11892559+swift1337@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Francisco de Borja Aranda Castillejo * refactor(zetaclient)!: improve AppContext (#2568) * Implement chain registry * Rewrite test-cases for AppContext * Drop `supplychecker` * Refactor app ctx Update worker * Refactor orchestrator * Refactor observer&signer; DROP postBlockHeaders * Fix test cases [1] * Update changelog * Allow Zeta Chain in appContext; address PR comments [1] * Fix app context update * Check for `chain.IsZeta()` * Add AppContext.FilterChains * Fix test cases [2] * Fix test cases [3] * Address PR comments [1] * Address PR comments [2] * Add tests for `slices` * Fix e2e tests [1] * Fix e2e tests [2] * Resolve conflicts, converge codebase between PRs * Add lodash; remove slices pkg * Address PR comments * Minor logging fix * Address PR comments tmp * feat(zetaclient): add generic rpc metrics (#2597) * feat(zetaclient): add generic rpc metrics * feedback * changelog * fmt * fix(zetaclient): use name in pending tx metric (#2642) * feat(pkg): add `ticker` package (#2617) * Add `pkg/ticker` * Sample ticker usage in evm observer * Change naming * Address PR comments * Address PR comments * feat(zetaclient)!: Add support for EIP-1559 gas fees (#2634) * Add Gas struct * Add EIP-1559 fees * Update changelog * Add test cases for legacy vs dynamicFee txs * Fix typo; Add E2E coverage * Address PR comments * Address PR comments * Use gasFeeCap formula * Revert "Use gasFeeCap formula" This reverts commit 2260925ee1fd4c0b4613559d6946f59882401708. * Address PR comments * Fix e2e upgrade tests * fix: adjust evm outbound tracker reporter to avoid submitting invalid hashes (#2628) * refactor and fix evm outbound tracker reporter to avoid invalid hashes; print log when outbound tracker is full of invalid hashes * add changelog entry * used predefined log fields * remove repeated fields information from log message; Devops team would configure Datadog to show the fields * remove redundant fields in log message; unified logs * remove pending transaction map from observer; the outbound tracker reporter will no longer report pending hash * use bg.Work() to launch outbound tracker reporter goroutines * bring the checking EnsureNoTrackers() back * add more rationale to EVM outbound tracker submission * sync observer and signers without wait on startup * try fixing tss migration E2E failure by increase timeout * feat: Solana relayer (fee payer) key importer, encryption and decryption (#2673) * configure observer relayer key for Solana; remove hardcoded solana test key from zetaclient code * implementation of relayer key importer, encryption and decryption * integrate relayer key into E2E and Solana signer * add relayer_key_balance metrics and unit tests * use TrimSpace to trim password * add changelog entry * use relayer account array in E2E config; a few renaming; add private key validation when importing * fix linter * remove GetNetworkName method for simplification * added PromptPassword method to prompt single password * use network name as map index to store relayer key passwords * moved relayer passwords to chain registry * airdrop SOL token only if solana local node is available --------- Co-authored-by: Lucas Bertrand * ci: Set Docker Workflow to use Go 1.22 (#2722) * Set go 1.22.2 * Set go 1.22.2 * Set go 1.22 * Set go 1.22 * Refactor contrib/rpc and contrib/docker-scripts to use snapshots API (#2724) Co-authored-by: Julian Rubino --------- Co-authored-by: dev-bitSmiley <153714963+bitSmiley@users.noreply.github.com> Co-authored-by: Dmitry S <11892559+swift1337@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Francisco de Borja Aranda Castillejo Co-authored-by: Charlie Chen <34498985+ws4charlie@users.noreply.github.com> Co-authored-by: Lucas Bertrand Co-authored-by: Charlie <31941002+CharlieMc0@users.noreply.github.com> Co-authored-by: Julian Rubino Co-authored-by: Julian Rubino --- Dockerfile | 2 +- changelog.md | 13 +- cmd/zetaclientd-supervisor/lib.go | 22 - cmd/zetaclientd-supervisor/main.go | 8 +- cmd/zetaclientd/debug.go | 93 ++- cmd/zetaclientd/encrypt_tss.go | 36 +- cmd/zetaclientd/import_relayer_keys.go | 153 ++++ cmd/zetaclientd/init.go | 16 +- cmd/zetaclientd/solana_test_key.go | 37 - cmd/zetaclientd/start.go | 70 +- cmd/zetae2e/config/localnet.yml | 6 + cmd/zetae2e/local/admin.go | 2 +- cmd/zetae2e/local/bitcoin.go | 5 +- cmd/zetae2e/local/erc20.go | 2 +- cmd/zetae2e/local/ethereum.go | 3 +- cmd/zetae2e/local/local.go | 7 +- cmd/zetae2e/local/performance.go | 2 +- cmd/zetae2e/local/zeta.go | 2 +- cmd/zetae2e/local/zevm_mp.go | 2 +- cmd/zetae2e/stress.go | 2 +- contrib/docker-scripts/start.sh | 20 +- .../orchestrator/Dockerfile.fastbuild | 6 +- .../localnet/orchestrator/start-zetae2e.sh | 34 +- contrib/localnet/scripts/password.file | 1 + contrib/localnet/scripts/start-zetaclientd.sh | 20 + contrib/rpc/zetacored/docker-compose.yml | 2 +- contrib/rpc/zetacored/networks/.athens3 | 4 +- .../zetacored/networks/.athens3-localbuild | 4 +- contrib/rpc/zetacored/networks/.mainnet | 2 +- .../zetacored/networks/.mainnet-localbuild | 2 +- docs/development/DEPLOY_NODES.md | 2 +- e2e/config/config.go | 20 +- e2e/e2etests/test_eth_deposit.go | 2 +- e2e/e2etests/test_eth_withdraw.go | 19 + e2e/e2etests/test_migrate_chain_support.go | 2 +- e2e/e2etests/test_stress_eth_deposit.go | 2 +- e2e/runner/bitcoin.go | 97 +-- e2e/runner/evm.go | 76 +- e2e/runner/runner.go | 12 + e2e/utils/zetacore.go | 2 +- go.mod | 17 +- go.sum | 34 +- pkg/chains/chain_test.go | 2 +- pkg/constant/constant.go | 6 + pkg/crypto/aes256_gcm.go | 120 +++ pkg/crypto/aes256_gcm_test.go | 203 +++++ pkg/crypto/privkey.go | 23 + pkg/crypto/privkey_test.go | 67 ++ pkg/os/console.go | 47 ++ pkg/os/console_test.go | 111 +++ pkg/os/path.go | 33 + pkg/os/path_test.go | 83 ++ pkg/ticker/ticker.go | 140 ++++ pkg/ticker/ticker_test.go | 173 +++++ rpc/namespaces/ethereum/debug/api.go | 3 +- rpc/namespaces/ethereum/debug/trace.go | 4 +- rpc/namespaces/ethereum/debug/utils.go | 21 +- server/start.go | 4 +- testutil/sample/crypto.go | 11 +- zetaclient/chains/base/observer.go | 11 +- zetaclient/chains/base/observer_test.go | 2 +- zetaclient/chains/base/signer.go | 6 +- zetaclient/chains/bitcoin/observer/inbound.go | 21 +- .../chains/bitcoin/observer/observer.go | 40 - .../chains/bitcoin/observer/outbound.go | 2 +- zetaclient/chains/bitcoin/observer/witness.go | 187 +++++ .../chains/bitcoin/observer/witness_test.go | 238 ++++++ zetaclient/chains/bitcoin/tokenizer.go | 162 ++++ zetaclient/chains/bitcoin/tx_script.go | 71 ++ zetaclient/chains/bitcoin/tx_script_test.go | 63 ++ zetaclient/chains/evm/constant.go | 7 +- zetaclient/chains/evm/observer/inbound.go | 139 ++-- .../chains/evm/observer/inbound_test.go | 80 +- zetaclient/chains/evm/observer/observer.go | 61 -- .../chains/evm/observer/observer_gas.go | 2 +- .../chains/evm/observer/observer_gas_test.go | 4 +- .../chains/evm/observer/observer_test.go | 47 +- zetaclient/chains/evm/observer/outbound.go | 213 +++-- .../chains/evm/observer/outbound_test.go | 13 +- zetaclient/chains/evm/rpc/rpc.go | 52 ++ zetaclient/chains/evm/rpc/rpc_live_test.go | 45 ++ zetaclient/chains/evm/signer/gas.go | 120 +++ zetaclient/chains/evm/signer/gas_test.go | 144 ++++ zetaclient/chains/evm/signer/outbound_data.go | 238 +++--- .../chains/evm/signer/outbound_data_test.go | 174 +++-- .../evm/signer/outbound_tracker_reporter.go | 85 ++ zetaclient/chains/evm/signer/signer.go | 352 ++++----- zetaclient/chains/evm/signer/signer_test.go | 121 ++- zetaclient/chains/interfaces/interfaces.go | 7 +- zetaclient/chains/solana/observer/inbound.go | 2 +- .../chains/solana/observer/inbound_tracker.go | 2 +- zetaclient/chains/solana/observer/outbound.go | 25 +- .../signer/outbound_tracker_reporter.go | 39 +- zetaclient/chains/solana/signer/signer.go | 70 +- .../chains/solana/signer/signer_test.go | 142 ++++ zetaclient/chains/solana/signer/withdraw.go | 8 +- zetaclient/config/types.go | 43 +- zetaclient/context/app.go | 394 +++++----- zetaclient/context/app_test.go | 726 +++++------------- zetaclient/context/chain.go | 192 +++++ zetaclient/context/chain_test.go | 87 +++ zetaclient/context/context_test.go | 4 +- zetaclient/keys/relayer_key.go | 155 ++++ zetaclient/keys/relayer_key_test.go | 293 +++++++ zetaclient/logs/fields.go | 18 + zetaclient/metrics/metrics.go | 64 ++ zetaclient/metrics/metrics_test.go | 60 +- zetaclient/orchestrator/bootstap_test.go | 134 +--- zetaclient/orchestrator/bootstrap.go | 501 ++++++------ zetaclient/orchestrator/orchestrator.go | 187 +++-- zetaclient/orchestrator/orchestrator_test.go | 436 ++++++----- zetaclient/supplychecker/logger.go | 31 - zetaclient/supplychecker/validate.go | 34 - .../supplychecker/zeta_supply_checker.go | 280 ------- .../supplychecker/zeta_supply_checker_test.go | 61 -- ...9550e344bdc14ac38f71fc050096887e535c8.json | 42 + ...2bf7afbf0c78f21e4118b85d00d9e4cce0c2c.json | 32 + zetaclient/testutils/constant.go | 34 +- zetaclient/testutils/mocks/chain_params.go | 25 +- zetaclient/testutils/mocks/solana_rpc.go | 30 + zetaclient/testutils/mocks/zetacore_client.go | 16 +- zetaclient/zetacore/client.go | 109 ++- zetaclient/zetacore/client_query_observer.go | 12 +- zetaclient/zetacore/client_query_test.go | 2 +- zetaclient/zetacore/client_worker.go | 2 +- zetaclient/zetacore/tx_test.go | 27 +- 126 files changed, 5616 insertions(+), 3324 deletions(-) create mode 100644 cmd/zetaclientd/import_relayer_keys.go delete mode 100644 cmd/zetaclientd/solana_test_key.go create mode 100644 pkg/crypto/aes256_gcm.go create mode 100644 pkg/crypto/aes256_gcm_test.go create mode 100644 pkg/crypto/privkey.go create mode 100644 pkg/crypto/privkey_test.go create mode 100644 pkg/os/console.go create mode 100644 pkg/os/console_test.go create mode 100644 pkg/os/path.go create mode 100644 pkg/os/path_test.go create mode 100644 pkg/ticker/ticker.go create mode 100644 pkg/ticker/ticker_test.go create mode 100644 zetaclient/chains/bitcoin/observer/witness.go create mode 100644 zetaclient/chains/bitcoin/observer/witness_test.go create mode 100644 zetaclient/chains/bitcoin/tokenizer.go create mode 100644 zetaclient/chains/evm/rpc/rpc.go create mode 100644 zetaclient/chains/evm/rpc/rpc_live_test.go create mode 100644 zetaclient/chains/evm/signer/gas.go create mode 100644 zetaclient/chains/evm/signer/gas_test.go create mode 100644 zetaclient/chains/evm/signer/outbound_tracker_reporter.go create mode 100644 zetaclient/chains/solana/signer/signer_test.go create mode 100644 zetaclient/context/chain.go create mode 100644 zetaclient/context/chain_test.go create mode 100644 zetaclient/keys/relayer_key.go create mode 100644 zetaclient/keys/relayer_key_test.go create mode 100644 zetaclient/logs/fields.go delete mode 100644 zetaclient/supplychecker/logger.go delete mode 100644 zetaclient/supplychecker/validate.go delete mode 100644 zetaclient/supplychecker/zeta_supply_checker.go delete mode 100644 zetaclient/supplychecker/zeta_supply_checker_test.go create mode 100644 zetaclient/testdata/btc/chain_8332_inbound_raw_result_37777defed8717c581b4c0509329550e344bdc14ac38f71fc050096887e535c8.json create mode 100644 zetaclient/testdata/btc/chain_8332_inbound_raw_result_7a57f987a3cb605896a5909d9ef2bf7afbf0c78f21e4118b85d00d9e4cce0c2c.json diff --git a/Dockerfile b/Dockerfile index 570a08ec4d..0f9804d087 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build Stage -FROM golang:1.20-alpine3.18 AS builder +FROM golang:1.22-alpine3.18 AS builder ENV GOPATH /go ENV GOOS=linux diff --git a/changelog.md b/changelog.md index 522ef7aafa..68ed22496f 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,15 @@ # CHANGELOG -## Unreleased +## v19.1.0 + +* [2524](https://github.com/zeta-chain/node/pull/2524) - add inscription envolop parsing +* [2533](https://github.com/zeta-chain/node/pull/2533) - parse memo from both OP_RETURN and inscription +* [2568](https://github.com/zeta-chain/node/pull/2568) - improve AppContext by converging chains, chainParams, enabledChains, and additionalChains into a single zctx.Chain +* [2597](https://github.com/zeta-chain/node/pull/2597) - Add generic rpc metrics to zetaclient +* [2634](https://github.com/zeta-chain/node/pull/2634) - add support for EIP-1559 gas fees +* [2628](https://github.com/zeta-chain/node/pull/2628) - avoid submitting invalid hashes to outbound tracker +* [2673](https://github.com/zeta-chain/node/pull/2673) - add relayer key importer, encryption and decryption + ## v19.0.1 @@ -592,4 +601,4 @@ Getting the correct TSS address for Bitcoin now requires proviidng the Bitcoin c ### CI * [1218](https://github.com/zeta-chain/node/pull/1218) - cross-compile release binaries and simplify PR testings -* [1302](https://github.com/zeta-chain/node/pull/1302) - add mainnet builds to goreleaser \ No newline at end of file +* [1302](https://github.com/zeta-chain/node/pull/1302) - add mainnet builds to goreleaser diff --git a/cmd/zetaclientd-supervisor/lib.go b/cmd/zetaclientd-supervisor/lib.go index 71f492e88b..fe62e0c07a 100644 --- a/cmd/zetaclientd-supervisor/lib.go +++ b/cmd/zetaclientd-supervisor/lib.go @@ -1,7 +1,6 @@ package main import ( - "bufio" "context" "encoding/json" "errors" @@ -10,7 +9,6 @@ import ( "os" "path" "runtime" - "strings" "sync" "syscall" "time" @@ -383,23 +381,3 @@ func (s *zetaclientdSupervisor) downloadZetaclientd(ctx context.Context, plan *u } return nil } - -func promptPasswords() (string, string, error) { - reader := bufio.NewReader(os.Stdin) - fmt.Print("HotKey Password: ") - hotKeyPass, err := reader.ReadString('\n') - if err != nil { - return "", "", err - } - fmt.Print("TSS Password: ") - tssKeyPass, err := reader.ReadString('\n') - if err != nil { - return "", "", err - } - - //trim delimiters - hotKeyPass = strings.TrimSuffix(hotKeyPass, "\n") - tssKeyPass = strings.TrimSuffix(tssKeyPass, "\n") - - return hotKeyPass, tssKeyPass, err -} diff --git a/cmd/zetaclientd-supervisor/main.go b/cmd/zetaclientd-supervisor/main.go index ee1e247be4..955a0097f1 100644 --- a/cmd/zetaclientd-supervisor/main.go +++ b/cmd/zetaclientd-supervisor/main.go @@ -7,12 +7,14 @@ import ( "os" "os/exec" "os/signal" + "strings" "syscall" "time" "golang.org/x/sync/errgroup" "github.com/zeta-chain/zetacore/app" + zetaos "github.com/zeta-chain/zetacore/pkg/os" "github.com/zeta-chain/zetacore/zetaclient/config" ) @@ -36,7 +38,9 @@ func main() { shutdownChan := make(chan os.Signal, 1) signal.Notify(shutdownChan, syscall.SIGINT, syscall.SIGTERM) - hotkeyPassword, tssPassword, err := promptPasswords() + // prompt for all necessary passwords + titles := []string{"HotKey", "TSS", "Solana Relayer Key"} + passwords, err := zetaos.PromptPasswords(titles) if err != nil { logger.Error().Err(err).Msg("unable to get passwords") os.Exit(1) @@ -65,7 +69,7 @@ func main() { cmd.Stderr = os.Stderr // must reset the passwordInputBuffer every iteration because reads are stateful (seek to end) passwordInputBuffer := bytes.Buffer{} - passwordInputBuffer.Write([]byte(hotkeyPassword + "\n" + tssPassword + "\n")) + passwordInputBuffer.Write([]byte(strings.Join(passwords, "\n") + "\n")) cmd.Stdin = &passwordInputBuffer eg, ctx := errgroup.WithContext(ctx) diff --git a/cmd/zetaclientd/debug.go b/cmd/zetaclientd/debug.go index d28f5cb898..9d7ece9a0c 100644 --- a/cmd/zetaclientd/debug.go +++ b/cmd/zetaclientd/debug.go @@ -3,9 +3,11 @@ package main import ( "context" "fmt" + "os" "strconv" "strings" + "cosmossdk.io/errors" "github.com/btcsuite/btcd/rpcclient" sdk "github.com/cosmos/cosmos-sdk/types" ethcommon "github.com/ethereum/go-ethereum/common" @@ -14,10 +16,8 @@ import ( "github.com/rs/zerolog" "github.com/spf13/cobra" - "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/pkg/coin" "github.com/zeta-chain/zetacore/testutil/sample" - observertypes "github.com/zeta-chain/zetacore/x/observer/types" btcobserver "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin/observer" evmobserver "github.com/zeta-chain/zetacore/zetaclient/chains/evm/observer" "github.com/zeta-chain/zetacore/zetaclient/config" @@ -35,11 +35,14 @@ type debugArguments struct { } func init() { - RootCmd.AddCommand(DebugCmd()) - DebugCmd().Flags(). - StringVar(&debugArgs.zetaCoreHome, "core-home", "/Users/tanmay/.zetacored", "peer address, e.g. /dns/tss1/tcp/6668/ipfs/16Uiu2HAmACG5DtqmQsHtXg4G2sLS65ttv84e7MrL4kapkjfmhxAp") - DebugCmd().Flags().StringVar(&debugArgs.zetaNode, "node", "46.4.15.110", "public ip address") - DebugCmd().Flags().StringVar(&debugArgs.zetaChainID, "chain-id", "athens_7001-1", "pre-params file path") + defaultHomeDir := os.ExpandEnv("$HOME/.zetacored") + + cmd := DebugCmd() + cmd.Flags().StringVar(&debugArgs.zetaCoreHome, "core-home", defaultHomeDir, "zetacore home directory") + cmd.Flags().StringVar(&debugArgs.zetaNode, "node", "46.4.15.110", "public ip address") + cmd.Flags().StringVar(&debugArgs.zetaChainID, "chain-id", "athens_7001-1", "pre-params file path") + + RootCmd.AddCommand(cmd) } func DebugCmd() *cobra.Command { @@ -54,20 +57,16 @@ func debugCmd(_ *cobra.Command, args []string) error { cobra.ExactArgs(2) cfg, err := config.Load(debugArgs.zetaCoreHome) if err != nil { - return err + return errors.Wrap(err, "failed to load config") } - appContext := zctx.New(cfg, zerolog.Nop()) - ctx := zctx.WithAppContext(context.Background(), appContext) + inboundHash := args[0] chainID, err := strconv.ParseInt(args[1], 10, 64) if err != nil { - return err + return errors.Wrap(err, "failed to parse chain id") } - inboundHash := args[0] - var ballotIdentifier string - // create a new zetacore client client, err := zetacore.NewClient( &keys.Keys{OperatorAddress: sdk.MustAccAddressFromBech32(sample.AccAddress())}, @@ -80,21 +79,30 @@ func debugCmd(_ *cobra.Command, args []string) error { if err != nil { return err } - chainParams, err := client.GetChainParams(ctx) - if err != nil { - return err + + appContext := zctx.New(cfg, nil, zerolog.Nop()) + ctx := zctx.WithAppContext(context.Background(), appContext) + + if err := client.UpdateAppContext(ctx, appContext, zerolog.Nop()); err != nil { + return errors.Wrap(err, "failed to update app context") } + + var ballotIdentifier string + tssEthAddress, err := client.GetEVMTSSAddress(ctx) if err != nil { return err } - chain, found := chains.GetChainFromChainID(chainID, appContext.GetAdditionalChains()) - if !found { - return fmt.Errorf("invalid chain id") + + chain, err := appContext.GetChain(chainID) + if err != nil { + return err } + chainProto := chain.RawChain() + // get ballot identifier according to the chain type - if chains.IsEVMChain(chain.ChainId, appContext.GetAdditionalChains()) { + if chain.IsEVM() { evmObserver := evmobserver.Observer{} evmObserver.WithZetacoreClient(client) var ethRPC *ethrpc.EthRPC @@ -109,43 +117,34 @@ func debugCmd(_ *cobra.Command, args []string) error { } evmObserver.WithEvmClient(client) evmObserver.WithEvmJSONRPC(ethRPC) - evmObserver.WithChain(chain) + evmObserver.WithChain(*chainProto) } } hash := ethcommon.HexToHash(inboundHash) tx, isPending, err := evmObserver.TransactionByHash(inboundHash) if err != nil { - return fmt.Errorf("tx not found on chain %s , %d", err.Error(), chain.ChainId) + return fmt.Errorf("tx not found on chain %s, %d", err.Error(), chain.ID()) } + if isPending { return fmt.Errorf("tx is still pending") } + receipt, err := client.TransactionReceipt(context.Background(), hash) if err != nil { - return fmt.Errorf("tx receipt not found on chain %s, %d", err.Error(), chain.ChainId) + return fmt.Errorf("tx receipt not found on chain %s, %d", err.Error(), chain.ID()) } - for _, chainParams := range chainParams { - if chainParams.ChainId == chainID { - evmObserver.SetChainParams(observertypes.ChainParams{ - ChainId: chainID, - ConnectorContractAddress: chainParams.ConnectorContractAddress, - ZetaTokenContractAddress: chainParams.ZetaTokenContractAddress, - Erc20CustodyContractAddress: chainParams.Erc20CustodyContractAddress, - }) - evmChainParams, found := appContext.GetEVMChainParams(chainID) - if !found { - return fmt.Errorf("missing chain params for chain %d", chainID) - } - evmChainParams.ZetaTokenContractAddress = chainParams.ZetaTokenContractAddress - if strings.EqualFold(tx.To, chainParams.ConnectorContractAddress) { - coinType = coin.CoinType_Zeta - } else if strings.EqualFold(tx.To, chainParams.Erc20CustodyContractAddress) { - coinType = coin.CoinType_ERC20 - } else if strings.EqualFold(tx.To, tssEthAddress) { - coinType = coin.CoinType_Gas - } - } + params := chain.Params() + + evmObserver.SetChainParams(*params) + + if strings.EqualFold(tx.To, params.ConnectorContractAddress) { + coinType = coin.CoinType_Zeta + } else if strings.EqualFold(tx.To, params.Erc20CustodyContractAddress) { + coinType = coin.CoinType_ERC20 + } else if strings.EqualFold(tx.To, tssEthAddress) { + coinType = coin.CoinType_Gas } switch coinType { @@ -170,10 +169,10 @@ func debugCmd(_ *cobra.Command, args []string) error { fmt.Println("CoinType not detected") } fmt.Println("CoinType : ", coinType) - } else if chains.IsBitcoinChain(chain.ChainId, appContext.GetAdditionalChains()) { + } else if chain.IsUTXO() { btcObserver := btcobserver.Observer{} btcObserver.WithZetacoreClient(client) - btcObserver.WithChain(chain) + btcObserver.WithChain(*chainProto) connCfg := &rpcclient.ConnConfig{ Host: cfg.BitcoinConfig.RPCHost, User: cfg.BitcoinConfig.RPCUsername, diff --git a/cmd/zetaclientd/encrypt_tss.go b/cmd/zetaclientd/encrypt_tss.go index 6fca9064cb..99322c0ecd 100644 --- a/cmd/zetaclientd/encrypt_tss.go +++ b/cmd/zetaclientd/encrypt_tss.go @@ -1,17 +1,14 @@ package main import ( - "crypto/aes" - "crypto/cipher" - "crypto/rand" - "crypto/sha256" "encoding/json" - "errors" - "io" "os" "path/filepath" + "github.com/pkg/errors" "github.com/spf13/cobra" + + "github.com/zeta-chain/zetacore/pkg/crypto" ) var encTssCmd = &cobra.Command{ @@ -25,9 +22,10 @@ func init() { RootCmd.AddCommand(encTssCmd) } +// EncryptTSSFile encrypts the given file with the given secret key func EncryptTSSFile(_ *cobra.Command, args []string) error { filePath := args[0] - secretKey := args[1] + password := args[1] filePath = filepath.Clean(filePath) data, err := os.ReadFile(filePath) @@ -39,29 +37,11 @@ func EncryptTSSFile(_ *cobra.Command, args []string) error { return errors.New("file does not contain valid json, may already be encrypted") } - block, err := aes.NewCipher(getFragmentSeed(secretKey)) - if err != nil { - return err - } - - // Creating GCM mode - gcm, err := cipher.NewGCM(block) + // encrypt the data + cipherText, err := crypto.EncryptAES256GCM(data, password) if err != nil { - return err - } - // Generating random nonce - nonce := make([]byte, gcm.NonceSize()) - if _, err := io.ReadFull(rand.Reader, nonce); err != nil { - return err + return errors.Wrap(err, "failed to encrypt data") } - cipherText := gcm.Seal(nonce, nonce, data, nil) return os.WriteFile(filePath, cipherText, 0o600) } - -func getFragmentSeed(password string) []byte { - h := sha256.New() - h.Write([]byte(password)) - seed := h.Sum(nil) - return seed -} diff --git a/cmd/zetaclientd/import_relayer_keys.go b/cmd/zetaclientd/import_relayer_keys.go new file mode 100644 index 0000000000..caf2db9538 --- /dev/null +++ b/cmd/zetaclientd/import_relayer_keys.go @@ -0,0 +1,153 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" + + "github.com/zeta-chain/zetacore/pkg/chains" + "github.com/zeta-chain/zetacore/pkg/crypto" + zetaos "github.com/zeta-chain/zetacore/pkg/os" + "github.com/zeta-chain/zetacore/zetaclient/keys" +) + +var CmdImportRelayerKey = &cobra.Command{ + Use: "import-relayer-key --network= --private-key= --password= --relayer-key-path=", + Short: "Import a relayer private key", + Example: `zetaclientd import-relayer-key --network=7 --private-key= --password=`, + RunE: ImportRelayerKey, +} + +var CmdRelayerAddress = &cobra.Command{ + Use: "relayer-address --network= --password= --relayer-key-path=", + Short: "Show the relayer address", + Example: `zetaclientd relayer-address --network=7 --password=my_password`, + RunE: ShowRelayerAddress, +} + +var importArgs = importRelayerKeyArguments{} +var addressArgs = relayerAddressArguments{} + +// importRelayerKeyArguments is the struct that holds the arguments for the import command +type importRelayerKeyArguments struct { + network int32 + privateKey string + password string + relayerKeyPath string +} + +// relayerAddressArguments is the struct that holds the arguments for the show command +type relayerAddressArguments struct { + network int32 + password string + relayerKeyPath string +} + +func init() { + RootCmd.AddCommand(CmdImportRelayerKey) + RootCmd.AddCommand(CmdRelayerAddress) + + // resolve default relayer key path + defaultRelayerKeyPath := "~/.zetacored/relayer-keys" + defaultRelayerKeyPath, err := zetaos.ExpandHomeDir(defaultRelayerKeyPath) + if err != nil { + log.Fatal().Err(err).Msg("failed to resolve default relayer key path") + } + + CmdImportRelayerKey.Flags().Int32Var(&importArgs.network, "network", 7, "network id, (7: solana)") + CmdImportRelayerKey.Flags(). + StringVar(&importArgs.privateKey, "private-key", "", "the relayer private key to import") + CmdImportRelayerKey.Flags(). + StringVar(&importArgs.password, "password", "", "the password to encrypt the relayer private key") + CmdImportRelayerKey.Flags(). + StringVar(&importArgs.relayerKeyPath, "relayer-key-path", defaultRelayerKeyPath, "path to relayer keys") + + CmdRelayerAddress.Flags().Int32Var(&addressArgs.network, "network", 7, "network id, (7:solana)") + CmdRelayerAddress.Flags(). + StringVar(&addressArgs.password, "password", "", "the password to decrypt the relayer private key") + CmdRelayerAddress.Flags(). + StringVar(&addressArgs.relayerKeyPath, "relayer-key-path", defaultRelayerKeyPath, "path to relayer keys") +} + +// ImportRelayerKey imports a relayer private key +func ImportRelayerKey(_ *cobra.Command, _ []string) error { + // validate private key and password + if importArgs.privateKey == "" { + return errors.New("must provide a private key") + } + if importArgs.password == "" { + return errors.New("must provide a password") + } + if !keys.IsRelayerPrivateKeyValid(importArgs.privateKey, chains.Network(importArgs.network)) { + return errors.New("invalid private key") + } + + // resolve the relayer key file path + fileName, err := keys.ResolveRelayerKeyFile(importArgs.relayerKeyPath, chains.Network(importArgs.network)) + if err != nil { + return errors.Wrap(err, "failed to resolve relayer key file path") + } + + // create path (owner `rwx` permissions) if it does not exist + keyPath := filepath.Dir(fileName) + if _, err := os.Stat(keyPath); os.IsNotExist(err) { + if err := os.MkdirAll(keyPath, 0o700); err != nil { + return errors.Wrapf(err, "failed to create relayer key path: %s", keyPath) + } + } + + // avoid overwriting existing key file + if zetaos.FileExists(fileName) { + return errors.Errorf( + "relayer key %s already exists, please backup and remove it before importing a new key", + fileName, + ) + } + + // encrypt the private key + ciphertext, err := crypto.EncryptAES256GCMBase64(importArgs.privateKey, importArgs.password) + if err != nil { + return errors.Wrap(err, "private key encryption failed") + } + + // create the relayer key file + err = keys.WriteRelayerKeyToFile(fileName, keys.RelayerKey{PrivateKey: ciphertext}) + if err != nil { + return errors.Wrapf(err, "failed to create relayer key file: %s", fileName) + } + fmt.Printf("successfully imported relayer key: %s\n", fileName) + + return nil +} + +// ShowRelayerAddress shows the relayer address +func ShowRelayerAddress(_ *cobra.Command, _ []string) error { + // try loading the relayer key if present + network := chains.Network(addressArgs.network) + relayerKey, err := keys.LoadRelayerKey(addressArgs.relayerKeyPath, network, addressArgs.password) + if err != nil { + return errors.Wrap(err, "failed to load relayer key") + } + + // relayer key does not exist, return error + if relayerKey == nil { + return fmt.Errorf( + "relayer key not found for network %d in path: %s", + addressArgs.network, + addressArgs.relayerKeyPath, + ) + } + + // resolve the relayer address + networkName, address, err := relayerKey.ResolveAddress(network) + if err != nil { + return errors.Wrap(err, "failed to resolve relayer address") + } + fmt.Printf("relayer address (%s): %s\n", networkName, address) + + return nil +} diff --git a/cmd/zetaclientd/init.go b/cmd/zetaclientd/init.go index 1b58265f90..2d3e67d698 100644 --- a/cmd/zetaclientd/init.go +++ b/cmd/zetaclientd/init.go @@ -1,8 +1,6 @@ package main import ( - "path" - "github.com/rs/zerolog" "github.com/spf13/cobra" @@ -38,7 +36,7 @@ type initArguments struct { KeyringBackend string HsmMode bool HsmHotKey string - SolanaKey string + RelayerKeyPath string } func init() { @@ -72,7 +70,8 @@ func init() { InitCmd.Flags().BoolVar(&initArgs.HsmMode, "hsm-mode", false, "enable hsm signer, default disabled") InitCmd.Flags(). StringVar(&initArgs.HsmHotKey, "hsm-hotkey", "hsm-hotkey", "name of hotkey associated with hardware security module") - InitCmd.Flags().StringVar(&initArgs.SolanaKey, "solana-key", "solana-key.json", "solana key file name") + InitCmd.Flags(). + StringVar(&initArgs.RelayerKeyPath, "relayer-key-path", "~/.zetacored/relayer-keys", "path to relayer keys") } func Initialize(_ *cobra.Command, _ []string) error { @@ -110,16 +109,9 @@ func Initialize(_ *cobra.Command, _ []string) error { configData.KeyringBackend = config.KeyringBackend(initArgs.KeyringBackend) configData.HsmMode = initArgs.HsmMode configData.HsmHotKey = initArgs.HsmHotKey - configData.SolanaKeyFile = initArgs.SolanaKey + configData.RelayerKeyPath = initArgs.RelayerKeyPath configData.ComplianceConfig = testutils.ComplianceConfigTest() - // Save solana test fee payer key file - keyFile := path.Join(rootArgs.zetaCoreHome, initArgs.SolanaKey) - err = createSolanaTestKeyFile(keyFile) - if err != nil { - return err - } - // Save config file return config.Save(&configData, rootArgs.zetaCoreHome) } diff --git a/cmd/zetaclientd/solana_test_key.go b/cmd/zetaclientd/solana_test_key.go deleted file mode 100644 index 12a266dd9d..0000000000 --- a/cmd/zetaclientd/solana_test_key.go +++ /dev/null @@ -1,37 +0,0 @@ -package main - -import ( - "encoding/json" - "os" -) - -// solanaTestKey is a local test private key for Solana -// TODO: use separate keys for each zetaclient in Solana E2E tests -// https://github.com/zeta-chain/node/issues/2614 -var solanaTestKey = []uint8{ - 199, 16, 63, 28, 125, 103, 131, 13, 6, 94, 68, 109, 13, 68, 132, 17, - 71, 33, 216, 51, 49, 103, 146, 241, 245, 162, 90, 228, 71, 177, 32, 199, - 31, 128, 124, 2, 23, 207, 48, 93, 141, 113, 91, 29, 196, 95, 24, 137, - 170, 194, 90, 4, 124, 113, 12, 222, 166, 209, 119, 19, 78, 20, 99, 5, -} - -// createSolanaTestKeyFile creates a solana test key json file -func createSolanaTestKeyFile(keyFile string) error { - // marshal the byte array to JSON - keyBytes, err := json.Marshal(solanaTestKey) - if err != nil { - return err - } - - // create file (or overwrite if it already exists) - // #nosec G304 -- for E2E testing purposes only - file, err := os.Create(keyFile) - if err != nil { - return err - } - defer file.Close() - - // write the key bytes to the file - _, err = file.Write(keyBytes) - return err -} diff --git a/cmd/zetaclientd/start.go b/cmd/zetaclientd/start.go index bbec0723f4..67c54262ac 100644 --- a/cmd/zetaclientd/start.go +++ b/cmd/zetaclientd/start.go @@ -1,7 +1,6 @@ package main import ( - "bufio" "context" "encoding/json" "fmt" @@ -23,6 +22,7 @@ import ( "github.com/zeta-chain/zetacore/pkg/authz" "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/pkg/constant" + zetaos "github.com/zeta-chain/zetacore/pkg/os" observerTypes "github.com/zeta-chain/zetacore/x/observer/types" "github.com/zeta-chain/zetacore/zetaclient/chains/base" "github.com/zeta-chain/zetacore/zetaclient/config" @@ -50,10 +50,15 @@ func start(_ *cobra.Command, _ []string) error { SetupConfigForTest() - //Prompt for Hotkey and TSS key-share passwords - hotkeyPass, tssKeyPass, err := promptPasswords() + // Prompt for Hotkey, TSS key-share and relayer key passwords + titles := []string{"HotKey", "TSS", "Solana Relayer Key"} + passwords, err := zetaos.PromptPasswords(titles) if err != nil { - return err + return errors.Wrap(err, "unable to get passwords") + } + hotkeyPass, tssKeyPass, solanaKeyPass := passwords[0], passwords[1], passwords[2] + relayerKeyPasswords := map[string]string{ + chains.Network_solana.String(): solanaKeyPass, } //Load Config file given path @@ -77,7 +82,7 @@ func start(_ *cobra.Command, _ []string) error { masterLogger := logger.Std startLogger := logger.Std.With().Str("module", "startup").Logger() - appContext := zctx.New(cfg, masterLogger) + appContext := zctx.New(cfg, relayerKeyPasswords, masterLogger) ctx := zctx.WithAppContext(context.Background(), appContext) // Wait until zetacore is up @@ -143,11 +148,11 @@ func start(_ *cobra.Command, _ []string) error { startLogger.Debug().Msgf("CreateAuthzSigner is ready") // Initialize core parameters from zetacore - err = zetacoreClient.UpdateAppContext(ctx, appContext, true, startLogger) - if err != nil { + if err = zetacoreClient.UpdateAppContext(ctx, appContext, startLogger); err != nil { startLogger.Error().Err(err).Msg("Error getting core parameters") return err } + startLogger.Info().Msgf("Config is updated from zetacore %s", maskCfg(cfg)) go zetacoreClient.UpdateAppContextWorker(ctx, appContext) @@ -214,16 +219,21 @@ func start(_ *cobra.Command, _ []string) error { return err } - bitcoinChainID := chains.BitcoinRegtest.ChainId - btcChain, _, btcEnabled := appContext.GetBTCChainAndConfig() - if btcEnabled { - bitcoinChainID = btcChain.ChainId + btcChains := appContext.FilterChains(zctx.Chain.IsUTXO) + switch { + case len(btcChains) == 0: + return errors.New("no BTC chains found") + case len(btcChains) > 1: + // In the future we might support multiple UTXO chains; + // right now we only support BTC. Let's make sure there are no surprises. + return errors.New("more than one BTC chain found") } + tss, err := mc.NewTSS( ctx, zetacoreClient, tssHistoricalList, - bitcoinChainID, + btcChains[0].ID(), hotkeyPass, server, ) @@ -263,11 +273,16 @@ func start(_ *cobra.Command, _ []string) error { tss.CurrentPubkey = currentTss.TssPubkey if tss.EVMAddress() == (ethcommon.Address{}) || tss.BTCAddress() == "" { startLogger.Error().Msg("TSS address is not set in zetacore") + } else { + startLogger.Info(). + Str("tss.eth", tss.EVMAddress().String()). + Str("tss.btc", tss.BTCAddress()). + Str("tss.pub_key", tss.CurrentPubkey). + Msg("Current TSS") } - startLogger.Info(). - Msgf("Current TSS address \n ETH : %s \n BTC : %s \n PubKey : %s ", tss.EVMAddress(), tss.BTCAddress(), tss.CurrentPubkey) - if len(appContext.GetEnabledChains()) == 0 { - startLogger.Error().Msgf("No chains enabled in updated config %s ", cfg.String()) + + if len(appContext.ListChainIDs()) == 0 { + startLogger.Error().Interface("config", cfg).Msgf("No chains in updated config") } isObserver, err := isObserverNode(ctx, zetacoreClient) @@ -388,29 +403,6 @@ func initPreParams(path string) { } } -// promptPasswords() This function will prompt for passwords which will be used to decrypt two key files: -// 1. HotKey -// 2. TSS key-share -func promptPasswords() (string, string, error) { - reader := bufio.NewReader(os.Stdin) - fmt.Print("HotKey Password: ") - hotKeyPass, err := reader.ReadString('\n') - if err != nil { - return "", "", err - } - fmt.Print("TSS Password: ") - TSSKeyPass, err := reader.ReadString('\n') - if err != nil { - return "", "", err - } - - //trim delimiters - hotKeyPass = strings.TrimSuffix(hotKeyPass, "\n") - TSSKeyPass = strings.TrimSuffix(TSSKeyPass, "\n") - - return hotKeyPass, TSSKeyPass, err -} - // isObserverNode checks whether THIS node is an observer node. func isObserverNode(ctx context.Context, client *zetacore.Client) (bool, error) { observers, err := client.GetObserverList(ctx) diff --git a/cmd/zetae2e/config/localnet.yml b/cmd/zetae2e/config/localnet.yml index eb6ba8fbb9..98410bc368 100644 --- a/cmd/zetae2e/config/localnet.yml +++ b/cmd/zetae2e/config/localnet.yml @@ -54,6 +54,12 @@ policy_accounts: bech32_address: "zeta142ds9x7raljv2qz9euys93e64gjmgdfnc47dwq" evm_address: "0xAa9b029BC3EFe4c50045Cf0902c73aAa25b43533" private_key: "0595CB0CD9BF5264A85A603EC8E43C30ADBB5FD2D9E2EF84C374EA4A65BB616C" +observer_relayer_accounts: + relayer_accounts: + - solana_address: "2qBVcNBZCubcnSR3NyCnFjCfkCVUB3G7ECPoaW5rxVjx" + solana_private_key: "3EMjCcCJg53fMEGVj13UPQpo6py9AKKyLE2qroR4yL1SvAN2tUznBvDKRYjntw7m6Jof1R2CSqjTddL27rEb6sFQ" + - solana_address: "4kkCV8H38xirwQTkE5kL6FHNtYGHnMQQ7SkCjAxibHFK" + solana_private_key: "5SSv7jWzamtjWNKGiKf3gvCPHcq9mE5x6LhYgzJCKNSxoQ83gFpmMgmg2JS2zdKcBEdwy7y9bvWgX4LBiUpvnrPf" rpcs: zevm: "http://zetacore0:8545" evm: "http://eth:8545" diff --git a/cmd/zetae2e/local/admin.go b/cmd/zetae2e/local/admin.go index bc76aeeedc..6aaf386496 100644 --- a/cmd/zetae2e/local/admin.go +++ b/cmd/zetae2e/local/admin.go @@ -45,7 +45,7 @@ func adminTestRoutine( // depositing the necessary tokens on ZetaChain txZetaDeposit := adminRunner.DepositZeta() - txEtherDeposit := adminRunner.DepositEther(false) + txEtherDeposit := adminRunner.DepositEther() txERC20Deposit := adminRunner.DepositERC20() adminRunner.WaitForMinedCCTX(txZetaDeposit) adminRunner.WaitForMinedCCTX(txEtherDeposit) diff --git a/cmd/zetae2e/local/bitcoin.go b/cmd/zetae2e/local/bitcoin.go index 05098fd5a9..184277d0cc 100644 --- a/cmd/zetae2e/local/bitcoin.go +++ b/cmd/zetae2e/local/bitcoin.go @@ -17,7 +17,6 @@ func bitcoinTestRoutine( deployerRunner *runner.E2ERunner, verbose bool, initBitcoinNetwork bool, - testHeader bool, testNames ...string, ) func() error { return func() (err error) { @@ -42,14 +41,14 @@ func bitcoinTestRoutine( bitcoinRunner.WaitForTxReceiptOnEvm(txERC20Send) // depositing the necessary tokens on ZetaChain - txEtherDeposit := bitcoinRunner.DepositEther(false) + txEtherDeposit := bitcoinRunner.DepositEther() txERC20Deposit := bitcoinRunner.DepositERC20() bitcoinRunner.WaitForMinedCCTX(txEtherDeposit) bitcoinRunner.WaitForMinedCCTX(txERC20Deposit) bitcoinRunner.SetupBitcoinAccount(initBitcoinNetwork) - bitcoinRunner.DepositBTC(testHeader) + bitcoinRunner.DepositBTC() // run bitcoin test // Note: due to the extensive block generation in Bitcoin localnet, block header test is run first diff --git a/cmd/zetae2e/local/erc20.go b/cmd/zetae2e/local/erc20.go index 8b0d21e564..94c3cbfc29 100644 --- a/cmd/zetae2e/local/erc20.go +++ b/cmd/zetae2e/local/erc20.go @@ -41,7 +41,7 @@ func erc20TestRoutine( erc20Runner.WaitForTxReceiptOnEvm(txERC20Send) // depositing the necessary tokens on ZetaChain - txEtherDeposit := erc20Runner.DepositEther(false) + txEtherDeposit := erc20Runner.DepositEther() txERC20Deposit := erc20Runner.DepositERC20() erc20Runner.WaitForMinedCCTX(txEtherDeposit) erc20Runner.WaitForMinedCCTX(txERC20Deposit) diff --git a/cmd/zetae2e/local/ethereum.go b/cmd/zetae2e/local/ethereum.go index ae2eebc268..84b68608c8 100644 --- a/cmd/zetae2e/local/ethereum.go +++ b/cmd/zetae2e/local/ethereum.go @@ -16,7 +16,6 @@ func ethereumTestRoutine( conf config.Config, deployerRunner *runner.E2ERunner, verbose bool, - testHeader bool, testNames ...string, ) func() error { return func() (err error) { @@ -36,7 +35,7 @@ func ethereumTestRoutine( startTime := time.Now() // depositing the necessary tokens on ZetaChain - txEtherDeposit := ethereumRunner.DepositEther(testHeader) + txEtherDeposit := ethereumRunner.DepositEther() ethereumRunner.WaitForMinedCCTX(txEtherDeposit) // run ethereum test diff --git a/cmd/zetae2e/local/local.go b/cmd/zetae2e/local/local.go index 0344fab7c4..8b93f2da9c 100644 --- a/cmd/zetae2e/local/local.go +++ b/cmd/zetae2e/local/local.go @@ -283,14 +283,11 @@ func localE2ETest(cmd *cobra.Command, _ []string) { ethereumTests = append(ethereumTests, ethereumAdvancedTests...) } - // skip the header proof test if we run light test or skipHeaderProof is enabled - testHeader := !light && !skipHeaderProof - eg.Go(erc20TestRoutine(conf, deployerRunner, verbose, erc20Tests...)) eg.Go(zetaTestRoutine(conf, deployerRunner, verbose, zetaTests...)) eg.Go(zevmMPTestRoutine(conf, deployerRunner, verbose, zevmMPTests...)) - eg.Go(bitcoinTestRoutine(conf, deployerRunner, verbose, !skipBitcoinSetup, testHeader, bitcoinTests...)) - eg.Go(ethereumTestRoutine(conf, deployerRunner, verbose, testHeader, ethereumTests...)) + eg.Go(bitcoinTestRoutine(conf, deployerRunner, verbose, !skipBitcoinSetup, bitcoinTests...)) + eg.Go(ethereumTestRoutine(conf, deployerRunner, verbose, ethereumTests...)) } if testAdmin { diff --git a/cmd/zetae2e/local/performance.go b/cmd/zetae2e/local/performance.go index d6ad06b29e..3bc8ed8377 100644 --- a/cmd/zetae2e/local/performance.go +++ b/cmd/zetae2e/local/performance.go @@ -86,7 +86,7 @@ func ethereumWithdrawPerformanceRoutine( startTime := time.Now() // depositing the necessary tokens on ZetaChain - txEtherDeposit := r.DepositEther(false) + txEtherDeposit := r.DepositEther() r.WaitForMinedCCTX(txEtherDeposit) tests, err := r.GetE2ETestsToRunByName( diff --git a/cmd/zetae2e/local/zeta.go b/cmd/zetae2e/local/zeta.go index 3fdb4f48cc..a0f6d49a09 100644 --- a/cmd/zetae2e/local/zeta.go +++ b/cmd/zetae2e/local/zeta.go @@ -41,7 +41,7 @@ func zetaTestRoutine( // depositing the necessary tokens on ZetaChain txZetaDeposit := zetaRunner.DepositZeta() - txEtherDeposit := zetaRunner.DepositEther(false) + txEtherDeposit := zetaRunner.DepositEther() zetaRunner.WaitForMinedCCTX(txZetaDeposit) zetaRunner.WaitForMinedCCTX(txEtherDeposit) diff --git a/cmd/zetae2e/local/zevm_mp.go b/cmd/zetae2e/local/zevm_mp.go index bc97c45e29..b8d6126ae0 100644 --- a/cmd/zetae2e/local/zevm_mp.go +++ b/cmd/zetae2e/local/zevm_mp.go @@ -41,7 +41,7 @@ func zevmMPTestRoutine( // depositing the necessary tokens on ZetaChain txZetaDeposit := zevmMPRunner.DepositZeta() - txEtherDeposit := zevmMPRunner.DepositEther(false) + txEtherDeposit := zevmMPRunner.DepositEther() zevmMPRunner.WaitForMinedCCTX(txZetaDeposit) zevmMPRunner.WaitForMinedCCTX(txEtherDeposit) diff --git a/cmd/zetae2e/stress.go b/cmd/zetae2e/stress.go index 51e4762635..b1d3a41bfc 100644 --- a/cmd/zetae2e/stress.go +++ b/cmd/zetae2e/stress.go @@ -144,7 +144,7 @@ func StressTest(cmd *cobra.Command, _ []string) { e2eTest.SetZEVMContracts() // deposit on ZetaChain - e2eTest.DepositEther(false) + e2eTest.DepositEther() e2eTest.DepositZeta() case "TESTNET": ethZRC20Addr := must(e2eTest.SystemContract.GasCoinZRC20ByChainId(&bind.CallOpts{}, big.NewInt(5))) diff --git a/contrib/docker-scripts/start.sh b/contrib/docker-scripts/start.sh index 6e2cebaf1f..6d79effa57 100644 --- a/contrib/docker-scripts/start.sh +++ b/contrib/docker-scripts/start.sh @@ -10,7 +10,7 @@ function load_defaults { export DAEMON_HOME=${DAEMON_HOME:=/root/.zetacored} export NETWORK=${NETWORK:=mainnet} export RESTORE_TYPE=${RESTORE_TYPE:=statesync} - export SNAPSHOT_API=${SNAPSHOT_API:=https://snapshots.zetachain.com} + export SNAPSHOT_API=${SNAPSHOT_API:=https://snapshots.rpc.zetachain.com} export TRUST_HEIGHT_DIFFERENCE_STATE_SYNC=${TRUST_HEIGHT_DIFFERENCE_STATE_SYNC:=40000} export COSMOVISOR_VERSION=${COSMOVISOR_VERSION:=v1.5.0} export CHAIN_ID=${CHAIN_ID:=zetachain_7000-1} @@ -109,8 +109,8 @@ function setup_restore_type { elif [ "${RESTORE_TYPE}" == "snapshot" ]; then if [ "${NETWORK}" == "mainnet" ]; then logt "Get Latest Snapshot URL" - SNAPSHOT_URL=$(curl -s ${SNAPSHOT_API}/latest-snapshot?network=mainnet | jq -r .latest_snapshot) - SNAPSHOT_FILENAME=$(basename "${SNAPSHOT_URL}") + SNAPSHOT_URL=$(curl -s ${SNAPSHOT_API}/mainnet/fullnode/latest.json | jq -r '.snapshots[0].link') + SNAPSHOT_FILENAME=$(curl -s ${SNAPSHOT_API}/mainnet/fullnode/latest.json | jq -r '.snapshots[0].filename') SNAPSHOT_DIR=$(pwd) logt "Download Snapshot from url: ${SNAPSHOT_URL}" curl -o "${SNAPSHOT_FILENAME}" "${SNAPSHOT_URL}" @@ -120,8 +120,8 @@ function setup_restore_type { logt " Cleanup Snapshot" rm -rf ${SNAPSHOT_DIR}/${SNAPSHOT_FILENAME} elif [ "${NETWORK}" == "athens3" ]; then - SNAPSHOT_URL=$(curl -s ${SNAPSHOT_API}/latest-snapshot?network=athens3 | jq -r .latest_snapshot) - SNAPSHOT_FILENAME=$(basename "${SNAPSHOT_URL}") + SNAPSHOT_URL=$(curl -s ${SNAPSHOT_API}/testnet/fullnode/latest.json | jq -r '.snapshots[0].link') + SNAPSHOT_FILENAME=$(curl -s ${SNAPSHOT_API}/testnet/fullnode/latest.json | jq -r '.snapshots[0].filename') SNAPSHOT_DIR=$(pwd) logt "Download Snapshot from url: ${SNAPSHOT_URL}" curl -o "${SNAPSHOT_FILENAME}" "${SNAPSHOT_URL}" @@ -134,8 +134,8 @@ function setup_restore_type { elif [ "${RESTORE_TYPE}" == "snapshot-archive" ]; then if [ "${NETWORK}" == "mainnet" ]; then logt "Get Latest Snapshot URL" - SNAPSHOT_URL=$(curl -s ${SNAPSHOT_API}/latest-archive-snapshot?network=mainnet | jq -r .latest_snapshot) - SNAPSHOT_FILENAME=$(basename "${SNAPSHOT_URL}") + SNAPSHOT_URL=$(curl -s ${SNAPSHOT_API}/mainnet/archive/latest.json | jq -r '.snapshots[0].link') + SNAPSHOT_FILENAME=$(curl -s ${SNAPSHOT_API}/mainnet/archive/latest.json | jq -r '.snapshots[0].filename') SNAPSHOT_DIR=$(pwd) logt "Download Snapshot from url: ${SNAPSHOT_URL}" curl -o "${SNAPSHOT_FILENAME}" "${SNAPSHOT_URL}" @@ -145,8 +145,8 @@ function setup_restore_type { logt " Cleanup Snapshot" rm -rf ${SNAPSHOT_DIR}/${SNAPSHOT_FILENAME} elif [ "${NETWORK}" == "athens3" ]; then - SNAPSHOT_URL=$(curl -s ${SNAPSHOT_API}/latest-archive-snapshot?network=athens3 | jq -r .latest_snapshot) - SNAPSHOT_FILENAME=$(basename "${SNAPSHOT_URL}") + SNAPSHOT_URL=$(curl -s ${SNAPSHOT_API}/testnet/archive/latest.json | jq -r '.snapshots[0].link') + SNAPSHOT_FILENAME=$(curl -s ${SNAPSHOT_API}/testnet/archive/latest.json | jq -r '.snapshots[0].filename') SNAPSHOT_DIR=$(pwd) logt "Download Snapshot from url: ${SNAPSHOT_URL}" curl -o "${SNAPSHOT_FILENAME}" "${SNAPSHOT_URL}" @@ -332,4 +332,4 @@ else logt "Start Network" start_network -fi \ No newline at end of file +fi diff --git a/contrib/localnet/orchestrator/Dockerfile.fastbuild b/contrib/localnet/orchestrator/Dockerfile.fastbuild index 96f437ed30..cb475f7663 100644 --- a/contrib/localnet/orchestrator/Dockerfile.fastbuild +++ b/contrib/localnet/orchestrator/Dockerfile.fastbuild @@ -1,12 +1,14 @@ FROM zetanode:latest as zeta FROM ethereum/client-go:v1.10.26 as geth -FROM golang:1.20.14-bookworm as orchestrator +FROM ghcr.io/zeta-chain/solana-docker:1.18.15 AS solana +FROM ghcr.io/zeta-chain/golang:1.22.5-bookworm AS orchestrator RUN apt update && \ - apt install -yq jq yq curl tmux python3 openssh-server iputils-ping iproute2 && \ + apt install -yq jq yq curl tmux python3 openssh-server iputils-ping iproute2 bind9-host && \ rm -rf /var/lib/apt/lists/* COPY --from=geth /usr/local/bin/geth /usr/local/bin/ +COPY --from=solana /usr/bin/solana /usr/local/bin/ COPY --from=zeta /usr/local/bin/zetacored /usr/local/bin/zetaclientd /usr/local/bin/zetae2e /usr/local/bin/ COPY contrib/localnet/orchestrator/start-zetae2e.sh /work/ diff --git a/contrib/localnet/orchestrator/start-zetae2e.sh b/contrib/localnet/orchestrator/start-zetae2e.sh index e68f12d55f..4634c15578 100644 --- a/contrib/localnet/orchestrator/start-zetae2e.sh +++ b/contrib/localnet/orchestrator/start-zetae2e.sh @@ -44,52 +44,66 @@ sleep 2 # unlock the default account account address=$(yq -r '.default_account.evm_address' config.yml) echo "funding deployer address ${address} with 10000 Ether" -geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: web3.toWei(10000,'ether')})" attach http://eth:8545 +geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: web3.toWei(10000,'ether')})" attach http://eth:8545 > /dev/null # unlock erc20 tester accounts address=$(yq -r '.additional_accounts.user_erc20.evm_address' config.yml) echo "funding erc20 address ${address} with 10000 Ether" -geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: web3.toWei(10000,'ether')})" attach http://eth:8545 +geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: web3.toWei(10000,'ether')})" attach http://eth:8545 > /dev/null # unlock zeta tester accounts address=$(yq -r '.additional_accounts.user_zeta_test.evm_address' config.yml) echo "funding zeta tester address ${address} with 10000 Ether" -geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: web3.toWei(10000,'ether')})" attach http://eth:8545 +geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: web3.toWei(10000,'ether')})" attach http://eth:8545 > /dev/null # unlock zevm message passing tester accounts address=$(yq -r '.additional_accounts.user_zevm_mp_test.evm_address' config.yml) echo "funding zevm mp tester address ${address} with 10000 Ether" -geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: web3.toWei(10000,'ether')})" attach http://eth:8545 +geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: web3.toWei(10000,'ether')})" attach http://eth:8545 > /dev/null # unlock bitcoin tester accounts address=$(yq -r '.additional_accounts.user_bitcoin.evm_address' config.yml) echo "funding bitcoin tester address ${address} with 10000 Ether" -geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: web3.toWei(10000,'ether')})" attach http://eth:8545 +geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: web3.toWei(10000,'ether')})" attach http://eth:8545 > /dev/null # unlock solana tester accounts address=$(yq -r '.additional_accounts.user_solana.evm_address' config.yml) echo "funding solana tester address ${address} with 10000 Ether" -geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: web3.toWei(10000,'ether')})" attach http://eth:8545 +geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: web3.toWei(10000,'ether')})" attach http://eth:8545 > /dev/null # unlock ethers tester accounts address=$(yq -r '.additional_accounts.user_ether.evm_address' config.yml) echo "funding ether tester address ${address} with 10000 Ether" -geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: web3.toWei(10000,'ether')})" attach http://eth:8545 +geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: web3.toWei(10000,'ether')})" attach http://eth:8545 > /dev/null # unlock miscellaneous tests accounts address=$(yq -r '.additional_accounts.user_misc.evm_address' config.yml) echo "funding misc tester address ${address} with 10000 Ether" -geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: web3.toWei(10000,'ether')})" attach http://eth:8545 +geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: web3.toWei(10000,'ether')})" attach http://eth:8545 > /dev/null # unlock admin erc20 tests accounts address=$(yq -r '.additional_accounts.user_admin.evm_address' config.yml) echo "funding admin tester address ${address} with 10000 Ether" -geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: web3.toWei(10000,'ether')})" attach http://eth:8545 +geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: web3.toWei(10000,'ether')})" attach http://eth:8545 > /dev/null # unlock migration tests accounts address=$(yq -r '.additional_accounts.user_migration.evm_address' config.yml) echo "funding migration tester address ${address} with 10000 Ether" -geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: web3.toWei(10000,'ether')})" attach http://eth:8545 +geth --exec "eth.sendTransaction({from: eth.coinbase, to: '${address}', value: web3.toWei(10000,'ether')})" attach http://eth:8545 > /dev/null + +# unlock local solana relayer accounts +if host solana > /dev/null; then + solana_url=$(yq -r '.rpcs.solana' config.yml) + solana config set --url "$solana_url" > /dev/null + + relayer=$(yq -r '.observer_relayer_accounts.relayer_accounts[0].solana_address' config.yml) + echo "funding solana relayer address ${relayer} with 100 SOL" + solana airdrop 100 "$relayer" > /dev/null + + relayer=$(yq -r '.observer_relayer_accounts.relayer_accounts[1].solana_address' config.yml) + echo "funding solana relayer address ${relayer} with 100 SOL" + solana airdrop 100 "$relayer" > /dev/null +fi ### Run zetae2e command depending on the option passed diff --git a/contrib/localnet/scripts/password.file b/contrib/localnet/scripts/password.file index 96b3814661..efedb37b66 100644 --- a/contrib/localnet/scripts/password.file +++ b/contrib/localnet/scripts/password.file @@ -1,2 +1,3 @@ password pass2 +pass_relayerkey diff --git a/contrib/localnet/scripts/start-zetaclientd.sh b/contrib/localnet/scripts/start-zetaclientd.sh index 9250385853..71ca33f589 100755 --- a/contrib/localnet/scripts/start-zetaclientd.sh +++ b/contrib/localnet/scripts/start-zetaclientd.sh @@ -14,6 +14,15 @@ set_sepolia_endpoint() { jq '.EVMChainConfigs."11155111".Endpoint = "http://eth2:8545"' /root/.zetacored/config/zetaclient_config.json > tmp.json && mv tmp.json /root/.zetacored/config/zetaclient_config.json } +# import a relayer private key (e.g. Solana relayer key) +import_relayer_key() { + local num="$1" + + # import solana (network=7) relayer private key + privkey_solana=$(yq -r ".observer_relayer_accounts.relayer_accounts[${num}].solana_private_key" /root/config.yml) + zetaclientd import-relayer-key --network=7 --private-key="$privkey_solana" --password=pass_relayerkey +} + PREPARAMS_PATH="/root/preparams/${HOSTNAME}.json" if [[ -n "${ZETACLIENTD_GEN_PREPARAMS}" ]]; then # generate pre-params as early as possible @@ -54,6 +63,11 @@ done operator=$(cat $HOME/.zetacored/os.json | jq '.ObserverAddress' ) operatorAddress=$(echo "$operator" | tr -d '"') echo "operatorAddress: $operatorAddress" + +# create the path that holds observer relayer private keys (e.g. Solana relayer key) +RELAYER_KEY_PATH="$HOME/.zetacored/relayer-keys" +mkdir -p "${RELAYER_KEY_PATH}" + echo "Start zetaclientd" # skip initialization if the config file already exists (zetaclientd init has already been run) if [[ $HOSTNAME == "zetaclient0" && ! -f ~/.zetacored/config/zetaclient_config.json ]] @@ -61,6 +75,9 @@ then MYIP=$(/sbin/ip -o -4 addr list eth0 | awk '{print $4}' | cut -d/ -f1) zetaclientd init --zetacore-url zetacore0 --chain-id athens_101-1 --operator "$operatorAddress" --log-format=text --public-ip "$MYIP" --keyring-backend "$BACKEND" --pre-params "$PREPARAMS_PATH" + # import relayer private key for zetaclient0 + import_relayer_key 0 + # if eth2 is enabled, set the endpoint in the zetaclient_config.json # in this case, the additional evm is represented with the sepolia chain, we set manually the eth2 endpoint to the sepolia chain (11155111 -> http://eth2:8545) # in /root/.zetacored/config/zetaclient_config.json @@ -81,6 +98,9 @@ then done zetaclientd init --peer "/ip4/172.20.0.21/tcp/6668/p2p/${SEED}" --zetacore-url "$node" --chain-id athens_101-1 --operator "$operatorAddress" --log-format=text --public-ip "$MYIP" --log-level 1 --keyring-backend "$BACKEND" --pre-params "$PREPARAMS_PATH" + # import relayer private key for zetaclient{$num} + import_relayer_key "${num}" + # check if the option is additional-evm # in this case, the additional evm is represented with the sepolia chain, we set manually the eth2 endpoint to the sepolia chain (11155111 -> http://eth2:8545) # in /root/.zetacored/config/zetaclient_config.json diff --git a/contrib/rpc/zetacored/docker-compose.yml b/contrib/rpc/zetacored/docker-compose.yml index fa0f8caacb..50dbd0a731 100644 --- a/contrib/rpc/zetacored/docker-compose.yml +++ b/contrib/rpc/zetacored/docker-compose.yml @@ -8,7 +8,7 @@ services: DAEMON_HOME: "${DAEMON_HOME:-/root/.zetacored}" NETWORK: ${NETWORK:-mainnet} RESTORE_TYPE: "${RESTORE_TYPE:-snapshot}" - SNAPSHOT_API: ${SNAPSHOT_API:-https://snapshots.zetachain.com} + SNAPSHOT_API: ${SNAPSHOT_API:-https://snapshots.rpc.zetachain.com} TRUST_HEIGHT_DIFFERENCE_STATE_SYNC: ${TRUST_HEIGHT_DIFFERENCE_STATE_SYNC:-40000} CHAIN_ID: "${CHAIN_ID:-zetachain_7000-1}" VISOR_NAME: "${VISOR_NAME:-cosmovisor}" diff --git a/contrib/rpc/zetacored/networks/.athens3 b/contrib/rpc/zetacored/networks/.athens3 index d89a99033d..7de4cede3b 100644 --- a/contrib/rpc/zetacored/networks/.athens3 +++ b/contrib/rpc/zetacored/networks/.athens3 @@ -1,7 +1,7 @@ export DAEMON_HOME="/root/.zetacored" export NETWORK=athens3 export RESTORE_TYPE="snapshot" -export SNAPSHOT_API=https://snapshots.zetachain.com +export SNAPSHOT_API=https://snapshots.rpc.zetachain.com export TRUST_HEIGHT_DIFFERENCE_STATE_SYNC=40000 export CHAIN_ID="athens_7001-1" export VISOR_NAME="cosmovisor" @@ -11,4 +11,4 @@ export DAEMON_RESTART_AFTER_UPGRADE="true" export UNSAFE_SKIP_BACKUP="true" export MONIKER=testnet-docker-rpc export RE_DO_START_SEQUENCE="false" -export IS_LOCAL_DEVELOPMENT="false" \ No newline at end of file +export IS_LOCAL_DEVELOPMENT="false" diff --git a/contrib/rpc/zetacored/networks/.athens3-localbuild b/contrib/rpc/zetacored/networks/.athens3-localbuild index b8e0acb6ad..b79c14c220 100644 --- a/contrib/rpc/zetacored/networks/.athens3-localbuild +++ b/contrib/rpc/zetacored/networks/.athens3-localbuild @@ -1,7 +1,7 @@ export DAEMON_HOME="/root/.zetacored" export NETWORK=athens3 export RESTORE_TYPE="snapshot" -export SNAPSHOT_API=https://snapshots.zetachain.com +export SNAPSHOT_API=https://snapshots.rpc.zetachain.com export TRUST_HEIGHT_DIFFERENCE_STATE_SYNC=40000 export CHAIN_ID="athens_7001-1" export VISOR_NAME="cosmovisor" @@ -11,4 +11,4 @@ export DAEMON_RESTART_AFTER_UPGRADE="false" export UNSAFE_SKIP_BACKUP="true" export MONIKER=testnet-docker-rpc export RE_DO_START_SEQUENCE="false" -export IS_LOCAL_DEVELOPMENT="true" \ No newline at end of file +export IS_LOCAL_DEVELOPMENT="true" diff --git a/contrib/rpc/zetacored/networks/.mainnet b/contrib/rpc/zetacored/networks/.mainnet index 4b7420b539..ff260bb5ca 100644 --- a/contrib/rpc/zetacored/networks/.mainnet +++ b/contrib/rpc/zetacored/networks/.mainnet @@ -1,7 +1,7 @@ export DAEMON_HOME="/root/.zetacored" export NETWORK=mainnet export RESTORE_TYPE="snapshot" -export SNAPSHOT_API=https://snapshots.zetachain.com +export SNAPSHOT_API=https://snapshots.rpc.zetachain.com export TRUST_HEIGHT_DIFFERENCE_STATE_SYNC=40000 export CHAIN_ID="zetachain_7000-1" export VISOR_NAME="cosmovisor" diff --git a/contrib/rpc/zetacored/networks/.mainnet-localbuild b/contrib/rpc/zetacored/networks/.mainnet-localbuild index 371e6fe215..381c34bd6d 100644 --- a/contrib/rpc/zetacored/networks/.mainnet-localbuild +++ b/contrib/rpc/zetacored/networks/.mainnet-localbuild @@ -1,7 +1,7 @@ export DAEMON_HOME="/root/.zetacored" export NETWORK=mainnet export RESTORE_TYPE="snapshot" -export SNAPSHOT_API=https://snapshots.zetachain.com +export SNAPSHOT_API=https://snapshots.rpc.zetachain.com export TRUST_HEIGHT_DIFFERENCE_STATE_SYNC=40000 export CHAIN_ID="zetachain_7000-1" export VISOR_NAME="cosmovisor" diff --git a/docs/development/DEPLOY_NODES.md b/docs/development/DEPLOY_NODES.md index dcbd1f6d40..326407d491 100644 --- a/docs/development/DEPLOY_NODES.md +++ b/docs/development/DEPLOY_NODES.md @@ -54,7 +54,7 @@ The environment variables for both Zetacored and Bitcoin nodes are defined in th | `NETWORK` | Network identifier | `mainnet`, `athens3` | | `CHAIN_ID` | Chain ID for the network | `zetachain_7000-1`, `athens_7001-1` | | `RESTORE_TYPE` | Node restoration method | `snapshot`, `statesync` | -| `SNAPSHOT_API` | API URL for fetching snapshots | `https://snapshots.zetachain.com` | +| `SNAPSHOT_API` | API URL for fetching snapshots | `https://snapshots.rpc.zetachain.com` | #### Example Environment Variables for Bitcoin diff --git a/e2e/config/config.go b/e2e/config/config.go index 8e8006c042..e1e8d54611 100644 --- a/e2e/config/config.go +++ b/e2e/config/config.go @@ -41,12 +41,13 @@ func (s DoubleQuotedString) AsEVMAddress() (ethcommon.Address, error) { // Config contains the configuration for the e2e test type Config struct { // Default account to use when running tests and running setup - DefaultAccount Account `yaml:"default_account"` - AdditionalAccounts AdditionalAccounts `yaml:"additional_accounts"` - PolicyAccounts PolicyAccounts `yaml:"policy_accounts"` - RPCs RPCs `yaml:"rpcs"` - Contracts Contracts `yaml:"contracts"` - ZetaChainID string `yaml:"zeta_chain_id"` + DefaultAccount Account `yaml:"default_account"` + AdditionalAccounts AdditionalAccounts `yaml:"additional_accounts"` + PolicyAccounts PolicyAccounts `yaml:"policy_accounts"` + ObserverRelayerAccounts ObserverRelayerAccounts `yaml:"observer_relayer_accounts"` + RPCs RPCs `yaml:"rpcs"` + Contracts Contracts `yaml:"contracts"` + ZetaChainID string `yaml:"zeta_chain_id"` } // Account contains configuration for an account @@ -54,6 +55,7 @@ type Account struct { RawBech32Address DoubleQuotedString `yaml:"bech32_address"` RawEVMAddress DoubleQuotedString `yaml:"evm_address"` RawPrivateKey DoubleQuotedString `yaml:"private_key"` + SolanaAddress DoubleQuotedString `yaml:"solana_address"` SolanaPrivateKey DoubleQuotedString `yaml:"solana_private_key"` } @@ -76,6 +78,12 @@ type PolicyAccounts struct { AdminPolicyAccount Account `yaml:"admin_policy_account"` } +// ObserverRelayerAccounts are the accounts used by the observers to interact with gateway contracts in non-EVM chains (e.g. Solana) +type ObserverRelayerAccounts struct { + // RelayerAccounts contains two relayer accounts used by zetaclient0 and zetaclient1 + RelayerAccounts [2]Account `yaml:"relayer_accounts"` +} + // RPCs contains the configuration for the RPC endpoints type RPCs struct { Zevm string `yaml:"zevm"` diff --git a/e2e/e2etests/test_eth_deposit.go b/e2e/e2etests/test_eth_deposit.go index 03da8f6da4..c5f0701516 100644 --- a/e2e/e2etests/test_eth_deposit.go +++ b/e2e/e2etests/test_eth_deposit.go @@ -16,7 +16,7 @@ func TestEtherDeposit(r *runner.E2ERunner, args []string) { amount, ok := big.NewInt(0).SetString(args[0], 10) require.True(r, ok, "Invalid amount specified for TestEtherDeposit.") - hash := r.DepositEtherWithAmount(false, amount) // in wei + hash := r.DepositEtherWithAmount(amount) // in wei // wait for the cctx to be mined cctx := utils.WaitCctxMinedByInboundHash(r.Ctx, hash.Hex(), r.CctxClient, r.Logger, r.CctxTimeout) r.Logger.CCTX(*cctx, "deposit") diff --git a/e2e/e2etests/test_eth_withdraw.go b/e2e/e2etests/test_eth_withdraw.go index 3415becca6..2d43bd095d 100644 --- a/e2e/e2etests/test_eth_withdraw.go +++ b/e2e/e2etests/test_eth_withdraw.go @@ -3,6 +3,8 @@ package e2etests import ( "math/big" + ethcommon "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/stretchr/testify/require" "github.com/zeta-chain/zetacore/e2e/runner" @@ -37,5 +39,22 @@ 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") + } + r.Logger.Info("TestEtherWithdraw completed") } + +func mustFetchEthReceipt(r *runner.E2ERunner, cctx *crosschaintypes.CrossChainTx) *ethtypes.Receipt { + hash := cctx.GetCurrentOutboundParam().Hash + require.NotEmpty(r, hash, "outbound hash is empty") + + receipt, err := r.EVMClient.TransactionReceipt(r.Ctx, ethcommon.HexToHash(hash)) + require.NoError(r, err) + + return receipt +} diff --git a/e2e/e2etests/test_migrate_chain_support.go b/e2e/e2etests/test_migrate_chain_support.go index 0fd2574c85..b8a92cd472 100644 --- a/e2e/e2etests/test_migrate_chain_support.go +++ b/e2e/e2etests/test_migrate_chain_support.go @@ -136,7 +136,7 @@ func TestMigrateChainSupport(r *runner.E2ERunner, _ []string) { // deposit Ethers and ERC20 on ZetaChain etherAmount := big.NewInt(1e18) etherAmount = etherAmount.Mul(etherAmount, big.NewInt(10)) - txEtherDeposit := newRunner.DepositEtherWithAmount(false, etherAmount) + txEtherDeposit := newRunner.DepositEtherWithAmount(etherAmount) newRunner.WaitForMinedCCTX(txEtherDeposit) // perform withdrawals on the new chain diff --git a/e2e/e2etests/test_stress_eth_deposit.go b/e2e/e2etests/test_stress_eth_deposit.go index 9e0208f7e3..04ef846889 100644 --- a/e2e/e2etests/test_stress_eth_deposit.go +++ b/e2e/e2etests/test_stress_eth_deposit.go @@ -31,7 +31,7 @@ func TestStressEtherDeposit(r *runner.E2ERunner, args []string) { // send the deposits for i := 0; i < numDeposits; i++ { i := i - hash := r.DepositEtherWithAmount(false, depositAmount) + hash := r.DepositEtherWithAmount(depositAmount) r.Logger.Print("index %d: starting deposit, tx hash: %s", i, hash.Hex()) eg.Go(func() error { return monitorEtherDeposit(r, hash, i, time.Now()) }) diff --git a/e2e/runner/bitcoin.go b/e2e/runner/bitcoin.go index 868c344766..3a4dad583e 100644 --- a/e2e/runner/bitcoin.go +++ b/e2e/runner/bitcoin.go @@ -2,7 +2,6 @@ package runner import ( "bytes" - "encoding/hex" "fmt" "sort" "time" @@ -19,17 +18,12 @@ import ( "github.com/zeta-chain/zetacore/e2e/utils" "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/pkg/constant" - "github.com/zeta-chain/zetacore/pkg/proofs" - "github.com/zeta-chain/zetacore/pkg/proofs/bitcoin" crosschaintypes "github.com/zeta-chain/zetacore/x/crosschain/types" - lightclienttypes "github.com/zeta-chain/zetacore/x/lightclient/types" zetabitcoin "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin" btcobserver "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin/observer" "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin/signer" ) -var blockHeaderBTCTimeout = 5 * time.Minute - // ListDeployerUTXOs list the deployer's UTXOs func (r *E2ERunner) ListDeployerUTXOs() ([]btcjson.ListUnspentResult, error) { // query UTXOs from node @@ -113,7 +107,7 @@ func (r *E2ERunner) DepositBTCWithAmount(amount float64) *chainhash.Hash { } // DepositBTC deposits BTC on ZetaChain -func (r *E2ERunner) DepositBTC(testHeader bool) { +func (r *E2ERunner) DepositBTC() { r.Logger.Print("⏳ depositing BTC into ZEVM") startTime := time.Now() defer func() { @@ -143,7 +137,7 @@ func (r *E2ERunner) DepositBTC(testHeader bool) { // send two transactions to the TSS address amount1 := 1.1 + zetabitcoin.DefaultDepositorFee - txHash1, err := r.SendToTSSFromDeployerToDeposit(amount1, utxos[:2]) + _, err = r.SendToTSSFromDeployerToDeposit(amount1, utxos[:2]) require.NoError(r, err) amount2 := 0.05 + zetabitcoin.DefaultDepositorFee @@ -169,12 +163,6 @@ func (r *E2ERunner) DepositBTC(testHeader bool) { balance, err := r.BTCZRC20.BalanceOf(&bind.CallOpts{}, r.EVMAddress()) require.NoError(r, err) require.Equal(r, 1, balance.Sign(), "balance should be positive") - - // due to the high block throughput in localnet, ZetaClient might catch up slowly with the blocks - // to optimize block header proof test, this test is directly executed here on the first deposit instead of having a separate test - if testHeader { - r.ProveBTCTransaction(txHash1) - } } func (r *E2ERunner) SendToTSSFromDeployerToDeposit(amount float64, inputUTXOs []btcjson.ListUnspentResult) ( @@ -346,84 +334,3 @@ func (r *E2ERunner) MineBlocksIfLocalBitcoin() func() { close(stopChan) } } - -// ProveBTCTransaction proves that a BTC transaction is in a block header and that the block header is in ZetaChain -func (r *E2ERunner) ProveBTCTransaction(txHash *chainhash.Hash) { - // get tx result - btc := r.BtcRPCClient - txResult, err := btc.GetTransaction(txHash) - require.NoError(r, err, "should get tx result") - require.True(r, txResult.Confirmations > 0, "tx should have already confirmed") - - txBytes, err := hex.DecodeString(txResult.Hex) - require.NoError(r, err) - - // get the block with verbose transactions - blockHash, err := chainhash.NewHashFromStr(txResult.BlockHash) - require.NoError(r, err) - - blockVerbose, err := btc.GetBlockVerboseTx(blockHash) - require.NoError(r, err, "should get block verbose tx") - - // get the block header - header, err := btc.GetBlockHeader(blockHash) - require.NoError(r, err, "should get block header") - - // collect all the txs in the block - txns := []*btcutil.Tx{} - for _, res := range blockVerbose.Tx { - txBytes, err := hex.DecodeString(res.Hex) - require.NoError(r, err) - - tx, err := btcutil.NewTxFromBytes(txBytes) - require.NoError(r, err) - - txns = append(txns, tx) - } - - // build merkle proof - mk := bitcoin.NewMerkle(txns) - path, index, err := mk.BuildMerkleProof(int(txResult.BlockIndex)) - require.NoError(r, err, "should build merkle proof") - - // verify merkle proof statically - pass := bitcoin.Prove(*txHash, header.MerkleRoot, path, index) - require.True(r, pass, "should verify merkle proof") - - // wait for block header to show up in ZetaChain - startTime := time.Now() - hash := header.BlockHash() - for { - // timeout - reachedTimeout := time.Since(startTime) > blockHeaderBTCTimeout - require.False(r, reachedTimeout, "timed out waiting for block header to show up in observer") - - _, err := r.LightclientClient.BlockHeader(r.Ctx, &lightclienttypes.QueryGetBlockHeaderRequest{ - BlockHash: hash.CloneBytes(), - }) - if err != nil { - r.Logger.Info( - "waiting for block header to show up in observer... current hash %s; err %s", - hash.String(), - err.Error(), - ) - } - if err == nil { - break - } - time.Sleep(2 * time.Second) - } - - // verify merkle proof through RPC - res, err := r.LightclientClient.Prove(r.Ctx, &lightclienttypes.QueryProveRequest{ - ChainId: chains.BitcoinRegtest.ChainId, - TxHash: txHash.String(), - BlockHash: blockHash.String(), - Proof: proofs.NewBitcoinProof(txBytes, path, index), - TxIndex: 0, // bitcoin doesn't use txIndex - }) - require.NoError(r, err) - require.True(r, res.Valid, "txProof should be valid") - - r.Logger.Info("OK: txProof verified for inTx: %s", txHash.String()) -} diff --git a/e2e/runner/evm.go b/e2e/runner/evm.go index 5fd4bdd565..10fd599d63 100644 --- a/e2e/runner/evm.go +++ b/e2e/runner/evm.go @@ -11,14 +11,8 @@ import ( "github.com/stretchr/testify/require" "github.com/zeta-chain/zetacore/e2e/utils" - "github.com/zeta-chain/zetacore/pkg/chains" - "github.com/zeta-chain/zetacore/pkg/proofs" - "github.com/zeta-chain/zetacore/pkg/proofs/ethereum" - lightclienttypes "github.com/zeta-chain/zetacore/x/lightclient/types" ) -var blockHeaderETHTimeout = 5 * time.Minute - // WaitForTxReceiptOnEvm waits for a tx receipt on EVM func (r *E2ERunner) WaitForTxReceiptOnEvm(tx *ethtypes.Transaction) { r.Lock() @@ -110,13 +104,13 @@ func (r *E2ERunner) DepositERC20WithAmountAndMessage(to ethcommon.Address, amoun } // DepositEther sends Ethers into ZEVM -func (r *E2ERunner) DepositEther(testHeader bool) ethcommon.Hash { +func (r *E2ERunner) DepositEther() ethcommon.Hash { amount := big.NewInt(0).Mul(big.NewInt(1e18), big.NewInt(100)) // 100 eth - return r.DepositEtherWithAmount(testHeader, amount) + return r.DepositEtherWithAmount(amount) } // DepositEtherWithAmount sends Ethers into ZEVM -func (r *E2ERunner) DepositEtherWithAmount(testHeader bool, amount *big.Int) ethcommon.Hash { +func (r *E2ERunner) DepositEtherWithAmount(amount *big.Int) ethcommon.Hash { r.Logger.Print("⏳ depositing Ethers into ZEVM") signedTx, err := r.SendEther(r.TSSAddress, amount, nil) @@ -129,12 +123,6 @@ func (r *E2ERunner) DepositEtherWithAmount(testHeader bool, amount *big.Int) eth r.Logger.EVMReceipt(*receipt, "send to TSS") - // due to the high block throughput in localnet, ZetaClient might catch up slowly with the blocks - // to optimize block header proof test, this test is directly executed here on the first deposit instead of having a separate test - if testHeader { - r.ProveEthTransaction(receipt) - } - return signedTx.Hash() } @@ -176,64 +164,6 @@ func (r *E2ERunner) SendEther(_ ethcommon.Address, value *big.Int, data []byte) return signedTx, nil } -// ProveEthTransaction proves an ETH transaction on ZetaChain -func (r *E2ERunner) ProveEthTransaction(receipt *ethtypes.Receipt) { - startTime := time.Now() - - txHash := receipt.TxHash - blockHash := receipt.BlockHash - - // #nosec G115 test - always in range - txIndex := int(receipt.TransactionIndex) - - block, err := r.EVMClient.BlockByHash(r.Ctx, blockHash) - require.NoError(r, err) - - for { - // check timeout - reachedTimeout := time.Since(startTime) > blockHeaderETHTimeout - require.False(r, reachedTimeout, "timeout waiting for block header") - - _, err := r.LightclientClient.BlockHeader(r.Ctx, &lightclienttypes.QueryGetBlockHeaderRequest{ - BlockHash: blockHash.Bytes(), - }) - if err != nil { - r.Logger.Info("WARN: block header not found; retrying... error: %s", err.Error()) - } else { - r.Logger.Info("OK: block header found") - break - } - - time.Sleep(2 * time.Second) - } - - trie := ethereum.NewTrie(block.Transactions()) - require.Equal(r, trie.Hash(), block.Header().TxHash, "tx root hash & block tx root mismatch") - - txProof, err := trie.GenerateProof(txIndex) - require.NoError(r, err, "error generating txProof") - - val, err := txProof.Verify(block.TxHash(), txIndex) - require.NoError(r, err, "error verifying txProof") - - var txx ethtypes.Transaction - require.NoError(r, txx.UnmarshalBinary(val)) - - res, err := r.LightclientClient.Prove(r.Ctx, &lightclienttypes.QueryProveRequest{ - BlockHash: blockHash.Hex(), - TxIndex: int64(txIndex), - TxHash: txHash.Hex(), - Proof: proofs.NewEthereumProof(txProof), - ChainId: chains.GoerliLocalnet.ChainId, - }) - - // FIXME: @lumtis: don't do this in production - require.NoError(r, err) - require.True(r, res.Valid, "txProof invalid") - - r.Logger.Info("OK: txProof verified") -} - // AnvilMineBlocks mines blocks on Anvil localnet // the block time is provided in seconds // the method returns a function to stop the mining diff --git a/e2e/runner/runner.go b/e2e/runner/runner.go index 2ff538584f..1dfe0d3fb8 100644 --- a/e2e/runner/runner.go +++ b/e2e/runner/runner.go @@ -42,6 +42,13 @@ import ( type E2ERunnerOption func(*E2ERunner) +// Important ENV +const ( + EnvKeyLocalnetMode = "LOCALNET_MODE" + + LocalnetModeUpgrade = "upgrade" +) + func WithZetaTxServer(txServer *txserver.ZetaTxServer) E2ERunnerOption { return func(r *E2ERunner) { r.ZetaTxServer = txServer @@ -322,6 +329,11 @@ func (r *E2ERunner) PrintContractAddresses() { r.Logger.Print("TestDappEVM: %s", r.EvmTestDAppAddr.Hex()) } +// IsRunningUpgrade returns true if the test is running an upgrade test suite. +func (r *E2ERunner) IsRunningUpgrade() bool { + return os.Getenv(EnvKeyLocalnetMode) == LocalnetModeUpgrade +} + // Errorf logs an error message. Mimics the behavior of testing.T.Errorf func (r *E2ERunner) Errorf(format string, args ...any) { r.Logger.Error(format, args...) diff --git a/e2e/utils/zetacore.go b/e2e/utils/zetacore.go index a10dc8d68b..9122860d75 100644 --- a/e2e/utils/zetacore.go +++ b/e2e/utils/zetacore.go @@ -20,7 +20,7 @@ const ( AdminPolicyName = "admin" OperationalPolicyName = "operational" - DefaultCctxTimeout = 4 * time.Minute + DefaultCctxTimeout = 6 * time.Minute ) // WaitCctxMinedByInboundHash waits until cctx is mined; returns the cctxIndex (the last one) diff --git a/go.mod b/go.mod index 36ed78c753..1536062b7f 100644 --- a/go.mod +++ b/go.mod @@ -66,6 +66,7 @@ require ( github.com/nanmu42/etherscan-api v1.10.0 github.com/near/borsh-go v0.3.1 github.com/onrik/ethrpc v1.2.0 + github.com/samber/lo v1.46.0 gitlab.com/thorchain/tss/tss-lib v0.2.0 go.nhat.io/grpcmock v0.25.0 ) @@ -325,16 +326,16 @@ require ( go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.24.0 // indirect - golang.org/x/crypto v0.17.0 + golang.org/x/crypto v0.23.0 golang.org/x/exp v0.0.0-20230711153332-06a737ee72cb - golang.org/x/mod v0.11.0 // indirect - golang.org/x/net v0.19.0 + golang.org/x/mod v0.17.0 // indirect + golang.org/x/net v0.25.0 golang.org/x/oauth2 v0.15.0 // indirect - golang.org/x/sync v0.5.0 - golang.org/x/sys v0.16.0 // indirect - golang.org/x/term v0.15.0 // indirect - golang.org/x/text v0.14.0 // indirect - golang.org/x/tools v0.9.1 // indirect + golang.org/x/sync v0.7.0 + golang.org/x/sys v0.20.0 // indirect + golang.org/x/term v0.20.0 // indirect + golang.org/x/text v0.16.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect google.golang.org/api v0.152.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/protobuf v1.32.0 diff --git a/go.sum b/go.sum index 2e12e9da23..725305dc70 100644 --- a/go.sum +++ b/go.sum @@ -1516,6 +1516,8 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/samber/lo v1.46.0 h1:w8G+oaCPgz1PoCJztqymCFaKwXt+5cCXn51uPxExFfQ= +github.com/samber/lo v1.46.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/sasha-s/go-deadlock v0.2.0/go.mod h1:StQn567HiB1fF2yJ44N9au7wOhrPS3iZqiDbRupzT10= github.com/sasha-s/go-deadlock v0.2.1-0.20190427202633-1595213edefa/go.mod h1:F73l+cr82YSh10GxyRI6qZiCgK64VaZjwesgfQ1/iLM= @@ -1853,8 +1855,8 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -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= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1902,8 +1904,8 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= -golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +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-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1976,8 +1978,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -2020,8 +2022,8 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= -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-20180810173357-98c5dad5d1a0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -2145,14 +2147,14 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= -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= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -2164,8 +2166,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -2243,8 +2245,8 @@ golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo= -golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= +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= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/pkg/chains/chain_test.go b/pkg/chains/chain_test.go index 23bc6adf18..d097d3946c 100644 --- a/pkg/chains/chain_test.go +++ b/pkg/chains/chain_test.go @@ -400,7 +400,7 @@ func TestGetChainFromChainID(t *testing.T) { chain, found := chains.GetChainFromChainID(chains.ZetaChainMainnet.ChainId, []chains.Chain{}) require.EqualValues(t, chains.ZetaChainMainnet, chain) require.True(t, found) - chain, found = chains.GetChainFromChainID(9999, []chains.Chain{}) + _, found = chains.GetChainFromChainID(9999, []chains.Chain{}) require.False(t, found) } diff --git a/pkg/constant/constant.go b/pkg/constant/constant.go index 6aba0f4dea..b296be7654 100644 --- a/pkg/constant/constant.go +++ b/pkg/constant/constant.go @@ -1,6 +1,12 @@ package constant +import "time" + const ( + // ZetaBlockTime is the block time of the ZetaChain network + // It's a rough estimate that can be used in non-critical path to estimate the time of a block + ZetaBlockTime = 6000 * time.Millisecond + // DonationMessage is the message for donation transactions // Transaction sent to the TSS or ERC20 Custody address containing this message are considered as a donation DonationMessage = "I am rich!" diff --git a/pkg/crypto/aes256_gcm.go b/pkg/crypto/aes256_gcm.go new file mode 100644 index 0000000000..e4fba7de7c --- /dev/null +++ b/pkg/crypto/aes256_gcm.go @@ -0,0 +1,120 @@ +package crypto + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/sha256" + "encoding/base64" + io "io" + + "github.com/pkg/errors" +) + +// EncryptAES256GCMBase64 encrypts the given string plaintext using AES-256-GCM with the given password and returns the base64-encoded ciphertext. +func EncryptAES256GCMBase64(plaintext string, password string) (string, error) { + // validate the input + if plaintext == "" { + return "", errors.New("plaintext must not be empty") + } + if password == "" { + return "", errors.New("password must not be empty") + } + + // encrypt the plaintext + ciphertext, err := EncryptAES256GCM([]byte(plaintext), password) + if err != nil { + return "", errors.Wrap(err, "failed to encrypt string plaintext") + } + return base64.StdEncoding.EncodeToString(ciphertext), nil +} + +// DecryptAES256GCMBase64 decrypts the given base64-encoded ciphertext using AES-256-GCM with the given password. +func DecryptAES256GCMBase64(ciphertextBase64 string, password string) (string, error) { + // validate the input + if ciphertextBase64 == "" { + return "", errors.New("ciphertext must not be empty") + } + if password == "" { + return "", errors.New("password must not be empty") + } + + // decode the base64-encoded ciphertext + ciphertext, err := base64.StdEncoding.DecodeString(ciphertextBase64) + if err != nil { + return "", errors.Wrap(err, "failed to decode base64 ciphertext") + } + + // decrypt the ciphertext + plaintext, err := DecryptAES256GCM(ciphertext, password) + if err != nil { + return "", errors.Wrap(err, "failed to decrypt ciphertext") + } + return string(plaintext), nil +} + +// EncryptAES256GCM encrypts the given plaintext using AES-256-GCM with the given password. +func EncryptAES256GCM(plaintext []byte, password string) ([]byte, error) { + // create AES cipher + block, err := aes.NewCipher(getAESKey(password)) + if err != nil { + return nil, err + } + + // create GCM mode + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + // generate random nonce + nonce := make([]byte, gcm.NonceSize()) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return nil, err + } + + // encrypt the plaintext + ciphertext := gcm.Seal(nonce, nonce, plaintext, nil) + + return ciphertext, nil +} + +// DecryptAES256GCM decrypts the given ciphertext using AES-256-GCM with the given password. +func DecryptAES256GCM(ciphertext []byte, password string) ([]byte, error) { + // create AES cipher + block, err := aes.NewCipher(getAESKey(password)) + if err != nil { + return nil, err + } + + // create GCM mode + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + // get the nonce size + nonceSize := gcm.NonceSize() + if len(ciphertext) < nonceSize { + return nil, errors.New("ciphertext too short") + } + + // extract the nonce from the ciphertext + nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:] + + // decrypt the ciphertext + plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) + if err != nil { + return nil, err + } + + return plaintext, nil +} + +// getAESKey uses SHA-256 to create a 32-byte key for AES encryption. +func getAESKey(key string) []byte { + h := sha256.New() + h.Write([]byte(key)) + + return h.Sum(nil) +} diff --git a/pkg/crypto/aes256_gcm_test.go b/pkg/crypto/aes256_gcm_test.go new file mode 100644 index 0000000000..92c1e4f1f0 --- /dev/null +++ b/pkg/crypto/aes256_gcm_test.go @@ -0,0 +1,203 @@ +package crypto_test + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/require" + "github.com/zeta-chain/zetacore/pkg/crypto" +) + +func Test_EncryptDecryptAES256GCM(t *testing.T) { + tests := []struct { + name string + plaintext string + encryptPass string + decryptPass string + modifyFunc func([]byte) []byte + fail bool + errMsg string + }{ + { + name: "Successful encryption and decryption", + plaintext: "Hello, World!", + encryptPass: "my_password", + decryptPass: "my_password", + fail: false, + }, + { + name: "Decryption with incorrect key should fail", + plaintext: "Hello, World!", + encryptPass: "my_password", + decryptPass: "my_password2", + fail: true, + }, + { + name: "Decryption with ciphertext too short should fail", + plaintext: "Hello, World!", + encryptPass: "my_password", + decryptPass: "my_password", + modifyFunc: func(ciphertext []byte) []byte { + // truncate the ciphertext, nonce size is 12 bytes + return ciphertext[:10] + }, + fail: true, + errMsg: "ciphertext too short", + }, + { + name: "Decryption with corrupted ciphertext should fail", + plaintext: "Hello, World!", + encryptPass: "my_password", + decryptPass: "my_password", + modifyFunc: func(ciphertext []byte) []byte { + // flip the last bit of the ciphertext + ciphertext[len(ciphertext)-1] ^= 0x01 + return ciphertext + }, + fail: true, + }, + { + name: "Decryption with incorrect nonce should fail", + plaintext: "Hello, World!", + encryptPass: "my_password", + decryptPass: "my_password", + modifyFunc: func(ciphertext []byte) []byte { + // flip the first bit of the nonce + ciphertext[0] ^= 0x01 + return ciphertext + }, + fail: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + encrypted, err := crypto.EncryptAES256GCM([]byte(tt.plaintext), tt.encryptPass) + require.NoError(t, err) + + // modify the encrypted data if needed + if tt.modifyFunc != nil { + encrypted = tt.modifyFunc(encrypted) + } + + // decrypt the data + decrypted, err := crypto.DecryptAES256GCM(encrypted, tt.decryptPass) + if tt.fail { + require.Error(t, err) + if tt.errMsg != "" { + require.Contains(t, err.Error(), tt.errMsg) + } + return + } + + require.True(t, bytes.Equal(decrypted, []byte(tt.plaintext)), "decrypted plaintext does not match") + }) + } +} + +func Test_EncryptAES256GCMBase64(t *testing.T) { + tests := []struct { + name string + plaintext string + encryptPass string + decryptPass string + errorMessage string + }{ + { + name: "Successful encryption and decryption", + plaintext: "Hello, World!", + encryptPass: "my_password", + decryptPass: "my_password", + }, + { + name: "Encryption with empty plaintext should fail", + plaintext: "", + errorMessage: "plaintext must not be empty", + }, + { + name: "Encryption with empty password should fail", + plaintext: "Hello, World!", + encryptPass: "", + errorMessage: "password must not be empty", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // encrypt the data + ciphertextBase64, err := crypto.EncryptAES256GCMBase64(tt.plaintext, tt.encryptPass) + if tt.errorMessage != "" { + require.ErrorContains(t, err, tt.errorMessage) + return + } + + // decrypt the data + decrypted, err := crypto.DecryptAES256GCMBase64(ciphertextBase64, tt.decryptPass) + require.NoError(t, err) + + require.Equal(t, tt.plaintext, decrypted) + }) + } +} + +func Test_DecryptAES256GCMBase64(t *testing.T) { + tests := []struct { + name string + ciphertextBase64 string + plaintext string + decryptKey string + modifyFunc func(string) string + errorMessage string + }{ + { + name: "Successful decryption", + ciphertextBase64: "CXLWgHdVeZQwVOZZyHeZ5n5VB+eVSLaWFF0v0QOm9DyB7XSiHDwhNwQ=", + plaintext: "Hello, World!", + decryptKey: "my_password", + }, + { + name: "Decryption with empty ciphertext should fail", + ciphertextBase64: "", + decryptKey: "my_password", + errorMessage: "ciphertext must not be empty", + }, + { + name: "Decryption with empty password should fail", + ciphertextBase64: "CXLWgHdVeZQwVOZZyHeZ5n5VB+eVSLaWFF0v0QOm9DyB7XSiHDwhNwQ=", + decryptKey: "", + errorMessage: "password must not be empty", + }, + { + name: "Decryption with invalid base64 ciphertext should fail", + ciphertextBase64: "CXLWgHdVeZQwVOZZyHeZ5n5VB*eVSLaWFF0v0QOm9DyB7XSiHDwhNwQ=", // use '*' instead of '+' + decryptKey: "my_password", + errorMessage: "failed to decode base64 ciphertext", + }, + { + name: "Decryption with incorrect password should fail", + ciphertextBase64: "CXLWgHdVeZQwVOZZyHeZ5n5VB+eVSLaWFF0v0QOm9DyB7XSiHDwhNwQ=", + decryptKey: "my_password2", + errorMessage: "failed to decrypt ciphertext", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ciphertextBase64 := tt.ciphertextBase64 + + // modify the encrypted data if needed + if tt.modifyFunc != nil { + ciphertextBase64 = tt.modifyFunc(ciphertextBase64) + } + + // decrypt the data + decrypted, err := crypto.DecryptAES256GCMBase64(ciphertextBase64, tt.decryptKey) + if tt.errorMessage != "" { + require.ErrorContains(t, err, tt.errorMessage) + return + } + + require.Equal(t, tt.plaintext, decrypted) + }) + } +} diff --git a/pkg/crypto/privkey.go b/pkg/crypto/privkey.go new file mode 100644 index 0000000000..2acbf1c609 --- /dev/null +++ b/pkg/crypto/privkey.go @@ -0,0 +1,23 @@ +package crypto + +import ( + fmt "fmt" + + "github.com/gagliardetto/solana-go" + "github.com/pkg/errors" +) + +// SolanaPrivateKeyFromString converts a base58 encoded private key to a solana.PrivateKey +func SolanaPrivateKeyFromString(privKeyBase58 string) (*solana.PrivateKey, error) { + privateKey, err := solana.PrivateKeyFromBase58(privKeyBase58) + if err != nil { + return nil, errors.Wrap(err, "invalid base58 private key") + } + + // Solana private keys are 64 bytes long + if len(privateKey) != 64 { + return nil, fmt.Errorf("invalid private key length: %d", len(privateKey)) + } + + return &privateKey, nil +} diff --git a/pkg/crypto/privkey_test.go b/pkg/crypto/privkey_test.go new file mode 100644 index 0000000000..cf8921b454 --- /dev/null +++ b/pkg/crypto/privkey_test.go @@ -0,0 +1,67 @@ +package crypto_test + +import ( + "testing" + + "github.com/gagliardetto/solana-go" + "github.com/stretchr/testify/require" + "github.com/zeta-chain/zetacore/pkg/crypto" +) + +func Test_SolanaPrivateKeyFromString(t *testing.T) { + tests := []struct { + name string + input string + output *solana.PrivateKey + errMsg string + }{ + { + name: "valid private key", + input: "3EMjCcCJg53fMEGVj13UPQpo6py9AKKyLE2qroR4yL1SvAN2tUznBvDKRYjntw7m6Jof1R2CSqjTddL27rEb6sFQ", + output: func() *solana.PrivateKey { + privKey, _ := solana.PrivateKeyFromBase58( + "3EMjCcCJg53fMEGVj13UPQpo6py9AKKyLE2qroR4yL1SvAN2tUznBvDKRYjntw7m6Jof1R2CSqjTddL27rEb6sFQ", + ) + return &privKey + }(), + }, + { + name: "invalid private key - too short", + input: "oR4yL1SvAN2tUznBvDKRYjntw7m6Jof1R2CSqjTddL27rEb6sFQ", + output: nil, + errMsg: "invalid private key length: 38", + }, + { + name: "invalid private key - too long", + input: "3EMjCcCJg53fMEGVj13UPQpo6py9AKKyLE2qroR4yL1SvAN2tUznBvDKRYjntw7m6Jof1R2CSqjTddL27rEb6sFQdJ", + output: nil, + errMsg: "invalid private key length: 66", + }, + { + name: "invalid private key - bad base58 encoding", + input: "!!!InvalidBase58!!!", + output: nil, + errMsg: "invalid base58 private key", + }, + { + name: "invalid private key - empty string", + input: "", + output: nil, + errMsg: "invalid base58 private key", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := crypto.SolanaPrivateKeyFromString(tt.input) + if tt.errMsg != "" { + require.ErrorContains(t, err, tt.errMsg) + require.Nil(t, result) + return + } + + require.NoError(t, err) + require.Equal(t, tt.output.String(), result.String()) + }) + } +} diff --git a/pkg/os/console.go b/pkg/os/console.go new file mode 100644 index 0000000000..c4a7c505c7 --- /dev/null +++ b/pkg/os/console.go @@ -0,0 +1,47 @@ +package os + +import ( + "bufio" + "fmt" + "os" + "strings" +) + +// PromptPassword prompts the user for a password with the given title +func PromptPassword(passwordTitle string) (string, error) { + reader := bufio.NewReader(os.Stdin) + + return readPassword(reader, passwordTitle) +} + +// PromptPasswords is a convenience function that prompts the user for multiple passwords +func PromptPasswords(passwordTitles []string) ([]string, error) { + reader := bufio.NewReader(os.Stdin) + passwords := make([]string, len(passwordTitles)) + + // iterate over password titles and prompt for each + for i, title := range passwordTitles { + password, err := readPassword(reader, title) + if err != nil { + return nil, err + } + passwords[i] = password + } + + return passwords, nil +} + +// readPassword is a helper function that reads a password from bufio.Reader +func readPassword(reader *bufio.Reader, passwordTitle string) (string, error) { + const delimitor = '\n' + + // prompt for password + fmt.Printf("%s Password: ", passwordTitle) + password, err := reader.ReadString(delimitor) + if err != nil { + return "", err + } + + // trim leading and trailing spaces + return strings.TrimSpace(password), nil +} diff --git a/pkg/os/console_test.go b/pkg/os/console_test.go new file mode 100644 index 0000000000..d5733744d2 --- /dev/null +++ b/pkg/os/console_test.go @@ -0,0 +1,111 @@ +package os_test + +import ( + "os" + "testing" + + "github.com/stretchr/testify/require" + zetaos "github.com/zeta-chain/zetacore/pkg/os" +) + +func Test_PromptPassword(t *testing.T) { + tests := []struct { + name string + input string + output string + }{ + { + name: "Valid password", + input: " pass123\n", + output: "pass123", + }, + { + name: "Empty password", + input: "\n", + output: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a pipe to simulate stdin + r, w, err := os.Pipe() + require.NoError(t, err) + + // Write the test input to the pipe + _, err = w.Write([]byte(tt.input)) + require.NoError(t, err) + w.Close() // Close the write end of the pipe + + // Backup the original stdin and restore it after the test + oldStdin := os.Stdin + defer func() { os.Stdin = oldStdin }() + + // Redirect stdin to the read end of the pipe + os.Stdin = r + + // Call the function with the test case data + password, err := zetaos.PromptPassword("anyTitle") + + // Check the returned passwords + require.NoError(t, err) + require.Equal(t, tt.output, password) + }) + } +} + +// Test function for PromptPasswords +func Test_PromptPasswords(t *testing.T) { + tests := []struct { + name string + passwordTitles []string + input string + expected []string + }{ + { + name: "Single password prompt", + passwordTitles: []string{"HotKey"}, + input: " pass123\n", + expected: []string{"pass123"}, + }, + { + name: "Multiple password prompts", + passwordTitles: []string{"HotKey", "TSS", "RelayerKey"}, + input: "pass_hotkey\npass_tss\npass_relayer\n", + expected: []string{"pass_hotkey", "pass_tss", "pass_relayer"}, + }, + { + name: "Empty input for passwords is allowed", + passwordTitles: []string{"HotKey", "TSS", "RelayerKey"}, + input: "\n\n\n", + expected: []string{"", "", ""}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a pipe to simulate stdin + r, w, err := os.Pipe() + require.NoError(t, err) + + // Write the test input to the pipe + _, err = w.Write([]byte(tt.input)) + require.NoError(t, err) + w.Close() // Close the write end of the pipe + + // Backup the original stdin and restore it after the test + oldStdin := os.Stdin + defer func() { os.Stdin = oldStdin }() + + // Redirect stdin to the read end of the pipe + os.Stdin = r + + // Call the function with the test case data + passwords, err := zetaos.PromptPasswords(tt.passwordTitles) + + // Check the returned passwords + require.NoError(t, err) + require.Equal(t, tt.expected, passwords) + }) + } +} diff --git a/pkg/os/path.go b/pkg/os/path.go new file mode 100644 index 0000000000..abf8368c64 --- /dev/null +++ b/pkg/os/path.go @@ -0,0 +1,33 @@ +package os + +import ( + "os" + "os/user" + "path/filepath" + "strings" +) + +// ExpandHomeDir expands a leading tilde in the path to the home directory of the current user. +// ~someuser/tmp will not be expanded. +func ExpandHomeDir(p string) (string, error) { + if p == "~" || + strings.HasPrefix(p, "~/") || + strings.HasPrefix(p, "~\\") { + usr, err := user.Current() + if err != nil { + return p, err + } + + p = filepath.Join(usr.HomeDir, p[1:]) + } + return filepath.Clean(p), nil +} + +// FileExists checks if a file exists. +func FileExists(filePath string) bool { + _, err := os.Stat(filePath) + if os.IsNotExist(err) { + return false + } + return err == nil +} diff --git a/pkg/os/path_test.go b/pkg/os/path_test.go new file mode 100644 index 0000000000..d02c55ef4e --- /dev/null +++ b/pkg/os/path_test.go @@ -0,0 +1,83 @@ +package os_test + +import ( + "os" + "os/user" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + zetaos "github.com/zeta-chain/zetacore/pkg/os" + "github.com/zeta-chain/zetacore/testutil/sample" +) + +func TestResolveHome(t *testing.T) { + usr, err := user.Current() + require.NoError(t, err) + + testCases := []struct { + name string + pathIn string + expected string + fail bool + }{ + { + name: `should resolve home with leading "~/"`, + pathIn: "~/tmp/file.json", + expected: filepath.Clean(filepath.Join(usr.HomeDir, "tmp/file.json")), + }, + { + name: "should resolve '~'", + pathIn: `~`, + expected: filepath.Clean(filepath.Join(usr.HomeDir, "")), + }, + { + name: "should not resolve '~someuser/tmp'", + pathIn: `~someuser/tmp`, + expected: `~someuser/tmp`, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + pathOut, err := zetaos.ExpandHomeDir(tc.pathIn) + require.NoError(t, err) + require.Equal(t, tc.expected, pathOut) + }) + } +} + +func TestFileExists(t *testing.T) { + path := sample.CreateTempDir(t) + + // create a test file + existingFile := filepath.Join(path, "test.txt") + _, err := os.Create(existingFile) + require.NoError(t, err) + + testCases := []struct { + name string + file string + expected bool + }{ + { + name: "should return true for existing file", + file: existingFile, + expected: true, + }, + { + name: "should return false for non-existing file", + file: filepath.Join(path, "non-existing.txt"), + expected: false, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + exists := zetaos.FileExists(tc.file) + require.Equal(t, tc.expected, exists) + }) + } +} diff --git a/pkg/ticker/ticker.go b/pkg/ticker/ticker.go new file mode 100644 index 0000000000..566fc03f8b --- /dev/null +++ b/pkg/ticker/ticker.go @@ -0,0 +1,140 @@ +// Package ticker provides a dynamic ticker that can change its interval at runtime. +// The ticker can be stopped gracefully and handles context-based termination. +// +// This package is useful for scenarios where periodic execution of a function is needed +// and the interval might need to change dynamically based on runtime conditions. +// +// It also invokes a first tick immediately after the ticker starts. It's safe to use it concurrently. +// +// It also terminates gracefully when the context is done (return ctx.Err()) or when the stop signal is received. +// +// Example usage: +// +// ticker := New(time.Second, func(ctx context.Context, t *Ticker) error { +// resp, err := client.GetPrice(ctx) +// if err != nil { +// logger.Err(err).Error().Msg("failed to get price") +// return nil +// } +// +// observer.SetPrice(resp.GasPrice) +// t.SetInterval(resp.GasPriceInterval) +// +// return nil +// }) +// +// err := ticker.Run(ctx) +package ticker + +import ( + "context" + "fmt" + "sync" + "time" + + "cosmossdk.io/errors" +) + +// Ticker represents a ticker that will run a function periodically. +// It also invokes BEFORE ticker starts. +type Ticker struct { + interval time.Duration + ticker *time.Ticker + task Task + signalChan chan struct{} + + // runnerMu is a mutex to prevent double run + runnerMu sync.Mutex + + // stateMu is a mutex to prevent concurrent SetInterval calls + stateMu sync.Mutex + + stopped bool +} + +// Task is a function that will be called by the Ticker +type Task func(ctx context.Context, t *Ticker) error + +// New creates a new Ticker. +func New(interval time.Duration, runner Task) *Ticker { + return &Ticker{interval: interval, task: runner} +} + +// Run creates and runs a new Ticker. +func Run(ctx context.Context, interval time.Duration, task Task) error { + return New(interval, task).Run(ctx) +} + +// SecondsFromUint64 converts uint64 to time.Duration in seconds. +func SecondsFromUint64(d uint64) time.Duration { + return time.Duration(d) * time.Second +} + +// Run runs the ticker by blocking current goroutine. It also invokes BEFORE ticker starts. +// Stops when (if any): +// - context is done (returns ctx.Err()) +// - task returns an error or panics +// - shutdown signal is received +func (t *Ticker) Run(ctx context.Context) (err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("panic during ticker run: %v", r) + } + }() + + // prevent concurrent runs + t.runnerMu.Lock() + defer t.runnerMu.Unlock() + + // setup + t.ticker = time.NewTicker(t.interval) + t.signalChan = make(chan struct{}) + t.stopped = false + + // initial run + if err := t.task(ctx, t); err != nil { + return errors.Wrap(err, "ticker task failed") + } + + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-t.ticker.C: + if err := t.task(ctx, t); err != nil { + return errors.Wrap(err, "ticker task failed") + } + case <-t.signalChan: + return nil + } + } +} + +// SetInterval updates the interval of the ticker. +func (t *Ticker) SetInterval(interval time.Duration) { + t.stateMu.Lock() + defer t.stateMu.Unlock() + + // noop + if t.interval == interval || t.ticker == nil { + return + } + + t.interval = interval + t.ticker.Reset(interval) +} + +// Stop stops the ticker. Safe to call concurrently or multiple times. +func (t *Ticker) Stop() { + t.stateMu.Lock() + defer t.stateMu.Unlock() + + // noop + if t.stopped || t.signalChan == nil { + return + } + + close(t.signalChan) + t.stopped = true + t.ticker.Stop() +} diff --git a/pkg/ticker/ticker_test.go b/pkg/ticker/ticker_test.go new file mode 100644 index 0000000000..671091c71f --- /dev/null +++ b/pkg/ticker/ticker_test.go @@ -0,0 +1,173 @@ +package ticker + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestTicker(t *testing.T) { + const ( + dur = time.Millisecond * 100 + durSmall = dur / 10 + ) + + t.Run("Basic case with context", func(t *testing.T) { + // ARRANGE + // Given a counter + var counter int + + // And a context + ctx, cancel := context.WithTimeout(context.Background(), dur+durSmall) + defer cancel() + + // And a ticker + ticker := New(dur, func(_ context.Context, t *Ticker) error { + counter++ + + return nil + }) + + // ACT + err := ticker.Run(ctx) + + // ASSERT + assert.ErrorIs(t, err, context.DeadlineExceeded) + + // two runs: start run + 1 tick + assert.Equal(t, 2, counter) + }) + + t.Run("Halts when error occurred", func(t *testing.T) { + // ARRANGE + // Given a counter + var counter int + + ctx := context.Background() + + // And a ticker func that returns an error after 10 runs + ticker := New(durSmall, func(_ context.Context, t *Ticker) error { + counter++ + if counter > 9 { + return fmt.Errorf("oops") + } + + return nil + }) + + // ACT + err := ticker.Run(ctx) + + // ASSERT + assert.ErrorContains(t, err, "oops") + assert.Equal(t, 10, counter) + }) + + t.Run("Dynamic interval update", func(t *testing.T) { + // ARRANGE + // Given a counter + var counter int + + // Given duration + duration := dur * 10 + + ctx, cancel := context.WithTimeout(context.Background(), duration) + defer cancel() + + // And a ticker what decreases the interval by 2 each time + ticker := New(durSmall, func(_ context.Context, ticker *Ticker) error { + t.Logf("Counter: %d, Duration: %s", counter, duration.String()) + + counter++ + duration /= 2 + + ticker.SetInterval(duration) + + return nil + }) + + // ACT + err := ticker.Run(ctx) + + // ASSERT + assert.ErrorIs(t, err, context.DeadlineExceeded) + + // It should have run at 2 times with ctxTimeout = tickerDuration (start + 1 tick), + // But it should have run more than that because of the interval decrease + assert.GreaterOrEqual(t, counter, 2) + }) + + t.Run("Stop ticker", func(t *testing.T) { + // ARRANGE + // Given a counter + var counter int + + // And a context + ctx := context.Background() + + // And a ticker + ticker := New(durSmall, func(_ context.Context, _ *Ticker) error { + counter++ + return nil + }) + + // And a function with a stop signal + go func() { + time.Sleep(dur) + ticker.Stop() + }() + + // ACT + err := ticker.Run(ctx) + + // ASSERT + assert.NoError(t, err) + assert.Greater(t, counter, 8) + + t.Run("Stop ticker for the second time", func(t *testing.T) { + ticker.Stop() + }) + }) + + t.Run("Panic", func(t *testing.T) { + // ARRANGE + // Given a context + ctx := context.Background() + + // And a ticker + ticker := New(durSmall, func(_ context.Context, _ *Ticker) error { + panic("oops") + }) + + // ACT + err := ticker.Run(ctx) + + // ASSERT + assert.ErrorContains(t, err, "panic during ticker run: oops") + }) + + t.Run("Run as a single call", func(t *testing.T) { + // ARRANGE + // Given a counter + var counter int + + // Given a context + ctx, cancel := context.WithTimeout(context.Background(), dur+durSmall) + defer cancel() + + tick := func(ctx context.Context, t *Ticker) error { + counter++ + return nil + } + + // ACT + err := Run(ctx, dur, tick) + + // ASSERT + assert.ErrorIs(t, err, context.DeadlineExceeded) + assert.Equal(t, 2, counter) + }) +} diff --git a/rpc/namespaces/ethereum/debug/api.go b/rpc/namespaces/ethereum/debug/api.go index 3496da92d8..1b07828b80 100644 --- a/rpc/namespaces/ethereum/debug/api.go +++ b/rpc/namespaces/ethereum/debug/api.go @@ -37,6 +37,7 @@ import ( evmtypes "github.com/evmos/ethermint/x/evm/types" stderrors "github.com/pkg/errors" + zetaos "github.com/zeta-chain/zetacore/pkg/os" "github.com/zeta-chain/zetacore/rpc/backend" rpctypes "github.com/zeta-chain/zetacore/rpc/types" ) @@ -199,7 +200,7 @@ func (a *API) StartCPUProfile(file string) error { a.logger.Debug("CPU profiling already in progress") return errors.New("CPU profiling already in progress") default: - fp, err := ExpandHome(file) + fp, err := zetaos.ExpandHomeDir(file) if err != nil { a.logger.Debug("failed to get filepath for the CPU profile file", "error", err.Error()) return err diff --git a/rpc/namespaces/ethereum/debug/trace.go b/rpc/namespaces/ethereum/debug/trace.go index 28ba1c8043..ae35b16fc2 100644 --- a/rpc/namespaces/ethereum/debug/trace.go +++ b/rpc/namespaces/ethereum/debug/trace.go @@ -25,6 +25,8 @@ import ( "runtime/trace" stderrors "github.com/pkg/errors" + + zetaos "github.com/zeta-chain/zetacore/pkg/os" ) // StartGoTrace turns on tracing, writing to the given file. @@ -37,7 +39,7 @@ func (a *API) StartGoTrace(file string) error { a.logger.Debug("trace already in progress") return errors.New("trace already in progress") } - fp, err := ExpandHome(file) + fp, err := zetaos.ExpandHomeDir(file) if err != nil { a.logger.Debug("failed to get filepath for the CPU profile file", "error", err.Error()) return err diff --git a/rpc/namespaces/ethereum/debug/utils.go b/rpc/namespaces/ethereum/debug/utils.go index 277c37df56..ae3f0c5ba5 100644 --- a/rpc/namespaces/ethereum/debug/utils.go +++ b/rpc/namespaces/ethereum/debug/utils.go @@ -17,13 +17,12 @@ package debug import ( "os" - "os/user" - "path/filepath" "runtime/pprof" - "strings" "github.com/cometbft/cometbft/libs/log" "github.com/cosmos/cosmos-sdk/server" + + zetaos "github.com/zeta-chain/zetacore/pkg/os" ) // isCPUProfileConfigurationActivated checks if cpuprofile was configured via flag @@ -33,25 +32,11 @@ func isCPUProfileConfigurationActivated(ctx *server.Context) bool { return ctx.Viper.GetString("cpu-profile") != "" } -// ExpandHome expands home directory in file paths. -// ~someuser/tmp will not be expanded. -func ExpandHome(p string) (string, error) { - if strings.HasPrefix(p, "~/") || strings.HasPrefix(p, "~\\") { - usr, err := user.Current() - if err != nil { - return p, err - } - home := usr.HomeDir - p = home + p[1:] - } - return filepath.Clean(p), nil -} - // writeProfile writes the data to a file func writeProfile(name, file string, log log.Logger) error { p := pprof.Lookup(name) log.Info("Writing profile records", "count", p.Count(), "type", name, "dump", file) - fp, err := ExpandHome(file) + fp, err := zetaos.ExpandHomeDir(file) if err != nil { return err } diff --git a/server/start.go b/server/start.go index 64bb1db4e6..79abab78bf 100644 --- a/server/start.go +++ b/server/start.go @@ -58,7 +58,7 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" - ethdebug "github.com/zeta-chain/zetacore/rpc/namespaces/ethereum/debug" + zetaos "github.com/zeta-chain/zetacore/pkg/os" "github.com/zeta-chain/zetacore/server/config" srvflags "github.com/zeta-chain/zetacore/server/flags" ) @@ -337,7 +337,7 @@ func startInProcess(ctx *server.Context, clientCtx client.Context, opts StartOpt logger := ctx.Logger if cpuProfile := ctx.Viper.GetString(srvflags.CPUProfile); cpuProfile != "" { - fp, err := ethdebug.ExpandHome(cpuProfile) + fp, err := zetaos.ExpandHomeDir(cpuProfile) if err != nil { ctx.Logger.Debug("failed to get filepath for the CPU profile file", "error", err.Error()) return err diff --git a/testutil/sample/crypto.go b/testutil/sample/crypto.go index a46310fb25..906b8f6ee0 100644 --- a/testutil/sample/crypto.go +++ b/testutil/sample/crypto.go @@ -57,11 +57,18 @@ func EthAddress() ethcommon.Address { return ethcommon.BytesToAddress(sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address()).Bytes()) } +// SolanaPrivateKey returns a sample solana private key +func SolanaPrivateKey(t *testing.T) solana.PrivateKey { + privKey, err := solana.NewRandomPrivateKey() + require.NoError(t, err) + return privKey +} + // SolanaAddress returns a sample solana address func SolanaAddress(t *testing.T) string { - keypair, err := solana.NewRandomPrivateKey() + privKey, err := solana.NewRandomPrivateKey() require.NoError(t, err) - return keypair.PublicKey().String() + return privKey.PublicKey().String() } // SolanaSignature returns a sample solana signature diff --git a/zetaclient/chains/base/observer.go b/zetaclient/chains/base/observer.go index 428946f0bf..23d2f13a76 100644 --- a/zetaclient/chains/base/observer.go +++ b/zetaclient/chains/base/observer.go @@ -17,6 +17,7 @@ import ( observertypes "github.com/zeta-chain/zetacore/x/observer/types" "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" "github.com/zeta-chain/zetacore/zetaclient/db" + "github.com/zeta-chain/zetacore/zetaclient/logs" "github.com/zeta-chain/zetacore/zetaclient/metrics" clienttypes "github.com/zeta-chain/zetacore/zetaclient/types" "github.com/zeta-chain/zetacore/zetaclient/zetacore" @@ -295,13 +296,13 @@ func (ob *Observer) Logger() *ObserverLogger { // WithLogger attaches a new logger to the observer. func (ob *Observer) WithLogger(logger Logger) *Observer { - chainLogger := logger.Std.With().Int64("chain", ob.chain.ChainId).Logger() + chainLogger := logger.Std.With().Int64(logs.FieldChain, ob.chain.ChainId).Logger() ob.logger = ObserverLogger{ Chain: chainLogger, - Inbound: chainLogger.With().Str("module", "inbound").Logger(), - Outbound: chainLogger.With().Str("module", "outbound").Logger(), - GasPrice: chainLogger.With().Str("module", "gasprice").Logger(), - Headers: chainLogger.With().Str("module", "headers").Logger(), + Inbound: chainLogger.With().Str(logs.FieldModule, logs.ModNameInbound).Logger(), + Outbound: chainLogger.With().Str(logs.FieldModule, logs.ModNameOutbound).Logger(), + GasPrice: chainLogger.With().Str(logs.FieldModule, logs.ModNameGasPrice).Logger(), + Headers: chainLogger.With().Str(logs.FieldModule, logs.ModNameHeaders).Logger(), Compliance: logger.Compliance, } return ob diff --git a/zetaclient/chains/base/observer_test.go b/zetaclient/chains/base/observer_test.go index b40802c0a7..d31e73ac85 100644 --- a/zetaclient/chains/base/observer_test.go +++ b/zetaclient/chains/base/observer_test.go @@ -53,7 +53,7 @@ func TestNewObserver(t *testing.T) { // constructor parameters chain := chains.Ethereum chainParams := *sample.ChainParams(chain.ChainId) - appContext := zctx.New(config.New(false), zerolog.Nop()) + appContext := zctx.New(config.New(false), nil, zerolog.Nop()) zetacoreClient := mocks.NewZetacoreClient(t) tss := mocks.NewTSSMainnet() blockCacheSize := base.DefaultBlockCacheSize diff --git a/zetaclient/chains/base/signer.go b/zetaclient/chains/base/signer.go index 6618c338de..781288b513 100644 --- a/zetaclient/chains/base/signer.go +++ b/zetaclient/chains/base/signer.go @@ -5,6 +5,7 @@ import ( "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" + "github.com/zeta-chain/zetacore/zetaclient/logs" "github.com/zeta-chain/zetacore/zetaclient/metrics" ) @@ -38,7 +39,10 @@ func NewSigner(chain chains.Chain, tss interfaces.TSSSigner, ts *metrics.Telemet tss: tss, ts: ts, logger: Logger{ - Std: logger.Std.With().Int64("chain", chain.ChainId).Str("module", "signer").Logger(), + Std: logger.Std.With(). + Int64(logs.FieldChain, chain.ChainId). + Str(logs.FieldModule, "signer"). + Logger(), Compliance: logger.Compliance, }, outboundBeingReported: make(map[string]bool), diff --git a/zetaclient/chains/bitcoin/observer/inbound.go b/zetaclient/chains/bitcoin/observer/inbound.go index 15a3bfdc99..a7dc5afe3d 100644 --- a/zetaclient/chains/bitcoin/observer/inbound.go +++ b/zetaclient/chains/bitcoin/observer/inbound.go @@ -49,7 +49,7 @@ func (ob *Observer) WatchInbound(ctx context.Context) error { for { select { case <-ticker.C(): - if !app.IsInboundObservationEnabled(ob.GetChainParams()) { + if !app.IsInboundObservationEnabled() { sampledLogger.Info(). Msgf("WatchInbound: inbound observation is disabled for chain %d", ob.Chain().ChainId) continue @@ -69,11 +69,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 { - app, err := zctx.FromContext(ctx) - if err != nil { - return err - } - zetaCoreClient := ob.ZetacoreClient() // get and update latest block height @@ -119,18 +114,6 @@ func (ob *Observer) ObserveInbound(ctx context.Context) error { blockNumber, len(res.Block.Tx), cnt, lastScanned) // add block header to zetacore - // TODO: consider having a separate ticker(from TSS scaning) for posting block headers - // https://github.com/zeta-chain/node/issues/1847 - // TODO: move this logic in its own routine - // https://github.com/zeta-chain/node/issues/2204 - blockHeaderVerification, found := app.GetBlockHeaderEnabledChains(ob.Chain().ChainId) - if found && blockHeaderVerification.Enabled { - // #nosec G115 always in range - err = ob.postBlockHeader(ctx, int64(blockNumber)) - if err != nil { - ob.logger.Inbound.Warn().Err(err).Msgf("observeInboundBTC: error posting block header %d", blockNumber) - } - } if len(res.Block.Tx) > 1 { // get depositor fee depositorFee := bitcoin.CalcDepositorFee(res.Block, ob.Chain().ChainId, ob.netParams, ob.logger.Inbound) @@ -206,7 +189,7 @@ func (ob *Observer) WatchInboundTracker(ctx context.Context) error { for { select { case <-ticker.C(): - if !app.IsInboundObservationEnabled(ob.GetChainParams()) { + if !app.IsInboundObservationEnabled() { continue } err := ob.ProcessInboundTrackers(ctx) diff --git a/zetaclient/chains/bitcoin/observer/observer.go b/zetaclient/chains/bitcoin/observer/observer.go index 8b4c79ba39..6a15173c33 100644 --- a/zetaclient/chains/bitcoin/observer/observer.go +++ b/zetaclient/chains/bitcoin/observer/observer.go @@ -2,7 +2,6 @@ package observer import ( - "bytes" "context" "encoding/hex" "fmt" @@ -21,7 +20,6 @@ import ( "github.com/zeta-chain/zetacore/pkg/bg" "github.com/zeta-chain/zetacore/pkg/chains" - "github.com/zeta-chain/zetacore/pkg/proofs" observertypes "github.com/zeta-chain/zetacore/x/observer/types" "github.com/zeta-chain/zetacore/zetaclient/chains/base" "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin" @@ -646,41 +644,3 @@ func (ob *Observer) isTssTransaction(txid string) bool { _, found := ob.includedTxHashes[txid] return found } - -// postBlockHeader posts block header to zetacore -// TODO(revamp): move to block header file -func (ob *Observer) postBlockHeader(ctx context.Context, tip int64) error { - ob.logger.Inbound.Info().Msgf("postBlockHeader: tip %d", tip) - bn := tip - chainState, err := ob.ZetacoreClient().GetBlockHeaderChainState(ctx, ob.Chain().ChainId) - if err == nil && chainState != nil && chainState.EarliestHeight > 0 { - bn = chainState.LatestHeight + 1 - } - if bn > tip { - return fmt.Errorf("postBlockHeader: must post block confirmed block header: %d > %d", bn, tip) - } - res2, err := ob.GetBlockByNumberCached(bn) - if err != nil { - return fmt.Errorf("error getting bitcoin block %d: %s", bn, err) - } - - var headerBuf bytes.Buffer - err = res2.Header.Serialize(&headerBuf) - if err != nil { // should never happen - ob.logger.Inbound.Error().Err(err).Msgf("error serializing bitcoin block header: %d", bn) - return err - } - blockHash := res2.Header.BlockHash() - _, err = ob.ZetacoreClient().PostVoteBlockHeader( - ctx, - ob.Chain().ChainId, - blockHash[:], - res2.Block.Height, - proofs.NewBitcoinHeader(headerBuf.Bytes()), - ) - ob.logger.Inbound.Info().Msgf("posted block header %d: %s", bn, blockHash) - if err != nil { // error shouldn't block the process - ob.logger.Inbound.Error().Err(err).Msgf("error posting bitcoin block header: %d", bn) - } - return err -} diff --git a/zetaclient/chains/bitcoin/observer/outbound.go b/zetaclient/chains/bitcoin/observer/outbound.go index d6dd003caa..009a49759e 100644 --- a/zetaclient/chains/bitcoin/observer/outbound.go +++ b/zetaclient/chains/bitcoin/observer/outbound.go @@ -46,7 +46,7 @@ func (ob *Observer) WatchOutbound(ctx context.Context) error { for { select { case <-ticker.C(): - if !app.IsOutboundObservationEnabled(ob.GetChainParams()) { + if !app.IsOutboundObservationEnabled() { sampledLogger.Info(). Msgf("WatchOutbound: outbound observation is disabled for chain %d", chainID) continue diff --git a/zetaclient/chains/bitcoin/observer/witness.go b/zetaclient/chains/bitcoin/observer/witness.go new file mode 100644 index 0000000000..0af55c62a9 --- /dev/null +++ b/zetaclient/chains/bitcoin/observer/witness.go @@ -0,0 +1,187 @@ +package observer + +import ( + "encoding/hex" + "fmt" + + "github.com/btcsuite/btcd/btcjson" + "github.com/btcsuite/btcd/chaincfg" + "github.com/pkg/errors" + "github.com/rs/zerolog" + + "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin" + "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" +) + +// GetBtcEventWithWitness either returns a valid BTCInboundEvent or nil. +// This method supports data with more than 80 bytes by scanning the witness for possible presence of a tapscript. +// It will first prioritize OP_RETURN over tapscript. +func GetBtcEventWithWitness( + client interfaces.BTCRPCClient, + tx btcjson.TxRawResult, + tssAddress string, + blockNumber uint64, + logger zerolog.Logger, + netParams *chaincfg.Params, + depositorFee float64, +) (*BTCInboundEvent, error) { + if len(tx.Vout) < 1 { + logger.Debug().Msgf("no output %s", tx.Txid) + return nil, nil + } + if len(tx.Vin) == 0 { + logger.Debug().Msgf("no input found for inbound: %s", tx.Txid) + return nil, nil + } + + if err := isValidRecipient(tx.Vout[0].ScriptPubKey.Hex, tssAddress, netParams); err != nil { + logger.Debug().Msgf("irrelevant recipient %s for tx %s, err: %s", tx.Vout[0].ScriptPubKey.Hex, tx.Txid, err) + return nil, nil + } + + isAmountValid, amount := isValidAmount(tx.Vout[0].Value, depositorFee) + if !isAmountValid { + logger.Info(). + Msgf("GetBtcEventWithWitness: btc deposit amount %v in txid %s is less than depositor fee %v", tx.Vout[0].Value, tx.Txid, depositorFee) + return nil, nil + } + + // Try to extract the memo from the BTC txn. First try to extract from OP_RETURN + // if not found then try to extract from inscription. Return nil if the above two + // cannot find the memo. + var memo []byte + if candidate := tryExtractOpRet(tx, logger); candidate != nil { + memo = candidate + logger.Debug(). + Msgf("GetBtcEventWithWitness: found OP_RETURN memo %s in tx %s", hex.EncodeToString(memo), tx.Txid) + } else if candidate = tryExtractInscription(tx, logger); candidate != nil { + memo = candidate + logger.Debug().Msgf("GetBtcEventWithWitness: found inscription memo %s in tx %s", hex.EncodeToString(memo), tx.Txid) + } else { + return nil, errors.Errorf("error getting memo for inbound: %s", tx.Txid) + } + + // event found, get sender address + fromAddress, err := GetSenderAddressByVin(client, tx.Vin[0], netParams) + if err != nil { + return nil, errors.Wrapf(err, "error getting sender address for inbound: %s", tx.Txid) + } + + return &BTCInboundEvent{ + FromAddress: fromAddress, + ToAddress: tssAddress, + Value: amount, + MemoBytes: memo, + BlockNumber: blockNumber, + TxHash: tx.Txid, + }, nil +} + +// ParseScriptFromWitness attempts to parse the script from the witness data. Ideally it should be handled by +// bitcoin library, however, it's not found in existing library version. Replace this with actual library implementation +// if libraries are updated. +func ParseScriptFromWitness(witness []string, logger zerolog.Logger) []byte { + length := len(witness) + + if length == 0 { + return nil + } + + lastElement, err := hex.DecodeString(witness[length-1]) + if err != nil { + logger.Debug().Msgf("invalid witness element") + return nil + } + + // From BIP341: + // 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 { + // account for the extra item removed from the end + witness = witness[:length-1] + } + + if len(witness) < 2 { + logger.Debug().Msgf("not script path spending detected, ignore") + return nil + } + + // only the script is the focus here, ignore checking control block or whatever else + script, err := hex.DecodeString(witness[len(witness)-2]) + if err != nil { + logger.Debug().Msgf("witness script cannot be decoded from hex, ignore") + return nil + } + return script +} + +// / Try to extract the memo from the OP_RETURN +func tryExtractOpRet(tx btcjson.TxRawResult, logger zerolog.Logger) []byte { + if len(tx.Vout) < 2 { + logger.Debug().Msgf("txn %s has fewer than 2 outputs, not target OP_RETURN txn", tx.Txid) + return nil + } + + memo, found, err := bitcoin.DecodeOpReturnMemo(tx.Vout[1].ScriptPubKey.Hex, tx.Txid) + if err != nil { + logger.Error().Err(err).Msgf("tryExtractOpRet: error decoding OP_RETURN memo: %s", tx.Vout[1].ScriptPubKey.Hex) + return nil + } + + if found { + return memo + } + return nil +} + +// / Try to extract the memo from inscription +func tryExtractInscription(tx btcjson.TxRawResult, logger zerolog.Logger) []byte { + for i, input := range tx.Vin { + script := ParseScriptFromWitness(input.Witness, logger) + if script == nil { + continue + } + + logger.Debug().Msgf("potential witness script, tx %s, input idx %d", tx.Txid, i) + + memo, found, err := bitcoin.DecodeScript(script) + if err != nil || !found { + logger.Debug().Msgf("invalid witness script, tx %s, input idx %d", tx.Txid, i) + continue + } + + logger.Debug().Msgf("found memo in inscription, tx %s, input idx %d", tx.Txid, i) + return memo + } + + return nil +} + +func isValidAmount( + incoming float64, + minimal float64, +) (bool, float64) { + if incoming < minimal { + return false, 0 + } + return true, incoming - minimal +} + +func isValidRecipient( + script string, + tssAddress string, + netParams *chaincfg.Params, +) error { + receiver, err := bitcoin.DecodeScriptP2WPKH(script, netParams) + if err != nil { + return fmt.Errorf("invalid p2wpkh script detected, %s", err) + } + + // skip irrelevant tx to us + if receiver != tssAddress { + return fmt.Errorf("irrelevant recipient, %s", receiver) + } + + return nil +} diff --git a/zetaclient/chains/bitcoin/observer/witness_test.go b/zetaclient/chains/bitcoin/observer/witness_test.go new file mode 100644 index 0000000000..4e93fb5cf1 --- /dev/null +++ b/zetaclient/chains/bitcoin/observer/witness_test.go @@ -0,0 +1,238 @@ +package observer_test + +import ( + "encoding/hex" + "testing" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/rs/zerolog/log" + "github.com/stretchr/testify/require" + + "github.com/zeta-chain/zetacore/pkg/chains" + "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin" + "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin/observer" + clientcommon "github.com/zeta-chain/zetacore/zetaclient/common" + "github.com/zeta-chain/zetacore/zetaclient/testutils" + "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" +) + +func TestParseScriptFromWitness(t *testing.T) { + t.Run("decode script ok", func(t *testing.T) { + witness := [3]string{ + "3a4b32aef0e6ecc62d185594baf4df186c6d48ec15e72515bf81c1bcc1f04c758f4d54486bc2e7c280e649761d9084dbd2e7cdfb20708a7f8d0f82e5277bba2b", + "20888269c4f0b7f6fe95d0cba364e2b1b879d9b00735d19cfab4b8d87096ce2b3cac00634d0802000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004c50000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000068", + "c0888269c4f0b7f6fe95d0cba364e2b1b879d9b00735d19cfab4b8d87096ce2b3c", + } + expected := "20888269c4f0b7f6fe95d0cba364e2b1b879d9b00735d19cfab4b8d87096ce2b3cac00634d0802000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004c50000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000068" + + script := observer.ParseScriptFromWitness(witness[:], log.Logger) + require.NotNil(t, script) + require.Equal(t, hex.EncodeToString(script), expected) + }) + + t.Run("no witness", func(t *testing.T) { + witness := [0]string{} + script := observer.ParseScriptFromWitness(witness[:], log.Logger) + require.Nil(t, script) + }) + + t.Run("ignore key spending path", func(t *testing.T) { + witness := [1]string{ + "134896c42cd95680b048845847c8054756861ffab7d4abab72f6508d67d1ec0c590287ec2161dd7884983286e1cd56ce65c08a24ee0476ede92678a93b1b180c", + } + script := observer.ParseScriptFromWitness(witness[:], log.Logger) + require.Nil(t, script) + }) +} + +func TestGetBtcEventFromInscription(t *testing.T) { + // load archived inbound P2WPKH raw result + // https://mempool.space/tx/847139aa65aa4a5ee896375951cbf7417cfc8a4d6f277ec11f40cd87319f04aa + txHash := "847139aa65aa4a5ee896375951cbf7417cfc8a4d6f277ec11f40cd87319f04aa" + chain := chains.BitcoinMainnet + + tssAddress := testutils.TSSAddressBTCMainnet + blockNumber := uint64(835640) + net := &chaincfg.MainNetParams + // 2.992e-05, see avgFeeRate https://mempool.space/api/v1/blocks/835640 + depositorFee := bitcoin.DepositorFee(22 * clientcommon.BTCOutboundGasPriceMultiplier) + + t.Run("decode OP_RETURN ok", func(t *testing.T) { + tx := testutils.LoadBTCInboundRawResult(t, TestDataDir, chain.ChainId, txHash, false) + + // https://mempool.space/tx/c5d224963832fc0b9a597251c2342a17b25e481a88cc9119008e8f8296652697 + preHash := "c5d224963832fc0b9a597251c2342a17b25e481a88cc9119008e8f8296652697" + tx.Vin[0].Txid = preHash + tx.Vin[0].Vout = 2 + + memo, _ := hex.DecodeString(tx.Vout[1].ScriptPubKey.Hex[4:]) + eventExpected := &observer.BTCInboundEvent{ + FromAddress: "bc1q68kxnq52ahz5vd6c8czevsawu0ux9nfrzzrh6e", + ToAddress: tssAddress, + Value: tx.Vout[0].Value - depositorFee, + MemoBytes: memo, + BlockNumber: blockNumber, + TxHash: tx.Txid, + } + + // load previous raw tx so so mock rpc client can return it + rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, preHash) + + // get BTC event + event, err := observer.GetBtcEventWithWitness( + rpcClient, + *tx, + tssAddress, + blockNumber, + log.Logger, + net, + depositorFee, + ) + require.NoError(t, err) + require.Equal(t, eventExpected, event) + }) + + t.Run("decode inscription ok", func(t *testing.T) { + txHash2 := "37777defed8717c581b4c0509329550e344bdc14ac38f71fc050096887e535c8" + tx := testutils.LoadBTCInboundRawResult(t, TestDataDir, chain.ChainId, txHash2, false) + + preHash := "c5d224963832fc0b9a597251c2342a17b25e481a88cc9119008e8f8296652697" + rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, preHash) + + eventExpected := &observer.BTCInboundEvent{ + FromAddress: "bc1qm24wp577nk8aacckv8np465z3dvmu7ry45el6y", + ToAddress: tssAddress, + Value: tx.Vout[0].Value - depositorFee, + MemoBytes: make([]byte, 600), + BlockNumber: blockNumber, + TxHash: tx.Txid, + } + + // get BTC event + event, err := observer.GetBtcEventWithWitness( + rpcClient, + *tx, + tssAddress, + blockNumber, + log.Logger, + net, + depositorFee, + ) + require.NoError(t, err) + require.Equal(t, event, eventExpected) + }) + + t.Run("decode inscription ok - mainnet", func(t *testing.T) { + // The input data is from the below mainnet, but output is modified for test case + txHash2 := "7a57f987a3cb605896a5909d9ef2bf7afbf0c78f21e4118b85d00d9e4cce0c2c" + tx := testutils.LoadBTCInboundRawResult(t, TestDataDir, chain.ChainId, txHash2, false) + + preHash := "c5d224963832fc0b9a597251c2342a17b25e481a88cc9119008e8f8296652697" + tx.Vin[0].Txid = preHash + tx.Vin[0].Sequence = 2 + rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, preHash) + + memo, _ := hex.DecodeString( + "72f080c854647755d0d9e6f6821f6931f855b9acffd53d87433395672756d58822fd143360762109ab898626556b1c3b8d3096d2361f1297df4a41c1b429471a9aa2fc9be5f27c13b3863d6ac269e4b587d8389f8fd9649859935b0d48dea88cdb40f20c", + ) + eventExpected := &observer.BTCInboundEvent{ + FromAddress: "bc1qm24wp577nk8aacckv8np465z3dvmu7ry45el6y", + ToAddress: tssAddress, + Value: tx.Vout[0].Value - depositorFee, + MemoBytes: memo, + BlockNumber: blockNumber, + TxHash: tx.Txid, + } + + // get BTC event + event, err := observer.GetBtcEventWithWitness( + rpcClient, + *tx, + tssAddress, + blockNumber, + log.Logger, + net, + depositorFee, + ) + require.NoError(t, err) + require.Equal(t, event, eventExpected) + }) + + t.Run("should skip tx if receiver address is not TSS address", func(t *testing.T) { + // load tx and modify receiver address to any non-tss address: bc1qw8wrek2m7nlqldll66ajnwr9mh64syvkt67zlu + tx := testutils.LoadBTCInboundRawResult(t, TestDataDir, chain.ChainId, txHash, false) + tx.Vout[0].ScriptPubKey.Hex = "001471dc3cd95bf4fe0fb7ffd6bb29b865ddf5581196" + + // get BTC event + rpcClient := mocks.NewMockBTCRPCClient() + event, err := observer.GetBtcEventWithWitness( + rpcClient, + *tx, + tssAddress, + blockNumber, + log.Logger, + net, + depositorFee, + ) + require.NoError(t, err) + require.Nil(t, event) + }) + + t.Run("should skip tx if amount is less than depositor fee", func(t *testing.T) { + // load tx and modify amount to less than depositor fee + tx := testutils.LoadBTCInboundRawResult(t, TestDataDir, chain.ChainId, txHash, false) + tx.Vout[0].Value = depositorFee - 1.0/1e8 // 1 satoshi less than depositor fee + + // get BTC event + rpcClient := mocks.NewMockBTCRPCClient() + event, err := observer.GetBtcEventWithWitness( + rpcClient, + *tx, + tssAddress, + blockNumber, + log.Logger, + net, + depositorFee, + ) + require.NoError(t, err) + require.Nil(t, event) + }) + + t.Run("should return error if RPC client fails to get raw tx", func(t *testing.T) { + // load tx and leave rpc client without preloaded tx + tx := testutils.LoadBTCInboundRawResult(t, TestDataDir, chain.ChainId, txHash, false) + rpcClient := mocks.NewMockBTCRPCClient() + + // get BTC event + event, err := observer.GetBtcEventWithWitness( + rpcClient, + *tx, + tssAddress, + blockNumber, + log.Logger, + net, + depositorFee, + ) + require.Error(t, err) + require.Nil(t, event) + }) + + t.Run("should return error if RPC client fails to get raw tx", func(t *testing.T) { + // load tx and leave rpc client without preloaded tx + tx := testutils.LoadBTCInboundRawResult(t, TestDataDir, chain.ChainId, txHash, false) + rpcClient := mocks.NewMockBTCRPCClient() + + // get BTC event + event, err := observer.GetBtcEventWithWitness( + rpcClient, + *tx, + tssAddress, + blockNumber, + log.Logger, + net, + depositorFee, + ) + require.Error(t, err) + require.Nil(t, event) + }) +} diff --git a/zetaclient/chains/bitcoin/tokenizer.go b/zetaclient/chains/bitcoin/tokenizer.go new file mode 100644 index 0000000000..5708bfa250 --- /dev/null +++ b/zetaclient/chains/bitcoin/tokenizer.go @@ -0,0 +1,162 @@ +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 b5f0bed226..8c0ebf5ff4 100644 --- a/zetaclient/chains/bitcoin/tx_script.go +++ b/zetaclient/chains/bitcoin/tx_script.go @@ -192,6 +192,36 @@ func DecodeOpReturnMemo(scriptHex string, txid string) ([]byte, bool, error) { return nil, false, nil } +// DecodeScript decodes memo wrapped in an inscription like script in witness +// returns (memo, found, error) +// +// Note: the format of the script is following that of "inscription" defined in ordinal theory. +// However, to separate from inscription (as this use case is not an NFT), simplifications are made. +// The bitcoin envelope script is as follows: +// OP_DATA_32 <32 byte of public key> OP_CHECKSIG +// OP_FALSE +// OP_IF +// +// OP_PUSH 0x... +// OP_PUSH 0x... +// +// 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) + + if err := checkInscriptionEnvelope(&t); err != nil { + return nil, false, errors.Wrap(err, "checkInscriptionEnvelope: unable to check the envelope") + } + + memoBytes, err := decodeInscriptionPayload(&t) + if err != nil { + return nil, false, errors.Wrap(err, "decodeInscriptionPayload: unable to decode the payload") + } + + return memoBytes, true, nil +} + // EncodeAddress returns a human-readable payment address given a ripemd160 hash // and netID which encodes the bitcoin network and address type. It is used // in both pay-to-pubkey-hash (P2PKH) and pay-to-script-hash (P2SH) address @@ -245,3 +275,44 @@ func DecodeTSSVout(vout btcjson.Vout, receiverExpected string, chain chains.Chai return receiverVout, amount, nil } + +func decodeInscriptionPayload(t *scriptTokenizer) ([]byte, error) { + if !t.Next() || t.Opcode() != txscript.OP_FALSE { + return nil, fmt.Errorf("OP_FALSE not found") + } + + if !t.Next() || t.Opcode() != txscript.OP_IF { + return nil, fmt.Errorf("OP_IF not found") + } + + memo := make([]byte, 0) + var next byte + for t.Next() { + next = t.Opcode() + if next == txscript.OP_ENDIF { + return memo, nil + } + if next < txscript.OP_DATA_1 || next > txscript.OP_PUSHDATA4 { + return nil, fmt.Errorf("expecting data push, found %d", next) + } + memo = append(memo, t.Data()...) + } + if t.Err() != nil { + return nil, t.Err() + } + return nil, fmt.Errorf("should contain more data, but script ended") +} + +// checkInscriptionEnvelope decodes the envelope for the script monitoring. The format is +// OP_PUSHBYTES_32 <32 bytes> OP_CHECKSIG +func checkInscriptionEnvelope(t *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()) + } + + if !t.Next() || t.Opcode() != txscript.OP_CHECKSIG { + return fmt.Errorf("cannot parse OP_CHECKSIG, op %d or err %s", t.Opcode(), t.Err()) + } + + return nil +} diff --git a/zetaclient/chains/bitcoin/tx_script_test.go b/zetaclient/chains/bitcoin/tx_script_test.go index eea97fc7b5..f1b17f2119 100644 --- a/zetaclient/chains/bitcoin/tx_script_test.go +++ b/zetaclient/chains/bitcoin/tx_script_test.go @@ -491,3 +491,66 @@ func TestDecodeTSSVoutErrors(t *testing.T) { require.Zero(t, amount) }) } + +func TestDecodeScript(t *testing.T) { + t.Run("should decode longer data ok", func(t *testing.T) { + // 600 bytes of random data generated offline + data := "2001a7bae79bd61c2368fe41a565061d6cf22b4f509fbc1652caea06d98b8fd0c7ac00634d0802c7faa771dd05f27993d22c42988758882d20080241074462884c8774e1cdf4b04e5b3b74b6568bd1769722708306c66270b6b2a7f68baced83627eeeb2d494e8a1749277b92a4c5a90b1b4f6038e5f704405515109d4d0021612ad298b8dad6e12245f8f0020e11a7a319652ba6abe261958201ce5e83131cd81302c0ecec60d4afa9f72540fc84b6b9c1f3d903ab25686df263b192a403a4aa22b799ba24369c49ff4042012589a07d4211e05f80f18a1262de5a1577ce0ec9e1fa9283cfa25d98d7d0b4217951dfcb8868570318c63f1e1424cfdb7d7a33c6b9e3ced4b2ffa0178b3a5fac8bace2991e382a402f56a2c6a9191463740910056483e4fd0f5ac729ffac66bf1b3ec4570c4e75c116f7d9fd65718ec3ed6c7647bf335b77e7d6a4e2011276dc8031b78403a1ad82c92fb339ec916c263b6dd0f003ba4381ad5410e90e88effbfa7f961b8e8a6011c525643a434f7abe2c1928a892cc57d6291831216c4e70cb80a39a79a3889211070e767c23db396af9b4c2093c3743d8cbcbfcb73d29361ecd3857e94ab3c800be1299fd36a5685ec60607a60d8c2e0f99ff0b8b9e86354d39a43041f7d552e95fe2d33b6fc0f540715da0e7e1b344c778afe73f82d00881352207b719f67dcb00b4ff645974d4fd7711363d26400e2852890cb6ea9cbfe63ac43080870049b1023be984331560c6350bb64da52b4b81bc8910934915f0a96701f4c50646d5386146596443bee9b2d116706e1687697fb42542196c1d764419c23a914896f9212946518ac59e1ba5d1fc37e503313133ebdf2ced5785e0eaa9738fe3f9ad73646e733931ebb7cff26e96106fe68" + script, _ := hex.DecodeString(data) + + memo, isFound, err := DecodeScript(script) + require.Nil(t, err) + require.True(t, isFound) + + // the expected memo + expected := "c7faa771dd05f27993d22c42988758882d20080241074462884c8774e1cdf4b04e5b3b74b6568bd1769722708306c66270b6b2a7f68baced83627eeeb2d494e8a1749277b92a4c5a90b1b4f6038e5f704405515109d4d0021612ad298b8dad6e12245f8f0020e11a7a319652ba6abe261958201ce5e83131cd81302c0ecec60d4afa9f72540fc84b6b9c1f3d903ab25686df263b192a403a4aa22b799ba24369c49ff4042012589a07d4211e05f80f18a1262de5a1577ce0ec9e1fa9283cfa25d98d7d0b4217951dfcb8868570318c63f1e1424cfdb7d7a33c6b9e3ced4b2ffa0178b3a5fac8bace2991e382a402f56a2c6a9191463740910056483e4fd0f5ac729ffac66bf1b3ec4570c4e75c116f7d9fd65718ec3ed6c7647bf335b77e7d6a4e2011276dc8031b78403a1ad82c92fb339ec916c263b6dd0f003ba4381ad5410e90e88effbfa7f961b8e8a6011c525643a434f7abe2c1928a892cc57d6291831216c4e70cb80a39a79a3889211070e767c23db396af9b4c2093c3743d8cbcbfcb73d29361ecd3857e94ab3c800be1299fd36a5685ec60607a60d8c2e0f99ff0b8b9e86354d39a43041f7d552e95fe2d33b6fc0f540715da0e7e1b344c778afe73f82d00881352207b719f67dcb00b4ff645974d4fd7711363d26400e2852890cb6ea9cbfe63ac43080870049b1023be984331560c6350bb64da52b4b81bc8910934915f0a96701f646d5386146596443bee9b2d116706e1687697fb42542196c1d764419c23a914896f9212946518ac59e1ba5d1fc37e503313133ebdf2ced5785e0eaa9738fe3f9ad73646e733931ebb7cff26e96106fe" + require.Equal(t, hex.EncodeToString(memo), expected) + }) + + t.Run("should decode shorter data ok", func(t *testing.T) { + // 81 bytes of random data generated offline + data := "20d6f59371037bf30115d9fd6016f0e3ef552cdfc0367ee20aa9df3158f74aaeb4ac00634c51bdd33073d76f6b4ae6510d69218100575eafabadd16e5faf9f42bd2fbbae402078bdcaa4c0413ce96d053e3c0bbd4d5944d6857107d640c248bdaaa7de959d9c1e6b9962b51428e5a554c28c397160881668" + script, _ := hex.DecodeString(data) + + memo, isFound, err := DecodeScript(script) + require.Nil(t, err) + require.True(t, isFound) + + // the expected memo + expected := "bdd33073d76f6b4ae6510d69218100575eafabadd16e5faf9f42bd2fbbae402078bdcaa4c0413ce96d053e3c0bbd4d5944d6857107d640c248bdaaa7de959d9c1e6b9962b51428e5a554c28c3971608816" + require.Equal(t, hex.EncodeToString(memo), expected) + }) + + t.Run("decode error due to missing data byte", func(t *testing.T) { + // missing OP_ENDIF at the end + data := "20cabd6ecc0245c40f27ca6299dcd3732287c317f3946734f04e27568fc5334218ac00634d0802000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004c500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000068" + script, _ := hex.DecodeString(data) + + memo, isFound, err := DecodeScript(script) + require.ErrorContains(t, err, "should contain more data, but script ended") + require.False(t, isFound) + require.Nil(t, memo) + }) + + t.Run("decode error due to missing data for public key", func(t *testing.T) { + // missing OP_ENDIF at the end + data := "2001a7bae79bd61c2368fe41a565061d6cf22b4f509fbc1652caea06d98b8fd0" + script, _ := hex.DecodeString(data) + + memo, isFound, err := DecodeScript(script) + require.ErrorContains(t, err, "cannot obtain public key bytes") + require.False(t, isFound) + require.Nil(t, memo) + }) + + t.Run("decode error due to missing OP_CHECKSIG", func(t *testing.T) { + // missing OP_ENDIF at the end + data := "2001a7bae79bd61c2368fe41a565061d6cf22b4f509fbc1652caea06d98b8fd0c7ab" + script, _ := hex.DecodeString(data) + + memo, isFound, err := DecodeScript(script) + require.ErrorContains(t, err, "cannot parse OP_CHECKSIG") + require.False(t, isFound) + require.Nil(t, memo) + }) +} diff --git a/zetaclient/chains/evm/constant.go b/zetaclient/chains/evm/constant.go index b754d57f30..beaeb6143f 100644 --- a/zetaclient/chains/evm/constant.go +++ b/zetaclient/chains/evm/constant.go @@ -3,12 +3,13 @@ package evm import "time" const ( - // ZetaBlockTime is the block time of the Zeta network - ZetaBlockTime = 6500 * time.Millisecond - // OutboundInclusionTimeout is the timeout for waiting for an outbound to be included in a block OutboundInclusionTimeout = 20 * time.Minute + // ReorgProtectBlockCount is confirmations count to protect against reorg + // Short 1~2 block reorgs could happen often on Ethereum due to network congestion or block production race conditions + ReorgProtectBlockCount = 2 + // OutboundTrackerReportTimeout is the timeout for waiting for an outbound tracker report OutboundTrackerReportTimeout = 10 * time.Minute diff --git a/zetaclient/chains/evm/observer/inbound.go b/zetaclient/chains/evm/observer/inbound.go index b8ee361f37..abf21e7e5b 100644 --- a/zetaclient/chains/evm/observer/inbound.go +++ b/zetaclient/chains/evm/observer/inbound.go @@ -20,9 +20,11 @@ import ( "github.com/zeta-chain/protocol-contracts/pkg/contracts/evm/erc20custody.sol" "github.com/zeta-chain/protocol-contracts/pkg/contracts/evm/zetaconnector.non-eth.sol" + "github.com/zeta-chain/zetacore/pkg/bg" "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/pkg/coin" "github.com/zeta-chain/zetacore/pkg/constant" + "github.com/zeta-chain/zetacore/pkg/ticker" "github.com/zeta-chain/zetacore/x/crosschain/types" "github.com/zeta-chain/zetacore/zetaclient/chains/evm" "github.com/zeta-chain/zetacore/zetaclient/compliance" @@ -36,42 +38,46 @@ import ( // WatchInbound watches evm chain for incoming txs and post votes to zetacore // TODO(revamp): move ticker function to a separate file func (ob *Observer) WatchInbound(ctx context.Context) error { - app, err := zctx.FromContext(ctx) - if err != nil { - return err + sampledLogger := ob.Logger().Inbound.Sample(&zerolog.BasicSampler{N: 10}) + interval := ticker.SecondsFromUint64(ob.GetChainParams().InboundTicker) + task := func(ctx context.Context, t *ticker.Ticker) error { + return ob.watchInboundOnce(ctx, t, sampledLogger) } - ticker, err := clienttypes.NewDynamicTicker( - fmt.Sprintf("EVM_WatchInbound_%d", ob.Chain().ChainId), - ob.GetChainParams().InboundTicker, - ) + t := ticker.New(interval, task) + + bg.Work(ctx, func(_ context.Context) error { + <-ob.StopChannel() + t.Stop() + ob.Logger().Inbound.Info().Msg("WatchInbound stopped") + return nil + }) + + ob.Logger().Inbound.Info().Msgf("WatchInbound started") + + return t.Run(ctx) +} + +func (ob *Observer) watchInboundOnce(ctx context.Context, t *ticker.Ticker, sampledLogger zerolog.Logger) error { + app, err := zctx.FromContext(ctx) if err != nil { - ob.Logger().Inbound.Error().Err(err).Msg("error creating ticker") return err } - defer ticker.Stop() - ob.Logger().Inbound.Info().Msgf("WatchInbound started for chain %d", ob.Chain().ChainId) - sampledLogger := ob.Logger().Inbound.Sample(&zerolog.BasicSampler{N: 10}) + // noop + if !app.IsInboundObservationEnabled() { + ob.Logger().Inbound.Warn().Msg("WatchInbound: inbound observation is disabled") + return nil + } - for { - select { - case <-ticker.C(): - if !app.IsInboundObservationEnabled(ob.GetChainParams()) { - sampledLogger.Info(). - Msgf("WatchInbound: inbound observation is disabled for chain %d", ob.Chain().ChainId) - continue - } - err := ob.ObserveInbound(ctx, sampledLogger) - if err != nil { - ob.Logger().Inbound.Err(err).Msg("WatchInbound: observeInbound error") - } - ticker.UpdateInterval(ob.GetChainParams().InboundTicker, ob.Logger().Inbound) - case <-ob.StopChannel(): - ob.Logger().Inbound.Info().Msgf("WatchInbound stopped for chain %d", ob.Chain().ChainId) - return nil - } + if err := ob.ObserveInbound(ctx, sampledLogger); err != nil { + ob.Logger().Inbound.Err(err).Msg("WatchInbound: observeInbound error") } + + newInterval := ticker.SecondsFromUint64(ob.GetChainParams().InboundTicker) + t.SetInterval(newInterval) + + return nil } // WatchInboundTracker gets a list of Inbound tracker suggestions from zeta-core at each tick and tries to check if the in-tx was confirmed. @@ -97,7 +103,7 @@ func (ob *Observer) WatchInboundTracker(ctx context.Context) error { for { select { case <-ticker.C(): - if !app.IsInboundObservationEnabled(ob.GetChainParams()) { + if !app.IsInboundObservationEnabled() { continue } err := ob.ProcessInboundTrackers(ctx) @@ -311,18 +317,17 @@ func (ob *Observer) ObserveZetaSent(ctx context.Context, startBlock, toBlock uin guard[event.Raw.TxHash.Hex()] = true msg := ob.BuildInboundVoteMsgForZetaSentEvent(app, event) - if msg != nil { - _, err = ob.PostVoteInbound( - ctx, - msg, - zetacore.PostVoteInboundMessagePassingExecutionGasLimit, - ) - if err != nil { - // we have to re-scan from this block next time - return beingScanned - 1, err - } + if msg == nil { + continue + } + + const gasLimit = zetacore.PostVoteInboundMessagePassingExecutionGasLimit + if _, err = ob.PostVoteInbound(ctx, msg, gasLimit); err != nil { + // we have to re-scan from this block next time + return beingScanned - 1, err } } + // successful processed all events in [startBlock, toBlock] return toBlock, nil } @@ -414,34 +419,10 @@ func (ob *Observer) ObserveERC20Deposited(ctx context.Context, startBlock, toBlo // ObserverTSSReceive queries the incoming gas asset to TSS address and posts to zetacore // returns the last block successfully scanned func (ob *Observer) ObserverTSSReceive(ctx context.Context, startBlock, toBlock uint64) (uint64, error) { - app, err := zctx.FromContext(ctx) - if err != nil { - return 0, err - } - - var ( - // post new block header (if any) to zetacore and ignore error - // TODO: consider having a independent ticker(from TSS scaning) for posting block headers - // https://github.com/zeta-chain/node/issues/1847 - chainID = ob.Chain().ChainId - blockHeaderVerification, found = app.GetBlockHeaderEnabledChains(chainID) - shouldPostBlockHeader = found && blockHeaderVerification.Enabled - ) + chainID := ob.Chain().ChainId // query incoming gas asset for bn := startBlock; bn <= toBlock; bn++ { - if shouldPostBlockHeader { - // post block header for supported chains - // TODO: move this logic in its own routine - // https://github.com/zeta-chain/node/issues/2204 - if err := ob.postBlockHeader(ctx, toBlock); err != nil { - ob.Logger().Inbound. - Error().Err(err). - Uint64("tss.to_block", toBlock). - Msg("error posting block header") - } - } - // observe TSS received gas token in block 'bn' err := ob.ObserveTSSReceiveInBlock(ctx, bn) if err != nil { @@ -532,7 +513,7 @@ func (ob *Observer) CheckAndVoteInboundTokenERC20( } // get erc20 custody contract - addrCustory, custody, err := ob.GetERC20CustodyContract() + addrCustody, custody, err := ob.GetERC20CustodyContract() if err != nil { return "", err } @@ -544,7 +525,7 @@ func (ob *Observer) CheckAndVoteInboundTokenERC20( zetaDeposited, err := custody.ParseDeposited(*log) if err == nil && zetaDeposited != nil { // sanity check tx event - err = evm.ValidateEvmTxLog(&zetaDeposited.Raw, addrCustory, tx.Hash, evm.TopicsDeposited) + err = evm.ValidateEvmTxLog(&zetaDeposited.Raw, addrCustody, tx.Hash, evm.TopicsDeposited) if err == nil { msg = ob.BuildInboundVoteMsgForDepositedEvent(zetaDeposited, sender) } else { @@ -671,14 +652,13 @@ func (ob *Observer) BuildInboundVoteMsgForZetaSentEvent( appContext *zctx.AppContext, event *zetaconnector.ZetaConnectorNonEthZetaSent, ) *types.MsgVoteInbound { - destChain, found := chains.GetChainFromChainID( - event.DestinationChainId.Int64(), - appContext.GetAdditionalChains(), - ) - if !found { - ob.Logger().Inbound.Warn().Msgf("chain id not supported %d", event.DestinationChainId.Int64()) + // note that this is most likely zeta chain + destChain, err := appContext.GetChain(event.DestinationChainId.Int64()) + if err != nil { + ob.Logger().Inbound.Warn().Err(err).Msgf("chain id %d not supported", event.DestinationChainId.Int64()) return nil } + destAddr := clienttypes.BytesToEthHex(event.DestinationAddress) // compliance check @@ -689,17 +669,10 @@ func (ob *Observer) BuildInboundVoteMsgForZetaSentEvent( return nil } - if !destChain.IsZetaChain() { - paramsDest, found := appContext.GetEVMChainParams(destChain.ChainId) - if !found { - ob.Logger().Inbound.Warn(). - Msgf("chain id not present in EVMChainParams %d", event.DestinationChainId.Int64()) - return nil - } - - if strings.EqualFold(destAddr, paramsDest.ZetaTokenContractAddress) { + if !destChain.IsZeta() { + if strings.EqualFold(destAddr, destChain.Params().ZetaTokenContractAddress) { ob.Logger().Inbound.Warn(). - Msgf("potential attack attempt: %s destination address is ZETA token contract address %s", destChain.String(), destAddr) + Msgf("potential attack attempt: %s destination address is ZETA token contract address", destAddr) return nil } } @@ -713,7 +686,7 @@ func (ob *Observer) BuildInboundVoteMsgForZetaSentEvent( ob.Chain().ChainId, event.SourceTxOriginAddress.Hex(), destAddr, - destChain.ChainId, + destChain.ID(), sdkmath.NewUintFromBigInt(event.ZetaValueAndGas), message, event.Raw.TxHash.Hex(), diff --git a/zetaclient/chains/evm/observer/inbound_test.go b/zetaclient/chains/evm/observer/inbound_test.go index 9e01c214b3..26290fc6c0 100644 --- a/zetaclient/chains/evm/observer/inbound_test.go +++ b/zetaclient/chains/evm/observer/inbound_test.go @@ -45,8 +45,10 @@ func Test_CheckAndVoteInboundTokenZeta(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) - ballot, err := ob.CheckAndVoteInboundTokenZeta(ctx, tx, receipt, false) + ob, appContext := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) + voteCtx := zctx.WithAppContext(context.Background(), appContext) + + ballot, err := ob.CheckAndVoteInboundTokenZeta(voteCtx, tx, receipt, false) require.NoError(t, err) require.Equal(t, cctx.InboundParams.BallotIndex, ballot) }) @@ -61,7 +63,7 @@ func Test_CheckAndVoteInboundTokenZeta(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - 1 - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) + ob, _ := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) _, err := ob.CheckAndVoteInboundTokenZeta(ctx, tx, receipt, false) require.ErrorContains(t, err, "not been confirmed") }) @@ -77,24 +79,27 @@ func Test_CheckAndVoteInboundTokenZeta(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) + ob, _ := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) ballot, err := ob.CheckAndVoteInboundTokenZeta(ctx, tx, receipt, true) require.NoError(t, err) require.Equal(t, "", ballot) }) t.Run("should not act if emitter is not ZetaConnector", func(t *testing.T) { - tx, receipt, _ := testutils.LoadEVMInboundNReceiptNCctx( - t, + // Given tx from ETH + tx, receipt, _ := testutils.LoadEVMInboundNReceiptNCctx(t, TestDataDir, - chainID, + chains.Ethereum.ChainId, inboundHash, coin.CoinType_Zeta, ) require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - chainID = 56 // use BSC chain connector - ob := MockEVMObserver( + // Given BSC observer + chain := chains.BscMainnet + params := mocks.MockChainParams(chain.ChainId, confirmation) + + ob, _ := MockEVMObserver( t, chain, nil, @@ -102,9 +107,13 @@ func Test_CheckAndVoteInboundTokenZeta(t *testing.T) { nil, nil, lastBlock, - mocks.MockChainParams(chainID, confirmation), + params, ) + + // ACT _, err := ob.CheckAndVoteInboundTokenZeta(ctx, tx, receipt, true) + + // ASSERT require.ErrorContains(t, err, "emitter address mismatch") }) } @@ -131,7 +140,7 @@ func Test_CheckAndVoteInboundTokenERC20(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) + ob, _ := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) ballot, err := ob.CheckAndVoteInboundTokenERC20(ctx, tx, receipt, false) require.NoError(t, err) require.Equal(t, cctx.InboundParams.BallotIndex, ballot) @@ -147,7 +156,7 @@ func Test_CheckAndVoteInboundTokenERC20(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - 1 - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) + ob, _ := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) _, err := ob.CheckAndVoteInboundTokenERC20(ctx, tx, receipt, false) require.ErrorContains(t, err, "not been confirmed") }) @@ -163,24 +172,29 @@ func Test_CheckAndVoteInboundTokenERC20(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) + ob, _ := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) ballot, err := ob.CheckAndVoteInboundTokenERC20(ctx, tx, receipt, true) require.NoError(t, err) require.Equal(t, "", ballot) }) t.Run("should not act if emitter is not ERC20 Custody", func(t *testing.T) { + // ARRANGE + // Given tx from ETH tx, receipt, _ := testutils.LoadEVMInboundNReceiptNCctx( t, TestDataDir, - chainID, + chains.Ethereum.ChainId, inboundHash, coin.CoinType_ERC20, ) require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - chainID = 56 // use BSC chain ERC20 custody - ob := MockEVMObserver( + // Given BSC observer + chain := chains.BscMainnet + params := mocks.MockChainParams(chain.ChainId, confirmation) + + ob, _ := MockEVMObserver( t, chain, nil, @@ -188,9 +202,13 @@ func Test_CheckAndVoteInboundTokenERC20(t *testing.T) { nil, nil, lastBlock, - mocks.MockChainParams(chainID, confirmation), + params, ) + + // ACT _, err := ob.CheckAndVoteInboundTokenERC20(ctx, tx, receipt, true) + + // ASSERT require.ErrorContains(t, err, "emitter address mismatch") }) } @@ -217,7 +235,7 @@ func Test_CheckAndVoteInboundTokenGas(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) + ob, _ := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) ballot, err := ob.CheckAndVoteInboundTokenGas(ctx, tx, receipt, false) require.NoError(t, err) require.Equal(t, cctx.InboundParams.BallotIndex, ballot) @@ -227,7 +245,7 @@ func Test_CheckAndVoteInboundTokenGas(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - 1 - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) + ob, _ := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) _, err := ob.CheckAndVoteInboundTokenGas(ctx, tx, receipt, false) require.ErrorContains(t, err, "not been confirmed") }) @@ -237,7 +255,7 @@ func Test_CheckAndVoteInboundTokenGas(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) + ob, _ := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) ballot, err := ob.CheckAndVoteInboundTokenGas(ctx, tx, receipt, false) require.ErrorContains(t, err, "not TSS address") require.Equal(t, "", ballot) @@ -248,7 +266,7 @@ func Test_CheckAndVoteInboundTokenGas(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) + ob, _ := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) ballot, err := ob.CheckAndVoteInboundTokenGas(ctx, tx, receipt, false) require.ErrorContains(t, err, "not a successful tx") require.Equal(t, "", ballot) @@ -259,7 +277,7 @@ func Test_CheckAndVoteInboundTokenGas(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) + ob, _ := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) ballot, err := ob.CheckAndVoteInboundTokenGas(ctx, tx, receipt, false) require.NoError(t, err) require.Equal(t, "", ballot) @@ -276,7 +294,7 @@ func Test_BuildInboundVoteMsgForZetaSentEvent(t *testing.T) { cctx := testutils.LoadCctxByInbound(t, chainID, coin.CoinType_Zeta, inboundHash) // parse ZetaSent event - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, mocks.MockChainParams(1, 1)) + ob, app := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, mocks.MockChainParams(1, 1)) connector := mocks.MockConnectorNonEth(t, chainID) event := testutils.ParseReceiptZetaSent(receipt, connector) @@ -285,8 +303,6 @@ func Test_BuildInboundVoteMsgForZetaSentEvent(t *testing.T) { ComplianceConfig: config.ComplianceConfig{}, } - _, app := makeAppContext(t) - t.Run("should return vote msg for archived ZetaSent event", func(t *testing.T) { msg := ob.BuildInboundVoteMsgForZetaSentEvent(app, event) require.NotNil(t, msg) @@ -325,7 +341,7 @@ func Test_BuildInboundVoteMsgForDepositedEvent(t *testing.T) { cctx := testutils.LoadCctxByInbound(t, chainID, coin.CoinType_ERC20, inboundHash) // parse Deposited event - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, mocks.MockChainParams(1, 1)) + ob, _ := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, mocks.MockChainParams(1, 1)) custody := mocks.MockERC20Custody(t, chainID) event := testutils.ParseReceiptERC20Deposited(receipt, custody) sender := ethcommon.HexToAddress(tx.From) @@ -383,7 +399,7 @@ func Test_BuildInboundVoteMsgForTokenSentToTSS(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(txDonation)) // create test compliance config - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, mocks.MockChainParams(1, 1)) + ob, _ := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, mocks.MockChainParams(1, 1)) cfg := config.Config{ ComplianceConfig: config.ComplianceConfig{}, } @@ -460,7 +476,7 @@ func Test_ObserveTSSReceiveInBlock(t *testing.T) { ctx := context.Background() t.Run("should observe TSS receive in block", func(t *testing.T) { - ob := MockEVMObserver(t, chain, evmClient, evmJSONRPC, zetacoreClient, tss, lastBlock, chainParam) + ob, _ := MockEVMObserver(t, chain, evmClient, evmJSONRPC, zetacoreClient, tss, lastBlock, chainParam) // feed archived block and receipt evmJSONRPC.WithBlock(block) @@ -469,13 +485,13 @@ func Test_ObserveTSSReceiveInBlock(t *testing.T) { require.NoError(t, err) }) t.Run("should not observe on error getting block", func(t *testing.T) { - ob := MockEVMObserver(t, chain, evmClient, evmJSONRPC, zetacoreClient, tss, lastBlock, chainParam) + ob, _ := MockEVMObserver(t, chain, evmClient, evmJSONRPC, zetacoreClient, tss, lastBlock, chainParam) err := ob.ObserveTSSReceiveInBlock(ctx, blockNumber) // error getting block is expected because the mock JSONRPC contains no block require.ErrorContains(t, err, "error getting block") }) t.Run("should not observe on error getting receipt", func(t *testing.T) { - ob := MockEVMObserver(t, chain, evmClient, evmJSONRPC, zetacoreClient, tss, lastBlock, chainParam) + ob, _ := MockEVMObserver(t, chain, evmClient, evmJSONRPC, zetacoreClient, tss, lastBlock, chainParam) evmJSONRPC.WithBlock(block) err := ob.ObserveTSSReceiveInBlock(ctx, blockNumber) // error getting block is expected because the mock evmClient contains no receipt @@ -483,9 +499,9 @@ func Test_ObserveTSSReceiveInBlock(t *testing.T) { }) } -func makeAppContext(_ *testing.T) (context.Context, *zctx.AppContext) { +func makeAppContext(t *testing.T) (context.Context, *zctx.AppContext) { var ( - app = zctx.New(config.New(false), zerolog.Nop()) + app = zctx.New(config.New(false), nil, zerolog.New(zerolog.NewTestWriter(t))) ctx = context.Background() ) diff --git a/zetaclient/chains/evm/observer/observer.go b/zetaclient/chains/evm/observer/observer.go index b6ff80c769..b5e7be3f6e 100644 --- a/zetaclient/chains/evm/observer/observer.go +++ b/zetaclient/chains/evm/observer/observer.go @@ -11,7 +11,6 @@ import ( ethcommon "github.com/ethereum/go-ethereum/common" ethtypes "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/rlp" "github.com/onrik/ethrpc" "github.com/pkg/errors" "github.com/zeta-chain/protocol-contracts/pkg/contracts/evm/erc20custody.sol" @@ -20,7 +19,6 @@ import ( "github.com/zeta-chain/protocol-contracts/pkg/contracts/evm/zetaconnector.non-eth.sol" "github.com/zeta-chain/zetacore/pkg/bg" - "github.com/zeta-chain/zetacore/pkg/proofs" observertypes "github.com/zeta-chain/zetacore/x/observer/types" "github.com/zeta-chain/zetacore/zetaclient/chains/base" "github.com/zeta-chain/zetacore/zetaclient/chains/evm" @@ -45,9 +43,6 @@ type Observer struct { // evmJSONRPC is the EVM JSON RPC client for the observed chain evmJSONRPC interfaces.EVMJSONRPCClient - // outboundPendingTransactions is the map to index pending transactions by hash - outboundPendingTransactions map[string]*ethtypes.Transaction - // outboundConfirmedReceipts is the map to index confirmed receipts by hash outboundConfirmedReceipts map[string]*ethtypes.Receipt @@ -94,7 +89,6 @@ func NewObserver( Observer: *baseObserver, evmClient: evmClient, evmJSONRPC: ethrpc.NewEthRPC(evmCfg.Endpoint), - outboundPendingTransactions: make(map[string]*ethtypes.Transaction), outboundConfirmedReceipts: make(map[string]*ethtypes.Receipt), outboundConfirmedTransactions: make(map[string]*ethtypes.Transaction), priorityFeeConfig: priorityFeeConfig{}, @@ -232,25 +226,10 @@ func (ob *Observer) WatchRPCStatus(ctx context.Context) error { } } -// SetPendingTx sets the pending transaction in memory -func (ob *Observer) SetPendingTx(nonce uint64, transaction *ethtypes.Transaction) { - ob.Mu().Lock() - defer ob.Mu().Unlock() - ob.outboundPendingTransactions[ob.OutboundID(nonce)] = transaction -} - -// GetPendingTx gets the pending transaction from memory -func (ob *Observer) GetPendingTx(nonce uint64) *ethtypes.Transaction { - ob.Mu().Lock() - defer ob.Mu().Unlock() - return ob.outboundPendingTransactions[ob.OutboundID(nonce)] -} - // SetTxNReceipt sets the receipt and transaction in memory func (ob *Observer) SetTxNReceipt(nonce uint64, receipt *ethtypes.Receipt, transaction *ethtypes.Transaction) { ob.Mu().Lock() defer ob.Mu().Unlock() - delete(ob.outboundPendingTransactions, ob.OutboundID(nonce)) // remove pending transaction, if any ob.outboundConfirmedReceipts[ob.OutboundID(nonce)] = receipt ob.outboundConfirmedTransactions[ob.OutboundID(nonce)] = transaction } @@ -389,43 +368,3 @@ func (ob *Observer) LoadLastBlockScanned(ctx context.Context) error { return nil } - -// postBlockHeader posts the block header to zetacore -// TODO(revamp): move to a block header file -func (ob *Observer) postBlockHeader(ctx context.Context, tip uint64) error { - bn := tip - - chainState, err := ob.ZetacoreClient().GetBlockHeaderChainState(ctx, ob.Chain().ChainId) - if err == nil && chainState != nil && chainState.EarliestHeight > 0 { - // #nosec G115 always positive - bn = uint64(chainState.LatestHeight) + 1 // the next header to post - } - - if bn > tip { - return fmt.Errorf("postBlockHeader: must post block confirmed block header: %d > %d", bn, tip) - } - - header, err := ob.GetBlockHeaderCached(ctx, bn) - if err != nil { - ob.Logger().Inbound.Error().Err(err).Msgf("postBlockHeader: error getting block: %d", bn) - return err - } - headerRLP, err := rlp.EncodeToBytes(header) - if err != nil { - ob.Logger().Inbound.Error().Err(err).Msgf("postBlockHeader: error encoding block header: %d", bn) - return err - } - - _, err = ob.ZetacoreClient().PostVoteBlockHeader( - ctx, - ob.Chain().ChainId, - header.Hash().Bytes(), - header.Number.Int64(), - proofs.NewEthereumHeader(headerRLP), - ) - if err != nil { - ob.Logger().Inbound.Error().Err(err).Msgf("postBlockHeader: error posting block header: %d", bn) - return err - } - return nil -} diff --git a/zetaclient/chains/evm/observer/observer_gas.go b/zetaclient/chains/evm/observer/observer_gas.go index 8bbc32d3a0..311ea187b9 100644 --- a/zetaclient/chains/evm/observer/observer_gas.go +++ b/zetaclient/chains/evm/observer/observer_gas.go @@ -126,7 +126,7 @@ func (ob *Observer) supportsPriorityFee(ctx context.Context) (bool, error) { defer ob.Mu().Unlock() ob.priorityFeeConfig.checked = true - ob.priorityFeeConfig.checked = isSupported + ob.priorityFeeConfig.supported = isSupported return isSupported, nil } diff --git a/zetaclient/chains/evm/observer/observer_gas_test.go b/zetaclient/chains/evm/observer/observer_gas_test.go index ce0b681d43..3a416f1733 100644 --- a/zetaclient/chains/evm/observer/observer_gas_test.go +++ b/zetaclient/chains/evm/observer/observer_gas_test.go @@ -34,7 +34,7 @@ func TestPostGasPrice(t *testing.T) { confirmation := uint64(10) chainParam := mocks.MockChainParams(chain.ChainId, confirmation) - observer := MockEVMObserver(t, chain, ethRPC, nil, zetacoreClient, nil, blockNumber, chainParam) + observer, _ := MockEVMObserver(t, chain, ethRPC, nil, zetacoreClient, nil, blockNumber, chainParam) // Given empty baseFee from RPC ethRPC.WithHeader(ðtypes.Header{BaseFee: nil}) @@ -79,7 +79,7 @@ func TestPostGasPrice(t *testing.T) { confirmation := uint64(10) chainParam := mocks.MockChainParams(chain.ChainId, confirmation) - observer := MockEVMObserver(t, chain, ethRPC, nil, zetacoreClient, nil, blockNumber, chainParam) + observer, _ := MockEVMObserver(t, chain, ethRPC, nil, zetacoreClient, nil, blockNumber, chainParam) // Given 1 gwei baseFee from RPC ethRPC.WithHeader(ðtypes.Header{BaseFee: big.NewInt(gwei)}) diff --git a/zetaclient/chains/evm/observer/observer_test.go b/zetaclient/chains/evm/observer/observer_test.go index f0b47044d5..69ff6a977d 100644 --- a/zetaclient/chains/evm/observer/observer_test.go +++ b/zetaclient/chains/evm/observer/observer_test.go @@ -13,6 +13,7 @@ import ( "github.com/onrik/ethrpc" "github.com/rs/zerolog" "github.com/stretchr/testify/require" + "github.com/zeta-chain/zetacore/pkg/ptr" zctx "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/zetaclient/db" "github.com/zeta-chain/zetacore/zetaclient/keys" @@ -36,6 +37,7 @@ var TestDataDir = "../../../" // getAppContext creates an AppContext for unit tests func getAppContext( + t *testing.T, evmChain chains.Chain, endpoint string, evmChainParams *observertypes.ChainParams, @@ -45,6 +47,8 @@ func getAppContext( endpoint = "http://localhost:8545" } + require.Equal(t, evmChain.ChainId, evmChainParams.ChainId, "chain id mismatch between chain and params") + // create config cfg := config.New(false) cfg.EVMChainConfigs[evmChain.ChainId] = config.EVMConfig{ @@ -52,24 +56,28 @@ func getAppContext( Endpoint: endpoint, } + logger := zerolog.New(zerolog.NewTestWriter(t)) + // create AppContext - appContext := zctx.New(cfg, zerolog.Nop()) - evmChainParamsMap := make(map[int64]*observertypes.ChainParams) - evmChainParamsMap[evmChain.ChainId] = evmChainParams + appContext := zctx.New(cfg, nil, logger) + chainParams := map[int64]*observertypes.ChainParams{ + evmChain.ChainId: evmChainParams, + chains.ZetaChainMainnet.ChainId: ptr.Ptr( + mocks.MockChainParams(chains.ZetaChainMainnet.ChainId, 10), + ), + } // feed chain params - appContext.Update( - &observertypes.Keygen{}, - []chains.Chain{evmChain}, - evmChainParamsMap, + err := appContext.Update( + observertypes.Keygen{}, + []chains.Chain{evmChain, chains.ZetaChainMainnet}, nil, - nil, - "", + chainParams, + "tssPubKey", *sample.CrosschainFlags(), - []chains.Chain{}, - sample.HeaderSupportedChains(), - true, ) + require.NoError(t, err) + // create AppContext return appContext, cfg.EVMChainConfigs[evmChain.ChainId] } @@ -84,7 +92,7 @@ func MockEVMObserver( tss interfaces.TSSSigner, lastBlock uint64, params observertypes.ChainParams, -) *observer.Observer { +) (*observer.Observer, *zctx.AppContext) { ctx := context.Background() // use default mock evm client if not provided @@ -105,18 +113,21 @@ func MockEVMObserver( tss = mocks.NewTSSMainnet() } // create AppContext - _, evmCfg := getAppContext(chain, "", ¶ms) + appContext, evmCfg := getAppContext(t, chain, "", ¶ms) database, err := db.NewFromSqliteInMemory(true) require.NoError(t, err) + testLogger := zerolog.New(zerolog.NewTestWriter(t)) + logger := base.Logger{Std: testLogger, Compliance: testLogger} + // create observer - ob, err := observer.NewObserver(ctx, evmCfg, evmClient, params, zetacoreClient, tss, database, base.Logger{}, nil) + ob, err := observer.NewObserver(ctx, evmCfg, evmClient, params, zetacoreClient, tss, database, logger, nil) require.NoError(t, err) ob.WithEvmJSONRPC(evmJSONRPC) ob.WithLastBlock(lastBlock) - return ob + return ob, appContext } func Test_NewObserver(t *testing.T) { @@ -242,7 +253,7 @@ func Test_LoadLastBlockScanned(t *testing.T) { // create observer using mock evm client evmClient := mocks.NewMockEvmClient().WithBlockNumber(100) - ob := MockEVMObserver(t, chain, evmClient, nil, nil, nil, 1, params) + ob, _ := MockEVMObserver(t, chain, evmClient, nil, nil, nil, 1, params) t.Run("should load last block scanned", func(t *testing.T) { // create db and write 123 as last block scanned @@ -265,7 +276,7 @@ func Test_LoadLastBlockScanned(t *testing.T) { }) t.Run("should fail on RPC error", func(t *testing.T) { // create observer on separate path, as we need to reset last block scanned - obOther := MockEVMObserver(t, chain, evmClient, nil, nil, nil, 1, params) + obOther, _ := MockEVMObserver(t, chain, evmClient, nil, nil, nil, 1, params) // reset last block scanned to 0 so that it will be loaded from RPC obOther.WithLastBlockScanned(0) diff --git a/zetaclient/chains/evm/observer/outbound.go b/zetaclient/chains/evm/observer/outbound.go index ff256613a9..598b9b6a44 100644 --- a/zetaclient/chains/evm/observer/outbound.go +++ b/zetaclient/chains/evm/observer/outbound.go @@ -13,25 +13,33 @@ import ( ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/pkg/errors" "github.com/rs/zerolog" - "github.com/rs/zerolog/log" "github.com/zeta-chain/protocol-contracts/pkg/contracts/evm/erc20custody.sol" "github.com/zeta-chain/protocol-contracts/pkg/contracts/evm/zetaconnector.non-eth.sol" "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/pkg/coin" + crosschainkeeper "github.com/zeta-chain/zetacore/x/crosschain/keeper" crosschaintypes "github.com/zeta-chain/zetacore/x/crosschain/types" "github.com/zeta-chain/zetacore/zetaclient/chains/evm" "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" "github.com/zeta-chain/zetacore/zetaclient/compliance" zctx "github.com/zeta-chain/zetacore/zetaclient/context" + "github.com/zeta-chain/zetacore/zetaclient/logs" clienttypes "github.com/zeta-chain/zetacore/zetaclient/types" "github.com/zeta-chain/zetacore/zetaclient/zetacore" ) // WatchOutbound watches evm chain for outgoing txs status // TODO(revamp): move ticker function to ticker file -// TODO(revamp): move inner logic to a separate function func (ob *Observer) WatchOutbound(ctx context.Context) error { + // get app context + app, err := zctx.FromContext(ctx) + if err != nil { + return err + } + + // create outbound ticker + chainID := ob.Chain().ChainId ticker, err := clienttypes.NewDynamicTicker( fmt.Sprintf("EVM_WatchOutbound_%d", ob.Chain().ChainId), ob.GetChainParams().OutboundTicker, @@ -41,54 +49,27 @@ func (ob *Observer) WatchOutbound(ctx context.Context) error { return err } - app, err := zctx.FromContext(ctx) - if err != nil { - return err - } - ob.Logger().Outbound.Info().Msgf("WatchOutbound started for chain %d", ob.Chain().ChainId) sampledLogger := ob.Logger().Outbound.Sample(&zerolog.BasicSampler{N: 10}) defer ticker.Stop() for { select { case <-ticker.C(): - if !app.IsOutboundObservationEnabled(ob.GetChainParams()) { + if !app.IsOutboundObservationEnabled() { sampledLogger.Info(). Msgf("WatchOutbound: outbound observation is disabled for chain %d", ob.Chain().ChainId) continue } - trackers, err := ob.ZetacoreClient(). - GetAllOutboundTrackerByChain(ctx, ob.Chain().ChainId, interfaces.Ascending) + + // process outbound trackers + err := ob.ProcessOutboundTrackers(ctx) if err != nil { - continue - } - for _, tracker := range trackers { - nonceInt := tracker.Nonce - if ob.IsTxConfirmed(nonceInt) { // Go to next tracker if this one already has a confirmed tx - continue - } - txCount := 0 - var outboundReceipt *ethtypes.Receipt - var outbound *ethtypes.Transaction - for _, txHash := range tracker.HashList { - if receipt, tx, ok := ob.checkConfirmedTx(ctx, txHash.TxHash, nonceInt); ok { - txCount++ - outboundReceipt = receipt - outbound = tx - ob.Logger().Outbound.Info(). - Msgf("WatchOutbound: confirmed outbound %s for chain %d nonce %d", txHash.TxHash, ob.Chain().ChainId, nonceInt) - if txCount > 1 { - ob.Logger().Outbound.Error().Msgf( - "WatchOutbound: checkConfirmedTx passed, txCount %d chain %d nonce %d receipt %v transaction %v", txCount, ob.Chain().ChainId, nonceInt, outboundReceipt, outbound) - } - } - } - if txCount == 1 { // should be only one txHash confirmed for each nonce. - ob.SetTxNReceipt(nonceInt, outboundReceipt, outbound) - } else if txCount > 1 { // should not happen. We can't tell which txHash is true. It might happen (e.g. glitchy/hacked endpoint) - ob.Logger().Outbound.Error().Msgf("WatchOutbound: confirmed multiple (%d) outbound for chain %d nonce %d", txCount, ob.Chain().ChainId, nonceInt) - } + ob.Logger(). + Outbound.Error(). + Err(err). + Msgf("WatchOutbound: error ProcessOutboundTrackers for chain %d", chainID) } + ticker.UpdateInterval(ob.GetChainParams().OutboundTicker, ob.Logger().Outbound) case <-ob.StopChannel(): ob.Logger().Outbound.Info().Msg("WatchOutbound: stopped") @@ -97,6 +78,61 @@ func (ob *Observer) WatchOutbound(ctx context.Context) error { } } +// ProcessOutboundTrackers processes outbound trackers +func (ob *Observer) ProcessOutboundTrackers(ctx context.Context) error { + chainID := ob.Chain().ChainId + trackers, err := ob.ZetacoreClient().GetAllOutboundTrackerByChain(ctx, ob.Chain().ChainId, interfaces.Ascending) + if err != nil { + return errors.Wrap(err, "GetAllOutboundTrackerByChain error") + } + + // prepare logger fields + logger := ob.Logger().Outbound.With(). + Str(logs.FieldMethod, "ProcessOutboundTrackers"). + Int64(logs.FieldChain, chainID). + Logger() + + // process outbound trackers + for _, tracker := range trackers { + // go to next tracker if this one already has a confirmed tx + nonce := tracker.Nonce + if ob.IsTxConfirmed(nonce) { + continue + } + + // check each txHash and save tx and receipt if it's legit and confirmed + txCount := 0 + var outboundReceipt *ethtypes.Receipt + var outbound *ethtypes.Transaction + for _, txHash := range tracker.HashList { + if receipt, tx, ok := ob.checkConfirmedTx(ctx, txHash.TxHash, nonce); ok { + txCount++ + outboundReceipt = receipt + outbound = tx + logger.Info().Msgf("confirmed outbound %s for chain %d nonce %d", txHash.TxHash, chainID, nonce) + if txCount > 1 { + logger.Error(). + Msgf("checkConfirmedTx passed, txCount %d chain %d nonce %d receipt %v tx %v", txCount, chainID, nonce, receipt, tx) + } + } + } + + // should be only one txHash confirmed for each nonce. + if txCount == 1 { + ob.SetTxNReceipt(nonce, outboundReceipt, outbound) + } else if txCount > 1 { + // should not happen. We can't tell which txHash is true. It might happen (e.g. bug, glitchy/hacked endpoint) + ob.Logger().Outbound.Error().Msgf("WatchOutbound: confirmed multiple (%d) outbound for chain %d nonce %d", txCount, chainID, nonce) + } else { + if len(tracker.HashList) == crosschainkeeper.MaxOutboundTrackerHashes { + ob.Logger().Outbound.Error().Msgf("WatchOutbound: outbound tracker is full of hashes for chain %d nonce %d", chainID, nonce) + } + } + } + + return nil +} + // PostVoteOutbound posts vote to zetacore for the confirmed outbound func (ob *Observer) PostVoteOutbound( ctx context.Context, @@ -377,23 +413,27 @@ func (ob *Observer) checkConfirmedTx( ctx, cancel := context.WithTimeout(ctx, 3*time.Second) defer cancel() + // prepare logger + logger := ob.Logger().Outbound.With(). + Str(logs.FieldMethod, "checkConfirmedTx"). + Int64(logs.FieldChain, ob.Chain().ChainId). + Uint64(logs.FieldNonce, nonce). + Str(logs.FieldTx, txHash). + Logger() + // query transaction transaction, isPending, err := ob.evmClient.TransactionByHash(ctx, ethcommon.HexToHash(txHash)) if err != nil { - log.Error(). - Err(err). - Str("function", "confirmTxByHash"). - Str("outboundTxHash", txHash). - Int64("chainID", ob.Chain().ChainId). - Msg("error getting transaction for outbound") + logger.Error().Err(err).Msg("TransactionByHash error") return nil, nil, false } if transaction == nil { // should not happen - log.Error(). - Str("function", "confirmTxByHash"). - Str("outboundTxHash", txHash). - Uint64("nonce", nonce). - Msg("transaction is nil for txHash") + logger.Error().Msg("transaction is nil") + return nil, nil, false + } + if isPending { + // should not happen when we are here. The outbound tracker reporter won't report a pending tx. + logger.Error().Msg("transaction is pending") return nil, nil, false } @@ -401,12 +441,7 @@ func (ob *Observer) checkConfirmedTx( signer := ethtypes.NewLondonSigner(big.NewInt(ob.Chain().ChainId)) from, err := signer.Sender(transaction) if err != nil { - log.Error(). - Err(err). - Str("function", "confirmTxByHash"). - Str("outboundTxHash", transaction.Hash().Hex()). - Int64("chainID", ob.Chain().ChainId). - Msg("local recovery of sender address failed for outbound") + logger.Error().Err(err).Msg("local recovery of sender address failed") return nil, nil, false } if from != ob.TSS().EVMAddress() { // must be TSS address @@ -416,13 +451,8 @@ func (ob *Observer) checkConfirmedTx( // TODO : improve this logic to verify that the correct TSS address is the from address. // https://github.com/zeta-chain/node/issues/2487 - log.Info(). - Str("function", "confirmTxByHash"). - Str("sender", from.Hex()). - Str("outboundTxHash", transaction.Hash().Hex()). - Int64("chainID", ob.Chain().ChainId). - Str("currentTSSAddress", ob.TSS().EVMAddress().Hex()). - Msg("sender is not current TSS address") + logger.Warn(). + Msgf("tx sender %s is not matching current TSS address %s", from.String(), ob.TSS().EVMAddress().String()) addressList := ob.TSS().EVMAddressList() isOldTssAddress := false for _, addr := range addressList { @@ -431,70 +461,35 @@ func (ob *Observer) checkConfirmedTx( } } if !isOldTssAddress { - log.Error(). - Str("function", "confirmTxByHash"). - Str("sender", from.Hex()). - Str("outboundTxHash", transaction.Hash().Hex()). - Int64("chainID", ob.Chain().ChainId). - Str("currentTSSAddress", ob.TSS().EVMAddress().Hex()). - Msg("sender is not current or old TSS address") + logger.Error().Msgf("tx sender %s is not matching any of the TSS addresses", from.String()) return nil, nil, false } } - if transaction.Nonce() != nonce { // must match cctx nonce - log.Error(). - Str("function", "confirmTxByHash"). - Str("outboundTxHash", txHash). - Uint64("wantedNonce", nonce). - Uint64("gotTxNonce", transaction.Nonce()). - Msg("outbound nonce mismatch") - return nil, nil, false - } - - // save pending transaction - if isPending { - ob.SetPendingTx(nonce, transaction) + if transaction.Nonce() != nonce { // must match tracker nonce + logger.Error().Msgf("tx nonce %d is not matching tracker nonce", nonce) return nil, nil, false } // query receipt receipt, err := ob.evmClient.TransactionReceipt(ctx, ethcommon.HexToHash(txHash)) if err != nil { - log.Error(). - Err(err). - Str("function", "confirmTxByHash"). - Str("outboundTxHash", txHash). - Uint64("nonce", nonce). - Msg("transactionReceipt error") + logger.Error().Err(err).Msg("TransactionReceipt error") return nil, nil, false } if receipt == nil { // should not happen - log.Error(). - Str("function", "confirmTxByHash"). - Str("outboundTxHash", txHash). - Uint64("nonce", nonce). - Msg("receipt is nil") + logger.Error().Msg("receipt is nil") return nil, nil, false } ob.LastBlock() // check confirmations lastHeight, err := ob.evmClient.BlockNumber(ctx) if err != nil { - log.Error(). - Str("function", "confirmTxByHash"). - Err(err). - Int64("chainID", ob.GetChainParams().ChainId). - Msg("error getting block number for chain") + logger.Error().Err(err).Msg("BlockNumber error") return nil, nil, false } if !ob.HasEnoughConfirmations(receipt, lastHeight) { - log.Debug(). - Str("function", "confirmTxByHash"). - Str("txHash", txHash). - Uint64("nonce", nonce). - Uint64("receiptBlock", receipt.BlockNumber.Uint64()). - Uint64("currentBlock", lastHeight). - Msg("txHash included but not confirmed") + logger.Debug(). + Msgf("tx included but not confirmed, receipt block %d current block %d", receipt.BlockNumber.Uint64(), lastHeight) return nil, nil, false } @@ -502,13 +497,7 @@ func (ob *Observer) checkConfirmedTx( // Note: a guard for false BlockNumber in receipt. The blob-carrying tx won't come here err = ob.CheckTxInclusion(transaction, receipt) if err != nil { - log.Error(). - Err(err). - Str("function", "confirmTxByHash"). - Str("errorContext", "checkTxInclusion"). - Str("txHash", txHash). - Uint64("nonce", nonce). - Msg("checkTxInclusion error") + logger.Error().Err(err).Msg("CheckTxInclusion error") return nil, nil, false } diff --git a/zetaclient/chains/evm/observer/outbound_test.go b/zetaclient/chains/evm/observer/outbound_test.go index a2aae00433..4081f9a76c 100644 --- a/zetaclient/chains/evm/observer/outbound_test.go +++ b/zetaclient/chains/evm/observer/outbound_test.go @@ -12,7 +12,6 @@ import ( "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/pkg/coin" "github.com/zeta-chain/zetacore/testutil/sample" - observertypes "github.com/zeta-chain/zetacore/x/observer/types" "github.com/zeta-chain/zetacore/zetaclient/chains/evm/observer" "github.com/zeta-chain/zetacore/zetaclient/config" "github.com/zeta-chain/zetacore/zetaclient/testutils" @@ -60,7 +59,7 @@ func Test_IsOutboundProcessed(t *testing.T) { t.Run("should post vote and return true if outbound is processed", func(t *testing.T) { // create evm observer and set outbound and receipt - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, chainParam) + ob, _ := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, chainParam) ob.SetTxNReceipt(nonce, receipt, outbound) // post outbound vote @@ -76,7 +75,7 @@ func Test_IsOutboundProcessed(t *testing.T) { cctx.InboundParams.Sender = sample.EthAddress().Hex() // create evm observer and set outbound and receipt - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, chainParam) + ob, _ := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, chainParam) ob.SetTxNReceipt(nonce, receipt, outbound) // modify compliance config to restrict sender address @@ -93,14 +92,14 @@ func Test_IsOutboundProcessed(t *testing.T) { }) t.Run("should return false if outbound is not confirmed", func(t *testing.T) { // create evm observer and DO NOT set outbound as confirmed - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, chainParam) + ob, _ := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, chainParam) continueKeysign, err := ob.VoteOutboundIfConfirmed(ctx, cctx) require.NoError(t, err) require.True(t, continueKeysign) }) t.Run("should fail if unable to parse ZetaReceived event", func(t *testing.T) { // create evm observer and set outbound and receipt - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, chainParam) + ob, _ := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, chainParam) ob.SetTxNReceipt(nonce, receipt, outbound) // set connector contract address to an arbitrary address to make event parsing fail @@ -149,7 +148,7 @@ func Test_IsOutboundProcessed_ContractError(t *testing.T) { t.Run("should fail if unable to get connector/custody contract", func(t *testing.T) { // create evm observer and set outbound and receipt - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, chainParam) + ob, _ := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, chainParam) ob.SetTxNReceipt(nonce, receipt, outbound) abiConnector := zetaconnector.ZetaConnectorNonEthMetaData.ABI abiCustody := erc20custody.ERC20CustodyMetaData.ABI @@ -193,7 +192,7 @@ func Test_PostVoteOutbound(t *testing.T) { receiveStatus := chains.ReceiveStatus_success // create evm client using mock zetacore client and post outbound vote - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, observertypes.ChainParams{}) + ob, _ := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, mocks.MockChainParams(chain.ChainId, 100)) ob.PostVoteOutbound( ctx, cctx.Index, diff --git a/zetaclient/chains/evm/rpc/rpc.go b/zetaclient/chains/evm/rpc/rpc.go new file mode 100644 index 0000000000..6fcc3d007c --- /dev/null +++ b/zetaclient/chains/evm/rpc/rpc.go @@ -0,0 +1,52 @@ +package rpc + +import ( + "context" + + ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/pkg/errors" + + "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" +) + +// IsTxConfirmed checks if the transaction is confirmed with given confirmations +func IsTxConfirmed( + ctx context.Context, + client interfaces.EVMRPCClient, + txHash string, + confirmations uint64, +) (bool, error) { + // query the tx + _, isPending, err := client.TransactionByHash(ctx, ethcommon.HexToHash(txHash)) + if err != nil { + return false, errors.Wrapf(err, "error getting transaction for tx %s", txHash) + } + if isPending { + return false, nil + } + + // query receipt + receipt, err := client.TransactionReceipt(ctx, ethcommon.HexToHash(txHash)) + if err != nil { + return false, errors.Wrapf(err, "error getting transaction receipt for tx %s", txHash) + } + + // should not happen + if receipt == nil { + return false, errors.Errorf("receipt is nil for tx %s", txHash) + } + + // query last block height + lastHeight, err := client.BlockNumber(ctx) + if err != nil { + return false, errors.Wrap(err, "error getting block number") + } + + // check confirmations + if lastHeight < receipt.BlockNumber.Uint64() { + return false, nil + } + blocks := lastHeight - receipt.BlockNumber.Uint64() + 1 + + return blocks >= confirmations, nil +} diff --git a/zetaclient/chains/evm/rpc/rpc_live_test.go b/zetaclient/chains/evm/rpc/rpc_live_test.go new file mode 100644 index 0000000000..0c420c830e --- /dev/null +++ b/zetaclient/chains/evm/rpc/rpc_live_test.go @@ -0,0 +1,45 @@ +package rpc_test + +import ( + "context" + "math" + + "github.com/ethereum/go-ethereum/ethclient" + "github.com/stretchr/testify/require" + "github.com/zeta-chain/zetacore/zetaclient/chains/evm/rpc" + + "testing" +) + +const ( + URLEthMainnet = "https://rpc.ankr.com/eth" + URLEthSepolia = "https://rpc.ankr.com/eth_sepolia" + URLBscMainnet = "https://rpc.ankr.com/bsc" + URLPolygonMainnet = "https://rpc.ankr.com/polygon" +) + +// Test_EVMRPCLive is a phony test to run each live test individually +func Test_EVMRPCLive(t *testing.T) { + // LiveTest_IsTxConfirmed(t) +} + +func LiveTest_IsTxConfirmed(t *testing.T) { + client, err := ethclient.Dial(URLEthMainnet) + require.NoError(t, err) + + // check if the transaction is confirmed + ctx := context.Background() + txHash := "0xd2eba7ac3da1b62800165414ea4bcaf69a3b0fb9b13a0fc32f4be11bfef79146" + + t.Run("should confirm tx", func(t *testing.T) { + confirmed, err := rpc.IsTxConfirmed(ctx, client, txHash, 12) + require.NoError(t, err) + require.True(t, confirmed) + }) + + t.Run("should not confirm tx if confirmations is not enough", func(t *testing.T) { + confirmed, err := rpc.IsTxConfirmed(ctx, client, txHash, math.MaxUint64) + require.NoError(t, err) + require.False(t, confirmed) + }) +} diff --git a/zetaclient/chains/evm/signer/gas.go b/zetaclient/chains/evm/signer/gas.go new file mode 100644 index 0000000000..575f004eb7 --- /dev/null +++ b/zetaclient/chains/evm/signer/gas.go @@ -0,0 +1,120 @@ +package signer + +import ( + "fmt" + "math/big" + + "github.com/pkg/errors" + "github.com/rs/zerolog" + + "github.com/zeta-chain/zetacore/x/crosschain/types" +) + +const ( + minGasLimit = 100_000 + maxGasLimit = 1_000_000 +) + +// Gas represents gas parameters for EVM transactions. +// +// This is pretty interesting because all EVM chains now support EIP-1559, but some chains do it in a specific way +// https://eips.ethereum.org/EIPS/eip-1559 +// https://www.blocknative.com/blog/eip-1559-fees +// https://github.com/bnb-chain/BEPs/blob/master/BEPs/BEP226.md (tl;dr: baseFee is always zero) +// +// However, this doesn't affect tx creation nor broadcasting +type Gas struct { + Limit uint64 + + // This is a "total" gasPrice per 1 unit of gas. + // GasPrice for pre EIP-1559 transactions or maxFeePerGas for EIP-1559. + Price *big.Int + + // PriorityFee a fee paid directly to validators for EIP-1559. + PriorityFee *big.Int +} + +func (g Gas) validate() error { + switch { + case g.Limit == 0: + return errors.New("gas limit is zero") + case g.Price == nil: + return errors.New("max fee per unit is nil") + case g.PriorityFee == nil: + return errors.New("priority fee per unit is nil") + case g.Price.Cmp(g.PriorityFee) == -1: + return fmt.Errorf( + "max fee per unit (%d) is less than priority fee per unit (%d)", + g.Price.Int64(), + g.PriorityFee.Int64(), + ) + default: + return nil + } +} + +// isLegacy determines whether the gas is meant for LegacyTx{} (pre EIP-1559) +// or DynamicFeeTx{} (post EIP-1559). +// +// Returns true if priority fee is <= 0. +func (g Gas) isLegacy() bool { + return g.PriorityFee.Sign() < 1 +} + +func gasFromCCTX(cctx *types.CrossChainTx, logger zerolog.Logger) (Gas, error) { + var ( + params = cctx.GetCurrentOutboundParam() + limit = params.GasLimit + ) + + switch { + case limit < minGasLimit: + limit = minGasLimit + logger.Warn(). + Uint64("cctx.initial_gas_limit", params.GasLimit). + Uint64("cctx.gas_limit", limit). + Msgf("Gas limit is too low. Setting to the minimum (%d)", minGasLimit) + case limit > maxGasLimit: + limit = maxGasLimit + logger.Warn(). + Uint64("cctx.initial_gas_limit", params.GasLimit). + Uint64("cctx.gas_limit", limit). + Msgf("Gas limit is too high; Setting to the maximum (%d)", maxGasLimit) + } + + gasPrice, err := bigIntFromString(params.GasPrice) + if err != nil { + return Gas{}, errors.Wrap(err, "unable to parse gasPrice") + } + + priorityFee, err := bigIntFromString(params.GasPriorityFee) + switch { + case err != nil: + return Gas{}, errors.Wrap(err, "unable to parse priorityFee") + case gasPrice.Cmp(priorityFee) == -1: + return Gas{}, fmt.Errorf("gasPrice (%d) is less than priorityFee (%d)", gasPrice.Int64(), priorityFee.Int64()) + } + + return Gas{ + Limit: limit, + Price: gasPrice, + PriorityFee: priorityFee, + }, nil +} + +func bigIntFromString(s string) (*big.Int, error) { + if s == "" || s == "0" { + return big.NewInt(0), nil + } + + v, ok := new(big.Int).SetString(s, 10) + if !ok { + return nil, fmt.Errorf("unable to parse %q as big.Int", s) + } + + if v.Sign() == -1 { + return nil, fmt.Errorf("big.Int is negative: %d", v.Int64()) + } + + return v, nil +} diff --git a/zetaclient/chains/evm/signer/gas_test.go b/zetaclient/chains/evm/signer/gas_test.go new file mode 100644 index 0000000000..d68d8c26af --- /dev/null +++ b/zetaclient/chains/evm/signer/gas_test.go @@ -0,0 +1,144 @@ +package signer + +import ( + "math/big" + "testing" + + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "github.com/zeta-chain/zetacore/x/crosschain/types" +) + +func TestGasFromCCTX(t *testing.T) { + logger := zerolog.New(zerolog.NewTestWriter(t)) + + makeCCTX := func(gasLimit uint64, price, priorityFee string) *types.CrossChainTx { + cctx := getCCTX(t) + cctx.GetOutboundParams()[0].GasLimit = gasLimit + cctx.GetOutboundParams()[0].GasPrice = price + cctx.GetOutboundParams()[0].GasPriorityFee = priorityFee + + return cctx + } + + for _, tt := range []struct { + name string + cctx *types.CrossChainTx + errorContains string + assert func(t *testing.T, g Gas) + }{ + { + name: "legacy: gas is too low", + cctx: makeCCTX(minGasLimit-200, gwei(2).String(), ""), + assert: func(t *testing.T, g Gas) { + assert.True(t, g.isLegacy()) + assertGasEquals(t, Gas{ + Limit: minGasLimit, + PriorityFee: gwei(0), + Price: gwei(2), + }, g) + }, + }, + { + name: "london: gas is too low", + cctx: makeCCTX(minGasLimit-200, gwei(2).String(), gwei(1).String()), + assert: func(t *testing.T, g Gas) { + assert.False(t, g.isLegacy()) + assertGasEquals(t, Gas{ + Limit: minGasLimit, + Price: gwei(2), + PriorityFee: gwei(1), + }, g) + }, + }, + { + name: "pre London gas logic", + cctx: makeCCTX(minGasLimit+100, gwei(3).String(), ""), + assert: func(t *testing.T, g Gas) { + assert.True(t, g.isLegacy()) + assertGasEquals(t, Gas{ + Limit: 100_100, + Price: gwei(3), + PriorityFee: gwei(0), + }, g) + }, + }, + { + name: "post London gas logic", + cctx: makeCCTX(minGasLimit+200, gwei(4).String(), gwei(1).String()), + assert: func(t *testing.T, g Gas) { + assert.False(t, g.isLegacy()) + assertGasEquals(t, Gas{ + Limit: 100_200, + Price: gwei(4), + PriorityFee: gwei(1), + }, g) + }, + }, + { + name: "gas is too high, force to the ceiling", + cctx: makeCCTX(maxGasLimit+200, gwei(4).String(), gwei(1).String()), + assert: func(t *testing.T, g Gas) { + assert.False(t, g.isLegacy()) + assertGasEquals(t, Gas{ + Limit: maxGasLimit, + Price: gwei(4), + PriorityFee: gwei(1), + }, g) + }, + }, + { + name: "priority fee is invalid", + cctx: makeCCTX(123_000, gwei(4).String(), "oopsie"), + errorContains: "unable to parse priorityFee", + }, + { + name: "priority fee is negative", + cctx: makeCCTX(123_000, gwei(4).String(), "-1"), + errorContains: "unable to parse priorityFee: big.Int is negative", + }, + { + name: "gasPrice is less than priorityFee", + cctx: makeCCTX(123_000, gwei(4).String(), gwei(5).String()), + errorContains: "gasPrice (4000000000) is less than priorityFee (5000000000)", + }, + { + name: "gasPrice is invalid", + cctx: makeCCTX(123_000, "hello", gwei(5).String()), + errorContains: "unable to parse gasPrice", + }, + } { + t.Run(tt.name, func(t *testing.T) { + g, err := gasFromCCTX(tt.cctx, logger) + if tt.errorContains != "" { + assert.ErrorContains(t, err, tt.errorContains) + return + } + + assert.NoError(t, err) + assert.NoError(t, g.validate()) + tt.assert(t, g) + }) + } + + t.Run("empty priority fee", func(t *testing.T) { + gas := Gas{ + Limit: 123_000, + Price: gwei(4), + PriorityFee: nil, + } + + assert.Error(t, gas.validate()) + }) +} + +func assertGasEquals(t *testing.T, expected, actual Gas) { + assert.Equal(t, int64(expected.Limit), int64(actual.Limit), "gas limit") + assert.Equal(t, expected.Price.Int64(), actual.Price.Int64(), "max fee per unit") + assert.Equal(t, expected.PriorityFee.Int64(), actual.PriorityFee.Int64(), "priority fee per unit") +} + +func gwei(i int64) *big.Int { + const g = 1_000_000_000 + return big.NewInt(i * g) +} diff --git a/zetaclient/chains/evm/signer/outbound_data.go b/zetaclient/chains/evm/signer/outbound_data.go index a3fd4becc9..a430adb3ba 100644 --- a/zetaclient/chains/evm/signer/outbound_data.go +++ b/zetaclient/chains/evm/signer/outbound_data.go @@ -4,40 +4,35 @@ import ( "context" "encoding/base64" "encoding/hex" - "errors" "fmt" "math/big" ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/pkg/errors" "github.com/rs/zerolog" - "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/pkg/coin" "github.com/zeta-chain/zetacore/x/crosschain/types" - "github.com/zeta-chain/zetacore/zetaclient/chains/evm/observer" - "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" zctx "github.com/zeta-chain/zetacore/zetaclient/context" ) -const ( - MinGasLimit = 100_000 - MaxGasLimit = 1_000_000 -) - // OutboundData is a data structure containing input fields used to construct each type of transaction. // This is populated using cctx and other input parameters passed to TryProcessOutbound type OutboundData struct { srcChainID *big.Int - toChainID *big.Int sender ethcommon.Address - to ethcommon.Address - asset ethcommon.Address - amount *big.Int - gasPrice *big.Int - gasLimit uint64 - message []byte - nonce uint64 - height uint64 + + toChainID *big.Int + to ethcommon.Address + + asset ethcommon.Address + amount *big.Int + + gas Gas + nonce uint64 + height uint64 + + message []byte // cctxIndex field is the inbound message digest that is sent to the destination contract cctxIndex [32]byte @@ -46,78 +41,13 @@ type OutboundData struct { outboundParams *types.OutboundParams } -// SetChainAndSender populates the destination address and Chain ID based on the status of the cross chain tx -// returns true if transaction should be skipped -// returns false otherwise -func (txData *OutboundData) SetChainAndSender(cctx *types.CrossChainTx, logger zerolog.Logger) bool { - switch cctx.CctxStatus.Status { - case types.CctxStatus_PendingRevert: - txData.to = ethcommon.HexToAddress(cctx.InboundParams.Sender) - txData.toChainID = big.NewInt(cctx.InboundParams.SenderChainId) - logger.Info().Msgf("Abort: reverting inbound") - case types.CctxStatus_PendingOutbound: - txData.to = ethcommon.HexToAddress(cctx.GetCurrentOutboundParam().Receiver) - txData.toChainID = big.NewInt(cctx.GetCurrentOutboundParam().ReceiverChainId) - default: - logger.Info().Msgf("Transaction doesn't need to be processed status: %d", cctx.CctxStatus.Status) - return true - } - return false -} - -// SetupGas sets the gas limit and price -func (txData *OutboundData) SetupGas( - cctx *types.CrossChainTx, - logger zerolog.Logger, - client interfaces.EVMRPCClient, - chain chains.Chain, -) error { - txData.gasLimit = cctx.GetCurrentOutboundParam().GasLimit - if txData.gasLimit < MinGasLimit { - txData.gasLimit = MinGasLimit - logger.Warn(). - Msgf("gasLimit %d is too low; set to %d", cctx.GetCurrentOutboundParam().GasLimit, txData.gasLimit) - } - if txData.gasLimit > MaxGasLimit { - txData.gasLimit = MaxGasLimit - logger.Warn(). - Msgf("gasLimit %d is too high; set to %d", cctx.GetCurrentOutboundParam().GasLimit, txData.gasLimit) - } - - // use dynamic gas price for ethereum chains. - // The code below is a fix for https://github.com/zeta-chain/node/issues/1085 - // doesn't close directly the issue because we should determine if we want to keep using SuggestGasPrice if no GasPrice - // we should possibly remove it completely and return an error if no GasPrice is provided because it means no fee is processed on ZetaChain - specified, ok := new(big.Int).SetString(cctx.GetCurrentOutboundParam().GasPrice, 10) - if !ok { - if chain.Network == chains.Network_eth { - suggested, err := client.SuggestGasPrice(context.Background()) - if err != nil { - return errors.Join(err, fmt.Errorf("cannot get gas price from chain %s ", chain.String())) - } - txData.gasPrice = roundUpToNearestGwei(suggested) - } else { - return fmt.Errorf("cannot convert gas price %s ", cctx.GetCurrentOutboundParam().GasPrice) - } - } else { - txData.gasPrice = specified - } - return nil -} - -// NewOutboundData populates transaction input fields parsed from the cctx and other parameters -// returns -// 1. New NewOutboundData Data struct or nil if an error occurred. -// 2. bool (skipTx) - if the transaction doesn't qualify to be processed the function will return true, meaning that this -// cctx will be skipped and false otherwise. -// 3. error +// NewOutboundData creates OutboundData from the given CCTX. +// returns `bool true` when transaction should be skipped. func NewOutboundData( ctx context.Context, cctx *types.CrossChainTx, - evmObserver *observer.Observer, - evmRPC interfaces.EVMRPCClient, - logger zerolog.Logger, height uint64, + logger zerolog.Logger, ) (*OutboundData, bool, error) { txData := OutboundData{} txData.outboundParams = cctx.GetCurrentOutboundParam() @@ -128,57 +58,125 @@ func NewOutboundData( txData.asset = ethcommon.HexToAddress(cctx.InboundParams.Asset) txData.height = height + if cctx == nil { + return nil, false, errors.New("cctx is nil") + } - skipTx := txData.SetChainAndSender(cctx, logger) - if skipTx { - return nil, true, nil + outboundParams := cctx.GetCurrentOutboundParam() + if err := validateParams(outboundParams); err != nil { + return nil, false, errors.Wrap(err, "invalid outboundParams") } app, err := zctx.FromContext(ctx) if err != nil { - return nil, false, err + return nil, false, errors.Wrap(err, "unable to get app from context") } - nonce := cctx.GetCurrentOutboundParam().TssNonce - toChain, found := chains.GetChainFromChainID(txData.toChainID.Int64(), app.GetAdditionalChains()) - if !found { - return nil, true, fmt.Errorf("unknown chain: %d", txData.toChainID.Int64()) + // recipient + destination chain + to, toChainID, skip := getDestination(cctx, logger) + if skip { + return nil, true, nil } - // Set up gas limit and gas price - err = txData.SetupGas(cctx, logger, evmRPC, toChain) - if err != nil { - return nil, true, err + // ensure that chain exists in app's context + if _, err := app.GetChain(toChainID.Int64()); err != nil { + return nil, false, errors.Wrapf(err, "unable to get chain %d from app context", toChainID.Int64()) } - // Get sendHash - logger.Info(). - Msgf("chain %s minting %d to %s, nonce %d, finalized zeta bn %d", toChain.String(), cctx.InboundParams.Amount, txData.to.Hex(), nonce, cctx.InboundParams.FinalizedZetaHeight) - cctxIndex, err := hex.DecodeString(cctx.Index[2:]) // remove the leading 0x - if err != nil || len(cctxIndex) != 32 { - return nil, true, fmt.Errorf("decode CCTX %s error", cctx.Index) + gas, err := gasFromCCTX(cctx, logger) + if err != nil { + return nil, false, errors.Wrap(err, "unable to make gas from CCTX") } - copy(txData.cctxIndex[:32], cctxIndex[:32]) - - // In case there is a pending transaction, make sure this keysign is a transaction replacement - pendingTx := evmObserver.GetPendingTx(nonce) - if pendingTx != nil { - if txData.gasPrice.Cmp(pendingTx.GasPrice()) > 0 { - logger.Info(). - Msgf("replace pending outbound %s nonce %d using gas price %d", pendingTx.Hash().Hex(), nonce, txData.gasPrice) - } else { - logger.Info().Msgf("please wait for pending outbound %s nonce %d to be included", pendingTx.Hash().Hex(), nonce) - return nil, true, nil - } + + cctxIndex, err := getCCTXIndex(cctx) + if err != nil { + return nil, false, errors.Wrap(err, "unable to get cctx index") } // Base64 decode message + var message []byte if cctx.InboundParams.CoinType != coin.CoinType_Cmd { - txData.message, err = base64.StdEncoding.DecodeString(cctx.RelayedMessage) - if err != nil { - logger.Err(err).Msgf("decode CCTX.Message %s error", cctx.RelayedMessage) + msg, errDecode := base64.StdEncoding.DecodeString(cctx.RelayedMessage) + if errDecode != nil { + logger.Err(err).Str("cctx.relayed_message", cctx.RelayedMessage).Msg("Unable to decode relayed message") + } else { + message = msg } } - return &txData, false, nil + return &OutboundData{ + srcChainID: big.NewInt(cctx.InboundParams.SenderChainId), + sender: ethcommon.HexToAddress(cctx.InboundParams.Sender), + + toChainID: toChainID, + to: to, + + asset: ethcommon.HexToAddress(cctx.InboundParams.Asset), + amount: outboundParams.Amount.BigInt(), + + gas: gas, + nonce: outboundParams.TssNonce, + height: height, + + message: message, + + cctxIndex: cctxIndex, + + outboundParams: outboundParams, + }, false, nil +} + +func getCCTXIndex(cctx *types.CrossChainTx) ([32]byte, error) { + // `0x` + `64 chars`. Two chars ranging `00...FF` represent one byte (64 chars = 32 bytes) + if len(cctx.Index) != (2 + 64) { + return [32]byte{}, fmt.Errorf("cctx index %q is invalid", cctx.Index) + } + + // remove the leading `0x` + cctxIndexSlice, err := hex.DecodeString(cctx.Index[2:]) + if err != nil || len(cctxIndexSlice) != 32 { + return [32]byte{}, errors.Wrapf(err, "unable to decode cctx index %s", cctx.Index) + } + + var cctxIndex [32]byte + copy(cctxIndex[:32], cctxIndexSlice[:32]) + + return cctxIndex, nil +} + +// getDestination picks the destination address and Chain ID based on the status of the cross chain tx. +// returns true if transaction should be skipped. +func getDestination(cctx *types.CrossChainTx, logger zerolog.Logger) (ethcommon.Address, *big.Int, bool) { + switch cctx.CctxStatus.Status { + case types.CctxStatus_PendingRevert: + to := ethcommon.HexToAddress(cctx.InboundParams.Sender) + chainID := big.NewInt(cctx.InboundParams.SenderChainId) + + logger.Info(). + Str("cctx.index", cctx.Index). + Int64("cctx.chain_id", chainID.Int64()). + Msgf("Abort: reverting inbound") + + return to, chainID, false + case types.CctxStatus_PendingOutbound: + to := ethcommon.HexToAddress(cctx.GetCurrentOutboundParam().Receiver) + chainID := big.NewInt(cctx.GetCurrentOutboundParam().ReceiverChainId) + + return to, chainID, false + } + + logger.Info(). + Str("cctx.index", cctx.Index). + Str("cctx.status", cctx.CctxStatus.String()). + Msgf("CCTX doesn't need to be processed") + + return ethcommon.Address{}, nil, true +} + +func validateParams(params *types.OutboundParams) error { + if params == nil || params.GasLimit == 0 { + return errors.New("outboundParams is empty") + } + + return nil } diff --git a/zetaclient/chains/evm/signer/outbound_data_test.go b/zetaclient/chains/evm/signer/outbound_data_test.go index ac2b7061b5..2f53bab028 100644 --- a/zetaclient/chains/evm/signer/outbound_data_test.go +++ b/zetaclient/chains/evm/signer/outbound_data_test.go @@ -1,113 +1,147 @@ package signer import ( - "context" "math/big" "testing" ethcommon "github.com/ethereum/go-ethereum/common" "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zeta-chain/zetacore/zetaclient/config" - zctx "github.com/zeta-chain/zetacore/zetaclient/context" - "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/x/crosschain/types" ) -func TestSigner_SetChainAndSender(t *testing.T) { - // setup inputs - cctx := getCCTX(t) - txData := &OutboundData{} - logger := zerolog.Logger{} +func TestNewOutboundData(t *testing.T) { + logger := zerolog.New(zerolog.NewTestWriter(t)) - t.Run("SetChainAndSender PendingRevert", func(t *testing.T) { - cctx.CctxStatus.Status = types.CctxStatus_PendingRevert - skipTx := txData.SetChainAndSender(cctx, logger) + ctx := makeCtx(t) - require.False(t, skipTx) - require.Equal(t, ethcommon.HexToAddress(cctx.InboundParams.Sender), txData.to) - require.Equal(t, big.NewInt(cctx.InboundParams.SenderChainId), txData.toChainID) - }) + newOutbound := func(cctx *types.CrossChainTx) (*OutboundData, bool, error) { + return NewOutboundData(ctx, cctx, 123, logger) + } - t.Run("SetChainAndSender PendingOutbound", func(t *testing.T) { - cctx.CctxStatus.Status = types.CctxStatus_PendingOutbound - skipTx := txData.SetChainAndSender(cctx, logger) + t.Run("success", func(t *testing.T) { + // ARRANGE + cctx := getCCTX(t) - require.False(t, skipTx) - require.Equal(t, ethcommon.HexToAddress(cctx.GetCurrentOutboundParam().Receiver), txData.to) - require.Equal(t, big.NewInt(cctx.GetCurrentOutboundParam().ReceiverChainId), txData.toChainID) - }) + // ACT + out, skip, err := newOutbound(cctx) - t.Run("SetChainAndSender Should skip cctx", func(t *testing.T) { - cctx.CctxStatus.Status = types.CctxStatus_PendingInbound - skipTx := txData.SetChainAndSender(cctx, logger) - require.True(t, skipTx) - }) -} + // ASSERT + require.NoError(t, err) + assert.False(t, skip) -func TestSigner_SetupGas(t *testing.T) { - cctx := getCCTX(t) - evmSigner, err := getNewEvmSigner(nil) - require.NoError(t, err) + assert.NotEmpty(t, out) - txData := &OutboundData{} - logger := zerolog.Logger{} + assert.NotEmpty(t, out.srcChainID) + assert.NotEmpty(t, out.sender) - t.Run("SetupGas_success", func(t *testing.T) { - chain := chains.BscMainnet - err := txData.SetupGas(cctx, logger, evmSigner.EvmClient(), chain) - require.NoError(t, err) + assert.NotEmpty(t, out.toChainID) + assert.NotEmpty(t, out.to) + + assert.Equal(t, ethcommon.HexToAddress(cctx.InboundParams.Asset), out.asset) + assert.NotEmpty(t, out.amount) + + assert.NotEmpty(t, out.nonce) + assert.NotEmpty(t, out.height) + assert.NotEmpty(t, out.gas) + assert.True(t, out.gas.isLegacy()) + assert.Equal(t, uint64(minGasLimit), out.gas.Limit) + + assert.Empty(t, out.message) + assert.NotEmpty(t, out.cctxIndex) + assert.Equal(t, cctx.OutboundParams[0], out.outboundParams) }) - t.Run("SetupGas_error", func(t *testing.T) { - cctx.GetCurrentOutboundParam().GasPrice = "invalidGasPrice" - chain := chains.BscMainnet - err := txData.SetupGas(cctx, logger, evmSigner.EvmClient(), chain) - require.ErrorContains(t, err, "cannot convert gas price") + t.Run("pending revert", func(t *testing.T) { + // ARRANGE + cctx := getCCTX(t) + cctx.CctxStatus.Status = types.CctxStatus_PendingRevert + + // ACT + out, skip, err := newOutbound(cctx) + + // ASSERT + require.NoError(t, err) + assert.False(t, skip) + assert.Equal(t, ethcommon.HexToAddress(cctx.InboundParams.Sender), out.to) + assert.Equal(t, big.NewInt(cctx.InboundParams.SenderChainId), out.toChainID) }) -} -func TestSigner_NewOutboundData(t *testing.T) { - app := zctx.New(config.New(false), zerolog.Nop()) - ctx := zctx.WithAppContext(context.Background(), app) + t.Run("pending outbound", func(t *testing.T) { + // ARRANGE + cctx := getCCTX(t) + cctx.CctxStatus.Status = types.CctxStatus_PendingOutbound - // Setup evm signer - evmSigner, err := getNewEvmSigner(nil) - require.NoError(t, err) + // ACT + out, skip, err := newOutbound(cctx) - mockObserver, err := getNewEvmChainObserver(t, nil) - require.NoError(t, err) + // ASSERT + assert.NoError(t, err) + assert.False(t, skip) + assert.Equal(t, ethcommon.HexToAddress(cctx.GetCurrentOutboundParam().Receiver), out.to) + assert.Equal(t, big.NewInt(cctx.GetCurrentOutboundParam().ReceiverChainId), out.toChainID) + }) - t.Run("NewOutboundData success", func(t *testing.T) { + t.Run("skip inbound", func(t *testing.T) { + // ARRANGE cctx := getCCTX(t) - _, skip, err := NewOutboundData(ctx, cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) - require.False(t, skip) + cctx.CctxStatus.Status = types.CctxStatus_PendingInbound + + // ACT + _, skip, err := newOutbound(cctx) + + // ASSERT require.NoError(t, err) + assert.True(t, skip) }) - t.Run("NewOutboundData skip", func(t *testing.T) { + t.Run("skip aborted", func(t *testing.T) { + // ARRANGE cctx := getCCTX(t) cctx.CctxStatus.Status = types.CctxStatus_Aborted - _, skip, err := NewOutboundData(ctx, cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) + + // ACT + _, skip, err := newOutbound(cctx) + + // ASSERT require.NoError(t, err) - require.True(t, skip) + assert.True(t, skip) + }) + + t.Run("invalid gas price", func(t *testing.T) { + // ARRANGE + cctx := getCCTX(t) + cctx.GetCurrentOutboundParam().GasPrice = "invalidGasPrice" + + // ACT + _, _, err := newOutbound(cctx) + + // ASSERT + assert.ErrorContains(t, err, "unable to parse gasPrice") }) - t.Run("NewOutboundData unknown chain", func(t *testing.T) { + t.Run("unknown chain", func(t *testing.T) { + // ARRANGE cctx := getInvalidCCTX(t) - require.NoError(t, err) - _, skip, err := NewOutboundData(ctx, cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) - require.ErrorContains(t, err, "unknown chain") - require.True(t, skip) + + // ACT + _, _, err := newOutbound(cctx) + + // ASSERT + assert.ErrorContains(t, err, "chain not found") }) - t.Run("NewOutboundData setup gas error", func(t *testing.T) { + t.Run("no outbound params", func(t *testing.T) { + // ARRANGE cctx := getCCTX(t) - require.NoError(t, err) - cctx.GetCurrentOutboundParam().GasPrice = "invalidGasPrice" - _, skip, err := NewOutboundData(ctx, cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) - require.True(t, skip) - require.ErrorContains(t, err, "cannot convert gas price") + cctx.OutboundParams = nil + + // ACT + _, _, err := newOutbound(cctx) + + // ASSERT + assert.ErrorContains(t, err, "outboundParams is empty") }) } diff --git a/zetaclient/chains/evm/signer/outbound_tracker_reporter.go b/zetaclient/chains/evm/signer/outbound_tracker_reporter.go new file mode 100644 index 0000000000..7a9b6bcfa2 --- /dev/null +++ b/zetaclient/chains/evm/signer/outbound_tracker_reporter.go @@ -0,0 +1,85 @@ +// Package signer implements the ChainSigner interface for EVM chains +package signer + +import ( + "context" + "time" + + "github.com/rs/zerolog" + + "github.com/zeta-chain/zetacore/pkg/bg" + "github.com/zeta-chain/zetacore/zetaclient/chains/evm" + "github.com/zeta-chain/zetacore/zetaclient/chains/evm/rpc" + "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" + "github.com/zeta-chain/zetacore/zetaclient/logs" +) + +// reportToOutboundTracker reports outboundHash to tracker only when tx receipt is available +func (signer *Signer) reportToOutboundTracker( + ctx context.Context, + zetacoreClient interfaces.ZetacoreClient, + chainID int64, + nonce uint64, + outboundHash string, + logger zerolog.Logger, +) { + // prepare logger + logger = logger.With(). + Str(logs.FieldMethod, "reportToOutboundTracker"). + Int64(logs.FieldChain, chainID). + Uint64(logs.FieldNonce, nonce). + Str(logs.FieldTx, outboundHash). + Logger() + + // set being reported flag to avoid duplicate reporting + alreadySet := signer.SetBeingReportedFlag(outboundHash) + if alreadySet { + logger.Info().Msg("outbound is being reported to tracker") + return + } + + // launch a goroutine to monitor tx confirmation status + bg.Work(ctx, func(ctx context.Context) error { + defer func() { + signer.ClearBeingReportedFlag(outboundHash) + }() + + // try monitoring tx inclusion status for 20 minutes + tStart := time.Now() + for { + // take a rest between each check + time.Sleep(10 * time.Second) + + // give up (forget about the tx) after 20 minutes of monitoring, there are 2 reasons: + // 1. the gas stability pool should have kicked in and replaced the tx by then. + // 2. even if there is a chance that the tx is included later, most likely it's going to be a false tx hash (either replaced or dropped). + // 3. we prefer missed tx hash over potentially invalid txhash. + if time.Since(tStart) > evm.OutboundInclusionTimeout { + logger.Info().Msgf("timeout waiting outbound inclusion") + return nil + } + + // check tx confirmation status + confirmed, err := rpc.IsTxConfirmed(ctx, signer.client, outboundHash, evm.ReorgProtectBlockCount) + if err != nil { + logger.Err(err).Msg("unable to check confirmation status of outbound") + continue + } + if !confirmed { + continue + } + + // report outbound hash to tracker + zetaHash, err := zetacoreClient.AddOutboundTracker(ctx, chainID, nonce, outboundHash, nil, "", -1) + if err != nil { + logger.Err(err).Msg("error adding outbound to tracker") + } else if zetaHash != "" { + logger.Info().Msgf("added outbound to tracker; zeta txhash %s", zetaHash) + } else { + // exit goroutine until the tracker contains the hash (reported by either this or other signers) + logger.Info().Msg("outbound now exists in tracker") + return nil + } + } + }, bg.WithName("TrackerReporterEVM"), bg.WithLogger(logger)) +} diff --git a/zetaclient/chains/evm/signer/signer.go b/zetaclient/chains/evm/signer/signer.go index f0f00aa237..0a89b5a657 100644 --- a/zetaclient/chains/evm/signer/signer.go +++ b/zetaclient/chains/evm/signer/signer.go @@ -17,6 +17,7 @@ import ( ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethclient" + ethrpc "github.com/ethereum/go-ethereum/rpc" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/zeta-chain/protocol-contracts/pkg/contracts/evm/erc20custody.sol" @@ -24,14 +25,13 @@ import ( "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/pkg/coin" "github.com/zeta-chain/zetacore/pkg/constant" - crosschainkeeper "github.com/zeta-chain/zetacore/x/crosschain/keeper" "github.com/zeta-chain/zetacore/x/crosschain/types" "github.com/zeta-chain/zetacore/zetaclient/chains/base" "github.com/zeta-chain/zetacore/zetaclient/chains/evm" - "github.com/zeta-chain/zetacore/zetaclient/chains/evm/observer" "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" "github.com/zeta-chain/zetacore/zetaclient/compliance" zctx "github.com/zeta-chain/zetacore/zetaclient/context" + "github.com/zeta-chain/zetacore/zetaclient/logs" "github.com/zeta-chain/zetacore/zetaclient/metrics" "github.com/zeta-chain/zetacore/zetaclient/outboundprocessor" "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" @@ -168,16 +168,20 @@ func (signer *Signer) Sign( data []byte, to ethcommon.Address, amount *big.Int, - gasLimit uint64, - gasPrice *big.Int, + gas Gas, nonce uint64, height uint64, ) (*ethtypes.Transaction, []byte, []byte, error) { - log.Debug().Str("tss.pub_key", signer.TSS().EVMAddress().String()).Msg("Sign: TSS signer") + signer.Logger().Std.Debug(). + Str("tss_pub_key", signer.TSS().EVMAddress().String()). + Msg("Signing evm transaction") + + chainID := big.NewInt(signer.Chain().ChainId) + tx, err := newTx(chainID, data, to, amount, gas, nonce) + if err != nil { + return nil, nil, nil, err + } - // TODO: use EIP-1559 transaction type - // https://github.com/zeta-chain/node/issues/1952 - tx := ethtypes.NewTransaction(nonce, to, amount, gasLimit, gasPrice, data) hashBytes := signer.ethSigner.Hash(tx).Bytes() sig, err := signer.TSS().Sign(ctx, hashBytes, height, nonce, signer.Chain().ChainId, "") @@ -201,11 +205,46 @@ func (signer *Signer) Sign( return signedTX, sig[:], hashBytes[:], nil } -// Broadcast takes in signed tx, broadcast to external chain node -func (signer *Signer) Broadcast(tx *ethtypes.Transaction) error { - ctxt, cancel := context.WithTimeout(context.Background(), 1*time.Second) +func newTx( + chainID *big.Int, + data []byte, + to ethcommon.Address, + amount *big.Int, + gas Gas, + nonce uint64, +) (*ethtypes.Transaction, error) { + if err := gas.validate(); err != nil { + return nil, errors.Wrap(err, "invalid gas parameters") + } + + if gas.isLegacy() { + return ethtypes.NewTx(ðtypes.LegacyTx{ + To: &to, + Value: amount, + Data: data, + GasPrice: gas.Price, + Gas: gas.Limit, + Nonce: nonce, + }), nil + } + + return ethtypes.NewTx(ðtypes.DynamicFeeTx{ + ChainID: chainID, + To: &to, + Value: amount, + Data: data, + GasFeeCap: gas.Price, + GasTipCap: gas.PriorityFee, + Gas: gas.Limit, + Nonce: nonce, + }), nil +} + +func (signer *Signer) broadcast(ctx context.Context, tx *ethtypes.Transaction) error { + ctx, cancel := context.WithTimeout(ctx, 1*time.Second) defer cancel() - return signer.client.SendTransaction(ctxt, tx) + + return signer.client.SendTransaction(ctx, tx) } // SignOutbound @@ -239,10 +278,10 @@ func (signer *Signer) SignOutbound(ctx context.Context, txData *OutboundData) (* data, signer.zetaConnectorAddress, zeroValue, - txData.gasLimit, - txData.gasPrice, + txData.gas, txData.nonce, - txData.height) + txData.height, + ) if err != nil { return nil, fmt.Errorf("sign onReceive error: %w", err) } @@ -261,17 +300,15 @@ func (signer *Signer) SignOutbound(ctx context.Context, txData *OutboundData) (* // bytes32 internalSendHash // ) external override whenNotPaused onlyTssAddress func (signer *Signer) SignRevertTx(ctx context.Context, txData *OutboundData) (*ethtypes.Transaction, error) { - var data []byte - var err error - - data, err = signer.zetaConnectorABI.Pack("onRevert", + data, err := signer.zetaConnectorABI.Pack("onRevert", txData.sender, txData.srcChainID, txData.to.Bytes(), txData.toChainID, txData.amount, txData.message, - txData.cctxIndex) + txData.cctxIndex, + ) if err != nil { return nil, fmt.Errorf("onRevert pack error: %w", err) } @@ -281,10 +318,10 @@ func (signer *Signer) SignRevertTx(ctx context.Context, txData *OutboundData) (* data, signer.zetaConnectorAddress, zeroValue, - txData.gasLimit, - txData.gasPrice, + txData.gas, txData.nonce, - txData.height) + txData.height, + ) if err != nil { return nil, fmt.Errorf("sign onRevert error: %w", err) } @@ -294,13 +331,13 @@ func (signer *Signer) SignRevertTx(ctx context.Context, txData *OutboundData) (* // SignCancelTx signs a transaction from TSS address to itself with a zero amount in order to increment the nonce func (signer *Signer) SignCancelTx(ctx context.Context, txData *OutboundData) (*ethtypes.Transaction, error) { + txData.gas.Limit = evm.EthTransferGasLimit tx, _, _, err := signer.Sign( ctx, nil, signer.TSS().EVMAddress(), zeroValue, // zero out the amount to cancel the tx - evm.EthTransferGasLimit, - txData.gasPrice, + txData.gas, txData.nonce, txData.height, ) @@ -313,13 +350,13 @@ func (signer *Signer) SignCancelTx(ctx context.Context, txData *OutboundData) (* // SignWithdrawTx signs a withdrawal transaction sent from the TSS address to the destination func (signer *Signer) SignWithdrawTx(ctx context.Context, txData *OutboundData) (*ethtypes.Transaction, error) { + txData.gas.Limit = evm.EthTransferGasLimit tx, _, _, err := signer.Sign( ctx, nil, txData.to, txData.amount, - evm.EthTransferGasLimit, - txData.gasPrice, + txData.gas, txData.nonce, txData.height, ) @@ -358,47 +395,62 @@ func (signer *Signer) TryProcessOutbound( cctx *types.CrossChainTx, outboundProc *outboundprocessor.Processor, outboundID string, - chainObserver interfaces.ChainObserver, + _ interfaces.ChainObserver, zetacoreClient interfaces.ZetacoreClient, height uint64, ) { // end outbound process on panic defer func() { outboundProc.EndTryProcess(outboundID) - if err := recover(); err != nil { - signer.Logger().Std.Error().Msgf("EVM TryProcessOutbound: %s, caught panic error: %v", cctx.Index, err) + if r := recover(); r != nil { + signer.Logger().Std.Error().Msgf("TryProcessOutbound: %s, caught panic error: %v", cctx.Index, r) } }() - // prepare logger - params := cctx.GetCurrentOutboundParam() - logger := signer.Logger().Std.With(). - Str("method", "TryProcessOutbound"). - Int64("chain", signer.Chain().ChainId). - Uint64("nonce", params.TssNonce). - Str("cctx", cctx.Index). - Logger() - - myID := zetacoreClient.GetKeys().GetOperatorAddress() - logger.Info(). - Msgf("EVM TryProcessOutbound: %s, value %d to %s", cctx.Index, params.Amount.BigInt(), params.Receiver) - - evmObserver, ok := chainObserver.(*observer.Observer) - if !ok { - logger.Error().Msg("chain observer is not an EVM observer") + // prepare logger and a few local variables + var ( + params = cctx.GetCurrentOutboundParam() + myID = zetacoreClient.GetKeys().GetOperatorAddress() + logger = signer.Logger().Std.With(). + Str(logs.FieldMethod, "TryProcessOutbound"). + Int64(logs.FieldChain, signer.Chain().ChainId). + Uint64(logs.FieldNonce, params.TssNonce). + Str(logs.FieldCctx, cctx.Index). + Str("cctx.receiver", params.Receiver). + Str("cctx.amount", params.Amount.String()). + Logger() + ) + logger.Info().Msgf("TryProcessOutbound") + + // retrieve app context + app, err := zctx.FromContext(ctx) + if err != nil { + logger.Error().Err(err).Msg("error getting app context") return } // Setup Transaction input - txData, skipTx, err := NewOutboundData(ctx, cctx, evmObserver, signer.client, logger, height) + txData, skipTx, err := NewOutboundData(ctx, cctx, height, logger) if err != nil { logger.Err(err).Msg("error setting up transaction input fields") return } + if skipTx { return } + toChain, err := app.GetChain(txData.toChainID.Int64()) + switch { + case err != nil: + logger.Error().Err(err).Msgf("error getting toChain %d", txData.toChainID.Int64()) + return + case toChain.IsZeta(): + // should not happen + logger.Error().Msgf("unable to TryProcessOutbound when toChain is zetaChain (%d)", toChain.ID()) + return + } + // https://github.com/zeta-chain/node/issues/2050 var tx *ethtypes.Transaction // compliance check goes first @@ -447,27 +499,27 @@ func (signer *Signer) TryProcessOutbound( logger.Info().Msgf( "SignWithdrawTx: %d => %d, nonce %d, gasPrice %d", cctx.InboundParams.SenderChainId, - txData.toChainID.Int64(), + toChain.ID(), cctx.GetCurrentOutboundParam().TssNonce, - txData.gasPrice, + txData.gas.Price, ) tx, err = signer.SignWithdrawTx(ctx, txData) case coin.CoinType_ERC20: logger.Info().Msgf( "SignERC20WithdrawTx: %d => %d, nonce %d, gasPrice %d", cctx.InboundParams.SenderChainId, - txData.toChainID.Int64(), + toChain.ID(), cctx.GetCurrentOutboundParam().TssNonce, - txData.gasPrice, + txData.gas.Price, ) tx, err = signer.SignERC20WithdrawTx(ctx, txData) case coin.CoinType_Zeta: logger.Info().Msgf( "SignOutbound: %d => %d, nonce %d, gasPrice %d", cctx.InboundParams.SenderChainId, - txData.toChainID.Int64(), + toChain.ID(), cctx.GetCurrentOutboundParam().TssNonce, - txData.gasPrice, + txData.gas.Price, ) tx, err = signer.SignOutbound(ctx, txData) } @@ -481,8 +533,8 @@ func (signer *Signer) TryProcessOutbound( logger.Info().Msgf( "SignRevertTx: %d => %d, nonce %d, gasPrice %d", cctx.InboundParams.SenderChainId, - txData.toChainID.Int64(), cctx.GetCurrentOutboundParam().TssNonce, - txData.gasPrice, + toChain.ID(), cctx.GetCurrentOutboundParam().TssNonce, + txData.gas.Price, ) txData.srcChainID = big.NewInt(cctx.OutboundParams[0].ReceiverChainId) txData.toChainID = big.NewInt(cctx.GetCurrentOutboundParam().ReceiverChainId) @@ -491,17 +543,17 @@ func (signer *Signer) TryProcessOutbound( logger.Info().Msgf( "SignWithdrawTx: %d => %d, nonce %d, gasPrice %d", cctx.InboundParams.SenderChainId, - txData.toChainID.Int64(), + toChain.ID(), cctx.GetCurrentOutboundParam().TssNonce, - txData.gasPrice, + txData.gas.Price, ) tx, err = signer.SignWithdrawTx(ctx, txData) case coin.CoinType_ERC20: logger.Info().Msgf("SignERC20WithdrawTx: %d => %d, nonce %d, gasPrice %d", cctx.InboundParams.SenderChainId, - txData.toChainID.Int64(), + toChain.ID(), cctx.GetCurrentOutboundParam().TssNonce, - txData.gasPrice, + txData.gas.Price, ) tx, err = signer.SignERC20WithdrawTx(ctx, txData) } @@ -513,9 +565,9 @@ func (signer *Signer) TryProcessOutbound( logger.Info().Msgf( "SignRevertTx: %d => %d, nonce %d, gasPrice %d", cctx.InboundParams.SenderChainId, - txData.toChainID.Int64(), + toChain.ID(), cctx.GetCurrentOutboundParam().TssNonce, - txData.gasPrice, + txData.gas.Price, ) txData.srcChainID = big.NewInt(cctx.OutboundParams[0].ReceiverChainId) txData.toChainID = big.NewInt(cctx.GetCurrentOutboundParam().ReceiverChainId) @@ -529,9 +581,9 @@ func (signer *Signer) TryProcessOutbound( logger.Info().Msgf( "SignOutbound: %d => %d, nonce %d, gasPrice %d", cctx.InboundParams.SenderChainId, - txData.toChainID.Int64(), + toChain.ID(), cctx.GetCurrentOutboundParam().TssNonce, - txData.gasPrice, + txData.gas.Price, ) tx, err = signer.SignOutbound(ctx, txData) if err != nil { @@ -543,7 +595,7 @@ func (signer *Signer) TryProcessOutbound( logger.Info().Msgf( "Key-sign success: %d => %d, nonce %d", cctx.InboundParams.SenderChainId, - txData.toChainID.Int64(), + toChain.ID(), cctx.GetCurrentOutboundParam().TssNonce, ) @@ -567,14 +619,16 @@ func (signer *Signer) BroadcastOutbound( return } - // Get destination chain for logging - toChain, found := chains.GetChainFromChainID(txData.toChainID.Int64(), app.GetAdditionalChains()) - if !found { - logger.Warn().Msgf("BroadcastOutbound: unknown chain %d", txData.toChainID.Int64()) + toChain, err := app.GetChain(txData.toChainID.Int64()) + switch { + case err != nil: + logger.Error().Err(err).Msgf("error getting toChain %d", txData.toChainID.Int64()) return - } - - if tx == nil { + case toChain.IsZeta(): + // should not happen + logger.Error().Msgf("unable to broadcast when toChain is zetaChain (%d)", toChain.ID()) + return + case tx == nil: logger.Warn().Msgf("BroadcastOutbound: no tx to broadcast %s", cctx.Index) return } @@ -586,20 +640,20 @@ func (signer *Signer) BroadcastOutbound( backOff := broadcastBackoff for i := 0; i < broadcastRetries; i++ { time.Sleep(backOff) - err := signer.Broadcast(tx) + err := signer.broadcast(ctx, tx) if err != nil { log.Warn(). Err(err). Msgf("BroadcastOutbound: error broadcasting tx %s on chain %d nonce %d retry %d signer %s", - outboundHash, toChain.ChainId, cctx.GetCurrentOutboundParam().TssNonce, i, myID) + outboundHash, toChain.ID(), cctx.GetCurrentOutboundParam().TssNonce, i, myID) retry, report := zetacore.HandleBroadcastError( err, strconv.FormatUint(cctx.GetCurrentOutboundParam().TssNonce, 10), - toChain.String(), + fmt.Sprintf("%d", toChain.ID()), outboundHash, ) if report { - signer.reportToOutboundTracker(ctx, zetacoreClient, toChain.ChainId, tx.Nonce(), outboundHash, logger) + signer.reportToOutboundTracker(ctx, zetacoreClient, toChain.ID(), tx.Nonce(), outboundHash, logger) } if !retry { break @@ -608,8 +662,8 @@ func (signer *Signer) BroadcastOutbound( continue } logger.Info().Msgf("BroadcastOutbound: broadcasted tx %s on chain %d nonce %d signer %s", - outboundHash, toChain.ChainId, cctx.GetCurrentOutboundParam().TssNonce, myID) - signer.reportToOutboundTracker(ctx, zetacoreClient, toChain.ChainId, tx.Nonce(), outboundHash, logger) + outboundHash, toChain.ID(), cctx.GetCurrentOutboundParam().TssNonce, myID) + signer.reportToOutboundTracker(ctx, zetacoreClient, toChain.ID(), tx.Nonce(), outboundHash, logger) break // successful broadcast; no need to retry } } @@ -621,9 +675,7 @@ func (signer *Signer) BroadcastOutbound( // uint256 amount, // ) external onlyTssAddress func (signer *Signer) SignERC20WithdrawTx(ctx context.Context, txData *OutboundData) (*ethtypes.Transaction, error) { - var data []byte - var err error - data, err = signer.erc20CustodyABI.Pack("withdraw", txData.to, txData.asset, txData.amount) + data, err := signer.erc20CustodyABI.Pack("withdraw", txData.to, txData.asset, txData.amount) if err != nil { return nil, fmt.Errorf("withdraw pack error: %w", err) } @@ -633,8 +685,7 @@ func (signer *Signer) SignERC20WithdrawTx(ctx context.Context, txData *OutboundD data, signer.er20CustodyAddress, zeroValue, - txData.gasLimit, - txData.gasPrice, + txData.gas, txData.nonce, txData.height, ) @@ -687,27 +738,30 @@ func (signer *Signer) SignWhitelistERC20Cmd( if erc20 == (ethcommon.Address{}) { return nil, fmt.Errorf("SignCommandTx: invalid erc20 address %s", params) } + custodyAbi, err := erc20custody.ERC20CustodyMetaData.GetAbi() if err != nil { return nil, err } + data, err := custodyAbi.Pack("whitelist", erc20) if err != nil { return nil, fmt.Errorf("whitelist pack error: %w", err) } + tx, _, _, err := signer.Sign( ctx, data, txData.to, zeroValue, - txData.gasLimit, - txData.gasPrice, + txData.gas, outboundParams.TssNonce, txData.height, ) if err != nil { return nil, fmt.Errorf("sign whitelist error: %w", err) } + return tx, nil } @@ -719,8 +773,7 @@ func (signer *Signer) SignMigrateTssFundsCmd(ctx context.Context, txData *Outbou nil, txData.to, txData.amount, - txData.gasLimit, - txData.gasPrice, + txData.gas, txData.nonce, txData.height, ) @@ -730,115 +783,6 @@ func (signer *Signer) SignMigrateTssFundsCmd(ctx context.Context, txData *Outbou return tx, nil } -// reportToOutboundTracker reports outboundHash to tracker only when tx receipt is available -// TODO(revamp): move outbound tracker function to a outbound tracker file -func (signer *Signer) reportToOutboundTracker( - ctx context.Context, - zetacoreClient interfaces.ZetacoreClient, - chainID int64, - nonce uint64, - outboundHash string, - logger zerolog.Logger, -) { - // set being reported flag to avoid duplicate reporting - alreadySet := signer.Signer.SetBeingReportedFlag(outboundHash) - if alreadySet { - logger.Info(). - Msgf("reportToOutboundTracker: outboundHash %s for chain %d nonce %d is being reported", outboundHash, chainID, nonce) - return - } - - // report to outbound tracker with goroutine - go func() { - defer func() { - signer.Signer.ClearBeingReportedFlag(outboundHash) - }() - - // try monitoring tx inclusion status for 10 minutes - var err error - report := false - isPending := false - blockNumber := uint64(0) - tStart := time.Now() - for { - // give up after 10 minutes of monitoring - time.Sleep(10 * time.Second) - - if time.Since(tStart) > evm.OutboundInclusionTimeout { - // if tx is still pending after timeout, report to outboundTracker anyway as we cannot monitor forever - if isPending { - report = true // probably will be included later - } - logger.Info(). - Msgf("reportToOutboundTracker: timeout waiting tx inclusion for chain %d nonce %d outboundHash %s report %v", chainID, nonce, outboundHash, report) - break - } - // try getting the tx - _, isPending, err = signer.client.TransactionByHash(ctx, ethcommon.HexToHash(outboundHash)) - if err != nil { - logger.Info(). - Err(err). - Msgf("reportToOutboundTracker: error getting tx for chain %d nonce %d outboundHash %s", chainID, nonce, outboundHash) - continue - } - // if tx is include in a block, try getting receipt - if !isPending { - report = true // included - receipt, err := signer.client.TransactionReceipt(ctx, ethcommon.HexToHash(outboundHash)) - if err != nil { - logger.Info(). - Err(err). - Msgf("reportToOutboundTracker: error getting receipt for chain %d nonce %d outboundHash %s", chainID, nonce, outboundHash) - } - if receipt != nil { - blockNumber = receipt.BlockNumber.Uint64() - } - break - } - // keep monitoring pending tx - logger.Info(). - Msgf("reportToOutboundTracker: tx has not been included yet for chain %d nonce %d outboundHash %s", chainID, nonce, outboundHash) - } - - // try adding to outbound tracker for 10 minutes - if report { - tStart := time.Now() - for { - // give up after 10 minutes of retrying - if time.Since(tStart) > evm.OutboundTrackerReportTimeout { - logger.Info(). - Msgf("reportToOutboundTracker: timeout adding outbound tracker for chain %d nonce %d outboundHash %s, please add manually", chainID, nonce, outboundHash) - break - } - // stop if the cctx is already finalized - cctx, err := zetacoreClient.GetCctxByNonce(ctx, chainID, nonce) - if err != nil { - logger.Err(err). - Msgf("reportToOutboundTracker: error getting cctx for chain %d nonce %d outboundHash %s", chainID, nonce, outboundHash) - } else if !crosschainkeeper.IsPending(cctx) { - logger.Info().Msgf("reportToOutboundTracker: cctx already finalized for chain %d nonce %d outboundHash %s", chainID, nonce, outboundHash) - break - } - // report to outbound tracker - zetaHash, err := zetacoreClient.AddOutboundTracker(ctx, chainID, nonce, outboundHash, nil, "", -1) - if err != nil { - logger.Err(err). - Msgf("reportToOutboundTracker: error adding to outbound tracker for chain %d nonce %d outboundHash %s", chainID, nonce, outboundHash) - } else if zetaHash != "" { - logger.Info().Msgf("reportToOutboundTracker: added outboundHash to core successful %s, chain %d nonce %d outboundHash %s block %d", - zetaHash, chainID, nonce, outboundHash, blockNumber) - } else { - // stop if the tracker contains the outboundHash - logger.Info().Msgf("reportToOutboundTracker: outbound tracker contains outboundHash %s for chain %d nonce %d", outboundHash, chainID, nonce) - break - } - // retry otherwise - time.Sleep(evm.ZetaBlockTime * 3) - } - } - }() -} - // getEVMRPC is a helper function to set up the client and signer, also initializes a mock client for unit tests func getEVMRPC(ctx context.Context, endpoint string) (interfaces.EVMRPCClient, ethtypes.Signer, error) { if endpoint == mocks.EVMRPCEnabled { @@ -847,11 +791,16 @@ func getEVMRPC(ctx context.Context, endpoint string) (interfaces.EVMRPCClient, e client := &mocks.MockEvmClient{} return client, ethSigner, nil } + httpClient, err := metrics.GetInstrumentedHTTPClient(endpoint) + if err != nil { + return nil, nil, errors.Wrap(err, "unable to get instrumented HTTP client") + } - client, err := ethclient.Dial(endpoint) + rpcClient, err := ethrpc.DialHTTPWithClient(endpoint, httpClient) if err != nil { return nil, nil, errors.Wrapf(err, "unable to dial EVM client (endpoint %q)", endpoint) } + client := ethclient.NewClient(rpcClient) chainID, err := client.ChainID(ctx) if err != nil { @@ -862,14 +811,3 @@ func getEVMRPC(ctx context.Context, endpoint string) (interfaces.EVMRPCClient, e return client, ethSigner, nil } - -// roundUpToNearestGwei rounds up the gas price to the nearest Gwei -func roundUpToNearestGwei(gasPrice *big.Int) *big.Int { - oneGwei := big.NewInt(1_000_000_000) // 1 Gwei - mod := new(big.Int) - mod.Mod(gasPrice, oneGwei) - if mod.Cmp(big.NewInt(0)) == 0 { // gasprice is already a multiple of 1 Gwei - return gasPrice - } - return new(big.Int).Add(gasPrice, new(big.Int).Sub(oneGwei, mod)) -} diff --git a/zetaclient/chains/evm/signer/signer_test.go b/zetaclient/chains/evm/signer/signer_test.go index b0cf3e5504..495e7b4f63 100644 --- a/zetaclient/chains/evm/signer/signer_test.go +++ b/zetaclient/chains/evm/signer/signer_test.go @@ -10,7 +10,9 @@ import ( ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + observertypes "github.com/zeta-chain/zetacore/x/observer/types" zctx "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/zetaclient/db" "github.com/zeta-chain/zetacore/zetaclient/keys" @@ -161,7 +163,7 @@ func TestSigner_SetGetERC20CustodyAddress(t *testing.T) { } func TestSigner_TryProcessOutbound(t *testing.T) { - ctx := makeCtx() + ctx := makeCtx(t) evmSigner, err := getNewEvmSigner(nil) require.NoError(t, err) @@ -184,7 +186,7 @@ func TestSigner_TryProcessOutbound(t *testing.T) { } func TestSigner_SignOutbound(t *testing.T) { - ctx := makeCtx() + ctx := makeCtx(t) // Setup evm signer tss := mocks.NewTSSMainnet() @@ -194,13 +196,11 @@ func TestSigner_SignOutbound(t *testing.T) { // Setup txData struct cctx := getCCTX(t) - mockObserver, err := getNewEvmChainObserver(t, tss) - require.NoError(t, err) - txData, skip, err := NewOutboundData(ctx, cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) + txData, skip, err := NewOutboundData(ctx, cctx, 123, zerolog.Logger{}) require.False(t, skip) require.NoError(t, err) - t.Run("SignOutbound - should successfully sign", func(t *testing.T) { + t.Run("SignOutbound - should successfully sign LegacyTx", func(t *testing.T) { // Call SignOutbound tx, err := evmSigner.SignOutbound(ctx, txData) require.NoError(t, err) @@ -208,6 +208,9 @@ func TestSigner_SignOutbound(t *testing.T) { // Verify Signature tss := mocks.NewTSSMainnet() verifyTxSignature(t, tx, tss.Pubkey(), evmSigner.EvmSigner()) + + // check that by default tx type is legacy tx + assert.Equal(t, ethtypes.LegacyTxType, int(tx.Type())) }) t.Run("SignOutbound - should fail if keysign fails", func(t *testing.T) { // Pause tss to make keysign fail @@ -218,10 +221,46 @@ func TestSigner_SignOutbound(t *testing.T) { require.ErrorContains(t, err, "sign onReceive error") require.Nil(t, tx) }) + + t.Run("SignOutbound - should successfully sign DynamicFeeTx", func(t *testing.T) { + // ARRANGE + const ( + gwei = 1_000_000_000 + priorityFee = 1 * gwei + gasPrice = 3 * gwei + ) + + // Given a CCTX with gas price and priority fee + cctx := getCCTX(t) + cctx.OutboundParams[0].GasPrice = big.NewInt(gasPrice).String() + cctx.OutboundParams[0].GasPriorityFee = big.NewInt(priorityFee).String() + + // Given outbound data + txData, skip, err := NewOutboundData(ctx, cctx, 123, makeLogger(t)) + require.False(t, skip) + require.NoError(t, err) + + // Given a working TSS + tss.Unpause() + + // ACT + tx, err := evmSigner.SignOutbound(ctx, txData) + require.NoError(t, err) + + // ASSERT + verifyTxSignature(t, tx, mocks.NewTSSMainnet().Pubkey(), evmSigner.EvmSigner()) + + // check that by default tx type is a dynamic fee tx + assert.Equal(t, ethtypes.DynamicFeeTxType, int(tx.Type())) + + // check that the gasPrice & priorityFee are set correctly + assert.Equal(t, int64(gasPrice), tx.GasFeeCap().Int64()) + assert.Equal(t, int64(priorityFee), tx.GasTipCap().Int64()) + }) } func TestSigner_SignRevertTx(t *testing.T) { - ctx := makeCtx() + ctx := makeCtx(t) // Setup evm signer tss := mocks.NewTSSMainnet() @@ -230,9 +269,7 @@ func TestSigner_SignRevertTx(t *testing.T) { // Setup txData struct cctx := getCCTX(t) - mockObserver, err := getNewEvmChainObserver(t, tss) - require.NoError(t, err) - txData, skip, err := NewOutboundData(ctx, cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) + txData, skip, err := NewOutboundData(ctx, cctx, 123, zerolog.Logger{}) require.False(t, skip) require.NoError(t, err) @@ -261,7 +298,7 @@ func TestSigner_SignRevertTx(t *testing.T) { } func TestSigner_SignCancelTx(t *testing.T) { - ctx := makeCtx() + ctx := makeCtx(t) // Setup evm signer tss := mocks.NewTSSMainnet() @@ -270,9 +307,7 @@ func TestSigner_SignCancelTx(t *testing.T) { // Setup txData struct cctx := getCCTX(t) - mockObserver, err := getNewEvmChainObserver(t, tss) - require.NoError(t, err) - txData, skip, err := NewOutboundData(ctx, cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) + txData, skip, err := NewOutboundData(ctx, cctx, 123, zerolog.Logger{}) require.False(t, skip) require.NoError(t, err) @@ -301,7 +336,7 @@ func TestSigner_SignCancelTx(t *testing.T) { } func TestSigner_SignWithdrawTx(t *testing.T) { - ctx := makeCtx() + ctx := makeCtx(t) // Setup evm signer tss := mocks.NewTSSMainnet() @@ -310,9 +345,7 @@ func TestSigner_SignWithdrawTx(t *testing.T) { // Setup txData struct cctx := getCCTX(t) - mockObserver, err := getNewEvmChainObserver(t, tss) - require.NoError(t, err) - txData, skip, err := NewOutboundData(ctx, cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) + txData, skip, err := NewOutboundData(ctx, cctx, 123, zerolog.Logger{}) require.False(t, skip) require.NoError(t, err) @@ -340,7 +373,7 @@ func TestSigner_SignWithdrawTx(t *testing.T) { } func TestSigner_SignCommandTx(t *testing.T) { - ctx := makeCtx() + ctx := makeCtx(t) // Setup evm signer evmSigner, err := getNewEvmSigner(nil) @@ -348,9 +381,7 @@ func TestSigner_SignCommandTx(t *testing.T) { // Setup txData struct cctx := getCCTX(t) - mockObserver, err := getNewEvmChainObserver(t, nil) - require.NoError(t, err) - txData, skip, err := NewOutboundData(ctx, cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) + txData, skip, err := NewOutboundData(ctx, cctx, 123, zerolog.Logger{}) require.False(t, skip) require.NoError(t, err) @@ -386,7 +417,7 @@ func TestSigner_SignCommandTx(t *testing.T) { } func TestSigner_SignERC20WithdrawTx(t *testing.T) { - ctx := makeCtx() + ctx := makeCtx(t) // Setup evm signer tss := mocks.NewTSSMainnet() @@ -395,9 +426,7 @@ func TestSigner_SignERC20WithdrawTx(t *testing.T) { // Setup txData struct cctx := getCCTX(t) - mockObserver, err := getNewEvmChainObserver(t, tss) - require.NoError(t, err) - txData, skip, err := NewOutboundData(ctx, cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) + txData, skip, err := NewOutboundData(ctx, cctx, 123, zerolog.Logger{}) require.False(t, skip) require.NoError(t, err) @@ -427,7 +456,7 @@ func TestSigner_SignERC20WithdrawTx(t *testing.T) { } func TestSigner_BroadcastOutbound(t *testing.T) { - ctx := makeCtx() + ctx := makeCtx(t) // Setup evm signer evmSigner, err := getNewEvmSigner(nil) @@ -435,11 +464,9 @@ func TestSigner_BroadcastOutbound(t *testing.T) { // Setup txData struct cctx := getCCTX(t) - mockObserver, err := getNewEvmChainObserver(t, nil) + txData, skip, err := NewOutboundData(ctx, cctx, 123, zerolog.Logger{}) require.NoError(t, err) - txData, skip, err := NewOutboundData(ctx, cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.False(t, skip) - require.NoError(t, err) t.Run("BroadcastOutbound - should successfully broadcast", func(t *testing.T) { // Call SignERC20WithdrawTx @@ -481,7 +508,7 @@ func TestSigner_SignerErrorMsg(t *testing.T) { } func TestSigner_SignWhitelistERC20Cmd(t *testing.T) { - ctx := makeCtx() + ctx := makeCtx(t) // Setup evm signer tss := mocks.NewTSSMainnet() @@ -490,11 +517,9 @@ func TestSigner_SignWhitelistERC20Cmd(t *testing.T) { // Setup txData struct cctx := getCCTX(t) - mockObserver, err := getNewEvmChainObserver(t, tss) + txData, skip, err := NewOutboundData(ctx, cctx, 123, zerolog.Logger{}) require.NoError(t, err) - txData, skip, err := NewOutboundData(ctx, cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.False(t, skip) - require.NoError(t, err) t.Run("SignWhitelistERC20Cmd - should successfully sign", func(t *testing.T) { // Call SignWhitelistERC20Cmd @@ -526,7 +551,7 @@ func TestSigner_SignWhitelistERC20Cmd(t *testing.T) { } func TestSigner_SignMigrateTssFundsCmd(t *testing.T) { - ctx := makeCtx() + ctx := makeCtx(t) // Setup evm signer tss := mocks.NewTSSMainnet() @@ -535,9 +560,7 @@ func TestSigner_SignMigrateTssFundsCmd(t *testing.T) { // Setup txData struct cctx := getCCTX(t) - mockObserver, err := getNewEvmChainObserver(t, tss) - require.NoError(t, err) - txData, skip, err := NewOutboundData(ctx, cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) + txData, skip, err := NewOutboundData(ctx, cctx, 123, zerolog.Logger{}) require.False(t, skip) require.NoError(t, err) @@ -565,8 +588,26 @@ func TestSigner_SignMigrateTssFundsCmd(t *testing.T) { require.Nil(t, tx) }) } -func makeCtx() context.Context { - app := zctx.New(config.New(false), zerolog.Nop()) +func makeCtx(t *testing.T) context.Context { + app := zctx.New(config.New(false), nil, zerolog.Nop()) + + bscParams := mocks.MockChainParams(chains.BscMainnet.ChainId, 10) + + err := app.Update( + observertypes.Keygen{}, + []chains.Chain{chains.BscMainnet, chains.ZetaChainMainnet}, + nil, + map[int64]*observertypes.ChainParams{ + chains.BscMainnet.ChainId: &bscParams, + }, + "tssPubKey", + observertypes.CrosschainFlags{}, + ) + require.NoError(t, err, "unable to update app context") return zctx.WithAppContext(context.Background(), app) } + +func makeLogger(t *testing.T) zerolog.Logger { + return zerolog.New(zerolog.NewTestWriter(t)) +} diff --git a/zetaclient/chains/interfaces/interfaces.go b/zetaclient/chains/interfaces/interfaces.go index 4ca93b7259..edd656979b 100644 --- a/zetaclient/chains/interfaces/interfaces.go +++ b/zetaclient/chains/interfaces/interfaces.go @@ -106,7 +106,7 @@ type ZetacoreClient interface { GetLogger() *zerolog.Logger GetKeys() keyinterfaces.ObserverKeys - GetKeyGen(ctx context.Context) (*observertypes.Keygen, error) + GetKeyGen(ctx context.Context) (observertypes.Keygen, error) GetBlockHeight(ctx context.Context) (int64, error) GetBlockHeaderChainState(ctx context.Context, chainID int64) (*lightclienttypes.ChainState, error) @@ -193,6 +193,11 @@ type SolanaRPCClient interface { GetHealth(ctx context.Context) (string, error) GetSlot(ctx context.Context, commitment solrpc.CommitmentType) (uint64, error) GetAccountInfo(ctx context.Context, account solana.PublicKey) (*solrpc.GetAccountInfoResult, error) + GetBalance( + ctx context.Context, + account solana.PublicKey, + commitment solrpc.CommitmentType, + ) (*solrpc.GetBalanceResult, error) GetRecentBlockhash(ctx context.Context, commitment solrpc.CommitmentType) (*solrpc.GetRecentBlockhashResult, error) GetRecentPrioritizationFees( ctx context.Context, diff --git a/zetaclient/chains/solana/observer/inbound.go b/zetaclient/chains/solana/observer/inbound.go index 7c14ec34c6..cf05f94cf9 100644 --- a/zetaclient/chains/solana/observer/inbound.go +++ b/zetaclient/chains/solana/observer/inbound.go @@ -52,7 +52,7 @@ func (ob *Observer) WatchInbound(ctx context.Context) error { for { select { case <-ticker.C(): - if !app.IsInboundObservationEnabled(ob.GetChainParams()) { + if !app.IsInboundObservationEnabled() { sampledLogger.Info(). Msgf("WatchInbound: inbound observation is disabled for chain %d", ob.Chain().ChainId) continue diff --git a/zetaclient/chains/solana/observer/inbound_tracker.go b/zetaclient/chains/solana/observer/inbound_tracker.go index 7665359949..be5b6bf38d 100644 --- a/zetaclient/chains/solana/observer/inbound_tracker.go +++ b/zetaclient/chains/solana/observer/inbound_tracker.go @@ -33,7 +33,7 @@ func (ob *Observer) WatchInboundTracker(ctx context.Context) error { for { select { case <-ticker.C(): - if !app.IsInboundObservationEnabled(ob.GetChainParams()) { + if !app.IsInboundObservationEnabled() { continue } err := ob.ProcessInboundTrackers(ctx) diff --git a/zetaclient/chains/solana/observer/outbound.go b/zetaclient/chains/solana/observer/outbound.go index 59d93d2efb..396e49df1c 100644 --- a/zetaclient/chains/solana/observer/outbound.go +++ b/zetaclient/chains/solana/observer/outbound.go @@ -17,6 +17,7 @@ import ( crosschaintypes "github.com/zeta-chain/zetacore/x/crosschain/types" "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" zctx "github.com/zeta-chain/zetacore/zetaclient/context" + "github.com/zeta-chain/zetacore/zetaclient/logs" clienttypes "github.com/zeta-chain/zetacore/zetaclient/types" "github.com/zeta-chain/zetacore/zetaclient/zetacore" ) @@ -47,7 +48,7 @@ func (ob *Observer) WatchOutbound(ctx context.Context) error { for { select { case <-ticker.C(): - if !app.IsOutboundObservationEnabled(ob.GetChainParams()) { + if !app.IsOutboundObservationEnabled() { sampledLogger.Info().Msgf("WatchOutbound: outbound observation is disabled for chain %d", chainID) continue } @@ -254,15 +255,15 @@ func (ob *Observer) CheckFinalizedTx( // prepare logger fields chainID := ob.Chain().ChainId logger := ob.Logger().Outbound.With(). - Str("method", "checkFinalizedTx"). - Int64("chain", chainID). - Uint64("nonce", nonce). - Str("tx", txHash).Logger() + Str(logs.FieldMethod, "CheckFinalizedTx"). + Int64(logs.FieldChain, chainID). + Uint64(logs.FieldNonce, nonce). + Str(logs.FieldTx, txHash).Logger() // convert txHash to signature sig, err := solana.SignatureFromBase58(txHash) if err != nil { - logger.Error().Err(err).Msgf("SignatureFromBase58 err for chain %d nonce %d", chainID, nonce) + logger.Error().Err(err).Msg("SignatureFromBase58 error") return nil, false } @@ -271,20 +272,20 @@ func (ob *Observer) CheckFinalizedTx( Commitment: rpc.CommitmentFinalized, }) if err != nil { - logger.Error().Err(err).Msgf("GetTransaction err for chain %d nonce %d", chainID, nonce) + logger.Error().Err(err).Msg("GetTransaction error") return nil, false } // the tx must be successful in order to effectively increment the nonce if txResult.Meta.Err != nil { - logger.Error().Any("Err", txResult.Meta.Err).Msgf("tx is not successful for chain %d nonce %d", chainID, nonce) + logger.Error().Any("Err", txResult.Meta.Err).Msg("tx is not successful") return nil, false } // parse gateway instruction from tx result inst, err := ParseGatewayInstruction(txResult, ob.gatewayID, coinType) if err != nil { - logger.Error().Err(err).Msgf("ParseGatewayInstruction err for chain %d nonce %d", chainID, nonce) + logger.Error().Err(err).Msg("ParseGatewayInstruction error") return nil, false } txNonce := inst.GatewayNonce() @@ -292,19 +293,19 @@ func (ob *Observer) CheckFinalizedTx( // recover ECDSA signer from instruction signerECDSA, err := inst.Signer() if err != nil { - logger.Error().Err(err).Msgf("cannot get instruction signer for chain %d nonce %d", chainID, nonce) + logger.Error().Err(err).Msg("cannot get instruction signer") return nil, false } // check tx authorization if signerECDSA != ob.TSS().EVMAddress() { - logger.Error().Msgf("tx signer %s is not matching TSS, chain %d nonce %d", signerECDSA, chainID, nonce) + logger.Error().Msgf("tx signer %s is not matching current TSS address %s", signerECDSA, ob.TSS().EVMAddress()) return nil, false } // check tx nonce if txNonce != nonce { - logger.Error().Msgf("tx nonce %d is not matching cctx, chain %d nonce %d", txNonce, chainID, nonce) + logger.Error().Msgf("tx nonce %d is not matching tracker nonce", txNonce) return nil, false } diff --git a/zetaclient/chains/solana/signer/outbound_tracker_reporter.go b/zetaclient/chains/solana/signer/outbound_tracker_reporter.go index 6462060beb..0a5fb6432e 100644 --- a/zetaclient/chains/solana/signer/outbound_tracker_reporter.go +++ b/zetaclient/chains/solana/signer/outbound_tracker_reporter.go @@ -8,7 +8,9 @@ import ( "github.com/gagliardetto/solana-go/rpc" "github.com/rs/zerolog" + "github.com/zeta-chain/zetacore/pkg/bg" "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" + "github.com/zeta-chain/zetacore/zetaclient/logs" ) const ( @@ -27,16 +29,23 @@ func (signer *Signer) reportToOutboundTracker( txSig solana.Signature, logger zerolog.Logger, ) { + // prepare logger + logger = logger.With(). + Str(logs.FieldMethod, "reportToOutboundTracker"). + Int64(logs.FieldChain, chainID). + Uint64(logs.FieldNonce, nonce). + Str(logs.FieldTx, txSig.String()). + Logger() + // set being reported flag to avoid duplicate reporting alreadySet := signer.Signer.SetBeingReportedFlag(txSig.String()) if alreadySet { - logger.Info(). - Msgf("reportToOutboundTracker: outbound %s for chain %d nonce %d is being reported", txSig, chainID, nonce) + logger.Info().Msg("outbound is being reported to tracker") return } // launch a goroutine to monitor tx confirmation status - go func() { + bg.Work(ctx, func(ctx context.Context) error { defer func() { signer.Signer.ClearBeingReportedFlag(txSig.String()) }() @@ -48,9 +57,8 @@ func (signer *Signer) reportToOutboundTracker( // give up if we know the tx is too old and already expired if time.Since(start) > SolanaTransactionTimeout { - logger.Info(). - Msgf("reportToOutboundTracker: outbound %s expired for chain %d nonce %d", txSig, chainID, nonce) - return + logger.Info().Msg("outbound is expired") + return nil } // query tx using optimistic commitment level "confirmed" @@ -68,24 +76,21 @@ func (signer *Signer) reportToOutboundTracker( // unlike Ethereum, Solana doesn't have protocol-level nonce; the nonce is enforced by the gateway program. // a failed outbound (e.g. signature err, balance err) will never be able to increment the gateway program nonce. // a good/valid candidate of outbound tracker hash must come with a successful tx. - logger.Warn(). - Any("Err", tx.Meta.Err). - Msgf("reportToOutboundTracker: outbound %s failed for chain %d nonce %d", txSig, chainID, nonce) - return + logger.Warn().Any("Err", tx.Meta.Err).Msg("outbound is failed") + return nil } // report outbound hash to zetacore zetaHash, err := zetacoreClient.AddOutboundTracker(ctx, chainID, nonce, txSig.String(), nil, "", -1) if err != nil { - logger.Err(err). - Msgf("reportToOutboundTracker: error adding outbound %s for chain %d nonce %d", txSig, chainID, nonce) + logger.Err(err).Msg("error adding outbound to tracker") } else if zetaHash != "" { - logger.Info().Msgf("reportToOutboundTracker: added outbound %s for chain %d nonce %d; zeta txhash %s", txSig, chainID, nonce, zetaHash) + logger.Info().Msgf("added outbound to tracker; zeta txhash %s", zetaHash) } else { - // exit goroutine if the tracker already contains the hash (reported by other signer) - logger.Info().Msgf("reportToOutboundTracker: outbound %s already in tracker for chain %d nonce %d", txSig, chainID, nonce) - return + // exit goroutine until the tracker contains the hash (reported by either this or other signers) + logger.Info().Msg("outbound now exists in tracker") + return nil } } - }() + }, bg.WithName("TrackerReporterSolana"), bg.WithLogger(logger)) } diff --git a/zetaclient/chains/solana/signer/signer.go b/zetaclient/chains/solana/signer/signer.go index 3fb7512512..7fa989a071 100644 --- a/zetaclient/chains/solana/signer/signer.go +++ b/zetaclient/chains/solana/signer/signer.go @@ -11,10 +11,12 @@ import ( "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/pkg/coin" contracts "github.com/zeta-chain/zetacore/pkg/contracts/solana" + "github.com/zeta-chain/zetacore/pkg/crypto" "github.com/zeta-chain/zetacore/x/crosschain/types" observertypes "github.com/zeta-chain/zetacore/x/observer/types" "github.com/zeta-chain/zetacore/zetaclient/chains/base" "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" + "github.com/zeta-chain/zetacore/zetaclient/keys" "github.com/zeta-chain/zetacore/zetaclient/metrics" "github.com/zeta-chain/zetacore/zetaclient/outboundprocessor" ) @@ -28,8 +30,9 @@ type Signer struct { // client is the Solana RPC client that interacts with the Solana chain client interfaces.SolanaRPCClient - // solanaFeePayerKey is the private key of the fee payer account on Solana chain - solanaFeePayerKey solana.PrivateKey + // relayerKey is the private key of the relayer account for Solana chain + // relayerKey is optional, the signer will not relay transactions if it is not set + relayerKey *solana.PrivateKey // gatewayID is the program ID of gateway program on Solana chain gatewayID solana.PublicKey @@ -44,7 +47,7 @@ func NewSigner( chainParams observertypes.ChainParams, solClient interfaces.SolanaRPCClient, tss interfaces.TSSSigner, - solanaKey solana.PrivateKey, + relayerKey *keys.RelayerKey, ts *metrics.TelemetryServer, logger base.Logger, ) (*Signer, error) { @@ -56,16 +59,32 @@ func NewSigner( if err != nil { return nil, errors.Wrapf(err, "cannot parse gateway address %s", chainParams.GatewayAddress) } - logger.Std.Info().Msgf("Solana fee payer address: %s", solanaKey.PublicKey()) - - // create solana observer - return &Signer{ - Signer: baseSigner, - client: solClient, - solanaFeePayerKey: solanaKey, - gatewayID: gatewayID, - pda: pda, - }, nil + + // create Solana signer + signer := &Signer{ + Signer: baseSigner, + client: solClient, + gatewayID: gatewayID, + pda: pda, + } + + // construct Solana private key if present + if relayerKey != nil { + signer.relayerKey, err = crypto.SolanaPrivateKeyFromString(relayerKey.PrivateKey) + if err != nil { + return nil, errors.Wrap(err, "unable to construct solana private key") + } + logger.Std.Info().Msgf("Solana relayer address: %s", signer.relayerKey.PublicKey()) + } else { + logger.Std.Info().Msg("Solana relayer key is not provided") + } + + return signer, nil +} + +// HasRelayerKey returns true if the signer has a relayer key +func (signer *Signer) HasRelayerKey() bool { + return signer.relayerKey != nil } // TryProcessOutbound - signer interface implementation @@ -114,7 +133,15 @@ func (signer *Signer) TryProcessOutbound( return } - // sign the withdraw transaction by fee payer + // skip relaying the transaction if this signer hasn't set the relayer key + if !signer.HasRelayerKey() { + return + } + + // set relayer balance metrics + signer.SetRelayerBalanceMetrics(ctx) + + // sign the withdraw transaction by relayer key tx, err := signer.SignWithdrawTx(ctx, *msg) if err != nil { logger.Error().Err(err).Msgf("TryProcessOutbound: SignWithdrawTx error for chain %d nonce %d", chainID, nonce) @@ -167,6 +194,21 @@ func (signer *Signer) GetGatewayAddress() string { return signer.gatewayID.String() } +// SetRelayerBalanceMetrics sets the relayer balance metrics +func (signer *Signer) SetRelayerBalanceMetrics(ctx context.Context) { + if !signer.HasRelayerKey() { + return + } + + result, err := signer.client.GetBalance(ctx, signer.relayerKey.PublicKey(), rpc.CommitmentFinalized) + if err != nil { + signer.Logger().Std.Error().Err(err).Msg("GetBalance error") + return + } + solBalance := float64(result.Value) / float64(solana.LAMPORTS_PER_SOL) + metrics.RelayerKeyBalance.WithLabelValues(signer.Chain().Name).Set(solBalance) +} + // TODO: get rid of below four functions for Solana and Bitcoin // https://github.com/zeta-chain/node/issues/2532 func (signer *Signer) SetZetaConnectorAddress(_ ethcommon.Address) { diff --git a/zetaclient/chains/solana/signer/signer_test.go b/zetaclient/chains/solana/signer/signer_test.go new file mode 100644 index 0000000000..f3e1799d61 --- /dev/null +++ b/zetaclient/chains/solana/signer/signer_test.go @@ -0,0 +1,142 @@ +package signer_test + +import ( + "context" + "errors" + "testing" + + "github.com/gagliardetto/solana-go/rpc" + "github.com/prometheus/client_golang/prometheus/testutil" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "github.com/zeta-chain/zetacore/pkg/chains" + "github.com/zeta-chain/zetacore/testutil/sample" + observertypes "github.com/zeta-chain/zetacore/x/observer/types" + "github.com/zeta-chain/zetacore/zetaclient/chains/base" + "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" + "github.com/zeta-chain/zetacore/zetaclient/chains/solana/signer" + "github.com/zeta-chain/zetacore/zetaclient/keys" + "github.com/zeta-chain/zetacore/zetaclient/metrics" + "github.com/zeta-chain/zetacore/zetaclient/testutils" + "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" +) + +func Test_NewSigner(t *testing.T) { + // test parameters + chain := chains.SolanaDevnet + chainParams := sample.ChainParams(chain.ChainId) + chainParams.GatewayAddress = testutils.GatewayAddresses[chain.ChainId] + + tests := []struct { + name string + chain chains.Chain + chainParams observertypes.ChainParams + solClient interfaces.SolanaRPCClient + tss interfaces.TSSSigner + relayerKey *keys.RelayerKey + ts *metrics.TelemetryServer + logger base.Logger + errMessage string + }{ + { + name: "should create solana signer successfully with relayer key", + chain: chain, + chainParams: *chainParams, + solClient: nil, + tss: nil, + relayerKey: &keys.RelayerKey{ + PrivateKey: "3EMjCcCJg53fMEGVj13UPQpo6py9AKKyLE2qroR4yL1SvAN2tUznBvDKRYjntw7m6Jof1R2CSqjTddL27rEb6sFQ", + }, + ts: nil, + logger: base.DefaultLogger(), + }, + { + name: "should create solana signer successfully without relayer key", + chainParams: *chainParams, + solClient: nil, + tss: nil, + relayerKey: nil, + ts: nil, + logger: base.DefaultLogger(), + }, + { + name: "should fail to create solana signer with invalid gateway address", + chainParams: func() observertypes.ChainParams { + cp := *chainParams + cp.GatewayAddress = "invalid" + return cp + }(), + solClient: nil, + tss: nil, + relayerKey: nil, + ts: nil, + logger: base.DefaultLogger(), + errMessage: "cannot parse gateway address", + }, + { + name: "should fail to create solana signer with invalid relayer key", + chainParams: *chainParams, + solClient: nil, + tss: nil, + relayerKey: &keys.RelayerKey{ + PrivateKey: "3EMjCcCJg53fMEGVj13", // too short + }, + ts: nil, + logger: base.DefaultLogger(), + errMessage: "unable to construct solana private key", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, err := signer.NewSigner(tt.chain, tt.chainParams, tt.solClient, tt.tss, tt.relayerKey, tt.ts, tt.logger) + if tt.errMessage != "" { + require.ErrorContains(t, err, tt.errMessage) + require.Nil(t, s) + return + } + + require.NoError(t, err) + require.NotNil(t, s) + }) + } +} + +func Test_SetRelayerBalanceMetrics(t *testing.T) { + // test parameters + chain := chains.SolanaDevnet + chainParams := sample.ChainParams(chain.ChainId) + chainParams.GatewayAddress = testutils.GatewayAddresses[chain.ChainId] + relayerKey := &keys.RelayerKey{ + PrivateKey: "3EMjCcCJg53fMEGVj13UPQpo6py9AKKyLE2qroR4yL1SvAN2tUznBvDKRYjntw7m6Jof1R2CSqjTddL27rEb6sFQ", + } + ctx := context.Background() + + // mock solana client with RPC error + mckClient := mocks.NewSolanaRPCClient(t) + mckClient.On("GetBalance", mock.Anything, mock.Anything, mock.Anything).Return(nil, errors.New("rpc error")) + + // create signer and set relayer balance metrics + s, err := signer.NewSigner(chain, *chainParams, mckClient, nil, relayerKey, nil, base.DefaultLogger()) + require.NoError(t, err) + s.SetRelayerBalanceMetrics(ctx) + + // assert that relayer key balance metrics is not set (due to RPC error) + balance := testutil.ToFloat64(metrics.RelayerKeyBalance.WithLabelValues(chain.Name)) + require.Equal(t, 0.0, balance) + + // mock solana client with balance + mckClient = mocks.NewSolanaRPCClient(t) + mckClient.On("GetBalance", mock.Anything, mock.Anything, mock.Anything).Return(&rpc.GetBalanceResult{ + Value: 123400000, + }, nil) + + // create signer and set relayer balance metrics again + s, err = signer.NewSigner(chain, *chainParams, mckClient, nil, relayerKey, nil, base.DefaultLogger()) + require.NoError(t, err) + s.SetRelayerBalanceMetrics(ctx) + + // assert that relayer key balance metrics is set correctly + balance = testutil.ToFloat64(metrics.RelayerKeyBalance.WithLabelValues(chain.Name)) + require.Equal(t, 0.1234, balance) +} diff --git a/zetaclient/chains/solana/signer/withdraw.go b/zetaclient/chains/solana/signer/withdraw.go index 383d1c908a..f44dc3fc30 100644 --- a/zetaclient/chains/solana/signer/withdraw.go +++ b/zetaclient/chains/solana/signer/withdraw.go @@ -47,7 +47,7 @@ func (signer *Signer) SignMsgWithdraw( return msg.SetSignature(signature), nil } -// SignWithdrawTx wraps the withdraw 'msg' into a Solana transaction and signs it with the fee payer key. +// 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 @@ -65,7 +65,7 @@ func (signer *Signer) SignWithdrawTx(ctx context.Context, msg contracts.MsgWithd } // attach required accounts to the instruction - privkey := signer.solanaFeePayerKey + privkey := signer.relayerKey attachWithdrawAccounts(&inst, privkey.PublicKey(), signer.pda, msg.To(), signer.gatewayID) // get a recent blockhash @@ -89,10 +89,10 @@ func (signer *Signer) SignWithdrawTx(ctx context.Context, msg contracts.MsgWithd return nil, errors.Wrap(err, "NewTransaction error") } - // fee payer signs the transaction + // relayer signs the transaction _, err = tx.Sign(func(key solana.PublicKey) *solana.PrivateKey { if key.Equals(privkey.PublicKey()) { - return &privkey + return privkey } return nil }) diff --git a/zetaclient/config/types.go b/zetaclient/config/types.go index 1ec58e12aa..b43043e30e 100644 --- a/zetaclient/config/types.go +++ b/zetaclient/config/types.go @@ -2,15 +2,9 @@ package config import ( "encoding/json" - "fmt" - "os" - "path" "strings" "sync" - "cosmossdk.io/errors" - "github.com/gagliardetto/solana-go" - "github.com/zeta-chain/zetacore/pkg/chains" ) @@ -85,9 +79,9 @@ type Config struct { TssPath string `json:"TssPath"` TestTssKeysign bool `json:"TestTssKeysign"` KeyringBackend KeyringBackend `json:"KeyringBackend"` + RelayerKeyPath string `json:"RelayerKeyPath"` HsmMode bool `json:"HsmMode"` HsmHotKey string `json:"HsmHotKey"` - SolanaKeyFile string `json:"SolanaKeyFile"` // chain configs EVMChainConfigs map[int64]EVMConfig `json:"EVMChainConfigs"` @@ -165,32 +159,13 @@ func (c Config) GetKeyringBackend() KeyringBackend { return c.KeyringBackend } -// LoadSolanaPrivateKey loads the Solana private key from the key file -func (c Config) LoadSolanaPrivateKey() (solana.PrivateKey, error) { - // key file path - fileName := path.Join(c.ZetaCoreHome, c.SolanaKeyFile) - - // load the gateway keypair from a JSON file - // #nosec G304 -- user is allowed to specify the key file - fileContent, err := os.ReadFile(fileName) - if err != nil { - return solana.PrivateKey{}, errors.Wrapf(err, "unable to read Solana key file: %s", fileName) - } - - // unmarshal the JSON content into a slice of bytes - var keyBytes []byte - err = json.Unmarshal(fileContent, &keyBytes) - if err != nil { - return solana.PrivateKey{}, errors.Wrap(err, "unable to unmarshal Solana key bytes") - } - - // ensure the key length is 64 bytes - if len(keyBytes) != 64 { - return solana.PrivateKey{}, fmt.Errorf("invalid Solana key length: %d", len(keyBytes)) - } - - // create private key from the key bytes - privKey := solana.PrivateKey(keyBytes) +// GetRelayerKeyPath returns the relayer key path +func (c Config) GetRelayerKeyPath() string { + c.mu.RLock() + defer c.mu.RUnlock() + return c.RelayerKeyPath +} - return privKey, nil +func (c EVMConfig) Empty() bool { + return c.Endpoint == "" && c.Chain.IsEmpty() } diff --git a/zetaclient/context/app.go b/zetaclient/context/app.go index 187500e781..e3a219f00b 100644 --- a/zetaclient/context/app.go +++ b/zetaclient/context/app.go @@ -3,57 +3,56 @@ package context import ( "fmt" - "sort" "sync" + "github.com/pkg/errors" "github.com/rs/zerolog" + "github.com/samber/lo" + "golang.org/x/exp/constraints" + "golang.org/x/exp/maps" + "golang.org/x/exp/slices" "github.com/zeta-chain/zetacore/pkg/chains" - lightclienttypes "github.com/zeta-chain/zetacore/x/lightclient/types" observertypes "github.com/zeta-chain/zetacore/x/observer/types" "github.com/zeta-chain/zetacore/zetaclient/config" ) -// AppContext represents application context. +// AppContext represents application (zetaclient) context. type AppContext struct { + // config is the config of the app config config.Config + + // logger is the logger of the app logger zerolog.Logger - keygen observertypes.Keygen - chainsEnabled []chains.Chain - evmChainParams map[int64]*observertypes.ChainParams - bitcoinChainParams *observertypes.ChainParams - solanaChainParams *observertypes.ChainParams - currentTssPubkey string - crosschainFlags observertypes.CrosschainFlags + // chainRegistry is a registry of supported chains + chainRegistry *ChainRegistry + + // currentTssPubKey is the current tss pubKey + currentTssPubKey string - // additionalChains is a list of additional static chain information to use when searching from chain IDs - // it is stored in the protocol to dynamically support new chains without doing an upgrade - additionalChain []chains.Chain + // crosschainFlags is the current crosschain flags state + crosschainFlags observertypes.CrosschainFlags - // blockHeaderEnabledChains is used to store the list of chains that have block header verification enabled - // All chains in this list will have Enabled flag set to true - blockHeaderEnabledChains []lightclienttypes.HeaderSupportedChain + // keygen is the current tss keygen state + keygen observertypes.Keygen mu sync.RWMutex } // New creates and returns new empty AppContext -func New(cfg config.Config, logger zerolog.Logger) *AppContext { +func New(cfg config.Config, relayerKeyPasswords map[string]string, logger zerolog.Logger) *AppContext { return &AppContext{ config: cfg, logger: logger.With().Str("module", "appcontext").Logger(), - chainsEnabled: []chains.Chain{}, - evmChainParams: map[int64]*observertypes.ChainParams{}, - bitcoinChainParams: nil, - solanaChainParams: nil, - crosschainFlags: observertypes.CrosschainFlags{}, - blockHeaderEnabledChains: []lightclienttypes.HeaderSupportedChain{}, + chainRegistry: NewChainRegistry(relayerKeyPasswords), - currentTssPubkey: "", + crosschainFlags: observertypes.CrosschainFlags{}, + currentTssPubKey: "", keygen: observertypes.Keygen{}, - mu: sync.RWMutex{}, + + mu: sync.RWMutex{}, } } @@ -62,43 +61,45 @@ func (a *AppContext) Config() config.Config { return a.config } -// GetBTCChainAndConfig returns btc chain and config if enabled -func (a *AppContext) GetBTCChainAndConfig() (chains.Chain, config.BTCConfig, bool) { - cfg, configEnabled := a.Config().GetBTCConfig() - if !configEnabled { - return chains.Chain{}, config.BTCConfig{}, false - } +// GetChain returns the chain by ID. +func (a *AppContext) GetChain(chainID int64) (Chain, error) { + return a.chainRegistry.Get(chainID) +} - chain, _, paramsEnabled := a.GetBTCChainParams() - if !paramsEnabled { - return chains.Chain{}, config.BTCConfig{}, false - } +// ListChainIDs returns the list of existing chain ids in the registry. +func (a *AppContext) ListChainIDs() []int64 { + return a.chainRegistry.ChainIDs() +} - return chain, cfg, true +// ListChains returns the list of existing chains in the registry. +func (a *AppContext) ListChains() []Chain { + return a.chainRegistry.All() } -// GetSolanaChainAndConfig returns solana chain and config if enabled -func (a *AppContext) GetSolanaChainAndConfig() (chains.Chain, config.SolanaConfig, bool) { - solConfig, configEnabled := a.Config().GetSolanaConfig() - solChain, _, paramsEnabled := a.GetSolanaChainParams() +// FilterChains returns the list of chains that satisfy the filter +func (a *AppContext) FilterChains(filter func(Chain) bool) []Chain { + var ( + all = a.ListChains() + out = make([]Chain, 0, len(all)) + ) - if !configEnabled || !paramsEnabled { - return chains.Chain{}, config.SolanaConfig{}, false + for _, chain := range all { + if filter(chain) { + out = append(out, chain) + } } - return solChain, solConfig, true + return out } -// IsOutboundObservationEnabled returns true if the chain is supported and outbound flag is enabled -func (a *AppContext) IsOutboundObservationEnabled(chainParams observertypes.ChainParams) bool { - flags := a.GetCrossChainFlags() - return chainParams.IsSupported && flags.IsOutboundEnabled +// IsOutboundObservationEnabled returns true if outbound flag is enabled +func (a *AppContext) IsOutboundObservationEnabled() bool { + return a.GetCrossChainFlags().IsOutboundEnabled } -// IsInboundObservationEnabled returns true if the chain is supported and inbound flag is enabled -func (a *AppContext) IsInboundObservationEnabled(chainParams observertypes.ChainParams) bool { - flags := a.GetCrossChainFlags() - return chainParams.IsSupported && flags.IsInboundEnabled +// IsInboundObservationEnabled returns true if inbound flag is enabled +func (a *AppContext) IsInboundObservationEnabled() bool { + return a.GetCrossChainFlags().IsInboundEnabled } // GetKeygen returns the current keygen @@ -106,237 +107,188 @@ func (a *AppContext) GetKeygen() observertypes.Keygen { a.mu.RLock() defer a.mu.RUnlock() - var copiedPubkeys []string + var copiedPubKeys []string if a.keygen.GranteePubkeys != nil { - copiedPubkeys = make([]string, len(a.keygen.GranteePubkeys)) - copy(copiedPubkeys, a.keygen.GranteePubkeys) + copiedPubKeys = make([]string, len(a.keygen.GranteePubkeys)) + copy(copiedPubKeys, a.keygen.GranteePubkeys) } return observertypes.Keygen{ Status: a.keygen.Status, - GranteePubkeys: copiedPubkeys, + GranteePubkeys: copiedPubKeys, BlockNumber: a.keygen.BlockNumber, } } -// GetCurrentTssPubKey returns the current tss pubkey +// GetCurrentTssPubKey returns the current tss pubKey. func (a *AppContext) GetCurrentTssPubKey() string { a.mu.RLock() defer a.mu.RUnlock() - return a.currentTssPubkey + return a.currentTssPubKey } -// GetEnabledChains returns all enabled chains including zetachain -func (a *AppContext) GetEnabledChains() []chains.Chain { +// GetCrossChainFlags returns crosschain flags +func (a *AppContext) GetCrossChainFlags() observertypes.CrosschainFlags { a.mu.RLock() defer a.mu.RUnlock() - copiedChains := make([]chains.Chain, len(a.chainsEnabled)) - copy(copiedChains, a.chainsEnabled) - - return copiedChains + return a.crosschainFlags } -// GetEnabledExternalChains returns all enabled external chains -func (a *AppContext) GetEnabledExternalChains() []chains.Chain { - a.mu.RLock() - defer a.mu.RUnlock() - - externalChains := make([]chains.Chain, 0) - for _, chain := range a.chainsEnabled { - if chain.IsExternal { - externalChains = append(externalChains, chain) +// Update updates AppContext and params for all chains +// this must be the ONLY function that writes to AppContext +func (a *AppContext) Update( + keygen observertypes.Keygen, + freshChains, additionalChains []chains.Chain, + freshChainParams map[int64]*observertypes.ChainParams, + tssPubKey string, + crosschainFlags observertypes.CrosschainFlags, +) error { + // some sanity checks + switch { + case len(freshChains) == 0: + return fmt.Errorf("no chains present") + case len(freshChainParams) == 0: + return fmt.Errorf("no chain params present") + case tssPubKey == "" && a.currentTssPubKey != "": + // note that if we're doing a fresh start, we ALLOW an empty tssPubKey + return fmt.Errorf("tss pubkey is empty") + case len(additionalChains) > 0: + for _, c := range additionalChains { + if !c.IsExternal { + return fmt.Errorf("additional chain %d is not external", c.ChainId) + } } } - return externalChains -} - -// GetEVMChainParams returns chain params for a specific EVM chain -func (a *AppContext) GetEVMChainParams(chainID int64) (*observertypes.ChainParams, bool) { - a.mu.RLock() - defer a.mu.RUnlock() - evmChainParams, found := a.evmChainParams[chainID] - return evmChainParams, found -} - -// GetAllEVMChainParams returns all chain params for EVM chains -func (a *AppContext) GetAllEVMChainParams() map[int64]*observertypes.ChainParams { - a.mu.RLock() - defer a.mu.RUnlock() - - // deep copy evm chain params - copied := make(map[int64]*observertypes.ChainParams, len(a.evmChainParams)) - for chainID, evmConfig := range a.evmChainParams { - copied[chainID] = &observertypes.ChainParams{} - *copied[chainID] = *evmConfig - } - return copied -} - -// GetBTCChainParams returns (chain, chain params, found) for bitcoin chain -func (a *AppContext) GetBTCChainParams() (chains.Chain, *observertypes.ChainParams, bool) { - a.mu.RLock() - defer a.mu.RUnlock() - - // bitcoin is not enabled - if a.bitcoinChainParams == nil { - return chains.Chain{}, nil, false + err := a.updateChainRegistry(freshChains, additionalChains, freshChainParams) + if err != nil { + return errors.Wrap(err, "unable to update chain registry") } - chain, found := chains.GetChainFromChainID(a.bitcoinChainParams.ChainId, a.additionalChain) - if !found { - return chains.Chain{}, nil, false - } - - return chain, a.bitcoinChainParams, true -} - -// GetSolanaChainParams returns (chain, chain params, found) for solana chain -func (a *AppContext) GetSolanaChainParams() (chains.Chain, *observertypes.ChainParams, bool) { - a.mu.RLock() - defer a.mu.RUnlock() - - // solana is not enabled - if a.solanaChainParams == nil { - return chains.Chain{}, nil, false - } - - chain, found := chains.GetChainFromChainID(a.solanaChainParams.ChainId, a.additionalChain) - if !found { - fmt.Printf("solana Chain %d not found", a.solanaChainParams.ChainId) - return chains.Chain{}, nil, false - } - - return chain, a.solanaChainParams, true -} - -// GetCrossChainFlags returns crosschain flags -func (a *AppContext) GetCrossChainFlags() observertypes.CrosschainFlags { - a.mu.RLock() - defer a.mu.RUnlock() + a.mu.Lock() + defer a.mu.Unlock() - return a.crosschainFlags -} + a.crosschainFlags = crosschainFlags + a.keygen = keygen + a.currentTssPubKey = tssPubKey -// GetAdditionalChains returns additional chains -func (a *AppContext) GetAdditionalChains() []chains.Chain { - a.mu.RLock() - defer a.mu.RUnlock() - return a.additionalChain + return nil } -// GetAllHeaderEnabledChains returns all verification flags -func (a *AppContext) GetAllHeaderEnabledChains() []lightclienttypes.HeaderSupportedChain { - a.mu.RLock() - defer a.mu.RUnlock() +// updateChainRegistry updates the chain registry with fresh chains and chain params. +// Note that there's an edge-case for ZetaChain itself because we WANT to have it in chains list, +// but it doesn't have chain params. +func (a *AppContext) updateChainRegistry( + freshChains []chains.Chain, + additionalChains []chains.Chain, + freshChainParams map[int64]*observertypes.ChainParams, +) error { + var zetaChainID int64 - return a.blockHeaderEnabledChains -} + // 1. build map[chainId]Chain + freshChainsByID := make(map[int64]chains.Chain, len(freshChains)+len(additionalChains)) + for _, c := range freshChains { + freshChainsByID[c.ChainId] = c -// GetBlockHeaderEnabledChains checks if block header verification is enabled for a specific chain -func (a *AppContext) GetBlockHeaderEnabledChains(chainID int64) (lightclienttypes.HeaderSupportedChain, bool) { - a.mu.RLock() - defer a.mu.RUnlock() - - for _, flags := range a.blockHeaderEnabledChains { - if flags.ChainId == chainID { - return flags, true + if isZeta(c.ChainId) && zetaChainID == 0 { + zetaChainID = c.ChainId } } - return lightclienttypes.HeaderSupportedChain{}, false -} + for _, c := range additionalChains { + // shouldn't happen, but just in case + if _, found := freshChainsByID[c.ChainId]; found { + continue + } -// Update updates AppContext and params for all chains -// this must be the ONLY function that writes to AppContext -func (a *AppContext) Update( - keygen *observertypes.Keygen, - newChains []chains.Chain, - evmChainParams map[int64]*observertypes.ChainParams, - btcChainParams *observertypes.ChainParams, - solChainParams *observertypes.ChainParams, - tssPubKey string, - crosschainFlags observertypes.CrosschainFlags, - additionalChains []chains.Chain, - blockHeaderEnabledChains []lightclienttypes.HeaderSupportedChain, - init bool, -) { - if len(newChains) == 0 { - a.logger.Warn().Msg("UpdateChainParams: No chains enabled in ZeroCore") + freshChainsByID[c.ChainId] = c } - // Ignore whatever order zetacore organizes chain list in state - sort.SliceStable(newChains, func(i, j int) bool { - return newChains[i].ChainId < newChains[j].ChainId - }) - - a.mu.Lock() - defer a.mu.Unlock() + var ( + freshChainIDs = maps.Keys(freshChainsByID) + existingChainIDs = a.chainRegistry.ChainIDs() + ) - // Add some warnings if chain list changes at runtime - if !init && !chainsEqual(a.chainsEnabled, newChains) { + // 2. Compare existing chains with fresh ones + if len(existingChainIDs) > 0 && !elementsMatch(existingChainIDs, freshChainIDs) { a.logger.Warn(). - Interface("chains.current", a.chainsEnabled). - Interface("chains.new", newChains). - Msg("ChainsEnabled changed at runtime!") + Ints64("chains.current", existingChainIDs). + Ints64("chains.new", freshChainIDs). + Msg("Chain list changed at the runtime!") } - if keygen != nil { - a.keygen = *keygen + // Log warn if somehow chain doesn't chainParam + for _, chainID := range freshChainIDs { + if _, ok := freshChainParams[chainID]; !ok && !isZeta(chainID) { + a.logger.Warn(). + Int64("chain.id", chainID). + Msg("Chain doesn't have according ChainParams present. Skipping.") + } } - a.chainsEnabled = newChains - a.crosschainFlags = crosschainFlags - a.additionalChain = additionalChains - a.blockHeaderEnabledChains = blockHeaderEnabledChains - - // update core params for evm chains we have configs in file - freshEvmChainParams := make(map[int64]*observertypes.ChainParams) - for _, cp := range evmChainParams { - _, found := a.config.EVMChainConfigs[cp.ChainId] - if !found { - a.logger.Warn(). - Int64("chain.id", cp.ChainId). - Msg("Encountered EVM ChainParams that are not present in the config file") + // 3. If we have zeta chain, we want to force "fake" chainParams for it + if zetaChainID != 0 { + freshChainParams[zetaChainID] = zetaObserverChainParams(zetaChainID) + } - continue + // 3. Update chain registry + // okay, let's update the chains. + // Set() ensures that chain, chainID, and params are consistent and chain is not zeta + chain is supported + for chainID, params := range freshChainParams { + chain, ok := freshChainsByID[chainID] + if !ok { + return fmt.Errorf("unable to locate fresh chain %d based on chain params", chainID) } - if chains.IsZetaChain(cp.ChainId, nil) { - continue + if !isZeta(chainID) { + if err := observertypes.ValidateChainParams(params); err != nil { + return errors.Wrapf(err, "invalid chain params for chain %d", chainID) + } } - freshEvmChainParams[cp.ChainId] = cp + if err := a.chainRegistry.Set(chainID, &chain, params); err != nil { + return errors.Wrap(err, "unable to set chain in the registry") + } } - a.evmChainParams = freshEvmChainParams + a.chainRegistry.SetAdditionalChains(additionalChains) - // update chain params for bitcoin if it has config in file - if btcChainParams != nil { - a.bitcoinChainParams = btcChainParams - } + toBeDeleted, _ := lo.Difference(existingChainIDs, freshChainIDs) + if len(toBeDeleted) > 0 { + a.logger.Warn(). + Ints64("chains.deleted", toBeDeleted). + Msg("Deleting chains that are no longer relevant") - // update chain params for solana if it has config in file - if solChainParams != nil { - a.solanaChainParams = solChainParams + a.chainRegistry.Delete(toBeDeleted...) } - if tssPubKey != "" { - a.currentTssPubkey = tssPubKey - } + return nil +} + +func isZeta(chainID int64) bool { + return chains.IsZetaChain(chainID, nil) +} + +// zetaObserverChainParams returns "fake" chain params because +// actually chainParams is a concept of observer +func zetaObserverChainParams(chainID int64) *observertypes.ChainParams { + return &observertypes.ChainParams{ChainId: chainID, IsSupported: true} } -func chainsEqual(a []chains.Chain, b []chains.Chain) bool { +// elementsMatch returns true if two slices are equal. +// SORTS the slices before comparison. +func elementsMatch[T constraints.Ordered](a, b []T) bool { if len(a) != len(b) { return false } - for i, left := range a { - right := b[i] + slices.Sort(a) + slices.Sort(b) - if left.ChainId != right.ChainId { + for i := range a { + if a[i] != b[i] { return false } } diff --git a/zetaclient/context/app_test.go b/zetaclient/context/app_test.go index e591fac1d8..d3bba4f041 100644 --- a/zetaclient/context/app_test.go +++ b/zetaclient/context/app_test.go @@ -1,4 +1,4 @@ -package context_test +package context import ( "testing" @@ -6,572 +6,240 @@ import ( "github.com/rs/zerolog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" - "github.com/zeta-chain/zetacore/pkg/chains" - "github.com/zeta-chain/zetacore/testutil/sample" - lightclienttypes "github.com/zeta-chain/zetacore/x/lightclient/types" - observertypes "github.com/zeta-chain/zetacore/x/observer/types" + "github.com/zeta-chain/zetacore/x/observer/types" "github.com/zeta-chain/zetacore/zetaclient/config" - "github.com/zeta-chain/zetacore/zetaclient/context" + "golang.org/x/exp/maps" ) -func TestNew(t *testing.T) { +func TestAppContext(t *testing.T) { var ( testCfg = config.New(false) - logger = zerolog.Nop() - ) - - t.Run("should create new AppContext with empty config", func(t *testing.T) { - appContext := context.New(testCfg, logger) - require.NotNil(t, appContext) - - // assert keygen - keyGen := appContext.GetKeygen() - require.Equal(t, observertypes.Keygen{}, keyGen) - - // assert enabled chains - require.Empty(t, len(appContext.GetEnabledChains())) - - // assert external chains - require.Empty(t, len(appContext.GetEnabledExternalChains())) - - // assert current tss pubkey - require.Equal(t, "", appContext.GetCurrentTssPubKey()) + logger = zerolog.New(zerolog.NewTestWriter(t)) - // assert btc chain params - chain, btcChainParams, btcChainParamsFound := appContext.GetBTCChainParams() - require.Equal(t, chains.Chain{}, chain) - require.False(t, btcChainParamsFound) - require.Nil(t, btcChainParams) - - // assert evm chain params - allEVMChainParams := appContext.GetAllEVMChainParams() - require.Empty(t, allEVMChainParams) - }) - - t.Run("should return nil chain params if chain id is not found", func(t *testing.T) { - // create config with btc config - testCfg := config.New(false) - testCfg.BitcoinConfig = config.BTCConfig{ - RPCUsername: "test_user", - RPCPassword: "test_password", + keyGen = types.Keygen{ + Status: types.KeygenStatus_KeyGenSuccess, + GranteePubkeys: []string{"testPubKey1"}, + BlockNumber: 123, } + ccFlags = types.CrosschainFlags{ + IsInboundEnabled: true, + IsOutboundEnabled: true, + GasPriceIncreaseFlags: nil, + } + ttsPubKey = "tssPubKeyTest" + ) - // create AppContext with 0 chain id - appContext := context.New(testCfg, logger) - require.NotNil(t, appContext) + testCfg.BitcoinConfig.RPCUsername = "abc" - // assert btc chain params - chain, btcChainParams, btcChainParamsFound := appContext.GetBTCChainParams() - require.Equal(t, chains.Chain{}, chain) - require.False(t, btcChainParamsFound) - require.Nil(t, btcChainParams) - }) + ethParams := types.GetDefaultEthMainnetChainParams() + ethParams.IsSupported = true - t.Run("should create new AppContext with config containing evm chain params", func(t *testing.T) { - // ARRANGE - var ( - eth = chains.Ethereum.ChainId - matic = chains.Polygon.ChainId + btcParams := types.GetDefaultBtcMainnetChainParams() + btcParams.IsSupported = true - testCfg = config.New(false) + solParams := types.GetDefaultSolanaLocalnetChainParams() + solParams.IsSupported = true - ethChainParams = mocks.MockChainParams(eth, 200) - maticChainParams = mocks.MockChainParams(matic, 333) - ) + fancyL2 := chains.Chain{ + ChainId: 123, + Network: 0, + NetworkType: chains.NetworkType_mainnet, + Vm: chains.Vm_evm, + Consensus: chains.Consensus_ethereum, + IsExternal: true, + CctxGateway: 1, + } - // Given config with evm chain params (e.g. from a file) - testCfg.EVMChainConfigs = map[int64]config.EVMConfig{ - eth: {Chain: chains.Ethereum}, - matic: {Chain: chains.Polygon}, + fancyL2Params := types.GetDefaultEthMainnetChainParams() + fancyL2Params.ChainId = fancyL2.ChainId + fancyL2Params.IsSupported = true + + t.Run("Update", func(t *testing.T) { + // Given AppContext + appContext := New(testCfg, nil, logger) + + // With expected default behavior + _, err := appContext.GetChain(123) + require.ErrorIs(t, err, ErrChainNotFound) + + require.Equal(t, testCfg, appContext.Config()) + require.Empty(t, appContext.GetKeygen()) + require.Empty(t, appContext.GetCurrentTssPubKey()) + require.Empty(t, appContext.GetCrossChainFlags()) + require.False(t, appContext.IsInboundObservationEnabled()) + require.False(t, appContext.IsOutboundObservationEnabled()) + + // Given some data that is supposed to come from ZetaCore RPC + newChains := []chains.Chain{ + chains.Ethereum, + chains.BitcoinMainnet, + chains.SolanaLocalnet, } - // And chain params from zetacore - chainParams := map[int64]*observertypes.ChainParams{ - eth: ðChainParams, - matic: &maticChainParams, + chainParams := map[int64]*types.ChainParams{ + chains.Ethereum.ChainId: ethParams, + chains.BitcoinMainnet.ChainId: btcParams, + chains.SolanaLocalnet.ChainId: solParams, + fancyL2.ChainId: fancyL2Params, } - // Given app context - appContext := context.New(testCfg, logger) - - // That was updated with chain params - appContext.Update(nil, nil, chainParams, nil, nil, "", observertypes.CrosschainFlags{}, nil, nil, false) - - // assert evm chain params - allEVMChainParams := appContext.GetAllEVMChainParams() - require.Equal(t, 2, len(allEVMChainParams)) - require.Equal(t, ðChainParams, allEVMChainParams[eth]) - require.Equal(t, &maticChainParams, allEVMChainParams[matic]) - - evmChainParams1, found := appContext.GetEVMChainParams(eth) - require.True(t, found) - require.Equal(t, ðChainParams, evmChainParams1) - - evmChainParams2, found := appContext.GetEVMChainParams(matic) - require.True(t, found) - require.Equal(t, &maticChainParams, evmChainParams2) - }) - - t.Run("should create new AppContext with config containing btc config", func(t *testing.T) { - testCfg := config.New(false) - testCfg.BitcoinConfig = config.BTCConfig{ - RPCUsername: "test username", - RPCPassword: "test password", - RPCHost: "test host", - RPCParams: "test params", + additionalChains := []chains.Chain{ + fancyL2, } - appContext := context.New(testCfg, logger) - require.NotNil(t, appContext) - }) -} -func TestAppContextUpdate(t *testing.T) { - var ( - testCfg = config.New(false) - logger = zerolog.Nop() - ) + // ACT + err = appContext.Update(keyGen, newChains, additionalChains, chainParams, ttsPubKey, ccFlags) + + // ASSERT + require.NoError(t, err) + + // Check getters + assert.Equal(t, testCfg, appContext.Config()) + assert.Equal(t, keyGen, appContext.GetKeygen()) + assert.Equal(t, ttsPubKey, appContext.GetCurrentTssPubKey()) + assert.Equal(t, ccFlags, appContext.GetCrossChainFlags()) + assert.True(t, appContext.IsInboundObservationEnabled()) + assert.True(t, appContext.IsOutboundObservationEnabled()) + + // Check ETH Chain + ethChain, err := appContext.GetChain(1) + assert.NoError(t, err) + assert.True(t, ethChain.IsEVM()) + assert.False(t, ethChain.IsUTXO()) + assert.False(t, ethChain.IsSolana()) + assert.Equal(t, ethParams, ethChain.Params()) + + // Check that fancyL2 chain is added as well + fancyL2Chain, err := appContext.GetChain(fancyL2.ChainId) + assert.NoError(t, err) + assert.True(t, fancyL2Chain.IsEVM()) + assert.Equal(t, fancyL2Params, fancyL2Chain.Params()) + + // Check chain IDs + expectedIDs := []int64{ethParams.ChainId, btcParams.ChainId, solParams.ChainId, fancyL2.ChainId} + assert.ElementsMatch(t, expectedIDs, appContext.ListChainIDs()) + + // Check config + assert.Equal(t, "abc", appContext.Config().BitcoinConfig.RPCUsername) + + t.Run("edge-cases", func(t *testing.T) { + for _, tt := range []struct { + name string + act func(*AppContext) error + assert func(*testing.T, *AppContext, error) + }{ + { + name: "update with empty chains results in an error", + act: func(a *AppContext) error { + return appContext.Update(keyGen, newChains, nil, nil, ttsPubKey, ccFlags) + }, + assert: func(t *testing.T, a *AppContext, err error) { + assert.ErrorContains(t, err, "no chain params present") + }, + }, + { + name: "trying to add non-supported chain results in an error", + act: func(a *AppContext) error { + // ASSERT + // GIven Optimism chain params from ZetaCore, but it's not supported YET + op := chains.OptimismMainnet + opParams := types.GetDefaultEthMainnetChainParams() + opParams.ChainId = op.ChainId + opParams.IsSupported = false - t.Run("should update AppContext after being created from empty config", func(t *testing.T) { - appContext := context.New(testCfg, logger) - require.NotNil(t, appContext) + chainsWithOpt := append(newChains, op) - keyGenToUpdate := observertypes.Keygen{ - Status: observertypes.KeygenStatus_KeyGenSuccess, - GranteePubkeys: []string{"testpubkey1"}, - } - enabledChainsToUpdate := []chains.Chain{ - { - ChainId: 1, - IsExternal: true, - }, - { - ChainId: 2, - IsExternal: true, - }, - chains.ZetaChainTestnet, - } - evmChainParamsToUpdate := map[int64]*observertypes.ChainParams{ - 1: { - ChainId: 1, - }, - 2: { - ChainId: 2, - }, - } - btcChainParamsToUpdate := &observertypes.ChainParams{ - ChainId: 3, - } - tssPubKeyToUpdate := "tsspubkeytest" - crosschainFlags := sample.CrosschainFlags() - verificationFlags := sample.HeaderSupportedChains() - - require.NotNil(t, crosschainFlags) - appContext.Update( - &keyGenToUpdate, - enabledChainsToUpdate, - evmChainParamsToUpdate, - btcChainParamsToUpdate, - nil, - tssPubKeyToUpdate, - *crosschainFlags, - []chains.Chain{}, - verificationFlags, - false, - ) - - // assert keygen updated - keyGen := appContext.GetKeygen() - require.Equal(t, keyGenToUpdate, keyGen) - - // assert enabled chains updated - require.Equal(t, enabledChainsToUpdate, appContext.GetEnabledChains()) - - // assert enabled external chains - require.Equal(t, enabledChainsToUpdate[0:2], appContext.GetEnabledExternalChains()) - - // assert current tss pubkey updated - require.Equal(t, tssPubKeyToUpdate, appContext.GetCurrentTssPubKey()) - - // assert btc chain params still empty because they were not specified in config - chain, btcChainParams, btcChainParamsFound := appContext.GetBTCChainParams() - require.Equal(t, chains.Chain{}, chain) - require.False(t, btcChainParamsFound) - require.Nil(t, btcChainParams) - - // assert evm chain params still empty because they were not specified in config - allEVMChainParams := appContext.GetAllEVMChainParams() - require.Empty(t, allEVMChainParams) - - ccFlags := appContext.GetCrossChainFlags() - require.Equal(t, *crosschainFlags, ccFlags) - - verFlags := appContext.GetAllHeaderEnabledChains() - require.Equal(t, verificationFlags, verFlags) - }) + chainParamsWithOpt := maps.Clone(chainParams) + chainParamsWithOpt[opParams.ChainId] = opParams - t.Run( - "should update AppContext after being created from config with evm and btc chain params", - func(t *testing.T) { - testCfg := config.New(false) - testCfg.EVMChainConfigs = map[int64]config.EVMConfig{ - 1: { - Chain: chains.Chain{ - ChainId: 1, + return a.Update(keyGen, chainsWithOpt, additionalChains, chainParamsWithOpt, ttsPubKey, ccFlags) }, - }, - 2: { - Chain: chains.Chain{ - ChainId: 2, + assert: func(t *testing.T, a *AppContext, err error) { + assert.ErrorIs(t, err, ErrChainNotSupported) + mustBeNotFound(t, a, chains.OptimismMainnet.ChainId) }, }, - } - testCfg.BitcoinConfig = config.BTCConfig{ - RPCUsername: "test username", - RPCPassword: "test password", - RPCHost: "test host", - RPCParams: "test params", - } - - appContext := context.New(testCfg, logger) - require.NotNil(t, appContext) - - keyGenToUpdate := observertypes.Keygen{ - Status: observertypes.KeygenStatus_KeyGenSuccess, - GranteePubkeys: []string{"testpubkey1"}, - } - enabledChainsToUpdate := []chains.Chain{ { - ChainId: 1, + name: "trying to add zeta chain without chain params is allowed", + act: func(a *AppContext) error { + chainsWithZeta := append(newChains, chains.ZetaChainMainnet) + return a.Update(keyGen, chainsWithZeta, additionalChains, chainParams, ttsPubKey, ccFlags) + }, + assert: func(t *testing.T, a *AppContext, err error) { + assert.NoError(t, err) + + zc := mustBePresent(t, a, chains.ZetaChainMainnet.ChainId) + assert.True(t, zc.IsZeta()) + }, }, { - ChainId: 2, - }, - } - evmChainParamsToUpdate := map[int64]*observertypes.ChainParams{ - 1: { - ChainId: 1, - }, - 2: { - ChainId: 2, - }, - } + name: "trying to add zetachain with chain params is allowed but forces fake params", + act: func(a *AppContext) error { + zetaParams := types.GetDefaultZetaPrivnetChainParams() + zetaParams.ChainId = chains.ZetaChainMainnet.ChainId + zetaParams.IsSupported = true + zetaParams.GatewayAddress = "ABC123" - testBtcChain := chains.BitcoinTestnet - btcChainParamsToUpdate := &observertypes.ChainParams{ - ChainId: testBtcChain.ChainId, - } - tssPubKeyToUpdate := "tsspubkeytest" - crosschainFlags := sample.CrosschainFlags() - verificationFlags := sample.HeaderSupportedChains() - require.NotNil(t, crosschainFlags) - appContext.Update( - &keyGenToUpdate, - enabledChainsToUpdate, - evmChainParamsToUpdate, - btcChainParamsToUpdate, - nil, - tssPubKeyToUpdate, - *crosschainFlags, - []chains.Chain{}, - verificationFlags, - false, - ) - - // assert keygen updated - keyGen := appContext.GetKeygen() - require.Equal(t, keyGenToUpdate, keyGen) - - // assert enabled chains updated - require.Equal(t, enabledChainsToUpdate, appContext.GetEnabledChains()) - - // assert current tss pubkey updated - require.Equal(t, tssPubKeyToUpdate, appContext.GetCurrentTssPubKey()) - - // assert btc chain params - chain, btcChainParams, btcChainParamsFound := appContext.GetBTCChainParams() - require.Equal(t, testBtcChain, chain) - require.True(t, btcChainParamsFound) - require.Equal(t, btcChainParamsToUpdate, btcChainParams) - - // assert evm chain params - allEVMChainParams := appContext.GetAllEVMChainParams() - require.Equal(t, evmChainParamsToUpdate, allEVMChainParams) - - evmChainParams1, found := appContext.GetEVMChainParams(1) - require.True(t, found) - require.Equal(t, evmChainParamsToUpdate[1], evmChainParams1) - - evmChainParams2, found := appContext.GetEVMChainParams(2) - require.True(t, found) - require.Equal(t, evmChainParamsToUpdate[2], evmChainParams2) - - ccFlags := appContext.GetCrossChainFlags() - require.Equal(t, ccFlags, *crosschainFlags) - - verFlags := appContext.GetAllHeaderEnabledChains() - require.Equal(t, verFlags, verificationFlags) - }, - ) -} + chainParamsWithZeta := maps.Clone(chainParams) + chainParamsWithZeta[zetaParams.ChainId] = zetaParams -func TestIsOutboundObservationEnabled(t *testing.T) { - // create test chain params and flags - evmChain := chains.Ethereum - ccFlags := *sample.CrosschainFlags() - verificationFlags := sample.HeaderSupportedChains() - chainParams := &observertypes.ChainParams{ - ChainId: evmChain.ChainId, - IsSupported: true, - } - - t.Run("should return true if chain is supported and outbound flag is enabled", func(t *testing.T) { - appContext := makeAppContext(evmChain, chainParams, ccFlags, verificationFlags) - - require.True(t, appContext.IsOutboundObservationEnabled(*chainParams)) - }) - t.Run("should return false if chain is not supported yet", func(t *testing.T) { - paramsUnsupported := &observertypes.ChainParams{ChainId: evmChain.ChainId, IsSupported: false} - appContextUnsupported := makeAppContext(evmChain, paramsUnsupported, ccFlags, verificationFlags) - - require.False(t, appContextUnsupported.IsOutboundObservationEnabled(*paramsUnsupported)) - }) - t.Run("should return false if outbound flag is disabled", func(t *testing.T) { - flagsDisabled := ccFlags - flagsDisabled.IsOutboundEnabled = false - appContextDisabled := makeAppContext(evmChain, chainParams, flagsDisabled, verificationFlags) - - require.False(t, appContextDisabled.IsOutboundObservationEnabled(*chainParams)) - }) -} - -func TestIsInboundObservationEnabled(t *testing.T) { - // create test chain params and flags - evmChain := chains.Ethereum - ccFlags := *sample.CrosschainFlags() - verificationFlags := sample.HeaderSupportedChains() - chainParams := &observertypes.ChainParams{ - ChainId: evmChain.ChainId, - IsSupported: true, - } - - t.Run("should return true if chain is supported and inbound flag is enabled", func(t *testing.T) { - appContext := makeAppContext(evmChain, chainParams, ccFlags, verificationFlags) - - require.True(t, appContext.IsInboundObservationEnabled(*chainParams)) - }) - - t.Run("should return false if chain is not supported yet", func(t *testing.T) { - paramsUnsupported := &observertypes.ChainParams{ChainId: evmChain.ChainId, IsSupported: false} - appContextUnsupported := makeAppContext(evmChain, paramsUnsupported, ccFlags, verificationFlags) - - require.False(t, appContextUnsupported.IsInboundObservationEnabled(*paramsUnsupported)) - }) + chainsWithZeta := append(newChains, chains.ZetaChainMainnet) - t.Run("should return false if inbound flag is disabled", func(t *testing.T) { - flagsDisabled := ccFlags - flagsDisabled.IsInboundEnabled = false - appContextDisabled := makeAppContext(evmChain, chainParams, flagsDisabled, verificationFlags) - - require.False(t, appContextDisabled.IsInboundObservationEnabled(*chainParams)) - }) -} - -func TestGetBTCChainAndConfig(t *testing.T) { - logger := zerolog.Nop() - - emptyConfig := config.New(false) - nonEmptyConfig := config.New(true) - - assertEmpty := func(t *testing.T, chain chains.Chain, btcConfig config.BTCConfig, enabled bool) { - assert.Empty(t, chain) - assert.Empty(t, btcConfig) - assert.False(t, enabled) - } + return a.Update(keyGen, chainsWithZeta, additionalChains, chainParamsWithZeta, ttsPubKey, ccFlags) + }, + assert: func(t *testing.T, a *AppContext, err error) { + assert.NoError(t, err) - for _, tt := range []struct { - name string - cfg config.Config - setup func(app *context.AppContext) - assert func(t *testing.T, chain chains.Chain, btcConfig config.BTCConfig, enabled bool) - }{ - { - name: "no btc config", - cfg: emptyConfig, - setup: nil, - assert: assertEmpty, - }, - { - name: "btc config exists, but not chain params are set", - cfg: nonEmptyConfig, - setup: nil, - assert: assertEmpty, - }, - { - name: "btc config exists but chain is invalid", - cfg: nonEmptyConfig, - setup: func(app *context.AppContext) { - app.Update( - &observertypes.Keygen{}, - []chains.Chain{}, - nil, - &observertypes.ChainParams{ChainId: 123}, - nil, - "", - observertypes.CrosschainFlags{}, - []chains.Chain{}, - nil, - true, - ) - }, - assert: assertEmpty, - }, - { - name: "btc config exists and chain params are set", - cfg: nonEmptyConfig, - setup: func(app *context.AppContext) { - app.Update( - &observertypes.Keygen{}, - []chains.Chain{}, - nil, - &observertypes.ChainParams{ChainId: chains.BitcoinMainnet.ChainId}, - nil, - "", - observertypes.CrosschainFlags{}, - []chains.Chain{}, - nil, - true, - ) - }, - assert: func(t *testing.T, chain chains.Chain, btcConfig config.BTCConfig, enabled bool) { - assert.Equal(t, chains.BitcoinMainnet.ChainId, chain.ChainId) - assert.Equal(t, "smoketest", btcConfig.RPCUsername) - assert.True(t, enabled) - }, - }, - } { - t.Run(tt.name, func(t *testing.T) { - // ARRANGE - // Given app context - appContext := context.New(tt.cfg, logger) - - // And optional setup - if tt.setup != nil { - tt.setup(appContext) + zc := mustBePresent(t, a, chains.ZetaChainMainnet.ChainId) + assert.True(t, zc.IsZeta()) + assert.Equal(t, "", zc.Params().GatewayAddress) + }, + }, + { + name: "trying to add new chainParams without chain results in an error", + act: func(a *AppContext) error { + // ASSERT + // Given polygon chain params WITHOUT the chain itself + maticParams := types.GetDefaultMumbaiTestnetChainParams() + maticParams.ChainId = chains.Polygon.ChainId + maticParams.IsSupported = true + + updatedChainParams := maps.Clone(chainParams) + updatedChainParams[maticParams.ChainId] = maticParams + delete(updatedChainParams, chains.ZetaChainMainnet.ChainId) + + return a.Update(keyGen, newChains, additionalChains, updatedChainParams, ttsPubKey, ccFlags) + }, + assert: func(t *testing.T, a *AppContext, err error) { + assert.ErrorContains(t, err, "unable to locate fresh chain 137 based on chain params") + mustBeNotFound(t, a, chains.Polygon.ChainId) + }, + }, + } { + t.Run(tt.name, func(t *testing.T) { + // ACT + errAct := tt.act(appContext) + + // ASSERT + require.NotNil(t, tt.assert) + tt.assert(t, appContext, errAct) + }) } - - // ACT - chain, btcConfig, enabled := appContext.GetBTCChainAndConfig() - - // ASSERT - tt.assert(t, chain, btcConfig, enabled) }) - } -} - -func TestGetBlockHeaderEnabledChains(t *testing.T) { - // ARRANGE - // Given app config - appContext := context.New(config.New(false), zerolog.Nop()) - - // That was eventually updated - appContext.Update( - &observertypes.Keygen{}, - []chains.Chain{}, - nil, - &observertypes.ChainParams{ChainId: chains.BitcoinMainnet.ChainId}, - nil, - "", - observertypes.CrosschainFlags{}, - []chains.Chain{}, - []lightclienttypes.HeaderSupportedChain{ - {ChainId: 1, Enabled: true}, - }, - true, - ) - - // ACT #1 (found) - chain, found := appContext.GetBlockHeaderEnabledChains(1) - - // ASSERT #1 - assert.True(t, found) - assert.Equal(t, int64(1), chain.ChainId) - assert.True(t, chain.Enabled) - - // ACT #2 (not found) - chain, found = appContext.GetBlockHeaderEnabledChains(2) - - // ASSERT #2 - assert.False(t, found) - assert.Empty(t, chain) + }) } -func TestGetAdditionalChains(t *testing.T) { - // ARRANGE - // Given app config - appContext := context.New(config.New(false), zerolog.Nop()) - - additionalChains := []chains.Chain{ - sample.Chain(1), - sample.Chain(2), - sample.Chain(3), - } - - // That was eventually updated - appContext.Update( - &observertypes.Keygen{}, - []chains.Chain{}, - nil, - &observertypes.ChainParams{}, - nil, - "", - observertypes.CrosschainFlags{}, - additionalChains, - []lightclienttypes.HeaderSupportedChain{ - {ChainId: 1, Enabled: true}, - }, - true, - ) - - // ACT - found := appContext.GetAdditionalChains() - - // ASSERT - assert.EqualValues(t, additionalChains, found) +func mustBeNotFound(t *testing.T, a *AppContext, chainID int64) { + t.Helper() + _, err := a.GetChain(chainID) + require.ErrorIs(t, err, ErrChainNotFound) } -func makeAppContext( - evmChain chains.Chain, - evmChainParams *observertypes.ChainParams, - ccFlags observertypes.CrosschainFlags, - headerSupportedChains []lightclienttypes.HeaderSupportedChain, -) *context.AppContext { - // create config - cfg := config.New(false) - logger := zerolog.Nop() - cfg.EVMChainConfigs[evmChain.ChainId] = config.EVMConfig{ - Chain: evmChain, - } - - // create AppContext - appContext := context.New(cfg, logger) - evmChainParamsMap := make(map[int64]*observertypes.ChainParams) - evmChainParamsMap[evmChain.ChainId] = evmChainParams - - // feed chain params - appContext.Update( - &observertypes.Keygen{}, - []chains.Chain{evmChain}, - evmChainParamsMap, - nil, - nil, - "", - ccFlags, - []chains.Chain{}, - headerSupportedChains, - true, - ) +func mustBePresent(t *testing.T, a *AppContext, chainID int64) Chain { + t.Helper() + c, err := a.GetChain(chainID) + require.NoError(t, err) - return appContext + return c } diff --git a/zetaclient/context/chain.go b/zetaclient/context/chain.go new file mode 100644 index 0000000000..4271fdb46e --- /dev/null +++ b/zetaclient/context/chain.go @@ -0,0 +1,192 @@ +package context + +import ( + "fmt" + "sync" + + "github.com/pkg/errors" + "golang.org/x/exp/maps" + "golang.org/x/exp/slices" + + "github.com/zeta-chain/zetacore/pkg/chains" + observer "github.com/zeta-chain/zetacore/x/observer/types" +) + +// ChainRegistry is a registry of supported chains +type ChainRegistry struct { + chains map[int64]Chain + + // additionalChains is a list of additional static chain information to use when searching from + // chain IDs. It's stored in the protocol to dynamically support new chains without doing an upgrade + additionalChains []chains.Chain + + // relayerKeyPasswords maps network name to relayer key password + relayerKeyPasswords map[string]string + + mu sync.Mutex +} + +// Chain represents chain with its parameters +type Chain struct { + chainInfo *chains.Chain + observerParams *observer.ChainParams + + // reference to the registry it necessary for some operations + // like checking if the chain is EVM or not because it uses some "global" context state + registry *ChainRegistry +} + +var ( + ErrChainNotFound = errors.New("chain not found") + ErrChainNotSupported = errors.New("chain not supported") +) + +// NewChainRegistry constructs a new ChainRegistry +func NewChainRegistry(relayerKeyPasswords map[string]string) *ChainRegistry { + return &ChainRegistry{ + chains: make(map[int64]Chain), + additionalChains: []chains.Chain{}, + relayerKeyPasswords: relayerKeyPasswords, + mu: sync.Mutex{}, + } +} + +// Get returns a chain by ID. +func (cr *ChainRegistry) Get(chainID int64) (Chain, error) { + chain, ok := cr.chains[chainID] + if !ok { + return Chain{}, errors.Wrapf(ErrChainNotFound, "id=%d", chainID) + } + + return chain, nil +} + +// All returns all chains in the registry sorted by chain ID. +func (cr *ChainRegistry) All() []Chain { + items := maps.Values(cr.chains) + + slices.SortFunc(items, func(a, b Chain) bool { return a.ID() < b.ID() }) + + return items +} + +// Set sets a chain in the registry. +// A chain must be SUPPORTED; otherwise returns ErrChainNotSupported +func (cr *ChainRegistry) Set(chainID int64, chain *chains.Chain, params *observer.ChainParams) error { + item, err := newChain(cr, chainID, chain, params) + if err != nil { + return err + } + + item.registry = cr + + cr.mu.Lock() + defer cr.mu.Unlock() + + cr.chains[chainID] = item + + return nil +} + +// SetAdditionalChains sets additional chains to the registry +func (cr *ChainRegistry) SetAdditionalChains(chains []chains.Chain) { + cr.mu.Lock() + defer cr.mu.Unlock() + + cr.additionalChains = chains +} + +// Delete deletes one or more chains from the registry +func (cr *ChainRegistry) Delete(chainIDs ...int64) { + cr.mu.Lock() + defer cr.mu.Unlock() + + for _, id := range chainIDs { + delete(cr.chains, id) + } +} + +// Has checks if the chain is in the registry +func (cr *ChainRegistry) Has(chainID int64) bool { + _, ok := cr.chains[chainID] + return ok +} + +// ChainIDs returns a list of chain IDs in the registry +func (cr *ChainRegistry) ChainIDs() []int64 { + cr.mu.Lock() + defer cr.mu.Unlock() + + return maps.Keys(cr.chains) +} + +func newChain(cr *ChainRegistry, chainID int64, chain *chains.Chain, params *observer.ChainParams) (Chain, error) { + if err := validateNewChain(chainID, chain, params); err != nil { + return Chain{}, errors.Wrap(err, "invalid input") + } + + return Chain{ + chainInfo: chain, + observerParams: params, + registry: cr, + }, nil +} + +func (c Chain) ID() int64 { + return c.chainInfo.ChainId +} + +func (c Chain) Name() string { + return c.chainInfo.Name +} + +func (c Chain) Params() *observer.ChainParams { + return c.observerParams +} + +// RawChain returns the underlying Chain object. Better not to use this method +func (c Chain) RawChain() *chains.Chain { + return c.chainInfo +} + +func (c Chain) IsEVM() bool { + return chains.IsEVMChain(c.ID(), c.registry.additionalChains) +} + +func (c Chain) IsZeta() bool { + return chains.IsZetaChain(c.ID(), c.registry.additionalChains) +} + +func (c Chain) IsUTXO() bool { + return chains.IsBitcoinChain(c.ID(), c.registry.additionalChains) +} + +func (c Chain) IsSolana() bool { + return chains.IsSolanaChain(c.ID(), c.registry.additionalChains) +} + +// RelayerKeyPassword returns the relayer key password for the chain +func (c Chain) RelayerKeyPassword() string { + network := c.RawChain().Network + + return c.registry.relayerKeyPasswords[network.String()] +} + +func validateNewChain(chainID int64, chain *chains.Chain, params *observer.ChainParams) error { + switch { + case chainID < 1: + return fmt.Errorf("invalid chain id %d", chainID) + case chain == nil: + return fmt.Errorf("chain is nil") + case params == nil: + return fmt.Errorf("chain params is nil") + case chain.ChainId != chainID: + return fmt.Errorf("chain id %d does not match chain.ChainId %d", chainID, chain.ChainId) + case params.ChainId != chainID: + return fmt.Errorf("chain id %d does not match params.ChainId %d", chainID, params.ChainId) + case !params.IsSupported: + return ErrChainNotSupported + } + + return nil +} diff --git a/zetaclient/context/chain_test.go b/zetaclient/context/chain_test.go new file mode 100644 index 0000000000..29d1ecef7c --- /dev/null +++ b/zetaclient/context/chain_test.go @@ -0,0 +1,87 @@ +package context + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/zeta-chain/zetacore/pkg/chains" + observer "github.com/zeta-chain/zetacore/x/observer/types" + "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" +) + +func TestChainRegistry(t *testing.T) { + // Given chains & chainParams + var ( + btc = &chains.BitcoinMainnet + btcParams = makeParams(btc.ChainId, true) + + eth = &chains.Ethereum + ethParams = makeParams(eth.ChainId, true) + + matic = &chains.Polygon + maticParams = makeParams(matic.ChainId, true) + + // NOT supported! + opt = &chains.OptimismSepolia + optParams = makeParams(opt.ChainId, false) + + sol = &chains.SolanaMainnet + solParams = makeParams(sol.ChainId, true) + + // Zetachain itself + zeta = &chains.ZetaChainMainnet + zetaParams = makeParams(zeta.ChainId, true) + ) + + t.Run("Sample Flow", func(t *testing.T) { + // Given registry + r := NewChainRegistry(nil) + + // With some chains added + require.NoError(t, r.Set(btc.ChainId, btc, btcParams)) + require.NoError(t, r.Set(eth.ChainId, eth, ethParams)) + require.NoError(t, r.Set(matic.ChainId, matic, maticParams)) + require.NoError(t, r.Set(sol.ChainId, sol, solParams)) + require.NoError(t, r.Set(zeta.ChainId, zeta, zetaParams)) + + // With failures on invalid data + require.Error(t, r.Set(0, btc, btcParams)) + require.Error(t, r.Set(btc.ChainId, btc, nil)) + require.Error(t, r.Set(btc.ChainId, nil, btcParams)) + require.Error(t, r.Set(123, btc, btcParams)) + require.Error(t, r.Set(btc.ChainId, btc, ethParams)) + + // With failure on adding unsupported chains + require.ErrorIs(t, r.Set(opt.ChainId, opt, optParams), ErrChainNotSupported) + + // Should return a proper chain list + expectedChains := []int64{ + btc.ChainId, + eth.ChainId, + matic.ChainId, + sol.ChainId, + zeta.ChainId, + } + + require.ElementsMatch(t, expectedChains, r.ChainIDs()) + + // Should return not found error + _, err := r.Get(123) + require.ErrorIs(t, err, ErrChainNotFound) + + // Let's check ETH + ethChain, err := r.Get(eth.ChainId) + require.NoError(t, err) + require.True(t, ethChain.IsEVM()) + require.False(t, ethChain.IsUTXO()) + require.False(t, ethChain.IsSolana()) + require.Equal(t, ethParams, ethChain.Params()) + }) +} + +func makeParams(id int64, supported bool) *observer.ChainParams { + cp := mocks.MockChainParams(id, 123) + cp.IsSupported = supported + + return &cp +} diff --git a/zetaclient/context/context_test.go b/zetaclient/context/context_test.go index be9dab83a4..d0f623a86f 100644 --- a/zetaclient/context/context_test.go +++ b/zetaclient/context/context_test.go @@ -24,7 +24,7 @@ func TestFromContext(t *testing.T) { // ARRANGE #2 // Given basic app - app := context.New(config.New(false), zerolog.Nop()) + app := context.New(config.New(false), nil, zerolog.Nop()) // That is included in the ctx ctx = context.WithAppContext(ctx, app) @@ -42,7 +42,7 @@ func TestFromContext(t *testing.T) { func TestCopy(t *testing.T) { // ARRANGE var ( - app = context.New(config.New(false), zerolog.Nop()) + app = context.New(config.New(false), nil, zerolog.Nop()) ctx1 = context.WithAppContext(goctx.Background(), app) ) diff --git a/zetaclient/keys/relayer_key.go b/zetaclient/keys/relayer_key.go new file mode 100644 index 0000000000..935d11ac84 --- /dev/null +++ b/zetaclient/keys/relayer_key.go @@ -0,0 +1,155 @@ +package keys + +import ( + "encoding/json" + "os" + "path/filepath" + + "github.com/pkg/errors" + + "github.com/zeta-chain/zetacore/pkg/chains" + "github.com/zeta-chain/zetacore/pkg/crypto" + zetaos "github.com/zeta-chain/zetacore/pkg/os" +) + +// RelayerKey is the structure that holds the relayer private key +type RelayerKey struct { + PrivateKey string `json:"private_key"` +} + +// ResolveAddress returns the network name and address of the relayer key +func (rk RelayerKey) ResolveAddress(network chains.Network) (string, string, error) { + var address string + + switch network { + case chains.Network_solana: + privKey, err := crypto.SolanaPrivateKeyFromString(rk.PrivateKey) + if err != nil { + return "", "", errors.Wrap(err, "unable to construct solana private key") + } + address = privKey.PublicKey().String() + default: + return "", "", errors.Errorf("unsupported network %d: unable to derive relayer address", network) + } + + // return network name and address + return network.String(), address, nil +} + +// LoadRelayerKey loads the relayer key for given network and password +func LoadRelayerKey(relayerKeyPath string, network chains.Network, password string) (*RelayerKey, error) { + // resolve the relayer key file name + fileName, err := ResolveRelayerKeyFile(relayerKeyPath, network) + if err != nil { + return nil, errors.Wrap(err, "failed to resolve relayer key file name") + } + + // load the relayer key if it is present + if zetaos.FileExists(fileName) { + // read the relayer key file + relayerKey, err := ReadRelayerKeyFromFile(fileName) + if err != nil { + return nil, errors.Wrapf(err, "failed to read relayer key file: %s", fileName) + } + + // password must be set by operator + if password == "" { + return nil, errors.New("password is required to decrypt the private key") + } + + // decrypt the private key + privateKey, err := crypto.DecryptAES256GCMBase64(relayerKey.PrivateKey, password) + if err != nil { + return nil, errors.Wrap(err, "failed to decrypt private key") + } + + relayerKey.PrivateKey = privateKey + return relayerKey, nil + } + + // relayer key is optional, so it's okay if the relayer key is not provided + return nil, nil +} + +// ResolveRelayerKeyFile is a helper function to resolve the relayer key file with full path +func ResolveRelayerKeyFile(relayerKeyPath string, network chains.Network) (string, error) { + // resolve relayer key path if it contains a tilde + keyPath, err := zetaos.ExpandHomeDir(relayerKeyPath) + if err != nil { + return "", errors.Wrap(err, "failed to resolve relayer key path") + } + + // get relayer key file name by network + name, err := relayerKeyFileByNetwork(network) + if err != nil { + return "", errors.Wrap(err, "failed to get relayer key file name") + } + + return filepath.Join(keyPath, name), nil +} + +// WriteRelayerKeyToFile writes the relayer key to a file +func WriteRelayerKeyToFile(fileName string, relayerKey RelayerKey) error { + keyData, err := json.Marshal(relayerKey) + if err != nil { + return errors.Wrap(err, "failed to marshal relayer key") + } + + // create relay key file (owner `rw` permissions) + return os.WriteFile(fileName, keyData, 0o600) +} + +// ReadRelayerKeyFromFile reads the relayer key file and returns the key +func ReadRelayerKeyFromFile(fileName string) (*RelayerKey, error) { + // expand home directory in the file path if it exists + fileNameFull, err := zetaos.ExpandHomeDir(fileName) + if err != nil { + return nil, errors.Wrapf(err, "ExpandHome failed for file: %s", fileName) + } + + // read the file contents + // #nosec G304 -- relayer key file is controlled by the operator + fileData, err := os.ReadFile(fileNameFull) + if err != nil { + return nil, errors.Wrapf(err, "unable to read relayer key data: %s", fileNameFull) + } + + // unmarshal the JSON data into the struct + var key RelayerKey + err = json.Unmarshal(fileData, &key) + if err != nil { + return nil, errors.Wrap(err, "unable to unmarshal relayer key") + } + + return &key, nil +} + +// IsRelayerPrivateKeyValid checks if the given private key is valid for the given network +func IsRelayerPrivateKeyValid(privateKey string, network chains.Network) bool { + switch network { + case chains.Network_solana: + _, err := crypto.SolanaPrivateKeyFromString(privateKey) + if err != nil { + return false + } + default: + // unsupported network + return false + } + return true +} + +// relayerKeyFileByNetwork returns the relayer key JSON file name based on network +func relayerKeyFileByNetwork(network chains.Network) (string, error) { + // JSONFileSuffix is the suffix for the relayer key file + const JSONFileSuffix = ".json" + + // return file name for supported networks only + switch network { + case chains.Network_solana: + // return network name + '.json' + return network.String() + JSONFileSuffix, nil + default: + return "", errors.Errorf("network %d does not support relayer key", network) + } +} diff --git a/zetaclient/keys/relayer_key_test.go b/zetaclient/keys/relayer_key_test.go new file mode 100644 index 0000000000..08e82863d1 --- /dev/null +++ b/zetaclient/keys/relayer_key_test.go @@ -0,0 +1,293 @@ +package keys_test + +import ( + "os" + "os/user" + "path" + "testing" + + "github.com/stretchr/testify/require" + "github.com/zeta-chain/zetacore/pkg/chains" + "github.com/zeta-chain/zetacore/pkg/crypto" + "github.com/zeta-chain/zetacore/testutil/sample" + "github.com/zeta-chain/zetacore/zetaclient/keys" +) + +// createRelayerKeyFile creates a relayer key file for testing +func createRelayerKeyFile(t *testing.T, fileName, privKey, password string) { + // encrypt the private key + ciphertext, err := crypto.EncryptAES256GCMBase64(privKey, password) + require.NoError(t, err) + + // create relayer key file + err = keys.WriteRelayerKeyToFile(fileName, keys.RelayerKey{PrivateKey: ciphertext}) + require.NoError(t, err) +} + +// createBadRelayerKeyFile creates a bad relayer key file for testing +func createBadRelayerKeyFile(t *testing.T, fileName string) { + err := os.WriteFile(fileName, []byte("arbitrary data"), 0o600) + require.NoError(t, err) +} + +func Test_ResolveAddress(t *testing.T) { + // sample test keys + solanaPrivKey := sample.SolanaPrivateKey(t) + + tests := []struct { + name string + network chains.Network + relayerKey keys.RelayerKey + expectedNetworkName string + expectedAddress string + expectedError string + }{ + { + name: "should resolve solana address", + network: chains.Network_solana, + relayerKey: keys.RelayerKey{ + PrivateKey: solanaPrivKey.String(), + }, + expectedNetworkName: "solana", + expectedAddress: solanaPrivKey.PublicKey().String(), + }, + { + name: "should return error if private key is invalid", + network: chains.Network_solana, + relayerKey: keys.RelayerKey{ + PrivateKey: "invalid", + }, + expectedError: "unable to construct solana private key", + }, + { + name: "should return error if network is unsupported", + network: chains.Network_eth, + relayerKey: keys.RelayerKey{ + PrivateKey: solanaPrivKey.String(), + }, + expectedError: "unsupported network", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + networkName, address, err := tt.relayerKey.ResolveAddress(tt.network) + if tt.expectedError != "" { + require.Empty(t, networkName) + require.Empty(t, address) + require.ErrorContains(t, err, tt.expectedError) + return + } + + require.NoError(t, err) + require.Equal(t, tt.expectedNetworkName, networkName) + require.Equal(t, tt.expectedAddress, address) + }) + } +} + +func Test_LoadRelayerKey(t *testing.T) { + // sample test key and temp path + solanaPrivKey := sample.SolanaPrivateKey(t) + keyPath := sample.CreateTempDir(t) + fileName := path.Join(keyPath, "solana.json") + + // create relayer key file + createRelayerKeyFile(t, fileName, solanaPrivKey.String(), "password") + + // create a bad relayer key file + keyPath2 := sample.CreateTempDir(t) + badKeyFile := path.Join(keyPath2, "solana.json") + createBadRelayerKeyFile(t, badKeyFile) + + // test cases + tests := []struct { + name string + keyPath string + network chains.Network + password string + expectedKey *keys.RelayerKey + expectError string + }{ + { + name: "should load relayer key successfully", + keyPath: keyPath, + network: chains.Network_solana, + password: "password", + expectedKey: &keys.RelayerKey{PrivateKey: solanaPrivKey.String()}, + }, + { + name: "it's okay if relayer key is not provided", + keyPath: sample.CreateTempDir(t), // create a empty directory + network: chains.Network_solana, + password: "", + expectedKey: nil, + expectError: "", + }, + { + name: "should return error if network is unsupported", + keyPath: keyPath, + network: chains.Network_eth, + password: "", + expectedKey: nil, + expectError: "failed to resolve relayer key file name", + }, + { + name: "should return error if unable to read relayer key file", + keyPath: keyPath2, + network: chains.Network_solana, + password: "", + expectedKey: nil, + expectError: "failed to read relayer key file", + }, + { + name: "should return error if password is missing", + keyPath: keyPath, + network: chains.Network_solana, + password: "", + expectedKey: nil, + expectError: "password is required to decrypt the private key", + }, + { + name: "should return error if password is incorrect", + keyPath: keyPath, + network: chains.Network_solana, + password: "incorrect", + expectedKey: nil, + expectError: "failed to decrypt private key", + }, + } + + // Iterate over the test cases and run them + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + relayerKey, err := keys.LoadRelayerKey(tt.keyPath, tt.network, tt.password) + + if tt.expectError != "" { + require.ErrorContains(t, err, tt.expectError) + require.Nil(t, relayerKey) + } else { + require.NoError(t, err) + if tt.expectedKey != nil { + require.Equal(t, tt.expectedKey.PrivateKey, relayerKey.PrivateKey) + } + } + }) + } +} + +func Test_ResolveRelayerKeyPath(t *testing.T) { + usr, err := user.Current() + require.NoError(t, err) + + tests := []struct { + name string + relayerKeyPath string + network chains.Network + expectedName string + errMessage string + }{ + { + name: "should resolve relayer key path", + relayerKeyPath: "~/.zetacored/relayer-keys", + network: chains.Network_solana, + expectedName: path.Join(usr.HomeDir, ".zetacored/relayer-keys/solana.json"), + }, + { + name: "should return error if network is invalid", + relayerKeyPath: "~/.zetacored/relayer-keys", + network: chains.Network(999), + errMessage: "failed to get relayer key file name", + }, + { + name: "should return error if network does not support relayer key", + relayerKeyPath: "~/.zetacored/relayer-keys", + network: chains.Network_eth, + errMessage: "does not support relayer key", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + name, err := keys.ResolveRelayerKeyFile(tt.relayerKeyPath, tt.network) + if tt.errMessage != "" { + require.Empty(t, name) + require.ErrorContains(t, err, tt.errMessage) + return + } + + require.NoError(t, err) + require.Equal(t, tt.expectedName, name) + }) + } +} + +func Test_ReadWriteRelayerKeyFile(t *testing.T) { + // sample test key and temp path + solanaPrivKey := sample.SolanaPrivateKey(t) + keyPath := sample.CreateTempDir(t) + fileName := path.Join(keyPath, "solana.json") + + t.Run("should write and read relayer key file", func(t *testing.T) { + // create relayer key file + err := keys.WriteRelayerKeyToFile(fileName, keys.RelayerKey{PrivateKey: solanaPrivKey.String()}) + require.NoError(t, err) + + // read relayer key file + relayerKey, err := keys.ReadRelayerKeyFromFile(fileName) + require.NoError(t, err) + require.Equal(t, solanaPrivKey.String(), relayerKey.PrivateKey) + }) + + t.Run("should return error if relayer key file does not exist", func(t *testing.T) { + noFileName := path.Join(keyPath, "non-existing.json") + _, err := keys.ReadRelayerKeyFromFile(noFileName) + require.ErrorContains(t, err, "unable to read relayer key data") + }) + + t.Run("should return error if unmarsalling fails", func(t *testing.T) { + // create a bad key file + badKeyFile := path.Join(keyPath, "bad.json") + createBadRelayerKeyFile(t, badKeyFile) + + // try reading bad key file + key, err := keys.ReadRelayerKeyFromFile(badKeyFile) + require.ErrorContains(t, err, "unable to unmarshal relayer key") + require.Nil(t, key) + }) +} + +func Test_IsRelayerPrivateKeyValid(t *testing.T) { + tests := []struct { + name string + privKey string + network chains.Network + result bool + }{ + { + name: "valid private key - solana", + privKey: "3EMjCcCJg53fMEGVj13UPQpo6py9AKKyLE2qroR4yL1SvAN2tUznBvDKRYjntw7m6Jof1R2CSqjTddL27rEb6sFQ", + network: chains.Network(7), // solana + result: true, + }, + { + name: "invalid private key - unsupported network", + privKey: "3EMjCcCJg53fMEGVj13UPQpo6py9AKKyLE2qroR4yL1SvAN2tUznBvDKRYjntw7m6Jof1R2CSqjTddL27rEb6sFQ", + network: chains.Network(0), // eth + result: false, + }, + { + name: "invalid private key - invalid solana private key", + privKey: "3EMjCcCJg53fMEGVj13UPQpo6p", // too short + network: chains.Network(7), // solana + result: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := keys.IsRelayerPrivateKeyValid(tt.privKey, chains.Network(tt.network)) + require.Equal(t, tt.result, result) + }) + } +} diff --git a/zetaclient/logs/fields.go b/zetaclient/logs/fields.go new file mode 100644 index 0000000000..497690ffa4 --- /dev/null +++ b/zetaclient/logs/fields.go @@ -0,0 +1,18 @@ +package logs + +// A group of predefined field keys and module names for zetaclient logs +const ( + // field keys + FieldModule = "module" + FieldMethod = "method" + FieldChain = "chain" + FieldNonce = "nonce" + FieldTx = "tx" + FieldCctx = "cctx" + + // module names + ModNameInbound = "inbound" + ModNameOutbound = "outbound" + ModNameGasPrice = "gasprice" + ModNameHeaders = "headers" +) diff --git a/zetaclient/metrics/metrics.go b/zetaclient/metrics/metrics.go index 50d88b398b..a0a7341f94 100644 --- a/zetaclient/metrics/metrics.go +++ b/zetaclient/metrics/metrics.go @@ -4,6 +4,7 @@ package metrics import ( "context" "net/http" + "net/url" "time" "github.com/prometheus/client_golang/prometheus" @@ -49,6 +50,13 @@ var ( Help: "Tss node blame counter per pubkey", }, []string{"pubkey"}) + // RelayerKeyBalance is a gauge that contains the relayer key balance of the chain + RelayerKeyBalance = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: ZetaClientNamespace, + Name: "relayer_key_balance", + Help: "Relayer key balance of the chain", + }, []string{"chain"}) + // HotKeyBurnRate is a gauge that contains the fee burn rate of the hotkey HotKeyBurnRate = promauto.NewGauge(prometheus.GaugeOpts{ Namespace: ZetaClientNamespace, @@ -112,6 +120,34 @@ var ( Help: "Histogram of the TSS keysign latency", Buckets: []float64{1, 7, 15, 30, 60, 120, 240}, }, []string{"result"}) + + // RPCInProgress is a gauge that contains the number of RPCs requests in progress + RPCInProgress = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: ZetaClientNamespace, + Name: "rpc_in_progress", + Help: "Number of RPC requests in progress", + }, []string{"host"}) + + // RPCCount is a counter that contains the number of total RPC requests + RPCCount = promauto.NewCounterVec( + prometheus.CounterOpts{ + Namespace: ZetaClientNamespace, + Name: "rpc_count", + Help: "A counter for number of total RPC requests", + }, + []string{"host", "code"}, + ) + + // RPCLatency is a histogram of the RPC latency + RPCLatency = promauto.NewHistogramVec( + prometheus.HistogramOpts{ + Namespace: ZetaClientNamespace, + Name: "rpc_duration_seconds", + Help: "A histogram of the RPC duration in seconds", + Buckets: prometheus.DefBuckets, + }, + []string{"host"}, + ) ) // NewMetrics creates a new Metrics instance @@ -151,3 +187,31 @@ func (m *Metrics) Stop() error { defer cancel() return m.s.Shutdown(ctx) } + +// GetInstrumentedHTTPClient sets up a http client that emits prometheus metrics +func GetInstrumentedHTTPClient(endpoint string) (*http.Client, error) { + host := endpoint + // try to parse as url (so that we do not expose auth uuid in metrics) + endpointURL, err := url.Parse(endpoint) + if err == nil { + host = endpointURL.Host + } + labels := prometheus.Labels{"host": host} + rpcCounterMetric, err := RPCCount.CurryWith(labels) + if err != nil { + return nil, err + } + rpcLatencyMetric, err := RPCLatency.CurryWith(labels) + if err != nil { + return nil, err + } + + transport := http.DefaultTransport + transport = promhttp.InstrumentRoundTripperDuration(rpcLatencyMetric, transport) + transport = promhttp.InstrumentRoundTripperCounter(rpcCounterMetric, transport) + transport = promhttp.InstrumentRoundTripperInFlight(RPCInProgress.With(labels), transport) + + return &http.Client{ + Transport: transport, + }, nil +} diff --git a/zetaclient/metrics/metrics_test.go b/zetaclient/metrics/metrics_test.go index b73bf00530..239a6391c4 100644 --- a/zetaclient/metrics/metrics_test.go +++ b/zetaclient/metrics/metrics_test.go @@ -1,10 +1,16 @@ package metrics import ( + "fmt" + "io" "net/http" + "strings" "testing" "time" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/testutil" + "github.com/zeta-chain/zetacore/pkg/chains" . "gopkg.in/check.v1" ) @@ -23,20 +29,60 @@ func (ms *MetricsSuite) SetUpSuite(c *C) { ms.m = m } -func (ms *MetricsSuite) TestMetrics(c *C) { +// assert that the curried metric actually uses the same underlying storage +func (ms *MetricsSuite) TestCurryWith(c *C) { + rpcTotalsC := RPCCount.MustCurryWith(prometheus.Labels{"host": "test"}) + rpcTotalsC.With(prometheus.Labels{"code": "400"}).Add(1.0) + + rpcCtr := testutil.ToFloat64(RPCCount.With(prometheus.Labels{"host": "test", "code": "400"})) + c.Assert(rpcCtr, Equals, 1.0) + + RPCCount.Reset() +} + +func (ms *MetricsSuite) Test_RPCCount(c *C) { GetFilterLogsPerChain.WithLabelValues("chain1").Inc() GetFilterLogsPerChain.WithLabelValues("chain2").Inc() GetFilterLogsPerChain.WithLabelValues("chain2").Inc() time.Sleep(1 * time.Second) - res, err := http.Get("http://127.0.0.1:8886/metrics") + + chain1Ctr := testutil.ToFloat64(GetFilterLogsPerChain.WithLabelValues("chain1")) + c.Assert(chain1Ctr, Equals, 1.0) + + httpClient, err := GetInstrumentedHTTPClient("http://127.0.0.1:8886/myauthuuid") c.Assert(err, IsNil) - c.Assert(res.StatusCode, Equals, http.StatusOK) - defer res.Body.Close() - //out, err := ioutil.ReadAll(res.Body) - //fmt.Println(string(out)) - res, err = http.Get("http://127.0.0.1:8886") + res, err := httpClient.Get("http://127.0.0.1:8886") c.Assert(err, IsNil) + defer res.Body.Close() c.Assert(res.StatusCode, Equals, http.StatusOK) + + res, err = httpClient.Get("http://127.0.0.1:8886/metrics") + c.Assert(err, IsNil) defer res.Body.Close() + c.Assert(res.StatusCode, Equals, http.StatusOK) + body, err := io.ReadAll(res.Body) + c.Assert(err, IsNil) + metricsBody := string(body) + c.Assert(strings.Contains(metricsBody, fmt.Sprintf("%s_%s", ZetaClientNamespace, "rpc_count")), Equals, true) + + // assert that rpc count is being incremented at all + rpcCount := testutil.ToFloat64(RPCCount) + c.Assert(rpcCount, Equals, 2.0) + + // assert that rpc count is being incremented correctly + rpcCount = testutil.ToFloat64(RPCCount.With(prometheus.Labels{"host": "127.0.0.1:8886", "code": "200"})) + c.Assert(rpcCount, Equals, 2.0) + + // assert that rpc count is not being incremented incorrectly + rpcCount = testutil.ToFloat64(RPCCount.With(prometheus.Labels{"host": "127.0.0.1:8886", "code": "502"})) + c.Assert(rpcCount, Equals, 0.0) +} + +func (ms *MetricsSuite) Test_RelayerKeyBalance(c *C) { + RelayerKeyBalance.WithLabelValues(chains.SolanaDevnet.Name).Set(2.1564) + + // assert that relayer key balance is being set correctly + balance := testutil.ToFloat64(RelayerKeyBalance.WithLabelValues(chains.SolanaDevnet.Name)) + c.Assert(balance, Equals, 2.1564) } diff --git a/zetaclient/orchestrator/bootstap_test.go b/zetaclient/orchestrator/bootstap_test.go index 555c830df5..bac8507f7c 100644 --- a/zetaclient/orchestrator/bootstap_test.go +++ b/zetaclient/orchestrator/bootstap_test.go @@ -6,6 +6,7 @@ import ( "github.com/rs/zerolog" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/pkg/ptr" observertypes "github.com/zeta-chain/zetacore/x/observer/types" @@ -50,7 +51,7 @@ func TestCreateSignerMap(t *testing.T) { cfg.BitcoinConfig = btcConfig // Given AppContext - app := zctx.New(cfg, log) + app := zctx.New(cfg, nil, log) ctx := zctx.WithAppContext(context.Background(), app) // Given chain & chainParams "fetched" from zetacore @@ -172,36 +173,6 @@ func TestCreateSignerMap(t *testing.T) { hasSigner(t, signers, chains.BitcoinMainnet.ChainId) }) - t.Run("Polygon is there but not supported, should be disabled", func(t *testing.T) { - // ARRANGE - // Given updated data from zetacore containing polygon chain - supportedChain, evmParams, btcParams, solParams := chainParams([]chains.Chain{ - chains.Ethereum, - chains.Polygon, - chains.BitcoinMainnet, - }) - - // BUT (!) it's disabled via zetacore - evmParams[chains.Polygon.ChainId].IsSupported = false - - mustUpdateAppContext(t, app, supportedChain, evmParams, btcParams, solParams) - - // Should have signer BEFORE disabling - hasSigner(t, signers, chains.Polygon.ChainId) - - // ACT - added, removed, err := syncSignerMap(ctx, tss, baseLogger, ts, &signers) - - // ASSERT - assert.NoError(t, err) - assert.Equal(t, 0, added) - assert.Equal(t, 1, removed) - - hasSigner(t, signers, chains.Ethereum.ChainId) - missesSigner(t, signers, chains.Polygon.ChainId) - hasSigner(t, signers, chains.BitcoinMainnet.ChainId) - }) - t.Run("No changes", func(t *testing.T) { // ARRANGE before := len(signers) @@ -259,7 +230,7 @@ func TestCreateChainObserverMap(t *testing.T) { cfg.SolanaConfig = solConfig // Given AppContext - app := zctx.New(cfg, log) + app := zctx.New(cfg, nil, log) ctx := zctx.WithAppContext(context.Background(), app) // Given chain & chainParams "fetched" from zetacore @@ -401,36 +372,6 @@ func TestCreateChainObserverMap(t *testing.T) { hasObserver(t, observers, chains.BitcoinMainnet.ChainId) }) - t.Run("Polygon is there but not supported, should be disabled", func(t *testing.T) { - // ARRANGE - // Given updated data from zetacore containing polygon chain - supportedChain, evmParams, btcParams, solParams := chainParams([]chains.Chain{ - chains.Ethereum, - chains.Polygon, - chains.BitcoinMainnet, - }) - - // BUT (!) it's disabled via zetacore - evmParams[chains.Polygon.ChainId].IsSupported = false - - mustUpdateAppContext(t, app, supportedChain, evmParams, btcParams, solParams) - - // Should have signer BEFORE disabling - hasObserver(t, observers, chains.Polygon.ChainId) - - // ACT - added, removed, err := syncObserverMap(ctx, client, tss, dbPath, baseLogger, ts, &observers) - - // ASSERT - assert.NoError(t, err) - assert.Equal(t, 0, added) - assert.Equal(t, 1, removed) - - hasObserver(t, observers, chains.Ethereum.ChainId) - missesObserver(t, observers, chains.Polygon.ChainId) - hasObserver(t, observers, chains.BitcoinMainnet.ChainId) - }) - t.Run("No changes", func(t *testing.T) { // ARRANGE before := len(observers) @@ -447,69 +388,54 @@ func TestCreateChainObserverMap(t *testing.T) { }) } -func chainParams(supportedChains []chains.Chain) ( - []chains.Chain, - map[int64]*observertypes.ChainParams, - *observertypes.ChainParams, - *observertypes.ChainParams, -) { - var ( - evmParams = make(map[int64]*observertypes.ChainParams) - btcParams = &observertypes.ChainParams{} - solParams = &observertypes.ChainParams{} - ) +func chainParams(supportedChains []chains.Chain) ([]chains.Chain, map[int64]*observertypes.ChainParams) { + params := make(map[int64]*observertypes.ChainParams) for _, chain := range supportedChains { - if chains.IsBitcoinChain(chain.ChainId, nil) { - btcParams = &observertypes.ChainParams{ - ChainId: chain.ChainId, - IsSupported: true, - } - + chainID := chain.ChainId + if chains.IsBitcoinChain(chainID, nil) { + p := mocks.MockChainParams(chainID, 100) + params[chainID] = &p continue } - if chains.IsSolanaChain(chain.ChainId, nil) { - solParams = &observertypes.ChainParams{ - ChainId: chain.ChainId, - IsSupported: true, - GatewayAddress: solanaGatewayAddress, - } + if chains.IsSolanaChain(chainID, nil) { + p := mocks.MockChainParams(chainID, 100) + p.GatewayAddress = solanaGatewayAddress + params[chainID] = &p + continue } - if chains.IsEVMChain(chain.ChainId, nil) { - evmParams[chain.ChainId] = ptr.Ptr(mocks.MockChainParams(chain.ChainId, 100)) + if chains.IsEVMChain(chainID, nil) { + params[chainID] = ptr.Ptr(mocks.MockChainParams(chainID, 100)) + continue } } - return supportedChains, evmParams, btcParams, solParams + return supportedChains, params } func mustUpdateAppContextChainParams(t *testing.T, app *zctx.AppContext, chains []chains.Chain) { - supportedChain, evmParams, btcParams, solParams := chainParams(chains) - mustUpdateAppContext(t, app, supportedChain, evmParams, btcParams, solParams) + supportedChain, params := chainParams(chains) + mustUpdateAppContext(t, app, supportedChain, nil, params) } func mustUpdateAppContext( - _ *testing.T, + t *testing.T, app *zctx.AppContext, - chains []chains.Chain, - evmParams map[int64]*observertypes.ChainParams, - utxoParams *observertypes.ChainParams, - solParams *observertypes.ChainParams, + chains, additionalChains []chains.Chain, + chainParams map[int64]*observertypes.ChainParams, ) { - app.Update( - ptr.Ptr(app.GetKeygen()), + err := app.Update( + app.GetKeygen(), chains, - evmParams, - utxoParams, - solParams, - app.GetCurrentTssPubKey(), + additionalChains, + chainParams, + "tssPubKey", app.GetCrossChainFlags(), - app.GetAdditionalChains(), - nil, - false, ) + + require.NoError(t, err) } func hasSigner(t *testing.T, signers map[int64]interfaces.ChainSigner, chainId int64) { diff --git a/zetaclient/orchestrator/bootstrap.go b/zetaclient/orchestrator/bootstrap.go index cffb9085c7..61d2960468 100644 --- a/zetaclient/orchestrator/bootstrap.go +++ b/zetaclient/orchestrator/bootstrap.go @@ -5,10 +5,10 @@ import ( ethcommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" + ethrpc "github.com/ethereum/go-ethereum/rpc" solrpc "github.com/gagliardetto/solana-go/rpc" "github.com/pkg/errors" - "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/zetaclient/chains/base" btcobserver "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin/observer" "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin/rpc" @@ -21,6 +21,7 @@ import ( "github.com/zeta-chain/zetacore/zetaclient/config" zctx "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/zetaclient/db" + "github.com/zeta-chain/zetacore/zetaclient/keys" "github.com/zeta-chain/zetacore/zetaclient/metrics" ) @@ -84,20 +85,14 @@ func syncSignerMap( } ) - // EVM signers - for _, evmConfig := range app.Config().GetAllEVMConfigs() { - chainID := evmConfig.Chain.ChainId - - evmChainParams, found := app.GetEVMChainParams(chainID) - switch { - case !found: - logger.Std.Warn().Msgf("Unable to find chain params for EVM chain %d", chainID) - continue - case !evmChainParams.IsSupported: - logger.Std.Warn().Msgf("EVM chain %d is not supported", chainID) + for _, chain := range app.ListChains() { + // skip ZetaChain + if chain.IsZeta() { continue } + chainID := chain.ID() + presentChainIDs = append(presentChainIDs, chainID) // noop for existing signers @@ -106,118 +101,92 @@ func syncSignerMap( } var ( - mpiAddress = ethcommon.HexToAddress(evmChainParams.ConnectorContractAddress) - erc20CustodyAddress = ethcommon.HexToAddress(evmChainParams.Erc20CustodyContractAddress) + params = chain.Params() + rawChain = chain.RawChain() ) - signer, err := evmsigner.NewSigner( - ctx, - evmConfig.Chain, - tss, - ts, - logger, - evmConfig.Endpoint, - config.GetConnectorABI(), - config.GetERC20CustodyABI(), - mpiAddress, - erc20CustodyAddress, - ) - if err != nil { - logger.Std.Error().Err(err).Msgf("Unable to construct signer for EVM chain %d", chainID) - continue - } - - addSigner(chainID, signer) - } - - // BTC signer - // Emulate same loop semantics as for EVM chains - for i := 0; i < 1; i++ { - btcChain, btcChainParams, btcChainParamsFound := app.GetBTCChainParams() - switch { - case !btcChainParamsFound: - logger.Std.Warn().Msgf("Unable to find chain params for BTC chain") - continue - case !btcChainParams.IsSupported: - logger.Std.Warn().Msgf("BTC chain is not supported") - continue - } - - chainID := btcChainParams.ChainId - - presentChainIDs = append(presentChainIDs, chainID) - - // noop - if mapHas(signers, chainID) { - continue - } - - // get BTC config - cfg, found := app.Config().GetBTCConfig() - if !found { - logger.Std.Error().Msgf("Unable to find BTC config for chain %d", chainID) - continue - } - - signer, err := btcsigner.NewSigner(btcChain, tss, ts, logger, cfg) - if err != nil { - logger.Std.Error().Err(err).Msgf("Unable to construct signer for BTC chain %d", chainID) - continue - } - - addSigner(chainID, signer) - } - - // Solana signer - // Emulate same loop semantics as for EVM chains - for i := 0; i < 1; i++ { - solChain, solChainParams, solChainParamsFound := app.GetSolanaChainParams() switch { - case !solChainParamsFound: - logger.Std.Warn().Msgf("Unable to find chain params for Solana chain") - continue - case !solChainParams.IsSupported: - logger.Std.Warn().Msgf("Solana chain is not supported") - continue - } - - chainID := solChainParams.ChainId - presentChainIDs = append(presentChainIDs, chainID) - - // noop - if mapHas(signers, chainID) { - continue - } - - // get Solana config - cfg, found := app.Config().GetSolanaConfig() - if !found { - logger.Std.Error().Msgf("Unable to find Solana config for chain %d", chainID) - continue + case chain.IsEVM(): + var ( + mpiAddress = ethcommon.HexToAddress(chain.Params().ConnectorContractAddress) + erc20CustodyAddress = ethcommon.HexToAddress(chain.Params().Erc20CustodyContractAddress) + ) + + cfg, found := app.Config().GetEVMConfig(chainID) + if !found || cfg.Empty() { + logger.Std.Warn().Msgf("Unable to find EVM config for chain %d", chainID) + continue + } + + signer, err := evmsigner.NewSigner( + ctx, + *rawChain, + tss, + ts, + logger, + cfg.Endpoint, + config.GetConnectorABI(), + config.GetERC20CustodyABI(), + mpiAddress, + erc20CustodyAddress, + ) + if err != nil { + logger.Std.Error().Err(err).Msgf("Unable to construct signer for EVM chain %d", chainID) + continue + } + + addSigner(chainID, signer) + case chain.IsUTXO(): + cfg, found := app.Config().GetBTCConfig() + if !found { + logger.Std.Warn().Msgf("Unable to find UTXO config for chain %d", chainID) + continue + } + + signer, err := btcsigner.NewSigner(*rawChain, tss, ts, logger, cfg) + if err != nil { + logger.Std.Error().Err(err).Msgf("Unable to construct signer for UTXO chain %d", chainID) + continue + } + + addSigner(chainID, signer) + case chain.IsSolana(): + cfg, found := app.Config().GetSolanaConfig() + if !found { + logger.Std.Warn().Msgf("Unable to find SOL config for chain %d", chainID) + continue + } + + // create Solana client + rpcClient := solrpc.New(cfg.Endpoint) + if rpcClient == nil { + // should never happen + logger.Std.Error().Msgf("Unable to create SOL client from endpoint %s", cfg.Endpoint) + continue + } + + // try loading Solana relayer key if present + password := chain.RelayerKeyPassword() + relayerKey, err := keys.LoadRelayerKey(app.Config().GetRelayerKeyPath(), rawChain.Network, password) + if err != nil { + logger.Std.Error().Err(err).Msg("Unable to load Solana relayer key") + continue + } + + // create Solana signer + signer, err := solanasigner.NewSigner(*rawChain, *params, rpcClient, tss, relayerKey, ts, logger) + if err != nil { + logger.Std.Error().Err(err).Msgf("Unable to construct signer for SOL chain %d", chainID) + continue + } + + addSigner(chainID, signer) + default: + logger.Std.Warn(). + Int64("signer.chain_id", chain.ID()). + Str("signer.chain_name", chain.RawChain().Name). + Msgf("Unable to create a signer") } - - // create Solana client - rpcClient := solrpc.New(cfg.Endpoint) - if rpcClient == nil { - // should never happen - logger.Std.Error().Msgf("Unable to create Solana client from endpoint %s", cfg.Endpoint) - continue - } - - // load the Solana private key - solanaKey, err := app.Config().LoadSolanaPrivateKey() - if err != nil { - logger.Std.Error().Err(err).Msg("Unable to get Solana private key") - } - - // create Solana signer - signer, err := solanasigner.NewSigner(solChain, *solChainParams, rpcClient, tss, solanaKey, ts, logger) - if err != nil { - logger.Std.Error().Err(err).Msgf("Unable to construct signer for Solana chain %d", chainID) - continue - } - - addSigner(chainID, signer) } // Remove all disabled signers @@ -284,86 +253,13 @@ func syncObserverMap( } ) - // EVM observers - for _, evmConfig := range app.Config().GetAllEVMConfigs() { - var chainID = evmConfig.Chain.ChainId - - chain, found := chains.GetChainFromChainID(chainID, app.GetAdditionalChains()) - if !found { - logger.Std.Error().Msgf("Unable to find chain %d", chainID) - continue - } - - chainParams, found := app.GetEVMChainParams(chainID) - switch { - case !found: - logger.Std.Error().Msgf("Unable to find chain params for EVM chain %d", chainID) - continue - case !chainParams.IsSupported: - logger.Std.Error().Msgf("EVM chain %d is not supported", chainID) - continue - } - - presentChainIDs = append(presentChainIDs, chainID) - - // noop - if mapHas(observerMap, chainID) { - continue - } - - // create EVM client - evmClient, err := ethclient.DialContext(ctx, evmConfig.Endpoint) - if err != nil { - logger.Std.Error().Err(err).Str("rpc.endpoint", evmConfig.Endpoint).Msgf("Unable to dial EVM RPC") - continue - } - - database, err := db.NewFromSqlite(dbpath, chain.Name, true) - if err != nil { - logger.Std.Error().Err(err).Msgf("Unable to open a database for EVM chain %q", chain.Name) - continue - } - - // create EVM chain observer - observer, err := evmobserver.NewObserver( - ctx, - evmConfig, - evmClient, - *chainParams, - client, - tss, - database, - logger, - ts, - ) - if err != nil { - logger.Std.Error().Err(err).Msgf("NewObserver error for EVM chain %s", evmConfig.Chain.String()) - continue - } - - addObserver(chainID, observer) - } - - // Emulate same loop semantics as for EVM chains - // create BTC chain observer - for i := 0; i < 1; i++ { - btcChain, btcConfig, btcEnabled := app.GetBTCChainAndConfig() - if !btcEnabled { - continue - } - - chainID := btcChain.ChainId - - _, btcChainParams, found := app.GetBTCChainParams() - switch { - case !found: - logger.Std.Warn().Msgf("Unable to find chain params for BTC chain %d", chainID) - continue - case !btcChainParams.IsSupported: - logger.Std.Warn().Msgf("BTC chain %d is not supported", chainID) + for _, chain := range app.ListChains() { + // skip ZetaChain + if chain.IsZeta() { continue } + chainID := chain.ID() presentChainIDs = append(presentChainIDs, chainID) // noop @@ -371,100 +267,133 @@ func syncObserverMap( continue } - btcRPC, err := rpc.NewRPCClient(btcConfig) - if err != nil { - logger.Std.Error().Err(err).Msgf("unable to create rpc client for BTC chain %d", chainID) - continue - } - - database, err := db.NewFromSqlite(dbpath, btcDatabaseFilename, true) - if err != nil { - logger.Std.Error().Err(err).Msgf("unable to open database for BTC chain %d", chainID) - continue - } - - btcObserver, err := btcobserver.NewObserver( - btcChain, - btcRPC, - *btcChainParams, - client, - tss, - database, - logger, - ts, - ) - if err != nil { - logger.Std.Error().Err(err).Msgf("NewObserver error for BTC chain %d", chainID) - continue - } - - addObserver(chainID, btcObserver) - } - - // Emulate same loop semantics as for EVM chains - // create SOL chain observer - for i := 0; i < 1; i++ { - solChain, solConfig, solEnabled := app.GetSolanaChainAndConfig() - if !solEnabled { - continue - } - var ( - chainID = solChain.ChainId + params = chain.Params() + rawChain = chain.RawChain() + chainName = rawChain.Name ) - chain, found := chains.GetChainFromChainID(chainID, app.GetAdditionalChains()) - if !found { - logger.Std.Error().Msgf("Unable to find chain %d", chainID) - continue - } - - _, solanaChainParams, found := app.GetSolanaChainParams() switch { - case !found: - logger.Std.Warn().Msgf("Unable to find chain params for SOL chain %d", chainID) - continue - case !solanaChainParams.IsSupported: - logger.Std.Warn().Msgf("SOL chain %d is not supported", chainID) - continue + case chain.IsEVM(): + cfg, found := app.Config().GetEVMConfig(chainID) + if !found || cfg.Empty() { + logger.Std.Warn().Msgf("Unable to find EVM config for chain %d", chainID) + continue + } + + httpClient, err := metrics.GetInstrumentedHTTPClient(cfg.Endpoint) + if err != nil { + logger.Std.Error().Err(err).Str("rpc.endpoint", cfg.Endpoint).Msgf("Unable to create HTTP client") + continue + } + rpcClient, err := ethrpc.DialHTTPWithClient(cfg.Endpoint, httpClient) + if err != nil { + logger.Std.Error().Err(err).Str("rpc.endpoint", cfg.Endpoint).Msgf("Unable to dial EVM RPC") + continue + } + evmClient := ethclient.NewClient(rpcClient) + + database, err := db.NewFromSqlite(dbpath, chainName, true) + if err != nil { + logger.Std.Error().Err(err).Msgf("Unable to open a database for EVM chain %q", chainName) + continue + } + + // create EVM chain observer + observer, err := evmobserver.NewObserver( + ctx, + cfg, + evmClient, + *params, + client, + tss, + database, + logger, + ts, + ) + if err != nil { + logger.Std.Error().Err(err).Msgf("NewObserver error for EVM chain %d", chainID) + continue + } + + addObserver(chainID, observer) + case chain.IsUTXO(): + cfg, found := app.Config().GetBTCConfig() + if !found { + logger.Std.Warn().Msgf("Unable to find chain params for BTC chain %d", chainID) + continue + } + + btcRPC, err := rpc.NewRPCClient(cfg) + if err != nil { + logger.Std.Error().Err(err).Msgf("unable to create rpc client for BTC chain %d", chainID) + continue + } + + database, err := db.NewFromSqlite(dbpath, btcDatabaseFilename, true) + if err != nil { + logger.Std.Error().Err(err).Msgf("unable to open database for BTC chain %d", chainID) + continue + } + + btcObserver, err := btcobserver.NewObserver( + *rawChain, + btcRPC, + *params, + client, + tss, + database, + logger, + ts, + ) + if err != nil { + logger.Std.Error().Err(err).Msgf("NewObserver error for BTC chain %d", chainID) + continue + } + + addObserver(chainID, btcObserver) + case chain.IsSolana(): + cfg, found := app.Config().GetSolanaConfig() + if !found { + logger.Std.Warn().Msgf("Unable to find chain params for SOL chain %d", chainID) + continue + } + + rpcClient := solrpc.New(cfg.Endpoint) + if rpcClient == nil { + // should never happen + logger.Std.Error().Msg("solana create Solana client error") + continue + } + + database, err := db.NewFromSqlite(dbpath, chainName, true) + if err != nil { + logger.Std.Error().Err(err).Msgf("unable to open database for SOL chain %d", chainID) + continue + } + + solObserver, err := solbserver.NewObserver( + *rawChain, + rpcClient, + *params, + client, + tss, + database, + logger, + ts, + ) + if err != nil { + logger.Std.Error().Err(err).Msgf("NewObserver error for SOL chain %d", chainID) + continue + } + + addObserver(chainID, solObserver) + default: + logger.Std.Warn(). + Int64("observer.chain_id", chain.ID()). + Str("observer.chain_name", chain.RawChain().Name). + Msgf("Unable to create an observer") } - - presentChainIDs = append(presentChainIDs, chainID) - - // noop - if mapHas(observerMap, chainID) { - continue - } - - rpcClient := solrpc.New(solConfig.Endpoint) - if rpcClient == nil { - // should never happen - logger.Std.Error().Msgf("Unable to create Solana client from endpoint %s", solConfig.Endpoint) - continue - } - - database, err := db.NewFromSqlite(dbpath, chain.Name, true) - if err != nil { - logger.Std.Error().Err(err).Msgf("unable to open database for SOL chain %s", chain.Name) - continue - } - - solObserver, err := solbserver.NewObserver( - solChain, - rpcClient, - *solanaChainParams, - client, - tss, - database, - logger, - ts, - ) - if err != nil { - logger.Std.Error().Err(err).Msgf("NewObserver error for SOL chain %d", chainID) - continue - } - - addObserver(chainID, solObserver) } // Remove all disabled observers diff --git a/zetaclient/orchestrator/orchestrator.go b/zetaclient/orchestrator/orchestrator.go index edcaa3d9d5..b32c28d5b5 100644 --- a/zetaclient/orchestrator/orchestrator.go +++ b/zetaclient/orchestrator/orchestrator.go @@ -12,15 +12,15 @@ import ( ethcommon "github.com/ethereum/go-ethereum/common" "github.com/pkg/errors" "github.com/rs/zerolog" + "github.com/samber/lo" "github.com/zeta-chain/zetacore/pkg/bg" - "github.com/zeta-chain/zetacore/pkg/chains" + "github.com/zeta-chain/zetacore/pkg/constant" zetamath "github.com/zeta-chain/zetacore/pkg/math" "github.com/zeta-chain/zetacore/x/crosschain/types" observertypes "github.com/zeta-chain/zetacore/x/observer/types" "github.com/zeta-chain/zetacore/zetaclient/chains/base" btcobserver "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin/observer" - "github.com/zeta-chain/zetacore/zetaclient/chains/evm" "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" solanaobserver "github.com/zeta-chain/zetacore/zetaclient/chains/solana/observer" zctx "github.com/zeta-chain/zetacore/zetaclient/context" @@ -154,37 +154,41 @@ func (oc *Orchestrator) resolveSigner(app *zctx.AppContext, chainID int64) (inte return nil, err } - // update signer chain parameters - if chains.IsEVMChain(chainID, app.GetAdditionalChains()) { - evmParams, found := app.GetEVMChainParams(chainID) - if found { - // update zeta connector and ERC20 custody addresses - zetaConnectorAddress := ethcommon.HexToAddress(evmParams.GetConnectorContractAddress()) - if zetaConnectorAddress != signer.GetZetaConnectorAddress() { - signer.SetZetaConnectorAddress(zetaConnectorAddress) - oc.logger.Info(). - Str("signer.connector_address", zetaConnectorAddress.String()). - Msgf("updated zeta connector address for chain %d", chainID) - } + chain, err := app.GetChain(chainID) + switch { + case err != nil: + return nil, err + case chain.IsZeta(): + // should not happen + return nil, fmt.Errorf("unable to resolve signer for zeta chain %d", chainID) + case chain.IsEVM(): + params := chain.Params() + + // update zeta connector and ERC20 custody addresses + zetaConnectorAddress := ethcommon.HexToAddress(params.GetConnectorContractAddress()) + if zetaConnectorAddress != signer.GetZetaConnectorAddress() { + signer.SetZetaConnectorAddress(zetaConnectorAddress) + oc.logger.Info(). + Str("signer.connector_address", zetaConnectorAddress.String()). + Msgf("updated zeta connector address for chain %d", chainID) + } - erc20CustodyAddress := ethcommon.HexToAddress(evmParams.GetErc20CustodyContractAddress()) - if erc20CustodyAddress != signer.GetERC20CustodyAddress() { - signer.SetERC20CustodyAddress(erc20CustodyAddress) - oc.logger.Info(). - Str("signer.erc20_custody", erc20CustodyAddress.String()). - Msgf("updated zeta connector address for chain %d", chainID) - } + erc20CustodyAddress := ethcommon.HexToAddress(params.GetErc20CustodyContractAddress()) + if erc20CustodyAddress != signer.GetERC20CustodyAddress() { + signer.SetERC20CustodyAddress(erc20CustodyAddress) + oc.logger.Info(). + Str("signer.erc20_custody", erc20CustodyAddress.String()). + Msgf("updated zeta connector address for chain %d", chainID) } - } else if chains.IsSolanaChain(chainID, app.GetAdditionalChains()) { - _, solParams, found := app.GetSolanaChainParams() - if found { - // update solana gateway address - if solParams.GatewayAddress != signer.GetGatewayAddress() { - signer.SetGatewayAddress(solParams.GatewayAddress) - oc.logger.Info(). - Str("signer.gateway_address", solParams.GatewayAddress). - Msgf("updated gateway address for chain %d", chainID) - } + case chain.IsSolana(): + params := chain.Params() + + // update solana gateway address + if params.GatewayAddress != signer.GetGatewayAddress() { + signer.SetGatewayAddress(params.GatewayAddress) + oc.logger.Info(). + Str("signer.gateway_address", params.GatewayAddress). + Msgf("updated gateway address for chain %d", chainID) } } @@ -210,31 +214,26 @@ func (oc *Orchestrator) resolveObserver(app *zctx.AppContext, chainID int64) (in return nil, err } + chain, err := app.GetChain(chainID) + switch { + case err != nil: + return nil, errors.Wrapf(err, "unable to get chain %d", chainID) + case chain.IsZeta(): + // should not happen + return nil, fmt.Errorf("unable to resolve observer for zeta chain %d", chainID) + } + // update chain observer chain parameters - curParams := observer.GetChainParams() - if chains.IsEVMChain(chainID, app.GetAdditionalChains()) { - evmParams, found := app.GetEVMChainParams(chainID) - if found && !observertypes.ChainParamsEqual(curParams, *evmParams) { - observer.SetChainParams(*evmParams) - oc.logger.Info(). - Interface("observer.chain_params", *evmParams). - Msgf("updated chain params for EVM chainID %d", chainID) - } - } else if chains.IsBitcoinChain(chainID, app.GetAdditionalChains()) { - _, btcParams, found := app.GetBTCChainParams() - if found && !observertypes.ChainParamsEqual(curParams, *btcParams) { - observer.SetChainParams(*btcParams) - oc.logger.Info(). - Interface("observer.chain_params", *btcParams). - Msgf("updated chain params for UTXO chainID %d", btcParams.ChainId) - } - } else if chains.IsSolanaChain(chainID, app.GetAdditionalChains()) { - _, solParams, found := app.GetSolanaChainParams() - if found && !observertypes.ChainParamsEqual(curParams, *solParams) { - observer.SetChainParams(*solParams) - oc.logger.Info().Msgf( - "updated chain params for Solana, new params: %v", *solParams) - } + var ( + curParams = observer.GetChainParams() + freshParams = chain.Params() + ) + + if !observertypes.ChainParamsEqual(curParams, *freshParams) { + observer.SetChainParams(*freshParams) + oc.logger.Info(). + Interface("observer.chain_params", *freshParams). + Msgf("updated chain params for chainID %d", chainID) } return observer, nil @@ -253,10 +252,10 @@ func (oc *Orchestrator) getObserver(chainID int64) (interfaces.ChainObserver, er } // GetPendingCctxsWithinRateLimit get pending cctxs across foreign chains within rate limit -func (oc *Orchestrator) GetPendingCctxsWithinRateLimit( - ctx context.Context, - foreignChains []chains.Chain, -) (map[int64][]*types.CrossChainTx, error) { +func (oc *Orchestrator) GetPendingCctxsWithinRateLimit(ctx context.Context, chainIDs []int64) ( + map[int64][]*types.CrossChainTx, + error, +) { // get rate limiter flags rateLimitFlags, err := oc.zetacoreClient.GetRateLimiterFlags(ctx) if err != nil { @@ -269,10 +268,10 @@ func (oc *Orchestrator) GetPendingCctxsWithinRateLimit( // fallback to non-rate-limited query if rate limiter is not usable cctxsMap := make(map[int64][]*types.CrossChainTx) if !rateLimiterUsable { - for _, chain := range foreignChains { - resp, _, err := oc.zetacoreClient.ListPendingCCTX(ctx, chain.ChainId) + for _, chainID := range chainIDs { + resp, _, err := oc.zetacoreClient.ListPendingCCTX(ctx, chainID) if err == nil && resp != nil { - cctxsMap[chain.ChainId] = resp + cctxsMap[chainID] = resp } } return cctxsMap, nil @@ -352,51 +351,68 @@ func (oc *Orchestrator) runScheduler(ctx context.Context) error { // set current hot key burn rate metrics.HotKeyBurnRate.Set(float64(oc.ts.HotKeyBurnRate.GetBurnRate().Int64())) - // get supported external chains - externalChains := app.GetEnabledExternalChains() + // get chain ids without zeta chain + chainIDs := lo.FilterMap(app.ListChains(), func(c zctx.Chain, _ int) (int64, bool) { + return c.ID(), !c.IsZeta() + }) // query pending cctxs across all external chains within rate limit - cctxMap, err := oc.GetPendingCctxsWithinRateLimit(ctx, externalChains) + cctxMap, err := oc.GetPendingCctxsWithinRateLimit(ctx, chainIDs) if err != nil { oc.logger.Error().Err(err).Msgf("runScheduler: GetPendingCctxsWithinRatelimit failed") } // schedule keysign for pending cctxs on each chain - for _, c := range externalChains { + for _, chain := range app.ListChains() { + // skip zeta chain + if chain.IsZeta() { + continue + } + + chainID := chain.ID() + // get cctxs from map and set pending transactions prometheus gauge - cctxList := cctxMap[c.ChainId] - metrics.PendingTxsPerChain.WithLabelValues(c.Name).Set(float64(len(cctxList))) + cctxList := cctxMap[chainID] + + metrics.PendingTxsPerChain. + WithLabelValues(chain.Name()). + Set(float64(len(cctxList))) + if len(cctxList) == 0 { continue } // update chain parameters for signer and chain observer - signer, err := oc.resolveSigner(app, c.ChainId) + signer, err := oc.resolveSigner(app, chainID) if err != nil { oc.logger.Error().Err(err). - Msgf("runScheduler: unable to resolve signer for chain %d", c.ChainId) + Msgf("runScheduler: unable to resolve signer for chain %d", chainID) continue } - ob, err := oc.resolveObserver(app, c.ChainId) + + ob, err := oc.resolveObserver(app, chainID) if err != nil { oc.logger.Error().Err(err). - Msgf("runScheduler: resolveObserver failed for chain %d", c.ChainId) + Msgf("runScheduler: resolveObserver failed for chain %d", chainID) continue } - if !app.IsOutboundObservationEnabled(ob.GetChainParams()) { + + if !app.IsOutboundObservationEnabled() { continue } // #nosec G115 range is verified zetaHeight := uint64(bn) - if chains.IsEVMChain(c.ChainId, app.GetAdditionalChains()) { - oc.ScheduleCctxEVM(ctx, zetaHeight, c.ChainId, cctxList, ob, signer) - } else if chains.IsBitcoinChain(c.ChainId, app.GetAdditionalChains()) { - oc.ScheduleCctxBTC(ctx, zetaHeight, c.ChainId, cctxList, ob, signer) - } else if chains.IsSolanaChain(c.ChainId, app.GetAdditionalChains()) { - oc.ScheduleCctxSolana(ctx, zetaHeight, c.ChainId, cctxList, ob, signer) - } else { - oc.logger.Error().Msgf("runScheduler: unsupported chain %d", c.ChainId) + + switch { + case chain.IsEVM(): + oc.ScheduleCctxEVM(ctx, zetaHeight, chainID, cctxList, ob, signer) + case chain.IsUTXO(): + oc.ScheduleCctxBTC(ctx, zetaHeight, chainID, cctxList, ob, signer) + case chain.IsSolana(): + oc.ScheduleCctxSolana(ctx, zetaHeight, chainID, cctxList, ob, signer) + default: + oc.logger.Error().Msgf("runScheduler: no scheduler found chain %d", chainID) continue } } @@ -647,8 +663,13 @@ func (oc *Orchestrator) ScheduleCctxSolana( // 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 { - // check every other zeta block - const cadence = 2 * evm.ZetaBlockTime + // sync observers and signers right away to speed up zetaclient startup + if err := oc.syncObserverSigner(ctx); err != nil { + oc.logger.Error().Err(err).Msg("runObserverSignerSync: syncObserverSigner failed for initial sync") + } + + // sync observer and signer every 10 blocks (approx. 1 minute) + const cadence = 10 * constant.ZetaBlockTime ticker := time.NewTicker(cadence) defer ticker.Stop() diff --git a/zetaclient/orchestrator/orchestrator_test.go b/zetaclient/orchestrator/orchestrator_test.go index af4ca5c346..21d3998a84 100644 --- a/zetaclient/orchestrator/orchestrator_test.go +++ b/zetaclient/orchestrator/orchestrator_test.go @@ -7,6 +7,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ethcommon "github.com/ethereum/go-ethereum/common" "github.com/rs/zerolog" + "github.com/samber/lo" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" zctx "github.com/zeta-chain/zetacore/zetaclient/context" @@ -24,172 +25,64 @@ import ( "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" ) -// MockOrchestrator creates a mock orchestrator for testing -func MockOrchestrator( - t *testing.T, - zetacoreClient interfaces.ZetacoreClient, - evmChain, btcChain, solChain *chains.Chain, - evmChainParams, btcChainParams, solChainParams *observertypes.ChainParams, -) *Orchestrator { - // create maps to store signers and observers - signerMap := make(map[int64]interfaces.ChainSigner) - observerMap := make(map[int64]interfaces.ChainObserver) - - // a functor to add a signer and observer to the maps - addSignerObserver := func(chain *chains.Chain, signer interfaces.ChainSigner, observer interfaces.ChainObserver) { - signerMap[chain.ChainId] = signer - observerMap[chain.ChainId] = observer - } - - // create evm mock signer/observer - if evmChain != nil { - evmSigner := mocks.NewEVMSigner( - *evmChain, - ethcommon.HexToAddress(evmChainParams.ConnectorContractAddress), - ethcommon.HexToAddress(evmChainParams.Erc20CustodyContractAddress), - ) - evmObserver := mocks.NewEVMObserver(evmChainParams) - addSignerObserver(evmChain, evmSigner, evmObserver) - } - - // create btc mock signer/observer - if btcChain != nil { - btcSigner := mocks.NewBTCSigner() - btcObserver := mocks.NewBTCObserver(btcChainParams) - addSignerObserver(btcChain, btcSigner, btcObserver) - } - - // create solana mock signer/observer - if solChain != nil { - solSigner := mocks.NewSolanaSigner() - solObserver := mocks.NewSolanaObserver(solChainParams) - addSignerObserver(solChain, solSigner, solObserver) - } - - // create orchestrator - orchestrator := &Orchestrator{ - zetacoreClient: zetacoreClient, - signerMap: signerMap, - observerMap: observerMap, - } - return orchestrator -} - -func CreateAppContext( - evmChain, btcChain, solChain chains.Chain, - evmChainParams, btcChainParams, solChainParams *observertypes.ChainParams, -) *zctx.AppContext { - // new config - cfg := config.New(false) - cfg.EVMChainConfigs[evmChain.ChainId] = config.EVMConfig{ - Chain: evmChain, - } - cfg.BitcoinConfig = config.BTCConfig{ - RPCHost: "localhost", - } - // new AppContext - appContext := zctx.New(cfg, zerolog.Nop()) - evmChainParamsMap := make(map[int64]*observertypes.ChainParams) - evmChainParamsMap[evmChain.ChainId] = evmChainParams - ccFlags := sample.CrosschainFlags() - verificationFlags := sample.HeaderSupportedChains() +func Test_GetUpdatedSigner(t *testing.T) { + // initial parameters for orchestrator creation + var ( + evmChain = chains.Ethereum + btcChain = chains.BitcoinMainnet + solChain = chains.SolanaMainnet + ) - // feed chain params - appContext.Update( - &observertypes.Keygen{}, - []chains.Chain{evmChain, btcChain, solChain}, - evmChainParamsMap, - btcChainParams, - solChainParams, - "", - *ccFlags, - []chains.Chain{}, - verificationFlags, - true, + var ( + evmChainParams = mocks.MockChainParams(evmChain.ChainId, 100) + btcChainParams = mocks.MockChainParams(btcChain.ChainId, 100) + solChainParams = mocks.MockChainParams(solChain.ChainId, 100) ) - return appContext -} -func Test_GetUpdatedSigner(t *testing.T) { - // initial parameters for orchestrator creation - evmChain := chains.Ethereum - btcChain := chains.BitcoinMainnet - solChain := chains.SolanaMainnet - evmChainParams := &observertypes.ChainParams{ - ChainId: evmChain.ChainId, - ConnectorContractAddress: testutils.ConnectorAddresses[evmChain.ChainId].Hex(), - Erc20CustodyContractAddress: testutils.CustodyAddresses[evmChain.ChainId].Hex(), - } - btcChainParams := &observertypes.ChainParams{} - solChainParams := &observertypes.ChainParams{ - ChainId: solChain.ChainId, - GatewayAddress: solanacontracts.SolanaGatewayProgramID, - } + solChainParams.GatewayAddress = solanacontracts.SolanaGatewayProgramID - // new evm chain params in AppContext - evmChainParamsNew := &observertypes.ChainParams{ - ChainId: evmChain.ChainId, - ConnectorContractAddress: testutils.OtherAddress1, - Erc20CustodyContractAddress: testutils.OtherAddress2, - } + // new chain params in AppContext + evmChainParamsNew := mocks.MockChainParams(evmChainParams.ChainId, 100) + evmChainParamsNew.ConnectorContractAddress = testutils.OtherAddress1 + evmChainParamsNew.Erc20CustodyContractAddress = testutils.OtherAddress2 // new solana chain params in AppContext - solChainParamsNew := &observertypes.ChainParams{ - ChainId: solChain.ChainId, - GatewayAddress: sample.SolanaAddress(t), - } - - t.Run("evm signer should not be found", func(t *testing.T) { - orchestrator := MockOrchestrator( - t, - nil, - &evmChain, - &btcChain, - &solChain, - evmChainParams, - btcChainParams, - solChainParams, - ) - context := CreateAppContext(evmChain, btcChain, solChain, evmChainParamsNew, btcChainParams, solChainParams) + solChainParamsNew := mocks.MockChainParams(solChain.ChainId, 100) + solChainParamsNew.GatewayAddress = sample.SolanaAddress(t) + 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) // BSC signer should not be found - _, err := orchestrator.resolveSigner(context, chains.BscMainnet.ChainId) + _, err := orchestrator.resolveSigner(appContext, chains.BscMainnet.ChainId) require.ErrorContains(t, err, "signer not found") }) - t.Run("should be able to update evm connector and erc20 custody address", func(t *testing.T) { - orchestrator := MockOrchestrator( - t, - nil, - &evmChain, - &btcChain, - &solChain, - evmChainParams, - btcChainParams, - solChainParams, - ) - context := CreateAppContext(evmChain, btcChain, solChain, evmChainParamsNew, btcChainParams, solChainParams) + + t.Run("should be able to update connector and erc20 custody address", func(t *testing.T) { + orchestrator := mockOrchestrator(t, nil, evmChain, btcChain, evmChainParams, btcChainParams) + appContext := createAppContext(t, evmChain, btcChain, evmChainParamsNew, btcChainParams) // update signer with new connector and erc20 custody address - signer, err := orchestrator.resolveSigner(context, evmChain.ChainId) + signer, err := orchestrator.resolveSigner(appContext, evmChain.ChainId) require.NoError(t, err) + require.Equal(t, testutils.OtherAddress1, signer.GetZetaConnectorAddress().Hex()) require.Equal(t, testutils.OtherAddress2, signer.GetERC20CustodyAddress().Hex()) }) + t.Run("should be able to update solana gateway address", func(t *testing.T) { - orchestrator := MockOrchestrator( - t, - nil, - &evmChain, - &btcChain, - &solChain, - evmChainParams, - btcChainParams, - solChainParams, + orchestrator := mockOrchestrator(t, nil, + evmChain, btcChain, solChain, + evmChainParams, btcChainParams, solChainParams, + ) + + appContext := createAppContext(t, + evmChain, btcChain, solChain, + evmChainParams, btcChainParams, solChainParamsNew, ) - context := CreateAppContext(evmChain, btcChain, solChain, evmChainParams, btcChainParams, solChainParamsNew) // update signer with new gateway address - signer, err := orchestrator.resolveSigner(context, solChain.ChainId) + signer, err := orchestrator.resolveSigner(appContext, solChain.ChainId) require.NoError(t, err) require.Equal(t, solChainParamsNew.GatewayAddress, signer.GetGatewayAddress()) }) @@ -197,21 +90,19 @@ func Test_GetUpdatedSigner(t *testing.T) { func Test_GetUpdatedChainObserver(t *testing.T) { // initial parameters for orchestrator creation - evmChain := chains.Ethereum - btcChain := chains.BitcoinMainnet - solChain := chains.SolanaMainnet - evmChainParams := &observertypes.ChainParams{ - ChainId: evmChain.ChainId, - ConnectorContractAddress: testutils.ConnectorAddresses[evmChain.ChainId].Hex(), - Erc20CustodyContractAddress: testutils.CustodyAddresses[evmChain.ChainId].Hex(), - } - btcChainParams := &observertypes.ChainParams{ - ChainId: btcChain.ChainId, - } - solChainParams := &observertypes.ChainParams{ - ChainId: solChain.ChainId, - GatewayAddress: solanacontracts.SolanaGatewayProgramID, - } + var ( + evmChain = chains.Ethereum + btcChain = chains.BitcoinMainnet + solChain = chains.SolanaMainnet + ) + + var ( + evmChainParams = mocks.MockChainParams(evmChain.ChainId, 100) + btcChainParams = mocks.MockChainParams(btcChain.ChainId, 100) + solChainParams = mocks.MockChainParams(solChain.ChainId, 100) + ) + + solChainParams.GatewayAddress = solanacontracts.SolanaGatewayProgramID // new chain params in AppContext evmChainParamsNew := &observertypes.ChainParams{ @@ -264,67 +155,91 @@ func Test_GetUpdatedChainObserver(t *testing.T) { } t.Run("evm chain observer should not be found", func(t *testing.T) { - orchestrator := MockOrchestrator( + orchestrator := mockOrchestrator( t, nil, - &evmChain, - &btcChain, - &solChain, + evmChain, + btcChain, + solChain, evmChainParams, btcChainParams, solChainParams, ) - appContext := CreateAppContext(evmChain, btcChain, solChain, evmChainParamsNew, btcChainParams, solChainParams) + appContext := createAppContext(t, evmChain, btcChain, evmChainParamsNew, btcChainParams) + // BSC chain observer should not be found _, err := orchestrator.resolveObserver(appContext, chains.BscMainnet.ChainId) require.ErrorContains(t, err, "observer not found") }) t.Run("chain params in evm chain observer should be updated successfully", func(t *testing.T) { - orchestrator := MockOrchestrator( + orchestrator := mockOrchestrator( t, nil, - &evmChain, - &btcChain, - &solChain, + evmChain, + btcChain, + solChain, evmChainParams, btcChainParams, solChainParams, ) - appContext := CreateAppContext(evmChain, btcChain, solChain, evmChainParamsNew, btcChainParams, solChainParams) + appContext := createAppContext( + t, + evmChain, + btcChain, + solChain, + evmChainParamsNew, + btcChainParams, + solChainParams, + ) + // update evm chain observer with new chain params chainOb, err := orchestrator.resolveObserver(appContext, evmChain.ChainId) require.NoError(t, err) require.NotNil(t, chainOb) require.True(t, observertypes.ChainParamsEqual(*evmChainParamsNew, chainOb.GetChainParams())) }) + t.Run("btc chain observer should not be found", func(t *testing.T) { - orchestrator := MockOrchestrator( + orchestrator := mockOrchestrator( t, nil, - &evmChain, - &btcChain, - &solChain, + evmChain, + btcChain, + solChain, evmChainParams, btcChainParams, solChainParams, ) - appContext := CreateAppContext(btcChain, btcChain, solChain, evmChainParams, btcChainParamsNew, solChainParams) + appContext := createAppContext( + t, + evmChain, + btcChain, + solChain, + evmChainParams, + btcChainParamsNew, + solChainParams, + ) + // BTC testnet chain observer should not be found _, err := orchestrator.resolveObserver(appContext, chains.BitcoinTestnet.ChainId) require.ErrorContains(t, err, "observer not found") }) t.Run("chain params in btc chain observer should be updated successfully", func(t *testing.T) { - orchestrator := MockOrchestrator( + orchestrator := mockOrchestrator( t, nil, - &evmChain, - &btcChain, - &solChain, + evmChain, btcChain, solChain, + evmChainParams, btcChainParams, solChainParams, + ) + appContext := createAppContext( + t, + evmChain, + btcChain, + solChain, evmChainParams, - btcChainParams, + btcChainParamsNew, solChainParams, ) - appContext := CreateAppContext(btcChain, btcChain, solChain, evmChainParams, btcChainParamsNew, solChainParams) // update btc chain observer with new chain params chainOb, err := orchestrator.resolveObserver(appContext, btcChain.ChainId) require.NoError(t, err) @@ -332,33 +247,37 @@ func Test_GetUpdatedChainObserver(t *testing.T) { require.True(t, observertypes.ChainParamsEqual(*btcChainParamsNew, chainOb.GetChainParams())) }) t.Run("solana chain observer should not be found", func(t *testing.T) { - orchestrator := MockOrchestrator( + orchestrator := mockOrchestrator( t, nil, - &evmChain, - &btcChain, - &solChain, + evmChain, btcChain, solChain, + evmChainParams, btcChainParams, solChainParams, + ) + + appContext := createAppContext( + t, + evmChain, + btcChain, + solChain, evmChainParams, btcChainParams, - solChainParams, + solChainParamsNew, ) - appContext := CreateAppContext(solChain, btcChain, solChain, evmChainParams, btcChainParams, solChainParamsNew) + // Solana Devnet chain observer should not be found _, err := orchestrator.resolveObserver(appContext, chains.SolanaDevnet.ChainId) require.ErrorContains(t, err, "observer not found") }) t.Run("chain params in solana chain observer should be updated successfully", func(t *testing.T) { - orchestrator := MockOrchestrator( - t, - nil, - &evmChain, - &btcChain, - &solChain, - evmChainParams, - btcChainParams, - solChainParams, + orchestrator := mockOrchestrator(t, nil, + evmChain, btcChain, solChain, + evmChainParams, btcChainParams, solChainParams, + ) + appContext := createAppContext(t, + evmChain, btcChain, solChain, + evmChainParams, btcChainParams, solChainParamsNew, ) - appContext := CreateAppContext(solChain, btcChain, solChain, evmChainParams, btcChainParams, solChainParamsNew) + // update solana chain observer with new chain params chainOb, err := orchestrator.resolveObserver(appContext, solChain.ChainId) require.NoError(t, err) @@ -527,10 +446,12 @@ func Test_GetPendingCctxsWithinRateLimit(t *testing.T) { client.WithPendingCctx(btcChain.ChainId, tt.btcCctxsFallback) // create orchestrator - orchestrator := MockOrchestrator(t, client, ðChain, &btcChain, nil, ethChainParams, btcChainParams, nil) + orchestrator := mockOrchestrator(t, client, ethChain, btcChain, ethChainParams, btcChainParams) + + chainIDs := lo.Map(foreignChains, func(c chains.Chain, _ int) int64 { return c.ChainId }) // run the test - cctxsMap, err := orchestrator.GetPendingCctxsWithinRateLimit(ctx, foreignChains) + cctxsMap, err := orchestrator.GetPendingCctxsWithinRateLimit(ctx, chainIDs) if tt.fail { assert.Error(t, err) assert.Empty(t, cctxsMap) @@ -541,3 +462,118 @@ func Test_GetPendingCctxsWithinRateLimit(t *testing.T) { }) } } + +func mockOrchestrator(t *testing.T, zetaClient interfaces.ZetacoreClient, chainsOrParams ...any) *Orchestrator { + supportedChains, obsParams := parseChainsWithParams(t, chainsOrParams...) + + var ( + signers = make(map[int64]interfaces.ChainSigner) + observers = make(map[int64]interfaces.ChainObserver) + ) + + mustFindChain := func(chainID int64) chains.Chain { + for _, c := range supportedChains { + if c.ChainId == chainID { + return c + } + } + + t.Fatalf("mock orchestrator: must find chain: chain %d not found", chainID) + + return chains.Chain{} + } + + for i := range obsParams { + cp := obsParams[i] + + switch { + case chains.IsEVMChain(cp.ChainId, nil): + observers[cp.ChainId] = mocks.NewEVMObserver(cp) + signers[cp.ChainId] = mocks.NewEVMSigner( + mustFindChain(cp.ChainId), + ethcommon.HexToAddress(cp.ConnectorContractAddress), + ethcommon.HexToAddress(cp.Erc20CustodyContractAddress), + ) + case chains.IsBitcoinChain(cp.ChainId, nil): + observers[cp.ChainId] = mocks.NewBTCObserver(cp) + signers[cp.ChainId] = mocks.NewBTCSigner() + case chains.IsSolanaChain(cp.ChainId, nil): + observers[cp.ChainId] = mocks.NewSolanaObserver(cp) + signers[cp.ChainId] = mocks.NewSolanaSigner() + default: + t.Fatalf("mock orcestrator: unsupported chain %d", cp.ChainId) + } + } + + return &Orchestrator{ + zetacoreClient: zetaClient, + signerMap: signers, + observerMap: observers, + } +} + +func createAppContext(t *testing.T, chainsOrParams ...any) *zctx.AppContext { + supportedChains, obsParams := parseChainsWithParams(t, chainsOrParams...) + + cfg := config.New(false) + + // Mock config + cfg.BitcoinConfig = config.BTCConfig{ + RPCHost: "localhost", + } + + for _, c := range supportedChains { + if chains.IsEVMChain(c.ChainId, nil) { + cfg.EVMChainConfigs[c.ChainId] = config.EVMConfig{Chain: c} + } + } + + params := map[int64]*observertypes.ChainParams{} + for i := range obsParams { + cp := obsParams[i] + params[cp.ChainId] = cp + } + + // new AppContext + appContext := zctx.New(cfg, nil, zerolog.New(zerolog.NewTestWriter(t))) + + ccFlags := sample.CrosschainFlags() + + // feed chain params + err := appContext.Update( + observertypes.Keygen{}, + supportedChains, + nil, + params, + "tssPubKey", + *ccFlags, + ) + require.NoError(t, err, "failed to update app context") + + return appContext +} + +// handy helper for testing +func parseChainsWithParams(t *testing.T, chainsOrParams ...any) ([]chains.Chain, []*observertypes.ChainParams) { + var ( + supportedChains = make([]chains.Chain, 0, len(chainsOrParams)) + obsParams = make([]*observertypes.ChainParams, 0, len(chainsOrParams)) + ) + + for _, something := range chainsOrParams { + switch tt := something.(type) { + case *chains.Chain: + supportedChains = append(supportedChains, *tt) + case chains.Chain: + supportedChains = append(supportedChains, tt) + case *observertypes.ChainParams: + obsParams = append(obsParams, tt) + case observertypes.ChainParams: + obsParams = append(obsParams, &tt) + default: + t.Fatalf("parse chains and params: unsupported type %T (%+v)", tt, tt) + } + } + + return supportedChains, obsParams +} diff --git a/zetaclient/supplychecker/logger.go b/zetaclient/supplychecker/logger.go deleted file mode 100644 index 89da0300d6..0000000000 --- a/zetaclient/supplychecker/logger.go +++ /dev/null @@ -1,31 +0,0 @@ -package supplychecker - -import ( - sdkmath "cosmossdk.io/math" - "github.com/rs/zerolog" - - "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin" -) - -// ZetaSupplyCheckLogs is a struct to log the output of the ZetaSupplyChecker -type ZetaSupplyCheckLogs struct { - Logger zerolog.Logger - AbortedTxAmounts sdkmath.Int `json:"aborted_tx_amounts"` - ZetaInTransit sdkmath.Int `json:"zeta_in_transit"` - ExternalChainTotalSupply sdkmath.Int `json:"external_chain_total_supply"` - ZetaTokenSupplyOnNode sdkmath.Int `json:"zeta_token_supply_on_node"` - EthLockedAmount sdkmath.Int `json:"eth_locked_amount"` - NodeAmounts sdkmath.Int `json:"node_amounts"` - LHS sdkmath.Int `json:"LHS"` - RHS sdkmath.Int `json:"RHS"` - SupplyCheckSuccess bool `json:"supply_check_success"` -} - -// LogOutput logs the output of the ZetaSupplyChecker -func (z ZetaSupplyCheckLogs) LogOutput() { - output, err := bitcoin.PrettyPrintStruct(z) - if err != nil { - z.Logger.Error().Err(err).Msgf("error pretty printing struct") - } - z.Logger.Info().Msgf(output) -} diff --git a/zetaclient/supplychecker/validate.go b/zetaclient/supplychecker/validate.go deleted file mode 100644 index f9e4dbaf79..0000000000 --- a/zetaclient/supplychecker/validate.go +++ /dev/null @@ -1,34 +0,0 @@ -package supplychecker - -import ( - sdkmath "cosmossdk.io/math" - "github.com/rs/zerolog" -) - -// ValidateZetaSupply validates the zeta supply from the checked values -func ValidateZetaSupply( - logger zerolog.Logger, - abortedTxAmounts, zetaInTransit, genesisAmounts, externalChainTotalSupply, zetaTokenSupplyOnNode, ethLockedAmount sdkmath.Int, -) bool { - lhs := ethLockedAmount.Sub(abortedTxAmounts) - rhs := zetaTokenSupplyOnNode.Add(zetaInTransit).Add(externalChainTotalSupply).Sub(genesisAmounts) - - copyZetaTokenSupplyOnNode := zetaTokenSupplyOnNode - copyGenesisAmounts := genesisAmounts - nodeAmounts := copyZetaTokenSupplyOnNode.Sub(copyGenesisAmounts) - logs := ZetaSupplyCheckLogs{ - Logger: logger, - AbortedTxAmounts: abortedTxAmounts, - ZetaInTransit: zetaInTransit, - ExternalChainTotalSupply: externalChainTotalSupply, - NodeAmounts: nodeAmounts, - ZetaTokenSupplyOnNode: zetaTokenSupplyOnNode, - EthLockedAmount: ethLockedAmount, - LHS: lhs, - RHS: rhs, - } - defer logs.LogOutput() - - logs.SupplyCheckSuccess = lhs.Equal(rhs) - return logs.SupplyCheckSuccess -} diff --git a/zetaclient/supplychecker/zeta_supply_checker.go b/zetaclient/supplychecker/zeta_supply_checker.go deleted file mode 100644 index 53a61c707b..0000000000 --- a/zetaclient/supplychecker/zeta_supply_checker.go +++ /dev/null @@ -1,280 +0,0 @@ -// Package supplychecker provides functionalities to check the total supply of Zeta tokens -// Currently not used in the codebase -package supplychecker - -import ( - "context" - "fmt" - - sdkmath "cosmossdk.io/math" - ethcommon "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/ethclient" - "github.com/pkg/errors" - "github.com/rs/zerolog" - - "github.com/zeta-chain/zetacore/pkg/chains" - "github.com/zeta-chain/zetacore/pkg/coin" - "github.com/zeta-chain/zetacore/x/crosschain/types" - "github.com/zeta-chain/zetacore/zetaclient/chains/evm/observer" - "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" - zctx "github.com/zeta-chain/zetacore/zetaclient/context" - clienttypes "github.com/zeta-chain/zetacore/zetaclient/types" - "github.com/zeta-chain/zetacore/zetaclient/zetacore" -) - -// ZetaSupplyChecker is a utility to check the total supply of Zeta tokens -type ZetaSupplyChecker struct { - evmClient map[int64]*ethclient.Client - zetaClient *zetacore.Client - ticker *clienttypes.DynamicTicker - stop chan struct{} - logger zerolog.Logger - externalEvmChain []chains.Chain - ethereumChain chains.Chain - genesisSupply sdkmath.Int -} - -// NewZetaSupplyChecker creates a new ZetaSupplyChecker -func NewZetaSupplyChecker( - ctx context.Context, - zetaClient *zetacore.Client, - logger zerolog.Logger, -) (*ZetaSupplyChecker, error) { - dynamicTicker, err := clienttypes.NewDynamicTicker("ZETASupplyTicker", 15) - if err != nil { - return nil, err - } - - app, err := zctx.FromContext(ctx) - if err != nil { - return nil, err - } - - zetaSupplyChecker := &ZetaSupplyChecker{ - stop: make(chan struct{}), - ticker: dynamicTicker, - evmClient: make(map[int64]*ethclient.Client), - logger: logger.With(). - Str("module", "ZetaSupplyChecker"). - Logger(), - zetaClient: zetaClient, - } - - for _, evmConfig := range app.Config().GetAllEVMConfigs() { - if evmConfig.Chain.IsZetaChain() { - continue - } - client, err := ethclient.Dial(evmConfig.Endpoint) - if err != nil { - return nil, err - } - - zetaSupplyChecker.evmClient[evmConfig.Chain.ChainId] = client - } - - for chainID := range zetaSupplyChecker.evmClient { - chain, found := chains.GetChainFromChainID(chainID, app.GetAdditionalChains()) - if !found { - return zetaSupplyChecker, fmt.Errorf("chain not found for chain id %d", chainID) - } - if chain.IsExternalChain() && chain.IsEVMChain() && - chain.Network != chains.Network_eth { - zetaSupplyChecker.externalEvmChain = append(zetaSupplyChecker.externalEvmChain, chain) - } else { - zetaSupplyChecker.ethereumChain = chain - } - } - - balances, err := zetaSupplyChecker.zetaClient.GetGenesisSupply(ctx) - if err != nil { - return nil, err - } - - tokensMintedAtBeginBlock, ok := sdkmath.NewIntFromString("200000000000000000") - if !ok { - return nil, fmt.Errorf("error parsing tokens minted at begin block") - } - - zetaSupplyChecker.genesisSupply = balances.Add(tokensMintedAtBeginBlock) - - logger.Info(). - Msgf("zeta supply checker initialized , external chains : %v ,ethereum chain :%v", zetaSupplyChecker.externalEvmChain, zetaSupplyChecker.ethereumChain) - - return zetaSupplyChecker, nil -} - -// Start starts the ZetaSupplyChecker -func (zs *ZetaSupplyChecker) Start(ctx context.Context) { - defer zs.ticker.Stop() - for { - select { - case <-zs.ticker.C(): - err := zs.CheckZetaTokenSupply(ctx) - if err != nil { - zs.logger.Error().Err(err).Msgf("ZetaSupplyChecker error") - } - case <-zs.stop: - return - } - } -} - -// Stop stops the ZetaSupplyChecker -func (zs *ZetaSupplyChecker) Stop() { - zs.logger.Info().Msgf("ZetaSupplyChecker is stopping") - close(zs.stop) -} - -// CheckZetaTokenSupply checks the total supply of Zeta tokens -func (zs *ZetaSupplyChecker) CheckZetaTokenSupply(ctx context.Context) error { - app, err := zctx.FromContext(ctx) - if err != nil { - return err - } - - externalChainTotalSupply := sdkmath.ZeroInt() - for _, chain := range zs.externalEvmChain { - externalEvmChainParams, ok := app.GetEVMChainParams(chain.ChainId) - if !ok { - return fmt.Errorf("externalEvmChainParams not found for chain id %d", chain.ChainId) - } - - zetaTokenAddressString := externalEvmChainParams.ZetaTokenContractAddress - zetaTokenAddress := ethcommon.HexToAddress(zetaTokenAddressString) - zetatokenNonEth, err := observer.FetchZetaTokenContract(zetaTokenAddress, zs.evmClient[chain.ChainId]) - if err != nil { - return err - } - - totalSupply, err := zetatokenNonEth.TotalSupply(nil) - if err != nil { - return err - } - - totalSupplyInt, ok := sdkmath.NewIntFromString(totalSupply.String()) - if !ok { - zs.logger.Error().Msgf("error parsing total supply for chain %d", chain.ChainId) - continue - } - - externalChainTotalSupply = externalChainTotalSupply.Add(totalSupplyInt) - } - - evmChainParams, ok := app.GetEVMChainParams(zs.ethereumChain.ChainId) - if !ok { - return fmt.Errorf("eth config not found for chain id %d", zs.ethereumChain.ChainId) - } - - ethConnectorAddressString := evmChainParams.ConnectorContractAddress - ethConnectorAddress := ethcommon.HexToAddress(ethConnectorAddressString) - ethConnectorContract, err := observer.FetchConnectorContractEth( - ethConnectorAddress, - zs.evmClient[zs.ethereumChain.ChainId], - ) - if err != nil { - return err - } - - ethLockedAmount, err := ethConnectorContract.GetLockedAmount(nil) - if err != nil { - return err - } - - ethLockedAmountInt, ok := sdkmath.NewIntFromString(ethLockedAmount.String()) - if !ok { - return fmt.Errorf("error parsing eth locked amount") - } - - zetaInTransit, err := zs.GetAmountOfZetaInTransit(ctx) - if err != nil { - return err - } - zetaTokenSupplyOnNode, err := zs.zetaClient.GetZetaTokenSupplyOnNode(ctx) - if err != nil { - return err - } - - abortedAmount, err := zs.AbortedTxAmount(ctx) - if err != nil { - return err - } - - ValidateZetaSupply( - zs.logger, - abortedAmount, - zetaInTransit, - zs.genesisSupply, - externalChainTotalSupply, - zetaTokenSupplyOnNode, - ethLockedAmountInt, - ) - - return nil -} - -// AbortedTxAmount returns the amount of Zeta tokens in aborted transactions -func (zs *ZetaSupplyChecker) AbortedTxAmount(ctx context.Context) (sdkmath.Int, error) { - amount, err := zs.zetaClient.GetAbortedZetaAmount(ctx) - if err != nil { - return sdkmath.ZeroInt(), errors.Wrap(err, "error getting aborted zeta amount") - } - amountInt, ok := sdkmath.NewIntFromString(amount) - if !ok { - return sdkmath.ZeroInt(), errors.New("error parsing aborted zeta amount") - } - return amountInt, nil -} - -// GetAmountOfZetaInTransit returns the amount of Zeta tokens in transit -func (zs *ZetaSupplyChecker) GetAmountOfZetaInTransit(ctx context.Context) (sdkmath.Int, error) { - chainsToCheck := make([]chains.Chain, len(zs.externalEvmChain)+1) - chainsToCheck = append(append(chainsToCheck, zs.externalEvmChain...), zs.ethereumChain) - cctxs := zs.GetPendingCCTXInTransit(ctx, chainsToCheck) - amount := sdkmath.ZeroUint() - - for _, cctx := range cctxs { - amount = amount.Add(cctx.GetCurrentOutboundParam().Amount) - } - amountInt, ok := sdkmath.NewIntFromString(amount.String()) - if !ok { - return sdkmath.ZeroInt(), fmt.Errorf("error parsing amount %s", amount.String()) - } - - return amountInt, nil -} - -// GetPendingCCTXInTransit returns the pending CCTX in transit -func (zs *ZetaSupplyChecker) GetPendingCCTXInTransit( - ctx context.Context, - receivingChains []chains.Chain, -) []*types.CrossChainTx { - cctxInTransit := make([]*types.CrossChainTx, 0) - for _, chain := range receivingChains { - cctx, _, err := zs.zetaClient.ListPendingCCTX(ctx, chain.ChainId) - if err != nil { - continue - } - nonceToCctxMap := make(map[uint64]*types.CrossChainTx) - for _, c := range cctx { - if c.InboundParams.CoinType == coin.CoinType_Zeta { - nonceToCctxMap[c.GetCurrentOutboundParam().TssNonce] = c - } - } - - trackers, err := zs.zetaClient.GetAllOutboundTrackerByChain(ctx, chain.ChainId, interfaces.Ascending) - if err != nil { - continue - } - for _, tracker := range trackers { - zs.logger.Info().Msgf("tracker exists for nonce: %d , removing from supply checks", tracker.Nonce) - delete(nonceToCctxMap, tracker.Nonce) - } - for _, c := range nonceToCctxMap { - if c != nil { - cctxInTransit = append(cctxInTransit, c) - } - } - } - - return cctxInTransit -} diff --git a/zetaclient/supplychecker/zeta_supply_checker_test.go b/zetaclient/supplychecker/zeta_supply_checker_test.go deleted file mode 100644 index ed984de2d8..0000000000 --- a/zetaclient/supplychecker/zeta_supply_checker_test.go +++ /dev/null @@ -1,61 +0,0 @@ -package supplychecker - -import ( - "os" - "testing" - - sdkmath "cosmossdk.io/math" - "github.com/rs/zerolog" - "github.com/stretchr/testify/require" -) - -func MustNewIntFromString(t *testing.T, val string) sdkmath.Int { - v, ok := sdkmath.NewIntFromString(val) - require.True(t, ok) - return v -} -func TestZetaSupplyChecker_ValidateZetaSupply(t *testing.T) { - tt := []struct { - name string - abortedTxAmount sdkmath.Int - zetaInTransit sdkmath.Int - genesisAmounts sdkmath.Int - externalChainTotalSupply sdkmath.Int - zetaTokenSupplyOnNode sdkmath.Int - ethLockedAmount sdkmath.Int - validate require.BoolAssertionFunc - }{ - { - name: "1 zeta cctx in progress", - abortedTxAmount: MustNewIntFromString(t, "0"), - zetaInTransit: MustNewIntFromString(t, "1000000000000000000"), - externalChainTotalSupply: MustNewIntFromString(t, "9000000000000000000"), - genesisAmounts: MustNewIntFromString(t, "1000000000000000000"), - zetaTokenSupplyOnNode: MustNewIntFromString(t, "1000000000000000000"), - ethLockedAmount: MustNewIntFromString(t, "10000000000000000000"), - validate: func(t require.TestingT, b bool, i ...interface{}) { - require.True(t, b, i...) - }, - }, - // Todo add more scenarios - //https://github.com/zeta-chain/node/issues/1375 - } - - for _, tc := range tt { - t.Run(tc.name, func(t *testing.T) { - logger := zerolog.New(os.Stdout).With().Timestamp().Logger() - tc.validate( - t, - ValidateZetaSupply( - logger, - tc.abortedTxAmount, - tc.zetaInTransit, - tc.genesisAmounts, - tc.externalChainTotalSupply, - tc.zetaTokenSupplyOnNode, - tc.ethLockedAmount, - ), - ) - }) - } -} diff --git a/zetaclient/testdata/btc/chain_8332_inbound_raw_result_37777defed8717c581b4c0509329550e344bdc14ac38f71fc050096887e535c8.json b/zetaclient/testdata/btc/chain_8332_inbound_raw_result_37777defed8717c581b4c0509329550e344bdc14ac38f71fc050096887e535c8.json new file mode 100644 index 0000000000..a4e964500d --- /dev/null +++ b/zetaclient/testdata/btc/chain_8332_inbound_raw_result_37777defed8717c581b4c0509329550e344bdc14ac38f71fc050096887e535c8.json @@ -0,0 +1,42 @@ +{ + "hex": "020000000001027bc0bba407bc67178f100e352bf6e047fae4cbf960d783586cb5e430b3b700e70000000000feffffff7bc0bba407bc67178f100e352bf6e047fae4cbf960d783586cb5e430b3b700e70100000000feffffff01b4ba0e0000000000160014173fd310e9db2c7e9550ce0f03f1e6c01d833aa90140134896c42cd95680b048845847c8054756861ffab7d4abab72f6508d67d1ec0c590287ec2161dd7884983286e1cd56ce65c08a24ee0476ede92678a93b1b180c03407b5d614a4610bf9196775791fcc589597ca066dcd10048e004cd4c7341bb4bb90cee4705192f3f7db524e8067a5222c7f09baf29ef6b805b8327ecd1e5ab83ca2220f5b059b9a72298ccbefff59d9b943f7e0fc91d8a3b944a95e7b6390cc99eb5f4ac41c0d9dfdf0fe3c83e9870095d67fff59a8056dad28c6dfb944bb71cf64b90ace9a7776b22a1185fb2dc9524f6b178e2693189bf01655d7f38f043923668dc5af45bffd30a00", + "txid": "37777defed8717c581b4c0509329550e344bdc14ac38f71fc050096887e535c8", + "version": 2, + "locktime": 0, + "vin": [ + { + "vout": 0, + "scriptSig": { + "asm": "", + "hex": "" + }, + "sequence": 4294967294, + "txid": "e700b7b330e4b56c5883d760f9cbe4fa47e0f62b350e108f1767bc07a4bbc07b", + "txinwitness": [ + "134896c42cd95680b048845847c8054756861ffab7d4abab72f6508d67d1ec0c590287ec2161dd7884983286e1cd56ce65c08a24ee0476ede92678a93b1b180c" + ] + }, + { + "scriptSig": {"asm": "", "hex": ""}, + "sequence": 4294967294, + "txid": "c5d224963832fc0b9a597251c2342a17b25e481a88cc9119008e8f8296652697", + "vout": 2, + "txinwitness": [ + "3a4b32aef0e6ecc62d185594baf4df186c6d48ec15e72515bf81c1bcc1f04c758f4d54486bc2e7c280e649761d9084dbd2e7cdfb20708a7f8d0f82e5277bba2b", + "20888269c4f0b7f6fe95d0cba364e2b1b879d9b00735d19cfab4b8d87096ce2b3cac00634d0802000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004c50000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000068", + "c0888269c4f0b7f6fe95d0cba364e2b1b879d9b00735d19cfab4b8d87096ce2b3c" + ] + } + ], + "vout": [ + { + "value": 0.36557203, + "n": 0, + "scriptPubKey": { + "asm": "0 daaae0d3de9d8fdee31661e61aea828b59be7864", + "hex": "0014daaae0d3de9d8fdee31661e61aea828b59be7864", + "type": "witness_v0_keyhash" + } + } + ] +} diff --git a/zetaclient/testdata/btc/chain_8332_inbound_raw_result_7a57f987a3cb605896a5909d9ef2bf7afbf0c78f21e4118b85d00d9e4cce0c2c.json b/zetaclient/testdata/btc/chain_8332_inbound_raw_result_7a57f987a3cb605896a5909d9ef2bf7afbf0c78f21e4118b85d00d9e4cce0c2c.json new file mode 100644 index 0000000000..ab20339421 --- /dev/null +++ b/zetaclient/testdata/btc/chain_8332_inbound_raw_result_7a57f987a3cb605896a5909d9ef2bf7afbf0c78f21e4118b85d00d9e4cce0c2c.json @@ -0,0 +1,32 @@ +{ + "txid": "7a57f987a3cb605896a5909d9ef2bf7afbf0c78f21e4118b85d00d9e4cce0c2c", + "version": 2, + "locktime": 0, + "vin": [ + { + "txid": "213403e1efb29349a48ea9717096cf20d6e19091e496052ab591f310f0deebd6", + "vout": 0, + "scriptSig": { + "asm": "", + "hex": "" + }, + "txinwitness": [ + "7a8d20a4bb100ffd6399dc4fa1972e405e0e245775be1fcd7df3d5212d62c8d2e4b5534b3ae508a1f974d8995aac759454de9645f78245b8bee3b90ade86ea70", + "20a7172b841ddb8716fd0afa3400d18bfe1105df132e6938cca651b65ee3e64853ac00634c6472f080c854647755d0d9e6f6821f6931f855b9acffd53d87433395672756d58822fd143360762109ab898626556b1c3b8d3096d2361f1297df4a41c1b429471a9aa2fc9be5f27c13b3863d6ac269e4b587d8389f8fd9649859935b0d48dea88cdb40f20c68", + "c1a7172b841ddb8716fd0afa3400d18bfe1105df132e6938cca651b65ee3e64853" + ], + "sequence": 4294967295 + } + ], + "vout": [ + { + "scriptPubKey": { + "asm": "0 daaae0d3de9d8fdee31661e61aea828b59be7864", + "hex": "0014daaae0d3de9d8fdee31661e61aea828b59be7864", + "type": "witness_v0_keyhash" + }, + "value": 0.45, + "n": 0 + } + ] +} \ No newline at end of file diff --git a/zetaclient/testutils/constant.go b/zetaclient/testutils/constant.go index ad8302577d..3036035db4 100644 --- a/zetaclient/testutils/constant.go +++ b/zetaclient/testutils/constant.go @@ -1,6 +1,10 @@ package testutils -import ethcommon "github.com/ethereum/go-ethereum/common" +import ( + ethcommon "github.com/ethereum/go-ethereum/common" + + "github.com/zeta-chain/zetacore/pkg/chains" +) const ( // TSSAddressEVMMainnet the EVM TSS address for test purposes @@ -29,33 +33,39 @@ const ( EventERC20Withdraw = "Withdrawn" ) +// GatewayAddresses contains constants gateway addresses for testing +var GatewayAddresses = map[int64]string{ + // Gateway address on Solana devnet + chains.SolanaDevnet.ChainId: "94U5AHQMKkV5txNJ17QPXWoh474PheGou6cNP2FEuL1d", +} + // ConnectorAddresses contains constants ERC20 connector addresses for testing var ConnectorAddresses = map[int64]ethcommon.Address{ // Connector address on Ethereum mainnet - 1: ethcommon.HexToAddress("0x000007Cf399229b2f5A4D043F20E90C9C98B7C6a"), + chains.Ethereum.ChainId: ethcommon.HexToAddress("0x000007Cf399229b2f5A4D043F20E90C9C98B7C6a"), // Connector address on Binance Smart Chain mainnet - 56: ethcommon.HexToAddress("0x000063A6e758D9e2f438d430108377564cf4077D"), + chains.BscMainnet.ChainId: ethcommon.HexToAddress("0x000063A6e758D9e2f438d430108377564cf4077D"), // testnet - 5: ethcommon.HexToAddress("0x00005E3125aBA53C5652f9F0CE1a4Cf91D8B15eA"), - 97: ethcommon.HexToAddress("0x0000ecb8cdd25a18F12DAA23f6422e07fBf8B9E1"), - 11155111: ethcommon.HexToAddress("0x3963341dad121c9CD33046089395D66eBF20Fb03"), + chains.Goerli.ChainId: ethcommon.HexToAddress("0x00005E3125aBA53C5652f9F0CE1a4Cf91D8B15eA"), + chains.BscTestnet.ChainId: ethcommon.HexToAddress("0x0000ecb8cdd25a18F12DAA23f6422e07fBf8B9E1"), + chains.Sepolia.ChainId: ethcommon.HexToAddress("0x3963341dad121c9CD33046089395D66eBF20Fb03"), // localnet - 1337: ethcommon.HexToAddress("0xD28D6A0b8189305551a0A8bd247a6ECa9CE781Ca"), + chains.GoerliLocalnet.ChainId: ethcommon.HexToAddress("0xD28D6A0b8189305551a0A8bd247a6ECa9CE781Ca"), } // CustodyAddresses contains constants ERC20 custody addresses for testing var CustodyAddresses = map[int64]ethcommon.Address{ // ERC20 custody address on Ethereum mainnet - 1: ethcommon.HexToAddress("0x0000030Ec64DF25301d8414eE5a29588C4B0dE10"), + chains.Ethereum.ChainId: ethcommon.HexToAddress("0x0000030Ec64DF25301d8414eE5a29588C4B0dE10"), // ERC20 custody address on Binance Smart Chain mainnet - 56: ethcommon.HexToAddress("0x00000fF8fA992424957F97688015814e707A0115"), + chains.BscMainnet.ChainId: ethcommon.HexToAddress("0x00000fF8fA992424957F97688015814e707A0115"), // testnet - 5: ethcommon.HexToAddress("0x000047f11C6E42293F433C82473532E869Ce4Ec5"), - 97: ethcommon.HexToAddress("0x0000a7Db254145767262C6A81a7eE1650684258e"), - 11155111: ethcommon.HexToAddress("0x84725b70a239d3Faa7C6EF0C6C8E8b6c8e28338b"), + chains.Goerli.ChainId: ethcommon.HexToAddress("0x000047f11C6E42293F433C82473532E869Ce4Ec5"), + chains.BscTestnet.ChainId: ethcommon.HexToAddress("0x0000a7Db254145767262C6A81a7eE1650684258e"), + chains.Sepolia.ChainId: ethcommon.HexToAddress("0x84725b70a239d3Faa7C6EF0C6C8E8b6c8e28338b"), } diff --git a/zetaclient/testutils/mocks/chain_params.go b/zetaclient/testutils/mocks/chain_params.go index 45c5df2497..19603eda34 100644 --- a/zetaclient/testutils/mocks/chain_params.go +++ b/zetaclient/testutils/mocks/chain_params.go @@ -13,11 +13,32 @@ import ( ) func MockChainParams(chainID int64, confirmation uint64) observertypes.ChainParams { + const zeroAddress = "0x0000000000000000000000000000000000000000" + + connectorAddr := zeroAddress + if a, ok := testutils.ConnectorAddresses[chainID]; ok { + connectorAddr = a.Hex() + } + + erc20CustodyAddr := zeroAddress + if a, ok := testutils.CustodyAddresses[chainID]; ok { + erc20CustodyAddr = a.Hex() + } + return observertypes.ChainParams{ ChainId: chainID, ConfirmationCount: confirmation, - ConnectorContractAddress: testutils.ConnectorAddresses[chainID].Hex(), - Erc20CustodyContractAddress: testutils.CustodyAddresses[chainID].Hex(), + ZetaTokenContractAddress: zeroAddress, + ConnectorContractAddress: connectorAddr, + Erc20CustodyContractAddress: erc20CustodyAddr, + InboundTicker: 12, + OutboundTicker: 15, + WatchUtxoTicker: 0, + GasPriceTicker: 30, + OutboundScheduleInterval: 30, + OutboundScheduleLookahead: 60, + BallotThreshold: observertypes.DefaultBallotThreshold, + MinObserverDelegation: observertypes.DefaultMinObserverDelegation, IsSupported: true, } } diff --git a/zetaclient/testutils/mocks/solana_rpc.go b/zetaclient/testutils/mocks/solana_rpc.go index 953bde87e5..fad147037c 100644 --- a/zetaclient/testutils/mocks/solana_rpc.go +++ b/zetaclient/testutils/mocks/solana_rpc.go @@ -47,6 +47,36 @@ func (_m *SolanaRPCClient) GetAccountInfo(ctx context.Context, account solana.Pu return r0, r1 } +// GetBalance provides a mock function with given fields: ctx, account, commitment +func (_m *SolanaRPCClient) GetBalance(ctx context.Context, account solana.PublicKey, commitment rpc.CommitmentType) (*rpc.GetBalanceResult, error) { + ret := _m.Called(ctx, account, commitment) + + if len(ret) == 0 { + panic("no return value specified for GetBalance") + } + + var r0 *rpc.GetBalanceResult + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, solana.PublicKey, rpc.CommitmentType) (*rpc.GetBalanceResult, error)); ok { + return rf(ctx, account, commitment) + } + if rf, ok := ret.Get(0).(func(context.Context, solana.PublicKey, rpc.CommitmentType) *rpc.GetBalanceResult); ok { + r0 = rf(ctx, account, commitment) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*rpc.GetBalanceResult) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, solana.PublicKey, rpc.CommitmentType) error); ok { + r1 = rf(ctx, account, commitment) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetConfirmedTransactionWithOpts provides a mock function with given fields: ctx, signature, opts func (_m *SolanaRPCClient) GetConfirmedTransactionWithOpts(ctx context.Context, signature solana.Signature, opts *rpc.GetTransactionOpts) (*rpc.TransactionWithMeta, error) { ret := _m.Called(ctx, signature, opts) diff --git a/zetaclient/testutils/mocks/zetacore_client.go b/zetaclient/testutils/mocks/zetacore_client.go index b1dbd3f741..168b580ada 100644 --- a/zetaclient/testutils/mocks/zetacore_client.go +++ b/zetaclient/testutils/mocks/zetacore_client.go @@ -1,10 +1,10 @@ -// Code generated by mockery v2.38.0. DO NOT EDIT. +// Code generated by mockery v2.43.2. DO NOT EDIT. package mocks import ( - blame "gitlab.com/thorchain/tss/go-tss/blame" chains "github.com/zeta-chain/zetacore/pkg/chains" + blame "gitlab.com/thorchain/tss/go-tss/blame" context "context" @@ -283,24 +283,22 @@ func (_m *ZetacoreClient) GetInboundTrackersForChain(ctx context.Context, chainI } // GetKeyGen provides a mock function with given fields: ctx -func (_m *ZetacoreClient) GetKeyGen(ctx context.Context) (*observertypes.Keygen, error) { +func (_m *ZetacoreClient) GetKeyGen(ctx context.Context) (observertypes.Keygen, error) { ret := _m.Called(ctx) if len(ret) == 0 { panic("no return value specified for GetKeyGen") } - var r0 *observertypes.Keygen + var r0 observertypes.Keygen var r1 error - if rf, ok := ret.Get(0).(func(context.Context) (*observertypes.Keygen, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context) (observertypes.Keygen, error)); ok { return rf(ctx) } - if rf, ok := ret.Get(0).(func(context.Context) *observertypes.Keygen); ok { + if rf, ok := ret.Get(0).(func(context.Context) observertypes.Keygen); ok { r0 = rf(ctx) } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*observertypes.Keygen) - } + r0 = ret.Get(0).(observertypes.Keygen) } if rf, ok := ret.Get(1).(func(context.Context) error); ok { diff --git a/zetaclient/zetacore/client.go b/zetaclient/zetacore/client.go index d86fc1b4c2..0806c709fe 100644 --- a/zetaclient/zetacore/client.go +++ b/zetaclient/zetacore/client.go @@ -341,20 +341,15 @@ func (c *Client) WaitForZetacoreToCreateBlocks(ctx context.Context) error { // UpdateAppContext updates zctx.AppContext // zetacore stores AppContext for all clients -func (c *Client) UpdateAppContext( - ctx context.Context, - appContext *zctx.AppContext, - init bool, - sampledLogger zerolog.Logger, -) error { +func (c *Client) UpdateAppContext(ctx context.Context, appContext *zctx.AppContext, logger zerolog.Logger) error { bn, err := c.GetBlockHeight(ctx) if err != nil { - return fmt.Errorf("failed to get zetablock height: %w", err) + return errors.Wrap(err, "unable to get zetablock height") } plan, err := c.GetUpgradePlan(ctx) if err != nil { - return fmt.Errorf("failed to get upgrade plan: %w", err) + return errors.Wrap(err, "unable to get upgrade plan") } // Stop client and notify dependant services to stop (Orchestrator, Observers, and Signers) @@ -367,90 +362,72 @@ func (c *Client) UpdateAppContext( ) c.Stop() - } - additionalChains, err := c.GetAdditionalChains(ctx) - if err != nil { - return fmt.Errorf("failed to additional chains: %w", err) + return nil } - chainParams, err := c.GetChainParams(ctx) + supportedChains, err := c.GetSupportedChains(ctx) if err != nil { - return fmt.Errorf("failed to get chain params: %w", err) + return errors.Wrap(err, "unable to fetch supported chains") } - newEVMParams := make(map[int64]*observertypes.ChainParams) - var newBTCParams *observertypes.ChainParams - var newSolanaParams *observertypes.ChainParams - - // check and update chain params for each chain - for _, chainParam := range chainParams { - err := observertypes.ValidateChainParams(chainParam) - if err != nil { - sampledLogger.Warn().Err(err).Msgf("Invalid chain params for chain %d", chainParam.ChainId) - continue - } - if chains.IsBitcoinChain(chainParam.ChainId, additionalChains) { - newBTCParams = chainParam - } else if chains.IsSolanaChain(chainParam.ChainId, additionalChains) { - newSolanaParams = chainParam - } else if chains.IsEVMChain(chainParam.ChainId, additionalChains) { - newEVMParams[chainParam.ChainId] = chainParam - } - } - - supportedChains, err := c.GetSupportedChains(ctx) + additionalChains, err := c.GetAdditionalChains(ctx) if err != nil { - return fmt.Errorf("failed to get supported chains: %w", err) + return errors.Wrap(err, "unable to fetch additional chains") } - newChains := make([]chains.Chain, len(supportedChains)) - for i, chain := range supportedChains { - newChains[i] = chain + chainParams, err := c.GetChainParams(ctx) + if err != nil { + return errors.Wrap(err, "unable to fetch chain params") } keyGen, err := c.GetKeyGen(ctx) if err != nil { - c.logger.Info().Msg("Unable to fetch keygen from zetacore") - return fmt.Errorf("failed to get keygen: %w", err) + return errors.Wrap(err, "unable to fetch keygen from zetacore") } - tss, err := c.GetCurrentTSS(ctx) + crosschainFlags, err := c.GetCrosschainFlags(ctx) if err != nil { - c.logger.Info().Err(err).Msg("Unable to fetch TSS from zetacore") - return fmt.Errorf("failed to get current tss: %w", err) + return errors.Wrap(err, "unable to fetch crosschain flags from zetacore") } - tssPubKey := tss.GetTssPubkey() - crosschainFlags, err := c.GetCrosschainFlags(ctx) + tss, err := c.GetCurrentTSS(ctx) if err != nil { - c.logger.Info().Msg("Unable to fetch cross-chain flags from zetacore") - return fmt.Errorf("failed to get crosschain flags: %w", err) + return errors.Wrap(err, "unable to fetch current TSS") } - // hotfix-v19.0.1: hardcode blockHeaderEnabledChains to empty because the zetacore API somehow won't work - blockHeaderEnabledChains := []lightclienttypes.HeaderSupportedChain{} + freshParams := make(map[int64]*observertypes.ChainParams, len(chainParams)) + + // check and update chain params for each chain + // Note that we are EXCLUDING ZetaChain from the chainParams if it's present + for i := range chainParams { + cp := chainParams[i] + + if !cp.IsSupported { + logger.Warn().Int64("chain.id", cp.ChainId).Msg("Skipping unsupported chain") + continue + } + + if chains.IsZetaChain(cp.ChainId, nil) { + continue + } + + if err := observertypes.ValidateChainParams(cp); err != nil { + logger.Warn().Err(err).Int64("chain.id", cp.ChainId).Msg("Skipping invalid chain params") + continue + } - // blockHeaderEnabledChains, err := c.GetBlockHeaderEnabledChains(ctx) - // if err != nil { - // c.logger.Info().Msg("Unable to fetch block header enabled chains from zetacore") - // return err - // } + freshParams[cp.ChainId] = cp + } - appContext.Update( + return appContext.Update( keyGen, - newChains, - newEVMParams, - newBTCParams, - newSolanaParams, - tssPubKey, - crosschainFlags, + supportedChains, additionalChains, - blockHeaderEnabledChains, - init, + freshParams, + tss.GetTssPubkey(), + crosschainFlags, ) - - return nil } func cosmosREST(host string) string { diff --git a/zetaclient/zetacore/client_query_observer.go b/zetaclient/zetacore/client_query_observer.go index 45082aae16..4d98ca848e 100644 --- a/zetaclient/zetacore/client_query_observer.go +++ b/zetaclient/zetacore/client_query_observer.go @@ -2,6 +2,7 @@ package zetacore import ( "context" + "fmt" "cosmossdk.io/errors" @@ -95,18 +96,21 @@ func (c *Client) GetNonceByChain(ctx context.Context, chain chains.Chain) (types } // GetKeyGen returns the keygen -func (c *Client) GetKeyGen(ctx context.Context) (*types.Keygen, error) { +func (c *Client) GetKeyGen(ctx context.Context) (types.Keygen, error) { in := &types.QueryGetKeygenRequest{} resp, err := retry.DoTypedWithRetry(func() (*types.QueryGetKeygenResponse, error) { return c.client.observer.Keygen(ctx, in) }) - if err != nil { - return nil, errors.Wrap(err, "failed to get keygen") + switch { + case err != nil: + return types.Keygen{}, errors.Wrap(err, "failed to get keygen") + case resp.Keygen == nil: + return types.Keygen{}, fmt.Errorf("keygen is nil") } - return resp.GetKeygen(), nil + return *resp.Keygen, nil } // GetAllNodeAccounts returns all node accounts diff --git a/zetaclient/zetacore/client_query_test.go b/zetaclient/zetacore/client_query_test.go index 2b6a2a1c9c..ae995b069d 100644 --- a/zetaclient/zetacore/client_query_test.go +++ b/zetaclient/zetacore/client_query_test.go @@ -626,7 +626,7 @@ func TestZetacore_GetKeyGen(t *testing.T) { resp, err := client.GetKeyGen(ctx) require.NoError(t, err) - require.Equal(t, expectedOutput.Keygen, resp) + require.Equal(t, *expectedOutput.Keygen, resp) } func TestZetacore_GetBallotByID(t *testing.T) { diff --git a/zetaclient/zetacore/client_worker.go b/zetaclient/zetacore/client_worker.go index 05029a9a22..fcf02766a0 100644 --- a/zetaclient/zetacore/client_worker.go +++ b/zetaclient/zetacore/client_worker.go @@ -32,7 +32,7 @@ func (c *Client) UpdateAppContextWorker(ctx context.Context, app *appcontext.App select { case <-ticker.C: c.logger.Debug().Msg("UpdateAppContextWorker invocation") - if err := c.UpdateAppContext(ctx, app, false, logger); err != nil { + if err := c.UpdateAppContext(ctx, app, logger); err != nil { c.logger.Err(err).Msg("UpdateAppContextWorker failed to update config") } case <-c.stop: diff --git a/zetaclient/zetacore/tx_test.go b/zetaclient/zetacore/tx_test.go index 553783c5f2..05cdff6417 100644 --- a/zetaclient/zetacore/tx_test.go +++ b/zetaclient/zetacore/tx_test.go @@ -227,6 +227,8 @@ func TestZetacore_UpdateAppContext(t *testing.T) { listener, err := net.Listen("tcp", "127.0.0.1:9090") require.NoError(t, err) + ethChainParams := mocks.MockChainParams(chains.Ethereum.ChainId, 100) + server := grpcmock.MockUnstartedServer( grpcmock.RegisterService(crosschaintypes.RegisterQueryServer), grpcmock.RegisterService(upgradetypes.RegisterQueryServer), @@ -259,9 +261,8 @@ func TestZetacore_UpdateAppContext(t *testing.T) { WithPayload(observertypes.QueryGetChainParamsRequest{}). Return(observertypes.QueryGetChainParamsResponse{ChainParams: &observertypes.ChainParamsList{ ChainParams: []*observertypes.ChainParams{ - { - ChainId: 7000, - }, + {ChainId: 7000}, // ZetaChain + ðChainParams, }, }}) @@ -329,22 +330,6 @@ func TestZetacore_UpdateAppContext(t *testing.T) { GasPriceIncreaseFlags: nil, }}) - // hotfix-v19.0.1: hardcode blockHeaderEnabledChains to empty - // method = "/zetachain.zetacore.lightclient.Query/HeaderEnabledChains" - // s.ExpectUnary(method). - // UnlimitedTimes(). - // WithPayload(lightclienttypes.QueryHeaderEnabledChainsRequest{}). - // Return(lightclienttypes.QueryHeaderEnabledChainsResponse{HeaderEnabledChains: []lightclienttypes.HeaderSupportedChain{ - // { - // ChainId: chains.Ethereum.ChainId, - // Enabled: true, - // }, - // { - // ChainId: chains.BitcoinMainnet.ChainId, - // Enabled: false, - // }, - // }}) - method = "/zetachain.zetacore.authority.Query/ChainInfo" s.ExpectUnary(method). UnlimitedTimes(). @@ -372,8 +357,8 @@ func TestZetacore_UpdateAppContext(t *testing.T) { t.Run("zetacore update success", func(t *testing.T) { cfg := config.New(false) - appContext := zctx.New(cfg, zerolog.Nop()) - err := client.UpdateAppContext(ctx, appContext, false, zerolog.Logger{}) + appContext := zctx.New(cfg, nil, zerolog.Nop()) + err := client.UpdateAppContext(ctx, appContext, zerolog.New(zerolog.NewTestWriter(t))) require.NoError(t, err) }) }