Skip to content

internal/ethapi: unify relay endpoints and rate limit tx submissions#2203

Open
manav2401 wants to merge 3 commits intomasterfrom
unify-relay-endpoints
Open

internal/ethapi: unify relay endpoints and rate limit tx submissions#2203
manav2401 wants to merge 3 commits intomasterfrom
unify-relay-endpoints

Conversation

@manav2401
Copy link
Copy Markdown
Member

@manav2401 manav2401 commented Apr 29, 2026

Description

This PR does the following things:

  • Updates the eth_sendRawTransaction endpoint to be used for both preconf and private transaction submission instead of using separate endpoints for both of them.
  • The existing eth_sendRawTransactionPrivate endpoint will still operate as it does now.
  • Adds rate limiting on submitting transactions to block producers.

Changes

  • Bugfix (non-breaking change that solves an issue)
  • Hotfix (change that solves an urgent issue, and requires immediate attention)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (change that is not backwards-compatible and/or changes current functionality)
  • Changes only for a subset of nodes

Breaking changes

Please complete this section if any breaking changes have been made, otherwise delete it

Nodes audience

In case this PR includes changes that must be applied only to a subset of nodes, please specify how you handled it (e.g. by adding a flag with a default value...)

Checklist

  • I have added at least 2 reviewer or the whole pos-v1 team
  • I have added sufficient documentation in code
  • I will be resolving comments - if any - by pushing each fix in a separate commit and linking the commit hash in the comment reply
  • Created a task in Jira and informed the team for implementation in Erigon client (if applicable)
  • Includes RPC methods changes, and the Notion documentation has been updated

Cross repository changes

  • This PR requires changes to heimdall
    • In case link the PR here:
  • This PR requires changes to matic-cli
    • In case link the PR here:

Testing

  • I have added unit tests
  • I have added tests to CI
  • I have tested this code manually on local environment
  • I have tested this code manually on remote devnet using express-cli
  • I have tested this code manually on amoy
  • I have created new e2e tests into express-cli

Manual tests

Please complete this section with the steps you performed if you ran manual tests for this functionality, otherwise delete it

Additional comments

Please post additional comments in this section if you have them, otherwise delete it

Copilot AI review requested due to automatic review settings April 29, 2026 12:48
Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.

Tip: disable this comment in your organization's Code Review settings.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the internal/ethapi transaction submission flow so eth_sendRawTransaction can drive both preconfirmation and private-tx relay submission (depending on node flags), while keeping eth_sendRawTransactionPrivate working as before.

Changes:

  • Updated TransactionAPI.SendRawTransaction to (when PrivateTxEnabled()): record private tx hashes, purge on local submission failure, and submit private txs to BPs with a user-facing wrapped error on relay failure.
  • Improved logging to include tx hashes for preconf/private relay submission failures and unified the wrapped error message between SendRawTransaction and SendRawTransactionPrivate.
  • Added/updated unit tests to cover the unified behavior across flag combinations and key failure modes (local pool failure, ErrAlreadyKnown, relay failures).

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
internal/ethapi/api.go Implements unified private+preconf behavior in SendRawTransaction, adjusts logging, and aligns SendRawTransactionPrivate error/log output.
internal/ethapi/api_test.go Expands test coverage for unified submission behavior and updates expected wrapped error message text.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@claude
Copy link
Copy Markdown

claude Bot commented Apr 29, 2026

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

@manav2401 manav2401 changed the title internal/ethapi: unify preconf and private tx endpoints internal/ethapi: unify relay endpoints and rate limit tx submissions Apr 29, 2026
@manav2401 manav2401 changed the base branch from develop to master April 29, 2026 13:39
@pratikspatil024
Copy link
Copy Markdown
Member

@claude - review once

@claude
Copy link
Copy Markdown

claude Bot commented Apr 29, 2026

Claude finished @pratikspatil024's task in 3m 38s —— View job


Code Review: Unify Relay Endpoints & Rate Limit TX Submissions

  • Gather context and read changed files
  • Review eth/relay/ changes (relay service, rate limiting)
  • Review internal/ethapi/ changes (unified endpoint logic)
  • Review config/flags changes
  • Review test coverage
  • Post final review

Summary

The PR achieves its goals: eth_sendRawTransaction now handles both preconf and private tx relay submission, separate semaphores cap concurrent preconf/private-tx BP submissions, and comprehensive tests cover the new behavior. The overall design is sound. I found one bug and a few items to consider.


Bug: Type mismatch in test assertions will fail at runtime

eth/relay/service_test.go:41-42

require.Equal(t, defaultConfig.maxConcurrentPreconfs, cap(service.preconfSemaphore), ...)
require.Equal(t, defaultConfig.maxConcurrentPrivateTxs, cap(service.privateSemaphore), ...)

maxConcurrentPreconfs and maxConcurrentPrivateTxs are uint64, but cap() returns int. Testify's require.Equal uses reflect.DeepEqual which is type-strict — uint64(512) != int(512). The original field maxConcurrentTasks was int, so this worked before.

Fix: either cast to int in the assertion (require.Equal(t, int(defaultConfig.maxConcurrentPreconfs), cap(...))), or change the config fields back to int. Fix this →


Medium: Blocking semaphore in SubmitPrivateTx without context propagation

eth/relay/service.go:292-296

select {
case s.privateSemaphore <- struct{}{}:
case <-s.closeCh:
    return errRpcClientUnavailable
}

This blocks the RPC handler goroutine indefinitely when the semaphore is full. The only escape is closeCh (node shutdown). If requests arrive faster than BP RPC calls complete, goroutines accumulate — each blocked goroutine consumes ~4KB stack.

