Skip to content

Conversation

@Syzygy106
Copy link
Contributor

Motivation

Fixes #3186

The FallbackLayer implementation was using parallel fan-out for all RPC methods, which caused issues with methods that have non-deterministic results when executed concurrently across multiple nodes.

Specifically, eth_sendRawTransactionSync (EIP-7966) waits for a transaction receipt. When executed in parallel, for example:

  • Node A (slow): Processes the transaction and returns the receipt after 100ms
  • Node B (fast): Recognizes the transaction as "already known" and returns an error after 10ms

The FallbackLayer would incorrectly return the fast "already known" error instead of waiting for the actual receipt from the slower node.

Solution

Introduced selective execution strategies based on result determinism:

  1. Added requires_sequential_execution() function - Identifies RPC methods that return non-deterministic results when executed in parallel:

    • eth_sendRawTransactionSync (EIP-7966)
    • eth_sendTransactionSync
  2. Added make_request_sequential() method - Implements sequential fallback that tries transports one-by-one until success, respecting the ranking system.

  3. Modified make_request() - Routes non-deterministic methods to sequential execution while keeping parallel execution for all other methods (including eth_sendRawTransaction, which returns a deterministic transaction hash).

Key Design Decision: Not all stateful methods require sequential execution. Methods like eth_sendRawTransaction are stateful but return deterministic results (transaction hashes), making parallel execution safe and beneficial for performance.

Representation -

/// # Before Fix (Parallel Execution):
/// Time       Transport A               Transport B
/// ─────────────────────────────────────────────────────────
/// 0ms        🚀 START                  🚀 START (parallel!)
///            │                         │
/// 10ms       │ Processing...           ✓ "already_known"
///            │                         └──> Returns ❌ WRONG!
/// 50ms       ✓ Receipt {status: 0x1}
///            └──> Discarded (too late)
/// 
///
/// # After Fix (Sequential Execution - Success Case):
/// 
/// Time       Transport A               Transport B
/// ─────────────────────────────────────────────────────────
/// 0ms        🚀 START
///            │
/// 10ms       │ Processing...           (not called yet)
///            │
/// 50ms       ✓ Receipt {status: 0x1}  (not called - A succeeded!)
///            └──> Returns ✅ CORRECT!
/// 
///
/// # After Fix (Sequential Execution - Fallback Case):
/// 
/// Time       Transport A               Transport B
/// ─────────────────────────────────────────────────────────
/// 0ms        🚀 START
///            │
/// 10ms       │ Processing...           (waiting...)
///            │
/// 50ms       ❌ Connection timeout
///            │
/// 51ms       (A failed, trying B...)  🚀 START
///                                      │
/// 61ms                                 │ Processing...
///                                      │
/// 101ms                                ✓ Receipt {status: 0x1}
///                                      └──> Returns ✅ CORRECT!

PR Checklist

  • Added Tests
    • test_non_deterministic_method_uses_sequential_fallback - Verifies eth_sendRawTransactionSync uses sequential execution and returns correct receipt
    • test_deterministic_method_uses_parallel_execution - Verifies eth_sendRawTransaction continues using parallel execution for performance
  • Added Documentation
    • Comprehensive inline comments explaining the logic
    • Function documentation for new methods
  • Breaking changes

Copy link
Member

@mattsse mattsse left a comment

Choose a reason for hiding this comment

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

smol suggestions

Comment on lines 126 to 136
fn requires_sequential_execution(method: &str) -> bool {
matches!(
method,
// EIP-7966: eth_sendRawTransactionSync - waits for receipt
// Parallel issue: returns receipt from first node, "already known" from others
"eth_sendRawTransactionSync" |
// eth_sendTransactionSync - same as above but for unsigned transactions
// Parallel issue: returns receipt from first node, "already known" from others
"eth_sendTransactionSync"
)
}
Copy link
Member

Choose a reason for hiding this comment

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

can we make this a hashset in the fallbacklayer itself, with these values as the default, then a user can install addiitonal ones on demand

Copy link
Contributor Author

Choose a reason for hiding this comment

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

can we make this a hashset in the fallbacklayer itself, with these values as the default, then a user can install addiitonal ones on demand

ok, let me think about it

///
/// This approach ensures methods like `eth_sendRawTransactionSync` return the correct
/// receipt instead of "already known" errors from parallel execution.
async fn make_request_sequential(
Copy link
Member

Choose a reason for hiding this comment

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

this makes sense

Copy link
Member

@mattsse mattsse left a comment

Choose a reason for hiding this comment

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

ty!

@github-project-automation github-project-automation bot moved this from In Progress to Reviewed in Alloy Nov 20, 2025
@mattsse mattsse merged commit 2cce9cf into alloy-rs:main Nov 23, 2025
30 checks passed
@github-project-automation github-project-automation bot moved this from Reviewed to Done in Alloy Nov 23, 2025
@Syzygy106 Syzygy106 deleted the fix/fallback-sequential-for-sync-methods branch November 24, 2025 23:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

[Bug] FallbackLayer breaks stateful RPC methods such as eth_sendRawTransactionSync due to parallel fan-out

2 participants