18 min read
Overview
Ever wanted to build your own custom Web3 API? Reth now allows you to extend the standard Ethereum JSON-RPC API with custom functionality without the need for forking the codebase. This opens up possibilities like building a Marketplace add-on to be used for DeFi, specialized indexers, and more.
This guide will walk you through building and deploying a custom RPC method with Reth. Specifically, we'll create an RPC method called eth_getBlockUtilization
that retrieves the current or historical block utilization rate.
To learn more about how you can build your own Marketplace Add-on, check out this guide
What You Will Learn
- Learn about Reth
- Build a custom RPC method
- Run your Reth node with the custom RPC method configuration
- Test and validate the custom RPC method
What You Will Need
- A running Reth node - Follow our How to Run a Reth Node guide first
- Rust development environment (rustc 1.86+)
- Linux, macOS, or Windows with WSL
- Git installed
- Basic familiarity with Rust programming and JSON-RPC concepts
- Understanding of Ethereum architecture, transactions and smart contracts
What is Reth?
Reth is a high-performance Ethereum execution client written in Rust. It's designed to be modular, fast, and developer friendly while maintaining full compatibility with the Ethereum.
Custom RPC Methods on Reth
Reth now allows natively building custom RPC methods without needing to maintain chain updates, like how you might in a forked codebase. This means less time debugging and maintaining code. To utilize a custom RPC method, you'll need to enable the --extended-eth-namespace
flag when running your Reth node. This means we'll have to stop our node briefly in order to restart it with the configuration needed.
Let's dive into the code.
Code Review
This guide covers building a method called eth_getBlockUtilization
, which can query the current or historical block utilization rate.
The logic for this custom method will live in the reth/src/main.rs
file. Let's go over the code piece by piece.
Imports
//! Custom Reth client with extended ETH namespace for block utilization metrics
#![allow(unused_crate_dependencies)]
use clap::Parser;
use jsonrpsee::{core::RpcResult, proc_macros::rpc, types::ErrorObjectOwned};
use reth_ethereum::{
cli::{chainspec::EthereumChainSpecParser, interface::Cli},
node::EthereumNode,
};
use alloy_rpc_types::BlockNumberOrTag;
use reth_provider::{BlockReaderIdExt, HeaderProvider};
use reth_primitives::Header;
Main - fn main()
fn main() {
Cli::<EthereumChainSpecParser, RethCliExtended>::parse()
.run(|builder, args| async move {
let handle = builder
.node(EthereumNode::default())
.extend_rpc_modules(move |ctx| {
if !args.extended_eth_namespace {
return Ok(());
}
let provider = ctx.provider().clone();
let ext = BlockUtilizationImpl::new(provider);
ctx.modules.merge_configured(ext.into_rpc())?;
println!("Extended ETH namespace enabled with eth_getBlockUtilization");
Ok(())
})
.launch()
.await?;
handle.wait_for_node_exit().await
})
.unwrap();
}
Whats happening:
Cli::<EthereumChainSpecParser, RethCliExtended>::parse()
- Uses types to allow different chain specifications and CLI extensionsbuilder.node(EthereumNode::default())
- Sets the base Ethereum node implementationextend_rpc_modules(move)
|ctx| - Provides a hook to add custom RPC methodsctx.provider().clone()
- Access to the blockchain data providerctx.modules.merge_configured(ext.into_rpc())?
- Registers the new RPC method
Configuration Extension
#[derive(Default, clap::Args)]
struct RethCliExtended {
#[arg(long)]
extended_eth_namespace: bool,
}
What's happening:
#[derive(Default, clap::Args)]
- Automatic CLI argument parsing#[arg(long)]
- Creates --enable-custom-rpc command line flag- The struct extends Reth's base CLI without modifying core code
RPC Trait Definition
#[cfg_attr(not(test), rpc(server, namespace = "eth"))]
#[cfg_attr(test, rpc(server, client, namespace = "eth"))]
/// RPC trait for block utilization methods
pub trait BlockUtilizationNamespace {
/// Returns the gas utilization percentage for a given block
#[method(name = "getBlockUtilization")]
fn get_block_utilization(&self, block_number: BlockNumberOrTag) -> RpcResult<f64>;
}
What's happening:
#[rpc(server, namespace = "eth")]
- Macro generates JSON-RPC server boilerplatenamespace = "eth"
- Creates method eth_getBlockUtilization (namespace + method name)BlockNumberOrTag
- Standard Ethereum type for specifying blocks (latest, earliest, number)RpcResult<f64>
- Standardized error handling for RPC responses returning utilization percentage
Implementation Structure
/// Implementation of block utilization RPC methods
#[derive(Debug)]
pub struct BlockUtilizationImpl<Provider> {
provider: Provider,
}
impl<Provider> BlockUtilizationImpl<Provider>
where
Provider: BlockReaderIdExt + HeaderProvider + Clone + Unpin + 'static,
{
/// Creates a new BlockUtilizationImpl instance
pub fn new(provider: Provider) -> Self {
Self { provider }
}
}
What's happening:
- Generic
Provider
type allows flexibility in data source implementation - Trait bounds ensure provider supports block reading and header access
Clone
+Unpin
+static
requirements for async RPC handling
Core Logic Implementation
impl<Provider> BlockUtilizationNamespaceServer for BlockUtilizationImpl<Provider>
where
Provider: BlockReaderIdExt + HeaderProvider + Clone + Unpin + 'static,
Provider::Header: Into<Header> + Clone,
{
fn get_block_utilization(&self, block_number: BlockNumberOrTag) -> RpcResult<f64> {
// Convert BlockNumberOrTag to actual block number
let block_num = match block_number {
BlockNumberOrTag::Number(num) => num,
BlockNumberOrTag::Latest => {
match self.provider.best_block_number() {
Ok(num) => num,
Err(e) => return Err(ErrorObjectOwned::owned(
-32000,
format!("Failed to get latest block number: {}", e),
None::<()>,
)),
}
}
BlockNumberOrTag::Earliest => 0,
BlockNumberOrTag::Pending => {
return Err(ErrorObjectOwned::owned(
-32000,
"Pending block not supported for utilization calculation",
None::<()>,
));
}
_ => return Err(ErrorObjectOwned::owned(
-32000,
"Unsupported block number tag",
None::<()>,
)),
};
// Get the header using HeaderProvider
let header = match self.provider.header_by_number(block_num) {
Ok(Some(header)) => header,
Ok(None) => return Err(ErrorObjectOwned::owned(
-32000,
"Block header not found",
None::<()>,
)),
Err(e) => return Err(ErrorObjectOwned::owned(
-32000,
format!("Provider error: {}", e),
None::<()>,
)),
};
// Convert to Header type and access fields directly
let header: Header = header.into();
let gas_limit = header.gas_limit;
let gas_used = header.gas_used;
if gas_limit == 0 {
return Ok(0.0);
}
let utilization = (gas_used as f64 / gas_limit as f64) * 100.0;
let utilization = (utilization * 100.0).round() / 100.0;
Ok(utilization)
}
}
What's happening:
- Block number resolution handles different tag types (latest, earliest, specific block number)
- Header retrieval with comprehensive error handling for missing blocks
- Utilization calculation:
(gas_used / gas_limit) * 100
- Result rounded to 2 decimal places for consistent formatting
- Returns 0.0 for blocks with zero gas limit (edge case handling)
Now that we have a better understanding of the code, let's get into the project set up.
Prerequisites Check
First, for a sanity check, we'll verify our environment, check that our node is running and that it is synced. Note that you may need to update the cURL request to point to a different PORT if yours is not running on localhost:8545
.
# Check Rust version (need 1.86+)
rustc --version
# Check if you have a running Reth node
systemctl status reth.service
# Verify your node is synced
curl -X POST -H "Content-Type: application/json" \
--data '{"jsonrpc":"2.0","method":"eth_syncing","params":[],"id":1}' \
localhost:8545
Verify the node is synced before continuing to ensure we can fetch the latest block.
Setting Up the Reth Project
We'll modify an existing Reth installation to add our custom RPC methods. Note that you may need to update your PATH
where needed.
- First, navigate to your Reth directory:
cd /data/reth
Important: Adjust path to your Reth installation
- Then, backup your current main.rs:
cp bin/reth/src/main.rs bin/reth/src/main.rs.backup
Remember to save the file.
Implementing Block Utilization RPC Methods
Now let's implement our custom RPC methods that provide our block utilization metric.
Replace the contents of bin/reth/src/main.rs
with the code below that we went over earlier in the guide:
use clap::Parser;
use jsonrpsee::{core::RpcResult, proc_macros::rpc, types::ErrorObjectOwned};
use reth_ethereum::{
cli::{chainspec::EthereumChainSpecParser, interface::Cli},
node::EthereumNode,
};
use alloy_rpc_types::BlockNumberOrTag;
use reth_provider::{BlockReaderIdExt, HeaderProvider};
use reth_primitives::Header;
fn main() {
Cli::<EthereumChainSpecParser, RethCliExtended>::parse()
.run(|builder, args| async move {
let handle = builder
.node(EthereumNode::default())
.extend_rpc_modules(move |ctx| {
if !args.extended_eth_namespace {
return Ok(());
}
let provider = ctx.provider().clone();
let ext = BlockUtilizationImpl::new(provider);
ctx.modules.merge_configured(ext.into_rpc())?;
println!("Extended ETH namespace enabled with eth_getBlockUtilization");
Ok(())
})
.launch()
.await?;
handle.wait_for_node_exit().await
})
.unwrap();
}
/// Custom CLI args extension that adds the extended namespace flag
#[derive(Debug, Clone, Copy, Default, clap::Args)]
struct RethCliExtended {
/// CLI flag to enable the extended ETH namespace
#[arg(long)]
pub extended_eth_namespace: bool,
}
/// RPC trait defining our block utilization methods
#[cfg_attr(not(test), rpc(server, namespace = "eth"))]
#[cfg_attr(test, rpc(server, client, namespace = "eth"))]
pub trait BlockUtilizationNamespace {
/// Get block utilization as percentage (gas_used / gas_limit * 100)
#[method(name = "getBlockUtilization")]
fn get_block_utilization(&self, block_number: BlockNumberOrTag) -> RpcResult<f64>;
}
/// Implementation struct that holds the blockchain data provider
pub struct BlockUtilizationImpl<Provider> {
provider: Provider,
}
impl<Provider> BlockUtilizationImpl<Provider>
where
Provider: BlockReaderIdExt + HeaderProvider + Clone + Unpin + 'static,
Provider::Header: Into<Header> + Clone,
{
pub fn new(provider: Provider) -> Self {
Self { provider }
}
}
/// Implementation of our custom RPC methods
impl<Provider> BlockUtilizationNamespaceServer for BlockUtilizationImpl<Provider>
where
Provider: BlockReaderIdExt + HeaderProvider + Clone + Unpin + 'static,
Provider::Header: Into<Header> + Clone,
{
fn get_block_utilization(&self, block_number: BlockNumberOrTag) -> RpcResult<f64> {
// Convert BlockNumberOrTag to actual block number
let block_num = match block_number {
BlockNumberOrTag::Number(num) => num,
BlockNumberOrTag::Latest => {
match self.provider.best_block_number() {
Ok(num) => num,
Err(e) => return Err(ErrorObjectOwned::owned(
-32000,
format!("Failed to get latest block number: {}", e),
None::<()>,
)),
}
}
BlockNumberOrTag::Earliest => 0,
BlockNumberOrTag::Pending => {
return Err(ErrorObjectOwned::owned(
-32000,
"Pending block not supported for utilization calculation",
None::<()>,
));
}
_ => return Err(ErrorObjectOwned::owned(
-32000,
"Unsupported block number tag",
None::<()>,
)),
};
// Get the block header using HeaderProvider
let header = match self.provider.header_by_number(block_num) {
Ok(Some(header)) => header,
Ok(None) => return Err(ErrorObjectOwned::owned(
-32000,
"Block header not found",
None::<()>,
)),
Err(e) => return Err(ErrorObjectOwned::owned(
-32000,
format!("Provider error: {}", e),
None::<()>,
)),
};
// Convert to Header type and access gas fields
let header: Header = header.into();
let gas_limit = header.gas_limit;
let gas_used = header.gas_used;
if gas_limit == 0 {
return Ok(0.0);
}
let utilization = (gas_used as f64 / gas_limit as f64) * 100.0;
let utilization = (utilization * 100.0).round() / 100.0;
Ok(utilization)
}
}
Remember to save the file. Now let's move on to compiling the code.
Building the Custom RPC Methods
- Compile your custom RPC methods:
source ~/.cargo/env
cd /data/reth
cargo check --bin reth
If you encounter compilation errors, verify all imports are correct and dependencies are properly configured in Cargo.toml
.
- Build in release mode:
cargo build --release --bin reth
Note: The initial build may take 10-15 minutes as it compiles the entire Reth codebase with your custom RPC method.
Running the Custom RPC Method
Your Reth node requires the --extended-eth-namespace
command-line flag to be enabled in order to use the custom RPC method. This allows you to run the same binary with or without the extended functionality.
To do this, start Reth with extended eth-namespace flag:
cd /path/to/reth
./target/release/reth node \
--chain <your-chain-name> \
--datadir /path/to/data/<your-chain-name> \
--http --http.addr 0.0.0.0 --http.port 8545 \
--authrpc.addr 127.0.0.1 --authrpc.port 8551 \
--authrpc.jwtsecret /path/to/jwtsecret \
--full \
--extended-eth-namespace
Remember to update the /path/to/
to your correct path.
Check to make sure your node hasn't ran into any initialization errors and continues to synced with the chain tip.
Systemd Service Configuration
Usually you run nodes via Systemd so you don't need to always keep it running in terminal. To do this, let's first stop our node and then update the ExecStart
command to include the extended namespace.
sudo systemctl stop reth.service
sudo nano /etc/systemd/system/reth.service
The full code for our Reth service can be found here.
Update the ExecStart line to include the extended namespace flag:
ExecStart=/path/to/reth/target/release/reth node \
--chain <your-chain-name> \
--datadir /path/to/data/<your-chain-name> \
--http --http.addr 0.0.0.0 --http.port 8545 \
--authrpc.addr 127.0.0.1 --authrpc.port 8551 \
--authrpc.jwtsecret /path/to/jwtsecret \
--full \
--extended-eth-namespace
Where:
/path/to/reth
: folder where the Reth binary is built<your-chain-name>
:your chain (e.g., hoodi)/path/to/data/...
: directory for node data/path/to/jwtsecret
: path to your JWT secret file
Remember to save the file.
Then, restart the service:
sudo systemctl daemon-reload
sudo systemctl start reth.service
sudo journalctl -u reth.service -f
Now that our method is added, let's test it!
Testing Your Custom RPC Methods
Let's verify our custom RPC method is working correctly with current and historical block data.
- Test with latest block:
curl -X POST -H "Content-Type: application/json" \
--data '{"jsonrpc":"2.0","method":"eth_getBlockUtilization","params":["latest"],"id":1}' \
localhost:8545
Response:
{"jsonrpc":"2.0","id":1,"result":66.45}
At the time we tested this was block 1247242.
- Test with specific block (e.g., 1247240)
curl -X POST -H "Content-Type: application/json" \
--data '{"jsonrpc":"2.0","method":"eth_getBlockUtilization","params":["0x130808"],"id":2}' \
localhost:8545
Response:
{"jsonrpc":"2.0","id":1,"result":3.34}
- Now, let's test error handling:
curl -X POST -H "Content-Type: application/json" \
--data '{"jsonrpc":"2.0","method":"eth_getBlockUtilization","params":["0x99999999"],"id":3}' \
localhost:8545
Response:
{"jsonrpc":"2.0","id":3,"error":{"code":-32000,"message":"Block header not found"}}
To give us another sense of security, let's do a sanity check by cross-checking our results with a block explorer and manually via calling eth_getBlockByNumber
.
Let's test block 117500
. Etherscan shows 98.61%:
Now, let's call:
curl -s -X POST -H "Content-Type: application/json" \
--data '{"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["0x1cafc",false],"id":1}' \
http://127.0.0.1:8545
With the response, we can manually do a calculation by extracting the hex values from gasUsed
and gasLimit
, converting them to integers and dividing them.
..."gasLimit":"0x2243e7d","gasUsed":"0x21c9f10"...
For example:
This confirms that our custom eth_getBlockUtilization
RPC method is working correctly and providing accurate gas utilization percentages.
Earn Revenue via the QuickNode Marketplace
If you see an opportunity to build a useful RPC method, you could put it on the QuickNode Marketplace and earn revenue. Check out our technical guides and courses to build your own Marketplace add-on.
The custom RPC method you want to build may already be created! This means less time developing and maintaining infrastructure for you. Check out the QuickNode Marketplace to see a list of available add-ons across 70+ chains.
Final Thoughts
Congrats! You've successfully learned how to build, deploy, and manage custom RPC methods in Reth. This powerful pattern enables you to extend Ethereum's JSON-RPC API with specialized functionality without needing to maintain a forked codebase. Continue building on the knowledge you've learned by trying the next steps:
- Explore ideas that can be useful DeFi and NFT ecosystems such as building a RPC method that can swap multiple tokens or NFTs at once, check liquidity across multiple DEXs, or existing methods you currently use but could add a bit of improvement
- Join the Reth Discord community for advanced discussion
- Contribute your custom RPC methods back to the Reth ecosystem
Subscribe to our QuickNode Newsletter for more articles and guides on Ethereum infrastructure. Connect with us on Twitter or chat in our Discord community!
We ❤️ Feedback!
Let us know if you have any feedback or requests for new topics. We'd love to hear from you.