Skip to content

ift relay support#68

Merged
dhfang merged 15 commits into
mainfrom
df/ift-relay-support
Apr 24, 2026
Merged

ift relay support#68
dhfang merged 15 commits into
mainfrom
df/ift-relay-support

Conversation

@dhfang
Copy link
Copy Markdown
Contributor

@dhfang dhfang commented Apr 23, 2026

Summary

Adds end-to-end IFT transfer support to Catalyst. Senders can drive MsgIFTTransfer load from cosmos or EVM chains to cosmos or EVM destinations and configure Catalyst to forward transactions to an ibc relayer.

Changes

  • Extends txfactory to support creation of IFT cosmos or evm transactions. These perform a token a transfer using IBC and should be relayed. Recipient addresses are derived deterministically from the base mnemonic (see ift/accounts/{cosmos,evm}.go).
  • Extends catalyst to support optionally relaying transactions. The configs allow the operator to specify a relay host and what messages should be passed to the relayer.
  • For EVM gas estimation, use SuggestGasTipCap from the node rather than a hardcoded 2 gwei default — works on L2s without manual tuning.
  • Extends cosmos runner to choose a tx send interval based on time instead of just block based. It uses the send_interval config.
  • Extends stats collection to also track the number of relay failures.

In a future PR, I will extend prometheus metric collection in the runners to capture relaying success/failure and latency.

johnnylarner and others added 13 commits March 13, 2026 11:01
Remove the aggregate Err field from SentTx in both cosmos and ethereum
chains. Rename SourceErr to BroadcastErr and RelayerErr to
PostBroadcastErr to align with the txMode abstraction. Add Failed() and
Error() methods that derive overall status from the specific error
fields and TxResponse.
…elay config

- add MsgIFTTransfer as a case in both ethereum and cosmos TxFactory
  BuildTxs/CreateMsg, with baseline support for high-throughput runs
- replace implicit HandlePostBroadcast with explicit RelayConfig that
  specifies a relayer URL and which message types should be relayed
- remove txMode interface and mode.go from both chains
- move relayer config from ift.relayer to top-level relay in spec
- use SuggestGasTipCap instead of hardcoded 2 gwei default for EVM
  gas fee estimation
- fix EVM IFT timeout to use Unix seconds instead of nanoseconds
@dhfang dhfang requested a review from a team as a code owner April 23, 2026 17:13
}

