Rust Ethereum Development: Building Clients with ethers-rs

·

Ethereum development has evolved into a multi-language ecosystem, and Rust is emerging as one of the most powerful tools for building secure, high-performance blockchain applications. This article dives into using ethers-rs, Rust’s premier library for Ethereum interaction, to create robust clients capable of connecting to the network, reading data, and handling real-time events. Whether you're familiar with Go-based Ethereum development or coming from a JavaScript background, this guide offers a clear path to mastering Ethereum client creation in Rust.

Understanding Ethereum Development Categories

Before diving into code, it's essential to understand the two primary branches of Ethereum development:

While layer-2 and sidechain development are growing in importance, this series focuses on off-chain interactions using Rust.

This article centers on client-side Ethereum development with ethers-rs, making it ideal for developers who already grasp Rust basics and have foundational knowledge of Ethereum but want to bridge the two effectively.


Core Keywords

These keywords naturally align with user search intent around building reliable Ethereum clients in Rust and will be integrated contextually throughout the content.


Getting Started with ethers-rs

The ethers-rs library draws strong inspiration from ethers.js, especially in its conceptual design. One key abstraction is the Provider, equivalent to a "client" in other libraries like go-ethereum. The Provider allows your application to query blockchain data without sending transactions.

👉 Discover how to build secure blockchain clients with advanced Rust tooling.

Here’s a minimal example that connects to a public Ethereum node and retrieves the latest block number:

use ethers::prelude::*;
const RPC_URL: &str = "https://cloudflare-eth.com";

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let provider = Provider::<Http>::try_from(RPC_URL)?;
    let block_number = provider.get_block_number().await?;
    println!("Current block number: {block_number}");
    Ok(())
}

This snippet uses:

Ensure your Cargo.toml includes these dependencies:

[dependencies]
ethers = { version = "2.0", features = ["rustls", "ws"] }
tokio = { version = "1", features = ["full"] }
eyre = "0.6"
Note: rustls enables TLS support without OpenSSL, and ws adds WebSocket functionality for event streaming.

Connecting to Ethereum Nodes

To interact with Ethereum, your client must connect to a node. There are two main approaches:

1. Public (External) Nodes

Ideal for development and light production use. Popular services include:

Most require registration for API keys due to rate limiting and usage tracking.

For simple queries—like fetching block hashes or transaction lists—you can use free public endpoints such as:

Use WebSocket (wss://) when you need real-time updates, such as listening to contract events.

Example: Connecting via both HTTP and WebSocket

use ethers::prelude::*;

const HTTP_RPC_URL: &str = "https://cloudflare-eth.com";
const WEBSOCKET_RPC_URL: &str = "wss://cloudflare-eth.com";

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // HTTP connection
    let http_provider = Provider::<Http>::try_from(HTTP_RPC_URL)?;
    let block_num = http_provider.get_block_number().await?;
    println!("HTTP block number: {block_num}");

    // WebSocket connection
    let ws_provider = Provider::connect(WEBSOCKET_RPC_URL).await?;
    let block_num = ws_provider.get_block_number().await?;
    println!("WebSocket block number: {block_num}");

    Ok(())
}

2. Private Nodes

For full control and higher throughput, run your own Geth or Erigon node. Local nodes allow faster queries and eliminate third-party dependency risks.

IPC Connection (Recommended for Local Nodes)

Inter-process communication (IPC) is faster and more efficient than HTTP for local setups:

use ethers::providers::Provider;
const IPC_PATH: &str = "~/.ethereum/geth.ipc";

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let provider = Provider::connect_ipc(IPC_PATH).await?;
    let block_number = provider.get_block_number().await?;
    println!("Current block number: {block_number}");
    Ok(())
}

Enhancing Reliability with Provider Wrappers

Network instability is inevitable. ethers-rs provides advanced wrapper types to handle retries, consistency, and performance optimization seamlessly.

Quorum Provider

Ensures data integrity by querying multiple backends and returning only when a majority agree:

use ethers::providers::{QuorumProvider, Provider, Http};
let provider_a = Provider::<Http>::try_from("https://node-a.eth.org")?;
let provider_b = Provider::<Http>::try_from("https://node-b.eth.org")?;
let provider_c = Provider::<Http>::try_from("https://node-c.eth.org")?;

let quorum = QuorumProvider::new(vec![provider_a, provider_b, provider_c], 0.5);
let block_num = quorum.get_block_number().await?;

If responses are [1000, 1000, 1005], the result will be 1000.

Retry Provider

Automatically retries failed requests with exponential backoff:

use ethers::middleware::RetryPolicy;

let provider = Provider::<Http>::try_from("https://unstable-node.eth")?;
let retry_provider = provider.wrap_into(RetryPolicy::new(3, 100));

Configurable retry count and initial delay enhance resilience under load.

Read-Write (RW) Splitting

Separates read and write operations across different endpoints for better performance:

let read_provider = Provider::<Http>::try_from("https://infura.io/read")?;
let write_provider = Provider::<Http>::try_from("https://infura.io/write")?;

let rw_provider = RWProvider::new(read_provider, write_provider);

Useful in architectures where write-heavy operations (e.g., transaction submission) benefit from dedicated routing.

👉 Learn how top developers ensure resilient blockchain connectivity using Rust patterns.


Testing and Extensibility

ethers-rs supports mocking providers for unit testing:

use ethers::providers::MockProvider;
let mock = MockProvider::new();
mock.add_response("eth_blockNumber", "0x12345");

You can also extend functionality through custom middleware or implement traits for specialized behavior.


Frequently Asked Questions (FAQ)

Q: What is the difference between a Provider and a Client in ethers-rs?
A: In ethers-rs, "Provider" is the standard term for a client that interacts with Ethereum. It follows ethers.js naming conventions and supports read-only operations unless combined with a wallet for signing.

Q: Can I use ethers-rs for sending transactions?
A: Yes! While Providers handle reads, you can attach a wallet (via SignerMiddleware) to send signed transactions. That topic will be covered in the next article.

Q: Is WebSocket necessary for all applications?
A: No. Use HTTP for one-off queries. Switch to WebSocket (wss://) only when you need real-time event listening or subscription-based updates.

Q: How does Quorum prevent incorrect data?
A: By requiring consensus across multiple nodes. If one node returns corrupted or outdated data, the majority vote ensures accuracy—assuming most nodes are trustworthy.

Q: Are there alternatives to ethers-rs for Rust Ethereum development?
A: Yes, including web3.rs, but ethers-rs is widely preferred due to its modern async design, comprehensive features, and active maintenance.


Final Thoughts

Building reliable Ethereum clients in Rust with ethers-rs combines performance, safety, and flexibility. From simple HTTP queries to fault-tolerant multi-node setups with retry logic and quorum validation, Rust empowers developers to build production-grade blockchain integrations.

Whether you're bridging decentralized apps, building indexers, or creating monitoring tools, mastering client creation is the first step toward robust off-chain systems.

👉 Explore cutting-edge Rust tools for next-generation blockchain applications.