Ethereum light client
Introduction
Ethereum is a decentralized platform that runs smart contracts, which are applications that run exactly as programmed without any possibility of downtime, censorship, fraud or third-party interference. These applications are run on a custom built blockchain, an enormously powerful shared global infrastructure that can move value around and represent the ownership of property. Ethereum client software is a type of software that allows users to connect to the Ethereum network and interact with the blockchain. This can include various functions such as creating and managing smart contracts, sending and receiving transactions, and exploring the blockchain data. Some examples of Ethereum client software include Geth, Parity, and Mist. Ethereum recently moved to Proof-of-Stake (PoS) network which allows it to create clients with novel functionalities. These clients can be run on a variety of devices, including desktop computers, laptops, and mobile devices. The goal of the paper is to deconstruct what a client software is and how once can build or customize an existing client. Components of a blockchain client software
Cryptography
- Elliptic curve cryptography (ecdsa) for signing with a private key and sending it to the network. Public key is the your identity
Networking
- DEVp2p, libp2p
Execution
- Every block execution receives a new block then VM state transition function is applied and outputs a new state. Gas (cost) is used for transactions.
Incentives
- Interest of user and the community
Light client provides efficient and trustless access without having to run a full node and does not require any storage and connects within seconds and and have execution and consensus layer.
Ethereum 2.0
There are significant change in Ethereum 2.0 beside moving to PoS.
Libp2p (more functionality),
eWASM ( Supports different platforms like browser, phone etc)
BLS 12-381 ( previously used secp256k1) for signature aggregation
PoS ( consensus ) network
Eth 2.0 architecture
Beacon chain:
Parallel execution happens in different shards as a process where each shard has its own memory, state, execution engine. Each runs completely separate from each other. Beacon chain sits in the middle and coordinates among all the shards and identifies what is the consensus of different shards. The validators are in a beacon chain that goes to shards every 6 seconds to determine what is the largest chain based on protocol rules and sign that information on the beacon chain.
Ethereum Light Clients:
Ethereum light clients are Ethereum clients that do not need to download the entire Ethereum blockchain in order to operate. Instead, they rely on other full Ethereum nodes to provide them with the necessary data to validate transactions and blocks. This makes them significantly lighter and faster to operate than a full Ethereum node, which has to store and process the entire blockchain.
Ethereum light clients are particularly useful for users who do not have the resources (e.g. disk space, memory, bandwidth) to run a full Ethereum node, or who do not need the full functionality of a full node. They can be used to interact with the Ethereum network in a variety of ways, including sending and receiving transactions, accessing smart contracts, and more.
Light clients are useful for users who only need to send and receive transactions, rather than participating in mining or running smart contracts. They are also useful for users who want to interact with the Ethereum network from a device with limited resources, such as a smartphone. Eth2 which used PoS for consensus allowed building novel light clients and there are several different Ethereum light clients available, including such as Helios, Nimbus etc. Each light client has its own unique features and trade-offs, so it is important to choose one that meets your needs and preferences.
Getting Started with Helios: We will discuss Helios codebase and how to get it up and running with metamask.
Prerequisite: https://a16zcrypto.com/building-helios-ethereum-light-client/ https://github.com/a16z/helios
Sending 1st transaction through helios: Step 1. Git clone the helios open source codebase to your local machine.
Step 2. Get your rpc url from Alchemy or Infura. I have used an Alchemy rpc node for this and used goerli test network. Open an Alchemy account if you don't have any, It's free.
Step 3. Install helios by using below command, curl https://raw.githubusercontent.com/a16z/helios/master/heliosup/install | bash
Step 4. (optional) In the example folder there are some test scripts that you can run to ensure the helios package is working. e.g. cargo run --example client
Step 5. Start the client by running ( for goerli ), helios --execution-rpc https://eth-goerli.g.alchemy.com/v2/<API_KEY> --consensus-rpc http://testing.prater.beacon-api.nimbus.team -n goerli
This will start the client on http://localhost:8545 . You can change the port using -p in the above command.
Now I will use 2 metamask wallets for the example transaction. Essentially i will send some goerliETH from wallet-1 to wallet-2.
Step 6. The only configuration needed on wallet-1 or the sender is to update the goerli network RPC url to http://localhost:8545 since that's where our helios light client is running. The chain ID remains 5 as its still goerli network.
When a transaction is sent from wallet-1 to wallet-2, it's getting routed through a local client and the transaction is forwarded to Alchemy rpc node. To verify rpc calls sent by helios with the above transactions, you can select the eth_sendRawTransaction
Further you can confirm by searching the transaction id in https://goerli.etherscan.io
Understanding the architecture:
Helios team has provided high-level architecture
Src: https://github.com/a16z/helios/tree/master#architecture
Helios client downloads and proves the contact bytecode, loads it into a local evm, then runs it. Every time it needs access to some piece of state, it fetches and proves that data using state proofs (it actually pre fetches most of it for perf reasons but that's not too important to how it works)
pre-fetch arbitrary eth_calls using getProof. It is possible to prefetch by using eth_createAccessList to estimate what state we need so we can parallelize the proof fetching rather than waiting until its needed.
It works with archive nodes and full nodes. It can fetch state within the last 64 blocks (which is all Helios stores headers for), so as long as your rpc requests aren't using old block tags, you should be good with a normal full node as the execution rpc. It only stores headers that are 64 blocks old but it's possible to increase that number. It doesn't have the full state though, just headers. Any state it needs is fetched by an untrusted rpc and proving against the state root which it does have.
Helios actually works on the Uniswap frontend.
The goal of the execution layer are: Use the block header verified by the consensus layer to provide verified execution layer data through an untrusted execution layer RPC. Provide a locally hosted RPC server, so that users can obtain verified data through this RPC.
The verification process involves,
For eth_getBalance, getCode, getTransactionCount, getStorageAt, etc., we need call eth_getProof on the untrusted RPC to obtain the Merkle proof info, and verify it according to the known stateRoot in the block header.
eth_call and eth_estimateGas can be verified by running the code on a local EVM. 2.1 Call eth_createAccessList to generate a list of contract slots that will be accessed. 2.2 Call eth_getProof for each item in the list to verify the proof 2.3 Load the code and the slots on a local EVM to run the call. Note: The RPC provider should support eth_getProof and eth_createAccessList. Infura doesn't
support eth_createAccessList yet.
Looking at the codebase,
Cli is the highest level element for running the client with different parameters. Node is the brain that gets the transaction info and validates using consensus and executes it. Then rpc uses the node to forward it to external RPC using rpc code.
Next we look at a deeper level how the above transaction went from metamask and what parts of helios code,
RPC methods supported,
Remaining RPC methods, https://github.com/a16z/helios/issues/151
Client::Node.rs —-------------------
This code defines a struct called Node that represents a node in a blockchain system. The Node struct has several fields:
consensus: A ConsensusClient object that is used to communicate with the consensus layer of the blockchain system.
execution: An Arc<ExecutionClient> object that is used to communicate with the execution layer of the blockchain system. The Arc type is a reference-counted pointer, which is used here to allow multiple threads to access the ExecutionClient concurrently.
config: An Arc<Config> object that stores configuration information for the node.
payloads: A BTreeMap that stores ExecutionPayload objects indexed by block number.
finalized_payloads: A BTreeMap that stores ExecutionPayload objects for finalized blocks, indexed by block number.
history_size: An integer representing the maximum number of ExecutionPayload objects that should be stored in the payloads BTreeMap.
The Node struct also has several methods:
new(): A constructor that creates a new Node object. It creates a ConsensusClient and an ExecutionClient using the provided configuration, and initializes the other fields with default values.
sync(): A method that synchronizes the node with the latest state of the blockchain. It calls the sync() method of the consensus field, and then calls the update_payloads() method.
advance(): A method that advances the node to the next block in the blockchain. It calls the advance() method of the consensus field, and then calls the update_payloads() method.
duration_until_next_update(): A method that returns the duration until the next update of the node's state. It calls the duration_until_next_update() method of the consensus field, and converts the returned Duration to a std::time::Duration using the to_std() method.
update_payloads(): A method that updates the payloads and finalized_payloads fields of the Node object with the latest ExecutionPayload objects from the blockchain. It first retrieves the latest and finalized headers from the consensus field, and then uses these to fetch the corresponding ExecutionPayload objects. It then inserts these into the payloads and finalized_payloads maps, and removes any extra elements from these maps if they exceed the history_size.
Some key items,
starts an HTTP server using an async function. The function takes an RpcInner struct as an argument and returns a tuple containing an HttpServerHandle and a SocketAddr (which represents an IP address and port number).
The function begins by creating an HttpServerBuilder and building an HttpServer with it, using the port field of the RpcInner struct to determine the port number. It then creates a new instance of the Methods struct, which is used to store a mapping of RPC method names to method handlers.
Next, the function creates two new instances of the Methods struct, eth_methods and net_methods, by using the into_rpc method of the EthRpcServer and NetRpcServer structs (which are not shown in the code snippet). These methods are then merged into the original methods instance using the merge method.
Finally, the function starts the HTTP server by calling the start method on the server instance, passing it the methods struct as an argument. The function returns a tuple containing the handle of the server and the addr (the local address of the server).
This code appears to define a client for interacting with an Ethereum blockchain via a JSON-RPC interface (e.g. geth). The client is defined as a struct named ExecutionClient, which has a single field rpc of type R: ExecutionRpc, where ExecutionRpc is a trait with associated methods for performing various blockchain operations such as fetching accounts, storage, and code, sending transactions, and fetching logs.
The ExecutionClient struct has several methods, including new, get_account, send_raw_transaction, get_transaction_receipt, and get_logs. The new method creates a new instance of ExecutionClient by calling the new method of the ExecutionRpc trait, passing in a string argument representing the URL of the Ethereum node to connect to. The get_account method fetches the current state of an Ethereum account by its address and block number, returning the account as an Account struct. The send_raw_transaction method sends a raw transaction to the Ethereum blockchain. The get_transaction_receipt method fetches the transaction receipt for a given transaction hash. The get_logs method fetches logs that match a given filter.
The code also defines a constant MAX_SUPPORTED_LOGS_NUMBER and several use statements for external crates such as ethers, eyre, futures, and revm.
This code defines an ExecutionClient struct that provides methods to perform various Ethereum-related tasks using a provided ExecutionRpc trait object. The ExecutionClient struct has a single field, rpc, which is an object that implements the ExecutionRpc trait.
The ExecutionClient struct has a method called get_account which takes an Address, an optional slice of H256 values, and an ExecutionPayload and returns a result containing an Account struct. This method fetches a storage proof for the specified address from the Ethereum blockchain and verifies the proof. If the proof is valid, it returns an Account struct containing information about the specified Ethereum account, including its balance, nonce, code, code hash, storage hash, and storage slots.
The ExecutionClient struct also has a method called send_raw_transaction which takes a raw transaction as a slice of bytes and returns a result containing a TransactionReceipt. This method sends the provided raw transaction to the Ethereum blockchain and returns a receipt containing the transaction's hash and other information.
Finally, the ExecutionClient struct has a method called call which takes an which takes an
- The code appears to define an ExecutionClient struct that has a single field rpc of a generic type R which must implement the ExecutionRpc trait. The ExecutionClient has two methods: get_account and send_raw_transaction.
The get_account method takes an Ethereum address, an optional slice of hashes, an execution payload, and returns an Account struct. It first fetches a "proof" of the account's state from the rpc field and verifies the proof's validity. If the proof is valid, it fetches the account's storage slots and code and constructs an Account struct with this information.
The send_raw_transaction method takes a raw transaction as a hex string, an execution payload, and returns a TransactionReceipt. It first decodes the hex string into a Transaction struct and sends the transaction to the Ethereum network through the rpc field. It then waits for the transaction to be mined and returns the transaction receipt
This code appears to define an ExecutionClient struct that is used to interact with an Ethereum node using JSON-RPC. It has one field, rpc, which is an implementation of the ExecutionRPC trait. The ExecutionClient has several methods, including get_account, which retrieves information about an Ethereum account, and send_raw_transaction, which broadcasts a raw Ethereum transaction to the network. The ExecutionClient also has a new method which can be used to create a new instance of the struct with a provided JSON-RPC endpoint.
This code defines an ExecutionClient struct that is used to interact with an Ethereum node over an RPC interface. It has methods to fetch information about an Ethereum account (e.g., balance, code, storage), send raw transactions, and get transaction receipts. It also has a method to fetch logs for a given filter.
The ExecutionClient struct has a single field rpc, which is an implementation of the ExecutionRpc trait. This trait defines the interface for interacting with an Ethereum node over RPC. The ExecutionClient struct has a method new that allows you to create a new instance of the struct by providing the URL for the Ethereum node.
The ExecutionClient struct has a method get_account that takes an Ethereum address, an optional list of storage slots, and an ExecutionPayload as input and returns an Account struct containing information about the Ethereum account. It does this by first fetching a proof of the account's state from the Ethereum node using the get_proof method of the ExecutionRpc trait. It then verifies the proof to ensure it is valid. If the proof is valid, it fetches the code for the account from the Ethereum node and verifies that the code hash in the proof matches the hash of the code. Finally, it builds a map of storage slots to their values by fetching the storage proof for each slot and verifying it.
- This code defines an ExecutionClient struct that is used to interact with an Ethereum node over an RPC interface. It has methods to fetch information about an Ethereum account (e.g., balance, code, storage), send raw transactions, and get transaction receipts. It also has a method to fetch logs for a given filter.
The ExecutionClient struct has a single field rpc, which is an implementation of the ExecutionRpc trait. This trait defines the interface for interacting with an Ethereum node over RPC. The ExecutionClient struct has a method that allows you to create a new instance of the struct by providing the URL for the Ethereum node.
The ExecutionClient struct has a method get_account that takes an Ethereum address, an optional list of storage slots, and an ExecutionPayload as input and returns an Account struct containing information about the Ethereum account. It does this by first fetching a proof of the account's state from the Ethereum node using the get_proof method of the ExecutionRpc trait. It then verifies the proof to ensure it is valid. If the proof is valid, it fetches the code for the account from the Ethereum node and verifies that the code hash in the proof matches the hash of the code. Finally, it builds a map of storage slots to their values by fetching the storage proof for each slot and verifying it.
The ExecutionClient struct has a method send_raw_transaction that takes a raw transaction as input and sends it to the Ethereum node. It returns a hash of the transaction.
The ExecutionClient struct has a method get_transaction_receipt that takes a transaction hash as input and returns a TransactionReceipt struct containing information about the transaction.
The ExecutionClient struct has a method get_logs that takes a filter as input and returns a vector of Log structs containing the logs that match the filter. The number of logs returned is limited to MAX_SUPPORTED_LOGS_NUMBER to avoid blocking the client for too long.
- This code is defining an ExecutionClient struct that wraps an implementation of the ExecutionRpc trait. The ExecutionClient provides functionality for interacting with the Ethereum blockchain, including fetching accounts and sending transactions.
The ExecutionRpc trait has a single method, get_proof, which returns a "proof" for an Ethereum account at a particular block number. This proof consists of information about the account balance, nonce, code hash, storage hash, and storage slots for the account. The ExecutionClient also has a method, get_account, which uses the get_proof method to retrieve a proof for an account, and then verifies the proof to ensure its validity. If the proof is valid, the get_account method returns an Account struct containing the information contained in the proof.
The ExecutionClient also has a method, send_raw_transaction, which allows a raw Ethereum transaction to be sent to the blockchain. It first checks the transaction to ensure it is valid, and if it is, sends it to the blockchain and returns the transaction receipt.
Additionally, the ExecutionClient has a method, call, which allows a contract function to be called on the blockchain and returns the result of the call. It does this by sending a special kind of transaction, called a "call transaction", to the blockchain and returning the result of the call contained in the transaction receipt.
The ExecutionClient struct has two public methods, get_account and send_raw_transaction. The get_account method retrieves an Ethereum account's data, including its balance, nonce, code, and storage slots, given the account's address and a block number. It does this by making an RPC call to the get_proof method, which returns a "proof" containing the requested data and proof of the data's authenticity. The method then verifies the proof's validity before returning the account data.
The get_account method takes three arguments: an address of type Address, an optional slots argument of type Option<&[H256]>, and a payload argument of type &ExecutionPayload. It returns a Result that contains an Account on success. The method fetches an account proof from the Ethereum client using the ExecutionRpc trait method get_proof, verifies the account proof, and then fetches the account's code if the code hash of the account is not the empty string. It returns an error if any part of this process fails.
The send_raw_transactions method takes a transactions argument of type Transactions and returns a Result containing a vector of TransactionReceipt on success. The method sends the raw transactions to the Ethereum client using the ExecutionRpc trait method send_raw_transaction, waits for the transactions to be mined, and then fetches the receipts for the transactions using the ExecutionRpc trait method get_transaction_receipt. It returns an error if any part of this process fails.
—---------------------
This Rust function is used to call a function in an Ethereum contract. The function takes a mutable reference to self, which is a struct containing an EVM (Ethereum Virtual Machine) instance and a CallOpts struct. The function calls batch_fetch_accounts to get a map of accounts and sets this map in the EVM's database. It then sets the EVM's environment using get_env, and calls a function in the contract by calling transact on the EVM instance. Finally, it checks the reason for the function's return value and returns a Result with a vector of bytes or an EvmError variant.
—---------------
The ConsensusClient struct has an associated type R that represents a consensus RPC (remote procedure call) and holds an instance of R as a field, a LightClientStore instance, an initial_checkpoint and a last_checkpoint. It also has a field config of type Arc<Config>.
The LightClientStore struct has several fields: finalized_header, current_sync_committee, next_sync_committee, optimistic_header, previous_max_active_participants, and current_max_active_participants.
The ConsensusClient has several methods:
new which creates a new ConsensusClient by calling R::new and passing in an RPC address and then returning a ConsensusClient instance with the R instance, a default LightClientStore, config and initial_checkpoint as fields.
get_execution_payload which takes an optional slot number and retrieves a block from the RPC at that slot number. It then calculates the block hash and compares it with the verified block hash which is either the hash of the optimistic_header, finalized_header, or an error if the block is not found. If the block hash and verified block hash do not match, it returns an error, otherwise it returns the ExecutionPayload of the block.
get_header and get_finalized_header which return references to the optimistic_header and finalized_header fields of the LightClientStore, respectively.
sync which retrieves updates, finality and optimistic updates from the RPC, verifies them and applies them to the ConsensusClient.
advance which retrieves a finality update from the RPC, verifies it, and applies it to the ConsensusClient.
bootstrap which retrieves the block at the initial_checkpoint and sets the finalized_header field of the LightClientStore to this block.
verify_update, apply_update, verify_finality_update, apply_finality_update, verify_optimistic_update, and apply_optimistic_update which are used to verify and apply updates, finality updates, and optimistic updates to the ConsensusClient.
I hope this gives you an understanding about ethereum light clients and why it would be useful with eth2.0. Please let me know if you have any question or comment or need help with implementing light a light client.