Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"cSpell.words": [
"soundcalc",
"zkvm",
"zkvms"
]
}
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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³?"
Expand Down
35 changes: 35 additions & 0 deletions results.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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 |
14 changes: 10 additions & 4 deletions soundcalc/common/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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),
Expand Down
21 changes: 3 additions & 18 deletions soundcalc/common/fri.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
"""
Expand Down Expand Up @@ -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,
Expand Down
45 changes: 17 additions & 28 deletions soundcalc/common/utils.py
Original file line number Diff line number Diff line change
@@ -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)))
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
94 changes: 34 additions & 60 deletions soundcalc/main.py
Original file line number Diff line number Diff line change
@@ -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:
"""
Expand All @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions soundcalc/proxgaps/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@

Copy link
Collaborator

@asn-d6 asn-d6 Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed, it would be good if we could unify soundcalc/proxgaps/ and soundcalc/regimes/, as they essentially do things related to proximity gaps.



Loading