diff --git a/cmd/zetaclientd/init.go b/cmd/zetaclientd/init.go index 5096230db9..9969944763 100644 --- a/cmd/zetaclientd/init.go +++ b/cmd/zetaclientd/init.go @@ -13,6 +13,8 @@ import ( ) // solanaTestKey is a local test private key for Solana +// TODO: use separate keys for each zetaclient in Solana E2E tests +// https://github.com/zeta-chain/node/issues/2614 var solanaTestKey = []uint8{ 199, 16, 63, 28, 125, 103, 131, 13, 6, 94, 68, 109, 13, 68, 132, 17, 71, 33, 216, 51, 49, 103, 146, 241, 245, 162, 90, 228, 71, 177, 32, 199, diff --git a/e2e/e2etests/test_solana_withdraw.go b/e2e/e2etests/test_solana_withdraw.go index 78400c3ae8..776944c9b3 100644 --- a/e2e/e2etests/test_solana_withdraw.go +++ b/e2e/e2etests/test_solana_withdraw.go @@ -13,11 +13,10 @@ import ( func TestSolanaWithdraw(r *runner.E2ERunner, args []string) { require.Len(r, args, 1) - // print balance of from address - solZRC20 := r.SOLZRC20 - balance, err := solZRC20.BalanceOf(&bind.CallOpts{}, r.ZEVMAuth.From) + // print balanceAfter of from address + balanceBefore, err := r.SOLZRC20.BalanceOf(&bind.CallOpts{}, r.ZEVMAuth.From) require.NoError(r, err) - r.Logger.Info("from address %s balance of SOL before: %d", r.ZEVMAuth.From, balance) + r.Logger.Info("from address %s balance of SOL before: %d", r.ZEVMAuth.From, balanceBefore) // parse withdraw amount (in lamports), approve amount is 1 SOL approvedAmount := new(big.Int).SetUint64(solana.LAMPORTS_PER_SOL) @@ -38,7 +37,11 @@ func TestSolanaWithdraw(r *runner.E2ERunner, args []string) { r.WithdrawSOLZRC20(privkey.PublicKey(), withdrawAmount, approvedAmount) // print balance of from address after withdraw - balance, err = solZRC20.BalanceOf(&bind.CallOpts{}, r.ZEVMAuth.From) + balanceAfter, err := r.SOLZRC20.BalanceOf(&bind.CallOpts{}, r.ZEVMAuth.From) require.NoError(r, err) - r.Logger.Info("from address %s balance of SOL after: %d", r.ZEVMAuth.From, balance) + r.Logger.Info("from address %s balance of SOL after: %d", r.ZEVMAuth.From, balanceAfter) + + // check if the balance is reduced correctly + amountReduced := new(big.Int).Sub(balanceBefore, balanceAfter) + require.True(r, amountReduced.Cmp(withdrawAmount) >= 0, "balance is not reduced correctly") } diff --git a/e2e/runner/setup_solana.go b/e2e/runner/setup_solana.go index 157fa048bf..10aa4b1a15 100644 --- a/e2e/runner/setup_solana.go +++ b/e2e/runner/setup_solana.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/require" "github.com/zeta-chain/zetacore/pkg/chains" - solanacontract "github.com/zeta-chain/zetacore/pkg/contract/solana" + solanacontracts "github.com/zeta-chain/zetacore/pkg/contracts/solana" ) // SetupSolanaAccount imports the deployer's private key @@ -25,7 +25,7 @@ func (r *E2ERunner) SetSolanaContracts(deployerPrivateKey string) { r.Logger.Print("⚙️ initializing gateway program on Solana") // set Solana contracts - r.GatewayProgram = solana.MustPublicKeyFromBase58(solanacontract.SolanaGatewayProgramID) + r.GatewayProgram = solana.MustPublicKeyFromBase58(solanacontracts.SolanaGatewayProgramID) // get deployer account balance privkey, err := solana.PrivateKeyFromBase58(deployerPrivateKey) @@ -47,8 +47,8 @@ func (r *E2ERunner) SetSolanaContracts(deployerPrivateKey string) { inst.ProgID = r.GatewayProgram inst.AccountValues = accountSlice - inst.DataBytes, err = borsh.Serialize(solanacontract.InitializeParams{ - Discriminator: solanacontract.DiscriminatorInitialize(), + inst.DataBytes, err = borsh.Serialize(solanacontracts.InitializeParams{ + Discriminator: solanacontracts.DiscriminatorInitialize(), TssAddress: r.TSSAddress, ChainID: uint64(chains.SolanaLocalnet.ChainId), }) @@ -66,7 +66,7 @@ func (r *E2ERunner) SetSolanaContracts(deployerPrivateKey string) { require.NoError(r, err) // deserialize the PDA info - pda := solanacontract.PdaInfo{} + pda := solanacontracts.PdaInfo{} err = borsh.Deserialize(&pda, pdaInfo.Bytes()) require.NoError(r, err) tssAddress := ethcommon.BytesToAddress(pda.TssAddress[:]) diff --git a/e2e/runner/solana.go b/e2e/runner/solana.go index 601f3dd286..71d2cfb629 100644 --- a/e2e/runner/solana.go +++ b/e2e/runner/solana.go @@ -11,7 +11,7 @@ import ( "github.com/stretchr/testify/require" "github.com/zeta-chain/zetacore/e2e/utils" - solanacontract "github.com/zeta-chain/zetacore/pkg/contract/solana" + solanacontract "github.com/zeta-chain/zetacore/pkg/contracts/solana" crosschaintypes "github.com/zeta-chain/zetacore/x/crosschain/types" ) diff --git a/pkg/contract/solana/gateway.go b/pkg/contracts/solana/gateway.go similarity index 94% rename from pkg/contract/solana/gateway.go rename to pkg/contracts/solana/gateway.go index c4d03f1d77..c4e27918b5 100644 --- a/pkg/contract/solana/gateway.go +++ b/pkg/contracts/solana/gateway.go @@ -1,3 +1,4 @@ +// Package solana privides structures and constants that are used when interacting with the gateway program on Solana chain. package solana import ( diff --git a/pkg/contract/solana/gateway.json b/pkg/contracts/solana/gateway.json similarity index 100% rename from pkg/contract/solana/gateway.json rename to pkg/contracts/solana/gateway.json diff --git a/pkg/contract/solana/gateway_message.go b/pkg/contracts/solana/gateway_message.go similarity index 100% rename from pkg/contract/solana/gateway_message.go rename to pkg/contracts/solana/gateway_message.go diff --git a/pkg/contract/solana/gateway_message_test.go b/pkg/contracts/solana/gateway_message_test.go similarity index 85% rename from pkg/contract/solana/gateway_message_test.go rename to pkg/contracts/solana/gateway_message_test.go index 8275300ef9..b2ffc24489 100644 --- a/pkg/contract/solana/gateway_message_test.go +++ b/pkg/contracts/solana/gateway_message_test.go @@ -9,7 +9,7 @@ import ( "github.com/gagliardetto/solana-go" "github.com/zeta-chain/zetacore/pkg/chains" - contract "github.com/zeta-chain/zetacore/pkg/contract/solana" + contracts "github.com/zeta-chain/zetacore/pkg/contracts/solana" ) func Test_MsgWithdrawHash(t *testing.T) { @@ -25,7 +25,7 @@ func Test_MsgWithdrawHash(t *testing.T) { require.NoError(t, err) // create new withdraw message - hash := contract.NewMsgWithdraw(chainID, nonce, amount, to).Hash() + hash := contracts.NewMsgWithdraw(chainID, nonce, amount, to).Hash() require.True(t, bytes.Equal(hash[:], wantHashBytes)) }) } diff --git a/pkg/contract/solana/idl.go b/pkg/contracts/solana/idl.go similarity index 100% rename from pkg/contract/solana/idl.go rename to pkg/contracts/solana/idl.go diff --git a/pkg/contract/solana/types.go b/pkg/contracts/solana/instruction.go similarity index 77% rename from pkg/contract/solana/types.go rename to pkg/contracts/solana/instruction.go index 0a9de51ac0..95d5a7fd29 100644 --- a/pkg/contract/solana/types.go +++ b/pkg/contracts/solana/instruction.go @@ -1,26 +1,13 @@ -package solana // PdaInfo represents the PDA for the gateway program +package solana + import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" + "github.com/gagliardetto/solana-go" + "github.com/near/borsh-go" + "github.com/pkg/errors" ) -type PdaInfo struct { - // Discriminator is the unique identifier for the PDA - Discriminator [8]byte - - // Nonce is the current nonce for the PDA - Nonce uint64 - - // TssAddress is the TSS address for the PDA - TssAddress [20]byte - - // Authority is the authority for the PDA - Authority [32]byte - - // ChainId is the Solana chain id - ChainID uint64 -} - // InitializeParams contains the parameters for a gateway initialize instruction type InitializeParams struct { // Discriminator is the unique identifier for the initialize instruction @@ -99,6 +86,24 @@ func (inst *WithdrawInstructionParams) TokenAmount() uint64 { return inst.Amount } +// ParseInstructionWithdraw tries to parse the instruction as a 'withdraw'. +// It returns nil if the instruction can't be parsed as a 'withdraw'. +func ParseInstructionWithdraw(instruction solana.CompiledInstruction) (*WithdrawInstructionParams, error) { + // try deserializing instruction as a 'withdraw' + inst := &WithdrawInstructionParams{} + err := borsh.Deserialize(inst, instruction.Data) + if err != nil { + return nil, errors.Wrap(err, "error deserializing instruction") + } + + // check the discriminator to ensure it's a 'withdraw' instruction + if inst.Discriminator != DiscriminatorWithdraw() { + return nil, errors.New("not a withdraw instruction") + } + + return inst, nil +} + // RecoverSigner recover the ECDSA signer from given message hash and signature func RecoverSigner(msgHash []byte, msgSig []byte) (signer common.Address, err error) { // recover the public key diff --git a/pkg/contract/solana/types_test.go b/pkg/contracts/solana/instruction_test.go similarity index 87% rename from pkg/contract/solana/types_test.go rename to pkg/contracts/solana/instruction_test.go index 6742a8a529..7f284169f1 100644 --- a/pkg/contract/solana/types_test.go +++ b/pkg/contracts/solana/instruction_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/require" ethcommon "github.com/ethereum/go-ethereum/common" - contract "github.com/zeta-chain/zetacore/pkg/contract/solana" + contracts "github.com/zeta-chain/zetacore/pkg/contracts/solana" ) const ( @@ -42,7 +42,7 @@ func Test_SignerWithdraw(t *testing.T) { copy(sigRS[:], sigTest[:64]) // create a withdraw instruction - inst := contract.WithdrawInstructionParams{ + inst := contracts.WithdrawInstructionParams{ Signature: sigRS, RecoveryID: 0, MessageHash: getTestmessageHash(), @@ -59,21 +59,21 @@ func Test_RecoverSigner(t *testing.T) { hashTest := getTestmessageHash() // recover the signer from the test message hash and signature - signer, err := contract.RecoverSigner(hashTest[:], sigTest[:]) + signer, err := contracts.RecoverSigner(hashTest[:], sigTest[:]) require.NoError(t, err) require.EqualValues(t, testSigner, signer.String()) // slightly modify the signature and recover the signer sigFake := sigTest sigFake[0]++ - signer, err = contract.RecoverSigner(hashTest[:], sigFake[:]) + signer, err = contracts.RecoverSigner(hashTest[:], sigFake[:]) require.Error(t, err) require.Equal(t, ethcommon.Address{}, signer) // slightly modify the message hash and recover the signer hashFake := hashTest hashFake[0]++ - signer, err = contract.RecoverSigner(hashFake[:], sigTest[:]) + signer, err = contracts.RecoverSigner(hashFake[:], sigTest[:]) require.NoError(t, err) require.NotEqual(t, ethcommon.Address{}, signer) require.NotEqual(t, testSigner, signer.String()) diff --git a/pkg/contracts/solana/pda.go b/pkg/contracts/solana/pda.go new file mode 100644 index 0000000000..0452d23a15 --- /dev/null +++ b/pkg/contracts/solana/pda.go @@ -0,0 +1,19 @@ +package solana + +// PdaInfo represents the PDA for the gateway program +type PdaInfo struct { + // Discriminator is the unique identifier for the PDA + Discriminator [8]byte + + // Nonce is the current nonce for the PDA + Nonce uint64 + + // TssAddress is the TSS address for the PDA + TssAddress [20]byte + + // Authority is the authority for the PDA + Authority [32]byte + + // ChainId is the Solana chain id + ChainID uint64 +} diff --git a/x/observer/types/chain_params.go b/x/observer/types/chain_params.go index a15dc99fd8..b35d7da930 100644 --- a/x/observer/types/chain_params.go +++ b/x/observer/types/chain_params.go @@ -11,7 +11,7 @@ import ( "github.com/pkg/errors" "github.com/zeta-chain/zetacore/pkg/chains" - solanacontract "github.com/zeta-chain/zetacore/pkg/contract/solana" + solanacontracts "github.com/zeta-chain/zetacore/pkg/contracts/solana" ) const ( @@ -331,7 +331,7 @@ func GetDefaultSolanaLocalnetChainParams() *ChainParams { BallotThreshold: DefaultBallotThreshold, MinObserverDelegation: DefaultMinObserverDelegation, IsSupported: false, - GatewayAddress: solanacontract.SolanaGatewayProgramID, + GatewayAddress: solanacontracts.SolanaGatewayProgramID, } } func GetDefaultGoerliLocalnetChainParams() *ChainParams { diff --git a/zetaclient/chains/base/observer.go b/zetaclient/chains/base/observer.go index eb0adbe5d8..428946f0bf 100644 --- a/zetaclient/chains/base/observer.go +++ b/zetaclient/chains/base/observer.go @@ -267,6 +267,11 @@ func (ob *Observer) WithHeaderCache(cache *lru.Cache) *Observer { return ob } +// OutboundID returns a unique identifier for the outbound transaction. +func (ob *Observer) OutboundID(nonce uint64) string { + return fmt.Sprintf("%d-%d", ob.chain.ChainId, nonce) +} + // DB returns the database for the observer. func (ob *Observer) DB() *db.DB { return ob.db diff --git a/zetaclient/chains/bitcoin/observer/observer.go b/zetaclient/chains/bitcoin/observer/observer.go index 3ccb1fee1d..8b4c79ba39 100644 --- a/zetaclient/chains/bitcoin/observer/observer.go +++ b/zetaclient/chains/bitcoin/observer/observer.go @@ -535,7 +535,7 @@ func (ob *Observer) FetchUTXOs(ctx context.Context) error { // SaveBroadcastedTx saves successfully broadcasted transaction // TODO(revamp): move to db file func (ob *Observer) SaveBroadcastedTx(txHash string, nonce uint64) { - outboundID := ob.GetTxID(nonce) + outboundID := ob.OutboundID(nonce) ob.Mu().Lock() ob.broadcastedTx[outboundID] = txHash ob.Mu().Unlock() diff --git a/zetaclient/chains/bitcoin/observer/outbound.go b/zetaclient/chains/bitcoin/observer/outbound.go index a47a5aa7e0..504713969d 100644 --- a/zetaclient/chains/bitcoin/observer/outbound.go +++ b/zetaclient/chains/bitcoin/observer/outbound.go @@ -23,12 +23,6 @@ import ( "github.com/zeta-chain/zetacore/zetaclient/zetacore" ) -// GetTxID returns a unique id for outbound tx -func (ob *Observer) GetTxID(nonce uint64) string { - tssAddr := ob.TSS().BTCAddress() - return fmt.Sprintf("%d-%s-%d", ob.Chain().ChainId, tssAddr, nonce) -} - // WatchOutbound watches Bitcoin chain for outgoing txs status // TODO(revamp): move ticker functions to a specific file // TODO(revamp): move into a separate package @@ -66,7 +60,7 @@ func (ob *Observer) WatchOutbound(ctx context.Context) error { } for _, tracker := range trackers { // get original cctx parameters - outboundID := ob.GetTxID(tracker.Nonce) + outboundID := ob.OutboundID(tracker.Nonce) cctx, err := ob.ZetacoreClient().GetCctxByNonce(ctx, chainID, tracker.Nonce) if err != nil { ob.logger.Outbound.Info(). @@ -140,7 +134,7 @@ func (ob *Observer) IsOutboundProcessed( nonce := cctx.GetCurrentOutboundParam().TssNonce // get broadcasted outbound and tx result - outboundID := ob.GetTxID(nonce) + outboundID := ob.OutboundID(nonce) ob.Logger().Outbound.Info().Msgf("IsOutboundProcessed %s", outboundID) ob.Mu().Lock() @@ -443,7 +437,7 @@ func (ob *Observer) checkIncludedTx( cctx *crosschaintypes.CrossChainTx, txHash string, ) (*btcjson.GetTransactionResult, bool) { - outboundID := ob.GetTxID(cctx.GetCurrentOutboundParam().TssNonce) + outboundID := ob.OutboundID(cctx.GetCurrentOutboundParam().TssNonce) hash, getTxResult, err := rpc.GetTxResultByHash(ob.btcClient, txHash) if err != nil { ob.logger.Outbound.Error().Err(err).Msgf("checkIncludedTx: error GetTxResultByHash: %s", txHash) @@ -472,7 +466,7 @@ func (ob *Observer) checkIncludedTx( // setIncludedTx saves included tx result in memory func (ob *Observer) setIncludedTx(nonce uint64, getTxResult *btcjson.GetTransactionResult) { txHash := getTxResult.TxID - outboundID := ob.GetTxID(nonce) + outboundID := ob.OutboundID(nonce) ob.Mu().Lock() defer ob.Mu().Unlock() @@ -501,17 +495,17 @@ func (ob *Observer) setIncludedTx(nonce uint64, getTxResult *btcjson.GetTransact func (ob *Observer) getIncludedTx(nonce uint64) *btcjson.GetTransactionResult { ob.Mu().Lock() defer ob.Mu().Unlock() - return ob.includedTxResults[ob.GetTxID(nonce)] + return ob.includedTxResults[ob.OutboundID(nonce)] } // removeIncludedTx removes included tx from memory func (ob *Observer) removeIncludedTx(nonce uint64) { ob.Mu().Lock() defer ob.Mu().Unlock() - txResult, found := ob.includedTxResults[ob.GetTxID(nonce)] + txResult, found := ob.includedTxResults[ob.OutboundID(nonce)] if found { delete(ob.includedTxHashes, txResult.TxID) - delete(ob.includedTxResults, ob.GetTxID(nonce)) + delete(ob.includedTxResults, ob.OutboundID(nonce)) } } diff --git a/zetaclient/chains/bitcoin/observer/outbound_test.go b/zetaclient/chains/bitcoin/observer/outbound_test.go index d661b1c7bb..b1f7ae309b 100644 --- a/zetaclient/chains/bitcoin/observer/outbound_test.go +++ b/zetaclient/chains/bitcoin/observer/outbound_test.go @@ -71,7 +71,7 @@ func createObserverWithUTXOs(t *testing.T) *Observer { func mineTxNSetNonceMark(ob *Observer, nonce uint64, txid string, preMarkIndex int) { // Mine transaction - outboundID := ob.GetTxID(nonce) + outboundID := ob.OutboundID(nonce) ob.includedTxResults[outboundID] = &btcjson.GetTransactionResult{TxID: txid} // Set nonce mark diff --git a/zetaclient/chains/bitcoin/signer/signer.go b/zetaclient/chains/bitcoin/signer/signer.go index fc23e98aa4..7e701e523d 100644 --- a/zetaclient/chains/bitcoin/signer/signer.go +++ b/zetaclient/chains/bitcoin/signer/signer.go @@ -108,10 +108,12 @@ func (signer *Signer) GetERC20CustodyAddress() ethcommon.Address { } // SetGatewayAddress does nothing for BTC +// Note: TSS address will be used as gateway address for Bitcoin func (signer *Signer) SetGatewayAddress(_ string) { } // GetGatewayAddress returns empty address +// Note: same as SetGatewayAddress func (signer *Signer) GetGatewayAddress() string { return "" } diff --git a/zetaclient/chains/evm/observer/observer.go b/zetaclient/chains/evm/observer/observer.go index d49e5a53a3..b6ff80c769 100644 --- a/zetaclient/chains/evm/observer/observer.go +++ b/zetaclient/chains/evm/observer/observer.go @@ -236,31 +236,31 @@ func (ob *Observer) WatchRPCStatus(ctx context.Context) error { func (ob *Observer) SetPendingTx(nonce uint64, transaction *ethtypes.Transaction) { ob.Mu().Lock() defer ob.Mu().Unlock() - ob.outboundPendingTransactions[ob.GetTxID(nonce)] = transaction + ob.outboundPendingTransactions[ob.OutboundID(nonce)] = transaction } // GetPendingTx gets the pending transaction from memory func (ob *Observer) GetPendingTx(nonce uint64) *ethtypes.Transaction { ob.Mu().Lock() defer ob.Mu().Unlock() - return ob.outboundPendingTransactions[ob.GetTxID(nonce)] + return ob.outboundPendingTransactions[ob.OutboundID(nonce)] } // SetTxNReceipt sets the receipt and transaction in memory func (ob *Observer) SetTxNReceipt(nonce uint64, receipt *ethtypes.Receipt, transaction *ethtypes.Transaction) { ob.Mu().Lock() defer ob.Mu().Unlock() - delete(ob.outboundPendingTransactions, ob.GetTxID(nonce)) // remove pending transaction, if any - ob.outboundConfirmedReceipts[ob.GetTxID(nonce)] = receipt - ob.outboundConfirmedTransactions[ob.GetTxID(nonce)] = transaction + delete(ob.outboundPendingTransactions, ob.OutboundID(nonce)) // remove pending transaction, if any + ob.outboundConfirmedReceipts[ob.OutboundID(nonce)] = receipt + ob.outboundConfirmedTransactions[ob.OutboundID(nonce)] = transaction } // GetTxNReceipt gets the receipt and transaction from memory func (ob *Observer) GetTxNReceipt(nonce uint64) (*ethtypes.Receipt, *ethtypes.Transaction) { ob.Mu().Lock() defer ob.Mu().Unlock() - receipt := ob.outboundConfirmedReceipts[ob.GetTxID(nonce)] - transaction := ob.outboundConfirmedTransactions[ob.GetTxID(nonce)] + receipt := ob.outboundConfirmedReceipts[ob.OutboundID(nonce)] + transaction := ob.outboundConfirmedTransactions[ob.OutboundID(nonce)] return receipt, transaction } @@ -268,8 +268,8 @@ func (ob *Observer) GetTxNReceipt(nonce uint64) (*ethtypes.Receipt, *ethtypes.Tr func (ob *Observer) IsTxConfirmed(nonce uint64) bool { ob.Mu().Lock() defer ob.Mu().Unlock() - return ob.outboundConfirmedReceipts[ob.GetTxID(nonce)] != nil && - ob.outboundConfirmedTransactions[ob.GetTxID(nonce)] != nil + return ob.outboundConfirmedReceipts[ob.OutboundID(nonce)] != nil && + ob.outboundConfirmedTransactions[ob.OutboundID(nonce)] != nil } // CheckTxInclusion returns nil only if tx is included at the position indicated by the receipt ([block, index]) diff --git a/zetaclient/chains/evm/observer/outbound.go b/zetaclient/chains/evm/observer/outbound.go index c2c4ed970b..6b704e1cd2 100644 --- a/zetaclient/chains/evm/observer/outbound.go +++ b/zetaclient/chains/evm/observer/outbound.go @@ -28,12 +28,6 @@ import ( "github.com/zeta-chain/zetacore/zetaclient/zetacore" ) -// GetTxID returns a unique id for outbound tx -func (ob *Observer) GetTxID(nonce uint64) string { - tssAddr := ob.TSS().EVMAddress().String() - return fmt.Sprintf("%d-%s-%d", ob.Chain().ChainId, tssAddr, nonce) -} - // WatchOutbound watches evm chain for outgoing txs status // TODO(revamp): move ticker function to ticker file // TODO(revamp): move inner logic to a separate function diff --git a/zetaclient/chains/evm/signer/signer.go b/zetaclient/chains/evm/signer/signer.go index 8f9c2cc6dd..f0f00aa237 100644 --- a/zetaclient/chains/evm/signer/signer.go +++ b/zetaclient/chains/evm/signer/signer.go @@ -371,12 +371,14 @@ func (signer *Signer) TryProcessOutbound( }() // prepare logger + params := cctx.GetCurrentOutboundParam() logger := signer.Logger().Std.With(). - Str("OutboundID", outboundID). - Str("SendHash", cctx.Index). + Str("method", "TryProcessOutbound"). + Int64("chain", signer.Chain().ChainId). + Uint64("nonce", params.TssNonce). + Str("cctx", cctx.Index). Logger() - params := cctx.GetCurrentOutboundParam() myID := zetacoreClient.GetKeys().GetOperatorAddress() logger.Info(). Msgf("EVM TryProcessOutbound: %s, value %d to %s", cctx.Index, params.Amount.BigInt(), params.Receiver) diff --git a/zetaclient/chains/interfaces/interfaces.go b/zetaclient/chains/interfaces/interfaces.go index 38979c03c8..85328c1eb4 100644 --- a/zetaclient/chains/interfaces/interfaces.go +++ b/zetaclient/chains/interfaces/interfaces.go @@ -47,7 +47,6 @@ type ChainObserver interface { ) (bool, bool, error) SetChainParams(observertypes.ChainParams) GetChainParams() observertypes.ChainParams - GetTxID(nonce uint64) string WatchInboundTracker(ctx context.Context) error } diff --git a/zetaclient/chains/solana/observer/inbound.go b/zetaclient/chains/solana/observer/inbound.go index 4a819ce9cb..7c14ec34c6 100644 --- a/zetaclient/chains/solana/observer/inbound.go +++ b/zetaclient/chains/solana/observer/inbound.go @@ -15,7 +15,7 @@ import ( "github.com/zeta-chain/zetacore/pkg/coin" "github.com/zeta-chain/zetacore/pkg/constant" - solanacontract "github.com/zeta-chain/zetacore/pkg/contract/solana" + solanacontracts "github.com/zeta-chain/zetacore/pkg/contracts/solana" crosschaintypes "github.com/zeta-chain/zetacore/x/crosschain/types" solanarpc "github.com/zeta-chain/zetacore/zetaclient/chains/solana/rpc" "github.com/zeta-chain/zetacore/zetaclient/compliance" @@ -274,14 +274,14 @@ func (ob *Observer) ParseInboundAsDeposit( instruction := tx.Message.Instructions[instructionIndex] // try deserializing instruction as a 'deposit' - var inst solanacontract.DepositInstructionParams + var inst solanacontracts.DepositInstructionParams err := borsh.Deserialize(&inst, instruction.Data) if err != nil { return nil, nil } // check if the instruction is a deposit or not - if inst.Discriminator != solanacontract.DiscriminatorDeposit() { + if inst.Discriminator != solanacontracts.DiscriminatorDeposit() { return nil, nil } @@ -324,8 +324,8 @@ func (ob *Observer) ParseInboundAsDepositSPL( // Note: solana-go is not able to parse the AccountMeta 'is_signer' ATM. This is a workaround. func (ob *Observer) GetSignerDeposit(tx *solana.Transaction, inst *solana.CompiledInstruction) (string, error) { // there should be 4 accounts for a deposit instruction - if len(inst.Accounts) != solanacontract.AccountsNumDeposit { - return "", fmt.Errorf("want %d accounts, got %d", solanacontract.AccountsNumDeposit, len(inst.Accounts)) + if len(inst.Accounts) != solanacontracts.AccountsNumDeposit { + return "", fmt.Errorf("want %d accounts, got %d", solanacontracts.AccountsNumDeposit, len(inst.Accounts)) } // the accounts are [signer, pda, system_program, gateway_program] diff --git a/zetaclient/chains/solana/observer/observer.go b/zetaclient/chains/solana/observer/observer.go index 8a89959929..ad135aeecf 100644 --- a/zetaclient/chains/solana/observer/observer.go +++ b/zetaclient/chains/solana/observer/observer.go @@ -9,7 +9,7 @@ import ( "github.com/zeta-chain/zetacore/pkg/bg" "github.com/zeta-chain/zetacore/pkg/chains" - contract "github.com/zeta-chain/zetacore/pkg/contract/solana" + contracts "github.com/zeta-chain/zetacore/pkg/contracts/solana" observertypes "github.com/zeta-chain/zetacore/x/observer/types" "github.com/zeta-chain/zetacore/zetaclient/chains/base" "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" @@ -65,7 +65,7 @@ func NewObserver( } // parse gateway ID and PDA - gatewayID, pda, err := contract.ParseGatewayIDAndPda(chainParams.GatewayAddress) + gatewayID, pda, err := contracts.ParseGatewayIDAndPda(chainParams.GatewayAddress) if err != nil { return nil, errors.Wrapf(err, "cannot parse gateway address %s", chainParams.GatewayAddress) } @@ -144,19 +144,19 @@ func (ob *Observer) LoadLastTxScanned() error { func (ob *Observer) SetTxResult(nonce uint64, result *rpc.GetTransactionResult) { ob.Mu().Lock() defer ob.Mu().Unlock() - ob.finalizedTxResults[ob.GetTxID(nonce)] = result + ob.finalizedTxResults[ob.OutboundID(nonce)] = result } // GetTxResult returns the tx result for the given nonce func (ob *Observer) GetTxResult(nonce uint64) *rpc.GetTransactionResult { ob.Mu().Lock() defer ob.Mu().Unlock() - return ob.finalizedTxResults[ob.GetTxID(nonce)] + return ob.finalizedTxResults[ob.OutboundID(nonce)] } // IsTxFinalized returns true if there is a finalized tx for nonce func (ob *Observer) IsTxFinalized(nonce uint64) bool { ob.Mu().Lock() defer ob.Mu().Unlock() - return ob.finalizedTxResults[ob.GetTxID(nonce)] != nil + return ob.finalizedTxResults[ob.OutboundID(nonce)] != nil } diff --git a/zetaclient/chains/solana/observer/outbound.go b/zetaclient/chains/solana/observer/outbound.go index edccaa1c13..011c7f9902 100644 --- a/zetaclient/chains/solana/observer/outbound.go +++ b/zetaclient/chains/solana/observer/outbound.go @@ -8,13 +8,12 @@ import ( "cosmossdk.io/math" "github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go/rpc" - "github.com/near/borsh-go" "github.com/pkg/errors" "github.com/rs/zerolog" "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/pkg/coin" - contract "github.com/zeta-chain/zetacore/pkg/contract/solana" + contracts "github.com/zeta-chain/zetacore/pkg/contracts/solana" crosschaintypes "github.com/zeta-chain/zetacore/x/crosschain/types" "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" zctx "github.com/zeta-chain/zetacore/zetaclient/context" @@ -22,13 +21,7 @@ import ( "github.com/zeta-chain/zetacore/zetaclient/zetacore" ) -// GetTxID returns a unique id for Solana outbound -func (ob *Observer) GetTxID(nonce uint64) string { - tssAddr := ob.TSS().EVMAddress().String() - return fmt.Sprintf("%d-%s-%d", ob.Chain().ChainId, tssAddr, nonce) -} - -// WatchOutbound watches evm chain for outgoing txs status +// WatchOutbound watches solana chain for outgoing txs status // TODO(revamp): move ticker function to ticker file // TODO(revamp): move inner logic to a separate function func (ob *Observer) WatchOutbound(ctx context.Context) error { @@ -119,10 +112,7 @@ func (ob *Observer) WatchOutbound(ctx context.Context) error { // IsOutboundProcessed checks outbound status and returns (isIncluded, isFinalized, error) // It also posts vote to zetacore if the tx is finalized // TODO(revamp): rename as it also vote the outbound -func (ob *Observer) IsOutboundProcessed( - ctx context.Context, - cctx *crosschaintypes.CrossChainTx, -) (bool, bool, error) { +func (ob *Observer) IsOutboundProcessed(ctx context.Context, cctx *crosschaintypes.CrossChainTx) (bool, bool, error) { // get outbound params params := cctx.GetCurrentOutboundParam() nonce := params.TssNonce @@ -143,7 +133,7 @@ func (ob *Observer) IsOutboundProcessed( txSig := tx.Signatures[0] // parse gateway instruction from tx result - inst, err := ob.ParseGatewayInstruction(txResult, coinType) + inst, err := ParseGatewayInstruction(txResult, ob.gatewayID, coinType) if err != nil { // should never happen as it was already successfully parsed in CheckFinalizedTx return true, true, errors.Wrapf(err, "ParseGatewayInstruction error for sig %s", txSig) @@ -254,12 +244,12 @@ func (ob *Observer) CheckFinalizedTx( // the tx must be successful in order to effectively increment the nonce if txResult.Meta.Err != nil { - logger.Error().Msgf("tx is not successful for chain %d nonce %d", chainID, nonce) + logger.Error().Any("Err", txResult.Meta.Err).Msgf("tx is not successful for chain %d nonce %d", chainID, nonce) return nil, false } // parse gateway instruction from tx result - inst, err := ob.ParseGatewayInstruction(txResult, coinType) + inst, err := ParseGatewayInstruction(txResult, ob.gatewayID, coinType) if err != nil { logger.Error().Err(err).Msgf("ParseGatewayInstruction err for chain %d nonce %d", chainID, nonce) return nil, false @@ -288,11 +278,12 @@ func (ob *Observer) CheckFinalizedTx( return txResult, true } -// ParseGatewayInstruction parses the instruction signer and nonce from tx result -func (ob *Observer) ParseGatewayInstruction( +// ParseGatewayInstruction parses the outbound instruction from tx result +func ParseGatewayInstruction( txResult *rpc.GetTransactionResult, + gatewayID solana.PublicKey, coinType coin.CoinType, -) (contract.OutboundInstruction, error) { +) (contracts.OutboundInstruction, error) { // unmarshal transaction tx, err := txResult.Transaction.GetTransaction() if err != nil { @@ -312,39 +303,15 @@ func (ob *Observer) ParseGatewayInstruction( } // the instruction should be an invocation of the gateway program - if !programPk.Equals(ob.gatewayID) { + if !programPk.Equals(gatewayID) { return nil, errors.New("not a gateway program invocation") } // parse the instruction as a 'withdraw' or 'withdraw_spl_token' switch coinType { case coin.CoinType_Gas: - return ob.ParseInstructionWithdraw(tx, 0) + return contracts.ParseInstructionWithdraw(instruction) default: return nil, errors.New("unsupported outbound coin type") } } - -// ParseInstructionWithdraw tries to parse an instruction as a 'withdraw'. -// It returns nil if the instruction can't be parsed as a 'withdraw'. -func (ob *Observer) ParseInstructionWithdraw( - tx *solana.Transaction, - instructionIndex int, -) (*contract.WithdrawInstructionParams, error) { - // locate instruction by index - instruction := tx.Message.Instructions[instructionIndex] - - // try deserializing instruction as a 'withdraw' - inst := &contract.WithdrawInstructionParams{} - err := borsh.Deserialize(inst, instruction.Data) - if err != nil { - return nil, errors.Wrap(err, "error deserializing instruction") - } - - // check the discriminator to ensure it's a 'withdraw' instruction - if inst.Discriminator != contract.DiscriminatorWithdraw() { - return nil, errors.New("not a withdraw instruction") - } - - return inst, nil -} diff --git a/zetaclient/chains/solana/observer/outbound_test.go b/zetaclient/chains/solana/observer/outbound_test.go index 56e7490d9b..c3e1dfb61b 100644 --- a/zetaclient/chains/solana/observer/outbound_test.go +++ b/zetaclient/chains/solana/observer/outbound_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/require" "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/pkg/coin" + contracts "github.com/zeta-chain/zetacore/pkg/contracts/solana" "github.com/zeta-chain/zetacore/testutil/sample" "github.com/zeta-chain/zetacore/zetaclient/chains/base" "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" @@ -144,15 +145,16 @@ func Test_ParseGatewayInstruction(t *testing.T) { txHash := withdrawTxTest txAmount := uint64(890880) - // create observer - ob := createTestObserver(t, chain, nil, nil) + // gateway address + gatewayID, err := solana.PublicKeyFromBase58(GatewayAddressTest) + require.NoError(t, err) t.Run("should parse gateway instruction", func(t *testing.T) { // load archived outbound tx result txResult := testutils.LoadSolanaOutboundTxResult(t, TestDataDir, chain.ChainId, txHash) // parse gateway instruction - inst, err := ob.ParseGatewayInstruction(txResult, coin.CoinType_Gas) + inst, err := observer.ParseGatewayInstruction(txResult, gatewayID, coin.CoinType_Gas) require.NoError(t, err) // check sender, nonce and amount @@ -171,7 +173,7 @@ func Test_ParseGatewayInstruction(t *testing.T) { // remove all instructions tx.Message.Instructions = nil - inst, err := ob.ParseGatewayInstruction(txResult, coin.CoinType_Gas) + inst, err := observer.ParseGatewayInstruction(txResult, gatewayID, coin.CoinType_Gas) require.ErrorContains(t, err, "want 1 instruction, got 0") require.Nil(t, inst) }) @@ -184,7 +186,7 @@ func Test_ParseGatewayInstruction(t *testing.T) { // set invalid program id index (out of range) tx.Message.Instructions[0].ProgramIDIndex = 4 - inst, err := ob.ParseGatewayInstruction(txResult, coin.CoinType_Gas) + inst, err := observer.ParseGatewayInstruction(txResult, gatewayID, coin.CoinType_Gas) require.ErrorContains(t, err, "error getting program ID") require.Nil(t, inst) }) @@ -197,7 +199,7 @@ func Test_ParseGatewayInstruction(t *testing.T) { // set invalid program id index (pda account index) tx.Message.Instructions[0].ProgramIDIndex = 1 - inst, err := ob.ParseGatewayInstruction(txResult, coin.CoinType_Gas) + inst, err := observer.ParseGatewayInstruction(txResult, gatewayID, coin.CoinType_Gas) require.ErrorContains(t, err, "not a gateway program invocation") require.Nil(t, inst) }) @@ -210,7 +212,7 @@ func Test_ParseGatewayInstruction(t *testing.T) { // set invalid instruction data to cause parsing error tx.Message.Instructions[0].Data = []byte("invalid instruction data") - inst, err := ob.ParseGatewayInstruction(txResult, coin.CoinType_Gas) + inst, err := observer.ParseGatewayInstruction(txResult, gatewayID, coin.CoinType_Gas) require.Error(t, err) require.Nil(t, inst) }) @@ -218,7 +220,7 @@ func Test_ParseGatewayInstruction(t *testing.T) { // load and unmarshal archived transaction txResult := testutils.LoadSolanaOutboundTxResult(t, TestDataDir, chain.ChainId, txHash) - inst, err := ob.ParseGatewayInstruction(txResult, coin.CoinType_ERC20) + inst, err := observer.ParseGatewayInstruction(txResult, gatewayID, coin.CoinType_ERC20) require.ErrorContains(t, err, "unsupported outbound coin type") require.Nil(t, inst) }) @@ -230,16 +232,14 @@ func Test_ParseInstructionWithdraw(t *testing.T) { txHash := withdrawTxTest txAmount := uint64(890880) - // create observer - ob := createTestObserver(t, chain, nil, nil) - t.Run("should parse instruction withdraw", func(t *testing.T) { // load and unmarshal archived transaction txResult := testutils.LoadSolanaOutboundTxResult(t, TestDataDir, chain.ChainId, txHash) tx, err := txResult.Transaction.GetTransaction() require.NoError(t, err) - inst, err := ob.ParseInstructionWithdraw(tx, 0) + instruction := tx.Message.Instructions[0] + inst, err := contracts.ParseInstructionWithdraw(instruction) require.NoError(t, err) // check sender, nonce and amount @@ -256,9 +256,10 @@ func Test_ParseInstructionWithdraw(t *testing.T) { require.NoError(t, err) // set invalid instruction data - txFake.Message.Instructions[0].Data = []byte("invalid instruction data") + instruction := txFake.Message.Instructions[0] + instruction.Data = []byte("invalid instruction data") - inst, err := ob.ParseInstructionWithdraw(txFake, 0) + inst, err := contracts.ParseInstructionWithdraw(instruction) require.ErrorContains(t, err, "error deserializing instruction") require.Nil(t, inst) }) @@ -269,12 +270,13 @@ func Test_ParseInstructionWithdraw(t *testing.T) { require.NoError(t, err) // overwrite discriminator (first 8 bytes) + instruction := txFake.Message.Instructions[0] fakeDiscriminator := "b712469c946da12100980d0000000000" fakeDiscriminatorBytes, err := hex.DecodeString(fakeDiscriminator) require.NoError(t, err) - copy(txFake.Message.Instructions[0].Data, fakeDiscriminatorBytes) + copy(instruction.Data, fakeDiscriminatorBytes) - inst, err := ob.ParseInstructionWithdraw(txFake, 0) + inst, err := contracts.ParseInstructionWithdraw(instruction) require.ErrorContains(t, err, "not a withdraw instruction") require.Nil(t, inst) }) diff --git a/zetaclient/chains/solana/signer/signer.go b/zetaclient/chains/solana/signer/signer.go index d79cf671b1..cb64cab9d7 100644 --- a/zetaclient/chains/solana/signer/signer.go +++ b/zetaclient/chains/solana/signer/signer.go @@ -11,7 +11,7 @@ import ( "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/pkg/coin" - contract "github.com/zeta-chain/zetacore/pkg/contract/solana" + contracts "github.com/zeta-chain/zetacore/pkg/contracts/solana" "github.com/zeta-chain/zetacore/x/crosschain/types" observertypes "github.com/zeta-chain/zetacore/x/observer/types" "github.com/zeta-chain/zetacore/zetaclient/chains/base" @@ -53,7 +53,7 @@ func NewSigner( baseSigner := base.NewSigner(chain, tss, ts, logger) // parse gateway ID and PDA - gatewayID, pda, err := contract.ParseGatewayIDAndPda(chainParams.GatewayAddress) + gatewayID, pda, err := contracts.ParseGatewayIDAndPda(chainParams.GatewayAddress) if err != nil { return nil, errors.Wrapf(err, "cannot parse gateway address %s", chainParams.GatewayAddress) } @@ -74,7 +74,7 @@ func (signer *Signer) SignMsgWithdraw( ctx context.Context, params *types.OutboundParams, height uint64, -) (*contract.MsgWithdraw, error) { +) (*contracts.MsgWithdraw, error) { chain := signer.Chain() // #nosec G115 always positive chainID := uint64(signer.Chain().ChainId) @@ -88,7 +88,7 @@ func (signer *Signer) SignMsgWithdraw( } // prepare withdraw msg and compute hash - msg := contract.NewMsgWithdraw(chainID, nonce, amount, to) + msg := contracts.NewMsgWithdraw(chainID, nonce, amount, to) msgHash := msg.Hash() // sign the message with TSS to get an ECDSA signature. @@ -104,12 +104,12 @@ func (signer *Signer) SignMsgWithdraw( } // SignWithdrawTx signs the Solana gateway 'withdraw' transaction specified by 'msg' -func (signer *Signer) SignWithdrawTx(ctx context.Context, msg contract.MsgWithdraw) (*solana.Transaction, error) { +func (signer *Signer) SignWithdrawTx(ctx context.Context, msg contracts.MsgWithdraw) (*solana.Transaction, error) { // create withdraw instruction with program call data var err error var inst solana.GenericInstruction - inst.DataBytes, err = borsh.Serialize(contract.WithdrawInstructionParams{ - Discriminator: contract.DiscriminatorWithdraw(), + inst.DataBytes, err = borsh.Serialize(contracts.WithdrawInstructionParams{ + Discriminator: contracts.DiscriminatorWithdraw(), Amount: msg.Amount(), Signature: msg.SigRS(), RecoveryID: msg.SigV(), @@ -188,11 +188,11 @@ func (signer *Signer) TryProcessOutbound( // prepare logger params := cctx.GetCurrentOutboundParam() logger := signer.Logger().Std.With(). - Str("OutboundID", outboundID). - Str("SendHash", cctx.Index). + Str("method", "TryProcessOutbound"). + Int64("chain", signer.Chain().ChainId). + Uint64("nonce", params.TssNonce). + Str("cctx", cctx.Index). Logger() - logger.Info(). - Msgf("Solana TryProcessOutbound: %s, value %d to %s", cctx.Index, params.Amount.BigInt(), params.Receiver) // support gas token only for Solana outbound coinType := cctx.InboundParams.CoinType @@ -243,7 +243,7 @@ func (signer *Signer) TryProcessOutbound( // SetGatewayAddress sets the gateway address func (signer *Signer) SetGatewayAddress(address string) { // parse gateway ID and PDA - gatewayID, pda, err := contract.ParseGatewayIDAndPda(address) + gatewayID, pda, err := contracts.ParseGatewayIDAndPda(address) if err != nil { signer.Logger().Std.Error().Err(err).Msgf("cannot parse gateway address %s", address) } diff --git a/zetaclient/orchestrator/orchestrator.go b/zetaclient/orchestrator/orchestrator.go index 866a31abe8..1fb8dca4ac 100644 --- a/zetaclient/orchestrator/orchestrator.go +++ b/zetaclient/orchestrator/orchestrator.go @@ -567,7 +567,7 @@ func (oc *Orchestrator) ScheduleCctxBTC( Msgf("ScheduleCctxBTC: lookahead reached, signing %d, earliest pending %d", nonce, cctxList[0].GetCurrentOutboundParam().TssNonce) break } - // try confirming the outbound or scheduling a keysign + // schedule a TSS keysign if nonce%interval == zetaHeight%interval && !oc.outboundProc.IsOutboundActive(outboundID) { oc.outboundProc.StartTryProcess(outboundID) oc.logger.Debug().Msgf("ScheduleCctxBTC: sign outbound %s with value %d", outboundID, params.Amount) @@ -584,7 +584,7 @@ func (oc *Orchestrator) ScheduleCctxBTC( } } -// ScheduleCctxBTC schedules bitcoin outbound keysign on each ZetaChain block (the ticker) +// ScheduleCctxSolana schedules solana outbound keysign on each ZetaChain block (the ticker) func (oc *Orchestrator) ScheduleCctxSolana( ctx context.Context, zetaHeight uint64, @@ -627,7 +627,7 @@ func (oc *Orchestrator) ScheduleCctxSolana( continue } - // try confirming the outbound or scheduling a keysign + // schedule a TSS keysign if nonce%interval == zetaHeight%interval && !oc.outboundProc.IsOutboundActive(outboundID) { oc.outboundProc.StartTryProcess(outboundID) oc.logger.Debug().Msgf("ScheduleCctxSolana: sign outbound %s with value %d", outboundID, params.Amount) diff --git a/zetaclient/orchestrator/orchestrator_test.go b/zetaclient/orchestrator/orchestrator_test.go index b852da9183..af4ca5c346 100644 --- a/zetaclient/orchestrator/orchestrator_test.go +++ b/zetaclient/orchestrator/orchestrator_test.go @@ -13,7 +13,7 @@ import ( "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/pkg/coin" - solcontract "github.com/zeta-chain/zetacore/pkg/contract/solana" + solanacontracts "github.com/zeta-chain/zetacore/pkg/contracts/solana" "github.com/zeta-chain/zetacore/testutil/sample" crosschainkeeper "github.com/zeta-chain/zetacore/x/crosschain/keeper" crosschaintypes "github.com/zeta-chain/zetacore/x/crosschain/types" @@ -123,7 +123,7 @@ func Test_GetUpdatedSigner(t *testing.T) { btcChainParams := &observertypes.ChainParams{} solChainParams := &observertypes.ChainParams{ ChainId: solChain.ChainId, - GatewayAddress: solcontract.SolanaGatewayProgramID, + GatewayAddress: solanacontracts.SolanaGatewayProgramID, } // new evm chain params in AppContext @@ -210,7 +210,7 @@ func Test_GetUpdatedChainObserver(t *testing.T) { } solChainParams := &observertypes.ChainParams{ ChainId: solChain.ChainId, - GatewayAddress: solcontract.SolanaGatewayProgramID, + GatewayAddress: solanacontracts.SolanaGatewayProgramID, } // new chain params in AppContext