From dd4b884a0baaab024c8f8c04f0e9720b1640c39d Mon Sep 17 00:00:00 2001 From: MauroFab Date: Fri, 3 Jul 2026 15:13:52 -0300 Subject: [PATCH 1/2] Address review findings: dedupe private-input page math, drop leaky serde derives, fix stale docs - Extract private_input_page_count / is_private_input_page / private_input_page_bases / max_private_input_pages into tables::page as the single source of truth; the monolithic trace builder, monolithic verifier bound, continuation prover, and continuation verifier all previously re-derived the same wire-format math independently and could drift. - Add PRIVATE_INPUT_LENGTH_PREFIX_BYTES to executor::vm::memory next to the wire-format writer and use it everywhere instead of a bare 4. - Remove the now-unused serde derives from InitClaim/FiniClaim/CellBoundary: nothing serializes them since the bundle dropped EpochProof.boundary, and keeping them off makes re-introducing the value leak a compile error. - Make the verifier-side private-page config explicitly data-free (include_private_genesis flag) instead of a dead init_page_data lookup. - Reject a non-page-aligned touched_page_bases entry as a malformed bundle (new Error::MalformedContinuationBundle) instead of Ok(None), matching the count bound's Err semantics for structural validation of untrusted fields. - Fix bin/cli/README.md, which still claimed the continuation bundle ships the raw private input bytes. --- bin/cli/README.md | 12 ++- executor/src/vm/memory.rs | 10 ++- prover/src/continuation.rs | 125 ++++++++++++--------------- prover/src/lib.rs | 15 ++-- prover/src/tables/local_to_global.rs | 16 +++- prover/src/tables/page.rs | 62 +++++++++++++ prover/src/tables/trace_builder.rs | 31 +++---- 7 files changed, 165 insertions(+), 106 deletions(-) diff --git a/bin/cli/README.md b/bin/cli/README.md index bc5eb9d53..267fc61b7 100644 --- a/bin/cli/README.md +++ b/bin/cli/README.md @@ -118,10 +118,14 @@ As rough ethrex 10-transfer distinct-account reference points from a local sweep about 26.8 GB. For a new workload, use the highest value the machine can run without swapping. -Continuation proof bundles are self-contained for standalone verification. When -`--private-input` is used, the serialized continuation proof includes the raw -private input bytes so the verifier can rebuild the genesis memory commitment. -Do not treat continuation proof files as confidential-input hiding artifacts. +Continuation proof bundles are self-contained for standalone verification: the +verifier needs only the proof file and the ELF. When `--private-input` is used, +the serialized proof does **not** include the raw private input bytes — it +carries only the private-input page count; the private genesis lives in +committed, bus-enforced columns the verifier never recomputes (see +`docs/continuations_design.md` §3.6). This is not a zero-knowledge guarantee, +though: committed columns are still opened at STARK query positions, so do not +treat proof files as cryptographically hiding the private input. ## Guest Program Flamegraphs diff --git a/executor/src/vm/memory.rs b/executor/src/vm/memory.rs index f349eeae6..d4f8d1cee 100644 --- a/executor/src/vm/memory.rs +++ b/executor/src/vm/memory.rs @@ -51,6 +51,11 @@ pub const MAX_PRIVATE_INPUT_SIZE: u64 = 64 * 1024 * 1024; /// Layout: 4-byte LE length prefix at `PRIVATE_INPUT_START_INDEX`, then data at +4. /// Must match `PRIVATE_INPUT_START` in `syscalls/src/syscalls.rs`. pub const PRIVATE_INPUT_START_INDEX: u64 = 0xFF000000; +/// Size in bytes of the private input's wire-format length prefix (the `u32` LE +/// written at `PRIVATE_INPUT_START_INDEX` by [`Memory::store_private_inputs`]; the +/// data follows at `+ PRIVATE_INPUT_LENGTH_PREFIX_BYTES`). Single source of truth +/// for every page-span computation over the private-input region. +pub const PRIVATE_INPUT_LENGTH_PREFIX_BYTES: usize = size_of::(); #[derive(Default, Debug, Clone)] pub struct Memory { @@ -231,7 +236,10 @@ impl Memory { let len_u32 = u32::try_from(inputs.len()).map_err(|_| MemoryError::PrivateInputSizeExceeded)?; self.store_word(PRIVATE_INPUT_START_INDEX, len_u32)?; - self.set_bytes_aligned(PRIVATE_INPUT_START_INDEX + 4, &inputs)?; + self.set_bytes_aligned( + PRIVATE_INPUT_START_INDEX + PRIVATE_INPUT_LENGTH_PREFIX_BYTES as u64, + &inputs, + )?; Ok(()) } diff --git a/prover/src/continuation.rs b/prover/src/continuation.rs index 319b6ac03..ea1e2c8a8 100644 --- a/prover/src/continuation.rs +++ b/prover/src/continuation.rs @@ -49,7 +49,6 @@ use std::collections::HashMap; use crypto::fiat_shamir::default_transcript::DefaultTranscript; use executor::elf::Elf; use executor::vm::execution::Executor; -use executor::vm::memory::MAX_PRIVATE_INPUT_SIZE; use math::field::element::FieldElement; use stark::config::Commitment; use stark::lookup::{AirWithBuses, AuxiliaryTraceBuildData, NullBoundaryConstraintBuilder}; @@ -268,7 +267,7 @@ fn canonical_page_bases(page_bases: &[u64]) -> Vec { /// Private-input pages are built NON-preprocessed, so the verifier never recomputes their /// genesis from the ELF and never needs the raw private bytes. They are identified EXACTLY /// as the monolithic verifier does — the first `num_private_input_pages` pages from -/// `PRIVATE_INPUT_START_INDEX` (see [`is_private_input_page`]). +/// `PRIVATE_INPUT_START_INDEX` (see [`page::is_private_input_page`]). fn global_memory_configs( page_bases: &[u64], elf: &Elf, @@ -278,25 +277,40 @@ fn global_memory_configs( // non-preprocessed (their INIT is never recomputed). let image = build_initial_image_paged(elf, &[]); let init_page_data = build_init_page_data(&image); - global_memory_configs_from_init_page_data(page_bases, &init_page_data, num_private_input_pages) + global_memory_configs_from_init_page_data( + page_bases, + &init_page_data, + num_private_input_pages, + false, + ) } /// Shared genesis-config builder for prover and verifier, one `PageConfig` per page base /// in `page_bases` (which must be canonical: sorted + deduped). `init_page_data` holds /// each page's genesis bytes (ELF + private input on the prover side; ELF only on the -/// verifier side). Private-input pages are built NON-preprocessed: on the prover their -/// committed INIT column carries the genesis bytes; on the verifier the AIR is -/// non-preprocessed so those bytes are irrelevant. +/// verifier side). +/// +/// `include_private_genesis` — whether a private-input page's genesis bytes are loaded +/// from `init_page_data` into its config. The PROVER passes `true`: those bytes become +/// the committed INIT column. The VERIFIER passes `false`: its AIR for a private page is +/// non-preprocessed and never consults `init_values` (and its `init_page_data` is built +/// from the ELF alone, so there is nothing to load) — the config carries an explicitly +/// empty vec so no code path can silently start depending on verifier-side private data. fn global_memory_configs_from_init_page_data( page_bases: &[u64], init_page_data: &HashMap>, num_private_input_pages: usize, + include_private_genesis: bool, ) -> Vec { page_bases .iter() .map(|&page_base| { - if is_private_input_page(page_base, num_private_input_pages) { - let data = init_page_data.get(&page_base).cloned().unwrap_or_default(); + if page::is_private_input_page(page_base, num_private_input_pages) { + let data = if include_private_genesis { + init_page_data.get(&page_base).cloned().unwrap_or_default() + } else { + Vec::new() + }; PageConfig::with_private_input(page_base, data) } else { match init_page_data.get(&page_base) { @@ -308,38 +322,6 @@ fn global_memory_configs_from_init_page_data( .collect() } -/// Whether `page_base` is one of the first `num_private_input_pages` pages starting at -/// `PRIVATE_INPUT_START_INDEX` — the page-aligned span private input actually occupies. -/// This matches the monolithic verifier's `page_configs_from_elf_and_runtime` exactly, so -/// the continuation classifies private pages identically to the monolithic path. Classifying -/// by the count (not the raw `[START, START+MAX_PRIVATE_INPUT_SIZE)` byte range) keeps prover -/// and verifier in lockstep regardless of whether the region end is page-aligned. -/// -/// NOTE (shared with the monolithic path): a page classified private is built -/// non-preprocessed, so its genesis is NOT recomputed from the ELF. This is safe because -/// the private-input area is reserved and the reservation is enforced: `Elf::load` rejects -/// any loadable segment reaching at/above `PRIVATE_INPUT_START_INDEX` -/// (`ElfError::SegmentInPrivateInputRegion`) — covering every page this function can -/// classify private — so no ELF-declared data can live there and have its genesis go unbound. -fn is_private_input_page(page_base: u64, num_private_input_pages: usize) -> bool { - use executor::vm::memory::PRIVATE_INPUT_START_INDEX; - let page_size = page::DEFAULT_PAGE_SIZE as u64; - let end = PRIVATE_INPUT_START_INDEX + num_private_input_pages as u64 * page_size; - (PRIVATE_INPUT_START_INDEX..end).contains(&page_base) -} - -/// Number of pages the private input occupies, starting at `PRIVATE_INPUT_START_INDEX`. -/// Mirrors the monolithic prover (`from_elf_and_logs`): the wire format is a 4-byte -/// length prefix plus the data, and `PRIVATE_INPUT_START_INDEX` is page-aligned, so the -/// span is `ceil((4 + len) / page_size)` consecutive pages (0 when there is no input). -fn private_input_page_count(private_inputs: &[u8]) -> usize { - if private_inputs.is_empty() { - return 0; - } - let total_bytes = 4 + private_inputs.len(); - total_bytes.div_ceil(page::DEFAULT_PAGE_SIZE) -} - /// Per-epoch register state and label. struct EpochStart<'a> { register_init: &'a [u32], @@ -665,6 +647,7 @@ fn prove_global( page_bases, init_page_data, num_private_input_pages, + true, ); let mut l2g_traces: Vec> = boundaries @@ -886,7 +869,7 @@ pub fn prove_continuation( // One global LogUp over all the (kept) local-to-global tables. `all_boundaries` was // accumulated locally in the loop (never round-tripped through the bundle). - let num_private_input_pages = private_input_page_count(private_inputs); + let num_private_input_pages = page::private_input_page_count(private_inputs); // SINGLE source of truth: the same page-base list drives the committed GLOBAL_MEMORY // tables and is shipped in the bundle, so the two can never diverge in set or order. let touched_page_bases = touched_page_bases(&all_boundaries); @@ -909,8 +892,9 @@ pub fn prove_continuation( /// Verify a [`ContinuationProof`] using ONLY the bundle and the ELF — nothing from /// the prover's memory. Returns `Ok(Some(public_output))` (the run-wide committed -/// bytes, reconstructed from the per-epoch bound slices) iff every check holds, else -/// `Ok(None)`. +/// bytes, reconstructed from the per-epoch bound slices) iff every check holds, +/// `Ok(None)` if a well-formed proof fails verification, and `Err` if the bundle is +/// structurally malformed (fails validation before any proof is checked). /// /// The verifier (1) enumerates epochs itself, assigning `epoch_label` and `is_final` /// by position (a trusted enumeration); (2) verifies each epoch, deriving its @@ -934,10 +918,7 @@ pub fn verify_continuation( // Fiat-Shamir statement (`absorb_continuation_global_statement`), so any wrong value // diverges the verifier's challenges and `verify_global`'s `multi_verify` rejects — // on top of the committed-AIR-shape mismatch a wrong count causes on a touched page. - // The tight max is the honest span of a MAX-size input including its 4-byte length - // prefix: `ceil((MAX_PRIVATE_INPUT_SIZE + 4) / page_size)` pages (no slack). - let max_private_input_pages = - (MAX_PRIVATE_INPUT_SIZE as usize + 4).div_ceil(page::DEFAULT_PAGE_SIZE); + let max_private_input_pages = page::max_private_input_pages(); if bundle.num_private_input_pages > max_private_input_pages { return Err(Error::InvalidTableCounts(format!( "num_private_input_pages ({}) exceeds max ({max_private_input_pages})", @@ -1002,17 +983,21 @@ pub fn verify_continuation( let page_bases = canonical_page_bases(&bundle.touched_page_bases); // Every honest base is produced by `page::page_base_for_address`, so it is page-aligned; a // non-aligned base is only reachable via a hand-crafted bundle. Left unchecked, such a base - // still falls in the private-input range (`is_private_input_page`), so it would be built - // NON-preprocessed with a prover-controlled genesis. The GlobalMemory bus already prevents - // forging any real cell (no MEMW access exists at a non-aligned fake address, so no L2G row - // consumes its genesis token), but a self-cancelling junk page could otherwise ride along in - // an accepted proof. Reject here so the verifier's page set is exactly the aligned set the - // prover could honestly derive. + // still falls in the private-input range (`page::is_private_input_page`), so it would be + // built NON-preprocessed with a prover-controlled genesis. The GlobalMemory bus already + // prevents forging any real cell (no MEMW access exists at a non-aligned fake address, so no + // L2G row consumes its genesis token), but a self-cancelling junk page could otherwise ride + // along in an accepted proof. Reject here so the verifier's page set is exactly the aligned + // set the prover could honestly derive. Like the count bound above, this is structural + // validation of an untrusted bundle field, so it is an `Err` (malformed bundle), not + // `Ok(None)` (well-formed proof that failed verification). if page_bases .iter() .any(|&b| b != page::page_base_for_address(b)) { - return Ok(None); + return Err(Error::MalformedContinuationBundle( + "touched_page_bases contains a non-page-aligned entry".to_string(), + )); } if !verify_global( n, @@ -1443,27 +1428,27 @@ mod tests { // first page, not the last. Classification is by count alone, not the region span. let region_pages = MAX_PRIVATE_INPUT_SIZE / page_size; let last_region_page = start + (region_pages - 1) * page_size; - assert!(!is_private_input_page(start, 0)); - assert!(!is_private_input_page(last_region_page, 0)); + assert!(!page::is_private_input_page(start, 0)); + assert!(!page::is_private_input_page(last_region_page, 0)); // Count n → exactly the first n pages from start. - assert!(is_private_input_page(start, 1)); - assert!(!is_private_input_page(start + page_size, 1)); - assert!(is_private_input_page(start + page_size, 2)); + assert!(page::is_private_input_page(start, 1)); + assert!(!page::is_private_input_page(start + page_size, 1)); + assert!(page::is_private_input_page(start + page_size, 2)); // Pages below the region are never private. - assert!(!is_private_input_page(start - page_size, 10)); + assert!(!page::is_private_input_page(start - page_size, 10)); // private_input_page_count: wire format is [len:4][data], region is page-aligned. - assert_eq!(private_input_page_count(&[]), 0); - assert_eq!(private_input_page_count(&[0u8; 16]), 1); + assert_eq!(page::private_input_page_count(&[]), 0); + assert_eq!(page::private_input_page_count(&[0u8; 16]), 1); // 4-byte prefix + (page_size - 4) data exactly fills one page. assert_eq!( - private_input_page_count(&vec![0u8; page::DEFAULT_PAGE_SIZE - 4]), + page::private_input_page_count(&vec![0u8; page::DEFAULT_PAGE_SIZE - 4]), 1 ); // One more byte spills into a second page. assert_eq!( - private_input_page_count(&vec![0u8; page::DEFAULT_PAGE_SIZE - 3]), + page::private_input_page_count(&vec![0u8; page::DEFAULT_PAGE_SIZE - 3]), 2 ); } @@ -1476,8 +1461,7 @@ mod tests { let elf_bytes = asm_elf_bytes("all_loadstore_32"); let mut bundle = prove_continuation(&elf_bytes, &[], 3, &ProofOptions::default_test_options()).unwrap(); - let max_pages = (MAX_PRIVATE_INPUT_SIZE as usize + 4).div_ceil(page::DEFAULT_PAGE_SIZE); - bundle.num_private_input_pages = max_pages + 1; + bundle.num_private_input_pages = page::max_private_input_pages() + 1; assert!(matches!( verify_continuation(&elf_bytes, &bundle, &ProofOptions::default_test_options()), Err(Error::InvalidTableCounts(_)) @@ -1651,10 +1635,11 @@ mod tests { ); bundle.touched_page_bases[0] += 1; assert!( - verify_continuation(&elf_bytes, &bundle, &ProofOptions::default_test_options()) - .unwrap() - .is_none(), - "a non-page-aligned touched page base must be rejected" + matches!( + verify_continuation(&elf_bytes, &bundle, &ProofOptions::default_test_options()), + Err(Error::MalformedContinuationBundle(_)) + ), + "a non-page-aligned touched page base is a malformed bundle → must be an Err" ); } diff --git a/prover/src/lib.rs b/prover/src/lib.rs index 4ff59d339..7008619ef 100644 --- a/prover/src/lib.rs +++ b/prover/src/lib.rs @@ -192,6 +192,9 @@ pub enum Error { InvalidContinuationEpochSize(String), /// Continuation proof construction hit an internal invariant failure. ContinuationInvariant(String), + /// Continuation bundle is structurally malformed (fails validation before + /// any proof is checked). + MalformedContinuationBundle(String), /// A non-final continuation epoch contains the program-terminating /// instruction. The terminating instruction must be in the final epoch. HaltInNonFinalEpoch, @@ -214,6 +217,9 @@ impl fmt::Display for Error { Error::ContinuationInvariant(msg) => { write!(f, "continuation invariant failed: {msg}") } + Error::MalformedContinuationBundle(msg) => { + write!(f, "malformed continuation bundle: {msg}") + } Error::HaltInNonFinalEpoch => { write!( f, @@ -960,13 +966,10 @@ pub fn verify_with_options( // A malicious prover could set counts to 0, removing entire constraint sets. vm_proof.table_counts.validate()?; - // Bound num_private_input_pages before allocating PageConfigs. The tight max is the - // honest span of a MAX-size input plus its 4-byte length prefix: - // ceil((MAX_PRIVATE_INPUT_SIZE + 4) / DEFAULT_PAGE_SIZE) pages (no slack). + // Bound num_private_input_pages before allocating PageConfigs — the tight honest + // max, shared with the continuation verifier (see `page::max_private_input_pages`). { - use crate::tables::page::DEFAULT_PAGE_SIZE; - use executor::vm::memory::MAX_PRIVATE_INPUT_SIZE; - let max_pages = (MAX_PRIVATE_INPUT_SIZE as usize + 4).div_ceil(DEFAULT_PAGE_SIZE); + let max_pages = crate::tables::page::max_private_input_pages(); if vm_proof.num_private_input_pages > max_pages { return Err(Error::InvalidTableCounts(format!( "num_private_input_pages ({}) exceeds max ({max_pages})", diff --git a/prover/src/tables/local_to_global.rs b/prover/src/tables/local_to_global.rs index 6ff1e3284..668dc1353 100644 --- a/prover/src/tables/local_to_global.rs +++ b/prover/src/tables/local_to_global.rs @@ -83,7 +83,12 @@ pub const GENESIS_EPOCH: u64 = 0; pub const MAX_EPOCHS: u64 = 1 << 20; /// A cell's state when an epoch first touches it. -#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +// +// Deliberately NOT serde-derived: `value` is a private-input byte for a private +// first-read, so these types must never be serialized into a proof bundle (the +// bundle ships only the value-free `touched_page_bases`). Keeping the derives off +// makes re-introducing that leak a compile error, not a silent regression. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct InitClaim { /// Value the cell held when this epoch first touched it. pub value: u64, @@ -97,7 +102,8 @@ pub struct InitClaim { } /// A cell's state at the end of the epoch that touched it. -#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +/// (Not serde-derived — see [`InitClaim`].) +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct FiniClaim { /// Value the cell holds at this epoch's end. pub value: u64, @@ -107,8 +113,10 @@ pub struct FiniClaim { pub timestamp: u64, } -/// The init/fini boundary claims for a single touched cell. -#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +/// The init/fini boundary claims for a single touched cell. Prover-local only: +/// it holds cell values, so it is never serialized (not serde-derived — see +/// [`InitClaim`]). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct CellBoundary { pub address: u64, pub init: InitClaim, diff --git a/prover/src/tables/page.rs b/prover/src/tables/page.rs index e02a46a80..f17338f41 100644 --- a/prover/src/tables/page.rs +++ b/prover/src/tables/page.rs @@ -155,6 +155,68 @@ impl PageConfig { } } +// ========================================================================= +// Private-input page math (shared by the monolithic and continuation paths) +// ========================================================================= + +/// Number of pages the private input occupies, starting at +/// `PRIVATE_INPUT_START_INDEX`. The wire format is the 4-byte length prefix plus +/// the data ([`Memory::store_private_inputs`]), and `PRIVATE_INPUT_START_INDEX` is +/// page-aligned, so the span is `ceil((prefix + len) / page_size)` consecutive +/// pages (0 when there is no input). +/// +/// SINGLE source of truth: the monolithic trace builder, the continuation prover, +/// and both verifiers' classification all derive from this count — a divergence +/// would make one path build a private page preprocessed (ELF-recomputed) while +/// the other commits it, which is a soundness bug, so do not reimplement it. +/// +/// [`Memory::store_private_inputs`]: executor::vm::memory::Memory::store_private_inputs +pub fn private_input_page_count(private_inputs: &[u8]) -> usize { + use executor::vm::memory::PRIVATE_INPUT_LENGTH_PREFIX_BYTES; + if private_inputs.is_empty() { + return 0; + } + (PRIVATE_INPUT_LENGTH_PREFIX_BYTES + private_inputs.len()).div_ceil(DEFAULT_PAGE_SIZE) +} + +/// Whether `page_base` is one of the first `num_private_input_pages` pages starting +/// at `PRIVATE_INPUT_START_INDEX` — the page-aligned span private input actually +/// occupies (see [`private_input_page_count`]). Classifying by the count (not the +/// raw `[START, START+MAX_PRIVATE_INPUT_SIZE)` byte range) keeps prover and +/// verifier in lockstep regardless of whether the region end is page-aligned. +/// +/// NOTE: a page classified private is built non-preprocessed, so its genesis is NOT +/// recomputed from the ELF. This is safe because the private-input area is reserved +/// and the reservation is enforced: `Elf::load` rejects any loadable segment +/// reaching at/above `PRIVATE_INPUT_START_INDEX` +/// (`ElfError::SegmentInPrivateInputRegion`) — covering every page this function +/// can classify private — so no ELF-declared data can live there and have its +/// genesis go unbound. +pub fn is_private_input_page(page_base: u64, num_private_input_pages: usize) -> bool { + use executor::vm::memory::PRIVATE_INPUT_START_INDEX; + let page_size = DEFAULT_PAGE_SIZE as u64; + let end = PRIVATE_INPUT_START_INDEX + num_private_input_pages as u64 * page_size; + (PRIVATE_INPUT_START_INDEX..end).contains(&page_base) +} + +/// The page bases of the first `num_private_input_pages` private-input pages, in +/// ascending order — the enumeration counterpart of [`is_private_input_page`] +/// (`is_private_input_page(b, n)` holds exactly for the aligned bases this yields). +pub fn private_input_page_bases(num_private_input_pages: usize) -> impl Iterator { + use executor::vm::memory::PRIVATE_INPUT_START_INDEX; + let page_size = DEFAULT_PAGE_SIZE as u64; + (0..num_private_input_pages as u64).map(move |i| PRIVATE_INPUT_START_INDEX + i * page_size) +} + +/// Upper bound on `num_private_input_pages` any honest proof can claim: the span of +/// a MAX-size input including its length prefix — no slack (an honest max-size +/// input occupies exactly this many pages). Both the monolithic and continuation +/// verifiers bound the deserialized, untrusted count with this before sizing AIRs. +pub fn max_private_input_pages() -> usize { + use executor::vm::memory::{MAX_PRIVATE_INPUT_SIZE, PRIVATE_INPUT_LENGTH_PREFIX_BYTES}; + (MAX_PRIVATE_INPUT_SIZE as usize + PRIVATE_INPUT_LENGTH_PREFIX_BYTES).div_ceil(DEFAULT_PAGE_SIZE) +} + // ========================================================================= // Trace generation // ========================================================================= diff --git a/prover/src/tables/trace_builder.rs b/prover/src/tables/trace_builder.rs index f3ca090d7..44bbdf3be 100644 --- a/prover/src/tables/trace_builder.rs +++ b/prover/src/tables/trace_builder.rs @@ -2457,19 +2457,12 @@ fn generate_page_tables( let mut pages = Vec::new(); let mut page_configs = Vec::new(); - // Determine which page bases hold private input data. - let private_input_page_bases: std::collections::BTreeSet = if !private_input.is_empty() { - use executor::vm::memory::PRIVATE_INPUT_START_INDEX; - let total_bytes = 4 + private_input.len(); // length prefix + data - (0..total_bytes) - .map(|i| page::page_base_for_address(PRIVATE_INPUT_START_INDEX + i as u64)) - .collect() - } else { - std::collections::BTreeSet::new() - }; + // Determine which page bases hold private input data — count-based, via the + // shared helpers (single source of truth with the continuation path). + let num_private_input_pages = page::private_input_page_count(private_input); for &page_base in &page_bases { - let config = if private_input_page_bases.contains(&page_base) { + let config = if page::is_private_input_page(page_base, num_private_input_pages) { let init_data = init_page_data.get(&page_base).cloned().unwrap_or_default(); PageConfig::with_private_input(page_base, init_data) } else if let Some(init_data) = init_page_data.get(&page_base) { @@ -3871,16 +3864,12 @@ impl Traces { } // Add private-input pages (non-preprocessed, verifier doesn't know init values) - if num_private_input_pages > 0 { - use executor::vm::memory::PRIVATE_INPUT_START_INDEX; - let first_page_base = page::page_base_for_address(PRIVATE_INPUT_START_INDEX); - for i in 0..num_private_input_pages { - configs.push(PageConfig { - page_base: first_page_base + i as u64 * page_size as u64, - init_values: None, // Verifier doesn't know these - is_private_input: true, - }); - } + for page_base in page::private_input_page_bases(num_private_input_pages) { + configs.push(PageConfig { + page_base, + init_values: None, // Verifier doesn't know these + is_private_input: true, + }); } configs.sort_by_key(|c| c.page_base); From 4b527324e49bbfded1c1c59b2d8f4266d947057c Mon Sep 17 00:00:00 2001 From: MauroFab Date: Fri, 3 Jul 2026 15:19:04 -0300 Subject: [PATCH 2/2] Run cargo fmt --- prover/src/tables/page.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/prover/src/tables/page.rs b/prover/src/tables/page.rs index f17338f41..5907be201 100644 --- a/prover/src/tables/page.rs +++ b/prover/src/tables/page.rs @@ -214,7 +214,8 @@ pub fn private_input_page_bases(num_private_input_pages: usize) -> impl Iterator /// verifiers bound the deserialized, untrusted count with this before sizing AIRs. pub fn max_private_input_pages() -> usize { use executor::vm::memory::{MAX_PRIVATE_INPUT_SIZE, PRIVATE_INPUT_LENGTH_PREFIX_BYTES}; - (MAX_PRIVATE_INPUT_SIZE as usize + PRIVATE_INPUT_LENGTH_PREFIX_BYTES).div_ceil(DEFAULT_PAGE_SIZE) + (MAX_PRIVATE_INPUT_SIZE as usize + PRIVATE_INPUT_LENGTH_PREFIX_BYTES) + .div_ceil(DEFAULT_PAGE_SIZE) } // =========================================================================