Skip to content

Adds SPARK#345

Open
batmendbar wants to merge 57 commits into
mainfrom
adds-spark-squashed
Open

Adds SPARK#345
batmendbar wants to merge 57 commits into
mainfrom
adds-spark-squashed

Conversation

@batmendbar
Copy link
Copy Markdown
Collaborator

@batmendbar batmendbar commented Mar 18, 2026

SPARK

Reference for this implementation

Proposed protototype workflow

  1. Serve step

    • One time
      • Starts the server
      • Compiles the circuit
      • Calculates the SPARK matrix data and commits to them
    • Ongoing
      • Listens to SPARK query requests and produces SPARK proofs using the pre-calculated commitments
  2. Provekit prove step

    • Runs provekit prover and obtains a deferred evaluation
    • Sends a deferred evaluation request to the server
  3. Provekit and SPARK verify step

    • Verifies Provekit and SPARK proofs

Design decisions

Pack $A$, $B$, $C$ into one block matrix Z:

This is a result from Marcin (https://gist.github.com/kustosz/14b62de666f721ab855536e575891bd1)

The trick:

$$Z = \begin{bmatrix} A & B \ 0 & C \end{bmatrix}$$

Same total non-zeros, double the dimensions. Then for any $\beta$, $p$, and $q$:

$$A(p,q) + \beta B(p,q) + \beta^2 C(p,q) = (1+\beta)^2 \cdot Z!\left(\tfrac{\beta}{1+\beta}, p,\ \tfrac{\beta}{1+\beta}, q\right)$$

One matrix, one commitment, one opening.

Batching GPA and WHIR proofs

  • Combining GPA

    • Products of hashes corresponding to read sets and write sets of row-wise and column-wise memory check are combined into one GPA
    • Products of hashes corresponding to init and final vectors are combined into one GPA (separate for row-wise and col-wise memory). Possible optimization - if number of rows and columns for the matrix are ensured to be equal, we can combine them into one GPA.
  • WHIR Batching
    | num_terms_2batched e-values are committed and opened together. Opened once in sumcheck and once in rs_ws GPA
    | num_terms_4batched | Address/timestamp values for row-wise and col-wise memory checks are committed and opened together

Temporary Sumcheck for split witness

The current ZK WHIR doesn't support batching which would enable easier handling of split witness commitment. The repo currently uses an additional sumcheck proposed by Marcin until batch ZK commitment is supported https://gist.github.com/kustosz/c7c3f756aaae77f37e035c30c4961ea3.

The trick:

Collapsing two claims into one: With claims on $A(r,0,q_1)$ and $A(r,1,q_2)$. Draw random $\beta$ and note their RLC is just another sum over $A(r,\cdot)$, so run a Sumcheck #2 to reduce to a single claim $A(r, \gamma)$.

Full workflow for a Noir passport circuit:

# 1. Start the server (compiles circuits and pre-commits)
provekit-cli serve \
  --socket /tmp/spark.sock \
  --output-dir ./benchmark-inputs \
  --circuit t_attest:./target/t_attest.json &

# 2. Wait for server readiness
while [ ! -S /tmp/spark.sock ]; do sleep 1; done

# 3. Prove (generates Noir proof + SPARK proof)
provekit-cli prove \
  ./benchmark-inputs/t_attest.pkp \
  ./benchmark-inputs/tbs_720/t_attest.toml \
  -o ./benchmark-inputs/t_attest-proof.np \
  --socket /tmp/spark.sock \
  --circuit t_attest \
  --spark-out ./benchmark-inputs/t_attest-spark-proof.sp

# 4. Verify Noir + SPARK proof
provekit-cli verify \
  ./benchmark-inputs/t_attest.pkv \
  ./benchmark-inputs/t_attest-proof.np \
  --spark-proof ./benchmark-inputs/t_attest-spark-proof.sp

What is not included

  • Recursive SPARK

Benchmark

[TODO: Update with new benchmark]

@batmendbar batmendbar requested a review from Bisht13 March 18, 2026 08:37
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 29, 2026

CSP benchmarks

Metric Value
Workflow status [PASS] success
Commit fb76bc6249a0
Run #25849765206
Circuits benchmarked 21
Iterations averaged per circuit 3

Prover time, peak RSS, peak heap, and verifier time are arithmetic means across the iterations. Peak heap comes from the largest peak memory entry in provekit-cli prove's tracing output; peak RSS is reported by /usr/bin/time -v (max-resident-set-size).

Each metric cell shows the current value followed by the percentage delta against the latest successful main run #25494153164. (new) marks circuits absent from the baseline.

Results
Circuit Constraints Witnesses Prover time Peak RSS Peak heap Verifier time Proof size PKP size
ecdsa_p256 143,282 (±0.0%) 258,158 (±0.0%) 2.99 s (-0.3%) 257 MB (-0.9%) 225 MB (+0.1%) 340 ms (-2.9%) 2.79 MB (-0.6%) 810 KB (±0.0%)
keccak_1024 822,870 (±0.0%) 1,543,366 (±0.0%) 6.40 s (-0.6%) 986 MB (±0.0%) 954 MB (±0.0%) 863 ms (+0.4%) 3.13 MB (±0.0%) 6.07 MB (±0.0%)
keccak_128 163,058 (±0.0%) 313,707 (±0.0%) 2.13 s (-0.5%) 274 MB (-0.9%) 242 MB (+0.1%) 370 ms (±0.0%) 2.81 MB (-0.2%) 1.22 MB (±0.0%)
keccak_2048 1,575,606 (±0.0%) 2,945,822 (±0.0%) 11.91 s (-0.3%) 1.81 GB (±0.0%) 1.80 GB (±0.0%) 1.45 s (-1.4%) 3.25 MB (+0.4%) 12.36 MB (±0.0%)
keccak_256 256,206 (±0.0%) 487,012 (±0.0%) 2.31 s (-0.9%) 328 MB (±0.0%) 290 MB (-0.1%) 407 ms (-2.4%) 2.81 MB (-1.5%) 1.97 MB (±0.0%)
keccak_512 445,094 (±0.0%) 839,130 (±0.0%) 3.65 s (-0.5%) 594 MB (±0.0%) 509 MB (±0.0%) 560 ms (±0.0%) 3.01 MB (-0.8%) 3.40 MB (±0.0%)
poseidon2_12 479 (±0.0%) 563 (±0.0%) 350 ms (-1.9%) 24.15 MB (-0.5%) 14.69 MB (±0.0%) 100 ms (±0.0%) 1.00 MB (-2.5%) 436 KB (±0.0%)
poseidon2_16 556 (±0.0%) 719 (±0.0%) 353 ms (-1.9%) 24.55 MB (-0.3%) 14.88 MB (±0.0%) 100 ms (-3.2%) 1.06 MB (+3.2%) 530 KB (±0.0%)
poseidon2_2 231 (±0.0%) 278 (±0.0%) 353 ms (+0.9%) 23.45 MB (-0.6%) 14.11 MB (±0.0%) 100 ms (-3.2%) 1.00 MB (-2.8%) 108 KB (±0.0%)
poseidon2_4 529 (±0.0%) 535 (±0.0%) 347 ms (±0.0%) 23.62 MB (-1.0%) 14.31 MB (±0.0%) 100 ms (±0.0%) 1.03 MB (+1.6%) 31.67 KB (±0.0%)
poseidon2_8 363 (±0.0%) 423 (±0.0%) 350 ms (-1.9%) 24.34 MB (-0.7%) 14.50 MB (±0.0%) 100 ms (±0.0%) 1.04 MB (+0.9%) 365 KB (±0.0%)
poseidon_12 504 (±0.0%) 524 (±0.0%) 353 ms (-1.0%) 24.58 MB (-0.6%) 14.69 MB (±0.0%) 100 ms (-3.2%) 1.04 MB (+1.7%) 410 KB (±0.0%)
poseidon_16 609 (±0.0%) 633 (±0.0%) 353 ms (±0.0%) 24.44 MB (-0.6%) 14.97 MB (±0.0%) 100 ms (±0.0%) 1.02 MB (-1.8%) 536 KB (±0.0%)
poseidon_2 240 (±0.0%) 249 (±0.0%) 343 ms (±0.0%) 23.28 MB (-0.5%) 14.02 MB (±0.0%) 100 ms (±0.0%) 1.02 MB (-1.8%) 53.79 KB (±0.0%)
poseidon_4 297 (±0.0%) 309 (±0.0%) 347 ms (-1.9%) 23.78 MB (-0.6%) 14.31 MB (±0.0%) 100 ms (±0.0%) 1.04 MB (+1.4%) 210 KB (±0.0%)
poseidon_8 402 (±0.0%) 418 (±0.0%) 350 ms (±0.0%) 23.71 MB (-0.9%) 14.50 MB (±0.0%) 107 ms (+6.7%) 1.00 MB (-3.6%) 305 KB (±0.0%)
sha256_1024 196,940 (±0.0%) 339,764 (±0.0%) 2.22 s (-0.9%) 309 MB (+0.4%) 274 MB (+0.2%) 420 ms (-0.8%) 2.79 MB (-1.9%) 1.91 MB (+0.8%)
sha256_128 46,398 (±0.0%) 80,974 (±0.0%) 1.09 s (+0.3%) 101 MB (+0.7%) 83.73 MB (+0.1%) 260 ms (±0.0%) 2.51 MB (+0.3%) 510 KB (±0.0%)
sha256_2048 345,399 (±0.0%) 612,724 (±0.0%) 3.60 s (-0.5%) 548 MB (±0.0%) 484 MB (+0.1%) 610 ms (±0.0%) 2.95 MB (-1.3%) 3.13 MB (+3.1%)
sha256_256 67,904 (±0.0%) 117,944 (±0.0%) 1.39 s (-0.7%) 150 MB (-1.5%) 130 MB (±0.0%) 290 ms (-1.1%) 2.67 MB (±0.0%) 705 KB (-2.1%)
sha256_512 110,916 (±0.0%) 191,884 (±0.0%) 1.51 s (-1.3%) 184 MB (+1.7%) 158 MB (±0.0%) 327 ms (±0.0%) 2.69 MB (+0.1%) 1.14 MB (+4.9%)

Comment thread .github/workflows/end-to-end.yml.disabled
let request = if requests.len() == 1 {
requests[0].clone()
} else {
let alphas = alphas_from_spark(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

For multi-query batching we compute alphas from requests[0].row, but never ensure the rest of the requests use the same row. Please reject mixed-row batches before proving.


R1CSSparkQuery {
point_to_evaluate: Point {
row: requests[0].point_to_evaluate.row.clone(),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Same-row batching needs to be enforced verifier-side too. Otherwise verification can fold claims using requests[0].row even when later requests target different rows.

let scheme = SPARKProverScheme::new(num_rows, num_cols, nonzero_terms, hash_config);

let ds = DomainSeparator::protocol(&scheme.whir_configs).instance(&Empty);
let mut merlin = ProverState::new(&ds, TranscriptSponge::default());
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This uses TranscriptSponge::default() even though setup receives hash_config. SPARK should use the configured Fiat-Shamir hash consistently, or store the config in SPARKSetup.

.session(&setup.transcript.narg_string)
.instance(&hash_query_set(requests)),
&whir_proof,
TranscriptSponge::default(),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Verifier also uses the default transcript sponge. Please derive it from trusted setup/hash config so --hash does not affect commitments but not Fiat-Shamir.

let setup = setup?;
let queries = queries?;

SPARKVerifierScheme
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

verify and verify-spark share no cryptographic state, a prover can pair an honest NoirProof for C₁ with a self-consistent .sp+.spc+query JSON set for a different circuit C₂ with matching dimensions, and both verifiers will accept. Derive the expected query from the Noir transcript and require verify-spark to consume it, ship a combined verifier.

}

#[derive(Debug, Clone)]
pub struct COOMatrix {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

COOMatrix stores row: Vec and row_field: Vec (same data twice). Doubles memory for big circuits and the invariant row_field[i] == FieldElement::from(row[i] as u64) is unenforced. Drop one and derive on demand, or wrap construction in a constructor that enforces it.

}
}

fn collect_queries(dir: &Path) -> Result<Vec<R1CSSparkQuery>> {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

collect_queries walks spark_query_.json until first miss, if _3.json is missing but _4.json exists, _4 is silently skipped and the SPARK proof binds the wrong set. Use read_dir+sort, or an explicit manifest, and ensure! the discovered set is contiguous.

public_inputs: &PublicInputs,
) -> Result<WhirR1CSProof>;
produce_spark_query: bool,
) -> Result<(WhirR1CSProof, Vec<R1CSSparkQuery>)>;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Here and :159, :269, #[allow(clippy::too_many_arguments)] mutes a workspace-level lint. Group into a ProveFromAlphasCtx, SparkProverContext already sets the precedent.

},
};

pub trait SPARKProver {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

SPARK* acronym casing violates Rust API Guidelines C-CASE and trips clippy::upper_case_acronyms. The codebase already uses Spark* for SparkMatrix/SparkWitnesses/SparkProverContext, internally inconsistent. Rename to Spark*.

.try_into()
.expect("4 polys");

merlin.prover_hint_ark(&row_address_eval);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

One-line comment per hint call naming which WHIR check seals it would prevent a future refactor from breaking soundness.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants