diff --git a/buf.gen.yaml b/buf.gen.yaml new file mode 100644 index 0000000..3e37550 --- /dev/null +++ b/buf.gen.yaml @@ -0,0 +1,15 @@ +version: v2 +inputs: + - git_repo: https://github.com/cosmos/platform.git + subdir: apps/relayer/proto + paths: + - relayerapi/service.proto +plugins: + - remote: buf.build/protocolbuffers/go + out: ift/relayer/pb + opt: + - paths=source_relative + - remote: buf.build/grpc/go + out: ift/relayer/pb + opt: + - paths=source_relative diff --git a/buf.yaml b/buf.yaml new file mode 100644 index 0000000..f74da98 --- /dev/null +++ b/buf.yaml @@ -0,0 +1,9 @@ +version: v2 +modules: + - path: . +lint: + use: + - STANDARD +breaking: + use: + - FILE diff --git a/chains/cosmos/client/client.go b/chains/cosmos/client/client.go index 6d59f1e..5d22ea1 100644 --- a/chains/cosmos/client/client.go +++ b/chains/cosmos/client/client.go @@ -4,6 +4,7 @@ import ( "context" "crypto/tls" "fmt" + "math" "strings" "time" @@ -27,6 +28,7 @@ import ( "google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials/insecure" + cosmosift "github.com/skip-mev/catalyst/chains/cosmos/ift" "github.com/skip-mev/catalyst/chains/cosmos/types" logging "github.com/skip-mev/catalyst/chains/log" ) @@ -188,6 +190,9 @@ func (c *Chain) GetGasLimit(ctx context.Context) (int64, error) { } maxGas := params.ConsensusParams.Block.MaxGas + if maxGas == -1 { + return math.MaxInt64, nil + } if maxGas <= 0 { return 0, fmt.Errorf("invalid max gas value: %d", maxGas) } @@ -281,6 +286,7 @@ func getInterfaceRegistry() codectypes.InterfaceRegistry { std.RegisterInterfaces(registry) authtypes.RegisterInterfaces(registry) banktypes.RegisterInterfaces(registry) + cosmosift.RegisterInterfaces(registry) return registry } diff --git a/chains/cosmos/ift/msg.go b/chains/cosmos/ift/msg.go new file mode 100644 index 0000000..30f9e01 --- /dev/null +++ b/chains/cosmos/ift/msg.go @@ -0,0 +1,141 @@ +package ift + +import ( + "fmt" + "strings" + "sync" + + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + gogoproto "github.com/cosmos/gogoproto/proto" +) + +const DefaultMsgIFTTransferTypeURL = "catalyst.ift.v1.MsgIFTTransfer" + +var ( + typeRegistrationMu sync.Mutex + registeredTypeURLs = map[string]struct{}{} +) + +type MsgIFTTransfer struct { + Signer string `protobuf:"bytes,1,opt,name=signer,proto3" json:"signer,omitempty"` + Denom string `protobuf:"bytes,2,opt,name=denom,proto3" json:"denom,omitempty"` + ClientID string `protobuf:"bytes,3,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` + Receiver string `protobuf:"bytes,4,opt,name=receiver,proto3" json:"receiver,omitempty"` + Amount string `protobuf:"bytes,5,opt,name=amount,proto3" json:"amount,omitempty"` + TimeoutTimestamp uint64 `protobuf:"varint,6,opt,name=timeout_timestamp,json=timeoutTimestamp,proto3" json:"timeout_timestamp,omitempty"` +} + +func (m *MsgIFTTransfer) Reset() { *m = MsgIFTTransfer{} } +func (m *MsgIFTTransfer) String() string { return gogoproto.CompactTextString(m) } +func (*MsgIFTTransfer) ProtoMessage() {} + +// Descriptor satisfies the descriptorIface the cosmos-sdk tx decoder uses +// (codec/unknownproto/unknown_fields.go). Path index 4 corresponds to +// MsgIFTTransfer's position within wfchain/ift/tx.proto. +func (*MsgIFTTransfer) Descriptor() ([]byte, []int) { + return fileDescriptor, []int{4} +} + +// fileDescriptor is the gzipped FileDescriptorProto for wfchain/ift/tx.proto, +// copied verbatim from github.com/cosmos/wfchain/x/ift/types/tx.pb.go. +var fileDescriptor = []byte{ + // 721 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x55, 0xc1, 0x4e, 0xdb, 0x4a, + 0x14, 0x8d, 0x49, 0xc8, 0x83, 0x9b, 0x27, 0x78, 0xf8, 0x05, 0x30, 0x06, 0x25, 0xbc, 0xbc, 0x56, + 0xa5, 0x20, 0x92, 0x42, 0x51, 0xd5, 0xb2, 0xa9, 0x9a, 0x48, 0x48, 0x59, 0x44, 0xad, 0x4c, 0xba, + 0x69, 0x17, 0x91, 0xb1, 0xc7, 0xce, 0xa8, 0xf1, 0x4c, 0x3a, 0x33, 0xa1, 0xb0, 0xab, 0xba, 0xe8, + 0xaa, 0x0b, 0x3e, 0x85, 0x05, 0xea, 0x37, 0xb0, 0x44, 0xac, 0xaa, 0xaa, 0x42, 0x15, 0x2c, 0xf8, + 0x8a, 0x4a, 0x95, 0xed, 0x89, 0x89, 0x43, 0x4a, 0xa4, 0xaa, 0x62, 0x95, 0xdc, 0x7b, 0xce, 0xbd, + 0x73, 0xe6, 0xcc, 0x1d, 0x0f, 0x64, 0xdf, 0x39, 0x56, 0xd3, 0xc4, 0xa4, 0x84, 0x1d, 0x51, 0x12, + 0x7b, 0xc5, 0x36, 0xa3, 0x82, 0xaa, 0x19, 0x99, 0x2d, 0x62, 0x47, 0xe8, 0xb3, 0x16, 0xe5, 0x1e, + 0xe5, 0x25, 0x8f, 0xbb, 0xa5, 0xdd, 0x35, 0xff, 0x27, 0x64, 0xe9, 0x73, 0x21, 0xd0, 0x08, 0xa2, + 0x52, 0x18, 0x48, 0x28, 0xeb, 0x52, 0x97, 0x86, 0x79, 0xff, 0x9f, 0xcc, 0x4e, 0xf7, 0x2e, 0x86, + 0x1d, 0x11, 0xa6, 0x0b, 0x3f, 0x14, 0xc8, 0xd6, 0xb8, 0x6b, 0x20, 0x17, 0x73, 0x81, 0x58, 0x75, + 0xab, 0x5e, 0x66, 0xd8, 0x76, 0x91, 0xfa, 0x00, 0xd2, 0x1c, 0xbb, 0x04, 0x31, 0x4d, 0x59, 0x54, + 0x96, 0xc6, 0xcb, 0xda, 0xe9, 0xd1, 0x6a, 0x56, 0xae, 0xf3, 0xcc, 0xb6, 0x19, 0xe2, 0x7c, 0x5b, + 0x30, 0x4c, 0x5c, 0x43, 0xf2, 0xd4, 0x2c, 0x8c, 0xda, 0x88, 0x50, 0x4f, 0x1b, 0xf1, 0x0b, 0x8c, + 0x30, 0x50, 0xe7, 0x61, 0xdc, 0x6a, 0x61, 0x44, 0x44, 0x03, 0xdb, 0x5a, 0x32, 0x40, 0xc6, 0xc2, + 0x44, 0xd5, 0x56, 0x1f, 0x83, 0x66, 0xd1, 0x0e, 0x11, 0x88, 0xb5, 0x4d, 0x26, 0xf6, 0x1b, 0xd8, + 0x11, 0x0d, 0x33, 0x6c, 0xae, 0xa5, 0x02, 0xee, 0x4c, 0x2f, 0x5e, 0x75, 0x84, 0x5c, 0x5a, 0x7d, + 0x02, 0x73, 0x3e, 0x99, 0x23, 0x62, 0x37, 0x2c, 0xb3, 0xd5, 0x6a, 0x58, 0x94, 0x70, 0xc1, 0x3a, + 0x96, 0xa0, 0x4c, 0x1b, 0x0d, 0x4b, 0xb1, 0x23, 0xb6, 0x11, 0xb1, 0x2b, 0x66, 0xab, 0x55, 0xb9, + 0x42, 0x37, 0x33, 0x1f, 0x2e, 0x0f, 0x97, 0xa5, 0xe8, 0x42, 0x0e, 0x16, 0x06, 0x6d, 0xdf, 0x40, + 0xbc, 0x4d, 0x09, 0x47, 0x85, 0x8f, 0x0a, 0xa8, 0x01, 0xc1, 0xa3, 0xbb, 0xe8, 0x76, 0xdd, 0x89, + 0x0b, 0x5d, 0x00, 0xfd, 0xba, 0x8e, 0x48, 0xe6, 0xc1, 0x08, 0x4c, 0xd4, 0xb8, 0x5b, 0xdd, 0xaa, + 0xd7, 0x99, 0x49, 0xb8, 0x83, 0xd8, 0xed, 0x1c, 0xa0, 0x0e, 0x63, 0x0c, 0x59, 0x08, 0xef, 0x22, + 0x26, 0x0f, 0x2c, 0x8a, 0xd5, 0x0a, 0xa4, 0x4d, 0xcf, 0x3f, 0xbd, 0xf0, 0x3c, 0xca, 0x2b, 0xc7, + 0x67, 0xf9, 0xc4, 0xd7, 0xb3, 0xfc, 0x74, 0x28, 0x82, 0xdb, 0x6f, 0x8a, 0x98, 0x96, 0x3c, 0x53, + 0x34, 0x8b, 0x55, 0x22, 0x4e, 0x8f, 0x56, 0x41, 0xaa, 0xab, 0x12, 0x61, 0xc8, 0x52, 0x75, 0x05, + 0xa6, 0x04, 0xf6, 0x10, 0xed, 0x88, 0x86, 0xff, 0xcb, 0x85, 0xe9, 0xb5, 0xb5, 0xf4, 0xa2, 0xb2, + 0x94, 0x32, 0xfe, 0x91, 0x40, 0xbd, 0x9b, 0x8f, 0x1b, 0xb6, 0x01, 0x33, 0x71, 0x47, 0xba, 0x66, + 0xf9, 0xa2, 0x39, 0x7a, 0xdb, 0x41, 0xc4, 0x42, 0x81, 0x37, 0x29, 0x23, 0x8a, 0x0b, 0xdf, 0x14, + 0x80, 0xb0, 0xac, 0x86, 0x89, 0xf8, 0x63, 0x26, 0x6e, 0xf4, 0xf8, 0x94, 0x1c, 0xd2, 0x69, 0x90, + 0x83, 0xa9, 0xdf, 0x76, 0x30, 0x6e, 0x4a, 0x36, 0x98, 0x66, 0xb9, 0xbb, 0x68, 0x7a, 0x3e, 0x29, + 0x30, 0x59, 0xe3, 0xee, 0xcb, 0xb6, 0x6d, 0x0a, 0xf4, 0xc2, 0x64, 0xa6, 0xc7, 0xd5, 0x47, 0x30, + 0x6e, 0x76, 0x44, 0x93, 0x32, 0x2c, 0xf6, 0x87, 0x6e, 0xfe, 0x8a, 0xaa, 0xae, 0x41, 0xba, 0x1d, + 0x74, 0x08, 0x0c, 0xc8, 0xac, 0xff, 0x5b, 0xec, 0xf9, 0x9e, 0x15, 0xc3, 0xe6, 0xe5, 0x94, 0xbf, + 0x11, 0x43, 0x12, 0x37, 0x27, 0x7c, 0x85, 0x57, 0x2d, 0x0a, 0x73, 0x30, 0xdb, 0xa7, 0xa6, 0xab, + 0x74, 0xfd, 0x73, 0x12, 0x92, 0x35, 0xee, 0xaa, 0x26, 0x4c, 0x5d, 0xff, 0x64, 0xfd, 0x17, 0x5b, + 0x6a, 0xd0, 0xb5, 0xd6, 0xef, 0x0f, 0xa5, 0x44, 0x53, 0xf2, 0x1a, 0x26, 0xfb, 0x6f, 0x7d, 0xfe, + 0x7a, 0x75, 0x8c, 0xa0, 0xdf, 0x1b, 0x42, 0x88, 0x9a, 0x3f, 0x87, 0x4c, 0xef, 0x5d, 0x9d, 0xef, + 0xaf, 0xeb, 0x01, 0xf5, 0xff, 0x6f, 0x00, 0xa3, 0x86, 0x15, 0xf8, 0xab, 0x3b, 0xb3, 0xb3, 0x03, + 0xf8, 0x3e, 0xa0, 0xe7, 0x7f, 0x01, 0x44, 0x4d, 0x0c, 0xf8, 0x3b, 0x36, 0x03, 0x0b, 0xfd, 0x05, + 0xbd, 0xa8, 0x7e, 0xe7, 0x26, 0xb4, 0xdb, 0x53, 0x1f, 0x7d, 0x7f, 0x79, 0xb8, 0xac, 0x94, 0x9f, + 0x1e, 0x9f, 0xe7, 0x94, 0x93, 0xf3, 0x9c, 0xf2, 0xfd, 0x3c, 0xa7, 0x1c, 0x5c, 0xe4, 0x12, 0x27, + 0x17, 0xb9, 0xc4, 0x97, 0x8b, 0x5c, 0xe2, 0xd5, 0x5d, 0x17, 0x8b, 0x66, 0x67, 0xa7, 0x68, 0x51, + 0x4f, 0xbe, 0x63, 0xa5, 0xee, 0x53, 0xb5, 0x17, 0xbe, 0x8c, 0xfb, 0x6d, 0xc4, 0x77, 0xd2, 0xc1, + 0x7b, 0xf5, 0xf0, 0x67, 0x00, 0x00, 0x00, 0xff, 0xff, 0x4e, 0x90, 0x20, 0x0a, 0x35, 0x07, 0x00, + 0x00, +} + +func RegisterTypeURL(typeURL string) { + if typeURL == "" { + typeURL = DefaultMsgIFTTransferTypeURL + } + typeURL = strings.TrimPrefix(typeURL, "/") + + typeRegistrationMu.Lock() + defer typeRegistrationMu.Unlock() + + if _, exists := registeredTypeURLs[typeURL]; exists { + return + } + + gogoproto.RegisterType((*MsgIFTTransfer)(nil), typeURL) + registeredTypeURLs[typeURL] = struct{}{} +} + +func RegisterInterfaces(registry codectypes.InterfaceRegistry) { + registry.RegisterImplementations((*sdk.Msg)(nil), &MsgIFTTransfer{}) +} + +func (m *MsgIFTTransfer) GetSigners() []sdk.AccAddress { + if m.Signer == "" { + return []sdk.AccAddress{} + } + + return []sdk.AccAddress{sdk.MustAccAddressFromBech32(m.Signer)} +} + +func (m *MsgIFTTransfer) ValidateBasic() error { + if m.Signer == "" { + return fmt.Errorf("signer must be specified") + } + if m.Denom == "" { + return fmt.Errorf("denom must be specified") + } + if m.ClientID == "" { + return fmt.Errorf("client_id must be specified") + } + if m.Receiver == "" { + return fmt.Errorf("receiver must be specified") + } + if m.Amount == "" { + return fmt.Errorf("amount must be specified") + } + if m.TimeoutTimestamp == 0 { + return fmt.Errorf("timeout_timestamp must be specified") + } + return nil +} diff --git a/chains/cosmos/ift/msg_test.go b/chains/cosmos/ift/msg_test.go new file mode 100644 index 0000000..f21ff41 --- /dev/null +++ b/chains/cosmos/ift/msg_test.go @@ -0,0 +1,46 @@ +package ift + +import ( + "testing" + + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/stretchr/testify/require" +) + +func TestMsgIFTTransferPacksWithConfiguredTypeURL(t *testing.T) { + const typeURL = "example.ift.v1.MsgIFTTransfer" + + RegisterTypeURL(typeURL) + + msg := &MsgIFTTransfer{ + Signer: "cosmos1deadbeefdeadbeefdeadbeefdeadbeef00", + Denom: "stake", + ClientID: "client-0", + Receiver: "0x1234567890123456789012345678901234567890", + Amount: "100", + TimeoutTimestamp: 123, + } + + anyMsg, err := codectypes.NewAnyWithValue(msg) + require.NoError(t, err) + require.Equal(t, "/"+typeURL, anyMsg.TypeUrl) +} + +func TestMsgIFTTransferPacksWithConfiguredTypeURLLeadingSlash(t *testing.T) { + const typeURL = "/example.ift.v1.MsgIFTTransfer" + + RegisterTypeURL(typeURL) + + msg := &MsgIFTTransfer{ + Signer: "cosmos1deadbeefdeadbeefdeadbeefdeadbeef00", + Denom: "stake", + ClientID: "client-0", + Receiver: "0x1234567890123456789012345678901234567890", + Amount: "100", + TimeoutTimestamp: 123, + } + + anyMsg, err := codectypes.NewAnyWithValue(msg) + require.NoError(t, err) + require.Equal(t, "/example.ift.v1.MsgIFTTransfer", anyMsg.TypeUrl) +} diff --git a/chains/cosmos/metrics/collector.go b/chains/cosmos/metrics/collector.go index 1d659ff..c6967fe 100644 --- a/chains/cosmos/metrics/collector.go +++ b/chains/cosmos/metrics/collector.go @@ -76,12 +76,12 @@ func (m *Collector) GroupSentTxs( continue } - if tx.Err == nil { + if tx.SendTransactionErr == nil { randomClient := &clients[rand.Intn(len(clients))] txResponse, err := wallet.GetTxResponse(ctx, *randomClient, tx.TxHash) if err != nil { m.logger.Error("tx not found", zap.Error(err), zap.String("tx_hash", tx.TxHash)) - tx.Err = err + tx.SendTransactionErr = err mu.Lock() txNotFoundCount++ mu.Unlock() @@ -90,10 +90,6 @@ func (m *Collector) GroupSentTxs( tx.TxResponse = txResponse - if txResponse.Code != 0 { - tx.Err = fmt.Errorf("%s", txResponse.RawLog) - } - mu.Lock() m.txsByBlock[tx.TxResponse.Height] = append(m.txsByBlock[tx.TxResponse.Height], *tx) @@ -110,7 +106,7 @@ func (m *Collector) GroupSentTxs( for i := range sentTxs { tx := &sentTxs[i] - if tx.Err == nil { + if tx.SendTransactionErr == nil { workChan <- workItem{index: i, tx: tx} } } @@ -155,8 +151,8 @@ func (m *Collector) calculateGasStats(gasUsage []int64) loadtesttypes.GasStats { } // processMessageTypeStats processes statistics for each message type and returns overall totals -func (m *Collector) processMessageTypeStats(result *loadtesttypes.LoadTestResult) (int, int, int, int64) { - var totalTxs, successfulTxs, failedTxs int +func (m *Collector) processMessageTypeStats(result *loadtesttypes.LoadTestResult) (int, int, int, int, int64) { + var totalTxs, successfulTxs, failedTxs, relayFailures int var totalGasUsed int64 result.ByMessage = make(map[loadtesttypes.MsgType]loadtesttypes.MessageStats, len(m.txsByMsgType)) @@ -164,15 +160,19 @@ func (m *Collector) processMessageTypeStats(result *loadtesttypes.LoadTestResult for msgType, txs := range m.txsByMsgType { successful := 0 failed := 0 + relayFailed := 0 errorCounts := make(map[string]int) for _, tx := range txs { - if tx.Err != nil { + if tx.Failed() { failed++ - errMsg := tx.Err.Error() + errMsg := tx.Error().Error() errorCounts[errMsg]++ } else { successful++ } + if tx.RelayFailed() { + relayFailed++ + } } stats := loadtesttypes.MessageStats{ @@ -180,6 +180,7 @@ func (m *Collector) processMessageTypeStats(result *loadtesttypes.LoadTestResult TotalIncluded: len(txs), Successful: successful, Failed: failed, + RelayFailures: relayFailed, }, Gas: m.calculateGasStats(m.gasUsageByMsgType[msgType]), } @@ -188,10 +189,11 @@ func (m *Collector) processMessageTypeStats(result *loadtesttypes.LoadTestResult totalTxs += stats.Transactions.TotalIncluded successfulTxs += stats.Transactions.Successful failedTxs += stats.Transactions.Failed + relayFailures += stats.Transactions.RelayFailures totalGasUsed += stats.Gas.Total } - return totalTxs, successfulTxs, failedTxs, totalGasUsed + return totalTxs, successfulTxs, failedTxs, relayFailures, totalGasUsed } // processNodeStats processes statistics for each node @@ -212,15 +214,19 @@ func (m *Collector) processNodeStats(result *loadtesttypes.LoadTestResult) { successful := 0 failed := 0 + relayFailed := 0 for _, tx := range txs { msgCounts[tx.MsgType]++ - if tx.Err != nil { + if tx.Failed() { failed++ } else { successful++ } + if tx.RelayFailed() { + relayFailed++ + } if tx.TxResponse != nil && tx.TxResponse.GasUsed > 0 { gasUsage = append(gasUsage, tx.TxResponse.GasUsed) @@ -229,6 +235,7 @@ func (m *Collector) processNodeStats(result *loadtesttypes.LoadTestResult) { stats.TransactionStats.Successful = successful stats.TransactionStats.Failed = failed + stats.TransactionStats.RelayFailures = relayFailed stats.GasStats = m.calculateGasStats(gasUsage) result.ByNode[nodeAddr] = stats } @@ -269,7 +276,7 @@ func (m *Collector) processBlockStats(result *loadtesttypes.LoadTestResult, gasL stats := msgStats[tx.MsgType] stats.TransactionsSent++ - if tx.Err != nil { + if tx.Failed() { stats.FailedTxs++ if tx.TxResponse != nil && tx.TxResponse.GasUsed > 0 { stats.GasUsed += tx.TxResponse.GasUsed @@ -327,12 +334,13 @@ func (m *Collector) ProcessResults(gasLimit int64, numOfBlocksRequested int) loa ByBlock: make([]loadtesttypes.BlockStat, 0, len(m.txsByBlock)), } - totalTxs, successfulTxs, failedTxs, totalGasUsed := m.processMessageTypeStats(&result) + totalTxs, successfulTxs, failedTxs, relayFailures, totalGasUsed := m.processMessageTypeStats(&result) // Update overall stats result.Overall.TotalTransactions = totalTxs result.Overall.SuccessfulTransactions = successfulTxs result.Overall.FailedTransactions = failedTxs + result.Overall.RelayFailures = relayFailures totalTxsWithGasData := 0 for _, gasUsage := range m.gasUsageByMsgType { totalTxsWithGasData += len(gasUsage) @@ -398,6 +406,7 @@ func (m *Collector) PrintResults(result loadtesttypes.LoadTestResult) { fmt.Printf("Total Transactions: %d\n", result.Overall.TotalTransactions) fmt.Printf("Successful Transactions: %d\n", result.Overall.SuccessfulTransactions) fmt.Printf("Failed Transactions: %d\n", result.Overall.FailedTransactions) + fmt.Printf("Relay Failures: %d\n", result.Overall.RelayFailures) fmt.Printf("Transactions Not Found: %d\n", m.txNotFoundCount) fmt.Printf("Average Gas Per Transaction: %d\n", result.Overall.AvgGasPerTransaction) fmt.Printf("Average Block Gas Utilization: %.2f%%\n", result.Overall.AvgBlockGasUtilization*100) @@ -421,6 +430,7 @@ func (m *Collector) PrintResults(result loadtesttypes.LoadTestResult) { fmt.Printf(" Total: %d\n", stats.Transactions.TotalIncluded) fmt.Printf(" Successful: %d\n", stats.Transactions.Successful) fmt.Printf(" Failed: %d\n", stats.Transactions.Failed) + fmt.Printf(" Relay Failures: %d\n", stats.Transactions.RelayFailures) fmt.Printf(" Gas Usage:\n") fmt.Printf(" Average: %d\n", stats.Gas.Average) fmt.Printf(" Min: %d\n", stats.Gas.Min) @@ -435,6 +445,7 @@ func (m *Collector) PrintResults(result loadtesttypes.LoadTestResult) { fmt.Printf(" Total: %d\n", stats.TransactionStats.TotalIncluded) fmt.Printf(" Successful: %d\n", stats.TransactionStats.Successful) fmt.Printf(" Failed: %d\n", stats.TransactionStats.Failed) + fmt.Printf(" Relay Failures: %d\n", stats.TransactionStats.RelayFailures) fmt.Printf(" Message Distribution:\n") for msgType, count := range stats.MessageCounts { fmt.Printf(" %s: %d\n", msgType, count) diff --git a/chains/cosmos/runner/runner.go b/chains/cosmos/runner/runner.go index ef47d20..30110f4 100644 --- a/chains/cosmos/runner/runner.go +++ b/chains/cosmos/runner/runner.go @@ -18,6 +18,7 @@ import ( "go.uber.org/zap" "github.com/skip-mev/catalyst/chains/cosmos/client" + cosmosift "github.com/skip-mev/catalyst/chains/cosmos/ift" "github.com/skip-mev/catalyst/chains/cosmos/metrics" "github.com/skip-mev/catalyst/chains/cosmos/txfactory" inttypes "github.com/skip-mev/catalyst/chains/cosmos/types" @@ -25,6 +26,8 @@ import ( logging "github.com/skip-mev/catalyst/chains/log" "github.com/skip-mev/catalyst/chains/txdistribution" loadtesttypes "github.com/skip-mev/catalyst/chains/types" + iftaccounts "github.com/skip-mev/catalyst/ift/accounts" + iftrelayer "github.com/skip-mev/catalyst/ift/relayer" ) // MsgGasEstimation stores gas estimation for a specific message type @@ -50,6 +53,7 @@ type Runner struct { sentTxs []inttypes.SentTx sentTxsMu sync.RWMutex txFactory *txfactory.TxFactory + relayer iftrelayer.Client accountNumbers map[string]uint64 accountNumbersMu sync.Mutex walletNonces map[string]uint64 @@ -64,6 +68,10 @@ func NewRunner(ctx context.Context, spec loadtesttypes.LoadTestSpec) (*Runner, e logger := logging.FromContext(ctx) chainCfg := spec.ChainCfg.(*inttypes.ChainConfig) + if spec.IFT != nil && spec.IFT.Cosmos != nil { + cosmosift.RegisterTypeURL(spec.IFT.Cosmos.MsgTypeURL) + } + if err := spec.Validate(); err != nil { return nil, err } @@ -166,6 +174,28 @@ func NewRunner(ctx context.Context, spec loadtesttypes.LoadTestSpec) (*Runner, e } runner.txFactory = txfactory.NewTxFactory(chainCfg.GasDenom, wallets, distribution) + if spec.IFT != nil { + recipients, err := iftaccounts.GenerateRecipients(spec) + if err != nil { + return nil, fmt.Errorf("generate ift recipients: %w", err) + } + runner.txFactory.SetIFTConfig( + recipients, + spec.IFT.ClientID, + spec.IFT.Amount, + spec.IFT.Cosmos.Denom, + spec.IFT.Timeout, + ) + } + + if spec.Relay != nil { + relayerClient, err := iftrelayer.NewGRPCClient(*spec.Relay, spec.ChainID) + if err != nil { + return nil, fmt.Errorf("create relayer client: %w", err) + } + runner.relayer = relayerClient + } + if err := runner.initGasEstimation(ctx); err != nil { return nil, err } @@ -204,21 +234,9 @@ func (r *Runner) calculateMsgGasEstimations( gasEstimations := make(map[loadtesttypes.LoadTestMsg]uint64) for _, msgSpec := range r.spec.Msgs { - var msgs []sdk.Msg - var err error - - if msgSpec.Type == inttypes.MsgArr { - msgs, err = r.txFactory.CreateMsgs(msgSpec, fromWallet) - if err != nil { - return nil, fmt.Errorf("failed to create messages for gas estimation: %w", err) - } - - } else { - msg, err := r.txFactory.CreateMsg(msgSpec, fromWallet) - if err != nil { - return nil, fmt.Errorf("failed to create message for gas estimation: %w", err) - } - msgs = []sdk.Msg{msg} + msgs, err := r.createMessagesForType(msgSpec, fromWallet) + if err != nil { + return nil, fmt.Errorf("failed to create messages for gas estimation: %w", err) } acc, err := client.GetAccount(ctx, fromWallet.FormattedAddress()) @@ -311,6 +329,8 @@ func (r *Runner) Run(ctx context.Context) (loadtesttypes.LoadTestResult, error) subscriptionErr <- err }() + var lastSendTime time.Time + go func() { for { select { @@ -323,10 +343,16 @@ func (r *Runner) Run(ctx context.Context) (loadtesttypes.LoadTestResult, error) r.logger.Debug("processing block", zap.Int64("height", block.Height), zap.Time("timestamp", block.Timestamp), zap.Int64("gas_limit", block.GasLimit)) + if r.spec.SendInterval > 0 && time.Since(lastSendTime) < r.spec.SendInterval { + r.mu.Unlock() + continue + } + _, err := r.sendBlockTransactions(ctx) if err != nil { r.logger.Error("error sending block transactions", zap.Error(err)) } + lastSendTime = time.Now() r.logger.Info("processed block", zap.Int64("height", block.Height)) @@ -490,23 +516,18 @@ func (r *Runner) createMessagesForType( msgSpec loadtesttypes.LoadTestMsg, fromWallet *wallet.InteractingWallet, ) ([]sdk.Msg, error) { - var msgs []sdk.Msg - var err error - if msgSpec.Type == inttypes.MsgArr { if msgSpec.ContainedType == "" { return nil, fmt.Errorf("msgSpec.ContainedType must not be empty") } - - msgs, err = r.txFactory.CreateMsgs(msgSpec, fromWallet) - } else { - msg, err := r.txFactory.CreateMsg(msgSpec, fromWallet) - if err == nil { - msgs = []sdk.Msg{msg} - } + return r.txFactory.CreateMsgs(msgSpec, fromWallet) } - return msgs, err + msg, err := r.txFactory.CreateMsg(msgSpec, fromWallet) + if err != nil { + return nil, err + } + return []sdk.Msg{msg}, nil } // createAndSendTransaction creates and sends a transaction, handling the response @@ -591,9 +612,9 @@ func (r *Runner) broadcastAndHandleResponse( } sentTx := inttypes.SentTx{ - Err: err, - NodeAddress: client.GetNodeAddress().RPC, - MsgType: msgType, + SendTransactionErr: err, + NodeAddress: client.GetNodeAddress().RPC, + MsgType: msgType, } if res != nil { sentTx.TxHash = res.TxHash @@ -609,7 +630,15 @@ func (r *Runner) broadcastAndHandleResponse( TxHash: res.TxHash, NodeAddress: client.GetNodeAddress().RPC, MsgType: msgType, - Err: nil, + } + + if err := r.relayTxHash(ctx, msgType, res.TxHash); err != nil { + sentTx.RelayErr = err + r.logger.Error("failed to relay tx", + zap.Error(err), + zap.String("tx_hash", res.TxHash), + zap.String("node", client.GetNodeAddress().RPC), + zap.String("msg_type", msgType.String())) } updateNonce(walletAddress) @@ -621,6 +650,16 @@ func (r *Runner) broadcastAndHandleResponse( return sentTx, true } +func (r *Runner) relayTxHash(ctx context.Context, msgType loadtesttypes.MsgType, txHash string) error { + if r.relayer == nil { + return nil + } + if !r.spec.Relay.ShouldRelay(msgType) { + return nil + } + return r.relayer.SubmitTxHash(ctx, txHash) +} + // handleNonceMismatch extracts the expected nonce from the error message and updates the wallet nonce func (r *Runner) handleNonceMismatch(walletAddress string, _ uint64, rawLog string) { expectedNonceStr := regexp.MustCompile(`expected (\d+)`).FindStringSubmatch(rawLog) diff --git a/chains/cosmos/txfactory/factory.go b/chains/cosmos/txfactory/factory.go index 53fa04f..c926d27 100644 --- a/chains/cosmos/txfactory/factory.go +++ b/chains/cosmos/txfactory/factory.go @@ -3,11 +3,13 @@ package txfactory import ( "fmt" "math/rand" + "time" sdkmath "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + cosmosift "github.com/skip-mev/catalyst/chains/cosmos/ift" "github.com/skip-mev/catalyst/chains/cosmos/types" "github.com/skip-mev/catalyst/chains/cosmos/wallet" loadtesttypes "github.com/skip-mev/catalyst/chains/types" @@ -27,6 +29,12 @@ type TxFactory struct { gasDenom string wallets []*wallet.InteractingWallet txDistribution TxDistribution + + iftRecipients []string + iftClientID string + iftAmount string + iftDenom string + iftTimeout time.Duration } // NewTxFactory creates a new transaction factory @@ -73,6 +81,8 @@ func (f *TxFactory) CreateMsg( return f.createMsgSend(fromWallet, nil) case types.MsgMultiSend: return f.createMsgMultiSend(fromWallet, msgSpec.NumOfRecipients) + case types.MsgIFTTransfer: + return f.createMsgIFTTransfer(fromWallet) case types.MsgArr: return nil, fmt.Errorf("MsgArr requires using CreateMsgs instead of CreateMsg") default: @@ -192,3 +202,30 @@ func (f *TxFactory) CreateMsgs( return f.createMsgArray(msgSpec, fromWallet) } + +func (f *TxFactory) SetIFTConfig(recipients []string, clientID, amount, denom string, timeout time.Duration) { + f.iftRecipients = recipients + f.iftClientID = clientID + f.iftAmount = amount + f.iftDenom = denom + f.iftTimeout = timeout +} + +func (f *TxFactory) createMsgIFTTransfer(fromWallet *wallet.InteractingWallet) (sdk.Msg, error) { + if len(f.iftRecipients) == 0 { + return nil, fmt.Errorf("no ift recipients configured") + } + + receiver := f.iftRecipients[rand.Intn(len(f.iftRecipients))] + //nolint:gosec // G115: overflow unlikely in practice + timeout := uint64(time.Now().Add(f.iftTimeout).Unix()) + + return &cosmosift.MsgIFTTransfer{ + Signer: fromWallet.FormattedAddress(), + Denom: f.iftDenom, + ClientID: f.iftClientID, + Receiver: receiver, + Amount: f.iftAmount, + TimeoutTimestamp: timeout, + }, nil +} diff --git a/chains/cosmos/types/types.go b/chains/cosmos/types/types.go index b73d573..ece9511 100644 --- a/chains/cosmos/types/types.go +++ b/chains/cosmos/types/types.go @@ -12,17 +12,19 @@ import ( codectypes "github.com/cosmos/cosmos-sdk/codec/types" sdk "github.com/cosmos/cosmos-sdk/types" + cosmosift "github.com/skip-mev/catalyst/chains/cosmos/ift" loadtesttypes "github.com/skip-mev/catalyst/chains/types" ) const ( - MsgSend loadtesttypes.MsgType = "MsgSend" - MsgMultiSend loadtesttypes.MsgType = "MsgMultiSend" - MsgArr loadtesttypes.MsgType = "MsgArr" + MsgSend loadtesttypes.MsgType = "MsgSend" + MsgMultiSend loadtesttypes.MsgType = "MsgMultiSend" + MsgArr loadtesttypes.MsgType = "MsgArr" + MsgIFTTransfer loadtesttypes.MsgType = "MsgIFTTransfer" ) var ( - validMsgTypes = []loadtesttypes.MsgType{MsgSend, MsgMultiSend, MsgArr} + validMsgTypes = []loadtesttypes.MsgType{MsgSend, MsgMultiSend, MsgArr, MsgIFTTransfer} validContainedTypes = []loadtesttypes.MsgType{MsgSend, MsgMultiSend} ) @@ -57,8 +59,8 @@ type NodeAddress struct { RPC string `json:"rpc"` } -// BroadcastError represents errors during broadcasting transactions -type BroadcastError struct { +// SendTransactionError represents errors during broadcasting transactions +type SendTransactionError struct { BlockHeight int64 // Block height where the error occurred (0 indicates tx did not make it to a block) TxHash string // Hash of the transaction that failed Error string // Error message @@ -67,12 +69,30 @@ type BroadcastError struct { } type SentTx struct { - TxHash string - NodeAddress string - MsgType loadtesttypes.MsgType - Err error - TxResponse *sdk.TxResponse - InitialTxResponse *sdk.TxResponse + TxHash string + NodeAddress string + MsgType loadtesttypes.MsgType + SendTransactionErr error + RelayErr error + TxResponse *sdk.TxResponse +} + +func (s SentTx) Failed() bool { + return s.SendTransactionErr != nil || (s.TxResponse != nil && s.TxResponse.Code != 0) +} + +func (s SentTx) Error() error { + if s.SendTransactionErr != nil { + return s.SendTransactionErr + } + if s.TxResponse != nil && s.TxResponse.Code != 0 { + return fmt.Errorf("%s", s.TxResponse.RawLog) + } + return nil +} + +func (s SentTx) RelayFailed() bool { + return s.RelayErr != nil } type ChainConfig struct { @@ -118,6 +138,10 @@ func (s ChainConfig) Validate(mainCfg loadtesttypes.LoadTestSpec) error { if msg.NumOfRecipients > mainCfg.NumWallets { return fmt.Errorf("number of recipients must be less than or equal to number of wallets available") } + case MsgIFTTransfer: + if mainCfg.IFT == nil { + return fmt.Errorf("ift config must be specified when using MsgIFTTransfer") + } default: if seenMsgTypes[msg.Type] { return fmt.Errorf("duplicate message type: %s", msg.Type) @@ -148,6 +172,7 @@ func init() { func Register() { loadtesttypes.Register("cosmos", func() loadtesttypes.ChainConfig { return &ChainConfig{} }) + cosmosift.RegisterTypeURL(cosmosift.DefaultMsgIFTTransferTypeURL) } func validateMsgType(msg loadtesttypes.LoadTestMsg) error { diff --git a/chains/ethereum/contracts/load/Makefile b/chains/ethereum/contracts/load/Makefile index fa45156..ab96d46 100644 --- a/chains/ethereum/contracts/load/Makefile +++ b/chains/ethereum/contracts/load/Makefile @@ -12,4 +12,9 @@ build-target: build-weth: forge build --silent && jq '.abi' ./out/Weth.sol/Weth9.json > weth/weth.abi forge inspect src/Weth.sol:WETH9 bytecode > weth/weth.bin - abigen --abi weth/weth.abi --bin weth/weth.bin --pkg weth --out weth/weth.go \ No newline at end of file + abigen --abi weth/weth.abi --bin weth/weth.bin --pkg weth --out weth/weth.go + +build-ift: + forge build --silent && jq '.abi' ./out/IFT.sol/IFT.json > ift/ift.abi + forge inspect src/IFT.sol:IFT bytecode > ift/ift.bin + abigen --abi ift/ift.abi --bin ift/ift.bin --pkg ift --out ift/ift.go \ No newline at end of file diff --git a/chains/ethereum/contracts/load/ift/ift.abi b/chains/ethereum/contracts/load/ift/ift.abi new file mode 100644 index 0000000..c12d2a4 --- /dev/null +++ b/chains/ethereum/contracts/load/ift/ift.abi @@ -0,0 +1,30 @@ +[ + { + "type": "function", + "name": "iftTransfer", + "inputs": [ + { + "name": "clientId", + "type": "string", + "internalType": "string" + }, + { + "name": "receiver", + "type": "string", + "internalType": "string" + }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "timeoutTimestamp", + "type": "uint64", + "internalType": "uint64" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + } +] diff --git a/chains/ethereum/contracts/load/ift/ift.bin b/chains/ethereum/contracts/load/ift/ift.bin new file mode 100644 index 0000000..761a504 --- /dev/null +++ b/chains/ethereum/contracts/load/ift/ift.bin @@ -0,0 +1 @@ +0x6080604052348015600f57600080fd5b506101658061001f6000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c8063711708b314610030575b600080fd5b61004661003e366004610091565b505050505050565b005b60008083601f84011261005a57600080fd5b50813567ffffffffffffffff81111561007257600080fd5b60208301915083602082850101111561008a57600080fd5b9250929050565b600080600080600080608087890312156100aa57600080fd5b863567ffffffffffffffff8111156100c157600080fd5b6100cd89828a01610048565b909750955050602087013567ffffffffffffffff8111156100ed57600080fd5b6100f989828a01610048565b90955093505060408701359150606087013567ffffffffffffffff8116811461012157600080fd5b80915050929550929550929556fea2646970667358221220d8a942edfee86403f43ae2807e0d0f7e3b122e7521ad6df026ce5d632ca7ccaa64736f6c634300081c0033 diff --git a/chains/ethereum/contracts/load/ift/ift.go b/chains/ethereum/contracts/load/ift/ift.go new file mode 100644 index 0000000..67f29e1 --- /dev/null +++ b/chains/ethereum/contracts/load/ift/ift.go @@ -0,0 +1,224 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package ift + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType +) + +// IftMetaData contains all meta data concerning the Ift contract. +var IftMetaData = &bind.MetaData{ + ABI: "[{\"type\":\"function\",\"name\":\"iftTransfer\",\"inputs\":[{\"name\":\"clientId\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"receiver\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"amount\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"timeoutTimestamp\",\"type\":\"uint64\",\"internalType\":\"uint64\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"}]", + Bin: "0x6080604052348015600f57600080fd5b506101658061001f6000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c8063711708b314610030575b600080fd5b61004661003e366004610091565b505050505050565b005b60008083601f84011261005a57600080fd5b50813567ffffffffffffffff81111561007257600080fd5b60208301915083602082850101111561008a57600080fd5b9250929050565b600080600080600080608087890312156100aa57600080fd5b863567ffffffffffffffff8111156100c157600080fd5b6100cd89828a01610048565b909750955050602087013567ffffffffffffffff8111156100ed57600080fd5b6100f989828a01610048565b90955093505060408701359150606087013567ffffffffffffffff8116811461012157600080fd5b80915050929550929550929556fea2646970667358221220d8a942edfee86403f43ae2807e0d0f7e3b122e7521ad6df026ce5d632ca7ccaa64736f6c634300081c0033", +} + +// IftABI is the input ABI used to generate the binding from. +// Deprecated: Use IftMetaData.ABI instead. +var IftABI = IftMetaData.ABI + +// IftBin is the compiled bytecode used for deploying new contracts. +// Deprecated: Use IftMetaData.Bin instead. +var IftBin = IftMetaData.Bin + +// DeployIft deploys a new Ethereum contract, binding an instance of Ift to it. +func DeployIft(auth *bind.TransactOpts, backend bind.ContractBackend) (common.Address, *types.Transaction, *Ift, error) { + parsed, err := IftMetaData.GetAbi() + if err != nil { + return common.Address{}, nil, nil, err + } + if parsed == nil { + return common.Address{}, nil, nil, errors.New("GetABI returned nil") + } + + address, tx, contract, err := bind.DeployContract(auth, *parsed, common.FromHex(IftBin), backend) + if err != nil { + return common.Address{}, nil, nil, err + } + return address, tx, &Ift{IftCaller: IftCaller{contract: contract}, IftTransactor: IftTransactor{contract: contract}, IftFilterer: IftFilterer{contract: contract}}, nil +} + +// Ift is an auto generated Go binding around an Ethereum contract. +type Ift struct { + IftCaller // Read-only binding to the contract + IftTransactor // Write-only binding to the contract + IftFilterer // Log filterer for contract events +} + +// IftCaller is an auto generated read-only Go binding around an Ethereum contract. +type IftCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// IftTransactor is an auto generated write-only Go binding around an Ethereum contract. +type IftTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// IftFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type IftFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// IftSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type IftSession struct { + Contract *Ift // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// IftCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type IftCallerSession struct { + Contract *IftCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// IftTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type IftTransactorSession struct { + Contract *IftTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// IftRaw is an auto generated low-level Go binding around an Ethereum contract. +type IftRaw struct { + Contract *Ift // Generic contract binding to access the raw methods on +} + +// IftCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type IftCallerRaw struct { + Contract *IftCaller // Generic read-only contract binding to access the raw methods on +} + +// IftTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type IftTransactorRaw struct { + Contract *IftTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewIft creates a new instance of Ift, bound to a specific deployed contract. +func NewIft(address common.Address, backend bind.ContractBackend) (*Ift, error) { + contract, err := bindIft(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &Ift{IftCaller: IftCaller{contract: contract}, IftTransactor: IftTransactor{contract: contract}, IftFilterer: IftFilterer{contract: contract}}, nil +} + +// NewIftCaller creates a new read-only instance of Ift, bound to a specific deployed contract. +func NewIftCaller(address common.Address, caller bind.ContractCaller) (*IftCaller, error) { + contract, err := bindIft(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &IftCaller{contract: contract}, nil +} + +// NewIftTransactor creates a new write-only instance of Ift, bound to a specific deployed contract. +func NewIftTransactor(address common.Address, transactor bind.ContractTransactor) (*IftTransactor, error) { + contract, err := bindIft(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &IftTransactor{contract: contract}, nil +} + +// NewIftFilterer creates a new log filterer instance of Ift, bound to a specific deployed contract. +func NewIftFilterer(address common.Address, filterer bind.ContractFilterer) (*IftFilterer, error) { + contract, err := bindIft(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &IftFilterer{contract: contract}, nil +} + +// bindIft binds a generic wrapper to an already deployed contract. +func bindIft(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := IftMetaData.GetAbi() + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Ift *IftRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Ift.Contract.IftCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Ift *IftRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Ift.Contract.IftTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Ift *IftRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Ift.Contract.IftTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Ift *IftCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Ift.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Ift *IftTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Ift.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Ift *IftTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Ift.Contract.contract.Transact(opts, method, params...) +} + +// IftTransfer is a paid mutator transaction binding the contract method 0x711708b3. +// +// Solidity: function iftTransfer(string clientId, string receiver, uint256 amount, uint64 timeoutTimestamp) returns() +func (_Ift *IftTransactor) IftTransfer(opts *bind.TransactOpts, clientId string, receiver string, amount *big.Int, timeoutTimestamp uint64) (*types.Transaction, error) { + return _Ift.contract.Transact(opts, "iftTransfer", clientId, receiver, amount, timeoutTimestamp) +} + +// IftTransfer is a paid mutator transaction binding the contract method 0x711708b3. +// +// Solidity: function iftTransfer(string clientId, string receiver, uint256 amount, uint64 timeoutTimestamp) returns() +func (_Ift *IftSession) IftTransfer(clientId string, receiver string, amount *big.Int, timeoutTimestamp uint64) (*types.Transaction, error) { + return _Ift.Contract.IftTransfer(&_Ift.TransactOpts, clientId, receiver, amount, timeoutTimestamp) +} + +// IftTransfer is a paid mutator transaction binding the contract method 0x711708b3. +// +// Solidity: function iftTransfer(string clientId, string receiver, uint256 amount, uint64 timeoutTimestamp) returns() +func (_Ift *IftTransactorSession) IftTransfer(clientId string, receiver string, amount *big.Int, timeoutTimestamp uint64) (*types.Transaction, error) { + return _Ift.Contract.IftTransfer(&_Ift.TransactOpts, clientId, receiver, amount, timeoutTimestamp) +} diff --git a/chains/ethereum/contracts/load/src/IFT.sol b/chains/ethereum/contracts/load/src/IFT.sol new file mode 100644 index 0000000..a0d4d88 --- /dev/null +++ b/chains/ethereum/contracts/load/src/IFT.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +// IFT is a minimal stub of the deployed IFT transfer contract. +// Catalyst only needs the ABI of iftTransfer to encode calldata; the contract +// itself is never deployed from this repo. Keep the signature in sync with the +// upstream deployed contract. +contract IFT { + function iftTransfer( + string calldata clientId, + string calldata receiver, + uint256 amount, + uint64 timeoutTimestamp + ) external {} +} diff --git a/chains/ethereum/ift/contract.go b/chains/ethereum/ift/contract.go new file mode 100644 index 0000000..a768057 --- /dev/null +++ b/chains/ethereum/ift/contract.go @@ -0,0 +1,64 @@ +package ift + +import ( + "context" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + gethtypes "github.com/ethereum/go-ethereum/core/types" + + iftbindings "github.com/skip-mev/catalyst/chains/ethereum/contracts/load/ift" + ethwallet "github.com/skip-mev/catalyst/chains/ethereum/wallet" +) + +type TransferContract struct { + address common.Address + abi abi.ABI +} + +func NewTransferContract(address string) (*TransferContract, error) { + if !common.IsHexAddress(address) { + return nil, fmt.Errorf("invalid IFT contract address %q", address) + } + + parsedABI, err := iftbindings.IftMetaData.GetAbi() + if err != nil { + return nil, fmt.Errorf("parse ift transfer abi: %w", err) + } + + return &TransferContract{ + address: common.HexToAddress(address), + abi: *parsedABI, + }, nil +} + +func (c *TransferContract) BuildTransferTx( + ctx context.Context, + fromWallet *ethwallet.InteractingWallet, + clientID string, + receiver string, + amount *big.Int, + timeoutTimestamp uint64, + nonce uint64, + gasFeeCap *big.Int, + gasTipCap *big.Int, + gasLimit uint64, +) (*gethtypes.Transaction, error) { + calldata, err := c.abi.Pack("iftTransfer", clientID, receiver, amount, timeoutTimestamp) + if err != nil { + return nil, fmt.Errorf("pack iftTransfer calldata: %w", err) + } + + return fromWallet.CreateSignedDynamicFeeTx( + ctx, + &c.address, + big.NewInt(0), + gasLimit, + gasFeeCap, + gasTipCap, + calldata, + &nonce, + ) +} diff --git a/chains/ethereum/metrics/collector.go b/chains/ethereum/metrics/collector.go index d9f3ac1..d464f16 100644 --- a/chains/ethereum/metrics/collector.go +++ b/chains/ethereum/metrics/collector.go @@ -32,6 +32,13 @@ func ProcessResults( wg := sync.WaitGroup{} blockStats := make([]loadtesttypes.BlockStat, endBlock-startBlock+1) receipts := make(map[uint64]gethtypes.Receipts) + msgTypeByHash := make(map[common.Hash]loadtesttypes.MsgType, len(sentTxs)) + for _, sentTx := range sentTxs { + if sentTx == nil { + continue + } + msgTypeByHash[sentTx.TxHash] = sentTx.MsgType + } fetchReceiptsConcurrently := config.EnvFromContext(ctx).ConcurrentReceipts @@ -70,7 +77,7 @@ func ProcessResults( if len(blockReceipts) > 0 { receipts[blockReceipts[0].BlockNumber.Uint64()] = blockReceipts } - blockStats[blockNum-startBlock] = buildBlockStats(block, blockReceipts) + blockStats[blockNum-startBlock] = buildBlockStats(block, blockReceipts, msgTypeByHash) logger.Info( "Block collected", @@ -107,12 +114,7 @@ func ProcessResults( avgGasPerTx := 0.0 for _, blockReceipts := range receipts { for _, receipt := range blockReceipts { - var msgType loadtesttypes.MsgType - if receipt.ContractAddress.Cmp(common.Address{}) == 0 { - msgType = types.ContractCall - } else { - msgType = types.ContractCreate - } + msgType := classifyReceiptMsgType(receipt, msgTypeByHash) stat := msgStats[msgType] // update gas values @@ -138,6 +140,8 @@ func ProcessResults( } } + totalRelayFailures := countRelayFailures(sentTxs, msgStats) + // calculate statistics for ALL txs by type. (totals) // here we are using transactions from the blocks to update each msg type's statistics. avgGasUtilization := 0.0 @@ -159,6 +163,7 @@ func ProcessResults( TotalIncludedTransactions: totalIncluded, SuccessfulTransactions: totalSuccess, FailedTransactions: totalFailed, + RelayFailures: totalRelayFailures, AvgBlockGasUtilization: avgGasUtilization, AvgGasPerTransaction: int64(avgGasPerTx), Runtime: runtime, @@ -175,16 +180,14 @@ func ProcessResults( return result, nil } -func buildBlockStats(block *gethtypes.Block, receipts gethtypes.Receipts) loadtesttypes.BlockStat { +func buildBlockStats( + block *gethtypes.Block, + receipts gethtypes.Receipts, + msgTypeByHash map[common.Hash]loadtesttypes.MsgType, +) loadtesttypes.BlockStat { msgStats := make(map[loadtesttypes.MsgType]loadtesttypes.MessageBlockStats) for _, r := range receipts { - // if the receipt didnt have a created contract address, its a contract call receipt. - var txType loadtesttypes.MsgType - if r.ContractAddress.Cmp(common.Address{}) == 0 { - txType = types.ContractCall - } else { - txType = types.ContractCreate - } + txType := classifyReceiptMsgType(r, msgTypeByHash) stat := msgStats[txType] if r.Status == gethtypes.ReceiptStatusSuccessful { stat.SuccessfulTxs++ @@ -205,6 +208,19 @@ func buildBlockStats(block *gethtypes.Block, receipts gethtypes.Receipts) loadte return stats } +func classifyReceiptMsgType( + receipt *gethtypes.Receipt, + msgTypeByHash map[common.Hash]loadtesttypes.MsgType, +) loadtesttypes.MsgType { + if msgType, ok := msgTypeByHash[receipt.TxHash]; ok { + return msgType + } + if receipt.ContractAddress.Cmp(common.Address{}) == 0 { + return types.ContractCall + } + return types.ContractCreate +} + func getReceiptsForBlockTxs( ctx context.Context, block *gethtypes.Block, @@ -298,11 +314,24 @@ func trimBlocks(blocks []loadtesttypes.BlockStat) ([]loadtesttypes.BlockStat, er func calculateTotalSentByType(sentTxs []*types.SentTx) map[loadtesttypes.MsgType]uint64 { totalSentByType := make(map[loadtesttypes.MsgType]uint64) for _, tx := range sentTxs { - if tx.Tx.To() == nil { // no To == contract creation - totalSentByType[types.ContractCreate]++ - } else { // has a To = calling that contract - totalSentByType[types.ContractCall]++ - } + totalSentByType[tx.MsgType]++ } return totalSentByType } + +func countRelayFailures( + sentTxs []*types.SentTx, + msgStats map[loadtesttypes.MsgType]loadtesttypes.MessageStats, +) int { + total := 0 + for _, sentTx := range sentTxs { + if sentTx == nil || !sentTx.RelayFailed() { + continue + } + stat := msgStats[sentTx.MsgType] + stat.Transactions.RelayFailures++ + msgStats[sentTx.MsgType] = stat + total++ + } + return total +} diff --git a/chains/ethereum/metrics/collector_test.go b/chains/ethereum/metrics/collector_test.go index 4d460c1..8853a02 100644 --- a/chains/ethereum/metrics/collector_test.go +++ b/chains/ethereum/metrics/collector_test.go @@ -1,11 +1,15 @@ package metrics import ( + "errors" "testing" "time" + "github.com/ethereum/go-ethereum/common" + gethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/stretchr/testify/require" + ethtypes "github.com/skip-mev/catalyst/chains/ethereum/types" loadtesttypes "github.com/skip-mev/catalyst/chains/types" ) @@ -180,3 +184,64 @@ func TestTrimBlocks(t *testing.T) { }) } } + +func TestCalculateTotalSentByTypeUsesRecordedMessageTypes(t *testing.T) { + txs := []*ethtypes.SentTx{ + {MsgType: ethtypes.MsgIFTTransfer}, + {MsgType: ethtypes.MsgIFTTransfer}, + {MsgType: ethtypes.ContractCall}, + } + + totalSent := calculateTotalSentByType(txs) + + require.Equal(t, uint64(2), totalSent[ethtypes.MsgIFTTransfer]) + require.Equal(t, uint64(1), totalSent[ethtypes.ContractCall]) +} + +func TestCountRelayFailures(t *testing.T) { + sentTxs := []*ethtypes.SentTx{ + {MsgType: ethtypes.MsgIFTTransfer, RelayErr: errors.New("grpc down")}, + {MsgType: ethtypes.MsgIFTTransfer, RelayErr: errors.New("grpc down")}, + {MsgType: ethtypes.MsgIFTTransfer}, // clean — not counted + {MsgType: ethtypes.ContractCall, RelayErr: errors.New("grpc down")}, + nil, // skipped + } + msgStats := map[loadtesttypes.MsgType]loadtesttypes.MessageStats{ + ethtypes.MsgIFTTransfer: {}, + ethtypes.ContractCall: {}, + } + + total := countRelayFailures(sentTxs, msgStats) + + require.Equal(t, 3, total) + require.Equal(t, 2, msgStats[ethtypes.MsgIFTTransfer].Transactions.RelayFailures) + require.Equal(t, 1, msgStats[ethtypes.ContractCall].Transactions.RelayFailures) +} + +func TestCountRelayFailuresIgnoresBroadcastOrReceiptFailures(t *testing.T) { + sentTxs := []*ethtypes.SentTx{ + {MsgType: ethtypes.MsgIFTTransfer, SendTransactionErr: errors.New("rejected")}, + {MsgType: ethtypes.MsgIFTTransfer, Receipt: &gethtypes.Receipt{Status: gethtypes.ReceiptStatusFailed}}, + } + msgStats := map[loadtesttypes.MsgType]loadtesttypes.MessageStats{ + ethtypes.MsgIFTTransfer: {}, + } + + total := countRelayFailures(sentTxs, msgStats) + + require.Equal(t, 0, total) + require.Equal(t, 0, msgStats[ethtypes.MsgIFTTransfer].Transactions.RelayFailures) +} + +func TestClassifyReceiptMsgTypePrefersRecordedSentType(t *testing.T) { + txHash := common.HexToHash("0x1") + receipt := &gethtypes.Receipt{ + TxHash: txHash, + } + + msgType := classifyReceiptMsgType(receipt, map[common.Hash]loadtesttypes.MsgType{ + txHash: ethtypes.MsgIFTTransfer, + }) + + require.Equal(t, ethtypes.MsgIFTTransfer, msgType) +} diff --git a/chains/ethereum/metrics/printer.go b/chains/ethereum/metrics/printer.go index 90ab6d5..d20c47f 100644 --- a/chains/ethereum/metrics/printer.go +++ b/chains/ethereum/metrics/printer.go @@ -18,6 +18,7 @@ func PrintResults(result loadtesttypes.LoadTestResult) { fmt.Printf("Total Included Txs: %d\n", result.Overall.TotalIncludedTransactions) fmt.Printf("Successful Transactions: %d\n", result.Overall.SuccessfulTransactions) fmt.Printf("Failed Transactions: %d\n", result.Overall.FailedTransactions) + fmt.Printf("Relay Failures: %d\n", result.Overall.RelayFailures) fmt.Printf( "Transactions Not Found: %d\n", result.Overall.TotalTransactions-result.Overall.TotalIncludedTransactions, @@ -36,6 +37,7 @@ func PrintResults(result loadtesttypes.LoadTestResult) { fmt.Printf(" Total Included: %d\n", stats.Transactions.TotalIncluded) fmt.Printf(" Execution Successful: %d\n", stats.Transactions.Successful) fmt.Printf(" Execution Failed: %d\n", stats.Transactions.Failed) + fmt.Printf(" Relay Failures: %d\n", stats.Transactions.RelayFailures) fmt.Printf(" Gas Usage:\n") fmt.Printf(" Average: %d\n", stats.Gas.Average) fmt.Printf(" Min: %d\n", stats.Gas.Min) diff --git a/chains/ethereum/runner/block.go b/chains/ethereum/runner/block.go index 890125b..9040f69 100644 --- a/chains/ethereum/runner/block.go +++ b/chains/ethereum/runner/block.go @@ -149,23 +149,30 @@ func (r *Runner) submitLoad(ctx context.Context) (int, error) { defer wg.Done() // send the tx from the wallet assigned to this transaction's sender fromWallet := r.getWalletForTx(tx) - err := fromWallet.SendTransaction(ctx, tx) - if err != nil { - r.logger.Debug("failed to send transaction", zap.String("tx_hash", tx.Hash().String()), zap.Error(err)) + sendTransactionErr := fromWallet.SendTransaction(ctx, tx) + if sendTransactionErr != nil { + r.logger.Debug( + "failed to send transaction", + zap.String("tx_hash", tx.Hash().String()), + zap.Error(sendTransactionErr), + ) } - // TODO: for now its just easier to differ based on contract creation. ethereum txs dont really have - // obvious "msgtypes" inside the tx object itself. we would have to map txhash to the spec that built the tx to get anything more specific. - txType := inttypes.ContractCall - if tx.To() == nil { - txType = inttypes.ContractCreate + msgType := r.messageTypeForTx(tx) + var relayErr error + if sendTransactionErr == nil { + relayErr = r.relayTxHash(ctx, msgType, tx.Hash()) + if relayErr != nil { + r.logger.Debug("failed to relay tx", zap.String("tx_hash", tx.Hash().String()), zap.Error(relayErr)) + } } sentTxs[i] = &inttypes.SentTx{ - TxHash: tx.Hash(), - NodeAddress: "", // TODO: figure out what to do here. - MsgType: txType, - Err: err, - Tx: tx, + TxHash: tx.Hash(), + NodeAddress: "", // TODO: figure out what to do here. + MsgType: msgType, + SendTransactionErr: sendTransactionErr, + RelayErr: relayErr, + Tx: tx, } }() } diff --git a/chains/ethereum/runner/interval.go b/chains/ethereum/runner/interval.go index bb0b0ca..9908c93 100644 --- a/chains/ethereum/runner/interval.go +++ b/chains/ethereum/runner/interval.go @@ -128,13 +128,19 @@ loop: wg.Add(1) go func() { defer wg.Done() - sentTx := inttypes.SentTx{Tx: tx, TxHash: tx.Hash(), MsgType: getTxType(tx)} + sentTx := inttypes.SentTx{Tx: tx, TxHash: tx.Hash(), MsgType: r.messageTypeForTx(tx)} // send the tx from the wallet assigned to this transaction's sender wallet := r.getWalletForTx(tx) - err = wallet.SendTransaction(ctx, tx) - if err != nil { - r.logger.Error("failed to send tx", zap.Error(err), zap.Int("index", i), zap.Int("load_index", loadIndex)) - sentTx.Err = err + sendTransactionErr := wallet.SendTransaction(ctx, tx) + if sendTransactionErr != nil { + r.logger.Error("failed to send tx", zap.Error(sendTransactionErr), zap.Int("index", i), zap.Int("load_index", loadIndex)) + sentTx.SendTransactionErr = sendTransactionErr + } + if sendTransactionErr == nil { + sentTx.RelayErr = r.relayTxHash(ctx, sentTx.MsgType, tx.Hash()) + if sentTx.RelayErr != nil { + r.logger.Error("failed to relay tx", zap.Error(sentTx.RelayErr), zap.Int("index", i), zap.Int("load_index", loadIndex)) + } } collectionChannel <- &sentTx }() diff --git a/chains/ethereum/runner/persistent.go b/chains/ethereum/runner/persistent.go index ee5ae0c..782ecf2 100644 --- a/chains/ethereum/runner/persistent.go +++ b/chains/ethereum/runner/persistent.go @@ -192,18 +192,31 @@ func (r *Runner) sendAndRecord( for i, tx := range txs { wg.Go(func() { fromWallet := r.getWalletForTx(tx) - err := fromWallet.SendTransaction(ctx, tx) - if err != nil { - r.logger.Info("failed to send transaction", zap.String("tx_hash", tx.Hash().String()), zap.Error(err)) + sendTransactionErr := fromWallet.SendTransaction(ctx, tx) + if sendTransactionErr != nil { + r.logger.Info( + "failed to send transaction", + zap.String("tx_hash", tx.Hash().String()), + zap.Error(sendTransactionErr), + ) r.promMetrics.BroadcastFailure.Add(1) } else { r.promMetrics.BroadcastSuccess.Add(1) } + msgType := r.messageTypeForTx(tx) + var relayErr error + if sendTransactionErr == nil { + relayErr = r.relayTxHash(ctx, msgType, tx.Hash()) + if relayErr != nil { + r.logger.Info("failed to relay tx", zap.String("tx_hash", tx.Hash().String()), zap.Error(relayErr)) + } + } sentTxs[i] = &inttypes.SentTx{ - TxHash: tx.Hash(), - MsgType: inttypes.ContractCall, - Err: err, - Tx: tx, + TxHash: tx.Hash(), + MsgType: msgType, + SendTransactionErr: sendTransactionErr, + RelayErr: relayErr, + Tx: tx, } }) } @@ -213,7 +226,7 @@ func (r *Runner) sendAndRecord( // should be pretty close anyways. broadcastTime := time.Now() for _, tx := range sentTxs { - if tx.Err != nil { + if tx.SendTransactionErr != nil { continue } tracker.Set(tx.TxHash, broadcastTime) @@ -226,7 +239,13 @@ func (r *Runner) sendAsync(ctx context.Context, txs gethtypes.Transactions) { for _, tx := range txs { fromWallet := r.getWalletForTx(tx) go func() { - _ = fromWallet.SendTransaction(ctx, tx) + if err := fromWallet.SendTransaction(ctx, tx); err != nil { + return + } + msgType := r.messageTypeForTx(tx) + if err := r.relayTxHash(ctx, msgType, tx.Hash()); err != nil { + r.logger.Debug("failed to relay tx", zap.String("tx_hash", tx.Hash().String()), zap.Error(err)) + } }() } } @@ -243,26 +262,14 @@ func (r *Runner) buildLoadPersistent( for range maxLoadSize { wg.Go(func() { sender := r.txFactory.GetNextSender() - if sender == nil { return } - nonce, ok := r.nonces.Load(sender.Address()) - if !ok { - // this really should not happen ever. better safe than sorry. - r.logger.Error("nonce for wallet not found", zap.String("wallet", sender.Address().String())) - return - } - tx, err := r.txFactory.BuildTxs(msgSpec, sender, nonce.(uint64), useBaseline) + tx, err := r.buildTxsForWallet(msgSpec, sender, useBaseline) if err != nil { r.logger.Error("failed to build txs", zap.Error(err)) return } - lastTx := tx[len(tx)-1] - if lastTx == nil { - return - } - r.nonces.Store(sender.Address(), lastTx.Nonce()+1) // Only use single txn builders here for _, txn := range tx { txChan <- txn diff --git a/chains/ethereum/runner/runner.go b/chains/ethereum/runner/runner.go index 66f6f33..2d14cd0 100644 --- a/chains/ethereum/runner/runner.go +++ b/chains/ethereum/runner/runner.go @@ -3,6 +3,7 @@ package runner import ( "context" "fmt" + "math/big" "math/rand" "net" "net/http" @@ -18,12 +19,15 @@ import ( "go.uber.org/zap" "golang.org/x/sync/errgroup" + ethift "github.com/skip-mev/catalyst/chains/ethereum/ift" "github.com/skip-mev/catalyst/chains/ethereum/metrics" "github.com/skip-mev/catalyst/chains/ethereum/txfactory" inttypes "github.com/skip-mev/catalyst/chains/ethereum/types" "github.com/skip-mev/catalyst/chains/ethereum/wallet" "github.com/skip-mev/catalyst/chains/txdistribution" loadtesttypes "github.com/skip-mev/catalyst/chains/types" + iftaccounts "github.com/skip-mev/catalyst/ift/accounts" + iftrelayer "github.com/skip-mev/catalyst/ift/relayer" ) type Runner struct { @@ -38,9 +42,11 @@ type Runner struct { wallets []*wallet.InteractingWallet txFactory *txfactory.TxFactory + relayer iftrelayer.Client sentTxs []*inttypes.SentTx blocksProcessed uint64 + txTypes sync.Map // senderToWallet maps sender addresses to their assigned wallet senderToWallet map[common.Address]*wallet.InteractingWallet @@ -150,7 +156,7 @@ func NewRunner(ctx context.Context, logger *zap.Logger, spec loadtesttypes.LoadT logger.Info("Runner construction complete", zap.Stringer("duration", time.Since(start))) - return &Runner{ + runner := &Runner{ logger: logger, clients: clients, wsClients: wsClients, @@ -163,7 +169,23 @@ func NewRunner(ctx context.Context, logger *zap.Logger, spec loadtesttypes.LoadT nonces: &nonces, senderToWallet: senderToWallet, promMetrics: metrics.NewMetrics(), - }, nil + } + + if spec.IFT != nil { + if err := initIFT(runner, spec); err != nil { + return nil, err + } + } + + if spec.Relay != nil { + relayerClient, err := iftrelayer.NewGRPCClient(*spec.Relay, spec.ChainID) + if err != nil { + return nil, fmt.Errorf("create relayer client: %w", err) + } + runner.relayer = relayerClient + } + + return runner, nil } // getWalletForTx returns the appropriate wallet for sending a transaction based on the sender address. @@ -329,12 +351,20 @@ func (r *Runner) buildLoad(msgSpec loadtesttypes.LoadTestMsg, useBaseline bool) // For non-ERC20 transactions, keep random selection fromWallet = r.wallets[rand.Intn(len(r.wallets))] } + return r.buildTxsForWallet(msgSpec, fromWallet, useBaseline) +} +func (r *Runner) buildTxsForWallet( + msgSpec loadtesttypes.LoadTestMsg, + fromWallet *wallet.InteractingWallet, + useBaseline bool, +) ([]*gethtypes.Transaction, error) { nonce, ok := r.nonces.Load(fromWallet.Address()) if !ok { // this really should not happen ever. better safe than sorry. return nil, fmt.Errorf("nonce for wallet %s not found", fromWallet.Address()) } + txs, err := r.txFactory.BuildTxs(msgSpec, fromWallet, nonce.(uint64), useBaseline) if err != nil { return nil, fmt.Errorf("failed to build tx for %q: %w", msgSpec.Type, err) @@ -351,5 +381,46 @@ func (r *Runner) buildLoad(msgSpec loadtesttypes.LoadTestMsg, useBaseline bool) return nil, nil } r.nonces.Store(fromWallet.Address(), lastTx.Nonce()+1) + + for _, tx := range txs { + r.txTypes.Store(tx.Hash(), msgSpec.Type) + } return txs, nil } + +func (r *Runner) messageTypeForTx(tx *gethtypes.Transaction) loadtesttypes.MsgType { + if msgType, ok := r.txTypes.Load(tx.Hash()); ok { + return msgType.(loadtesttypes.MsgType) + } + return getTxType(tx) +} + +func (r *Runner) relayTxHash(ctx context.Context, msgType loadtesttypes.MsgType, txHash common.Hash) error { + if r.relayer == nil { + return nil + } + if !r.spec.Relay.ShouldRelay(msgType) { + return nil + } + return r.relayer.SubmitTxHash(ctx, txHash.Hex()) +} + +func initIFT(runner *Runner, spec loadtesttypes.LoadTestSpec) error { + recipients, err := iftaccounts.GenerateRecipients(spec) + if err != nil { + return fmt.Errorf("generate ift recipients: %w", err) + } + + contract, err := ethift.NewTransferContract(spec.IFT.EVM.ContractAddress) + if err != nil { + return fmt.Errorf("create ift transfer contract: %w", err) + } + + amount, ok := new(big.Int).SetString(spec.IFT.Amount, 10) + if !ok { + return fmt.Errorf("parse ift.amount %q", spec.IFT.Amount) + } + + runner.txFactory.SetIFTConfig(contract, recipients, spec.IFT.ClientID, amount, spec.IFT.Timeout) + return nil +} diff --git a/chains/ethereum/txfactory/factory.go b/chains/ethereum/txfactory/factory.go index 2fa33dc..4bfe439 100644 --- a/chains/ethereum/txfactory/factory.go +++ b/chains/ethereum/txfactory/factory.go @@ -7,6 +7,7 @@ import ( "math/big" "math/rand" "sync" + "time" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" @@ -16,6 +17,7 @@ import ( loader "github.com/skip-mev/catalyst/chains/ethereum/contracts/load" "github.com/skip-mev/catalyst/chains/ethereum/contracts/load/target" "github.com/skip-mev/catalyst/chains/ethereum/contracts/load/weth" + ethift "github.com/skip-mev/catalyst/chains/ethereum/ift" ethtypes "github.com/skip-mev/catalyst/chains/ethereum/types" ethwallet "github.com/skip-mev/catalyst/chains/ethereum/wallet" loadtesttypes "github.com/skip-mev/catalyst/chains/types" @@ -45,6 +47,12 @@ type TxFactory struct { baseLines map[loadtesttypes.MsgType][]*types.Transaction txDistribution TxDistribution + + iftContract *ethift.TransferContract + iftRecipients []string + iftClientID string + iftAmount *big.Int + iftTimeout time.Duration } func NewTxFactory(logger *zap.Logger, txOpts ethtypes.TxOpts, txDistribution TxDistribution) *TxFactory { @@ -153,11 +161,31 @@ func (f *TxFactory) BuildTxs( return nil, err } return []*types.Transaction{tx}, nil + case ethtypes.MsgIFTTransfer: + tx, err := f.createMsgIFTTransfer(ctx, fromWallet, nonce, useBaseline) + if err != nil { + return nil, err + } + return []*types.Transaction{tx}, nil default: return nil, fmt.Errorf("unsupported message type: %q", msgSpec.Type) } } +func (f *TxFactory) SetIFTConfig( + contract *ethift.TransferContract, + recipients []string, + clientID string, + amount *big.Int, + timeout time.Duration, +) { + f.iftContract = contract + f.iftRecipients = recipients + f.iftClientID = clientID + f.iftAmount = amount + f.iftTimeout = timeout +} + func (f *TxFactory) SetLoaderAddresses(addrs ...common.Address) { f.loaderAddresses = append(f.loaderAddresses, addrs...) } @@ -552,3 +580,49 @@ func (f *TxFactory) createMsgNativeGasTransfer(ctx context.Context, fromWallet * return signedTx, nil } + +func (f *TxFactory) createMsgIFTTransfer( + ctx context.Context, + fromWallet *ethwallet.InteractingWallet, + nonce uint64, + useBaseline bool, +) (*types.Transaction, error) { + if f.iftContract == nil { + return nil, fmt.Errorf("ift contract not configured") + } + if len(f.iftRecipients) == 0 { + return nil, fmt.Errorf("no ift recipients configured") + } + + receiver := f.iftRecipients[rand.Intn(len(f.iftRecipients))] + //nolint:gosec // G115: overflow unlikely in practice + timeout := uint64(time.Now().Add(f.iftTimeout).Unix()) + + gasFeeCap := f.txOpts.GasFeeCap + gasTipCap := f.txOpts.GasTipCap + var gasLimit uint64 + if useBaseline { + if baseline, ok := f.baseLines[ethtypes.MsgIFTTransfer]; ok && len(baseline) > 0 { + gasLimit = baseline[0].Gas() + if gasFeeCap == nil { + gasFeeCap = baseline[0].GasFeeCap() + } + if gasTipCap == nil { + gasTipCap = baseline[0].GasTipCap() + } + } + } + + return f.iftContract.BuildTransferTx( + ctx, + fromWallet, + f.iftClientID, + receiver, + new(big.Int).Set(f.iftAmount), + timeout, + nonce, + gasFeeCap, + gasTipCap, + gasLimit, + ) +} diff --git a/chains/ethereum/types/types.go b/chains/ethereum/types/types.go index 7c0840b..2b46216 100644 --- a/chains/ethereum/types/types.go +++ b/chains/ethereum/types/types.go @@ -37,6 +37,7 @@ const ( MsgNativeTransferERC20 loadtesttypes.MsgType = "MsgNativeTransferERC20" MsgNativeGasTransfer loadtesttypes.MsgType = "MsgNativeGasTransfer" + MsgIFTTransfer loadtesttypes.MsgType = "MsgIFTTransfer" ) var ( @@ -47,6 +48,7 @@ var ( MsgCallDataBlast, MsgDeployERC20, MsgTransferERC0, + MsgIFTTransfer, } // LoaderDependencies are the msg types that require the presence of the Loader contract. @@ -56,12 +58,32 @@ var ( ) type SentTx struct { - TxHash common.Hash - NodeAddress string - MsgType loadtesttypes.MsgType - Err error - Tx *gethtypes.Transaction - Receipt *gethtypes.Receipt + TxHash common.Hash + NodeAddress string + MsgType loadtesttypes.MsgType + SendTransactionErr error + RelayErr error + Tx *gethtypes.Transaction + Receipt *gethtypes.Receipt +} + +func (s SentTx) Failed() bool { + return s.SendTransactionErr != nil || + (s.Receipt != nil && s.Receipt.Status != gethtypes.ReceiptStatusSuccessful) +} + +func (s SentTx) Error() error { + if s.SendTransactionErr != nil { + return s.SendTransactionErr + } + if s.Receipt != nil && s.Receipt.Status != gethtypes.ReceiptStatusSuccessful { + return fmt.Errorf("tx execution failed: status=%d", s.Receipt.Status) + } + return nil +} + +func (s SentTx) RelayFailed() bool { + return s.RelayErr != nil } type NodeAddress struct { diff --git a/chains/ethereum/wallet/wallet.go b/chains/ethereum/wallet/wallet.go index 3a029e5..039e68d 100644 --- a/chains/ethereum/wallet/wallet.go +++ b/chains/ethereum/wallet/wallet.go @@ -275,24 +275,19 @@ func (w *InteractingWallet) CreateSignedDynamicFeeTx(ctx context.Context, to *co return nil, err } - // Get suggested gas prices if not provided - if gasFeeCap == nil || gasTipCap == nil { + if gasTipCap == nil { + tip, err := w.client.SuggestGasTipCap(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get suggested tip cap: %w", err) + } + gasTipCap = tip + } + if gasFeeCap == nil { header, err := w.client.HeaderByNumber(ctx, nil) if err != nil { return nil, fmt.Errorf("failed to get latest header: %w", err) } - - if gasFeeCap == nil { - // gasFeeCap = baseFee * 2 + gasTipCap (reasonable default) - baseFee := header.BaseFee - if gasTipCap == nil { - gasTipCap = big.NewInt(2000000000) // 2 gwei default tip - } - gasFeeCap = new(big.Int).Add(new(big.Int).Mul(baseFee, big.NewInt(2)), gasTipCap) - } - if gasTipCap == nil { - gasTipCap = big.NewInt(2000000000) // 2 gwei default tip - } + gasFeeCap = new(big.Int).Add(new(big.Int).Mul(header.BaseFee, big.NewInt(2)), gasTipCap) } // Estimate gas if not provided diff --git a/chains/types/ift.go b/chains/types/ift.go new file mode 100644 index 0000000..d05bc1b --- /dev/null +++ b/chains/types/ift.go @@ -0,0 +1,164 @@ +package types + +import ( + "fmt" + "time" +) + +const ( + ChainTypeCosmos = "cosmos" + ChainTypeEVM = "evm" + ChainTypeETH = "eth" +) + +type IFTConfig struct { + ClientID string `yaml:"client_id" json:"client_id"` + Amount string `yaml:"amount" json:"amount"` + Timeout time.Duration `yaml:"timeout" json:"timeout"` + Recipients IFTRecipientsConfig `yaml:"recipients,omitempty" json:"recipients,omitempty"` + Destination IFTDestinationConfig `yaml:"destination" json:"destination"` + Cosmos *IFTCosmosConfig `yaml:"cosmos,omitempty" json:"cosmos,omitempty"` + EVM *IFTEVMConfig `yaml:"evm,omitempty" json:"evm,omitempty"` +} + +type IFTRecipientsConfig struct { + Count int `yaml:"count,omitempty" json:"count,omitempty"` + Offset int `yaml:"offset,omitempty" json:"offset,omitempty"` +} + +type IFTCosmosConfig struct { + Denom string `yaml:"denom" json:"denom"` + MsgTypeURL string `yaml:"msg_type_url" json:"msg_type_url"` +} + +type IFTEVMConfig struct { + ContractAddress string `yaml:"contract_address" json:"contract_address"` +} + +type IFTDestinationConfig struct { + Kind string `yaml:"kind" json:"kind"` + Cosmos *IFTDestinationCosmosConfig `yaml:"cosmos,omitempty" json:"cosmos,omitempty"` + EVM *IFTDestinationEVMConfig `yaml:"evm,omitempty" json:"evm,omitempty"` +} + +type IFTDestinationCosmosConfig struct { + Bech32Prefix string `yaml:"bech32_prefix" json:"bech32_prefix"` +} + +type IFTDestinationEVMConfig struct{} + +func (c *IFTConfig) Validate(spec LoadTestSpec) error { + if c == nil { + return nil + } + + if c.Recipients.Count < 0 { + return fmt.Errorf("ift.recipients.count must be greater than or equal to zero") + } + + if c.Recipients.Offset < 0 { + return fmt.Errorf("ift.recipients.offset must be greater than or equal to zero") + } + + if c.ClientID == "" { + return fmt.Errorf("ift.client_id must be specified") + } + + if c.Amount == "" { + return fmt.Errorf("ift.amount must be specified") + } + + if c.Timeout <= 0 { + return fmt.Errorf("ift.timeout must be greater than zero") + } + + if err := c.Destination.Validate(); err != nil { + return err + } + + switch spec.Kind { + case ChainTypeCosmos: + if err := c.validateCosmos(); err != nil { + return err + } + if c.Destination.Kind != ChainTypeEVM && c.Destination.Kind != ChainTypeCosmos { + return fmt.Errorf( + "ift.destination.kind %q is incompatible with source kind %q", + c.Destination.Kind, + spec.Kind, + ) + } + case ChainTypeETH: + if err := c.validateEVM(); err != nil { + return err + } + if c.Destination.Kind != ChainTypeCosmos && c.Destination.Kind != ChainTypeEVM { + return fmt.Errorf( + "ift.destination.kind %q is incompatible with source kind %q", + c.Destination.Kind, + spec.Kind, + ) + } + default: + return fmt.Errorf("unsupported source kind %q for ift mode", spec.Kind) + } + + return nil +} + +func (c *IFTConfig) validateCosmos() error { + if c.Cosmos == nil { + return fmt.Errorf("ift.cosmos must be specified for cosmos runners") + } + if c.Cosmos.Denom == "" { + return fmt.Errorf("ift.cosmos.denom must be specified") + } + if c.Cosmos.MsgTypeURL == "" { + return fmt.Errorf("ift.cosmos.msg_type_url must be specified") + } + return nil +} + +func (c *IFTConfig) validateEVM() error { + if c.EVM == nil { + return fmt.Errorf("ift.evm must be specified for eth runners") + } + if c.EVM.ContractAddress == "" { + return fmt.Errorf("ift.evm.contract_address must be specified") + } + return nil +} + +func (c IFTDestinationConfig) Validate() error { + switch c.Kind { + case ChainTypeEVM: + if c.EVM == nil { + return fmt.Errorf("ift.destination.evm must be specified for evm destinations") + } + return nil + case ChainTypeCosmos: + if c.Cosmos == nil { + return fmt.Errorf("ift.destination.cosmos must be specified for cosmos destinations") + } + if c.Cosmos.Bech32Prefix == "" { + return fmt.Errorf("ift.destination.cosmos.bech32_prefix must be specified") + } + return nil + default: + return fmt.Errorf("invalid ift.destination.kind %q", c.Kind) + } +} + +func (c *IFTConfig) RecipientCount(defaultCount int) int { + if c == nil || c.Recipients.Count == 0 { + return defaultCount + } + return c.Recipients.Count +} + +func (c *IFTConfig) RecipientOffset(defaultOffset int) int { + if c == nil || c.Recipients.Offset == 0 { + return defaultOffset + } + return c.Recipients.Offset +} diff --git a/chains/types/ift_test.go b/chains/types/ift_test.go new file mode 100644 index 0000000..a65596d --- /dev/null +++ b/chains/types/ift_test.go @@ -0,0 +1,101 @@ +package types_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + + cosmostypes "github.com/skip-mev/catalyst/chains/cosmos/types" + loadtesttypes "github.com/skip-mev/catalyst/chains/types" +) + +func TestIFTConfigValidate_CosmosToEVM(t *testing.T) { + spec := loadtesttypes.LoadTestSpec{ + Kind: "cosmos", + ChainID: "chain-a", + BaseMnemonic: "test test test test test test test test test test test junk", + NumWallets: 1, + Msgs: []loadtesttypes.LoadTestMsg{ + {Type: cosmostypes.MsgIFTTransfer, NumMsgs: 1}, + }, + IFT: &loadtesttypes.IFTConfig{ + ClientID: "client-0", + Amount: "1", + Timeout: time.Second, + Cosmos: &loadtesttypes.IFTCosmosConfig{ + Denom: "stake", + MsgTypeURL: "/skip.ift.MsgIFTTransfer", + }, + Destination: loadtesttypes.IFTDestinationConfig{ + Kind: "evm", + EVM: &loadtesttypes.IFTDestinationEVMConfig{}, + }, + }, + } + + require.NoError(t, spec.IFT.Validate(spec)) +} + +func TestIFTConfigValidate_EthToEVMRejected(t *testing.T) { + spec := loadtesttypes.LoadTestSpec{ + Kind: "eth", + IFT: &loadtesttypes.IFTConfig{ + ClientID: "client-0", + Amount: "1", + Timeout: time.Second, + EVM: &loadtesttypes.IFTEVMConfig{ + ContractAddress: "0x1234", + }, + Destination: loadtesttypes.IFTDestinationConfig{ + Kind: "evm", + EVM: &loadtesttypes.IFTDestinationEVMConfig{}, + }, + }, + } + + require.NoError(t, spec.IFT.Validate(spec)) +} + +func TestIFTConfigValidate_EthToCosmos(t *testing.T) { + spec := loadtesttypes.LoadTestSpec{ + Kind: "eth", + IFT: &loadtesttypes.IFTConfig{ + ClientID: "client-0", + Amount: "1", + Timeout: time.Second, + EVM: &loadtesttypes.IFTEVMConfig{ + ContractAddress: "0x1234", + }, + Destination: loadtesttypes.IFTDestinationConfig{ + Kind: "cosmos", + Cosmos: &loadtesttypes.IFTDestinationCosmosConfig{ + Bech32Prefix: "cosmos", + }, + }, + }, + } + + require.NoError(t, spec.IFT.Validate(spec)) +} + +func TestIFTConfigValidate_EthRequiresEVMConfig(t *testing.T) { + spec := loadtesttypes.LoadTestSpec{ + Kind: "eth", + IFT: &loadtesttypes.IFTConfig{ + ClientID: "client-0", + Amount: "1", + Timeout: time.Second, + Destination: loadtesttypes.IFTDestinationConfig{ + Kind: "cosmos", + Cosmos: &loadtesttypes.IFTDestinationCosmosConfig{ + Bech32Prefix: "cosmos", + }, + }, + }, + } + + err := spec.IFT.Validate(spec) + require.Error(t, err) + require.Contains(t, err.Error(), "ift.evm must be specified") +} diff --git a/chains/types/results.go b/chains/types/results.go index a49c23f..353ab7e 100644 --- a/chains/types/results.go +++ b/chains/types/results.go @@ -23,6 +23,7 @@ type OverallStats struct { SuccessfulTransactions int // FailedTransactions are all txs that were included in a block, but failed execution. FailedTransactions int + RelayFailures int AvgGasPerTransaction int64 AvgBlockGasUtilization float64 Runtime time.Duration @@ -45,6 +46,7 @@ type TransactionStats struct { TotalIncluded int Successful int Failed int + RelayFailures int } // GasStats represents gas-related statistics diff --git a/chains/types/spec.go b/chains/types/spec.go index 34776d0..04c4258 100644 --- a/chains/types/spec.go +++ b/chains/types/spec.go @@ -2,11 +2,25 @@ package types import ( "fmt" + "slices" "time" "gopkg.in/yaml.v3" ) +type RelayConfig struct { + URL string `yaml:"url" json:"url"` + Timeout time.Duration `yaml:"timeout,omitempty" json:"timeout,omitempty"` + MsgTypes []MsgType `yaml:"msg_types" json:"msg_types"` +} + +func (c *RelayConfig) ShouldRelay(msgType MsgType) bool { + if c == nil { + return false + } + return slices.Contains(c.MsgTypes, msgType) +} + type LoadTestSpec struct { Name string `yaml:"name" json:"name"` Description string `yaml:"description" json:"description"` @@ -22,6 +36,8 @@ type LoadTestSpec struct { Msgs []LoadTestMsg `yaml:"msgs" json:"msgs"` TxTimeout time.Duration `yaml:"tx_timeout,omitempty" json:"tx_timeout,omitempty"` ChainCfg ChainConfig `yaml:"-" json:"-"` // decoded via custom UnmarshalYAML + IFT *IFTConfig `yaml:"ift,omitempty" json:"ift,omitempty"` + Relay *RelayConfig `yaml:"relay,omitempty" json:"relay,omitempty"` Cache CacheConfig `yaml:"cache_config" json:"cache_config"` PrometheusListenAddr string `yaml:"prometheus_listen_addr" json:"prometheus_listen_addr"` MetricsEnabled bool `yaml:"metrics_enabled" json:"metrics_enabled"` @@ -100,6 +116,10 @@ func (s *LoadTestSpec) Validate() error { return fmt.Errorf("InitialWallets %d cannot be higher than NumWallets %d", s.InitialWallets, s.NumWallets) } + if err := s.IFT.Validate(*s); err != nil { + return fmt.Errorf("validating ift config: %w", err) + } + if err := s.ChainCfg.Validate(*s); err != nil { return fmt.Errorf("validating chain config: %w", err) } diff --git a/example/loadtest_ift_cosmos_to_cosmos.yml b/example/loadtest_ift_cosmos_to_cosmos.yml new file mode 100644 index 0000000..b807f5a --- /dev/null +++ b/example/loadtest_ift_cosmos_to_cosmos.yml @@ -0,0 +1,53 @@ +# Example IFT load test configuration for Cosmos -> Cosmos. +name: "cosmos-to-cosmos-ift" +description: "Submit Cosmos MsgIFTTransfer txs to Cosmos recipients and push resulting tx hashes to the relayer" +kind: "cosmos" +chain_id: "localcosmos-1" +num_of_txs: 10 +num_of_blocks: 10 +send_interval: "1s" +num_batches: 1 +base_mnemonic: "rotate stumble once topic possible message powder recall turkey legend depart brick" +num_wallets: 100 +initial_wallets: 10 +tx_timeout: "30s" +metrics_enabled: false +prometheus_listen_addr: ":9090" + +chain_config: + gas_denom: "stake" + bech32_prefix: "cosmos" + nodes_addresses: + - grpc: "127.0.0.1:9090" + rpc: "http://127.0.0.1:26657" + +msgs: + - type: "MsgIFTTransfer" + weight: 1.0 + +relay: + url: "127.0.0.1:8080" + timeout: "10s" + msg_types: + - "MsgIFTTransfer" + +ift: + client_id: "07-tendermint-0" + amount: "1000" + timeout: "5m" + recipients: + count: 100 + offset: 0 + destination: + kind: "cosmos" + cosmos: + bech32_prefix: "cosmos" + cosmos: + denom: "stake" + msg_type_url: "/skip.ift.v1.MsgIFTTransfer" + +cache_config: + read_wallets_from: "" + read_txs_from: "" + write_wallets_to: "" + write_txs_to: "" diff --git a/example/loadtest_ift_cosmos_to_evm.yml b/example/loadtest_ift_cosmos_to_evm.yml new file mode 100644 index 0000000..ef45c49 --- /dev/null +++ b/example/loadtest_ift_cosmos_to_evm.yml @@ -0,0 +1,52 @@ +# Example IFT load test configuration for Cosmos -> EVM. +name: "cosmos-to-evm-ift" +description: "Submit Cosmos MsgIFTTransfer txs and push resulting tx hashes to the relayer" +kind: "cosmos" +chain_id: "localcosmos-1" +num_of_txs: 10 +num_of_blocks: 10 +send_interval: "1s" +num_batches: 1 +base_mnemonic: "rotate stumble once topic possible message powder recall turkey legend depart brick" +num_wallets: 100 +initial_wallets: 10 +tx_timeout: "30s" +metrics_enabled: false +prometheus_listen_addr: ":9090" + +chain_config: + gas_denom: "stake" + bech32_prefix: "cosmos" + nodes_addresses: + - grpc: "127.0.0.1:9090" + rpc: "http://127.0.0.1:26657" + +msgs: + - type: "MsgIFTTransfer" + weight: 1.0 + +relay: + url: "127.0.0.1:8080" + timeout: "10s" + msg_types: + - "MsgIFTTransfer" + +ift: + client_id: "07-tendermint-0" + amount: "1000" + timeout: "5m" + recipients: + count: 100 + offset: 0 + destination: + kind: "evm" + evm: {} + cosmos: + denom: "stake" + msg_type_url: "/skip.ift.v1.MsgIFTTransfer" + +cache_config: + read_wallets_from: "" + read_txs_from: "" + write_wallets_to: "" + write_txs_to: "" diff --git a/ift/accounts/cosmos.go b/ift/accounts/cosmos.go new file mode 100644 index 0000000..f57f80c --- /dev/null +++ b/ift/accounts/cosmos.go @@ -0,0 +1,49 @@ +package accounts + +import ( + "fmt" + "strconv" + "strings" + + "github.com/cosmos/cosmos-sdk/crypto/hd" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +const cosmosDerivationPath = "44'/118'/0'/0/0" + +type cosmosGenerator struct { + mnemonic string + bech32Prefix string +} + +func newCosmosGenerator(mnemonic, bech32Prefix string) Generator { + return &cosmosGenerator{ + mnemonic: strings.TrimSpace(mnemonic), + bech32Prefix: bech32Prefix, + } +} + +func (g *cosmosGenerator) GenerateRecipients(count, offset int) ([]string, error) { + recipients := make([]string, 0, count) + for i := range count { + addr, err := generateCosmosAddress(g.mnemonic, g.bech32Prefix, offset+i) + if err != nil { + return nil, err + } + + recipients = append(recipients, addr) + } + + return recipients, nil +} + +func generateCosmosAddress(mnemonic, bech32Prefix string, index int) (string, error) { + derivedPrivKey, err := hd.Secp256k1.Derive()(mnemonic, strconv.Itoa(index), cosmosDerivationPath) + if err != nil { + return "", fmt.Errorf("derive cosmos recipient key: %w", err) + } + + privKey := &secp256k1.PrivKey{Key: derivedPrivKey} + return sdk.MustBech32ifyAddressBytes(bech32Prefix, sdk.AccAddress(privKey.PubKey().Address())), nil +} diff --git a/ift/accounts/evm.go b/ift/accounts/evm.go new file mode 100644 index 0000000..3121a73 --- /dev/null +++ b/ift/accounts/evm.go @@ -0,0 +1,54 @@ +package accounts + +import ( + "fmt" + "strconv" + "strings" + + ethhd "github.com/cosmos/evm/crypto/hd" + "github.com/ethereum/go-ethereum/crypto" +) + +const evmDerivationPath = "m/44'/60'/0'/0/0" + +type evmGenerator struct { + mnemonic string +} + +func newEVMGenerator(mnemonic string) Generator { + return &evmGenerator{mnemonic: strings.TrimSpace(mnemonic)} +} + +func (g *evmGenerator) GenerateRecipients(count, offset int) ([]string, error) { + recipients := make([]string, 0, count) + for i := range count { + addr, err := generateEVMAddress(g.mnemonic, offset+i) + if err != nil { + return nil, err + } + + recipients = append(recipients, addr) + } + + return recipients, nil +} + +func generateEVMAddress(mnemonic string, index int) (string, error) { + passphrase := strconv.Itoa(index) + // matches the EVM wallet derivation convention in chains/ethereum/wallet/wallet.go. + if index == 0 { + passphrase = "" + } + + derivedPrivKey, err := ethhd.EthSecp256k1.Derive()(mnemonic, passphrase, evmDerivationPath) + if err != nil { + return "", fmt.Errorf("derive evm recipient key: %w", err) + } + + pk, err := crypto.ToECDSA(derivedPrivKey) + if err != nil { + return "", fmt.Errorf("parse evm recipient key: %w", err) + } + + return crypto.PubkeyToAddress(pk.PublicKey).Hex(), nil +} diff --git a/ift/accounts/generator.go b/ift/accounts/generator.go new file mode 100644 index 0000000..43dd4f3 --- /dev/null +++ b/ift/accounts/generator.go @@ -0,0 +1,46 @@ +package accounts + +import ( + "fmt" + + loadtesttypes "github.com/skip-mev/catalyst/chains/types" +) + +type Generator interface { + GenerateRecipients(count, offset int) ([]string, error) +} + +func NewGenerator(spec loadtesttypes.LoadTestSpec) (Generator, error) { + if spec.IFT == nil { + return nil, fmt.Errorf("ift config is required") + } + + switch spec.IFT.Destination.Kind { + case "evm": + return newEVMGenerator(spec.BaseMnemonic), nil + case "cosmos": + return newCosmosGenerator(spec.BaseMnemonic, spec.IFT.Destination.Cosmos.Bech32Prefix), nil + default: + return nil, fmt.Errorf("unsupported destination kind %q", spec.IFT.Destination.Kind) + } +} + +func GenerateRecipients(spec loadtesttypes.LoadTestSpec) ([]string, error) { + if spec.IFT == nil { + return nil, nil + } + + count := spec.IFT.RecipientCount(spec.NumWallets) + if count <= 0 { + return nil, fmt.Errorf("ift recipient count must be greater than zero") + } + + offset := spec.IFT.RecipientOffset(spec.NumWallets) + + generator, err := NewGenerator(spec) + if err != nil { + return nil, err + } + + return generator.GenerateRecipients(count, offset) +} diff --git a/ift/accounts/generator_test.go b/ift/accounts/generator_test.go new file mode 100644 index 0000000..d520407 --- /dev/null +++ b/ift/accounts/generator_test.go @@ -0,0 +1,53 @@ +package accounts + +import ( + "testing" + + loadtesttypes "github.com/skip-mev/catalyst/chains/types" +) + +func TestGenerateRecipientsForEVM(t *testing.T) { + spec := loadtesttypes.LoadTestSpec{ + BaseMnemonic: "test test test test test test test test test test test junk", + NumWallets: 2, + IFT: &loadtesttypes.IFTConfig{ + Destination: loadtesttypes.IFTDestinationConfig{ + Kind: "evm", + EVM: &loadtesttypes.IFTDestinationEVMConfig{}, + }, + }, + } + + recipients, err := GenerateRecipients(spec) + if err != nil { + t.Fatalf("GenerateRecipients returned error: %v", err) + } + + if got, want := len(recipients), 2; got != want { + t.Fatalf("len(recipients) = %d, want %d", got, want) + } +} + +func TestGenerateRecipientsForCosmos(t *testing.T) { + spec := loadtesttypes.LoadTestSpec{ + BaseMnemonic: "test test test test test test test test test test test junk", + NumWallets: 2, + IFT: &loadtesttypes.IFTConfig{ + Destination: loadtesttypes.IFTDestinationConfig{ + Kind: "cosmos", + Cosmos: &loadtesttypes.IFTDestinationCosmosConfig{ + Bech32Prefix: "cosmos", + }, + }, + }, + } + + recipients, err := GenerateRecipients(spec) + if err != nil { + t.Fatalf("GenerateRecipients returned error: %v", err) + } + + if got, want := len(recipients), 2; got != want { + t.Fatalf("len(recipients) = %d, want %d", got, want) + } +} diff --git a/ift/relayer/client.go b/ift/relayer/client.go new file mode 100644 index 0000000..d5c37dd --- /dev/null +++ b/ift/relayer/client.go @@ -0,0 +1,88 @@ +package relayer + +import ( + "context" + "fmt" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + loadtesttypes "github.com/skip-mev/catalyst/chains/types" + relayerapi "github.com/skip-mev/catalyst/ift/relayer/pb/relayerapi" +) + +const ( + maxRelayRetries = 15 + relayRetryDelay = 3 * time.Second +) + +type Client interface { + SubmitTxHash(ctx context.Context, txHash string) error +} + +type GRPCClient struct { + conn *grpc.ClientConn + client relayerapi.RelayerApiServiceClient + chainID string + timeout time.Duration +} + +func NewGRPCClient(cfg loadtesttypes.RelayConfig, chainID string) (*GRPCClient, error) { + timeout := cfg.Timeout + if timeout == 0 { + timeout = 10 * time.Second + } + + conn, err := grpc.NewClient( + cfg.URL, + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + if err != nil { + return nil, fmt.Errorf("create relayer grpc client: %w", err) + } + + return &GRPCClient{ + conn: conn, + client: relayerapi.NewRelayerApiServiceClient(conn), + chainID: chainID, + timeout: timeout, + }, nil +} + +func (c *GRPCClient) SubmitTxHash(ctx context.Context, txHash string) error { + var lastErr error + for attempt := range maxRelayRetries { + if attempt > 0 { + timer := time.NewTimer(relayRetryDelay) + select { + case <-ctx.Done(): + timer.Stop() + return ctx.Err() + case <-timer.C: + } + } + + callCtx, cancel := context.WithTimeout(ctx, c.timeout) + _, err := c.client.Relay(callCtx, &relayerapi.RelayRequest{ + TxHash: txHash, + ChainId: c.chainID, + }) + cancel() + + if err == nil { + return nil + } + lastErr = err + } + + return fmt.Errorf("submit tx hash to relayer after %d attempts: %w", maxRelayRetries, lastErr) +} + +func (c *GRPCClient) Close() error { + if c.conn == nil { + return nil + } + + return c.conn.Close() +} diff --git a/ift/relayer/pb/relayerapi/service.pb.go b/ift/relayer/pb/relayerapi/service.pb.go new file mode 100644 index 0000000..c410f38 --- /dev/null +++ b/ift/relayer/pb/relayerapi/service.pb.go @@ -0,0 +1,504 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: relayerapi/service.proto + +package relayerapi + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + _ "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type TransferState int32 + +const ( + TransferState_TRANSFER_STATE_UNKNOWN TransferState = 0 + TransferState_TRANSFER_STATE_PENDING TransferState = 1 + TransferState_TRANSFER_STATE_COMPLETE TransferState = 2 + TransferState_TRANSFER_STATE_FAILED TransferState = 3 +) + +// Enum value maps for TransferState. +var ( + TransferState_name = map[int32]string{ + 0: "TRANSFER_STATE_UNKNOWN", + 1: "TRANSFER_STATE_PENDING", + 2: "TRANSFER_STATE_COMPLETE", + 3: "TRANSFER_STATE_FAILED", + } + TransferState_value = map[string]int32{ + "TRANSFER_STATE_UNKNOWN": 0, + "TRANSFER_STATE_PENDING": 1, + "TRANSFER_STATE_COMPLETE": 2, + "TRANSFER_STATE_FAILED": 3, + } +) + +func (x TransferState) Enum() *TransferState { + p := new(TransferState) + *p = x + return p +} + +func (x TransferState) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (TransferState) Descriptor() protoreflect.EnumDescriptor { + return file_relayerapi_service_proto_enumTypes[0].Descriptor() +} + +func (TransferState) Type() protoreflect.EnumType { + return &file_relayerapi_service_proto_enumTypes[0] +} + +func (x TransferState) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use TransferState.Descriptor instead. +func (TransferState) EnumDescriptor() ([]byte, []int) { + return file_relayerapi_service_proto_rawDescGZIP(), []int{0} +} + +type StatusRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + TxHash string `protobuf:"bytes,1,opt,name=tx_hash,json=txHash,proto3" json:"tx_hash,omitempty"` + ChainId string `protobuf:"bytes,2,opt,name=chain_id,json=chainId,proto3" json:"chain_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StatusRequest) Reset() { + *x = StatusRequest{} + mi := &file_relayerapi_service_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StatusRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StatusRequest) ProtoMessage() {} + +func (x *StatusRequest) ProtoReflect() protoreflect.Message { + mi := &file_relayerapi_service_proto_msgTypes[0] + 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 StatusRequest.ProtoReflect.Descriptor instead. +func (*StatusRequest) Descriptor() ([]byte, []int) { + return file_relayerapi_service_proto_rawDescGZIP(), []int{0} +} + +func (x *StatusRequest) GetTxHash() string { + if x != nil { + return x.TxHash + } + return "" +} + +func (x *StatusRequest) GetChainId() string { + if x != nil { + return x.ChainId + } + return "" +} + +type TransactionInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + TxHash string `protobuf:"bytes,1,opt,name=tx_hash,json=txHash,proto3" json:"tx_hash,omitempty"` + ChainId string `protobuf:"bytes,2,opt,name=chain_id,json=chainId,proto3" json:"chain_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TransactionInfo) Reset() { + *x = TransactionInfo{} + mi := &file_relayerapi_service_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TransactionInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TransactionInfo) ProtoMessage() {} + +func (x *TransactionInfo) ProtoReflect() protoreflect.Message { + mi := &file_relayerapi_service_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 TransactionInfo.ProtoReflect.Descriptor instead. +func (*TransactionInfo) Descriptor() ([]byte, []int) { + return file_relayerapi_service_proto_rawDescGZIP(), []int{1} +} + +func (x *TransactionInfo) GetTxHash() string { + if x != nil { + return x.TxHash + } + return "" +} + +func (x *TransactionInfo) GetChainId() string { + if x != nil { + return x.ChainId + } + return "" +} + +type PacketStatus struct { + state protoimpl.MessageState `protogen:"open.v1"` + State TransferState `protobuf:"varint,1,opt,name=state,proto3,enum=skip.relayer.TransferState" json:"state,omitempty"` + SequenceNumber uint64 `protobuf:"varint,2,opt,name=sequence_number,json=sequenceNumber,proto3" json:"sequence_number,omitempty"` + SourceClientId string `protobuf:"bytes,3,opt,name=source_client_id,json=sourceClientId,proto3" json:"source_client_id,omitempty"` + SendTx *TransactionInfo `protobuf:"bytes,4,opt,name=send_tx,json=sendTx,proto3" json:"send_tx,omitempty"` + RecvTx *TransactionInfo `protobuf:"bytes,5,opt,name=recv_tx,json=recvTx,proto3" json:"recv_tx,omitempty"` + AckTx *TransactionInfo `protobuf:"bytes,6,opt,name=ack_tx,json=ackTx,proto3" json:"ack_tx,omitempty"` + TimeoutTx *TransactionInfo `protobuf:"bytes,7,opt,name=timeout_tx,json=timeoutTx,proto3" json:"timeout_tx,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PacketStatus) Reset() { + *x = PacketStatus{} + mi := &file_relayerapi_service_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PacketStatus) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PacketStatus) ProtoMessage() {} + +func (x *PacketStatus) ProtoReflect() protoreflect.Message { + mi := &file_relayerapi_service_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 PacketStatus.ProtoReflect.Descriptor instead. +func (*PacketStatus) Descriptor() ([]byte, []int) { + return file_relayerapi_service_proto_rawDescGZIP(), []int{2} +} + +func (x *PacketStatus) GetState() TransferState { + if x != nil { + return x.State + } + return TransferState_TRANSFER_STATE_UNKNOWN +} + +func (x *PacketStatus) GetSequenceNumber() uint64 { + if x != nil { + return x.SequenceNumber + } + return 0 +} + +func (x *PacketStatus) GetSourceClientId() string { + if x != nil { + return x.SourceClientId + } + return "" +} + +func (x *PacketStatus) GetSendTx() *TransactionInfo { + if x != nil { + return x.SendTx + } + return nil +} + +func (x *PacketStatus) GetRecvTx() *TransactionInfo { + if x != nil { + return x.RecvTx + } + return nil +} + +func (x *PacketStatus) GetAckTx() *TransactionInfo { + if x != nil { + return x.AckTx + } + return nil +} + +func (x *PacketStatus) GetTimeoutTx() *TransactionInfo { + if x != nil { + return x.TimeoutTx + } + return nil +} + +type StatusResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + PacketStatuses []*PacketStatus `protobuf:"bytes,1,rep,name=packet_statuses,json=packetStatuses,proto3" json:"packet_statuses,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StatusResponse) Reset() { + *x = StatusResponse{} + mi := &file_relayerapi_service_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StatusResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StatusResponse) ProtoMessage() {} + +func (x *StatusResponse) ProtoReflect() protoreflect.Message { + mi := &file_relayerapi_service_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 StatusResponse.ProtoReflect.Descriptor instead. +func (*StatusResponse) Descriptor() ([]byte, []int) { + return file_relayerapi_service_proto_rawDescGZIP(), []int{3} +} + +func (x *StatusResponse) GetPacketStatuses() []*PacketStatus { + if x != nil { + return x.PacketStatuses + } + return nil +} + +type RelayRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + TxHash string `protobuf:"bytes,1,opt,name=tx_hash,json=txHash,proto3" json:"tx_hash,omitempty"` + ChainId string `protobuf:"bytes,2,opt,name=chain_id,json=chainId,proto3" json:"chain_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RelayRequest) Reset() { + *x = RelayRequest{} + mi := &file_relayerapi_service_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RelayRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RelayRequest) ProtoMessage() {} + +func (x *RelayRequest) ProtoReflect() protoreflect.Message { + mi := &file_relayerapi_service_proto_msgTypes[4] + 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 RelayRequest.ProtoReflect.Descriptor instead. +func (*RelayRequest) Descriptor() ([]byte, []int) { + return file_relayerapi_service_proto_rawDescGZIP(), []int{4} +} + +func (x *RelayRequest) GetTxHash() string { + if x != nil { + return x.TxHash + } + return "" +} + +func (x *RelayRequest) GetChainId() string { + if x != nil { + return x.ChainId + } + return "" +} + +type RelayResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RelayResponse) Reset() { + *x = RelayResponse{} + mi := &file_relayerapi_service_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RelayResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RelayResponse) ProtoMessage() {} + +func (x *RelayResponse) ProtoReflect() protoreflect.Message { + mi := &file_relayerapi_service_proto_msgTypes[5] + 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 RelayResponse.ProtoReflect.Descriptor instead. +func (*RelayResponse) Descriptor() ([]byte, []int) { + return file_relayerapi_service_proto_rawDescGZIP(), []int{5} +} + +var File_relayerapi_service_proto protoreflect.FileDescriptor + +const file_relayerapi_service_proto_rawDesc = "" + + "\n" + + "\x18relayerapi/service.proto\x12\fskip.relayer\x1a\x1fgoogle/protobuf/timestamp.proto\"C\n" + + "\rStatusRequest\x12\x17\n" + + "\atx_hash\x18\x01 \x01(\tR\x06txHash\x12\x19\n" + + "\bchain_id\x18\x02 \x01(\tR\achainId\"E\n" + + "\x0fTransactionInfo\x12\x17\n" + + "\atx_hash\x18\x01 \x01(\tR\x06txHash\x12\x19\n" + + "\bchain_id\x18\x02 \x01(\tR\achainId\"\xf8\x02\n" + + "\fPacketStatus\x121\n" + + "\x05state\x18\x01 \x01(\x0e2\x1b.skip.relayer.TransferStateR\x05state\x12'\n" + + "\x0fsequence_number\x18\x02 \x01(\x04R\x0esequenceNumber\x12(\n" + + "\x10source_client_id\x18\x03 \x01(\tR\x0esourceClientId\x126\n" + + "\asend_tx\x18\x04 \x01(\v2\x1d.skip.relayer.TransactionInfoR\x06sendTx\x126\n" + + "\arecv_tx\x18\x05 \x01(\v2\x1d.skip.relayer.TransactionInfoR\x06recvTx\x124\n" + + "\x06ack_tx\x18\x06 \x01(\v2\x1d.skip.relayer.TransactionInfoR\x05ackTx\x12<\n" + + "\n" + + "timeout_tx\x18\a \x01(\v2\x1d.skip.relayer.TransactionInfoR\ttimeoutTx\"U\n" + + "\x0eStatusResponse\x12C\n" + + "\x0fpacket_statuses\x18\x01 \x03(\v2\x1a.skip.relayer.PacketStatusR\x0epacketStatuses\"B\n" + + "\fRelayRequest\x12\x17\n" + + "\atx_hash\x18\x01 \x01(\tR\x06txHash\x12\x19\n" + + "\bchain_id\x18\x02 \x01(\tR\achainId\"\x0f\n" + + "\rRelayResponse*\x7f\n" + + "\rTransferState\x12\x1a\n" + + "\x16TRANSFER_STATE_UNKNOWN\x10\x00\x12\x1a\n" + + "\x16TRANSFER_STATE_PENDING\x10\x01\x12\x1b\n" + + "\x17TRANSFER_STATE_COMPLETE\x10\x02\x12\x19\n" + + "\x15TRANSFER_STATE_FAILED\x10\x032\x9e\x01\n" + + "\x11RelayerApiService\x12B\n" + + "\x05Relay\x12\x1a.skip.relayer.RelayRequest\x1a\x1b.skip.relayer.RelayResponse\"\x00\x12E\n" + + "\x06Status\x12\x1b.skip.relayer.StatusRequest\x1a\x1c.skip.relayer.StatusResponse\"\x00B4Z2github.com/cosmos/ibc-relayer/proto/gen/relayerapib\x06proto3" + +var ( + file_relayerapi_service_proto_rawDescOnce sync.Once + file_relayerapi_service_proto_rawDescData []byte +) + +func file_relayerapi_service_proto_rawDescGZIP() []byte { + file_relayerapi_service_proto_rawDescOnce.Do(func() { + file_relayerapi_service_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_relayerapi_service_proto_rawDesc), len(file_relayerapi_service_proto_rawDesc))) + }) + return file_relayerapi_service_proto_rawDescData +} + +var file_relayerapi_service_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_relayerapi_service_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_relayerapi_service_proto_goTypes = []any{ + (TransferState)(0), // 0: skip.relayer.TransferState + (*StatusRequest)(nil), // 1: skip.relayer.StatusRequest + (*TransactionInfo)(nil), // 2: skip.relayer.TransactionInfo + (*PacketStatus)(nil), // 3: skip.relayer.PacketStatus + (*StatusResponse)(nil), // 4: skip.relayer.StatusResponse + (*RelayRequest)(nil), // 5: skip.relayer.RelayRequest + (*RelayResponse)(nil), // 6: skip.relayer.RelayResponse +} +var file_relayerapi_service_proto_depIdxs = []int32{ + 0, // 0: skip.relayer.PacketStatus.state:type_name -> skip.relayer.TransferState + 2, // 1: skip.relayer.PacketStatus.send_tx:type_name -> skip.relayer.TransactionInfo + 2, // 2: skip.relayer.PacketStatus.recv_tx:type_name -> skip.relayer.TransactionInfo + 2, // 3: skip.relayer.PacketStatus.ack_tx:type_name -> skip.relayer.TransactionInfo + 2, // 4: skip.relayer.PacketStatus.timeout_tx:type_name -> skip.relayer.TransactionInfo + 3, // 5: skip.relayer.StatusResponse.packet_statuses:type_name -> skip.relayer.PacketStatus + 5, // 6: skip.relayer.RelayerApiService.Relay:input_type -> skip.relayer.RelayRequest + 1, // 7: skip.relayer.RelayerApiService.Status:input_type -> skip.relayer.StatusRequest + 6, // 8: skip.relayer.RelayerApiService.Relay:output_type -> skip.relayer.RelayResponse + 4, // 9: skip.relayer.RelayerApiService.Status:output_type -> skip.relayer.StatusResponse + 8, // [8:10] is the sub-list for method output_type + 6, // [6:8] is the sub-list for method input_type + 6, // [6:6] is the sub-list for extension type_name + 6, // [6:6] is the sub-list for extension extendee + 0, // [0:6] is the sub-list for field type_name +} + +func init() { file_relayerapi_service_proto_init() } +func file_relayerapi_service_proto_init() { + if File_relayerapi_service_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_relayerapi_service_proto_rawDesc), len(file_relayerapi_service_proto_rawDesc)), + NumEnums: 1, + NumMessages: 6, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_relayerapi_service_proto_goTypes, + DependencyIndexes: file_relayerapi_service_proto_depIdxs, + EnumInfos: file_relayerapi_service_proto_enumTypes, + MessageInfos: file_relayerapi_service_proto_msgTypes, + }.Build() + File_relayerapi_service_proto = out.File + file_relayerapi_service_proto_goTypes = nil + file_relayerapi_service_proto_depIdxs = nil +} diff --git a/ift/relayer/pb/relayerapi/service_grpc.pb.go b/ift/relayer/pb/relayerapi/service_grpc.pb.go new file mode 100644 index 0000000..139f0e5 --- /dev/null +++ b/ift/relayer/pb/relayerapi/service_grpc.pb.go @@ -0,0 +1,169 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.1 +// - protoc (unknown) +// source: relayerapi/service.proto + +package relayerapi + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + RelayerApiService_Relay_FullMethodName = "/skip.relayer.RelayerApiService/Relay" + RelayerApiService_Status_FullMethodName = "/skip.relayer.RelayerApiService/Status" +) + +// RelayerApiServiceClient is the client API for RelayerApiService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type RelayerApiServiceClient interface { + // Relay will track the status of a transfer, and submit the transactions required to complete the transfer via + // the bridging protocol. The transfer will be updated to PROCESSING until the transfer is completed, or fails. + Relay(ctx context.Context, in *RelayRequest, opts ...grpc.CallOption) (*RelayResponse, error) + // Status will return the current status of a transfer. PENDING, PROCESSING, COMPLETED, or FAILED. We assume that + // the status is PENDING for any transaction that has not been verified by the VerifyRelayPayment operation. + // The status message will be set if the status is FAILED. + Status(ctx context.Context, in *StatusRequest, opts ...grpc.CallOption) (*StatusResponse, error) +} + +type relayerApiServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewRelayerApiServiceClient(cc grpc.ClientConnInterface) RelayerApiServiceClient { + return &relayerApiServiceClient{cc} +} + +func (c *relayerApiServiceClient) Relay(ctx context.Context, in *RelayRequest, opts ...grpc.CallOption) (*RelayResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RelayResponse) + err := c.cc.Invoke(ctx, RelayerApiService_Relay_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *relayerApiServiceClient) Status(ctx context.Context, in *StatusRequest, opts ...grpc.CallOption) (*StatusResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StatusResponse) + err := c.cc.Invoke(ctx, RelayerApiService_Status_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// RelayerApiServiceServer is the server API for RelayerApiService service. +// All implementations must embed UnimplementedRelayerApiServiceServer +// for forward compatibility. +type RelayerApiServiceServer interface { + // Relay will track the status of a transfer, and submit the transactions required to complete the transfer via + // the bridging protocol. The transfer will be updated to PROCESSING until the transfer is completed, or fails. + Relay(context.Context, *RelayRequest) (*RelayResponse, error) + // Status will return the current status of a transfer. PENDING, PROCESSING, COMPLETED, or FAILED. We assume that + // the status is PENDING for any transaction that has not been verified by the VerifyRelayPayment operation. + // The status message will be set if the status is FAILED. + Status(context.Context, *StatusRequest) (*StatusResponse, error) + mustEmbedUnimplementedRelayerApiServiceServer() +} + +// UnimplementedRelayerApiServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedRelayerApiServiceServer struct{} + +func (UnimplementedRelayerApiServiceServer) Relay(context.Context, *RelayRequest) (*RelayResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Relay not implemented") +} +func (UnimplementedRelayerApiServiceServer) Status(context.Context, *StatusRequest) (*StatusResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Status not implemented") +} +func (UnimplementedRelayerApiServiceServer) mustEmbedUnimplementedRelayerApiServiceServer() {} +func (UnimplementedRelayerApiServiceServer) testEmbeddedByValue() {} + +// UnsafeRelayerApiServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to RelayerApiServiceServer will +// result in compilation errors. +type UnsafeRelayerApiServiceServer interface { + mustEmbedUnimplementedRelayerApiServiceServer() +} + +func RegisterRelayerApiServiceServer(s grpc.ServiceRegistrar, srv RelayerApiServiceServer) { + // If the following call panics, it indicates UnimplementedRelayerApiServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&RelayerApiService_ServiceDesc, srv) +} + +func _RelayerApiService_Relay_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RelayRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RelayerApiServiceServer).Relay(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RelayerApiService_Relay_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RelayerApiServiceServer).Relay(ctx, req.(*RelayRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _RelayerApiService_Status_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(StatusRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RelayerApiServiceServer).Status(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RelayerApiService_Status_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RelayerApiServiceServer).Status(ctx, req.(*StatusRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// RelayerApiService_ServiceDesc is the grpc.ServiceDesc for RelayerApiService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var RelayerApiService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "skip.relayer.RelayerApiService", + HandlerType: (*RelayerApiServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Relay", + Handler: _RelayerApiService_Relay_Handler, + }, + { + MethodName: "Status", + Handler: _RelayerApiService_Status_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "relayerapi/service.proto", +}