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:

  1. Dry run: execute the Cairo module against live RPCs and collect the keys you touched.
  2. Fetcher: download proofs for every key (MMR for headers, MPT/Patricia for state).
  3. 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:

  • Architecture for how the system fits together.
  • Pipeline to understand each stage and its inputs/outputs.
  • Verification and State Management for proof details.
  • Cairo Library for the public API you use in modules.
  • Examples and Reference for 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

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-info prints the expected env vars and example values.
  • hdp env-check --inputs dry_run_output.json checks 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.json is produced after Stage 1 and contains the key set.
  • proofs.json is produced after Stage 2 and contains cryptographic proofs.
  • Stage 3 prints task_hash, output_root, and mmr_metas.

CLI quick reference

  • hdp dry-run: simulate and collect keys
  • hdp fetch-proofs: fetch MMR/MPT proofs
  • hdp sound-run: verify and execute offline
  • hdp env-info: print required env vars
  • hdp 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_cairo library 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_syscall via the hdp_cairo library.
  • 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, generates dry_run_output.json
  • hdp fetch-proofs: stage 2, generates proofs.json
  • hdp sound-run: stage 3, returns task_hash, output_root, and mmr_metas

Rust Crates

This repository is organized into focused Rust crates. The most important ones are listed below.

CratePurposeNotes
cliUser-facing CLI (hdp)Subcommands: dry-run, fetch-proofs, sound-run
dry_runStage 1 executionRuns Cairo VM with RPC-backed handlers
fetcherStage 2 proof collectionBuilds ProofsData from key sets
sound_runStage 3 executionOffline run with verified proofs
dry_hint_processorDry-run syscall handlersRPC reads and key collection
sound_hint_processorSound-run syscall handlersReads from memorizers
syscall_handlerShared syscall dispatchRoutes to EVM/Starknet/injected state
hintsCairo hint implementations250+ hints for VM integration
typesShared types and schemasInputs/outputs, keys, proofs
indexer_clientHerodotus Indexer API clientMMR proof requests
state_serverInjected state serverPatricia 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

  1. A Cairo1 module calls a method from hdp_cairo, which internally uses call_contract_syscall.
  2. The Cairo VM forwards the syscall to the Rust SyscallHandler.
  3. The handler decodes a CallContractRequest, routes it, and writes a CallContractResponse.
  4. The result is decoded back in Cairo1 and returned to the caller.

Syscall selectors

