diff --git a/changelog.md b/changelog.md index 6c85377b7b..61e78b56e6 100644 --- a/changelog.md +++ b/changelog.md @@ -62,6 +62,7 @@ * [1766](https://github.com/zeta-chain/node/pull/1766) - Refactors the `PostTxProcessing` EVM hook functionality to deal with invalid withdraw events * [1630](https://github.com/zeta-chain/node/pull/1630) - added password prompts for hotkey and tss keyshare in zetaclient * [1760](https://github.com/zeta-chain/node/pull/1760) - Make staking keeper private in crosschain module +* [1809](https://github.com/zeta-chain/node/pull/1809) - Refactored tryprocessout function in evm signer ### Fixes diff --git a/zetaclient/bitcoin/bitcoin_client_test.go b/zetaclient/bitcoin/bitcoin_client_test.go index b9a29db816..63aec7f099 100644 --- a/zetaclient/bitcoin/bitcoin_client_test.go +++ b/zetaclient/bitcoin/bitcoin_client_test.go @@ -19,13 +19,14 @@ import ( crosschaintypes "github.com/zeta-chain/zetacore/x/crosschain/types" observertypes "github.com/zeta-chain/zetacore/x/observer/types" "github.com/zeta-chain/zetacore/zetaclient/testutils" + "github.com/zeta-chain/zetacore/zetaclient/testutils/stub" ) func MockBTCClientMainnet() *BTCChainClient { return &BTCChainClient{ chain: common.BtcMainnetChain(), - zetaClient: testutils.MockCoreBridge(), - Tss: testutils.NewMockTSSMainnet(), + zetaClient: stub.NewZetaCoreBridge(), + Tss: stub.NewTSSMainnet(), } } diff --git a/zetaclient/config/types.go b/zetaclient/config/types.go index a85f9f269e..376c06dc54 100644 --- a/zetaclient/config/types.go +++ b/zetaclient/config/types.go @@ -82,7 +82,8 @@ type Config struct { func NewConfig() *Config { return &Config{ - cfgLock: &sync.RWMutex{}, + cfgLock: &sync.RWMutex{}, + EVMChainConfigs: make(map[int64]*EVMConfig), } } diff --git a/zetaclient/evm/evm_signer.go b/zetaclient/evm/evm_signer.go index ae632df241..c42e04b206 100644 --- a/zetaclient/evm/evm_signer.go +++ b/zetaclient/evm/evm_signer.go @@ -2,7 +2,6 @@ package evm import ( "context" - "encoding/base64" "encoding/hex" "fmt" "math/big" @@ -12,11 +11,7 @@ import ( "sync" "time" - clientcommon "github.com/zeta-chain/zetacore/zetaclient/common" - "github.com/zeta-chain/zetacore/zetaclient/interfaces" - "github.com/zeta-chain/zetacore/zetaclient/metrics" - "github.com/zeta-chain/zetacore/zetaclient/outtxprocessor" - + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/ethereum/go-ethereum/accounts/abi" ethcommon "github.com/ethereum/go-ethereum/common" ethtypes "github.com/ethereum/go-ethereum/core/types" @@ -29,6 +24,11 @@ import ( 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" + clientcommon "github.com/zeta-chain/zetacore/zetaclient/common" + "github.com/zeta-chain/zetacore/zetaclient/interfaces" + "github.com/zeta-chain/zetacore/zetaclient/metrics" + "github.com/zeta-chain/zetacore/zetaclient/outtxprocessor" + "github.com/zeta-chain/zetacore/zetaclient/testutils/stub" zbridge "github.com/zeta-chain/zetacore/zetaclient/zetabridge" ) @@ -69,16 +69,10 @@ func NewEVMSigner( loggers clientcommon.ClientLogger, ts *metrics.TelemetryServer, ) (*Signer, error) { - client, err := ethclient.Dial(endpoint) + client, chainID, ethSigner, err := getEVMRPC(endpoint) if err != nil { return nil, err } - - chainID, err := client.ChainID(context.TODO()) - if err != nil { - return nil, err - } - ethSigner := ethtypes.LatestSignerForChainID(chainID) connectorABI, err := abi.JSON(strings.NewReader(abiString)) if err != nil { return nil, err @@ -158,31 +152,29 @@ func (signer *Signer) Broadcast(tx *ethtypes.Transaction) error { // bytes32 internalSendHash // // ) external virtual {} -func (signer *Signer) SignOutboundTx(sender ethcommon.Address, - srcChainID *big.Int, - to ethcommon.Address, - amount *big.Int, - gasLimit uint64, - message []byte, - sendHash [32]byte, - nonce uint64, - gasPrice *big.Int, - height uint64) (*ethtypes.Transaction, error) { - - if len(sendHash) < 32 { - return nil, fmt.Errorf("sendHash len %d must be 32", len(sendHash)) - } +func (signer *Signer) SignOutboundTx(txData *OutBoundTransactionData) (*ethtypes.Transaction, error) { var data []byte var err error - data, err = signer.abi.Pack("onReceive", sender.Bytes(), srcChainID, to, amount, message, sendHash) + data, err = signer.abi.Pack("onReceive", + txData.sender.Bytes(), + txData.srcChainID, + txData.to, + txData.amount, + txData.message, + txData.sendHash) if err != nil { - return nil, fmt.Errorf("pack error: %w", err) + return nil, fmt.Errorf("onReceive pack error: %w", err) } - tx, _, _, err := signer.Sign(data, signer.metaContractAddress, gasLimit, gasPrice, nonce, height) + tx, _, _, err := signer.Sign(data, + signer.metaContractAddress, + txData.gasLimit, + txData.gasPrice, + txData.nonce, + txData.height) if err != nil { - return nil, fmt.Errorf("Sign error: %w", err) + return nil, fmt.Errorf("onReceive sign error: %w", err) } return tx, nil @@ -198,28 +190,28 @@ func (signer *Signer) SignOutboundTx(sender ethcommon.Address, // bytes calldata message, // bytes32 internalSendHash // ) external override whenNotPaused onlyTssAddress -func (signer *Signer) SignRevertTx( - sender ethcommon.Address, - srcChainID *big.Int, - to []byte, - toChainID *big.Int, - amount *big.Int, - gasLimit uint64, - message []byte, - sendHash [32]byte, - nonce uint64, - gasPrice *big.Int, - height uint64, -) (*ethtypes.Transaction, error) { +func (signer *Signer) SignRevertTx(txData *OutBoundTransactionData) (*ethtypes.Transaction, error) { var data []byte var err error - data, err = signer.abi.Pack("onRevert", sender, srcChainID, to, toChainID, amount, message, sendHash) + data, err = signer.abi.Pack("onRevert", + txData.sender, + txData.srcChainID, + txData.to.Bytes(), + txData.toChainID, + txData.amount, + txData.message, + txData.sendHash) if err != nil { return nil, fmt.Errorf("pack error: %w", err) } - tx, _, _, err := signer.Sign(data, signer.metaContractAddress, gasLimit, gasPrice, nonce, height) + tx, _, _, err := signer.Sign(data, + signer.metaContractAddress, + txData.gasLimit, + txData.gasPrice, + txData.nonce, + txData.height) if err != nil { return nil, fmt.Errorf("Sign error: %w", err) } @@ -227,6 +219,7 @@ func (signer *Signer) SignRevertTx( return tx, nil } +// SignCancelTx signs a transaction from TSS address to itself with a zero amount in order to increment the nonce func (signer *Signer) SignCancelTx(nonce uint64, gasPrice *big.Int, height uint64) (*ethtypes.Transaction, error) { tx := ethtypes.NewTransaction(nonce, signer.tssSigner.EVMAddress(), big.NewInt(0), 21000, gasPrice, nil) hashBytes := signer.ethSigner.Hash(tx).Bytes() @@ -248,16 +241,11 @@ func (signer *Signer) SignCancelTx(nonce uint64, gasPrice *big.Int, height uint6 return signedTX, nil } -func (signer *Signer) SignWithdrawTx( - to ethcommon.Address, - amount *big.Int, - nonce uint64, - gasPrice *big.Int, - height uint64, -) (*ethtypes.Transaction, error) { - tx := ethtypes.NewTransaction(nonce, to, amount, 21000, gasPrice, nil) +// SignWithdrawTx signs a withdrawal transaction sent from the TSS address to the destination +func (signer *Signer) SignWithdrawTx(txData *OutBoundTransactionData) (*ethtypes.Transaction, error) { + tx := ethtypes.NewTransaction(txData.nonce, txData.to, txData.amount, 21000, txData.gasPrice, nil) hashBytes := signer.ethSigner.Hash(tx).Bytes() - sig, err := signer.tssSigner.Sign(hashBytes, height, nonce, signer.chain, "") + sig, err := signer.tssSigner.Sign(hashBytes, txData.height, txData.nonce, signer.chain, "") if err != nil { return nil, err } @@ -275,58 +263,23 @@ func (signer *Signer) SignWithdrawTx( return signedTX, nil } -func (signer *Signer) SignCommandTx( - cmd string, - params string, - to ethcommon.Address, - outboundParams *types.OutboundTxParams, - gasLimit uint64, - gasPrice *big.Int, - height uint64, -) (*ethtypes.Transaction, error) { - if cmd == common.CmdWhitelistERC20 { - erc20 := ethcommon.HexToAddress(params) - if erc20 == (ethcommon.Address{}) { - return nil, fmt.Errorf("SignCommandTx: invalid erc20 address %s", params) - } - custodyAbi, err := erc20custody.ERC20CustodyMetaData.GetAbi() - if err != nil { - return nil, err - } - data, err := custodyAbi.Pack("whitelist", erc20) - if err != nil { - return nil, err - } - tx, _, _, err := signer.Sign(data, to, gasLimit, gasPrice, outboundParams.OutboundTxTssNonce, height) - if err != nil { - return nil, fmt.Errorf("sign error: %w", err) - } - return tx, nil - } - if cmd == common.CmdMigrateTssFunds { - tx := ethtypes.NewTransaction(outboundParams.OutboundTxTssNonce, to, outboundParams.Amount.BigInt(), outboundParams.OutboundTxGasLimit, gasPrice, nil) - hashBytes := signer.ethSigner.Hash(tx).Bytes() - sig, err := signer.tssSigner.Sign(hashBytes, height, outboundParams.OutboundTxTssNonce, signer.chain, "") - if err != nil { - return nil, err - } - pubk, err := crypto.SigToPub(hashBytes, sig[:]) - if err != nil { - signer.logger.Std.Error().Err(err).Msgf("SigToPub error") - } - addr := crypto.PubkeyToAddress(*pubk) - signer.logger.Std.Info().Msgf("Sign: Ecrecovery of signature: %s", addr.Hex()) - signedTX, err := tx.WithSignature(signer.ethSigner, sig[:]) - if err != nil { - return nil, err - } - - return signedTX, nil +// SignCommandTx signs a transaction based on the given command includes: +// +// cmd_whitelist_erc20 +// cmd_migrate_tss_funds +func (signer *Signer) SignCommandTx(txData *OutBoundTransactionData, cmd string, params string) (*ethtypes.Transaction, error) { + switch cmd { + case common.CmdWhitelistERC20: + return signer.SignWhitelistERC20Cmd(txData, params) + case common.CmdMigrateTssFunds: + return signer.SignMigrateTssFundsCmd(txData) } - return nil, fmt.Errorf("SignCommandTx: unknown command %s", cmd) } +// TryProcessOutTx - signer interface implementation +// This function will attempt to build and sign an evm transaction using the TSS signer. +// It will then broadcast the signed transaction to the outbound chain. func (signer *Signer) TryProcessOutTx( cctx *types.CrossChainTx, outTxMan *outtxprocessor.Processor, @@ -347,236 +300,130 @@ func (signer *Signer) TryProcessOutTx( }() myID := zetaBridge.GetKeys().GetOperatorAddress() - var to ethcommon.Address - var toChain *common.Chain - if cctx.CctxStatus.Status == types.CctxStatus_PendingRevert { - to = ethcommon.HexToAddress(cctx.InboundTxParams.Sender) - toChain = common.GetChainFromChainID(cctx.InboundTxParams.SenderChainId) - if toChain == nil { - logger.Error().Msgf("Unknown chain: %d", cctx.InboundTxParams.SenderChainId) - return - } - logger.Info().Msgf("Abort: reverting inbound") - } else if cctx.CctxStatus.Status == types.CctxStatus_PendingOutbound { - to = ethcommon.HexToAddress(cctx.GetCurrentOutTxParam().Receiver) - toChain = common.GetChainFromChainID(cctx.GetCurrentOutTxParam().ReceiverChainId) - if toChain == nil { - logger.Error().Msgf("Unknown chain: %d", cctx.GetCurrentOutTxParam().ReceiverChainId) - return - } - } else { - logger.Info().Msgf("Transaction doesn't need to be processed status: %d", cctx.CctxStatus.Status) - return - } evmClient, ok := chainclient.(*ChainClient) if !ok { - logger.Error().Msgf("chain client is not an EVMChainClient") + logger.Error().Msg("chain client is not an EVMChainClient") return } - // Early return if the cctx is already processed - nonce := cctx.GetCurrentOutTxParam().OutboundTxTssNonce - included, confirmed, err := evmClient.IsSendOutTxProcessed(cctx, logger) + // Setup Transaction input + txData, skipTx, err := NewOutBoundTransactionData(cctx, evmClient, signer.client, logger, height) if err != nil { - logger.Error().Err(err).Msg("IsSendOutTxProcessed failed") - } - if included || confirmed { - logger.Info().Msgf("CCTX already processed; exit signer") + logger.Err(err).Msg("error setting up transaction input fields") return } - - var message []byte - if cctx.GetCurrentOutTxParam().CoinType != common.CoinType_Cmd { - message, err = base64.StdEncoding.DecodeString(cctx.RelayedMessage) - if err != nil { - logger.Err(err).Msgf("decode CCTX.Message %s error", cctx.RelayedMessage) - } - } - - gasLimit := cctx.GetCurrentOutTxParam().OutboundTxGasLimit - if gasLimit < 100_000 { - gasLimit = 100_000 - logger.Warn().Msgf("gasLimit %d is too low; set to %d", cctx.GetCurrentOutTxParam().OutboundTxGasLimit, gasLimit) - } - if gasLimit > 1_000_000 { - gasLimit = 1_000_000 - logger.Warn().Msgf("gasLimit %d is too high; set to %d", cctx.GetCurrentOutTxParam().OutboundTxGasLimit, gasLimit) - } - - logger.Info().Msgf("chain %s minting %d to %s, nonce %d, finalized zeta bn %d", toChain, cctx.InboundTxParams.Amount, to.Hex(), nonce, cctx.InboundTxParams.InboundTxFinalizedZetaHeight) - sendHash, err := hex.DecodeString(cctx.Index[2:]) // remove the leading 0x - if err != nil || len(sendHash) != 32 { - logger.Error().Err(err).Msgf("decode CCTX %s error", cctx.Index) + if skipTx { return } - var sendhash [32]byte - copy(sendhash[:32], sendHash[:32]) - - // use dynamic gas price for ethereum chains - var gasprice *big.Int - // The code below is a fix for https://github.com/zeta-chain/node/issues/1085 - // doesn't close directly the issue because we should determine if we want to keep using SuggestGasPrice if no OutboundTxGasPrice - // we should possibly remove it completely and return an error if no OutboundTxGasPrice is provided because it means no fee is processed on ZetaChain - specified, ok := new(big.Int).SetString(cctx.GetCurrentOutTxParam().OutboundTxGasPrice, 10) - if !ok { - if common.IsEthereumChain(toChain.ChainId) { - suggested, err := signer.client.SuggestGasPrice(context.Background()) - if err != nil { - logger.Error().Err(err).Msgf("cannot get gas price from chain %s ", toChain) - return - } - gasprice = roundUpToNearestGwei(suggested) - } else { - logger.Error().Err(err).Msgf("cannot convert gas price %s ", cctx.GetCurrentOutTxParam().OutboundTxGasPrice) - return - } - } else { - gasprice = specified - } + // Get destination chain for logging + toChain := common.GetChainFromChainID(txData.toChainID.Int64()) - // In case there is a pending transaction, make sure this keysign is a transaction replacement - pendingTx := evmClient.GetPendingTx(nonce) - if pendingTx != nil { - if gasprice.Cmp(pendingTx.GasPrice()) > 0 { - logger.Info().Msgf("replace pending outTx %s nonce %d using gas price %d", pendingTx.Hash().Hex(), nonce, gasprice) - } else { - logger.Info().Msgf("please wait for pending outTx %s nonce %d to be included", pendingTx.Hash().Hex(), nonce) - return - } - } - - flags, err := zetaBridge.GetCrosschainFlags() + // Get cross-chain flags + crossChainflags, err := zetaBridge.GetCrosschainFlags() if err != nil { - logger.Error().Err(err).Msgf("cannot get crosschain flags") + logger.Err(err).Msg("couldn't retrieve crosschain flags from core") return } var tx *ethtypes.Transaction - // compliance check goes first if clientcommon.IsCctxRestricted(cctx) { clientcommon.PrintComplianceLog(logger, signer.logger.Compliance, - true, evmClient.chain.ChainId, cctx.Index, cctx.InboundTxParams.Sender, to.Hex(), cctx.GetCurrentOutTxParam().CoinType.String()) - tx, err = signer.SignCancelTx(nonce, gasprice, height) // cancel the tx + true, evmClient.chain.ChainId, cctx.Index, cctx.InboundTxParams.Sender, txData.to.Hex(), cctx.GetCurrentOutTxParam().CoinType.String()) + tx, err = signer.SignCancelTx(txData.nonce, txData.gasPrice, height) // cancel the tx + if err != nil { + logger.Warn().Err(err).Msg(SignerErrorMsg(cctx)) + return + } } else if cctx.GetCurrentOutTxParam().CoinType == common.CoinType_Cmd { // admin command to := ethcommon.HexToAddress(cctx.GetCurrentOutTxParam().Receiver) if to == (ethcommon.Address{}) { logger.Error().Msgf("invalid receiver %s", cctx.GetCurrentOutTxParam().Receiver) return } - msg := strings.Split(cctx.RelayedMessage, ":") if len(msg) != 2 { logger.Error().Msgf("invalid message %s", msg) return } - tx, err = signer.SignCommandTx(msg[0], msg[1], to, cctx.GetCurrentOutTxParam(), gasLimit, gasprice, height) - } else if cctx.InboundTxParams.SenderChainId == zetaBridge.ZetaChain().ChainId && cctx.CctxStatus.Status == types.CctxStatus_PendingOutbound && flags.IsOutboundEnabled { - if cctx.GetCurrentOutTxParam().CoinType == common.CoinType_Gas { - logger.Info().Msgf("SignWithdrawTx: %d => %s, nonce %d, gasprice %d", cctx.InboundTxParams.SenderChainId, toChain, cctx.GetCurrentOutTxParam().OutboundTxTssNonce, gasprice) - tx, err = signer.SignWithdrawTx( - to, - cctx.GetCurrentOutTxParam().Amount.BigInt(), - cctx.GetCurrentOutTxParam().OutboundTxTssNonce, - gasprice, - height, - ) + // cmd field is used to determine whether to execute ERC20 whitelist or migrate TSS funds given that the coin type + // from the cctx is common.CoinType_Cmd + cmd := msg[0] + // params field is used to pass input parameters for command requests, currently it is used to pass the ERC20 + // contract address when a whitelist command is requested + params := msg[1] + tx, err = signer.SignCommandTx(txData, cmd, params) + if err != nil { + logger.Warn().Err(err).Msg(SignerErrorMsg(cctx)) + return } - if cctx.GetCurrentOutTxParam().CoinType == common.CoinType_ERC20 { - asset := ethcommon.HexToAddress(cctx.InboundTxParams.Asset) - logger.Info().Msgf("SignERC20WithdrawTx: %d => %s, nonce %d, gasprice %d", cctx.InboundTxParams.SenderChainId, toChain, cctx.GetCurrentOutTxParam().OutboundTxTssNonce, gasprice) - tx, err = signer.SignERC20WithdrawTx( - to, - asset, - cctx.GetCurrentOutTxParam().Amount.BigInt(), - gasLimit, - cctx.GetCurrentOutTxParam().OutboundTxTssNonce, - gasprice, - height, - ) + } else if IsSenderZetaChain(cctx, zetaBridge, &crossChainflags) { + switch cctx.GetCurrentOutTxParam().CoinType { + case common.CoinType_Gas: + logger.Info().Msgf("SignWithdrawTx: %d => %s, nonce %d, gasPrice %d", cctx.InboundTxParams.SenderChainId, toChain, cctx.GetCurrentOutTxParam().OutboundTxTssNonce, txData.gasPrice) + tx, err = signer.SignWithdrawTx(txData) + case common.CoinType_ERC20: + logger.Info().Msgf("SignERC20WithdrawTx: %d => %s, nonce %d, gasPrice %d", cctx.InboundTxParams.SenderChainId, toChain, cctx.GetCurrentOutTxParam().OutboundTxTssNonce, txData.gasPrice) + tx, err = signer.SignERC20WithdrawTx(txData) + case common.CoinType_Zeta: + logger.Info().Msgf("SignOutboundTx: %d => %s, nonce %d, gasPrice %d", cctx.InboundTxParams.SenderChainId, toChain, cctx.GetCurrentOutTxParam().OutboundTxTssNonce, txData.gasPrice) + tx, err = signer.SignOutboundTx(txData) } - if cctx.GetCurrentOutTxParam().CoinType == common.CoinType_Zeta { - logger.Info().Msgf("SignOutboundTx: %d => %s, nonce %d, gasprice %d", cctx.InboundTxParams.SenderChainId, toChain, cctx.GetCurrentOutTxParam().OutboundTxTssNonce, gasprice) - tx, err = signer.SignOutboundTx( - ethcommon.HexToAddress(cctx.InboundTxParams.Sender), - big.NewInt(cctx.InboundTxParams.SenderChainId), - to, - cctx.GetCurrentOutTxParam().Amount.BigInt(), - gasLimit, - message, - sendhash, - cctx.GetCurrentOutTxParam().OutboundTxTssNonce, - gasprice, - height, - ) + if err != nil { + logger.Warn().Err(err).Msg(SignerErrorMsg(cctx)) + return } } else if cctx.CctxStatus.Status == types.CctxStatus_PendingRevert && cctx.OutboundTxParams[0].ReceiverChainId == zetaBridge.ZetaChain().ChainId { - if cctx.GetCurrentOutTxParam().CoinType == common.CoinType_Gas { - logger.Info().Msgf("SignWithdrawTx: %d => %s, nonce %d, gasprice %d", cctx.InboundTxParams.SenderChainId, toChain, cctx.GetCurrentOutTxParam().OutboundTxTssNonce, gasprice) - tx, err = signer.SignWithdrawTx( - to, - cctx.GetCurrentOutTxParam().Amount.BigInt(), - cctx.GetCurrentOutTxParam().OutboundTxTssNonce, - gasprice, - height, - ) + switch cctx.GetCurrentOutTxParam().CoinType { + case common.CoinType_Gas: + logger.Info().Msgf("SignWithdrawTx: %d => %s, nonce %d, gasPrice %d", cctx.InboundTxParams.SenderChainId, toChain, cctx.GetCurrentOutTxParam().OutboundTxTssNonce, txData.gasPrice) + tx, err = signer.SignWithdrawTx(txData) + case common.CoinType_ERC20: + logger.Info().Msgf("SignERC20WithdrawTx: %d => %s, nonce %d, gasPrice %d", cctx.InboundTxParams.SenderChainId, toChain, cctx.GetCurrentOutTxParam().OutboundTxTssNonce, txData.gasPrice) + tx, err = signer.SignERC20WithdrawTx(txData) } - if cctx.GetCurrentOutTxParam().CoinType == common.CoinType_ERC20 { - asset := ethcommon.HexToAddress(cctx.InboundTxParams.Asset) - logger.Info().Msgf("SignERC20WithdrawTx: %d => %s, nonce %d, gasprice %d", cctx.InboundTxParams.SenderChainId, toChain, cctx.GetCurrentOutTxParam().OutboundTxTssNonce, gasprice) - tx, err = signer.SignERC20WithdrawTx( - to, - asset, - cctx.GetCurrentOutTxParam().Amount.BigInt(), - gasLimit, - cctx.GetCurrentOutTxParam().OutboundTxTssNonce, - gasprice, - height, - ) + if err != nil { + logger.Warn().Err(err).Msg(SignerErrorMsg(cctx)) + return } } else if cctx.CctxStatus.Status == types.CctxStatus_PendingRevert { - logger.Info().Msgf("SignRevertTx: %d => %s, nonce %d, gasprice %d", cctx.InboundTxParams.SenderChainId, toChain, cctx.GetCurrentOutTxParam().OutboundTxTssNonce, gasprice) - tx, err = signer.SignRevertTx( - ethcommon.HexToAddress(cctx.InboundTxParams.Sender), - big.NewInt(cctx.OutboundTxParams[0].ReceiverChainId), - to.Bytes(), - big.NewInt(cctx.GetCurrentOutTxParam().ReceiverChainId), - cctx.GetCurrentOutTxParam().Amount.BigInt(), - gasLimit, - message, - sendhash, - cctx.GetCurrentOutTxParam().OutboundTxTssNonce, - gasprice, - height, - ) + logger.Info().Msgf("SignRevertTx: %d => %s, nonce %d, gasPrice %d", cctx.InboundTxParams.SenderChainId, toChain, cctx.GetCurrentOutTxParam().OutboundTxTssNonce, txData.gasPrice) + txData.srcChainID = big.NewInt(cctx.OutboundTxParams[0].ReceiverChainId) + txData.toChainID = big.NewInt(cctx.GetCurrentOutTxParam().ReceiverChainId) + + tx, err = signer.SignRevertTx(txData) + if err != nil { + logger.Warn().Err(err).Msg(SignerErrorMsg(cctx)) + return + } } else if cctx.CctxStatus.Status == types.CctxStatus_PendingOutbound { - logger.Info().Msgf("SignOutboundTx: %d => %s, nonce %d, gasprice %d", cctx.InboundTxParams.SenderChainId, toChain, cctx.GetCurrentOutTxParam().OutboundTxTssNonce, gasprice) - tx, err = signer.SignOutboundTx( - ethcommon.HexToAddress(cctx.InboundTxParams.Sender), - big.NewInt(cctx.InboundTxParams.SenderChainId), - to, - cctx.GetCurrentOutTxParam().Amount.BigInt(), - gasLimit, - message, - sendhash, - cctx.GetCurrentOutTxParam().OutboundTxTssNonce, - gasprice, - height, - ) + logger.Info().Msgf("SignOutboundTx: %d => %s, nonce %d, gasPrice %d", cctx.InboundTxParams.SenderChainId, toChain, cctx.GetCurrentOutTxParam().OutboundTxTssNonce, txData.gasPrice) + tx, err = signer.SignOutboundTx(txData) + if err != nil { + logger.Warn().Err(err).Msg(SignerErrorMsg(cctx)) + return + } } - if err != nil { - logger.Warn().Err(err).Msgf("signer SignOutbound error: nonce %d chain %d", cctx.GetCurrentOutTxParam().OutboundTxTssNonce, cctx.GetCurrentOutTxParam().ReceiverChainId) - return - } logger.Info().Msgf("Key-sign success: %d => %s, nonce %d", cctx.InboundTxParams.SenderChainId, toChain, cctx.GetCurrentOutTxParam().OutboundTxTssNonce) - _, err = zetaBridge.GetObserverList() - if err != nil { - logger.Warn().Err(err).Msgf("unable to get observer list: chain %d observation %s", cctx.GetCurrentOutTxParam().OutboundTxTssNonce, observertypes.ObservationType_OutBoundTx.String()) + // Broadcast Signed Tx + signer.BroadcastOutTx(tx, cctx, logger, myID, zetaBridge, txData) +} - } +// BroadcastOutTx signed transaction through evm rpc client +func (signer *Signer) BroadcastOutTx( + tx *ethtypes.Transaction, + cctx *types.CrossChainTx, + logger zerolog.Logger, + myID sdk.AccAddress, + zetaBridge interfaces.ZetaCoreBridger, + txData *OutBoundTransactionData) { + // Get destination chain for logging + toChain := common.GetChainFromChainID(txData.toChainID.Int64()) + + // Try to broadcast transaction if tx != nil { outTxHash := tx.Hash().Hex() logger.Info().Msgf("on chain %s nonce %d, outTxHash %s signer %s", signer.chain, cctx.GetCurrentOutTxParam().OutboundTxTssNonce, outTxHash, myID) @@ -707,23 +554,15 @@ func (signer *Signer) reportToOutTxTracker(zetaBridge interfaces.ZetaCoreBridger // address asset, // uint256 amount, // ) external onlyTssAddress -func (signer *Signer) SignERC20WithdrawTx( - recipient ethcommon.Address, - asset ethcommon.Address, - amount *big.Int, - gasLimit uint64, - nonce uint64, - gasPrice *big.Int, - height uint64, -) (*ethtypes.Transaction, error) { +func (signer *Signer) SignERC20WithdrawTx(txData *OutBoundTransactionData) (*ethtypes.Transaction, error) { var data []byte var err error - data, err = signer.erc20CustodyABI.Pack("withdraw", recipient, asset, amount) + data, err = signer.erc20CustodyABI.Pack("withdraw", txData.to, txData.asset, txData.amount) if err != nil { return nil, fmt.Errorf("pack error: %w", err) } - tx, _, _, err := signer.Sign(data, signer.erc20CustodyContractAddress, gasLimit, gasPrice, nonce, height) + tx, _, _, err := signer.Sign(data, signer.erc20CustodyContractAddress, txData.gasLimit, txData.gasPrice, txData.nonce, txData.height) if err != nil { return nil, fmt.Errorf("sign error: %w", err) } @@ -764,6 +603,42 @@ func (signer *Signer) SignWhitelistTx( return tx, nil } +// Exported for unit tests + +func (signer *Signer) GetReportedTxList() *map[string]bool { + return &signer.outTxHashBeingReported +} +func (signer *Signer) EvmClient() interfaces.EVMRPCClient { + return signer.client +} +func (signer *Signer) EvmSigner() ethtypes.Signer { + return signer.ethSigner +} + +// ________________________ + +// getEVMRPC is a helper function to set up the client and signer, also initializes a mock client for unit tests +func getEVMRPC(endpoint string) (interfaces.EVMRPCClient, *big.Int, ethtypes.Signer, error) { + if endpoint == stub.EVMRPCEnabled { + chainID := big.NewInt(common.BscMainnetChain().ChainId) + ethSigner := ethtypes.NewEIP155Signer(chainID) + client := stub.EvmClient{} + return client, chainID, ethSigner, nil + } + + client, err := ethclient.Dial(endpoint) + if err != nil { + return nil, nil, nil, err + } + + chainID, err := client.ChainID(context.TODO()) + if err != nil { + return nil, nil, nil, err + } + ethSigner := ethtypes.LatestSignerForChainID(chainID) + return client, chainID, ethSigner, nil +} + func roundUpToNearestGwei(gasPrice *big.Int) *big.Int { oneGwei := big.NewInt(1_000_000_000) // 1 Gwei mod := new(big.Int) @@ -773,3 +648,41 @@ func roundUpToNearestGwei(gasPrice *big.Int) *big.Int { } return new(big.Int).Add(gasPrice, new(big.Int).Sub(oneGwei, mod)) } + +func IsSenderZetaChain(cctx *types.CrossChainTx, zetaBridge interfaces.ZetaCoreBridger, flags *observertypes.CrosschainFlags) bool { + return cctx.InboundTxParams.SenderChainId == zetaBridge.ZetaChain().ChainId && cctx.CctxStatus.Status == types.CctxStatus_PendingOutbound && flags.IsOutboundEnabled +} + +func SignerErrorMsg(cctx *types.CrossChainTx) string { + return fmt.Sprintf("signer SignOutbound error: nonce %d chain %d", cctx.GetCurrentOutTxParam().OutboundTxTssNonce, cctx.GetCurrentOutTxParam().ReceiverChainId) +} + +func (signer *Signer) SignWhitelistERC20Cmd(txData *OutBoundTransactionData, params string) (*ethtypes.Transaction, error) { + outboundParams := txData.outboundParams + erc20 := ethcommon.HexToAddress(params) + if erc20 == (ethcommon.Address{}) { + return nil, fmt.Errorf("SignCommandTx: invalid erc20 address %s", params) + } + custodyAbi, err := erc20custody.ERC20CustodyMetaData.GetAbi() + if err != nil { + return nil, err + } + data, err := custodyAbi.Pack("whitelist", erc20) + if err != nil { + return nil, err + } + tx, _, _, err := signer.Sign(data, txData.to, txData.gasLimit, txData.gasPrice, outboundParams.OutboundTxTssNonce, txData.height) + if err != nil { + return nil, fmt.Errorf("sign error: %w", err) + } + return tx, nil +} + +func (signer *Signer) SignMigrateTssFundsCmd(txData *OutBoundTransactionData) (*ethtypes.Transaction, error) { + outboundParams := txData.outboundParams + tx, _, _, err := signer.Sign(nil, txData.to, txData.gasLimit, txData.gasPrice, outboundParams.OutboundTxTssNonce, txData.height) + if err != nil { + return nil, err + } + return tx, nil +} diff --git a/zetaclient/evm/evm_signer_test.go b/zetaclient/evm/evm_signer_test.go new file mode 100644 index 0000000000..e685128902 --- /dev/null +++ b/zetaclient/evm/evm_signer_test.go @@ -0,0 +1,326 @@ +package evm + +import ( + "path" + "testing" + + sdktypes "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/rs/zerolog" + "github.com/stretchr/testify/require" + corecommon "github.com/zeta-chain/zetacore/common" + "github.com/zeta-chain/zetacore/testutil/sample" + "github.com/zeta-chain/zetacore/x/crosschain/types" + crosschaintypes "github.com/zeta-chain/zetacore/x/crosschain/types" + appcontext "github.com/zeta-chain/zetacore/zetaclient/app_context" + "github.com/zeta-chain/zetacore/zetaclient/common" + "github.com/zeta-chain/zetacore/zetaclient/config" + corecontext "github.com/zeta-chain/zetacore/zetaclient/core_context" + "github.com/zeta-chain/zetacore/zetaclient/metrics" + "github.com/zeta-chain/zetacore/zetaclient/outtxprocessor" + "github.com/zeta-chain/zetacore/zetaclient/testutils" + "github.com/zeta-chain/zetacore/zetaclient/testutils/stub" +) + +var ( + // Dummy addresses as they are just used as transaction data to be signed + ConnectorAddress = sample.EthAddress() + ERC20CustodyAddress = sample.EthAddress() +) + +func getNewEvmSigner() (*Signer, error) { + mpiAddress := ConnectorAddress + erc20CustodyAddress := ERC20CustodyAddress + logger := common.ClientLogger{} + ts := &metrics.TelemetryServer{} + return NewEVMSigner( + corecommon.BscMainnetChain(), + stub.EVMRPCEnabled, + stub.NewTSSMainnet(), + config.GetConnectorABI(), + config.GetERC20CustodyABI(), + mpiAddress, + erc20CustodyAddress, + logger, + ts) +} + +func getNewEvmChainClient() (*ChainClient, error) { + logger := common.ClientLogger{} + ts := &metrics.TelemetryServer{} + cfg := config.NewConfig() + tss := stub.NewTSSMainnet() + + evmcfg := config.EVMConfig{Chain: corecommon.BscMainnetChain(), Endpoint: "http://localhost:8545"} + cfg.EVMChainConfigs[corecommon.BscMainnetChain().ChainId] = &evmcfg + coreCTX := corecontext.NewZetaCoreContext(cfg) + appCTX := appcontext.NewAppContext(coreCTX, cfg) + + return NewEVMChainClient(appCTX, stub.NewZetaCoreBridge(), tss, "", logger, evmcfg, ts) +} + +func getNewOutTxProcessor() *outtxprocessor.Processor { + logger := zerolog.Logger{} + return outtxprocessor.NewOutTxProcessorManager(logger) +} + +func getCCTX() (*types.CrossChainTx, error) { + var cctx crosschaintypes.CrossChainTx + err := testutils.LoadObjectFromJSONFile(&cctx, path.Join("../", testutils.TestDataPathCctx, "cctx_56_68270.json")) + return &cctx, err +} + +func getInvalidCCTX() (*types.CrossChainTx, error) { + var cctx crosschaintypes.CrossChainTx + err := testutils.LoadObjectFromJSONFile(&cctx, path.Join("../", testutils.TestDataPathCctx, "cctx_56_68270_invalidChainID.json")) + return &cctx, err +} + +func TestSigner_TryProcessOutTx(t *testing.T) { + evmSigner, err := getNewEvmSigner() + require.NoError(t, err) + cctx, err := getCCTX() + require.NoError(t, err) + processorManager := getNewOutTxProcessor() + mockChainClient, err := getNewEvmChainClient() + require.NoError(t, err) + + evmSigner.TryProcessOutTx(cctx, processorManager, "123", mockChainClient, stub.NewZetaCoreBridge(), 123) + + //Check if cctx was signed and broadcasted + list := evmSigner.GetReportedTxList() + require.Len(t, *list, 1) +} + +func TestSigner_SignOutboundTx(t *testing.T) { + // Setup evm signer + evmSigner, err := getNewEvmSigner() + require.NoError(t, err) + + // Setup txData struct + + cctx, err := getCCTX() + require.NoError(t, err) + mockChainClient, err := getNewEvmChainClient() + require.NoError(t, err) + txData, skip, err := NewOutBoundTransactionData(cctx, mockChainClient, evmSigner.EvmClient(), zerolog.Logger{}, 123) + require.False(t, skip) + require.NoError(t, err) + + t.Run("SignOutboundTx - should successfully sign", func(t *testing.T) { + // Call SignOutboundTx + tx, err := evmSigner.SignOutboundTx(txData) + require.NoError(t, err) + + // Verify Signature + tss := stub.NewTSSMainnet() + _, r, s := tx.RawSignatureValues() + signature := append(r.Bytes(), s.Bytes()...) + hash := evmSigner.EvmSigner().Hash(tx) + + verified := crypto.VerifySignature(tss.Pubkey(), hash.Bytes(), signature) + require.True(t, verified) + }) +} + +func TestSigner_SignRevertTx(t *testing.T) { + // Setup evm signer + evmSigner, err := getNewEvmSigner() + require.NoError(t, err) + + // Setup txData struct + cctx, err := getCCTX() + require.NoError(t, err) + mockChainClient, err := getNewEvmChainClient() + require.NoError(t, err) + txData, skip, err := NewOutBoundTransactionData(cctx, mockChainClient, evmSigner.EvmClient(), zerolog.Logger{}, 123) + require.False(t, skip) + require.NoError(t, err) + + t.Run("SignRevertTx - should successfully sign", func(t *testing.T) { + // Call SignRevertTx + tx, err := evmSigner.SignRevertTx(txData) + require.NoError(t, err) + + // Verify Signature + tss := stub.NewTSSMainnet() + _, r, s := tx.RawSignatureValues() + signature := append(r.Bytes(), s.Bytes()...) + hash := evmSigner.EvmSigner().Hash(tx) + + verified := crypto.VerifySignature(tss.Pubkey(), hash.Bytes(), signature) + require.True(t, verified) + }) +} + +func TestSigner_SignWithdrawTx(t *testing.T) { + // Setup evm signer + evmSigner, err := getNewEvmSigner() + require.NoError(t, err) + + // Setup txData struct + cctx, err := getCCTX() + require.NoError(t, err) + mockChainClient, err := getNewEvmChainClient() + require.NoError(t, err) + txData, skip, err := NewOutBoundTransactionData(cctx, mockChainClient, evmSigner.EvmClient(), zerolog.Logger{}, 123) + require.False(t, skip) + require.NoError(t, err) + + t.Run("SignWithdrawTx - should successfully sign", func(t *testing.T) { + // Call SignWithdrawTx + tx, err := evmSigner.SignWithdrawTx(txData) + require.NoError(t, err) + + // Verify Signature + tss := stub.NewTSSMainnet() + _, r, s := tx.RawSignatureValues() + signature := append(r.Bytes(), s.Bytes()...) + hash := evmSigner.EvmSigner().Hash(tx) + + verified := crypto.VerifySignature(tss.Pubkey(), hash.Bytes(), signature) + require.True(t, verified) + }) +} + +func TestSigner_SignCommandTx(t *testing.T) { + // Setup evm signer + evmSigner, err := getNewEvmSigner() + require.NoError(t, err) + + // Setup txData struct + cctx, err := getCCTX() + require.NoError(t, err) + mockChainClient, err := getNewEvmChainClient() + require.NoError(t, err) + txData, skip, err := NewOutBoundTransactionData(cctx, mockChainClient, evmSigner.EvmClient(), zerolog.Logger{}, 123) + require.False(t, skip) + require.NoError(t, err) + + t.Run("SignCommandTx CmdWhitelistERC20", func(t *testing.T) { + cmd := corecommon.CmdWhitelistERC20 + params := ConnectorAddress.Hex() + // Call SignCommandTx + tx, err := evmSigner.SignCommandTx(txData, cmd, params) + require.NoError(t, err) + + // Verify Signature + tss := stub.NewTSSMainnet() + _, r, s := tx.RawSignatureValues() + signature := append(r.Bytes(), s.Bytes()...) + hash := evmSigner.EvmSigner().Hash(tx) + + verified := crypto.VerifySignature(tss.Pubkey(), hash.Bytes(), signature) + require.True(t, verified) + }) + + t.Run("SignCommandTx CmdMigrateTssFunds", func(t *testing.T) { + cmd := corecommon.CmdMigrateTssFunds + // Call SignCommandTx + tx, err := evmSigner.SignCommandTx(txData, cmd, "") + require.NoError(t, err) + + // Verify Signature + tss := stub.NewTSSMainnet() + _, r, s := tx.RawSignatureValues() + signature := append(r.Bytes(), s.Bytes()...) + hash := evmSigner.EvmSigner().Hash(tx) + + verified := crypto.VerifySignature(tss.Pubkey(), hash.Bytes(), signature) + require.True(t, verified) + }) +} + +func TestSigner_SignERC20WithdrawTx(t *testing.T) { + // Setup evm signer + evmSigner, err := getNewEvmSigner() + require.NoError(t, err) + + // Setup txData struct + cctx, err := getCCTX() + require.NoError(t, err) + mockChainClient, err := getNewEvmChainClient() + require.NoError(t, err) + txData, skip, err := NewOutBoundTransactionData(cctx, mockChainClient, evmSigner.EvmClient(), zerolog.Logger{}, 123) + require.False(t, skip) + require.NoError(t, err) + + t.Run("SignERC20WithdrawTx - should successfully sign", func(t *testing.T) { + // Call SignERC20WithdrawTx + tx, err := evmSigner.SignERC20WithdrawTx(txData) + require.NoError(t, err) + + // Verify Signature + tss := stub.NewTSSMainnet() + _, r, s := tx.RawSignatureValues() + signature := append(r.Bytes(), s.Bytes()...) + hash := evmSigner.EvmSigner().Hash(tx) + + verified := crypto.VerifySignature(tss.Pubkey(), hash.Bytes(), signature) + require.True(t, verified) + }) +} + +func TestSigner_BroadcastOutTx(t *testing.T) { + // Setup evm signer + evmSigner, err := getNewEvmSigner() + require.NoError(t, err) + + // Setup txData struct + cctx, err := getCCTX() + require.NoError(t, err) + mockChainClient, err := getNewEvmChainClient() + require.NoError(t, err) + txData, skip, err := NewOutBoundTransactionData(cctx, mockChainClient, evmSigner.EvmClient(), zerolog.Logger{}, 123) + require.False(t, skip) + require.NoError(t, err) + + t.Run("BroadcastOutTx - should successfully broadcast", func(t *testing.T) { + // Call SignERC20WithdrawTx + tx, err := evmSigner.SignERC20WithdrawTx(txData) + require.NoError(t, err) + + evmSigner.BroadcastOutTx(tx, cctx, zerolog.Logger{}, sdktypes.AccAddress{}, stub.NewZetaCoreBridge(), txData) + + //Check if cctx was signed and broadcasted + list := evmSigner.GetReportedTxList() + require.Len(t, *list, 1) + }) +} + +func TestSigner_getEVMRPC(t *testing.T) { + t.Run("getEVMRPC error dialing", func(t *testing.T) { + client, chainId, signer, err := getEVMRPC("invalidEndpoint") + require.Nil(t, client) + require.Nil(t, chainId) + require.Nil(t, signer) + require.Error(t, err) + }) +} + +func TestSigner_SignerErrorMsg(t *testing.T) { + cctx, err := getCCTX() + require.NoError(t, err) + + msg := SignerErrorMsg(cctx) + require.Contains(t, msg, "nonce 68270 chain 56") +} + +func TestSigner_SignWhitelistERC20Cmd(t *testing.T) { + // Setup evm signer + evmSigner, err := getNewEvmSigner() + require.NoError(t, err) + + // Setup txData struct + cctx, err := getCCTX() + require.NoError(t, err) + mockChainClient, err := getNewEvmChainClient() + require.NoError(t, err) + txData, skip, err := NewOutBoundTransactionData(cctx, mockChainClient, evmSigner.EvmClient(), zerolog.Logger{}, 123) + require.False(t, skip) + require.NoError(t, err) + + tx, err := evmSigner.SignWhitelistERC20Cmd(txData, "") + require.Nil(t, tx) + require.ErrorContains(t, err, "invalid erc20 address") +} diff --git a/zetaclient/evm/inbounds_test.go b/zetaclient/evm/inbounds_test.go index 84b24dd200..bd8556be63 100644 --- a/zetaclient/evm/inbounds_test.go +++ b/zetaclient/evm/inbounds_test.go @@ -4,6 +4,8 @@ import ( "path" "testing" + "github.com/zeta-chain/zetacore/zetaclient/testutils/stub" + ethcommon "github.com/ethereum/go-ethereum/common" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethclient" @@ -18,7 +20,7 @@ import ( func MockEVMClient(chain common.Chain) *ChainClient { return &ChainClient{ chain: chain, - zetaClient: testutils.MockCoreBridge(), + zetaClient: stub.NewZetaCoreBridge(), } } diff --git a/zetaclient/evm/outbound_transaction_data.go b/zetaclient/evm/outbound_transaction_data.go new file mode 100644 index 0000000000..76f8cf6d8f --- /dev/null +++ b/zetaclient/evm/outbound_transaction_data.go @@ -0,0 +1,180 @@ +package evm + +import ( + "context" + "encoding/base64" + "encoding/hex" + "errors" + "fmt" + "math/big" + + ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/rs/zerolog" + "github.com/zeta-chain/zetacore/common" + "github.com/zeta-chain/zetacore/x/crosschain/types" + "github.com/zeta-chain/zetacore/zetaclient/interfaces" +) + +const ( + MinGasLimit = 100_000 + MaxGasLimit = 1_000_000 +) + +// OutBoundTransactionData is a data structure containing input fields used to construct each type of transaction. +// This is populated using cctx and other input parameters passed to TryProcessOutTx +type OutBoundTransactionData struct { + srcChainID *big.Int + toChainID *big.Int + sender ethcommon.Address + to ethcommon.Address + asset ethcommon.Address + amount *big.Int + gasPrice *big.Int + gasLimit uint64 + message []byte + nonce uint64 + height uint64 + + // sendHash field is the inbound message digest that is sent to the destination contract + sendHash [32]byte + + // outboundParams field contains data detailing the receiver chain and outbound transaction + outboundParams *types.OutboundTxParams +} + +// SetChainAndSender populates the destination address and Chain ID based on the status of the cross chain tx +// returns true if transaction should be skipped +// returns false otherwise +func (txData *OutBoundTransactionData) SetChainAndSender(cctx *types.CrossChainTx, logger zerolog.Logger) bool { + switch cctx.CctxStatus.Status { + case types.CctxStatus_PendingRevert: + txData.to = ethcommon.HexToAddress(cctx.InboundTxParams.Sender) + txData.toChainID = big.NewInt(cctx.InboundTxParams.SenderChainId) + logger.Info().Msgf("Abort: reverting inbound") + case types.CctxStatus_PendingOutbound: + txData.to = ethcommon.HexToAddress(cctx.GetCurrentOutTxParam().Receiver) + txData.toChainID = big.NewInt(cctx.GetCurrentOutTxParam().ReceiverChainId) + default: + logger.Info().Msgf("Transaction doesn't need to be processed status: %d", cctx.CctxStatus.Status) + return true + } + return false +} + +// SetupGas sets the gas limit and price +func (txData *OutBoundTransactionData) SetupGas( + cctx *types.CrossChainTx, + logger zerolog.Logger, + client interfaces.EVMRPCClient, + chain *common.Chain, +) error { + + txData.gasLimit = cctx.GetCurrentOutTxParam().OutboundTxGasLimit + if txData.gasLimit < MinGasLimit { + txData.gasLimit = MinGasLimit + logger.Warn().Msgf("gasLimit %d is too low; set to %d", cctx.GetCurrentOutTxParam().OutboundTxGasLimit, txData.gasLimit) + } + if txData.gasLimit > MaxGasLimit { + txData.gasLimit = MaxGasLimit + logger.Warn().Msgf("gasLimit %d is too high; set to %d", cctx.GetCurrentOutTxParam().OutboundTxGasLimit, txData.gasLimit) + } + + // use dynamic gas price for ethereum chains. + // The code below is a fix for https://github.com/zeta-chain/node/issues/1085 + // doesn't close directly the issue because we should determine if we want to keep using SuggestGasPrice if no OutboundTxGasPrice + // we should possibly remove it completely and return an error if no OutboundTxGasPrice is provided because it means no fee is processed on ZetaChain + specified, ok := new(big.Int).SetString(cctx.GetCurrentOutTxParam().OutboundTxGasPrice, 10) + if !ok { + if common.IsEthereumChain(chain.ChainId) { + suggested, err := client.SuggestGasPrice(context.Background()) + if err != nil { + return errors.Join(err, fmt.Errorf("cannot get gas price from chain %s ", chain)) + } + txData.gasPrice = roundUpToNearestGwei(suggested) + } else { + return fmt.Errorf("cannot convert gas price %s ", cctx.GetCurrentOutTxParam().OutboundTxGasPrice) + } + } else { + txData.gasPrice = specified + } + return nil +} + +// NewOutBoundTransactionData populates transaction input fields parsed from the cctx and other parameters +// returns +// 1. New OutBoundTransaction Data struct or nil if an error occurred. +// 2. bool (skipTx) - if the transaction doesn't qualify to be processed the function will return true, meaning that this +// cctx will be skipped and false otherwise. +// 3. error +func NewOutBoundTransactionData( + cctx *types.CrossChainTx, + evmClient *ChainClient, + evmRPC interfaces.EVMRPCClient, + logger zerolog.Logger, + height uint64, +) (*OutBoundTransactionData, bool, error) { + txData := OutBoundTransactionData{} + txData.outboundParams = cctx.GetCurrentOutTxParam() + txData.amount = cctx.GetCurrentOutTxParam().Amount.BigInt() + txData.nonce = cctx.GetCurrentOutTxParam().OutboundTxTssNonce + txData.sender = ethcommon.HexToAddress(cctx.InboundTxParams.Sender) + txData.srcChainID = big.NewInt(cctx.InboundTxParams.SenderChainId) + txData.asset = ethcommon.HexToAddress(cctx.InboundTxParams.Asset) + txData.height = height + + skipTx := txData.SetChainAndSender(cctx, logger) + if skipTx { + return nil, true, nil + } + + toChain := common.GetChainFromChainID(txData.toChainID.Int64()) + if toChain == nil { + return nil, true, fmt.Errorf("unknown chain: %d", txData.toChainID.Int64()) + } + + // Get nonce, Early return if the cctx is already processed + nonce := cctx.GetCurrentOutTxParam().OutboundTxTssNonce + included, confirmed, err := evmClient.IsSendOutTxProcessed(cctx, logger) + if err != nil { + return nil, true, errors.New("IsSendOutTxProcessed 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 { + return nil, true, err + } + + // Get sendHash + logger.Info().Msgf("chain %s minting %d to %s, nonce %d, finalized zeta bn %d", toChain, cctx.InboundTxParams.Amount, txData.to.Hex(), nonce, cctx.InboundTxParams.InboundTxFinalizedZetaHeight) + sendHash, err := hex.DecodeString(cctx.Index[2:]) // remove the leading 0x + if err != nil || len(sendHash) != 32 { + return nil, true, fmt.Errorf("decode CCTX %s error", cctx.Index) + } + copy(txData.sendHash[:32], sendHash[:32]) + + // In case there is a pending transaction, make sure this keysign is a transaction replacement + pendingTx := evmClient.GetPendingTx(nonce) + if pendingTx != nil { + if txData.gasPrice.Cmp(pendingTx.GasPrice()) > 0 { + logger.Info().Msgf("replace pending outTx %s nonce %d using gas price %d", pendingTx.Hash().Hex(), nonce, txData.gasPrice) + } else { + logger.Info().Msgf("please wait for pending outTx %s nonce %d to be included", pendingTx.Hash().Hex(), nonce) + return nil, true, nil + } + } + + // Base64 decode message + if cctx.GetCurrentOutTxParam().CoinType != common.CoinType_Cmd { + txData.message, err = base64.StdEncoding.DecodeString(cctx.RelayedMessage) + if err != nil { + logger.Err(err).Msgf("decode CCTX.Message %s error", cctx.RelayedMessage) + } + } + + return &txData, false, nil +} diff --git a/zetaclient/evm/outbound_transaction_data_test.go b/zetaclient/evm/outbound_transaction_data_test.go new file mode 100644 index 0000000000..cc5de0489b --- /dev/null +++ b/zetaclient/evm/outbound_transaction_data_test.go @@ -0,0 +1,112 @@ +package evm + +import ( + "math/big" + "testing" + + ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/rs/zerolog" + "github.com/stretchr/testify/require" + corecommon "github.com/zeta-chain/zetacore/common" + "github.com/zeta-chain/zetacore/x/crosschain/types" +) + +func TestSigner_SetChainAndSender(t *testing.T) { + // setup inputs + cctx, err := getCCTX() + require.NoError(t, err) + + txData := &OutBoundTransactionData{} + logger := zerolog.Logger{} + + t.Run("SetChainAndSender PendingRevert", func(t *testing.T) { + cctx.CctxStatus.Status = types.CctxStatus_PendingRevert + skipTx := txData.SetChainAndSender(cctx, logger) + + require.False(t, skipTx) + require.Equal(t, ethcommon.HexToAddress(cctx.InboundTxParams.Sender), txData.to) + require.Equal(t, big.NewInt(cctx.InboundTxParams.SenderChainId), txData.toChainID) + }) + + t.Run("SetChainAndSender PendingOutBound", func(t *testing.T) { + cctx.CctxStatus.Status = types.CctxStatus_PendingOutbound + skipTx := txData.SetChainAndSender(cctx, logger) + + require.False(t, skipTx) + require.Equal(t, ethcommon.HexToAddress(cctx.GetCurrentOutTxParam().Receiver), txData.to) + require.Equal(t, big.NewInt(cctx.GetCurrentOutTxParam().ReceiverChainId), txData.toChainID) + }) + + t.Run("SetChainAndSender Should skip cctx", func(t *testing.T) { + cctx.CctxStatus.Status = types.CctxStatus_PendingInbound + skipTx := txData.SetChainAndSender(cctx, logger) + require.True(t, skipTx) + }) +} + +func TestSigner_SetupGas(t *testing.T) { + cctx, err := getCCTX() + require.NoError(t, err) + + evmSigner, err := getNewEvmSigner() + require.NoError(t, err) + + txData := &OutBoundTransactionData{} + logger := zerolog.Logger{} + + t.Run("SetupGas_success", func(t *testing.T) { + chain := corecommon.BscMainnetChain() + err := txData.SetupGas(cctx, logger, evmSigner.EvmClient(), &chain) + require.NoError(t, err) + }) + + t.Run("SetupGas_error", func(t *testing.T) { + cctx.GetCurrentOutTxParam().OutboundTxGasPrice = "invalidGasPrice" + chain := corecommon.BscMainnetChain() + err := txData.SetupGas(cctx, logger, evmSigner.EvmClient(), &chain) + require.ErrorContains(t, err, "cannot convert gas price") + }) +} + +func TestSigner_NewOutBoundTransactionData(t *testing.T) { + // Setup evm signer + evmSigner, err := getNewEvmSigner() + require.NoError(t, err) + + mockChainClient, err := getNewEvmChainClient() + require.NoError(t, err) + + t.Run("NewOutBoundTransactionData success", func(t *testing.T) { + cctx, err := getCCTX() + require.NoError(t, err) + _, skip, err := NewOutBoundTransactionData(cctx, mockChainClient, evmSigner.EvmClient(), zerolog.Logger{}, 123) + require.False(t, skip) + require.NoError(t, err) + }) + + t.Run("NewOutBoundTransactionData skip", func(t *testing.T) { + cctx, err := getCCTX() + require.NoError(t, err) + cctx.CctxStatus.Status = types.CctxStatus_Aborted + _, skip, err := NewOutBoundTransactionData(cctx, mockChainClient, evmSigner.EvmClient(), zerolog.Logger{}, 123) + require.NoError(t, err) + require.True(t, skip) + }) + + t.Run("NewOutBoundTransactionData unknown chain", func(t *testing.T) { + cctx, err := getInvalidCCTX() + require.NoError(t, err) + _, skip, err := NewOutBoundTransactionData(cctx, mockChainClient, evmSigner.EvmClient(), zerolog.Logger{}, 123) + require.ErrorContains(t, err, "unknown chain") + require.True(t, skip) + }) + + t.Run("NewOutBoundTransactionData setup gas error", func(t *testing.T) { + cctx, err := getCCTX() + require.NoError(t, err) + cctx.GetCurrentOutTxParam().OutboundTxGasPrice = "invalidGasPrice" + _, skip, err := NewOutBoundTransactionData(cctx, mockChainClient, evmSigner.EvmClient(), zerolog.Logger{}, 123) + require.True(t, skip) + require.ErrorContains(t, err, "cannot convert gas price") + }) +} diff --git a/zetaclient/testdata/cctx/cctx_56_68270.json b/zetaclient/testdata/cctx/cctx_56_68270.json new file mode 100644 index 0000000000..1c0ae6b762 --- /dev/null +++ b/zetaclient/testdata/cctx/cctx_56_68270.json @@ -0,0 +1,43 @@ +{ + "creator": "", + "index": "0x541b570182950809f9b9077861a0fc7038af9a14ce8af4e151a83adfa308c7a9", + "zeta_fees": "0", + "relayed_message": "", + "cctx_status": { + "status": 1, + "status_message": "", + "lastUpdate_timestamp": 1709145057 + }, + "inbound_tx_params": { + "sender": "0xd91b507F2A3e2D4A32d0C86Ac19FEAD2D461008D", + "sender_chain_id": 7000, + "tx_origin": "0xb0C04e07A301927672A8A7a874DB6930576C90B8", + "coin_type": 1, + "asset": "", + "amount": "657177295293237048", + "inbound_tx_observed_hash": "0x093f4ca4c1884df0fd9dd59b75979342ded29d3c9b6861644287a2e1417b9a39", + "inbound_tx_observed_external_height": 1960153, + "inbound_tx_ballot_index": "0x541b570182950809f9b9077861a0fc7038af9a14ce8af4e151a83adfa308c7a9", + "inbound_tx_finalized_zeta_height": 0, + "tx_finalization_status": 0 + }, + "outbound_tx_params": [ + { + "receiver": "0xb0C04e07A301927672A8A7a874DB6930576C90B8", + "receiver_chainId": 56, + "coin_type": 1, + "amount": "657177295293237048", + "outbound_tx_tss_nonce": 68270, + "outbound_tx_gas_limit": 21000, + "outbound_tx_gas_price": "6000000000", + "outbound_tx_hash": "", + "outbound_tx_ballot_index": "", + "outbound_tx_observed_external_height": 0, + "outbound_tx_gas_used": 0, + "outbound_tx_effective_gas_price": "0", + "outbound_tx_effective_gas_limit": 0, + "tss_pubkey": "zetapub1addwnpepqtadxdyt037h86z60nl98t6zk56mw5zpnm79tsmvspln3hgt5phdc79kvfc", + "tx_finalization_status": 0 + } + ] +} diff --git a/zetaclient/testdata/cctx/cctx_56_68270_invalidChainID.json b/zetaclient/testdata/cctx/cctx_56_68270_invalidChainID.json new file mode 100644 index 0000000000..2e4cc4d97a --- /dev/null +++ b/zetaclient/testdata/cctx/cctx_56_68270_invalidChainID.json @@ -0,0 +1,43 @@ +{ + "creator": "", + "index": "0x541b570182950809f9b9077861a0fc7038af9a14ce8af4e151a83adfa308c7a9", + "zeta_fees": "0", + "relayed_message": "", + "cctx_status": { + "status": 1, + "status_message": "", + "lastUpdate_timestamp": 1709145057 + }, + "inbound_tx_params": { + "sender": "0xd91b507F2A3e2D4A32d0C86Ac19FEAD2D461008D", + "sender_chain_id": 7000, + "tx_origin": "0xb0C04e07A301927672A8A7a874DB6930576C90B8", + "coin_type": 1, + "asset": "", + "amount": "657177295293237048", + "inbound_tx_observed_hash": "0x093f4ca4c1884df0fd9dd59b75979342ded29d3c9b6861644287a2e1417b9a39", + "inbound_tx_observed_external_height": 1960153, + "inbound_tx_ballot_index": "0x541b570182950809f9b9077861a0fc7038af9a14ce8af4e151a83adfa308c7a9", + "inbound_tx_finalized_zeta_height": 0, + "tx_finalization_status": 0 + }, + "outbound_tx_params": [ + { + "receiver": "0xb0C04e07A301927672A8A7a874DB6930576C90B8", + "receiver_chainId": 13378337, + "coin_type": 1, + "amount": "657177295293237048", + "outbound_tx_tss_nonce": 68270, + "outbound_tx_gas_limit": 21000, + "outbound_tx_gas_price": "6000000000", + "outbound_tx_hash": "", + "outbound_tx_ballot_index": "", + "outbound_tx_observed_external_height": 0, + "outbound_tx_gas_used": 0, + "outbound_tx_effective_gas_price": "0", + "outbound_tx_effective_gas_limit": 0, + "tss_pubkey": "zetapub1addwnpepqtadxdyt037h86z60nl98t6zk56mw5zpnm79tsmvspln3hgt5phdc79kvfc", + "tx_finalization_status": 0 + } + ] +} diff --git a/zetaclient/testutils/mock.go b/zetaclient/testutils/mock.go deleted file mode 100644 index 92c0f6d1b2..0000000000 --- a/zetaclient/testutils/mock.go +++ /dev/null @@ -1,72 +0,0 @@ -package testutils - -import ( - "github.com/btcsuite/btcutil" - "github.com/cosmos/cosmos-sdk/types" - ethcommon "github.com/ethereum/go-ethereum/common" - "github.com/zeta-chain/zetacore/common" - "github.com/zeta-chain/zetacore/zetaclient/interfaces" - "github.com/zeta-chain/zetacore/zetaclient/keys" - "github.com/zeta-chain/zetacore/zetaclient/zetabridge" -) - -var _ interfaces.TSSSigner = (*MockTSS)(nil) - -// MockTSS is a mock of TSS signer for testing -type MockTSS struct { - evmAddress string - btcAddress string -} - -func NewMockTSS(evmAddress string, btcAddress string) *MockTSS { - return &MockTSS{ - evmAddress: evmAddress, - btcAddress: btcAddress, - } -} - -func NewMockTSSMainnet() *MockTSS { - return NewMockTSS(TSSAddressEVMMainnet, TSSAddressBTCMainnet) -} - -func NewMockTSSAthens3() *MockTSS { - return NewMockTSS(TSSAddressEVMAthens3, TSSAddressBTCAthens3) -} - -func (s *MockTSS) Sign(_ []byte, _ uint64, _ uint64, _ *common.Chain, _ string) ([65]byte, error) { - return [65]byte{}, nil -} - -func (s *MockTSS) Pubkey() []byte { - return []byte{} -} - -func (s *MockTSS) EVMAddress() ethcommon.Address { - return ethcommon.HexToAddress(s.evmAddress) -} - -func (s *MockTSS) BTCAddress() string { - return s.btcAddress -} - -func (s *MockTSS) BTCAddressWitnessPubkeyHash() *btcutil.AddressWitnessPubKeyHash { - return nil -} - -func (s *MockTSS) PubKeyCompressedBytes() []byte { - return []byte{} -} - -func MockCoreBridge() *zetabridge.ZetaCoreBridge { - bridge, err := zetabridge.NewZetaCoreBridge( - &keys.Keys{OperatorAddress: types.AccAddress{}}, - "127.0.0.1", - "", - "zetachain_7000-1", - false, - nil) - if err != nil { - panic(err) - } - return bridge -} diff --git a/zetaclient/testutils/stub/core_bridge.go b/zetaclient/testutils/stub/core_bridge.go new file mode 100644 index 0000000000..1bf06be84b --- /dev/null +++ b/zetaclient/testutils/stub/core_bridge.go @@ -0,0 +1,131 @@ +package stub + +import ( + "math/big" + + "cosmossdk.io/math" + "github.com/rs/zerolog" + "github.com/zeta-chain/go-tss/blame" + "github.com/zeta-chain/zetacore/common" + cctxtypes "github.com/zeta-chain/zetacore/x/crosschain/types" + observerTypes "github.com/zeta-chain/zetacore/x/observer/types" + "github.com/zeta-chain/zetacore/zetaclient/interfaces" + "github.com/zeta-chain/zetacore/zetaclient/keys" + "github.com/zeta-chain/zetacore/zetaclient/testutils" +) + +var _ interfaces.ZetaCoreBridger = &ZetaCoreBridge{} + +type ZetaCoreBridge struct { + zetaChain common.Chain +} + +func (z ZetaCoreBridge) PostVoteInbound(_, _ uint64, _ *cctxtypes.MsgVoteOnObservedInboundTx) (string, string, error) { + return "", "", nil +} + +func (z ZetaCoreBridge) PostVoteOutbound(_ string, _ string, _ uint64, _ uint64, _ *big.Int, _ uint64, _ *big.Int, _ common.ReceiveStatus, _ common.Chain, _ uint64, _ common.CoinType) (string, string, error) { + return "", "", nil +} + +func (z ZetaCoreBridge) PostGasPrice(_ common.Chain, _ uint64, _ string, _ uint64) (string, error) { + return "", nil +} + +func (z ZetaCoreBridge) PostAddBlockHeader(_ int64, _ []byte, _ int64, _ common.HeaderData) (string, error) { + return "", nil +} + +func (z ZetaCoreBridge) GetBlockHeaderStateByChain(_ int64) (observerTypes.QueryGetBlockHeaderStateResponse, error) { + return observerTypes.QueryGetBlockHeaderStateResponse{}, nil +} + +func (z ZetaCoreBridge) PostBlameData(_ *blame.Blame, _ int64, _ string) (string, error) { + return "", nil +} + +func (z ZetaCoreBridge) AddTxHashToOutTxTracker(_ int64, _ uint64, _ string, _ *common.Proof, _ string, _ int64) (string, error) { + return "", nil +} + +func (z ZetaCoreBridge) GetKeys() *keys.Keys { + return &keys.Keys{} +} + +func (z ZetaCoreBridge) GetBlockHeight() (int64, error) { + return 0, nil +} + +func (z ZetaCoreBridge) GetZetaBlockHeight() (int64, error) { + return 0, nil +} + +func (z ZetaCoreBridge) GetLastBlockHeightByChain(_ common.Chain) (*cctxtypes.LastBlockHeight, error) { + return &cctxtypes.LastBlockHeight{}, nil +} + +func (z ZetaCoreBridge) ListPendingCctx(_ int64) ([]*cctxtypes.CrossChainTx, uint64, error) { + return []*cctxtypes.CrossChainTx{}, 0, nil +} + +func (z ZetaCoreBridge) GetPendingNoncesByChain(_ int64) (observerTypes.PendingNonces, error) { + return observerTypes.PendingNonces{}, nil +} + +func (z ZetaCoreBridge) GetCctxByNonce(_ int64, _ uint64) (*cctxtypes.CrossChainTx, error) { + return &cctxtypes.CrossChainTx{}, nil +} + +func (z ZetaCoreBridge) GetOutTxTracker(_ common.Chain, _ uint64) (*cctxtypes.OutTxTracker, error) { + return &cctxtypes.OutTxTracker{}, nil +} + +func (z ZetaCoreBridge) GetAllOutTxTrackerByChain(_ int64, _ interfaces.Order) ([]cctxtypes.OutTxTracker, error) { + return []cctxtypes.OutTxTracker{}, nil +} + +func (z ZetaCoreBridge) GetCrosschainFlags() (observerTypes.CrosschainFlags, error) { + return observerTypes.CrosschainFlags{}, nil +} + +func (z ZetaCoreBridge) GetObserverList() ([]string, error) { + return []string{}, nil +} + +func (z ZetaCoreBridge) GetKeyGen() (*observerTypes.Keygen, error) { + return &observerTypes.Keygen{}, nil +} + +func (z ZetaCoreBridge) GetBtcTssAddress(_ int64) (string, error) { + return testutils.TSSAddressBTCMainnet, nil +} + +func (z ZetaCoreBridge) GetInboundTrackersForChain(_ int64) ([]cctxtypes.InTxTracker, error) { + return []cctxtypes.InTxTracker{}, nil +} + +func (z ZetaCoreBridge) GetLogger() *zerolog.Logger { + return nil +} + +func (z ZetaCoreBridge) ZetaChain() common.Chain { + return z.zetaChain +} + +func (z ZetaCoreBridge) Pause() { +} + +func (z ZetaCoreBridge) Unpause() { +} + +func (z ZetaCoreBridge) GetZetaHotKeyBalance() (math.Int, error) { + return math.NewInt(0), nil +} + +func NewZetaCoreBridge() *ZetaCoreBridge { + zetaChain, err := common.ZetaChainFromChainID("zetachain_7000-1") + if err != nil { + panic(err) + } + return &ZetaCoreBridge{zetaChain: zetaChain} +} diff --git a/zetaclient/testutils/stub/evm_rpc.go b/zetaclient/testutils/stub/evm_rpc.go new file mode 100644 index 0000000000..05f14fc59b --- /dev/null +++ b/zetaclient/testutils/stub/evm_rpc.go @@ -0,0 +1,98 @@ +package stub + +import ( + "math/big" + + "github.com/ethereum/go-ethereum" + ethcommon "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/zeta-chain/zetacore/zetaclient/interfaces" + "golang.org/x/net/context" +) + +const EVMRPCEnabled = "MockEVMRPCEnabled" + +// Subscription interface +var _ ethereum.Subscription = subscription{} + +type subscription struct { +} + +func (s subscription) Unsubscribe() { +} + +func (s subscription) Err() <-chan error { + return nil +} + +// EvmClient interface +var _ interfaces.EVMRPCClient = EvmClient{} + +type EvmClient struct { +} + +func (e EvmClient) SubscribeFilterLogs(_ context.Context, _ ethereum.FilterQuery, _ chan<- ethtypes.Log) (ethereum.Subscription, error) { + return subscription{}, nil +} + +func (e EvmClient) CodeAt(_ context.Context, _ ethcommon.Address, _ *big.Int) ([]byte, error) { + return []byte{}, nil +} + +func (e EvmClient) CallContract(_ context.Context, _ ethereum.CallMsg, _ *big.Int) ([]byte, error) { + return []byte{}, nil +} + +func (e EvmClient) HeaderByNumber(_ context.Context, _ *big.Int) (*ethtypes.Header, error) { + return ðtypes.Header{}, nil +} + +func (e EvmClient) PendingCodeAt(_ context.Context, _ ethcommon.Address) ([]byte, error) { + return []byte{}, nil +} + +func (e EvmClient) PendingNonceAt(_ context.Context, _ ethcommon.Address) (uint64, error) { + return 0, nil +} + +func (e EvmClient) SuggestGasPrice(_ context.Context) (*big.Int, error) { + return big.NewInt(0), nil +} + +func (e EvmClient) SuggestGasTipCap(_ context.Context) (*big.Int, error) { + return big.NewInt(0), nil +} + +func (e EvmClient) EstimateGas(_ context.Context, _ ethereum.CallMsg) (gas uint64, err error) { + gas = 0 + err = nil + return +} + +func (e EvmClient) SendTransaction(_ context.Context, _ *ethtypes.Transaction) error { + return nil +} + +func (e EvmClient) FilterLogs(_ context.Context, _ ethereum.FilterQuery) ([]ethtypes.Log, error) { + return []ethtypes.Log{}, nil +} + +func (e EvmClient) BlockNumber(_ context.Context) (uint64, error) { + return 0, nil +} + +func (e EvmClient) BlockByNumber(_ context.Context, _ *big.Int) (*ethtypes.Block, error) { + return ðtypes.Block{}, nil +} + +func (e EvmClient) TransactionByHash(_ context.Context, _ ethcommon.Hash) (tx *ethtypes.Transaction, isPending bool, err error) { + return ðtypes.Transaction{}, false, nil +} + +func (e EvmClient) TransactionReceipt(_ context.Context, _ ethcommon.Hash) (*ethtypes.Receipt, error) { + return ðtypes.Receipt{}, nil +} + +func (e EvmClient) TransactionSender(_ context.Context, _ *ethtypes.Transaction, _ ethcommon.Hash, _ uint) (ethcommon.Address, error) { + return ethcommon.Address{}, nil +} diff --git a/zetaclient/testutils/stub/tss_signer.go b/zetaclient/testutils/stub/tss_signer.go new file mode 100644 index 0000000000..da3f954588 --- /dev/null +++ b/zetaclient/testutils/stub/tss_signer.go @@ -0,0 +1,84 @@ +package stub + +import ( + "crypto/ecdsa" + "fmt" + + "github.com/btcsuite/btcutil" + ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/zeta-chain/zetacore/common" + "github.com/zeta-chain/zetacore/zetaclient/interfaces" + "github.com/zeta-chain/zetacore/zetaclient/testutils" +) + +var TestPrivateKey *ecdsa.PrivateKey + +var _ interfaces.TSSSigner = (*TSS)(nil) + +func init() { + var err error + TestPrivateKey, err = crypto.GenerateKey() + if err != nil { + fmt.Println(err.Error()) + } +} + +// TSS is a mock of TSS signer for testing +type TSS struct { + evmAddress string + btcAddress string +} + +func NewMockTSS(evmAddress string, btcAddress string) *TSS { + return &TSS{ + evmAddress: evmAddress, + btcAddress: btcAddress, + } +} + +func NewTSSMainnet() *TSS { + return NewMockTSS(testutils.TSSAddressEVMMainnet, testutils.TSSAddressBTCMainnet) +} + +func NewTSSAthens3() *TSS { + return NewMockTSS(testutils.TSSAddressEVMAthens3, testutils.TSSAddressBTCAthens3) +} + +// Sign uses test key unrelated to any tss key in production +func (s *TSS) Sign(data []byte, _ uint64, _ uint64, _ *common.Chain, _ string) ([65]byte, error) { + signature, err := crypto.Sign(data, TestPrivateKey) + if err != nil { + return [65]byte{}, err + } + var sigbyte [65]byte + _ = copy(sigbyte[:], signature[:65]) + + return sigbyte, nil +} + +// Pubkey uses the hardcoded private test key to generate the public key in bytes +func (s *TSS) Pubkey() []byte { + publicKey := TestPrivateKey.Public() + publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) + if !ok { + fmt.Println("error casting public key to ECDSA") + } + return crypto.FromECDSAPub(publicKeyECDSA) +} + +func (s *TSS) EVMAddress() ethcommon.Address { + return ethcommon.HexToAddress(s.evmAddress) +} + +func (s *TSS) BTCAddress() string { + return s.btcAddress +} + +func (s *TSS) BTCAddressWitnessPubkeyHash() *btcutil.AddressWitnessPubKeyHash { + return nil +} + +func (s *TSS) PubKeyCompressedBytes() []byte { + return []byte{} +}