
Note
The following documentation has been written with the supposition that the reader is already knowledgeable about the synchronisation protocol for Light Clients implemented on Ethereum.\(N\)
To read about it refer to the Light Client section of the Ethereum documentation and the Sync Protocol specification.
The Ethereum Light Client (LC) provides a streamlined and efficient way to verify blockchain state transitions and proofs without needing to store or synchronize the entire blockchain.
The following documentation aims to provide a high-level overview of the Ethereum LC and its components, along with a guide on how to set up and run or benchmark the Ethereum LC.
Sections
An overview of what is the Light Client and the feature set it provides.
A detailed description of the components that make up the Light Client.
A guide on how to set up and run the Light Client.
A guide on how to benchmark the Light Client.
Design of the Light Client
Light clients can be seen as lightweight nodes that enable users to interact with the blockchain without needing to download the entire blockchain history. They rely on full nodes to provide necessary data, such as block headers, and use cryptographic proofs to verify transactions and maintain security.
At the core of the LC there are two proofs:
- Prove Sync Committee change on the Ethereum chain, which is effectively proving a transition from one set of validators to another one.
- Prove at any given point that an account is part of the Ethereum state to provide the bridging capabilities between Ethereum and another blockchain.
This is implemented by two proofs, one for each statement. The light client needs to keep track of two hashes that uniquely identifies the latest known set of validators that it trusts and the one for the next period. The first program is responsible for updating those hashes, whereas the second program makes use of them to confirm the presence of a given account in the state.
The first proof needs to be generated and submitted to the light client at least every 54.6 hours to ensure that the light client's \(N\) internal state is kept up to date with the running chain.
The second proof is generated and submitted when a proof about some on-chain value is required, for example when a deposit to some account needs to be validated.
The current Verifying Key Hashes which uniquely identify the specific RISC-V
binaries for the proof programs, located in the
ethereum/ethereum-programs/artifacts/
directory are:
epoch_change
:0x00de53d6fe5e4e43f950fbc1f6b2c0f9c172097ad7263ab913241267b29d03f2
inclusion
:0x00deddfe90263f93c03e88dc9c33603f3ea0493b2af5da7e37d515794958bbb7
These values are also present in and used by the move fixtures.
Sync committee change proof
The Ethereum chain has (at any given time) a committee of 512 validators that is randomly selected every sync committee period (~1 day), and while a validator is part of the currently active sync committee they are expected to continually sign the block header that is the new head of the chain at each slot.
At the start of each period \(N\) any Light Client can trustfully know and verify the current valid sync committee and the one for the period \(N+1\). The Light Client needs to keep track of those two hashes.
For a given period \(N\) with a set of validators \(V_n\), it is expected to be able to find a block containing information about the new validator set \(V_{\text{n+1}}\) signed by \(V_n\).
It is the job of the light client to produce a proof at least every other period to verify the signature for the next validator set. This is handled by the Sync Committee Change program.
Epoch Change program IO
Inputs
The following data structures are required for proof generation:
LightClientStore
: The current state of the Light Client, containing information about the latest handled finalized block and the known committees.Update
: A Light Client update, containing information about a change of the Sync Committee.
Outputs
- Finalized header slot: The slot of the finalized beacon header.
- Hash of the signing sync committee: The hash of the signing committee for the finalized beacon block.
- Hash of the new sync committee: The hash of the new sync committee set in the store.
- Hash of the new sync committee for the next period: The hash of the new sync committee for the following period set in the update.
Inclusion proof
To bridge an account from the Ethereum chain to another chain at any given time the Light Client needs to prove that the given account exists in the chain state for the latest block produced.
To do so, the Light Client will first need to verify that the signature on the latest block corresponds to the sync committee known for the given period. Then, it will have to prove that the account is part of the updated state that this block commits.
The inclusion program takes in an arbitrary Ethereum Merkle proof generated by and fetched through the EIP-1186 RPC endpoint.
Inclusion program IO
Inputs
The following data structures are required for proof generation :
- Light Client Store: The current state of the Light Client, containing information about the latest handled finalized block and the known committees.
EIP1186Proof
: Data structure the data received from theeth_getProof
RPC call.
Outputs
- Finalized header slot: The slot of the finalized beacon header.
- Hash of the signing sync committee: The hash of the signing committee for the finalized beacon block.
- Account address: The address of the account being checked for inclusion.
- Account value: The value of the account being checked for inclusion.
- Number of storage keys: The number of storage keys being checked for inclusion.
- Storage keys: The keys of the storage being checked for inclusion.
- Storage values: The values of the storage being checked for inclusion.
Edge cases
Latency-wise, Ethereum Light Client's do not have a worst case scenario. As we explained earlier, it is possible for a Light Client to know at any given time in a period \(N\) the current valid sync committee and the one for the period \(N+1\).
This allows the Light Client to generate inclusion proof for both the current period and the one after if the Sync Committee Change proof has yet to be generated.
This effectively means that the Light Client has 2 periods (~2 days) to generate the Sync Committee Change proof, which is more than enough time to generate the proof. It also means that the Light Client can generate the Inclusion Proof at any time, even when the Sync Committee Change proof is being generated.
Security considerations
Architecture components
Light clients can be seen as lightweight nodes that enable users to interact with the blockchain without needing to download the entire blockchain history. They rely on full nodes to provide necessary data, such as block headers, and use cryptographic proofs to verify transactions and maintain security.
There are four core components that need to exist to have a functional light client bridge running:
- Source Chain Node: A full node of the source chain from which it is possible to fetch the necessary data to generate our proofs.
- Coordinator Middleware: This middleware is responsible for orchestrating the other components that are part of the architecture.
- Light Client Proving Servers: The core service developed by Lurk. It contains all the necessary logic to generate the necessary proofs and exposes it through a simple RPC endpoint.
- Verifier: A software that can verify the proofs generated by the Light Client. This verification can happen in a regular computer, using a Rust verifier exposed by the Proof servers, or it can be implemented as a smart contract living on a destination chain.

