Introduction
HDP (Herodotus Data Processor) enables provable computations on historical blockchain data. It connects to live RPC nodes during a simulation phase, fetches cryptographic proofs for every accessed value, and replays the computation offline with verification guarantees.
The problem
Historical on-chain data is hard to trust at scale. Indexers are fast but not verifiable, and replaying full chain history is too expensive for most applications. When you need to prove a statement about past state, you need both the data and a cryptographic trail that ties it back to a trusted root.
The HDP approach
HDP splits execution into three stages:
- Dry run: execute the Cairo module against live RPCs and collect the keys you touched.
- Fetcher: download proofs for every key (MMR for headers, MPT/Patricia for state).
- Sound run: verify proofs and execute the same Cairo module offline.
The result is deterministic output plus commitments (task_hash, output_root, and mmr_metas) that can be verified on-chain.
Key capabilities
- Cross-chain access: Ethereum, Optimism, and Starknet in one module.
- Cryptographic verification: MMR for headers, MPT/Patricia for state.
- Offline execution: sound run has zero network calls.
- Optional proof generation: STWO prover input support.
Use cases
- Solvency and reserve proofs
- Compliance and audit trails
- Historical analytics with provable results
- Cross-chain state composition
Architecture at a glance
+------------------------+
| Cairo1 module |
| uses hdp.* APIs |
+------------------------+
| Cairo0 bootloader |
| verifiers + outputs |
+------------------------+
| Rust hints/handlers |
| Dry: RPC keys |
| Sound: memorizer reads |
+------------------------+
How to read this book
Start with Getting Started to run a full pipeline. Then:
Architecturefor how the system fits together.Pipelineto understand each stage and its inputs/outputs.VerificationandState Managementfor proof details.Cairo Libraryfor the public API you use in modules.ExamplesandReferencefor practical workflows and CLI details.
Getting Started
This guide gets you from zero to a full HDP pipeline run in about 10 minutes.
Prerequisites
- Rust (stable) and Cargo
- Scarb (Cairo toolchain)
uv(Python package manager for Cairo tooling)- RPC endpoints for the chains you will query
Optional:
- Docker (for containerized builds)
- Nightly Rust if you plan to generate STWO prover input
Installation
Option 1: Install the CLI (recommended)
curl -fsSL https://raw.githubusercontent.com/HerodotusDev/hdp-cairo/main/install-cli.sh | bash
The installer builds hdp-cli and creates a hdp symlink in $HOME/.local/bin.
Install a specific version:
VERSION=vX.X.X curl -fsSL https://raw.githubusercontent.com/HerodotusDev/hdp-cairo/main/install-cli.sh | bash
Option 2: Build from source
git clone https://github.com/HerodotusDev/hdp-cairo.git
cd hdp-cairo
git submodule update --init
uv sync
source .venv/bin/activate
Makefile shortcuts (optional)
If you prefer make targets:
make setup # uv sync + cargo check
make build # scarb build + cargo build --release
make test # scarb build -p tests + cargo nextest run
Option 3: Docker (optional)
The Docker image installs the CLI from crates/cli and sets hdp-cli as the entrypoint.
docker build -t hdp-cairo .
docker run --rm -it hdp-cairo --help
Environment setup
Copy the example env file and fill in RPC endpoints:
cp example.env .env
Useful CLI helpers:
hdp env-infoprints the expected env vars and example values.hdp env-check --inputs dry_run_output.jsonchecks which RPCs are required.
First run (StarkGate example)
The examples/starkgate project demonstrates a full pipeline. Build the Cairo module first:
cd examples/starkgate
scarb build
Run the pipeline using the installed hdp binary:
hdp dry-run -m target/dev/example_starkgate_module.compiled_contract_class.json --print_output
hdp fetch-proofs
hdp sound-run -m target/dev/example_starkgate_module.compiled_contract_class.json --print_output
If you built from source, you can run the binaries directly:
cargo run --release --bin dry_run -- -m target/dev/example_starkgate_module.compiled_contract_class.json --print_output
cargo run --bin fetcher
cargo run --release --bin sound_run -- -m target/dev/example_starkgate_module.compiled_contract_class.json --print_output
What to expect
dry_run_output.jsonis produced after Stage 1 and contains the key set.proofs.jsonis produced after Stage 2 and contains cryptographic proofs.- Stage 3 prints
task_hash,output_root, andmmr_metas.
CLI quick reference
hdp dry-run: simulate and collect keyshdp fetch-proofs: fetch MMR/MPT proofshdp sound-run: verify and execute offlinehdp env-info: print required env varshdp env-check --inputs dry_run_output.json: validate RPC config
Tests
scarb build
cargo nextest run
Make sure .env is configured before running tests.
Architecture Overview
HDP Cairo is a layered system that combines Cairo0 verifiers, Cairo1 user modules, and Rust-based hint processors. The goal is to make historical blockchain data verifiable and deterministic.
Layers
- Cairo1 user layer: your module uses the
hdp_cairolibrary to request data. - Bootloader layer (Cairo0): loads the compiled Cairo1 class and executes it.
- Verification layer (Cairo0): validates MMR/MPT/Patricia proofs before data is used.
- Hint processors (Rust): bridge the Cairo VM with external data structures.
- Orchestration (Rust): CLI commands for dry run, fetcher, and sound run.
Data flow
+-----------+ +-----------+ +-----------+
| Dry Run | -> | Fetcher | -> | Sound Run |
| RPC keys | | Proofs | | Verify run|
+-----------+ +-----------+ +-----------+
dry_run_output.json -> proofs.json -> outputs
Cairo0 vs Cairo1 boundary
- Cairo1 code calls
call_contract_syscallvia thehdp_cairolibrary. - The Cairo VM forwards syscalls to the Rust
SyscallHandler. - The handler either performs RPC reads (dry run) or memorizer reads (sound run).
Entry points
hdp dry-run: stage 1, generatesdry_run_output.jsonhdp fetch-proofs: stage 2, generatesproofs.jsonhdp sound-run: stage 3, returnstask_hash,output_root, andmmr_metas
Rust Crates
This repository is organized into focused Rust crates. The most important ones are listed below.
| Crate | Purpose | Notes |
|---|---|---|
cli | User-facing CLI (hdp) | Subcommands: dry-run, fetch-proofs, sound-run |
dry_run | Stage 1 execution | Runs Cairo VM with RPC-backed handlers |
fetcher | Stage 2 proof collection | Builds ProofsData from key sets |
sound_run | Stage 3 execution | Offline run with verified proofs |
dry_hint_processor | Dry-run syscall handlers | RPC reads and key collection |
sound_hint_processor | Sound-run syscall handlers | Reads from memorizers |
syscall_handler | Shared syscall dispatch | Routes to EVM/Starknet/injected state |
hints | Cairo hint implementations | 250+ hints for VM integration |
types | Shared types and schemas | Inputs/outputs, keys, proofs |
indexer_client | Herodotus Indexer API client | MMR proof requests |
state_server | Injected state server | Patricia trie + proof API |
If you are new to the codebase, start with cli, dry_run, fetcher, and sound_run, then follow their dependencies.
Rust-Cairo Integration
HDP uses the Cairo VM syscall interface to connect Cairo1 code to Rust logic. The same Cairo1 module runs in both dry run and sound run; only the Rust handlers change.
Call flow
- A Cairo1 module calls a method from
hdp_cairo, which internally usescall_contract_syscall. - The Cairo VM forwards the syscall to the Rust
SyscallHandler. - The handler decodes a
CallContractRequest, routes it, and writes aCallContractResponse. - The result is decoded back in Cairo1 and returned to the caller.
Syscall selectors
HDP handles two syscall selectors:
CallContractfor all data access paths.Keccakfor Cairo keccak hashing.
The Keccak selector is handled by KeccakHandler inside the Rust SyscallHandler.
Routing logic
The Rust SyscallHandler dispatches requests based on:
- Contract address for special handlers (debug, injected_state, unconstrained).
- Chain ID in calldata for EVM vs Starknet routing.
Relevant code paths:
crates/syscall_handler/src/lib.rs(routing and execution)crates/types/src/cairo/new_syscalls.rs(request/response layout)hdp_cairo/src/evm/*.cairoandhdp_cairo/src/starknet/*.cairo(Cairo API)
Dict manager and hints
The Cairo VM uses a shared DictManager for memorizers. During execution:
- Cairo hints can read/write dicts via the manager.
- Rust hint processors populate dicts during verification.
This is how verified data becomes available to Cairo1 calls without network access.
Dry vs sound
- Dry run uses
dry_hint_processorhandlers that make RPC calls and collect keys. - Sound run uses
sound_hint_processorhandlers that read from memorizers filled during verification.
This separation keeps Cairo code deterministic and identical across stages.
Pipeline Overview
HDP executes in three stages to separate data discovery, proof fetching, and deterministic execution.
Why three stages?
Historical data access requires cryptographic proofs that you can only fetch after you know which keys your module needs. The dry run discovers those keys, the fetcher collects the proofs, and the sound run replays the module with verified data.
Execution flow
+-----------+ +-----------+ +-----------+
| Dry Run | -> | Fetcher | -> | Sound Run |
| RPC keys | | Proofs | | Verify run|
+-----------+ +-----------+ +-----------+
dry_run_output.json -> proofs.json -> outputs
Inputs and outputs
- Stage 1 output:
dry_run_output.json(serialized syscall handler with key sets) - Stage 2 output:
proofs.json(ProofsDatawith chain proofs and injected state proofs) - Stage 3 output:
task_hash,output_root, andmmr_metas
Each stage can be executed independently, which is helpful for debugging and caching.
Stage 1: Dry Run
The dry run simulates your Cairo module against live RPC data. It produces the key set that the fetcher later turns into proofs.
Entry point
crates/dry_run/src/lib.rs
Execution flow
- Load the compiled Cairo module.
- Construct
HDPDryRunInput(compiled class, params, injected state). - Run the Cairo VM with
CustomHintProcessor. - Syscall handlers call RPC endpoints and collect keys.
- Write
dry_run_output.jsonwith the serialized handler state.
The dry run uses the all_cairo layout in the Cairo VM.
Output artifacts
dry_run_output.json: serializedSyscallHandlercontaining key sets for each handler.- Printed output (optional):
HDPDryRunOutputwithtask_hashandoutput_rootfields.
dry_run_output.json structure (high level)
The file is a JSON serialization of the Rust SyscallHandler. At a high level it looks like:
{
"call_contract_handler": {
"evm_call_contract_handler": { "key_set": [ ... ] },
"starknet_call_contract_handler": { "key_set": [ ... ] },
"injected_state_call_contract_handler": { "key_set": { "<label>": [ ... ] } },
"unconstrained_call_contract_handler": { "key_set": [ ... ] }
}
}
This file is the only input needed by the fetcher.
If you pass --print_output, the command prints the decoded HDPDryRunOutput struct, which includes task hash and output root values.
Requirements
Dry run requires RPC endpoints for any chain your module queries.
CLI usage
hdp dry-run -m <module.compiled_contract_class.json> --print_output
Important flags:
--output <path>: output path fordry_run_output.json--inputs <path>: JSON module input parameters--injected_state <path>: injected state JSON--print_output: prints the Cairo output segment
Stage 2: Fetcher
The fetcher parses the key set from the dry run and downloads proofs from RPC endpoints and the Herodotus indexer.
Entry point
crates/fetcher/src/lib.rs
Proof sources
- Headers (MMR proofs): Herodotus Indexer
- Accounts/Storage (MPT proofs):
eth_getProofRPC calls - Transactions/Receipts: MPT proofs built from RPC data
- Injected state: State server
get_state_proofsendpoint - Unconstrained data: bytecode values verified by code hash
Output format
Fetcher outputs a ProofsData JSON file:
{
"chain_proofs": [ ... ],
"state_proofs": [ ... ],
"unconstrained": { ... }
}
chain_proofs is a list of chain-specific proof bundles (ChainProofs).
Each bundle is one of:
EthereumMainnet/EthereumSepoliaOptimismMainnet/OptimismSepoliaStarknetMainnet/StarknetSepolia
state_proofs is a list of injected state proofs (read/write).
Inputs and environment
dry_run_output.jsonis required (default input).RPC_URL_HERODOTUS_INDEXERmust be set for MMR proofs.INJECTED_STATE_BASE_URLis used for injected state proofs.- Chain RPC URLs (
RPC_URL_*) are required for account/storage/tx/receipt proofs.
CLI usage
hdp fetch-proofs --inputs dry_run_output.json --output proofs.json
Optional flags:
--mmr-hasher-config <path>: configure Poseidon vs Keccak per chain--mmr-deployment-config <path>: override MMR deployment config
Stage 3: Sound Run
Sound run verifies all proofs and executes your Cairo module offline. No RPC calls are performed in this stage.
Entry point
crates/sound_run/src/lib.rs
Phases
- Verification: Cairo0 verifiers check all proofs and populate memorizers.
- Execution: the bootloader executes the Cairo1 module using the memorizers.
- Output: compute
task_hash,output_root, andmmr_metas.
Output composition happens in src/hdp.cairo, which writes the output segment in a fixed layout.
The sound run config uses the all_cairo_stwo layout, and --proof_mode toggles proof-oriented tracing.
Outputs
The Cairo output segment is decoded into HDPOutput:
task_hash_low/task_hash_highoutput_tree_root_low/output_tree_root_highmmr_metas(Poseidon or Keccak variants)
See docs/output/overview.md for the exact output layout.
The sound run assumes that proofs.json was generated from the matching dry_run_output.json for the same compiled module and inputs.
CLI usage
hdp sound-run -m <module.compiled_contract_class.json> --proofs proofs.json --print_output
Proof-mode options
Sound run can emit prover artifacts:
--proof_mode: enable proof-mode execution--cairo_pie <path>: write Cairo PIE zip--stwo_prover_input <path>: write STWO prover input (requires--features stwo)
Verification Overview
Verification happens inside Cairo0 during the sound run. It ensures that all values used by your module are backed by valid proofs.
Verification order
- Headers are verified against MMR peaks (trusted roots).
- Accounts are verified against the header
state_root. - Storage is verified against the account
storage_root. - Transactions and receipts are verified against block roots.
This order matters because each step depends on the root validated by the previous step.
Verification cascade
MMR Meta -> Headers -> Accounts -> Storage
state_root storage_root
Where this lives
src/verifiers/contains Cairo0 verifiers.crates/hints/src/verifiers/provides supporting hint logic.
Hints bridge complex operations (like RLP decoding or trie traversal) into Rust while keeping the verification logic inside Cairo0.
MPT Proofs
Merkle Patricia Tries (MPTs) are used to prove Ethereum account and storage data.
What is an MPT?
An MPT is a trie-based authenticated data structure with three node types:
- Branch: up to 16 children + value
- Extension: shared key prefix and next node
- Leaf: final key/value pair
Keys are nibbles derived from keccak256 of the original key.
Proof structure
An MPT proof is a list of encoded nodes from root to leaf. Verification:
- Decode each node (RLP).
- Traverse using the hashed key path.
- Confirm the final value or non-inclusion marker.
HDP performs this verification twice:
- Account proof against the block
state_root - Storage proof against the account
storage_root
Non-inclusion proofs
If a key does not exist, MPT proofs return the empty value (0x80) for the requested slot. The verifier checks this condition explicitly.
Implementation references
src/verifiers/evm/account_verifier.cairosrc/verifiers/evm/storage_item_verifier.cairocrates/hints/src/verifiers/mpt.rs
MMR Proofs
Merkle Mountain Ranges (MMRs) provide an append-only commitment to block headers. They serve as the trusted anchor for historical data access.
MMR basics
- An MMR is a set of perfect binary trees ("peaks") over an append-only sequence.
- Each peak root is hashed into a single commitment.
- Inclusion proofs show that a header hash appears at a given position.
Hash functions
HDP supports two hashing modes:
- Poseidon (default for most chains)
- Keccak (used for specific deployments)
The fetcher can be configured per chain using --mmr-hasher-config.
Verification steps
- Validate the MMR meta (size, root, peak list).
- Verify the header hash against the computed peak.
- Use the verified header as the root of further proofs.
MMR metadata includes the id, size, chain_id, root, and peak list, along with the hashing function.
Implementation references
src/verifiers/evm/header_verifier.cairocrates/hints/src/verifiers/evm/header_verifier.rs
Syscall Handlers Overview
Syscall handlers are the Rust side of the Cairo VM syscall interface. They decode CallContract requests and return the data your Cairo module expects.
Core types
CallContractRequestandCallContractResponsedefine the memory layout.SyscallHandlerowns the routing logic and per-chain handlers.CallHandlerincrates/syscall_handler/src/traits.rsdefines the handler interface.
Keccak syscall
The Keccak syscall selector is handled by KeccakHandler, which exposes Cairo keccak hashing to the VM.
Relevant code:
crates/syscall_handler/src/lib.rscrates/types/src/cairo/new_syscalls.rs
Routing behavior
The handler inspects the contract_address and calldata:
- Special handlers:
'debug','arbitrary_type','injected_state','unconstrained' - All other calls use the chain ID encoded in calldata to select EVM vs Starknet handlers.
The EVM and Starknet handlers use separate internal CallHandlerId enums to select header vs storage vs receipt logic.
EVM call handler IDs
When routing to the EVM handler, the numeric contract_address selects the call handler:
0: header1: account2: storage3: transaction4: receipt5: log
Handler state
Handlers store:
- Dry run: key sets (
HashSet<DryRunKey>) used by the fetcher. - Sound run: memorizer references for verified data.
Dry vs Sound Handlers
Dry run and sound run execute the same Cairo code, but their syscall handlers behave differently.
| Aspect | Dry Run | Sound Run |
|---|---|---|
| Network access | Yes (RPC calls) | No (offline) |
| Determinism | No (live data) | Yes |
| Purpose | Discover keys | Execute with proofs |
| State source | RPC + key collection | Memorizers |
| Handler crate | dry_hint_processor | sound_hint_processor |
Example behavior (header fetch)
Dry run:
fetch header via RPC
insert DryRunKey::Header
return decoded header fields
Sound run:
read header bytes from memorizer
decode RLP
return header fields
The interface presented to Cairo remains the same in both modes.
EVM Handlers
EVM handlers support headers, accounts, storage, transactions, receipts, and logs. They exist in both dry run and sound run variants.
Implementations
- Dry run:
crates/dry_hint_processor/src/syscall_handler/evm/ - Sound run:
crates/sound_hint_processor/src/syscall_handler/evm/
Key types
Handlers share key types defined in crates/types/src/keys/evm/:
HeaderKey(chain_id, block_number)AccountKey(chain_id, block_number, address)StorageKey(chain_id, block_number, address, slot)BlockTxKey(chain_id, block_number, index)BlockReceiptKey(chain_id, block_number, index)LogKey(chain_id, block_number, tx_index, log_index)
Contract address mapping
The Cairo syscall uses numeric contract_address values to select which handler to use:
0: header1: account2: storage3: transaction4: receipt5: log
Data sources
- Dry run: RPC calls fetch and decode data, then record keys.
- Sound run: values are decoded from memorizers filled during verification.
Log handling
Logs are derived from receipt proofs. Log keys hash the same receipt label as receipts, and the sound run extracts log data from the verified receipt payload.
Starknet Handlers
Starknet handlers provide access to block headers and storage values.
Implementations
- Dry run:
crates/dry_hint_processor/src/syscall_handler/starknet/ - Sound run:
crates/sound_hint_processor/src/syscall_handler/starknet/
Key types
Key types live in crates/types/src/keys/starknet/:
HeaderKey(chain_id, block_number)StorageKey(chain_id, block_number, address, storage_slot)
Contract address mapping
The Cairo syscall uses numeric contract_address values to select which handler to use:
0: header1: storage
Data sources
- Dry run: Starknet RPC reads and key collection.
- Sound run: memorizer reads populated during verification.
Memorizers
Memorizers are Cairo0 dictionaries that store verified data for fast lookup during the sound run. Each memorizer maps a hashed key to encoded values.
Why memorizers?
- Keep the sound run fully offline
- Allow deterministic reads in Cairo1
- Separate verification from execution
Key hashing
Key hashing must be consistent across Cairo and Rust. For EVM keys, the system uses Poseidon hashing:
// Cairo: src/memorizers/evm/memorizer.cairo
let hash = poseidon_hash_many([chain_id, block_number, address, ...]);
// Rust: crates/types/src/keys/evm/header.rs (similar pattern)
poseidon_hash_many(&[chain_id, block_number, ...])
If the hash differs, the sound run will not find the requested data.
For transaction and receipt keys, the hash includes a label ('block_tx' or 'block_receipt') to avoid collisions with other key types.
Memorizer types
- EVM: header, account, storage, transaction, receipt, log
- Starknet: header, storage
- Injected state: Patricia-trie-backed custom state
- Unconstrained: bytecode retrieval with code-hash validation
Implementation references
src/memorizers/evm/memorizer.cairosrc/memorizers/starknet/memorizer.cairosrc/memorizers/injected_state/memorizer.cairosrc/memorizers/unconstrained/memorizer.cairo
Injected State
Injected state is a custom, persistent state that can be read and updated across HDP runs. It is backed by a Patricia trie and served by the state_server.
State server API
The state server exposes HTTP endpoints:
POST /create_trieto initialize a trie labelPOST /writeto update key/value pairsGET /readto read key/value pairsGET /get_trie_root_node_idxto fetch the current rootPOST /get_state_proofsto return Patricia proofs
Implementation: crates/state_server/src/lib.rs
The base URL is configured with INJECTED_STATE_BASE_URL.
Set this explicitly when using injected state; defaults differ between components.
Running the state server
cargo run --release --bin state_server -- --host 0.0.0.0 --port 3000 --db_root_path db
Cairo API
The Cairo1 interface lives in hdp_cairo/src/injected_state/state.cairo:
read_injected_state_trie_root(label)->Option<felt252>read_key(label, key)->Option<felt252>write_key(label, key, value)-> updated trie root
Proofs and verification
During the dry run, the handler records ActionRead and ActionWrite entries per trie label. The fetcher turns these actions into StateProofs, which are verified in Cairo0 by:
src/verifiers/injected_state/verify.cairo
Types
Injected state proofs are defined in crates/types/src/proofs/injected_state/:
ActionRead,ActionWrite(dry-run actions)StateProofRead,StateProofWrite(proof payloads)
state_proofs is a list that mixes read and write proofs in execution order.
Typical workflow
- Provide an injected state JSON for dry run and sound run.
- Fetch proofs from the state server during Stage 2.
- Use
InjectedStateMemorizerreads inside your module.
Unconstrained Data
Unconstrained data is used for large blobs (like EVM bytecode) that are too expensive to verify inline. Instead, HDP verifies a hash and then returns the raw data.
Use case: EVM bytecode
The unconstrained memorizer returns bytecode and verifies it against the account code hash:
- Fetch bytecode during dry run and record a key
- During sound run, load the bytecode from memorizer
- Compute
keccak(bytecode)and compare toaccount_get_code_hash
Dry-run behavior
The dry-run handler fetches bytecode via RPC (eth_getCode) and stores it as BytecodeLeWords in the unconstrained key set.
BytecodeLeWords stores the bytecode as little-endian 64-bit words plus a final partial word.
Cairo API
hdp_cairo/src/unconstrained/state.cairo exposes:
evm_account_get_bytecode(hdp, key)->ByteCode
If the hash check fails, execution halts with Account: code hash mismatch.
Output Overview
HDP returns three core outputs after the sound run:
- task_hash: unique identifier for the computation.
- output_root: Merkle root for the computed results.
- mmr_metas: MMR metadata for the headers used (id, size, chain, root).
These outputs are designed for on-chain verification and result extraction.
Output layout
The Cairo output segment is serialized in this order:
task_hash_low
task_hash_high
output_root_low
output_root_high
poseidon_len
keccak_len
poseidon_metas (4 felts each)
keccak_metas (5 felts each)
Poseidon entries are (id, size, chain_id, root). Keccak entries are (id, size, chain_id, root_low, root_high).
Where it is computed
- Task hash:
src/utils/utils.cairo - Output root:
src/utils/merkle.cairo - MMR metas:
src/utils/utils.cairoandsrc/hdp.cairo
Task Hash
The task hash uniquely identifies an HDP computation. It matches the Solidity encoding used by the Satellite contracts.
Formula
task_hash = keccak(
module_hash ||
0x40 ||
public_inputs_len ||
public_inputs
)
0x40 is the Solidity dynamic array offset (64 bytes).
The hash uses only the public inputs provided to the module, not private inputs.
module_hash is the program hash of the compiled Cairo module.
Implementation
See calculate_task_hash in src/utils/utils.cairo.
Output Root
The output root is a Merkle root over computation results. HDP follows the OpenZeppelin StandardMerkleTree approach with double-hashed leaves.
Key properties
- Leaves are hashed twice with Keccak.
- The tree is built over unordered leaves.
- Results are endian-adjusted for Solidity compatibility.
What gets hashed
The leaf list is derived from the Cairo module return data (retdata). The output root commits to the module's results, not the full execution trace.
Implementation
See compute_merkle_root in src/utils/merkle.cairo.
Cairo Library Overview
The hdp_cairo package exposes the Cairo1 API for accessing verified data in HDP. Your Cairo module receives an HDP instance with memorizers for each data domain.
HDP struct
pub struct HDP {
pub evm: EvmMemorizer,
pub starknet: StarknetMemorizer,
pub injected_state: InjectedStateMemorizer,
pub unconstrained: UnconstrainedMemorizer,
}
Source: hdp_cairo/src/lib.cairo
Basic usage
#[starknet::contract]
mod example {
use hdp_cairo::HDP;
use hdp_cairo::evm::header::{HeaderKey, HeaderImpl};
#[storage]
struct Storage {}
#[external(v0)]
pub fn main(ref self: ContractState, hdp: HDP, block_number: u32) -> u256 {
hdp.evm.header_get_timestamp(
HeaderKey { chain_id: 0x1, block_number: block_number.into() }
)
}
}
Your module should expose a main entrypoint (or a chosen external entrypoint) that receives an HDP instance and any module-specific inputs.
The rest of this section documents the module-specific APIs.
EVM API
The EVM API provides access to headers, accounts, storage, transactions, receipts, and logs. All functions live under hdp_cairo::evm::*.
Keys
HeaderKey { chain_id, block_number }AccountKey { chain_id, block_number, address }StorageKey { chain_id, block_number, address, storage_slot }BlockTxKey { chain_id, block_number, transaction_index }BlockReceiptKey { chain_id, block_number, transaction_index }LogKey { chain_id, block_number, transaction_index, log_index }
Chain ID constants:
ETHEREUM_MAINNET_CHAIN_IDETHEREUM_TESTNET_CHAIN_IDOPTIMISM_MAINNET_CHAIN_IDOPTIMISM_TESTNET_CHAIN_ID
Header methods
Available getters:
header_get_parentheader_get_uncleheader_get_coinbaseheader_get_state_rootheader_get_transaction_rootheader_get_receipt_rootheader_get_bloomheader_get_difficultyheader_get_numberheader_get_gas_limitheader_get_gas_usedheader_get_timestampheader_get_mix_hashheader_get_nonceheader_get_base_fee_per_gasheader_get_blob_gas_usedheader_get_excess_blob_gasheader_get_requests_hash
Source: hdp_cairo/src/evm/header.cairo
Return types:
- Most header getters return
u256. header_get_bloomreturns aByteArray(logs bloom).
Not yet exposed:
header_get_extra_dataheader_get_withdrawals_rootheader_get_parent_beacon_block_root
Account methods
account_get_nonceaccount_get_balanceaccount_get_state_rootaccount_get_code_hash
Source: hdp_cairo/src/evm/account.cairo
Return types: all account getters return u256.
Storage methods
storage_get_slot
Source: hdp_cairo/src/evm/storage.cairo
Return type: u256.
Transaction methods
block_tx_get_nonceblock_tx_get_gas_priceblock_tx_get_gas_limitblock_tx_get_receiverblock_tx_get_valueblock_tx_get_vblock_tx_get_rblock_tx_get_sblock_tx_get_chain_idblock_tx_get_max_fee_per_gasblock_tx_get_max_priority_fee_per_gasblock_tx_get_max_fee_per_blob_gasblock_tx_get_tx_typeblock_tx_get_senderblock_tx_get_hash
Source: hdp_cairo/src/evm/block_tx.cairo
Return types: all transaction getters return u256.
Not yet exposed:
block_tx_get_inputblock_tx_get_access_listblock_tx_get_blob_versioned_hashesblock_tx_get_authorization_list
Receipt methods
block_receipt_get_statusblock_receipt_get_cumulative_gas_usedblock_receipt_get_bloom
Source: hdp_cairo/src/evm/block_receipt.cairo
Return types:
- Status and gas used return
u256. block_receipt_get_bloomreturns aByteArray.
Log methods
log_get_addresslog_get_topic0log_get_topic1log_get_topic2log_get_topic3log_get_topic4log_get_data
Source: hdp_cairo/src/evm/log.cairo
Return types:
- Address and topics return
u256. log_get_datareturnsArray<u128>.
Example
use hdp_cairo::HDP;
use hdp_cairo::evm::{ETHEREUM_MAINNET_CHAIN_ID, account::{AccountImpl, AccountKey}};
let key = AccountKey {
chain_id: ETHEREUM_MAINNET_CHAIN_ID,
block_number: 18_500_000,
address: 0x1234,
};
let balance = hdp.evm.account_get_balance(key);
Starknet API
The Starknet API provides access to block headers and storage values. All functions live under hdp_cairo::starknet::*.
Header methods
header_get_block_numberheader_get_state_rootheader_get_sequencer_addressheader_get_block_timestampheader_get_transaction_countheader_get_transaction_commitmentheader_get_event_countheader_get_event_commitmentheader_get_parent_block_hashheader_get_state_diff_commitmentheader_get_state_diff_lengthheader_get_l1_gas_price_in_weiheader_get_l1_gas_price_in_friheader_get_l1_data_gas_price_in_weiheader_get_l1_data_gas_price_in_friheader_get_receipts_commitmentheader_get_l1_data_modeheader_get_protocol_version
Source: hdp_cairo/src/starknet/header.cairo
Return type: all header getters return felt252.
Storage method
storage_get_slot
Source: hdp_cairo/src/starknet/storage.cairo
Return type: felt252.
Chain ID constants
STARKNET_MAINNET_CHAIN_IDSTARKNET_TESTNET_CHAIN_ID
Example
use hdp_cairo::HDP;
use hdp_cairo::starknet::{STARKNET_MAINNET_CHAIN_ID, header::{HeaderImpl, HeaderKey}};
let key = HeaderKey { chain_id: STARKNET_MAINNET_CHAIN_ID, block_number: 1_000_000 };
let sequencer = hdp.starknet.header_get_sequencer_address(key);
Injected State API
Injected state provides a persistent key/value store backed by a Patricia trie. Use it when your computation needs state across runs.
Methods
read_injected_state_trie_root(label)->Option<felt252>read_key(label, key)->Option<felt252>write_key(label, key, value)-> updated trie root
Source: hdp_cairo/src/injected_state/state.cairo
Example
use hdp_cairo::HDP;
use hdp_cairo::injected_state::state::InjectedStateMemorizerTrait;
let label = 1;
let value = hdp.injected_state.read_key(label, 0x42);
eth_call
HDP includes a provable EVM interpreter that can execute eth_call-style requests inside Cairo.
Entry point
hdp_cairo re-exports the helper:
execute_eth_call(hdp, time_and_space, sender, target, calldata)
Source: hdp_cairo/src/eth_call/execute_call.cairo
What it does
- Builds an EIP-1559 transaction wrapper for the call
- Executes an EVM interpreter in Cairo
- Reads required state through HDP memorizers
- Returns a
TransactionResultwith success flag and return data
Supported instructions
The interpreter is implemented in hdp_cairo/src/eth_call/evm/instructions/. If you need to verify opcode coverage, start there and in hdp_cairo/src/eth_call/evm/instructions.cairo.
Limitations
- The interpreter is designed for
eth_call-style execution (no state mutation). - Opcode coverage is intentionally scoped; check the instruction modules for details.
Example
use hdp_cairo::{HDP, execute_eth_call};
use hdp_cairo::eth_call::hdp_backend::TimeAndSpace;
use starknet::EthAddress;
let time_and_space = TimeAndSpace { chain_id: 0x1, block_number: 18_500_000 };
let result = execute_eth_call(
@hdp,
@time_and_space,
EthAddress::from(0x01),
EthAddress::from(0x02),
array![0x12, 0x34].span(),
);
STWO Integration
HDP can emit STWO prover inputs during the sound run. This requires building the CLI with the stwo feature.
Build with STWO
cargo +nightly build --release --bin hdp-cli --features stwo
Generate prover input
hdp sound-run \
-m <module.compiled_contract_class.json> \
--proof_mode \
--stwo_prover_input stwo_input.json
Notes:
--proof_modeis required.- If the binary was not built with
--features stwo, HDP will fail with a clear error. - The sound run uses the
all_cairo_stwolayout internally.
Implementation: crates/sound_run/src/prove.rs
StarkGate Solvency Example
This example compares StarkGate balances between Ethereum L1 and Starknet L2.
Location
examples/starkgate/
Build
cd examples/starkgate
scarb build
Run (source build)
The example includes a script:
./run.sh
It performs:
dry_runonexample_starkgate_modulefetcherto buildproofs.jsonsound_runto verify and execute
Run (CLI)
If you installed the hdp binary:
hdp dry-run -m target/dev/example_starkgate_module.compiled_contract_class.json --print_output
hdp fetch-proofs
hdp sound-run -m target/dev/example_starkgate_module.compiled_contract_class.json --print_output
What to look for
The output prints the task hash, output root, and MMR metadata for the accessed headers.
Multi-Chain Example
This example shows how to combine data from multiple chains in a single HDP module.
Location
examples/multi_chain_access/
Build
cd examples/multi_chain_access
scarb build
Run (source build)
./run.sh
The script uses a custom MMR hasher config:
cargo run --bin fetcher -- --mmr-hasher-config examples/multi_chain_access/mmr_hasher_config.json
Run (CLI)
hdp dry-run -m target/dev/example_multi_chain_access_module.compiled_contract_class.json --print_output
hdp fetch-proofs --mmr-hasher-config examples/multi_chain_access/mmr_hasher_config.json
hdp sound-run -m target/dev/example_multi_chain_access_module.compiled_contract_class.json --print_output
Injected State Example
This example demonstrates how to use injected state with a local state server.
Location
examples/injected_state/
Build
cd examples/injected_state
scarb build
Run (source build)
The script starts the state server and runs the pipeline:
./run.sh
Key details:
state_serverruns in the background.--injected_state examples/injected_state/injected_state.jsonis passed to dry run and sound run.
Run (CLI)
If you use the hdp binary, start the state server first:
cargo run --release --bin state_server
Then run the pipeline with --injected_state pointing to the JSON file.
If your state server runs on a non-default host/port, export INJECTED_STATE_BASE_URL before running the pipeline.
CLI Reference
The CLI binary is called hdp (built from the hdp-cli crate).
Usage
hdp <command> [options]
Commands
dry-run: run Stage 1 and generatedry_run_output.jsonfetch-proofs: run Stage 2 and generateproofs.jsonsound-run: run Stage 3 and print outputsprogram-hash: compute program hash for a compiled Cairo programenv-info: print expected environment variablesenv-check: check required RPC env vars for a dry run outputlink: symlink thehdp_cairolibrary into a projectupdate: re-run the install scriptpwd: print the HDP repository path
Common flags
--log-level <LEVEL>: trace, debug, info, warn, error--debug: shortcut for--log-level debug
dry-run
hdp dry-run --compiled_module <path> [--inputs <path>] [--injected_state <path>]
Options:
--program <path>(-p): compiled dry-run program override--compiled_module <path>(-m): compiled Cairo1 module--inputs <path>(-i): JSON module input params--injected_state <path>(-s): injected state JSON--output <path>(-o): output path (default:dry_run_output.json)--print_output: print the Cairo output segment
fetch-proofs
hdp fetch-proofs [--inputs dry_run_output.json] [--output proofs.json]
Options:
--inputs <path>(-i): dry run output (default:dry_run_output.json)--output <path>(-o): output path (default:proofs.json)--mmr-hasher-config <path>: chain-specific hasher config--mmr-deployment-config <path>: chain deployment config
sound-run
hdp sound-run --compiled_module <path> --proofs proofs.json
Options:
--program <path>(-p): compiled sound-run program override--compiled_module <path>(-m): compiled Cairo1 module--inputs <path>(-i): JSON module input params--injected_state <path>(-s): injected state JSON--print_output: print the Cairo output segment--proof_mode: enable proof-mode execution--cairo_pie <path>: write a Cairo PIE zip--stwo_prover_input <path>: write STWO prover input (requires--features stwo)
env-check
hdp env-check --inputs dry_run_output.json
env-info
hdp env-info
Prints an example .env file and the variables required by HDP.
program-hash
hdp program-hash [--program <path>]
Computes the program hash of a compiled Cairo program. If omitted, it uses the default HDP program.
link
hdp link
Creates a hdp_cairo symlink in the current directory for local Scarb development.
update
hdp update [--clean] [--local]
--clean: clean build outputs before rebuilding.--local: build from the existing local repo without pulling from GitHub.
pwd
hdp pwd
Prints the path to the HDP repository directory.
Configuration
HDP reads configuration from environment variables. The CLI loads .env automatically via dotenvy.
RPC endpoints
These variables are listed in example.env:
RPC_URL_ETHEREUM_MAINNET=
RPC_URL_ETHEREUM_TESTNET=
RPC_URL_OPTIMISM_MAINNET=
RPC_URL_OPTIMISM_TESTNET=
RPC_URL_STARKNET_MAINNET=
RPC_URL_STARKNET_TESTNET=
RPC_URL_HERODOTUS_INDEXER=https://rs-indexer.api.herodotus.cloud/
Injected state:
INJECTED_STATE_BASE_URL=http://localhost:3000
Set this explicitly when using injected state to avoid mismatched defaults between dry run and fetcher.
Use hdp env-check --inputs dry_run_output.json to see which RPCs are required for a given run.
Fetcher uses RPC_URL_HERODOTUS_INDEXER and INJECTED_STATE_BASE_URL when proofs are needed.
Logging
Set the log level with either:
--log-level <LEVEL>/--debugRUST_LOG=<LEVEL>
Advanced
HDP_SOUND_RUN_PATH: override the compiled sound-run program path
Types Reference
This page summarizes the most important Rust and Cairo types used by HDP.
Rust input/output types
| Type | Description | Location |
|---|---|---|
HDPDryRunInput | Compiled class, params, injected state | crates/types/src/lib.rs |
HDPInput | Compiled class, params, proofs, injected state | crates/types/src/lib.rs |
ProofsData | chain_proofs, state_proofs, unconstrained | crates/types/src/lib.rs |
HDPDryRunOutput | Task hash and output root fields | crates/types/src/lib.rs |
HDPOutput | Task hash, output root, mmr_metas | crates/types/src/lib.rs |
MmrMetaOutput | Poseidon or Keccak MMR metadata | crates/types/src/lib.rs |
StateProofs | Injected state proof list | crates/types/src/proofs/injected_state/ |
InjectedState | Initial injected state map | crates/types/src/lib.rs |
UnconstrainedState | Unconstrained data map | crates/types/src/lib.rs |
Chain identifiers
| Type | Description |
|---|---|
ChainIds | Enum of supported chains |
ChainProofs | Per-chain proof bundles |
HashingFunction | Poseidon or Keccak |
ProofsData (the proofs.json file) is structured as:
{
"chain_proofs": [ ... ],
"state_proofs": [ ... ],
"unconstrained": { ... }
}
state_proofs is ordered and can include both read and write proofs.
Chain ID constants (Cairo)
The Cairo library exposes chain ID constants:
hdp_cairo::evm::{ETHEREUM_MAINNET_CHAIN_ID, ETHEREUM_TESTNET_CHAIN_ID, OPTIMISM_MAINNET_CHAIN_ID, OPTIMISM_TESTNET_CHAIN_ID}hdp_cairo::starknet::{STARKNET_MAINNET_CHAIN_ID, STARKNET_TESTNET_CHAIN_ID}
Dry run keys
Dry run handlers collect keys in a DryRunKey enum:
Header,Account,Storage,Tx,Receiptfor EVM- Starknet keys are tracked in the Starknet handler
Source: crates/dry_hint_processor/src/syscall_handler/
Injected state and unconstrained handlers also define their own DryRunKey enums for state actions and bytecode.
Cairo key structs
These are used when calling hdp_cairo APIs:
- EVM:
HeaderKey,AccountKey,StorageKey,BlockTxKey,BlockReceiptKey,LogKey - Starknet:
HeaderKey,StorageKey
Output fields
The sound run output segment is decoded as:
task_hash_low, task_hash_high, output_root_low, output_root_high,
poseidon_len, keccak_len,
poseidon_metas (4 felts each), keccak_metas (5 felts each)
MmrMetaOutput is encoded as:
- Poseidon:
id, size, chain_id, root - Keccak:
id, size, chain_id, root_low, root_high
Glossary
- Dry run: Simulation phase that discovers which on-chain keys are needed.
- Fetcher: Stage that collects cryptographic proofs for the discovered keys.
- Sound run: Offline verification and execution phase.
- Memorizer: Dict-based storage for verified data in Cairo0.
- MMR: Merkle Mountain Range accumulator for block headers.
- MPT: Merkle Patricia Trie for Ethereum state proofs.
- Task hash: Unique identifier for a computation.
- Output root: Merkle root of computed results.
- Injected state: Custom persistent state backed by a Patricia trie.
- Unconstrained data: Data verified by hash, not by inclusion proof.
- STWO: Zero-knowledge prover for Cairo.
- Bootloader: Cairo0 program that executes compiled Cairo1 modules.
Debugging Guide
This page collects common troubleshooting steps for HDP pipelines and Cairo modules.
Logging and verbosity
The CLI uses tracing and respects both flags and environment variables:
--log-level <trace|debug|info|warn|error>sets the level explicitly.--debugis a shortcut for--log-level debug.RUST_LOG=debugworks for all binaries (dry run, fetcher, sound run).
Check your environment
Most failures in Stage 1 and Stage 2 are missing RPC endpoints. Use:
hdp env-info
hdp env-check --inputs dry_run_output.json
env-check inspects the dry run output and reports only the RPCs needed for that run.
Inspect pipeline artifacts
dry_run_output.jsonshould contain key sets for EVM, Starknet, injected state, and unconstrained data.proofs.jsonshould containchain_proofs,state_proofs, andunconstrainedsections.- Use
--print_outputon dry run or sound run to print computed outputs.
If you suspect a mismatch, re-run the pipeline in order and make sure the compiled module and inputs have not changed between stages.
Cairo module debugging
You can use println! inside Cairo1 contracts. To allow it during development, disable the validation pass:
[[target.starknet-contract]]
allowed-libfuncs-deny = true
#[starknet::contract]
mod contract {
use hdp_cairo::HDP;
#[storage]
struct Storage {}
#[external(v0)]
pub fn main(ref self: ContractState, hdp: HDP) {
println!("Hello, world!");
}
}
Sound run is offline
Sound run does not hit the network. If you see missing data errors, confirm that:
proofs.jsonwas generated from the samedry_run_output.json.- The proof file includes the chain you queried and the required proof types.
Injected state issues
If injected state reads fail, confirm that INJECTED_STATE_BASE_URL points to the correct state server and that the server is running.
Prover outputs
For proof-mode debugging:
sound-run --proof_modeenables trace generation.sound-run --cairo_pie <path>writes a Cairo PIE zip.sound-run --stwo_prover_input <path>writes STWO inputs (requires--features stwo).