maxGas := params.ConsensusParams.Block.MaxGas
if maxGas == -1 {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our test chain did not have max gas configured causing catalyst to fail on the maxGas check below in lin 196

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 23, 2026

Greptile Summary

This PR adds IFT (Inter-chain Fund Transfer) relay support to both the Cosmos and Ethereum load-test runners: new MsgIFTTransfer message types, recipient address generators, an EVM contract wrapper, and a gRPC client that submits confirmed tx hashes to an external relay service.

  • P1GRPCClient.SubmitTxHash retries synchronously for up to 45 s per call (15 × 3 s); it is invoked inside the per-transaction send goroutines in all three Ethereum runners and in the Cosmos runner, meaning a degraded relayer can stall the entire load-test batch for tens of seconds and corrupt throughput measurements.
  • P1generateEVMAddress uses passphrase "" for index 0 but strconv.Itoa(index) for all others, producing an address set with an off-by-one in the first entry compared with the Cosmos generator and with what callers passing offset=0 would expect.

Confidence Score: 3/5

Not safe to merge as-is: the synchronous relay retry loop can stall load-test execution for up to 45 seconds when the relayer is slow, and the EVM address generator silently produces a wrong first recipient.

Two P1 findings: a blocking retry loop in the hot send path that defeats the purpose of a load test under relay degradation, and an inconsistent EVM passphrase that silently generates the wrong first recipient address. Both need to be addressed before the relay path is trustworthy.

ift/relayer/client.go (blocking retry loop), ift/accounts/evm.go (passphrase inconsistency for index 0)

Important Files Changed

Filename Overview
ift/relayer/client.go New gRPC relay client with blocking 15-retry loop (up to 45 s) that can stall load-test send goroutines; also leaks timers and never closes the connection.
ift/accounts/evm.go EVM recipient generator uses empty passphrase for index 0 but numeric string for all others, producing an inconsistent first address compared with the Cosmos generator.
chains/cosmos/ift/msg.go Hand-rolled MsgIFTTransfer proto message with embedded gzipped FileDescriptorProto; hardcoded message index [4] is fragile but otherwise correct.
chains/ethereum/ift/contract.go New EVM IFT transfer contract wrapper; cleanly packs ABI calldata and delegates signing to the wallet layer.
chains/types/ift.go New IFTConfig type with full validation covering source/destination kind compatibility, required fields, and recipient bounds.
chains/types/spec.go Adds RelayConfig and IFT/Relay fields to LoadTestSpec with ShouldRelay helper; IFT validation is wired into LoadTestSpec.Validate().
chains/ethereum/runner/runner.go Adds IFT and relay setup; refactors buildLoad into buildTxsForWallet and tracks tx→MsgType in a sync.Map; gRPC connection is never closed.
chains/cosmos/runner/runner.go Wires IFT config, relay client, and SendInterval logic into Cosmos runner; relay connection is never explicitly closed.
ift/accounts/generator.go Generator factory and GenerateRecipients helper; defaults recipient count/offset to NumWallets.
chains/ethereum/wallet/wallet.go Fixes gas price derivation to fetch tip first then compute feecap, removing the hardcoded 2 gwei default.

Sequence Diagram

sequenceDiagram
    participant Runner as Cosmos/ETH Runner
    participant TxFactory as TxFactory
    participant Chain as Chain Node
    participant RelayClient as GRPCClient (Relayer)

    Runner->>TxFactory: CreateMsg / BuildTxs (MsgIFTTransfer)
    TxFactory-->>Runner: signed tx

    Runner->>Chain: BroadcastTx / SendTransaction
    Chain-->>Runner: TxHash / error

    alt send succeeded
        Runner->>RelayClient: SubmitTxHash(txHash)
        loop up to 15 retries (3s apart)
            RelayClient->>Relayer: gRPC Relay(TxHash, ChainID)
            alt success
                Relayer-->>RelayClient: OK
            else failure
                Note over RelayClient: wait 3s then retry
            end
        end
        RelayClient-->>Runner: nil or error (RelayErr)
    end

    Runner->>Runner: record SentTx{SendTransactionErr, RelayErr}
Loading

Comments Outside Diff (1)

  1. chains/cosmos/runner/runner.go, line 502-508 (link)

    P2 gRPC relayer connection is never closed

    NewGRPCClient opens a *grpc.ClientConn and exposes a Close() method, but neither the Cosmos runner nor the Ethereum runner (chains/ethereum/runner/runner.go) ever calls it. After the load test completes the connection stays open indefinitely, leaking OS file descriptors. Consider wiring Close() into the runner's shutdown path or adding a defer relayerClient.Close() after the runner finishes.

Reviews (1): Last reviewed commit: "relayer failures are surfaced as a separ..." | Re-trigger Greptile

Comment thread ift/relayer/client.go
Comment on lines +55 to +77
for attempt := range maxRelayRetries {
if attempt > 0 {
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(relayRetryDelay):
}
}

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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Synchronous retry loop blocks load-test send goroutines

SubmitTxHash is called synchronously inside the transaction-sending goroutines in all three Ethereum runners (block.go, interval.go, persistent.go) and in the Cosmos runner's broadcastAndHandleResponse. Each call can block for up to 15 × 3 s = 45 s when the relayer is slow or down. Because each sending goroutine holds a wg.Done that the outer wg.Wait() waits on, a relayer outage can stall the entire load-test batch for tens of seconds per round, completely skewing throughput measurements.

Consider running the relay submission in a fire-and-forget background goroutine (storing the result separately) or using a channel-based worker pool so that transaction sending is never gated on relay delivery.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is true, but not something I would consider important to address unless it prevents me from generating sufficient load to stress the relayer. For single chain testing, the relayer does not even get called, so there is no impact there.

Comment thread ift/accounts/evm.go
Comment thread ift/relayer/client.go Outdated
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 {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Time based tx submission interval

Err: nil,
}

if err := r.relayTxHash(ctx, msgType, res.TxHash); err != nil {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only executes if the operator has relaying configured.

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))
Copy link
Copy Markdown
Contributor Author

@dhfang dhfang Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small refactor to store the msg type by tx hash in a map and look it up when building test stats later instead of inferring based on properties of the tx

// gasFeeCap = baseFee * 2 + gasTipCap (reasonable default)
baseFee := header.BaseFee
if gasTipCap == nil {
gasTipCap = big.NewInt(2000000000) // 2 gwei default tip
Copy link
Copy Markdown
Contributor Author

@dhfang dhfang Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These hard coded values were not appropriate for the networks I was testing against.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have no idea if changing these does anything negative to our existing eth tests 🤷

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the generated protobuf and whatever client code we need for the relayer eventually be in some separate repo that integrators can import instead of having to generate/define client impls everywhere we're using the relayer?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The client is already importable actually. But I tried just now and the resulting dependency version bumps created some compatibility issues with the CI runner. I'm just going to merge as is for now - if you feel strongly that this should be addressed I'm happy to in another PR.

Comment thread chains/cosmos/ift/msg.go
Comment thread chains/ethereum/ift/contract.go Outdated
// gasFeeCap = baseFee * 2 + gasTipCap (reasonable default)
baseFee := header.BaseFee
if gasTipCap == nil {
gasTipCap = big.NewInt(2000000000) // 2 gwei default tip
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have no idea if changing these does anything negative to our existing eth tests 🤷

@dhfang dhfang force-pushed the df/ift-relay-support branch from fc8de08 to f510461 Compare April 24, 2026 20:45
@dhfang dhfang merged commit e6c968f into main Apr 24, 2026
3 checks passed
@dhfang dhfang deleted the df/ift-relay-support branch April 24, 2026 21:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants