Skip to content

fix(zk): source intermediate root interval from on-chain like TEE#2433

Open
mw2000 wants to merge 6 commits intomainfrom
fix/zk-intermediate-root-interval
Open

fix(zk): source intermediate root interval from on-chain like TEE#2433
mw2000 wants to merge 6 commits intomainfrom
fix/zk-intermediate-root-interval

Conversation

@mw2000
Copy link
Copy Markdown
Contributor

@mw2000 mw2000 commented Apr 28, 2026

Summary

The ZK prover hardcoded DEFAULT_INTERMEDIATE_ROOT_INTERVAL = 10, but the on-chain AggregateVerifier may use a different value (e.g. 30 on Sepolia). This mismatch causes ZK challenge proofs to fail verification.

Fix

Collapse to a single source matching the TEE — chain → ProofRequest → BootInfo → executor:

  • Validity binary reads INTERMEDIATE_BLOCK_INTERVAL on startup (DGF → impl → AggregateVerifier) and threads it into ProofRequest.intermediate_block_interval.
  • Host preimage server answers boot key 9 with the on-chain value.
  • Both the SP1 range program and the native WitnessExecutor now read the interval from BootInfo, the same channel the TEE enclave uses.
  • Falls back to DEFAULT_INTERMEDIATE_ROOT_INTERVAL only when DGF is unset or the chain read fails.

Removed (now redundant)

  • INTERMEDIATE_ROOT_INTERVAL env var
  • SP1 stdin scalar (host write + guest read)
  • Interval params on WitnessExecutor::run and WitnessGenerator::get_sp1_stdin

Notes

  • Range ELF source changed → rebuild once and update range_vkey_commitment. The ELF then accepts any on-chain interval without rebuilding (interval is data, not code).
  • Aggregation program is unchanged.

@cb-heimdall
Copy link
Copy Markdown
Collaborator

cb-heimdall commented Apr 28, 2026

✅ Heimdall Review Status

Requirement Status More Info
Reviews 1/1
Denominator calculation
Show calculation
1 if user is bot 0
1 if user is external 0
2 if repo is sensitive 0
From .codeflow.yml 1
Additional review requirements
Show calculation
Max 0
0
From CODEOWNERS 0
Global minimum 0
Max 1
1
1 if commit is unverified 1
Sum 2

@mw2000 mw2000 changed the title fix(proof): wire intermediate root interval through ZK prover pipeline fix(zk): wire intermediate root interval through ZK prover pipeline Apr 28, 2026
@mw2000 mw2000 force-pushed the fix/zk-intermediate-root-interval branch from 1a9ed2b to 0c05242 Compare April 28, 2026 22:05
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 28, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
base Ignored Ignored Preview Apr 30, 2026 6:59am

Request Review

Comment thread crates/proof/succinct/utils/ethereum/host/src/host.rs Outdated
Comment thread crates/proof/zk/service/src/worker/prover_worker_pool.rs Outdated
@mw2000 mw2000 force-pushed the fix/zk-intermediate-root-interval branch from 0c05242 to 979210a Compare April 28, 2026 23:34
Comment thread crates/proof/zk/service/src/backends/network.rs
@mw2000 mw2000 force-pushed the fix/zk-intermediate-root-interval branch from 979210a to febef35 Compare April 29, 2026 00:47
@mw2000 mw2000 changed the title fix(zk): wire intermediate root interval through ZK prover pipeline fix(zk): source intermediate root interval from on-chain like TEE Apr 29, 2026
Comment thread crates/proof/succinct/validity/bin/validity.rs Outdated
Comment thread crates/proof/succinct/validity/bin/validity.rs Outdated
Comment thread crates/proof/succinct/validity/bin/validity.rs Outdated
@mw2000 mw2000 force-pushed the fix/zk-intermediate-root-interval branch from febef35 to 90e06c7 Compare April 29, 2026 01:00
Comment thread crates/proof/succinct/validity/Cargo.toml Outdated
@mw2000 mw2000 force-pushed the fix/zk-intermediate-root-interval branch from 90e06c7 to d3b2f18 Compare April 29, 2026 01:06
@mw2000 mw2000 marked this pull request as ready for review April 29, 2026 01:08
@mw2000 mw2000 marked this pull request as draft April 29, 2026 01:09
Comment thread crates/proof/zk/service/src/backends/op_succinct/provider.rs Outdated
@mw2000 mw2000 force-pushed the fix/zk-intermediate-root-interval branch from d3b2f18 to 0765742 Compare April 29, 2026 02:03
Comment thread crates/proof/succinct/validity/src/intermediate_interval.rs
@mw2000 mw2000 marked this pull request as ready for review April 29, 2026 02:11
@mw2000 mw2000 enabled auto-merge April 29, 2026 02:12
Comment thread crates/proof/succinct/validity/src/intermediate_interval.rs
@mw2000 mw2000 force-pushed the fix/zk-intermediate-root-interval branch from 842d0a5 to 800e720 Compare April 29, 2026 02:36
@mw2000
Copy link
Copy Markdown
Contributor Author

