Merge pull request #481 from mir-protocol/fix_hash_or_noop_merkle_proof

Use `hash_or_noop` for Merkle tree leaves
This commit is contained in:
wborgeaud 2022-02-15 08:12:36 +01:00 committed by GitHub
commit f4640bb5a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 172 additions and 63 deletions

View File

@ -1,3 +1,5 @@
#![feature(generic_const_exprs)]
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
use plonky2::field::goldilocks_field::GoldilocksField;
use plonky2::hash::hash_types::RichField;
@ -9,7 +11,10 @@ use tynm::type_name;
const ELEMS_PER_LEAF: usize = 135;
pub(crate) fn bench_merkle_tree<F: RichField, H: Hasher<F>>(c: &mut Criterion) {
pub(crate) fn bench_merkle_tree<F: RichField, H: Hasher<F>>(c: &mut Criterion)
where
[(); H::HASH_SIZE]:,
{
let mut group = c.benchmark_group(&format!(
"merkle-tree<{}, {}>",
type_name::<F>(),

View File

@ -12,7 +12,7 @@ use crate::fri::FriParams;
use crate::hash::hash_types::RichField;
use crate::hash::merkle_tree::MerkleTree;
use crate::iop::challenger::Challenger;
use crate::plonk::config::GenericConfig;
use crate::plonk::config::{GenericConfig, Hasher};
use crate::timed;
use crate::util::reducing::ReducingFactor;
use crate::util::reverse_bits;
@ -43,7 +43,10 @@ impl<F: RichField + Extendable<D>, C: GenericConfig<D, F = F>, const D: usize>
cap_height: usize,
timing: &mut TimingTree,
fft_root_table: Option<&FftRootTable<F>>,
) -> Self {
) -> Self
where
[(); C::Hasher::HASH_SIZE]:,
{
let coeffs = timed!(
timing,
"IFFT",
@ -68,7 +71,10 @@ impl<F: RichField + Extendable<D>, C: GenericConfig<D, F = F>, const D: usize>
cap_height: usize,
timing: &mut TimingTree,
fft_root_table: Option<&FftRootTable<F>>,
) -> Self {
) -> Self
where
[(); C::Hasher::HASH_SIZE]:,
{
let degree = polynomials[0].len();
let lde_values = timed!(
timing,
@ -133,7 +139,10 @@ impl<F: RichField + Extendable<D>, C: GenericConfig<D, F = F>, const D: usize>
challenger: &mut Challenger<F, C::Hasher>,
fri_params: &FriParams,
timing: &mut TimingTree,
) -> FriProof<F, C::Hasher, D> {
) -> FriProof<F, C::Hasher, D>
where
[(); C::Hasher::HASH_SIZE]:,
{
assert!(D > 1, "Not implemented for D=1.");
let alpha = challenger.get_extension_challenge::<D>();
let mut alpha = ReducingFactor::new(alpha);

View File

@ -245,7 +245,10 @@ impl<F: RichField + Extendable<D>, H: Hasher<F>, const D: usize> CompressedFriPr
challenges: &ProofChallenges<F, D>,
fri_inferred_elements: FriInferredElements<F, D>,
params: &FriParams,
) -> FriProof<F, H, D> {
) -> FriProof<F, H, D>
where
[(); H::HASH_SIZE]:,
{
let CompressedFriProof {
commit_phase_merkle_caps,
query_round_proofs,

View File

@ -24,7 +24,10 @@ pub fn fri_proof<F: RichField + Extendable<D>, C: GenericConfig<D, F = F>, const
challenger: &mut Challenger<F, C::Hasher>,
fri_params: &FriParams,
timing: &mut TimingTree,
) -> FriProof<F, C::Hasher, D> {
) -> FriProof<F, C::Hasher, D>
where
[(); C::Hasher::HASH_SIZE]:,
{
let n = lde_polynomial_values.len();
assert_eq!(lde_polynomial_coeffs.len(), n);
@ -68,7 +71,10 @@ fn fri_committed_trees<F: RichField + Extendable<D>, C: GenericConfig<D, F = F>,
) -> (
Vec<MerkleTree<F, C::Hasher>>,
PolynomialCoeffs<F::Extension>,
) {
)
where
[(); C::Hasher::HASH_SIZE]:,
{
let mut trees = Vec::new();
let mut shift = F::MULTIPLICATIVE_GROUP_GENERATOR;

View File

@ -56,18 +56,17 @@ pub(crate) fn fri_verify_proof_of_work<F: RichField + Extendable<D>, const D: us
Ok(())
}
pub fn verify_fri_proof<
F: RichField + Extendable<D>,
C: GenericConfig<D, F = F>,
const D: usize,
>(
pub fn verify_fri_proof<F: RichField + Extendable<D>, C: GenericConfig<D, F = F>, const D: usize>(
instance: &FriInstanceInfo<F, D>,
openings: &FriOpenings<F, D>,
challenges: &FriChallenges<F, D>,
initial_merkle_caps: &[MerkleCap<F, C::Hasher>],
proof: &FriProof<F, C::Hasher, D>,
params: &FriParams,
) -> Result<()> {
) -> Result<()>
where
[(); C::Hasher::HASH_SIZE]:,
{
ensure!(
params.final_poly_len() == proof.final_poly.len(),
"Final polynomial has wrong degree."
@ -112,7 +111,10 @@ fn fri_verify_initial_proof<F: RichField, H: Hasher<F>>(
x_index: usize,
proof: &FriInitialTreeProof<F, H>,
initial_merkle_caps: &[MerkleCap<F, H>],
) -> Result<()> {
) -> Result<()>
where
[(); H::HASH_SIZE]:,
{
for ((evals, merkle_proof), cap) in proof.evals_proofs.iter().zip(initial_merkle_caps) {
verify_merkle_proof::<F, H>(evals.clone(), x_index, cap, merkle_proof)?;
}
@ -177,7 +179,10 @@ fn fri_verifier_query_round<
n: usize,
round_proof: &FriQueryRound<F, C::Hasher, D>,
params: &FriParams,
) -> Result<()> {
) -> Result<()>
where
[(); C::Hasher::HASH_SIZE]:,
{
fri_verify_initial_proof::<F, C::Hasher>(
x_index,
&round_proof.initial_trees_proof,

View File

@ -10,7 +10,7 @@ use crate::hash::hash_types::RichField;
use crate::iop::witness::{PartialWitness, Witness};
use crate::plonk::circuit_builder::CircuitBuilder;
use crate::plonk::circuit_data::CircuitConfig;
use crate::plonk::config::GenericConfig;
use crate::plonk::config::{GenericConfig, Hasher};
use crate::plonk::vars::{EvaluationTargets, EvaluationVars, EvaluationVarsBaseBatch};
use crate::plonk::verifier::verify;
use crate::util::transpose;
@ -92,7 +92,10 @@ pub fn test_eval_fns<
const D: usize,
>(
gate: G,
) -> Result<()> {
) -> Result<()>
where
[(); C::Hasher::HASH_SIZE]:,
{
// Test that `eval_unfiltered` and `eval_unfiltered_base` are coherent.
let wires_base = F::rand_vec(gate.num_wires());
let constants_base = F::rand_vec(gate.num_constants());

View File

@ -12,16 +12,6 @@ pub(crate) const SPONGE_RATE: usize = 8;
pub(crate) const SPONGE_CAPACITY: usize = 4;
pub const SPONGE_WIDTH: usize = SPONGE_RATE + SPONGE_CAPACITY;
/// Hash the slice if necessary to reduce its length to ~256 bits. If it already fits, this is a
/// no-op.
pub fn hash_or_noop<F: RichField, P: PlonkyPermutation<F>>(inputs: &[F]) -> HashOut<F> {
if inputs.len() <= 4 {
HashOut::from_partial(inputs)
} else {
hash_n_to_hash_no_pad::<F, P>(inputs)
}
}
impl<F: RichField + Extendable<D>, const D: usize> CircuitBuilder<F, D> {
pub fn hash_or_noop<H: AlgebraicHasher<F>>(&mut self, inputs: Vec<Target>) -> HashOutTarget {
let zero = self.zero();

View File

@ -30,9 +30,12 @@ pub(crate) fn verify_merkle_proof<F: RichField, H: Hasher<F>>(
leaf_index: usize,
merkle_cap: &MerkleCap<F, H>,
proof: &MerkleProof<F, H>,
) -> Result<()> {
) -> Result<()>
where
[(); H::HASH_SIZE]:,
{
let mut index = leaf_index;
let mut current_digest = H::hash_no_pad(&leaf_data);
let mut current_digest = H::hash_or_noop(&leaf_data);
for &sibling_digest in proof.siblings.iter() {
let bit = index & 1;
index >>= 1;

View File

@ -60,10 +60,13 @@ fn capacity_up_to_mut<T>(v: &mut Vec<T>, len: usize) -> &mut [MaybeUninit<T>] {
fn fill_subtree<F: RichField, H: Hasher<F>>(
digests_buf: &mut [MaybeUninit<H::Hash>],
leaves: &[Vec<F>],
) -> H::Hash {
) -> H::Hash
where
[(); H::HASH_SIZE]:,
{
assert_eq!(leaves.len(), digests_buf.len() / 2 + 1);
if digests_buf.is_empty() {
H::hash_no_pad(&leaves[0])
H::hash_or_noop(&leaves[0])
} else {
// Layout is: left recursive output || left child digest
// || right child digest || right recursive output.
@ -89,7 +92,9 @@ fn fill_digests_buf<F: RichField, H: Hasher<F>>(
cap_buf: &mut [MaybeUninit<H::Hash>],
leaves: &[Vec<F>],
cap_height: usize,
) {
) where
[(); H::HASH_SIZE]:,
{
// Special case of a tree that's all cap. The usual case will panic because we'll try to split
// an empty slice into chunks of `0`. (We would not need this if there was a way to split into
// `blah` chunks as opposed to chunks _of_ `blah`.)
@ -99,7 +104,7 @@ fn fill_digests_buf<F: RichField, H: Hasher<F>>(
.par_iter_mut()
.zip(leaves)
.for_each(|(cap_buf, leaf)| {
cap_buf.write(H::hash_no_pad(leaf));
cap_buf.write(H::hash_or_noop(leaf));
});
return;
}
@ -121,7 +126,10 @@ fn fill_digests_buf<F: RichField, H: Hasher<F>>(
}
impl<F: RichField, H: Hasher<F>> MerkleTree<F, H> {
pub fn new(leaves: Vec<Vec<F>>, cap_height: usize) -> Self {
pub fn new(leaves: Vec<Vec<F>>, cap_height: usize) -> Self
where
[(); H::HASH_SIZE]:,
{
let log2_leaves_len = log2_strict(leaves.len());
assert!(
cap_height <= log2_leaves_len,
@ -208,14 +216,13 @@ mod tests {
(0..n).map(|_| F::rand_vec(k)).collect()
}
fn verify_all_leaves<
F: RichField + Extendable<D>,
C: GenericConfig<D, F = F>,
const D: usize,
>(
fn verify_all_leaves<F: RichField + Extendable<D>, C: GenericConfig<D, F = F>, const D: usize>(
leaves: Vec<Vec<F>>,
cap_height: usize,
) -> Result<()> {
) -> Result<()>
where
[(); C::Hasher::HASH_SIZE]:,
{
let tree = MerkleTree::<F, C::Hasher>::new(leaves.clone(), cap_height);
for (i, leaf) in leaves.into_iter().enumerate() {
let proof = tree.prove(i);

View File

@ -57,7 +57,10 @@ pub(crate) fn decompress_merkle_proofs<F: RichField, H: Hasher<F>>(
compressed_proofs: &[MerkleProof<F, H>],
height: usize,
cap_height: usize,
) -> Vec<MerkleProof<F, H>> {
) -> Vec<MerkleProof<F, H>>
where
[(); H::HASH_SIZE]:,
{
let num_leaves = 1 << height;
let compressed_proofs = compressed_proofs.to_vec();
let mut decompressed_proofs = Vec::with_capacity(compressed_proofs.len());
@ -66,7 +69,7 @@ pub(crate) fn decompress_merkle_proofs<F: RichField, H: Hasher<F>>(
for (&i, v) in leaves_indices.iter().zip(leaves_data) {
// Observe the leaves.
seen.insert(i + num_leaves, H::hash_no_pad(v));
seen.insert(i + num_leaves, H::hash_or_noop(v));
}
// Iterators over the siblings.

View File

@ -610,7 +610,10 @@ impl<F: RichField + Extendable<D>, const D: usize> CircuitBuilder<F, D> {
}
/// Builds a "full circuit", with both prover and verifier data.
pub fn build<C: GenericConfig<D, F = F>>(mut self) -> CircuitData<F, C, D> {
pub fn build<C: GenericConfig<D, F = F>>(mut self) -> CircuitData<F, C, D>
where
[(); C::Hasher::HASH_SIZE]:,
{
let mut timing = TimingTree::new("preprocess", Level::Trace);
let start = Instant::now();
let rate_bits = self.config.fri_config.rate_bits;
@ -776,7 +779,10 @@ impl<F: RichField + Extendable<D>, const D: usize> CircuitBuilder<F, D> {
}
/// Builds a "prover circuit", with data needed to generate proofs but not verify them.
pub fn build_prover<C: GenericConfig<D, F = F>>(self) -> ProverCircuitData<F, C, D> {
pub fn build_prover<C: GenericConfig<D, F = F>>(self) -> ProverCircuitData<F, C, D>
where
[(); C::Hasher::HASH_SIZE]:,
{
// TODO: Can skip parts of this.
let CircuitData {
prover_only,
@ -790,7 +796,10 @@ impl<F: RichField + Extendable<D>, const D: usize> CircuitBuilder<F, D> {
}
/// Builds a "verifier circuit", with data needed to verify proofs but not generate them.
pub fn build_verifier<C: GenericConfig<D, F = F>>(self) -> VerifierCircuitData<F, C, D> {
pub fn build_verifier<C: GenericConfig<D, F = F>>(self) -> VerifierCircuitData<F, C, D>
where
[(); C::Hasher::HASH_SIZE]:,
{
// TODO: Can skip parts of this.
let CircuitData {
verifier_only,

View File

@ -104,7 +104,10 @@ pub struct CircuitData<F: RichField + Extendable<D>, C: GenericConfig<D, F = F>,
impl<F: RichField + Extendable<D>, C: GenericConfig<D, F = F>, const D: usize>
CircuitData<F, C, D>
{
pub fn prove(&self, inputs: PartialWitness<F>) -> Result<ProofWithPublicInputs<F, C, D>> {
pub fn prove(&self, inputs: PartialWitness<F>) -> Result<ProofWithPublicInputs<F, C, D>>
where
[(); C::Hasher::HASH_SIZE]:,
{
prove(
&self.prover_only,
&self.common,
@ -113,14 +116,20 @@ impl<F: RichField + Extendable<D>, C: GenericConfig<D, F = F>, const D: usize>
)
}
pub fn verify(&self, proof_with_pis: ProofWithPublicInputs<F, C, D>) -> Result<()> {
pub fn verify(&self, proof_with_pis: ProofWithPublicInputs<F, C, D>) -> Result<()>
where
[(); C::Hasher::HASH_SIZE]:,
{
verify(proof_with_pis, &self.verifier_only, &self.common)
}
pub fn verify_compressed(
&self,
compressed_proof_with_pis: CompressedProofWithPublicInputs<F, C, D>,
) -> Result<()> {
) -> Result<()>
where
[(); C::Hasher::HASH_SIZE]:,
{
compressed_proof_with_pis.verify(&self.verifier_only, &self.common)
}
}
@ -144,7 +153,10 @@ pub struct ProverCircuitData<
impl<F: RichField + Extendable<D>, C: GenericConfig<D, F = F>, const D: usize>
ProverCircuitData<F, C, D>
{
pub fn prove(&self, inputs: PartialWitness<F>) -> Result<ProofWithPublicInputs<F, C, D>> {
pub fn prove(&self, inputs: PartialWitness<F>) -> Result<ProofWithPublicInputs<F, C, D>>
where
[(); C::Hasher::HASH_SIZE]:,
{
prove(
&self.prover_only,
&self.common,
@ -168,14 +180,20 @@ pub struct VerifierCircuitData<
impl<F: RichField + Extendable<D>, C: GenericConfig<D, F = F>, const D: usize>
VerifierCircuitData<F, C, D>
{
pub fn verify(&self, proof_with_pis: ProofWithPublicInputs<F, C, D>) -> Result<()> {
pub fn verify(&self, proof_with_pis: ProofWithPublicInputs<F, C, D>) -> Result<()>
where
[(); C::Hasher::HASH_SIZE]:,
{
verify(proof_with_pis, &self.verifier_only, &self.common)
}
pub fn verify_compressed(
&self,
compressed_proof_with_pis: CompressedProofWithPublicInputs<F, C, D>,
) -> Result<()> {
) -> Result<()>
where
[(); C::Hasher::HASH_SIZE]:,
{
compressed_proof_with_pis.verify(&self.verifier_only, &self.common)
}
}

View File

@ -46,6 +46,24 @@ pub trait Hasher<F: RichField>: Sized + Clone + Debug + Eq + PartialEq {
Self::hash_no_pad(&padded_input)
}
/// Hash the slice if necessary to reduce its length to ~256 bits. If it already fits, this is a
/// no-op.
fn hash_or_noop(inputs: &[F]) -> Self::Hash
where
[(); Self::HASH_SIZE]:,
{
if inputs.len() <= 4 {
let mut inputs_bytes = [0u8; Self::HASH_SIZE];
for i in 0..inputs.len() {
inputs_bytes[i * 8..(i + 1) * 8]
.copy_from_slice(&inputs[i].to_canonical_u64().to_le_bytes());
}
Self::Hash::from_bytes(&inputs_bytes)
} else {
Self::hash_no_pad(inputs)
}
}
fn two_to_one(left: Self::Hash, right: Self::Hash) -> Self::Hash;
}

View File

@ -138,7 +138,10 @@ impl<F: RichField + Extendable<D>, C: GenericConfig<D, F = F>, const D: usize>
challenges: &ProofChallenges<F, D>,
fri_inferred_elements: FriInferredElements<F, D>,
params: &FriParams,
) -> Proof<F, C, D> {
) -> Proof<F, C, D>
where
[(); C::Hasher::HASH_SIZE]:,
{
let CompressedProof {
wires_cap,
plonk_zs_partial_products_cap,
@ -174,7 +177,10 @@ impl<F: RichField + Extendable<D>, C: GenericConfig<D, F = F>, const D: usize>
pub fn decompress(
self,
common_data: &CommonCircuitData<F, C, D>,
) -> anyhow::Result<ProofWithPublicInputs<F, C, D>> {
) -> anyhow::Result<ProofWithPublicInputs<F, C, D>>
where
[(); C::Hasher::HASH_SIZE]:,
{
let challenges = self.get_challenges(self.get_public_inputs_hash(), common_data)?;
let fri_inferred_elements = self.get_inferred_elements(&challenges, common_data);
let decompressed_proof =
@ -190,7 +196,10 @@ impl<F: RichField + Extendable<D>, C: GenericConfig<D, F = F>, const D: usize>
self,
verifier_data: &VerifierOnlyCircuitData<C, D>,
common_data: &CommonCircuitData<F, C, D>,
) -> anyhow::Result<()> {
) -> anyhow::Result<()>
where
[(); C::Hasher::HASH_SIZE]:,
{
ensure!(
self.public_inputs.len() == common_data.num_public_inputs,
"Number of public inputs doesn't match circuit data."

View File

@ -31,7 +31,10 @@ pub(crate) fn prove<F: RichField + Extendable<D>, C: GenericConfig<D, F = F>, co
common_data: &CommonCircuitData<F, C, D>,
inputs: PartialWitness<F>,
timing: &mut TimingTree,
) -> Result<ProofWithPublicInputs<F, C, D>> {
) -> Result<ProofWithPublicInputs<F, C, D>>
where
[(); C::Hasher::HASH_SIZE]:,
{
let config = &common_data.config;
let num_challenges = config.num_challenges;
let quotient_degree = common_data.quotient_degree();

View File

@ -187,7 +187,9 @@ mod tests {
use crate::gates::noop::NoopGate;
use crate::iop::witness::{PartialWitness, Witness};
use crate::plonk::circuit_data::{CircuitConfig, VerifierOnlyCircuitData};
use crate::plonk::config::{GenericConfig, KeccakGoldilocksConfig, PoseidonGoldilocksConfig};
use crate::plonk::config::{
GenericConfig, Hasher, KeccakGoldilocksConfig, PoseidonGoldilocksConfig,
};
use crate::plonk::proof::{CompressedProofWithPublicInputs, ProofWithPublicInputs};
use crate::plonk::prover::prove;
use crate::util::timing::TimingTree;
@ -322,7 +324,10 @@ mod tests {
ProofWithPublicInputs<F, C, D>,
VerifierOnlyCircuitData<C, D>,
CommonCircuitData<F, C, D>,
)> {
)>
where
[(); C::Hasher::HASH_SIZE]:,
{
let mut builder = CircuitBuilder::<F, D>::new(config.clone());
for _ in 0..num_dummy_gates {
builder.add_gate(NoopGate, vec![]);
@ -356,6 +361,7 @@ mod tests {
)>
where
InnerC::Hasher: AlgebraicHasher<F>,
[(); C::Hasher::HASH_SIZE]:,
{
let mut builder = CircuitBuilder::<F, D>::new(config.clone());
let mut pw = PartialWitness::new();
@ -407,7 +413,10 @@ mod tests {
>(
proof: &ProofWithPublicInputs<F, C, D>,
cd: &CommonCircuitData<F, C, D>,
) -> Result<()> {
) -> Result<()>
where
[(); C::Hasher::HASH_SIZE]:,
{
let proof_bytes = proof.to_bytes()?;
info!("Proof length: {} bytes", proof_bytes.len());
let proof_from_bytes = ProofWithPublicInputs::from_bytes(proof_bytes, cd)?;

View File

@ -15,7 +15,10 @@ pub(crate) fn verify<F: RichField + Extendable<D>, C: GenericConfig<D, F = F>, c
proof_with_pis: ProofWithPublicInputs<F, C, D>,
verifier_data: &VerifierOnlyCircuitData<C, D>,
common_data: &CommonCircuitData<F, C, D>,
) -> Result<()> {
) -> Result<()>
where
[(); C::Hasher::HASH_SIZE]:,
{
ensure!(
proof_with_pis.public_inputs.len() == common_data.num_public_inputs,
"Number of public inputs doesn't match circuit data."
@ -42,7 +45,10 @@ pub(crate) fn verify_with_challenges<
challenges: ProofChallenges<F, D>,
verifier_data: &VerifierOnlyCircuitData<C, D>,
common_data: &CommonCircuitData<F, C, D>,
) -> Result<()> {
) -> Result<()>
where
[(); C::Hasher::HASH_SIZE]:,
{
let local_constants = &proof.openings.constants;
let local_wires = &proof.openings.wires;
let vars = EvaluationVars {

View File

@ -7,7 +7,7 @@ use plonky2::field::zero_poly_coset::ZeroPolyOnCoset;
use plonky2::fri::oracle::PolynomialBatch;
use plonky2::hash::hash_types::RichField;
use plonky2::iop::challenger::Challenger;
use plonky2::plonk::config::GenericConfig;
use plonky2::plonk::config::{GenericConfig, Hasher};
use plonky2::timed;
use plonky2::util::timing::TimingTree;
use plonky2::util::transpose;
@ -33,6 +33,7 @@ where
S: Stark<F, D>,
[(); S::COLUMNS]:,
[(); S::PUBLIC_INPUTS]:,
[(); C::Hasher::HASH_SIZE]:,
{
let degree = trace.len();
let degree_bits = log2_strict(degree);

View File

@ -3,7 +3,7 @@ use plonky2::field::extension_field::{Extendable, FieldExtension};
use plonky2::field::field_types::Field;
use plonky2::fri::verifier::verify_fri_proof;
use plonky2::hash::hash_types::RichField;
use plonky2::plonk::config::GenericConfig;
use plonky2::plonk::config::{GenericConfig, Hasher};
use plonky2::plonk::plonk_common::reduce_with_powers;
use plonky2_util::log2_strict;
@ -26,6 +26,7 @@ pub fn verify<
where
[(); S::COLUMNS]:,
[(); S::PUBLIC_INPUTS]:,
[(); C::Hasher::HASH_SIZE]:,
{
let degree_bits = log2_strict(recover_degree(&proof_with_pis.proof, config));
let challenges = proof_with_pis.get_challenges(config, degree_bits)?;
@ -47,6 +48,7 @@ pub(crate) fn verify_with_challenges<
where
[(); S::COLUMNS]:,
[(); S::PUBLIC_INPUTS]:,
[(); C::Hasher::HASH_SIZE]:,
{
let StarkProofWithPublicInputs {
proof,