diff --git a/changelog.md b/changelog.md index 909e2e9601..4a2a0da9f9 100644 --- a/changelog.md +++ b/changelog.md @@ -6,6 +6,7 @@ * [3461](https://github.com/zeta-chain/node/pull/3461) - add new `ConfirmationParams` field to chain params to enable multiple confirmation count values, deprecating `confirmation_count` * [3489](https://github.com/zeta-chain/node/pull/3489) - add Sui chain info +* [3455](https://github.com/zeta-chain/node/pull/3455) - add `track-cctx` command to zetatools ### Refactor diff --git a/cmd/zetae2e/local/bitcoin.go b/cmd/zetae2e/local/bitcoin.go index 443ab26c0c..483f489bea 100644 --- a/cmd/zetae2e/local/bitcoin.go +++ b/cmd/zetae2e/local/bitcoin.go @@ -6,6 +6,7 @@ import ( "github.com/fatih/color" "github.com/stretchr/testify/require" + "golang.org/x/sync/errgroup" "github.com/zeta-chain/node/e2e/config" "github.com/zeta-chain/node/e2e/e2etests" @@ -13,6 +14,64 @@ import ( "github.com/zeta-chain/node/testutil" ) +// startBitcoinTests starts Bitcoin related tests +func startBitcoinTests( + eg *errgroup.Group, + conf config.Config, + deployerRunner *runner.E2ERunner, + verbose bool, + light, skipBitcoinSetup bool, +) { + // start the bitcoin tests + // btc withdraw tests are those that need a Bitcoin node wallet to send UTXOs + bitcoinDepositTests := []string{ + e2etests.TestBitcoinDonationName, + e2etests.TestBitcoinDepositName, + e2etests.TestBitcoinDepositAndCallName, + e2etests.TestBitcoinDepositAndCallRevertName, + e2etests.TestBitcoinStdMemoDepositName, + e2etests.TestBitcoinStdMemoDepositAndCallName, + e2etests.TestBitcoinStdMemoDepositAndCallRevertName, + e2etests.TestBitcoinStdMemoInscribedDepositAndCallName, + e2etests.TestBitcoinDepositAndAbortWithLowDepositFeeName, + e2etests.TestCrosschainSwapName, + } + bitcoinDepositTestsAdvanced := []string{ + e2etests.TestBitcoinDepositAndCallRevertWithDustName, + e2etests.TestBitcoinStdMemoDepositAndCallRevertOtherAddressName, + e2etests.TestBitcoinDepositAndWithdrawWithDustName, + } + bitcoinWithdrawTests := []string{ + e2etests.TestBitcoinWithdrawSegWitName, + e2etests.TestBitcoinWithdrawInvalidAddressName, + e2etests.TestLegacyZetaWithdrawBTCRevertName, + } + bitcoinWithdrawTestsAdvanced := []string{ + e2etests.TestBitcoinWithdrawTaprootName, + e2etests.TestBitcoinWithdrawLegacyName, + e2etests.TestBitcoinWithdrawP2SHName, + e2etests.TestBitcoinWithdrawP2WSHName, + e2etests.TestBitcoinWithdrawMultipleName, + e2etests.TestBitcoinWithdrawRestrictedName, + } + + if !light { + // if light is enabled, only the most basic tests are run and advanced are skipped + bitcoinDepositTests = append(bitcoinDepositTests, bitcoinDepositTestsAdvanced...) + bitcoinWithdrawTests = append(bitcoinWithdrawTests, bitcoinWithdrawTestsAdvanced...) + } + bitcoinDepositTestRoutine, bitcoinWithdrawTestRoutine := bitcoinTestRoutines( + conf, + deployerRunner, + verbose, + !skipBitcoinSetup, + bitcoinDepositTests, + bitcoinWithdrawTests, + ) + eg.Go(bitcoinDepositTestRoutine) + eg.Go(bitcoinWithdrawTestRoutine) +} + // bitcoinTestRoutines returns test routines for deposit and withdraw tests func bitcoinTestRoutines( conf config.Config, diff --git a/cmd/zetae2e/local/local.go b/cmd/zetae2e/local/local.go index ad589a58a6..716acc81ad 100644 --- a/cmd/zetae2e/local/local.go +++ b/cmd/zetae2e/local/local.go @@ -291,55 +291,7 @@ func localE2ETest(cmd *cobra.Command, _ []string) { if !skipRegular { // start the EVM tests startEVMTests(&eg, conf, deployerRunner, verbose) - - // start the bitcoin tests - // btc withdraw tests are those that need a Bitcoin node wallet to send UTXOs - bitcoinDepositTests := []string{ - e2etests.TestBitcoinDonationName, - e2etests.TestBitcoinDepositName, - e2etests.TestBitcoinDepositAndCallName, - e2etests.TestBitcoinDepositAndCallRevertName, - e2etests.TestBitcoinStdMemoDepositName, - e2etests.TestBitcoinStdMemoDepositAndCallName, - e2etests.TestBitcoinStdMemoDepositAndCallRevertName, - e2etests.TestBitcoinStdMemoInscribedDepositAndCallName, - e2etests.TestBitcoinDepositAndAbortWithLowDepositFeeName, - e2etests.TestCrosschainSwapName, - } - bitcoinDepositTestsAdvanced := []string{ - e2etests.TestBitcoinDepositAndCallRevertWithDustName, - e2etests.TestBitcoinStdMemoDepositAndCallRevertOtherAddressName, - e2etests.TestBitcoinDepositAndWithdrawWithDustName, - } - bitcoinWithdrawTests := []string{ - e2etests.TestBitcoinWithdrawSegWitName, - e2etests.TestBitcoinWithdrawInvalidAddressName, - e2etests.TestLegacyZetaWithdrawBTCRevertName, - } - bitcoinWithdrawTestsAdvanced := []string{ - e2etests.TestBitcoinWithdrawTaprootName, - e2etests.TestBitcoinWithdrawLegacyName, - e2etests.TestBitcoinWithdrawP2SHName, - e2etests.TestBitcoinWithdrawP2WSHName, - e2etests.TestBitcoinWithdrawMultipleName, - e2etests.TestBitcoinWithdrawRestrictedName, - } - - if !light { - // if light is enabled, only the most basic tests are run and advanced are skipped - bitcoinDepositTests = append(bitcoinDepositTests, bitcoinDepositTestsAdvanced...) - bitcoinWithdrawTests = append(bitcoinWithdrawTests, bitcoinWithdrawTestsAdvanced...) - } - bitcoinDepositTestRoutine, bitcoinWithdrawTestRoutine := bitcoinTestRoutines( - conf, - deployerRunner, - verbose, - !skipBitcoinSetup, - bitcoinDepositTests, - bitcoinWithdrawTests, - ) - eg.Go(bitcoinDepositTestRoutine) - eg.Go(bitcoinWithdrawTestRoutine) + startBitcoinTests(&eg, conf, deployerRunner, verbose, light, skipBitcoinSetup) } if !skipPrecompiles { diff --git a/cmd/zetatool/cctx/cctx_details.go b/cmd/zetatool/cctx/cctx_details.go new file mode 100644 index 0000000000..f1162a0306 --- /dev/null +++ b/cmd/zetatool/cctx/cctx_details.go @@ -0,0 +1,178 @@ +package cctx + +import ( + "fmt" + + "github.com/zeta-chain/node/cmd/zetatool/context" + "github.com/zeta-chain/node/pkg/chains" + crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" +) + +// TrackingDetails tracks the status of a CCTX transaction +type TrackingDetails struct { + CCTXIdentifier string `json:"cctx_identifier"` + Status Status `json:"status"` + OutboundChain chains.Chain `json:"outbound_chain_id"` + OutboundTssNonce uint64 `json:"outbound_tss_nonce"` + OutboundTrackerHashList []string `json:"outbound_tracker_hash_list"` + Message string `json:"message"` +} + +func NewTrackingDetails() *TrackingDetails { + return &TrackingDetails{ + CCTXIdentifier: "", + Status: Unknown, + } +} + +// UpdateStatusFromZetacoreCCTX updates the status of the TrackingDetails from the zetacore CCTX status +func (c *TrackingDetails) UpdateStatusFromZetacoreCCTX(status crosschaintypes.CctxStatus) { + switch status { + case crosschaintypes.CctxStatus_PendingOutbound: + c.Status = PendingOutbound + case crosschaintypes.CctxStatus_OutboundMined: + c.Status = OutboundMined + case crosschaintypes.CctxStatus_Reverted: + c.Status = Reverted + case crosschaintypes.CctxStatus_PendingRevert: + c.Status = PendingRevert + case crosschaintypes.CctxStatus_Aborted: + c.Status = Aborted + default: + c.Status = Unknown + } +} + +func (c *TrackingDetails) Print() string { + return fmt.Sprintf("CCTX Identifier: %s Status: %s", c.CCTXIdentifier, c.Status.String()) +} + +func (c *TrackingDetails) DebugPrint() string { + return fmt.Sprintf("CCTX Identifier: %s Status: %s Message: %s", c.CCTXIdentifier, c.Status.String(), c.Message) +} + +// UpdateCCTXStatus updates the TrackingDetails with status from zetacore +func (c *TrackingDetails) UpdateCCTXStatus(ctx *context.Context) { + var ( + zetacoreClient = ctx.GetZetaCoreClient() + goCtx = ctx.GetContext() + ) + + CCTX, err := zetacoreClient.GetCctxByHash(goCtx, c.CCTXIdentifier) + if err != nil { + c.Message = fmt.Sprintf("failed to get cctx: %v", err) + return + } + + c.UpdateStatusFromZetacoreCCTX(CCTX.CctxStatus.Status) + + return +} + +// UpdateCCTXOutboundDetails updates the TrackingDetails with the outbound chain and nonce +func (c *TrackingDetails) UpdateCCTXOutboundDetails(ctx *context.Context) { + var ( + zetacoreClient = ctx.GetZetaCoreClient() + goCtx = ctx.GetContext() + ) + CCTX, err := zetacoreClient.GetCctxByHash(goCtx, c.CCTXIdentifier) + if err != nil { + c.Message = fmt.Sprintf("failed to get cctx: %v", err) + } + outboundParams := CCTX.GetCurrentOutboundParam() + if outboundParams == nil { + c.Message = "outbound params not found" + return + } + chainID := CCTX.GetCurrentOutboundParam().ReceiverChainId + + // This is almost impossible to happen as the cctx would not have been created if the chain was not supported + chain, found := chains.GetChainFromChainID(chainID, []chains.Chain{}) + if !found { + c.Message = fmt.Sprintf("receiver chain not supported,chain id: %d", chainID) + } + c.OutboundChain = chain + c.OutboundTssNonce = CCTX.GetCurrentOutboundParam().TssNonce + return +} + +// UpdateHashListAndPendingStatus updates the TrackingDetails with the hash list and updates pending status +// If the tracker is found, it means the outbound is broadcast, but we are waiting for the confirmations +// If the tracker is not found, it means the outbound is not broadcast yet; we are waiting for the tss to sign the outbound +func (c *TrackingDetails) UpdateHashListAndPendingStatus(ctx *context.Context) { + var ( + zetacoreClient = ctx.GetZetaCoreClient() + goCtx = ctx.GetContext() + outboundChain = c.OutboundChain + outboundNonce = c.OutboundTssNonce + ) + + tracker, err := zetacoreClient.GetOutboundTracker(goCtx, outboundChain, outboundNonce) + // the tracker is found that means the outbound has been broadcast, but we are waiting for confirmations + if err == nil && tracker != nil { + c.updateOutboundConfirmation() + var hashList []string + for _, hash := range tracker.HashList { + hashList = append(hashList, hash.TxHash) + } + c.OutboundTrackerHashList = hashList + return + } + // the cctx is in pending state, but the outbound signing has not been done + c.updateOutboundSigning() + return +} + +// IsInboundFinalized checks if the inbound voting has been finalized +func (c *TrackingDetails) IsInboundFinalized() bool { + return !(c.Status == PendingInboundConfirmation || c.Status == PendingInboundVoting) +} + +// IsPendingOutbound checks if the cctx is pending processing the outbound transaction (outbound or revert) +func (c *TrackingDetails) IsPendingOutbound() bool { + return c.Status == PendingOutbound || c.Status == PendingRevert +} + +// IsPendingConfirmation checks if the cctx is pending outbound confirmation (outbound or revert +func (c *TrackingDetails) IsPendingConfirmation() bool { + return c.Status == PendingOutboundConfirmation || c.Status == PendingRevertConfirmation +} + +// State transitions for TrackingDetails +// 0 - Inbound Confirmation +func (c *TrackingDetails) updateInboundConfirmation(isConfirmed bool) { + c.Status = PendingInboundConfirmation + if isConfirmed { + c.Status = PendingInboundVoting + } +} + +// 1 - Outbound Signing +func (c *TrackingDetails) updateOutboundSigning() { + switch { + case c.Status == PendingOutbound: + c.Status = PendingOutboundSigning + case c.Status == PendingRevert: + c.Status = PendingRevertSigning + } +} + +// 2 - Outbound Confirmation +func (c *TrackingDetails) updateOutboundConfirmation() { + switch { + case c.Status == PendingOutbound: + c.Status = PendingOutboundConfirmation + case c.Status == PendingRevert: + c.Status = PendingRevertConfirmation + } +} + +// 3 - Outbound Voting +func (c *TrackingDetails) updateOutboundVoting() { + switch { + case c.Status == PendingOutboundConfirmation: + c.Status = PendingOutboundVoting + case c.Status == PendingRevertConfirmation: + c.Status = PendingRevertVoting + } +} diff --git a/cmd/zetatool/cctx/cctx_status.go b/cmd/zetatool/cctx/cctx_status.go new file mode 100644 index 0000000000..14088dda43 --- /dev/null +++ b/cmd/zetatool/cctx/cctx_status.go @@ -0,0 +1,67 @@ +package cctx + +// Status represents the status of a CCTX transaction, it is more granular than the status present on zetacore +type Status int + +const ( + Unknown Status = iota + // Zetacore statuses + PendingOutbound Status = 1 + OutboundMined Status = 2 + PendingRevert Status = 3 + Reverted Status = 4 + Aborted Status = 5 + // Zetatool only statuses + // PendingInboundConfirmation the inbound transaction is pending confirmation on the inbound chain + PendingInboundConfirmation Status = 6 + // PendingInboundVoting the inbound transaction is confirmed on the inbound chain, and we are waiting for observers to vote + PendingInboundVoting Status = 7 + // PendingOutboundSigning the outbound transaction is pending signing by the tss + PendingOutboundSigning Status = 8 + // PendingRevertSigning the revert transaction is pending signing by the tss + PendingRevertSigning Status = 9 + // PendingOutboundConfirmation the outbound transaction + // broadcast by the tss is pending confirmation on the outbound chain + PendingOutboundConfirmation Status = 10 + // PendingRevertConfirmation the revert transaction broadcast by the tss is pending confirmation on the outbound chain + PendingRevertConfirmation Status = 11 + // PendingOutboundVoting the outbound transaction is confirmed on the outbound chain, + //and we are waiting for observers to vote + PendingOutboundVoting Status = 12 + // PendingRevertVoting the revert transaction is confirmed on the outbound chain, + //and we are waiting for observers to vote + PendingRevertVoting Status = 13 +) + +func (s Status) String() string { + switch s { + case PendingInboundConfirmation: + return "PendingInboundConfirmation" + case PendingInboundVoting: + return "PendingInboundVoting" + case PendingOutbound: + return "PendingOutbound" + case OutboundMined: + return "OutboundMined" + case PendingRevert: + return "PendingRevert" + case Reverted: + return "Reverted" + case PendingOutboundConfirmation: + return "PendingOutboundConfirmation" + case PendingRevertConfirmation: + return "PendingRevertConfirmation" + case PendingRevertVoting: + return "PendingRevertVoting" + case Aborted: + return "Aborted" + case PendingOutboundSigning: + return "PendingOutboundSigning" + case PendingRevertSigning: + return "PendingRevertSigning" + case PendingOutboundVoting: + return "PendingOutboundVoting" + default: + return "Unknown" + } +} diff --git a/cmd/zetatool/cctx/inbound.go b/cmd/zetatool/cctx/inbound.go new file mode 100644 index 0000000000..fdd591589b --- /dev/null +++ b/cmd/zetatool/cctx/inbound.go @@ -0,0 +1,373 @@ +package cctx + +import ( + "fmt" + "strings" + + ethcommon "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/gagliardetto/solana-go" + solrpc "github.com/gagliardetto/solana-go/rpc" + "github.com/zeta-chain/protocol-contracts/pkg/erc20custody.sol" + "github.com/zeta-chain/protocol-contracts/pkg/gatewayevm.sol" + "github.com/zeta-chain/protocol-contracts/pkg/zetaconnector.non-eth.sol" + + zetatoolchains "github.com/zeta-chain/node/cmd/zetatool/chains" + "github.com/zeta-chain/node/cmd/zetatool/context" + "github.com/zeta-chain/node/pkg/chains" + solanacontracts "github.com/zeta-chain/node/pkg/contracts/solana" + crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" + "github.com/zeta-chain/node/x/observer/types" + "github.com/zeta-chain/node/zetaclient/chains/bitcoin/client" + zetaevmclient "github.com/zeta-chain/node/zetaclient/chains/evm/client" + "github.com/zeta-chain/node/zetaclient/chains/solana/observer" + solanarpc "github.com/zeta-chain/node/zetaclient/chains/solana/rpc" + zetaclientConfig "github.com/zeta-chain/node/zetaclient/config" +) + +// CheckInbound checks the inbound chain,gets the inbound ballot identifier and updates the TrackingDetails +func (c *TrackingDetails) CheckInbound(ctx *context.Context) error { + var ( + inboundChain = ctx.GetInboundChain() + err error + ) + + switch { + case inboundChain.IsZetaChain(): + { + err = c.zevmInboundBallotIdentifier(ctx) + if err != nil { + return fmt.Errorf( + "failed to get inbound ballot for zeta chain %d, %w", + inboundChain.ChainId, + err, + ) + } + } + + case inboundChain.IsEVMChain(): + { + err = c.evmInboundBallotIdentifier(ctx) + if err != nil { + return fmt.Errorf( + "failed to get inbound ballot for evm chain %d, %w", + inboundChain.ChainId, + err, + ) + } + } + case inboundChain.IsBitcoinChain(): + { + err = c.btcInboundBallotIdentifier(ctx) + if err != nil { + return fmt.Errorf( + "failed to get inbound ballot for bitcoin chain %d, %w", + inboundChain.ChainId, + err, + ) + } + } + case inboundChain.IsSolanaChain(): + { + err = c.solanaInboundBallotIdentifier(ctx) + if err != nil { + return fmt.Errorf( + "failed to get inbound ballot for solana chain %d, %w", + inboundChain.ChainId, + err, + ) + } + } + default: + return fmt.Errorf("unsupported chain type %d", inboundChain.ChainId) + } + return nil +} + +// btcInboundBallotIdentifier gets the inbound ballot identifier for the inbound hash from bitcoin chain +func (c *TrackingDetails) btcInboundBallotIdentifier(ctx *context.Context) error { + var ( + inboundHash = ctx.GetInboundHash() + inboundChain = ctx.GetInboundChain() + zetacoreClient = ctx.GetZetaCoreClient() + zetaChainID = ctx.GetConfig().ZetaChainID + cfg = ctx.GetConfig() + logger = ctx.GetLogger() + goCtx = ctx.GetContext() + ) + + params, err := chains.BitcoinNetParamsFromChainID(inboundChain.ChainId) + if err != nil { + return fmt.Errorf("unable to get bitcoin net params from chain id: %w", err) + } + + connCfg := zetaclientConfig.BTCConfig{ + RPCUsername: cfg.BtcUser, + RPCPassword: cfg.BtcPassword, + RPCHost: cfg.BtcHost, + RPCParams: params.Name, + } + + rpcClient, err := client.New(connCfg, inboundChain.ChainId, logger) + if err != nil { + return fmt.Errorf("unable to create rpc client: %w", err) + } + + err = rpcClient.Ping(goCtx) + if err != nil { + return fmt.Errorf("error ping the bitcoin server: %w", err) + } + + res, err := zetacoreClient.Observer.GetTssAddress(goCtx, &types.QueryGetTssAddressRequest{}) + if err != nil { + return fmt.Errorf("failed to get tss address: %w", err) + } + tssBtcAddress := res.GetBtc() + + chainParams, err := zetacoreClient.GetChainParamsForChainID(goCtx, inboundChain.ChainId) + if err != nil { + return fmt.Errorf("failed to get chain params: %w", err) + } + + cctxIdentifier, isConfirmed, err := zetatoolchains.BitcoinBallotIdentifier( + ctx, + rpcClient, + params, + tssBtcAddress, + inboundHash, + inboundChain.ChainId, + zetaChainID, + chainParams.ConfirmationCount, + ) + if err != nil { + return fmt.Errorf("failed to get bitcoin ballot identifier: %w", err) + } + c.CCTXIdentifier = cctxIdentifier + c.updateInboundConfirmation(isConfirmed) + return nil +} + +// evmInboundBallotIdentifier gets the inbound ballot identifier for the inbound hash from evm chain +func (c *TrackingDetails) evmInboundBallotIdentifier(ctx *context.Context) error { + var ( + inboundHash = ctx.GetInboundHash() + inboundChain = ctx.GetInboundChain() + zetacoreClient = ctx.GetZetaCoreClient() + zetaChainID = ctx.GetConfig().ZetaChainID + goCtx = ctx.GetContext() + ) + + chainParams, err := zetacoreClient.GetChainParamsForChainID(goCtx, inboundChain.ChainId) + if err != nil { + return fmt.Errorf("failed to get chain params: %w", err) + } + + evmClient, err := zetatoolchains.GetEvmClient(ctx, inboundChain) + if err != nil { + return fmt.Errorf("failed to create evm client: %w", err) + } + // create evm client for the observation chain + tx, receipt, err := zetatoolchains.GetEvmTx(ctx, evmClient, inboundHash, inboundChain) + if err != nil { + return fmt.Errorf("failed to get tx: %w", err) + } + // Signer is unused + zetaEvmClient := zetaevmclient.New(evmClient, ethtypes.NewLondonSigner(tx.ChainId())) + isConfirmed, err := zetaEvmClient.IsTxConfirmed(goCtx, inboundHash, chainParams.ConfirmationCount) + if err != nil { + return fmt.Errorf("unable to confirm tx: %w", err) + } + res, err := zetacoreClient.Observer.GetTssAddress(goCtx, &types.QueryGetTssAddressRequest{}) + if err != nil { + return fmt.Errorf("failed to get tss address: %w", err) + } + tssEthAddress := res.GetEth() + + if tx.To() == nil { + return fmt.Errorf("invalid transaction,to field is empty %s", inboundHash) + } + + msg := &crosschaintypes.MsgVoteInbound{} + // Create inbound vote message based on the cointype and protocol version + + switch { + case compareAddress(tx.To().Hex(), chainParams.ConnectorContractAddress): + { + // build inbound vote message and post vote + addrConnector := ethcommon.HexToAddress(chainParams.ConnectorContractAddress) + connector, err := zetaconnector.NewZetaConnectorNonEth(addrConnector, evmClient) + if err != nil { + return fmt.Errorf("failed to get connector contract: %w", err) + } + for _, log := range receipt.Logs { + event, err := connector.ParseZetaSent(*log) + if err == nil && event != nil { + msg = zetatoolchains.ZetaTokenVoteV1(event, inboundChain.ChainId) + } + } + } + case compareAddress(tx.To().Hex(), chainParams.Erc20CustodyContractAddress): + { + addrCustody := ethcommon.HexToAddress(chainParams.Erc20CustodyContractAddress) + custody, err := erc20custody.NewERC20Custody(addrCustody, evmClient) + if err != nil { + return fmt.Errorf("failed to get custody contract: %w", err) + } + sender, err := evmClient.TransactionSender(goCtx, tx, receipt.BlockHash, receipt.TransactionIndex) + if err != nil { + return fmt.Errorf("failed to get tx sender: %w", err) + } + for _, log := range receipt.Logs { + zetaDeposited, err := custody.ParseDeposited(*log) + if err == nil && zetaDeposited != nil { + msg = zetatoolchains.Erc20VoteV1(zetaDeposited, sender, inboundChain.ChainId, zetaChainID) + } + } + } + case compareAddress(tx.To().Hex(), tssEthAddress): + { + if receipt.Status != ethtypes.ReceiptStatusSuccessful { + return fmt.Errorf("tx failed on chain %d", inboundChain.ChainId) + } + sender, err := evmClient.TransactionSender(goCtx, tx, receipt.BlockHash, receipt.TransactionIndex) + if err != nil { + return fmt.Errorf("failed to get tx sender: %w", err) + } + msg = zetatoolchains.GasVoteV1(tx, sender, receipt.BlockNumber.Uint64(), inboundChain.ChainId, zetaChainID) + } + case compareAddress(tx.To().Hex(), chainParams.GatewayAddress): + { + gatewayAddr := ethcommon.HexToAddress(chainParams.GatewayAddress) + gateway, err := gatewayevm.NewGatewayEVM(gatewayAddr, evmClient) + if err != nil { + return fmt.Errorf("failed to get gateway contract: %w", err) + } + for _, log := range receipt.Logs { + if log == nil || log.Address != gatewayAddr { + continue + } + eventDeposit, err := gateway.ParseDeposited(*log) + if err == nil { + msg = zetatoolchains.DepositInboundVoteV2(eventDeposit, inboundChain.ChainId, zetaChainID) + break + } + eventDepositAndCall, err := gateway.ParseDepositedAndCalled(*log) + if err == nil { + msg = zetatoolchains.DepositAndCallInboundVoteV2( + eventDepositAndCall, + inboundChain.ChainId, + zetaChainID, + ) + break + } + eventCall, err := gateway.ParseCalled(*log) + if err == nil { + msg = zetatoolchains.CallInboundVoteV2(eventCall, inboundChain.ChainId, zetaChainID) + break + } + } + } + default: + return fmt.Errorf( + "irrelevant transaction , not sent to any known address txHash: %s to address %s", + inboundHash, + tx.To(), + ) + } + + c.CCTXIdentifier = msg.Digest() + c.updateInboundConfirmation(isConfirmed) + return nil +} + +// solanaInboundBallotIdentifier gets the inbound ballot identifier for the inbound hash from solana chain +func (c *TrackingDetails) solanaInboundBallotIdentifier(ctx *context.Context) error { + var ( + inboundHash = ctx.GetInboundHash() + inboundChain = ctx.GetInboundChain() + zetacoreClient = ctx.GetZetaCoreClient() + zetaChainID = ctx.GetConfig().ZetaChainID + cfg = ctx.GetConfig() + logger = ctx.GetLogger() + goCtx = ctx.GetContext() + ) + solClient := solrpc.New(cfg.SolanaRPC) + if solClient == nil { + return fmt.Errorf("error creating rpc client") + } + + signature, err := solana.SignatureFromBase58(inboundHash) + if err != nil { + return fmt.Errorf("error parsing signature: %w", err) + } + + txResult, err := solanarpc.GetTransaction(goCtx, solClient, signature) + if err != nil { + return fmt.Errorf("error getting transaction: %w", err) + } + + chainParams, err := zetacoreClient.GetChainParamsForChainID(goCtx, inboundChain.ChainId) + if err != nil { + return fmt.Errorf("failed to get chain params: %w", err) + } + + gatewayID, _, err := solanacontracts.ParseGatewayWithPDA(chainParams.GatewayAddress) + if err != nil { + return fmt.Errorf("cannot parse gateway address: %s, err: %w", chainParams.GatewayAddress, err) + } + + events, err := observer.FilterInboundEvents(txResult, + gatewayID, + inboundChain.ChainId, + logger, + ) + + if err != nil { + return fmt.Errorf("failed to filter solana inbound events: %w", err) + } + + msg := &crosschaintypes.MsgVoteInbound{} + + // build inbound vote message from events and post to zetacore + for _, event := range events { + msg, err = zetatoolchains.VoteMsgFromSolEvent(event, zetaChainID) + if err != nil { + return fmt.Errorf("failed to create vote message: %w", err) + } + } + + c.CCTXIdentifier = msg.Digest() + c.Status = PendingInboundVoting + + return nil +} + +// zevmInboundBallotIdentifier gets the inbound ballot identifier for the inbound hash from zetachain +func (c *TrackingDetails) zevmInboundBallotIdentifier(ctx *context.Context) error { + var ( + inboundHash = ctx.GetInboundHash() + zetacoreClient = ctx.GetZetaCoreClient() + goCtx = ctx.GetContext() + ) + + inboundHashToCCTX, err := zetacoreClient.Crosschain.InboundHashToCctx( + goCtx, &crosschaintypes.QueryGetInboundHashToCctxRequest{ + InboundHash: inboundHash, + }) + if err != nil { + return fmt.Errorf("inbound chain is zetachain , cctx should be available in the same block: %w", err) + } + if len(inboundHashToCCTX.InboundHashToCctx.CctxIndex) < 1 { + return fmt.Errorf("inbound hash does not have any cctx linked %s", inboundHash) + } + + c.CCTXIdentifier = inboundHashToCCTX.InboundHashToCctx.CctxIndex[0] + c.Status = PendingOutbound + return nil +} + +func compareAddress(a string, b string) bool { + lowerA := strings.ToLower(a) + lowerB := strings.ToLower(b) + return strings.EqualFold(lowerA, lowerB) +} diff --git a/cmd/zetatool/cctx/inbound_test.go b/cmd/zetatool/cctx/inbound_test.go new file mode 100644 index 0000000000..33ae9c8ed1 --- /dev/null +++ b/cmd/zetatool/cctx/inbound_test.go @@ -0,0 +1,71 @@ +package cctx_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + "github.com/zeta-chain/node/cmd/zetatool/cctx" + zetatoolcontext "github.com/zeta-chain/node/cmd/zetatool/context" + "github.com/zeta-chain/node/pkg/chains" +) + +func Test_InboundBallotIdentifier(t *testing.T) { + tt := []struct { + name string + inboundHash string + inboundChainID int64 + expectedBallotIdentifier string + expectError bool + }{ + { + name: chains.Ethereum.Name, + inboundHash: "0x61008d7f79b2955a15e3cb95154a80e19c7385993fd0e083ff0cbe0b0f56cb9a", + inboundChainID: chains.Ethereum.ChainId, + expectedBallotIdentifier: "0xae189ab5cd884af784835297ac43eb55deb8a7800023534c580f44ee2b3eb5ed", + expectError: false, + }, + { + name: chains.BaseMainnet.Name, + inboundHash: "0x88ee0943863fd8649546eb3affaf725f8caf09f44ebc5aa64de592b2edf378c8", + inboundChainID: chains.BaseMainnet.ChainId, + expectedBallotIdentifier: "0xe2b4c3f5dbef8fb7feb14bdf0a3f63ca7018678ecb6ae99ff697ccd962932ca2", + expectError: false, + }, + { + name: chains.BscMainnet.Name, + inboundHash: "0xfa18cbcdbf70e987600647ee77a1a28f5ca707acf9b72462fada02fff2a94d2f", + inboundChainID: chains.BscMainnet.ChainId, + expectedBallotIdentifier: "0xc7b289172db825b3c0490f263f35c8596b6f1fab8ec4c44db46de3020fe9e6e6", + expectError: false, + }, + { + name: chains.Polygon.Name, + inboundHash: "0x70b9b3ba89ff647257ab0085d90d60dc99b693c66931c4535e117b66a25236ce", + inboundChainID: chains.Polygon.ChainId, + expectedBallotIdentifier: "0xf8ed419d9798aed83070763355628e2638ae9a4a47aa9c93ffc32f4b72c9fef4", + expectError: false, + }, + { + name: chains.SolanaMainnet.Name, + inboundHash: "5oj38HmTH4k2NSsqHK9oRrLjpPNBkm17dNXHFsaT6cTuJQRPWTCGqsPpRumPEbpL2B6Wuv51M69WoJwM24864PjB", + inboundChainID: chains.SolanaMainnet.ChainId, + expectedBallotIdentifier: "0xfb5f2adc2a23c301d3231613284d937f6f45cc7b1139011abbc8486de7fcbd5f", + expectError: false, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + ctx, err := zetatoolcontext.NewContext(context.Background(), tc.inboundChainID, tc.inboundHash, "") + require.NoError(t, err) + c := cctx.NewTrackingDetails() + err = c.CheckInbound(ctx) + require.NoError(t, err) + if !tc.expectError && c.CCTXIdentifier != tc.expectedBallotIdentifier { + t.Errorf("expected %s, got %s", tc.expectedBallotIdentifier, c.CCTXIdentifier) + } + }) + } + +} diff --git a/cmd/zetatool/cctx/outbound.go b/cmd/zetatool/cctx/outbound.go new file mode 100644 index 0000000000..fb4f18992d --- /dev/null +++ b/cmd/zetatool/cctx/outbound.go @@ -0,0 +1,173 @@ +package cctx + +import ( + "fmt" + + "github.com/btcsuite/btcd/chaincfg/chainhash" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/gagliardetto/solana-go" + solrpc "github.com/gagliardetto/solana-go/rpc" + + zetatoolchains "github.com/zeta-chain/node/cmd/zetatool/chains" + "github.com/zeta-chain/node/cmd/zetatool/context" + "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/zetaclient/chains/bitcoin/client" + zetaevmclient "github.com/zeta-chain/node/zetaclient/chains/evm/client" + solanarpc "github.com/zeta-chain/node/zetaclient/chains/solana/rpc" + zetaclientConfig "github.com/zeta-chain/node/zetaclient/config" +) + +func (c *TrackingDetails) CheckOutbound(ctx *context.Context) error { + var ( + outboundChain = ctx.GetInboundChain() + ) + + // We do not need to handle the case for zeta chain as the outbound is confirmed in the same block. + switch { + case outboundChain.IsEVMChain(): + return c.checkEvmOutboundTx(ctx) + case outboundChain.IsBitcoinChain(): + return c.checkBitcoinOutboundTx(ctx) + case outboundChain.IsSolanaChain(): + return c.checkSolanaOutboundTx(ctx) + default: + return fmt.Errorf("unsupported outbound chain") + } +} + +// checkEvmOutboundTx checks if the outbound transaction is confirmed on the outbound chain. +// If it's confirmed, we update the status to PendingOutboundVoting or PendingRevertVoting. Which means that the confirmation is done and we are not waiting for observers to vote +// Transition Status PendingConfirmation -> Status PendingVoting +func (c *TrackingDetails) checkEvmOutboundTx(ctx *context.Context) error { + var ( + txHashList = c.OutboundTrackerHashList + outboundChain = c.OutboundChain + zetacoreClient = ctx.GetZetaCoreClient() + goCtx = ctx.GetContext() + ) + + chainParams, err := zetacoreClient.GetChainParamsForChainID(goCtx, outboundChain.ChainId) + if err != nil { + return fmt.Errorf("failed to get chain params: %w", err) + } + + // create evm client for the observation chain + evmClient, err := zetatoolchains.GetEvmClient(ctx, outboundChain) + if err != nil { + return fmt.Errorf("failed to create evm client: %w", err) + } + + foundConfirmedTx := false + + // If one of the hash is confirmed, we update the status to pending voting + // There might be a condition where we have multiple txs and the wrong tx is confirmed. + // To verify that we need, check CCTX data + for _, hash := range txHashList { + tx, _, err := zetatoolchains.GetEvmTx(ctx, evmClient, hash, outboundChain) + if err != nil { + continue + } + // Signer is unused + c := zetaevmclient.New(evmClient, ethtypes.NewLondonSigner(tx.ChainId())) + confirmed, err := c.IsTxConfirmed(goCtx, hash, chainParams.ConfirmationCount) + if err != nil { + continue + } + if confirmed { + foundConfirmedTx = true + break + } + } + if foundConfirmedTx { + c.updateOutboundVoting() + } + return nil +} + +func (c *TrackingDetails) checkSolanaOutboundTx(ctx *context.Context) error { + var ( + txHashList = c.OutboundTrackerHashList + goCtx = ctx.GetContext() + cfg = ctx.GetConfig() + ) + + foundConfirmedTx := false + solClient := solrpc.New(cfg.SolanaRPC) + if solClient == nil { + return fmt.Errorf("error creating rpc client") + } + for _, hash := range txHashList { + signature := solana.MustSignatureFromBase58(hash) + _, err := solanarpc.GetTransaction(goCtx, solClient, signature) + if err != nil { + continue + } + foundConfirmedTx = true + } + + if foundConfirmedTx { + c.updateOutboundVoting() + } + return nil +} + +func (c *TrackingDetails) checkBitcoinOutboundTx(ctx *context.Context) error { + var ( + txHashList = c.OutboundTrackerHashList + outboundChain = c.OutboundChain + zetacoreClient = ctx.GetZetaCoreClient() + goCtx = ctx.GetContext() + cfg = ctx.GetConfig() + logger = ctx.GetLogger() + ) + + chainParams, err := zetacoreClient.GetChainParamsForChainID(goCtx, outboundChain.ChainId) + if err != nil { + return fmt.Errorf("failed to get chain params: %w", err) + } + confirmationCount := chainParams.ConfirmationCount + + params, err := chains.BitcoinNetParamsFromChainID(outboundChain.ChainId) + if err != nil { + return fmt.Errorf("unable to get bitcoin net params from chain id: %w", err) + } + + connCfg := zetaclientConfig.BTCConfig{ + RPCUsername: cfg.BtcUser, + RPCPassword: cfg.BtcPassword, + RPCHost: cfg.BtcHost, + RPCParams: params.Name, + } + + btcClient, err := client.New(connCfg, outboundChain.ChainId, logger) + if err != nil { + return fmt.Errorf("unable to create rpc client: %w", err) + } + + err = btcClient.Ping(goCtx) + if err != nil { + return fmt.Errorf("error ping the bitcoin server: %w", err) + } + + foundConfirmedTx := false + + for _, hash := range txHashList { + txHash, err := chainhash.NewHashFromStr(hash) + if err != nil { + continue + } + tx, err := btcClient.GetRawTransactionVerbose(goCtx, txHash) + if err != nil { + continue + } + + if tx.Confirmations >= confirmationCount { + foundConfirmedTx = true + } + } + + if foundConfirmedTx { + c.updateOutboundVoting() + } + return nil +} diff --git a/cmd/zetatool/chains/bitcoin.go b/cmd/zetatool/chains/bitcoin.go new file mode 100644 index 0000000000..9962cb857c --- /dev/null +++ b/cmd/zetatool/chains/bitcoin.go @@ -0,0 +1,182 @@ +package chains + +import ( + "encoding/hex" + "fmt" + "math/big" + + cosmosmath "cosmossdk.io/math" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/rs/zerolog" + + "github.com/zeta-chain/node/cmd/zetatool/context" + "github.com/zeta-chain/node/pkg/coin" + "github.com/zeta-chain/node/pkg/memo" + crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" + "github.com/zeta-chain/node/zetaclient/chains/bitcoin/client" + "github.com/zeta-chain/node/zetaclient/chains/bitcoin/common" + zetaclientObserver "github.com/zeta-chain/node/zetaclient/chains/bitcoin/observer" +) + +func BitcoinBallotIdentifier( + ctx *context.Context, + btcClient *client.Client, + params *chaincfg.Params, + tss string, + txHash string, + senderChainID int64, + zetacoreChainID int64, + confirmationCount uint64, +) (cctxIdentifier string, isConfirmed bool, err error) { + var ( + goCtx = ctx.GetContext() + ) + + hash, err := chainhash.NewHashFromStr(txHash) + if err != nil { + return + } + tx, err := btcClient.GetRawTransactionVerbose(goCtx, hash) + if err != nil { + return + } + + if tx.Confirmations >= confirmationCount { + isConfirmed = true + } + + blockHash, err := chainhash.NewHashFromStr(tx.BlockHash) + if err != nil { + return + } + + blockVb, err := btcClient.GetBlockVerbose(goCtx, blockHash) + if err != nil { + return + } + + event, err := zetaclientObserver.GetBtcEvent( + goCtx, + btcClient, + *tx, + tss, + uint64(blockVb.Height), // #nosec G115 always positive + zerolog.New(zerolog.Nop()), + params, + common.CalcDepositorFee, + ) + if err != nil { + return + } + if event == nil { + err = fmt.Errorf("no event built for btc sent to TSS") + return + } + + cctxIdentifier, err = identifierFromBtcEvent(event, senderChainID, zetacoreChainID) + return +} + +func identifierFromBtcEvent(event *zetaclientObserver.BTCInboundEvent, + senderChainID int64, + zetacoreChainID int64) (cctxIdentifier string, err error) { + // decode event memo bytes + err = event.DecodeMemoBytes(senderChainID) + if err != nil { + return + } + + // convert the amount to integer (satoshis) + amountSats, err := common.GetSatoshis(event.Value) + if err != nil { + return + } + amountInt := big.NewInt(amountSats) + + var msg *crosschaintypes.MsgVoteInbound + switch event.MemoStd { + case nil: + { + msg = voteFromLegacyMemo(event, amountInt, senderChainID, zetacoreChainID) + } + default: + { + msg = voteFromStdMemo(event, amountInt, senderChainID, zetacoreChainID) + } + } + if msg == nil { + return + } + + cctxIdentifier = msg.Digest() + return +} + +// NewInboundVoteFromLegacyMemo creates a MsgVoteInbound message for inbound that uses legacy memo +func voteFromLegacyMemo( + event *zetaclientObserver.BTCInboundEvent, + amountSats *big.Int, + senderChainID int64, + zetacoreChainID int64, +) *crosschaintypes.MsgVoteInbound { + message := hex.EncodeToString(event.MemoBytes) + + return crosschaintypes.NewMsgVoteInbound( + "", + event.FromAddress, + senderChainID, + event.FromAddress, + event.ToAddress, + zetacoreChainID, + cosmosmath.NewUintFromBigInt(amountSats), + message, + event.TxHash, + event.BlockNumber, + 0, + coin.CoinType_Gas, + "", + 0, + crosschaintypes.ProtocolContractVersion_V2, + false, // not relevant for v1 + crosschaintypes.InboundStatus_SUCCESS, + crosschaintypes.WithCrossChainCall(len(event.MemoBytes) > 0), + ) +} + +func voteFromStdMemo( + event *zetaclientObserver.BTCInboundEvent, + amountSats *big.Int, + senderChainID int64, + zetacoreChainID int64, +) *crosschaintypes.MsgVoteInbound { + // zetacore will create a revert outbound that points to the custom revert address. + revertOptions := crosschaintypes.RevertOptions{ + RevertAddress: event.MemoStd.RevertOptions.RevertAddress, + } + + // check if the memo is a cross-chain call, or simple token deposit + isCrosschainCall := event.MemoStd.OpCode == memo.OpCodeCall || event.MemoStd.OpCode == memo.OpCodeDepositAndCall + + return crosschaintypes.NewMsgVoteInbound( + "", + event.FromAddress, + senderChainID, + event.FromAddress, + event.ToAddress, + zetacoreChainID, + cosmosmath.NewUintFromBigInt(amountSats), + hex.EncodeToString(event.MemoStd.Payload), + event.TxHash, + event.BlockNumber, + 0, + coin.CoinType_Gas, + "", + 0, + crosschaintypes.ProtocolContractVersion_V2, + false, // not relevant for v1 + event.Status, + crosschaintypes.WithRevertOptions(revertOptions), + crosschaintypes.WithCrossChainCall(isCrosschainCall), + ) +} diff --git a/cmd/zetatool/inbound/evm.go b/cmd/zetatool/chains/evm.go similarity index 54% rename from cmd/zetatool/inbound/evm.go rename to cmd/zetatool/chains/evm.go index 97da3a39e1..8cdb1f023c 100644 --- a/cmd/zetatool/inbound/evm.go +++ b/cmd/zetatool/chains/evm.go @@ -1,8 +1,7 @@ -package inbound +package chains import ( "bytes" - "context" "encoding/base64" "encoding/hex" "fmt" @@ -17,19 +16,17 @@ import ( "github.com/zeta-chain/protocol-contracts/pkg/zetaconnector.non-eth.sol" "github.com/zeta-chain/node/cmd/zetatool/config" + "github.com/zeta-chain/node/cmd/zetatool/context" "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/pkg/coin" "github.com/zeta-chain/node/pkg/constant" "github.com/zeta-chain/node/pkg/crypto" - "github.com/zeta-chain/node/pkg/rpc" crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" - "github.com/zeta-chain/node/x/observer/types" - evmclient "github.com/zeta-chain/node/zetaclient/chains/evm/client" clienttypes "github.com/zeta-chain/node/zetaclient/types" "github.com/zeta-chain/node/zetaclient/zetacore" ) -func resolveRPC(chain chains.Chain, cfg config.Config) string { +func resolveRPC(chain chains.Chain, cfg *config.Config) string { return map[chains.Network]string{ chains.Network_eth: cfg.EthereumRPC, chains.Network_base: cfg.BaseRPC, @@ -38,162 +35,42 @@ func resolveRPC(chain chains.Chain, cfg config.Config) string { }[chain.Network] } -func evmInboundBallotIdentifier(ctx context.Context, - cfg config.Config, - zetacoreClient rpc.Clients, - inboundHash string, - inboundChain chains.Chain, - zetaChainID int64) (string, error) { - evmRRC := resolveRPC(inboundChain, cfg) +func GetEvmClient(ctx *context.Context, chain chains.Chain) (*ethclient.Client, error) { + evmRRC := resolveRPC(chain, ctx.GetConfig()) if evmRRC == "" { - return "", fmt.Errorf("rpc not found for chain %d network %s", inboundChain.ChainId, inboundChain.Network) + return nil, fmt.Errorf("rpc not found for chain %d network %s", chain.ChainId, chain.Network) } rpcClient, err := ethrpc.DialHTTP(evmRRC) if err != nil { - return "", fmt.Errorf("failed to connect to eth rpc: %w", err) - } - evmClient := ethclient.NewClient(rpcClient) - - // create evm client for the observation chain - tx, receipt, err := getEvmTx(ctx, evmClient, inboundHash, inboundChain) - if err != nil { - return "", fmt.Errorf("failed to get tx: %w", err) - } - - chainParams, err := zetacoreClient.GetChainParamsForChainID(context.Background(), inboundChain.ChainId) - if err != nil { - return "", fmt.Errorf("failed to get chain params: %w", err) - } - - res, err := zetacoreClient.Observer.GetTssAddress(context.Background(), &types.QueryGetTssAddressRequest{}) - if err != nil { - return "", fmt.Errorf("failed to get tss address: %w", err) - } - tssEthAddress := res.GetEth() - - if tx.To() == nil { - return "", fmt.Errorf("invalid transaction,to field is empty %s", inboundHash) - } - - confirmationMessage := "" - - // Signer is unused - c := evmclient.New(evmClient, ethtypes.NewLondonSigner(tx.ChainId())) - confirmed, err := c.IsTxConfirmed(ctx, inboundHash, chainParams.ConfirmationCount) - if err != nil { - return "", fmt.Errorf("unable to confirm tx: %w", err) - } - if !confirmed { - confirmationMessage = fmt.Sprintf("tx might not be confirmed on chain %d", inboundChain.ChainId) - } - - msg := &crosschaintypes.MsgVoteInbound{} - // Create inbound vote message based on the cointype and protocol version - switch tx.To().Hex() { - case chainParams.ConnectorContractAddress: - { - // build inbound vote message and post vote - addrConnector := ethcommon.HexToAddress(chainParams.ConnectorContractAddress) - connector, err := zetaconnector.NewZetaConnectorNonEth(addrConnector, evmClient) - if err != nil { - return "", fmt.Errorf("failed to get connector contract: %w", err) - } - for _, log := range receipt.Logs { - event, err := connector.ParseZetaSent(*log) - if err == nil && event != nil { - msg = zetaTokenVoteV1(event, inboundChain.ChainId) - } - } - } - case chainParams.Erc20CustodyContractAddress: - { - addrCustody := ethcommon.HexToAddress(chainParams.Erc20CustodyContractAddress) - custody, err := erc20custody.NewERC20Custody(addrCustody, evmClient) - if err != nil { - return "", fmt.Errorf("failed to get custody contract: %w", err) - } - sender, err := evmClient.TransactionSender(ctx, tx, receipt.BlockHash, receipt.TransactionIndex) - if err != nil { - return "", fmt.Errorf("failed to get tx sender: %w", err) - } - for _, log := range receipt.Logs { - zetaDeposited, err := custody.ParseDeposited(*log) - if err == nil && zetaDeposited != nil { - msg = erc20VoteV1(zetaDeposited, sender, inboundChain.ChainId, zetaChainID) - } - } - } - case tssEthAddress: - { - if receipt.Status != ethtypes.ReceiptStatusSuccessful { - return "", fmt.Errorf("tx failed on chain %d", inboundChain.ChainId) - } - sender, err := evmClient.TransactionSender(ctx, tx, receipt.BlockHash, receipt.TransactionIndex) - if err != nil { - return "", fmt.Errorf("failed to get tx sender: %w", err) - } - msg = gasVoteV1(tx, sender, receipt.BlockNumber.Uint64(), inboundChain.ChainId, zetaChainID) - } - case chainParams.GatewayAddress: - { - gatewayAddr := ethcommon.HexToAddress(chainParams.GatewayAddress) - gateway, err := gatewayevm.NewGatewayEVM(gatewayAddr, evmClient) - if err != nil { - return "", fmt.Errorf("failed to get gateway contract: %w", err) - } - for _, log := range receipt.Logs { - if log == nil || log.Address != gatewayAddr { - continue - } - eventDeposit, err := gateway.ParseDeposited(*log) - if err == nil { - msg = depositInboundVoteV2(eventDeposit, inboundChain.ChainId, zetaChainID) - return msg.Digest(), nil - } - eventDepositAndCall, err := gateway.ParseDepositedAndCalled(*log) - if err == nil { - msg = depositAndCallInboundVoteV2(eventDepositAndCall, inboundChain.ChainId, zetaChainID) - return msg.Digest(), nil - } - eventCall, err := gateway.ParseCalled(*log) - if err == nil { - msg = callInboundVoteV2(eventCall, inboundChain.ChainId, zetaChainID) - } - } - } - default: - return "", fmt.Errorf("irrelevant transaction , not sent to any known address txHash: %s", inboundHash) - } - - if confirmationMessage != "" { - return fmt.Sprintf("ballot identifier: %s warning: %s", msg.Digest(), confirmationMessage), nil + return nil, fmt.Errorf("failed to connect to eth rpc: %w", err) } - return fmt.Sprintf("ballot identifier: %s", msg.Digest()), nil + return ethclient.NewClient(rpcClient), nil } -func getEvmTx( - ctx context.Context, +func GetEvmTx( + ctx *context.Context, evmClient *ethclient.Client, inboundHash string, - inboundChain chains.Chain, + chain chains.Chain, ) (*ethtypes.Transaction, *ethtypes.Receipt, error) { + goCtx := ctx.GetContext() // Fetch transaction from the inbound hash := ethcommon.HexToHash(inboundHash) - tx, isPending, err := evmClient.TransactionByHash(ctx, hash) + tx, isPending, err := evmClient.TransactionByHash(goCtx, hash) if err != nil { - return nil, nil, fmt.Errorf("tx not found on chain: %w,chainID: %d", err, inboundChain.ChainId) + return nil, nil, fmt.Errorf("tx not found on chain: %w,chainID: %d", err, chain.ChainId) } if isPending { - return nil, nil, fmt.Errorf("tx is still pending on chain: %d", inboundChain.ChainId) + return nil, nil, fmt.Errorf("tx is still pending on chain: %d", chain.ChainId) } - receipt, err := evmClient.TransactionReceipt(ctx, hash) + receipt, err := evmClient.TransactionReceipt(goCtx, hash) if err != nil { return nil, nil, fmt.Errorf("failed to get receipt: %w, tx hash: %s", err, inboundHash) } return tx, receipt, nil } -func zetaTokenVoteV1( +func ZetaTokenVoteV1( event *zetaconnector.ZetaConnectorNonEthZetaSent, observationChain int64, ) *crosschaintypes.MsgVoteInbound { @@ -226,7 +103,7 @@ func zetaTokenVoteV1( ) } -func erc20VoteV1( +func Erc20VoteV1( event *erc20custody.ERC20CustodyDeposited, sender ethcommon.Address, observationChain int64, @@ -256,7 +133,7 @@ func erc20VoteV1( ) } -func gasVoteV1( +func GasVoteV1( tx *ethtypes.Transaction, sender ethcommon.Address, blockNumber uint64, @@ -288,7 +165,7 @@ func gasVoteV1( ) } -func depositInboundVoteV2(event *gatewayevm.GatewayEVMDeposited, +func DepositInboundVoteV2(event *gatewayevm.GatewayEVMDeposited, senderChainID int64, zetacoreChainID int64) *crosschaintypes.MsgVoteInbound { // if event.Asset is zero, it's a native token @@ -326,7 +203,7 @@ func depositInboundVoteV2(event *gatewayevm.GatewayEVMDeposited, ) } -func depositAndCallInboundVoteV2(event *gatewayevm.GatewayEVMDepositedAndCalled, +func DepositAndCallInboundVoteV2(event *gatewayevm.GatewayEVMDepositedAndCalled, senderChainID int64, zetacoreChainID int64) *crosschaintypes.MsgVoteInbound { // if event.Asset is zero, it's a native token @@ -358,7 +235,7 @@ func depositAndCallInboundVoteV2(event *gatewayevm.GatewayEVMDepositedAndCalled, ) } -func callInboundVoteV2(event *gatewayevm.GatewayEVMCalled, +func CallInboundVoteV2(event *gatewayevm.GatewayEVMCalled, senderChainID int64, zetacoreChainID int64) *crosschaintypes.MsgVoteInbound { return crosschaintypes.NewMsgVoteInbound( diff --git a/cmd/zetatool/chains/solana.go b/cmd/zetatool/chains/solana.go new file mode 100644 index 0000000000..03ecabb13e --- /dev/null +++ b/cmd/zetatool/chains/solana.go @@ -0,0 +1,36 @@ +package chains + +import ( + "encoding/hex" + + cosmosmath "cosmossdk.io/math" + + crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" + clienttypes "github.com/zeta-chain/node/zetaclient/types" +) + +// voteMsgFromSolEvent builds a MsgVoteInbound from an inbound event +func VoteMsgFromSolEvent(event *clienttypes.InboundEvent, + zetaChainID int64) (*crosschaintypes.MsgVoteInbound, error) { + // create inbound vote message + return crosschaintypes.NewMsgVoteInbound( + "", + event.Sender, + event.SenderChainID, + event.Sender, + event.Receiver, + zetaChainID, + cosmosmath.NewUint(event.Amount), + hex.EncodeToString(event.Memo), + event.TxHash, + event.BlockNumber, + 0, + event.CoinType, + event.Asset, + 0, // not a smart contract call + crosschaintypes.ProtocolContractVersion_V2, + false, + crosschaintypes.InboundStatus_SUCCESS, + crosschaintypes.WithCrossChainCall(event.IsCrossChainCall), + ), nil +} diff --git a/cmd/zetatool/cli/cctx_tracker.go b/cmd/zetatool/cli/cctx_tracker.go new file mode 100644 index 0000000000..5b98ee62c6 --- /dev/null +++ b/cmd/zetatool/cli/cctx_tracker.go @@ -0,0 +1,138 @@ +package cli + +import ( + "context" + "fmt" + "strconv" + + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" + + "github.com/zeta-chain/node/cmd/zetatool/cctx" + "github.com/zeta-chain/node/cmd/zetatool/config" + zetatoolcontext "github.com/zeta-chain/node/cmd/zetatool/context" +) + +func NewTrackCCTXCMD() *cobra.Command { + return &cobra.Command{ + Use: "track-cctx [inboundHash] [chainID]", + Short: "track a cross chain transaction", + RunE: TrackCCTX, + Args: cobra.ExactArgs(2), + } +} + +func TrackCCTX(cmd *cobra.Command, args []string) error { + var ( + trackingDetailsList []cctx.TrackingDetails + cctxTrackingDetails *cctx.TrackingDetails + maxCCTXChainLength = 5 // Maximum number of cctx chains to track + ) + + inboundHash := args[0] + inboundChainID, err := strconv.ParseInt(args[1], 10, 64) + if err != nil { + return fmt.Errorf("failed to parse chain id") + } + configFile, err := cmd.Flags().GetString(config.FlagConfig) + if err != nil { + return fmt.Errorf("failed to read value for flag %s , err %w", config.FlagConfig, err) + } + for i := 0; i < maxCCTXChainLength; i++ { + chainID := inboundChainID + chainHash := inboundHash + + // if len of tx is greated than 0, we have already tracked the cctx and we are trying to continue tracking the chain + // In thin case we should use the cctx identifier from the last cctx as the inbound hash and outbound chain id as theinbound chain id + if len(trackingDetailsList) > 0 { + lastTrackingDetails := trackingDetailsList[len(trackingDetailsList)-1] + // The last inbound was not finalized, this means that next cctx has not been created, yet we can return. + // There is no need to log the error here as we are just trying to track the cctx + if !lastTrackingDetails.IsInboundFinalized() { + return nil + } + // Update the chain id and hash to the last cctx details + chainID = lastTrackingDetails.OutboundChain.ChainId + chainHash = lastTrackingDetails.CCTXIdentifier + } + + // Create a new context based on the chain id and hash + ctx, err := zetatoolcontext.NewContext(context.Background(), chainID, chainHash, configFile) + if err != nil { + return fmt.Errorf("failed to create context: %w", err) + } + + // fetch the cctx details and status + cctxTrackingDetails, err = trackCCTX(ctx) + if err != nil { + // The error can be caused by two reasons + // 1. We have reached the end of the cctx error chain, we can return + // 2. There was an error in tracking the cctx. + + // If debug flag is set, we log everything + // If the length of the tracking details is 0, it means that we have not tracked any cctx yet, we log the error + if cmd.Flag(config.FlagDebug).Changed || len(trackingDetailsList) == 0 { + log.Error().Msgf("failed to track cctx : %v", err) + if cctxTrackingDetails != nil { + log.Info().Msg(cctxTrackingDetails.DebugPrint()) + } + } + + return nil + } + + log.Info().Msg(cctxTrackingDetails.Print()) + trackingDetailsList = append(trackingDetailsList, *cctxTrackingDetails) + } + return nil +} + +func trackCCTX(ctx *zetatoolcontext.Context) (*cctx.TrackingDetails, error) { + var ( + cctxTrackingDetails = cctx.NewTrackingDetails() + err error + ) + // Get the ballot identifier for the inbound transaction and confirm that cctx status in atleast either PendingInboundConfirmation or PendingInboundVoting + err = cctxTrackingDetails.CheckInbound(ctx) + if err != nil { + return cctxTrackingDetails, fmt.Errorf("failed to get ballot identifier: %w", err) + } + // Reject unknown status, as it is not valid + if cctxTrackingDetails.Status == cctx.Unknown || cctxTrackingDetails.CCTXIdentifier == "" { + return cctxTrackingDetails, fmt.Errorf("unknown status") + } + + // At this point, we have confirmed the inbound hash is valid, and it was sent to valid address. + // After this we attach error messages to the message field as we already have some details about the cctx which can be printed + // Update cctx status from zetacore.This copies the status from zetacore to the cctx details.The cctx status can only be `PendingInboundVoting` or `PendingInboundConfirmation` at this point + cctxTrackingDetails.UpdateCCTXStatus(ctx) + + // UpdateCCTXStatus can return without updating the cctx details if the cctx is not found.That is fine which just means that the cctx is not yet created + // If the inbound is finalized, we can update the outbound details as they would now be available + if cctxTrackingDetails.IsInboundFinalized() { + cctxTrackingDetails.UpdateCCTXOutboundDetails(ctx) + } + + // The cctx details now have status from zetacore, we have not tried to a get more granular status from the outbound chain yet. + // If it's not pending, we can just return here. + if !cctxTrackingDetails.IsPendingOutbound() { + return cctxTrackingDetails, nil + } + + // Update tx hash list from outbound tracker + // If the tracker is found, it means the outbound is broadcast, but we are waiting for the confirmations + // If the tracker is not found, it means the outbound is not broadcast yet, we are wwaiting for the tss to sign the outbound + cctxTrackingDetails.UpdateHashListAndPendingStatus(ctx) + + // If its not pending confirmation, we can return here, it means the outbound is not broadcast yet its pending tss signing + if !cctxTrackingDetails.IsPendingConfirmation() { + return cctxTrackingDetails, nil + } + + // Check outbound tx, we are waiting for the outbound tx to be confirmed + err = cctxTrackingDetails.CheckOutbound(ctx) + if err != nil { + return cctxTrackingDetails, err + } + return cctxTrackingDetails, nil +} diff --git a/cmd/zetatool/cli/inbound_ballot.go b/cmd/zetatool/cli/inbound_ballot.go new file mode 100644 index 0000000000..2cf5482b4e --- /dev/null +++ b/cmd/zetatool/cli/inbound_ballot.go @@ -0,0 +1,56 @@ +package cli + +import ( + "context" + "fmt" + "strconv" + + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" + + "github.com/zeta-chain/node/cmd/zetatool/cctx" + "github.com/zeta-chain/node/cmd/zetatool/config" + zetacontext "github.com/zeta-chain/node/cmd/zetatool/context" +) + +func NewGetInboundBallotCMD() *cobra.Command { + return &cobra.Command{ + Use: "get-ballot [inboundHash] [chainID]", + Short: "fetch ballot identifier from the inbound hash", + RunE: GetInboundBallot, + Args: cobra.ExactArgs(2), + } +} + +func GetInboundBallot(cmd *cobra.Command, args []string) error { + inboundHash := args[0] + inboundChainID, err := strconv.ParseInt(args[1], 10, 64) + if err != nil { + return fmt.Errorf("failed to parse chain id") + } + configFile, err := cmd.Flags().GetString(config.FlagConfig) + if err != nil { + return fmt.Errorf("failed to read value for flag %s , err %w", config.FlagConfig, err) + } + + ctx, err := zetacontext.NewContext(context.Background(), inboundChainID, inboundHash, configFile) + if err != nil { + return fmt.Errorf("failed to create context: %w", err) + } + + cctxTrackingDetails := cctx.NewTrackingDetails() + + err = cctxTrackingDetails.CheckInbound(ctx) + if err != nil { + return fmt.Errorf("failed to get ballot identifier: %w", err) + } + if cctxTrackingDetails.Status == cctx.PendingInboundConfirmation { + log.Printf( + "Ballot Identifier: %s, warning the inbound hash might not be confirmed yet", + cctxTrackingDetails.CCTXIdentifier, + ) + return nil + } + log.Print("Ballot Identifier: ", cctxTrackingDetails.CCTXIdentifier) + return nil +} diff --git a/cmd/zetatool/config/config.go b/cmd/zetatool/config/config.go index c513d3586b..11a7c1664d 100644 --- a/cmd/zetatool/config/config.go +++ b/cmd/zetatool/config/config.go @@ -14,18 +14,19 @@ var AppFs = afero.NewOsFs() const ( FlagConfig = "config" defaultCfgFileName = "zetatool_config.json" + FlagDebug = "debug" ) func TestnetConfig() *Config { return &Config{ - ZetaChainRPC: "https://zetachain-testnet-rpc.itrocket.net:443", + ZetaChainRPC: "https://zetachain-athens.g.allthatnode.com/archive/tendermint", EthereumRPC: "https://ethereum-sepolia-rpc.publicnode.com", - ZetaChainID: 101, + ZetaChainID: chains.ZetaChainTestnet.ChainId, BtcUser: "", BtcPassword: "", BtcHost: "", BtcParams: "", - SolanaRPC: "", + SolanaRPC: "https://api.testnet.solana.com", BscRPC: "https://bsc-testnet-rpc.publicnode.com", PolygonRPC: "https://polygon-amoy.gateway.tenderly.com", BaseRPC: "https://base-sepolia-rpc.publicnode.com", @@ -36,7 +37,7 @@ func DevnetConfig() *Config { return &Config{ ZetaChainRPC: "", EthereumRPC: "", - ZetaChainID: 101, + ZetaChainID: chains.ZetaChainDevnet.ChainId, BtcUser: "", BtcPassword: "", BtcHost: "", @@ -52,23 +53,24 @@ func MainnetConfig() *Config { return &Config{ ZetaChainRPC: "https://zetachain-mainnet.g.allthatnode.com:443/archive/tendermint", EthereumRPC: "https://eth-mainnet.public.blastapi.io", - ZetaChainID: 7000, + ZetaChainID: chains.ZetaChainMainnet.ChainId, BtcUser: "", BtcPassword: "", BtcHost: "", BtcParams: "", - SolanaRPC: "", + SolanaRPC: "https://api.mainnet-beta.solana.com", BaseRPC: "https://base-mainnet.public.blastapi.io", BscRPC: "https://bsc-mainnet.public.blastapi.io", PolygonRPC: "https://polygon-bor-rpc.publicnode.com", } } +// PrivateNetConfig returns a config for a private network, used for localnet testing func PrivateNetConfig() *Config { return &Config{ ZetaChainRPC: "http://127.0.0.1:26657", EthereumRPC: "http://127.0.0.1:8545", - ZetaChainID: 101, + ZetaChainID: chains.ZetaChainPrivnet.ChainId, BtcUser: "smoketest", BtcPassword: "123", BtcHost: "127.0.0.1:18443", diff --git a/cmd/zetatool/config/config_test.go b/cmd/zetatool/config/config_test.go index 8fc711c055..3c51bdbf34 100644 --- a/cmd/zetatool/config/config_test.go +++ b/cmd/zetatool/config/config_test.go @@ -36,7 +36,7 @@ func TestGetConfig(t *testing.T) { cfg, err = config.GetConfig(chains.Sepolia, "") require.NoError(t, err) - require.Equal(t, "https://zetachain-testnet-rpc.itrocket.net:443", cfg.ZetaChainRPC) + require.Equal(t, "https://zetachain-athens.g.allthatnode.com/archive/tendermint", cfg.ZetaChainRPC) cfg, err = config.GetConfig(chains.GoerliLocalnet, "") require.NoError(t, err) diff --git a/cmd/zetatool/context/context.go b/cmd/zetatool/context/context.go new file mode 100644 index 0000000000..1a61be38c6 --- /dev/null +++ b/cmd/zetatool/context/context.go @@ -0,0 +1,75 @@ +package context + +import ( + "context" + "fmt" + "time" + + "github.com/rs/zerolog" + + "github.com/zeta-chain/node/cmd/zetatool/config" + "github.com/zeta-chain/node/pkg/chains" + zetacorerpc "github.com/zeta-chain/node/pkg/rpc" +) + +type Context struct { + ctx context.Context + config *config.Config + zetaCoreClient zetacorerpc.Clients + inboundHash string + inboundChain chains.Chain + logger zerolog.Logger +} + +func NewContext(ctx context.Context, inboundChainID int64, inboundHash string, configFile string) (*Context, error) { + observationChain, found := chains.GetChainFromChainID(inboundChainID, []chains.Chain{}) + if !found { + return nil, fmt.Errorf("chain not supported,chain id: %d", inboundChainID) + } + cfg, err := config.GetConfig(observationChain, configFile) + if err != nil { + return nil, fmt.Errorf("failed to get config: %w", err) + } + zetacoreClient, err := zetacorerpc.NewCometBFTClients(cfg.ZetaChainRPC) + if err != nil { + return nil, fmt.Errorf("failed to create zetacore client: %w", err) + } + // logger is used when calling internal zetaclient functions which need a logger. + // we do not need to log those messages for this tool + logger := zerolog.New(zerolog.ConsoleWriter{ + Out: zerolog.Nop(), + TimeFormat: time.RFC3339, + }).With().Timestamp().Logger() + return &Context{ + ctx: ctx, + config: cfg, + zetaCoreClient: zetacoreClient, + inboundChain: observationChain, + inboundHash: inboundHash, + logger: logger, + }, nil +} + +func (c *Context) GetContext() context.Context { + return c.ctx +} + +func (c *Context) GetConfig() *config.Config { + return c.config +} + +func (c *Context) GetZetaCoreClient() zetacorerpc.Clients { + return c.zetaCoreClient +} + +func (c *Context) GetInboundHash() string { + return c.inboundHash +} + +func (c *Context) GetInboundChain() chains.Chain { + return c.inboundChain +} + +func (c *Context) GetLogger() zerolog.Logger { + return c.logger +} diff --git a/cmd/zetatool/inbound/bitcoin.go b/cmd/zetatool/inbound/bitcoin.go deleted file mode 100644 index 9cf652b110..0000000000 --- a/cmd/zetatool/inbound/bitcoin.go +++ /dev/null @@ -1,233 +0,0 @@ -package inbound - -import ( - "context" - "encoding/hex" - "fmt" - "math/big" - - cosmosmath "cosmossdk.io/math" - "github.com/btcsuite/btcd/chaincfg" - "github.com/btcsuite/btcd/chaincfg/chainhash" - "github.com/rs/zerolog" - - "github.com/zeta-chain/node/cmd/zetatool/config" - "github.com/zeta-chain/node/pkg/chains" - "github.com/zeta-chain/node/pkg/coin" - "github.com/zeta-chain/node/pkg/rpc" - crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" - "github.com/zeta-chain/node/x/observer/types" - "github.com/zeta-chain/node/zetaclient/chains/bitcoin/client" - "github.com/zeta-chain/node/zetaclient/chains/bitcoin/common" - zetaclientObserver "github.com/zeta-chain/node/zetaclient/chains/bitcoin/observer" - zetaclientConfig "github.com/zeta-chain/node/zetaclient/config" -) - -func btcInboundBallotIdentifier( - ctx context.Context, - cfg config.Config, - zetacoreClient rpc.Clients, - inboundHash string, - inboundChain chains.Chain, - zetaChainID int64, - logger zerolog.Logger) (string, error) { - params, err := chains.BitcoinNetParamsFromChainID(inboundChain.ChainId) - if err != nil { - return "", fmt.Errorf("unable to get bitcoin net params from chain id: %w", err) - } - - connCfg := zetaclientConfig.BTCConfig{ - RPCUsername: cfg.BtcUser, - RPCPassword: cfg.BtcPassword, - RPCHost: cfg.BtcHost, - RPCParams: params.Name, - } - - rpcClient, err := client.New(connCfg, inboundChain.ChainId, logger) - if err != nil { - return "", fmt.Errorf("unable to create rpc client: %w", err) - } - - err = rpcClient.Ping(ctx) - if err != nil { - return "", fmt.Errorf("error ping the bitcoin server: %w", err) - } - res, err := zetacoreClient.Observer.GetTssAddress(context.Background(), &types.QueryGetTssAddressRequest{}) - if err != nil { - return "", fmt.Errorf("failed to get tss address: %w", err) - } - tssBtcAddress := res.GetBtc() - - chainParams, err := zetacoreClient.GetChainParamsForChainID(context.Background(), inboundChain.ChainId) - if err != nil { - return "", fmt.Errorf("failed to get chain params: %w", err) - } - - return bitcoinBallotIdentifier( - ctx, - rpcClient, - params, - tssBtcAddress, - inboundHash, - inboundChain.ChainId, - zetaChainID, - chainParams.ConfirmationCount, - ) -} - -func bitcoinBallotIdentifier( - ctx context.Context, - btcClient *client.Client, - params *chaincfg.Params, - tss string, - txHash string, - senderChainID int64, - zetacoreChainID int64, - confirmationCount uint64) (string, error) { - hash, err := chainhash.NewHashFromStr(txHash) - if err != nil { - return "", err - } - confirmationMessage := "" - tx, err := btcClient.GetRawTransactionVerbose(ctx, hash) - if err != nil { - return "", err - } - if tx.Confirmations < confirmationCount { - confirmationMessage = fmt.Sprintf("tx might not be confirmed on chain: %d", senderChainID) - } - - blockHash, err := chainhash.NewHashFromStr(tx.BlockHash) - if err != nil { - return "", err - } - - blockVb, err := btcClient.GetBlockVerbose(ctx, blockHash) - if err != nil { - return "", err - } - - event, err := zetaclientObserver.GetBtcEvent( - ctx, - btcClient, - *tx, - tss, - uint64(blockVb.Height), // #nosec G115 always positive - zerolog.New(zerolog.Nop()), - params, - common.CalcDepositorFee, - ) - if err != nil { - return "", fmt.Errorf("error getting btc event: %w", err) - } - if event == nil { - return "", fmt.Errorf("no event built for btc sent to TSS") - } - - return identifierFromBtcEvent(event, senderChainID, zetacoreChainID, confirmationMessage) -} - -func identifierFromBtcEvent(event *zetaclientObserver.BTCInboundEvent, - senderChainID int64, - zetacoreChainID int64, confirmationMessage string) (string, error) { - // decode event memo bytes - err := event.DecodeMemoBytes(senderChainID) - if err != nil { - return "", fmt.Errorf("error decoding memo bytes: %w", err) - } - - // convert the amount to integer (satoshis) - amountSats, err := common.GetSatoshis(event.Value) - if err != nil { - return "", fmt.Errorf("error converting amount to satoshis: %w", err) - } - amountInt := big.NewInt(amountSats) - - var msg *crosschaintypes.MsgVoteInbound - switch event.MemoStd { - case nil: - { - msg = voteFromLegacyMemo(event, amountInt, senderChainID, zetacoreChainID) - } - default: - { - msg = voteFromStdMemo(event, amountInt, senderChainID, zetacoreChainID) - } - } - if msg == nil { - return "", fmt.Errorf("failed to create vote message") - } - - index := msg.Digest() - if confirmationMessage != "" { - return fmt.Sprintf("ballot identifier: %s warning: %s", index, confirmationMessage), nil - } - return fmt.Sprintf("ballot identifier: %s", msg.Digest()), nil -} - -// NewInboundVoteFromLegacyMemo creates a MsgVoteInbound message for inbound that uses legacy memo -func voteFromLegacyMemo( - event *zetaclientObserver.BTCInboundEvent, - amountSats *big.Int, - senderChainID int64, - zetacoreChainID int64, -) *crosschaintypes.MsgVoteInbound { - message := hex.EncodeToString(event.MemoBytes) - - return crosschaintypes.NewMsgVoteInbound( - "", - event.FromAddress, - senderChainID, - event.FromAddress, - event.ToAddress, - zetacoreChainID, - cosmosmath.NewUintFromBigInt(amountSats), - message, - event.TxHash, - event.BlockNumber, - 0, - coin.CoinType_Gas, - "", - 0, - crosschaintypes.ProtocolContractVersion_V1, - false, // not relevant for v1 - crosschaintypes.InboundStatus_SUCCESS, - ) -} - -func voteFromStdMemo( - event *zetaclientObserver.BTCInboundEvent, - amountSats *big.Int, - senderChainID int64, - zetacoreChainID int64, -) *crosschaintypes.MsgVoteInbound { - // zetacore will create a revert outbound that points to the custom revert address. - revertOptions := crosschaintypes.RevertOptions{ - RevertAddress: event.MemoStd.RevertOptions.RevertAddress, - } - - // make a legacy message so that zetacore can process it as V1 - msgBytes := append(event.MemoStd.Receiver.Bytes(), event.MemoStd.Payload...) - message := hex.EncodeToString(msgBytes) - - return crosschaintypes.NewMsgVoteInbound( - "", - event.FromAddress, - senderChainID, - event.FromAddress, - event.ToAddress, - zetacoreChainID, - cosmosmath.NewUintFromBigInt(amountSats), - message, - event.TxHash, - event.BlockNumber, - 0, - coin.CoinType_Gas, - "", - 0, - crosschaintypes.ProtocolContractVersion_V1, - false, // not relevant for v1 - crosschaintypes.InboundStatus_SUCCESS, - crosschaintypes.WithRevertOptions(revertOptions), - ) -} diff --git a/cmd/zetatool/inbound/inbound.go b/cmd/zetatool/inbound/inbound.go deleted file mode 100644 index bf31a84618..0000000000 --- a/cmd/zetatool/inbound/inbound.go +++ /dev/null @@ -1,125 +0,0 @@ -package inbound - -import ( - "context" - "fmt" - "strconv" - "time" - - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" - "github.com/spf13/cobra" - - "github.com/zeta-chain/node/cmd/zetatool/config" - "github.com/zeta-chain/node/pkg/chains" - zetacorerpc "github.com/zeta-chain/node/pkg/rpc" -) - -func NewGetInboundBallotCMD() *cobra.Command { - return &cobra.Command{ - Use: "get-ballot [inboundHash] [chainID]", - Short: "fetch ballot identifier from the inbound hash", - RunE: GetInboundBallot, - Args: cobra.ExactArgs(2), - } -} - -func GetInboundBallot(cmd *cobra.Command, args []string) error { - inboundHash := args[0] - inboundChainID, err := strconv.ParseInt(args[1], 10, 64) - if err != nil { - return fmt.Errorf("failed to parse chain id") - } - configFile, err := cmd.Flags().GetString(config.FlagConfig) - if err != nil { - return fmt.Errorf("failed to read value for flag %s , err %w", config.FlagConfig, err) - } - - return GetBallotIdentifier(inboundHash, inboundChainID, configFile) -} - -func GetBallotIdentifier(inboundHash string, inboundChainID int64, configFile string) error { - observationChain, found := chains.GetChainFromChainID(inboundChainID, []chains.Chain{}) - if !found { - return fmt.Errorf("chain not supported,chain id: %d", inboundChainID) - } - - cfg, err := config.GetConfig(observationChain, configFile) - if err != nil { - return fmt.Errorf("failed to get config: %w", err) - } - - zetacoreClient, err := zetacorerpc.NewCometBFTClients(cfg.ZetaChainRPC) - if err != nil { - return fmt.Errorf("failed to create zetacore client: %w", err) - } - - ctx := context.Background() - ballotIdentifierMessage := "" - - // logger is used when calling internal zetaclient functions which need a logger. - // we do not need to log those messages for this tool - logger := zerolog.New(zerolog.ConsoleWriter{ - Out: zerolog.Nop(), - TimeFormat: time.RFC3339, - }).With().Timestamp().Logger() - - if observationChain.IsEVMChain() { - ballotIdentifierMessage, err = evmInboundBallotIdentifier( - ctx, - *cfg, - zetacoreClient, - inboundHash, - observationChain, - cfg.ZetaChainID, - ) - if err != nil { - return fmt.Errorf( - "failed to get inbound ballot for evm chain %d, %w", - observationChain.ChainId, - err, - ) - } - } - - if observationChain.IsBitcoinChain() { - ballotIdentifierMessage, err = btcInboundBallotIdentifier( - ctx, - *cfg, - zetacoreClient, - inboundHash, - observationChain, - cfg.ZetaChainID, - logger, - ) - if err != nil { - return fmt.Errorf( - "failed to get inbound ballot for bitcoin chain %d, %w", - observationChain.ChainId, - err, - ) - } - } - - if observationChain.IsSolanaChain() { - ballotIdentifierMessage, err = solanaInboundBallotIdentifier( - ctx, - *cfg, - zetacoreClient, - inboundHash, - observationChain, - cfg.ZetaChainID, - logger, - ) - if err != nil { - return fmt.Errorf( - "failed to get inbound ballot for solana chain %d, %w", - observationChain.ChainId, - err, - ) - } - } - - log.Info().Msgf("%s", ballotIdentifierMessage) - return nil -} diff --git a/cmd/zetatool/inbound/solana.go b/cmd/zetatool/inbound/solana.go deleted file mode 100644 index 014e51845f..0000000000 --- a/cmd/zetatool/inbound/solana.go +++ /dev/null @@ -1,104 +0,0 @@ -package inbound - -import ( - "context" - "encoding/hex" - "fmt" - - cosmosmath "cosmossdk.io/math" - "github.com/gagliardetto/solana-go" - solrpc "github.com/gagliardetto/solana-go/rpc" - "github.com/rs/zerolog" - - "github.com/zeta-chain/node/cmd/zetatool/config" - "github.com/zeta-chain/node/pkg/chains" - solanacontracts "github.com/zeta-chain/node/pkg/contracts/solana" - "github.com/zeta-chain/node/pkg/rpc" - crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" - "github.com/zeta-chain/node/zetaclient/chains/solana/observer" - solanarpc "github.com/zeta-chain/node/zetaclient/chains/solana/rpc" - clienttypes "github.com/zeta-chain/node/zetaclient/types" -) - -func solanaInboundBallotIdentifier(ctx context.Context, - cfg config.Config, - zetacoreClient rpc.Clients, - inboundHash string, - inboundChain chains.Chain, - zetaChainID int64, - logger zerolog.Logger) (string, error) { - solClient := solrpc.New(cfg.SolanaRPC) - if solClient == nil { - return "", fmt.Errorf("error creating rpc client") - } - - signature := solana.MustSignatureFromBase58(inboundHash) - - txResult, err := solanarpc.GetTransaction(ctx, solClient, signature) - if err != nil { - return "", fmt.Errorf("error getting transaction: %w", err) - } - - chainParams, err := zetacoreClient.GetChainParamsForChainID(context.Background(), inboundChain.ChainId) - if err != nil { - return "", fmt.Errorf("failed to get chain params: %w", err) - } - - gatewayID, _, err := solanacontracts.ParseGatewayWithPDA(chainParams.GatewayAddress) - if err != nil { - return "", fmt.Errorf("cannot parse gateway address: %s, err: %w", chainParams.GatewayAddress, err) - } - - events, err := observer.FilterInboundEvents(txResult, - gatewayID, - inboundChain.ChainId, - logger, - ) - - if err != nil { - return "", fmt.Errorf("failed to filter solana inbound events: %w", err) - } - - msg := &crosschaintypes.MsgVoteInbound{} - - // build inbound vote message from events and post to zetacore - for _, event := range events { - msg, err = voteMsgFromSolEvent(event, zetaChainID) - if err != nil { - return "", fmt.Errorf("failed to create vote message: %w", err) - } - } - - return fmt.Sprintf("ballot identifier: %s", msg.Digest()), nil -} - -// voteMsgFromSolEvent builds a MsgVoteInbound from an inbound event -func voteMsgFromSolEvent(event *clienttypes.InboundEvent, - zetaChainID int64) (*crosschaintypes.MsgVoteInbound, error) { - // decode event memo bytes to get the receiver - err := event.DecodeMemo() - if err != nil { - return nil, fmt.Errorf("failed to decode memo: %w", err) - } - - // create inbound vote message - return crosschaintypes.NewMsgVoteInbound( - "", - event.Sender, - event.SenderChainID, - event.Sender, - event.Receiver, - zetaChainID, - cosmosmath.NewUint(event.Amount), - hex.EncodeToString(event.Memo), - event.TxHash, - event.BlockNumber, - 0, - event.CoinType, - event.Asset, - 0, // not a smart contract call - crosschaintypes.ProtocolContractVersion_V1, - false, // not relevant for v1 - crosschaintypes.InboundStatus_SUCCESS, - ), nil -} diff --git a/cmd/zetatool/main.go b/cmd/zetatool/main.go index c79451d413..e26806fee6 100644 --- a/cmd/zetatool/main.go +++ b/cmd/zetatool/main.go @@ -6,8 +6,8 @@ import ( "github.com/spf13/cobra" + "github.com/zeta-chain/node/cmd/zetatool/cli" "github.com/zeta-chain/node/cmd/zetatool/config" - "github.com/zeta-chain/node/cmd/zetatool/inbound" ) var rootCmd = &cobra.Command{ @@ -16,8 +16,11 @@ var rootCmd = &cobra.Command{ } func init() { - rootCmd.AddCommand(inbound.NewGetInboundBallotCMD()) + rootCmd.AddCommand(cli.NewGetInboundBallotCMD()) + rootCmd.AddCommand(cli.NewTrackCCTXCMD()) rootCmd.PersistentFlags().String(config.FlagConfig, "", "custom config file: --config filename.json") + rootCmd.PersistentFlags(). + Bool(config.FlagDebug, false, "enable debug mode, to show more details on why the command might be failing") } func main() { diff --git a/docs/cli/zetatool/get_ballot.md b/docs/cli/zetatool/get_ballot.md new file mode 100644 index 0000000000..052f94cebe --- /dev/null +++ b/docs/cli/zetatool/get_ballot.md @@ -0,0 +1,23 @@ +## Usage + +### Fetching the Inbound Ballot Identifier + +### Command +```shell +zetatool get-ballot [inboundHash] [chainID] --config +``` +### Example +```shell +zetatool get-ballot 0x61008d7f79b2955a15e3cb95154a80e19c7385993fd0e083ff0cbe0b0f56cb9a 1 +{"level":"info","time":"2025-01-20T11:30:47-05:00","message":"ballot identifier: 0xae189ab5cd884af784835297ac43eb55deb8a7800023534c580f44ee2b3eb5ed"} +``` + +- `inboundHash`: The inbound hash of the transaction for which the ballot identifier is to be fetched +- `chainID`: The chain ID of the chain to which the transaction belongs +- `config`: [Optional] The path to the configuration file. When not provided, the configuration in the file is user. A sample config is provided at `cmd/zetatool/config/sample_config.json` + +The Config contains the rpcs needed for the tool to function, +if not provided the tool automatically uses the default rpcs.It is able to fetch the rpc needed using the chain ID + +The command returns a ballot identifier for the given inbound hash. + diff --git a/docs/cli/zetatool/readme.md b/docs/cli/zetatool/readme.md index 8d522755ab..0b91a14a67 100644 --- a/docs/cli/zetatool/readme.md +++ b/docs/cli/zetatool/readme.md @@ -1,30 +1,8 @@ # ZetaTool -ZetaTool is a utility CLI for Zetachain.It currently provides a command to fetch the ballot/cctx identifier from the inbound hash +ZetaTool is a utility CLI for Zetachain.It currently provides the following functionalities: +- `get-ballot` : Fetch the (inbound ballot/cctx) identifier from the inbound hash and chain id +- `track-cctx` : Track the status of a cctx using from the inbound hash and chain id ## Installation Use the target : `make install-zetatool` - -## Usage - -### Fetching the Ballot Identifier - -### Command -```shell -zetatool get-ballot [inboundHash] [chainID] --config -``` -### Example -```shell -zetatool get-ballot 0x61008d7f79b2955a15e3cb95154a80e19c7385993fd0e083ff0cbe0b0f56cb9a 1 -{"level":"info","time":"2025-01-20T11:30:47-05:00","message":"ballot identifier: 0xae189ab5cd884af784835297ac43eb55deb8a7800023534c580f44ee2b3eb5ed"} -``` - -- `inboundHash`: The inbound hash of the transaction for which the ballot identifier is to be fetched -- `chainID`: The chain ID of the chain to which the transaction belongs -- `config`: [Optional] The path to the configuration file. When not provided, the configuration in the file is user. A sample config is provided at `cmd/zetatool/config/sample_config.json` - -The Config contains the rpcs needed for the tool to function, -if not provided the tool automatically uses the default rpcs.It is able to fetch the rpc needed using the chain ID - -The command returns a ballot identifier for the given inbound hash. - diff --git a/docs/cli/zetatool/track_cctx.md b/docs/cli/zetatool/track_cctx.md new file mode 100644 index 0000000000..62953cdf4c --- /dev/null +++ b/docs/cli/zetatool/track_cctx.md @@ -0,0 +1,26 @@ +## Usage + +### Track the status of a CCTX + +### Command +```shell +zetatool track-cctx [inboundHash] [chainID] --config +``` +### Example +```shell +zetatool track-cctx 0x61008d7f79b2955a15e3cb95154a80e19c7385993fd0e083ff0cbe0b0f56cb9a 1 +{"level":"info","time":"2025-02-03T12:59:33-05:00","message":"CCTX Identifier: 0xae189ab5cd884af784835297ac43eb55deb8a7800023534c580f44ee2b3eb5ed Status: OutboundMined"} +``` + +- `inboundHash`: The inbound hash of the transaction for which the ballot identifier is to be fetched +- `chainID`: The chain ID of the chain to which the transaction belongs +- `config`: [Optional] The path to the configuration file. When not provided, the configuration in the file is user. A sample config is provided at `cmd/zetatool/config/sample_config.json` +- `debug`: [Optional] The debug flag is used to print additional debug information when set to true + +The Config contains the rpcs needed for the tool to function, +if not provided the tool automatically uses the default rpc .It is able to fetch the rpc needed using the chain ID + +The command returns +- The CCTX identifier +- The status of the CCTX + diff --git a/zetaclient/zetacore/tx.go b/zetaclient/zetacore/tx.go index 3d06feb202..8b24a59fbd 100644 --- a/zetaclient/zetacore/tx.go +++ b/zetaclient/zetacore/tx.go @@ -90,7 +90,9 @@ func WrapMessageWithAuthz(msg sdk.Msg) (sdk.Msg, clientauthz.Signer, error) { // PostOutboundTracker adds an outbound tracker func (c *Client) PostOutboundTracker(ctx context.Context, chainID int64, nonce uint64, txHash string) (string, error) { // don't report if the tracker already contains the txHash - tracker, err := c.GetOutboundTracker(ctx, chains.Chain{ChainId: chainID}, nonce) + tracker, err := c.GetOutboundTracker(ctx, chains.Chain{ + ChainId: chainID, + }, nonce) if err == nil { for _, hash := range tracker.HashList { if strings.EqualFold(hash.TxHash, txHash) {