ETH Full Node and Pathfinder

Based on the optimization methods, our node processing speed is significantly faster than Alchemy's: we can handle 58.20 requests per second, while Alchemy only manages 37.88 requests per second.

ETH Full Node and Pathfinder

When working on projects, we frequently come across situations where we need to input the ETH RPC or retrieve information about a particular token locally. For instance, in the code snippet below, our objective is to obtain the decimal information of a contract:

package main

import (
	"fmt"

	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/ethclient"
	"github.com/reddio-com/standard-contracts/erc20"
)

func main() {
	client, err := ethclient.Dial("<https://goerli.infura.io/v3/SOME_TOKEN_HERE>")
	if err != nil {
		panic(err)
	}

	contractAddress := common.HexToAddress("0x07865c6E87B9F70255377e024ace6630C1Eaa37F")

	// Query baseuri
	erc20, err := erc20.NewErc20(contractAddress, client)
	if err != nil {
		panic(err)
	}

	// Query BaseURI
	decimals, err := erc20.Decimals(nil)
	if err != nil {
		panic(err)
	}
	fmt.Println("Decimals: ", decimals)
	totalSupply, err := erc20.TotalSupply(nil)
	if err != nil {
		panic(err)
	}
	fmt.Println("TotalSupply: ", totalSupply)

}

The ethclient.Dial("<https://goerli.infura.io/v3/SOME_TOKEN_HERE>") is an ETH RPC Endpoint. It allows us to interact with events on the Ethereum chain through HTTP.

Infura, developed by Consensys, is an infrastructure service that simplifies Ethereum blockchain interaction for developers. By using Infura, developers can avoid the complexity and cost of setting up and maintaining their own Ethereum full node. Infura offers a range of APIs that enable developers to access Ethereum's data and functionality via remote nodes. This means they don't have to run their own Ethereum node but can rely on Infura's nodes for communication with the blockchain. As a result, developers can focus on building core functionalities for their applications without investing significant time and effort in node operations and maintenance. However, Infura is a paid service with the following pricing plans:

If your service requires a lot of interaction with the blockchain or needs to analyze historical data, using Infura may be very expensive. In this case, you can consider setting up your own ETH full node so that you can retrieve the required data from a local node.

Setting up your own Ethereum full node has several advantages. Firstly, it provides higher data privacy and security as developers have complete control over the node's operating environment and access permissions. Secondly, self-built nodes allow developers to customize the configuration of the node to meet specific requirements, such as adjusting synchronization speed or storage capacity. Additionally, in some cases, self-built nodes may be more reliable as developers do not rely on the stability of third-party services.

However, there are also challenges associated with self-built nodes including hardware and network requirements, time and resources needed for node synchronization, as well as operational and maintenance costs. In comparison, Infura offers a more convenient and flexible option that allows developers to quickly launch their projects and focus on core development tasks without having to worry too much about managing underlying nodes. Taking all factors into consideration, developers can choose between using Infura or setting up their own nodes based on their specific needs and priorities when interacting with the Ethereum blockchain.

Geth is an execution client. Historically, having just one execution client was sufficient to run a full Ethereum node. However, since Ethereum switched from Proof of Work (PoW) to Proof of Stake (PoS) consensus mechanism, Geth needs to work in conjunction with another software called "consensus client".

From https://geth.ethereum.org/docs/getting-started/consensus-clients, we can find that the official recommendation for consensus clients is as follows:

The launch of the consensus client requires the execution client as support. In this case, we have chosen Prysm as the consensus client because we are using Geth, which is an official execution client.

At Reddio, all of our services are containerized, which reduces the hassle of dealing with configuration environments. Therefore, in this article, all examples will be written using docker-compose.

Firstly, prepare a machine with a sufficiently large disk (>900G SSD) and enough memory (>32G). Create an empty directory and then create a docker-compose.yml file with the following content:

version: "3"
services:
  geth:
    image: ethereum/client-go:v1.12.2
    restart: unless-stopped
    ports:
      - 30303:30303
      - 30303:30303/udp
      - 127.0.0.1:8545:8545
      - 127.0.0.1:8546:8546
      - 127.0.0.1:8551:8551
    volumes:
      - ./data:/root/.ethereum
    healthcheck:
      test: [ "CMD-SHELL", "geth attach --exec eth.blockNumber" ]
      interval: 10s
      timeout: 5s
      retries: 5
    command:
      - --http
      - --cache=8192
      - --http.api=eth,net,web3,engine,admin
      - --http.addr=0.0.0.0
      - --http.vhosts=*
      - --http.corsdomain=*
      - --maxpeers=200
      - --ws
      - --ws.origins=*
      - --ws.addr=0.0.0.0
      - --ws.api=eth,net,web3
      - --graphql
      - --graphql.corsdomain=*
      - --graphql.vhosts=*
      - --authrpc.addr=0.0.0.0
      - --authrpc.jwtsecret=/root/.ethereum/jwt.hex
      - --authrpc.vhosts=*
      - --authrpc.port=8551
      - --txlookuplimit=0

You can start the service using:

docker-compose up -d

At this point, Geth should have started up successfully.

INFO [08-17|06:43:02.867] Starting Geth on Ethereum mainnet... 
INFO [08-17|06:43:02.870] Maximum peer count                       ETH=200 LES=0 total=200
INFO [08-17|06:43:02.872] Smartcard socket not found, disabling    err="stat /run/pcscd/pcscd.comm: no such file or directory"
WARN [08-17|06:43:02.874] Sanitizing cache to Go's GC limits       provided=8192 updated=2579
INFO [08-17|06:43:02.875] Set global gas cap                       cap=50,000,000
INFO [08-17|06:43:02.877] Initializing the KZG library             backend=gokzg
INFO [08-17|06:43:03.005] Allocated trie memory caches             clean=386.00MiB dirty=644.00MiB
INFO [08-17|06:43:03.006] Defaulting to pebble as the backing database 
INFO [08-17|06:43:03.006] Allocated cache and file handles         database=/root/.ethereum/geth/chaindata cache=1.26GiB handles=524,288
INFO [08-17|06:43:03.022] Opened ancient database                  database=/root/.ethereum/geth/chaindata/ancient/chain readonly=false
INFO [08-17|06:43:03.023] Initialising Ethereum protocol           network=1 dbversion=<nil>
INFO [08-17|06:43:03.024] Writing default main-net genesis block 
INFO [08-17|06:43:03.664] Persisted trie from memory database      nodes=12356 size=1.79MiB time=95.551853ms gcnodes=0 gcsize=0.00B gctime=0s livenodes=0 livesize=0.00B
INFO [08-17|06:43:03.718]  
INFO [08-17|06:43:03.718] --------------------------------------------------------------------------------------------------------------------------------------------------------- 
INFO [08-17|06:43:03.719] Chain ID:  1 (mainnet) 
INFO [08-17|06:43:03.719] Consensus: Beacon (proof-of-stake), merged from Ethash (proof-of-work) 
INFO [08-17|06:43:03.719]  
INFO [08-17|06:43:03.719] Pre-Merge hard forks (block based): 
INFO [08-17|06:43:03.719]  - Homestead:                   #1150000  (<https://github.com/ethereum/execution-specs/blob/master/network-upgrades/mainnet-upgrades/homestead.md>) 
INFO [08-17|06:43:03.719]  - DAO Fork:                    #1920000  (<https://github.com/ethereum/execution-specs/blob/master/network-upgrades/mainnet-upgrades/dao-fork.md>) 
INFO [08-17|06:43:03.719]  - Tangerine Whistle (EIP 150): #2463000  (<https://github.com/ethereum/execution-specs/blob/master/network-upgrades/mainnet-upgrades/tangerine-whistle.md>) 
INFO [08-17|06:43:03.719]  - Spurious Dragon/1 (EIP 155): #2675000  (<https://github.com/ethereum/execution-specs/blob/master/network-upgrades/mainnet-upgrades/spurious-dragon.md>) 
INFO [08-17|06:43:03.719]  - Spurious Dragon/2 (EIP 158): #2675000  (<https://github.com/ethereum/execution-specs/blob/master/network-upgrades/mainnet-upgrades/spurious-dragon.md>) 
INFO [08-17|06:43:03.719]  - Byzantium:                   #4370000  (<https://github.com/ethereum/execution-specs/blob/master/network-upgrades/mainnet-upgrades/byzantium.md>) 
INFO [08-17|06:43:03.719]  - Constantinople:              #7280000  (<https://github.com/ethereum/execution-specs/blob/master/network-upgrades/mainnet-upgrades/constantinople.md>) 
INFO [08-17|06:43:03.719]  - Petersburg:                  #7280000  (<https://github.com/ethereum/execution-specs/blob/master/network-upgrades/mainnet-upgrades/petersburg.md>) 
INFO [08-17|06:43:03.719]  - Istanbul:                    #9069000  (<https://github.com/ethereum/execution-specs/blob/master/network-upgrades/mainnet-upgrades/istanbul.md>) 
INFO [08-17|06:43:03.719]  - Muir Glacier:                #9200000  (<https://github.com/ethereum/execution-specs/blob/master/network-upgrades/mainnet-upgrades/muir-glacier.md>) 
INFO [08-17|06:43:03.719]  - Berlin:                      #12244000 (<https://github.com/ethereum/execution-specs/blob/master/network-upgrades/mainnet-upgrades/berlin.md>) 
INFO [08-17|06:43:03.719]  - London:                      #12965000 (<https://github.com/ethereum/execution-specs/blob/master/network-upgrades/mainnet-upgrades/london.md>) 
INFO [08-17|06:43:03.719]  - Arrow Glacier:               #13773000 (<https://github.com/ethereum/execution-specs/blob/master/network-upgrades/mainnet-upgrades/arrow-glacier.md>) 
INFO [08-17|06:43:03.719]  - Gray Glacier:                #15050000 (<https://github.com/ethereum/execution-specs/blob/master/network-upgrades/mainnet-upgrades/gray-glacier.md>) 
INFO [08-17|06:43:03.719]  
INFO [08-17|06:43:03.719] Merge configured: 
INFO [08-17|06:43:03.719]  - Hard-fork specification:    <https://github.com/ethereum/execution-specs/blob/master/network-upgrades/mainnet-upgrades/paris.md> 
INFO [08-17|06:43:03.719]  - Network known to be merged: true 
INFO [08-17|06:43:03.719]  - Total terminal difficulty:  58750000000000000000000 
INFO [08-17|06:43:03.719]  
INFO [08-17|06:43:03.719] Post-Merge hard forks (timestamp based): 
INFO [08-17|06:43:03.719]  - Shanghai:                    @1681338455 (<https://github.com/ethereum/execution-specs/blob/master/network-upgrades/mainnet-upgrades/shanghai.md>) 
INFO [08-17|06:43:03.719]  
INFO [08-17|06:43:03.719] --------------------------------------------------------------------------------------------------------------------------------------------------------- 
INFO [08-17|06:43:03.719]  
INFO [08-17|06:43:03.719] Loaded most recent local block           number=0 hash=d4e567..cb8fa3 td=17,179,869,184 age=54y4mo3w
WARN [08-17|06:43:03.719] Failed to load snapshot                  err="missing or corrupted snapshot"
INFO [08-17|06:43:03.721] Rebuilding state snapshot 
INFO [08-17|06:43:03.722] Resuming state snapshot generation       root=d7f897..0f0544 accounts=0 slots=0 storage=0.00B dangling=0 elapsed=1.221ms
INFO [08-17|06:43:03.722] Regenerated local transaction journal    transactions=0 accounts=0
INFO [08-17|06:43:03.759] Chain post-merge, sync via beacon client 
INFO [08-17|06:43:03.760] Gasprice oracle is ignoring threshold set threshold=2
WARN [08-17|06:43:03.761] Error reading unclean shutdown markers   error="pebble: not found"
WARN [08-17|06:43:03.768] Engine API enabled                       protocol=eth
INFO [08-17|06:43:03.768] Starting peer-to-peer node               instance=Geth/v1.12.2-stable-bed84606/linux-arm64/go1.20.7
INFO [08-17|06:43:03.782] New local node record                    seq=1,692,254,583,780 id=23d7280689379076 ip=127.0.0.1 udp=30303 tcp=30303
INFO [08-17|06:43:03.790] Started P2P networking                   self=enode://7b35cab1bcc1258a96814b204223ce90ee71ff7df44c3bd314e0cda0677e93a80bb5fd0efc3f166c28e4dde41771d0195eecd910f532fe3fb98ee14728046e0f@127.0.0.1:30303
INFO [08-17|06:43:03.793] IPC endpoint opened                      url=/root/.ethereum/geth.ipc
INFO [08-17|06:43:03.795] Generated JWT secret                     path=/root/.ethereum/jwt.hex
INFO [08-17|06:43:03.797] HTTP server started                      endpoint=[::]:8545 auth=false prefix= cors=* vhosts=*
INFO [08-17|06:43:03.797] GraphQL enabled                          url=http://[::]:8545/graphql
INFO [08-17|06:43:03.798] GraphQL UI enabled                       url=http://[::]:8545/graphql/ui
INFO [08-17|06:43:03.798] WebSocket enabled                        url=ws://[::]:8546
INFO [08-17|06:43:03.798] WebSocket enabled                        url=ws://[::]:8551
INFO [08-17|06:43:03.798] HTTP server started                      endpoint=[::]:8551 auth=true  prefix= cors=localhost vhosts=*
INFO [08-17|06:43:03.835] Generated state snapshot                 accounts=8893 slots=0 storage=409.64KiB dangling=0 elapsed=114.105ms
INFO [08-17|06:43:09.120] New local node record                    seq=1,692,254,583,781 id=23d7280689379076 ip=49.12.77.197 udp=30303 tcp=30303
INFO [08-17|06:43:14.797] Looking for peers                        peercount=1 tried=19 static=0
INFO [08-17|06:43:25.466] Looking for peers                        peercount=1 tried=34 static=0
INFO [08-17|06:43:35.466] Looking for peers                        peercount=1 tried=33 static=0
WARN [08-17|06:43:38.770] Post-merge network, but no beacon client seen. Please launch one to follow the chain!

The following entry can be found in the log:

WARN [08-17|06:43:38.770] Post-merge network, but no beacon client seen. Please launch one to follow the chain!

This is as mentioned at the beginning of the article, we need to start a "consensus client", and here we choose Prysm. Prysm needs to specify Geth's Endpoint for startup, so let's modify the docker-compose.yml file accordingly with the following content:

version: "3"
services:
  geth:
    image: ethereum/client-go:v1.12.2
    restart: unless-stopped
    ports:
      - 30303:30303
      - 30303:30303/udp
      - 127.0.0.1:8545:8545
      - 127.0.0.1:8546:8546
      - 127.0.0.1:8551:8551
    volumes:
      - ./data:/root/.ethereum
    healthcheck:
      test: [ "CMD-SHELL", "geth attach --exec eth.blockNumber" ]
      interval: 10s
      timeout: 5s
      retries: 5
    command:
      - --http
      - --cache=8192
      - --http.api=eth,net,web3,engine,admin
      - --http.addr=0.0.0.0
      - --http.vhosts=*
      - --http.corsdomain=*
      - --maxpeers=200
      - --ws
      - --ws.origins=*
      - --ws.addr=0.0.0.0
      - --ws.api=eth,net,web3
      - --graphql
      - --graphql.corsdomain=*
      - --graphql.vhosts=*
      - --authrpc.addr=0.0.0.0
      - --authrpc.jwtsecret=/root/.ethereum/jwt.hex
      - --authrpc.vhosts=*
      - --authrpc.port=8551
      - --txlookuplimit=0

  prysm:
    image: gcr.io/prysmaticlabs/prysm/beacon-chain
    pull_policy: always
    container_name: beacon
    restart: unless-stopped
    stop_grace_period: 2m
    volumes:
      - ./prysm_data:/data
      - ./data:/geth
    depends_on:
      geth:
        condition: service_healthy
    ports:
      - 127.0.0.1:4000:4000
      - 127.0.0.1:3500:3500
    command:
      - --accept-terms-of-use
      - --datadir=/data
      - --disable-monitoring
      - --rpc-host=0.0.0.0
      - --execution-endpoint=http://geth:8551
      - --jwt-secret=/geth/jwt.hex
      - --rpc-host=0.0.0.0
      - --rpc-port=4000
      - --grpc-gateway-corsdomain=*
      - --grpc-gateway-host=0.0.0.0
      - --grpc-gateway-port=3500
      - --checkpoint-sync-url=https://mainnet-checkpoint-sync.attestant.io/
      - --genesis-beacon-api-url=https://mainnet-checkpoint-sync.attestant.io/

--checkpoint-sync-url=https://mainnet-checkpoint-sync.attestant.io/ are part of a mechanism called checkpoint-sync. You can find detailed information about it here: https://docs.prylabs.network/docs/prysm-usage/checkpoint-sync. In simple terms, it allows syncing information from a node that is already synchronized, reducing the time required for syncing from scratch.

At this point, we can already see from Geth's logs that we are successfully synchronizing data. Next, let's take a look at Pathfinder.

Pathfinder

https://github.com/eqlabs/pathfinder

Pathfinder is a Starknet full node written in Rust. Follow the “Running with Docker” instructions on GitHub:

# Ensure the directory has been created before invoking docker
mkdir -p $HOME/pathfinder
# Start the pathfinder container instance running in the background
sudo docker run \\
  --name pathfinder \\
  --restart unless-stopped \\
  --detach \\
  -p 9545:9545 \\
  --user "$(id -u):$(id -g)" \\
  -e RUST_LOG=info \\
  -e PATHFINDER_ETHEREUM_API_URL="<https://goerli.infura.io/v3/><project-id>" \\
  -v $HOME/pathfinder:/usr/share/pathfinder/data \\
  eqlabs/pathfinder

For PATHFINDER_ETHEREUM_API_URL , since we already have our own full node, we should directly use our Geth's Endpoint to interact with Pathfinder and the chain. Let's continue integrating the docker-compose.yml file above, with the following content:

version: "3"
services:
  geth:
    image: ethereum/client-go:v1.12.2
    restart: unless-stopped
    ports:
      - 30303:30303
      - 30303:30303/udp
      - 127.0.0.1:8545:8545
      - 127.0.0.1:8546:8546
      - 127.0.0.1:8551:8551
    volumes:
      - ./data:/root/.ethereum
    healthcheck:
      test: [ "CMD-SHELL", "geth attach --exec eth.blockNumber" ]
      interval: 10s
      timeout: 5s
      retries: 5
    command:
      - --http
      - --cache=8192
      - --http.api=eth,net,web3,engine,admin
      - --http.addr=0.0.0.0
      - --http.vhosts=*
      - --http.corsdomain=*
      - --maxpeers=200
      - --ws
      - --ws.origins=*
      - --ws.addr=0.0.0.0
      - --ws.api=eth,net,web3
      - --graphql
      - --graphql.corsdomain=*
      - --graphql.vhosts=*
      - --authrpc.addr=0.0.0.0
      - --authrpc.jwtsecret=/root/.ethereum/jwt.hex
      - --authrpc.vhosts=*
      - --authrpc.port=8551
      - --txlookuplimit=0

  pathfinder:
    image: eqlabs/pathfinder:latest
    restart: always
    ports:
      - '127.0.0.1:9545:9545'
    environment:
      PATHFINDER_ETHEREUM_API_URL: '<http://geth:8545>'
    volumes:
      - ./pathfinder:/usr/share/pathfinder/data

  prysm:
    image: gcr.io/prysmaticlabs/prysm/beacon-chain
    container_name: beacon
    restart: unless-stopped
    stop_grace_period: 2m
    volumes:
      - ./prysm_data:/data
      - ./data:/geth
    depends_on:
      geth:
        condition: service_healthy
    ports:
      - 127.0.0.1:4000:4000
      - 127.0.0.1:3500:3500
    command:
      - --accept-terms-of-use
      - --datadir=/data
      - --disable-monitoring
      - --rpc-host=0.0.0.0
      - --execution-endpoint=http://geth:8551
      - --jwt-secret=/geth/jwt.hex
      - --rpc-host=0.0.0.0
      - --rpc-port=4000
      - --grpc-gateway-corsdomain=*
      - --grpc-gateway-host=0.0.0.0
      - --grpc-gateway-port=3500
      - --checkpoint-sync-url=https://mainnet-checkpoint-sync.attestant.io/
      - --genesis-beacon-api-url=https://mainnet-checkpoint-sync.attestant.io/

Within the same docker-compose, all containers can interact directly with each other using container names. Therefore, our syntax for environment variables is:

PATHFINDER_ETHEREUM_API_URL: '<http://geth:8545>'

After the startup is complete, we can interact using the API interface specification found here: https://github.com/eqlabs/pathfinder/blob/main/doc/rpc/pathfinder_rpc_api.json

After setting up our own full node, we can use various tools to test and compare it with nodes from other service providers. One interesting tool for this purpose is: https://github.com/shazow/ethspam

  • ethspam generates an infinite stream of realistic read-only Ethereum JSONRPC queries, anchored around the latest block with some amount of random jitter. The latest state is updated every 15 seconds, so it can run continuously without becoming stale.

In our own practice, the test results of our node are as follows:

./ethspam | ./versus --stop-after=1000 --concurrency=5 "<https://geth.reddio.network/>"
Endpoints:

0. "<https://geth.reddio.network/>"

   Requests:   58.20 per second
   Timing:     0.0859s avg, 0.0239s min, 4.0492s max
               0.2834s standard deviation

   Percentiles:
     25% in 0.0374s
     50% in 0.0408s
     75% in 0.0467s
     90% in 0.0612s
     95% in 0.1085s
     99% in 1.7751s

   Errors: 0.00%

** Summary for 1 endpoints:
   Completed:  1000 results with 1000 total requests
   Timing:     85.904856ms request avg, 17.86287743s total run time
   Errors:     0 (0.00%)
   Mismatched: 0

As a comparison, we compared our results with nodes provided by Alchemy:

/ethspam | ./versus --stop-after=1000 --concurrency=5 "<https://eth-goerli.g.alchemy.com/v2/xxxxxxxxxx>"
Endpoints:

0. "<https://eth-goerli.g.alchemy.com/v2/xxxxxxxxxx>"

   Requests:   37.88 per second, 8.34 per second for errors
   Timing:     0.1320s avg, 0.1049s min, 0.5299s max
               0.0332s standard deviation

   Percentiles:
     25% in 0.1192s
     50% in 0.1255s
     75% in 0.1361s
     90% in 0.1466s
     95% in 0.1555s
     99% in 0.3645s

   Errors: 9.70%
     97 × "bad status code: 429"

** Summary for 1 endpoints:
   Completed:  1000 results with 1000 total requests
   Timing:     131.98702ms request avg, 27.179966564s total run time
   Errors:     97 (9.70%)
   Mismatched: 0

Based on the results, our node processing speed is significantly faster than Alchemy's: we can handle 58.20 requests per second, while Alchemy only manages 37.88 requests per second.