fix(zk): source intermediate root interval from on-chain like TEE#2433
fix(zk): source intermediate root interval from on-chain like TEE#2433
Conversation
✅ Heimdall Review Status
|
1a9ed2b to
0c05242
Compare
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
0c05242 to
979210a
Compare
979210a to
febef35
Compare
febef35 to
90e06c7
Compare
90e06c7 to
d3b2f18
Compare
d3b2f18 to
0765742
Compare
842d0a5 to
800e720
Compare
|
Attached to linear issue CHAIN-4080 |
800e720 to
6254939
Compare
6254939 to
72d816e
Compare
| session_id: None, | ||
| prover_address: proof_request.prover_address.clone(), | ||
| l1_head: proof_request.l1_head.clone(), | ||
| intermediate_root_interval: None, |
There was a problem hiding this comment.
The proof_request DB model now carries intermediate_root_interval: Option<i64>, but this reconstruction discards it. Should be:
| 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::*; |
There was a problem hiding this comment.
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.
|
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? |
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
92f78af to
1f76f19
Compare
| 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, | ||
| ); |
There was a problem hiding this comment.
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 == 0with a comment explaining why gas-based ranges enforce the invariant elsewhere, or - Requiring that the gas-based splitting logic aligns ranges to
intermediate_root_intervalboundaries (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); |
There was a problem hiding this comment.
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:
| 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), |
There was a problem hiding this comment.
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 == 0with 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_intervalboundaries (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); |
There was a problem hiding this comment.
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:
| 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 |
There was a problem hiding this comment.
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.
Review SummaryThe direction is sound — collapsing to a single source-of-truth for FindingsMedium — Vacuous validation in gas-based mode ( Medium — Silent Low — Cargo.toml dependency ordering ( Note on stale comments from earlier revisionsSeveral existing inline comments from
|
Summary
The ZK prover hardcoded
DEFAULT_INTERMEDIATE_ROOT_INTERVAL = 10, but the on-chainAggregateVerifiermay 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:INTERMEDIATE_BLOCK_INTERVALon startup (DGF → impl →AggregateVerifier) and threads it intoProofRequest.intermediate_block_interval.WitnessExecutornow read the interval fromBootInfo, the same channel the TEE enclave uses.DEFAULT_INTERMEDIATE_ROOT_INTERVALonly when DGF is unset or the chain read fails.Removed (now redundant)
INTERMEDIATE_ROOT_INTERVALenv varWitnessExecutor::runandWitnessGenerator::get_sp1_stdinNotes
range_vkey_commitment. The ELF then accepts any on-chain interval without rebuilding (interval is data, not code).