This repository contains a RISC Zero guest program that demonstrates ZK data availability challenges with Celestia and Blobstream. Users can use this program to generate a proof that a specific blob inside an Eclipse index blob (any of the blobs inside the index or the index itself) is unavailable on Celestia.
The guest program uses Blobstream to attest the authenticity of Celestia blocks, Celestia row proofs to derive the size of each Celestia block's Extended Data Square (EDS) and then performs a range check of the share indices of the challenged blob. If the challenged blob is out of bounds, the blob is proven as unavailable.
Given the large data throughput of Eclipse, several Celestia blobs are required per Eclipse block batch. To get O(1) storage requirements regardless of the amount of blobs actually used, we use an indirection, the index blob. An index blob is simply a Celestia blob that contains the list of all blobs used for a batch.
This implies that there are three subtypes of challenges:
- The index blob is not available
- The index blob is available, but not deserializable
- The index blob is valid, but a blob inside the index is unavailable.
#1 and #3 are the same problem; we need to prove that a blob is unavailable given a commitment (a span sequence, i.e., a (Celestia block height, start index, size) tuple).
#2 amounts to downloading the blob data and proving that it matches the span sequence but does not respect the expected format.
Note: Contrary to the format used in other projects, our span sequences refer to Original Data Square (ODS) indexes and not Extended Data Square (EDS) indexes.
Proving the unavailability of a span sequence is achieved by proving that either of the following is true:
- The Celestia block height is out of the range covered in Blobstream (either too old or not published yet)
- Part of the span sequence is out of bounds of the ODS for the specified Celestia block.
To determine the first Celestia block covered by Blobstream, we use get_first_data_commitment_event()
.
This function filters through Blobstream events to find the first data commitment event, or uses hardcoded
values for Ethereum mainnet and Sepolia (to save on RPC calls).
Using this event, it is possible to download the corresponding Blobstream inclusion proof and therefore identify the first Celestia block covered by Blobstream, verifiably.
For the last block currently in Blobstream, we use SP1Blobstream
's latestBlock()
method
(or the equivalent for Blobstream0
, latestHeight()
).
This gives us the supported block range, a quick range check being enough to identify bad Celestia blocks.
Proving that the span sequence is inside the ODS for the block is achieved through the method recommended by Celestia, using a Rust implementation of the same idea.
To prove that the index blob is in the wrong format, we:
- Download the corresponding shares and share proofs
- Verify the share proofs, authenticating the data
- Check that the share indexes match the span sequence, tying the data to the index blob
- Attempt to deserialize the data.
You will need the following tools on your machine:
- The RISC Zero toolchain (v2.0.1)
- Foundry (v1 or above)
The cli
package is used to run the guest program and attempts to publish a proof to a deployed Counter contract.
The Counter is an example contract that increments a counter if the supplied ZK proof is valid. It only serves
as a minimal on-chain example of verifying a ZK proof generated by the DA challenger.
You will need:
- An Ethereum private key with funds on the network you use, to deploy the Counter contract and then publish the proof
- An RPC URL for the same Ethereum network to fetch Blobstream data then publish the proof.
- A Celestia Mocha RPC URL, to fetch data about the index blob.
In this example we will use Sepolia as the Ethereum network and PublicNode as the RPC URL. For Celestia, we will use the Mocha testnet. You will need to either run your own light node or use an RPC service like Quicknode.
# Set essential configuration values
export ETH_WALLET_PRIVATE_KEY=<Your private key>
export ETH_RPC_URL="https://ethereum-sepolia-rpc.publicnode.com"
export CELESTIA_RPC_URL=<Your Celestia RPC URL>
# Deploy the counter smart contract and a mock RISCZero verifier contract
RISC0_DEV_MODE=1 forge script --rpc-url "${ETH_RPC_URL}" --broadcast DeployCounter
export COUNTER_ADDRESS=$(jq -re '.transactions[] | select(.contractName == "Counter") | .contractAddress' ./broadcast/DeployCounter.s.sol/11155111/run-latest.json)
Check that the current value of the counter is 0:
cast call --rpc-url "${ETH_RPC_URL}" "${COUNTER_ADDRESS}" 'get()(uint256)'
In the following example, we will submit a proof that the sequence of spans at 6671289:6:4
is not available.
RUST_LOG=info RISC0_DEV_MODE=1 cargo run --package cli -- \
--eth-wallet-private-key ${ETH_WALLET_PRIVATE_KEY} \
--eth-rpc-url ${ETH_RPC_URL} \
--celestia-rpc-url ${CELESTIA_RPC_URL} \
--counter-address ${COUNTER_ADDRESS} \
--index-blob 6671289:6:4 \
--challenged-blob 6671289:6:4
You can then check that the counter value has been incremented:
cast call --rpc-url "${ETH_RPC_URL}" "${COUNTER_ADDRESS}" 'get()(uint256)'
For additional testing, we uploaded a few bad index blobs for testing on Mocha. You can test the following challenges:
- Valid DA challenge: Specify a blob with an out-of-bounds start index (ex: 6671289:1000000:1024) and challenge the same blob.
- Invalid index challenge: Specify a valid index blob (ex: 6671289:6:4) and challenge the same blob.
- Invalid blob challenge inside the index: Specify a valid index blob (ex: 6671289:6:4) and challenge a random blob inside it (ex: 6671289:1000:1000)
- Deserialization error: Specify an existing blob that does not deserialize nicely (ex: 6671289:6:4).
- Block height out of range: Specify a span sequence with a nonexisting block height (ex: 1000000000:1:1).
All these challenge types and more are tested in the integration tests.
If you want to run the tests with valid Groth16 RISC Zero proofs, simply unset the RISC0_DEV_MODE
variable
when deploying the counter contract and running the CLI. Beware that local proving time is long (minutes).
We advise configuring a BONSAI API key to speed up proving time:
export BONSAI_API_KEY="YOUR_API_KEY" # see form linked above
export BONSAI_API_URL="BONSAI_URL" # provided with your api key
Note: To request an API key complete the form here.
The integration tests use a Docker Compose setup with the following components:
- A local Celestia deployment
- [Anvil] to emulate Ethereum
- A Blobstream service to publish Celestia block ranges to Anvil.
This is all managed by the ci/docker-compose.yml
file.
The integration tests can be run with the following command:
bash scripts/run-tests.sh
If you want to reset the test environment, run bash scripts/reset-tests.sh --reset
.
Currently, tests must be run sequentially because the Ethereum RPC calls are not thread safe.