From ef2147aa8d3ac1b38fb947af8484138b5505020b Mon Sep 17 00:00:00 2001 From: Charlie Chen <34498985+ws4charlie@users.noreply.github.com> Date: Thu, 1 Aug 2024 17:31:31 -0500 Subject: [PATCH 1/6] feat: withdraw SOL from ZEVM to Solana (#2560) * port Panruo's outbound code and make compile pass * make SOL withdraw e2e test passing * make solana outbound tracker goroutine working * allow solana gateway address to update * integrate sub methods of SignMsgWithdraw and SignWithdrawTx * initiate solana outbound tracker reporter * implemented solana outbound tx verification * use the amount in tx result for outbound vote * post Solana priority fee to zetacore * config Solana fee payer private key * resolve 1st wave of comments in PR review * resolve 2nd wave of comments * refactor IsOutboundProcessed as VoteOutboundIfConfirmed; move outbound tracker iteration logic into ProcessOutboundTrackers sub method * resolve 3rd wave of PR feedback * added description to explain what do we do about the outbound tracker txHash * add additional error message; add additional method comment * fix gosec err * replace contex.TODO() with context.Background() --- changelog.md | 3 +- cmd/zetaclientd/init.go | 14 +- cmd/zetaclientd/solana_test_key.go | 37 ++ cmd/zetaclientd/utils.go | 2 - cmd/zetae2e/local/local.go | 6 +- .../localnet/orchestrator/start-zetae2e.sh | 5 + contrib/localnet/scripts/start-zetacored.sh | 3 + ...red_tx_fungible_update-gateway-contract.md | 2 +- e2e/e2etests/e2etests.go | 13 +- e2e/e2etests/test_solana_deposit.go | 17 +- e2e/e2etests/test_solana_withdraw.go | 47 +++ e2e/runner/setup_solana.go | 32 +- e2e/runner/solana.go | 28 +- e2e/txserver/zeta_tx_server.go | 2 +- pkg/contract/solana/types.go | 44 --- .../solana/gateway.go} | 23 ++ .../solana/gateway.json | 0 pkg/contracts/solana/gateway_message.go | 107 ++++++ pkg/contracts/solana/gateway_message_test.go | 31 ++ pkg/{contract => contracts}/solana/idl.go | 0 pkg/contracts/solana/instruction.go | 118 ++++++ pkg/contracts/solana/instruction_test.go | 80 ++++ pkg/contracts/solana/pda.go | 19 + x/observer/types/chain_params.go | 18 +- zetaclient/chains/base/observer.go | 5 + zetaclient/chains/base/signer.go | 55 ++- zetaclient/chains/base/signer_test.go | 18 + .../chains/bitcoin/observer/observer.go | 10 +- .../chains/bitcoin/observer/outbound.go | 66 ++-- .../chains/bitcoin/observer/outbound_test.go | 2 +- zetaclient/chains/bitcoin/signer/signer.go | 51 +-- zetaclient/chains/evm/observer/observer.go | 18 +- zetaclient/chains/evm/observer/outbound.go | 59 ++- .../chains/evm/observer/outbound_test.go | 30 +- zetaclient/chains/evm/signer/outbound_data.go | 13 +- zetaclient/chains/evm/signer/signer.go | 136 +++---- zetaclient/chains/interfaces/interfaces.go | 34 +- zetaclient/chains/solana/observer/inbound.go | 10 +- .../chains/solana/observer/inbound_test.go | 6 +- zetaclient/chains/solana/observer/observer.go | 57 ++- .../chains/solana/observer/observer_gas.go | 103 ++++++ zetaclient/chains/solana/observer/outbound.go | 349 +++++++++++++++++- .../chains/solana/observer/outbound_test.go | 296 +++++++++++++++ .../signer/outbound_tracker_reporter.go | 91 +++++ zetaclient/chains/solana/signer/signer.go | 184 +++++++++ zetaclient/chains/solana/signer/withdraw.go | 123 ++++++ zetaclient/common/constant.go | 3 + zetaclient/config/types.go | 37 ++ zetaclient/orchestrator/bootstrap.go | 68 +++- zetaclient/orchestrator/orchestrator.go | 151 ++++++-- zetaclient/orchestrator/orchestrator_test.go | 238 ++++++++++-- ...VdkmUeyXfn7eWbV8932uSsPfBxgA7UgERNTvq.json | 52 +++ ...hFuTFPWFuV6aWAfzEMfVfCdNu9DfzVp5FsHg5.json | 52 +++ zetaclient/testutils/mocks/chain_clients.go | 57 ++- zetaclient/testutils/mocks/chain_signer.go | 61 +++ zetaclient/testutils/mocks/solana_rpc.go | 328 ++++++++++++++++ zetaclient/testutils/testdata.go | 13 + zetaclient/testutils/testdata_naming.go | 5 + zetaclient/zetacore/client_vote.go | 7 +- zetaclient/zetacore/tx.go | 15 +- zetaclient/zetacore/tx_test.go | 28 +- 61 files changed, 3008 insertions(+), 474 deletions(-) create mode 100644 cmd/zetaclientd/solana_test_key.go create mode 100644 e2e/e2etests/test_solana_withdraw.go delete mode 100644 pkg/contract/solana/types.go rename pkg/{contract/solana/contract.go => contracts/solana/gateway.go} (65%) rename pkg/{contract => contracts}/solana/gateway.json (100%) create mode 100644 pkg/contracts/solana/gateway_message.go create mode 100644 pkg/contracts/solana/gateway_message_test.go rename pkg/{contract => contracts}/solana/idl.go (100%) create mode 100644 pkg/contracts/solana/instruction.go create mode 100644 pkg/contracts/solana/instruction_test.go create mode 100644 pkg/contracts/solana/pda.go create mode 100644 zetaclient/chains/solana/observer/observer_gas.go create mode 100644 zetaclient/chains/solana/observer/outbound_test.go create mode 100644 zetaclient/chains/solana/signer/outbound_tracker_reporter.go create mode 100644 zetaclient/chains/solana/signer/signer.go create mode 100644 zetaclient/chains/solana/signer/withdraw.go create mode 100644 zetaclient/testdata/solana/chain_901_outbound_tx_result_5iBYjBYCphzjHKfmPwddMWpV2RNssmzk9Z8NNmV9Rei71pZKBTEVdkmUeyXfn7eWbV8932uSsPfBxgA7UgERNTvq.json create mode 100644 zetaclient/testdata/solana/chain_901_outbound_tx_result_5nFUQgNSdqTd4aPS4a1xNcbehj19hDzuQLfBqFRj8g7BJdESVY6hFuTFPWFuV6aWAfzEMfVfCdNu9DfzVp5FsHg5.json create mode 100644 zetaclient/testutils/mocks/solana_rpc.go diff --git a/changelog.md b/changelog.md index ae097c3163..39fff5b0d5 100644 --- a/changelog.md +++ b/changelog.md @@ -43,7 +43,8 @@ * [2518](https://github.com/zeta-chain/node/pull/2518) - add support for Solana address in zetacore * [2483](https://github.com/zeta-chain/node/pull/2483) - add priorityFee (gasTipCap) gas to the state * [2567](https://github.com/zeta-chain/node/pull/2567) - add sign latency metric to zetaclient (zetaclient_sign_latency) -* [2524](https://github.com/zeta-chain/node/pull/2524) - add inscription envolop parsing +* [2524](https://github.com/zeta-chain/node/pull/2524) - add inscription envolop parsing +* [2560](https://github.com/zeta-chain/node/pull/2560) - add support for Solana SOL token withdraw ### Refactor diff --git a/cmd/zetaclientd/init.go b/cmd/zetaclientd/init.go index c6c231bf77..1b58265f90 100644 --- a/cmd/zetaclientd/init.go +++ b/cmd/zetaclientd/init.go @@ -1,6 +1,8 @@ package main import ( + "path" + "github.com/rs/zerolog" "github.com/spf13/cobra" @@ -36,6 +38,7 @@ type initArguments struct { KeyringBackend string HsmMode bool HsmHotKey string + SolanaKey string } func init() { @@ -69,6 +72,7 @@ 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") } func Initialize(_ *cobra.Command, _ []string) error { @@ -106,8 +110,16 @@ 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.ComplianceConfig = testutils.ComplianceConfigTest() - //Save config file + // 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 new file mode 100644 index 0000000000..12a266dd9d --- /dev/null +++ b/cmd/zetaclientd/solana_test_key.go @@ -0,0 +1,37 @@ +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/utils.go b/cmd/zetaclientd/utils.go index d2b5801bef..b25de0a2b5 100644 --- a/cmd/zetaclientd/utils.go +++ b/cmd/zetaclientd/utils.go @@ -41,5 +41,3 @@ func CreateZetacoreClient(cfg config.Config, hotkeyPassword string, logger zerol return client, nil } - -// TODO diff --git a/cmd/zetae2e/local/local.go b/cmd/zetae2e/local/local.go index 72d01e4584..77041b00f4 100644 --- a/cmd/zetae2e/local/local.go +++ b/cmd/zetae2e/local/local.go @@ -321,7 +321,11 @@ func localE2ETest(cmd *cobra.Command, _ []string) { logger.Print("❌ solana client is nil, maybe solana rpc is not set") os.Exit(1) } - eg.Go(solanaTestRoutine(conf, deployerRunner, verbose, e2etests.TestSolanaDepositName)) + solanaTests := []string{ + e2etests.TestSolanaDepositName, + e2etests.TestSolanaWithdrawName, + } + eg.Go(solanaTestRoutine(conf, deployerRunner, verbose, solanaTests...)) } // while tests are executed, monitor blocks in parallel to check if system txs are on top and they have biggest priority diff --git a/contrib/localnet/orchestrator/start-zetae2e.sh b/contrib/localnet/orchestrator/start-zetae2e.sh index 5c91edbd2e..dda5b12cd7 100644 --- a/contrib/localnet/orchestrator/start-zetae2e.sh +++ b/contrib/localnet/orchestrator/start-zetae2e.sh @@ -66,6 +66,11 @@ 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 +# 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 + # unlock ethers tester accounts address=$(yq -r '.additional_accounts.user_ether.evm_address' config.yml) echo "funding ether tester address ${address} with 10000 Ether" diff --git a/contrib/localnet/scripts/start-zetacored.sh b/contrib/localnet/scripts/start-zetacored.sh index fca8e25152..43c4ee72e2 100755 --- a/contrib/localnet/scripts/start-zetacored.sh +++ b/contrib/localnet/scripts/start-zetacored.sh @@ -247,6 +247,9 @@ then # bitcoin tester address=$(yq -r '.additional_accounts.user_bitcoin.bech32_address' /root/config.yml) zetacored add-genesis-account "$address" 100000000000000000000000000azeta +# solana tester + address=$(yq -r '.additional_accounts.user_solana.bech32_address' /root/config.yml) + zetacored add-genesis-account "$address" 100000000000000000000000000azeta # ethers tester address=$(yq -r '.additional_accounts.user_ether.bech32_address' /root/config.yml) zetacored add-genesis-account "$address" 100000000000000000000000000azeta diff --git a/docs/cli/zetacored/zetacored_tx_fungible_update-gateway-contract.md b/docs/cli/zetacored/zetacored_tx_fungible_update-gateway-contract.md index d75877c440..0b4217c65b 100644 --- a/docs/cli/zetacored/zetacored_tx_fungible_update-gateway-contract.md +++ b/docs/cli/zetacored/zetacored_tx_fungible_update-gateway-contract.md @@ -3,7 +3,7 @@ Broadcast message UpdateGatewayContract to update the gateway contract address ``` -zetacored tx fungible update-gateway-contract [contract-address] [flags] +zetacored tx fungible update-gateway-contract [contract-address] [flags] ``` ### Options diff --git a/e2e/e2etests/e2etests.go b/e2e/e2etests/e2etests.go index ace69bd84f..fc0f0988dc 100644 --- a/e2e/e2etests/e2etests.go +++ b/e2e/e2etests/e2etests.go @@ -54,7 +54,8 @@ const ( /* Solana tests */ - TestSolanaDepositName = "solana_deposit" + TestSolanaDepositName = "solana_deposit" + TestSolanaWithdrawName = "solana_withdraw" /* Bitcoin tests @@ -338,10 +339,18 @@ var AllE2ETests = []runner.E2ETest{ TestSolanaDepositName, "deposit SOL into ZEVM", []runner.ArgDefinition{ - {Description: "amount in SOL", DefaultValue: "0.1"}, + {Description: "amount in lamport", DefaultValue: "13370000"}, }, TestSolanaDeposit, ), + runner.NewE2ETest( + TestSolanaWithdrawName, + "withdraw SOL from ZEVM", + []runner.ArgDefinition{ + {Description: "amount in lamport", DefaultValue: "1336000"}, + }, + TestSolanaWithdraw, + ), /* Bitcoin tests */ diff --git a/e2e/e2etests/test_solana_deposit.go b/e2e/e2etests/test_solana_deposit.go index 581a420776..486ae89782 100644 --- a/e2e/e2etests/test_solana_deposit.go +++ b/e2e/e2etests/test_solana_deposit.go @@ -1,20 +1,29 @@ package e2etests import ( + "math/big" + "github.com/gagliardetto/solana-go" + "github.com/stretchr/testify/require" "github.com/zeta-chain/zetacore/e2e/runner" "github.com/zeta-chain/zetacore/e2e/utils" crosschaintypes "github.com/zeta-chain/zetacore/x/crosschain/types" ) -func TestSolanaDeposit(r *runner.E2ERunner, _ []string) { +func TestSolanaDeposit(r *runner.E2ERunner, args []string) { + require.Len(r, args, 1) + + // parse deposit amount (in lamports) + // #nosec G115 e2e - always in range + depositAmount := big.NewInt(int64(parseInt(r, args[0]))) + // load deployer private key - privkey := solana.MustPrivateKeyFromBase58(r.Account.SolanaPrivateKey.String()) + privkey, err := solana.PrivateKeyFromBase58(r.Account.SolanaPrivateKey.String()) + require.NoError(r, err) // create 'deposit' instruction - amount := uint64(13370000) - instruction := r.CreateDepositInstruction(privkey.PublicKey(), r.EVMAddress(), amount) + instruction := r.CreateDepositInstruction(privkey.PublicKey(), r.EVMAddress(), depositAmount.Uint64()) // create and sign the transaction signedTx := r.CreateSignedTransaction([]solana.Instruction{instruction}, privkey) diff --git a/e2e/e2etests/test_solana_withdraw.go b/e2e/e2etests/test_solana_withdraw.go new file mode 100644 index 0000000000..776944c9b3 --- /dev/null +++ b/e2e/e2etests/test_solana_withdraw.go @@ -0,0 +1,47 @@ +package e2etests + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/gagliardetto/solana-go" + "github.com/stretchr/testify/require" + + "github.com/zeta-chain/zetacore/e2e/runner" +) + +func TestSolanaWithdraw(r *runner.E2ERunner, args []string) { + require.Len(r, args, 1) + + // print balanceAfter of from address + balanceBefore, err := r.SOLZRC20.BalanceOf(&bind.CallOpts{}, r.ZEVMAuth.From) + require.NoError(r, err) + r.Logger.Info("from address %s balance of SOL before: %d", r.ZEVMAuth.From, balanceBefore) + + // parse withdraw amount (in lamports), approve amount is 1 SOL + approvedAmount := new(big.Int).SetUint64(solana.LAMPORTS_PER_SOL) + // #nosec G115 e2e - always in range + withdrawAmount := big.NewInt(int64(parseInt(r, args[0]))) + require.Equal( + r, + -1, + withdrawAmount.Cmp(approvedAmount), + "Withdrawal amount must be less than the approved amount (1e9).", + ) + + // load deployer private key + privkey, err := solana.PrivateKeyFromBase58(r.Account.SolanaPrivateKey.String()) + require.NoError(r, err) + + // withdraw + r.WithdrawSOLZRC20(privkey.PublicKey(), withdrawAmount, approvedAmount) + + // print balance of from address after withdraw + balanceAfter, err := r.SOLZRC20.BalanceOf(&bind.CallOpts{}, r.ZEVMAuth.From) + require.NoError(r, err) + r.Logger.Info("from address %s balance of SOL after: %d", r.ZEVMAuth.From, balanceAfter) + + // check if the balance is reduced correctly + amountReduced := new(big.Int).Sub(balanceBefore, balanceAfter) + require.True(r, amountReduced.Cmp(withdrawAmount) >= 0, "balance is not reduced correctly") +} diff --git a/e2e/runner/setup_solana.go b/e2e/runner/setup_solana.go index c9663dc3b8..10aa4b1a15 100644 --- a/e2e/runner/setup_solana.go +++ b/e2e/runner/setup_solana.go @@ -1,8 +1,6 @@ package runner import ( - "time" - ethcommon "github.com/ethereum/go-ethereum/common" "github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go/rpc" @@ -10,22 +8,13 @@ import ( "github.com/stretchr/testify/require" "github.com/zeta-chain/zetacore/pkg/chains" - solanacontract "github.com/zeta-chain/zetacore/pkg/contract/solana" + solanacontracts "github.com/zeta-chain/zetacore/pkg/contracts/solana" ) +// SetupSolanaAccount imports the deployer's private key func (r *E2ERunner) SetupSolanaAccount() { - r.Logger.Print("⚙️ setting up Solana account") - startTime := time.Now() - defer func() { - r.Logger.Print("✅ Solana account setup in %s", time.Since(startTime)) - }() - - r.SetSolanaAddress() -} - -// SetSolanaAddress imports the deployer's private key -func (r *E2ERunner) SetSolanaAddress() { - privateKey := solana.MustPrivateKeyFromBase58(r.Account.SolanaPrivateKey.String()) + privateKey, err := solana.PrivateKeyFromBase58(r.Account.SolanaPrivateKey.String()) + require.NoError(r, err) r.SolanaDeployerAddress = privateKey.PublicKey() r.Logger.Info("SolanaDeployerAddress: %s", r.SolanaDeployerAddress) @@ -33,13 +22,14 @@ func (r *E2ERunner) SetSolanaAddress() { // SetSolanaContracts set Solana contracts func (r *E2ERunner) SetSolanaContracts(deployerPrivateKey string) { - r.Logger.Print("⚙️ setting up Solana contracts") + r.Logger.Print("⚙️ initializing gateway program on Solana") // set Solana contracts - r.GatewayProgram = solana.MustPublicKeyFromBase58(solanacontract.SolanaGatewayProgramID) + r.GatewayProgram = solana.MustPublicKeyFromBase58(solanacontracts.SolanaGatewayProgramID) // get deployer account balance - privkey := solana.MustPrivateKeyFromBase58(deployerPrivateKey) + privkey, err := solana.PrivateKeyFromBase58(deployerPrivateKey) + require.NoError(r, err) bal, err := r.SolanaClient.GetBalance(r.Ctx, privkey.PublicKey(), rpc.CommitmentFinalized) require.NoError(r, err) r.Logger.Info("deployer address: %s, balance: %f SOL", privkey.PublicKey().String(), float64(bal.Value)/1e9) @@ -57,8 +47,8 @@ func (r *E2ERunner) SetSolanaContracts(deployerPrivateKey string) { inst.ProgID = r.GatewayProgram inst.AccountValues = accountSlice - inst.DataBytes, err = borsh.Serialize(solanacontract.InitializeParams{ - Discriminator: solanacontract.DiscriminatorInitialize(), + inst.DataBytes, err = borsh.Serialize(solanacontracts.InitializeParams{ + Discriminator: solanacontracts.DiscriminatorInitialize(), TssAddress: r.TSSAddress, ChainID: uint64(chains.SolanaLocalnet.ChainId), }) @@ -76,7 +66,7 @@ func (r *E2ERunner) SetSolanaContracts(deployerPrivateKey string) { require.NoError(r, err) // deserialize the PDA info - pda := solanacontract.PdaInfo{} + pda := solanacontracts.PdaInfo{} err = borsh.Deserialize(&pda, pdaInfo.Bytes()) require.NoError(r, err) tssAddress := ethcommon.BytesToAddress(pda.TssAddress[:]) diff --git a/e2e/runner/solana.go b/e2e/runner/solana.go index 3338eaa6f4..71d2cfb629 100644 --- a/e2e/runner/solana.go +++ b/e2e/runner/solana.go @@ -1,6 +1,7 @@ package runner import ( + "math/big" "time" ethcommon "github.com/ethereum/go-ethereum/common" @@ -9,7 +10,9 @@ import ( "github.com/near/borsh-go" "github.com/stretchr/testify/require" - solanacontract "github.com/zeta-chain/zetacore/pkg/contract/solana" + "github.com/zeta-chain/zetacore/e2e/utils" + solanacontract "github.com/zeta-chain/zetacore/pkg/contracts/solana" + crosschaintypes "github.com/zeta-chain/zetacore/x/crosschain/types" ) // ComputePdaAddress computes the PDA address for the gateway program @@ -105,3 +108,26 @@ func (r *E2ERunner) BroadcastTxSync(tx *solana.Transaction) (solana.Signature, * return sig, out } + +// WithdrawSOLZRC20 withdraws an amount of ZRC20 SOL tokens +func (r *E2ERunner) WithdrawSOLZRC20(to solana.PublicKey, amount *big.Int, approveAmount *big.Int) { + // approve + tx, err := r.SOLZRC20.Approve(r.ZEVMAuth, r.SOLZRC20Addr, approveAmount) + require.NoError(r, err) + receipt := utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequireTxSuccessful(r, receipt) + + // withdraw + tx, err = r.SOLZRC20.Withdraw(r.ZEVMAuth, []byte(to.String()), amount) + require.NoError(r, err) + r.Logger.EVMTransaction(*tx, "withdraw") + + // wait for tx receipt + receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequireTxSuccessful(r, receipt) + r.Logger.Info("Receipt txhash %s status %d", receipt.TxHash, receipt.Status) + + // wait for the cctx to be mined + cctx := utils.WaitCctxMinedByInboundHash(r.Ctx, tx.Hash().Hex(), r.CctxClient, r.Logger, r.CctxTimeout) + utils.RequireCCTXStatus(r, cctx, crosschaintypes.CctxStatus_OutboundMined) +} diff --git a/e2e/txserver/zeta_tx_server.go b/e2e/txserver/zeta_tx_server.go index 2ce07f0fc5..8ec4496539 100644 --- a/e2e/txserver/zeta_tx_server.go +++ b/e2e/txserver/zeta_tx_server.go @@ -431,7 +431,7 @@ func (zts ZetaTxServer) DeploySystemContractsAndZRC20( 100000, )) if err != nil { - return SystemContractAddresses{}, fmt.Errorf("failed to deploy btc zrc20: %s", err.Error()) + return SystemContractAddresses{}, fmt.Errorf("failed to deploy sol zrc20: %s", err.Error()) } // deploy erc20 zrc20 diff --git a/pkg/contract/solana/types.go b/pkg/contract/solana/types.go deleted file mode 100644 index eede621c06..0000000000 --- a/pkg/contract/solana/types.go +++ /dev/null @@ -1,44 +0,0 @@ -package solana - -// PdaInfo represents the PDA for the gateway program -type PdaInfo struct { - // Discriminator is the unique identifier for the PDA - Discriminator [8]byte - - // Nonce is the current nonce for the PDA - Nonce uint64 - - // TssAddress is the TSS address for the PDA - TssAddress [20]byte - - // Authority is the authority for the PDA - Authority [32]byte - - // ChainId is the chain ID for the gateway program - // Note: this field exists in latest version of gateway program, but not in the current e2e test program - // ChainId uint64 -} - -// InitializeParams contains the parameters for a gateway initialize instruction -type InitializeParams struct { - // Discriminator is the unique identifier for the initialize instruction - Discriminator [8]byte - - // TssAddress is the TSS address - TssAddress [20]byte - - // ChainID is the chain ID for the gateway program - ChainID uint64 -} - -// DepositInstructionParams contains the parameters for a gateway deposit instruction -type DepositInstructionParams struct { - // Discriminator is the unique identifier for the deposit instruction - Discriminator [8]byte - - // Amount is the lamports amount for the deposit - Amount uint64 - - // Memo is the memo for the deposit - Memo []byte -} diff --git a/pkg/contract/solana/contract.go b/pkg/contracts/solana/gateway.go similarity index 65% rename from pkg/contract/solana/contract.go rename to pkg/contracts/solana/gateway.go index 2e2acd7ad6..356614e4ce 100644 --- a/pkg/contract/solana/contract.go +++ b/pkg/contracts/solana/gateway.go @@ -1,5 +1,11 @@ +// Package solana privides structures and constants that are used when interacting with the gateway program on Solana chain. package solana +import ( + "github.com/gagliardetto/solana-go" + "github.com/pkg/errors" +) + const ( // SolanaGatewayProgramID is the program ID of the Solana gateway program SolanaGatewayProgramID = "94U5AHQMKkV5txNJ17QPXWoh474PheGou6cNP2FEuL1d" @@ -36,3 +42,20 @@ func DiscriminatorWithdraw() [8]byte { func DiscriminatorWithdrawSPL() [8]byte { return [8]byte{156, 234, 11, 89, 235, 246, 32} } + +// ParseGatewayAddressAndPda parses the gateway id and program derived address from the given string +func ParseGatewayIDAndPda(address string) (solana.PublicKey, solana.PublicKey, error) { + var gatewayID, pda solana.PublicKey + + // decode gateway address + gatewayID, err := solana.PublicKeyFromBase58(address) + if err != nil { + return gatewayID, pda, errors.Wrap(err, "unable to decode address") + } + + // compute gateway PDA + seed := []byte(PDASeed) + pda, _, err = solana.FindProgramAddress([][]byte{seed}, gatewayID) + + return gatewayID, pda, err +} diff --git a/pkg/contract/solana/gateway.json b/pkg/contracts/solana/gateway.json similarity index 100% rename from pkg/contract/solana/gateway.json rename to pkg/contracts/solana/gateway.json diff --git a/pkg/contracts/solana/gateway_message.go b/pkg/contracts/solana/gateway_message.go new file mode 100644 index 0000000000..021af3cf1f --- /dev/null +++ b/pkg/contracts/solana/gateway_message.go @@ -0,0 +1,107 @@ +package solana + +import ( + "encoding/binary" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/gagliardetto/solana-go" +) + +// MsgWithdraw is the message for the Solana gateway withdraw/withdraw_spl instruction +type MsgWithdraw struct { + // chainID is the chain ID of Solana chain + chainID uint64 + + // Nonce is the nonce for the withdraw/withdraw_spl + nonce uint64 + + // amount is the lamports amount for the withdraw/withdraw_spl + amount uint64 + + // To is the recipient address for the withdraw/withdraw_spl + to solana.PublicKey + + // signature is the signature of the message + signature [65]byte +} + +// NewMsgWithdraw returns a new withdraw message +func NewMsgWithdraw(chainID, nonce, amount uint64, to solana.PublicKey) *MsgWithdraw { + return &MsgWithdraw{ + chainID: chainID, + nonce: nonce, + amount: amount, + to: to, + } +} + +// ChainID returns the chain ID of the message +func (msg *MsgWithdraw) ChainID() uint64 { + return msg.chainID +} + +// Nonce returns the nonce of the message +func (msg *MsgWithdraw) Nonce() uint64 { + return msg.nonce +} + +// Amount returns the amount of the message +func (msg *MsgWithdraw) Amount() uint64 { + return msg.amount +} + +// To returns the recipient address of the message +func (msg *MsgWithdraw) To() solana.PublicKey { + return msg.to +} + +// Hash packs the withdraw message and computes the hash +func (msg *MsgWithdraw) Hash() [32]byte { + var message []byte + buff := make([]byte, 8) + + binary.BigEndian.PutUint64(buff, msg.chainID) + message = append(message, buff...) + + binary.BigEndian.PutUint64(buff, msg.nonce) + message = append(message, buff...) + + binary.BigEndian.PutUint64(buff, msg.amount) + message = append(message, buff...) + + message = append(message, msg.to.Bytes()...) + + return crypto.Keccak256Hash(message) +} + +// SetSignature attaches the signature to the message +func (msg *MsgWithdraw) SetSignature(signature [65]byte) *MsgWithdraw { + msg.signature = signature + return msg +} + +// SigRSV returns the full 65-byte [R+S+V] signature +func (msg *MsgWithdraw) SigRSV() [65]byte { + return msg.signature +} + +// SigRS returns the 64-byte [R+S] core part of the signature +func (msg *MsgWithdraw) SigRS() [64]byte { + var sig [64]byte + copy(sig[:], msg.signature[:64]) + return sig +} + +// SigV returns the V part (recovery ID) of the signature +func (msg *MsgWithdraw) SigV() uint8 { + return msg.signature[64] +} + +// Signer returns the signer of the message +func (msg *MsgWithdraw) Signer() (common.Address, error) { + msgHash := msg.Hash() + msgSig := msg.SigRSV() + + return RecoverSigner(msgHash[:], msgSig[:]) +} diff --git a/pkg/contracts/solana/gateway_message_test.go b/pkg/contracts/solana/gateway_message_test.go new file mode 100644 index 0000000000..b2ffc24489 --- /dev/null +++ b/pkg/contracts/solana/gateway_message_test.go @@ -0,0 +1,31 @@ +package solana_test + +import ( + "bytes" + "encoding/hex" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/gagliardetto/solana-go" + "github.com/zeta-chain/zetacore/pkg/chains" + contracts "github.com/zeta-chain/zetacore/pkg/contracts/solana" +) + +func Test_MsgWithdrawHash(t *testing.T) { + t.Run("should pass for archived inbound, receipt and cctx", func(t *testing.T) { + // #nosec G115 always positive + chainID := uint64(chains.SolanaLocalnet.ChainId) + nonce := uint64(0) + amount := uint64(1336000) + to := solana.MustPublicKeyFromBase58("37yGiHAnLvWZUNVwu9esp74YQFqxU1qHCbABkDvRddUQ") + + wantHash := "a20cddb3f888f4064ced892a477101f45469a8c50f783b966d3fec2455887c05" + wantHashBytes, err := hex.DecodeString(wantHash) + require.NoError(t, err) + + // create new withdraw message + hash := contracts.NewMsgWithdraw(chainID, nonce, amount, to).Hash() + require.True(t, bytes.Equal(hash[:], wantHashBytes)) + }) +} diff --git a/pkg/contract/solana/idl.go b/pkg/contracts/solana/idl.go similarity index 100% rename from pkg/contract/solana/idl.go rename to pkg/contracts/solana/idl.go diff --git a/pkg/contracts/solana/instruction.go b/pkg/contracts/solana/instruction.go new file mode 100644 index 0000000000..f338129c9b --- /dev/null +++ b/pkg/contracts/solana/instruction.go @@ -0,0 +1,118 @@ +package solana + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/gagliardetto/solana-go" + "github.com/near/borsh-go" + "github.com/pkg/errors" +) + +// InitializeParams contains the parameters for a gateway initialize instruction +type InitializeParams struct { + // Discriminator is the unique identifier for the initialize instruction + Discriminator [8]byte + + // TssAddress is the TSS address + TssAddress [20]byte + + // ChainID is the chain ID for the gateway program + ChainID uint64 +} + +// DepositInstructionParams contains the parameters for a gateway deposit instruction +type DepositInstructionParams struct { + // Discriminator is the unique identifier for the deposit instruction + Discriminator [8]byte + + // Amount is the lamports amount for the deposit + Amount uint64 + + // Memo is the memo for the deposit + Memo []byte +} + +// OutboundInstruction is the interface for all gateway outbound instructions +type OutboundInstruction interface { + // Signer returns the signer of the instruction + Signer() (common.Address, error) + + // GatewayNonce returns the nonce of the instruction + GatewayNonce() uint64 + + // TokenAmount returns the amount of the instruction + TokenAmount() uint64 +} + +var _ OutboundInstruction = (*WithdrawInstructionParams)(nil) + +// WithdrawInstructionParams contains the parameters for a gateway withdraw instruction +type WithdrawInstructionParams struct { + // Discriminator is the unique identifier for the withdraw instruction + Discriminator [8]byte + + // Amount is the lamports amount for the withdraw + Amount uint64 + + // Signature is the ECDSA signature (by TSS) for the withdraw + Signature [64]byte + + // RecoveryID is the recovery ID used to recover the public key from ECDSA signature + RecoveryID uint8 + + // MessageHash is the hash of the message signed by TSS + MessageHash [32]byte + + // Nonce is the nonce for the withdraw + Nonce uint64 +} + +// Signer returns the signer of the signature contained +func (inst *WithdrawInstructionParams) Signer() (signer common.Address, err error) { + var signature [65]byte + copy(signature[:], inst.Signature[:64]) + signature[64] = inst.RecoveryID + + return RecoverSigner(inst.MessageHash[:], signature[:]) +} + +// GatewayNonce returns the nonce of the instruction +func (inst *WithdrawInstructionParams) GatewayNonce() uint64 { + return inst.Nonce +} + +// TokenAmount returns the amount of the instruction +func (inst *WithdrawInstructionParams) TokenAmount() uint64 { + return inst.Amount +} + +// ParseInstructionWithdraw tries to parse the instruction as a 'withdraw'. +// It returns nil if the instruction can't be parsed as a 'withdraw'. +func ParseInstructionWithdraw(instruction solana.CompiledInstruction) (*WithdrawInstructionParams, error) { + // try deserializing instruction as a 'withdraw' + inst := &WithdrawInstructionParams{} + err := borsh.Deserialize(inst, instruction.Data) + if err != nil { + return nil, errors.Wrap(err, "error deserializing instruction") + } + + // check the discriminator to ensure it's a 'withdraw' instruction + if inst.Discriminator != DiscriminatorWithdraw() { + return nil, fmt.Errorf("not a withdraw instruction: %v", inst.Discriminator) + } + + return inst, nil +} + +// RecoverSigner recover the ECDSA signer from given message hash and signature +func RecoverSigner(msgHash []byte, msgSig []byte) (signer common.Address, err error) { + // recover the public key + pubKey, err := crypto.SigToPub(msgHash, msgSig) + if err != nil { + return + } + + return crypto.PubkeyToAddress(*pubKey), nil +} diff --git a/pkg/contracts/solana/instruction_test.go b/pkg/contracts/solana/instruction_test.go new file mode 100644 index 0000000000..7f284169f1 --- /dev/null +++ b/pkg/contracts/solana/instruction_test.go @@ -0,0 +1,80 @@ +package solana_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + ethcommon "github.com/ethereum/go-ethereum/common" + contracts "github.com/zeta-chain/zetacore/pkg/contracts/solana" +) + +const ( + // testSigner is the address of the signer for unit tests + testSigner = "0xaD32427bA235a8350b7805C1b85147c8ea03F437" +) + +// getTestSignature returns the signature produced by 'testSigner' for the withdraw instruction: +// ChainID: 902 +// Nonce: 0 +// Amount: 1336000 +// To: 37yGiHAnLvWZUNVwu9esp74YQFqxU1qHCbABkDvRddUQ +func getTestSignature() [65]byte { + return [65]byte{ + 57, 160, 150, 241, 113, 78, 5, 205, 104, 97, 176, 136, 113, 84, 183, 119, + 213, 119, 29, 1, 183, 3, 43, 27, 140, 39, 33, 185, 6, 122, 69, 140, + 42, 102, 187, 143, 110, 9, 106, 162, 158, 26, 135, 253, 130, 157, 216, 191, + 117, 23, 179, 243, 109, 175, 101, 19, 95, 192, 16, 240, 40, 99, 105, 216, 0, + } +} + +// getTestmessageHash returns the message hash used to produce 'testSignature' +func getTestmessageHash() [32]byte { + return [32]byte{ + 162, 12, 221, 179, 248, 136, 244, 6, 76, 237, 137, 42, 71, 113, 1, 244, + 84, 105, 168, 197, 15, 120, 59, 150, 109, 63, 236, 36, 85, 136, 124, 5, + } +} + +func Test_SignerWithdraw(t *testing.T) { + var sigRS [64]byte + sigTest := getTestSignature() + copy(sigRS[:], sigTest[:64]) + + // create a withdraw instruction + inst := contracts.WithdrawInstructionParams{ + Signature: sigRS, + RecoveryID: 0, + MessageHash: getTestmessageHash(), + } + + // recover signer + signer, err := inst.Signer() + require.NoError(t, err) + require.EqualValues(t, testSigner, signer.String()) +} + +func Test_RecoverSigner(t *testing.T) { + sigTest := getTestSignature() + hashTest := getTestmessageHash() + + // recover the signer from the test message hash and signature + signer, err := contracts.RecoverSigner(hashTest[:], sigTest[:]) + require.NoError(t, err) + require.EqualValues(t, testSigner, signer.String()) + + // slightly modify the signature and recover the signer + sigFake := sigTest + sigFake[0]++ + signer, err = contracts.RecoverSigner(hashTest[:], sigFake[:]) + require.Error(t, err) + require.Equal(t, ethcommon.Address{}, signer) + + // slightly modify the message hash and recover the signer + hashFake := hashTest + hashFake[0]++ + signer, err = contracts.RecoverSigner(hashFake[:], sigTest[:]) + require.NoError(t, err) + require.NotEqual(t, ethcommon.Address{}, signer) + require.NotEqual(t, testSigner, signer.String()) +} diff --git a/pkg/contracts/solana/pda.go b/pkg/contracts/solana/pda.go new file mode 100644 index 0000000000..0452d23a15 --- /dev/null +++ b/pkg/contracts/solana/pda.go @@ -0,0 +1,19 @@ +package solana + +// PdaInfo represents the PDA for the gateway program +type PdaInfo struct { + // Discriminator is the unique identifier for the PDA + Discriminator [8]byte + + // Nonce is the current nonce for the PDA + Nonce uint64 + + // TssAddress is the TSS address for the PDA + TssAddress [20]byte + + // Authority is the authority for the PDA + Authority [32]byte + + // ChainId is the Solana chain id + ChainID uint64 +} diff --git a/x/observer/types/chain_params.go b/x/observer/types/chain_params.go index b2cbb63c32..b35d7da930 100644 --- a/x/observer/types/chain_params.go +++ b/x/observer/types/chain_params.go @@ -11,7 +11,7 @@ import ( "github.com/pkg/errors" "github.com/zeta-chain/zetacore/pkg/chains" - solanacontract "github.com/zeta-chain/zetacore/pkg/contract/solana" + solanacontracts "github.com/zeta-chain/zetacore/pkg/contracts/solana" ) const ( @@ -163,6 +163,7 @@ func GetDefaultChainParams() ChainParamsList { GetDefaultMumbaiTestnetChainParams(), GetDefaultBtcTestnetChainParams(), GetDefaultBtcRegtestChainParams(), + GetDefaultSolanaLocalnetChainParams(), GetDefaultGoerliLocalnetChainParams(), GetDefaultZetaPrivnetChainParams(), }, @@ -321,16 +322,16 @@ func GetDefaultSolanaLocalnetChainParams() *ChainParams { ZetaTokenContractAddress: zeroAddress, ConnectorContractAddress: zeroAddress, Erc20CustodyContractAddress: zeroAddress, - GasPriceTicker: 100, + GasPriceTicker: 5, WatchUtxoTicker: 0, - InboundTicker: 5, - OutboundTicker: 5, - OutboundScheduleInterval: 10, - OutboundScheduleLookahead: 10, + InboundTicker: 2, + OutboundTicker: 2, + OutboundScheduleInterval: 2, + OutboundScheduleLookahead: 5, BallotThreshold: DefaultBallotThreshold, MinObserverDelegation: DefaultMinObserverDelegation, IsSupported: false, - GatewayAddress: solanacontract.SolanaGatewayProgramID, + GatewayAddress: solanacontracts.SolanaGatewayProgramID, } } func GetDefaultGoerliLocalnetChainParams() *ChainParams { @@ -385,5 +386,6 @@ func ChainParamsEqual(params1, params2 ChainParams) bool { params1.OutboundScheduleLookahead == params2.OutboundScheduleLookahead && params1.BallotThreshold.Equal(params2.BallotThreshold) && params1.MinObserverDelegation.Equal(params2.MinObserverDelegation) && - params1.IsSupported == params2.IsSupported + params1.IsSupported == params2.IsSupported && + params1.GatewayAddress == params2.GatewayAddress } diff --git a/zetaclient/chains/base/observer.go b/zetaclient/chains/base/observer.go index eb0adbe5d8..428946f0bf 100644 --- a/zetaclient/chains/base/observer.go +++ b/zetaclient/chains/base/observer.go @@ -267,6 +267,11 @@ func (ob *Observer) WithHeaderCache(cache *lru.Cache) *Observer { return ob } +// OutboundID returns a unique identifier for the outbound transaction. +func (ob *Observer) OutboundID(nonce uint64) string { + return fmt.Sprintf("%d-%d", ob.chain.ChainId, nonce) +} + // DB returns the database for the observer. func (ob *Observer) DB() *db.DB { return ob.db diff --git a/zetaclient/chains/base/signer.go b/zetaclient/chains/base/signer.go index 2545c3639a..6618c338de 100644 --- a/zetaclient/chains/base/signer.go +++ b/zetaclient/chains/base/signer.go @@ -23,12 +23,15 @@ type Signer struct { // logger contains the loggers used by signer logger Logger + // outboundBeingReported is a map of outbound being reported to tracker + outboundBeingReported map[string]bool + // mu protects fields from concurrent access // Note: base signer simply provides the mutex. It's the sub-struct's responsibility to use it to be thread-safe mu sync.Mutex } -// NewSigner creates a new base signer +// NewSigner creates a new base signer. func NewSigner(chain chains.Chain, tss interfaces.TSSSigner, ts *metrics.TelemetryServer, logger Logger) *Signer { return &Signer{ chain: chain, @@ -38,53 +41,85 @@ func NewSigner(chain chains.Chain, tss interfaces.TSSSigner, ts *metrics.Telemet Std: logger.Std.With().Int64("chain", chain.ChainId).Str("module", "signer").Logger(), Compliance: logger.Compliance, }, + outboundBeingReported: make(map[string]bool), } } -// Chain returns the chain for the signer +// Chain returns the chain for the signer. func (s *Signer) Chain() chains.Chain { return s.chain } -// WithChain attaches a new chain to the signer +// WithChain attaches a new chain to the signer. func (s *Signer) WithChain(chain chains.Chain) *Signer { s.chain = chain return s } -// Tss returns the tss signer for the signer +// Tss returns the tss signer for the signer. func (s *Signer) TSS() interfaces.TSSSigner { return s.tss } -// WithTSS attaches a new tss signer to the signer +// WithTSS attaches a new tss signer to the signer. func (s *Signer) WithTSS(tss interfaces.TSSSigner) *Signer { s.tss = tss return s } -// TelemetryServer returns the telemetry server for the signer +// TelemetryServer returns the telemetry server for the signer. func (s *Signer) TelemetryServer() *metrics.TelemetryServer { return s.ts } -// WithTelemetryServer attaches a new telemetry server to the signer +// WithTelemetryServer attaches a new telemetry server to the signer. func (s *Signer) WithTelemetryServer(ts *metrics.TelemetryServer) *Signer { s.ts = ts return s } -// Logger returns the logger for the signer +// Logger returns the logger for the signer. func (s *Signer) Logger() *Logger { return &s.logger } -// Lock locks the signer +// SetBeingReportedFlag sets the outbound as being reported if not already set. +// Returns true if the outbound is already being reported. +// This method is used by outbound tracker reporter to avoid repeated reporting of same hash. +func (s *Signer) SetBeingReportedFlag(hash string) (alreadySet bool) { + s.Lock() + defer s.Unlock() + + alreadySet = s.outboundBeingReported[hash] + if !alreadySet { + // mark as being reported + s.outboundBeingReported[hash] = true + } + return +} + +// ClearBeingReportedFlag clears the being reported flag for the outbound. +func (s *Signer) ClearBeingReportedFlag(hash string) { + s.Lock() + defer s.Unlock() + delete(s.outboundBeingReported, hash) +} + +// Exported for unit tests + +// GetReportedTxList returns a list of outboundHash being reported. +// TODO: investigate pointer usage +// https://github.com/zeta-chain/node/issues/2084 +func (s *Signer) GetReportedTxList() *map[string]bool { + return &s.outboundBeingReported +} + +// Lock locks the signer. func (s *Signer) Lock() { s.mu.Lock() } -// Unlock unlocks the signer +// Unlock unlocks the signer. func (s *Signer) Unlock() { s.mu.Unlock() } diff --git a/zetaclient/chains/base/signer_test.go b/zetaclient/chains/base/signer_test.go index e35ed01792..dde542a4a0 100644 --- a/zetaclient/chains/base/signer_test.go +++ b/zetaclient/chains/base/signer_test.go @@ -61,3 +61,21 @@ func TestSignerGetterAndSetter(t *testing.T) { logger.Compliance.Info().Msg("print compliance log") }) } + +func Test_BeingReportedFlag(t *testing.T) { + signer := createSigner(t) + + // hash to be reported + hash := "0x1234" + alreadySet := signer.SetBeingReportedFlag(hash) + require.False(t, alreadySet) + + // set reported outbound again and check + alreadySet = signer.SetBeingReportedFlag(hash) + require.True(t, alreadySet) + + // clear reported outbound and check again + signer.ClearBeingReportedFlag(hash) + alreadySet = signer.SetBeingReportedFlag(hash) + require.False(t, alreadySet) +} diff --git a/zetaclient/chains/bitcoin/observer/observer.go b/zetaclient/chains/bitcoin/observer/observer.go index 519b3b99d8..8b4c79ba39 100644 --- a/zetaclient/chains/bitcoin/observer/observer.go +++ b/zetaclient/chains/bitcoin/observer/observer.go @@ -324,8 +324,7 @@ func (ob *Observer) WatchGasPrice(ctx context.Context) error { // start gas price ticker ticker, err := clienttypes.NewDynamicTicker("Bitcoin_WatchGasPrice", ob.GetChainParams().GasPriceTicker) if err != nil { - ob.logger.GasPrice.Error().Err(err).Msg("error creating ticker") - return err + return errors.Wrapf(err, "NewDynamicTicker error") } ob.logger.GasPrice.Info().Msgf("WatchGasPrice started for chain %d with interval %d", ob.Chain().ChainId, ob.GetChainParams().GasPriceTicker) @@ -383,7 +382,7 @@ func (ob *Observer) PostGasPrice(ctx context.Context) error { // query the current block number blockNumber, err := ob.btcClient.GetBlockCount() if err != nil { - return err + return errors.Wrap(err, "GetBlockCount error") } // UTXO has no concept of priority fee (like eth) @@ -392,8 +391,7 @@ func (ob *Observer) PostGasPrice(ctx context.Context) error { // #nosec G115 always positive _, err = ob.ZetacoreClient().PostVoteGasPrice(ctx, ob.Chain(), feeRateEstimated, priorityFee, uint64(blockNumber)) if err != nil { - ob.logger.GasPrice.Err(err).Msg("err PostGasPrice") - return err + return errors.Wrap(err, "PostVoteGasPrice error") } return nil @@ -537,7 +535,7 @@ func (ob *Observer) FetchUTXOs(ctx context.Context) error { // SaveBroadcastedTx saves successfully broadcasted transaction // TODO(revamp): move to db file func (ob *Observer) SaveBroadcastedTx(txHash string, nonce uint64) { - outboundID := ob.GetTxID(nonce) + outboundID := ob.OutboundID(nonce) ob.Mu().Lock() ob.broadcastedTx[outboundID] = txHash ob.Mu().Unlock() diff --git a/zetaclient/chains/bitcoin/observer/outbound.go b/zetaclient/chains/bitcoin/observer/outbound.go index c7c1c649d7..d6dd003caa 100644 --- a/zetaclient/chains/bitcoin/observer/outbound.go +++ b/zetaclient/chains/bitcoin/observer/outbound.go @@ -23,12 +23,6 @@ import ( "github.com/zeta-chain/zetacore/zetaclient/zetacore" ) -// GetTxID returns a unique id for outbound tx -func (ob *Observer) GetTxID(nonce uint64) string { - tssAddr := ob.TSS().BTCAddress() - return fmt.Sprintf("%d-%s-%d", ob.Chain().ChainId, tssAddr, nonce) -} - // WatchOutbound watches Bitcoin chain for outgoing txs status // TODO(revamp): move ticker functions to a specific file // TODO(revamp): move into a separate package @@ -66,7 +60,7 @@ func (ob *Observer) WatchOutbound(ctx context.Context) error { } for _, tracker := range trackers { // get original cctx parameters - outboundID := ob.GetTxID(tracker.Nonce) + outboundID := ob.OutboundID(tracker.Nonce) cctx, err := ob.ZetacoreClient().GetCctxByNonce(ctx, chainID, tracker.Nonce) if err != nil { ob.logger.Outbound.Info(). @@ -120,13 +114,11 @@ func (ob *Observer) WatchOutbound(ctx context.Context) error { } } -// IsOutboundProcessed returns isIncluded(or inMempool), isConfirmed, Error -// TODO(revamp): rename as it vote the outbound and doesn't only check if outbound is processed -func (ob *Observer) IsOutboundProcessed( +// VoteOutboundIfConfirmed checks outbound status and returns (continueKeysign, error) +func (ob *Observer) VoteOutboundIfConfirmed( ctx context.Context, cctx *crosschaintypes.CrossChainTx, - logger zerolog.Logger, -) (bool, bool, error) { +) (bool, error) { const ( // not used with Bitcoin outboundGasUsed = 0 @@ -141,8 +133,8 @@ func (ob *Observer) IsOutboundProcessed( nonce := cctx.GetCurrentOutboundParam().TssNonce // get broadcasted outbound and tx result - outboundID := ob.GetTxID(nonce) - logger.Info().Msgf("IsOutboundProcessed %s", outboundID) + outboundID := ob.OutboundID(nonce) + ob.Logger().Outbound.Info().Msgf("VoteOutboundIfConfirmed %s", outboundID) ob.Mu().Lock() txnHash, broadcasted := ob.broadcastedTx[outboundID] @@ -151,7 +143,7 @@ func (ob *Observer) IsOutboundProcessed( if !included { if !broadcasted { - return false, false, nil + return true, nil } // If the broadcasted outbound is nonce 0, just wait for inclusion and don't schedule more keysign // Schedule more than one keysign for nonce 0 can lead to duplicate payments. @@ -159,16 +151,16 @@ func (ob *Observer) IsOutboundProcessed( // prevents double spending of same UTXO. However, for nonce 0, we don't have a prior nonce (e.g., -1) // for the signer to check against when making the payment. Signer treats nonce 0 as a special case in downstream code. if nonce == 0 { - return true, false, nil + return false, nil } // Try including this outbound broadcasted by myself txResult, inMempool := ob.checkIncludedTx(ctx, cctx, txnHash) if txResult == nil { // check failed, try again next time - return false, false, nil + return true, nil } else if inMempool { // still in mempool (should avoid unnecessary Tss keysign) - ob.logger.Outbound.Info().Msgf("IsOutboundProcessed: outbound %s is still in mempool", outboundID) - return true, false, nil + ob.logger.Outbound.Info().Msgf("VoteOutboundIfConfirmed: outbound %s is still in mempool", outboundID) + return false, nil } // included ob.setIncludedTx(nonce, txResult) @@ -176,9 +168,9 @@ func (ob *Observer) IsOutboundProcessed( // Get tx result again in case it is just included res = ob.getIncludedTx(nonce) if res == nil { - return false, false, nil + return true, nil } - ob.logger.Outbound.Info().Msgf("IsOutboundProcessed: setIncludedTx succeeded for outbound %s", outboundID) + ob.logger.Outbound.Info().Msgf("VoteOutboundIfConfirmed: setIncludedTx succeeded for outbound %s", outboundID) } // It's safe to use cctx's amount to post confirmation because it has already been verified in observeOutbound() @@ -187,22 +179,24 @@ func (ob *Observer) IsOutboundProcessed( ob.logger.Outbound.Debug(). Int64("currentConfirmations", res.Confirmations). Int64("requiredConfirmations", ob.ConfirmationsThreshold(amountInSat)). - Msg("IsOutboundProcessed: outbound not confirmed yet") + Msg("VoteOutboundIfConfirmed: outbound not confirmed yet") - return true, false, nil + return false, nil } // Get outbound block height blockHeight, err := rpc.GetBlockHeightByHash(ob.btcClient, res.BlockHash) if err != nil { - return true, false, errors.Wrapf( + return false, errors.Wrapf( err, - "IsOutboundProcessed: error getting block height by hash %s", + "VoteOutboundIfConfirmed: error getting block height by hash %s", res.BlockHash, ) } - logger.Debug().Msgf("Bitcoin outbound confirmed: txid %s, amount %s\n", res.TxID, amountInSat.String()) + ob.Logger(). + Outbound.Debug(). + Msgf("Bitcoin outbound confirmed: txid %s, amount %s\n", res.TxID, amountInSat.String()) signer := ob.ZetacoreClient().GetKeys().GetOperatorAddress() @@ -236,12 +230,16 @@ func (ob *Observer) IsOutboundProcessed( } if err != nil { - logger.Error().Err(err).Fields(logFields).Msg("IsOutboundProcessed: error confirming bitcoin outbound") + ob.Logger(). + Outbound.Error(). + Err(err). + Fields(logFields). + Msg("VoteOutboundIfConfirmed: error confirming bitcoin outbound") } else if zetaHash != "" { - logger.Info().Fields(logFields).Msgf("IsOutboundProcessed: confirmed Bitcoin outbound") + ob.Logger().Outbound.Info().Fields(logFields).Msgf("VoteOutboundIfConfirmed: confirmed Bitcoin outbound") } - return true, true, nil + return false, nil } // SelectUTXOs selects a sublist of utxos to be used as inputs. @@ -438,7 +436,7 @@ func (ob *Observer) checkIncludedTx( cctx *crosschaintypes.CrossChainTx, txHash string, ) (*btcjson.GetTransactionResult, bool) { - outboundID := ob.GetTxID(cctx.GetCurrentOutboundParam().TssNonce) + outboundID := ob.OutboundID(cctx.GetCurrentOutboundParam().TssNonce) hash, getTxResult, err := rpc.GetTxResultByHash(ob.btcClient, txHash) if err != nil { ob.logger.Outbound.Error().Err(err).Msgf("checkIncludedTx: error GetTxResultByHash: %s", txHash) @@ -467,7 +465,7 @@ func (ob *Observer) checkIncludedTx( // setIncludedTx saves included tx result in memory func (ob *Observer) setIncludedTx(nonce uint64, getTxResult *btcjson.GetTransactionResult) { txHash := getTxResult.TxID - outboundID := ob.GetTxID(nonce) + outboundID := ob.OutboundID(nonce) ob.Mu().Lock() defer ob.Mu().Unlock() @@ -496,17 +494,17 @@ func (ob *Observer) setIncludedTx(nonce uint64, getTxResult *btcjson.GetTransact func (ob *Observer) getIncludedTx(nonce uint64) *btcjson.GetTransactionResult { ob.Mu().Lock() defer ob.Mu().Unlock() - return ob.includedTxResults[ob.GetTxID(nonce)] + return ob.includedTxResults[ob.OutboundID(nonce)] } // removeIncludedTx removes included tx from memory func (ob *Observer) removeIncludedTx(nonce uint64) { ob.Mu().Lock() defer ob.Mu().Unlock() - txResult, found := ob.includedTxResults[ob.GetTxID(nonce)] + txResult, found := ob.includedTxResults[ob.OutboundID(nonce)] if found { delete(ob.includedTxHashes, txResult.TxID) - delete(ob.includedTxResults, ob.GetTxID(nonce)) + delete(ob.includedTxResults, ob.OutboundID(nonce)) } } diff --git a/zetaclient/chains/bitcoin/observer/outbound_test.go b/zetaclient/chains/bitcoin/observer/outbound_test.go index d661b1c7bb..b1f7ae309b 100644 --- a/zetaclient/chains/bitcoin/observer/outbound_test.go +++ b/zetaclient/chains/bitcoin/observer/outbound_test.go @@ -71,7 +71,7 @@ func createObserverWithUTXOs(t *testing.T) *Observer { func mineTxNSetNonceMark(ob *Observer, nonce uint64, txid string, preMarkIndex int) { // Mine transaction - outboundID := ob.GetTxID(nonce) + outboundID := ob.OutboundID(nonce) ob.includedTxResults[outboundID] = &btcjson.GetTransactionResult{TxID: txid} // Set nonce mark diff --git a/zetaclient/chains/bitcoin/signer/signer.go b/zetaclient/chains/bitcoin/signer/signer.go index 7d1cfc366e..7e701e523d 100644 --- a/zetaclient/chains/bitcoin/signer/signer.go +++ b/zetaclient/chains/bitcoin/signer/signer.go @@ -28,7 +28,6 @@ import ( "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" "github.com/zeta-chain/zetacore/zetaclient/compliance" "github.com/zeta-chain/zetacore/zetaclient/config" - zctx "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/zetaclient/metrics" "github.com/zeta-chain/zetacore/zetaclient/outboundprocessor" ) @@ -47,7 +46,7 @@ const ( broadcastRetries = 5 ) -var _ interfaces.ChainSigner = &Signer{} +var _ interfaces.ChainSigner = (*Signer)(nil) // Signer deals with signing BTC transactions and implements the ChainSigner interface type Signer struct { @@ -63,7 +62,8 @@ func NewSigner( tss interfaces.TSSSigner, ts *metrics.TelemetryServer, logger base.Logger, - cfg config.BTCConfig) (*Signer, error) { + cfg config.BTCConfig, +) (*Signer, error) { // create base signer baseSigner := base.NewSigner(chain, tss, ts, logger) @@ -87,6 +87,8 @@ func NewSigner( }, nil } +// TODO: get rid of below four get/set functions for Bitcoin, as they are not needed in future +// https://github.com/zeta-chain/node/issues/2532 // SetZetaConnectorAddress does nothing for BTC func (signer *Signer) SetZetaConnectorAddress(_ ethcommon.Address) { } @@ -105,6 +107,17 @@ func (signer *Signer) GetERC20CustodyAddress() ethcommon.Address { return ethcommon.Address{} } +// SetGatewayAddress does nothing for BTC +// Note: TSS address will be used as gateway address for Bitcoin +func (signer *Signer) SetGatewayAddress(_ string) { +} + +// GetGatewayAddress returns empty address +// Note: same as SetGatewayAddress +func (signer *Signer) GetGatewayAddress() string { + return "" +} + // AddWithdrawTxOutputs adds the 3 outputs to the withdraw tx // 1st output: the nonce-mark btc to TSS itself // 2nd output: the payment to the recipient @@ -328,12 +341,7 @@ func (signer *Signer) TryProcessOutbound( zetacoreClient interfaces.ZetacoreClient, height uint64, ) { - app, err := zctx.FromContext(ctx) - if err != nil { - signer.Logger().Std.Error().Msgf("BTC TryProcessOutbound: %s, cannot get app context", cctx.Index) - return - } - + // end outbound process on panic defer func() { outboundProcessor.EndTryProcess(outboundID) if err := recover(); err != nil { @@ -341,35 +349,34 @@ func (signer *Signer) TryProcessOutbound( } }() + // prepare logger + params := cctx.GetCurrentOutboundParam() logger := signer.Logger().Std.With(). - Str("OutboundID", outboundID). - Str("SendHash", cctx.Index). + Str("method", "TryProcessOutbound"). + Int64("chain", signer.Chain().ChainId). + Uint64("nonce", params.TssNonce). + Str("cctx", cctx.Index). Logger() - params := cctx.GetCurrentOutboundParam() + // support gas token only for Bitcoin outbound coinType := cctx.InboundParams.CoinType if coinType == coin.CoinType_Zeta || coinType == coin.CoinType_ERC20 { - logger.Error().Msgf("BTC TryProcessOutbound: can only send BTC to a BTC network") + logger.Error().Msg("can only send BTC to a BTC network") return } - logger.Info(). - Msgf("BTC TryProcessOutbound: %s, value %d to %s", cctx.Index, params.Amount.BigInt(), params.Receiver) + // convert chain observer to BTC observer btcObserver, ok := chainObserver.(*observer.Observer) if !ok { - logger.Error().Msgf("chain observer is not a bitcoin observer") - return - } - flags := app.GetCrossChainFlags() - if !flags.IsOutboundEnabled { - logger.Info().Msgf("outbound is disabled") + logger.Error().Msg("chain observer is not a bitcoin observer") return } + chain := btcObserver.Chain() outboundTssNonce := params.TssNonce signerAddress, err := zetacoreClient.GetKeys().GetAddress() if err != nil { - logger.Error().Err(err).Msgf("cannot get signer address") + logger.Error().Err(err).Msg("cannot get signer address") return } diff --git a/zetaclient/chains/evm/observer/observer.go b/zetaclient/chains/evm/observer/observer.go index d49e5a53a3..b6ff80c769 100644 --- a/zetaclient/chains/evm/observer/observer.go +++ b/zetaclient/chains/evm/observer/observer.go @@ -236,31 +236,31 @@ func (ob *Observer) WatchRPCStatus(ctx context.Context) error { func (ob *Observer) SetPendingTx(nonce uint64, transaction *ethtypes.Transaction) { ob.Mu().Lock() defer ob.Mu().Unlock() - ob.outboundPendingTransactions[ob.GetTxID(nonce)] = transaction + 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.GetTxID(nonce)] + 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.GetTxID(nonce)) // remove pending transaction, if any - ob.outboundConfirmedReceipts[ob.GetTxID(nonce)] = receipt - ob.outboundConfirmedTransactions[ob.GetTxID(nonce)] = transaction + delete(ob.outboundPendingTransactions, ob.OutboundID(nonce)) // remove pending transaction, if any + ob.outboundConfirmedReceipts[ob.OutboundID(nonce)] = receipt + ob.outboundConfirmedTransactions[ob.OutboundID(nonce)] = transaction } // GetTxNReceipt gets the receipt and transaction from memory func (ob *Observer) GetTxNReceipt(nonce uint64) (*ethtypes.Receipt, *ethtypes.Transaction) { ob.Mu().Lock() defer ob.Mu().Unlock() - receipt := ob.outboundConfirmedReceipts[ob.GetTxID(nonce)] - transaction := ob.outboundConfirmedTransactions[ob.GetTxID(nonce)] + receipt := ob.outboundConfirmedReceipts[ob.OutboundID(nonce)] + transaction := ob.outboundConfirmedTransactions[ob.OutboundID(nonce)] return receipt, transaction } @@ -268,8 +268,8 @@ func (ob *Observer) GetTxNReceipt(nonce uint64) (*ethtypes.Receipt, *ethtypes.Tr func (ob *Observer) IsTxConfirmed(nonce uint64) bool { ob.Mu().Lock() defer ob.Mu().Unlock() - return ob.outboundConfirmedReceipts[ob.GetTxID(nonce)] != nil && - ob.outboundConfirmedTransactions[ob.GetTxID(nonce)] != nil + return ob.outboundConfirmedReceipts[ob.OutboundID(nonce)] != nil && + ob.outboundConfirmedTransactions[ob.OutboundID(nonce)] != nil } // CheckTxInclusion returns nil only if tx is included at the position indicated by the receipt ([block, index]) diff --git a/zetaclient/chains/evm/observer/outbound.go b/zetaclient/chains/evm/observer/outbound.go index 7b4f200d52..ff256613a9 100644 --- a/zetaclient/chains/evm/observer/outbound.go +++ b/zetaclient/chains/evm/observer/outbound.go @@ -28,12 +28,6 @@ import ( "github.com/zeta-chain/zetacore/zetaclient/zetacore" ) -// GetTxID returns a unique id for outbound tx -func (ob *Observer) GetTxID(nonce uint64) string { - tssAddr := ob.TSS().EVMAddress().String() - return fmt.Sprintf("%d-%s-%d", ob.Chain().ChainId, tssAddr, nonce) -} - // 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 @@ -116,11 +110,6 @@ func (ob *Observer) PostVoteOutbound( logger zerolog.Logger, ) { chainID := ob.Chain().ChainId - logFields := map[string]any{ - "outbound.chain_id": chainID, - "outbound.external_tx_hash": receipt.TxHash.String(), - "outbound.nonce": nonce, - } signerAddress := ob.ZetacoreClient().GetKeys().GetOperatorAddress() @@ -146,47 +135,51 @@ func (ob *Observer) PostVoteOutbound( retryGasLimit = zetacore.PostVoteOutboundRevertGasLimit } + // post vote to zetacore + logFields := map[string]any{ + "chain": chainID, + "nonce": nonce, + "outbound": receipt.TxHash.String(), + } zetaTxHash, ballot, err := ob.ZetacoreClient().PostVoteOutbound(ctx, gasLimit, retryGasLimit, msg) if err != nil { - logger.Error().Err(err).Fields(logFields).Msgf("PostVoteOutbound: error posting vote for chain %d", chainID) + logger.Error(). + Err(err). + Fields(logFields). + Msgf("PostVoteOutbound: error posting vote for chain %d", chainID) return } - if zetaTxHash == "" { - return + // print vote tx hash and ballot + if zetaTxHash != "" { + logFields["vote"] = zetaTxHash + logFields["ballot"] = ballot + logger.Info().Fields(logFields).Msgf("PostVoteOutbound: posted vote for chain %d", chainID) } - - logFields["outbound.zeta_tx_hash"] = zetaTxHash - logFields["outbound.ballot"] = ballot - - logger.Info().Fields(logFields).Msgf("PostVoteOutbound: posted vote for chain %d", chainID) } -// IsOutboundProcessed checks outbound status and returns (isIncluded, isConfirmed, error) -// It also posts vote to zetacore if the tx is confirmed -// TODO(revamp): rename as it also vote the outbound -func (ob *Observer) IsOutboundProcessed( +// VoteOutboundIfConfirmed checks outbound status and returns (continueKeysign, error) +func (ob *Observer) VoteOutboundIfConfirmed( ctx context.Context, cctx *crosschaintypes.CrossChainTx, - logger zerolog.Logger, -) (bool, bool, error) { +) (bool, error) { // skip if outbound is not confirmed nonce := cctx.GetCurrentOutboundParam().TssNonce if !ob.IsTxConfirmed(nonce) { - return false, false, nil + return true, nil } receipt, transaction := ob.GetTxNReceipt(nonce) sendID := fmt.Sprintf("%d-%d", ob.Chain().ChainId, nonce) - logger = logger.With().Str("sendID", sendID).Logger() + logger := ob.Logger().Outbound.With().Str("sendID", sendID).Logger() // get connector and erce20Custody contracts connectorAddr, connector, err := ob.GetConnectorContract() if err != nil { - return false, false, errors.Wrapf(err, "error getting zeta connector for chain %d", ob.Chain().ChainId) + return true, errors.Wrapf(err, "error getting zeta connector for chain %d", ob.Chain().ChainId) } custodyAddr, custody, err := ob.GetERC20CustodyContract() if err != nil { - return false, false, errors.Wrapf(err, "error getting erc20 custody for chain %d", ob.Chain().ChainId) + return true, errors.Wrapf(err, "error getting erc20 custody for chain %d", ob.Chain().ChainId) } // define a few common variables @@ -203,7 +196,7 @@ func (ob *Observer) IsOutboundProcessed( receiveStatus = chains.ReceiveStatus_success } ob.PostVoteOutbound(ctx, cctx.Index, receipt, transaction, receiveValue, receiveStatus, nonce, cointype, logger) - return true, true, nil + return false, nil } // parse the received value from the outbound receipt @@ -220,13 +213,13 @@ func (ob *Observer) IsOutboundProcessed( if err != nil { logger.Error(). Err(err). - Msgf("IsOutboundProcessed: error parsing outbound event for chain %d txhash %s", ob.Chain().ChainId, receipt.TxHash) - return false, false, err + Msgf("VoteOutboundIfConfirmed: error parsing outbound event for chain %d txhash %s", ob.Chain().ChainId, receipt.TxHash) + return true, err } // post vote to zetacore ob.PostVoteOutbound(ctx, cctx.Index, receipt, transaction, receiveValue, receiveStatus, nonce, cointype, logger) - return true, true, nil + return false, nil } // ParseAndCheckZetaEvent parses and checks ZetaReceived/ZetaReverted event from the outbound receipt diff --git a/zetaclient/chains/evm/observer/outbound_test.go b/zetaclient/chains/evm/observer/outbound_test.go index 8b0ad1573c..a2aae00433 100644 --- a/zetaclient/chains/evm/observer/outbound_test.go +++ b/zetaclient/chains/evm/observer/outbound_test.go @@ -64,10 +64,9 @@ func Test_IsOutboundProcessed(t *testing.T) { ob.SetTxNReceipt(nonce, receipt, outbound) // post outbound vote - isIncluded, isConfirmed, err := ob.IsOutboundProcessed(ctx, cctx, zerolog.Nop()) + continueKeysign, err := ob.VoteOutboundIfConfirmed(ctx, cctx) require.NoError(t, err) - require.True(t, isIncluded) - require.True(t, isConfirmed) + require.False(t, continueKeysign) }) t.Run("should post vote and return true on restricted address", func(t *testing.T) { // load cctx and modify sender address to arbitrary address @@ -88,18 +87,16 @@ func Test_IsOutboundProcessed(t *testing.T) { config.LoadComplianceConfig(cfg) // post outbound vote - isIncluded, isConfirmed, err := ob.IsOutboundProcessed(ctx, cctx, zerolog.Nop()) + continueKeysign, err := ob.VoteOutboundIfConfirmed(ctx, cctx) require.NoError(t, err) - require.True(t, isIncluded) - require.True(t, isConfirmed) + require.False(t, continueKeysign) }) 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) - isIncluded, isConfirmed, err := ob.IsOutboundProcessed(ctx, cctx, zerolog.Nop()) + continueKeysign, err := ob.VoteOutboundIfConfirmed(ctx, cctx) require.NoError(t, err) - require.False(t, isIncluded) - require.False(t, isConfirmed) + 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 @@ -110,10 +107,9 @@ func Test_IsOutboundProcessed(t *testing.T) { chainParamsNew := ob.GetChainParams() chainParamsNew.ConnectorContractAddress = sample.EthAddress().Hex() ob.SetChainParams(chainParamsNew) - isIncluded, isConfirmed, err := ob.IsOutboundProcessed(ctx, cctx, zerolog.Nop()) + continueKeysign, err := ob.VoteOutboundIfConfirmed(ctx, cctx) require.Error(t, err) - require.False(t, isIncluded) - require.False(t, isConfirmed) + require.True(t, continueKeysign) }) } @@ -160,18 +156,16 @@ func Test_IsOutboundProcessed_ContractError(t *testing.T) { // set invalid connector ABI zetaconnector.ZetaConnectorNonEthMetaData.ABI = "invalid abi" - isIncluded, isConfirmed, err := ob.IsOutboundProcessed(ctx, cctx, zerolog.Nop()) + continueKeysign, err := ob.VoteOutboundIfConfirmed(ctx, cctx) zetaconnector.ZetaConnectorNonEthMetaData.ABI = abiConnector // reset connector ABI require.ErrorContains(t, err, "error getting zeta connector") - require.False(t, isIncluded) - require.False(t, isConfirmed) + require.True(t, continueKeysign) // set invalid custody ABI erc20custody.ERC20CustodyMetaData.ABI = "invalid abi" - isIncluded, isConfirmed, err = ob.IsOutboundProcessed(ctx, cctx, zerolog.Nop()) + continueKeysign, err = ob.VoteOutboundIfConfirmed(ctx, cctx) require.ErrorContains(t, err, "error getting erc20 custody") - require.False(t, isIncluded) - require.False(t, isConfirmed) + require.True(t, continueKeysign) erc20custody.ERC20CustodyMetaData.ABI = abiCustody // reset custody ABI }) } diff --git a/zetaclient/chains/evm/signer/outbound_data.go b/zetaclient/chains/evm/signer/outbound_data.go index 2509b79198..58ade4faf6 100644 --- a/zetaclient/chains/evm/signer/outbound_data.go +++ b/zetaclient/chains/evm/signer/outbound_data.go @@ -126,7 +126,6 @@ func NewOutboundData( txData.sender = ethcommon.HexToAddress(cctx.InboundParams.Sender) txData.srcChainID = big.NewInt(cctx.InboundParams.SenderChainId) txData.asset = ethcommon.HexToAddress(cctx.InboundParams.Asset) - txData.height = height skipTx := txData.SetChainAndSender(cctx, logger) @@ -139,22 +138,12 @@ func NewOutboundData( return nil, false, err } + 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()) } - // Get nonce, Early return if the cctx is already processed - nonce := cctx.GetCurrentOutboundParam().TssNonce - included, confirmed, err := evmObserver.IsOutboundProcessed(ctx, cctx, logger) - if err != nil { - return nil, true, errors.New("IsOutboundProcessed failed") - } - if included || confirmed { - logger.Info().Msgf("CCTX already processed; exit signer") - return nil, true, nil - } - // Set up gas limit and gas price err = txData.SetupGas(cctx, logger, evmRPC, toChain) if err != nil { diff --git a/zetaclient/chains/evm/signer/signer.go b/zetaclient/chains/evm/signer/signer.go index 7235aa4456..f0f00aa237 100644 --- a/zetaclient/chains/evm/signer/signer.go +++ b/zetaclient/chains/evm/signer/signer.go @@ -26,7 +26,6 @@ import ( "github.com/zeta-chain/zetacore/pkg/constant" crosschainkeeper "github.com/zeta-chain/zetacore/x/crosschain/keeper" "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/evm" "github.com/zeta-chain/zetacore/zetaclient/chains/evm/observer" @@ -48,7 +47,7 @@ const ( ) var ( - _ interfaces.ChainSigner = &Signer{} + _ interfaces.ChainSigner = (*Signer)(nil) // zeroValue is for outbounds that carry no ETH (gas token) value zeroValue = big.NewInt(0) @@ -75,9 +74,6 @@ type Signer struct { // er20CustodyAddress is the address of the ERC20Custody contract er20CustodyAddress ethcommon.Address - - // outboundHashBeingReported is a map of outboundHash being reported - outboundHashBeingReported map[string]bool } // NewSigner creates a new EVM signer @@ -114,14 +110,13 @@ func NewSigner( } return &Signer{ - Signer: baseSigner, - client: client, - ethSigner: ethSigner, - zetaConnectorABI: connectorABI, - erc20CustodyABI: custodyABI, - zetaConnectorAddress: zetaConnectorAddress, - er20CustodyAddress: erc20CustodyAddress, - outboundHashBeingReported: make(map[string]bool), + Signer: baseSigner, + client: client, + ethSigner: ethSigner, + zetaConnectorABI: connectorABI, + erc20CustodyABI: custodyABI, + zetaConnectorAddress: zetaConnectorAddress, + er20CustodyAddress: erc20CustodyAddress, }, nil } @@ -139,6 +134,12 @@ func (signer *Signer) SetERC20CustodyAddress(addr ethcommon.Address) { signer.er20CustodyAddress = addr } +// SetGatewayAddress sets the gateway address +func (signer *Signer) SetGatewayAddress(_ string) { + // Note: do nothing for now + // gateway address will be needed in the future contract architecture +} + // GetZetaConnectorAddress returns the zeta connector address func (signer *Signer) GetZetaConnectorAddress() ethcommon.Address { signer.Lock() @@ -153,6 +154,13 @@ func (signer *Signer) GetERC20CustodyAddress() ethcommon.Address { return signer.er20CustodyAddress } +// GetGatewayAddress returns the gateway address +func (signer *Signer) GetGatewayAddress() string { + // Note: return empty string for now + // gateway address will be needed in the future contract architecture + return "" +} + // Sign given data, and metadata (gas, nonce, etc) // returns a signed transaction, sig bytes, hash bytes, and error func (signer *Signer) Sign( @@ -354,28 +362,26 @@ func (signer *Signer) TryProcessOutbound( zetacoreClient interfaces.ZetacoreClient, height uint64, ) { - app, err := zctx.FromContext(ctx) - if err != nil { - signer.Logger().Std.Error().Err(err).Msg("error getting app context") - return - } + // 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) + } + }() + // prepare logger + params := cctx.GetCurrentOutboundParam() logger := signer.Logger().Std.With(). - Str("outboundID", outboundID). - Str("SendHash", cctx.Index). + Str("method", "TryProcessOutbound"). + Int64("chain", signer.Chain().ChainId). + Uint64("nonce", params.TssNonce). + Str("cctx", cctx.Index). Logger() - logger.Info().Msgf("start processing outboundID %s", outboundID) - logger.Info().Msgf( - "EVM Chain TryProcessOutbound: %s, value %d to %s", - cctx.Index, - cctx.GetCurrentOutboundParam().Amount.BigInt(), - cctx.GetCurrentOutboundParam().Receiver, - ) - defer func() { - outboundProc.EndTryProcess(outboundID) - }() 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 { @@ -393,14 +399,6 @@ func (signer *Signer) TryProcessOutbound( return } - toChain, found := chains.GetChainFromChainID(txData.toChainID.Int64(), app.GetAdditionalChains()) - if !found { - logger.Warn().Msgf("unknown chain: %d", txData.toChainID.Int64()) - return - } - - // Get cross-chain flags - crossChainflags := app.GetCrossChainFlags() // https://github.com/zeta-chain/node/issues/2050 var tx *ethtypes.Transaction // compliance check goes first @@ -443,31 +441,31 @@ func (signer *Signer) TryProcessOutbound( logger.Warn().Err(err).Msg(ErrorMsg(cctx)) return } - } else if IsSenderZetaChain(cctx, zetacoreClient, &crossChainflags) { + } else if IsSenderZetaChain(cctx, zetacoreClient) { switch cctx.InboundParams.CoinType { case coin.CoinType_Gas: logger.Info().Msgf( - "SignWithdrawTx: %d => %s, nonce %d, gasPrice %d", + "SignWithdrawTx: %d => %d, nonce %d, gasPrice %d", cctx.InboundParams.SenderChainId, - toChain.String(), + txData.toChainID.Int64(), cctx.GetCurrentOutboundParam().TssNonce, txData.gasPrice, ) tx, err = signer.SignWithdrawTx(ctx, txData) case coin.CoinType_ERC20: logger.Info().Msgf( - "SignERC20WithdrawTx: %d => %s, nonce %d, gasPrice %d", + "SignERC20WithdrawTx: %d => %d, nonce %d, gasPrice %d", cctx.InboundParams.SenderChainId, - toChain.String(), + txData.toChainID.Int64(), cctx.GetCurrentOutboundParam().TssNonce, txData.gasPrice, ) tx, err = signer.SignERC20WithdrawTx(ctx, txData) case coin.CoinType_Zeta: logger.Info().Msgf( - "SignOutbound: %d => %s, nonce %d, gasPrice %d", + "SignOutbound: %d => %d, nonce %d, gasPrice %d", cctx.InboundParams.SenderChainId, - toChain.String(), + txData.toChainID.Int64(), cctx.GetCurrentOutboundParam().TssNonce, txData.gasPrice, ) @@ -481,9 +479,9 @@ func (signer *Signer) TryProcessOutbound( switch cctx.InboundParams.CoinType { case coin.CoinType_Zeta: logger.Info().Msgf( - "SignRevertTx: %d => %s, nonce %d, gasPrice %d", + "SignRevertTx: %d => %d, nonce %d, gasPrice %d", cctx.InboundParams.SenderChainId, - toChain.String(), cctx.GetCurrentOutboundParam().TssNonce, + txData.toChainID.Int64(), cctx.GetCurrentOutboundParam().TssNonce, txData.gasPrice, ) txData.srcChainID = big.NewInt(cctx.OutboundParams[0].ReceiverChainId) @@ -491,17 +489,17 @@ func (signer *Signer) TryProcessOutbound( tx, err = signer.SignRevertTx(ctx, txData) case coin.CoinType_Gas: logger.Info().Msgf( - "SignWithdrawTx: %d => %s, nonce %d, gasPrice %d", + "SignWithdrawTx: %d => %d, nonce %d, gasPrice %d", cctx.InboundParams.SenderChainId, - toChain.String(), + txData.toChainID.Int64(), cctx.GetCurrentOutboundParam().TssNonce, txData.gasPrice, ) tx, err = signer.SignWithdrawTx(ctx, txData) case coin.CoinType_ERC20: - logger.Info().Msgf("SignERC20WithdrawTx: %d => %s, nonce %d, gasPrice %d", + logger.Info().Msgf("SignERC20WithdrawTx: %d => %d, nonce %d, gasPrice %d", cctx.InboundParams.SenderChainId, - toChain.String(), + txData.toChainID.Int64(), cctx.GetCurrentOutboundParam().TssNonce, txData.gasPrice, ) @@ -513,9 +511,9 @@ func (signer *Signer) TryProcessOutbound( } } else if cctx.CctxStatus.Status == types.CctxStatus_PendingRevert { logger.Info().Msgf( - "SignRevertTx: %d => %s, nonce %d, gasPrice %d", + "SignRevertTx: %d => %d, nonce %d, gasPrice %d", cctx.InboundParams.SenderChainId, - toChain.String(), + txData.toChainID.Int64(), cctx.GetCurrentOutboundParam().TssNonce, txData.gasPrice, ) @@ -529,9 +527,9 @@ func (signer *Signer) TryProcessOutbound( } } else if cctx.CctxStatus.Status == types.CctxStatus_PendingOutbound { logger.Info().Msgf( - "SignOutbound: %d => %s, nonce %d, gasPrice %d", + "SignOutbound: %d => %d, nonce %d, gasPrice %d", cctx.InboundParams.SenderChainId, - toChain.String(), + txData.toChainID.Int64(), cctx.GetCurrentOutboundParam().TssNonce, txData.gasPrice, ) @@ -543,9 +541,9 @@ func (signer *Signer) TryProcessOutbound( } logger.Info().Msgf( - "Key-sign success: %d => %s, nonce %d", + "Key-sign success: %d => %d, nonce %d", cctx.InboundParams.SenderChainId, - toChain.String(), + txData.toChainID.Int64(), cctx.GetCurrentOutboundParam().TssNonce, ) @@ -647,15 +645,6 @@ func (signer *Signer) SignERC20WithdrawTx(ctx context.Context, txData *OutboundD return tx, nil } -// Exported for unit tests - -// GetReportedTxList returns a list of outboundHash being reported -// TODO: investigate pointer usage -// https://github.com/zeta-chain/node/issues/2084 -func (signer *Signer) GetReportedTxList() *map[string]bool { - return &signer.outboundHashBeingReported -} - // EvmClient returns the EVM RPC client func (signer *Signer) EvmClient() interfaces.EVMRPCClient { return signer.client @@ -672,10 +661,9 @@ func (signer *Signer) EvmSigner() ethtypes.Signer { func IsSenderZetaChain( cctx *types.CrossChainTx, zetacoreClient interfaces.ZetacoreClient, - flags *observertypes.CrosschainFlags, ) bool { return cctx.InboundParams.SenderChainId == zetacoreClient.Chain().ChainId && - cctx.CctxStatus.Status == types.CctxStatus_PendingOutbound && flags.IsOutboundEnabled + cctx.CctxStatus.Status == types.CctxStatus_PendingOutbound } // ErrorMsg returns a error message for SignOutbound failure with cctx data @@ -752,22 +740,18 @@ func (signer *Signer) reportToOutboundTracker( outboundHash string, logger zerolog.Logger, ) { - // skip if already being reported - signer.Lock() - defer signer.Unlock() - if _, found := signer.outboundHashBeingReported[outboundHash]; found { + // 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 } - signer.outboundHashBeingReported[outboundHash] = true // mark as being reported // report to outbound tracker with goroutine go func() { defer func() { - signer.Lock() - delete(signer.outboundHashBeingReported, outboundHash) - signer.Unlock() + signer.Signer.ClearBeingReportedFlag(outboundHash) }() // try monitoring tx inclusion status for 10 minutes diff --git a/zetaclient/chains/interfaces/interfaces.go b/zetaclient/chains/interfaces/interfaces.go index 82125ca619..4ca93b7259 100644 --- a/zetaclient/chains/interfaces/interfaces.go +++ b/zetaclient/chains/interfaces/interfaces.go @@ -41,14 +41,12 @@ const ( type ChainObserver interface { Start(ctx context.Context) Stop() - IsOutboundProcessed( + VoteOutboundIfConfirmed( ctx context.Context, cctx *crosschaintypes.CrossChainTx, - logger zerolog.Logger, - ) (bool, bool, error) + ) (bool, error) SetChainParams(observertypes.ChainParams) GetChainParams() observertypes.ChainParams - GetTxID(nonce uint64) string WatchInboundTracker(ctx context.Context) error } @@ -67,6 +65,8 @@ type ChainSigner interface { SetERC20CustodyAddress(address ethcommon.Address) GetZetaConnectorAddress() ethcommon.Address GetERC20CustodyAddress() ethcommon.Address + SetGatewayAddress(address string) + GetGatewayAddress() string } // ZetacoreVoter represents voter interface. @@ -189,19 +189,35 @@ type EVMRPCClient interface { // SolanaRPCClient is the interface for Solana RPC client type SolanaRPCClient interface { - GetVersion(ctx context.Context) (out *solrpc.GetVersionResult, err error) - GetHealth(ctx context.Context) (out string, err error) - GetAccountInfo(ctx context.Context, account solana.PublicKey) (out *solrpc.GetAccountInfoResult, err error) + GetVersion(ctx context.Context) (*solrpc.GetVersionResult, error) + 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) + GetRecentBlockhash(ctx context.Context, commitment solrpc.CommitmentType) (*solrpc.GetRecentBlockhashResult, error) + GetRecentPrioritizationFees( + ctx context.Context, + accounts solana.PublicKeySlice, + ) ([]solrpc.PriorizationFeeResult, error) GetTransaction( ctx context.Context, txSig solana.Signature, // transaction signature opts *solrpc.GetTransactionOpts, - ) (out *solrpc.GetTransactionResult, err error) + ) (*solrpc.GetTransactionResult, error) + GetConfirmedTransactionWithOpts( + ctx context.Context, + signature solana.Signature, + opts *solrpc.GetTransactionOpts, + ) (*solrpc.TransactionWithMeta, error) GetSignaturesForAddressWithOpts( ctx context.Context, account solana.PublicKey, opts *solrpc.GetSignaturesForAddressOpts, - ) (out []*solrpc.TransactionSignature, err error) + ) ([]*solrpc.TransactionSignature, error) + SendTransactionWithOpts( + ctx context.Context, + transaction *solana.Transaction, + opts solrpc.TransactionOpts, + ) (solana.Signature, error) } // EVMJSONRPCClient is the interface for EVM JSON RPC client diff --git a/zetaclient/chains/solana/observer/inbound.go b/zetaclient/chains/solana/observer/inbound.go index 4a819ce9cb..7c14ec34c6 100644 --- a/zetaclient/chains/solana/observer/inbound.go +++ b/zetaclient/chains/solana/observer/inbound.go @@ -15,7 +15,7 @@ import ( "github.com/zeta-chain/zetacore/pkg/coin" "github.com/zeta-chain/zetacore/pkg/constant" - solanacontract "github.com/zeta-chain/zetacore/pkg/contract/solana" + solanacontracts "github.com/zeta-chain/zetacore/pkg/contracts/solana" crosschaintypes "github.com/zeta-chain/zetacore/x/crosschain/types" solanarpc "github.com/zeta-chain/zetacore/zetaclient/chains/solana/rpc" "github.com/zeta-chain/zetacore/zetaclient/compliance" @@ -274,14 +274,14 @@ func (ob *Observer) ParseInboundAsDeposit( instruction := tx.Message.Instructions[instructionIndex] // try deserializing instruction as a 'deposit' - var inst solanacontract.DepositInstructionParams + var inst solanacontracts.DepositInstructionParams err := borsh.Deserialize(&inst, instruction.Data) if err != nil { return nil, nil } // check if the instruction is a deposit or not - if inst.Discriminator != solanacontract.DiscriminatorDeposit() { + if inst.Discriminator != solanacontracts.DiscriminatorDeposit() { return nil, nil } @@ -324,8 +324,8 @@ func (ob *Observer) ParseInboundAsDepositSPL( // Note: solana-go is not able to parse the AccountMeta 'is_signer' ATM. This is a workaround. func (ob *Observer) GetSignerDeposit(tx *solana.Transaction, inst *solana.CompiledInstruction) (string, error) { // there should be 4 accounts for a deposit instruction - if len(inst.Accounts) != solanacontract.AccountsNumDeposit { - return "", fmt.Errorf("want %d accounts, got %d", solanacontract.AccountsNumDeposit, len(inst.Accounts)) + if len(inst.Accounts) != solanacontracts.AccountsNumDeposit { + return "", fmt.Errorf("want %d accounts, got %d", solanacontracts.AccountsNumDeposit, len(inst.Accounts)) } // the accounts are [signer, pda, system_program, gateway_program] diff --git a/zetaclient/chains/solana/observer/inbound_test.go b/zetaclient/chains/solana/observer/inbound_test.go index a7090d6821..40f53ce0bc 100644 --- a/zetaclient/chains/solana/observer/inbound_test.go +++ b/zetaclient/chains/solana/observer/inbound_test.go @@ -36,7 +36,7 @@ func Test_FilterInboundEventAndVote(t *testing.T) { // create observer chainParams := sample.ChainParams(chain.ChainId) - chainParams.GatewayAddress = "2kJndCL9NBR36ySiQ4bmArs4YgWQu67LmCDfLzk5Gb7s" + chainParams.GatewayAddress = GatewayAddressTest zetacoreClient := mocks.NewZetacoreClient(t) zetacoreClient.WithKeys(&keys.Keys{}).WithZetaChain().WithPostVoteInbound("", "") @@ -61,7 +61,7 @@ func Test_FilterInboundEvents(t *testing.T) { // create observer chainParams := sample.ChainParams(chain.ChainId) - chainParams.GatewayAddress = "2kJndCL9NBR36ySiQ4bmArs4YgWQu67LmCDfLzk5Gb7s" + chainParams.GatewayAddress = GatewayAddressTest ob, err := observer.NewObserver(chain, nil, *chainParams, nil, nil, database, base.DefaultLogger(), nil) require.NoError(t, err) @@ -169,7 +169,7 @@ func Test_ParseInboundAsDeposit(t *testing.T) { // create observer chainParams := sample.ChainParams(chain.ChainId) - chainParams.GatewayAddress = "2kJndCL9NBR36ySiQ4bmArs4YgWQu67LmCDfLzk5Gb7s" + chainParams.GatewayAddress = GatewayAddressTest ob, err := observer.NewObserver(chain, nil, *chainParams, nil, nil, database, base.DefaultLogger(), nil) require.NoError(t, err) diff --git a/zetaclient/chains/solana/observer/observer.go b/zetaclient/chains/solana/observer/observer.go index 1cdb3f6768..ad135aeecf 100644 --- a/zetaclient/chains/solana/observer/observer.go +++ b/zetaclient/chains/solana/observer/observer.go @@ -4,11 +4,12 @@ import ( "context" "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/rpc" "github.com/pkg/errors" "github.com/zeta-chain/zetacore/pkg/bg" "github.com/zeta-chain/zetacore/pkg/chains" - solanacontract "github.com/zeta-chain/zetacore/pkg/contract/solana" + contracts "github.com/zeta-chain/zetacore/pkg/contracts/solana" 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" @@ -31,6 +32,9 @@ type Observer struct { // pda is the program derived address of the gateway program pda solana.PublicKey + + // finalizedTxResults indexes tx results with the outbound hash + finalizedTxResults map[string]*rpc.GetTransactionResult } // NewObserver returns a new Solana chain observer @@ -60,28 +64,24 @@ func NewObserver( return nil, err } - pubKey, err := solana.PublicKeyFromBase58(chainParams.GatewayAddress) + // parse gateway ID and PDA + gatewayID, pda, err := contracts.ParseGatewayIDAndPda(chainParams.GatewayAddress) if err != nil { - return nil, errors.Wrap(err, "unable to derive public key") + return nil, errors.Wrapf(err, "cannot parse gateway address %s", chainParams.GatewayAddress) } // create solana observer - ob := Observer{ - Observer: *baseObserver, - solClient: solClient, - gatewayID: pubKey, - } - - // compute gateway PDA - seed := []byte(solanacontract.PDASeed) - ob.pda, _, err = solana.FindProgramAddress([][]byte{seed}, ob.gatewayID) - if err != nil { - return nil, err + ob := &Observer{ + Observer: *baseObserver, + solClient: solClient, + gatewayID: gatewayID, + pda: pda, + finalizedTxResults: make(map[string]*rpc.GetTransactionResult), } ob.Observer.LoadLastTxScanned() - return &ob, nil + return ob, nil } // SolClient returns the solana rpc client @@ -122,6 +122,12 @@ func (ob *Observer) Start(ctx context.Context) { // watch Solana chain for incoming txs and post votes to zetacore bg.Work(ctx, ob.WatchInbound, bg.WithName("WatchInbound"), bg.WithLogger(ob.Logger().Inbound)) + // watch Solana chain for outbound trackers + bg.Work(ctx, ob.WatchOutbound, bg.WithName("WatchOutbound"), bg.WithLogger(ob.Logger().Outbound)) + + // watch Solana chain for fee rate and post to zetacore + bg.Work(ctx, ob.WatchGasPrice, bg.WithName("WatchGasPrice"), bg.WithLogger(ob.Logger().GasPrice)) + // watch zetacore for Solana inbound trackers bg.Work(ctx, ob.WatchInboundTracker, bg.WithName("WatchInboundTracker"), bg.WithLogger(ob.Logger().Inbound)) } @@ -133,3 +139,24 @@ func (ob *Observer) LoadLastTxScanned() error { return nil } + +// SetTxResult sets the tx result for the given nonce +func (ob *Observer) SetTxResult(nonce uint64, result *rpc.GetTransactionResult) { + ob.Mu().Lock() + defer ob.Mu().Unlock() + ob.finalizedTxResults[ob.OutboundID(nonce)] = result +} + +// GetTxResult returns the tx result for the given nonce +func (ob *Observer) GetTxResult(nonce uint64) *rpc.GetTransactionResult { + ob.Mu().Lock() + defer ob.Mu().Unlock() + return ob.finalizedTxResults[ob.OutboundID(nonce)] +} + +// IsTxFinalized returns true if there is a finalized tx for nonce +func (ob *Observer) IsTxFinalized(nonce uint64) bool { + ob.Mu().Lock() + defer ob.Mu().Unlock() + return ob.finalizedTxResults[ob.OutboundID(nonce)] != nil +} diff --git a/zetaclient/chains/solana/observer/observer_gas.go b/zetaclient/chains/solana/observer/observer_gas.go new file mode 100644 index 0000000000..a323da16ec --- /dev/null +++ b/zetaclient/chains/solana/observer/observer_gas.go @@ -0,0 +1,103 @@ +package observer + +import ( + "context" + "fmt" + + "github.com/gagliardetto/solana-go/rpc" + "github.com/pkg/errors" + + zetamath "github.com/zeta-chain/zetacore/pkg/math" + clienttypes "github.com/zeta-chain/zetacore/zetaclient/types" +) + +const ( + // SolanaTransactionFee is the static fee per transaction, 5k lamports. + SolanaTransactionFee = 5000 + + // MicroLamportsPerLamport is the number of micro lamports in a lamport. + MicroLamportsPerLamport = 1_000_000 + + // SolanaDefaultComputeBudget is the default compute budget for a transaction. + SolanaDefaultComputeBudget = 200_000 + + // Solana uses micro lamports (0.000001 lamports) as the smallest unit of gas price. + // The gas fee formula 'gasFee = gasPrice * gasLimit' won't fit Solana in the ZRC20 SOL contract. + // We could use lamports as the unit of gas price and 10K CU as the smallest unit of compute units. + // SolanaDefaultGasPrice10KCUs is the default gas price (in lamports) per 10K compute units. + SolanaDefaultGasPrice10KCUs = 100 + + // SolanaDefaultGasLimit is the default compute units (in 10K CU) for a transaction. + SolanaDefaultGasLimit10KCU = 50 +) + +// WatchGasPrice watches the gas price of the chain and posts it to the zetacore +func (ob *Observer) WatchGasPrice(ctx context.Context) error { + // report gas price right away as the ticker takes time to kick in + err := ob.PostGasPrice(ctx) + if err != nil { + ob.Logger().GasPrice.Error().Err(err).Msgf("PostGasPrice error for chain %d", ob.Chain().ChainId) + } + + // start gas price ticker + ticker, err := clienttypes.NewDynamicTicker( + fmt.Sprintf("Solana_WatchGasPrice_%d", ob.Chain().ChainId), + ob.GetChainParams().GasPriceTicker, + ) + if err != nil { + return errors.Wrapf(err, "NewDynamicTicker error") + } + ob.Logger().GasPrice.Info().Msgf("WatchGasPrice started for chain %d with interval %d", + ob.Chain().ChainId, ob.GetChainParams().GasPriceTicker) + defer ticker.Stop() + + for { + select { + case <-ticker.C(): + if !ob.GetChainParams().IsSupported { + continue + } + err = ob.PostGasPrice(ctx) + if err != nil { + ob.Logger().GasPrice.Error().Err(err).Msgf("PostGasPrice error for chain %d", ob.Chain().ChainId) + } + ticker.UpdateInterval(ob.GetChainParams().GasPriceTicker, ob.Logger().GasPrice) + case <-ob.StopChannel(): + ob.Logger().GasPrice.Info().Msgf("WatchGasPrice stopped for chain %d", ob.Chain().ChainId) + return nil + } + } +} + +// PostGasPrice posts gas price to zetacore +func (ob *Observer) PostGasPrice(ctx context.Context) error { + // get current slot + slot, err := ob.solClient.GetSlot(ctx, rpc.CommitmentConfirmed) + if err != nil { + return errors.Wrap(err, "GetSlot error") + } + + // query recent priority fees + recentFees, err := ob.solClient.GetRecentPrioritizationFees(ctx, nil) + if err != nil { + return errors.Wrap(err, "GetRecentPrioritizationFees error") + } + + // locate median priority fee + priorityFees := make([]uint64, len(recentFees)) + for i, fee := range recentFees { + if fee.PrioritizationFee > 0 { + priorityFees[i] = fee.PrioritizationFee + } + } + // the priority fee is in increments of 0.000001 lamports (micro lamports) + medianFee := zetamath.SliceMedianValue(priorityFees, true) + + // there is no Ethereum-like gas price in Solana, we only post priority fee for now + _, err = ob.ZetacoreClient().PostVoteGasPrice(ctx, ob.Chain(), 1, medianFee, slot) + if err != nil { + return errors.Wrapf(err, "PostVoteGasPrice error for chain %d", ob.Chain().ChainId) + } + + return nil +} diff --git a/zetaclient/chains/solana/observer/outbound.go b/zetaclient/chains/solana/observer/outbound.go index a75641e072..59d93d2efb 100644 --- a/zetaclient/chains/solana/observer/outbound.go +++ b/zetaclient/chains/solana/observer/outbound.go @@ -2,22 +2,349 @@ package observer import ( "context" + "fmt" + "math/big" + "cosmossdk.io/math" + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/rpc" + "github.com/pkg/errors" "github.com/rs/zerolog" - "github.com/zeta-chain/zetacore/x/crosschain/types" + "github.com/zeta-chain/zetacore/pkg/chains" + "github.com/zeta-chain/zetacore/pkg/coin" + contracts "github.com/zeta-chain/zetacore/pkg/contracts/solana" + 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" + clienttypes "github.com/zeta-chain/zetacore/zetaclient/types" + "github.com/zeta-chain/zetacore/zetaclient/zetacore" ) -// GetTxID returns a unique id for Solana outbound -func (ob *Observer) GetTxID(_ uint64) string { - return "" +// WatchOutbound watches solana chain for outgoing txs status +// TODO(revamp): move ticker function to ticker file +func (ob *Observer) WatchOutbound(ctx context.Context) error { + // get app context + app, err := zctx.FromContext(ctx) + if err != nil { + return err + } + + // create outbound ticker based on chain params + chainID := ob.Chain().ChainId + ticker, err := clienttypes.NewDynamicTicker( + fmt.Sprintf("Solana_WatchOutbound_%d", chainID), + ob.GetChainParams().OutboundTicker, + ) + if err != nil { + ob.Logger().Outbound.Error().Err(err).Msg("error creating ticker") + return err + } + + ob.Logger().Outbound.Info().Msgf("WatchOutbound started for chain %d", chainID) + sampledLogger := ob.Logger().Outbound.Sample(&zerolog.BasicSampler{N: 10}) + defer ticker.Stop() + for { + select { + case <-ticker.C(): + if !app.IsOutboundObservationEnabled(ob.GetChainParams()) { + sampledLogger.Info().Msgf("WatchOutbound: outbound observation is disabled for chain %d", chainID) + continue + } + + // process outbound trackers + err := ob.ProcessOutboundTrackers(ctx) + if err != nil { + 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().Msgf("WatchOutbound: watcher stopped for chain %d", chainID) + return nil + } + } +} + +// ProcessOutboundTrackers processes Solana outbound trackers +func (ob *Observer) ProcessOutboundTrackers(ctx context.Context) error { + chainID := ob.Chain().ChainId + trackers, err := ob.ZetacoreClient().GetAllOutboundTrackerByChain(ctx, chainID, interfaces.Ascending) + if err != nil { + return errors.Wrap(err, "GetAllOutboundTrackerByChain error") + } + + // prepare logger fields + logger := ob.Logger().Outbound.With(). + Str("method", "ProcessOutboundTrackers"). + Int64("chain", chainID). + Logger() + + // process outbound trackers + for _, tracker := range trackers { + // go to next tracker if this one already has a finalized tx + nonce := tracker.Nonce + if ob.IsTxFinalized(tracker.Nonce) { + continue + } + + // get original cctx parameters + cctx, err := ob.ZetacoreClient().GetCctxByNonce(ctx, chainID, tracker.Nonce) + if err != nil { + // take a rest if zetacore RPC breaks + return errors.Wrapf(err, "GetCctxByNonce error for chain %d nonce %d", chainID, tracker.Nonce) + } + coinType := cctx.InboundParams.CoinType + + // check each txHash and save its txResult if it's finalized and legit + txCount := 0 + var txResult *rpc.GetTransactionResult + for _, txHash := range tracker.HashList { + if result, ok := ob.CheckFinalizedTx(ctx, txHash.TxHash, nonce, coinType); ok { + txCount++ + txResult = result + logger.Info().Msgf("confirmed outbound %s for chain %d nonce %d", txHash.TxHash, chainID, nonce) + if txCount > 1 { + logger.Error(). + Msgf("checkFinalizedTx passed, txCount %d chain %d nonce %d txResult %v", txCount, chainID, nonce, txResult) + } + } + } + // should be only one finalized txHash for each nonce + if txCount == 1 { + ob.SetTxResult(nonce, txResult) + } 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("finalized multiple (%d) outbound for chain %d nonce %d", txCount, chainID, nonce) + } + } + + return nil +} + +// VoteOutboundIfConfirmed checks outbound status and returns (continueKeysign, error) +func (ob *Observer) VoteOutboundIfConfirmed(ctx context.Context, cctx *crosschaintypes.CrossChainTx) (bool, error) { + // get outbound params + params := cctx.GetCurrentOutboundParam() + nonce := params.TssNonce + coinType := cctx.InboundParams.CoinType + + // early return if outbound is not finalized yet + txResult := ob.GetTxResult(nonce) + if txResult == nil { + return true, nil + } + + // extract tx signature from tx result + tx, err := txResult.Transaction.GetTransaction() + if err != nil { + // should not happen + return false, errors.Wrapf(err, "GetTransaction error for nonce %d", nonce) + } + txSig := tx.Signatures[0] + + // parse gateway instruction from tx result + inst, err := ParseGatewayInstruction(txResult, ob.gatewayID, coinType) + if err != nil { + // should never happen as it was already successfully parsed in CheckFinalizedTx + return false, errors.Wrapf(err, "ParseGatewayInstruction error for sig %s", txSig) + } + + // the amount and status of the outbound + outboundAmount := new(big.Int).SetUint64(inst.TokenAmount()) + // status was already verified as successful in CheckFinalizedTx + outboundStatus := chains.ReceiveStatus_success + + // post vote to zetacore + ob.PostVoteOutbound(ctx, cctx.Index, txSig.String(), txResult, outboundAmount, outboundStatus, nonce, coinType) + return false, nil +} + +// PostVoteOutbound posts vote to zetacore for the finalized outbound +func (ob *Observer) PostVoteOutbound( + ctx context.Context, + cctxIndex string, + outboundHash string, + txResult *rpc.GetTransactionResult, + valueReceived *big.Int, + status chains.ReceiveStatus, + nonce uint64, + coinType coin.CoinType, +) { + // create outbound vote message + msg := ob.CreateMsgVoteOutbound(cctxIndex, outboundHash, txResult, valueReceived, status, nonce, coinType) + + // prepare logger fields + logFields := map[string]any{ + "chain": ob.Chain().ChainId, + "nonce": nonce, + "tx": outboundHash, + } + + // so we set retryGasLimit to 0 because the solana gateway withdrawal will always succeed + // and the vote msg won't trigger ZEVM interaction + const ( + gasLimit = zetacore.PostVoteOutboundGasLimit + retryGasLimit = 0 + ) + + // post vote to zetacore + zetaTxHash, ballot, err := ob.ZetacoreClient().PostVoteOutbound(ctx, gasLimit, retryGasLimit, msg) + if err != nil { + ob.Logger().Outbound.Error().Err(err).Fields(logFields).Msg("PostVoteOutbound: error posting outbound vote") + return + } + + // print vote tx hash and ballot + if zetaTxHash != "" { + logFields["vote"] = zetaTxHash + logFields["ballot"] = ballot + ob.Logger().Outbound.Info().Fields(logFields).Msg("PostVoteOutbound: posted outbound vote successfully") + } } -// IsOutboundProcessed checks outbound status and returns (isIncluded, isConfirmed, error) -func (ob *Observer) IsOutboundProcessed( - _ context.Context, - _ *types.CrossChainTx, - _ zerolog.Logger, -) (bool, bool, error) { - return false, false, nil +// CreateMsgVoteOutbound creates a vote outbound message for Solana chain +func (ob *Observer) CreateMsgVoteOutbound( + cctxIndex string, + outboundHash string, + txResult *rpc.GetTransactionResult, + valueReceived *big.Int, + status chains.ReceiveStatus, + nonce uint64, + coinType coin.CoinType, +) *crosschaintypes.MsgVoteOutbound { + const ( + // Solana implements a different gas fee model than Ethereum, below values are not used. + // Solana tx fee is based on both static fee and dynamic fee (priority fee), setting + // zero values to by pass incorrectly funded gas stability pool. + outboundGasUsed = 0 + outboundGasPrice = 0 + outboundGasLimit = 0 + ) + + creator := ob.ZetacoreClient().GetKeys().GetOperatorAddress() + + return crosschaintypes.NewMsgVoteOutbound( + creator.String(), + cctxIndex, + outboundHash, + txResult.Slot, // instead of using block, Solana explorer uses slot for indexing + outboundGasUsed, + math.NewInt(outboundGasPrice), + outboundGasLimit, + math.NewUintFromBigInt(valueReceived), + status, + ob.Chain().ChainId, + nonce, + coinType, + ) +} + +// CheckFinalizedTx checks if a txHash is finalized for given nonce and coinType +// returns (tx result, true) if finalized or (nil, false) otherwise +func (ob *Observer) CheckFinalizedTx( + ctx context.Context, + txHash string, + nonce uint64, + coinType coin.CoinType, +) (*rpc.GetTransactionResult, bool) { + // 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() + + // 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) + return nil, false + } + + // query transaction using "finalized" commitment to avoid re-org + txResult, err := ob.solClient.GetTransaction(ctx, sig, &rpc.GetTransactionOpts{ + Commitment: rpc.CommitmentFinalized, + }) + if err != nil { + logger.Error().Err(err).Msgf("GetTransaction err for chain %d nonce %d", chainID, nonce) + 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) + 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) + return nil, false + } + txNonce := inst.GatewayNonce() + + // 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) + 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) + 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) + return nil, false + } + + return txResult, true +} + +// ParseGatewayInstruction parses the outbound instruction from tx result +func ParseGatewayInstruction( + txResult *rpc.GetTransactionResult, + gatewayID solana.PublicKey, + coinType coin.CoinType, +) (contracts.OutboundInstruction, error) { + // unmarshal transaction + tx, err := txResult.Transaction.GetTransaction() + if err != nil { + return nil, errors.Wrap(err, "error unmarshaling transaction") + } + + // there should be only one single instruction ('withdraw' or 'withdraw_spl_token') + if len(tx.Message.Instructions) != 1 { + return nil, fmt.Errorf("want 1 instruction, got %d", len(tx.Message.Instructions)) + } + instruction := tx.Message.Instructions[0] + + // get the program ID + programID, err := tx.Message.Program(instruction.ProgramIDIndex) + if err != nil { + return nil, errors.Wrap(err, "error getting program ID") + } + + // the instruction should be an invocation of the gateway program + if !programID.Equals(gatewayID) { + return nil, fmt.Errorf("programID %s is not matching gatewayID %s", programID, gatewayID) + } + + // parse the instruction as a 'withdraw' or 'withdraw_spl_token' + switch coinType { + case coin.CoinType_Gas: + return contracts.ParseInstructionWithdraw(instruction) + default: + return nil, fmt.Errorf("unsupported outbound coin type %s", coinType) + } } diff --git a/zetaclient/chains/solana/observer/outbound_test.go b/zetaclient/chains/solana/observer/outbound_test.go new file mode 100644 index 0000000000..654fa8f4ee --- /dev/null +++ b/zetaclient/chains/solana/observer/outbound_test.go @@ -0,0 +1,296 @@ +package observer_test + +import ( + "context" + "encoding/hex" + "testing" + + "github.com/gagliardetto/solana-go" + "github.com/pkg/errors" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "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/testutil/sample" + "github.com/zeta-chain/zetacore/zetaclient/chains/base" + "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" + "github.com/zeta-chain/zetacore/zetaclient/chains/solana/observer" + "github.com/zeta-chain/zetacore/zetaclient/db" + "github.com/zeta-chain/zetacore/zetaclient/testutils" + "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" +) + +const ( + // gatewayAddressDevnet is the gateway address on devnet for testing + GatewayAddressTest = "2kJndCL9NBR36ySiQ4bmArs4YgWQu67LmCDfLzk5Gb7s" + + // withdrawTxTest is an archived withdraw tx result on devnet for testing + // https://explorer.solana.com/tx/5iBYjBYCphzjHKfmPwddMWpV2RNssmzk9Z8NNmV9Rei71pZKBTEVdkmUeyXfn7eWbV8932uSsPfBxgA7UgERNTvq?cluster=devnet + withdrawTxTest = "5iBYjBYCphzjHKfmPwddMWpV2RNssmzk9Z8NNmV9Rei71pZKBTEVdkmUeyXfn7eWbV8932uSsPfBxgA7UgERNTvq" + + // withdrawFailedTxTest is an archived failed withdraw tx result on devnet for testing + // https://explorer.solana.com/tx/5nFUQgNSdqTd4aPS4a1xNcbehj19hDzuQLfBqFRj8g7BJdESVY6hFuTFPWFuV6aWAfzEMfVfCdNu9DfzVp5FsHg5?cluster=devnet + withdrawFailedTxTest = "5nFUQgNSdqTd4aPS4a1xNcbehj19hDzuQLfBqFRj8g7BJdESVY6hFuTFPWFuV6aWAfzEMfVfCdNu9DfzVp5FsHg5" + + // tssAddressTest is the TSS address for testing + tssAddressTest = "0x05C7dBdd1954D59c9afaB848dA7d8DD3F35e69Cd" +) + +// createTestObserver creates a test observer for testing +func createTestObserver( + t *testing.T, + chain chains.Chain, + solClient interfaces.SolanaRPCClient, + tss interfaces.TSSSigner, +) *observer.Observer { + database, err := db.NewFromSqliteInMemory(true) + require.NoError(t, err) + + // create observer + chainParams := sample.ChainParams(chain.ChainId) + chainParams.GatewayAddress = GatewayAddressTest + ob, err := observer.NewObserver(chain, solClient, *chainParams, nil, tss, database, base.DefaultLogger(), nil) + require.NoError(t, err) + + return ob +} + +func Test_CheckFinalizedTx(t *testing.T) { + // the test chain and transaction hash + chain := chains.SolanaDevnet + txHash := withdrawTxTest + txHashFailed := withdrawFailedTxTest + txSig := solana.MustSignatureFromBase58(txHash) + coinType := coin.CoinType_Gas + nonce := uint64(0) + + // load archived outbound tx result + txResult := testutils.LoadSolanaOutboundTxResult(t, TestDataDir, chain.ChainId, txHash) + + // mock GetTransaction result + solClient := mocks.NewSolanaRPCClient(t) + solClient.On("GetTransaction", mock.Anything, txSig, mock.Anything).Return(txResult, nil) + + // mock TSS + tss := mocks.NewMockTSS(chain, tssAddressTest, "") + + // create observer with and TSS + ob := createTestObserver(t, chain, solClient, tss) + ctx := context.Background() + + t.Run("should successfully check finalized tx", func(t *testing.T) { + tx, finalized := ob.CheckFinalizedTx(ctx, txHash, nonce, coinType) + require.True(t, finalized) + require.NotNil(t, tx) + }) + + t.Run("should return error on invalid tx hash", func(t *testing.T) { + tx, finalized := ob.CheckFinalizedTx(ctx, "invalid_hash_1234", nonce, coinType) + require.False(t, finalized) + require.Nil(t, tx) + }) + + t.Run("should return error on GetTransaction error", func(t *testing.T) { + // mock GetTransaction error + client := mocks.NewSolanaRPCClient(t) + client.On("GetTransaction", mock.Anything, mock.Anything, mock.Anything).Return(nil, errors.New("error")) + + // create observer + ob := createTestObserver(t, chain, client, tss) + + tx, finalized := ob.CheckFinalizedTx(ctx, txHash, nonce, coinType) + require.False(t, finalized) + require.Nil(t, tx) + }) + + t.Run("should return error on if transaction is failed", func(t *testing.T) { + // load archived outbound tx result which is failed due to nonce mismatch + failedResult := testutils.LoadSolanaOutboundTxResult(t, TestDataDir, chain.ChainId, txHashFailed) + + // mock GetTransaction result with failed status + client := mocks.NewSolanaRPCClient(t) + client.On("GetTransaction", mock.Anything, txSig, mock.Anything).Return(failedResult, nil) + + // create observer + ob := createTestObserver(t, chain, client, tss) + + tx, finalized := ob.CheckFinalizedTx(ctx, txHash, nonce, coinType) + require.False(t, finalized) + require.Nil(t, tx) + }) + + t.Run("should return error on ParseGatewayInstruction error", func(t *testing.T) { + // use CoinType_Zeta to cause ParseGatewayInstruction error + tx, finalized := ob.CheckFinalizedTx(ctx, txHash, nonce, coin.CoinType_Zeta) + require.False(t, finalized) + require.Nil(t, tx) + }) + + t.Run("should return error on ECDSA signer mismatch", func(t *testing.T) { + // create observer with other TSS address + tssOther := mocks.NewMockTSS(chain, sample.EthAddress().String(), "") + ob := createTestObserver(t, chain, solClient, tssOther) + + tx, finalized := ob.CheckFinalizedTx(ctx, txHash, nonce, coinType) + require.False(t, finalized) + require.Nil(t, tx) + }) + + t.Run("should return error on nonce mismatch", func(t *testing.T) { + // use different nonce + tx, finalized := ob.CheckFinalizedTx(ctx, txHash, nonce+1, coinType) + require.False(t, finalized) + require.Nil(t, tx) + }) +} + +func Test_ParseGatewayInstruction(t *testing.T) { + // the test chain and transaction hash + chain := chains.SolanaDevnet + txHash := withdrawTxTest + txAmount := uint64(890880) + + // gateway address + gatewayID, err := solana.PublicKeyFromBase58(GatewayAddressTest) + require.NoError(t, err) + + t.Run("should parse gateway instruction", func(t *testing.T) { + // load archived outbound tx result + txResult := testutils.LoadSolanaOutboundTxResult(t, TestDataDir, chain.ChainId, txHash) + + // parse gateway instruction + inst, err := observer.ParseGatewayInstruction(txResult, gatewayID, coin.CoinType_Gas) + require.NoError(t, err) + + // check sender, nonce and amount + sender, err := inst.Signer() + require.NoError(t, err) + require.Equal(t, tssAddressTest, sender.String()) + require.EqualValues(t, inst.GatewayNonce(), 0) + require.EqualValues(t, inst.TokenAmount(), txAmount) + }) + + t.Run("should return error on invalid number of instructions", func(t *testing.T) { + // load and unmarshal archived transaction + txResult := testutils.LoadSolanaOutboundTxResult(t, TestDataDir, chain.ChainId, txHash) + tx, err := txResult.Transaction.GetTransaction() + require.NoError(t, err) + + // remove all instructions + tx.Message.Instructions = nil + + inst, err := observer.ParseGatewayInstruction(txResult, gatewayID, coin.CoinType_Gas) + require.ErrorContains(t, err, "want 1 instruction, got 0") + require.Nil(t, inst) + }) + + t.Run("should return error on invalid program id index", func(t *testing.T) { + // load and unmarshal archived transaction + txResult := testutils.LoadSolanaOutboundTxResult(t, TestDataDir, chain.ChainId, txHash) + tx, err := txResult.Transaction.GetTransaction() + require.NoError(t, err) + + // set invalid program id index (out of range) + tx.Message.Instructions[0].ProgramIDIndex = 4 + + inst, err := observer.ParseGatewayInstruction(txResult, gatewayID, coin.CoinType_Gas) + require.ErrorContains(t, err, "error getting program ID") + require.Nil(t, inst) + }) + + t.Run("should return error when invoked program is not gateway", func(t *testing.T) { + // load and unmarshal archived transaction + txResult := testutils.LoadSolanaOutboundTxResult(t, TestDataDir, chain.ChainId, txHash) + tx, err := txResult.Transaction.GetTransaction() + require.NoError(t, err) + + // set invalid program id index (pda account index) + tx.Message.Instructions[0].ProgramIDIndex = 1 + + inst, err := observer.ParseGatewayInstruction(txResult, gatewayID, coin.CoinType_Gas) + require.ErrorContains(t, err, "not matching gatewayID") + require.Nil(t, inst) + }) + + t.Run("should return error when instruction parsing fails", func(t *testing.T) { + // load and unmarshal archived transaction + txResult := testutils.LoadSolanaOutboundTxResult(t, TestDataDir, chain.ChainId, txHash) + tx, err := txResult.Transaction.GetTransaction() + require.NoError(t, err) + + // set invalid instruction data to cause parsing error + tx.Message.Instructions[0].Data = []byte("invalid instruction data") + + inst, err := observer.ParseGatewayInstruction(txResult, gatewayID, coin.CoinType_Gas) + require.Error(t, err) + require.Nil(t, inst) + }) + + t.Run("should return error on unsupported coin type", func(t *testing.T) { + // load and unmarshal archived transaction + txResult := testutils.LoadSolanaOutboundTxResult(t, TestDataDir, chain.ChainId, txHash) + + inst, err := observer.ParseGatewayInstruction(txResult, gatewayID, coin.CoinType_ERC20) + require.ErrorContains(t, err, "unsupported outbound coin type") + require.Nil(t, inst) + }) +} + +func Test_ParseInstructionWithdraw(t *testing.T) { + // the test chain and transaction hash + chain := chains.SolanaDevnet + txHash := withdrawTxTest + txAmount := uint64(890880) + + t.Run("should parse instruction withdraw", func(t *testing.T) { + // load and unmarshal archived transaction + txResult := testutils.LoadSolanaOutboundTxResult(t, TestDataDir, chain.ChainId, txHash) + tx, err := txResult.Transaction.GetTransaction() + require.NoError(t, err) + + instruction := tx.Message.Instructions[0] + inst, err := contracts.ParseInstructionWithdraw(instruction) + require.NoError(t, err) + + // check sender, nonce and amount + sender, err := inst.Signer() + require.NoError(t, err) + require.Equal(t, tssAddressTest, sender.String()) + require.EqualValues(t, inst.GatewayNonce(), 0) + require.EqualValues(t, inst.TokenAmount(), txAmount) + }) + + t.Run("should return error on invalid instruction data", func(t *testing.T) { + // load and unmarshal archived transaction + txResult := testutils.LoadSolanaOutboundTxResult(t, TestDataDir, chain.ChainId, txHash) + txFake, err := txResult.Transaction.GetTransaction() + require.NoError(t, err) + + // set invalid instruction data + instruction := txFake.Message.Instructions[0] + instruction.Data = []byte("invalid instruction data") + + inst, err := contracts.ParseInstructionWithdraw(instruction) + require.ErrorContains(t, err, "error deserializing instruction") + require.Nil(t, inst) + }) + + t.Run("should return error on discriminator mismatch", func(t *testing.T) { + // load and unmarshal archived transaction + txResult := testutils.LoadSolanaOutboundTxResult(t, TestDataDir, chain.ChainId, txHash) + txFake, err := txResult.Transaction.GetTransaction() + require.NoError(t, err) + + // overwrite discriminator (first 8 bytes) + instruction := txFake.Message.Instructions[0] + fakeDiscriminator := "b712469c946da12100980d0000000000" + fakeDiscriminatorBytes, err := hex.DecodeString(fakeDiscriminator) + require.NoError(t, err) + copy(instruction.Data, fakeDiscriminatorBytes) + + inst, err := contracts.ParseInstructionWithdraw(instruction) + require.ErrorContains(t, err, "not a withdraw instruction") + require.Nil(t, inst) + }) +} diff --git a/zetaclient/chains/solana/signer/outbound_tracker_reporter.go b/zetaclient/chains/solana/signer/outbound_tracker_reporter.go new file mode 100644 index 0000000000..6462060beb --- /dev/null +++ b/zetaclient/chains/solana/signer/outbound_tracker_reporter.go @@ -0,0 +1,91 @@ +package signer + +import ( + "context" + "time" + + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/rpc" + "github.com/rs/zerolog" + + "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" +) + +const ( + // SolanaTransactionTimeout is the timeout for waiting for an outbound to be confirmed + // Transaction referencing a blockhash older than 150 blocks will expire and be rejected by Solana. + SolanaTransactionTimeout = 2 * time.Minute +) + +// reportToOutboundTracker launch a go routine with timeout to check for tx confirmation; +// it reports tx to outbound tracker only if it's confirmed by the Solana network. +func (signer *Signer) reportToOutboundTracker( + ctx context.Context, + zetacoreClient interfaces.ZetacoreClient, + chainID int64, + nonce uint64, + txSig solana.Signature, + logger zerolog.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) + return + } + + // launch a goroutine to monitor tx confirmation status + go func() { + defer func() { + signer.Signer.ClearBeingReportedFlag(txSig.String()) + }() + + start := time.Now() + for { + // Solana block time is 0.4~0.8 seconds; wait 5 seconds between each check + time.Sleep(5 * time.Second) + + // 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 + } + + // query tx using optimistic commitment level "confirmed" + tx, err := signer.client.GetTransaction(ctx, txSig, &rpc.GetTransactionOpts{ + // commitment "processed" seems to be a better choice but it's not supported + // see: https://solana.com/docs/rpc/http/gettransaction + Commitment: rpc.CommitmentConfirmed, + }) + if err != nil { + continue + } + + // exit goroutine if tx failed. + if tx.Meta.Err != nil { + // 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 + } + + // 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) + } else if zetaHash != "" { + logger.Info().Msgf("reportToOutboundTracker: added outbound %s for chain %d nonce %d; zeta txhash %s", txSig, chainID, nonce, 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 + } + } + }() +} diff --git a/zetaclient/chains/solana/signer/signer.go b/zetaclient/chains/solana/signer/signer.go new file mode 100644 index 0000000000..3fb7512512 --- /dev/null +++ b/zetaclient/chains/solana/signer/signer.go @@ -0,0 +1,184 @@ +package signer + +import ( + "context" + + "cosmossdk.io/errors" + ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/rpc" + + "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/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/metrics" + "github.com/zeta-chain/zetacore/zetaclient/outboundprocessor" +) + +var _ interfaces.ChainSigner = (*Signer)(nil) + +// Signer deals with signing BTC transactions and implements the ChainSigner interface +type Signer struct { + *base.Signer + + // 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 + + // gatewayID is the program ID of gateway program on Solana chain + gatewayID solana.PublicKey + + // pda is the program derived address of the gateway program + pda solana.PublicKey +} + +// NewSigner creates a new Bitcoin signer +func NewSigner( + chain chains.Chain, + chainParams observertypes.ChainParams, + solClient interfaces.SolanaRPCClient, + tss interfaces.TSSSigner, + solanaKey solana.PrivateKey, + ts *metrics.TelemetryServer, + logger base.Logger, +) (*Signer, error) { + // create base signer + baseSigner := base.NewSigner(chain, tss, ts, logger) + + // parse gateway ID and PDA + gatewayID, pda, err := contracts.ParseGatewayIDAndPda(chainParams.GatewayAddress) + 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 +} + +// TryProcessOutbound - signer interface implementation +// This function will attempt to build and sign a Solana transaction using the TSS signer. +// It will then broadcast the signed transaction to the Solana chain. +func (signer *Signer) TryProcessOutbound( + ctx context.Context, + cctx *types.CrossChainTx, + outboundProc *outboundprocessor.Processor, + outboundID string, + _ 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("TryProcessOutbound: %s, caught panic error: %v", cctx.Index, err) + } + }() + + // 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() + + // support gas token only for Solana outbound + chainID := signer.Chain().ChainId + nonce := params.TssNonce + coinType := cctx.InboundParams.CoinType + if coinType != coin.CoinType_Gas { + logger.Error(). + Msgf("TryProcessOutbound: can only send SOL to the Solana network for chain %d nonce %d", chainID, nonce) + return + } + + // sign gateway withdraw message by TSS + msg, err := signer.SignMsgWithdraw(ctx, params, height) + if err != nil { + logger.Error().Err(err).Msgf("TryProcessOutbound: SignMsgWithdraw error for chain %d nonce %d", chainID, nonce) + return + } + + // sign the withdraw transaction by fee payer + tx, err := signer.SignWithdrawTx(ctx, *msg) + if err != nil { + logger.Error().Err(err).Msgf("TryProcessOutbound: SignWithdrawTx error for chain %d nonce %d", chainID, nonce) + return + } + + // broadcast the signed tx to the Solana network with preflight check + txSig, err := signer.client.SendTransactionWithOpts( + ctx, + tx, + // Commitment "finalized" is too conservative for preflight check and + // it results in repeated broadcast attempts that only 1 will succeed. + // Commitment "processed" will simulate tx against more recent state + // thus fails faster once a tx is already broadcasted and processed by the cluster. + // This reduces the number of "failed" txs due to repeated broadcast attempts. + rpc.TransactionOpts{PreflightCommitment: rpc.CommitmentProcessed}, + ) + if err != nil { + signer.Logger(). + Std.Warn(). + Err(err). + Msgf("TryProcessOutbound: broadcast error for chain %d nonce %d", chainID, nonce) + return + } + + // report the outbound to the outbound tracker + signer.reportToOutboundTracker(ctx, zetacoreClient, chainID, nonce, txSig, logger) +} + +// SetGatewayAddress sets the gateway address +func (signer *Signer) SetGatewayAddress(address string) { + // parse gateway ID and PDA + gatewayID, pda, err := contracts.ParseGatewayIDAndPda(address) + if err != nil { + signer.Logger().Std.Error().Err(err).Msgf("cannot parse gateway address %s", address) + } + + // update gateway ID and PDA + signer.Lock() + defer signer.Unlock() + + signer.gatewayID = gatewayID + signer.pda = pda +} + +// GetGatewayAddress returns the gateway address +func (signer *Signer) GetGatewayAddress() string { + signer.Lock() + defer signer.Unlock() + return signer.gatewayID.String() +} + +// 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) { +} + +func (signer *Signer) SetERC20CustodyAddress(_ ethcommon.Address) { +} + +func (signer *Signer) GetZetaConnectorAddress() ethcommon.Address { + return ethcommon.Address{} +} + +func (signer *Signer) GetERC20CustodyAddress() ethcommon.Address { + return ethcommon.Address{} +} diff --git a/zetaclient/chains/solana/signer/withdraw.go b/zetaclient/chains/solana/signer/withdraw.go new file mode 100644 index 0000000000..383d1c908a --- /dev/null +++ b/zetaclient/chains/solana/signer/withdraw.go @@ -0,0 +1,123 @@ +package signer + +import ( + "context" + + "cosmossdk.io/errors" + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/rpc" + "github.com/near/borsh-go" + + "github.com/zeta-chain/zetacore/pkg/chains" + contracts "github.com/zeta-chain/zetacore/pkg/contracts/solana" + "github.com/zeta-chain/zetacore/x/crosschain/types" +) + +// SignMsgWithdraw signs a withdraw message (for gateway withdraw/withdraw_spl instruction) with TSS. +func (signer *Signer) SignMsgWithdraw( + ctx context.Context, + params *types.OutboundParams, + height uint64, +) (*contracts.MsgWithdraw, error) { + chain := signer.Chain() + // #nosec G115 always positive + chainID := uint64(signer.Chain().ChainId) + nonce := params.TssNonce + amount := params.Amount.Uint64() + + // check receiver address + to, err := chains.DecodeSolanaWalletAddress(params.Receiver) + if err != nil { + return nil, errors.Wrapf(err, "cannot decode receiver address %s", params.Receiver) + } + + // prepare withdraw msg and compute hash + msg := contracts.NewMsgWithdraw(chainID, nonce, amount, to) + msgHash := msg.Hash() + + // sign the message with TSS to get an ECDSA signature. + // the produced signature is in the [R || S || V] format where V is 0 or 1. + signature, err := signer.TSS().Sign(ctx, msgHash[:], height, nonce, chain.ChainId, "") + if err != nil { + return nil, errors.Wrap(err, "Key-sign failed") + } + signer.Logger().Std.Info().Msgf("Key-sign succeed for chain %d nonce %d", chainID, nonce) + + // attach the signature and return + return msg.SetSignature(signature), nil +} + +// SignWithdrawTx wraps the withdraw 'msg' into a Solana transaction and signs it with the fee payer key. +func (signer *Signer) SignWithdrawTx(ctx context.Context, msg contracts.MsgWithdraw) (*solana.Transaction, error) { + // create withdraw instruction with program call data + var err error + var inst solana.GenericInstruction + inst.DataBytes, err = borsh.Serialize(contracts.WithdrawInstructionParams{ + Discriminator: contracts.DiscriminatorWithdraw(), + Amount: msg.Amount(), + Signature: msg.SigRS(), + RecoveryID: msg.SigV(), + MessageHash: msg.Hash(), + Nonce: msg.Nonce(), + }) + if err != nil { + return nil, errors.Wrap(err, "cannot serialize withdraw instruction") + } + + // attach required accounts to the instruction + privkey := signer.solanaFeePayerKey + attachWithdrawAccounts(&inst, privkey.PublicKey(), signer.pda, msg.To(), signer.gatewayID) + + // get a recent blockhash + recent, err := signer.client.GetRecentBlockhash(ctx, rpc.CommitmentFinalized) + if err != nil { + return nil, errors.Wrap(err, "GetRecentBlockhash error") + } + + // create a transaction that wraps the instruction + tx, err := solana.NewTransaction( + []solana.Instruction{ + // TODO: outbound now uses 5K lamports as the fixed fee, we could explore priority fee and compute budget + // https://github.com/zeta-chain/node/issues/2599 + // programs.ComputeBudgetSetComputeUnitLimit(computeUnitLimit), + // programs.ComputeBudgetSetComputeUnitPrice(computeUnitPrice), + &inst}, + recent.Value.Blockhash, + solana.TransactionPayer(privkey.PublicKey()), + ) + if err != nil { + return nil, errors.Wrap(err, "NewTransaction error") + } + + // fee payer signs the transaction + _, err = tx.Sign(func(key solana.PublicKey) *solana.PrivateKey { + if key.Equals(privkey.PublicKey()) { + return &privkey + } + return nil + }) + if err != nil { + return nil, errors.Wrap(err, "signer unable to sign transaction") + } + + return tx, nil +} + +// attachWithdrawAccounts attaches the required accounts for the gateway withdraw instruction. +func attachWithdrawAccounts( + inst *solana.GenericInstruction, + signer solana.PublicKey, + pda solana.PublicKey, + to solana.PublicKey, + gatewayID solana.PublicKey, +) { + // attach required accounts to the instruction + var accountSlice []*solana.AccountMeta + accountSlice = append(accountSlice, solana.Meta(signer).WRITE().SIGNER()) + accountSlice = append(accountSlice, solana.Meta(pda).WRITE()) + accountSlice = append(accountSlice, solana.Meta(to).WRITE()) + accountSlice = append(accountSlice, solana.Meta(gatewayID)) + inst.ProgID = gatewayID + + inst.AccountValues = accountSlice +} diff --git a/zetaclient/common/constant.go b/zetaclient/common/constant.go index 5e410d4886..64d906ca0d 100644 --- a/zetaclient/common/constant.go +++ b/zetaclient/common/constant.go @@ -1,6 +1,9 @@ package common const ( + // DefaultGasPriceMultiplier is the default gas price multiplier for all chains + DefaultGasPriceMultiplier = 1.0 + // EVMOutboundGasPriceMultiplier is the default gas price multiplier for EVM-chain outbond txs EVMOutboundGasPriceMultiplier = 1.2 diff --git a/zetaclient/config/types.go b/zetaclient/config/types.go index b9535f3937..1ec58e12aa 100644 --- a/zetaclient/config/types.go +++ b/zetaclient/config/types.go @@ -2,9 +2,15 @@ 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" ) @@ -81,6 +87,7 @@ type Config struct { KeyringBackend KeyringBackend `json:"KeyringBackend"` HsmMode bool `json:"HsmMode"` HsmHotKey string `json:"HsmHotKey"` + SolanaKeyFile string `json:"SolanaKeyFile"` // chain configs EVMChainConfigs map[int64]EVMConfig `json:"EVMChainConfigs"` @@ -157,3 +164,33 @@ func (c Config) GetKeyringBackend() KeyringBackend { defer c.mu.RUnlock() 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) + + return privKey, nil +} diff --git a/zetaclient/orchestrator/bootstrap.go b/zetaclient/orchestrator/bootstrap.go index 04cd4646ea..cffb9085c7 100644 --- a/zetaclient/orchestrator/bootstrap.go +++ b/zetaclient/orchestrator/bootstrap.go @@ -17,6 +17,7 @@ import ( evmsigner "github.com/zeta-chain/zetacore/zetaclient/chains/evm/signer" "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" solbserver "github.com/zeta-chain/zetacore/zetaclient/chains/solana/observer" + solanasigner "github.com/zeta-chain/zetacore/zetaclient/chains/solana/signer" "github.com/zeta-chain/zetacore/zetaclient/config" zctx "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/zetaclient/db" @@ -151,15 +152,72 @@ func syncSignerMap( continue } - cfg, _ := app.Config().GetBTCConfig() + // get BTC config + cfg, found := app.Config().GetBTCConfig() + if !found { + logger.Std.Error().Msgf("Unable to find BTC config for chain %d", chainID) + continue + } - utxoSigner, err := btcsigner.NewSigner(btcChain, tss, ts, logger, cfg) + signer, err := btcsigner.NewSigner(btcChain, tss, ts, logger, cfg) if err != nil { - logger.Std.Error().Err(err).Msgf("Unable to construct signer for UTXO chain %d", chainID) + logger.Std.Error().Err(err).Msgf("Unable to construct signer for BTC chain %d", chainID) continue } - addSigner(chainID, utxoSigner) + 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 + } + + // 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 @@ -381,7 +439,7 @@ func syncObserverMap( rpcClient := solrpc.New(solConfig.Endpoint) if rpcClient == nil { // should never happen - logger.Std.Error().Msg("solana create Solana client error") + logger.Std.Error().Msgf("Unable to create Solana client from endpoint %s", solConfig.Endpoint) continue } diff --git a/zetaclient/orchestrator/orchestrator.go b/zetaclient/orchestrator/orchestrator.go index e96935509e..edcaa3d9d5 100644 --- a/zetaclient/orchestrator/orchestrator.go +++ b/zetaclient/orchestrator/orchestrator.go @@ -22,6 +22,7 @@ import ( 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" "github.com/zeta-chain/zetacore/zetaclient/metrics" "github.com/zeta-chain/zetacore/zetaclient/outboundprocessor" @@ -153,31 +154,38 @@ func (oc *Orchestrator) resolveSigner(app *zctx.AppContext, chainID int64) (inte return nil, err } - // noop for non-EVM chains - if !chains.IsEVMChain(chainID, app.GetAdditionalChains()) { - return signer, nil - } - - evmParams, found := app.GetEVMChainParams(chainID) - if !found { - return signer, nil - } - - // 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) - } + // 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) + } - 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(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) + } + } + } 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) + } + } } return signer, nil @@ -220,6 +228,13 @@ func (oc *Orchestrator) resolveObserver(app *zctx.AppContext, chainID int64) (in 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) + } } return observer, nil @@ -378,8 +393,10 @@ func (oc *Orchestrator) runScheduler(ctx context.Context) error { 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("StartCctxScheduler: unsupported chain %d", c.ChainId) + oc.logger.Error().Msgf("runScheduler: unsupported chain %d", c.ChainId) continue } } @@ -433,17 +450,17 @@ func (oc *Orchestrator) ScheduleCctxEVM( break } - // try confirming the outbound - included, _, err := observer.IsOutboundProcessed(ctx, cctx, oc.logger.Logger) + // vote outbound if it's already confirmed + continueKeysign, err := observer.VoteOutboundIfConfirmed(ctx, cctx) if err != nil { oc.logger.Error(). Err(err). - Msgf("ScheduleCctxEVM: IsOutboundProcessed faild for chain %d nonce %d", chainID, nonce) + Msgf("ScheduleCctxEVM: VoteOutboundIfConfirmed failed for chain %d nonce %d", chainID, nonce) continue } - if included { + if !continueKeysign { oc.logger.Info(). - Msgf("ScheduleCctxEVM: outbound %s already included; do not schedule keysign", outboundID) + Msgf("ScheduleCctxEVM: outbound %s already processed; do not schedule keysign", outboundID) continue } @@ -473,7 +490,7 @@ func (oc *Orchestrator) ScheduleCctxEVM( !oc.outboundProc.IsOutboundActive(outboundID) { oc.outboundProc.StartTryProcess(outboundID) oc.logger.Debug(). - Msgf("ScheduleCctxEVM: sign outbound %s with value %d\n", outboundID, cctx.GetCurrentOutboundParam().Amount) + Msgf("ScheduleCctxEVM: sign outbound %s with value %d", outboundID, cctx.GetCurrentOutboundParam().Amount) go signer.TryProcessOutbound( ctx, cctx, @@ -525,16 +542,16 @@ func (oc *Orchestrator) ScheduleCctxBTC( continue } // try confirming the outbound - included, confirmed, err := btcObserver.IsOutboundProcessed(ctx, cctx, oc.logger.Logger) + continueKeysign, err := btcObserver.VoteOutboundIfConfirmed(ctx, cctx) if err != nil { oc.logger.Error(). Err(err). - Msgf("ScheduleCctxBTC: IsOutboundProcessed faild for chain %d nonce %d", chainID, nonce) + Msgf("ScheduleCctxBTC: VoteOutboundIfConfirmed failed for chain %d nonce %d", chainID, nonce) continue } - if included || confirmed { + if !continueKeysign { oc.logger.Info(). - Msgf("ScheduleCctxBTC: outbound %s already included; do not schedule keysign", outboundID) + Msgf("ScheduleCctxBTC: outbound %s already processed; do not schedule keysign", outboundID) continue } @@ -550,10 +567,70 @@ func (oc *Orchestrator) ScheduleCctxBTC( Msgf("ScheduleCctxBTC: lookahead reached, signing %d, earliest pending %d", nonce, cctxList[0].GetCurrentOutboundParam().TssNonce) break } - // try confirming the outbound or scheduling a keysign + // schedule a TSS keysign + if nonce%interval == zetaHeight%interval && !oc.outboundProc.IsOutboundActive(outboundID) { + oc.outboundProc.StartTryProcess(outboundID) + oc.logger.Debug().Msgf("ScheduleCctxBTC: sign outbound %s with value %d", outboundID, params.Amount) + go signer.TryProcessOutbound( + ctx, + cctx, + oc.outboundProc, + outboundID, + observer, + oc.zetacoreClient, + zetaHeight, + ) + } + } +} + +// ScheduleCctxSolana schedules solana outbound keysign on each ZetaChain block (the ticker) +func (oc *Orchestrator) ScheduleCctxSolana( + ctx context.Context, + zetaHeight uint64, + chainID int64, + cctxList []*types.CrossChainTx, + observer interfaces.ChainObserver, + signer interfaces.ChainSigner, +) { + solObserver, ok := observer.(*solanaobserver.Observer) + if !ok { // should never happen + oc.logger.Error().Msgf("ScheduleCctxSolana: chain observer is not a solana observer") + return + } + // #nosec G701 positive + interval := uint64(observer.GetChainParams().OutboundScheduleInterval) + + // schedule keysign for each pending cctx + for _, cctx := range cctxList { + params := cctx.GetCurrentOutboundParam() + nonce := params.TssNonce + outboundID := outboundprocessor.ToOutboundID(cctx.Index, params.ReceiverChainId, nonce) + + if params.ReceiverChainId != chainID { + oc.logger.Error(). + Msgf("ScheduleCctxSolana: outbound %s chainid mismatch: want %d, got %d", outboundID, chainID, params.ReceiverChainId) + continue + } + + // vote outbound if it's already confirmed + continueKeysign, err := solObserver.VoteOutboundIfConfirmed(ctx, cctx) + if err != nil { + oc.logger.Error(). + Err(err). + Msgf("ScheduleCctxSolana: VoteOutboundIfConfirmed failed for chain %d nonce %d", chainID, nonce) + continue + } + if !continueKeysign { + oc.logger.Info(). + Msgf("ScheduleCctxSolana: outbound %s already processed; do not schedule keysign", outboundID) + continue + } + + // schedule a TSS keysign if nonce%interval == zetaHeight%interval && !oc.outboundProc.IsOutboundActive(outboundID) { oc.outboundProc.StartTryProcess(outboundID) - oc.logger.Debug().Msgf("ScheduleCctxBTC: sign outbound %s with value %d\n", outboundID, params.Amount) + oc.logger.Debug().Msgf("ScheduleCctxSolana: sign outbound %s with value %d", outboundID, params.Amount) go signer.TryProcessOutbound( ctx, cctx, diff --git a/zetaclient/orchestrator/orchestrator_test.go b/zetaclient/orchestrator/orchestrator_test.go index 94c134cad6..af4ca5c346 100644 --- a/zetaclient/orchestrator/orchestrator_test.go +++ b/zetaclient/orchestrator/orchestrator_test.go @@ -13,6 +13,7 @@ import ( "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/pkg/coin" + solanacontracts "github.com/zeta-chain/zetacore/pkg/contracts/solana" "github.com/zeta-chain/zetacore/testutil/sample" crosschainkeeper "github.com/zeta-chain/zetacore/x/crosschain/keeper" crosschaintypes "github.com/zeta-chain/zetacore/x/crosschain/types" @@ -27,37 +28,56 @@ import ( func MockOrchestrator( t *testing.T, zetacoreClient interfaces.ZetacoreClient, - evmChain, btcChain chains.Chain, - evmChainParams, btcChainParams *observertypes.ChainParams, + evmChain, btcChain, solChain *chains.Chain, + evmChainParams, btcChainParams, solChainParams *observertypes.ChainParams, ) *Orchestrator { - // create mock signers and clients - evmSigner := mocks.NewEVMSigner( - evmChain, - ethcommon.HexToAddress(evmChainParams.ConnectorContractAddress), - ethcommon.HexToAddress(evmChainParams.Erc20CustodyContractAddress), - ) - btcSigner := mocks.NewBTCSigner() - evmObserver := mocks.NewEVMObserver(evmChainParams) - btcObserver := mocks.NewBTCObserver(btcChainParams) + // 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: map[int64]interfaces.ChainSigner{ - evmChain.ChainId: evmSigner, - btcChain.ChainId: btcSigner, - }, - observerMap: map[int64]interfaces.ChainObserver{ - evmChain.ChainId: evmObserver, - btcChain.ChainId: btcObserver, - }, + signerMap: signerMap, + observerMap: observerMap, } return orchestrator } func CreateAppContext( - evmChain, btcChain chains.Chain, - evmChainParams, btcChainParams *observertypes.ChainParams, + evmChain, btcChain, solChain chains.Chain, + evmChainParams, btcChainParams, solChainParams *observertypes.ChainParams, ) *zctx.AppContext { // new config cfg := config.New(false) @@ -77,10 +97,10 @@ func CreateAppContext( // feed chain params appContext.Update( &observertypes.Keygen{}, - []chains.Chain{evmChain, btcChain}, + []chains.Chain{evmChain, btcChain, solChain}, evmChainParamsMap, btcChainParams, - nil, + solChainParams, "", *ccFlags, []chains.Chain{}, @@ -94,42 +114,92 @@ 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, + } - // new chain params in AppContext + // new evm chain params in AppContext evmChainParamsNew := &observertypes.ChainParams{ ChainId: evmChain.ChainId, ConnectorContractAddress: testutils.OtherAddress1, Erc20CustodyContractAddress: testutils.OtherAddress2, } - t.Run("signer should not be found", func(t *testing.T) { - orchestrator := MockOrchestrator(t, nil, evmChain, btcChain, evmChainParams, btcChainParams) - context := CreateAppContext(evmChain, btcChain, evmChainParamsNew, btcChainParams) + // 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) + // BSC signer should not be found _, err := orchestrator.resolveSigner(context, chains.BscMainnet.ChainId) require.ErrorContains(t, err, "signer not found") }) - t.Run("should be able to update connector and erc20 custody address", func(t *testing.T) { - orchestrator := MockOrchestrator(t, nil, evmChain, btcChain, evmChainParams, btcChainParams) - context := CreateAppContext(evmChain, btcChain, evmChainParamsNew, btcChainParams) + 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) + // update signer with new connector and erc20 custody address signer, err := orchestrator.resolveSigner(context, 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, + ) + context := CreateAppContext(evmChain, btcChain, solChain, evmChainParams, btcChainParams, solChainParamsNew) + + // update signer with new gateway address + signer, err := orchestrator.resolveSigner(context, solChain.ChainId) + require.NoError(t, err) + require.Equal(t, solChainParamsNew.GatewayAddress, signer.GetGatewayAddress()) + }) } 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(), @@ -138,6 +208,10 @@ func Test_GetUpdatedChainObserver(t *testing.T) { btcChainParams := &observertypes.ChainParams{ ChainId: btcChain.ChainId, } + solChainParams := &observertypes.ChainParams{ + ChainId: solChain.ChainId, + GatewayAddress: solanacontracts.SolanaGatewayProgramID, + } // new chain params in AppContext evmChainParamsNew := &observertypes.ChainParams{ @@ -172,17 +246,51 @@ func Test_GetUpdatedChainObserver(t *testing.T) { MinObserverDelegation: sdk.OneDec(), IsSupported: true, } + solChainParamsNew := &observertypes.ChainParams{ + ChainId: solChain.ChainId, + ConfirmationCount: 10, + GasPriceTicker: 5, + InboundTicker: 6, + OutboundTicker: 6, + WatchUtxoTicker: 1, + ZetaTokenContractAddress: "", + ConnectorContractAddress: "", + Erc20CustodyContractAddress: "", + OutboundScheduleInterval: 10, + OutboundScheduleLookahead: 10, + BallotThreshold: sdk.OneDec(), + MinObserverDelegation: sdk.OneDec(), + IsSupported: true, + } t.Run("evm chain observer should not be found", func(t *testing.T) { - orchestrator := MockOrchestrator(t, nil, evmChain, btcChain, evmChainParams, btcChainParams) - appContext := CreateAppContext(evmChain, btcChain, evmChainParamsNew, btcChainParams) + orchestrator := MockOrchestrator( + t, + nil, + &evmChain, + &btcChain, + &solChain, + evmChainParams, + btcChainParams, + solChainParams, + ) + appContext := CreateAppContext(evmChain, btcChain, solChain, evmChainParamsNew, btcChainParams, solChainParams) // 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(t, nil, evmChain, btcChain, evmChainParams, btcChainParams) - appContext := CreateAppContext(evmChain, btcChain, evmChainParamsNew, btcChainParams) + orchestrator := MockOrchestrator( + t, + nil, + &evmChain, + &btcChain, + &solChain, + evmChainParams, + btcChainParams, + solChainParams, + ) + appContext := CreateAppContext(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) @@ -190,21 +298,73 @@ func Test_GetUpdatedChainObserver(t *testing.T) { require.True(t, observertypes.ChainParamsEqual(*evmChainParamsNew, chainOb.GetChainParams())) }) t.Run("btc chain observer should not be found", func(t *testing.T) { - orchestrator := MockOrchestrator(t, nil, evmChain, btcChain, evmChainParams, btcChainParams) - appContext := CreateAppContext(btcChain, btcChain, evmChainParams, btcChainParamsNew) + orchestrator := MockOrchestrator( + t, + nil, + &evmChain, + &btcChain, + &solChain, + evmChainParams, + btcChainParams, + solChainParams, + ) + appContext := CreateAppContext(btcChain, 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(t, nil, evmChain, btcChain, evmChainParams, btcChainParams) - appContext := CreateAppContext(btcChain, btcChain, evmChainParams, btcChainParamsNew) + orchestrator := MockOrchestrator( + t, + nil, + &evmChain, + &btcChain, + &solChain, + evmChainParams, + btcChainParams, + 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) require.NotNil(t, chainOb) require.True(t, observertypes.ChainParamsEqual(*btcChainParamsNew, chainOb.GetChainParams())) }) + t.Run("solana chain observer should not be found", func(t *testing.T) { + orchestrator := MockOrchestrator( + t, + nil, + &evmChain, + &btcChain, + &solChain, + evmChainParams, + btcChainParams, + solChainParams, + ) + 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, + ) + 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) + require.NotNil(t, chainOb) + require.True(t, observertypes.ChainParamsEqual(*solChainParamsNew, chainOb.GetChainParams())) + }) } func Test_GetPendingCctxsWithinRateLimit(t *testing.T) { @@ -367,7 +527,7 @@ func Test_GetPendingCctxsWithinRateLimit(t *testing.T) { client.WithPendingCctx(btcChain.ChainId, tt.btcCctxsFallback) // create orchestrator - orchestrator := MockOrchestrator(t, client, ethChain, btcChain, ethChainParams, btcChainParams) + orchestrator := MockOrchestrator(t, client, ðChain, &btcChain, nil, ethChainParams, btcChainParams, nil) // run the test cctxsMap, err := orchestrator.GetPendingCctxsWithinRateLimit(ctx, foreignChains) diff --git a/zetaclient/testdata/solana/chain_901_outbound_tx_result_5iBYjBYCphzjHKfmPwddMWpV2RNssmzk9Z8NNmV9Rei71pZKBTEVdkmUeyXfn7eWbV8932uSsPfBxgA7UgERNTvq.json b/zetaclient/testdata/solana/chain_901_outbound_tx_result_5iBYjBYCphzjHKfmPwddMWpV2RNssmzk9Z8NNmV9Rei71pZKBTEVdkmUeyXfn7eWbV8932uSsPfBxgA7UgERNTvq.json new file mode 100644 index 0000000000..068a1bcea9 --- /dev/null +++ b/zetaclient/testdata/solana/chain_901_outbound_tx_result_5iBYjBYCphzjHKfmPwddMWpV2RNssmzk9Z8NNmV9Rei71pZKBTEVdkmUeyXfn7eWbV8932uSsPfBxgA7UgERNTvq.json @@ -0,0 +1,52 @@ +{ + "slot": 313938016, + "blockTime": 1721837007, + "transaction": { + "signatures": [ + "5iBYjBYCphzjHKfmPwddMWpV2RNssmzk9Z8NNmV9Rei71pZKBTEVdkmUeyXfn7eWbV8932uSsPfBxgA7UgERNTvq" + ], + "message": { + "accountKeys": [ + "AKbG83jg2V65R7XvaPFrnUvUTWsFENEzDPbLJFEiAk6L", + "4pA5vqGeo4ipLoJzH3rdvguhifj1tCzoNM8vDRc4Xbmq", + "63AA8QAokSfoTDxqPXn8UsnYLSFas6MuGt6PyAAkfnr8", + "2kJndCL9NBR36ySiQ4bmArs4YgWQu67LmCDfLzk5Gb7s" + ], + "header": { + "numRequiredSignatures": 1, + "numReadonlySignedAccounts": 0, + "numReadonlyUnsignedAccounts": 1 + }, + "recentBlockhash": "FuPpUtJXMbtM4tP4dhLi5nJKYANmVHfmogov8exp1uYB", + "instructions": [ + { + "programIdIndex": 3, + "accounts": [0, 1, 2, 3], + "data": "2w17NoTLyzKUXw8xjdycsviQm9PxEF1weowhTVmYRKQaNkDVmEZnyeZEBFvu6qeYMAEKyRNdodzyzDgerQT96zmkRg2ECU4XPJ1GdsaiLDArWxB4WR8AaUFDnLwvkwM2kYZb133VKo2GD1Uz63sEiTn6aUGh4ptdiCd9R1" + } + ] + } + }, + "meta": { + "err": null, + "fee": 5000, + "preBalances": [7171092800, 1001448960, 0, 1141440], + "postBalances": [7171087800, 1000558080, 890880, 1141440], + "innerInstructions": [], + "preTokenBalances": [], + "postTokenBalances": [], + "logMessages": [ + "Program 2kJndCL9NBR36ySiQ4bmArs4YgWQu67LmCDfLzk5Gb7s invoke [1]", + "Program log: Instruction: Withdraw", + "Program log: recovered address [5, 199, 219, 221, 25, 84, 213, 156, 154, 250, 184, 72, 218, 125, 141, 211, 243, 94, 105, 205]", + "Program log: recovered address [5, 199, 219, 221, 25, 84, 213, 156, 154, 250, 184, 72, 218, 125, 141, 211, 243, 94, 105, 205]", + "Program 2kJndCL9NBR36ySiQ4bmArs4YgWQu67LmCDfLzk5Gb7s consumed 40719 of 200000 compute units", + "Program 2kJndCL9NBR36ySiQ4bmArs4YgWQu67LmCDfLzk5Gb7s success" + ], + "status": { "Ok": null }, + "rewards": [], + "loadedAddresses": { "readonly": [], "writable": [] }, + "computeUnitsConsumed": 40719 + }, + "version": 0 +} diff --git a/zetaclient/testdata/solana/chain_901_outbound_tx_result_5nFUQgNSdqTd4aPS4a1xNcbehj19hDzuQLfBqFRj8g7BJdESVY6hFuTFPWFuV6aWAfzEMfVfCdNu9DfzVp5FsHg5.json b/zetaclient/testdata/solana/chain_901_outbound_tx_result_5nFUQgNSdqTd4aPS4a1xNcbehj19hDzuQLfBqFRj8g7BJdESVY6hFuTFPWFuV6aWAfzEMfVfCdNu9DfzVp5FsHg5.json new file mode 100644 index 0000000000..bf9182a719 --- /dev/null +++ b/zetaclient/testdata/solana/chain_901_outbound_tx_result_5nFUQgNSdqTd4aPS4a1xNcbehj19hDzuQLfBqFRj8g7BJdESVY6hFuTFPWFuV6aWAfzEMfVfCdNu9DfzVp5FsHg5.json @@ -0,0 +1,52 @@ +{ + "slot": 314424406, + "blockTime": 1722020059, + "transaction": { + "signatures": [ + "5nFUQgNSdqTd4aPS4a1xNcbehj19hDzuQLfBqFRj8g7BJdESVY6hFuTFPWFuV6aWAfzEMfVfCdNu9DfzVp5FsHg5" + ], + "message": { + "accountKeys": [ + "AKbG83jg2V65R7XvaPFrnUvUTWsFENEzDPbLJFEiAk6L", + "4pA5vqGeo4ipLoJzH3rdvguhifj1tCzoNM8vDRc4Xbmq", + "9fA4vYZfCa9k9UHjnvYCk4YoipsooapGciKMgaTBw9UH", + "2kJndCL9NBR36ySiQ4bmArs4YgWQu67LmCDfLzk5Gb7s" + ], + "header": { + "numRequiredSignatures": 1, + "numReadonlySignedAccounts": 0, + "numReadonlyUnsignedAccounts": 1 + }, + "recentBlockhash": "77YRY3YAi9Yvp7V68QJHMkNKRqYAZ7GSAYVDPomCs74J", + "instructions": [ + { + "programIdIndex": 3, + "accounts": [0, 1, 2, 3], + "data": "2w17NoTLyzKYBFJZsc7f4RUTW5fBUqVdzamb3zt37gRNDgSipheFFs83ZMzPDBM363UDMRAcVrEcdcdpnD7nooKwsZRogVp3xv1zgVE9t95m7KrL7DZgHh85wvzePeVoraWzLgQLPuktDHg5FkeDN2L14xjeBu1S8ZQPHH" + } + ] + } + }, + "meta": { + "err": { "InstructionError": [0, { "Custom": 6002 }] }, + "fee": 5000, + "preBalances": [7171082800, 1000558080, 0, 1141440], + "postBalances": [7171077800, 1000558080, 0, 1141440], + "innerInstructions": [], + "preTokenBalances": [], + "postTokenBalances": [], + "logMessages": [ + "Program 2kJndCL9NBR36ySiQ4bmArs4YgWQu67LmCDfLzk5Gb7s invoke [1]", + "Program log: Instruction: Withdraw", + "Program log: mismatch nonce", + "Program log: AnchorError thrown in programs/protocol-contracts-solana/src/lib.rs:109. Error Code: NonceMismatch. Error Number: 6002. Error Message: NonceMismatch.", + "Program 2kJndCL9NBR36ySiQ4bmArs4YgWQu67LmCDfLzk5Gb7s consumed 4801 of 200000 compute units", + "Program 2kJndCL9NBR36ySiQ4bmArs4YgWQu67LmCDfLzk5Gb7s failed: custom program error: 0x1772" + ], + "status": { "Err": { "InstructionError": [0, { "Custom": 6002 }] } }, + "rewards": [], + "loadedAddresses": { "readonly": [], "writable": [] }, + "computeUnitsConsumed": 4801 + }, + "version": 0 +} diff --git a/zetaclient/testutils/mocks/chain_clients.go b/zetaclient/testutils/mocks/chain_clients.go index 6f004420e5..0c02dff5ca 100644 --- a/zetaclient/testutils/mocks/chain_clients.go +++ b/zetaclient/testutils/mocks/chain_clients.go @@ -3,8 +3,6 @@ package mocks import ( "context" - "github.com/rs/zerolog" - crosschaintypes "github.com/zeta-chain/zetacore/x/crosschain/types" observertypes "github.com/zeta-chain/zetacore/x/observer/types" "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" @@ -29,12 +27,11 @@ func NewEVMObserver(chainParams *observertypes.ChainParams) *EVMObserver { func (ob *EVMObserver) Start(_ context.Context) {} func (ob *EVMObserver) Stop() {} -func (ob *EVMObserver) IsOutboundProcessed( +func (ob *EVMObserver) VoteOutboundIfConfirmed( _ context.Context, _ *crosschaintypes.CrossChainTx, - _ zerolog.Logger, -) (bool, bool, error) { - return false, false, nil +) (bool, error) { + return false, nil } func (ob *EVMObserver) SetChainParams(chainParams observertypes.ChainParams) { @@ -73,12 +70,11 @@ func (ob *BTCObserver) Start(_ context.Context) {} func (ob *BTCObserver) Stop() {} -func (ob *BTCObserver) IsOutboundProcessed( +func (ob *BTCObserver) VoteOutboundIfConfirmed( _ context.Context, _ *crosschaintypes.CrossChainTx, - _ zerolog.Logger, -) (bool, bool, error) { - return false, false, nil +) (bool, error) { + return false, nil } func (ob *BTCObserver) SetChainParams(chainParams observertypes.ChainParams) { @@ -94,3 +90,44 @@ func (ob *BTCObserver) GetTxID(_ uint64) string { } func (ob *BTCObserver) WatchInboundTracker(_ context.Context) error { return nil } + +// ---------------------------------------------------------------------------- +// SolanaObserver +// ---------------------------------------------------------------------------- +var _ interfaces.ChainObserver = (*SolanaObserver)(nil) + +// SolanaObserver is a mock of solana chain observer for testing +type SolanaObserver struct { + ChainParams observertypes.ChainParams +} + +func NewSolanaObserver(chainParams *observertypes.ChainParams) *SolanaObserver { + return &SolanaObserver{ + ChainParams: *chainParams, + } +} + +func (ob *SolanaObserver) Start(_ context.Context) {} + +func (ob *SolanaObserver) Stop() {} + +func (ob *SolanaObserver) VoteOutboundIfConfirmed( + _ context.Context, + _ *crosschaintypes.CrossChainTx, +) (bool, error) { + return false, nil +} + +func (ob *SolanaObserver) SetChainParams(chainParams observertypes.ChainParams) { + ob.ChainParams = chainParams +} + +func (ob *SolanaObserver) GetChainParams() observertypes.ChainParams { + return ob.ChainParams +} + +func (ob *SolanaObserver) GetTxID(_ uint64) string { + return "" +} + +func (ob *SolanaObserver) WatchInboundTracker(_ context.Context) error { return nil } diff --git a/zetaclient/testutils/mocks/chain_signer.go b/zetaclient/testutils/mocks/chain_signer.go index 3785c34ee0..cb8080a958 100644 --- a/zetaclient/testutils/mocks/chain_signer.go +++ b/zetaclient/testutils/mocks/chain_signer.go @@ -46,6 +46,13 @@ func (s *EVMSigner) TryProcessOutbound( ) { } +func (s *EVMSigner) SetGatewayAddress(_ string) { +} + +func (s *EVMSigner) GetGatewayAddress() string { + return "" +} + func (s *EVMSigner) SetZetaConnectorAddress(address ethcommon.Address) { s.ZetaConnectorAddress = address } @@ -86,6 +93,13 @@ func (s *BTCSigner) TryProcessOutbound( ) { } +func (s *BTCSigner) SetGatewayAddress(_ string) { +} + +func (s *BTCSigner) GetGatewayAddress() string { + return "" +} + func (s *BTCSigner) SetZetaConnectorAddress(_ ethcommon.Address) { } @@ -99,3 +113,50 @@ func (s *BTCSigner) GetZetaConnectorAddress() ethcommon.Address { func (s *BTCSigner) GetERC20CustodyAddress() ethcommon.Address { return ethcommon.Address{} } + +// ---------------------------------------------------------------------------- +// SolanaSigner +// ---------------------------------------------------------------------------- +var _ interfaces.ChainSigner = (*SolanaSigner)(nil) + +// SolanaSigner is a mock of solana chain signer for testing +type SolanaSigner struct { + GatewayAddress string +} + +func NewSolanaSigner() *SolanaSigner { + return &SolanaSigner{} +} + +func (s *SolanaSigner) TryProcessOutbound( + _ context.Context, + _ *crosschaintypes.CrossChainTx, + _ *outboundprocessor.Processor, + _ string, + _ interfaces.ChainObserver, + _ interfaces.ZetacoreClient, + _ uint64, +) { +} + +func (s *SolanaSigner) SetGatewayAddress(address string) { + s.GatewayAddress = address +} + +func (s *SolanaSigner) GetGatewayAddress() string { + return s.GatewayAddress +} + +func (s *SolanaSigner) SetZetaConnectorAddress(_ ethcommon.Address) { +} + +func (s *SolanaSigner) SetERC20CustodyAddress(_ ethcommon.Address) { +} + +func (s *SolanaSigner) GetZetaConnectorAddress() ethcommon.Address { + return ethcommon.Address{} +} + +func (s *SolanaSigner) GetERC20CustodyAddress() ethcommon.Address { + return ethcommon.Address{} +} diff --git a/zetaclient/testutils/mocks/solana_rpc.go b/zetaclient/testutils/mocks/solana_rpc.go new file mode 100644 index 0000000000..953bde87e5 --- /dev/null +++ b/zetaclient/testutils/mocks/solana_rpc.go @@ -0,0 +1,328 @@ +// Code generated by mockery v2.38.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + + rpc "github.com/gagliardetto/solana-go/rpc" + + solana "github.com/gagliardetto/solana-go" +) + +// SolanaRPCClient is an autogenerated mock type for the SolanaRPCClient type +type SolanaRPCClient struct { + mock.Mock +} + +// GetAccountInfo provides a mock function with given fields: ctx, account +func (_m *SolanaRPCClient) GetAccountInfo(ctx context.Context, account solana.PublicKey) (*rpc.GetAccountInfoResult, error) { + ret := _m.Called(ctx, account) + + if len(ret) == 0 { + panic("no return value specified for GetAccountInfo") + } + + var r0 *rpc.GetAccountInfoResult + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, solana.PublicKey) (*rpc.GetAccountInfoResult, error)); ok { + return rf(ctx, account) + } + if rf, ok := ret.Get(0).(func(context.Context, solana.PublicKey) *rpc.GetAccountInfoResult); ok { + r0 = rf(ctx, account) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*rpc.GetAccountInfoResult) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, solana.PublicKey) error); ok { + r1 = rf(ctx, account) + } 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) + + if len(ret) == 0 { + panic("no return value specified for GetConfirmedTransactionWithOpts") + } + + var r0 *rpc.TransactionWithMeta + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, solana.Signature, *rpc.GetTransactionOpts) (*rpc.TransactionWithMeta, error)); ok { + return rf(ctx, signature, opts) + } + if rf, ok := ret.Get(0).(func(context.Context, solana.Signature, *rpc.GetTransactionOpts) *rpc.TransactionWithMeta); ok { + r0 = rf(ctx, signature, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*rpc.TransactionWithMeta) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, solana.Signature, *rpc.GetTransactionOpts) error); ok { + r1 = rf(ctx, signature, opts) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetHealth provides a mock function with given fields: ctx +func (_m *SolanaRPCClient) GetHealth(ctx context.Context) (string, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetHealth") + } + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (string, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) string); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetRecentBlockhash provides a mock function with given fields: ctx, commitment +func (_m *SolanaRPCClient) GetRecentBlockhash(ctx context.Context, commitment rpc.CommitmentType) (*rpc.GetRecentBlockhashResult, error) { + ret := _m.Called(ctx, commitment) + + if len(ret) == 0 { + panic("no return value specified for GetRecentBlockhash") + } + + var r0 *rpc.GetRecentBlockhashResult + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, rpc.CommitmentType) (*rpc.GetRecentBlockhashResult, error)); ok { + return rf(ctx, commitment) + } + if rf, ok := ret.Get(0).(func(context.Context, rpc.CommitmentType) *rpc.GetRecentBlockhashResult); ok { + r0 = rf(ctx, commitment) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*rpc.GetRecentBlockhashResult) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, rpc.CommitmentType) error); ok { + r1 = rf(ctx, commitment) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetRecentPrioritizationFees provides a mock function with given fields: ctx, accounts +func (_m *SolanaRPCClient) GetRecentPrioritizationFees(ctx context.Context, accounts solana.PublicKeySlice) ([]rpc.PriorizationFeeResult, error) { + ret := _m.Called(ctx, accounts) + + if len(ret) == 0 { + panic("no return value specified for GetRecentPrioritizationFees") + } + + var r0 []rpc.PriorizationFeeResult + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, solana.PublicKeySlice) ([]rpc.PriorizationFeeResult, error)); ok { + return rf(ctx, accounts) + } + if rf, ok := ret.Get(0).(func(context.Context, solana.PublicKeySlice) []rpc.PriorizationFeeResult); ok { + r0 = rf(ctx, accounts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]rpc.PriorizationFeeResult) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, solana.PublicKeySlice) error); ok { + r1 = rf(ctx, accounts) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetSignaturesForAddressWithOpts provides a mock function with given fields: ctx, account, opts +func (_m *SolanaRPCClient) GetSignaturesForAddressWithOpts(ctx context.Context, account solana.PublicKey, opts *rpc.GetSignaturesForAddressOpts) ([]*rpc.TransactionSignature, error) { + ret := _m.Called(ctx, account, opts) + + if len(ret) == 0 { + panic("no return value specified for GetSignaturesForAddressWithOpts") + } + + var r0 []*rpc.TransactionSignature + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, solana.PublicKey, *rpc.GetSignaturesForAddressOpts) ([]*rpc.TransactionSignature, error)); ok { + return rf(ctx, account, opts) + } + if rf, ok := ret.Get(0).(func(context.Context, solana.PublicKey, *rpc.GetSignaturesForAddressOpts) []*rpc.TransactionSignature); ok { + r0 = rf(ctx, account, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*rpc.TransactionSignature) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, solana.PublicKey, *rpc.GetSignaturesForAddressOpts) error); ok { + r1 = rf(ctx, account, opts) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetSlot provides a mock function with given fields: ctx, commitment +func (_m *SolanaRPCClient) GetSlot(ctx context.Context, commitment rpc.CommitmentType) (uint64, error) { + ret := _m.Called(ctx, commitment) + + if len(ret) == 0 { + panic("no return value specified for GetSlot") + } + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, rpc.CommitmentType) (uint64, error)); ok { + return rf(ctx, commitment) + } + if rf, ok := ret.Get(0).(func(context.Context, rpc.CommitmentType) uint64); ok { + r0 = rf(ctx, commitment) + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func(context.Context, rpc.CommitmentType) error); ok { + r1 = rf(ctx, commitment) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetTransaction provides a mock function with given fields: ctx, txSig, opts +func (_m *SolanaRPCClient) GetTransaction(ctx context.Context, txSig solana.Signature, opts *rpc.GetTransactionOpts) (*rpc.GetTransactionResult, error) { + ret := _m.Called(ctx, txSig, opts) + + if len(ret) == 0 { + panic("no return value specified for GetTransaction") + } + + var r0 *rpc.GetTransactionResult + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, solana.Signature, *rpc.GetTransactionOpts) (*rpc.GetTransactionResult, error)); ok { + return rf(ctx, txSig, opts) + } + if rf, ok := ret.Get(0).(func(context.Context, solana.Signature, *rpc.GetTransactionOpts) *rpc.GetTransactionResult); ok { + r0 = rf(ctx, txSig, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*rpc.GetTransactionResult) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, solana.Signature, *rpc.GetTransactionOpts) error); ok { + r1 = rf(ctx, txSig, opts) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetVersion provides a mock function with given fields: ctx +func (_m *SolanaRPCClient) GetVersion(ctx context.Context) (*rpc.GetVersionResult, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetVersion") + } + + var r0 *rpc.GetVersionResult + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (*rpc.GetVersionResult, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) *rpc.GetVersionResult); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*rpc.GetVersionResult) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SendTransactionWithOpts provides a mock function with given fields: ctx, transaction, opts +func (_m *SolanaRPCClient) SendTransactionWithOpts(ctx context.Context, transaction *solana.Transaction, opts rpc.TransactionOpts) (solana.Signature, error) { + ret := _m.Called(ctx, transaction, opts) + + if len(ret) == 0 { + panic("no return value specified for SendTransactionWithOpts") + } + + var r0 solana.Signature + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *solana.Transaction, rpc.TransactionOpts) (solana.Signature, error)); ok { + return rf(ctx, transaction, opts) + } + if rf, ok := ret.Get(0).(func(context.Context, *solana.Transaction, rpc.TransactionOpts) solana.Signature); ok { + r0 = rf(ctx, transaction, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(solana.Signature) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *solana.Transaction, rpc.TransactionOpts) error); ok { + r1 = rf(ctx, transaction, opts) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewSolanaRPCClient creates a new instance of SolanaRPCClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewSolanaRPCClient(t interface { + mock.TestingT + Cleanup(func()) +}) *SolanaRPCClient { + mock := &SolanaRPCClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/zetaclient/testutils/testdata.go b/zetaclient/testutils/testdata.go index ec97d6d1c4..146a73e243 100644 --- a/zetaclient/testutils/testdata.go +++ b/zetaclient/testutils/testdata.go @@ -309,6 +309,19 @@ func LoadSolanaInboundTxResult( return txResult } +// LoadSolanaOutboundTxResult loads archived Solana outbound tx result from file +func LoadSolanaOutboundTxResult( + t *testing.T, + dir string, + chainID int64, + txHash string, +) *rpc.GetTransactionResult { + name := path.Join(dir, TestDataPathSolana, FileNameSolanaOutbound(chainID, txHash)) + txResult := &rpc.GetTransactionResult{} + LoadObjectFromJSONFile(t, txResult, name) + return txResult +} + //============================================================================== // other helpers methods diff --git a/zetaclient/testutils/testdata_naming.go b/zetaclient/testutils/testdata_naming.go index 940d475780..10a705ccef 100644 --- a/zetaclient/testutils/testdata_naming.go +++ b/zetaclient/testutils/testdata_naming.go @@ -88,3 +88,8 @@ func FileNameSolanaInbound(chainID int64, inboundHash string, donation bool) str } return fmt.Sprintf("chain_%d_inbound_tx_result_donation_%s.json", chainID, inboundHash) } + +// FileNameSolanaOutbound returns archive file name for outbound tx result +func FileNameSolanaOutbound(chainID int64, txHash string) string { + return fmt.Sprintf("chain_%d_outbound_tx_result_%s.json", chainID, txHash) +} diff --git a/zetaclient/zetacore/client_vote.go b/zetaclient/zetacore/client_vote.go index 49eea1f813..4883eb7677 100644 --- a/zetaclient/zetacore/client_vote.go +++ b/zetaclient/zetacore/client_vote.go @@ -48,11 +48,8 @@ func (c *Client) PostVoteGasPrice( chain chains.Chain, gasPrice uint64, priorityFee, blockNum uint64, ) (string, error) { - // apply gas price multiplier for the chain - multiplier, err := GasPriceMultiplier(chain) - if err != nil { - return "", err - } + // get gas price multiplier for the chain + multiplier := GasPriceMultiplier(chain) // #nosec G115 always in range gasPrice = uint64(float64(gasPrice) * multiplier) diff --git a/zetaclient/zetacore/tx.go b/zetaclient/zetacore/tx.go index 90a6b1d136..fb57cd449b 100644 --- a/zetaclient/zetacore/tx.go +++ b/zetaclient/zetacore/tx.go @@ -2,7 +2,6 @@ package zetacore import ( "context" - "fmt" "strings" "cosmossdk.io/errors" @@ -56,13 +55,15 @@ func GetInboundVoteMessage( } // GasPriceMultiplier returns the gas price multiplier for the given chain -func GasPriceMultiplier(chain chains.Chain) (float64, error) { - if chain.IsEVMChain() { - return clientcommon.EVMOutboundGasPriceMultiplier, nil - } else if chain.IsBitcoinChain() { - return clientcommon.BTCOutboundGasPriceMultiplier, nil +func GasPriceMultiplier(chain chains.Chain) float64 { + switch chain.Consensus { + case chains.Consensus_ethereum: + return clientcommon.EVMOutboundGasPriceMultiplier + case chains.Consensus_bitcoin: + return clientcommon.BTCOutboundGasPriceMultiplier + default: + return clientcommon.DefaultGasPriceMultiplier } - return 0, fmt.Errorf("cannot get gas price multiplier for unknown chain %d", chain.ChainId) } // WrapMessageWithAuthz wraps a message with an authz message diff --git a/zetaclient/zetacore/tx_test.go b/zetaclient/zetacore/tx_test.go index 651e39266e..e5100ef3c7 100644 --- a/zetaclient/zetacore/tx_test.go +++ b/zetaclient/zetacore/tx_test.go @@ -46,73 +46,61 @@ func Test_GasPriceMultiplier(t *testing.T) { name string chain chains.Chain multiplier float64 - fail bool }{ { name: "get Ethereum multiplier", chain: chains.Ethereum, multiplier: 1.2, - fail: false, }, { name: "get Goerli multiplier", chain: chains.Goerli, multiplier: 1.2, - fail: false, }, { name: "get BSC multiplier", chain: chains.BscMainnet, multiplier: 1.2, - fail: false, }, { name: "get BSC Testnet multiplier", chain: chains.BscTestnet, multiplier: 1.2, - fail: false, }, { name: "get Polygon multiplier", chain: chains.Polygon, multiplier: 1.2, - fail: false, }, { name: "get Mumbai Testnet multiplier", chain: chains.Mumbai, multiplier: 1.2, - fail: false, }, { name: "get Bitcoin multiplier", chain: chains.BitcoinMainnet, multiplier: 2.0, - fail: false, }, { name: "get Bitcoin Testnet multiplier", chain: chains.BitcoinTestnet, multiplier: 2.0, - fail: false, }, { - name: "get unknown chain gas price multiplier", - chain: chains.Chain{ - Consensus: chains.Consensus_tendermint, - }, + name: "get Solana multiplier", + chain: chains.SolanaMainnet, + multiplier: 1.0, + }, + { + name: "get Solana devnet multiplier", + chain: chains.SolanaDevnet, multiplier: 1.0, - fail: true, }, } for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { - multiplier, err := GasPriceMultiplier(tc.chain) - if tc.fail { - require.Error(t, err) - return - } - require.NoError(t, err) + multiplier := GasPriceMultiplier(tc.chain) require.Equal(t, tc.multiplier, multiplier) }) } From 65ff18f5507989307c8d48433576605ce5898493 Mon Sep 17 00:00:00 2001 From: dev-bitSmiley <153714963+bitSmiley@users.noreply.github.com> Date: Fri, 2 Aug 2024 19:07:08 +0800 Subject: [PATCH 2/6] 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 --- changelog.md | 3 +- zetaclient/chains/bitcoin/observer/inbound.go | 36 --- zetaclient/chains/bitcoin/observer/witness.go | 187 ++++++++++++++ .../chains/bitcoin/observer/witness_test.go | 238 ++++++++++++++++++ ...9550e344bdc14ac38f71fc050096887e535c8.json | 42 ++++ ...2bf7afbf0c78f21e4118b85d00d9e4cce0c2c.json | 32 +++ 6 files changed, 501 insertions(+), 37 deletions(-) create mode 100644 zetaclient/chains/bitcoin/observer/witness.go create mode 100644 zetaclient/chains/bitcoin/observer/witness_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/changelog.md b/changelog.md index 39fff5b0d5..c31c3fe3f4 100644 --- a/changelog.md +++ b/changelog.md @@ -43,8 +43,9 @@ * [2518](https://github.com/zeta-chain/node/pull/2518) - add support for Solana address in zetacore * [2483](https://github.com/zeta-chain/node/pull/2483) - add priorityFee (gasTipCap) gas to the state * [2567](https://github.com/zeta-chain/node/pull/2567) - add sign latency metric to zetaclient (zetaclient_sign_latency) -* [2524](https://github.com/zeta-chain/node/pull/2524) - add inscription envolop parsing +* [2524](https://github.com/zeta-chain/node/pull/2524) - add inscription envelop parsing * [2560](https://github.com/zeta-chain/node/pull/2560) - add support for Solana SOL token withdraw +* [2533](https://github.com/zeta-chain/node/pull/2533) - parse memo from both OP_RETURN and inscription ### Refactor diff --git a/zetaclient/chains/bitcoin/observer/inbound.go b/zetaclient/chains/bitcoin/observer/inbound.go index 470c56d86f..15a3bfdc99 100644 --- a/zetaclient/chains/bitcoin/observer/inbound.go +++ b/zetaclient/chains/bitcoin/observer/inbound.go @@ -477,39 +477,3 @@ func GetBtcEvent( } return nil, nil } - -// 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) { - // first check for OP_RETURN data - event, err := GetBtcEvent( - client, - tx, - tssAddress, - blockNumber, - logger, - netParams, - depositorFee, - ) - - if err != nil { - return nil, errors.Wrap(err, "unable to get btc event") - } - - if event != nil { - return event, nil - } - - // TODO: integrate parsing script - - return nil, nil -} 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/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 From b4251e176c326bdaeba9212d082a9bc5e445869b Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Fri, 2 Aug 2024 15:41:03 +0200 Subject: [PATCH 3/6] 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 --- changelog.md | 1 + cmd/zetaclientd/debug.go | 93 ++- cmd/zetaclientd/start.go | 33 +- 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 +- e2e/e2etests/test_eth_deposit.go | 2 +- 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 +- go.mod | 17 +- go.sum | 34 +- x/observer/types/chain_params.go | 17 +- zetaclient/chains/bitcoin/observer/inbound.go | 21 +- .../chains/bitcoin/observer/observer.go | 40 - .../chains/bitcoin/observer/outbound.go | 2 +- zetaclient/chains/evm/observer/inbound.go | 79 +- .../chains/evm/observer/inbound_test.go | 80 +- zetaclient/chains/evm/observer/observer.go | 42 - .../chains/evm/observer/observer_gas_test.go | 4 +- .../chains/evm/observer/observer_test.go | 47 +- zetaclient/chains/evm/observer/outbound.go | 2 +- .../chains/evm/observer/outbound_test.go | 13 +- zetaclient/chains/evm/signer/outbound_data.go | 27 +- .../chains/evm/signer/outbound_data_test.go | 38 +- zetaclient/chains/evm/signer/signer.go | 61 +- zetaclient/chains/evm/signer/signer_test.go | 42 +- zetaclient/chains/interfaces/interfaces.go | 2 +- zetaclient/chains/solana/observer/inbound.go | 2 +- .../chains/solana/observer/inbound_tracker.go | 2 +- zetaclient/chains/solana/observer/outbound.go | 2 +- zetaclient/config/types.go | 4 + zetaclient/context/app.go | 385 ++++------ zetaclient/context/app_test.go | 726 +++++------------- zetaclient/context/chain.go | 177 +++++ zetaclient/context/chain_test.go | 87 +++ zetaclient/orchestrator/bootstap_test.go | 130 +--- zetaclient/orchestrator/bootstrap.go | 498 +++++------- zetaclient/orchestrator/orchestrator.go | 176 +++-- 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 -- zetaclient/testutils/mocks/chain_params.go | 25 +- zetaclient/testutils/mocks/zetacore_client.go | 16 +- zetaclient/zetacore/client.go | 106 ++- 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 | 24 +- 58 files changed, 1634 insertions(+), 2485 deletions(-) create mode 100644 zetaclient/context/chain.go create mode 100644 zetaclient/context/chain_test.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 diff --git a/changelog.md b/changelog.md index c31c3fe3f4..822cefe360 100644 --- a/changelog.md +++ b/changelog.md @@ -80,6 +80,7 @@ * [2542](https://github.com/zeta-chain/node/pull/2542) - adjust permissions to be more restrictive * [2572](https://github.com/zeta-chain/node/pull/2572) - turn off IBC modules * [2556](https://github.com/zeta-chain/node/pull/2556) - refactor migrator length check to use consensus type +* [2568](https://github.com/zeta-chain/node/pull/2568) - improve AppContext by converging chains, chainParams, enabledChains, and additionalChains into a single zctx.Chain ### Tests diff --git a/cmd/zetaclientd/debug.go b/cmd/zetaclientd/debug.go index d28f5cb898..6fc46f71f9 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, 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/start.go b/cmd/zetaclientd/start.go index bbec0723f4..281043cb27 100644 --- a/cmd/zetaclientd/start.go +++ b/cmd/zetaclientd/start.go @@ -21,7 +21,6 @@ import ( "github.com/spf13/cobra" "github.com/zeta-chain/zetacore/pkg/authz" - "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/pkg/constant" observerTypes "github.com/zeta-chain/zetacore/x/observer/types" "github.com/zeta-chain/zetacore/zetaclient/chains/base" @@ -143,11 +142,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 +213,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 +267,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) 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 77041b00f4..0eee41b744 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/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_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/go.mod b/go.mod index bd46b60e9b..7f952f66a6 100644 --- a/go.mod +++ b/go.mod @@ -68,6 +68,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 ) @@ -326,16 +327,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.6.0 - golang.org/x/sys v0.18.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 9579b0702f..36e3a288c5 100644 --- a/go.sum +++ b/go.sum @@ -1456,6 +1456,8 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 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/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= @@ -1782,8 +1784,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= @@ -1830,8 +1832,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= @@ -1902,8 +1904,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= @@ -1946,8 +1948,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.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= -golang.org/x/sync v0.6.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= @@ -2070,14 +2072,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.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.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= @@ -2089,8 +2091,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= @@ -2166,8 +2168,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/x/observer/types/chain_params.go b/x/observer/types/chain_params.go index b35d7da930..5fb41aab2e 100644 --- a/x/observer/types/chain_params.go +++ b/x/observer/types/chain_params.go @@ -8,7 +8,6 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" ethchains "github.com/ethereum/go-ethereum/common" - "github.com/pkg/errors" "github.com/zeta-chain/zetacore/pkg/chains" solanacontracts "github.com/zeta-chain/zetacore/pkg/contracts/solana" @@ -57,19 +56,8 @@ func ValidateChainParams(params *ChainParams) error { return fmt.Errorf("chain params cannot be nil") } - // TODO: ZetaChain chain params should be completely removed - // Once removed, this check is no longer necessary as all chasin params would need the same checks - // https://github.com/zeta-chain/node/issues/2419 - _, err := chains.ZetaChainFromChainID(params.ChainId) - if err == nil { - // zeta chain skips the rest of the checks for now - return nil - } - - // ignore error from ZetaChainFromChainID if reason is chain is not zeta chain - // return error otherwise - if !errors.Is(err, chains.ErrNotZetaChain) { - return err + if chains.IsZetaChain(params.ChainId, nil) { + return errorsmod.Wrap(sdkerrors.ErrInvalidChainID, "zeta chain cannot have observer chain parameters") } if params.ConfirmationCount == 0 { @@ -165,7 +153,6 @@ func GetDefaultChainParams() ChainParamsList { GetDefaultBtcRegtestChainParams(), GetDefaultSolanaLocalnetChainParams(), GetDefaultGoerliLocalnetChainParams(), - GetDefaultZetaPrivnetChainParams(), }, } } 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/evm/observer/inbound.go b/zetaclient/chains/evm/observer/inbound.go index b8ee361f37..19ad1f14d5 100644 --- a/zetaclient/chains/evm/observer/inbound.go +++ b/zetaclient/chains/evm/observer/inbound.go @@ -57,7 +57,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 @@ -97,7 +97,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 +311,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 +413,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 +507,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 +519,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 +646,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 +663,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 +680,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..231a3ae6c7 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), 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..4959b13b9a 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" @@ -389,43 +387,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_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..95d2ed2140 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, 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..680fb4e3af 100644 --- a/zetaclient/chains/evm/observer/outbound.go +++ b/zetaclient/chains/evm/observer/outbound.go @@ -52,7 +52,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", ob.Chain().ChainId) continue 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/signer/outbound_data.go b/zetaclient/chains/evm/signer/outbound_data.go index 58ade4faf6..ebeef1c5b1 100644 --- a/zetaclient/chains/evm/signer/outbound_data.go +++ b/zetaclient/chains/evm/signer/outbound_data.go @@ -4,11 +4,11 @@ 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" @@ -93,7 +93,7 @@ func (txData *OutboundData) SetupGas( 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())) + return errors.Wrapf(err, "cannot get gas price from chain %s ", chain.String()) } txData.gasPrice = roundUpToNearestGwei(suggested) } else { @@ -138,21 +138,30 @@ func NewOutboundData( return nil, false, err } - 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()) + chainID := txData.toChainID.Int64() + + toChain, err := app.GetChain(chainID) + switch { + case err != nil: + return nil, true, errors.Wrapf(err, "unable to get chain %d from app context", chainID) + case toChain.IsZeta(): + // should not happen + return nil, true, errors.New("destination chain is Zeta") } + rawChain := toChain.RawChain() + // Set up gas limit and gas price - err = txData.SetupGas(cctx, logger, evmRPC, toChain) + err = txData.SetupGas(cctx, logger, evmRPC, *rawChain) if err != nil { - return nil, true, err + return nil, true, errors.Wrap(err, "unable to setup gas") } + nonce := cctx.GetCurrentOutboundParam().TssNonce + // 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) + Msgf("chain %d minting %d to %s, nonce %d, finalized zeta bn %d", toChain.ID(), 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) diff --git a/zetaclient/chains/evm/signer/outbound_data_test.go b/zetaclient/chains/evm/signer/outbound_data_test.go index ac2b7061b5..0d44fd3dbb 100644 --- a/zetaclient/chains/evm/signer/outbound_data_test.go +++ b/zetaclient/chains/evm/signer/outbound_data_test.go @@ -7,9 +7,12 @@ import ( ethcommon "github.com/ethereum/go-ethereum/common" "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + observertypes "github.com/zeta-chain/zetacore/x/observer/types" "github.com/zeta-chain/zetacore/zetaclient/config" zctx "github.com/zeta-chain/zetacore/zetaclient/context" + "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/x/crosschain/types" @@ -72,6 +75,19 @@ func TestSigner_NewOutboundData(t *testing.T) { app := zctx.New(config.New(false), zerolog.Nop()) ctx := zctx.WithAppContext(context.Background(), app) + bscParams := mocks.MockChainParams(chains.BscMainnet.ChainId, 10) + + // Given app context + err := app.Update( + observertypes.Keygen{}, + []chains.Chain{chains.BscMainnet}, + nil, + map[int64]*observertypes.ChainParams{chains.BscMainnet.ChainId: &bscParams}, + "tssPubKey", + observertypes.CrosschainFlags{}, + ) + require.NoError(t, err) + // Setup evm signer evmSigner, err := getNewEvmSigner(nil) require.NoError(t, err) @@ -81,33 +97,35 @@ func TestSigner_NewOutboundData(t *testing.T) { t.Run("NewOutboundData success", func(t *testing.T) { cctx := getCCTX(t) + _, skip, err := NewOutboundData(ctx, cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) - require.False(t, skip) - require.NoError(t, err) + assert.NoError(t, err) + assert.False(t, skip) }) t.Run("NewOutboundData skip", func(t *testing.T) { cctx := getCCTX(t) cctx.CctxStatus.Status = types.CctxStatus_Aborted + _, skip, err := NewOutboundData(ctx, cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) - require.NoError(t, err) - require.True(t, skip) + assert.NoError(t, err) + assert.True(t, skip) }) t.Run("NewOutboundData unknown chain", func(t *testing.T) { 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) + assert.ErrorContains(t, err, "unable to get chain 13378337 from app context: id=13378337: chain not found") + assert.True(t, skip) }) t.Run("NewOutboundData setup gas error", func(t *testing.T) { 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") + assert.True(t, skip) + assert.ErrorContains(t, err, "cannot convert gas price") }) } diff --git a/zetaclient/chains/evm/signer/signer.go b/zetaclient/chains/evm/signer/signer.go index f0f00aa237..9c928face1 100644 --- a/zetaclient/chains/evm/signer/signer.go +++ b/zetaclient/chains/evm/signer/signer.go @@ -362,6 +362,12 @@ func (signer *Signer) TryProcessOutbound( zetacoreClient interfaces.ZetacoreClient, height uint64, ) { + app, err := zctx.FromContext(ctx) + if err != nil { + signer.Logger().Std.Error().Err(err).Msg("error getting app context") + return + } + // end outbound process on panic defer func() { outboundProc.EndTryProcess(outboundID) @@ -399,6 +405,17 @@ func (signer *Signer) TryProcessOutbound( 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,7 +464,7 @@ 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, ) @@ -456,7 +473,7 @@ func (signer *Signer) TryProcessOutbound( logger.Info().Msgf( "SignERC20WithdrawTx: %d => %d, nonce %d, gasPrice %d", cctx.InboundParams.SenderChainId, - txData.toChainID.Int64(), + toChain.ID(), cctx.GetCurrentOutboundParam().TssNonce, txData.gasPrice, ) @@ -465,7 +482,7 @@ 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, ) @@ -481,7 +498,7 @@ func (signer *Signer) TryProcessOutbound( logger.Info().Msgf( "SignRevertTx: %d => %d, nonce %d, gasPrice %d", cctx.InboundParams.SenderChainId, - txData.toChainID.Int64(), cctx.GetCurrentOutboundParam().TssNonce, + toChain.ID(), cctx.GetCurrentOutboundParam().TssNonce, txData.gasPrice, ) txData.srcChainID = big.NewInt(cctx.OutboundParams[0].ReceiverChainId) @@ -491,7 +508,7 @@ 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, ) @@ -499,7 +516,7 @@ func (signer *Signer) TryProcessOutbound( 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, ) @@ -513,7 +530,7 @@ 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, ) @@ -529,7 +546,7 @@ 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, ) @@ -543,7 +560,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 +584,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 } @@ -591,15 +610,15 @@ func (signer *Signer) BroadcastOutbound( 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 +627,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 } } diff --git a/zetaclient/chains/evm/signer/signer_test.go b/zetaclient/chains/evm/signer/signer_test.go index b0cf3e5504..95da02e648 100644 --- a/zetaclient/chains/evm/signer/signer_test.go +++ b/zetaclient/chains/evm/signer/signer_test.go @@ -11,6 +11,7 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/rs/zerolog" "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 +162,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 +185,7 @@ func TestSigner_TryProcessOutbound(t *testing.T) { } func TestSigner_SignOutbound(t *testing.T) { - ctx := makeCtx() + ctx := makeCtx(t) // Setup evm signer tss := mocks.NewTSSMainnet() @@ -221,7 +222,7 @@ func TestSigner_SignOutbound(t *testing.T) { } func TestSigner_SignRevertTx(t *testing.T) { - ctx := makeCtx() + ctx := makeCtx(t) // Setup evm signer tss := mocks.NewTSSMainnet() @@ -261,7 +262,7 @@ func TestSigner_SignRevertTx(t *testing.T) { } func TestSigner_SignCancelTx(t *testing.T) { - ctx := makeCtx() + ctx := makeCtx(t) // Setup evm signer tss := mocks.NewTSSMainnet() @@ -301,7 +302,7 @@ func TestSigner_SignCancelTx(t *testing.T) { } func TestSigner_SignWithdrawTx(t *testing.T) { - ctx := makeCtx() + ctx := makeCtx(t) // Setup evm signer tss := mocks.NewTSSMainnet() @@ -340,7 +341,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) @@ -386,7 +387,7 @@ func TestSigner_SignCommandTx(t *testing.T) { } func TestSigner_SignERC20WithdrawTx(t *testing.T) { - ctx := makeCtx() + ctx := makeCtx(t) // Setup evm signer tss := mocks.NewTSSMainnet() @@ -427,7 +428,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) @@ -437,9 +438,10 @@ func TestSigner_BroadcastOutbound(t *testing.T) { cctx := getCCTX(t) mockObserver, err := getNewEvmChainObserver(t, nil) require.NoError(t, err) + txData, skip, err := NewOutboundData(ctx, cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) - require.False(t, skip) require.NoError(t, err) + require.False(t, skip) t.Run("BroadcastOutbound - should successfully broadcast", func(t *testing.T) { // Call SignERC20WithdrawTx @@ -481,7 +483,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 +492,13 @@ func TestSigner_SignWhitelistERC20Cmd(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) - require.False(t, skip) require.NoError(t, err) + require.False(t, skip) t.Run("SignWhitelistERC20Cmd - should successfully sign", func(t *testing.T) { // Call SignWhitelistERC20Cmd @@ -526,7 +530,7 @@ func TestSigner_SignWhitelistERC20Cmd(t *testing.T) { } func TestSigner_SignMigrateTssFundsCmd(t *testing.T) { - ctx := makeCtx() + ctx := makeCtx(t) // Setup evm signer tss := mocks.NewTSSMainnet() @@ -565,8 +569,20 @@ func TestSigner_SignMigrateTssFundsCmd(t *testing.T) { require.Nil(t, tx) }) } -func makeCtx() context.Context { +func makeCtx(t *testing.T) context.Context { app := zctx.New(config.New(false), zerolog.Nop()) + bscParams := mocks.MockChainParams(chains.BscMainnet.ChainId, 10) + + err := app.Update( + observertypes.Keygen{}, + []chains.Chain{chains.BscMainnet}, + 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) } diff --git a/zetaclient/chains/interfaces/interfaces.go b/zetaclient/chains/interfaces/interfaces.go index 4ca93b7259..8377d2ba33 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) 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..6eca0b437e 100644 --- a/zetaclient/chains/solana/observer/outbound.go +++ b/zetaclient/chains/solana/observer/outbound.go @@ -47,7 +47,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/config/types.go b/zetaclient/config/types.go index 1ec58e12aa..31298b00a3 100644 --- a/zetaclient/config/types.go +++ b/zetaclient/config/types.go @@ -194,3 +194,7 @@ func (c Config) LoadSolanaPrivateKey() (solana.PrivateKey, error) { 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..032d0b759c 100644 --- a/zetaclient/context/app.go +++ b/zetaclient/context/app.go @@ -3,37 +3,30 @@ 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 config.Config 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 *ChainRegistry - // 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 - - // 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 + currentTssPubKey string + crosschainFlags observertypes.CrosschainFlags + keygen observertypes.Keygen mu sync.RWMutex } @@ -44,16 +37,13 @@ func New(cfg config.Config, logger zerolog.Logger) *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(), - currentTssPubkey: "", + crosschainFlags: observertypes.CrosschainFlags{}, + currentTssPubKey: "", keygen: observertypes.Keygen{}, - mu: sync.RWMutex{}, + + mu: sync.RWMutex{}, } } @@ -62,43 +52,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 +98,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..0dd8e2daed 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, 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..b3ec993766 --- /dev/null +++ b/zetaclient/context/chain.go @@ -0,0 +1,177 @@ +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 + + 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() *ChainRegistry { + return &ChainRegistry{ + chains: make(map[int64]Chain), + additionalChains: []chains.Chain{}, + 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) 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) +} + +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..a679ed020a --- /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() + + // 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/orchestrator/bootstap_test.go b/zetaclient/orchestrator/bootstap_test.go index 555c830df5..55b6f47614 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" @@ -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) @@ -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..cd4d2a223c 100644 --- a/zetaclient/orchestrator/bootstrap.go +++ b/zetaclient/orchestrator/bootstrap.go @@ -8,7 +8,6 @@ import ( 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" @@ -84,20 +83,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 @@ -105,119 +98,93 @@ func syncSignerMap( continue } - var ( - mpiAddress = ethcommon.HexToAddress(evmChainParams.ConnectorContractAddress) - erc20CustodyAddress = ethcommon.HexToAddress(evmChainParams.Erc20CustodyContractAddress) - ) - - 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) - } + rawChain := chain.RawChain() - // 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 + 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 + } + + // 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") + } + + var ( + chainRaw = chain.RawChain() + paramsRaw = chain.Params() + ) + + // create Solana signer + signer, err := solanasigner.NewSigner(*chainRaw, *paramsRaw, rpcClient, tss, solanaKey, 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") } - - 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 - } - - // 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 +251,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 +265,128 @@ 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 + } + + // create EVM client + evmClient, err := ethclient.DialContext(ctx, cfg.Endpoint) + if err != nil { + logger.Std.Error().Err(err).Str("rpc.endpoint", cfg.Endpoint).Msgf("Unable to dial EVM RPC") + continue + } + + 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..8bff065099 100644 --- a/zetaclient/orchestrator/orchestrator.go +++ b/zetaclient/orchestrator/orchestrator.go @@ -12,9 +12,9 @@ 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" 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" @@ -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(fmt.Sprintf("chain_%d", chainID)). + 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 } } diff --git a/zetaclient/orchestrator/orchestrator_test.go b/zetaclient/orchestrator/orchestrator_test.go index af4ca5c346..3594accc2a 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, 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/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/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 8789dce26b..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,87 +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") } - blockHeaderEnabledChains, err := c.GetBlockHeaderEnabledChains(ctx) - if err != nil { - c.logger.Info().Msg("Unable to fetch block header enabled chains from zetacore") - return err + 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 + } + + 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 e5100ef3c7..a01c11be2b 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,21 +330,6 @@ func TestZetacore_UpdateAppContext(t *testing.T) { GasPriceIncreaseFlags: nil, }}) - 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,7 +358,7 @@ 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{}) + err := client.UpdateAppContext(ctx, appContext, zerolog.New(zerolog.NewTestWriter(t))) require.NoError(t, err) }) } From 3b0431eba17657d3e13fae4078b00d6657539776 Mon Sep 17 00:00:00 2001 From: Lucas Bertrand Date: Mon, 5 Aug 2024 14:37:44 +0200 Subject: [PATCH 4/6] fix(`crosschain`): set sender for ERC20 whitelist admin CCTX inbound (#2631) * fix whitelist sender * add E2E test --- cmd/zetae2e/local/local.go | 1 + e2e/e2etests/e2etests.go | 7 ++ e2e/e2etests/test_whitelist_erc20.go | 107 ++++++++++++++++++ .../keeper/msg_server_whitelist_erc20.go | 2 +- 4 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 e2e/e2etests/test_whitelist_erc20.go diff --git a/cmd/zetae2e/local/local.go b/cmd/zetae2e/local/local.go index 0eee41b744..8b93f2da9c 100644 --- a/cmd/zetae2e/local/local.go +++ b/cmd/zetae2e/local/local.go @@ -292,6 +292,7 @@ func localE2ETest(cmd *cobra.Command, _ []string) { if testAdmin { eg.Go(adminTestRoutine(conf, deployerRunner, verbose, + e2etests.TestWhitelistERC20Name, e2etests.TestRateLimiterName, e2etests.TestPauseZRC20Name, e2etests.TestUpdateBytecodeZRC20Name, diff --git a/e2e/e2etests/e2etests.go b/e2e/e2etests/e2etests.go index fc0f0988dc..402eed367e 100644 --- a/e2e/e2etests/e2etests.go +++ b/e2e/e2etests/e2etests.go @@ -100,6 +100,7 @@ const ( Admin tests Test admin functionalities */ + TestWhitelistERC20Name = "whitelist_erc20" TestDepositEtherLiquidityCapName = "deposit_eth_liquidity_cap" TestMigrateChainSupportName = "migrate_chain_support" TestPauseZRC20Name = "pause_zrc20" @@ -521,6 +522,12 @@ var AllE2ETests = []runner.E2ETest{ /* Admin tests */ + runner.NewE2ETest( + TestWhitelistERC20Name, + "whitelist a new ERC20 token", + []runner.ArgDefinition{}, + TestWhitelistERC20, + ), runner.NewE2ETest( TestDepositEtherLiquidityCapName, "deposit Ethers into ZEVM with a liquidity cap", diff --git a/e2e/e2etests/test_whitelist_erc20.go b/e2e/e2etests/test_whitelist_erc20.go new file mode 100644 index 0000000000..5a09decd71 --- /dev/null +++ b/e2e/e2etests/test_whitelist_erc20.go @@ -0,0 +1,107 @@ +package e2etests + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + ethcommon "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/stretchr/testify/require" + "github.com/zeta-chain/protocol-contracts/pkg/contracts/zevm/zrc20.sol" + + "github.com/zeta-chain/zetacore/e2e/contracts/erc20" + "github.com/zeta-chain/zetacore/e2e/runner" + "github.com/zeta-chain/zetacore/e2e/txserver" + "github.com/zeta-chain/zetacore/e2e/utils" + "github.com/zeta-chain/zetacore/pkg/chains" + crosschaintypes "github.com/zeta-chain/zetacore/x/crosschain/types" +) + +// TestWhitelistERC20 tests the whitelist ERC20 functionality +func TestWhitelistERC20(r *runner.E2ERunner, _ []string) { + // Deploy a new ERC20 on the new EVM chain + r.Logger.Info("Deploying new ERC20 contract") + erc20Addr, txERC20, _, err := erc20.DeployERC20(r.EVMAuth, r.EVMClient, "NEWERC20", "NEWERC20", 6) + require.NoError(r, err) + + // wait for the ERC20 to be mined + receipt := utils.MustWaitForTxReceipt(r.Ctx, r.EVMClient, txERC20, r.Logger, r.ReceiptTimeout) + require.Equal(r, ethtypes.ReceiptStatusSuccessful, receipt.Status) + + // ERC20 test + + // whitelist erc20 zrc20 + r.Logger.Info("whitelisting ERC20 on new network") + res, err := r.ZetaTxServer.BroadcastTx(utils.AdminPolicyName, crosschaintypes.NewMsgWhitelistERC20( + r.ZetaTxServer.MustGetAccountAddressFromName(utils.AdminPolicyName), + erc20Addr.Hex(), + chains.GoerliLocalnet.ChainId, + "NEWERC20", + "NEWERC20", + 6, + 100000, + )) + require.NoError(r, err) + + // retrieve zrc20 and cctx from event + whitelistCCTXIndex, err := txserver.FetchAttributeFromTxResponse(res, "whitelist_cctx_index") + require.NoError(r, err) + + erc20zrc20Addr, err := txserver.FetchAttributeFromTxResponse(res, "zrc20_address") + require.NoError(r, err) + + // ensure CCTX created + resCCTX, err := r.CctxClient.Cctx(r.Ctx, &crosschaintypes.QueryGetCctxRequest{Index: whitelistCCTXIndex}) + require.NoError(r, err) + + cctx := resCCTX.CrossChainTx + r.Logger.CCTX(*cctx, "whitelist_cctx") + + // wait for the whitelist cctx to be mined + r.WaitForMinedCCTXFromIndex(whitelistCCTXIndex) + + // save old ERC20 attribute to set it back after the test + oldERC20Addr := r.ERC20Addr + oldERC20 := r.ERC20 + oldERC20ZRC20Addr := r.ERC20ZRC20Addr + oldERC20ZRC20 := r.ERC20ZRC20 + defer func() { + r.ERC20Addr = oldERC20Addr + r.ERC20 = oldERC20 + r.ERC20ZRC20Addr = oldERC20ZRC20Addr + r.ERC20ZRC20 = oldERC20ZRC20 + }() + + // set erc20 and zrc20 in runner + require.True(r, ethcommon.IsHexAddress(erc20zrc20Addr), "invalid contract address: %s", erc20zrc20Addr) + erc20zrc20AddrHex := ethcommon.HexToAddress(erc20zrc20Addr) + erc20ZRC20, err := zrc20.NewZRC20(erc20zrc20AddrHex, r.ZEVMClient) + require.NoError(r, err) + r.ERC20ZRC20Addr = erc20zrc20AddrHex + r.ERC20ZRC20 = erc20ZRC20 + + erc20ERC20, err := erc20.NewERC20(erc20Addr, r.EVMClient) + require.NoError(r, err) + r.ERC20Addr = erc20Addr + r.ERC20 = erc20ERC20 + + // get balance + balance, err := r.ERC20.BalanceOf(&bind.CallOpts{}, r.Account.EVMAddress()) + require.NoError(r, err) + r.Logger.Info("ERC20 balance: %s", balance.String()) + + // run deposit and withdraw ERC20 test + txHash := r.DepositERC20WithAmountAndMessage(r.EVMAddress(), balance, []byte{}) + r.WaitForMinedCCTX(txHash) + + // approve 1 unit of the gas token to cover the gas fee + tx, err := r.ETHZRC20.Approve(r.ZEVMAuth, r.ERC20ZRC20Addr, big.NewInt(1e18)) + require.NoError(r, err) + + receipt = utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + utils.RequireTxSuccessful(r, receipt) + r.Logger.Info("eth zrc20 approve receipt: status %d", receipt.Status) + + tx = r.WithdrawERC20(balance) + r.WaitForMinedCCTX(tx.Hash()) +} diff --git a/x/crosschain/keeper/msg_server_whitelist_erc20.go b/x/crosschain/keeper/msg_server_whitelist_erc20.go index 7c9fe2363d..8b6ad08b26 100644 --- a/x/crosschain/keeper/msg_server_whitelist_erc20.go +++ b/x/crosschain/keeper/msg_server_whitelist_erc20.go @@ -149,7 +149,7 @@ func (k msgServer) WhitelistERC20( LastUpdateTimestamp: 0, }, InboundParams: &types.InboundParams{ - Sender: "", + Sender: msg.Creator, SenderChainId: 0, TxOrigin: "", CoinType: coin.CoinType_Cmd, From 5dd6fd66354a6b43d036b8c3c07f5580b0c6a9ae Mon Sep 17 00:00:00 2001 From: Christopher Fuka <97121270+CryptoFewka@users.noreply.github.com> Date: Mon, 5 Aug 2024 19:27:58 -0500 Subject: [PATCH 5/6] fix(ci): Update golang cross compile to 1.22.4 (#2635) * Update golang cross compile to 1.22.4 * update deprecated --skip-validate --skip-release flags --- Makefile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 5788975d79..0e86d8be76 100644 --- a/Makefile +++ b/Makefile @@ -322,7 +322,7 @@ start-upgrade-import-mainnet-test: zetanode-upgrade ############################################################################### PACKAGE_NAME := github.com/zeta-chain/node -GOLANG_CROSS_VERSION ?= v1.20.7 +GOLANG_CROSS_VERSION ?= v1.22.4 GOPATH ?= '$(HOME)/go' release-dry-run: docker run \ @@ -334,7 +334,7 @@ release-dry-run: -v ${GOPATH}/pkg:/go/pkg \ -w /go/src/$(PACKAGE_NAME) \ ghcr.io/goreleaser/goreleaser-cross:${GOLANG_CROSS_VERSION} \ - --clean --skip-validate --skip-publish --snapshot + --clean --skip=validate --skip=publish --snapshot release: @if [ ! -f ".release-env" ]; then \ @@ -350,7 +350,7 @@ release: -v `pwd`:/go/src/$(PACKAGE_NAME) \ -w /go/src/$(PACKAGE_NAME) \ ghcr.io/goreleaser/goreleaser-cross:${GOLANG_CROSS_VERSION} \ - release --clean --skip-validate + release --clean --skip=validate ############################################################################### ### Local Mainnet Development ### @@ -430,4 +430,4 @@ filter-missed-eth: install-zetatool zetatool filterdeposit eth \ --config ./tool/filter_missed_deposits/zetatool_config.json \ --evm-max-range 1000 \ - --evm-start-block 19464041 \ No newline at end of file + --evm-start-block 19464041 From eb61f2da703c21034d21b1bdbcc076cff67b74cf Mon Sep 17 00:00:00 2001 From: Charlie <31941002+CharlieMc0@users.noreply.github.com> Date: Tue, 6 Aug 2024 02:36:24 -0500 Subject: [PATCH 6/6] Added goreleaser check (#2636) --- .github/workflows/publish-release.yml | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index ff454a4032..3e848ff66c 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -23,7 +23,7 @@ concurrency: cancel-in-progress: false jobs: - check_branch: + check-branch: if: ${{ (startsWith(github.ref, 'refs/heads/release/v') || startsWith(github.ref, 'refs/heads/hotfix/v')) }} runs-on: ubuntu-22.04 steps: @@ -31,9 +31,18 @@ jobs: run: | echo "${{ github.ref }}" + check-goreleaser: + needs: + - check-branch + runs-on: ubuntu-22.04 + steps: + - name: Branch + run: | + make release-dry-run + check-changelog: needs: - - check_branch + - check-branch runs-on: ubuntu-22.04 steps: @@ -75,7 +84,7 @@ jobs: check-upgrade-handler-updated: needs: - - check_branch + - check-branch runs-on: ubuntu-22.04 timeout-minutes: 10 steps: @@ -114,7 +123,7 @@ jobs: needs: - check-changelog - check-upgrade-handler-updated - - check_branch + - check-branch runs-on: ubuntu-22.04 timeout-minutes: 60 environment: release