Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
65a2730
feat(zk-dex): rewrite SP1 binary to use DexCircuit + AppProgramInput
Zena-park Feb 23, 2026
c8e0150
feat(zk-dex): convert ProgramInput to AppProgramInput in serialize_input
Zena-park Feb 23, 2026
e41c3e6
docs(zk): add ZK-DEX project progress summary
Zena-park Feb 23, 2026
edd4764
feat(zk-dex): add L1 deployer pipeline for ZK-DEX guest program regis…
Zena-park Feb 23, 2026
48329d5
fix(zk-dex): resolve SP1 guest crash and add Timelock VK registration
Zena-park Feb 24, 2026
63263c5
feat(zk-dex): add one-command localnet setup script
Zena-park Feb 24, 2026
48cb576
fix(zk-dex): increase localnet timeouts and fix output formatting
Zena-park Feb 24, 2026
9bdb341
feat(zk-dex): add Docker-based E2E localnet with real SP1 ZK proofs
Zena-park Feb 24, 2026
466b91a
feat(zk-dex): add withdrawal tracker page and permissionless batch ve…
Zena-park Feb 24, 2026
2675405
fix(zk-dex): resolve SP1 proof verification failure (00e) and add Doc…
Zena-park Feb 24, 2026
4fae4e0
fix(zk-dex): modularize handlers and fix SP1 proof verification (00e)
Zena-park Feb 24, 2026
ce4098b
test(zk-dex): add byte-level tests for withdrawal, deposit, and gas f…
Zena-park Feb 24, 2026
0753b82
fix(zk-dex): use block header gas for fee distribution and conditiona…
Zena-park Feb 24, 2026
d68aa1d
feat(l2): add empty batch auto-verification without ZK proof
Zena-park Feb 24, 2026
6b7716d
fix(l2): fix empty batch handling in committer and proof sender
Zena-park Feb 24, 2026
2e9f7ce
feat(zk-dex): implement full circuit with 7 new operations
Zena-park Feb 24, 2026
de7081b
fix(zk-dex): add settleOrder old-note slots to witness analyzer
Zena-park Feb 24, 2026
80b8801
fix(zk-dex): resolve missing storage proofs for dependent slots in wi…
Zena-park Feb 24, 2026
ac027c0
test(zk-dex): add settleOrder lifecycle test and harden input validation
Zena-park Feb 24, 2026
3bb463c
docs(zk-dex): add L2 genesis deployment plan for ZkDex contracts
Zena-park Feb 24, 2026
566f73a
feat(zk-dex): add L2 genesis deployment pipeline and fix storage slot…
Zena-park Feb 26, 2026
9578b34
fix(zk-dex): fix localnet L2 startup (--no-monitor, P2P port conflict)
Zena-park Feb 26, 2026
185a2dd
fix(zk-dex): set ETHREX_GUEST_PROGRAM_ID env var for correct programT…
Zena-park Feb 26, 2026
0908f67
docs(zk-dex): add E2E benchmark results and update progress document
Zena-park Feb 26, 2026
795e829
fix(l2): add genesis state root verification and fix prover version m…
Zena-park Feb 26, 2026
ce38bf2
feat(l2): trigger early batch commit on pending withdrawals
Zena-park Feb 26, 2026
7f2c47a
style(l2): run cargo fmt on zk-dex e2e code
Zena-park Mar 1, 2026
8f47dea
style(docker): simplify solc installation in Dockerfile.sp1
Zena-park Mar 1, 2026
e4795d9
fix(zk): remove duplicate code outside cfg(feature = "l2") in zk_dex …
Zena-park Mar 1, 2026
08df3fd
Merge remote-tracking branch 'origin/pr/zk/02-circuit-framework' into…
Zena-park Mar 3, 2026
b4143d4
style: fix clippy warnings for CI lint pass
Zena-park Mar 3, 2026
856de48
fix(l2): include empty blocks in blob for KZG proof consistency
Zena-park Mar 3, 2026
9c8eaa5
fix(l2): regenerate blob bundle for empty blocks and fix Makefile env
Zena-park Mar 3, 2026
a0f3d97
style: fix rustfmt formatting
Zena-park Mar 3, 2026
0f7495e
fix(l2): skip early withdrawal commit in based mode
Zena-park Mar 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ GEMINI.md
dev_ethrex_l1/
dev_ethrex_l2/

# ZK-DEX localnet runtime directory
.zk-dex-localnet/

# Logs
logs/

Expand Down
127 changes: 127 additions & 0 deletions Dockerfile.sp1
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# Dockerfile.sp1 — SP1 ZK-DEX prover image with GPU support
#
# Extends the standard ethrex Dockerfile with SP1 toolchain installation
# for building and proving with real ZK proofs.
#
# Build:
# docker build -f Dockerfile.sp1 -t ethrex:sp1 .
#
# Build args:
# GUEST_PROGRAMS: Comma-separated guest programs (default: evm-l2,zk-dex)

FROM rust:1.90 AS chef

RUN apt-get update && apt-get install -y \
build-essential \
clang \
libclang-dev \
libc6 \
libssl-dev \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
RUN cargo install cargo-chef

WORKDIR /ethrex


# --- Planner Stage ---
FROM chef AS planner

COPY benches ./benches
COPY crates ./crates
COPY metrics ./metrics
COPY cmd ./cmd
COPY test ./test
COPY tooling ./tooling
COPY Cargo.* .
COPY .cargo/ ./.cargo

RUN cargo chef prepare --recipe-path recipe.json


# --- Builder Stage ---
FROM chef AS builder

ARG PROFILE="release"
ARG BUILD_FLAGS="--features l2,l2-sql,sp1"
ARG GUEST_PROGRAMS="evm-l2,zk-dex"

# Install SP1 toolchain (succinct)
ENV SP1_DIR=/root/.sp1
RUN curl -L https://sp1.succinct.xyz | bash \
&& ${SP1_DIR}/bin/sp1up

# Add succinct toolchain to PATH
ENV PATH="${SP1_DIR}/bin:${PATH}"
ENV RUSTUP_TOOLCHAIN=succinct

# Dependency cache
COPY --from=planner /ethrex/recipe.json recipe.json
RUN RUSTUP_TOOLCHAIN=stable cargo chef cook --release --recipe-path recipe.json $BUILD_FLAGS

# Install solc 0.8.31
RUN SOLC_URL="https://github.com/ethereum/solidity/releases/download/v0.8.31/solc-static-linux"; \
if [ "$(uname -m)" = "aarch64" ]; then SOLC_URL="${SOLC_URL}-arm"; fi; \
curl -L -o /usr/bin/solc "$SOLC_URL" && chmod +x /usr/bin/solc

# Copy source
COPY benches ./benches
COPY crates ./crates
COPY cmd ./cmd
COPY metrics ./metrics
COPY tooling ./tooling
COPY fixtures/genesis ./fixtures/genesis
COPY .git ./.git
COPY Cargo.* ./
COPY fixtures ./fixtures
COPY .cargo/ ./.cargo

ENV COMPILE_CONTRACTS=true
ENV GUEST_PROGRAMS=${GUEST_PROGRAMS}

# Build with SP1 features and guest programs
# RUSTUP_TOOLCHAIN=stable ensures the host build uses stable,
# while sp1_build internally uses the succinct toolchain for guest programs
RUN RUSTUP_TOOLCHAIN=stable cargo build --profile $PROFILE $BUILD_FLAGS

RUN mkdir -p /ethrex/bin && \
cp /ethrex/target/${PROFILE}/ethrex /ethrex/bin/ethrex


# --- Final Image ---
FROM ubuntu:24.04
WORKDIR /usr/local/bin

