Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
18ef54f
WIP commit of our groth16 work
Vindaar Aug 5, 2024
79445df
add partially manual Groth16 proof file
Vindaar Aug 13, 2024
ff4ea0f
minor comments, cleanup, TODOs
Vindaar Aug 13, 2024
fd856bb
add note about typed R1CS
Vindaar Aug 13, 2024
15965b2
add 'typed' variant of Zkey
Vindaar Aug 13, 2024
7954f7b
add 'typed' variant of Witness file data
Vindaar Aug 13, 2024
3ec132f
move `asEC*`, `randomFieldElement` to utils
Vindaar Aug 13, 2024
9291bad
use 'typed' form of zkey / witnessss data objects
Vindaar Aug 13, 2024
cb48a09
[tests] add test file for finite field FFT
Vindaar Aug 14, 2024
f774ece
[groth16] whitespace
Vindaar Aug 14, 2024
37eb08e
clean up FFT for finite fields
Vindaar Aug 14, 2024
4603783
remove old FFT finite field test file
Vindaar Aug 14, 2024
d835945
remove old TODO from FFT
Vindaar Aug 14, 2024
80344bd
move groth16 files one dir up (proof_systems)
Vindaar Aug 14, 2024
0c41a2e
add notes about binary zkey, wtns file format / parser
Vindaar Aug 14, 2024
47b2020
fixup code for changed path, minor cleanup
Vindaar Aug 14, 2024
ed80daf
bind sysrand in utils
Vindaar Aug 15, 2024
b48f02f
export some modules, export prover & prove
Vindaar Aug 15, 2024
38658f5
bind two identifiers in `sumImpl`
Vindaar Aug 15, 2024
b174b14
[example] add Groth16 prover example
Vindaar Aug 15, 2024
e9fe204
[examples] clean up outputs of code blocks
Vindaar Aug 15, 2024
e92471f
remove left over `:END:`
Vindaar Aug 15, 2024
020e88b
remove old echoes
Vindaar Aug 15, 2024
f79152d
export EC related modules from Groth16
Vindaar Aug 15, 2024
3d7eade
update tangled example Nim code
Vindaar Aug 15, 2024
6a43160
[io_fields] annotate `fromDecimal` with raises, pop `raises: []` before
Vindaar Aug 15, 2024
dbab52b
[tests] add groth16 prover test case, binary files for test
Vindaar Aug 15, 2024
0525317
export wtns field of Witness section
Vindaar Aug 15, 2024
13b1957
[tests] add test case for Witness binary parser
Vindaar Aug 15, 2024
2381b2e
[tests] add `.zkey` parser test case
Vindaar Aug 20, 2024
4a12fe7
[tests] add expected zkey test files as JSON data
Vindaar Aug 20, 2024
6ef7dd5
[nimble] add finite field FFT, Groth16 related tests to nimble file
Vindaar Aug 20, 2024
928d4d1
add booldefine variables to choose what is Montgomery encoded
Vindaar Aug 22, 2024
e3cb336
add a bunch more echo outputs for SnarkJS comparison
Vindaar Aug 22, 2024
c92ff5a
[parser] minor cleanup of the binary file parsing logic
Vindaar Jan 25, 2025
5ceda05
remove comment about truncation in random field element sampling
Vindaar Jan 25, 2025
694755f
remove debugging ~booldefine~ variables
Vindaar Jan 25, 2025
65b4d53
[groth16] add parser utils submodule for Groth16 binary files
Vindaar Jan 25, 2025
854dc5e
[groth16] rename the Groth16 main file
Vindaar Jan 25, 2025
9a33863
clean up FFT LUT calculation code
Vindaar Jan 25, 2025
28cb7e2
[groth16] clean up Groth16 main file
Vindaar Jan 25, 2025
aa114bb
[tests] clean up Groth16 test case, adjust for correct API
Vindaar Jan 25, 2025
f1607f9
remove duplicate `pow_vartime` due to rebase
Vindaar Jan 25, 2025
1ea90dd
[tests] update ZKey test vectors from wrong `booldefine` branch
Vindaar Jan 25, 2025
bd59352
[utils] add comment reference to double Montgomery encoding
Vindaar Jan 25, 2025
c1f21f2
[tests] fix paths in Zkey and Groth16 prover tests
Vindaar Jan 25, 2025
9a8a706
[groth16] add proper type for Groth16 proof
Vindaar Jan 25, 2025
4bfa1eb
[groth16] implement Groth16 proof verification and test it
Vindaar Jan 25, 2025
8efc5bb
[groth16] remove the now unused CMM_WN booldefine
Vindaar Jan 25, 2025
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
4 changes: 4 additions & 0 deletions constantine.nimble
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,7 @@ const testDesc: seq[tuple[path: string, useGMP: bool]] = @[
# Polynomials
# ----------------------------------------------------------
("tests/math_polynomials/t_polynomials.nim", false),
("tests/math_polynomials/t_finite_field_fft.nim", false),

# Protocols
# ----------------------------------------------------------
Expand All @@ -633,6 +634,9 @@ const testDesc: seq[tuple[path: string, useGMP: bool]] = @[
# Proof systems
# ----------------------------------------------------------
("tests/proof_systems/t_r1cs_parser.nim", false),
("tests/proof_systems/t_wtns_parser.nim", false),
("tests/proof_systems/t_zkey_parser.nim", false),
("tests/proof_systems/t_groth16_prover.nim", false),
("tests/interactive_proofs/t_multilinear_extensions.nim", false),
]

Expand Down
2 changes: 2 additions & 0 deletions constantine/math/elliptic/ec_shortweierstrass_jacobian.nim
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,8 @@ template sumImpl[F; G: static Subgroup](

# if P or R were infinity points they would have spread 0 with Z₁Z₂
block: # Infinity points
bind isNeutral
bind ccopy
o.ccopy(Q, P.isNeutral())
o.ccopy(P, Q.isNeutral())

Expand Down
25 changes: 25 additions & 0 deletions constantine/math/io/io_ec.nim
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,31 @@ func toHex*[EC: EC_ShortW_Prj or EC_ShortW_Jac or EC_ShortW_Aff or EC_ShortW_Jac
result.appendHex(aff.y)
result &= "\n" & sp & ")"

func toDecimal*[EC: EC_ShortW_Prj or EC_ShortW_Jac or EC_ShortW_Aff or EC_ShortW_JacExt](P: EC, indent: static int = 0): string =
## Stringify an elliptic curve point to Hex
Copy link
Owner

Choose a reason for hiding this comment

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

Suggested change
## Stringify an elliptic curve point to Hex
## Stringify an elliptic curve point to decimal

## Note. Leading zeros are not removed.
## Output as decimal.
##
## WARNING: NOT constant time!
Copy link
Owner

Choose a reason for hiding this comment

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

Actually it is constant-time see low-level impl:

func toDecimal*(a: BigInt): string =
## Convert to a decimal string.
##
## This procedure is intended for configuration, prototyping, research and debugging purposes.
## You MUST NOT use it for production.
##
## This function is constant-time.
## This function does heap-allocation.
const len = decimalLength(BigInt.bits)
result = newString(len)
var a = a
for i in countdown(len-1, 0):
let c = ord('0') + a.div10().int
result[i] = char(c)

The mid-level impl is wrong:

func toDecimal*(f: FF): string =
## Convert to a decimal string.
##
## It is intended for configuration, prototyping, research and debugging purposes.
## You MUST NOT use it for production.
##
## This function is NOT constant-time at the moment.
f.toBig().toDecimal()

But it's fine, afaik, it's only used for debugging purposes.

##
## This proc output may change format in the future

var aff {.noInit.}: EC_ShortW_Aff[EC.F, EC.G]
when EC isnot EC_ShortW_Aff:
aff.affine(P)
else:
aff = P

const sp = spaces(indent)

result = sp & $EC & "(\n" & sp & " x: "
result.add toDecimal(aff.x)
result &= ",\n" & sp & " y: "
result.add toDecimal(aff.y)
result &= "\n" & sp & ")"



func toHex*[EC: EC_TwEdw_Aff or EC_TwEdw_Prj](P: EC, indent: static int = 0): string =
## Stringify an elliptic curve point to Hex for Twisted Edwards Curve
## Note, leading zeros are not removed.
Expand Down
34 changes: 34 additions & 0 deletions constantine/math/io/io_extfields.nim
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,40 @@ func toHex*(f: ExtensionField, indent = 0, order: static Endianness = bigEndian)
## - no leaks
result.appendHex(f, indent, order)

func appendDecimal*(accum: var string, f: Fp, indent = 0, order: static Endianness = bigEndian) =
accum.add toDecimal(f)

func appendDecimal*(accum: var string, f: ExtensionField, indent = 0, order: static Endianness = bigEndian) =
## Stringify a tower field element to hex.
Copy link
Owner

Choose a reason for hiding this comment

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

Suggested change
## Stringify a tower field element to hex.
## Stringify a tower field element to decimal.

## Note. Leading zeros are not removed.
## Result is prefixed with 0x
##
## Output will be padded with 0s to maintain constant-time.
##
## CT:
## - no leaks
accum.add static($f.typeof.genericHead() & '(')
staticFor i, 0, f.coords.len:
when i != 0:
accum.add ", "
accum.add "\n" & spaces(indent+2) & "c" & $i & ": "
when f is Fp2:
accum.appendDecimal(f.coords[i], order = order)
else:
accum.appendDecimal(f.coords[i], indent+2, order)
accum.add ")"

func toDecimal*(f: ExtensionField, indent = 0, order: static Endianness = bigEndian): string =
## Stringify a tower field element to hex.
## Note. Leading zeros are not removed.
## Result is prefixed with 0x
##
## Output will be padded with 0s to maintain constant-time.
##
## CT:
## - no leaks
result.appendDecimal(f, indent, order)

func fromHex*(dst: var Fp2, c0, c1: string) =
## Convert 2 coordinates to an element of 𝔽p2
## with dst = c0 + β * c1
Expand Down
6 changes: 4 additions & 2 deletions constantine/math/io/io_fields.nim
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,9 @@ func toDecimal*(f: FF): string =
## This function is NOT constant-time at the moment.
f.toBig().toDecimal()

func fromDecimal*(dst: var FF, decimalString: string) =
{.pop.} # `fromDecimal` can raise `ValueError`

func fromDecimal*(dst: var FF, decimalString: string) {.raises: [ValueError].} =
## Convert a decimal string. The input must be packed
## with no spaces or underscores.
## This assumes that bits and decimal length are **public.**
Expand All @@ -136,7 +138,7 @@ func fromDecimal*(dst: var FF, decimalString: string) =
let raw {.noinit.} = fromDecimal(dst.mres.typeof, decimalString)
dst.fromBig(raw)

func fromDecimal*(T: type FF, hexString: string): T {.noInit.}=
func fromDecimal*(T: type FF, hexString: string): T {.raises: [ValueError], noInit.}=
## Convert a decimal string. The input must be packed
## with no spaces or underscores.
## This assumes that bits and decimal length are **public.**
Expand Down
2 changes: 1 addition & 1 deletion constantine/math/polynomials/fft.nim
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ func fft_internal[EC; bits: static int](

for i in 0 ..< half:
# FFT Butterfly
y_times_root .scalarMul_vartime(output[i+half], rootsOfUnity[i])
y_times_root .scalarMul_vartime(rootsOfUnity[i], output[i+half])
Copy link
Owner

Choose a reason for hiding this comment

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

Curious the PR that introduced scalarMul_vartime also rewrote this part of the FFT and the argument order should be scalarMul_vartime(EC, scalar).

Copy link
Owner

Choose a reason for hiding this comment

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

Ah my mistake, the argument order when out-of-place is correct. (EC out, scalar, EC in)

output[i+half] .diff_vartime(output[i], y_times_root)
output[i] .sum_vartime(output[i], y_times_root)

Expand Down
225 changes: 225 additions & 0 deletions constantine/math/polynomials/fft_fields.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
# Constantine
# Copyright (c) 2018-2019 Status Research & Development GmbH
# Copyright (c) 2020-Present Mamy André-Ratsimbazafy
# Licensed and distributed under either of
# * MIT license (license terms in the root directory or at http://opensource.org/licenses/MIT).
# * Apache v2 license (license terms in the root directory or at http://www.apache.org/licenses/LICENSE-2.0).
# at your option. This file may not be copied, modified, or distributed except according to those terms.

import
constantine/named/algebras,
constantine/math/arithmetic,
constantine/math/io/io_bigints,
constantine/platforms/[abstractions, allocs, views],
./fft_lut,
constantine/platforms/bithacks # for nextPowerOf2

# ############################################################
#
# Fast Fourier Transform
#
# ############################################################

# Fast Fourier Transform (Number Theoretic Transform - NTT) over finite fields
# ----------------------------------------------------------------

type
FFTStatus* = enum
FFTS_Success
FFTS_TooManyValues = "Input length greater than the field 2-adicity (number of roots of unity)"
FFTS_SizeNotPowerOfTwo = "Input must be of a power of 2 length"

FFT_Descriptor*[F] = object # `F` is either `Fp[Name]` or `Fr[Name]`
## Metadata for FFT on Elliptic Curve
Copy link
Owner

Choose a reason for hiding this comment

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

on fields.

Note that there was an implementation here: https://github.com/mratsim/constantine/blob/v0.2.0/research/kzg/fft_fr.nim

order*: int
rouGen*: F ## Roots of unity generator based on primitive root of `F`: `ω = g^( (p - 1) // n )`
rootsOfUnity*: ptr UncheckedArray[getBigInt(F)] # `getBigInt` gives us the right type depending on Fr/Fp
## domain, starting and ending with 1, length is cardinality+1
## This allows FFT and inverse FFT to use the same buffer for roots.

func computeRootsOfUnity[F](ctx: var FFT_Descriptor[F], generatorRootOfUnity: auto) =
static:
doAssert typeof(generatorRootOfUnity) is Fr[F.Name] or typeof(generatorRootOfUnity) is Fp[F.Name]

ctx.rootsOfUnity[0].setOne()

var cur = generatorRootOfUnity
for i in 1 .. ctx.order:
ctx.rootsOfUnity[i].fromField(cur)
cur *= generatorRootOfUnity

doAssert ctx.rootsOfUnity[ctx.order].isOne().bool(), "The given generator does not seem to be a root of unity " &
"of " & $F & " for order: " & $ctx.order & "."

func init*[Name: static Algebra](T: type FFT_Descriptor, order: int, generatorRootOfUnity: FF[Name]): T =
result.order = order
result.rouGen = generatorRootOfUnity
result.rootsOfUnity = allocHeapArrayAligned(T.F.getBigInt(), order+1, alignment = 64)

result.computeRootsOfUnity(generatorRootOfUnity)

proc rootOfUnityGenerator*[F](_: typedesc[F], order: int): F =
## Computes a root of unity generator for the order `n`, using
## `ω = g^( (p - 1) // n )` where `g` is the primitive root
## of the field `F`.
##
## `p` = prime of the field (or order of subgroup)
## `n` = FFT order
## Highlighted the part we compute in each comment.
# ω = g^( `(p - 1)` // n )
var exponentI {.noInit.}: BigInt[F.bits()]
exponentI = F.getModulus()
exponentI -= One
Copy link
Owner

Choose a reason for hiding this comment

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

There is a macro:

macro getPrimeMinus1*(ff: type FF): untyped =
## Get P-1
## Warning ⚠️: Result in canonical domain (not Montgomery)
result = bindConstant(ff, "PrimeMinus1")

or the Montgomery repr:

macro getMontyPrimeMinus1*(ff: type FF): untyped =
## Get (P-1)
result = bindConstant(ff, "MontyPrimeMinus1")

var exponent = F.fromBig(exponentI)

# ω = g^( (p - 1) // `n` )
var n = F.fromInt(order.uint64)
# ω = g^( (p - 1) `// n` )
n.inv()
Copy link
Owner

Choose a reason for hiding this comment

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

n is of the form 2^i hence we can actually use shifts:

func buildRootLUT(F: type Fr): array[32, F] =
## [pow(PRIMITIVE_ROOT, (MODULUS - 1) // (2**i), MODULUS) for i in range(32)]
var exponent {.noInit.}: BigInt[F.bits()]
exponent = F.getModulus()
exponent -= One
# Start by the end
var i = result.len - 1
exponent.shiftRight(i)
result[i].fromUint(BLS12_381_Fr_primitive_root)
result[i].pow_vartime(exponent)
while i > 0:
result[i-1].square(result[i])
dec i

# ω = g^( `(p - 1) // n` )
exponent *= n

var g: F = F.fromUint(primitiveRoot(F.Name).uint64)
# ω = `g^( (p - 1) // n )`
g.pow_vartime(toBig(exponent))
result = g

proc init*(T: typedesc[FFT_Descriptor], order: int): T =
## Initialize an `FFT_Descriptor` for the given `order`. The root of unity generator is
## computed automatically. However, a primitive root is required for the field over which
## the FFT is to be done. See `fft_lut.nim` for definitions and more information.
let g = rootOfUnityGenerator(T.F, order)
result = T.init(order, g)

func delete*(ctx: FFT_Descriptor) =
ctx.rootsOfUnity.freeHeapAligned()

proc toFr[S: static int, Name: static Algebra](x: BigInt[S]): Fr[Name] =
result.fromBig(x)

proc toFp[S: static int, Name: static Algebra](x: BigInt[S]): Fp[Name] =
result.fromBig(x)

proc toF[F; S: static int](T: typedesc[F], x: BigInt[S]): auto =
when T is Fr:
toFr[S, T.Name](x)
else:
toFp[S, T.Name](x)

func simpleFT[F; bits: static int](
output: var StridedView[F],
vals: StridedView[F],
rootsOfUnity: StridedView[BigInt[bits]]) =
# FFT is a recursive algorithm
# This is the base-case using a O(n²) algorithm

let L = output.len
var last {.noInit.}, v {.noInit.}: F

var v0w0 {.noinit}: F
v0w0.prod(vals[0], F.toF(rootsOfUnity[0]))

for i in 0 ..< L:
last = v0w0
for j in 1 ..< L:
v.prod(F.toF(rootsOfUnity[(i*j) mod L]), vals[j])
last.sum(last, v)
output[i] = last

func fft_internal[F; bits: static int](
output: var StridedView[F],
vals: StridedView[F],
rootsOfUnity: StridedView[BigInt[bits]]) =
if output.len <= 4:
simpleFT(output, vals, rootsOfUnity)
return

let (evenVals, oddVals) = vals.splitAlternate()
var (outLeft, outRight) = output.splitHalf()
let halfROI = rootsOfUnity.skipHalf()

fft_internal(outLeft, evenVals, halfROI)
fft_internal(outRight, oddVals, halfROI)

let half = outLeft.len
var y_times_root{.noinit.}: F

for i in 0 ..< half:
# FFT Butterfly
y_times_root.prod(F.toF(rootsOfUnity[i]), output[i+half])
output[i+half].diff(output[i], y_times_root)
output[i].sum(output[i], y_times_root)


func fft_vartime*[F](
desc: FFT_Descriptor[F],
output: var openarray[F],
vals: openarray[F]): FFT_Status =
if vals.len > desc.order:
return FFTS_TooManyValues
if not vals.len.uint64.isPowerOf2_vartime():
return FFTS_SizeNotPowerOfTwo

let rootz = desc.rootsOfUnity
.toStridedView(desc.order)
.slice(0, desc.order-1, desc.order div vals.len)

var voutput = output.toStridedView()
fft_internal(voutput, vals.toStridedView(), rootz)
return FFTS_Success

# Similar adjustments would be made for ifft_vartime

func ifft_vartime*[F](
desc: FFT_Descriptor[F],
output: var openarray[F],
vals: openarray[F]): FFT_Status =
## Inverse FFT
if vals.len > desc.order:
return FFTS_TooManyValues
if not vals.len.uint64.isPowerOf2_vartime():
return FFTS_SizeNotPowerOfTwo

let rootz = desc.rootsOfUnity
.toStridedView(desc.order+1) # Extra 1 at the end so that when reversed the buffer starts with 1
.reversed()
.slice(0, desc.order-1, desc.order div vals.len)

var voutput = output.toStridedView()
fft_internal(voutput, vals.toStridedView(), rootz)

var invLen {.noInit.}: F.getBigInt()
invLen.fromUint(vals.len.uint64)
invLen.invmod_vartime(invLen, F.getModulus())

for i in 0 ..< output.len:
let inp = output[i]
output[i].prod(inp, F.toF(invLen))

return FFTS_Success

func fft_vartime*[F](vals: openarray[F]): seq[F] =
## Performs an FFT on the given values and returns a seq of the result.
##
## For convenience only!
Copy link
Owner

Choose a reason for hiding this comment

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

Suggested change
## For convenience only!
## TODO: remove Nim allocation in core internal primitive

The allocation should be removed and left to the caller. This can be done in future PRs for:

  • FRI
  • WHIR
  • Erasure Code like PeerDAS

The reasons are multiple:

  • avoid allocation in low-level code that will have repeated calls
  • use in embedded devices
  • escape analysis
  • Nim allocated memory is hard to reclaim, Constantine has dedicated heap-alloc primitives that use the underlying malloc.

See also

# Due to the following constraints:
# - No dynamic allocation in single-threaded codepaths (for compatibility with embedded devices like TPM or secure hardware)
# - Avoiding cryptographic material in third-party libraries (like a memory allocator)
# - Giving full control of the library user on allocation strategy
# - Performance, especially for long-running processes (fragmentation, multithreaded allocation...)
#
# stack allocation is strongly preferred where necessary.

let order = nextPowerOfTwo_vartime(vals.len.uint64)
var fftDesc = FFTDescriptor[F].init(order.int)
defer: fftDesc.delete()

result = newSeq[F](order)
let status = fftDesc.fft_vartime(result, vals)

doAssert (status == FFTS_Success).bool, "FFT failed with " & $status

proc ifft_vartime*[F](vals: openarray[F]): seq[F] =
## Performs an inverse FFT on the given values and returns a seq of the result.
##
## For convenience only!
Copy link
Owner

Choose a reason for hiding this comment

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

ditto

let order = nextPowerOfTwo_vartime(vals.len.uint64)
var fftDesc = FFTDescriptor[F].init(order.int)
defer: fftDesc.delete()

result = newSeq[F](order)
let status = fftDesc.ifft_vartime(result, vals)

doAssert (status == FFTS_Success).bool, "FFT failed with " & $status
Loading
Loading