Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions e2e/e2etests/e2etests.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ const (
TestSolanaWithdrawName = "solana_withdraw"
TestSolanaWithdrawRevertExecutableReceiverName = "solana_withdraw_revert_executable_receiver"
TestSolanaWithdrawAndCallName = "solana_withdraw_and_call"
TestSolanaWithdrawAndCallInvalidTxSizeName = "solana_withdraw_and_call_invalid_tx_size"
TestSolanaWithdrawAndCallInvalidMsgEncodingName = "solana_withdraw_and_call_invalid_msg_encoding"
TestZEVMToSolanaCallName = "zevm_to_solana_call"
TestSolanaWithdrawAndCallRevertWithCallName = "solana_withdraw_and_call_revert_with_call"
Expand Down Expand Up @@ -722,6 +723,14 @@ var AllE2ETests = []runner.E2ETest{
TestZEVMToSolanaCall,
runner.WithMinimumVersion("v29.0.0"),
),
runner.NewE2ETest(
TestSolanaWithdrawAndCallInvalidTxSizeName,
"withdraw SOL from ZEVM and call solana program with invalid tx size",
[]runner.ArgDefinition{
{Description: "amount in lamport", DefaultValue: "1000000"},
},
TestSolanaWithdrawAndCallInvalidTxSize,
),
runner.NewE2ETest(
TestSolanaWithdrawAndCallInvalidMsgEncodingName,
"withdraw SOL from ZEVM and call solana program with invalid msg encoding",
Expand Down
114 changes: 114 additions & 0 deletions e2e/e2etests/test_solana_withdraw_and_call_invalid_tx_size.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package e2etests

import (
"crypto/rand"
"errors"
"math/big"
"strings"

"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/gagliardetto/solana-go"
"github.com/stretchr/testify/require"
"github.com/zeta-chain/protocol-contracts/pkg/gatewayzevm.sol"

"github.com/zeta-chain/node/e2e/runner"
"github.com/zeta-chain/node/e2e/utils"
solanacontract "github.com/zeta-chain/node/pkg/contracts/solana"
crosschaintypes "github.com/zeta-chain/node/x/crosschain/types"
)

// TestSolanaWithdrawAndCallInvalidTxSize executes withdrawAndCall, but with invalid tx size
// in that case, cctx is reverted due to "transaction is too large" error
func TestSolanaWithdrawAndCallInvalidTxSize(r *runner.E2ERunner, args []string) {
require.Len(r, args, 1)

// ARRANGE
// Given withdraw amount
withdrawAmount := utils.ParseBigInt(r, args[0])

// ensure runner has enough balance
balanceBefore, err := r.SOLZRC20.BalanceOf(&bind.CallOpts{}, r.EVMAddress())
require.NoError(r, err)
r.Logger.Info("runner balance of SOL before withdraw: %d", balanceBefore)

require.Equal(r, 1, balanceBefore.Cmp(withdrawAmount), "Insufficient balance for withdrawal")

// parse withdraw amount (in lamports), approve amount is 1 SOL
approvedAmount := new(big.Int).SetUint64(solana.LAMPORTS_PER_SOL)
require.Equal(
r,
-1,
withdrawAmount.Cmp(approvedAmount),
"Withdrawal amount must be less than the approved amount: %v",
approvedAmount,
)

// get connected program pda
connectedPda, err := solanacontract.ComputeConnectedPdaAddress(r.ConnectedProgram)
require.NoError(r, err)

// generate a big enough data that exceeds Solana max tx size (encoded/raw 1644/1232)
data := make([]byte, 2048)
_, err = rand.Read(data)
require.NoError(r, err)

// encode msg
msg := solanacontract.ExecuteMsg{
Accounts: []solanacontract.AccountMeta{
{PublicKey: [32]byte(connectedPda.Bytes()), IsWritable: true},
{PublicKey: [32]byte(r.ComputePdaAddress().Bytes()), IsWritable: false},
{PublicKey: [32]byte(r.GetSolanaPrivKey().PublicKey().Bytes()), IsWritable: true},
{PublicKey: [32]byte(solana.SystemProgramID.Bytes()), IsWritable: false},
{PublicKey: [32]byte(solana.SysVarInstructionsPubkey.Bytes()), IsWritable: false},
},
Data: data,
}

msgEncoded, err := msg.Encode()
require.NoError(r, err)

// ACT
// withdraw and call
tx := r.WithdrawAndCallSOLZRC20(
withdrawAmount,
approvedAmount,
msgEncoded,
gatewayzevm.RevertOptions{OnRevertGasLimit: big.NewInt(0)},
)

// refresh ERC20 SOL balance immediately after withdraw
// the refund is now on the way, we need to remember the balance and wait for update
balanceBefore, err = r.SOLZRC20.BalanceOf(&bind.CallOpts{}, r.EVMAddress())
require.NoError(r, err)
r.Logger.Info("runner balance of SOL right after withdraw: %d", balanceBefore)

// ASSERT
// wait for the cctx to be reverted
cctx := utils.WaitCctxMinedByInboundHash(r.Ctx, tx.Hash().Hex(), r.CctxClient, r.Logger, r.CctxTimeout)
utils.RequireCCTXStatus(r, cctx, crosschaintypes.CctxStatus_Reverted)

// get ERC20 SOL balance after withdraw
balanceAfter, err := r.SOLZRC20.BalanceOf(&bind.CallOpts{}, r.EVMAddress())
require.NoError(r, err)
r.Logger.Info("runner balance of SOL after CCTX is reverted: %d", balanceAfter)

// check if the balance is increased correctly
balanceExpected := new(big.Int).Add(balanceBefore, withdrawAmount)
require.True(r, balanceExpected.Cmp(balanceAfter) == 0, "balance is not refunded correctly")

// check that failure log is attached to increment nonce instruction
txIncNonce, err := r.SolanaClient.GetTransaction(
r.Ctx,
solana.MustSignatureFromBase58(cctx.OutboundParams[0].Hash),
nil,
)
require.NoError(r, err)

expectedLog := "Program log: Failure reason: transaction is too large"
for _, log := range txIncNonce.Meta.LogMessages {
if strings.Contains(log, expectedLog) {
return
}
}
require.NoError(r, errors.New("expected log not found"))
}
31 changes: 31 additions & 0 deletions zetaclient/chains/solana/signer/fallback_tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ import (
"github.com/gagliardetto/solana-go/rpc/jsonrpc"
)

const (
transactionSizeError = "transaction is too large"

// Solana uses JSON-RPC error code -32602 for invalid params
// see: https://github.com/paritytech/jsonrpc/blob/dc9550b4b0d8bf409d025eba7e9b229b67af9401/core/src/types/error.rs#L32
errorCodeJSONRPCInvalidParams = -32602
)

// parseRPCErrorForFallback parse error as RPCError and verifies if fallback tx should be used
// and which failure reason to attach to fallback tx
func parseRPCErrorForFallback(err error, program string) (useFallback bool, failureReason string) {
Expand All @@ -14,6 +22,11 @@ func parseRPCErrorForFallback(err error, program string) (useFallback bool, fail
return false, ""
}

// Special handling for transaction size errors
if isTransactionSizeError(rpcErr) {
return true, transactionSizeError
}

if !strings.Contains(rpcErr.Message, "Error processing Instruction") {
return false, ""
}
Expand Down Expand Up @@ -102,3 +115,21 @@ func containsNonceMismatch(logs []string) bool {
}
return false
}

// isTransactionSizeError checks if the error matches transaction size error patterns
//
// the invalid transaction size error message will be like:
// "base64 encoded solana_transaction::versioned::VersionedTransaction too large: 2012 bytes (max: encoded/raw 1644/1232)"
//
// see:
//
// https://github.com/solana-labs/solana/blob/bfacaf616fa4a1c57e2a337fcc864c92c25815a0/rpc/src/rpc.rs#L4506
// https://github.com/solana-labs/solana/blob/bfacaf616fa4a1c57e2a337fcc864c92c25815a0/rpc/src/rpc.rs#L4521
func isTransactionSizeError(rpcErr *jsonrpc.RPCError) bool {
if rpcErr.Code == errorCodeJSONRPCInvalidParams && strings.Contains(rpcErr.Message, "too large") &&
strings.Contains(rpcErr.Message, "bytes (max: encoded/raw") {
return true
}

return false
}
26 changes: 26 additions & 0 deletions zetaclient/chains/solana/signer/fallback_tx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,30 @@ func Test_ParseRPCErrorForFallback(t *testing.T) {
require.False(t, shouldUseFallbackTx)
require.Empty(t, failureReason)
})

t.Run("use fallback, transaction size error", func(t *testing.T) {
err := &jsonrpc.RPCError{
Code: errorCodeJSONRPCInvalidParams,
Message: "base64 encoded solana_transaction::versioned::VersionedTransaction too large: 2012 bytes (max: encoded/raw 1644/1232)",
Data: nil,
}

shouldUseFallbackTx, failureReason := parseRPCErrorForFallback(err, gateway)

require.True(t, shouldUseFallbackTx)
require.Equal(t, transactionSizeError, failureReason)
})

t.Run("don't use fallback, invalid params error but not transaction size", func(t *testing.T) {
err := &jsonrpc.RPCError{
Code: errorCodeJSONRPCInvalidParams,
Message: "some other invalid parameter error",
Data: nil,
}

shouldUseFallbackTx, failureReason := parseRPCErrorForFallback(err, gateway)

require.False(t, shouldUseFallbackTx)
require.Empty(t, failureReason)
})
}