Skip to content

Conversation

@mrworker27
Copy link

No description provided.

@mrworker27 mrworker27 changed the title first commit: add second listener that forwards txs through TOR add second listener that forwards txs through TOR Nov 4, 2025
@mrworker27
Copy link
Author

grep MOO for lines that I want to discuss :)

@igor53627
Copy link

Add Dual-Listener Architecture for Tor Transaction Broadcasting

This PR implements a bidirectional proxy system that enables secure transaction broadcasting through the Tor network while maintaining backward compatibility with existing RPC functionality.

Key Changes

1. Dual Server Architecture

  • Outbound Server (OUTBOUND_ADDR, default: 127.0.0.1:8080): Accepts incoming connections from the Tor network
  • Inbound Server (INBOUND_ADDR, default: 127.0.0.1:8545): Accepts local node connections and routes transactions through Tor

Both servers run concurrently using tokio::join! for parallel execution.

2. Tor Network Integration (src/tor.rs)

Introduced new Tor client infrastructure with:

  • TorJsonRpcClient: HTTP client configured with SOCKS5 proxy for Tor connections
  • Onion: Connection pool manager with round-robin load balancing
  • OnionConfig: Flexible configuration for timeouts, connection pooling, and peer management
  • Automatic retry mechanism with exponential backoff (configurable retry count)

Configuration Options:

- Dial timeout: 60s
- Keep-alive: 60s  
- Request timeout: 60s
- Idle connection timeout: 60s
- Configurable max idle connections

3. Smart Transaction Routing (src/proxy.rs)

New handle_inbound() handler implements conditional routing:

  • Send methods (eth_sendRawTransaction): Routed through Tor network for privacy
  • Read methods (all others): Forwarded directly to local Geth for performance

This approach maximizes privacy for sensitive operations while maintaining low latency for read-only calls.

4. Environment Variable Updates

  • BIND_ADDR → Split into OUTBOUND_ADDR and INBOUND_ADDR
  • New: ONION_PEERS - Comma-separated list of .onion addresses
    • Default: ethereumbbdyhyy33d4f3frmsxm6anm6bdrffvzft4kdk5p43odcvaid.onion:8545

5. Enhanced Method Whitelist (src/whitelist.rs)

Added is_send_method() helper to identify transaction submission methods for Tor routing decisions.

Architecture Benefits

Privacy: Transaction broadcasts are anonymized through Tor
Performance: Read operations bypass Tor for minimal latency
Resilience: Round-robin load balancing across multiple onion peers
Reliability: Automatic retry with backoff for failed Tor connections
Backward Compatibility: Existing RPC clients can continue using standard endpoints

Files Changed

  • src/main.rs: Dual server setup and configuration
  • src/proxy.rs: New inbound handler and routing logic
  • src/tor.rs: Complete Tor client implementation (+175 lines)
  • src/whitelist.rs: Send method detection

Testing

Existing test suite updated to reflect refactored routing structure. All tests pass with the new architecture.


Note: Requires Tor daemon running locally (default SOCKS5 proxy: 127.0.0.1:9050)

@igor53627
Copy link

MOO Comment Analysis & Solutions

I've analyzed all the TODO (MOO) comments in the PR and proposed solutions with tradeoffs for each.


1. Variable Naming: GETH_URL → Execution Layer Client

Location: src/main.rs:48-49

// MOO: not GETH but EL client in general\!
let geth_url = std::env::var("GETH_URL").unwrap_or_else(|_| "http://127.0.0.1:8656".to_string());

Problem: Variable names assume Geth, but could be any EL client (Besu, Nethermind, Erigon, etc.)

Solutions:

Solution Pros Cons
A. Rename to EL_CLIENT_URL ✅ More accurate
✅ Future-proof
❌ Breaking change for existing configs
❌ Less familiar to users
B. Keep GETH_URL with alias ✅ Backward compatible
✅ Clear migration path
❌ Technical debt
❌ Maintains confusion
C. Support both with deprecation ✅ Smooth migration
✅ User-friendly
❌ More code to maintain
❌ Longer deprecation period