Ethereum Nodes
In order to generate the two proofs composing the Light Client, it is needed to fetch data from the Ethereum network. To retrieve this data, the Light Client needs to interact with both a node from the Beacon chain and from the execution chain.
Beacon Chain Node
The Beacon Node is responsible for providing the Light Client with the necessary data to handle the parts of the proving related to consensus on the chain. There are multiple ways to get such an endpoint, such as leveraging one provided by an infrastructure company (such as Ankr or leveraging a public one, such as the one provided by a16z.
Execution RPC Endpoint
The Execution RPC endpoint is responsible for providing the Light Client with the necessary data to prove value inclusion in the state of the chain. The Light Client needs to connect to an Ethereum node that exposes the necessary RPC endpoints.
The RPC endpoint to be used to fetch this data is eth_getProof
. This RPC
endpoint can be accessed through various RPC provider such
as Infura
or Chainstack.
Proof Server
The Proof Server is a component of the Light Client that is responsible for generating and serving proofs to the client. The server is designed to be stateless and can be scaled horizontally to handle a large number of requests. The Proof Server can be divided in two distinct implementations:
- Proof programs: The proof program contains the logic that will be executed by our Proof server, generating the succinct proof to be verified. The proof programs are run inside the Sphinx zkVM and prover.
- Server: The server is a layer added on top of the proving service that makes it available to external users via a simple protocol.
Proof programs
This layer of the Proof Server corresponds to the code for which the execution has to be proven. Its logic is the core
of our whole implementation and ensures the correctness of what we are trying to achieve. The programs are written in Rust
and leverages the argumentcomputer/sphinx
zkVM to generate the proofs and verify them.
In the design document of both the Sync Committee change proof and the inclusion proof, we describe what each program has to prove. Most computations performed by the proof programs are directed towards cryptographic operations, such as verifying signatures on the block header.
To accelerate those operations, we leverage some out-of-VM circuits called pre-compiles that are optimized for those specific operations. The following libraries that make use of our pre-compiles are used in our codebase:
- bls12_381: A library for BLS12-381 operations based on
zkcrypto/bls12_381
making use of pre-compiles for non-native arithmetic. Used for verifying the signatures over block header. - sha2: A library for SHA-256 hashing making use of pre-compiles for the compression function. Used to reconstruct the Merkle Root from a Merkle Proof.
- tiny-keccak: A library for SHA-3 hashing, making use of pre-compiles for the compression function. Used for hashing the sync committee data.
The code to be proven is written in Rust and then compiled to RISC-V binaries, stored in ethereum/ethereum-programs/artifacts/
.
We then use Sphinx to generate the proofs and verify them based on those binaries. The generated proofs can be STARKs, which
are faster to generate but cannot be verified directly on-chain, or wrapped in a SNARK, which take longer to generate but can
be verified cheaply on-chain.
Server
The server is a layer added on top of the proving service that makes it available to external users. It is a simple TCP server that is open to incoming connections on a port specified at runtime.
The server is divided in two, with one main entrypoint. This allows us to handle the worst-case scenario of having to generate both proofs in parallel, since each server handles one proof at a time. It is possible to generate and verify both STARK core proofs and SNARK proofs.
The RPC protocol used by the servers is a very simple length-prefixed protocol passing serialized messages back and forth.
The messages are defined in proof-server/src/types/proof_server.rs
.
See also the documentation on the client.
Client
The client is the coordinator of the Light Client. It is responsible for orchestrating the communication between the Proof Server and the Ethereum nodes. In our current example implementation it can also serve as a drop-in replacement for what an on-chain verifier would be responsible for.
The client also demonstrates how to request data from the Ethereum nodes endpoints, how to forward it to the proof servers using the simple binary RPC protocol, example and how to parse the received responses from the server. See the source for more details.
The client has two phases:
- Initialization: The client fetches the initial data from the Ethereum nodes and generates the initial state for itself and the verifier.
- Main Loop: The client listens for new data from the Ethereum nodes and generates proofs for the verifier to verify. This includes new proofs for epoch changes.
Run the Light Client
In the previous section, we covered all the architecture components of the Light Client and explained their specific roles. In this section we will cover how to set them up and run the Light Client to start proving epoch change and inclusion proofs.
As the computational requirements for the Proof Server are heavy, here are the machine specs for each component, based on off-the-shelf machines from cloud providers:
CPU | Memory (GB) | Disk Size (GB) | Example | |
---|---|---|---|---|
Client | 8 cores, 16 threads | 32 | 64 | GCP C3 |
Proof Server | Intel x86_64 Sapphire Rapids, 128 vCPU, bare metal, supports avx512_ifma and avx512_vbmi2 | 1024 | 150 | AWS r7iz.metal-32xl |
Note
Generating a proof needs a minimum of 128GB, but does not make use of more than ~200GB of memory.
Configuration
To run the Proof Server and the Client there are a few requirements that needs to be followed on the host machine.
First, you need to install nightly Rust and Golang. You can find the installation instructions for Rust here and for Golang here.
Make sure to install nightly Rust, which is necessary for AVX-512 acceleration:
rustup default nightly
We pin the nightly Rust version in rust-toolchain.toml
to prevent unknown future changes
to nightly from interfering with the build process. In principle however, any recent nightly release of Rust should
work.
Second, you need to install the cargo-prove
binary.
- Install
cargo-prove
from Sphinx:
git clone git@github.com:argumentcomputer/sphinx.git && \
cd sphinx/cli && \
cargo install --locked --path .
- Install the toolchain. This downloads the pre-built toolchain from SP1
cd ~ && \
cargo prove install-toolchain
- Verify the installation by checking if
succinct
is present in the output ofrustup toolchain list
Finally, there's a few extra packages needed for the build:
sudo apt update && sudo apt-get install -y build-essential libssl-dev pkg-config libudev-dev cmake
For non-Ubuntu/non-Debian based distros, make sure to install the equivalent packages.
Logging Configuration
The light client uses two logging systems:
- Rust logging via
tokio-tracing
, configured through theRUST_LOG
environment variable. See the tracing documentation for detailed configuration options. - Go logging for FFI calls, configured through the
SP1_GO_LOG
environment variable.
To set a global log level (e.g. warn), configure both variables:
RUST_LOG=warn SP1_GO_LOG=warn cargo run ...
Valid log levels are: error, warn, info, debug, trace, off
Connect to Ethereum
This section will guide you through the process of connecting the Ethereum Light Client to an Ethereum node so that it can fetch the necessary data to generate the proofs.
There three main components that the Light Client needs to connect to:
- Checkpoint provider: The Checkpoint Provider is responsible for providing the Light Client with the latest checkpoints made available for the sync protocol.
- Beacon node: The Beacon Node is responsible for providing the Light Client with the necessary data to handle the parts of the proving related to consensus on the chain.
- Execution RPC endpoint: The Execution RPC endpoint is responsible for providing the Light Client with the necessary data to prove value inclusion in the state of the chain.
Checkpoint Provider
The Checkpoint Provider is responsible for providing the Light Client with the latest checkpoints made available for the sync protocol. A community maintained list of Checkpoint Providers can be found on eth-clients.github.io.
For our Light Client, we recommend to use the https://sync-mainnet.beaconcha.in endpoint.
Beacon Node
The Beacon Node is responsible for providing the Light Client with the necessary data to handle the parts of the proving related to consensus on the chain. There are multiple ways to get such an endpoint, such as leveraging one provided by an infrastructure company (such as Ankr ...) or leverage a public one, such as the one provided by a16z.
In this documentation, we will use the endpoint https://www.lightclientdata.org.
Note
If you decide to use an infrastructure provider, make sure that the endpoints we need are properly available. Those endpoints can be found
Execution RPC Endpoint
The Execution RPC endpoint is responsible for providing the Light Client with the necessary data to prove value inclusion in the state of the chain. The Light Client needs to connect to an Ethereum node that exposes the necessary RPC endpoints.
The RPC endpoint to be used to fetch this data is eth_getProof
. This RPC
endpoint can be access through various RPC provider such
as Infura
or Chainstack.
Deploy the Proof Server
Note
We will deploy the server as through the execution of the bianry with
cargo
in this example. It is also possible to deploy the proof server through its docker image. To do so, please refer to the dedicated documentation.
For the Proof Server, we have to take into account that generating a proof is a heavy operation. To avoid overloading the server, we can split the proof generation into two servers. The primary server will handle inclusion proofs, and the secondary server will handle epoch change proofs.
For best results, the primary and secondary servers should be deployed to different server instances, so that proof generation can happen in parallel if necessary.
Requirements
Make sure to finish the initial configuration first.
Environment variables
For more details onthe optimal environment variables to set to run the Proof Server, please refer to our dedicated documentation.
Note
One can also set the
RUST_LOG
environment variable todebug
to get more information about the execution of the server.
Deploy the secondary server
Now that our deployment machine is properly configured, we can run the secondary server.
git clone git@github.com:argumentcomputer/zk-light-clients.git && \
cd zk-light-clients/ethereum/light-client && \
RECONSTRUCT_COMMITMENTS=false SHARD_BATCH_SIZE=0 SHARD_CHUNKING_MULTIPLIER=64 SHARD_SIZE=4194304 RUSTFLAGS="-C target-cpu=native -C opt-level=3" cargo run --release --bin proof_server -- --mode "single" -a <NETWORK_ADDRESS>
Deploy the primary server
Finally, once the primary server is configured in the same fashion, run it:
git clone git@github.com:argumentcomputer/zk-light-clients.git && \
cd zk-light-clients/ethereum/light-client && \
RECONSTRUCT_COMMITMENTS=false SHARD_BATCH_SIZE=0 SHARD_CHUNKING_MULTIPLIER=64 SHARD_SIZE=4194304 RUSTFLAGS="-C target-cpu=native -C opt-level=3" cargo run --release --bin proof_server -- --mode "split" -a <NETWORK_ADDESS> --snd-addr <SECONDARY_SERVER_ADDRESS>
Note
Logging can be configured via
RUST_LOG
for Rust logging andSP1_GO_LOG
for Go FFI logging. For example:RUST_LOG=debug SP1_GO_LOG=debug cargo run ...
See the configuration documentation for more details.
Run the Client
To coordinate the communication to all the components, we need to run the Client. The Client will communicate each component to fetch the data and generate the proofs.
Requirements
Make sure to finish the initial configuration first.
Launch the Client
With our deployment machine properly configured, we can run the client.
The client can either work with STARK or SNARK proofs. To configure this, the
environment variable MODE
can be set to either STARK
or SNARK
. The default is STARK
.
git clone git@github.com:argumentcomputer/zk-light-clients.git && \
cd zk-light-clients/ethereum && \
MODE=SNARK RUST_LOG="debug" cargo run -p light-client --release --bin client -- -c <CHECKPOINT_PROVIDER_ADDRESS> -b <BEACON_NODE_ADDRESS> -p <PROOF_SERVER_ADDRESS> -r <RPC_PROVIDER_ADDRESS>
The client only needs to communicate with the primary proof server, since requests to the secondary server are automatically forwarded.
With this, the Client should run through its initialization process and then start making requests to both the Proof Server and the Ethereum nodes, generating proofs as needed in a loop.
Operate the bridge
In the previous sections we have gone over the steps to setup each components that are available in the source of the repository so that they can start interacting with each other.
However, in a practical scenario, the client and verifier contracts will have to be adapted to the use case that a user wants to implement. We will go over the steps to adapt the components for any use case in this section.
Adapt the client
Initialize the client
Before we can start fetching data from the Ethereum network, we need to initialize the client. To do so we have to leverage the checkpoint mechanism available for the sync protocol. The logic to initialize the client is quite straight forward and an example implementation can be found in our mock client.
Fetch Merkle Proof data
The first piece that will need some refactoring to adapt to a new use case is the client. The client should be considered as the main entry point for a bridge, and is responsible for fetching data from the Ethereum network and submitting it to the prover.
The first thing to note is that the data to be fetched from the Ethereum network will differ depending on how a liquidity provider handles its assets. However, most assets will be living in a smart contract on Ethereum at a given storage key, making our current implementation that leverages the EIP-1186 flexible to most use cases.
The EIP-1186 has two main arguments that need to be passed, the address of the targeted contract and the storage key.
The address of the targeted contract can be easily retrieved from an explorer of the chain. The storage key you are looking for is a bit more tricky to compute.To understand which storage key you will want to use as a pointer to the data to bridge, refer to the Ethereum documentation on "Layout of State Variables in Storage and Transient Storage".
Once we have a hold on the storage key we can use it as a parameter to call
the eth_getProof
RPC endpoint, thus fetching a Merkle Proof for the inclusion of
the data for the storage key at a given block height on the Ethereum Network.
The payload we will get from the eth_getProof
RPC endpoint will be used to generate
a proof that can be verified on-chain. The proof will be generated by the prover.
In the mock client we have developed to showcase the bridge, we can actually find the values for the smart contract address and the storage key being declared as constants in the Rust code base.
Transform the Merkle Proof data
In our codebase the structure representing the fetched Merkle Proof data is
GetProofResponse
.
This data has to be transformed in the inner type used by the prover, EIP1186Proof
.
An example of this data transformation implementation can be found in the codebase.
Run the prover
The prover is quite straight forward to run. When ran in single
mode, the only
parameter to properly set is the address it should listen to for incoming request.
It consists of a lightweight router the will listen to the following routes:
- (GET)
/health
: Operationnal endpoint the returns a 200 HTTP code when the server is ready to receive requests - (GET)
/ready
: Operationnal endpoint the returns a 200 HTTP code when the server is not currently handling a request - (POST)
/inclusion/proof
: Endpoint to submit a proof request for an inclusion proof - (POST)
/inclusion/verify
: Endpoint to submit a proof request for an inclusion proof verification - (POST)
/committee/proof
: Endpoint to submit a proof request for a committee proof - (POST)
/committee/verify
: Endpoint to submit a proof request for a committee proof verification
For proofs related endpoint the payload is a binary serialized payload that is sent over
HTTP. The Rust type in our codebase representing such types is Request
.
The bytes payload format is the following:
Proof generation
Name | Byte offset | Description |
---|---|---|
Request type | 0 | Type of the request payload |
Proving mode | 1 | Type of the proof that the proof server should generate. 0 for STARK and 1 for SNARK |
Proof inputs | 2 | SSZ encoded inputs for the proof generation. Serialized StorageInclusionIn for inclusion and serialized CommitteeChangeIn for committee change. |
Proof verification
Name | Byte offset | Description |
---|---|---|
Request type | 0 | Type of the request payload |
Proof type | 1 | Type of the proof that the payload contains. 0 for STARK and 1 for SNARK |
Proof | 2 | Bytes representing a JSON serialized SphinxProofWithPublicValues . |
The response bodies are more straight forward:
Proof generation
Name | Byte offset | Description |
---|---|---|
Proof type | 0 | Type of the proof that the payload contains. 0 for STARK and 1 for SNARK |
Proof | 1 | Bytes representing a JSON serialized SphinxProofWithPublicValues . |
Proof verification
Name | Byte offset | Description |
---|---|---|
Successful proof verification | 0 | A 0 (fail) or 1 (success) byte value representing the success of a proof verification. |
Adapt the verifier
In the following section we will touch upon how a verifier contract has to be updated depending on a use case. However, it has to be kept in mind that some core data will have to be passed even thought some modifications have to be done for different use cases.
Core data
Note
The following documentation will be for SNARK proofs, as they are the only proofs that can be verified on our home chains.
The core data to be passed to any verification contrtact are the following:
- Verifying key: A unique key represented as 32 bytes, related to the program that is meant to be verified
- Public values: Serialized public values of the proof
- Proof: The serialized proof to be verified
Verifying key
The verifying key for a program at a given commit can be found in its fixture file
in the format of a hexified string prefixed by 0x
. There is one file for the committee
change
program and one file for the inclusion program.
Public values
The public values and serialized proof data can be found through the type SphinxProofWithPublicValues
returned as an HTTP response body by the prover.
The public values can be found under the public_values
property and are already
represented as a Buffer
which data are to be transmitted to the verifier contract.
In the fixture files we leverage in our codebase, the public values are represented
as a hexified string prefixed by 0x
.
Proof
The proof data to be passed to the verifier contract will depend on the chain hosting it. The difference emerges from how the verification is ran on the network. On the one hand, if the verification is ran natively via an smart contract FFI call for example, then the proof will have to not be encoded and be passed in its raw form. On the other hand, if the verification is directly done in a smart contract, then the proof will have to be passed in its encoded format.
In our case we have showcased the serialization format for two chains, Aptos for the smart contract verification and Kadena for the native verification.
For Aptos, the proof data is an array of bytes with the following format:
Name | Byte offset | Description |
---|---|---|
Verifying key prefix | 0 | Prefix to the encoded proof, a 4 bytes value corresponding to the first 4 bytes of the verifying key. |
Encoded proof | 4 | Encoded proof which value can be found in the returned SNARK proof from the prover represented as SphinxProofWithPublicValues under proof.encoded_proof |
For Kadena, the proof data is an array of bytes with the following format:
Name | Byte offset | Description |
---|---|---|
Raw proof | 0 | Raw proof which value can be found in the returned SNARK proof from the prover represented as SphinxProofWithPublicValues under proof.raw_proof |
Example of the proof data extraction can be found for both Aptos and Kadena in our fixture generation crate.
Wrapper logic
The wrapper logic refers to a smart contract wrapping the proof verification logic with the use case specific logic. It is needed to ensure that the verified proof corresponds to the expected data.
The logic to be executed in the wrapper contract will depend on the use case. However, there are some core logic that have to be executed for the inclusion and committee change proof verification. The logic that has to be kept for the inclusion verification and the committee change program are showcased in both our Move contracts (inclusion and commitee change).
The place where a user can add its own use case logic is where we currently print out some values in the Move contracts (inclusion and committee change).
Benchmark proving time
There are two types of benchmarks that can be used to get insight on the proving time necessary for each kind of proof generated by the proof server. The first type will generate STARK core proofs, and represents the time it takes to generate and prove execution of one of the programs. The second type will generate a SNARK proof that can be verified on-chain, and represents the end-to-end time it takes to generate a proof that can be verified directly on-chain. Due to the SNARK compression, the SNARK proofs take longer to generate and require more resources.
GPU acceleration
Currently, the Sphinx prover is CPU-only, and there is no GPU acceleration integrated yet. We are working on integrating future work for GPU acceleration as soon as we can to improve the overall proving time.
Configuration for the benchmarks
In this section we will cover the configuration that should be set to run the benchmarks. It is also important to run the benchmarks on proper machines, such as the one described for the Proof Server in the Run the Light Client section.
Requirements
The requirements to run the benchmarks are the same as the ones for the client. You will need to follow the instructions listed here.
Other settings
Here are the standard config variables that are worth setting for any benchmark:
-
RUSTFLAGS="-C target-cpu=native -C opt-level=3"
This can also be configured in
~/.cargo/config.toml
by adding:[target.'cfg(all())'] rustflags = ["--cfg", "tokio_unstable", "-C", "target-cpu=native", "-C", "opt-level=3"]
-
SHARD_SIZE=4194304
(for SNARK),SHARD_SIZE=1048576
(for STARK)The highest possible setting, giving the fewest shards. Because the compression phase dominates the timing of the SNARK proofs, we need as few shards as possible.
-
SHARD_BATCH_SIZE=0
This disables checkpointing making proving faster at the expense of higher memory usage
-
RECONSTRUCT_COMMITMENTS=false
This setting enables keeping the FFT's data and the entire Merkle Tree in memory without necessity to recompute them in every shard.
-
SHARD_CHUNKING_MULTIPLIER=<32|64>
(for SNARK),SHARD_CHUNKING_MULTIPLIER=1
(for STARK)This settings is usually selected depending on specific hardware where proving is executed. It is used to determine how many shards get chunked per core on the CPU. For STARK
-
cargo bench --release <...>
Make sure to always run in release mode with
--release
. Alternatively, specify the proper compiler options viaRUSTFLAGS="-C opt-level=3 <...>"
,~/.cargo/config.toml
or Cargo profiles -
RUST_LOG=debug
(optional)This prints out useful Sphinx metrics, such as cycle counts, iteration speed, proof size, etc. NOTE: This may cause a significant performance degradation, and is only recommended for collecting metrics other than wall clock time.
SNARK proofs
When running any tests or benchmarks that makes Plonk proofs over BN254, the prover leverages some pre-built circuits artifacts. Those circuits artifacts are generated when we release new versions of Sphinx and are automatically downloaded on first use. The current address for downloading the artifacts can be found here, but it should not be necessary to download them manually.
Benchmark individual proofs
In this section we will cover how to run the benchmarks for the individual proofs. The benchmarks are located in the
light-client
crate folder. Those benchmarks are associated with programs that are meant to reproduce a production
environment settings. They are meant to measure performance for a complete end-to-end flow.
The numbers we've measured using our production configuration are further detailed in the following section.
Sync committee change
Benchmark that will run a proof generation for the sync committee change program. This program will execute a hash for
the received LightClientStore::current_sync_committee
to ensure that the signature is from the previous sync
committee set, execute a LightClientStore::process_light_client_update
and finally generate the hash for the new
LightClientStore::current_sync_committee
.
On our production configuration, we currently get the following results for SNARK generation for this benchmark:
For STARKS:
{
// Time in milliseconds, 2 minutes 02s ~
"proving_time": 122772,
"verification_time": 4376
}
For SNARKS:
{
// Time in milliseconds, 10 minute 54s ~
"proving_time": 654767,
"verification_time": 1
}
Storage inclusion
Benchmark that will run a proof generation for the storage inclusion program. This program will execute a hash for the
received CompactStore::sync_committee
to ensure that the signature is from the current known sync committee set,
execute a CompactStore::validate_compact_update
to confirm that the received block information is one signed by the
committee, and finally run an EIP1186Proof::verify
against the state root of the finalized execution block header.
On our production configuration, we currently get the following results for SNARK generation for this benchmark:
For STARKS:
{
// Time in milliseconds, 1 minutes 02s ~
"proving_time": 62165,
"verification_time": 2886
}
For SNARKS:
{
// Time in milliseconds, 7 minute 51s ~
"proving_time": 531021,
"verification_time": 3
}
Running the benchmarks
Using Makefile
To ease benchmark run we created a Makefile in the light-client
crate folder. Just run:
make benchmark
You will then be asked for the name of the benchmark you want to run. Just fill in the one that is of interest to you:
$ make benchmark
Enter benchmark name: committee_change
...
Manual
Run the following command:
SHARD_BATCH_SIZE=0 cargo bench --bench execute -- <benchmark_name>
Interpreting the results
Before delving into the details, please take a look at the cycle tracking documentation from SP1 to get a rough sense of what the numbers mean.
The benchmark will output a lot of information. The most important parts are the following:
Total cycles for the program execution
This value can be found on the following line:
INFO summary: cycles=63736, e2e=2506, khz=25.43, proofSize=2.66 MiB
It contains the total number of cycles needed for the program, the end-to-end time in milliseconds, the frequency of the CPU in kHz, and the size of the proof generated.
Specific cycle count
In the output, you will find a section that looks like this:
DEBUG ┌╴read_inputs
DEBUG └╴9,553 cycles
DEBUG ┌╴verify_merkle_proof
DEBUG └╴40,398 cycles
These specific cycles count are generated by us to track the cost of specific operations in the program.
Proving time
The proving time is an output at the end of a benchmark in the shape of the following data structure, with each time in milliseconds:
{
ratchet_proving_time: 100000,
merkle_proving_time: 100000
}
Alternative
Another solution to get some information about proving time is to run the tests located in the light-client
crate. They will output the same logs as the benchmarks, only the time necessary to generate a proof will change shape:
Starting generation of Merkle inclusion proof with 18 siblings...
Proving locally
Proving took 5.358508094s
Starting verification of Merkle inclusion proof...
Verification took 805.530068ms
To run the test efficiently, first install
nextest
following its documentation. Ensure that you also have the previously
described environment variables set, then run the following command:
SHARD_BATCH_SIZE=0 cargo nextest run --verbose --release --profile ci --package ethereum-lc --no-capture --all-features
Note
The
--no-capture
flag is necessary to see the logs generated by the tests.
Some tests are ignored by default due to heavier resource requirements. To run them, pass --run-ignored all
to nextest
.
A short list of useful tests:
test_execute_committee_change
: Executes thecommittee_change
program.test_prove_stark_committee_change
: Generates and verifies a STARK proof of thecommittee_change
program.test_prove_snark_committee_change
: Generates and verifies a SNARK proof of thecommittee_change
program.
Benchmark on-chain verification
Our Light Client is able to produce SNARK proofs that can be verified on-chain. This section will cover how to run the benchmarks for the on-chain verification.
To be able to execute such tests the repository contains a project called move
that demonstrates the verification on
Aptos devnet / testnet using so-called fixtures (JSON files) containing the proof data (proof itself, public values and
verification key) required for running the verification for both epoch-change and inclusion programs. These fixtures
are generated from a SNARK proof generated by the proof servers, but currently the fixtures generated are meant for
simple testing only.
The contracts used for testing can be found in ethereum/move/sources
directory
Run the tests
Running Aptos tests requires installing Aptos CLI. Please, follow this instructions to install it.
To run verifier's tests:
cd ethereum/move
% aptos move test --named-addresses plonk_verifier_addr=testnet
INCLUDING DEPENDENCY AptosFramework
INCLUDING DEPENDENCY AptosStdlib
INCLUDING DEPENDENCY MoveStdlib
BUILDING plonk-verifier
Running Move unit tests
[ PASS ] _::utilities::test_32bytes_u256_conversion
[ PASS ] _::plonk_verifier::test_compute_public_values_hash
[ PASS ] _::plonk_verifier::test_compute_zeta_power_r_minus_one
[ PASS ] _::plonk_verifier::test_derive_alpha
[ PASS ] _::utilities::test_bn254_g1_points_addition
[ PASS ] _::utilities::test_bn254_g1_scalar_multiplication
[ PASS ] _::utilities::test_bn254_scalars_addition
[ PASS ] _::utilities::test_bn254_scalars_multiplication
[ PASS ] _::plonk_verifier::test_derive_beta
[ PASS ] _::utilities::test_point_acc_mul
[ PASS ] _::plonk_verifier::test_derive_gamma
[ PASS ] _::utilities::test_pow_small
[ PASS ] _::utilities::test_sha256_move_eth_precompile_compatibility
[ PASS ] _::plonk_verifier::test_derive_zeta
[ PASS ] _::plonk_verifier::test_pairing_kzg_check
[ PASS ] _::wrapper::test_committee_change_is_allowed
[ PASS ] _::plonk_verifier::test_verify_inner
[ PASS ] _::wrapper::test_committee_change_is_allowed_too
[ PASS ] _::wrapper::test_committee_change_is_not_allowed
[ PASS ] _::wrapper::test_inclusion_is_allowed
[ PASS ] _::wrapper::test_inclusion_is_allowed_too
[ PASS ] _::wrapper::test_inclusion_is_not_allowed
[ PASS ] _::wrapper::test_storage_flow
Test result: OK. Total tests: 23; passed: 23; failed: 0
{
"Result": "Success"
}
This module is configured using setup from this tutorial.
It is also possible to run verification over custom JSON fixtures via Move scripting mechanism. In this settings, proof, public inputs and
verification key are passed via arguments to Move script. Note, that fixture should have Aptos-specific format (see /move/sources/fixtures
for
examples).
To run Move script that executes verification code using JSON fixture (running on Aptos testnet
):
aptos move compile --named-addresses plonk_verifier_addr=testnet
aptos move create-resource-account-and-publish-package --address-name plonk_verifier_addr --profile testnet --seed $(openssl rand -hex 32) --assume-yes
aptos move run-script --compiled-script-path build/plonk-verifier/bytecode_scripts/run_verification.mv --json-file sources/fixtures/epoch_change_fixture.json --profile testnet --assume-yes
You should see tentatively following result if verification passed:
{
"Result": {
"transaction_hash": "0x62f976db6ba0eaa1951d3ec70d4e7afa9c3189856f54bc3f029e86dc6e6f0330",
"gas_used": 786,
"gas_unit_price": 100,
"sender": "4207422239492c11a6499620c869fe2248c7fe52c05ca1c443bffe8a8878d32d",
"success": true,
"version": 26230959,
"vm_status": "status EXECUTED of type Execution"
}
}
It is possible to run Move verification flow locally. This requires running Aptos node locally using Docker (see this tutorial for more details).
Fixture generation
If you wish to run the Move script with custom fixtures, you can regeenrate them by running the
fixture-generator
Rust program. This program will run the end-to-end proving (either epoch-change or inclusion) and
export the fixture file to the relevant place (ethereum/move/sources/fixtures
).
To run the fixture-generator
for the inclusion program, execute the following command:
cd fixture-generator
RUST_LOG=info RUSTFLAGS="-C target-cpu=native --cfg tokio_unstable -C opt-level=3" SHARD_SIZE=4194304 SHARD_BATCH_SIZE=0 cargo run --release --bin generate-fixture -- --program inclusion --language move
GitHub Release and Patch process
This section is for internal usage. It documents the current release and patch process to ensure that anyone is able to run it.
Release process
The release process is mostly automated through the usage of GitHub Actions.
A release should be initiated through the manually triggered GitHub Action Create release PR. When triggering a release,
the reference base that should be chosen is the dev
branch, with a major
or minor
Semver release type, ethereum
light-client and the desired release version. The specified release version should follow the Semver standard.
This action pushes a new branch named ``release/ethereum-v(where
release-versionomits the patch number, e.g.
1.0) based on the most recent corresponding major/minor
release/branch, or
devif none exists. This will be the base of the PR, which will persist across any patches. Then, a PR branch is created off of the base branch called
release-pr-ethereum-v. A commit is then automatically applied to the PR branch to bump all the
Cargo.toml` version of the relevant crates, and the PR is opened. The developer in charge of the release should use this branch to make any necessary updates to the codebase and documentation to have the release ready.
Once all the changes are done, the PR can be merged with a merge commit. This will trigger the Tag release action that is charged with the publication of a release and a tag named ethereum-v<release-version>
. It will use each commit to create a rich changelog categorized by commit prefix, e.g. feat:
, fix:
, and chore:
.
The base branch should be saved as the release source and in case of any future patches.
Patch process
The patch process is similar to that of a major/minor release.
Create release PR should also be triggered with the patch
Semver release type and the desired patch version, e.g. 1.0.1 for a patch to 1.0.0. A PR will be opened from a branch named patch/ethereum-v<patch-version>
(e.g. v1.0.1
) with the base release/ethereum-v<release-to-fix>
(e.g. v1.0
). A commit is automatically applied to bump all the Cargo.toml
version of the relevant crates. The developer in charge of the patch should use this branch to make any necessary updates to the codebase and documentation to have the patch ready.
Once all the changes are done, the PR can be squash and merged in release/ethereum-v<release-to-fix>
. This will trigger the Tag release action that is charged with the publication of a release and a tag named ethereum-v<patch-version>
.
Finally, the developer may also need to port the relevant changes to dev
, such as incrementing the version number for a latest release, so that they are reflected on the latest development stage of the Light Client.