Preconf submissions avoid this problem by using a non-blocking queue (taskCh) that drops on overflow. Consider:

  • Passing ctx context.Context through to the select so RPC timeouts are respected, or
  • Using a non-blocking default case that returns an error immediately when at capacity (like preconfs do with errQueueOverflow)

With the default of 2048 and 2s RPC timeout per submission, worst-case backlog under sustained load is manageable, but a thundering herd (e.g., relay restart with queued submissions) could stack up goroutines.


Low: Config naming inconsistency

internal/cli/server/config.go:812-813

The TOML/HCL key for private txs is max-concurrent-private-tx (singular), while preconfs use max-concurrent-preconfs (plural). For consistency, consider max-concurrent-private-txs (plural) to match the Go field name MaxConcurrentPrivateTxs.

Similarly, the CLI flag relay.max-concurrent-private-tx vs relay.max-concurrent-preconfs.


Low: No upper-bound validation on concurrency config values

eth/relay/relay.go:54-58

if maxConcurrentPreconfs > 0 {
    serviceConfig.maxConcurrentPreconfs = maxConcurrentPreconfs
}

A misconfigured uint64 value (e.g., MaxConcurrentPreconfs: 18446744073709551615) would attempt to allocate a channel with that capacity, causing OOM. Consider capping at a sane max (e.g., 100,000) and logging a warning.


Note: SendRawTransactionSync does not participate

SendRawTransactionSync does not call RecordPrivateTx, so if a user calls it with PrivateTxEnabled=true, the transaction will still be gossiped publicly. This is pre-existing behavior and may be intentional (the sync endpoint is for a different use case), but worth documenting if users might assume all sendRawTransaction* variants respect the private tx flag.


What looks good

  • Unified endpoint logic in SendRawTransaction — clean: record → submit to pool → preconf → private, with proper purge-on-failure and ErrAlreadyKnown handling
  • Separate semaphores for preconf vs private tx — appropriate since they have different concurrency profiles (queue-based vs synchronous)
  • Test coverage — the TestSendRawTransaction_Unified suite is thorough, covering all flag combinations, error paths, and the ErrAlreadyKnown short-circuit
  • Logging improvements — adding hash to error log lines is valuable for debugging
  • Semaphore testsservice_test.go properly tests slot release on both success and error, and blocking/unblocking behavior

|

Copilot AI review requested due to automatic review settings April 29, 2026 13:44
@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
3.0% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread eth/relay/service.go
Comment on lines +90 to +93
taskCh: make(chan TxTask, config.maxQueuedTasks),
preconfSemaphore: make(chan struct{}, config.maxConcurrentPreconfs),
privateSemaphore: make(chan struct{}, config.maxConcurrentPrivateTxs),
closeCh: make(chan struct{}),
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

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

ServiceConfig.maxConcurrentPreconfs / maxConcurrentPrivateTxs are uint64, but make(chan struct{}, ...) requires an int capacity. As written, this won’t compile. Convert safely (with bounds checking/clamping) or change these config fields to int (and validate CLI/config inputs) before using them for channel capacities.

Copilot uses AI. Check for mistakes.
Comment thread eth/relay/service.go
case task := <-s.taskCh:
// Acquire semaphore to limit concurrent submissions
select {
case s.semaphore <- struct{}{}:
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.

This previous semaphore just existed for private submissions. Because I see this being replaced by preconfSemaphore and privateSemaphore. But what about Txs that doesnt fit on any of these tasks? Is it possible?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

That's not really possible - it could only belong to preconf or private tx task.

I separated them out so that we can have different limits for both if needed.

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 29, 2026

Codecov Report

❌ Patch coverage is 88.57143% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 52.21%. Comparing base (ad2d891) to head (cf62604).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
eth/relay/relay.go 33.33% 4 Missing and 2 partials ⚠️
eth/relay/service.go 85.71% 2 Missing ⚠️

❌ Your patch check has failed because the patch coverage (88.57%) is below the target coverage (90.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #2203      +/-   ##
==========================================
- Coverage   52.21%   52.21%   -0.01%     
==========================================
  Files         884      884              
  Lines      155479   155528      +49     
==========================================
+ Hits        81190    81203      +13     
- Misses      69060    69086      +26     
- Partials     5229     5239      +10     
Files with missing lines Coverage Δ
eth/backend.go 52.53% <100.00%> (ø)
eth/ethconfig/config.go 78.94% <ø> (ø)
internal/cli/server/config.go 63.96% <100.00%> (+0.11%) ⬆️
internal/cli/server/flags.go 100.00% <100.00%> (ø)
internal/ethapi/api.go 41.86% <100.00%> (+0.54%) ⬆️
eth/relay/service.go 94.70% <85.71%> (-0.92%) ⬇️
eth/relay/relay.go 87.23% <33.33%> (-5.87%) ⬇️

... and 25 files with indirect coverage changes

Files with missing lines Coverage Δ
eth/backend.go 52.53% <100.00%> (ø)
eth/ethconfig/config.go 78.94% <ø> (ø)
internal/cli/server/config.go 63.96% <100.00%> (+0.11%) ⬆️
internal/cli/server/flags.go 100.00% <100.00%> (ø)
internal/ethapi/api.go 41.86% <100.00%> (+0.54%) ⬆️
eth/relay/service.go 94.70% <85.71%> (-0.92%) ⬇️
eth/relay/relay.go 87.23% <33.33%> (-5.87%) ⬇️

... and 25 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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.

5 participants