From afd479e8223e0a5c6fb28dff21208fc2581b590e Mon Sep 17 00:00:00 2001 From: munna0908 Date: Sun, 3 May 2026 20:38:07 +0530 Subject: [PATCH 1/5] use SharedBuf to facilitate safe cross-GC boundary task spawning --- groth16.nimble | 10 ++++ groth16/bn128/msm.nim | 40 +++++++++++-- groth16/partial/precalc.nim | 3 - groth16/prover/groth16.nim | 3 - groth16/prover/shared.nim | 109 ++++++++++++++++++++++++++---------- groth16/sharedbuf.nim | 43 ++++++++++++++ 6 files changed, 166 insertions(+), 42 deletions(-) create mode 100644 groth16/sharedbuf.nim diff --git a/groth16.nimble b/groth16.nimble index 8c74b38..d1f8ecf 100644 --- a/groth16.nimble +++ b/groth16.nimble @@ -12,3 +12,13 @@ requires "nim >= 2.2.0" requires "https://github.com/status-im/nim-taskpools >= 0.0.5" requires "https://github.com/mratsim/constantine" # requires "https://github.com/mratsim/constantine#bc3845aa492b52f7fef047503b1592e830d1a774" + +task test_arc, "run the test suite under --mm:arc": + exec "nim c -r --threads:on --mm:arc tests/test.nim" + +task test_refc, "run the test suite under --mm:refc": + exec "nim c -r --threads:on --mm:refc tests/test.nim" + +task test, "run the test suite under both --mm:arc and --mm:refc": + exec "nim c -r --threads:on --mm:arc tests/test.nim" + exec "nim c -r --threads:on --mm:refc tests/test.nim" diff --git a/groth16/bn128/msm.nim b/groth16/bn128/msm.nim index b471e01..f4c0cb0 100644 --- a/groth16/bn128/msm.nim +++ b/groth16/bn128/msm.nim @@ -22,6 +22,7 @@ import constantine/math/elliptic/ec_multi_scalar_mul as msm except Su #import groth16/bn128/fields import groth16/bn128/curves as mycurves +import groth16/sharedbuf #import groth16/misc # TEMP DEBUGGING #import std/cpuinfo @@ -79,6 +80,33 @@ func msmConstantineG2*( coeffs: openArray[Fr[BN254_Snarks]] , points: openArray[ return rAff +#------------------------------------------------------------------------------- +# spawnable wrappers: take SharedBuf views, delegate to the core. +# These are what `pool.spawn` calls, so they carry {.gcsafe, raises: [].}. +# +# Local aliases `AffG1`/`AffG2` are required because taskpools' `spawn` macro +# does `getImpl().replaceSymsByIdents()`, which strips qualifications. With +# `SharedBuf[mycurves.G1]` the bare ident `G1` then re-resolves to the enum +# value `aff.G1` (of type `Subgroup`), not the type alias. Renaming dodges +# the collision. + +type + AffG1 = mycurves.G1 + AffG2 = mycurves.G2 + FrBN = Fr[BN254_Snarks] + +func msmConstantineG1Range( coeffs: SharedBuf[FrBN] , + points: SharedBuf[AffG1] ): AffG1 + {.gcsafe, raises: [].} = + msmConstantineG1( toOpenArray(coeffs.payload, 0, coeffs.len - 1), + toOpenArray(points.payload, 0, points.len - 1) ) + +func msmConstantineG2Range( coeffs: SharedBuf[FrBN] , + points: SharedBuf[AffG2] ): AffG2 + {.gcsafe, raises: [].} = + msmConstantineG2( toOpenArray(coeffs.payload, 0, coeffs.len - 1), + toOpenArray(points.payload, 0, points.len - 1) ) + #------------------------------------------------------------------------------- const task_multiplier : int = 1 @@ -105,9 +133,9 @@ proc msmMultiThreadedG1*( coeffs: seq[Fr[BN254_Snarks]] , points: seq[G1], pool: b = (N*(k+1)) div ntasks else: b = N - let cs = coeffs[a.. Date: Tue, 5 May 2026 20:53:35 +0530 Subject: [PATCH 3/5] fix tests --- cli/nim.cfg | 2 +- groth16.nimble | 11 +-- groth16/example/nim.cfg | 3 + tests/groth16/testMultithreading.nim | 113 +++++++++++++++++++++++++++ tests/nim.cfg | 2 + tests/test.nim | 1 + 6 files changed, 123 insertions(+), 9 deletions(-) create mode 100644 groth16/example/nim.cfg create mode 100644 tests/groth16/testMultithreading.nim diff --git a/cli/nim.cfg b/cli/nim.cfg index abe4065..40003bd 100644 --- a/cli/nim.cfg +++ b/cli/nim.cfg @@ -1,3 +1,3 @@ --path:".." --threads:on ---mm:arc \ No newline at end of file +--mm:refc \ No newline at end of file diff --git a/groth16.nimble b/groth16.nimble index d1f8ecf..cc6dccc 100644 --- a/groth16.nimble +++ b/groth16.nimble @@ -1,3 +1,4 @@ + version = "0.1.1" author = "Balazs Komuves" description = "Groth16 proof system" @@ -13,12 +14,6 @@ requires "https://github.com/status-im/nim-taskpools >= 0.0.5" requires "https://github.com/mratsim/constantine" # requires "https://github.com/mratsim/constantine#bc3845aa492b52f7fef047503b1592e830d1a774" -task test_arc, "run the test suite under --mm:arc": - exec "nim c -r --threads:on --mm:arc tests/test.nim" -task test_refc, "run the test suite under --mm:refc": - exec "nim c -r --threads:on --mm:refc tests/test.nim" - -task test, "run the test suite under both --mm:arc and --mm:refc": - exec "nim c -r --threads:on --mm:arc tests/test.nim" - exec "nim c -r --threads:on --mm:refc tests/test.nim" +task test, "Run All Tests": + exec "nim c -r -d:release --opt=speed tests/test.nim " \ No newline at end of file diff --git a/groth16/example/nim.cfg b/groth16/example/nim.cfg new file mode 100644 index 0000000..23eed4d --- /dev/null +++ b/groth16/example/nim.cfg @@ -0,0 +1,3 @@ +--path:"../.." +--threads:on +--mm:arc diff --git a/tests/groth16/testMultithreading.nim b/tests/groth16/testMultithreading.nim new file mode 100644 index 0000000..f801c02 --- /dev/null +++ b/tests/groth16/testMultithreading.nim @@ -0,0 +1,113 @@ + +{.used.} + +# Multi-threading determinism tests. +# +# `generateProofWithTrivialMask` zeros the masking coefficients (r=s=0), so +# the proof is a pure deterministic function of (zkey, witness). Sweeping the +# taskpool thread count must produce byte-identical proof points. Any +# divergence ⇒ data race in the multi-threaded MSM/NTT path. +# +# These tests run under both --mm:arc and --mm:refc via the `test` nimble + +import std/unittest +import std/sequtils + +import taskpools + +# Without this, unittest's echo of the trailing `[OK]` line for the last test +# can stay in stdio buffers when the process exits right after pool.shutdown() +# joins its worker threads — making the suite look like it stopped early. +#setStdIoUnbuffered() + +import groth16/prover +import groth16/prover/groth16 as proverImpl +import groth16/verifier +import groth16/fake_setup +import groth16/zkey_types +import groth16/files/witness +import groth16/files/r1cs +import groth16/bn128/fields + +#------------------------------------------------------------------------------- +# Same simple multiplication circuit testProver.nim uses: 7*11*13 + 1022 = 2023. +# Small but exercises the full prover path (4 MSMs + quotient computation). + +const myWitnessCfg = + WitnessConfig( nWires: 8 + , nPubOut: 1 + , nPubIn: 1 + , nPrivIn: 3 + , nLabels: 0 + ) + +const myEq1 : Constraint = ( @[] , @[] , @[ (1,minusOneFr) , (2,oneFr) , (7,oneFr) ] ) +const myEq2 : Constraint = ( @[ (3,oneFr) ] , @[ (4,oneFr) ] , @[ (6,oneFr) ] ) +const myEq3 : Constraint = ( @[ (5,oneFr) ] , @[ (6,oneFr) ] , @[ (7,oneFr) ] ) + +const myConstraints : seq[Constraint] = @[ myEq1, myEq2, myEq3 ] + +const myR1CS = + R1CS( r: primeR + , cfg: myWitnessCfg + , nConstr: myConstraints.len + , constraints: myConstraints + , wireToLabel: @[] + ) + +let myWitnessValues = map( @[ 1, 2023, 1022, 7, 11, 13, 7*11, 7*11*13 ] , intToFr ) + +let myWitness = + Witness( curve: "bn128" + , r: primeR + , nvars: 8 + , values: myWitnessValues + ) + +const ThreadCounts = [1, 2, 4, 8] + +#------------------------------------------------------------------------------- + +proc proveWithThreads(zkey: ZKey, witness: Witness, nThreads: int): Proof = + var pool = Taskpool.new(numThreads = nThreads) + result = generateProofWithTrivialMask( zkey, witness, pool, printTimings = false ) + pool.shutdown() + +proc verifyWith(zkey: ZKey, proof: Proof): bool = + let vkey = extractVKey(zkey) + return verifyProof(vkey, proof) + +#------------------------------------------------------------------------------- + +suite "multithreading": + + test "repeated proofs on the same pool match (no per-call state leak)": + # Reusing one pool across many proofs must not change the output: rules + # out residual state in worker-local buffers between invocations. + let zkey = createFakeCircuitSetup( myR1cs, flavour=Snarkjs ) + var pool = Taskpool.new(numThreads = 4) + defer: pool.shutdown() + let first = generateProofWithTrivialMask(zkey, myWitness, pool, false) + for _ in 0 ..< 4: + let again = generateProofWithTrivialMask(zkey, myWitness, pool, false) + check isEqualProof(first, again) + + test "trivial-mask proof is deterministic across thread counts (JensGroth)": + let zkey = createFakeCircuitSetup( myR1cs, flavour=JensGroth ) + let reference = proveWithThreads(zkey, myWitness, ThreadCounts[0]) + check verifyWith(zkey, reference) + for j in ThreadCounts[1..^1]: + let proof = proveWithThreads(zkey, myWitness, j) + check isEqualProof(reference, proof) + check verifyWith(zkey, proof) + + test "trivial-mask proof is deterministic across thread counts (Snarkjs)": + let zkey = createFakeCircuitSetup( myR1cs, flavour=Snarkjs ) + let reference = proveWithThreads(zkey, myWitness, ThreadCounts[0]) + check verifyWith(zkey, reference) + for j in ThreadCounts[1..^1]: + let proof = proveWithThreads(zkey, myWitness, j) + check isEqualProof(reference, proof) + check verifyWith(zkey, proof) + + diff --git a/tests/nim.cfg b/tests/nim.cfg index 0f840a1..40003bd 100644 --- a/tests/nim.cfg +++ b/tests/nim.cfg @@ -1 +1,3 @@ --path:".." +--threads:on +--mm:refc \ No newline at end of file diff --git a/tests/test.nim b/tests/test.nim index 36aea78..194247e 100644 --- a/tests/test.nim +++ b/tests/test.nim @@ -2,4 +2,5 @@ import ./groth16/testPtCompression import ./groth16/testCurve import ./groth16/testProver +import ./groth16/testMultithreading From 704f0caed8f2ffff575d4cbdb5b914b096d90cf4 Mon Sep 17 00:00:00 2001 From: munna0908 Date: Tue, 5 May 2026 21:11:58 +0530 Subject: [PATCH 4/5] code cleanup --- groth16.nimble | 4 ---- groth16/prover/shared.nim | 4 ++-- tests/groth16/testMultithreading.nim | 8 -------- 3 files changed, 2 insertions(+), 14 deletions(-) diff --git a/groth16.nimble b/groth16.nimble index cc6dccc..29b38d3 100644 --- a/groth16.nimble +++ b/groth16.nimble @@ -13,7 +13,3 @@ requires "nim >= 2.2.0" requires "https://github.com/status-im/nim-taskpools >= 0.0.5" requires "https://github.com/mratsim/constantine" # requires "https://github.com/mratsim/constantine#bc3845aa492b52f7fef047503b1592e830d1a774" - - -task test, "Run All Tests": - exec "nim c -r -d:release --opt=speed tests/test.nim " \ No newline at end of file diff --git a/groth16/prover/shared.nim b/groth16/prover/shared.nim index e9bc358..3862205 100644 --- a/groth16/prover/shared.nim +++ b/groth16/prover/shared.nim @@ -134,10 +134,10 @@ proc shiftEvalDomainTask( var local = newSeq[FrBN](n) for i in 0 ..< n: local[i] = values.payload[i] - let result = shiftEvalDomain(local, D, eta) + let res = shiftEvalDomain(local, D, eta) # Copy result → caller's output buffer through the SharedBuf payload pointer. - for i in 0 ..< n: output.payload[i] = result[i] + for i in 0 ..< n: output.payload[i] = res[i] return true # computes the quotient polynomial Q = (A*B - C) / Z diff --git a/tests/groth16/testMultithreading.nim b/tests/groth16/testMultithreading.nim index f801c02..8e85911 100644 --- a/tests/groth16/testMultithreading.nim +++ b/tests/groth16/testMultithreading.nim @@ -1,4 +1,3 @@ - {.used.} # Multi-threading determinism tests. @@ -7,19 +6,12 @@ # the proof is a pure deterministic function of (zkey, witness). Sweeping the # taskpool thread count must produce byte-identical proof points. Any # divergence ⇒ data race in the multi-threaded MSM/NTT path. -# -# These tests run under both --mm:arc and --mm:refc via the `test` nimble import std/unittest import std/sequtils import taskpools -# Without this, unittest's echo of the trailing `[OK]` line for the last test -# can stay in stdio buffers when the process exits right after pool.shutdown() -# joins its worker threads — making the suite look like it stopped early. -#setStdIoUnbuffered() - import groth16/prover import groth16/prover/groth16 as proverImpl import groth16/verifier From fc56b91393a65b931658dc5f014e93b72aed26e4 Mon Sep 17 00:00:00 2001 From: munna0908 Date: Thu, 7 May 2026 15:53:36 +0530 Subject: [PATCH 5/5] add tests to generate random proofs --- tests/groth16/testMultithreading.nim | 39 ++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/tests/groth16/testMultithreading.nim b/tests/groth16/testMultithreading.nim index 8e85911..c6badd9 100644 --- a/tests/groth16/testMultithreading.nim +++ b/tests/groth16/testMultithreading.nim @@ -1,11 +1,20 @@ {.used.} -# Multi-threading determinism tests. +# Multi-threading correctness tests. # -# `generateProofWithTrivialMask` zeros the masking coefficients (r=s=0), so -# the proof is a pure deterministic function of (zkey, witness). Sweeping the -# taskpool thread count must produce byte-identical proof points. Any -# divergence ⇒ data race in the multi-threaded MSM/NTT path. +# Two complementary checks: +# +# 1. Trivial-mask determinism (r=s=0): proof is a pure deterministic function +# of (zkey, witness), so sweeping the thread count must produce +# byte-identical proof points. Catches races that produce *different but +# still valid* proofs across configurations. +# +# 2. Random-mask end-to-end verify: proves with random masking (the +# production code path) under varied (gc-mode, thread-count) and asserts +# every resulting proof verifies. Random masks change the MSM coefficient +# inputs, which exercises the data-dependent (non-constant-time) parts of +# the MSM where coefficient-magnitude-driven races have historically +# hidden — invisible under trivial-mask testing. import std/unittest import std/sequtils @@ -102,4 +111,24 @@ suite "multithreading": check isEqualProof(reference, proof) check verifyWith(zkey, proof) + test "random-mask proofs verify across thread counts (Snarkjs)": + let zkey = createFakeCircuitSetup( myR1cs, flavour=Snarkjs ) + let vkey = extractVKey(zkey) + for j in ThreadCounts: + var pool = Taskpool.new(numThreads = j) + defer: pool.shutdown() + for _ in 0 ..< 100: + let proof = generateProof(zkey, myWitness, pool, false) + check verifyProof(vkey, proof) + + test "random-mask proofs verify across thread counts (JensGroth)": + let zkey = createFakeCircuitSetup( myR1cs, flavour=JensGroth ) + let vkey = extractVKey(zkey) + for j in ThreadCounts: + var pool = Taskpool.new(numThreads = j) + defer: pool.shutdown() + for _ in 0 ..< 100: + let proof = generateProof(zkey, myWitness, pool, false) + check verifyProof(vkey, proof) +