HDP handles two syscall selectors:

  • CallContract for all data access paths.
  • Keccak for 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/*.cairo and hdp_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_processor handlers that make RPC calls and collect keys.
  • Sound run uses sound_hint_processor handlers 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 (ProofsData with chain proofs and injected state proofs)
  • Stage 3 output: task_hash, output_root, and mmr_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

  1. Load the compiled Cairo module.
  2. Construct HDPDryRunInput (compiled class, params, injected state).
  3. Run the Cairo VM with CustomHintProcessor.
  4. Syscall handlers call RPC endpoints and collect keys.
  5. Write dry_run_output.json with the serialized handler state.

The dry run uses the all_cairo layout in the Cairo VM.

Output artifacts

  • dry_run_output.json: serialized SyscallHandler containing key sets for each handler.
  • Printed output (optional): HDPDryRunOutput with task_hash and output_root fields.

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 for dry_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_getProof RPC calls
  • Transactions/Receipts: MPT proofs built from RPC data
  • Injected state: State server get_state_proofs endpoint
  • 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 / EthereumSepolia
  • OptimismMainnet / OptimismSepolia
  • StarknetMainnet / StarknetSepolia

state_proofs is a list of injected state proofs (read/write).

Inputs and environment

  • dry_run_output.json is required (default input).
  • RPC_URL_HERODOTUS_INDEXER must be set for MMR proofs.
  • INJECTED_STATE_BASE_URL is 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

  1. Verification: Cairo0 verifiers check all proofs and populate memorizers.
  2. Execution: the bootloader executes the Cairo1 module using the memorizers.
  3. Output: compute task_hash, output_root, and mmr_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_high
  • output_tree_root_low / output_tree_root_high
  • mmr_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

  1. Headers are verified against MMR peaks (trusted roots).
  2. Accounts are verified against the header state_root.
  3. Storage is verified against the account storage_root.
  4. 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:

  1. Decode each node (RLP).
  2. Traverse using the hashed key path.
  3. 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.cairo
  • src/verifiers/evm/storage_item_verifier.cairo
  • crates/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

  1. Validate the MMR meta (size, root, peak list).
  2. Verify the header hash against the computed peak.
  3. 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.cairo
  • crates/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

  • CallContractRequest and CallContractResponse define the memory layout.
  • SyscallHandler owns the routing logic and per-chain handlers.
  • CallHandler in crates/syscall_handler/src/traits.rs defines 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.rs
  • crates/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: header
  • 1: account
  • 2: storage
  • 3: transaction
  • 4: receipt
  • 5: 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.

AspectDry RunSound Run
Network accessYes (RPC calls)No (offline)
DeterminismNo (live data)Yes
PurposeDiscover keysExecute with proofs
State sourceRPC + key collectionMemorizers
Handler cratedry_hint_processorsound_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: header
  • 1: account
  • 2: storage
  • 3: transaction
  • 4: receipt
  • 5: 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: header
  • 1: 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.cairo
  • src/memorizers/starknet/memorizer.cairo
  • src/memorizers/injected_state/memorizer.cairo
  • src/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_trie to initialize a trie label
  • POST /write to update key/value pairs
  • GET /read to read key/value pairs
  • GET /get_trie_root_node_idx to fetch the current root
  • POST /get_state_proofs to 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

  1. Provide an injected state JSON for dry run and sound run.
  2. Fetch proofs from the state server during Stage 2.
  3. Use InjectedStateMemorizer reads 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 to account_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:

  1. task_hash: unique identifier for the computation.
  2. output_root: Merkle root for the computed results.
  3. 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.cairo and src/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_ID
  • ETHEREUM_TESTNET_CHAIN_ID
  • OPTIMISM_MAINNET_CHAIN_ID
  • OPTIMISM_TESTNET_CHAIN_ID

Header methods

Available getters:

  • header_get_parent
  • header_get_uncle
  • header_get_coinbase
  • header_get_state_root
  • header_get_transaction_root
  • header_get_receipt_root
  • header_get_bloom
  • header_get_difficulty
  • header_get_number
  • header_get_gas_limit
  • header_get_gas_used
  • header_get_timestamp
  • header_get_mix_hash
  • header_get_nonce
  • header_get_base_fee_per_gas
  • header_get_blob_gas_used
  • header_get_excess_blob_gas
  • header_get_requests_hash

Source: hdp_cairo/src/evm/header.cairo

Return types:

  • Most header getters return u256.
  • header_get_bloom returns a ByteArray (logs bloom).

Not yet exposed:

  • header_get_extra_data
  • header_get_withdrawals_root
  • header_get_parent_beacon_block_root

Account methods

  • account_get_nonce
  • account_get_balance
  • account_get_state_root
  • account_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_nonce
  • block_tx_get_gas_price
  • block_tx_get_gas_limit
  • block_tx_get_receiver
  • block_tx_get_value
  • block_tx_get_v
  • block_tx_get_r
  • block_tx_get_s
  • block_tx_get_chain_id
  • block_tx_get_max_fee_per_gas
  • block_tx_get_max_priority_fee_per_gas
  • block_tx_get_max_fee_per_blob_gas
  • block_tx_get_tx_type
  • block_tx_get_sender
  • block_tx_get_hash

Source: hdp_cairo/src/evm/block_tx.cairo

Return types: all transaction getters return u256.

Not yet exposed:

  • block_tx_get_input
  • block_tx_get_access_list
  • block_tx_get_blob_versioned_hashes
  • block_tx_get_authorization_list

Receipt methods

  • block_receipt_get_status
  • block_receipt_get_cumulative_gas_used
  • block_receipt_get_bloom

Source: hdp_cairo/src/evm/block_receipt.cairo

Return types:

  • Status and gas used return u256.
  • block_receipt_get_bloom returns a ByteArray.

Log methods

  • log_get_address
  • log_get_topic0
  • log_get_topic1
  • log_get_topic2
  • log_get_topic3
  • log_get_topic4
  • log_get_data

Source: hdp_cairo/src/evm/log.cairo

Return types:

  • Address and topics return u256.
  • log_get_data returns Array<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_number
  • header_get_state_root
  • header_get_sequencer_address
  • header_get_block_timestamp
  • header_get_transaction_count
  • header_get_transaction_commitment
  • header_get_event_count
  • header_get_event_commitment
  • header_get_parent_block_hash
  • header_get_state_diff_commitment
  • header_get_state_diff_length
  • header_get_l1_gas_price_in_wei
  • header_get_l1_gas_price_in_fri
  • header_get_l1_data_gas_price_in_wei
  • header_get_l1_data_gas_price_in_fri
  • header_get_receipts_commitment
  • header_get_l1_data_mode
  • header_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_ID
  • STARKNET_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 TransactionResult with 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_mode is required.
  • If the binary was not built with --features stwo, HDP will fail with a clear error.
  • The sound run uses the all_cairo_stwo layout 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_run on example_starkgate_module
  • fetcher to build proofs.json
  • sound_run to 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_server runs in the background.
  • --injected_state examples/injected_state/injected_state.json is 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 generate dry_run_output.json
  • fetch-proofs: run Stage 2 and generate proofs.json
  • sound-run: run Stage 3 and print outputs
  • program-hash: compute program hash for a compiled Cairo program
  • env-info: print expected environment variables
  • env-check: check required RPC env vars for a dry run output
  • link: symlink the hdp_cairo library into a project
  • update: re-run the install script
  • pwd: 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.

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> / --debug
  • RUST_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

TypeDescriptionLocation
HDPDryRunInputCompiled class, params, injected statecrates/types/src/lib.rs
HDPInputCompiled class, params, proofs, injected statecrates/types/src/lib.rs
ProofsDatachain_proofs, state_proofs, unconstrainedcrates/types/src/lib.rs
HDPDryRunOutputTask hash and output root fieldscrates/types/src/lib.rs
HDPOutputTask hash, output root, mmr_metascrates/types/src/lib.rs
MmrMetaOutputPoseidon or Keccak MMR metadatacrates/types/src/lib.rs
StateProofsInjected state proof listcrates/types/src/proofs/injected_state/
InjectedStateInitial injected state mapcrates/types/src/lib.rs
UnconstrainedStateUnconstrained data mapcrates/types/src/lib.rs

Chain identifiers

TypeDescription
ChainIdsEnum of supported chains
ChainProofsPer-chain proof bundles
HashingFunctionPoseidon 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, Receipt for 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.
  • --debug is a shortcut for --log-level debug.
  • RUST_LOG=debug works 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.json should contain key sets for EVM, Starknet, injected state, and unconstrained data.
  • proofs.json should contain chain_proofs, state_proofs, and unconstrained sections.
  • Use --print_output on 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.json was generated from the same dry_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_mode enables trace generation.
  • sound-run --cairo_pie <path> writes a Cairo PIE zip.
  • sound-run --stwo_prover_input <path> writes STWO inputs (requires --features stwo).