mw2000 commented Apr 29, 2026

Attached to linear issue CHAIN-4080

@mw2000 mw2000 force-pushed the fix/zk-intermediate-root-interval branch from 800e720 to 6254939 Compare April 29, 2026 21:25
@mw2000 mw2000 force-pushed the fix/zk-intermediate-root-interval branch from 6254939 to 72d816e Compare April 29, 2026 21:27
session_id: None,
prover_address: proof_request.prover_address.clone(),
l1_head: proof_request.l1_head.clone(),
intermediate_root_interval: None,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The proof_request DB model now carries intermediate_root_interval: Option<i64>, but this reconstruction discards it. Should be:

Suggested change
intermediate_root_interval: None,
intermediate_root_interval: proof_request.intermediate_root_interval.map(|v| v as u64),

Low severity since this only affects mock backend, but it's inconsistent with the goal of preserving the interval end-to-end. The mock build_mock_public_values doesn't use the interval today, but if it ever does (or if mock proof verification checks the field), this None would silently use the default.

pub use contract::*;
pub use db::*;
pub use env::*;
pub use intermediate_interval::*;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pre-existing, but since you're touching this file: per workspace conventions (CLAUDE.md), each mod declaration should be grouped with its pub use re-export:

mod intermediate_interval;
pub use intermediate_interval::*;

rather than listing all mods then all pub uses. This applies to the entire file but is a pre-existing issue — flagging only because you're adding a new module here.

@jackchuma
Copy link
Copy Markdown
Contributor

Generally looks good to me. Can we add enforcement somewhere that the range proof block intervals are always a multiple of the intermediate block interval?

mw2000 added 5 commits April 29, 2026 23:41
The ZK proving pipeline carried the intermediate root interval through
three parallel channels (env var, SP1 stdin scalar, ProofRequest), each
defaulting to DEFAULT_INTERMEDIATE_ROOT_INTERVAL=10 and drifting from the
on-chain AggregateVerifier.INTERMEDIATE_BLOCK_INTERVAL.

Collapse to a single source matching the TEE: the validity binary reads
INTERMEDIATE_BLOCK_INTERVAL from chain (DGF -> impl -> AggregateVerifier)
and threads it into ProofRequest, so the host preimage server answers
boot key 9 with the on-chain value and both the zkVM range program and
the native witness executor read it from BootInfo. Falls back to
DEFAULT_INTERMEDIATE_ROOT_INTERVAL when DGF is unset or the chain read
fails. Removes INTERMEDIATE_ROOT_INTERVAL env var, the stdin scalar
read in the guest, and the now-redundant interval params on
WitnessExecutor::run and WitnessGenerator::get_sp1_stdin.

