-
Notifications
You must be signed in to change notification settings - Fork 1
perf: page preprocessed commitment #645
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -337,18 +337,29 @@ impl VmAirs { | |
| /// constant (e.g. the recursion guest, where the in-VM recompute is too | ||
| /// expensive). When `None`, the commitment is computed from the ELF. | ||
| /// | ||
| /// The trust anchor for `decode_commitment` is the caller's compiled | ||
| /// binary — never accept prover-supplied bytes here. A wrong value is | ||
| /// rejected, never silently accepted: it either mismatches the prover's | ||
| /// committed precomputed root (an explicit verifier check) or yields | ||
| /// diverging Fiat-Shamir challenges. | ||
| /// `page_commitments` is an optional list of precomputed ELF-data-page | ||
| /// preprocessed commitments, keyed by `page_base`. For each ELF data page | ||
| /// the verifier constructs, if a matching `(page_base, commitment)` pair | ||
| /// is supplied, it is used directly and that page's FFT + Merkle build is | ||
| /// skipped. Pages not in the list — including all zero-init pages and | ||
| /// pages without a match — take the normal compute path (zero-init pages | ||
| /// hit a compile-time constant via `page::preprocessed_commitment`, | ||
| /// ELF data pages recompute from the ELF). When `None`, every ELF data | ||
| /// page recomputes from scratch. | ||
| /// | ||
| /// The trust anchor for both `decode_commitment` and `page_commitments` | ||
| /// is the caller's compiled binary — never accept prover-supplied bytes | ||
| /// here. A wrong value is rejected, never silently accepted: it either | ||
| /// mismatches the prover's committed precomputed root (an explicit | ||
| /// verifier check) or yields diverging Fiat-Shamir challenges. | ||
| pub fn new( | ||
| elf: &Elf, | ||
| proof_options: &ProofOptions, | ||
| minimal_bitwise: bool, | ||
| page_configs: &[crate::tables::page::PageConfig], | ||
| table_counts: &TableCounts, | ||
| decode_commitment: Option<Commitment>, | ||
| page_commitments: Option<&[(u64, Commitment)]>, | ||
| ) -> Self { | ||
| let cpus: Vec<_> = (0..table_counts.cpu) | ||
| .map(|i| create_cpu_air(proof_options).with_name(&format!("CPU[{}]", i))) | ||
|
|
@@ -411,13 +422,30 @@ impl VmAirs { | |
| // The verifier doesn't see the init values; correctness is enforced | ||
| // by the memory bus constraints. | ||
| create_page_air(proof_options, config.page_base) | ||
| } else { | ||
| // ELF and zero-init pages: OFFSET + INIT are preprocessed. | ||
| // The verifier independently recomputes the commitment from public data. | ||
| } else if config.init_values.is_none() { | ||
| // Zero-init pages: OFFSET + INIT are preprocessed and uniformly zero. | ||
| // `preprocessed_commitment` returns the compile-time constant | ||
| // for standard `(page_size, blowup_factor)` pairs and recomputes | ||
| // otherwise — caller-supplied values aren't useful here because the | ||
| // const is already cheaper than reading bytes through the API. | ||
| create_page_air(proof_options, config.page_base).with_preprocessed( | ||
| page::precomputed_commitment_cached(config, proof_options), | ||
| page::preprocessed_commitment(config, proof_options), | ||
| page::NUM_PREPROCESSED_COLS, | ||
| ) | ||
| } else { | ||
| // ELF data pages: INIT depends on the program's bytes. If the caller | ||
| // supplied a matching `(page_base, commitment)` pair via | ||
| // `page_commitments`, use it directly and skip the FFT + Merkle build; | ||
| // otherwise recompute from the ELF data. | ||
| let commitment = page_commitments | ||
| .and_then(|list| { | ||
| list.iter() | ||
| .find(|(pb, _)| *pb == config.page_base) | ||
| .map(|(_, c)| *c) | ||
| }) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
| .unwrap_or_else(|| page::preprocessed_commitment(config, proof_options)); | ||
| create_page_air(proof_options, config.page_base) | ||
| .with_preprocessed(commitment, page::NUM_PREPROCESSED_COLS) | ||
| } | ||
| }) | ||
| .collect(); | ||
|
|
@@ -663,6 +691,7 @@ pub fn prove_with_options_and_inputs( | |
| &traces.page_configs, | ||
| &table_counts, | ||
| None, | ||
| None, | ||
| ); | ||
|
|
||
| #[cfg(feature = "instruments")] | ||
|
|
@@ -735,6 +764,7 @@ pub fn verify(vm_proof: &VmProof, elf_bytes: &[u8]) -> Result<bool, Error> { | |
| elf_bytes, | ||
| &GoldilocksCubicProofOptions::with_blowup(2).expect("blowup=2 is always valid"), | ||
| None, | ||
| None, | ||
| ) | ||
| } | ||
|
|
||
|
|
@@ -751,8 +781,18 @@ pub fn verify(vm_proof: &VmProof, elf_bytes: &[u8]) -> Result<bool, Error> { | |
| /// commitment as a compile-time constant to avoid the in-VM recompute | ||
| /// cost. When `None`, the verifier computes the commitment from the ELF. | ||
| /// | ||
| /// Trust model: `decode_commitment`, when supplied, must come from the | ||
| /// caller's compiled binary (e.g. a `const [u8; 32]`), never from prover- | ||
| /// `page_commitments` is an optional list of precomputed ELF-data-page | ||
| /// preprocessed commitments, keyed by `page_base`. For each ELF data page | ||
| /// the verifier constructs, if a matching `(page_base, commitment)` pair is | ||
| /// supplied, the FFT + Merkle build for that page is skipped. Pages without | ||
| /// a match — including all zero-init pages — fall through to | ||
| /// `page::preprocessed_commitment`, which returns a compile-time | ||
| /// constant for zero-init pages and recomputes for ELF data pages. When | ||
| /// `None`, every ELF data page recomputes from scratch. | ||
| /// | ||
| /// Trust model: both `decode_commitment` and `page_commitments`, when | ||
| /// supplied, must come from the caller's compiled binary (e.g. a | ||
| /// `const [u8; 32]` and a `const [(u64, [u8; 32])]`), never from prover- | ||
| /// supplied bytes. A wrong value is rejected, never silently accepted: it | ||
| /// either mismatches the prover's committed precomputed root (an explicit | ||
| /// verifier check) or yields diverging Fiat-Shamir challenges. | ||
|
|
@@ -761,6 +801,7 @@ pub fn verify_with_options( | |
| elf_bytes: &[u8], | ||
| proof_options: &ProofOptions, | ||
| decode_commitment: Option<Commitment>, | ||
| page_commitments: Option<&[(u64, Commitment)]>, | ||
| ) -> Result<bool, Error> { | ||
| // Validate table_counts before constructing AIRs. | ||
| // A malicious prover could set counts to 0, removing entire constraint sets. | ||
|
|
@@ -807,6 +848,7 @@ pub fn verify_with_options( | |
| &page_configs, | ||
| &vm_proof.table_counts, | ||
| decode_commitment, | ||
| page_commitments, | ||
| ); | ||
|
|
||
| // Recompute the COMMIT output bus offset from VmProof.public_output. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,7 +31,6 @@ | |
| //! | PAGE-C4 | Memory | `[0, address, timestamp, fini]` | 1 (sender) | | ||
|
|
||
| use std::collections::HashMap; | ||
| use std::sync::OnceLock; | ||
|
|
||
| use math::fft::bit_reversing::in_place_bit_reverse_permute; | ||
| use math::polynomial::Polynomial; | ||
|
|
@@ -225,17 +224,67 @@ pub fn generate_page_trace( | |
| // Preprocessed commitment | ||
| // ========================================================================= | ||
|
|
||
| /// Cached commitment for zero-initialized 4KB pages. | ||
| /// All zero-init pages of the same size have identical OFFSET and INIT columns. | ||
| /// Returns the static zero-init PAGE preprocessed commitment for | ||
| /// `(page_size, blowup_factor)`, or `None` if no value is shipped for that | ||
| /// pair. Values were generated by the `compute_static_commitments` binary at | ||
| /// the project's standard `coset_offset = 3` (the value every in-tree | ||
| /// `ProofOptions` constructor pins) and pinned by | ||
| /// `zero_page_static_matches_recompute_for_all_blowups` so any drift in the | ||
| /// AIR or FFT pipeline is caught at test time. The verifier reads these | ||
| /// from its compiled binary — no input data is trusted. | ||
| /// | ||
| /// INVARIANT: All callers within a process must use identical `ProofOptions`. | ||
| /// The cache is keyed only by page content, not by options. | ||
| static ZERO_PAGE_4K_COMMITMENT: OnceLock<Commitment> = OnceLock::new(); | ||
| /// Because OFFSET is page-relative (0..page_size-1) and INIT is uniformly | ||
| /// zero for zero-init pages, the commitment depends only on geometry — not | ||
| /// on `page_base` or the program being verified. A single entry covers | ||
| /// every zero-init page of that `page_size` in the system. | ||
| /// | ||
| /// # Regenerating | ||
| /// | ||
| /// Only regenerate these match arms after a *deliberate, reviewed* change | ||
| /// to the PAGE table layout, the AIR's preprocessed column count, or the | ||
| /// FFT / LDE / Merkle pipeline. Run: | ||
| /// | ||
| /// ```text | ||
| /// cargo run --bin compute_static_commitments --release | ||
| /// ``` | ||
| /// | ||
| /// and paste the printed match arms over the ones below. | ||
| /// | ||
| /// **If a drift test failed, do not regenerate first.** The drift tests | ||
| /// exist to force a human to ask "why did this change?" before the new | ||
| /// bytes get blessed. Re-pasting on a drift failure silently launders an | ||
| /// unintended table change into the verifier's compiled-in trust anchor. | ||
| fn static_zero_page_commitment(page_size: usize, blowup_factor: u8) -> Option<Commitment> { | ||
| match (page_size, blowup_factor) { | ||
| (DEFAULT_PAGE_SIZE, 2) => Some([ | ||
| 0xf9, 0x80, 0x0e, 0x45, 0x72, 0x5a, 0x8e, 0x8e, 0x5e, 0xd7, 0x5b, 0x60, 0xce, 0xd0, | ||
| 0x8e, 0xa3, 0x27, 0x3b, 0x8a, 0xb5, 0x98, 0xc0, 0xe3, 0x16, 0xf6, 0x86, 0x75, 0x39, | ||
| 0x4c, 0xe5, 0x88, 0x5e, | ||
| ]), | ||
| (DEFAULT_PAGE_SIZE, 4) => Some([ | ||
| 0x0f, 0xb5, 0x0c, 0xa8, 0x3b, 0x69, 0x4f, 0x91, 0x60, 0xbf, 0x0d, 0x0d, 0xd3, 0x33, | ||
| 0x25, 0x38, 0x11, 0xbb, 0xf8, 0xfd, 0x54, 0xbd, 0x06, 0x7d, 0xd1, 0xeb, 0xa3, 0x58, | ||
| 0xe8, 0x37, 0x45, 0x56, | ||
| ]), | ||
| (DEFAULT_PAGE_SIZE, 8) => Some([ | ||
| 0x4a, 0xfb, 0xc9, 0x6d, 0x46, 0x29, 0xa3, 0xc2, 0x36, 0x14, 0xd8, 0x24, 0x3e, 0xef, | ||
| 0x97, 0x3f, 0xe1, 0xda, 0x2b, 0xf7, 0x87, 0xb6, 0x54, 0xe1, 0xc6, 0x46, 0xc0, 0x85, | ||
| 0x96, 0x7f, 0x7f, 0x48, | ||
| ]), | ||
| _ => None, | ||
| } | ||
| } | ||
|
|
||
| /// Computes the Merkle root commitment over the LDE of PAGE precomputed columns. | ||
| /// | ||
| /// The commitment covers OFFSET (0..page_size-1) and INIT (from config). | ||
| /// Each page may have different INIT data, producing a different commitment. | ||
| /// | ||
| /// Exposed for the `compute_static_commitments` binary and the | ||
| /// drift-detection tests in `static_commitments_tests`. Production callers | ||
| /// should go through [`preprocessed_commitment`] so the static const-table | ||
| /// shortcut is used when applicable. | ||
| #[doc(hidden)] | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor naming nit: this function is |
||
| pub fn compute_precomputed_commitment(config: &PageConfig, options: &ProofOptions) -> Commitment { | ||
| let page_size = config.page_size; | ||
| assert!(page_size.is_power_of_two(), "Page size must be power of 2"); | ||
|
|
@@ -294,16 +343,33 @@ pub fn compute_precomputed_commitment(config: &PageConfig, options: &ProofOption | |
| tree.root | ||
| } | ||
|
|
||
| /// Returns the preprocessed commitment for a PAGE table, with caching for zero-init pages. | ||
| /// Returns the preprocessed commitment for a PAGE table. | ||
| /// | ||
| /// Zero-init pages of DEFAULT_PAGE_SIZE share a cached commitment. | ||
| /// ELF data pages compute their commitment fresh. | ||
| pub fn precomputed_commitment_cached(config: &PageConfig, options: &ProofOptions) -> Commitment { | ||
| if config.init_values.is_none() && config.page_size == DEFAULT_PAGE_SIZE { | ||
| *ZERO_PAGE_4K_COMMITMENT.get_or_init(|| compute_precomputed_commitment(config, options)) | ||
| } else { | ||
| compute_precomputed_commitment(config, options) | ||
| /// For zero-init pages, looks up `(page_size, blowup_factor)` in | ||
| /// [`static_zero_page_commitment`] when `coset_offset == 3` (the value the | ||
| /// static bytes were generated for); on miss — either a non-3 coset or a | ||
| /// `(page_size, blowup_factor)` pair outside the shipped match arms — | ||
| /// logs a warning and recomputes from scratch. ELF data pages always | ||
| /// recompute (no warning): their INIT column is program-dependent. | ||
| pub fn preprocessed_commitment(config: &PageConfig, options: &ProofOptions) -> Commitment { | ||
| if config.init_values.is_none() { | ||
| if options.coset_offset == 3 | ||
| && let Some(commitment) = | ||
| static_zero_page_commitment(config.page_size, options.blowup_factor) | ||
| { | ||
| return commitment; | ||
| } | ||
| log::warn!( | ||
| "zero-init page preprocessed commitment not static for \ | ||
| (page_size={}, blowup={}, coset={}); falling back to recompute. \ | ||
| Add a match arm to `static_zero_page_commitment` by running \ | ||
| `cargo run --bin compute_static_commitments --release`.", | ||
| config.page_size, | ||
| options.blowup_factor, | ||
| options.coset_offset, | ||
| ); | ||
| } | ||
| compute_precomputed_commitment(config, options) | ||
| } | ||
|
|
||
| // ========================================================================= | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Low – undocumented first-match-wins behaviour
If
page_commitmentscontains two entries with the samepage_base(e.g. a caller accidentally duplicates an entry),.find()silently uses the first one. A wrong commitment will still cause Fiat-Shamir rejection, so this can't silently accept a bad proof, but it can cause a head-scratcher where a caller updates the second copy of a duplicate and wonders why nothing changes.Consider documenting the contract ("each
page_basemust appear at most once") in theverify_with_options/VmAirs::newdoc-comments, or adding adebug_assert!that the slice has no duplicatepage_basevalues.