We have already established a long term goal: build a Rust core driven by a thin Python shell.
Communication with the blockchain is done via Remote Procedure Call (RPC) exposed by a node directly, or through some gateway service. An RPC is your link to the chain, so interacting with it is critical to the searcher experience. You will use it to build your local view of the chain, fetch and receive data, inspect state, and simulate / send transactions.
The layer that communicates with the RPC is generally called a provider. It encapsulates the translation of chain and block values to language primitives (e.g. converting values to integers, strings, and structs), plus the low-level concerns of communicating over the major transports (HTTP, Websocket, IPC socket).
Python users use the web3py package for this. It’s easy to use, just feed a provider instance to the Web3 object and then you can interact with the chain:
In addition to HTTP, web3py includes a provider to handle Websocket and IPC connections. And there are asynchronous versions of each provider.
Rust users generally reach for Alloy, which includes a dedicated provider crate that supports the expected transports. Per their documentation, the correct way to build an Alloy Provider is to use its builder. The builder generally returns an asynchronous provider, so you need to use an appropriate async runtime like Tokio.
The setup is simple, and similar enough to web3py that you should be able to predict how this example behaves:
We shouldn’t be surprised that Alloy is faster than web3py. But how much? Let’s benchmark them with a realistic scenario: fetch the last 1,000 full blocks from the RPC using HTTP and Websocket, logging the elapsed time.
I am running against my local Reth node. Reth provides a built-in cache for RPC requests, with a default block size of 5,000. After the warm-up fetch, the blocks are available in the memory cache, so disk I/O overhead should have minimal influence on the results.
fetch_blocks.py
fetch_blocks.rs
Web3py HTTP: median 50.5 s
Web3py WS: median 46.6 s
Alloy HTTP: median 16.7 s
Alloy WS: median 16.0 s
In both cases the HTTP provider is a bit slower than the Websocket, which tells us that the transport overhead is not the primary bottleneck.
Alloy is ~3x faster vs web3py on this sequential, serialization-heavy workload.
Let’s compare the two libraries when using parallel workers and async requests that request multiple blocks per call.
fetch_blocks_parallel.py
fetch_blocks_parallel.rs
Web3py HTTP: median 16.0 s
Web3py WS: median 15.5 s
Alloy HTTP: median 1.4 s
Alloy WS: median 1.3 s
Web3py improved by ~5x, and Alloy improved ~12x. Excellent!
The Python code has also become more convoluted. Distributing to multiple workers requires creating a separate process pool so that workers are not bottlenecked by the GIL. The RPC requests themselves are not impacted by this since they use async-native libraries (aiohttp and websockets), but the transformation of the JSON response into Python for printing requires a trip through the single-threaded json library. Contrast this with the Rust version which simply clones a provider for each worker and then starts hitting the RPC.
Alloy is faster and easier to use for parallel work. Easy as that.
And as a bonus, the RPC calls all happen on the Rust side of the language boundary so that the results can be decoded and passed to pool data structs without involving the PyO3 FFI which necessarily holds onto the GIL and requires additional memory allocations to transfer data between the Python interpreter and the Rust runtime.
I have already converted much of the lingering web3py use in degenbot to Alloy, and am migrating the database update commands now. You can follow along in the dev branch, or just wait until I cut a new release

Comments
Nothing yet. Say the first thing.
Sign in to join the conversation.