RUN apt-get update && apt-get install -y --no-install-recommends \
libssl3 \
ca-certificates \
curl \
&& rm -rf /var/lib/apt/lists/*

# Install Docker CLI (needed by SP1 Groth16 gnark wrapper)
RUN curl -fsSL https://download.docker.com/linux/static/stable/$(uname -m)/docker-27.5.1.tgz \
| tar xz --strip-components=1 -C /usr/local/bin docker/docker

COPY cmd/ethrex/networks ./cmd/ethrex/networks
COPY --from=builder /ethrex/bin/ethrex .

# Copy guest program VK files so the deployer can register them on L1.
# The deployer resolves VK paths via canonicalize() on:
# env!("CARGO_MANIFEST_DIR")/../../crates/guest-program/bin/sp1-zk-dex/out/...
# where CARGO_MANIFEST_DIR = /ethrex/cmd/ethrex (baked at compile time).
# We need /ethrex/cmd/ethrex/ to exist for canonicalize() to resolve "../..".
RUN mkdir -p /ethrex/cmd/ethrex
COPY --from=builder /ethrex/crates/guest-program/bin/sp1/out/riscv32im-succinct-zkvm-vk-* \
/ethrex/crates/guest-program/bin/sp1/out/
COPY --from=builder /ethrex/crates/guest-program/bin/sp1-zk-dex/out/riscv32im-succinct-zkvm-vk-* \
/ethrex/crates/guest-program/bin/sp1-zk-dex/out/

EXPOSE 8545
EXPOSE 8551
EXPOSE 30303/tcp
EXPOSE 30303/udp
EXPOSE 9090
EXPOSE 1729
EXPOSE 3900

ENTRYPOINT [ "./ethrex" ]
183 changes: 183 additions & 0 deletions cmd/ethrex/l2/deployer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,23 @@ pub struct DeployerOptions {
help = "This address will be registered as an initial fee token"
)]
pub initial_fee_token: Option<Address>,
#[arg(
long = "register-guest-programs",
value_delimiter = ',',
value_name = "PROGRAM_IDS",
env = "ETHREX_REGISTER_GUEST_PROGRAMS",
help_heading = "Deployer options",
help = "Guest programs to register on L1 (e.g., zk-dex,tokamon). Registers in GuestProgramRegistry and sets up verification keys."
)]
pub register_guest_programs: Vec<String>,
#[arg(
long = "zk-dex-sp1-vk",
value_name = "PATH",
env = "ETHREX_ZK_DEX_SP1_VK",
help_heading = "Deployer options",
help = "Path to the ZK-DEX SP1 verification key. Defaults to build output path."
)]
pub zk_dex_sp1_vk_path: Option<String>,
}

impl Default for DeployerOptions {
Expand Down Expand Up @@ -453,6 +470,8 @@ impl Default for DeployerOptions {
router: None,
deploy_router: false,
initial_fee_token: None,
register_guest_programs: Vec::new(),
zk_dex_sp1_vk_path: None,
}
}
}
Expand Down Expand Up @@ -564,6 +583,10 @@ const BRIDGE_INITIALIZER_SIGNATURE: &str = "initialize(address,address,uint256,a
const ROUTER_INITIALIZER_SIGNATURE: &str = "initialize(address)";
const ROUTER_REGISTER_SIGNATURE: &str = "register(uint256,address)";
const GUEST_PROGRAM_REGISTRY_INITIALIZER_SIGNATURE: &str = "initialize(address)";
const GUEST_PROGRAM_REGISTRY_REGISTER_OFFICIAL_SIGNATURE: &str =
"registerOfficialProgram(string,string,address,uint8)";
const UPGRADE_VERIFICATION_KEY_SIGNATURE: &str =
"upgradeVerificationKey(bytes32,uint8,uint8,bytes32)";

// Gas limit for deploying and initializing contracts
// Needed to avoid estimating gas of initializations when the
Expand Down Expand Up @@ -1545,10 +1568,170 @@ async fn initialize_contracts(
// GuestProgramRegistry is linked to OnChainProposer via the initialize() parameter,
// so no separate setGuestProgramRegistry() call is needed.

// Register additional guest programs (e.g., zk-dex, tokamon) and their VKs.
for program_id in &opts.register_guest_programs {
let program_type_id = resolve_deployer_program_type_id(program_id);
if program_type_id <= 1 {
warn!(program_id, "Skipping unknown or default program");
continue;
}

// 1. Register official program in GuestProgramRegistry.
info!(program_id, program_type_id, "Registering guest program");
let register_nonce = eth_client
.get_nonce(deployer_address, BlockIdentifier::Tag(BlockTag::Pending))
.await?;
let register_calldata = encode_calldata(
GUEST_PROGRAM_REGISTRY_REGISTER_OFFICIAL_SIGNATURE,
&[
Value::String(program_id.clone()),
Value::String(format!("{program_id} guest program")),
Value::Address(deployer_address),
Value::Uint(U256::from(program_type_id)),
],
)?;
let register_tx = build_generic_tx(
eth_client,
TxType::EIP1559,
contract_addresses.guest_program_registry_address,
deployer_address,
register_calldata.into(),
Overrides {
nonce: Some(register_nonce),
gas_limit: Some(TRANSACTION_GAS_LIMIT),
max_fee_per_gas: Some(gas_price),
max_priority_fee_per_gas: Some(gas_price),
..Default::default()
},
)
.await?;
let register_tx_hash =
send_generic_transaction(eth_client, register_tx, initializer).await?;
info!(
tx_hash = %format!("{register_tx_hash:#x}"),
program_id,
"Guest program registered in GuestProgramRegistry"
);
tx_hashes.push(register_tx_hash);

// 2. Register VK for this program if SP1 is enabled.
// OnChainProposer.upgradeVerificationKey() is onlyOwner (owner = Timelock).
// We call Timelock.upgradeVerificationKey() which forwards to OnChainProposer.
// Requires SECURITY_COUNCIL role (= bridge_owner / on_chain_proposer_owner).
if opts.sp1 {
let vk = get_vk_for_program(program_id, opts)?;
if vk.is_empty() {
warn!(program_id, "No SP1 VK found, skipping VK registration");
continue;
}

let timelock_address =
contract_addresses
.timelock_address
.ok_or(DeployerError::InternalError(
"Timelock address required for VK registration".to_string(),
))?;

let security_council_pk =
opts.bridge_owner_pk
.ok_or(DeployerError::ConfigValueNotSet(
"--bridge-owner-pk (needed as security council for VK registration)"
.to_string(),
))?;
let security_council_signer: Signer = LocalSigner::new(security_council_pk).into();

const SP1_VERIFIER_ID: u8 = 1;
let vk_calldata = encode_calldata(
UPGRADE_VERIFICATION_KEY_SIGNATURE,
&[
Value::FixedBytes(commit_hash.0.to_vec().into()),
Value::Uint(U256::from(program_type_id)),
Value::Uint(U256::from(SP1_VERIFIER_ID)),
Value::FixedBytes(vk.to_vec().into()),
],
)?;

let sc_nonce = eth_client
.get_nonce(
security_council_signer.address(),
BlockIdentifier::Tag(BlockTag::Pending),
)
.await?;
let vk_tx = build_generic_tx(
eth_client,
TxType::EIP1559,
timelock_address,
security_council_signer.address(),
vk_calldata.into(),
Overrides {
nonce: Some(sc_nonce),
gas_limit: Some(TRANSACTION_GAS_LIMIT),
max_fee_per_gas: Some(gas_price),
max_priority_fee_per_gas: Some(gas_price),
..Default::default()
},
)
.await?;
let vk_tx_hash =
send_generic_transaction(eth_client, vk_tx, &security_council_signer).await?;
info!(
tx_hash = %format!("{vk_tx_hash:#x}"),
program_id,
program_type_id,
"SP1 verification key registered via Timelock"
);
tx_hashes.push(vk_tx_hash);
}
}

trace!("Contracts initialized");
Ok(tx_hashes)
}

/// Maps a guest program ID string to its on-chain programTypeId.
fn resolve_deployer_program_type_id(program_id: &str) -> u8 {
match program_id {
"evm-l2" => 1,
"zk-dex" => 2,
"tokamon" => 3,
_ => 0,
}
}

/// Reads the SP1 verification key for a guest program from its build output path.
fn get_vk_for_program(program_id: &str, opts: &DeployerOptions) -> Result<Bytes, DeployerError> {
match program_id {
"zk-dex" => {
if let Some(ref path) = opts.zk_dex_sp1_vk_path {
read_vk(path)
} else {
let path = format!(
"{}/../../crates/guest-program/bin/sp1-zk-dex/out/riscv32im-succinct-zkvm-vk-bn254",
env!("CARGO_MANIFEST_DIR")
);
match std::fs::canonicalize(&path) {
Ok(canonical) => read_vk(
canonical
.to_str()
.ok_or(DeployerError::FailedToGetStringFromPath)?,
),
Err(e) => {
warn!(
program_id,
path, "VK file not found, build with GUEST_PROGRAMS=zk-dex: {e}"
);
Ok(Bytes::new())
}
}
}
}
_ => {
warn!(program_id, "No VK path configured for this program");
Ok(Bytes::new())
}
}
}

async fn register_chain(
eth_client: &EthClient,
contract_addresses: ContractAddresses,
Expand Down
Loading
Loading