mirror of
https://github.com/logos-storage/rln-fast.git
synced 2026-04-17 03:23:08 +00:00
initial version of the test input generator (generating inputs for the RLN circuit)
This commit is contained in:
parent
c671b860fe
commit
91d11cda21
49
README.md
49
README.md
@ -13,15 +13,59 @@ Our [`circom`](https://docs.circom.io/) circuit is very similar to the one in [t
|
||||
|
||||
TODO after the implementation :)
|
||||
|
||||
We use the default circuit parameters:
|
||||
|
||||
- `LIMIT_BITS = 16` (max 65536 messages per epoch per user)
|
||||
- `MERKLE_DEPTH = 20` (max 1 million registered users)
|
||||
|
||||
Circuit sizes
|
||||
|
||||
- witness size = 5637
|
||||
- unchanging = 5123
|
||||
- remaining = 514
|
||||
|
||||
So we can see that only less than 10% of the circuit is changing at every proof generation, the rest is changing when a new user registers (and thus the Merkle tree changes).
|
||||
|
||||
### Differences from the PSE circuit
|
||||
|
||||
Note that we are not generic in the curve/field choice, requiring BN254 curve. This is only a limitation of the implementation of this PoC, it would work exactly the same with eg. BLS12-381.
|
||||
Note that we are not generic in the curve/field choice, requiring the BN254 curve. This is only a limitation of the implementation of this PoC, it would work exactly the same with eg. BLS12-381.
|
||||
|
||||
Actual differences:
|
||||
|
||||
- we use Poseidon2 hash instead of `circomlib`'s Poseidon
|
||||
- we only use the Poseidon2 permutation with fixed width `t=3` (for simplicity)
|
||||
- when computing `a1`, we use the formula `a1 := H(sk+j|ext)` instead of `H(sk|ext|j)`, as this results in one less hash invocation (but shouldn't really cause any difference)
|
||||
- when computing `a1`, we use the formula `a1 := H(sk+j|ext)` instead of `H(sk|ext|j)`, as this results in one less `t=3` hash invocation (but this shouldn't really cause any issues, as`sk` is authenticated and `j` is range checked)
|
||||
- the Merkle root is an input, not an output (you won't forget to check out externally)
|
||||
- we input the Merkle leaf index directly, not as bits (you need check them to be bits anyway, so this is essentially free; and somewhat simpler to use)
|
||||
- no external dependencies
|
||||
|
||||
Remarks:
|
||||
|
||||
1. if one feels "uneasy" with `sk+j` in `a1`, an alternative while still keeping `t=3` would be to use `a1 = H(H(sk|ext)|j)`. This is more expensive (an extra hash invocation), but the inner hash `H(sk|ext)` only changes once per epoch. So technically one could do a three-layered computation: Precompute most of the things when the Merkle root changes; precompute this one hash at the start of epoch; and finish at each message. We don't implement this 3 layers here, as it would add a lot of complexity.
|
||||
2. the computation `local_null = H(a1)` could be optimized by using a `t=2` Poseidon instance (instead of `t=3`). We don't do that here because the lack of pre-made such Poseidon2 instance.
|
||||
|
||||
### Circuit I/O
|
||||
|
||||
Public inputs:
|
||||
|
||||
- `"merkle_root"` (rarely changes)
|
||||
- `"ext_null"` (changes once per epoch)
|
||||
- `"msg_hash"` (changes at each message)
|
||||
|
||||
Private inputs:
|
||||
|
||||
- `"secret_key"` (never changes)
|
||||
- `"msg_limit"` (never changes)
|
||||
- `"leaf_idx"` (normally never changes, though I guess in theory the registry could "garbage collect" once a while)
|
||||
- `"merkle_path"` (changes only when the Merkle root changes)
|
||||
- `"msg_idx"` (changes at each message)
|
||||
|
||||
Public outputs:
|
||||
|
||||
- `"y_value"`
|
||||
- `"local_null"`
|
||||
|
||||
We want to precalculate the part of the witness which only depends on the rarely changing inputs, including `"merkle_path"`.
|
||||
|
||||
### Partial witness generation
|
||||
|
||||
@ -58,4 +102,3 @@ where $\mathcal{F}\subset[1\dots M]$ is the set of witness indices which are unc
|
||||
|
||||
The only other significant computation is computing the quotient polynomial; that's usually done with FFT. Some part of that can be partially precomputed, but probably won't give a significant speedup.
|
||||
|
||||
|
||||
|
||||
@ -34,6 +34,9 @@ template RangeCheck(limit_bits) {
|
||||
signal input what;
|
||||
signal input limit;
|
||||
|
||||
assert( limit > 0 );
|
||||
assert( limit <= 2**limit_bits );
|
||||
|
||||
_ <== ToBits(limit_bits)( what ); // 0 <= what < 2^limit_bits
|
||||
_ <== ToBits(limit_bits)( limit - 1 - what ); // 0 <= limit - 1 - what < 2^limit_bits
|
||||
|
||||
|
||||
@ -8,9 +8,9 @@ include "misc.circom";
|
||||
template RLN(limit_bits, merkle_depth) {
|
||||
|
||||
// public input
|
||||
signal input msg_hash; // x = hash of the message
|
||||
signal input ext_null; // the external "nullifier" ext = H(protocol|epoch)
|
||||
signal input merkle_root; // the Merkle root we check against
|
||||
signal input ext_null; // the external "nullifier" ext = H(protocol|epoch)
|
||||
signal input msg_hash; // x = hash of the message
|
||||
|
||||
// private input
|
||||
signal input secret_key; // the user's secret key
|
||||
@ -24,11 +24,10 @@ template RLN(limit_bits, merkle_depth) {
|
||||
signal output local_null; // the "epoch-local nullifier" null = H(a1) (to detect repeated a1)
|
||||
|
||||
// computations
|
||||
signal pk <== Compress()( secret_key , msg_limit ); // public key - this doesn't ever change
|
||||
signal pre_a1 <== Compress()( secret_key , ext_null ); // H(sk|ext) - this doesn't change often
|
||||
signal a1 <== Compress()( pre_a1 , msg_idx ); // a1 = H(H(sk|ext)|j)
|
||||
local_null <== Compress()( a1 , 0 ); // H(a1);
|
||||
y_value <== secret_key + msg_hash * a1; // y = sk + x*a1
|
||||
signal pk <== Compress()( secret_key , msg_limit ); // public key - this doesn't ever change
|
||||
signal a1 <== Compress()( secret_key + msg_idx , ext_null ); // a1 = Hs(sk+j|ext)
|
||||
local_null <== Compress()( a1 , 0 ); // H(a1);
|
||||
y_value <== secret_key + msg_hash * a1; // y = sk + x*a1
|
||||
|
||||
// checks
|
||||
RangeCheck (limit_bits) ( msg_idx , msg_limit ); // range check for the message index
|
||||
|
||||
4
test-input/.gitignore
vendored
Normal file
4
test-input/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
.DS_Store
|
||||
cli
|
||||
*.circom
|
||||
*.json
|
||||
12
test-input/README.md
Normal file
12
test-input/README.md
Normal file
@ -0,0 +1,12 @@
|
||||
|
||||
create test input data for `rln-fast`
|
||||
-------------------------------------
|
||||
|
||||
Quickstart:
|
||||
|
||||
$ nimble build cli
|
||||
$ ./cli --help
|
||||
|
||||
Example:
|
||||
|
||||
$ ./cli -v --merkle_depth=18 --limit_bits=12 --circom=main.circom --output=input.json
|
||||
174
test-input/src/cli.nim
Normal file
174
test-input/src/cli.nim
Normal file
@ -0,0 +1,174 @@
|
||||
|
||||
{. warning[UnusedImport]:off .}
|
||||
|
||||
import sugar
|
||||
import std/strutils
|
||||
import std/sequtils
|
||||
import std/os
|
||||
import std/parseopt
|
||||
|
||||
import std/random
|
||||
|
||||
import types
|
||||
import json/bn254
|
||||
import gen_inputs
|
||||
import misc
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
type FullConfig = object
|
||||
globCfg: GlobalConfig
|
||||
seed: int64
|
||||
outFile: string
|
||||
circomFile: string
|
||||
partial: bool
|
||||
verbose: bool
|
||||
how_many: int
|
||||
|
||||
const defaultFullCfg =
|
||||
FullConfig( globCfg: defaultGlobalConfig
|
||||
, seed: 0
|
||||
, outFile: ""
|
||||
, circomFile: ""
|
||||
, partial: false
|
||||
, verbose: false
|
||||
, how_many: 1
|
||||
)
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
proc printHelp() =
|
||||
echo "usage:"
|
||||
echo "$ ./cli [options] --output=proof_input.json --circom=proof_main.circom"
|
||||
echo ""
|
||||
echo "available options:"
|
||||
echo " -h, --help : print this help"
|
||||
echo " -v, --verbose : verbose output (print the actual parameters)"
|
||||
echo " -d, --merkle_depth = <depth> : Merkle tree depth (default: 20)"
|
||||
echo " -b, --limit_bits = <bits> : log2 of maximum number of messages per epoch (default: 16)"
|
||||
echo " -s, --seed = <seed> : seed to generate the fake data (eg. 12345; default: random)"
|
||||
echo " -o, --output = <input.json> : the JSON file into which we write the proof inputs"
|
||||
echo " -c, --circom = <main.circom> : the circom main component to create with these parameters"
|
||||
echo " -n, --count = <K> : generate K proof inputs at the same time (instead of 1)"
|
||||
echo " -p, --partial : generate partial input"
|
||||
echo ""
|
||||
|
||||
quit()
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
proc parseCliOptions(): FullConfig =
|
||||
|
||||
var globCfg = defaultGlobalConfig
|
||||
var fullCfg = defaultFullCfg
|
||||
|
||||
# randomize the seed
|
||||
fullCfg.seed = rand(int64)
|
||||
|
||||
var argCtr: int = 0
|
||||
for kind, key, value in getOpt():
|
||||
case kind
|
||||
|
||||
# Positional arguments
|
||||
of cmdArgument:
|
||||
# echo ("arg #" & $argCtr & " = " & key)
|
||||
argCtr += 1
|
||||
|
||||
# Switches
|
||||
of cmdLongOption, cmdShortOption:
|
||||
case key
|
||||
|
||||
of "h", "help" : printHelp()
|
||||
of "v", "verbose" : fullCfg.verbose = true
|
||||
of "d", "merkle_depth" : globCfg.merkle_depth = parseInt(value)
|
||||
of "b", "limit_bits" : globCfg.limit_bits = parseInt(value)
|
||||
of "s", "seed" : fullCfg.seed = int64(parseInt(value))
|
||||
of "o", "output" : fullCfg.outFile = value
|
||||
of "c", "circom" : fullCfg.circomFile = value
|
||||
of "p", "partial" : fullCfg.partial = true
|
||||
of "n", "count" : fullCfg.how_many = parseInt(value)
|
||||
else:
|
||||
echo "Unknown option: ", key
|
||||
echo "use --help to get a list of options"
|
||||
quit()
|
||||
|
||||
of cmdEnd:
|
||||
discard
|
||||
|
||||
fullCfg.globCfg = globCfg
|
||||
|
||||
assert( globCfg.merkle_depth >= 2 and globCfg.merkle_depth <= 22 )
|
||||
assert( globCfg.limit_bits >= 2 and globCfg.limit_bits <= 32 )
|
||||
assert( fullCfg.how_many >= 1 and fullCfg.how_many <= 100 )
|
||||
|
||||
return fullCfg
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
proc printConfig(fullCfg: FullConfig) =
|
||||
|
||||
let globCfg = fullCfg.globCfg
|
||||
|
||||
# echo "field = BN254"
|
||||
# echo "hash function = Poseidon2"
|
||||
echo "merkle_depth = " & ($globCfg.merkle_depth)
|
||||
echo "limit_bits = " & ($globCfg.limit_bits)
|
||||
echo "random seed = " & ($fullCfg.seed)
|
||||
echo "partial = " & ($fullCfg.partial)
|
||||
echo "how many inputs = " & ($fullCfg.how_many)
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
proc writeCircomMainComponent(fullCfg: FullConfig, fname: string) =
|
||||
|
||||
let params: (int,int) =
|
||||
( fullCfg.globCfg.limit_bits
|
||||
, fullCfg.globCfg.merkle_depth
|
||||
)
|
||||
|
||||
let f = open(fname, fmWrite)
|
||||
defer: f.close()
|
||||
|
||||
f.writeLine("pragma circom 2.1.1;")
|
||||
f.writeLine("include \"rln_proof.circom\";")
|
||||
f.writeLine("// argument order convention: RLN(limit_bits, merkle_depth)")
|
||||
f.writeLine("component main {public [msg_hash, ext_null, merkle_root]} = RLN" & ($params) & ";")
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
when isMainModule:
|
||||
|
||||
randomize()
|
||||
|
||||
let fullCfg = parseCliOptions()
|
||||
let globCfg = fullCfg.globCfg
|
||||
|
||||
# now use the set seed
|
||||
randomize(fullCfg.seed)
|
||||
|
||||
if fullCfg.verbose:
|
||||
printConfig(fullCfg)
|
||||
|
||||
if fullCfg.circomFile == "" and fullCfg.outFile == "":
|
||||
echo "nothing to do!"
|
||||
echo "use --help for getting a list of options"
|
||||
quit()
|
||||
|
||||
if fullCfg.circomFile != "":
|
||||
let fname = fullCfg.circomFile
|
||||
echo "writing circom main component into `" & fname & "`"
|
||||
writeCircomMainComponent(fullCfg, fname)
|
||||
|
||||
if fullCfg.outFile != "":
|
||||
let fname = fullCfg.outFile
|
||||
let secretTree = genTreeWithSecrets( globCfg )
|
||||
let prfInput = genProofInput( globCfg , secretTree )
|
||||
if fullCfg.partial:
|
||||
echo "writing partial proof input into `" & fname & "`..."
|
||||
let partial = extractPartialInputs( prfInput )
|
||||
exportPartialInput( fname, partial )
|
||||
else:
|
||||
echo "writing full proof input into `" & fname & "`..."
|
||||
exportProofInput( fname, prfInput )
|
||||
|
||||
echo "done"
|
||||
108
test-input/src/gen_inputs.nim
Normal file
108
test-input/src/gen_inputs.nim
Normal file
@ -0,0 +1,108 @@
|
||||
|
||||
import std/random
|
||||
|
||||
import poseidon2/compress
|
||||
|
||||
import ./types
|
||||
import ./misc
|
||||
import ./merkle
|
||||
import ./simulate
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
type
|
||||
|
||||
Leaf* = object
|
||||
secret_key* : F
|
||||
public_key* : F
|
||||
msg_limit* : int
|
||||
|
||||
TreeWithSecrets* = object
|
||||
leaves* : seq[Leaf]
|
||||
tree* : MerkleTree
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
proc genTreeWithSecrets*(globCfg: GlobalConfig): TreeWithSecrets =
|
||||
|
||||
let N : int = pow2(globCfg.merkle_depth)
|
||||
let M : int = pow2(globCfg.limit_bits )
|
||||
|
||||
let maxMsgLimit = M - 1
|
||||
|
||||
var public_keys : seq[F] = newSeq[F ]( N )
|
||||
var leaves : seq[Leaf] = newSeq[Leaf]( N )
|
||||
|
||||
for i in 0..<N:
|
||||
let secret_key = randomF()
|
||||
let msg_limit = min( 1 , rand(M) )
|
||||
let public_key = compress( secret_key, intToBN254(msg_limit) );
|
||||
|
||||
assert( msg_limit <= M , "msg limit hard bound failed")
|
||||
|
||||
let leaf = Leaf( secret_key : secret_key
|
||||
, public_key : public_key
|
||||
, msg_limit : msg_limit
|
||||
)
|
||||
|
||||
public_keys[i] = public_key
|
||||
leaves[i] = leaf
|
||||
|
||||
let tree = buildMerkleTree( public_keys )
|
||||
|
||||
return TreeWithSecrets(leaves: leaves, tree: tree)
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
proc genProofInput*( globCfg: GlobalConfig, secretTree: TreeWithSecrets): ProofInput =
|
||||
|
||||
let tree = secretTree.tree
|
||||
|
||||
let N : int = pow2(globCfg.merkle_depth)
|
||||
let maxLeafIdx : int = N - 1
|
||||
let leaf_idx : int = rand(maxLeafIdx)
|
||||
|
||||
let leaf = secretTree.leaves[leaf_idx]
|
||||
|
||||
let msg_limit : int = leaf.msg_limit
|
||||
let msg_idx : int = rand( msg_limit - 1 )
|
||||
|
||||
assert( leaf_idx < N )
|
||||
assert( msg_idx < msg_limit )
|
||||
|
||||
let ext_null = randomF()
|
||||
let msg_hash = randomF()
|
||||
|
||||
let merkle_proof = getMerkleInclusionProof(tree, leaf_idx)
|
||||
let merkle_root = extractMerkleRoot(tree)
|
||||
let merkle_path = merkle_proof.merklePath
|
||||
|
||||
assert( checkMerkleInclusionProof( merkle_root , merkle_proof ) , "merkle inclusion proof sanity check failed" )
|
||||
|
||||
let proof_inputs = ProofInput(
|
||||
# public inputs
|
||||
merkle_root : merkle_root
|
||||
, ext_null : ext_null
|
||||
, msg_hash : msg_hash
|
||||
# private inputs
|
||||
, secret_key : leaf.secret_key
|
||||
, msg_limit : msg_limit
|
||||
, leaf_idx : leaf_idx
|
||||
, merkle_path : merkle_path
|
||||
, msg_idx : msg_idx
|
||||
)
|
||||
|
||||
let ok = sanityCheckRLNProof( globCfg, proof_inputs, false )
|
||||
assert( ok , "RLN proof sanity check failed" )
|
||||
|
||||
return proof_inputs
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
proc genManyProofInputs*( globCfg: GlobalConfig, secretTree: TreeWithSecrets, K: int): seq[ProofInput] =
|
||||
var inputs : seq[ProofInput] = newSeq[ProofInput]( K )
|
||||
for i in 0..<K:
|
||||
inputs[i] = genProofInput( globCfg, secretTree )
|
||||
return inputs
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
66
test-input/src/json/bn254.nim
Normal file
66
test-input/src/json/bn254.nim
Normal file
@ -0,0 +1,66 @@
|
||||
|
||||
#
|
||||
# export the proof inputs as a JSON file suitable for `snarkjs`
|
||||
#
|
||||
|
||||
import std/streams
|
||||
|
||||
import ../types
|
||||
import shared
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
proc writeFieldElems(h: Stream, prefix: string, xs: seq[F]) =
|
||||
writeList[F]( h, prefix, xs, writeLnF )
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
proc writeSingleMerklePath(h: Stream, prefix: string, path: seq[F]) =
|
||||
writeFieldElems(h, prefix, path)
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
#[
|
||||
// public input
|
||||
signal input merkle_root; // the Merkle root we check against
|
||||
signal input ext_null; // the external "nullifier" ext = H(protocol|epoch)
|
||||
signal input msg_hash; // x = hash of the message
|
||||
|
||||
// private input
|
||||
signal input secret_key; // the user's secret key
|
||||
signal input msg_limit; // message limit per epoch
|
||||
signal input msg_idx; // the message index (should be 0 <= msg_idx < msg_limit)
|
||||
signal input leaf_idx; // leaf index in the Merkle tree
|
||||
signal input merkle_path[merkle_depth]; // the Merkle inclusion proof
|
||||
]#
|
||||
|
||||
proc exportProofInput*(fname: string, prfInput: ProofInput) =
|
||||
let h = openFileStream(fname, fmWrite)
|
||||
defer: h.close()
|
||||
|
||||
h.writeLine("{")
|
||||
h.writeLine(" \"merkle_root\": " & toQuotedDecimalF(prfInput.merkle_root) )
|
||||
h.writeLine(", \"ext_null\": " & toQuotedDecimalF(prfInput.ext_null ) )
|
||||
h.writeLine(", \"msg_hash\": " & toQuotedDecimalF(prfInput.msg_hash ) )
|
||||
h.writeLine(", \"secret_key\": " & toQuotedDecimalF(prfInput.secret_key ) )
|
||||
h.writeLine(", \"msg_limit\": " & toQuotedDecimalF(intToBN254(prfInput.msg_limit)) )
|
||||
h.writeLine(", \"msg_idx\": " & toQuotedDecimalF(intToBN254(prfInput.msg_idx )) )
|
||||
h.writeLine(", \"leaf_idx\": " & toQuotedDecimalF(intToBN254(prfInput.leaf_idx )) )
|
||||
h.writeLine(", \"merkle_path\":")
|
||||
writeSingleMerklePath(h, " ", prfInput.merkle_path )
|
||||
h.writeLine("}")
|
||||
|
||||
proc exportPartialInput*(fname: string, prfInput: PartialInput) =
|
||||
let h = openFileStream(fname, fmWrite)
|
||||
defer: h.close()
|
||||
|
||||
h.writeLine("{")
|
||||
h.writeLine(" \"merkle_root\": " & toQuotedDecimalF(prfInput.merkle_root) )
|
||||
h.writeLine(", \"secret_key\": " & toQuotedDecimalF(prfInput.secret_key ) )
|
||||
h.writeLine(", \"msg_limit\": " & toQuotedDecimalF(intToBN254(prfInput.msg_limit)) )
|
||||
h.writeLine(", \"leaf_idx\": " & toQuotedDecimalF(intToBN254(prfInput.leaf_idx )) )
|
||||
h.writeLine(", \"merkle_path\":")
|
||||
writeSingleMerklePath(h, " ", prfInput.merkle_path )
|
||||
h.writeLine("}")
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
48
test-input/src/json/shared.nim
Normal file
48
test-input/src/json/shared.nim
Normal file
@ -0,0 +1,48 @@
|
||||
|
||||
import std/strutils
|
||||
import std/streams
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
func mkIndent*(foo: string): string =
|
||||
return spaces(foo.len)
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
type
|
||||
WriteFun*[T] = proc (stream: Stream, prefix: string, what: T) {.closure.}
|
||||
|
||||
proc writeList*[T](h: Stream, prefix: string, xs: seq[T], writeFun: WriteFun[T]) =
|
||||
let n = xs.len
|
||||
let indent = mkIndent(prefix)
|
||||
for i in 0..<n:
|
||||
if i==0:
|
||||
writeFun(h, prefix & "[ ", xs[i])
|
||||
else:
|
||||
writeFun(h, indent & ", ", xs[i])
|
||||
h.writeLine( indent & "]" )
|
||||
|
||||
#---------------------------------------
|
||||
|
||||
proc writeListList*[T](h: Stream, prefix: string, xs: seq[seq[T]], writeFun: WriteFun[T]) =
|
||||
let n = xs.len
|
||||
let indent = mkIndent(prefix)
|
||||
for i in 0..<n:
|
||||
|
||||
if i==0:
|
||||
h.write( prefix & "[ ")
|
||||
else:
|
||||
h.write( indent & ", ")
|
||||
|
||||
let ys = xs[i]
|
||||
let m = ys.len
|
||||
for j in 0..<m:
|
||||
if j==0:
|
||||
writeFun(h, "" , ys[j])
|
||||
else:
|
||||
writeFun(h, " , ", ys[j])
|
||||
|
||||
h.write("\n")
|
||||
h.writeLine( indent & "]" )
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
115
test-input/src/merkle.nim
Normal file
115
test-input/src/merkle.nim
Normal file
@ -0,0 +1,115 @@
|
||||
|
||||
import poseidon2/compress
|
||||
|
||||
import ./types
|
||||
import ./misc
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
type
|
||||
|
||||
MerkleProof* = object
|
||||
leafIndex* : int # linear index of the leaf, starting from 0
|
||||
leafHash* : F # (hash of the value of the leaf
|
||||
merklePath* : seq[F] # order: from the bottom to the top
|
||||
depth* : int # depth of the tree
|
||||
|
||||
# note: the first layer is the bottom layer, and the last layer is the root
|
||||
MerkleTree* = object
|
||||
layers* : seq[seq[F]]
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
func merkleTreeDepth*(tree: MerkleTree): int = tree.layers.len - 1
|
||||
|
||||
func extractMerkleRoot*(tree: MerkleTree): Hash =
|
||||
let n = tree.layers.len
|
||||
let xs = tree.layers[n-1]
|
||||
assert( xs.len == 1 , "merkle root is not a singleton" )
|
||||
return xs[0]
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
proc getMerkleInclusionProof*(tree: MerkleTree, leaf_idx: int): MerkleProof =
|
||||
|
||||
let N = tree.layers[0].len
|
||||
let D = merkleTreeDepth( tree )
|
||||
|
||||
assert( pow2(D) == N , "not a full binary tree or depth/size inconsistency" )
|
||||
|
||||
assert( 0 <= leaf_idx and leaf_idx < N , "leaf index out of bounds")
|
||||
|
||||
var idx : int = leaf_idx
|
||||
var path : seq[F] = newSeq[F]( D )
|
||||
for k in 0..<D:
|
||||
let is_even = isEven(idx)
|
||||
let j = (idx shr 1)
|
||||
if is_even:
|
||||
path[k] = tree.layers[k][2*j+1]
|
||||
else:
|
||||
path[k] = tree.layers[k][2*j]
|
||||
|
||||
idx = j
|
||||
|
||||
return MerkleProof( leafIndex : leaf_idx
|
||||
, leafHash : tree.layers[0][leaf_idx]
|
||||
, merklePath : path
|
||||
, depth : D
|
||||
)
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
func checkMerkleInclusionProof*(expected_root: F, proof: MerkleProof): bool =
|
||||
|
||||
let path: seq[F] = proof.merklePath
|
||||
var this: F = proof.leafHash
|
||||
var idx: int = proof.leafIndex
|
||||
for i in 0..<proof.depth:
|
||||
var next : F
|
||||
if isEven(idx):
|
||||
next = compress( this , path[i] )
|
||||
else:
|
||||
next = compress( path[i] , this )
|
||||
idx = (idx shr 1)
|
||||
this = next
|
||||
|
||||
return (expected_root == this)
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
proc buildMerkleTree*( leaf_hashes: seq[F] ): MerkleTree =
|
||||
|
||||
let zeroF : F = intToBN254(0)
|
||||
|
||||
let depth : int = ceilingLog2( leaf_hashes.len )
|
||||
|
||||
var layers : seq[seq[F]] = newSeq[seq[F]](depth+1)
|
||||
layers[0] = leaf_hashes
|
||||
|
||||
var N : int = leaf_hashes.len
|
||||
var prev : seq[F] = leaf_hashes
|
||||
var i : int = 0;
|
||||
|
||||
while( N > 1 ):
|
||||
i += 1
|
||||
|
||||
let H = (N+1) div 2
|
||||
let is_even = (H+H == N)
|
||||
|
||||
var next: seq[F] = newSeq[F](H)
|
||||
for k in 0..<H:
|
||||
let j = 2*k
|
||||
if j+1 < N:
|
||||
next[k] = compress( prev[j] , prev[j+1] )
|
||||
else:
|
||||
next[k] = compress( prev[j] , zeroF )
|
||||
|
||||
layers[i] = next
|
||||
prev = next
|
||||
N = H
|
||||
|
||||
return MerkleTree(layers: layers)
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
48
test-input/src/misc.nim
Normal file
48
test-input/src/misc.nim
Normal file
@ -0,0 +1,48 @@
|
||||
|
||||
#
|
||||
# helper functions
|
||||
#
|
||||
|
||||
import std/math
|
||||
import std/bitops
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
func isEven*(x : int): bool =
|
||||
let y = bitand[int]( x , 1 )
|
||||
return (y == 0)
|
||||
|
||||
func isOdd*(x : int): bool =
|
||||
let y = bitand[int]( x , 1 )
|
||||
return (y != 0)
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
func floorLog2* (x: int): int =
|
||||
var k = -1
|
||||
var y = x
|
||||
while (y > 0):
|
||||
k += 1
|
||||
y = y shr 1
|
||||
return k
|
||||
|
||||
func ceilingLog2* (x: int): int =
|
||||
if (x==0):
|
||||
return -1
|
||||
else:
|
||||
return (floorLog2(x-1) + 1)
|
||||
|
||||
func exactLog2*(x: int): int =
|
||||
let k = ceilingLog2(x)
|
||||
assert( x == 2^k, "exactLog2: not a power of two" )
|
||||
return k
|
||||
|
||||
func checkPowerOfTwo*(x: int , what: string): int =
|
||||
let k = ceilingLog2(x)
|
||||
assert( x == 2^k, ("`" & what & "` is expected to be a power of 2") )
|
||||
return x
|
||||
|
||||
# wtf Nim, serously
|
||||
func pow2*(k: int): int = 2^k
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
60
test-input/src/simulate.nim
Normal file
60
test-input/src/simulate.nim
Normal file
@ -0,0 +1,60 @@
|
||||
|
||||
# simulate to RLN proof for sanity checking
|
||||
|
||||
import std/options
|
||||
|
||||
import poseidon2/types
|
||||
import poseidon2/compress
|
||||
|
||||
import ./types
|
||||
import ./merkle
|
||||
import ./misc
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
type
|
||||
|
||||
ProofOutput* = object
|
||||
y_value* : F
|
||||
local_null* : F
|
||||
|
||||
proc simulateRLNProof*( globCfg: GlobalConfig, inp: ProofInput, verbose: bool): Option[ProofOutput] =
|
||||
|
||||
if inp.msg_limit < 1 or inp.msg_limit > pow2(globCfg.limit_bits):
|
||||
if verbose:
|
||||
echo "rln proof: FATAL: message upper bound not fitting into limit_bits"
|
||||
return none(ProofOutput)
|
||||
|
||||
if inp.msg_idx < 0 or inp.msg_idx >= inp.msg_limit:
|
||||
if verbose:
|
||||
echo "rln proof: message index range check failed"
|
||||
return none(ProofOutput)
|
||||
|
||||
let public_key : F = compress( inp.secret_key , intToBN254(inp.msg_limit) )
|
||||
let sk_plus_j : F = addF( inp.secret_key , intToBN254(inp.msg_idx) )
|
||||
let a1 : F = compress( sk_plus_j , inp.ext_null )
|
||||
let y_value : F = addF( inp.secret_key , mulf( inp.msg_hash , a1 ) )
|
||||
let local_null : F = compress( a1 , intToBN254(0) )
|
||||
|
||||
let merkle_proof = MerkleProof(
|
||||
leafIndex : inp.leaf_idx
|
||||
, leafHash : public_key
|
||||
, merklePath : inp.merkle_path
|
||||
, depth : globCfg.merkle_depth
|
||||
)
|
||||
|
||||
if not checkMerkleInclusionProof( inp.merkle_root , merkle_proof ):
|
||||
if verbose:
|
||||
echo "rln proof: Merkle inclusion check failed"
|
||||
return none(ProofOutput)
|
||||
|
||||
if verbose:
|
||||
echo "rln proof sanity checked OK."
|
||||
|
||||
return some( ProofOutput( y_value: y_value , local_null: local_null ) )
|
||||
|
||||
|
||||
proc sanityCheckRLNProof*( globCfg: GlobalConfig, proof_inputs: ProofInput , verbose: bool ): bool =
|
||||
isSome( simulateRLNProof( globCfg , proof_inputs , verbose ) )
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
99
test-input/src/types.nim
Normal file
99
test-input/src/types.nim
Normal file
@ -0,0 +1,99 @@
|
||||
|
||||
import std/strutils
|
||||
import std/streams
|
||||
import std/random
|
||||
|
||||
import
|
||||
constantine/math/arithmetic,
|
||||
constantine/math/io/io_fields,
|
||||
constantine/math/io/io_bigints,
|
||||
constantine/named/algebras
|
||||
|
||||
#from constantine/math/io/io_fields import toDecimal
|
||||
|
||||
import poseidon2/types
|
||||
import poseidon2/io
|
||||
export types
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
type BN254_T* = F
|
||||
type Entropy* = F
|
||||
type Hash* = F
|
||||
type Root* = Hash
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
type
|
||||
|
||||
Seed* = uint64
|
||||
|
||||
GlobalConfig* = object
|
||||
limit_bits* : int
|
||||
merkle_depth* : int
|
||||
|
||||
ProofInput* = object
|
||||
# public inputs
|
||||
merkle_root* : F
|
||||
ext_null* : F
|
||||
msg_hash* : F
|
||||
# private inputs
|
||||
secret_key* : F
|
||||
msg_limit* : int
|
||||
leaf_idx* : int
|
||||
merkle_path* : seq[F]
|
||||
msg_idx* : int
|
||||
|
||||
PartialInput* = object
|
||||
merkle_root* : F
|
||||
secret_key* : F
|
||||
msg_limit* : int
|
||||
leaf_idx* : int
|
||||
merkle_path* : seq[F]
|
||||
|
||||
const defaultGlobalConfig* =
|
||||
GlobalConfig( limit_bits: 16 , merkle_depth: 20 )
|
||||
|
||||
func extractPartialInputs*(input: ProofInput): PartialInput =
|
||||
PartialInput( merkle_root : input.merkle_root
|
||||
, secret_key : input.secret_key
|
||||
, msg_limit : input.msg_limit
|
||||
, leaf_idx : input.leaf_idx
|
||||
, merkle_path : input.merkle_path
|
||||
)
|
||||
|
||||
#-----------------------------------------------
|
||||
|
||||
func intToBN254*(x: int): F = toF(x)
|
||||
|
||||
func uint64ToBN254*(x: uint64): F = toF(x)
|
||||
|
||||
func toDecimalF*(a : F): string =
|
||||
var s : string = toDecimal(a)
|
||||
s = s.strip( leading=true, trailing=false, chars={'0'} )
|
||||
if s.len == 0: s="0"
|
||||
return s
|
||||
|
||||
func toQuotedDecimalF*(x: F): string =
|
||||
let s : string = toDecimalF(x)
|
||||
return ("\"" & s & "\"")
|
||||
|
||||
func addF*(x, y: F): F = x + y
|
||||
func mulF*(x, y: F): F = x * y
|
||||
|
||||
# this is stupid, but it doesn't really matter here
|
||||
proc randomF*(): F =
|
||||
let mult : F = F.fromHex("0x0000000000000000000000000000000000000000000000010000000000000000")
|
||||
let a : F = uint64ToBN254(rand(uint64))
|
||||
let b : F = a * mult + uint64ToBN254(rand(uint64))
|
||||
let c : F = b * mult + uint64ToBN254(rand(uint64))
|
||||
let d : F = c * mult + uint64ToBN254(rand(uint64))
|
||||
return d
|
||||
|
||||
proc writeLnF*(h: Stream, prefix: string, x: F) =
|
||||
h.writeLine(prefix & toQuotedDecimalF(x))
|
||||
|
||||
proc writeF*(h: Stream, prefix: string, x: F) =
|
||||
h.write(prefix & toQuotedDecimalF(x))
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
11
test-input/test_input.nimble
Normal file
11
test-input/test_input.nimble
Normal file
@ -0,0 +1,11 @@
|
||||
|
||||
version = "0.0.0"
|
||||
author = "Balazs Komuves"
|
||||
description = "create test inputs for the RLN circuit"
|
||||
license = "MIT or Apache-2.0"
|
||||
srcDir = "src"
|
||||
bin = @["cli"]
|
||||
|
||||
requires "nim >= 2.0.0"
|
||||
requires "https://github.com/mratsim/constantine#bc3845aa492b52f7fef047503b1592e830d1a774"
|
||||
requires "https://github.com/logos-storage/nim-poseidon2#7749c368a9302167f94bd0133fb881cb83392caf"
|
||||
@ -22,19 +22,18 @@ To have an overview of what all the different steps and files are, see [PROOFS.m
|
||||
- furthermore install `circom-witnesscalc`: <https://github.com/iden3/circom-witnesscalc/> (note: we need the legacy `build-circuit` version!)
|
||||
- install Nim: <https://nim-lang.org/>
|
||||
|
||||
TODO: fix this:
|
||||
Build the Nim cli proof input generator:
|
||||
|
||||
$ cd ../reference/nim/proof_input/
|
||||
$ cd ../test-input
|
||||
$ nimble build -d:release cli
|
||||
$ cd ../../../workflow
|
||||
$ cd ../workflow
|
||||
|
||||
### Powers of tau setup
|
||||
|
||||
Either download a ready-to-use "powers of tau" setup file (section 7), or generate one
|
||||
youself using `snarkjs` (sections 1..7), see the README here: <https://github.com/iden3/snarkjs>
|
||||
|
||||
Size `2^13` (file size about 10MB) should be big enough:
|
||||
Size `2^13 = 8192` (file size about 10MB) should be big enough:
|
||||
|
||||
$ cd ../ceremony
|
||||
$ wget https://storage.googleapis.com/zkevm/ptau/powersOfTau28_hez_final_13.ptau
|
||||
|
||||
@ -5,8 +5,7 @@ MY_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
source ${MY_DIR}/params.sh
|
||||
|
||||
CLI_ARGS="--merkle_depth=${MERKLE_DEPTH} \
|
||||
--limit_bits=${LIMIT_BITS} \
|
||||
--seed=$SEED"
|
||||
--limit_bits=${LIMIT_BITS}"
|
||||
|
||||
if [[ "$1" == "--export" ]]
|
||||
then
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
MERKLE_DEPTH=20 # depth of the Merkle tree
|
||||
LIMIT_BITS=16 # log2 of the maximal possible rate limit per epoch
|
||||
MERKLE_DEPTH=10 # depth of the Merkle tree
|
||||
LIMIT_BITS=6 # log2 of the maximal possible rate limit per epoch
|
||||
|
||||
SEED=1234567 # seed for creating fake data
|
||||
# SEED=1234567 # seed for creating fake data
|
||||
|
||||
@ -2,11 +2,12 @@
|
||||
|
||||
ORIG=`pwd`
|
||||
|
||||
NIMCLI_DIR="${ORIG}/../test_data/proof_input/"
|
||||
PTAU_DIR="${ORIG}/../ceremony"
|
||||
NIMCLI_DIR="${ORIG}/../test-input/"
|
||||
PTAU_DIR="${ORIG}/../ceremony/"
|
||||
|
||||
CIRCUIT_ROOT="${ORIG}/../circuit/"
|
||||
CIRCUIT_POS_DIR="${CIRCUIT_ROOT}/poseidon2/"
|
||||
CIRCUIT_INCLUDES="-l${CIRCUIT_ROOT} -l${CIRCUIT_POS_DIR}"
|
||||
|
||||
PTAU_FILE="powersOfTau28_hez_final_13.ptau"
|
||||
PTAU_PATH="${PTAU_DIR}/${PTAU_FILE}"
|
||||
|
||||
@ -12,18 +12,18 @@ cd build
|
||||
|
||||
if command -v build-circuit
|
||||
then
|
||||
CIRCUIT_INCLUDES="-l${CIRCUIT_LIB_DIR} -l${CIRCUIT_POS_DIR} -l${CIRCUIT_PRF_DIR}"
|
||||
build-circuit ${CIRCUIT_MAIN}.circom ${CIRCUIT_MAIN}_graph.bin ${CIRCUIT_INCLUDES}
|
||||
build-circuit ${CIRCUIT_MAIN}.circom ${CIRCUIT_MAIN}.graph ${CIRCUIT_INCLUDES}
|
||||
else
|
||||
echo " "
|
||||
echo "\`circom-witnesscalc\` not found; skipping graph extraction"
|
||||
echo "\`circom-witnesscalc\` (in particular \`build-circuit\') not found; skipping graph extraction"
|
||||
fi
|
||||
|
||||
# --- generate input for the circuit ---
|
||||
|
||||
echo ""
|
||||
echo "generating the input for the proof circuit..."
|
||||
${NIMCLI_DIR}/cli $CLI_ARGS -v --output=input.json
|
||||
|
||||
${NIMCLI_DIR}/cli $CLI_ARGS --output=input.json
|
||||
|
||||
# --- generate the witness ---
|
||||
|
||||
|
||||
@ -10,15 +10,13 @@ cd build
|
||||
|
||||
# --- generate the main component ---
|
||||
|
||||
### ${NIMCLI_DIR}/cli $CLI_ARGS -v --circom=${CIRCUIT_MAIN}.circom
|
||||
|
||||
cp ${CIRCUIT_ROOT}/example_main.circom ./${CIRCUIT_MAIN}.circom
|
||||
# cp ${CIRCUIT_ROOT}/example_main.circom ./${CIRCUIT_MAIN}.circom
|
||||
${NIMCLI_DIR}/cli $CLI_ARGS --circom=${CIRCUIT_MAIN}.circom
|
||||
|
||||
# --- compile the circuit ---
|
||||
|
||||
echo ""
|
||||
start=`date +%s`
|
||||
CIRCUIT_INCLUDES="-l${CIRCUIT_ROOT} -l${CIRCUIT_POS_DIR}"
|
||||
circom --r1cs --wasm --O2 ${CIRCUIT_INCLUDES} ${CIRCUIT_MAIN}.circom
|
||||
end=`date +%s`
|
||||
echo "Compiling the circuit took `expr $end - $start` seconds."
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user