diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f5edc89 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "cSpell.words": [ + "soundcalc", + "zkvm", + "zkvms" + ] +} \ No newline at end of file diff --git a/README.md b/README.md index aaafc6c..52c073f 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ > 🔎 **Latest report lives in [`results.md`](results.md)** -A universal soundness calculator across FRI-based zkEVM proof systems and security regimes. +A universal soundness calculator across hash-based zkEVM proof systems and security regimes. It aims to answer questions like: - "What if RISC0 moves from Babybear⁴ to Goldilocks³?" diff --git a/results.md b/results.md index b627555..e79b8a5 100644 --- a/results.md +++ b/results.md @@ -11,15 +11,20 @@ How to read this report: - [ZisK](#zisk) - [Miden](#miden) - [RISC0](#risc0) +- [DummyWHIR](#dummywhir) ## ZisK **Parameters:** +- Polynomial commitment scheme: FRI +- Hash size (bits): 256 - Number of queries: 128 - Grinding (bits): 0 - Field: Goldilocks³ - Rate (ρ): 0.5 - Trace length (H): $2^{22}$ +- FRI folding factor: 16 +- FRI early stop degree: 32 - Batching: Powers **Proof Size Estimate:** 992 KiB, where 1 KiB = 1024 bytes @@ -33,11 +38,15 @@ How to read this report: ## Miden **Parameters:** +- Polynomial commitment scheme: FRI +- Hash size (bits): 256 - Number of queries: 27 - Grinding (bits): 16 - Field: Goldilocks² - Rate (ρ): 0.125 - Trace length (H): $2^{18}$ +- FRI folding factor: 4 +- FRI early stop degree: 128 - Batching: Powers **Proof Size Estimate:** 175 KiB, where 1 KiB = 1024 bytes @@ -51,11 +60,15 @@ How to read this report: ## RISC0 **Parameters:** +- Polynomial commitment scheme: FRI +- Hash size (bits): 256 - Number of queries: 50 - Grinding (bits): 0 - Field: BabyBear⁴ - Rate (ρ): 0.25 - Trace length (H): $2^{21}$ +- FRI folding factor: 16 +- FRI early stop degree: 256 - Batching: Powers **Proof Size Estimate:** 576 KiB, where 1 KiB = 1024 bytes @@ -65,3 +78,25 @@ How to read this report: | UDR | 33 | 115 | 100 | 92 | 96 | 33 | | JBR | 47 | 110 | 95 | 70 | 90 | 47 | | best attack | 99 | — | — | — | — | — | + +## DummyWHIR + +**Parameters:** +- Polynomial commitment scheme: WHIR +- Hash size (bits): 256 +- Field: Goldilocks² +- Iterations (M): 5 +- Folding factor (k): 4 +- Constraint degree: 1 +- Batch size: 100 +- Batching: Powers +- Queries per iteration: [80, 35, 22, 12, 9] +- OOD samples per iteration: [2, 2, 2, 2] +- Total grinding overhead log2: 20.03 + +**Proof Size Estimate:** 168 KiB, where 1 KiB = 1024 bytes + +| regime | total | OOD(i=1) | OOD(i=2) | OOD(i=3) | OOD(i=4) | Shift(i=1) | Shift(i=2) | Shift(i=3) | Shift(i=4) | batching | fin | fold(i=0,s=1) | fold(i=0,s=2) | fold(i=0,s=3) | fold(i=0,s=4) | fold(i=1,s=1) | fold(i=1,s=2) | fold(i=1,s=3) | fold(i=1,s=4) | fold(i=2,s=1) | fold(i=2,s=2) | fold(i=2,s=3) | fold(i=2,s=4) | fold(i=3,s=1) | fold(i=3,s=2) | fold(i=3,s=3) | fold(i=3,s=4) | fold(i=4,s=1) | fold(i=4,s=2) | fold(i=4,s=3) | fold(i=4,s=4) | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| UDR | 21 | 219 | 227 | 235 | 243 | 33 | 31 | 21 | 23 | 107 | 28 | 113 | 114 | 115 | 116 | 114 | 115 | 116 | 117 | 115 | 116 | 117 | 118 | 116 | 117 | 118 | 119 | 117 | 118 | 119 | 120 | +| JBR | 36 | 203 | 205 | 207 | 209 | 36 | 68 | 76 | 71 | 53 | 78 | 62 | 64 | 66 | 68 | 59 | 61 | 63 | 65 | 57 | 59 | 61 | 63 | 54 | 56 | 58 | 60 | 52 | 54 | 56 | 58 | diff --git a/soundcalc/common/fields.py b/soundcalc/common/fields.py index 5dd5ac6..38571cb 100644 --- a/soundcalc/common/fields.py +++ b/soundcalc/common/fields.py @@ -18,6 +18,12 @@ class FieldParams: # Extension field size |F| = p^{ext_size} F: float + def to_string(self) -> str: + """ + Returns a human-readable string representing the field, + """ + return self.name + def _F(p: int, ext_size: int) -> float: # Keep as float to match existing zkEVMConfig expectations @@ -31,28 +37,28 @@ def _F(p: int, ext_size: int) -> float: # Preset extension fields GOLDILOCKS_2 = FieldParams( - name="Goldilocks^2", + name="Goldilocks²", p=GOLDILOCKS_P, field_extension_degree=2, F=_F(GOLDILOCKS_P, 2), ) GOLDILOCKS_3 = FieldParams( - name="Goldilocks^3", + name="Goldilocks³", p=GOLDILOCKS_P, field_extension_degree=3, F=_F(GOLDILOCKS_P, 3), ) BABYBEAR_4 = FieldParams( - name="BabyBear^4", + name="BabyBear⁴", p=BABYBEAR_P, field_extension_degree=4, F=_F(BABYBEAR_P, 4), ) BABYBEAR_5 = FieldParams( - name="BabyBear^5", + name="BabyBear⁵", p=BABYBEAR_P, field_extension_degree=5, F=_F(BABYBEAR_P, 5), diff --git a/soundcalc/common/fri.py b/soundcalc/common/fri.py index fe365d4..07cbed0 100644 --- a/soundcalc/common/fri.py +++ b/soundcalc/common/fri.py @@ -7,8 +7,10 @@ import math from typing import TYPE_CHECKING +from soundcalc.common.utils import get_size_of_merkle_path_bits + if TYPE_CHECKING: - from ..zkevms.zkevm import zkEVMParams + from ..zkvms.zkvm import FRIBasedVM def get_johnson_parameter_m() -> float: """ @@ -52,23 +54,6 @@ def get_FRI_query_phase_error(theta: float, num_queries: int, grinding_bits: int return FRI_query_phase_error -def get_size_of_merkle_path_bits(num_leafs: int, tuple_size: int, element_size_bits: int, hash_size_bits: int) -> int: - """ - Compute the size of a Merkle path in bits. - - We assume a Merkle tree that represents num_leafs tuples of elements - where each element has size element_size_bits and one tuple contains tuple_size - many elements. Each leaf of the tree contains one such tuple. - - Note: the result counts both the leaf itself and the Merkle path. - """ - assert num_leafs > 0 - leaf_size = tuple_size * element_size_bits - sibling = tuple_size * element_size_bits - tree_depth = math.ceil(math.log2(num_leafs)) - co_path = (tree_depth - 1) * hash_size_bits - return leaf_size + sibling + co_path - def get_FRI_proof_size_bits( hash_size_bits: int, diff --git a/soundcalc/common/utils.py b/soundcalc/common/utils.py index 45e4241..735bebe 100644 --- a/soundcalc/common/utils.py +++ b/soundcalc/common/utils.py @@ -1,47 +1,36 @@ from __future__ import annotations -from ..zkevms.zkevm import zkEVMParams - import math KIB = (1024 * 8) # Kilobytes + def get_rho_plus(H: int, D: float, max_combo: int) -> float: """Compute rho+. See page 16 of Ha22""" # XXX Should this be (H + 2) / D? This part is cryptic in [Ha22] # TODO Figure out return (H + max_combo) / D -def get_DEEP_ALI_errors(L_plus: float, params: zkEVMParams): +def get_bits_of_security_from_error(error: float) -> int: """ - Compute common proof system error components that are shared across regimes. - Some of them depend on the list size L_plus - - Returns a dictionary containing levels for ALI and DEEP + Returns the maximum k such that error <= 2^{-k} """ + return int(math.floor(-math.log2(error))) - # TODO Check that it holds for all regimes - # XXX These proof system errors are actually quite RISC0 specific. - # See Section 3.4 from the RISC0 technical report. - # We might want to generalize this further for other zkEVMs. - # For example, Miden also computes similar values for DEEP-ALI in: - # https://github.com/facebook/winterfell/blob/2f78ee9bf667a561bdfcdfa68668d0f9b18b8315/air/src/proof/security.rs#L188-L210 - e_ALI = L_plus * params.num_columns / params.F - e_DEEP = ( - L_plus - * (params.AIR_max_degree * (params.trace_length + params.max_combo - 1) + (params.trace_length - 1)) - / (params.F - params.trace_length - params.D) - ) - - levels = {} - levels["ALI"] = get_bits_of_security_from_error(e_ALI) - levels["DEEP"] = get_bits_of_security_from_error(e_DEEP) +def get_size_of_merkle_path_bits(num_leafs: int, tuple_size: int, element_size_bits: int, hash_size_bits: int) -> int: + """ + Compute the size of a Merkle path in bits. - return levels + We assume a Merkle tree that represents num_leafs tuples of elements + where each element has size element_size_bits and one tuple contains tuple_size + many elements. Each leaf of the tree contains one such tuple. -def get_bits_of_security_from_error(error: float) -> int: - """ - Returns the maximum k such that error <= 2^{-k} + Note: the result counts both the leaf itself and the Merkle path. """ - return int(math.floor(-math.log2(error))) \ No newline at end of file + assert num_leafs > 0 + leaf_size = tuple_size * element_size_bits + sibling = tuple_size * element_size_bits + tree_depth = math.ceil(math.log2(num_leafs)) + co_path = (tree_depth - 1) * hash_size_bits + return leaf_size + sibling + co_path \ No newline at end of file diff --git a/soundcalc/main.py b/soundcalc/main.py index 913cc7b..06e19b8 100644 --- a/soundcalc/main.py +++ b/soundcalc/main.py @@ -1,48 +1,15 @@ from __future__ import annotations import json -from soundcalc.common.utils import KIB, get_DEEP_ALI_errors -from soundcalc.regimes.best_attack import best_attack_security -from soundcalc.zkevms.risc0 import Risc0Preset -from soundcalc.zkevms.miden import MidenPreset -from soundcalc.zkevms.zisk import ZiskPreset -from soundcalc.regimes.johnson_bound import JohnsonBoundRegime -from soundcalc.regimes.unique_decoding import UniqueDecodingRegime +from soundcalc.common.utils import KIB +from soundcalc.zkvms.dummy_whir import DummyWHIRPreset +from soundcalc.zkvms.risc0 import Risc0Preset +from soundcalc.zkvms.miden import MidenPreset +from soundcalc.zkvms.zisk import ZiskPreset from soundcalc.report import build_markdown_report +from soundcalc.zkvms.zkvm import zkVM -def get_rbr_levels_for_zkevm_and_regime(regime, params) -> dict[str, int]: - - # the round-by-round errors consist of the ones for FRI and for the proof system - # and we also add a total, which is the minimum over all of them. - - fri_levels = regime.get_rbr_levels(params) - list_size = regime.get_bound_on_list_size(params) - - proof_system_levels = get_DEEP_ALI_errors(list_size, params) - - total = min(list(fri_levels.values()) + list(proof_system_levels.values())) - - return fri_levels | proof_system_levels | {"total": total} - - - -def compute_security_for_zkevm(fri_regimes: list, params) -> dict[str, dict]: - """ - Compute bits of security for a single zkEVM across all security regimes. - """ - results: dict[str, dict] = {} - - # first all reasonable regimes - for fri_regime in fri_regimes: - rbr_errors = get_rbr_levels_for_zkevm_and_regime(fri_regime, params) - results[fri_regime.identifier()] = rbr_errors - - # now the security based on the best known attack - for reference - results["best attack"] = best_attack_security(params) - - return results - def generate_and_save_md_report(sections) -> None: """ @@ -57,44 +24,51 @@ def generate_and_save_md_report(sections) -> None: print(f"wrote :: {md_path}") -def print_summary_for_zkevm(zkevm_params, results: dict[str, dict]) -> None: +def print_summary_for_zkvm(zkvm: zkVM, security_levels: dict | None = None) -> None: """ - Print a summary of security results for a single zkEVM. + Print a summary of security results for a single zkVM. """ - print(f"zkEVM: {zkevm_params.name}") - proof_size_kib = zkevm_params.proof_size_bits // KIB - print(f" proof size estimate: {proof_size_kib} KiB, where 1 KiB = 1024 bytes") - print(json.dumps(results, indent=4)) + print("") + print("#############################################") + print(f"# zkVM: {zkvm.get_name()}") + print("#############################################") + proof_size_kib = zkvm.get_proof_size_bits() // KIB + print("") + print(f"proof size estimate: {proof_size_kib} KiB, where 1 KiB = 1024 bytes") + print("") + print(f"parameters: \n {zkvm.get_parameter_summary()}") + print("") + if security_levels is None: + security_levels = zkvm.get_security_levels() + print(f"security levels (rbr): \n {json.dumps(security_levels, indent=4)}") + print("") + print("") print("") print("") - def main() -> None: """ Main entry point for soundcalc - Analyze multiple zkEVMs across different security regimes, + Analyze multiple zkVMs across different security regimes, generate reports, and save results to disk. """ - # Data structure for compiling the markdown report - sections = {} - zkevms = [ + sections: dict[str, tuple[zkVM, dict[str, dict]]] = {} + + # We consider the following zkVMs + zkvms = [ ZiskPreset.default(), MidenPreset.default(), Risc0Preset.default(), + DummyWHIRPreset.default(), ] - security_regimes = [ - UniqueDecodingRegime(), - JohnsonBoundRegime(), - ] - - # Analyze each zkEVM across all security regimes - for zkevm_params in zkevms: - results = compute_security_for_zkevm(security_regimes, zkevm_params) - print_summary_for_zkevm(zkevm_params, results) - sections[zkevm_params.name] = (zkevm_params, results) + # Analyze each zkVM + for zkvm in zkvms: + security_levels = zkvm.get_security_levels() + print_summary_for_zkvm(zkvm, security_levels) + sections[zkvm.get_name()] = (zkvm, security_levels) # Generate and save markdown report generate_and_save_md_report(sections) diff --git a/soundcalc/proxgaps/__init__.py b/soundcalc/proxgaps/__init__.py new file mode 100644 index 0000000..b28b04f --- /dev/null +++ b/soundcalc/proxgaps/__init__.py @@ -0,0 +1,3 @@ + + + diff --git a/soundcalc/proxgaps/johnson_bound.py b/soundcalc/proxgaps/johnson_bound.py new file mode 100644 index 0000000..0d30749 --- /dev/null +++ b/soundcalc/proxgaps/johnson_bound.py @@ -0,0 +1,60 @@ + + +from soundcalc.common.fields import FieldParams +from soundcalc.proxgaps.proxgaps_regime import ProximityGapsRegime + +import math + +class JohnsonBoundRegime(ProximityGapsRegime): + """ + Johnson Bound Regime (JBR). + """ + def identifier(self) -> str: + return "JBR" + + def get_max_delta(self, rate: float, dimension: int, field: FieldParams) -> float: + + eta = self.get_eta(rate) + + # And proximity parameter theta = 1 - sqrt(rho) - eta + # = 1 - sqrt(rho) * (1 + 1/ (2m) ) + # as required by Theorem 2 of Ha22. + alpha = math.sqrt(rate) + eta + theta = 1 - alpha + return theta + + def get_max_list_size(self, rate: float, dimension: int, field: FieldParams, delta: float) -> int: + assert delta <= self.get_max_delta(rate, dimension, field) + + # following https://github.com/WizardOfMenlo/stir-whir-scripts/blob/main/src/errors.rs#L43 + # By the JB, RS codes are (1 - √ρ - η, (2*η*√ρ)^-1)-list decodable. + eta = self.get_eta(rate) + return 1.0 / (2 * eta * math.sqrt(rate)) + + def get_eta(self, rate) -> float: + # ASN This is hardcoded to 16, whereas winterfell brute forces it: + # https://github.com/facebook/winterfell/blob/main/air/src/proof/security.rs#L290-L306 + + m = 16 + + # ASN Is this a good value for eta? + eta = math.sqrt(rate) / (2 * m) + + return eta + + def get_error_powers(self, rate: float, dimension: int, field: FieldParams, num_functions: int) -> float: + return self.get_error_linear(rate, dimension, field, num_functions) * (num_functions - 1) + + + def get_error_linear(self, rate: float, dimension: int, field: FieldParams, num_functions: int) -> float: + + # following WHIR bound in Conjecture 4.12, and noting that 1 - √ρ - delta = η + exponent = 7 # TODO: in new results, this is probably just 5 + sqrt_rate_div_20 = math.sqrt(rate) / 20 + eta = self.get_eta(rate) + denominator = (2 * min(eta, sqrt_rate_div_20)) ** exponent + denominator *= field.F + + numerator = dimension * dimension # TODO: in new results, this is probably just dimension + + return numerator / denominator diff --git a/soundcalc/proxgaps/proxgaps_regime.py b/soundcalc/proxgaps/proxgaps_regime.py new file mode 100644 index 0000000..470cc4c --- /dev/null +++ b/soundcalc/proxgaps/proxgaps_regime.py @@ -0,0 +1,59 @@ +from soundcalc.common.fields import FieldParams + + +class ProximityGapsRegime: + """ + A class representing a regime for proximity gaps or (mutual) correlated agreement. + We only consider Reed-Solomon codes here, of dimension k, size n, and rate k/n. + """ + + def identifier(self) -> str: + """ + Returns the name of the regime. + """ + raise NotImplementedError + + def get_max_delta(self, rate: float, dimension: int, field: FieldParams) -> float: + """ + Returns the maximum delta for this regime, based on the rate + and the dimension of the code. + """ + raise NotImplementedError + + def get_max_list_size(self, rate: float, dimension: int, field: FieldParams, delta: float) -> int: + """ + Returns an upper bound on the list size for this regime, and for a given delta + E.g., unique decoding regime may return 1. + """ + raise NotImplementedError + + def get_error_powers(self, rate: float, dimension: int, field: FieldParams, num_functions: int) -> float: + """ + Returns an upper bound on the MCA error when applying a random linear combination. + The coefficients are assumed to be powers here. + + Note: the errors for correlated agreement in the following two cases differ, + which is related to the batching method: + + Case 1: we batch with randomness r^0, r^1, ..., r^{num_functions-1} + This is what is called batching over parameterized curves in BCIKS20. + Here, the error depends on num_functions (called l in BCIKS20), and we find + the error in Theorem 6.2. + + Case 2: we batch with randomness r_0 = 1, r_1, r_2, r_{num_functions-1} + This is what is called batching over affine spaces in BCIKS20. + Here, the error does not depend on num_functions (called l in BCIKS20), and we find + the error in Theorem 1.6. + + Then easiest way to see the difference is to compare Theorems 1.5 and 1.6. + """ + raise NotImplementedError + + def get_error_linear(self, rate: float, dimension: int, field: FieldParams, num_functions: int) -> float: + """ + Returns an upper bound on the MCA error when applying a random linear combination. + The coefficients are assumed to be independent here. + + See the comment above about the difference between powers and linear. + """ + raise NotImplementedError diff --git a/soundcalc/proxgaps/unique_decoding.py b/soundcalc/proxgaps/unique_decoding.py new file mode 100644 index 0000000..6e43790 --- /dev/null +++ b/soundcalc/proxgaps/unique_decoding.py @@ -0,0 +1,28 @@ + + +from soundcalc.common.fields import FieldParams +from soundcalc.proxgaps.proxgaps_regime import ProximityGapsRegime + + +class UniqueDecodingRegime(ProximityGapsRegime): + """ + Unique decoding Regime (UDR). + """ + def identifier(self) -> str: + return "UDR" + + def get_max_delta(self, rate: float, dimension: int, field: FieldParams) -> float: + return (1 - rate) / 2 + + def get_max_list_size(self, rate: float, dimension: int, field: FieldParams, delta: float) -> int: + assert delta <= self.get_max_delta(rate, dimension, field) + return 1 + + def get_error_powers(self, rate: float, dimension: int, field: FieldParams, num_functions: int) -> float: + n = dimension / rate + return num_functions * n / field.F + + + def get_error_linear(self, rate: float, dimension: int, field: FieldParams, num_functions: int) -> float: + n = dimension / rate + return n / field.F diff --git a/soundcalc/regimes/best_attack.py b/soundcalc/regimes/best_attack.py index c21b88d..776ed01 100644 --- a/soundcalc/regimes/best_attack.py +++ b/soundcalc/regimes/best_attack.py @@ -1,11 +1,12 @@ from __future__ import annotations +from soundcalc.regimes.fri_regime import FRIParameters + -from ..zkevms.zkevm import zkEVMParams from ..common.utils import get_bits_of_security_from_error -def best_attack_security(params: zkEVMParams) -> int: +def best_attack_security(params: FRIParameters) -> int: """ Security level based on the best known attack. diff --git a/soundcalc/regimes/capacity_bound.py b/soundcalc/regimes/capacity_bound.py index 68aedea..75f64cc 100644 --- a/soundcalc/regimes/capacity_bound.py +++ b/soundcalc/regimes/capacity_bound.py @@ -7,8 +7,7 @@ C2 = 1.0 C3 = 1.0 # List size related -from .fri_regime import FRIRegime -from ..zkevms.zkevm import zkEVMParams +from .fri_regime import FRIParameters, FRIRegime from soundcalc.common.fri import get_johnson_parameter_m from ..common.utils import get_rho_plus @@ -25,7 +24,7 @@ def identifier(self) -> str: return "CBR" - def get_bound_on_list_size(self, params: zkEVMParams) -> int: + def get_bound_on_list_size(self, params: FRIParameters) -> int: """ Returns an upper bound on the list size of this regime, i.e., the number of codewords a function is close to. @@ -44,7 +43,7 @@ def get_bound_on_list_size(self, params: zkEVMParams) -> int: return math.ceil((params.D / eta_plus) ** C3) - def get_theta(self, params: zkEVMParams) -> float: + def get_theta(self, params: FRIParameters) -> float: """ Returns the theta for the query phase error. """ @@ -53,7 +52,7 @@ def get_theta(self, params: zkEVMParams) -> float: return theta - def get_batching_error(self, params: zkEVMParams) -> float: + def get_batching_error(self, params: FRIParameters) -> float: """ Returns the error for the FRI batching step for this regime. """ @@ -64,14 +63,14 @@ def get_batching_error(self, params: zkEVMParams) -> float: # Note: the errors for correlated agreement in the following two cases differ, # which is related to the batching method: # - # Case 1: we batch with randomness r^0, r^1, ..., r^{num_polys-1} + # Case 1: we batch with randomness r^0, r^1, ..., r^{num_functions-1} # This is what is called batching over parameterized curves in BCIKS20. - # Here, the error depends on num_polys (called l in BCIKS20), and we find + # Here, the error depends on num_functions (called l in BCIKS20), and we find # the error in Conjecture 8.4, second item. # - # Case 2: we batch with randomness r_0 = 1, r_1, r_2, r_{num_polys-1} + # Case 2: we batch with randomness r_0 = 1, r_1, r_2, r_{num_functions-1} # This is what is called batching over affine spaces in BCIKS20. - # Here, the error does not depend on num_polys (called l in BCIKS20), and we find + # Here, the error does not depend on num_functions (called l in BCIKS20), and we find # the error in Conjecture 8.4, first item. # # Then easiest way to see the difference is to compare Theorems 1.5 and 1.6. @@ -79,10 +78,10 @@ def get_batching_error(self, params: zkEVMParams) -> float: term_two = (params.D ** C2) / params.F error = term_one * term_two if params.power_batching: - error *= params.num_polys ** C2 + error *= params.num_functions ** C2 return error - def get_commit_phase_error(self, params: zkEVMParams) -> float: + def get_commit_phase_error(self, params: FRIParameters) -> float: """ Returns the error for the FRI commit phase for this regime. """ @@ -90,7 +89,7 @@ def get_commit_phase_error(self, params: zkEVMParams) -> float: # It is just copied from JBR. # TODO Find a better formula for CBR. m = self._get_m() - error = (2 * m + 1) * (params.D + 1) * params.FRI_folding_factor / (math.sqrt(params.rho) * params.F) + error = (2 * m + 1) * (params.D + 1) * params.folding_factor / (math.sqrt(params.rho) * params.F) return error diff --git a/soundcalc/regimes/fri_regime.py b/soundcalc/regimes/fri_regime.py index 8328bbe..62ab212 100644 --- a/soundcalc/regimes/fri_regime.py +++ b/soundcalc/regimes/fri_regime.py @@ -1,12 +1,35 @@ from __future__ import annotations import math +from dataclasses import dataclass from typing import Optional, Dict, Any from soundcalc.common.fri import get_FRI_query_phase_error from soundcalc.common.utils import get_bits_of_security_from_error -from ..zkevms.zkevm import zkEVMParams +@dataclass(frozen=True) +class FRIParameters: + """ + Models the parameters that the FRI protocol has. + Note that this is different from FRI-based zkVM parameters, + as such a VM may have additional parameters. + """ + hash_size_bits: int + field_size_bits: int + rho: float + D: int + F: float + power_batching: bool + num_functions: int + num_queries: int + witness_size: int + field_extension_degree: int + early_stop_degree: int + FRI_rounds_n: int + folding_factor: int + grinding_query_phase: int + trace_length: int + max_combo: int class FRIRegime: @@ -22,32 +45,32 @@ def identifier(self) -> str: raise NotImplementedError - def get_bound_on_list_size(self, params: zkEVMParams) -> int: + def get_bound_on_list_size(self, params: FRIParameters) -> int: """ Returns an upper bound on the list size of this regime, i.e., the number of codewords a function is close to. For instance, this is 1 for the unique decoding regime. """ raise NotImplementedError - def get_theta(self, params: zkEVMParams) -> float: + def get_theta(self, params: FRIParameters) -> float: """ Returns the theta for the query phase error. """ raise NotImplementedError - def get_batching_error(self, params: zkEVMParams) -> float: + def get_batching_error(self, params: FRIParameters) -> float: """ Returns the error for the FRI batching step for this regime. """ raise NotImplementedError - def get_commit_phase_error(self, params: zkEVMParams) -> float: + def get_commit_phase_error(self, params: FRIParameters) -> float: """ Returns the error for the FRI commit phase for this regime. """ raise NotImplementedError - def get_rbr_levels(self, params: zkEVMParams) -> dict[str, int]: + def get_rbr_levels(self, params: FRIParameters) -> dict[str, int]: """ Returns a dictionary that contains the round-by-round soundness levels. It maps from a label that explains which round it is for to an integer. diff --git a/soundcalc/regimes/johnson_bound.py b/soundcalc/regimes/johnson_bound.py index 33f668b..e755be9 100644 --- a/soundcalc/regimes/johnson_bound.py +++ b/soundcalc/regimes/johnson_bound.py @@ -1,7 +1,6 @@ from __future__ import annotations -from .fri_regime import FRIRegime -from ..zkevms.zkevm import zkEVMParams +from .fri_regime import FRIParameters, FRIRegime from typing import Any from ..common.utils import get_rho_plus from soundcalc.common.fri import ( @@ -21,7 +20,7 @@ class JohnsonBoundRegime(FRIRegime): def identifier(self) -> str: return "JBR" - def get_bound_on_list_size(self, params: zkEVMParams) -> int: + def get_bound_on_list_size(self, params: FRIParameters) -> int: """ Returns an upper bound on the list size of this regime, i.e., the number of codewords a function is close to. @@ -46,7 +45,7 @@ def get_bound_on_list_size(self, params: zkEVMParams) -> int: # RISC0=35, Miden=64 return (m_plus + 0.5) / math.sqrt(r_plus) - def get_theta(self, params: zkEVMParams) -> float: + def get_theta(self, params: FRIParameters) -> float: """ Returns the theta for the query phase error. """ @@ -55,7 +54,7 @@ def get_theta(self, params: zkEVMParams) -> float: return theta - def get_batching_error(self, params: zkEVMParams) -> float: + def get_batching_error(self, params: FRIParameters) -> float: """ Returns the error for the FRI batching step for this regime. """ @@ -63,14 +62,14 @@ def get_batching_error(self, params: zkEVMParams) -> float: # Note: the errors for correlated agreement in the following two cases differ, # which is related to the batching method: # - # Case 1: we batch with randomness r^0, r^1, ..., r^{num_polys-1} + # Case 1: we batch with randomness r^0, r^1, ..., r^{num_functions-1} # This is what is called batching over parameterized curves in BCIKS20. - # Here, the error depends on num_polys (called l in BCIKS20), and we find + # Here, the error depends on num_functions (called l in BCIKS20), and we find # the error in Theorem 6.2. # - # Case 2: we batch with randomness r_0 = 1, r_1, r_2, r_{num_polys-1} + # Case 2: we batch with randomness r_0 = 1, r_1, r_2, r_{num_functions-1} # This is what is called batching over affine spaces in BCIKS20. - # Here, the error does not depend on num_polys (called l in BCIKS20), and we find + # Here, the error does not depend on num_functions (called l in BCIKS20), and we find # the error in Theorem 1.6. # # Then easiest way to see the difference is to compare Theorems 1.5 and 1.6. @@ -79,10 +78,10 @@ def get_batching_error(self, params: zkEVMParams) -> float: rho = params.rho error = ((m + 0.5) ** 5) / (3 * (rho ** 1.5)) * (params.D) / params.F if params.power_batching: - error *= params.num_polys + error *= params.num_functions return error - def get_commit_phase_error(self, params: zkEVMParams) -> float: + def get_commit_phase_error(self, params: FRIParameters) -> float: """ Returns the error for the FRI commit phase for this regime. """ @@ -95,7 +94,7 @@ def get_commit_phase_error(self, params: zkEVMParams) -> float: # TODO: check this formula carefully m = self._get_m() - error = (2 * m + 1) * (params.D + 1) * params.FRI_folding_factor / (math.sqrt(params.rho) * params.F) + error = (2 * m + 1) * (params.D + 1) * params.folding_factor / (math.sqrt(params.rho) * params.F) return error diff --git a/soundcalc/regimes/unique_decoding.py b/soundcalc/regimes/unique_decoding.py index cf4a5ca..a72e45c 100644 --- a/soundcalc/regimes/unique_decoding.py +++ b/soundcalc/regimes/unique_decoding.py @@ -1,7 +1,7 @@ from __future__ import annotations -from .fri_regime import FRIRegime -from ..zkevms.zkevm import zkEVMParams + +from .fri_regime import FRIParameters, FRIRegime class UniqueDecodingRegime(FRIRegime): @@ -17,18 +17,18 @@ class UniqueDecodingRegime(FRIRegime): def identifier(self) -> str: return "UDR" - def get_bound_on_list_size(self, params: zkEVMParams) -> int: + def get_bound_on_list_size(self, params: FRIParameters) -> int: # For unique decoding, list size is naturally 1 return 1 - def get_theta(self, params: zkEVMParams) -> float: + def get_theta(self, params: FRIParameters) -> float: """ Returns the theta for the query phase error. """ theta = (1 - params.rho) / 2 return theta - def get_batching_error(self, params: zkEVMParams) -> float: + def get_batching_error(self, params: FRIParameters) -> float: """ Returns the error for the FRI batching step for this regime. """ @@ -36,29 +36,29 @@ def get_batching_error(self, params: zkEVMParams) -> float: # Note: the errors for correlated agreement in the following two cases differ, # which is related to the batching method: # - # Case 1: we batch with randomness r^0, r^1, ..., r^{num_polys-1} + # Case 1: we batch with randomness r^0, r^1, ..., r^{num_functions-1} # This is what is called batching over parameterized curves in BCIKS20. - # Here, the error depends on num_polys (called l in BCIKS20), and we find + # Here, the error depends on num_functions (called l in BCIKS20), and we find # the error in Theorem 6.1 and Theorem 1.5. # - # Case 2: we batch with randomness r_0 = 1, r_1, r_2, r_{num_polys-1} + # Case 2: we batch with randomness r_0 = 1, r_1, r_2, r_{num_functions-1} # This is what is called batching over affine spaces in BCIKS20. - # Here, the error does not depend on num_polys (called l in BCIKS20), and we find + # Here, the error does not depend on num_functions (called l in BCIKS20), and we find # the error in Theorem 1.6. # # Then easiest way to see the difference is to compare Theorems 1.5 and 1.6. error = params.D / params.F if params.power_batching: - error *= params.num_polys + error *= params.num_functions return error - def get_commit_phase_error(self, params: zkEVMParams) -> float: + def get_commit_phase_error(self, params: FRIParameters) -> float: """ Returns the error for the FRI commit phase for this regime. """ D = params.D - FRI_folding_factor = params.FRI_folding_factor + FRI_folding_factor = params.folding_factor F = params.F fri_folding_error = (D * (FRI_folding_factor - 1)) / F diff --git a/soundcalc/report.py b/soundcalc/report.py index 64981e1..33ebb62 100644 --- a/soundcalc/report.py +++ b/soundcalc/report.py @@ -6,6 +6,65 @@ from typing import Dict, Any, List, Tuple from soundcalc.common.utils import KIB +from soundcalc.zkvms.fri_based_vm import FRIBasedVM +from soundcalc.zkvms.whir_based_vm import WHIRBasedVM + + +def _field_label(field) -> str: + if hasattr(field, "to_string"): + return field.to_string() + return "Unknown" + + +def _fri_parameter_lines(zkvm_obj: FRIBasedVM) -> list[str]: + batching = "Powers" if zkvm_obj.power_batching else "Affine" + return [ + f"- Polynomial commitment scheme: FRI", + f"- Hash size (bits): {zkvm_obj.hash_size_bits}", + f"- Number of queries: {zkvm_obj.num_queries}", + f"- Grinding (bits): {zkvm_obj.grinding_query_phase}", + f"- Field: {_field_label(zkvm_obj.field)}", + f"- Rate (ρ): {zkvm_obj.rho}", + f"- Trace length (H): $2^{{{zkvm_obj.h}}}$", + f"- FRI folding factor: {zkvm_obj.FRI_folding_factor}", + f"- FRI early stop degree: {zkvm_obj.FRI_early_stop_degree}", + f"- Batching: {batching}", + ] + + +def _whir_parameter_lines(zkvm_obj: WHIRBasedVM) -> list[str]: + batching = "Powers" if zkvm_obj.power_batching else "Affine" + return [ + f"- Polynomial commitment scheme: WHIR", + f"- Hash size (bits): {zkvm_obj.hash_size_bits}", + f"- Field: {_field_label(zkvm_obj.field)}", + f"- Iterations (M): {zkvm_obj.num_iterations}", + f"- Folding factor (k): {zkvm_obj.folding_factor}", + f"- Constraint degree: {zkvm_obj.constraint_degree}", + f"- Batch size: {zkvm_obj.batch_size}", + f"- Batching: {batching}", + f"- Queries per iteration: {zkvm_obj.num_queries}", + f"- OOD samples per iteration: {zkvm_obj.num_ood_samples}", + f"- Total grinding overhead log2: {zkvm_obj.log_grinding_overhead}", + ] + + +def _generic_parameter_lines(zkvm_obj) -> list[str]: + lines: list[str] = [] + lines.append(f"- Polynomial commitment scheme: Unknown") + if hasattr(zkvm_obj, "hash_size_bits"): + lines.append(f"- Hash size (bits): {zkvm_obj.hash_size_bits}") + if hasattr(zkvm_obj, "field"): + lines.append(f"- Field: {_field_label(zkvm_obj.field)}") + return lines + + +def _describe_vm(zkvm_obj): + if isinstance(zkvm_obj, FRIBasedVM): + return "", _fri_parameter_lines(zkvm_obj) + if isinstance(zkvm_obj, WHIRBasedVM): + return "", _whir_parameter_lines(zkvm_obj) + return "", _generic_parameter_lines(zkvm_obj) @@ -31,10 +90,13 @@ def build_markdown_report(sections) -> str: for zkevm in sections: anchor = zkevm.lower().replace(" ", "-") + + (zkvm_obj, results) = sections[zkevm] + commitment_label, parameter_lines = _describe_vm(zkvm_obj) + lines.append(f"## {zkevm}") lines.append("") - (zkevm_params, results) = sections[zkevm] display_results: dict[str, Any] = { name: data.copy() if isinstance(data, dict) else data for name, data in results.items() @@ -42,30 +104,14 @@ def build_markdown_report(sections) -> str: # Add parameter information lines.append(f"**Parameters:**") - lines.append(f"- Number of queries: {zkevm_params.num_queries}") - lines.append(f"- Grinding (bits): {zkevm_params.grinding_query_phase}") - # Get field name from the field extension degree and base field - field_name = "Unknown" - if hasattr(zkevm_params, 'field_extension_degree'): - if zkevm_params.field_extension_degree == 2: - field_name = "Goldilocks²" - elif zkevm_params.field_extension_degree == 3: - field_name = "Goldilocks³" - elif zkevm_params.field_extension_degree == 4: - field_name = "BabyBear⁴" - elif zkevm_params.field_extension_degree == 5: - field_name = "BabyBear⁵" - lines.append(f"- Field: {field_name}") - lines.append(f"- Rate (ρ): {zkevm_params.rho}") - lines.append(f"- Trace length (H): $2^{{{zkevm_params.h}}}$") - if zkevm_params.power_batching: - lines.append(f"- Batching: Powers") + if parameter_lines: + lines.extend(parameter_lines) else: - lines.append(f"- Batching: Affine") + lines.append("- No parameter summary available.") lines.append("") # Proof size - proof_size_kib = zkevm_params.proof_size_bits // KIB + proof_size_kib = zkvm_obj.get_proof_size_bits() // KIB lines.append(f"**Proof Size Estimate:** {proof_size_kib} KiB, where 1 KiB = 1024 bytes") lines.append("") diff --git a/soundcalc/zkevms/zkevm.py b/soundcalc/zkevms/zkevm.py deleted file mode 100644 index e777c46..0000000 --- a/soundcalc/zkevms/zkevm.py +++ /dev/null @@ -1,120 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass -from typing import Protocol, Mapping, Any - -from math import log2 -from ..common.fields import FieldParams, field_element_size_bits -from ..common.fri import get_FRI_proof_size_bits, get_num_FRI_folding_rounds - - - -@dataclass(frozen=True) -class zkEVMConfig: - """ - A zkEVM configuration that should be provided by the user. - """ - - # Name of the proof system - name: str - - # The output length of the hash function that is used in bits - # Note: this concerns the hash function used for Merkle trees - hash_size_bits: int - - # The code rate ρ - rho: float - # Domain size before low-degree extension (i.e. trace length) - trace_length: int - # Preset field parameters (contains p, ext_size, F) - field: FieldParams - # Total columns of AIR table - num_columns: int - # Number of polynomials appearing in the batched-FRI - # This can be greater than `num_columns`: some zkEVMs have to use "segment polynomials" (aka "composition polynomials") - num_polys: int - # Boolean flag to indicate if batched-FRI is implemented using coefficients - # r^0, r^1, ... r^{num_polys-1} (power_batching = True) or - # 1, r_1, r_2, ... r_{num_polys - 1} (power_batching = False) - power_batching: bool - # Number of FRI queries - num_queries: int - # Maximum constraint degree - AIR_max_degree: int - - # FRI folding factor (arity of folding per round) - FRI_folding_factor: int - # Many zkEVMs don't FRI fold until the final poly is of degree 1. They instead stop earlier. - # This is the degree they stop at (and it influences the number of FRI folding rounds). - FRI_early_stop_degree: int - - # Maximum number of entries from a single column referenced in a single constraint - max_combo: int - - # Proof of Work grinding compute during FRI query phase (expressed in bits of security) - grinding_query_phase: int - - -class zkEVMParams: - """ - zkEVM parameters used by the soundness calculator. - """ - def __init__(self, zkevm_cfg: zkEVMConfig): - """ - Given a zkEVMConfig, compute all the parameters relevant for the zkEVM. - """ - # Copy the parameters over (also see docs just above) - self.name = zkevm_cfg.name - self.hash_size_bits = zkevm_cfg.hash_size_bits - self.rho = zkevm_cfg.rho - self.trace_length = zkevm_cfg.trace_length - self.num_columns = zkevm_cfg.num_columns - self.num_polys = zkevm_cfg.num_polys - self.power_batching = zkevm_cfg.power_batching - self.num_queries = zkevm_cfg.num_queries - self.max_combo = zkevm_cfg.max_combo - self.FRI_folding_factor = zkevm_cfg.FRI_folding_factor - self.FRI_early_stop_degree = zkevm_cfg.FRI_early_stop_degree - self.grinding_query_phase = zkevm_cfg.grinding_query_phase - self.AIR_max_degree = zkevm_cfg.AIR_max_degree - - # Number of columns should be less or equal to the final number of polynomials in batched-FRI - assert self.num_columns <= self.num_polys - - # Now, also compute some auxiliary parameters - - # Negative log of rate - self.k = int(round(-log2(self.rho))) - # Log of trace length - self.h = int(round(log2(self.trace_length))) - # Domain size, after low-degree extension - self.D = self.trace_length / self.rho - - # Extract field parameters from the preset field - # Extension field degree (e.g., ext_size = 2 for Fp²) - self.field_extension_degree = zkevm_cfg.field.field_extension_degree - # Extension field size |F| = p^{ext_size} - self.F = zkevm_cfg.field.F - - # Compute number of FRI folding rounds - self.FRI_rounds_n = get_num_FRI_folding_rounds( - witness_size=int(self.D), - field_extension_degree=int(self.field_extension_degree), - folding_factor=int(self.FRI_folding_factor), - fri_early_stop_degree=int(self.FRI_early_stop_degree), - ) - - # Compute the proof size - # XXX (BW): note that it is not clear that this is the - # proof size for every zkEVM we can think of - # XXX (BW): we should probably also add something for the OOD samples and plookup, lookup etc. - self.proof_size_bits = get_FRI_proof_size_bits( - hash_size_bits=self.hash_size_bits, - field_size_bits=field_element_size_bits(zkevm_cfg.field), - num_functions=self.num_polys, - num_queries=self.num_queries, - witness_size=int(self.D), - field_extension_degree=int(self.field_extension_degree), - early_stop_degree=int(self.FRI_early_stop_degree), - folding_factor=int(self.FRI_folding_factor), - ) diff --git a/soundcalc/zkevms/__init__.py b/soundcalc/zkvms/__init__.py similarity index 100% rename from soundcalc/zkevms/__init__.py rename to soundcalc/zkvms/__init__.py diff --git a/soundcalc/zkvms/dummy_whir.py b/soundcalc/zkvms/dummy_whir.py new file mode 100644 index 0000000..e6e56a3 --- /dev/null +++ b/soundcalc/zkvms/dummy_whir.py @@ -0,0 +1,54 @@ +from __future__ import annotations + +from soundcalc.zkvms.whir_based_vm import WHIRBasedVM, WHIRBasedVMConfig + + +from ..common.fields import * + + +class DummyWHIRPreset: + @staticmethod + def default() -> "DummyWHIR": + """ + A dummy zkVM using WHIR for testing WHIR + """ + + name = "DummyWHIR" + hash_size_bits = 256 + log_inv_rate = 1 # rate 1/2 + num_iterations = 5 + folding_factor = 4 + field = GOLDILOCKS_2 + log_degree = 23 + batch_size = 100 + power_batching = True + constraint_degree = 1 + num_queries = [80,35,22,12,9] + num_ood_samples = [2,2,2,2] + + # grinding + grinding_bits_batching = 10 + grinding_bits_folding = [[10,10,10,10], [10,10,10,10], [10,10,10,10], [10,10,10,10], [10,10,10,10]] + grinding_bits_queries = [0,0,0,12,20] + grinding_bits_ood = [0,0,0,0] + + + cfg = WHIRBasedVMConfig( + name=name, + hash_size_bits=hash_size_bits, + log_inv_rate=log_inv_rate, + num_iterations=num_iterations, + folding_factor=folding_factor, + field=field, + log_degree=log_degree, + batch_size=batch_size, + power_batching=power_batching, + grinding_bits_batching=grinding_bits_batching, + grinding_bits_folding=grinding_bits_folding, + constraint_degree=constraint_degree, + num_queries=num_queries, + grinding_bits_queries=grinding_bits_queries, + num_ood_samples=num_ood_samples, + grinding_bits_ood=grinding_bits_ood + ) + return WHIRBasedVM(cfg) diff --git a/soundcalc/zkvms/fri_based_vm.py b/soundcalc/zkvms/fri_based_vm.py new file mode 100644 index 0000000..5f07085 --- /dev/null +++ b/soundcalc/zkvms/fri_based_vm.py @@ -0,0 +1,267 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Protocol, Mapping, Any + +from math import log2 + +from soundcalc.common.utils import get_bits_of_security_from_error +from soundcalc.regimes.best_attack import best_attack_security +from soundcalc.regimes.fri_regime import FRIParameters +from soundcalc.regimes.johnson_bound import JohnsonBoundRegime +from soundcalc.regimes.unique_decoding import UniqueDecodingRegime +from soundcalc.zkvms.zkvm import zkVM +from ..common.fields import FieldParams, field_element_size_bits +from ..common.fri import get_FRI_proof_size_bits, get_num_FRI_folding_rounds + + + +def get_DEEP_ALI_errors(L_plus: float, params: FRIBasedVM): + """ + Compute common proof system error components that are shared across regimes. + Some of them depend on the list size L_plus + + Returns a dictionary containing levels for ALI and DEEP + """ + + # TODO Check that it holds for all regimes + + # XXX These proof system errors are actually quite RISC0 specific. + # See Section 3.4 from the RISC0 technical report. + # We might want to generalize this further for other zkEVMs. + # For example, Miden also computes similar values for DEEP-ALI in: + # https://github.com/facebook/winterfell/blob/2f78ee9bf667a561bdfcdfa68668d0f9b18b8315/air/src/proof/security.rs#L188-L210 + e_ALI = L_plus * params.num_columns / params.F + e_DEEP = ( + L_plus + * (params.AIR_max_degree * (params.trace_length + params.max_combo - 1) + (params.trace_length - 1)) + / (params.F - params.trace_length - params.D) + ) + + levels = {} + levels["ALI"] = get_bits_of_security_from_error(e_ALI) + levels["DEEP"] = get_bits_of_security_from_error(e_DEEP) + + return levels + +@dataclass(frozen=True) +class FRIBasedVMConfig: + """ + A configuration of a FRI-based zkVM + """ + + # Name of the proof system + name: str + + # The output length of the hash function that is used in bits + # Note: this concerns the hash function used for Merkle trees + hash_size_bits: int + + # The code rate ρ + rho: float + # Domain size before low-degree extension (i.e. trace length) + trace_length: int + # Preset field parameters (contains p, ext_size, F) + field: FieldParams + # Total columns of AIR table + num_columns: int + # Number of functions appearing in the batched-FRI + # This can be greater than `num_columns`: some zkEVMs have to use "segment polynomials" (aka "composition polynomials") + batch_size: int + # Boolean flag to indicate if batched-FRI is implemented using coefficients + # r^0, r^1, ... r^{num_polys-1} (power_batching = True) or + # 1, r_1, r_2, ... r_{num_polys - 1} (power_batching = False) + power_batching: bool + # Number of FRI queries + num_queries: int + # Maximum constraint degree + AIR_max_degree: int + + # FRI folding factor (arity of folding per round) + FRI_folding_factor: int + # Many zkEVMs don't FRI fold until the final poly is of degree 1. They instead stop earlier. + # This is the degree they stop at (and it influences the number of FRI folding rounds). + FRI_early_stop_degree: int + + # Maximum number of entries from a single column referenced in a single constraint + max_combo: int + + # Proof of Work grinding compute during FRI query phase (expressed in bits of security) + grinding_query_phase: int + + +class FRIBasedVM(zkVM): + """ + Models a zkVM that is based on FRI. + """ + def __init__(self, config: FRIBasedVMConfig): + """ + Given a config, compute all the parameters relevant for the zkVM. + """ + # Copy the parameters over (also see docs just above) + self.name = config.name + self.hash_size_bits = config.hash_size_bits + self.rho = config.rho + self.trace_length = config.trace_length + self.num_columns = config.num_columns + self.num_polys = config.batch_size + self.power_batching = config.power_batching + self.num_queries = config.num_queries + self.max_combo = config.max_combo + self.FRI_folding_factor = config.FRI_folding_factor + self.FRI_early_stop_degree = config.FRI_early_stop_degree + self.grinding_query_phase = config.grinding_query_phase + self.AIR_max_degree = config.AIR_max_degree + + # Number of columns should be less or equal to the final number of polynomials in batched-FRI + assert self.num_columns <= self.num_polys + + # Now, also compute some auxiliary parameters + + # Negative log of rate + self.k = int(round(-log2(self.rho))) + # Log of trace length + self.h = int(round(log2(self.trace_length))) + # Domain size, after low-degree extension + self.D = self.trace_length / self.rho + + # Extract field parameters from the preset field + # Extension field degree (e.g., ext_size = 2 for Fp²) + self.field_extension_degree = config.field.field_extension_degree + # Extension field size |F| = p^{ext_size} + self.field = config.field + self.F = config.field.F + + # Compute number of FRI folding rounds + self.FRI_rounds_n = get_num_FRI_folding_rounds( + witness_size=int(self.D), + field_extension_degree=int(self.field_extension_degree), + folding_factor=int(self.FRI_folding_factor), + fri_early_stop_degree=int(self.FRI_early_stop_degree), + ) + + + + def get_name(self) -> str: + return self.name + + def get_parameter_summary(self) -> str: + """ + Returns a description of the parameters of the zkVM. + The description is given as a string; formatted so that it looks good + in both console output and in markdown reports. + """ + + # We put everything inside a markdown code block so it looks + # identical in plain terminal output. + lines = [] + lines.append("") + lines.append("```") + + # Key–value table + params = { + "name": self.name, + "hash_size_bits": self.hash_size_bits, + "rho": self.rho, + "k = -log2(rho)": self.k, + "trace_length": self.trace_length, + "h = log2(trace_length)": self.h, + "domain_size D = trace_length / rho": self.D, + "num_columns": self.num_columns, + "num_polys": self.num_polys, + "power_batching": self.power_batching, + "num_queries": self.num_queries, + "max_combo": self.max_combo, + "FRI_folding_factor": self.FRI_folding_factor, + "FRI_early_stop_degree": self.FRI_early_stop_degree, + "FRI_rounds_n": self.FRI_rounds_n, + "grinding_query_phase": self.grinding_query_phase, + "AIR_max_degree": self.AIR_max_degree, + "field": self.field.to_string(), + "field_extension_degree": self.field_extension_degree, + } + + # Determine alignment width + key_width = max(len(k) for k in params.keys()) + + # Format lines with aligned columns + for k, v in params.items(): + lines.append(f" {k:<{key_width}} : {v}") + + lines.append("```") + return "\n".join(lines) + + def get_proof_size_bits(self) -> int: + """ + Returns an estimate for the proof size, given in bits. + """ + + # Compute the proof size + # XXX (BW): note that it is not clear that this is the + # proof size for every zkEVM we can think of + # XXX (BW): we should probably also add something for the OOD samples and plookup, lookup etc. + + return get_FRI_proof_size_bits( + hash_size_bits=self.hash_size_bits, + field_size_bits=field_element_size_bits(self.field), + num_functions=self.num_polys, + num_queries=self.num_queries, + witness_size=int(self.D), + field_extension_degree=int(self.field_extension_degree), + early_stop_degree=int(self.FRI_early_stop_degree), + folding_factor=int(self.FRI_folding_factor), + ) + + def get_security_levels(self) -> dict[str, dict[str, int]]: + """ + Returns a dictionary that maps each regime (i.e., a way of doing security analysis) + to a dictionary that contains the round-by-round soundness levels. + + It maps from a label that describes the regime (e.g., UDR, JBR in case of FRI) to a + regime-specific dictionary. Any such regime-specific dictionary is as follows: + + It maps from a label that explains which round it is for to an integer. + If this integer is, say, k, then it means the error for this round is at + most 2^{-k}. + """ + + # we consider the following regimes, and for each regime do the analysis + regimes = [ + UniqueDecodingRegime(), + JohnsonBoundRegime(), + ] + + fri_parameters = FRIParameters( + hash_size_bits=self.hash_size_bits, + field_size_bits=field_element_size_bits(self.field), + rho=self.rho, + num_functions=self.num_polys, + num_queries=self.num_queries, + witness_size=int(self.D), + D=self.D, + F=self.F, + FRI_rounds_n=self.FRI_rounds_n, + power_batching=self.power_batching, + field_extension_degree=int(self.field_extension_degree), + early_stop_degree=int(self.FRI_early_stop_degree), + folding_factor=int(self.FRI_folding_factor), + grinding_query_phase=self.grinding_query_phase, + trace_length=self.trace_length, + max_combo=self.max_combo + ) + + result = {} + for regime in regimes: + id = regime.identifier() + + # errors consist of FRI errors and proof system errors + fri_levels = regime.get_rbr_levels(fri_parameters) + list_size = regime.get_bound_on_list_size(fri_parameters) + proof_system_levels = get_DEEP_ALI_errors(list_size, self) + total = min(list(fri_levels.values()) + list(proof_system_levels.values())) + + result[id] = fri_levels | proof_system_levels | {"total": total} + + result["best attack"] = best_attack_security(fri_parameters) + + return result diff --git a/soundcalc/zkevms/miden.py b/soundcalc/zkvms/miden.py similarity index 92% rename from soundcalc/zkevms/miden.py rename to soundcalc/zkvms/miden.py index 4405661..630b49c 100644 --- a/soundcalc/zkevms/miden.py +++ b/soundcalc/zkvms/miden.py @@ -1,6 +1,7 @@ from __future__ import annotations -from .zkevm import zkEVMConfig, zkEVMParams +from soundcalc.zkvms.fri_based_vm import FRIBasedVM, FRIBasedVMConfig + from ..common.fields import * @@ -41,7 +42,7 @@ def default() -> "MidenPreset": trace_length = 1 << 18 #note that this is smaller than for other VMs, thus the security is higher for the same settings # XXX need to check the numbers below by running the prover num_columns = 100 - num_polys = 100 + batch_size = 100 # XXX ??? TODO: ask the main Miden channel max_combo = 2 @@ -51,14 +52,14 @@ def default() -> "MidenPreset": hash_size_bits = 256 # TODO: check if that is actually true - cfg = zkEVMConfig( + cfg = FRIBasedVMConfig( name="Miden", hash_size_bits=hash_size_bits, rho=rho, trace_length=trace_length, field=field, num_columns=num_columns, - num_polys=num_polys, + batch_size=batch_size, power_batching=power_batching, num_queries=num_queries, max_combo=max_combo, @@ -67,4 +68,4 @@ def default() -> "MidenPreset": grinding_query_phase=grinding_query_phase, AIR_max_degree=AIR_max_degree, ) - return zkEVMParams(cfg) + return FRIBasedVM(cfg) diff --git a/soundcalc/zkevms/risc0.py b/soundcalc/zkvms/risc0.py similarity index 92% rename from soundcalc/zkevms/risc0.py rename to soundcalc/zkvms/risc0.py index 3e500e5..8246262 100644 --- a/soundcalc/zkevms/risc0.py +++ b/soundcalc/zkvms/risc0.py @@ -1,6 +1,7 @@ from __future__ import annotations -from .zkevm import zkEVMConfig, zkEVMParams +from soundcalc.zkvms.fri_based_vm import FRIBasedVM, FRIBasedVMConfig + from ..common.fields import * @@ -44,14 +45,14 @@ def default() -> "Risc0Preset": hash_size_bits = 256 # TODO: check if that is actually true - cfg = zkEVMConfig( + cfg = FRIBasedVMConfig( name="RISC0", hash_size_bits=hash_size_bits, rho=rho, trace_length=trace_length, field=field, num_columns=C, - num_polys=L, + batch_size=L, power_batching=power_batching, num_queries=s, max_combo=max_combo, @@ -60,4 +61,4 @@ def default() -> "Risc0Preset": grinding_query_phase=0, AIR_max_degree=AIR_max_degree, ) - return zkEVMParams(cfg) + return FRIBasedVM(cfg) diff --git a/soundcalc/zkvms/whir_based_vm.py b/soundcalc/zkvms/whir_based_vm.py new file mode 100644 index 0000000..0d59828 --- /dev/null +++ b/soundcalc/zkvms/whir_based_vm.py @@ -0,0 +1,470 @@ + +from dataclasses import dataclass + +from soundcalc.common.fields import FieldParams, field_element_size_bits +from soundcalc.common.utils import get_bits_of_security_from_error, get_size_of_merkle_path_bits +from soundcalc.proxgaps.johnson_bound import JohnsonBoundRegime +from soundcalc.proxgaps.proxgaps_regime import ProximityGapsRegime +from soundcalc.proxgaps.unique_decoding import UniqueDecodingRegime +from soundcalc.zkvms.zkvm import zkVM +from typing import Tuple +import math + +@dataclass(frozen=True) +class WHIRBasedVMConfig: + """ + A configuration of a WHIR-based zkVM + """ + + # Name of the proof system + name: str + + # The output length of the hash function that is used in bits + # Note: this concerns the hash function used for Merkle trees + hash_size_bits: int + + # Parameters are inspired by Giacomo's script here for inspiration + # https://github.com/WizardOfMenlo/stir-whir-scripts/blob/main/src/whir.rs + + # log2(1/rate), e.g., 2 if rate is 1/4 + # note that this is the rate of the initial code + log_inv_rate: int + + # this is the number of WHIR iterations + # note that a WHIR iteration consists of multiple rounds + # this is denoted by M in the paper + num_iterations: int + + # this is what is called k_0,...,k_{M-1} in the paper + # as in Giacomo's script, we assume that this is the same for all + # see https://github.com/WizardOfMenlo/stir-whir-scripts/blob/main/src/whir.rs#L72 + # in each iteration, we go from dimension 2^m_i to dimension 2^(m_i - k) + folding_factor: int + + # the field that is used + field: FieldParams + + # the log2 of the degree that we test + # in the WHIR paper, this is denoted by m + log_degree: int + + # how many functions do we check in one go, i.e., this is the batch size + # for reference: https://github.com/WizardOfMenlo/stir-whir-scripts/blob/main/src/whir.rs#L144 + batch_size: int + + # Boolean flag to indicate if batching is implemented using coefficients + # r^0, r^1, ... r^{num_polys-1} (power_batching = True) or + # 1, r_1, r_2, ... r_{num_polys - 1} (power_batching = False) + power_batching: bool + + # Number of bits of grinding for the batching round + grinding_bits_batching: int + + # degree of constraints being proven on the committed words + # This is d in Construction 5.1 in WHIR. Note that d = max{d*,3}, + # and d* = 1 + deg_Z(hat{w}0) + max_i deg_Xi(hat{w}0) + constraint_degree: int + + # number of bits of grinding for reducing the folding errors (length M x k) + # this impacts epsilon^fold_{i,s}, 0 <= i <= M-1, 0 <= s <= k + grinding_bits_folding: list[list[int]] + + # the number of queries for each round (length M) + # this is t_0, ... , t_{M-1} from the WHIR paper + num_queries: list[int] + + # number of bits of grinding for the query rounds (length M) + # this changes the errors eps_shift and eps_fin + grinding_bits_queries: list[int] + + # the number of OOD samples for each round (length M-1) + num_ood_samples: list[int] + + # number of bits of grinding for each OOD round (length M-1) + grinding_bits_ood: list[int] + + + + +class WHIRBasedVM(zkVM): + """ + Models a zkVM that is based on WHIR. + """ + def __init__(self, config: WHIRBasedVMConfig): + """ + Given a config, compute all the parameters relevant for the zkVM. + """ + self.name = config.name + + # inherit parameters from the given config + self.hash_size_bits = config.hash_size_bits + self.folding_factor = config.folding_factor + self.num_iterations = config.num_iterations + self.field = config.field + self.batch_size = config.batch_size + self.power_batching = config.power_batching + self.grinding_bits_batching = config.grinding_bits_batching + self.constraint_degree = config.constraint_degree + self.grinding_bits_folding = config.grinding_bits_folding + self.num_queries = config.num_queries + self.grinding_bits_queries = config.grinding_bits_queries + self.num_ood_samples = config.num_ood_samples + self.grinding_bits_ood = config.grinding_bits_ood + + # determine all rates (in contrast to FRI, these change over the iterations) + # this also involves determining all log degrees + assert(config.log_inv_rate > 0 and config.folding_factor >= 1) + + # the log degrees that we have are (m0, m1 = m0 - k, m2 = m0 - 2k, ..., m(M) = m0 - (M)k) + assert (self.num_iterations * self.folding_factor <= config.log_degree) + self.log_degrees = [config.log_degree - i * self.folding_factor for i in range(self.num_iterations + 1)] + + # the eval domain sizes shrink by a factor of two, so the rates decrease by a factor of + self.log_inv_rates = [config.log_inv_rate + i * (self.folding_factor - 1) for i in range(self.num_iterations + 1)] + + # ensure that we did not mess up with the number of rounds + assert(len(self.num_ood_samples) == self.num_iterations - 1) + assert(len(self.log_degrees) == self.num_iterations + 1) + assert(len(self.log_inv_rates) == self.num_iterations + 1) + assert(len(self.num_queries) == self.num_iterations) + + # ensure that grinding parameters have correct length + assert(len(self.grinding_bits_ood) == self.num_iterations - 1) + assert(len(self.grinding_bits_queries) == self.num_iterations) + assert(len(self.grinding_bits_folding) == self.num_iterations) + for grinding_bits_folding_for_iteration in self.grinding_bits_folding: + assert(len(grinding_bits_folding_for_iteration) == self.folding_factor) + + # determine the total grinding overhead (sum of 2^grinding_bits) + self.log_grinding_overhead = self.get_log_grinding_overhead() + + def get_name(self) -> str: + return self.name + + def get_parameter_summary(self) -> str: + lines = [] + lines.append("") + lines.append("```") + + # Collect scalar parameters + params = { + "name": self.name, + "hash_size_bits": self.hash_size_bits, + "folding_factor": self.folding_factor, + "batch_size": self.batch_size, + "power_batching": self.power_batching, + "grinding_bits_batching": self.grinding_bits_batching, + "num_iterations": self.num_iterations, + "constraint_degree": self.constraint_degree, + "field": self.field.to_string(), + } + + key_width = max(len(k) for k in params) + + for k, v in params.items(): + lines.append(f" {k:<{key_width}} : {v}") + + lines.append("") + lines.append(" Per-round parameters:") + lines.append(f" log_degrees : {self.log_degrees}") + lines.append(f" log_inv_rates : {self.log_inv_rates}") + lines.append(f" num_queries : {self.num_queries}") + lines.append(f" grinding_bits_queries : {self.grinding_bits_queries}") + lines.append(f" num_ood_samples : {self.num_ood_samples}") + lines.append(f" grinding_bits_ood : {self.grinding_bits_ood}") + lines.append(f" grinding_bits_folding : {self.grinding_bits_folding}") + lines.append("") + lines.append(f" Total grinding overhead (sum of 2^grinding_bits) = 2^({self.log_grinding_overhead})") + + + lines.append("```") + return "\n".join(lines) + + def get_proof_size_bits(self) -> int: + + # We estimate the proof size by looking at the WHIR paper, counting sizes of prover messages. + # Note that verifier messages do not count into proof size, as they are obtained from Fiat-Shamir. + # Here, messages are either field elements, polynomials, functions, or function evaluations. + # + # Field elements are included directly in the proof; + # Polynomials are sent by the vector of their coefficients; + # Functions are sent in the form of a Merkle root; + # Function evaluations are sent in the form of a Merkle path; + + field_size_bits = field_element_size_bits(self.field) + + # Prover sends the initial function (Merkle root) + proof_size = self.hash_size_bits + + # Initial sum check: Prover sends k0 polynomials of degree d + proof_size += self.folding_factor * self.constraint_degree * field_size_bits + + # Main loop, runs for i = 1 to i = M - 1 + for i in range(1, self.num_iterations): + # In each iteration: send a function, then do OOD samples, then do sum check rounds + + # Send function + proof_size += self.hash_size_bits + + # Send evaluations for the OOD samples + proof_size += self.num_ood_samples[i-1] * field_size_bits + + # Sum check rounds + proof_size += self.folding_factor * self.constraint_degree * field_size_bits + + # Prover sends the final polynomial. This is a multi-linear polynomial in + # m_M variables, i.e., it has 2^{m_M} coefficients. + assert self.log_degrees + proof_size += (2 ** self.log_degrees[-1]) * field_size_bits + + # Decision phase: we query each function f_0,...,f_{M-1} that the prover sent + # at t_i groups of points. Each group is a set of "folding siblings", also + # called a "Block" in the literature. As in the WHIR paper, we assume that + # an entire block is stored in the Merkle leaf. That is, we simply count + # t_i Merkle leafs. + assert(len(self.num_queries) == self.num_iterations) + for i in range(self.num_iterations): + domain_size = 2 ** (self.log_degrees[i] + self.log_inv_rates[i]) + block_size = 2 ** self.folding_factor + num_leafs = domain_size / block_size + merkle_path_size = get_size_of_merkle_path_bits(num_leafs=num_leafs, tuple_size=block_size, element_size_bits=field_size_bits, hash_size_bits=self.hash_size_bits) + proof_size += self.num_queries[i] * merkle_path_size + + + return proof_size + + def get_security_levels(self) -> dict[str, dict[str, int]]: + + regimes = [UniqueDecodingRegime(), JohnsonBoundRegime()] + + result = {} + for regime in regimes: + id = regime.identifier() + result[id] = self.get_security_levels_for_regime(regime) + + return result + + def get_security_levels_for_regime(self, regime: ProximityGapsRegime) -> dict[str, int]: + """ + Same as get_security_levels, but for a specific regime. + """ + levels = {} + + # add an error from the batching step + if self.batch_size > 1: + epsilon_batch = self.epsilon_batch(regime) + levels[f"batching"] = get_bits_of_security_from_error(epsilon_batch) + + # initial iteration (just sum check / fold errors) + iteration = 0 + for round in range(1, self.folding_factor + 1): + epsilon = self.epsilon_fold(iteration, round, regime) + levels[f"fold(i={iteration},s={round})"] = get_bits_of_security_from_error(epsilon) + + # for each iteration i = 1, ... M - 1: OOD errors, shift errors, fold errors + for iteration in range(1, self.num_iterations): + # out of domain samples + epsilon_ood = self.epsilon_out(iteration, regime) + levels[f"OOD(i={iteration})"] = get_bits_of_security_from_error(epsilon_ood) + # shift queries + epsilon_shift = self.epsilon_shift(iteration, regime) + levels[f"Shift(i={iteration})"] = get_bits_of_security_from_error(epsilon_shift) + # sum check (one error for each round) + for round in range(1, self.folding_factor + 1): + epsilon = self.epsilon_fold(iteration, round, regime) + levels[f"fold(i={iteration},s={round})"] = get_bits_of_security_from_error(epsilon) + + # final error + epsilon_fin = self.epsilon_fin(regime) + levels["fin"] = get_bits_of_security_from_error(epsilon_fin) + + # add a "total" level + levels["total"] = min(list(levels.values())) + + return levels + + + def get_code_for_iteration_and_round(self, iteration: int, round: int) -> tuple[float, int]: + """ + Returns the code for the given iteration and round. That is, this returns a pair (rate, dimension) + of the code C_{RS}^{i,s} (in notation of Theorem 5.2 in WHIR paper). + Here, 0<= i <= M-1 is the iteration and 0 <= s <= k is the round. + """ + + + assert iteration <= self.num_iterations - 1 and iteration >= 0 + assert round <= self.folding_factor and round >= 0 + + # The code is C_{RS}^{i,s} = RS[F, L_i^{(2^s)}, m_i - s] + # So the dimension is 2^{m_i - s} + log_dimension = self.log_degrees[iteration] - round + dimension = 2 ** log_dimension + + # We know what the rate of C_{RS}^{i,0} = RS[F, L_i, m_i] is. + # Namely, it is 2**(-self.log_inv_rates[i]). + # The rate of C_{RS}^{i,s} is: + # 2^{m_i-s} / |L_i^{(2^s)}| =(2^{m_i} / |L_i|) * (2^{-s}/2^{-s}) = 2^{m_i} / |L_i|. + # So this code has the same rate. + rate = 2**(-self.log_inv_rates[iteration]) + return (rate, dimension) + + def get_delta_for_iteration(self, iteration: int, regime: ProximityGapsRegime) -> float: + """ + Returns delta_i, in the notation of the paper, where i = iteration. + + This is determined so that it is small enough for the proximity gaps regime on + all codes C_{RS}^{i,s}, s <= k. + """ + + # we iterate over all codes, i.e., over all rounds s + # and for each code, we determine the max delta that is supported, + # and then take the smallest of them, so that the condition is satisfied. + + delta = 1.0 + + for round in range(1, self.folding_factor + 1): + (rate, dimension) = self.get_code_for_iteration_and_round(iteration, round) + delta_round = regime.get_max_delta(rate, dimension, self.field) + delta = min(delta, delta_round) + + return delta + + + def get_list_size_for_iteration_and_round(self, iteration: int, round: int, regime: ProximityGapsRegime) -> float: + """ + Returns ell_{i,s}, so that code C_{RS}^{i,s} is (ell_{i,s},delta_i)-list decodable. + This uses the proximity gaps regime to determine the list size. + + Here, delta_i = get_delta_for_iteration(iteration,regime), i = iteration, s = round. + """ + + assert iteration <= self.num_iterations - 1 and iteration >= 0 + assert round <= self.folding_factor and round >= 0 + + # first figure out the delta for this iteration + delta = self.get_delta_for_iteration(iteration, regime) + + # now compute the list size from it. This requires the code parameters for C_{RS}^{i,s}. + (rate, dimension) = self.get_code_for_iteration_and_round(iteration, round) + list_size = regime.get_max_list_size(rate, dimension, self.field, delta) + + return list_size + + def epsilon_batch(self, regime: ProximityGapsRegime) -> float: + """ + Returns the error due to the batching step. This depends on whether batching is done + with powers or with random coefficients. + + This follows https://github.com/WizardOfMenlo/stir-whir-scripts/blob/main/src/whir.rs#L144 + """ + + rate = 2 ** (-self.log_inv_rates[0]) + dimension = 2 ** self.log_degrees[0] + + epsilon = regime.get_error_linear(rate, dimension, self.field, self.batch_size) + if self.power_batching: + epsilon = regime.get_error_powers(rate, dimension, self.field, self.batch_size) + + # grinding + epsilon *= 2 ** (-self.grinding_bits_batching) + return epsilon + + def epsilon_fold(self, iteration: int, round: int, regime: ProximityGapsRegime) -> float: + """ + Returns the error of a folding round. This is epsilon^fold_{i,s} in the notation + of the paper (Theorem 5.2 in WHIR paper), where i is the iteration and s <= k is the round. + """ + + # the error has two terms + epsilon = 0 + + # first term is d * ell_{i,s-1} / F + list_size = self.get_list_size_for_iteration_and_round(iteration, round - 1, regime) + epsilon += self.constraint_degree * list_size / self.field.F + + # second term is the proximity gaps error err(C_{RS}^{i,s}, 2, delta_i) + # the WHIR theorem assumes that powers is a prox generator, + # so we use the error for powers here. + num_functions = 2 + (rate, dimension) = self.get_code_for_iteration_and_round(iteration, round) + epsilon += regime.get_error_powers(rate, dimension, self.field, num_functions) + + # grinding + epsilon *= 2 ** (-self.grinding_bits_folding[iteration][round - 1]) + + return epsilon + + def epsilon_out(self, iteration: int, regime: ProximityGapsRegime) -> float: + """ + Returns the error epsilon^out_i from the paper (Theorem 5.2 in WHIR paper), where i is the iteration. + + Follows https://github.com/WizardOfMenlo/stir-whir-scripts/blob/main/src/errors.rs#L146, as WHIR paper + does not cover the case of having more than one OOD sample. + """ + + # term is ell_{i,0}^2 * 2^{m_i} / (2F) for one OOD sample. + # for w many OOD samples, the 2^{m_i} / (2F) part is raised to the power w + list_size = self.get_list_size_for_iteration_and_round(iteration, 0, regime) + mi = self.log_degrees[iteration] + w = self.num_ood_samples[iteration - 1] + epsilon = list_size * list_size * ((2**mi) / (2 * self.field.F)) ** w + + # grinding + epsilon *= 2 ** (-self.grinding_bits_ood[iteration - 1]) + + return epsilon + + def epsilon_shift(self, iteration: int, regime: ProximityGapsRegime) -> float: + """ + Returns the error epsilon^shift_i from the paper (Theorem 5.2 in WHIR paper), where i is the iteration. + """ + + assert iteration <= self.num_iterations - 1 and iteration >= 1 + + # the error has two terms, both depend on number of queries t_{M-1} + epsilon = 0 + t = self.num_queries[iteration - 1] + + # first term is (1-delta_{M-1})^{t_{M-1}} + delta = self.get_delta_for_iteration(iteration - 1, regime) + epsilon += (1.0 - delta) ** t + + # second term is ell_{i,0} * (t_{i-1}+1)/F + list_size = self.get_list_size_for_iteration_and_round(iteration, 0, regime) + epsilon += list_size * (t + 1) / self.field.F + + # grinding + epsilon *= 2 ** (-self.grinding_bits_queries[iteration - 1]) + + return epsilon + + def epsilon_fin(self, regime: ProximityGapsRegime) -> float: + """ + Returns the error epsilon^fin from the paper (Theorem 5.2 in WHIR paper). + """ + + # the error is (1-delta_{M-1})^{t_{M-1}} + delta = self.get_delta_for_iteration(self.num_iterations - 1, regime) + epsilon = (1.0 - delta) ** self.num_queries[self.num_iterations - 1] + + # grinding + epsilon *= 2 ** (-self.grinding_bits_queries[-1]) + return epsilon + + def get_log_grinding_overhead(self) -> float: + """ + Determine the total grinding overhead, which is the sum of all individual grinding overheads. + The grinding overhead for c bits of grinding is 2^c. + """ + grinding_sum = 0 + + # grinding for batching, queries, OOD + grinding_sum += 2 ** self.grinding_bits_batching + grinding_sum += sum([2 ** g for g in self.grinding_bits_queries]) + grinding_sum += sum([2 ** g for g in self.grinding_bits_ood]) + + # grinding for folding + for iteration_g in self.grinding_bits_folding: + grinding_sum += sum([2 ** g for g in iteration_g]) + + return round(math.log2(grinding_sum), 2) \ No newline at end of file diff --git a/soundcalc/zkevms/zisk.py b/soundcalc/zkvms/zisk.py similarity index 89% rename from soundcalc/zkevms/zisk.py rename to soundcalc/zkvms/zisk.py index b4ed034..a073ff8 100644 --- a/soundcalc/zkevms/zisk.py +++ b/soundcalc/zkvms/zisk.py @@ -1,6 +1,7 @@ from __future__ import annotations -from .zkevm import zkEVMConfig, zkEVMParams +from soundcalc.zkvms.fri_based_vm import FRIBasedVM, FRIBasedVMConfig + from ..common.fields import * @@ -31,7 +32,7 @@ def default() -> "ZiskPreset": trace_length = 1 << 22 num_columns = 66 - num_polys = num_columns + 2 # +2 for the composition polynomials + batch_size = num_columns + 2 # +2 for the composition polynomials num_queries = 128 // int(math.log2(blowup_factor)) @@ -44,14 +45,14 @@ def default() -> "ZiskPreset": hash_size_bits = 256 # TODO: check if that is actually true - cfg = zkEVMConfig( + cfg = FRIBasedVMConfig( name="ZisK", hash_size_bits=hash_size_bits, rho=rho, trace_length=trace_length, field=field, num_columns=num_columns, - num_polys=num_polys, + batch_size=batch_size, power_batching=True, num_queries=num_queries, AIR_max_degree=AIR_max_degree, @@ -60,4 +61,4 @@ def default() -> "ZiskPreset": max_combo=max_combo, grinding_query_phase=0, ) - return zkEVMParams(cfg) + return FRIBasedVM(cfg) diff --git a/soundcalc/zkvms/zkvm.py b/soundcalc/zkvms/zkvm.py new file mode 100644 index 0000000..6031bf8 --- /dev/null +++ b/soundcalc/zkvms/zkvm.py @@ -0,0 +1,39 @@ + + +class zkVM: + """ + A class modeling a zkVM. + """ + + def get_name(self) -> str: + """ + Returns the name of the zkVM. + """ + raise NotImplementedError + + def get_parameter_summary(self) -> str: + """ + Returns a description of the parameters of the zkVM. + The description is given as a string. + """ + raise NotImplementedError + + def get_proof_size_bits(self) -> int: + """ + Returns an estimate for the proof size, given in bits. + """ + raise NotImplementedError + + def get_security_levels(self) -> dict[str, dict[str, int]]: + """ + Returns a dictionary that maps each regime (i.e., a way of doing security analysis) + to a dictionary that contains the round-by-round soundness levels. + + It maps from a label that describes the regime (e.g., UDR, JBR in case of FRI) to a + regime-specific dictionary. Any such regime-specific dictionary is as follows: + + It maps from a label that explains which round it is for to an integer. + If this integer is, say, k, then it means the error for this round is at + most 2^{-k}. + """ + raise NotImplementedError