diff --git a/e2e/interchaintestv8/solana/solana.go b/e2e/interchaintestv8/solana/solana.go index c31f110fc..28018b16f 100644 --- a/e2e/interchaintestv8/solana/solana.go +++ b/e2e/interchaintestv8/solana/solana.go @@ -58,7 +58,7 @@ func (s *Solana) NewTransactionFromInstructions(payerPubKey solana.PublicKey, in ) } -// SignTx signs a transaction with the provided signers, broadcasts it, and confirms it. +// SignTx signs a transaction with the provided signers, broadcasts it, and confirms it is finalized. func (s *Solana) SignAndBroadcastTx(ctx context.Context, tx *solana.Transaction, signers ...*solana.Wallet) (solana.Signature, error) { _, err := s.SignTx(ctx, tx, signers...) if err != nil { @@ -113,6 +113,7 @@ func confirmationStatusLevel(status rpc.ConfirmationStatusType) int { } } +// Waits for transaction reaching status func (s *Solana) WaitForTxStatus(txSig solana.Signature, status rpc.ConfirmationStatusType) error { return testutil.WaitForCondition(time.Second*30, time.Second, func() (bool, error) { out, err := s.RPCClient.GetSignatureStatuses(context.TODO(), false, txSig) @@ -189,13 +190,12 @@ func (s *Solana) WaitForProgramAvailabilityWithTimeout(ctx context.Context, prog return false } -// SignAndBroadcastTxWithRetry retries transaction broadcasting with default timeout +// SignTx signs a transaction with the provided signers, broadcasts it, and confirms it is finalized, retries with default timeout func (s *Solana) SignAndBroadcastTxWithRetry(ctx context.Context, tx *solana.Transaction, signers ...*solana.Wallet) (solana.Signature, error) { return s.SignAndBroadcastTxWithRetryTimeout(ctx, tx, 30, signers...) } -// SignAndBroadcastTxWithRetryTimeout retries transaction broadcasting with specified timeout -// It refreshes the blockhash on each attempt to handle expired blockhashes +// SignTx signs a transaction with the provided signers, broadcasts it, and confirms it is finalized, retries with timeout func (s *Solana) SignAndBroadcastTxWithRetryTimeout(ctx context.Context, tx *solana.Transaction, timeoutSeconds int, signers ...*solana.Wallet) (solana.Signature, error) { var lastErr error for range timeoutSeconds { @@ -218,10 +218,12 @@ func (s *Solana) SignAndBroadcastTxWithRetryTimeout(ctx context.Context, tx *sol return solana.Signature{}, fmt.Errorf("transaction broadcast timed out after %d seconds: %w", timeoutSeconds, lastErr) } +// SignTx signs a transaction with the provided signers, broadcasts it, and confirms it is in confirmed status func (s *Solana) SignAndBroadcastTxWithConfirmedStatus(ctx context.Context, tx *solana.Transaction, wallet *solana.Wallet) (solana.Signature, error) { return s.SignAndBroadcastTxWithOpts(ctx, tx, wallet, rpc.ConfirmationStatusConfirmed) } +// SignTx signs a transaction with the provided signers, broadcasts it, and confirms it is in requested status func (s *Solana) SignAndBroadcastTxWithOpts(ctx context.Context, tx *solana.Transaction, wallet *solana.Wallet, status rpc.ConfirmationStatusType) (solana.Signature, error) { _, err := s.SignTx(ctx, tx, wallet) if err != nil { diff --git a/e2e/interchaintestv8/solana_gmp_test.go b/e2e/interchaintestv8/solana_gmp_test.go index bdeff97e5..0ff93752b 100644 --- a/e2e/interchaintestv8/solana_gmp_test.go +++ b/e2e/interchaintestv8/solana_gmp_test.go @@ -419,10 +419,9 @@ func (s *IbcEurekaSolanaTestSuite) Test_GMPCounterFromCosmos() { DstClientId: SolanaClientID, }) s.Require().NoError(err, "Relayer Update Client failed") - s.Require().NotEmpty(updateResp.Txs, "Relayer Update client should return chunked transactions") + s.Require().NotEmpty(updateResp.Tx, "Relayer Update client should return chunked transactions") s.submitChunkedUpdateClient(ctx, updateResp, s.SolanaUser) - s.T().Logf("%s: Updated Tendermint client on Solana using %d chunked transactions", userLabel, len(updateResp.Txs)) // Now retrieve and relay the GMP packet resp, err := s.RelayerClient.RelayByTx(context.Background(), &relayertypes.RelayByTxRequest{ @@ -433,8 +432,7 @@ func (s *IbcEurekaSolanaTestSuite) Test_GMPCounterFromCosmos() { DstClientId: SolanaClientID, }) s.Require().NoError(err) - s.Require().NotEmpty(resp.Txs, "Relay should return chunked transactions") - s.T().Logf("%s: Retrieved %d relay transactions (chunks + final instructions)", userLabel, len(resp.Txs)) + s.Require().NotEmpty(resp.Tx, "Relay should return chunked transactions") // Execute on Solana using chunked submission solanaRelayTxSig = s.submitChunkedRelayPackets(ctx, resp, s.SolanaUser) @@ -716,10 +714,9 @@ func (s *IbcEurekaSolanaTestSuite) Test_GMPSPLTokenTransferFromCosmos() { DstClientId: SolanaClientID, }) s.Require().NoError(err, "Relayer Update Client failed") - s.Require().NotEmpty(updateResp.Txs, "Relayer Update client should return chunked transactions") + s.Require().NotEmpty(updateResp.Tx, "Relayer Update client should return chunked transactions") s.submitChunkedUpdateClient(ctx, updateResp, s.SolanaUser) - s.T().Logf("Updated Tendermint client on Solana using %d chunked transactions", len(updateResp.Txs)) })) s.Require().True(s.Run("Retrieve relay tx from relayer", func() { @@ -731,8 +728,7 @@ func (s *IbcEurekaSolanaTestSuite) Test_GMPSPLTokenTransferFromCosmos() { DstClientId: SolanaClientID, }) s.Require().NoError(err) - s.Require().NotEmpty(resp.Txs, "Relay should return chunked transactions") - s.T().Logf("Retrieved %d relay transactions (chunks + final instructions)", len(resp.Txs)) + s.Require().NotEmpty(resp.Tx, "Relay should return chunked transactions") solanaRelayTxSig = s.submitChunkedRelayPackets(ctx, resp, s.SolanaUser) s.T().Logf("SPL transfer executed on Solana: %s", solanaRelayTxSig) @@ -831,8 +827,8 @@ func (s *IbcEurekaSolanaTestSuite) createSPLTokenMint(ctx context.Context, decim return solanago.PublicKey{}, err } - // Sign and broadcast with both payer and mint account (with retry) - _, err = s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx, s.SolanaUser, mintAccount) + // Sign and broadcast with both payer and mint account + _, err = s.SolanaChain.SignAndBroadcastTx(ctx, tx, s.SolanaUser, mintAccount) if err != nil { return solanago.PublicKey{}, err } @@ -881,8 +877,8 @@ func (s *IbcEurekaSolanaTestSuite) createTokenAccount(ctx context.Context, mint, return solanago.PublicKey{}, err } - // Sign and broadcast with both payer and token account (with retry) - _, err = s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx, s.SolanaUser, tokenAccount) + // Sign and broadcast with both payer and token account + _, err = s.SolanaChain.SignAndBroadcastTx(ctx, tx, s.SolanaUser, tokenAccount) if err != nil { return solanago.PublicKey{}, err } @@ -1199,10 +1195,9 @@ func (s *IbcEurekaSolanaTestSuite) Test_GMPSendCallFromSolana() { DstClientId: SolanaClientID, }) s.Require().NoError(err, "Relayer Update Client failed") - s.Require().NotEmpty(resp.Txs, "Relayer Update client should return transactions") + s.Require().NotEmpty(resp.Tx, "Relayer Update client should return transactions") s.submitChunkedUpdateClient(ctx, resp, s.SolanaUser) - s.T().Logf("Successfully updated Tendermint client on Solana using %d transaction(s)", len(resp.Txs)) })) s.Require().True(s.Run("Relay acknowledgement", func() { @@ -1214,8 +1209,7 @@ func (s *IbcEurekaSolanaTestSuite) Test_GMPSendCallFromSolana() { DstClientId: SolanaClientID, }) s.Require().NoError(err) - s.Require().NotEmpty(resp.Txs, "Relay should return chunked transactions") - s.T().Logf("Retrieved %d relay transactions (chunks + final instructions)", len(resp.Txs)) + s.Require().NotEmpty(resp.Tx, "Relay should return chunked transactions") sig := s.submitChunkedRelayPackets(ctx, resp, s.SolanaUser) s.T().Logf("Acknowledgement transaction broadcasted: %s", sig) @@ -1418,10 +1412,9 @@ func (s *IbcEurekaSolanaTestSuite) Test_GMPTimeoutFromSolana() { DstClientId: SolanaClientID, }) s.Require().NoError(err, "Relayer Update Client failed") - s.Require().NotEmpty(resp.Txs, "Relayer Update client should return transactions") + s.Require().NotEmpty(resp.Tx, "Relayer Update client should return transactions") s.submitChunkedUpdateClient(ctx, resp, s.SolanaUser) - s.T().Logf("Successfully updated Tendermint client on Solana using %d transaction(s)", len(resp.Txs)) })) s.Require().True(s.Run("Relay timeout transaction", func() { @@ -1433,8 +1426,7 @@ func (s *IbcEurekaSolanaTestSuite) Test_GMPTimeoutFromSolana() { DstClientId: SolanaClientID, }) s.Require().NoError(err) - s.Require().NotEmpty(resp.Txs, "Relay should return chunked transactions") - s.T().Logf("Retrieved %d relay transactions (chunks + final instructions)", len(resp.Txs)) + s.Require().NotEmpty(resp.Tx, "Relay should return chunked transactions") sig := s.submitChunkedRelayPackets(ctx, resp, s.SolanaUser) s.T().Logf("Timeout transaction broadcasted: %s", sig) diff --git a/e2e/interchaintestv8/solana_test.go b/e2e/interchaintestv8/solana_test.go index ebd9d5bfa..3a981e84d 100644 --- a/e2e/interchaintestv8/solana_test.go +++ b/e2e/interchaintestv8/solana_test.go @@ -11,6 +11,7 @@ import ( bin "github.com/gagliardetto/binary" "github.com/stretchr/testify/suite" + "google.golang.org/protobuf/proto" solanago "github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go/rpc" @@ -553,11 +554,10 @@ func (s *IbcEurekaSolanaTestSuite) Test_SolanaToCosmosTransfer_SendPacket() { DstClientId: SolanaClientID, }) s.Require().NoError(err, "Relayer Update Client failed") - s.Require().NotEmpty(resp.Txs, "Relayer Update client should return chunked transactions") + s.Require().NotEmpty(resp.Tx, "Relayer Update client should return transaction") s.submitChunkedUpdateClient(ctx, resp, s.SolanaUser) s.Require().NoError(err, "Failed to submit chunked update client transactions") - s.T().Logf("Successfully updated Tendermint client on Solana using %d chunked transactions", len(resp.Txs)) })) s.Require().True(s.Run("Relay acknowledgment", func() { @@ -569,11 +569,9 @@ func (s *IbcEurekaSolanaTestSuite) Test_SolanaToCosmosTransfer_SendPacket() { DstClientId: SolanaClientID, }) s.Require().NoError(err) - s.Require().NotEmpty(resp.Txs, "Relay should return chunked transactions") - s.T().Logf("Retrieved %d relay transactions (chunks + final instructions)", len(resp.Txs)) + s.Require().NotEmpty(resp.Tx, "Relay should return transaction") _ = s.submitChunkedRelayPackets(ctx, resp, s.SolanaUser) - s.T().Logf("Successfully relayed acknowledgment to Solana using %d transactions", len(resp.Txs)) s.verifyPacketCommitmentDeleted(ctx, SolanaClientID, 1) })) @@ -721,11 +719,10 @@ func (s *IbcEurekaSolanaTestSuite) Test_SolanaToCosmosTransfer_SendTransfer() { DstClientId: SolanaClientID, }) s.Require().NoError(err, "Relayer failed to generate update txs") - s.Require().NotEmpty(resp.Txs, "Update client should return chunked transactions") + s.Require().NotEmpty(resp.Tx, "Update client should return transaction") s.submitChunkedUpdateClient(ctx, resp, s.SolanaUser) s.Require().NoError(err, "Failed to submit chunked update client transactions") - s.T().Logf("Successfully updated Tendermint client on Solana using %d chunked transactions", len(resp.Txs)) })) s.Require().True(s.Run("Relay acknowledgment", func() { @@ -737,11 +734,9 @@ func (s *IbcEurekaSolanaTestSuite) Test_SolanaToCosmosTransfer_SendTransfer() { DstClientId: SolanaClientID, }) s.Require().NoError(err) - s.Require().NotEmpty(resp.Txs, "Relay should return chunked transactions") - s.T().Logf("Retrieved %d relay transactions (chunks + final instructions)", len(resp.Txs)) + s.Require().NotEmpty(resp.Tx, "Relay should return transaction") _ = s.submitChunkedRelayPackets(ctx, resp, s.SolanaUser) - s.T().Logf("Successfully relayed acknowledgment to Solana using %d transactions", len(resp.Txs)) s.verifyPacketCommitmentDeleted(ctx, SolanaClientID, 1) })) @@ -840,11 +835,10 @@ func (s *IbcEurekaSolanaTestSuite) Test_CosmosToSolanaTransfer() { DstClientId: SolanaClientID, }) s.Require().NoError(err, "Relayer Update Client failed") - s.Require().NotEmpty(resp.Txs, "Relayer Update client should return chunked transactions") + s.Require().NotEmpty(resp.Tx, "Relayer Update client should return transaction") s.submitChunkedUpdateClient(ctx, resp, s.SolanaUser) s.Require().NoError(err, "Failed to submit chunked update client transactions") - s.T().Logf("Successfully updated Tendermint client on Solana using %d chunked transactions", len(resp.Txs)) })) s.Require().True(s.Run("Relay acknowledgment", func() { @@ -856,11 +850,9 @@ func (s *IbcEurekaSolanaTestSuite) Test_CosmosToSolanaTransfer() { DstClientId: SolanaClientID, }) s.Require().NoError(err) - s.Require().NotEmpty(resp.Txs, "Relay should return chunked transactions") - s.T().Logf("Retrieved %d relay transactions (chunks + final instructions)", len(resp.Txs)) + s.Require().NotEmpty(resp.Tx, "Relay should return transaction") solanaRelayTxSig = s.submitChunkedRelayPackets(ctx, resp, s.SolanaUser) - s.T().Logf("Successfully relayed acknowledgment to Solana using %d transactions", len(resp.Txs)) s.verifyPacketCommitmentDeleted(ctx, SolanaClientID, 1) })) @@ -930,19 +922,23 @@ func (s *IbcEurekaSolanaTestSuite) Test_CosmosToSolanaTransfer() { // Helpers func (s *IbcEurekaSolanaTestSuite) submitChunkedUpdateClient(ctx context.Context, resp *relayertypes.UpdateClientResponse, user *solanago.Wallet) { - s.Require().NotEqual(0, len(resp.Txs), "no chunked transactions provided") + // Deserialize TransactionBatch from resp.Tx + var batch relayertypes.TransactionBatch + err := proto.Unmarshal(resp.Tx, &batch) + s.Require().NoError(err, "Failed to unmarshal TransactionBatch") + s.Require().NotEqual(0, len(batch.Txs), "no chunked transactions provided") totalStart := time.Now() // Transaction structure: [chunk1, chunk2, ..., chunkN, assembly] - chunkCount := len(resp.Txs) - 1 // Total minus assembly + chunkCount := len(batch.Txs) - 1 // Total minus assembly s.T().Logf("=== Starting Chunked Update Client ===") s.T().Logf("Total transactions: %d (%d chunks + 1 assembly)", - len(resp.Txs), + len(batch.Txs), chunkCount) chunkStart := 0 - chunkEnd := len(resp.Txs) - 1 // Everything except last (assembly) + chunkEnd := len(batch.Txs) - 1 // Everything except last (assembly) type chunkResult struct { index int @@ -961,7 +957,7 @@ func (s *IbcEurekaSolanaTestSuite) submitChunkedUpdateClient(ctx context.Context chunkTxStart := time.Now() // Decode - tx, err := solanago.TransactionFromDecoder(bin.NewBinDecoder(resp.Txs[idx])) + tx, err := solanago.TransactionFromDecoder(bin.NewBinDecoder(batch.Txs[idx])) if err != nil { chunkResults <- chunkResult{ index: idx, @@ -1015,7 +1011,7 @@ func (s *IbcEurekaSolanaTestSuite) submitChunkedUpdateClient(ctx context.Context s.T().Logf("--- Phase 2: Assembling and updating client ---") assemblyStart := time.Now() - tx, err := solanago.TransactionFromDecoder(bin.NewBinDecoder(resp.Txs[len(resp.Txs)-1])) + tx, err := solanago.TransactionFromDecoder(bin.NewBinDecoder(batch.Txs[len(batch.Txs)-1])) s.Require().NoError(err, "Failed to decode assembly tx") sig, err := s.SolanaChain.SignAndBroadcastTxWithConfirmedStatus(ctx, tx, user) @@ -1032,42 +1028,204 @@ func (s *IbcEurekaSolanaTestSuite) submitChunkedUpdateClient(ctx context.Context } func (s *IbcEurekaSolanaTestSuite) submitChunkedRelayPackets(ctx context.Context, resp *relayertypes.RelayByTxResponse, user *solanago.Wallet) solanago.Signature { - s.Require().NotEqual(0, len(resp.Txs), "no relay transactions provided") + var batch relayertypes.RelayPacketBatch + err := proto.Unmarshal(resp.Tx, &batch) + s.Require().NoError(err, "Failed to unmarshal RelayPacketBatch") + s.Require().NotEqual(0, len(batch.Packets), "no relay packets provided") totalStart := time.Now() s.T().Logf("=== Starting Chunked Relay Packets ===") - s.T().Logf("Total transactions: %d (chunks + final instructions)", len(resp.Txs)) + s.T().Logf("Total packets: %d", len(batch.Packets)) + totalChunks := 0 + for _, packet := range batch.Packets { + totalChunks += len(packet.Chunks) + } + s.T().Logf("Total chunks across all packets: %d", totalChunks) + + type packetResult struct { + packetIdx int + finalSig solanago.Signature + err error + chunksDuration time.Duration + finalDuration time.Duration + totalDuration time.Duration + } + + // Process all packets in parallel + packetResults := make(chan packetResult, len(batch.Packets)) + + for packetIdx, packet := range batch.Packets { + go func(pktIdx int, pkt *relayertypes.PacketTransactions) { + packetStart := time.Now() + s.T().Logf("--- Packet %d: Starting (%d chunks + 1 final tx) ---", pktIdx+1, len(pkt.Chunks)) + + type chunkResult struct { + chunkIdx int + sig solanago.Signature + err error + duration time.Duration + } + + // Phase 1: Submit all chunks for this packet in parallel + chunksStart := time.Now() + chunkResults := make(chan chunkResult, len(pkt.Chunks)) + + for chunkIdx, chunkBytes := range pkt.Chunks { + go func(chkIdx int, chunkData []byte) { + chunkStart := time.Now() + + tx, err := solanago.TransactionFromDecoder(bin.NewBinDecoder(chunkData)) + if err != nil { + chunkResults <- chunkResult{ + chunkIdx: chkIdx, + err: fmt.Errorf("failed to decode chunk %d: %w", chkIdx, err), + duration: time.Since(chunkStart), + } + return + } + + recent, err := s.SolanaChain.RPCClient.GetLatestBlockhash(ctx, rpc.CommitmentFinalized) + if err != nil { + chunkResults <- chunkResult{ + chunkIdx: chkIdx, + err: fmt.Errorf("failed to get blockhash for chunk %d: %w", chkIdx, err), + duration: time.Since(chunkStart), + } + return + } + tx.Message.RecentBlockhash = recent.Value.Blockhash + + sig, err := s.SolanaChain.SignAndBroadcastTx(ctx, tx, user) + chunkDuration := time.Since(chunkStart) + + if err != nil { + chunkResults <- chunkResult{ + chunkIdx: chkIdx, + err: fmt.Errorf("failed to submit chunk %d: %w", chkIdx, err), + duration: chunkDuration, + } + return + } + + chunkResults <- chunkResult{ + chunkIdx: chkIdx, + sig: sig, + duration: chunkDuration, + } + }(chunkIdx, chunkBytes) + } + + // Collect all chunk results for this packet + var chunkErr error + for i := 0; i < len(pkt.Chunks); i++ { + result := <-chunkResults + if result.err != nil { + chunkErr = result.err + s.T().Logf("✗ Packet %d, Chunk %d failed: %v", pktIdx+1, result.chunkIdx+1, result.err) + } else { + s.T().Logf("✓ Packet %d, Chunk %d/%d completed in %v - tx: %s", + pktIdx+1, result.chunkIdx+1, len(pkt.Chunks), result.duration, result.sig) + } + } + close(chunkResults) + chunksDuration := time.Since(chunksStart) + + if chunkErr != nil { + packetResults <- packetResult{ + packetIdx: pktIdx, + err: fmt.Errorf("packet %d chunk upload failed: %w", pktIdx, chunkErr), + chunksDuration: chunksDuration, + totalDuration: time.Since(packetStart), + } + return + } + + s.T().Logf("--- Packet %d: All %d chunks completed in %v, submitting final tx ---", + pktIdx+1, len(pkt.Chunks), chunksDuration) + + // Phase 2: Submit final transaction for this packet + finalStart := time.Now() + + finalTx, err := solanago.TransactionFromDecoder(bin.NewBinDecoder(pkt.FinalTx)) + if err != nil { + packetResults <- packetResult{ + packetIdx: pktIdx, + err: fmt.Errorf("packet %d failed to decode final tx: %w", pktIdx, err), + chunksDuration: chunksDuration, + finalDuration: time.Since(finalStart), + totalDuration: time.Since(packetStart), + } + return + } + + recent, err := s.SolanaChain.RPCClient.GetLatestBlockhash(ctx, rpc.CommitmentFinalized) + if err != nil { + packetResults <- packetResult{ + packetIdx: pktIdx, + err: fmt.Errorf("packet %d failed to get blockhash for final tx: %w", pktIdx, err), + chunksDuration: chunksDuration, + finalDuration: time.Since(finalStart), + totalDuration: time.Since(packetStart), + } + return + } + finalTx.Message.RecentBlockhash = recent.Value.Blockhash + + // For final tx we need finalized status + sig, err := s.SolanaChain.SignAndBroadcastTx(ctx, finalTx, user) + finalDuration := time.Since(finalStart) + totalDuration := time.Since(packetStart) + + if err != nil { + packetResults <- packetResult{ + packetIdx: pktIdx, + err: fmt.Errorf("packet %d failed to submit final tx: %w", pktIdx, err), + chunksDuration: chunksDuration, + finalDuration: finalDuration, + totalDuration: totalDuration, + } + return + } + + s.T().Logf("✓ Packet %d: Final tx completed in %v - tx: %s", pktIdx+1, finalDuration, sig) + s.T().Logf("--- Packet %d: Complete in %v (chunks: %v, final: %v) ---", + pktIdx+1, totalDuration, chunksDuration, finalDuration) + + packetResults <- packetResult{ + packetIdx: pktIdx, + finalSig: sig, + chunksDuration: chunksDuration, + finalDuration: finalDuration, + totalDuration: totalDuration, + } + }(packetIdx, packet) + } + + // Collect all packet results var lastSig solanago.Signature - // Submit all transactions sequentially - // Structure: [packet1_chunk0, packet1_chunk1, ..., packet1_final, packet2_chunk0, ...] - for i, txBytes := range resp.Txs { - txStart := time.Now() - - tx, err := solanago.TransactionFromDecoder(bin.NewBinDecoder(txBytes)) - s.Require().NoError(err, "Failed to decode transaction %d", i) - - recent, err := s.SolanaChain.RPCClient.GetLatestBlockhash(ctx, rpc.CommitmentFinalized) - s.Require().NoError(err, "Failed to get latest blockhash for transaction %d", i) - tx.Message.RecentBlockhash = recent.Value.Blockhash - - // TODO: We can speed up test by waiting for processed on all chunks and then finalized on relay assemble tx - sig, err := s.SolanaChain.SignAndBroadcastTx(ctx, tx, user) - s.Require().NoError(err, "Failed to submit transaction %d", i) - - lastSig = sig - txDuration := time.Since(txStart) - s.T().Logf("✓ Transaction %d/%d completed in %v - tx: %s", - i+1, len(resp.Txs), txDuration, sig) + var totalChunksDuration time.Duration + var totalFinalsDuration time.Duration + + for i := 0; i < len(batch.Packets); i++ { + result := <-packetResults + s.Require().NoError(result.err, "Packet submission failed") + lastSig = result.finalSig + totalChunksDuration += result.chunksDuration + totalFinalsDuration += result.finalDuration } + close(packetResults) totalDuration := time.Since(totalStart) - avgTxTime := totalDuration / time.Duration(len(resp.Txs)) + avgChunksDuration := totalChunksDuration / time.Duration(len(batch.Packets)) + avgFinalsDuration := totalFinalsDuration / time.Duration(len(batch.Packets)) + s.T().Logf("=== Chunked Relay Packets Complete ===") - s.T().Logf("Total time: %v for %d transactions (avg: %v/tx)", - totalDuration, len(resp.Txs), avgTxTime) - s.T().Logf("NOTE: for simplicity all tx chunks are waiting for finalization and are sent sequentially") - s.T().Logf("In real use only final packet tx (recv/ack/timeout) needs to be finalized") + s.T().Logf("Total wall time: %v for %d packets (%d total chunks)", totalDuration, len(batch.Packets), totalChunks) + s.T().Logf("All packets processed in parallel:") + s.T().Logf(" - Avg chunks phase per packet: %v", avgChunksDuration) + s.T().Logf(" - Avg final tx per packet: %v", avgFinalsDuration) + s.T().Logf("Parallelization: All packets + all chunks within each packet submitted concurrently") return lastSig } diff --git a/e2e/interchaintestv8/types/gmp/gmp.pb.go b/e2e/interchaintestv8/types/gmp/gmp.pb.go index d39ec68d0..597d04096 100644 --- a/e2e/interchaintestv8/types/gmp/gmp.pb.go +++ b/e2e/interchaintestv8/types/gmp/gmp.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.7 +// protoc-gen-go v1.36.10 // protoc (unknown) // source: gmp/gmp.proto diff --git a/e2e/interchaintestv8/types/relayer/relayer.pb.go b/e2e/interchaintestv8/types/relayer/relayer.pb.go index 4847d37d7..e1199d68b 100644 --- a/e2e/interchaintestv8/types/relayer/relayer.pb.go +++ b/e2e/interchaintestv8/types/relayer/relayer.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.7 +// protoc-gen-go v1.36.10 // protoc (unknown) // source: relayer/relayer.proto @@ -135,22 +135,169 @@ func (x *RelayByTxRequest) GetDstPacketSequences() []uint64 { return nil } +// Transaction batch wrapper for multiple transactions +type TransactionBatch struct { + state protoimpl.MessageState `protogen:"open.v1"` + // List of transaction bytes + Txs [][]byte `protobuf:"bytes,1,rep,name=txs,proto3" json:"txs,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TransactionBatch) Reset() { + *x = TransactionBatch{} + mi := &file_relayer_relayer_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TransactionBatch) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TransactionBatch) ProtoMessage() {} + +func (x *TransactionBatch) ProtoReflect() protoreflect.Message { + mi := &file_relayer_relayer_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TransactionBatch.ProtoReflect.Descriptor instead. +func (*TransactionBatch) Descriptor() ([]byte, []int) { + return file_relayer_relayer_proto_rawDescGZIP(), []int{1} +} + +func (x *TransactionBatch) GetTxs() [][]byte { + if x != nil { + return x.Txs + } + return nil +} + +// Transactions for a single packet (chunks + final instruction) +type PacketTransactions struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Chunk upload transactions (can be submitted in parallel) + Chunks [][]byte `protobuf:"bytes,1,rep,name=chunks,proto3" json:"chunks,omitempty"` + // Final packet transaction (recv_packet, ack_packet, or timeout_packet) + FinalTx []byte `protobuf:"bytes,2,opt,name=final_tx,json=finalTx,proto3" json:"final_tx,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PacketTransactions) Reset() { + *x = PacketTransactions{} + mi := &file_relayer_relayer_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PacketTransactions) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PacketTransactions) ProtoMessage() {} + +func (x *PacketTransactions) ProtoReflect() protoreflect.Message { + mi := &file_relayer_relayer_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PacketTransactions.ProtoReflect.Descriptor instead. +func (*PacketTransactions) Descriptor() ([]byte, []int) { + return file_relayer_relayer_proto_rawDescGZIP(), []int{2} +} + +func (x *PacketTransactions) GetChunks() [][]byte { + if x != nil { + return x.Chunks + } + return nil +} + +func (x *PacketTransactions) GetFinalTx() []byte { + if x != nil { + return x.FinalTx + } + return nil +} + +// Batch of packet transactions for relay operations +type RelayPacketBatch struct { + state protoimpl.MessageState `protogen:"open.v1"` + // List of packet transactions + Packets []*PacketTransactions `protobuf:"bytes,1,rep,name=packets,proto3" json:"packets,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RelayPacketBatch) Reset() { + *x = RelayPacketBatch{} + mi := &file_relayer_relayer_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RelayPacketBatch) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RelayPacketBatch) ProtoMessage() {} + +func (x *RelayPacketBatch) ProtoReflect() protoreflect.Message { + mi := &file_relayer_relayer_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RelayPacketBatch.ProtoReflect.Descriptor instead. +func (*RelayPacketBatch) Descriptor() ([]byte, []int) { + return file_relayer_relayer_proto_rawDescGZIP(), []int{3} +} + +func (x *RelayPacketBatch) GetPackets() []*PacketTransactions { + if x != nil { + return x.Packets + } + return nil +} + // The relay by tx response message type RelayByTxResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // The multicall transaction to be submitted by caller + // For single transactions: contains the raw transaction bytes + // For multiple transactions (e.g. Solana chunks): contains serialized RelayPacketBatch Tx []byte `protobuf:"bytes,1,opt,name=tx,proto3" json:"tx,omitempty"` // The contract address to submit the transaction, if applicable - Address string `protobuf:"bytes,2,opt,name=address,proto3" json:"address,omitempty"` - // Optional transactions when more than one is required (for instance Solana needs to split packet to multi tx) - Txs [][]byte `protobuf:"bytes,3,rep,name=txs,proto3" json:"txs,omitempty"` + Address string `protobuf:"bytes,2,opt,name=address,proto3" json:"address,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *RelayByTxResponse) Reset() { *x = RelayByTxResponse{} - mi := &file_relayer_relayer_proto_msgTypes[1] + mi := &file_relayer_relayer_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -162,7 +309,7 @@ func (x *RelayByTxResponse) String() string { func (*RelayByTxResponse) ProtoMessage() {} func (x *RelayByTxResponse) ProtoReflect() protoreflect.Message { - mi := &file_relayer_relayer_proto_msgTypes[1] + mi := &file_relayer_relayer_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -175,7 +322,7 @@ func (x *RelayByTxResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayByTxResponse.ProtoReflect.Descriptor instead. func (*RelayByTxResponse) Descriptor() ([]byte, []int) { - return file_relayer_relayer_proto_rawDescGZIP(), []int{1} + return file_relayer_relayer_proto_rawDescGZIP(), []int{4} } func (x *RelayByTxResponse) GetTx() []byte { @@ -192,13 +339,6 @@ func (x *RelayByTxResponse) GetAddress() string { return "" } -func (x *RelayByTxResponse) GetTxs() [][]byte { - if x != nil { - return x.Txs - } - return nil -} - // The create client request message type CreateClientRequest struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -214,7 +354,7 @@ type CreateClientRequest struct { func (x *CreateClientRequest) Reset() { *x = CreateClientRequest{} - mi := &file_relayer_relayer_proto_msgTypes[2] + mi := &file_relayer_relayer_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -226,7 +366,7 @@ func (x *CreateClientRequest) String() string { func (*CreateClientRequest) ProtoMessage() {} func (x *CreateClientRequest) ProtoReflect() protoreflect.Message { - mi := &file_relayer_relayer_proto_msgTypes[2] + mi := &file_relayer_relayer_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -239,7 +379,7 @@ func (x *CreateClientRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateClientRequest.ProtoReflect.Descriptor instead. func (*CreateClientRequest) Descriptor() ([]byte, []int) { - return file_relayer_relayer_proto_rawDescGZIP(), []int{2} + return file_relayer_relayer_proto_rawDescGZIP(), []int{5} } func (x *CreateClientRequest) GetSrcChain() string { @@ -276,7 +416,7 @@ type CreateClientResponse struct { func (x *CreateClientResponse) Reset() { *x = CreateClientResponse{} - mi := &file_relayer_relayer_proto_msgTypes[3] + mi := &file_relayer_relayer_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -288,7 +428,7 @@ func (x *CreateClientResponse) String() string { func (*CreateClientResponse) ProtoMessage() {} func (x *CreateClientResponse) ProtoReflect() protoreflect.Message { - mi := &file_relayer_relayer_proto_msgTypes[3] + mi := &file_relayer_relayer_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -301,7 +441,7 @@ func (x *CreateClientResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateClientResponse.ProtoReflect.Descriptor instead. func (*CreateClientResponse) Descriptor() ([]byte, []int) { - return file_relayer_relayer_proto_rawDescGZIP(), []int{3} + return file_relayer_relayer_proto_rawDescGZIP(), []int{6} } func (x *CreateClientResponse) GetTx() []byte { @@ -333,7 +473,7 @@ type UpdateClientRequest struct { func (x *UpdateClientRequest) Reset() { *x = UpdateClientRequest{} - mi := &file_relayer_relayer_proto_msgTypes[4] + mi := &file_relayer_relayer_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -345,7 +485,7 @@ func (x *UpdateClientRequest) String() string { func (*UpdateClientRequest) ProtoMessage() {} func (x *UpdateClientRequest) ProtoReflect() protoreflect.Message { - mi := &file_relayer_relayer_proto_msgTypes[4] + mi := &file_relayer_relayer_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -358,7 +498,7 @@ func (x *UpdateClientRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateClientRequest.ProtoReflect.Descriptor instead. func (*UpdateClientRequest) Descriptor() ([]byte, []int) { - return file_relayer_relayer_proto_rawDescGZIP(), []int{4} + return file_relayer_relayer_proto_rawDescGZIP(), []int{7} } func (x *UpdateClientRequest) GetSrcChain() string { @@ -385,19 +525,19 @@ func (x *UpdateClientRequest) GetDstClientId() string { // The update client response message type UpdateClientResponse struct { state protoimpl.MessageState `protogen:"open.v1"` - // Single transaction for chains where header could fit in single tx + // The transaction to be submitted by caller + // For single transactions: contains the raw transaction bytes + // For multiple transactions (e.g. Solana chunks): contains serialized TransactionBatch Tx []byte `protobuf:"bytes,1,opt,name=tx,proto3" json:"tx,omitempty"` // The contract address to submit the transaction, if applicable - Address string `protobuf:"bytes,2,opt,name=address,proto3" json:"address,omitempty"` - // Multiple transactions for chunked update (used by Solana) - Txs [][]byte `protobuf:"bytes,3,rep,name=txs,proto3" json:"txs,omitempty"` + Address string `protobuf:"bytes,2,opt,name=address,proto3" json:"address,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *UpdateClientResponse) Reset() { *x = UpdateClientResponse{} - mi := &file_relayer_relayer_proto_msgTypes[5] + mi := &file_relayer_relayer_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -409,7 +549,7 @@ func (x *UpdateClientResponse) String() string { func (*UpdateClientResponse) ProtoMessage() {} func (x *UpdateClientResponse) ProtoReflect() protoreflect.Message { - mi := &file_relayer_relayer_proto_msgTypes[5] + mi := &file_relayer_relayer_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -422,7 +562,7 @@ func (x *UpdateClientResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateClientResponse.ProtoReflect.Descriptor instead. func (*UpdateClientResponse) Descriptor() ([]byte, []int) { - return file_relayer_relayer_proto_rawDescGZIP(), []int{5} + return file_relayer_relayer_proto_rawDescGZIP(), []int{8} } func (x *UpdateClientResponse) GetTx() []byte { @@ -439,13 +579,6 @@ func (x *UpdateClientResponse) GetAddress() string { return "" } -func (x *UpdateClientResponse) GetTxs() [][]byte { - if x != nil { - return x.Txs - } - return nil -} - // Information request message type InfoRequest struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -459,7 +592,7 @@ type InfoRequest struct { func (x *InfoRequest) Reset() { *x = InfoRequest{} - mi := &file_relayer_relayer_proto_msgTypes[6] + mi := &file_relayer_relayer_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -471,7 +604,7 @@ func (x *InfoRequest) String() string { func (*InfoRequest) ProtoMessage() {} func (x *InfoRequest) ProtoReflect() protoreflect.Message { - mi := &file_relayer_relayer_proto_msgTypes[6] + mi := &file_relayer_relayer_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -484,7 +617,7 @@ func (x *InfoRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use InfoRequest.ProtoReflect.Descriptor instead. func (*InfoRequest) Descriptor() ([]byte, []int) { - return file_relayer_relayer_proto_rawDescGZIP(), []int{6} + return file_relayer_relayer_proto_rawDescGZIP(), []int{9} } func (x *InfoRequest) GetSrcChain() string { @@ -516,7 +649,7 @@ type InfoResponse struct { func (x *InfoResponse) Reset() { *x = InfoResponse{} - mi := &file_relayer_relayer_proto_msgTypes[7] + mi := &file_relayer_relayer_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -528,7 +661,7 @@ func (x *InfoResponse) String() string { func (*InfoResponse) ProtoMessage() {} func (x *InfoResponse) ProtoReflect() protoreflect.Message { - mi := &file_relayer_relayer_proto_msgTypes[7] + mi := &file_relayer_relayer_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -541,7 +674,7 @@ func (x *InfoResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use InfoResponse.ProtoReflect.Descriptor instead. func (*InfoResponse) Descriptor() ([]byte, []int) { - return file_relayer_relayer_proto_rawDescGZIP(), []int{7} + return file_relayer_relayer_proto_rawDescGZIP(), []int{10} } func (x *InfoResponse) GetTargetChain() *Chain { @@ -580,7 +713,7 @@ type Chain struct { func (x *Chain) Reset() { *x = Chain{} - mi := &file_relayer_relayer_proto_msgTypes[8] + mi := &file_relayer_relayer_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -592,7 +725,7 @@ func (x *Chain) String() string { func (*Chain) ProtoMessage() {} func (x *Chain) ProtoReflect() protoreflect.Message { - mi := &file_relayer_relayer_proto_msgTypes[8] + mi := &file_relayer_relayer_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -605,7 +738,7 @@ func (x *Chain) ProtoReflect() protoreflect.Message { // Deprecated: Use Chain.ProtoReflect.Descriptor instead. func (*Chain) Descriptor() ([]byte, []int) { - return file_relayer_relayer_proto_rawDescGZIP(), []int{8} + return file_relayer_relayer_proto_rawDescGZIP(), []int{11} } func (x *Chain) GetChainId() string { @@ -642,11 +775,17 @@ const file_relayer_relayer_proto_rawDesc = "" + "\rsrc_client_id\x18\x05 \x01(\tR\vsrcClientId\x12\"\n" + "\rdst_client_id\x18\x06 \x01(\tR\vdstClientId\x120\n" + "\x14src_packet_sequences\x18\a \x03(\x04R\x12srcPacketSequences\x120\n" + - "\x14dst_packet_sequences\x18\b \x03(\x04R\x12dstPacketSequences\"O\n" + + "\x14dst_packet_sequences\x18\b \x03(\x04R\x12dstPacketSequences\"$\n" + + "\x10TransactionBatch\x12\x10\n" + + "\x03txs\x18\x01 \x03(\fR\x03txs\"G\n" + + "\x12PacketTransactions\x12\x16\n" + + "\x06chunks\x18\x01 \x03(\fR\x06chunks\x12\x19\n" + + "\bfinal_tx\x18\x02 \x01(\fR\afinalTx\"I\n" + + "\x10RelayPacketBatch\x125\n" + + "\apackets\x18\x01 \x03(\v2\x1b.relayer.PacketTransactionsR\apackets\"=\n" + "\x11RelayByTxResponse\x12\x0e\n" + "\x02tx\x18\x01 \x01(\fR\x02tx\x12\x18\n" + - "\aaddress\x18\x02 \x01(\tR\aaddress\x12\x10\n" + - "\x03txs\x18\x03 \x03(\fR\x03txs\"\xdc\x01\n" + + "\aaddress\x18\x02 \x01(\tR\aaddress\"\xdc\x01\n" + "\x13CreateClientRequest\x12\x1b\n" + "\tsrc_chain\x18\x01 \x01(\tR\bsrcChain\x12\x1b\n" + "\tdst_chain\x18\x02 \x01(\tR\bdstChain\x12L\n" + @@ -662,11 +801,10 @@ const file_relayer_relayer_proto_rawDesc = "" + "\x13UpdateClientRequest\x12\x1b\n" + "\tsrc_chain\x18\x01 \x01(\tR\bsrcChain\x12\x1b\n" + "\tdst_chain\x18\x02 \x01(\tR\bdstChain\x12\"\n" + - "\rdst_client_id\x18\x03 \x01(\tR\vdstClientId\"R\n" + + "\rdst_client_id\x18\x03 \x01(\tR\vdstClientId\"@\n" + "\x14UpdateClientResponse\x12\x0e\n" + "\x02tx\x18\x01 \x01(\fR\x02tx\x12\x18\n" + - "\aaddress\x18\x02 \x01(\tR\aaddress\x12\x10\n" + - "\x03txs\x18\x03 \x03(\fR\x03txs\"G\n" + + "\aaddress\x18\x02 \x01(\tR\aaddress\"G\n" + "\vInfoRequest\x12\x1b\n" + "\tsrc_chain\x18\x01 \x01(\tR\bsrcChain\x12\x1b\n" + "\tdst_chain\x18\x02 \x01(\tR\bdstChain\"\xf2\x01\n" + @@ -701,38 +839,42 @@ func file_relayer_relayer_proto_rawDescGZIP() []byte { return file_relayer_relayer_proto_rawDescData } -var file_relayer_relayer_proto_msgTypes = make([]protoimpl.MessageInfo, 11) +var file_relayer_relayer_proto_msgTypes = make([]protoimpl.MessageInfo, 14) var file_relayer_relayer_proto_goTypes = []any{ (*RelayByTxRequest)(nil), // 0: relayer.RelayByTxRequest - (*RelayByTxResponse)(nil), // 1: relayer.RelayByTxResponse - (*CreateClientRequest)(nil), // 2: relayer.CreateClientRequest - (*CreateClientResponse)(nil), // 3: relayer.CreateClientResponse - (*UpdateClientRequest)(nil), // 4: relayer.UpdateClientRequest - (*UpdateClientResponse)(nil), // 5: relayer.UpdateClientResponse - (*InfoRequest)(nil), // 6: relayer.InfoRequest - (*InfoResponse)(nil), // 7: relayer.InfoResponse - (*Chain)(nil), // 8: relayer.Chain - nil, // 9: relayer.CreateClientRequest.ParametersEntry - nil, // 10: relayer.InfoResponse.MetadataEntry + (*TransactionBatch)(nil), // 1: relayer.TransactionBatch + (*PacketTransactions)(nil), // 2: relayer.PacketTransactions + (*RelayPacketBatch)(nil), // 3: relayer.RelayPacketBatch + (*RelayByTxResponse)(nil), // 4: relayer.RelayByTxResponse + (*CreateClientRequest)(nil), // 5: relayer.CreateClientRequest + (*CreateClientResponse)(nil), // 6: relayer.CreateClientResponse + (*UpdateClientRequest)(nil), // 7: relayer.UpdateClientRequest + (*UpdateClientResponse)(nil), // 8: relayer.UpdateClientResponse + (*InfoRequest)(nil), // 9: relayer.InfoRequest + (*InfoResponse)(nil), // 10: relayer.InfoResponse + (*Chain)(nil), // 11: relayer.Chain + nil, // 12: relayer.CreateClientRequest.ParametersEntry + nil, // 13: relayer.InfoResponse.MetadataEntry } var file_relayer_relayer_proto_depIdxs = []int32{ - 9, // 0: relayer.CreateClientRequest.parameters:type_name -> relayer.CreateClientRequest.ParametersEntry - 8, // 1: relayer.InfoResponse.target_chain:type_name -> relayer.Chain - 8, // 2: relayer.InfoResponse.source_chain:type_name -> relayer.Chain - 10, // 3: relayer.InfoResponse.metadata:type_name -> relayer.InfoResponse.MetadataEntry - 0, // 4: relayer.RelayerService.RelayByTx:input_type -> relayer.RelayByTxRequest - 2, // 5: relayer.RelayerService.CreateClient:input_type -> relayer.CreateClientRequest - 4, // 6: relayer.RelayerService.UpdateClient:input_type -> relayer.UpdateClientRequest - 6, // 7: relayer.RelayerService.Info:input_type -> relayer.InfoRequest - 1, // 8: relayer.RelayerService.RelayByTx:output_type -> relayer.RelayByTxResponse - 3, // 9: relayer.RelayerService.CreateClient:output_type -> relayer.CreateClientResponse - 5, // 10: relayer.RelayerService.UpdateClient:output_type -> relayer.UpdateClientResponse - 7, // 11: relayer.RelayerService.Info:output_type -> relayer.InfoResponse - 8, // [8:12] is the sub-list for method output_type - 4, // [4:8] is the sub-list for method input_type - 4, // [4:4] is the sub-list for extension type_name - 4, // [4:4] is the sub-list for extension extendee - 0, // [0:4] is the sub-list for field type_name + 2, // 0: relayer.RelayPacketBatch.packets:type_name -> relayer.PacketTransactions + 12, // 1: relayer.CreateClientRequest.parameters:type_name -> relayer.CreateClientRequest.ParametersEntry + 11, // 2: relayer.InfoResponse.target_chain:type_name -> relayer.Chain + 11, // 3: relayer.InfoResponse.source_chain:type_name -> relayer.Chain + 13, // 4: relayer.InfoResponse.metadata:type_name -> relayer.InfoResponse.MetadataEntry + 0, // 5: relayer.RelayerService.RelayByTx:input_type -> relayer.RelayByTxRequest + 5, // 6: relayer.RelayerService.CreateClient:input_type -> relayer.CreateClientRequest + 7, // 7: relayer.RelayerService.UpdateClient:input_type -> relayer.UpdateClientRequest + 9, // 8: relayer.RelayerService.Info:input_type -> relayer.InfoRequest + 4, // 9: relayer.RelayerService.RelayByTx:output_type -> relayer.RelayByTxResponse + 6, // 10: relayer.RelayerService.CreateClient:output_type -> relayer.CreateClientResponse + 8, // 11: relayer.RelayerService.UpdateClient:output_type -> relayer.UpdateClientResponse + 10, // 12: relayer.RelayerService.Info:output_type -> relayer.InfoResponse + 9, // [9:13] is the sub-list for method output_type + 5, // [5:9] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name } func init() { file_relayer_relayer_proto_init() } @@ -746,7 +888,7 @@ func file_relayer_relayer_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_relayer_relayer_proto_rawDesc), len(file_relayer_relayer_proto_rawDesc)), NumEnums: 0, - NumMessages: 11, + NumMessages: 14, NumExtensions: 0, NumServices: 1, }, diff --git a/e2e/interchaintestv8/types/solana/solana_instruction.pb.go b/e2e/interchaintestv8/types/solana/solana_instruction.pb.go index 42b676450..9bb47368d 100644 --- a/e2e/interchaintestv8/types/solana/solana_instruction.pb.go +++ b/e2e/interchaintestv8/types/solana/solana_instruction.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.7 +// protoc-gen-go v1.36.10 // protoc (unknown) // source: solana/solana_instruction.proto diff --git a/packages/relayer/modules/cosmos-to-cosmos/src/lib.rs b/packages/relayer/modules/cosmos-to-cosmos/src/lib.rs index dbbb5ecf8..e34f56b3f 100644 --- a/packages/relayer/modules/cosmos-to-cosmos/src/lib.rs +++ b/packages/relayer/modules/cosmos-to-cosmos/src/lib.rs @@ -161,7 +161,6 @@ impl RelayerService for CosmosToCosmosRelayerModuleService { Ok(Response::new(api::RelayByTxResponse { tx, address: String::new(), - txs: vec![], })) } @@ -206,7 +205,6 @@ impl RelayerService for CosmosToCosmosRelayerModuleService { Ok(Response::new(api::UpdateClientResponse { tx, address: String::new(), - txs: vec![], })) } } diff --git a/packages/relayer/modules/cosmos-to-eth/src/lib.rs b/packages/relayer/modules/cosmos-to-eth/src/lib.rs index 54d64a343..9ff6dc2af 100644 --- a/packages/relayer/modules/cosmos-to-eth/src/lib.rs +++ b/packages/relayer/modules/cosmos-to-eth/src/lib.rs @@ -323,7 +323,6 @@ impl RelayerService for CosmosToEthRelayerModuleService { Ok(Response::new(api::RelayByTxResponse { tx: multicall_tx, address: self.tx_builder.ics26_router.address().to_string(), - txs: vec![], })) } @@ -368,7 +367,6 @@ impl RelayerService for CosmosToEthRelayerModuleService { Ok(Response::new(api::UpdateClientResponse { tx, address: self.tx_builder.ics26_router.address().to_string(), - txs: vec![], })) } } diff --git a/packages/relayer/modules/cosmos-to-solana/src/lib.rs b/packages/relayer/modules/cosmos-to-solana/src/lib.rs index 125fe9726..409b5863b 100644 --- a/packages/relayer/modules/cosmos-to-solana/src/lib.rs +++ b/packages/relayer/modules/cosmos-to-solana/src/lib.rs @@ -177,7 +177,7 @@ impl RelayerService for CosmosToSolanaRelayerModuleService { target_events.len() ); - let txs = self + let packet_txs = self .tx_builder .relay_events_chunked( src_events, @@ -191,14 +191,25 @@ impl RelayerService for CosmosToSolanaRelayerModuleService { .map_err(to_tonic_status)?; tracing::info!( - "Relay by tx request completed with {} transactions.", - txs.len() + "Relay by tx request completed with {} packets.", + packet_txs.len() ); + // Serialize packet transactions into RelayPacketBatch + let packets = packet_txs + .iter() + .map(|pkt| api::PacketTransactions { + chunks: pkt.chunks().to_vec(), + final_tx: pkt.final_tx().to_vec(), + }) + .collect(); + + let batch = api::RelayPacketBatch { packets }; + let tx = prost::Message::encode_to_vec(&batch); + Ok(Response::new(api::RelayByTxResponse { - tx: vec![], + tx, address: String::new(), - txs, })) } @@ -245,10 +256,13 @@ impl RelayerService for CosmosToSolanaRelayerModuleService { } txs.push(chunked.assembly_tx); + // Serialize multiple transactions into TransactionBatch + let batch = api::TransactionBatch { txs }; + let tx = prost::Message::encode_to_vec(&batch); + Ok(Response::new(api::UpdateClientResponse { - tx: vec![], + tx, address: self.solana_ics07_program_id.to_string(), - txs, })) } } diff --git a/packages/relayer/modules/cosmos-to-solana/src/tx_builder.rs b/packages/relayer/modules/cosmos-to-solana/src/tx_builder.rs index ee5147de4..982e8bcd9 100644 --- a/packages/relayer/modules/cosmos-to-solana/src/tx_builder.rs +++ b/packages/relayer/modules/cosmos-to-solana/src/tx_builder.rs @@ -107,6 +107,39 @@ pub struct TimeoutPacketChunkedTxs { pub timeout_tx: Vec, } +/// Enum for different types of chunked packet transactions +#[derive(Debug)] +pub enum PacketChunkedTxs { + /// Recv packet with chunks + Recv(RecvPacketChunkedTxs), + /// Ack packet with chunks + Ack(AckPacketChunkedTxs), + /// Timeout packet with chunks + Timeout(TimeoutPacketChunkedTxs), +} + +impl PacketChunkedTxs { + /// Get the chunk transactions for this packet + #[must_use] + pub fn chunks(&self) -> &[Vec] { + match self { + Self::Recv(r) => &r.chunk_txs, + Self::Ack(a) => &a.chunk_txs, + Self::Timeout(t) => &t.chunk_txs, + } + } + + /// Get the final transaction for this packet + #[must_use] + pub fn final_tx(&self) -> &[u8] { + match self { + Self::Recv(r) => &r.recv_tx, + Self::Ack(a) => &a.ack_tx, + Self::Timeout(t) => &t.timeout_tx, + } + } +} + /// Helper to derive header chunk PDA fn derive_header_chunk( submitter: Pubkey, @@ -1130,7 +1163,7 @@ impl TxBuilder { } /// Build relay transaction from Cosmos events to Solana - /// Returns a vector of transactions to support chunking + /// Returns a vector of packet transactions with chunks preserved /// /// # Errors /// @@ -1148,7 +1181,7 @@ impl TxBuilder { dst_client_id: String, src_packet_seqs: Vec, dst_packet_seqs: Vec, - ) -> Result>> { + ) -> Result> { tracing::info!( "Relaying chunked events from Cosmos to Solana for client {}", dst_client_id @@ -1335,7 +1368,7 @@ impl TxBuilder { } } - let mut all_txs = Vec::new(); + let mut packet_txs = Vec::new(); let chain_id = self.chain_id().await?; // Process recv messages with chunking @@ -1351,9 +1384,7 @@ impl TxBuilder { &recv_with_chunks.proof_chunks, )?; - // Add all chunks first, then the final recv instruction - all_txs.extend(chunked.chunk_txs); - all_txs.push(chunked.recv_tx); + packet_txs.push(PacketChunkedTxs::Recv(chunked)); } // Process ack messages with chunking @@ -1370,9 +1401,7 @@ impl TxBuilder { ) .await?; - // Add all chunks first, then the final ack instruction - all_txs.extend(chunked.chunk_txs); - all_txs.push(chunked.ack_tx); + packet_txs.push(PacketChunkedTxs::Ack(chunked)); } // Process timeout messages with chunking @@ -1385,12 +1414,10 @@ impl TxBuilder { &timeout_with_chunks.proof_chunks, )?; - // Add all chunks first, then the final timeout instruction - all_txs.extend(chunked.chunk_txs); - all_txs.push(chunked.timeout_tx); + packet_txs.push(PacketChunkedTxs::Timeout(chunked)); } - Ok(all_txs) + Ok(packet_txs) } fn create_tx_bytes(&self, instructions: &[Instruction]) -> Result> { diff --git a/packages/relayer/modules/eth-to-cosmos-compat/src/lib.rs b/packages/relayer/modules/eth-to-cosmos-compat/src/lib.rs index 9ea8fa05b..1ad7bc390 100644 --- a/packages/relayer/modules/eth-to-cosmos-compat/src/lib.rs +++ b/packages/relayer/modules/eth-to-cosmos-compat/src/lib.rs @@ -119,7 +119,6 @@ impl RelayerService for EthToCosmosCompatRelayerModuleService { Ok(Response::new(api::RelayByTxResponse { tx: resp.tx, address: resp.address, - txs: vec![], })) } else { self.new_service.relay_by_tx(request).await @@ -199,7 +198,6 @@ impl RelayerService for EthToCosmosCompatRelayerModuleService { Ok(Response::new(api::UpdateClientResponse { tx: resp.tx, address: resp.address, - txs: vec![], })) } else { self.new_service.update_client(request).await diff --git a/packages/relayer/modules/eth-to-cosmos/src/lib.rs b/packages/relayer/modules/eth-to-cosmos/src/lib.rs index b3e0e9f97..cf5f9b294 100644 --- a/packages/relayer/modules/eth-to-cosmos/src/lib.rs +++ b/packages/relayer/modules/eth-to-cosmos/src/lib.rs @@ -187,7 +187,6 @@ impl RelayerService for EthToCosmosRelayerModuleService { Ok(Response::new(api::RelayByTxResponse { tx, address: String::new(), - txs: vec![], })) } @@ -232,7 +231,6 @@ impl RelayerService for EthToCosmosRelayerModuleService { Ok(Response::new(api::UpdateClientResponse { tx, address: String::new(), - txs: vec![], })) } } diff --git a/packages/relayer/modules/solana-to-cosmos/src/lib.rs b/packages/relayer/modules/solana-to-cosmos/src/lib.rs index 2331a7fc3..dab899c88 100644 --- a/packages/relayer/modules/solana-to-cosmos/src/lib.rs +++ b/packages/relayer/modules/solana-to-cosmos/src/lib.rs @@ -186,7 +186,6 @@ impl RelayerService for SolanaToCosmosRelayerModuleService { Ok(Response::new(api::RelayByTxResponse { tx, address: String::new(), - txs: vec![], })) } @@ -229,7 +228,6 @@ impl RelayerService for SolanaToCosmosRelayerModuleService { Ok(Response::new(api::UpdateClientResponse { tx, address: String::new(), - txs: vec![], })) } } diff --git a/proto/relayer/relayer.proto b/proto/relayer/relayer.proto index df8e4619d..c73215dd8 100644 --- a/proto/relayer/relayer.proto +++ b/proto/relayer/relayer.proto @@ -43,14 +43,34 @@ message RelayByTxRequest { repeated uint64 dst_packet_sequences = 8; } +// Transaction batch wrapper for multiple transactions +message TransactionBatch { + // List of transaction bytes + repeated bytes txs = 1; +} + +// Transactions for a single packet (chunks + final instruction) +message PacketTransactions { + // Chunk upload transactions (can be submitted in parallel) + repeated bytes chunks = 1; + // Final packet transaction (recv_packet, ack_packet, or timeout_packet) + bytes final_tx = 2; +} + +// Batch of packet transactions for relay operations +message RelayPacketBatch { + // List of packet transactions + repeated PacketTransactions packets = 1; +} + // The relay by tx response message message RelayByTxResponse { // The multicall transaction to be submitted by caller + // For single transactions: contains the raw transaction bytes + // For multiple transactions (e.g. Solana chunks): contains serialized RelayPacketBatch bytes tx = 1; // The contract address to submit the transaction, if applicable string address = 2; - // Optional transactions when more than one is required (for instance Solana needs to split packet to multi tx) - repeated bytes txs = 3; } // The create client request message @@ -83,12 +103,12 @@ message UpdateClientRequest { // The update client response message message UpdateClientResponse { - // Single transaction for chains where header could fit in single tx + // The transaction to be submitted by caller + // For single transactions: contains the raw transaction bytes + // For multiple transactions (e.g. Solana chunks): contains serialized TransactionBatch bytes tx = 1; // The contract address to submit the transaction, if applicable string address = 2; - // Multiple transactions for chunked update (used by Solana) - repeated bytes txs = 3; } // Information request message