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
| 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? |
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.
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
e0cd25c to
c6ac13b
Compare
Approved review 4205898775 from jackchuma is now dismissed due to new commit. Re-request for approval.
| 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 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_intervalboundaries.
| 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. 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:
| 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 | |
| }; |
Review SummaryThe 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. Findings1. Divisibility validation is vacuous in gas-based mode ( When 2. Silent The executor is reachable from Both findings have inline comments with suggested fixes. |
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).