Skip to content

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

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

fix(zk): source intermediate root interval from on-chain like TEE#2433
mw2000 wants to merge 7 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 🟡 0/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 May 1, 2026 0:00am

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
Comment thread crates/proof/zk/service/src/backends/mock.rs Outdated
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 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.

jackchuma
jackchuma previously approved these changes 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
@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 added 7 commits April 30, 2026 16:57
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 e0cd25c to c6ac13b Compare May 1, 2026 00:00
@cb-heimdall cb-heimdall dismissed jackchuma’s stale review May 1, 2026 00:00

Approved review 4205898775 from jackchuma is now dismissed due to new commit. Re-request for approval.

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 in gas-based splitting mode (evm_gas_limit > 0, range_proof_interval == 0): 0u64.is_multiple_of(x) is always true because 0 = 0 * x.

In that mode, actual range sizes are determined dynamically by EVM gas consumption, so there's no static range_proof_interval to validate. If those dynamically-computed ranges aren't aligned to intermediate_root_interval boundaries, proofs will still fail on-chain verification.

Consider either:

  • Skipping this check when range_proof_interval == 0 (with a comment documenting that the gas-based splitting logic is responsible for alignment), or
  • Ensuring the gas-based range splitting snaps to intermediate_root_interval boundaries.

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. This executor is reachable from WitnessGenerator::generate_witness_data (in traits.rs:77) which doesn't pass through the gRPC validation or RequesterConfig::validate() gates, so a zero intermediate_block_interval in BootInfo would silently produce proofs with interval 1.

Since the PR's goal is to refuse proceeding with a wrong interval, consider returning an error here instead:

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
};

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 1, 2026

Review Summary

The overall design is sound — collapsing the intermediate root interval to a single on-chain source of truth (matching the TEE path) eliminates an important class of silent proof failures. The plumbing through gRPC, database, host, and executor is thorough.

Findings

1. Divisibility validation is vacuous in gas-based mode (config.rs:160-165)

When evm_gas_limit > 0 and range_proof_interval == 0 (gas-based splitting), 0u64.is_multiple_of(x) is always true. The actual range sizes are computed dynamically by gas consumption and aren't validated against intermediate_root_interval. Misaligned gas-based ranges would silently produce proofs that fail on-chain.

2. Silent .max(1) clamp on zero interval (executor.rs:144)

The executor is reachable from WitnessGenerator::generate_witness_data (traits.rs:77), which bypasses both gRPC validation and RequesterConfig::validate(). A zero intermediate_block_interval in BootInfo would be silently clamped to 1, producing proofs with the wrong sampling rate — exactly the failure mode this PR aims to prevent. Consider returning an error instead.

Both findings have inline comments with suggested fixes.

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