Recommendation: Solution C - Support both with deprecation warning

let el_client_url = std::env::var("EL_CLIENT_URL")
    .or_else(|_| {
        if let Ok(url) = std::env::var("GETH_URL") {
            warn\!("GETH_URL is deprecated, use EL_CLIENT_URL instead");
            Ok(url)
        } else {
            Ok("http://127.0.0.1:8545".to_string())
        }
    })
    .unwrap();

2. Default Port: 8545 vs 8656

Location: src/main.rs:49

.unwrap_or_else(|_| "http://127.0.0.1:8656".to_string()); // MOO: figure out default addr\!

Problem: Changed from standard 8545 to 8656 - unclear why

Port Analysis:

  • 8545: Standard Ethereum HTTP-RPC port (widely expected)
  • 8656: Non-standard (possibly custom setup?)
  • 8551: Engine API port (post-merge, authenticated)

Solutions:

Port Use Case Pros Cons
8545 Standard HTTP RPC ✅ Industry standard
✅ User expectation
❌ May conflict with local node
8656 Custom setup ✅ Avoids conflicts ❌ Non-standard
❌ Confusing
Make it required No default ✅ Explicit config
✅ No wrong assumptions
❌ Less convenient

Recommendation: Use 8545 (standard) with clear documentation

.unwrap_or_else(|_| {
    warn\!("EL_CLIENT_URL not set, defaulting to http://127.0.0.1:8545");
    "http://127.0.0.1:8545".to_string()
})

3. Empty Onion Peers Handling ⚠️ CRITICAL

Location: src/main.rs:56-62

// MOO: how we want to handle case with no onion peers?
let onion_peers: Vec<_> = std::env::var("ONION_PEERS")
    .unwrap_or_else(|_| {
        "ethereumbbdyhyy33d4f3frmsxm6anm6bdrffvzft4kdk5p43odcvaid.onion:8545".to_string()
    })
    .split(',')
    .map(|s| s.to_string())
    .collect();

Problem: What if no peers configured or all fail to connect?

Solutions:

Solution Behavior Pros Cons
A. Fail Fast (current in tor.rs) Return error if no peers ✅ Explicit failure
✅ No silent degradation
❌ Service won't start
❌ Less resilient
B. Fallback to Direct Route through local node ✅ Service stays up
✅ Degraded mode
❌ Privacy leak
❌ Silent failure
C. Make Optional Allow empty, reject sends ✅ Flexible deployment
✅ Clear error messages
❌ Runtime failures
❌ More error handling
D. Dynamic Peer Discovery Bootstrap from known peers ✅ Self-healing
✅ Resilient
❌ Complex implementation
❌ Trust assumptions

Recommendation: Solution C - Make optional with clear error messages

// In InboundState
pub struct InboundState {
    pub proxy_state: ProxyState,
    pub onion_peers: Option<Onion>,  // Make optional
}

// In handle_inbound
if is_send_method(&request.method) {
    match &state.onion_peers {
        Some(onion) => Ok(Json(onion.send_request(&request, 3).await?)),
        None => Err(ProxyError::ServiceUnavailable(
            "Tor network unavailable - transaction broadcasting disabled".to_string()
        ))
    }
}

Alternative: Environment-driven mode

enum TorMode {
    Required,    // Fail if no peers
    Optional,    // Warn and continue without Tor
    Fallback,    // Use direct connection if Tor fails
}

let tor_mode = std::env::var("TOR_MODE")
    .unwrap_or_else(|_| "required".to_string());

4. Additional Send Methods for Tor Routing

Location: src/whitelist.rs:58

methods.insert("eth_sendRawTransaction");
// MOO: do we want to handle other send methods ?

Problem: Only eth_sendRawTransaction routes through Tor. What about other submission methods?

Candidate Methods:

Method Should Route Through Tor? Privacy Risk
eth_sendRawTransaction Yes (current) High - reveals IP
eth_sendTransaction ⚠️ Maybe High - but requires unlocked account
eth_sendBundle Yes High - MEV bundle submission
eth_sendPrivateTransaction Yes Very High - explicitly private
eth_submitWork Yes (mining) Medium - reveals miner IP
debug_* / admin_* No N/A - should be blocked anyway

Solutions:

Approach Pros Cons
A. Conservative (current) ✅ Simple
✅ Predictable
❌ Incomplete privacy
❌ Manual updates needed
B. Comprehensive List ✅ Better privacy coverage
✅ Future-proof
❌ May route unnecessary traffic
❌ Performance overhead
C. Configurable ✅ Flexible
✅ User choice
❌ Complex config
❌ Easy to misconfigure
D. Pattern-based ✅ Catch all send variants
✅ Automatic
❌ May over-match
❌ Less explicit

Recommendation: Solution B - Comprehensive list

static SEND_METHODS: Lazy<HashSet<&'static str>> = Lazy::new(|| {
    let mut methods = HashSet::new();
    
    // Transaction submission (primary use case)
    methods.insert("eth_sendRawTransaction");
    methods.insert("eth_sendTransaction");
    
    // MEV-related submissions
    methods.insert("eth_sendBundle");
    methods.insert("eth_sendPrivateTransaction");
    methods.insert("flashbots_sendBundle");
    methods.insert("flashbots_sendPrivateTransaction");
    
    // Mining submissions (if supporting PoW)
    methods.insert("eth_submitWork");
    methods.insert("eth_submitHashrate");
    
    methods
});

5. Module Organization for is_send_method()

Location: src/whitelist.rs:72

/// Check if a method is send tx method
/// MOO: move this to other module?
pub fn is_send_method(method: &str) -> bool {
    SEND_METHODS.contains(method)
}

Problem: whitelist.rs is for allowed methods, but is_send_method() is for routing logic

Solutions:

Location Justification Pros Cons
A. Keep in whitelist.rs It's method classification ✅ No refactor needed
✅ Related to method filtering
❌ Mixed concerns
❌ Module confusion
B. Move to proxy.rs Used only in proxy ✅ Collocated with usage
✅ Clear ownership
❌ Not reusable
❌ Business logic in handler
C. New routing.rs module Dedicated routing logic ✅ Clean separation
✅ Extensible
❌ Overhead for small function
❌ More files
D. New method_types.rs Method categorization ✅ Semantic clarity
✅ Reusable taxonomy
❌ Generic naming

Recommendation: Solution C - Create routing.rs for future extensibility

// src/routing.rs
use once_cell::sync::Lazy;
use std::collections::HashSet;

/// Routing decision for RPC methods
pub enum MethodRoute {
    TorNetwork,    // Must go through Tor
    Direct,        // Can go direct to EL client  
    Blocked,       // Not allowed
}

static TOR_METHODS: Lazy<HashSet<&'static str>> = Lazy::new(|| {
    // ... send methods
});

pub fn get_route_for_method(method: &str) -> MethodRoute {
    if \!is_method_allowed(method) {
        return MethodRoute::Blocked;
    }
    
    if TOR_METHODS.contains(method) {
        return MethodRoute::TorNetwork;
    }
    
    MethodRoute::Direct
}

This enables future enhancements like:

  • Method-specific Tor circuits
  • Per-method retry policies
  • Dynamic routing based on network conditions
  • A/B testing routing strategies

Summary Recommendations

Issue Recommended Solution Priority
Variable naming Support both GETH_URL + EL_CLIENT_URL with deprecation Medium
Default port Use 8545 (standard) High
Empty peers Optional Tor with clear error messages Critical
Send methods Expand to comprehensive list High
Module structure Create routing.rs module Low

Happy to implement any of these if helpful!

@mrworker27
Copy link
Author

mrworker27 commented Nov 5, 2025

^^^ nice summary

Regarding 2. Default Port: 8545 vs 8656:

The trick is to listen default JSON-RPC port 8545 by torpc, instead of EL client. This will provide seamless integration with any app that expects local EL client running. However, this implies that we need actual EL client to listen on different port (8656 for now). So we need to pick a port that a) is not yet picked b) that can't be confused with 8545

@mrworker27
Copy link
Author

mrworker27 commented Nov 5, 2025

regarding tests:

while existing functionality is not broken, my code lacks test coverage ('cause I'm not quire sure how to test it properly); I hand-tested it for basic scenario and it works, but it's obviously not enough

I will be glad to receive recommendations on how to test this new feature

@CPerezz
Copy link
Owner

CPerezz commented Nov 6, 2025

What is this PR exactly trying to do? COuld you explain a bit?

@igor53627
Copy link

What is this PR exactly trying to do? COuld you explain a bit?

one part is kinda similar to https://github.com/CPerezz/torpc?tab=readme-ov-file#flashbots-integration

                            LOCAL ENVIRONMENT
                                    |
                                    |
                      +-------------v-------------+
                      |   Local Geth/EL Client   |
                      |                          |
                      +-------------+-------------+
                                    |
                                    | RPC Requests
                                    |
                      +-------------v-------------+
                      |  INBOUND SERVER :8545    |
                      | (receives from local)    |
                      +-------------+-------------+
                                    |
                      +-------------v-------------+
                      |   Method Type Router     |
                      |                          |
                      | eth_sendRawTransaction?  |
                      +------+------------+------+
                             |            |
                      YES ---+            +--- NO
                  (Transaction)        (Query)
                             |                  |
                             |                  |
          +------------------v-------+          |
          | PRIVACY PATH (Tor)      |          |
          |                         |          |
          | +---------------------+ |          |
          | | Round-Robin         | |          |
          | | Load Balancer       | |          |
          | +----------+----------+ |          |
          |            |            |          |
          | +----------v----------+ |          |
          | | Tor SOCKS5 Proxy    | |          |
          | |     :9050           | |          |
          | +----------+----------+ |          |
          |            |            |          |
          |     +======v======+     |          |
          |     | Tor Network |     |          |
          |     +======+======+     |          |
          |            |            |          |
          |     +------+------+     |          |
          |     |      |      |     |          |
          |     v      v      v     |          |
          |  .onion .onion .onion  |          |
          |  Peer1  Peer2  Peer3   |          |
          |  :8545  :8545  :8545   |          |
          |     |      |      |     |          |
          |     +------+------+     |          |
          |            |            |          |
          |            v            |          |
          |    Ethereum Network     |          |
          |                         |          |
          | * 3 retry attempts      |          |
          | * Exponential backoff   |          |
          | * Never falls back      |          |
          +-------------------------+          |
                                                |
                                   +------------v------------+
                                   | PERFORMANCE PATH        |
                                   | (Direct)                |
                                   |                         |
                                   | * eth_blockNumber       |
                                   | * eth_getBalance        |
                                   | * eth_call              |
                                   | * No Tor overhead       |
                                   |                         |
                                   |    Direct to            |
                                   |  Local Geth :8656       |
                                   +-------------------------+

and a second listener is a .onion hidden address listener?

          +-----------------------------------------------------+
          |         OUTBOUND SERVER :8080                      |
          |      (receives from Tor network)                   |
          |                                                    |
          | External RPC clients -> Tor -> Server -> Geth      |
          |                                                    |
          | * Provides external access via Tor                 |
          | * Standard RPC + Flashbots endpoints               |
          +-----------------------------------------------------+

@mrworker27
Copy link
Author

Yep, I try to cover two scenarios using torpc:

  1. TOR -> hidden service -> torpc -> EL client (this is the case torpc offers in master already)
  2. local node -> torpc -> TOR -> hidden serivce (new case, with opposite direction)

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