Made-with: Cursor
@mw2000 mw2000 force-pushed the fix/zk-intermediate-root-interval branch from 92f78af to 1f76f19 Compare April 30, 2026 06:41
Comment on lines +160 to +165
ensure!(
self.range_proof_interval.is_multiple_of(self.intermediate_root_interval),
"range_proof_interval ({}) must be a multiple of intermediate_root_interval ({}); range proofs that do not end on an intermediate-root boundary fail on-chain verification",
self.range_proof_interval,
self.intermediate_root_interval,
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This divisibility check is vacuous when evm_gas_limit > 0 and range_proof_interval == 0 (the gas-based splitting mode): 0u64.is_multiple_of(x) is always true because 0 = 0·x.

In gas-based mode the actual range sizes are determined dynamically by EVM gas consumption, so there is no static range_proof_interval to validate. If those dynamically-computed ranges aren't multiples of intermediate_root_interval, proofs will still fail on-chain verification — exactly the scenario the error message warns about.

Consider either:

  • Skipping this check when range_proof_interval == 0 with a comment explaining why gas-based ranges enforce the invariant elsewhere, or
  • Requiring that the gas-based splitting logic aligns ranges to intermediate_root_interval boundaries (and documenting that guarantee here).

revm::precompile::install_crypto(CustomCrypto::default());

let boot_clone = boot.clone();
let intermediate_block_interval = boot.intermediate_block_interval.max(1);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Silent .max(1) clamp on an interval of 0 hides a misconfiguration that would produce proofs with the wrong sampling rate. The PR validates != 0 at the gRPC boundary and in RequesterConfig::validate(), but this executor is also reachable from WitnessGenerator::generate_witness_data (the native witness-gen path in traits.rs) which doesn't pass through either validation gate.

Consider replacing this with a hard error:

Suggested change
let intermediate_block_interval = boot.intermediate_block_interval.max(1);
let intermediate_block_interval = if boot.intermediate_block_interval == 0 {
anyhow::bail!("intermediate_block_interval in BootInfo is 0; this indicates a misconfigured preimage server");
} else {
boot.intermediate_block_interval
};

This is consistent with the PR's philosophy of refusing to proceed with a wrong interval.

"intermediate_root_interval must be non-zero; refusing to run with disabled intermediate root sampling"
);
ensure!(
self.range_proof_interval.is_multiple_of(self.intermediate_root_interval),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This divisibility check is vacuous when evm_gas_limit > 0 and range_proof_interval == 0 (gas-based splitting mode): 0u64.is_multiple_of(x) is always true because 0 = 0 * x.

In gas-based mode the actual range sizes are determined dynamically by EVM gas consumption, so there is no static range_proof_interval to validate against. If those dynamically-computed ranges aren't aligned to intermediate_root_interval boundaries, proofs will still fail on-chain verification -- exactly the scenario the error message warns about.

Consider either:

  • Skipping this check when range_proof_interval == 0 with a comment explaining that gas-based range splitting enforces the invariant elsewhere, or
  • Adding a guard in the gas-based splitting logic that snaps ranges to intermediate_root_interval boundaries (and documenting that guarantee here).

revm::precompile::install_crypto(CustomCrypto::default());

let boot_clone = boot.clone();
let intermediate_block_interval = boot.intermediate_block_interval.max(1);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Silent .max(1) clamp hides a misconfiguration that would produce proofs with the wrong sampling rate. The PR validates != 0 at the gRPC boundary and in RequesterConfig::validate(), but this executor is also reachable from WitnessGenerator::generate_witness_data (the native witness-gen path in traits.rs:77) which doesn't pass through either validation gate.

Consider replacing this with a hard error so a zero interval is caught regardless of entry point:

Suggested change
let intermediate_block_interval = boot.intermediate_block_interval.max(1);
let intermediate_block_interval = if boot.intermediate_block_interval == 0 {
anyhow::bail!("intermediate_block_interval in BootInfo is 0; this indicates a misconfigured preimage server");
} else {
boot.intermediate_block_interval
};

This is consistent with the PR's philosophy of refusing to proceed with a wrong interval.

alloy-sol-types.workspace = true

anyhow.workspace = true
base-proof-contracts.workspace = true
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per workspace conventions, dependencies should be sorted by line length (waterfall style). base-proof-contracts (22 chars) is longer than anyhow (6 chars) above it and longer than dotenv, serde, serde_json, and reqwest below it. Consider reordering so it sits among the other base-proof-* local crates (lines 25-29) where it fits naturally by both grouping and length.

@github-actions
Copy link
Copy Markdown
Contributor

Review Summary

The direction is sound — collapsing to a single source-of-truth for intermediate_block_interval (on-chain AggregateVerifierBootInfo → executor) eliminates the class of bugs where the ZK proposer disagrees with the contracts. The new WitnessParams struct, gRPC boundary validation, and database round-tripping are well done.

Findings

Medium — Vacuous validation in gas-based mode (config.rs:161)
0u64.is_multiple_of(x) is always true, so when evm_gas_limit > 0 and range_proof_interval == 0 (gas-based splitting), the divisibility check passes trivially. Dynamically-computed gas-based ranges could still violate the intermediate root boundary constraint without being caught here.

Medium — Silent .max(1) clamp in executor (executor.rs:144)
A zero intermediate_block_interval in BootInfo is silently clamped to 1 rather than producing a hard error. This executor path is reachable from WitnessGenerator::generate_witness_data which bypasses both the gRPC validation and RequesterConfig::validate(). Consistent with the PR philosophy, this should bail rather than silently produce proofs with an unexpected interval.

Low — Cargo.toml dependency ordering (validity/Cargo.toml:42)
base-proof-contracts is placed between anyhow and dotenv rather than grouped with the other base-proof-* local crates. This breaks the waterfall-by-length convention.

Note on stale comments from earlier revisions

Several existing inline comments from github-actions[bot] appear to be from earlier revisions and no longer apply to the current code:

  • The "Bug: interval is never defined" comment — interval is properly bound on line 38
  • The "ProveBlockRequestParams does not include intermediate_root_interval" comment — it now does (line 27)
  • The "logic should be in library crate" comment — resolve_intermediate_root_interval is already in validity/src/intermediate_interval.rs
  • The "on-chain value is not validated for > 0" comment — lines 39-42 check for zero
  • The WitnessParams struct suggestion — the struct exists
  • The url crate / reqwest::Url comments — no separate url dependency exists

@mw2000 mw2000 added this pull request to the merge queue Apr 30, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Apr 30, 2026
@mw2000 mw2000 added this pull request to the merge queue Apr 30, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Apr 30, 2026
@mw2000 mw2000 added this pull request to the merge queue Apr 30, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Apr 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants