mirror of
https://github.com/logos-messaging/logos-messaging-nim.git
synced 2026-05-22 02:09:32 +00:00
feat: migrate to zerokit v2.0.2 (#3868)
This commit is contained in:
parent
c6e448a0ba
commit
eb1891dc0e
2
Makefile
2
Makefile
@ -176,7 +176,7 @@ deps: | nimble
|
||||
.PHONY: librln
|
||||
|
||||
LIBRLN_BUILDDIR := $(CURDIR)/vendor/zerokit
|
||||
LIBRLN_VERSION := v0.9.0
|
||||
LIBRLN_VERSION := v2.0.2
|
||||
|
||||
ifeq ($(detected_OS),Windows)
|
||||
LIBRLN_FILE ?= rln.lib
|
||||
|
||||
6
flake.lock
generated
6
flake.lock
generated
@ -72,17 +72,15 @@
|
||||
"rust-overlay": "rust-overlay_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1771279884,
|
||||
"narHash": "sha256-tzkQPwSl4vPTUo1ixHh6NCENjsBDroMKTjifg2q8QX8=",
|
||||
"owner": "vacp2p",
|
||||
"repo": "zerokit",
|
||||
"rev": "53b18098e6d5d046e3eb1ac338a8f4f651432477",
|
||||
"rev": "5e64cb8822bee65eed6cf459f95ae72b80c6ba63",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "vacp2p",
|
||||
"repo": "zerokit",
|
||||
"rev": "53b18098e6d5d046e3eb1ac338a8f4f651432477",
|
||||
"rev": "5e64cb8822bee65eed6cf459f95ae72b80c6ba63",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
|
||||
38
flake.nix
38
flake.nix
@ -21,7 +21,10 @@
|
||||
# External flake input: Zerokit pinned to a specific commit.
|
||||
# Update the rev here when a new zerokit version is needed.
|
||||
zerokit = {
|
||||
url = "github:vacp2p/zerokit/53b18098e6d5d046e3eb1ac338a8f4f651432477";
|
||||
# Pinned to v2.0.2 (5e64cb8822bee65eed6cf459f95ae72b80c6ba63) to match
|
||||
# the vendor/zerokit submodule. Keep these two in sync: the nix build
|
||||
# links librln from this input, the Makefile build from the submodule.
|
||||
url = "github:vacp2p/zerokit/5e64cb8822bee65eed6cf459f95ae72b80c6ba63";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
@ -70,10 +73,41 @@
|
||||
packages = forAllSystems (system:
|
||||
let
|
||||
pkgs = pkgsFor system;
|
||||
|
||||
# zerokit's nix/default.nix hardcodes a cargoHash that is stale for
|
||||
# our pinned nixpkgs on a cold runner (the status.im substituter is
|
||||
# untrusted here, so the cargo-vendor FOD is recomputed). v2.0.2 did
|
||||
# NOT fix this for consumers — its committed hash is the old v2.0.1
|
||||
# value while v2.0.2's Cargo.lock changed. Rebuild librln here from
|
||||
# the pinned zerokit source with the correct cargoHash. Keep the
|
||||
# version + cargoHash in sync with the zerokit input rev.
|
||||
rustToolchain = pkgs.rust-bin.stable.latest.default;
|
||||
zerokitRln = pkgs.rustPlatform.buildRustPackage {
|
||||
pname = "zerokit";
|
||||
version = "2.0.2";
|
||||
src = zerokit;
|
||||
cargo = rustToolchain;
|
||||
rustc = rustToolchain;
|
||||
cargoHash = "sha256-PNwEdZLgGQPqQDrEK2hsQtSybVfBbD6xn4K47fPFJUU=";
|
||||
nativeBuildInputs = [ pkgs.rust-cbindgen ];
|
||||
doCheck = false;
|
||||
buildPhase = ''
|
||||
export CARGO_HOME=$TMPDIR/cargo
|
||||
cargo build --lib --release --manifest-path rln/Cargo.toml
|
||||
'';
|
||||
installPhase = ''
|
||||
set -eu
|
||||
mkdir -p $out/lib $out/include
|
||||
find target -type f -name 'librln.*' -not -path '*/deps/*' \
|
||||
-exec cp -v '{}' "$out/lib/" \;
|
||||
cbindgen ./rln -l c > "$out/include/rln.h"
|
||||
'';
|
||||
};
|
||||
|
||||
liblogosdelivery = pkgs.callPackage ./nix/default.nix {
|
||||
inherit pkgs;
|
||||
src = ./.;
|
||||
zerokitRln = zerokit.packages.${system}.rln;
|
||||
inherit zerokitRln;
|
||||
gitVersion = "v${nimbleVersion}-g${builtins.substring 0 6 shortRev}";
|
||||
};
|
||||
in {
|
||||
|
||||
@ -33,8 +33,16 @@ if [[ "v${submodule_version}" != "${rln_version}" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build rln from source
|
||||
cargo build --release -p rln --manifest-path "${build_dir}/rln/Cargo.toml"
|
||||
# Build rln from source.
|
||||
# `stateless` feature: logos-delivery does not maintain a local Merkle tree
|
||||
# (post-PR #3312); the contract is the source of truth and the path is fetched
|
||||
# via getMerkleProof(index). The stateless build compiles out tree code.
|
||||
#
|
||||
# --no-default-features is required because zerokit's default features include
|
||||
# `pmtree-ft` (a Merkle tree backend); `stateless` and any Merkle-tree feature
|
||||
# are mutually exclusive (rln/src/lib.rs:32 compile_error).
|
||||
cargo build --release -p rln --manifest-path "${build_dir}/rln/Cargo.toml" \
|
||||
--no-default-features --features stateless
|
||||
cp "${build_dir}/target/release/librln.a" "${output_filename}"
|
||||
|
||||
echo "Successfully built ${output_filename}"
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
{.used.}
|
||||
|
||||
import std/options, chronos, libp2p/crypto/crypto
|
||||
import std/options, chronos, chronicles, libp2p/crypto/crypto
|
||||
|
||||
logScope:
|
||||
topics = "test waku_lightpush_legacy"
|
||||
|
||||
import
|
||||
waku/node/peer_manager,
|
||||
|
||||
@ -1,11 +1,4 @@
|
||||
import waku/waku_rln_relay/rln/rln_interface
|
||||
|
||||
proc `==`*(a: Buffer, b: seq[uint8]): bool =
|
||||
if a.len != uint(b.len):
|
||||
return false
|
||||
|
||||
let bufferArray = cast[ptr UncheckedArray[uint8]](a.ptr)
|
||||
for i in 0 ..< b.len:
|
||||
if bufferArray[i] != b[i]:
|
||||
return false
|
||||
return true
|
||||
# buffer_utils.nim — intentionally empty.
|
||||
# The v0.9 Buffer type and toBuffer helper were removed in the zerokit v2.0.1
|
||||
# migration. This file is kept as a placeholder so that any future test imports
|
||||
# do not break the build; the content that was here is no longer needed.
|
||||
|
||||
@ -1,17 +1,36 @@
|
||||
import testutils/unittests
|
||||
import testutils/unittests, results
|
||||
|
||||
import waku/waku_rln_relay/rln/rln_interface, ./buffer_utils
|
||||
import waku/waku_rln_relay/rln/rln_interface
|
||||
import waku/waku_rln_relay/rln/wrappers
|
||||
|
||||
suite "Buffer":
|
||||
suite "toBuffer":
|
||||
suite "Vec_uint8":
|
||||
suite "toVecUint8":
|
||||
test "valid":
|
||||
# Given
|
||||
let bytes: seq[byte] = @[0x01, 0x02, 0x03]
|
||||
|
||||
# When
|
||||
let buffer = bytes.toBuffer()
|
||||
# When — wrap as a Vec_uint8 view then read the bytes back
|
||||
var vec = toVecUint8(bytes)
|
||||
let roundtrip = vecToSeq(vec)
|
||||
|
||||
# Then
|
||||
let expectedBuffer: seq[uint8] = @[1, 2, 3]
|
||||
# Then — byte values are preserved
|
||||
check:
|
||||
buffer == expectedBuffer
|
||||
roundtrip == bytes
|
||||
|
||||
suite "RlnConfig":
|
||||
suite "createRLNInstance":
|
||||
test "ok":
|
||||
# When we create the RLN instance (stateless build — no tree_depth arg)
|
||||
let rlnRes = createRLNInstance()
|
||||
|
||||
# Then it succeeds
|
||||
check:
|
||||
rlnRes.isOk()
|
||||
|
||||
test "default":
|
||||
# When we create the RLN instance
|
||||
let rlnRes = createRLNInstance()
|
||||
|
||||
# Then it succeeds
|
||||
check:
|
||||
rlnRes.isOk()
|
||||
|
||||
@ -1,37 +1,6 @@
|
||||
import
|
||||
std/options,
|
||||
testutils/unittests,
|
||||
chronicles,
|
||||
chronos,
|
||||
eth/keys,
|
||||
bearssl,
|
||||
stew/[results],
|
||||
metrics,
|
||||
metrics/chronos_httpserver
|
||||
import testutils/unittests, results
|
||||
|
||||
import
|
||||
waku/waku_rln_relay,
|
||||
waku/waku_rln_relay/rln,
|
||||
waku/waku_rln_relay/rln/wrappers,
|
||||
./waku_rln_relay_utils,
|
||||
../../testlib/[simple_mock, assertions],
|
||||
../../waku_keystore/utils,
|
||||
../../testlib/testutils
|
||||
|
||||
from std/times import epochTime
|
||||
|
||||
const Empty32Array = default(array[32, byte])
|
||||
|
||||
proc valid(x: seq[byte]): bool =
|
||||
if x.len != 32:
|
||||
error "Length should be 32", length = x.len
|
||||
return false
|
||||
|
||||
if x == Empty32Array:
|
||||
error "Should not be empty array", array = x
|
||||
return false
|
||||
|
||||
return true
|
||||
import waku/waku_rln_relay/rln, waku/waku_rln_relay/rln/wrappers, ./waku_rln_relay_utils
|
||||
|
||||
suite "membershipKeyGen":
|
||||
test "ok":
|
||||
@ -41,60 +10,20 @@ suite "membershipKeyGen":
|
||||
# Then it contains valid identity credentials
|
||||
let identityCredentials = identityCredentialsRes.get()
|
||||
|
||||
proc nonEmpty(x: seq[byte]): bool =
|
||||
x.len == 32 and x != newSeq[byte](32)
|
||||
|
||||
check:
|
||||
identityCredentials.idTrapdoor.valid()
|
||||
identityCredentials.idNullifier.valid()
|
||||
identityCredentials.idSecretHash.valid()
|
||||
identityCredentials.idCommitment.valid()
|
||||
|
||||
test "done is false":
|
||||
# Given the key_gen function fails
|
||||
let backup = key_gen
|
||||
mock(key_gen):
|
||||
proc keyGenMock(ctx: ptr RLN, output_buffer: ptr Buffer): bool =
|
||||
return false
|
||||
|
||||
keyGenMock
|
||||
|
||||
# When we generate the membership keys
|
||||
let identityCredentialsRes = membershipKeyGen()
|
||||
|
||||
# Then it fails
|
||||
check:
|
||||
identityCredentialsRes.error() == "error in key generation"
|
||||
|
||||
# Cleanup
|
||||
mock(key_gen):
|
||||
backup
|
||||
|
||||
test "generatedKeys length is not 128":
|
||||
# Given the key_gen function succeeds with wrong values
|
||||
let backup = key_gen
|
||||
mock(key_gen):
|
||||
proc keyGenMock(ctx: ptr RLN, output_buffer: ptr Buffer): bool =
|
||||
echo "# RUNNING MOCK"
|
||||
output_buffer.len = 0
|
||||
output_buffer.ptr = cast[ptr uint8](newSeq[byte](0))
|
||||
return true
|
||||
|
||||
keyGenMock
|
||||
|
||||
# When we generate the membership keys
|
||||
let identityCredentialsRes = membershipKeyGen()
|
||||
|
||||
# Then it fails
|
||||
check:
|
||||
identityCredentialsRes.error() == "keysBuffer is of invalid length"
|
||||
|
||||
# Cleanup
|
||||
mock(key_gen):
|
||||
backup
|
||||
identityCredentials.idTrapdoor.nonEmpty()
|
||||
identityCredentials.idNullifier.nonEmpty()
|
||||
identityCredentials.idSecretHash.nonEmpty()
|
||||
identityCredentials.idCommitment.nonEmpty()
|
||||
|
||||
suite "RlnConfig":
|
||||
suite "createRLNInstance":
|
||||
test "ok":
|
||||
# When we create the RLN instance
|
||||
let rlnRes: RLNResult = createRLNInstance(15)
|
||||
# When we create the RLN instance (stateless build — no tree_depth arg)
|
||||
let rlnRes = createRLNInstance()
|
||||
|
||||
# Then it succeeds
|
||||
check:
|
||||
@ -102,30 +31,8 @@ suite "RlnConfig":
|
||||
|
||||
test "default":
|
||||
# When we create the RLN instance
|
||||
let rlnRes: RLNResult = createRLNInstance()
|
||||
let rlnRes = createRLNInstance()
|
||||
|
||||
# Then it succeeds
|
||||
check:
|
||||
rlnRes.isOk()
|
||||
|
||||
test "new_circuit fails":
|
||||
# Given the new_circuit function fails
|
||||
let backup = new_circuit
|
||||
mock(new_circuit):
|
||||
proc newCircuitMock(
|
||||
tree_height: uint, input_buffer: ptr Buffer, ctx: ptr (ptr RLN)
|
||||
): bool =
|
||||
return false
|
||||
|
||||
newCircuitMock
|
||||
|
||||
# When we create the RLN instance
|
||||
let rlnRes: RLNResult = createRLNInstance(15)
|
||||
|
||||
# Then it fails
|
||||
check:
|
||||
rlnRes.error() == "error in parameters generation"
|
||||
|
||||
# Cleanup
|
||||
mock(new_circuit):
|
||||
backup
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
{.push raises: [].}
|
||||
|
||||
import
|
||||
std/[options, sequtils, deques, random, locks, osproc],
|
||||
std/[options, sequtils, deques, random, locks, osproc, algorithm],
|
||||
results,
|
||||
stew/byteutils,
|
||||
testutils/unittests,
|
||||
@ -253,6 +253,9 @@ suite "Onchain group manager":
|
||||
manager.merkleProofCache = newSeq[byte](640)
|
||||
for i in 0 ..< 640:
|
||||
manager.merkleProofCache[i] = byte(rand(255))
|
||||
# chunk[0] becomes the MSB after reversal in group_manager; must be < 0x30
|
||||
for i in 0 ..< 20:
|
||||
manager.merkleProofCache[i * 32] = 0
|
||||
|
||||
let messageBytes = "Hello".toBytes()
|
||||
|
||||
@ -335,6 +338,9 @@ suite "Onchain group manager":
|
||||
manager.merkleProofCache = newSeq[byte](640)
|
||||
for i in 0 ..< 640:
|
||||
manager.merkleProofCache[i] = byte(rand(255))
|
||||
# chunk[0] becomes the MSB after reversal in group_manager; must be < 0x30
|
||||
for i in 0 ..< 20:
|
||||
manager.merkleProofCache[i * 32] = 0
|
||||
|
||||
let epoch = default(Epoch)
|
||||
info "epoch in bytes", epochHex = epoch.inHex()
|
||||
@ -419,3 +425,81 @@ suite "Onchain group manager":
|
||||
|
||||
check:
|
||||
isReady == true
|
||||
|
||||
test "proof roundtrip: generateRlnProofWithWitness -> verifyRlnProof":
|
||||
## Smoke test: proof gen -> wire serialize -> deserialize -> ffi_verify_with_roots.
|
||||
let credentials = generateCredentials()
|
||||
|
||||
(waitFor manager.init()).isOkOr:
|
||||
raiseAssert $error
|
||||
|
||||
(waitFor manager.register(credentials, UserMessageLimit(20))).isOkOr:
|
||||
assert false, "register failed: " & error
|
||||
|
||||
discard waitFor manager.updateRoots()
|
||||
let roots = manager.validRoots.items().toSeq()
|
||||
require:
|
||||
roots.len > 0
|
||||
|
||||
let proofElements = (waitFor manager.fetchMerkleProofElements()).valueOr:
|
||||
raiseAssert "fetchMerkleProofElements failed: " & error
|
||||
|
||||
let signal = "Hello, RLN!".toBytes()
|
||||
let epoch = default(Epoch)
|
||||
|
||||
# Build RLNWitnessInput the same way group_manager.generateProof does.
|
||||
var pathElements = newSeq[byte]()
|
||||
for i in 0 ..< proofElements.len div 32:
|
||||
pathElements.add(proofElements[i * 32 .. (i + 1) * 32 - 1].reversed())
|
||||
|
||||
let xCfr = hashToFieldLe(signal).valueOr:
|
||||
raiseAssert "hashToFieldLe failed: " & error
|
||||
defer:
|
||||
ffi_cfr_free(xCfr)
|
||||
let x = cfrToBytesLe(xCfr).valueOr:
|
||||
raiseAssert "cfrToBytesLe failed: " & error
|
||||
|
||||
let extNullifier = generateExternalNullifier(epoch, DefaultRlnIdentifier).valueOr:
|
||||
raiseAssert "generateExternalNullifier failed: " & error
|
||||
|
||||
let witness = RLNWitnessInput(
|
||||
identity_secret: seqToField(credentials.idSecretHash),
|
||||
user_message_limit: uint64ToField(uint64(UserMessageLimit(20))),
|
||||
message_id: uint64ToField(uint64(MessageId(1))),
|
||||
path_elements: pathElements,
|
||||
identity_path_index: uint64ToIndex(manager.membershipIndex.get(), 20),
|
||||
x: x,
|
||||
external_nullifier: extNullifier,
|
||||
)
|
||||
|
||||
# Step 1: generate proof via the FFI wrapper
|
||||
let proof = generateRlnProofWithWitness(
|
||||
manager.rlnInstance, witness, epoch, DefaultRlnIdentifier
|
||||
).valueOr:
|
||||
raiseAssert "generateRlnProofWithWitness failed: " & error
|
||||
|
||||
let zeroField = default(array[32, byte])
|
||||
check:
|
||||
proof.merkleRoot != zeroField
|
||||
proof.nullifier != zeroField
|
||||
|
||||
# Step 2: serialize -> deserialize -> verify (the actual roundtrip)
|
||||
let verified = verifyRlnProof(manager.rlnInstance, proof, signal, roots).valueOr:
|
||||
raiseAssert "verifyRlnProof failed: " & error
|
||||
check verified == true
|
||||
|
||||
# Step 3: wrong signal -> x mismatch -> false
|
||||
let wrongSignalVerified = verifyRlnProof(
|
||||
manager.rlnInstance, proof, "wrong".toBytes(), roots
|
||||
).valueOr:
|
||||
raiseAssert "verifyRlnProof (wrong signal) failed: " & error
|
||||
check wrongSignalVerified == false
|
||||
|
||||
# Step 4: bad root -> root not in set -> false
|
||||
# byte[31] in LE is the MSB; 0x01 < 0x30 so this is a canonical field element.
|
||||
var badRoot: MerkleNode
|
||||
for i in 0 ..< 32:
|
||||
badRoot[i] = 0x01
|
||||
let badRootVerified = verifyRlnProof(manager.rlnInstance, proof, signal, @[badRoot]).valueOr:
|
||||
raiseAssert "verifyRlnProof (bad root) failed: " & error
|
||||
check badRootVerified == false
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{.used.}
|
||||
|
||||
import
|
||||
std/[options, os, sequtils, tempfiles, strutils, osproc],
|
||||
std/[options, os, sequtils, tempfiles, strutils, osproc, algorithm],
|
||||
stew/byteutils,
|
||||
testutils/unittests,
|
||||
chronos,
|
||||
@ -36,23 +36,16 @@ suite "Waku rln relay":
|
||||
teardown:
|
||||
stopAnvil(anvilProc)
|
||||
|
||||
test "key_gen Nim Wrappers":
|
||||
let merkleDepth: csize_t = 20
|
||||
test "ffi_extended_key_gen raw FFI":
|
||||
# When we call the raw key-generation FFI
|
||||
var vec = ffi_extended_key_gen()
|
||||
|
||||
# keysBufferPtr will hold the generated identity credential i.e., id trapdoor, nullifier, secret hash and commitment
|
||||
var keysBuffer: Buffer
|
||||
let
|
||||
keysBufferPtr = addr(keysBuffer)
|
||||
done = key_gen(keysBufferPtr, true)
|
||||
require:
|
||||
# check whether the keys are generated successfully
|
||||
done
|
||||
|
||||
let generatedKeys = cast[ptr array[4 * 32, byte]](keysBufferPtr.`ptr`)[]
|
||||
# Then it returns exactly 4 field elements
|
||||
# (idTrapdoor, idNullifier, idSecretHash, idCommitment — each 32 bytes)
|
||||
defer:
|
||||
ffi_vec_cfr_free(vec)
|
||||
check:
|
||||
# the id trapdoor, nullifier, secert hash and commitment together are 4*32 bytes
|
||||
generatedKeys.len == 4 * 32
|
||||
info "generated keys: ", generatedKeys
|
||||
int(ffi_vec_cfr_len(addr vec)) == 4
|
||||
|
||||
test "membership Key Generation":
|
||||
let idCredentialsRes = membershipKeyGen()
|
||||
@ -80,18 +73,22 @@ suite "Waku rln relay":
|
||||
rlnInstance.isOk()
|
||||
let rln = rlnInstance.get()
|
||||
|
||||
# prepare the input
|
||||
let msg = @[
|
||||
"126f4c026cd731979365f79bd345a46d673c5a3f6f588bdc718e6356d02b6fdc".toBytes(),
|
||||
"1f0e5db2b69d599166ab16219a97b82b662085c93220382b39f9f911d3b943b1".toBytes(),
|
||||
]
|
||||
# prepare the input — hex-decoded then reversed to little-endian field elements
|
||||
let
|
||||
left = hexToSeqByte(
|
||||
"126f4c026cd731979365f79bd345a46d673c5a3f6f588bdc718e6356d02b6fdc"
|
||||
)
|
||||
.reversed()
|
||||
right = hexToSeqByte(
|
||||
"1f0e5db2b69d599166ab16219a97b82b662085c93220382b39f9f911d3b943b1"
|
||||
)
|
||||
.reversed()
|
||||
|
||||
let hashRes = poseidon(msg)
|
||||
let hashRes = poseidon(left, right)
|
||||
|
||||
# Value taken from zerokit
|
||||
check:
|
||||
hashRes.isOk()
|
||||
"28a15a991fe3d2a014485c7fa905074bfb55c0909112f865ded2be0a26a932c3" ==
|
||||
"180543bc9afb81d9c2282df9c9946f87b4596cf6d3fec2cc32b6637427685353" ==
|
||||
hashRes.get().inHex()
|
||||
|
||||
test "RateLimitProof Protobuf encode/init test":
|
||||
|
||||
2
vendor/zerokit
vendored
2
vendor/zerokit
vendored
@ -1 +1 @@
|
||||
Subproject commit a4bb3feb5054e6fd24827adf204493e6e173437b
|
||||
Subproject commit 5e64cb8822bee65eed6cf459f95ae72b80c6ba63
|
||||
@ -25,8 +25,6 @@ const
|
||||
# the size of poseidon hash output as the number hex digits
|
||||
HashHexSize* = int(HashBitSize / 4)
|
||||
|
||||
const DefaultRlnTreePath* = "rln_tree.db"
|
||||
|
||||
const
|
||||
# pre-processed "rln/waku-rln-relay/v2.0.0" to array[32, byte]
|
||||
DefaultRlnIdentifier*: RlnIdentifier = [
|
||||
|
||||
@ -75,48 +75,6 @@ proc serialize*(
|
||||
)
|
||||
return output
|
||||
|
||||
proc serialize*(witness: RLNWitnessInput): seq[byte] =
|
||||
## Serializes the RLN witness into a byte array following zerokit's expected format.
|
||||
## The serialized format includes:
|
||||
## - identity_secret (32 bytes, little-endian with zero padding)
|
||||
## - user_message_limit (32 bytes, little-endian with zero padding)
|
||||
## - message_id (32 bytes, little-endian with zero padding)
|
||||
## - merkle tree depth (8 bytes, little-endian) = path_elements.len / 32
|
||||
## - path_elements (each 32 bytes, ordered bottom-to-top)
|
||||
## - merkle tree depth again (8 bytes, little-endian)
|
||||
## - identity_path_index (sequence of bits as bytes, 0 = left, 1 = right)
|
||||
## - x (32 bytes, little-endian with zero padding)
|
||||
## - external_nullifier (32 bytes, little-endian with zero padding)
|
||||
var buffer: seq[byte]
|
||||
buffer.add(@(witness.identity_secret))
|
||||
buffer.add(@(witness.user_message_limit))
|
||||
buffer.add(@(witness.message_id))
|
||||
buffer.add(toBytes(uint64(witness.path_elements.len / 32), Endianness.littleEndian))
|
||||
for element in witness.path_elements:
|
||||
buffer.add(element)
|
||||
buffer.add(toBytes(uint64(witness.path_elements.len / 32), Endianness.littleEndian))
|
||||
buffer.add(witness.identity_path_index)
|
||||
buffer.add(@(witness.x))
|
||||
buffer.add(@(witness.external_nullifier))
|
||||
return buffer
|
||||
|
||||
proc serialize*(proof: RateLimitProof, data: openArray[byte]): seq[byte] =
|
||||
## a private proc to convert RateLimitProof and data to a byte seq
|
||||
## this conversion is used in the proof verification proc
|
||||
## [ proof<128> | root<32> | epoch<32> | share_x<32> | share_y<32> | nullifier<32> | rln_identifier<32> | signal_len<8> | signal<var> ]
|
||||
let lenPrefMsg = encodeLengthPrefix(@data)
|
||||
var proofBytes = concat(
|
||||
@(proof.proof),
|
||||
@(proof.merkleRoot),
|
||||
@(proof.externalNullifier),
|
||||
@(proof.shareX),
|
||||
@(proof.shareY),
|
||||
@(proof.nullifier),
|
||||
lenPrefMsg,
|
||||
)
|
||||
|
||||
return proofBytes
|
||||
|
||||
# Serializes a sequence of MerkleNodes
|
||||
proc serialize*(roots: seq[MerkleNode]): seq[byte] =
|
||||
var rootsBytes: seq[byte] = @[]
|
||||
|
||||
@ -11,7 +11,7 @@ import
|
||||
stint,
|
||||
json,
|
||||
std/[strutils, tables, algorithm, strformat],
|
||||
stew/[byteutils, arrayops],
|
||||
stew/byteutils,
|
||||
sequtils
|
||||
|
||||
import
|
||||
@ -327,7 +327,7 @@ proc getRootFromProofAndIndex(
|
||||
# it's currently not used anywhere, but can be used to verify the root from the proof and index
|
||||
# Compute leaf hash from idCommitment and messageLimit
|
||||
let messageLimitField = uint64ToField(g.userMessageLimit.get())
|
||||
var hash = poseidon(@[g.idCredentials.get().idCommitment, @messageLimitField]).valueOr:
|
||||
var hash = poseidon(g.idCredentials.get().idCommitment, @messageLimitField).valueOr:
|
||||
return err("Failed to compute leaf hash: " & error)
|
||||
|
||||
for i in 0 ..< bits.len:
|
||||
@ -335,9 +335,9 @@ proc getRootFromProofAndIndex(
|
||||
|
||||
let hashRes =
|
||||
if bits[i] == 0:
|
||||
poseidon(@[@hash, sibling])
|
||||
poseidon(@hash, sibling)
|
||||
else:
|
||||
poseidon(@[sibling, @hash])
|
||||
poseidon(sibling, @hash)
|
||||
|
||||
hash = hashRes.valueOr:
|
||||
return err("Failed to compute poseidon hash: " & error)
|
||||
@ -373,7 +373,12 @@ method generateProof*(
|
||||
let chunk = g.merkleProofCache[i * 32 .. (i + 1) * 32 - 1]
|
||||
path_elements.add(chunk.reversed())
|
||||
|
||||
let x = keccak.keccak256.digest(data)
|
||||
let xCfr = hashToFieldLe(data).valueOr:
|
||||
return err("Failed to hash signal to field: " & error)
|
||||
defer:
|
||||
ffi_cfr_free(xCfr)
|
||||
let x = cfrToBytesLe(xCfr).valueOr:
|
||||
return err("Failed to serialize signal hash: " & error)
|
||||
|
||||
let extNullifier = generateExternalNullifier(epoch, rlnIdentifier).valueOr:
|
||||
return err("Failed to compute external nullifier: " & error)
|
||||
@ -388,57 +393,8 @@ method generateProof*(
|
||||
external_nullifier: extNullifier,
|
||||
)
|
||||
|
||||
let serializedWitness = serialize(witness)
|
||||
|
||||
var input_witness_buffer = toBuffer(serializedWitness)
|
||||
|
||||
# Generate the proof using the zerokit API
|
||||
var output_witness_buffer: Buffer
|
||||
let witness_success = generate_proof_with_witness(
|
||||
g.rlnInstance, addr input_witness_buffer, addr output_witness_buffer
|
||||
)
|
||||
|
||||
if not witness_success:
|
||||
return err("Failed to generate proof")
|
||||
|
||||
# Parse the proof into a RateLimitProof object
|
||||
var proofValue = cast[ptr array[320, byte]](output_witness_buffer.`ptr`)
|
||||
let proofBytes: array[320, byte] = proofValue[]
|
||||
|
||||
## Parse the proof as [ proof<128> | root<32> | external_nullifier<32> | share_x<32> | share_y<32> | nullifier<32> ]
|
||||
let
|
||||
proofOffset = 128
|
||||
rootOffset = proofOffset + 32
|
||||
externalNullifierOffset = rootOffset + 32
|
||||
shareXOffset = externalNullifierOffset + 32
|
||||
shareYOffset = shareXOffset + 32
|
||||
nullifierOffset = shareYOffset + 32
|
||||
|
||||
var
|
||||
zkproof: ZKSNARK
|
||||
proofRoot, shareX, shareY: MerkleNode
|
||||
externalNullifier: ExternalNullifier
|
||||
nullifier: Nullifier
|
||||
|
||||
discard zkproof.copyFrom(proofBytes[0 .. proofOffset - 1])
|
||||
discard proofRoot.copyFrom(proofBytes[proofOffset .. rootOffset - 1])
|
||||
discard
|
||||
externalNullifier.copyFrom(proofBytes[rootOffset .. externalNullifierOffset - 1])
|
||||
discard shareX.copyFrom(proofBytes[externalNullifierOffset .. shareXOffset - 1])
|
||||
discard shareY.copyFrom(proofBytes[shareXOffset .. shareYOffset - 1])
|
||||
discard nullifier.copyFrom(proofBytes[shareYOffset .. nullifierOffset - 1])
|
||||
|
||||
# Create the RateLimitProof object
|
||||
let output = RateLimitProof(
|
||||
proof: zkproof,
|
||||
merkleRoot: proofRoot,
|
||||
externalNullifier: externalNullifier,
|
||||
epoch: epoch,
|
||||
rlnIdentifier: rlnIdentifier,
|
||||
shareX: shareX,
|
||||
shareY: shareY,
|
||||
nullifier: nullifier,
|
||||
)
|
||||
let output = generateRlnProofWithWitness(g.rlnInstance, witness, epoch, rlnIdentifier).valueOr:
|
||||
return err("Failed to generate proof: " & error)
|
||||
|
||||
info "Proof generated successfully", proof = output
|
||||
|
||||
@ -449,34 +405,12 @@ method generateProof*(
|
||||
method verifyProof*(
|
||||
g: OnchainGroupManager, input: seq[byte], proof: RateLimitProof
|
||||
): GroupManagerResult[bool] {.gcsafe.} =
|
||||
## -- Verifies an RLN rate-limit proof against the set of valid Merkle roots --
|
||||
|
||||
var normalizedProof = proof
|
||||
|
||||
let externalNullifier = generateExternalNullifier(proof.epoch, proof.rlnIdentifier).valueOr:
|
||||
return err("Failed to compute external nullifier: " & error)
|
||||
normalizedProof.externalNullifier = externalNullifier
|
||||
|
||||
let proofBytes = serialize(normalizedProof, input)
|
||||
let proofBuffer = proofBytes.toBuffer()
|
||||
|
||||
let rootsBytes = serialize(g.validRoots.items().toSeq())
|
||||
let rootsBuffer = rootsBytes.toBuffer()
|
||||
|
||||
var validProof: bool # out-param
|
||||
let ffiOk = verify_with_roots(
|
||||
g.rlnInstance, # RLN context created at init()
|
||||
addr proofBuffer, # (proof + signal)
|
||||
addr rootsBuffer, # valid Merkle roots
|
||||
addr validProof # will be set by the FFI call
|
||||
,
|
||||
)
|
||||
|
||||
if not ffiOk:
|
||||
return err("could not verify the proof")
|
||||
else:
|
||||
info "Proof verified successfully"
|
||||
let validProof = verifyRlnProof(
|
||||
g.rlnInstance, proof, input, g.validRoots.items().toSeq()
|
||||
).valueOr:
|
||||
return err("could not verify the proof: " & error)
|
||||
|
||||
info "Proof verified", isValid = validProof
|
||||
return ok(validProof)
|
||||
|
||||
method onRegister*(g: OnchainGroupManager, cb: OnRegisterCallback) {.gcsafe.} =
|
||||
@ -623,6 +557,10 @@ method stop*(g: OnchainGroupManager): Future[void] {.async, gcsafe.} =
|
||||
g.ethRpc.get().ondisconnect = nil
|
||||
await g.ethRpc.get().close()
|
||||
|
||||
if not g.rlnInstance.isNil:
|
||||
ffi_rln_free(g.rlnInstance)
|
||||
g.rlnInstance = nil
|
||||
|
||||
g.initialized = false
|
||||
|
||||
method isReady*(g: OnchainGroupManager): Future[bool] {.async.} =
|
||||
|
||||
@ -56,7 +56,7 @@ declarePublicGauge(
|
||||
)
|
||||
declarePublicGauge(
|
||||
waku_rln_membership_insertion_duration_seconds,
|
||||
"time taken to insert a new member into the local merkle tree",
|
||||
"time taken to process a new membership registration",
|
||||
)
|
||||
declarePublicGauge(
|
||||
waku_rln_membership_credentials_import_duration_seconds,
|
||||
|
||||
@ -1,168 +1,378 @@
|
||||
## Nim wrappers for the functions defined in librln
|
||||
## Nim wrappers for librln (zerokit v2.0.2, safer-ffi typed handles).
|
||||
##
|
||||
## Built against the `stateless` zerokit feature: tree-mutation FFI is not
|
||||
## bound here because logos-delivery does not maintain a local Merkle tree
|
||||
## (post-PR #3312); the WakuRlnV2 contract is the source of truth and the
|
||||
## per-index Merkle path is fetched via getMerkleProof(index).
|
||||
##
|
||||
## Memory model: every CResult.err must be checked with `hasError` and
|
||||
## consumed via `consumeError`. Every CFr / Vec_CFr / Vec_uint8 returned by
|
||||
## the FFI owns memory the caller must release with the corresponding
|
||||
## ffi_*_free. Use `defer:` immediately after acquisition.
|
||||
##
|
||||
## Wire format (v2.0.2 single-message-id):
|
||||
## RLNProof: [ 0x00 | proof<128> | RLNProofValues(0x00) ]
|
||||
## RLNProofValues: [ 0x00 | root<32> | external_nullifier<32> |
|
||||
## x<32> | y<32> | nullifier<32> ]
|
||||
## Total RLNProof byte size: 1 + 128 + 1 + 5*32 = 290 bytes.
|
||||
|
||||
import results
|
||||
import ../protocol_types
|
||||
|
||||
{.push raises: [].}
|
||||
{.push raises: [], gcsafe.}
|
||||
|
||||
## Buffer struct is taken from
|
||||
# https://github.com/celo-org/celo-threshold-bls-rs/blob/master/crates/threshold-bls-ffi/src/ffi.rs
|
||||
type Buffer* = object
|
||||
`ptr`*: ptr uint8
|
||||
len*: uint
|
||||
# --- Types ------------------------------------------------------------------
|
||||
|
||||
proc toBuffer*(x: openArray[byte]): Buffer =
|
||||
## converts the input to a Buffer object
|
||||
## the Buffer object is used to communicate data with the rln lib
|
||||
var temp = @x
|
||||
let baseAddr = cast[pointer](x)
|
||||
let output = Buffer(`ptr`: cast[ptr uint8](baseAddr), len: uint(temp.len))
|
||||
return output
|
||||
type
|
||||
CSize = csize_t
|
||||
|
||||
######################################################################
|
||||
## RLN Zerokit module APIs
|
||||
######################################################################
|
||||
CFr* = object ## opaque ark_bn254::Fr handle
|
||||
FFI_RLNProof* = object
|
||||
FFI_RLNPartialProof* = object
|
||||
FFI_RLNWitnessInput* = object
|
||||
FFI_RLNPartialWitnessInput* = object
|
||||
FFI_RLNProofValues* = object
|
||||
|
||||
#-------------------------------- zkSNARKs operations -----------------------------------------
|
||||
proc key_gen*(
|
||||
output_buffer: ptr Buffer, is_little_endian: bool
|
||||
): bool {.importc: "extended_key_gen".}
|
||||
Vec_CFr* = object
|
||||
dataPtr*: ptr CFr
|
||||
len*: CSize
|
||||
cap*: CSize
|
||||
|
||||
## generates identity trapdoor, identity nullifier, identity secret hash and id commitment tuple serialized inside output_buffer as | identity_trapdoor<32> | identity_nullifier<32> | identity_secret_hash<32> | id_commitment<32> |
|
||||
## identity secret hash is the poseidon hash of [identity_trapdoor, identity_nullifier]
|
||||
## id commitment is the poseidon hash of the identity secret hash
|
||||
## the return bool value indicates the success or failure of the operation
|
||||
Vec_uint8* = object
|
||||
dataPtr*: ptr uint8
|
||||
len*: CSize
|
||||
cap*: CSize
|
||||
|
||||
proc seeded_key_gen*(
|
||||
input_buffer: ptr Buffer, output_buffer: ptr Buffer, is_little_endian: bool
|
||||
): bool {.importc: "seeded_extended_key_gen".}
|
||||
# CResult variants — safer-ffi lowers Result<T, E> to a struct of
|
||||
# (ok: T-or-null, err: Vec_uint8-or-null). Exactly one is populated.
|
||||
CBoolResult* = object
|
||||
ok*: bool
|
||||
err*: Vec_uint8
|
||||
|
||||
## generates identity trapdoor, identity nullifier, identity secret hash and id commitment tuple serialized inside output_buffer as | identity_trapdoor<32> | identity_nullifier<32> | identity_secret_hash<32> | id_commitment<32> | using ChaCha20
|
||||
## seeded with an arbitrary long seed serialized in input_buffer
|
||||
## The input seed provided by the user is hashed using Keccak256 before being passed to ChaCha20 as seed.
|
||||
## identity secret hash is the poseidon hash of [identity_trapdoor, identity_nullifier]
|
||||
## id commitment is the poseidon hash of the identity secret hash
|
||||
# use_little_endian: if true, uses big or little endian for serialization (default: true)
|
||||
## the return bool value indicates the success or failure of the operation
|
||||
CResultRLNPtrVecU8* = object
|
||||
ok*: ptr RLN
|
||||
err*: Vec_uint8
|
||||
|
||||
proc generate_proof*(
|
||||
ctx: ptr RLN, input_buffer: ptr Buffer, output_buffer: ptr Buffer
|
||||
): bool {.importc: "generate_rln_proof".}
|
||||
CResultCFrPtrVecU8* = object
|
||||
ok*: ptr CFr
|
||||
err*: Vec_uint8
|
||||
|
||||
## rln-v2
|
||||
## input_buffer has to be serialized as [ identity_secret<32> | identity_index<8> | user_message_limit<32> | message_id<32> | external_nullifier<32> | signal_len<8> | signal<var> ]
|
||||
## output_buffer holds the proof data and should be parsed as [ proof<128> | root<32> | external_nullifier<32> | share_x<32> | share_y<32> | nullifier<32> ]
|
||||
## rln-v1
|
||||
## input_buffer has to be serialized as [ id_key<32> | id_index<8> | epoch<32> | signal_len<8> | signal<var> ]
|
||||
## output_buffer holds the proof data and should be parsed as [ proof<128> | root<32> | epoch<32> | share_x<32> | share_y<32> | nullifier<32> | rln_identifier<32> ]
|
||||
## integers wrapped in <> indicate value sizes in bytes
|
||||
## the return bool value indicates the success or failure of the operation
|
||||
CResultProofPtrVecU8* = object
|
||||
ok*: ptr FFI_RLNProof
|
||||
err*: Vec_uint8
|
||||
|
||||
proc generate_proof_with_witness*(
|
||||
ctx: ptr RLN, input_buffer: ptr Buffer, output_buffer: ptr Buffer
|
||||
): bool {.importc: "generate_rln_proof_with_witness".}
|
||||
CResultPartialProofPtrVecU8* = object
|
||||
ok*: ptr FFI_RLNPartialProof
|
||||
err*: Vec_uint8
|
||||
|
||||
## rln-v2
|
||||
## "witness" term refer to collection of secret inputs with proper serialization
|
||||
## input_buffer has to be serialized as [ identity_secret<32> | user_message_limit<32> | message_id<32> | path_elements<Vec<32>> | identity_path_index<Vec<1>> | x<32> | external_nullifier<32> ]
|
||||
## output_buffer holds the proof data and should be parsed as [ proof<128> | root<32> | external_nullifier<32> | share_x<32> | share_y<32> | nullifier<32> ]
|
||||
## rln-v1
|
||||
## input_buffer has to be serialized as [ id_key<32> | path_elements<Vec<32>> | identity_path_index<Vec<1>> | x<32> | epoch<32> | rln_identifier<32> ]
|
||||
## output_buffer holds the proof data and should be parsed as [ proof<128> | root<32> | epoch<32> | share_x<32> | share_y<32> | nullifier<32> | rln_identifier<32> ]
|
||||
## integers wrapped in <> indicate value sizes in bytes
|
||||
## path_elements and identity_path_index serialize a merkle proof and are vectors of elements of 32 and 1 bytes respectively
|
||||
## the return bool value indicates the success or failure of the operation
|
||||
CResultWitnessInputPtrVecU8* = object
|
||||
ok*: ptr FFI_RLNWitnessInput
|
||||
err*: Vec_uint8
|
||||
|
||||
proc verify*(
|
||||
ctx: ptr RLN, proof_buffer: ptr Buffer, proof_is_valid_ptr: ptr bool
|
||||
): bool {.importc: "verify_rln_proof".}
|
||||
CResultPartialWitnessInputPtrVecU8* = object
|
||||
ok*: ptr FFI_RLNPartialWitnessInput
|
||||
err*: Vec_uint8
|
||||
|
||||
## rln-v2
|
||||
## proof_buffer has to be serialized as [ proof<128> | root<32> | external_nullifier<32> | share_x<32> | share_y<32> | nullifier<32> | signal_len<8> | signal<var> ]
|
||||
## rln-v1
|
||||
## ## proof_buffer has to be serialized as [ proof<128> | root<32> | epoch<32> | share_x<32> | share_y<32> | nullifier<32> | rln_identifier<32> | signal_len<8> | signal<var> ]
|
||||
## the return bool value indicates the success or failure of the call to the verify function
|
||||
## the verification of the zk proof is available in proof_is_valid_ptr, where a value of true indicates success and false a failure
|
||||
CResultVecCFrVecU8* = object
|
||||
ok*: Vec_CFr
|
||||
err*: Vec_uint8
|
||||
|
||||
proc verify_with_roots*(
|
||||
ctx: ptr RLN,
|
||||
proof_buffer: ptr Buffer,
|
||||
roots_buffer: ptr Buffer,
|
||||
proof_is_valid_ptr: ptr bool,
|
||||
): bool {.importc: "verify_with_roots".}
|
||||
CResultVecU8VecU8* = object
|
||||
ok*: Vec_uint8
|
||||
err*: Vec_uint8
|
||||
|
||||
## rln-v2
|
||||
## proof_buffer has to be serialized as [ proof<128> | root<32> | external_nullifier<32> | share_x<32> | share_y<32> | nullifier<32> | signal_len<8> | signal<var> ]
|
||||
## rln-v1
|
||||
## proof_buffer has to be serialized as [ proof<128> | root<32> | epoch<32> | share_x<32> | share_y<32> | nullifier<32> | rln_identifier<32> | signal_len<8> | signal<var> ]
|
||||
## roots_buffer contains the concatenation of 32 bytes long serializations in little endian of root values
|
||||
## the return bool value indicates the success or failure of the call to the verify function
|
||||
## the verification of the zk proof is available in proof_is_valid_ptr, where a value of true indicates success and false a failure
|
||||
const
|
||||
FieldElementSize* = 32
|
||||
ZksnarkProofSize* = 128
|
||||
## Single-message-id serialized RLNProof size: outer version + proof
|
||||
## + inner RLNProofValues (inner version + 5 field elements).
|
||||
RlnProofWireSize* = 1 + ZksnarkProofSize + 1 + 5 * FieldElementSize
|
||||
|
||||
proc zk_prove*(
|
||||
ctx: ptr RLN, input_buffer: ptr Buffer, output_buffer: ptr Buffer
|
||||
): bool {.importc: "prove".}
|
||||
# FFI declarations — source of truth: vendor/zerokit/rln/src/ffi/{ffi_rln,ffi_utils}.rs
|
||||
|
||||
## Computes the zkSNARK proof and stores it in output_buffer for input values stored in input_buffer
|
||||
## rln-v2
|
||||
## input_buffer is serialized as input_data as [ identity_secret<32> | user_message_limit<32> | message_id<32> | path_elements<Vec<32>> | identity_path_index<Vec<1>> | x<32> | external_nullifier<32> ]
|
||||
## rln-v1
|
||||
## input_buffer is serialized as input_data as [ id_key<32> | path_elements<Vec<32>> | identity_path_index<Vec<1>> | x<32> | epoch<32> | rln_identifier<32> ]
|
||||
## output_buffer holds the proof data and should be parsed as [ proof<128> ]
|
||||
## path_elements and indentity_path elements serialize a merkle proof for id_key and are vectors of elements of 32 and 1 bytes, respectively (not. Vec<>).
|
||||
## x is the x coordinate of the Shamir's secret share for which the proof is computed
|
||||
## epoch is the input epoch (equivalently, the nullifier)
|
||||
## the return bool value indicates the success or failure of the operation
|
||||
# --- RLN instance lifecycle (stateless variants) --------------------------
|
||||
|
||||
proc zk_verify*(
|
||||
ctx: ptr RLN, proof_buffer: ptr Buffer, proof_is_valid_ptr: ptr bool
|
||||
): bool {.importc: "verify".}
|
||||
proc ffi_rln_new*(): CResultRLNPtrVecU8 {.importc: "ffi_rln_new", cdecl.}
|
||||
|
||||
## Verifies the zkSNARK proof passed in proof_buffer
|
||||
## input_buffer is serialized as input_data as [ proof<128> ]
|
||||
## the verification of the zk proof is available in proof_is_valid_ptr, where a value of true indicates success and false a failure
|
||||
## the return bool value indicates the success or failure of the operation
|
||||
proc ffi_rln_new_with_params*(
|
||||
zkey_data: ptr Vec_uint8, graph_data: ptr Vec_uint8
|
||||
): CResultRLNPtrVecU8 {.importc: "ffi_rln_new_with_params", cdecl.}
|
||||
|
||||
#-------------------------------- Common procedures -------------------------------------------
|
||||
# stateful version
|
||||
proc new_circuit*(
|
||||
tree_depth: uint, input_buffer: ptr Buffer, ctx: ptr (ptr RLN)
|
||||
): bool {.importc: "new".}
|
||||
proc ffi_rln_free*(rln: ptr RLN) {.importc: "ffi_rln_free", cdecl.}
|
||||
|
||||
## creates an instance of rln object as defined by the zerokit RLN lib
|
||||
## input_buffer contains a serialization of the path where the circuit resources can be found (.r1cs, .wasm, .zkey and optionally the verification_key.json)
|
||||
## ctx holds the final created rln object
|
||||
## the return bool value indicates the success or failure of the operation
|
||||
# --- Keygen ---------------------------------------------------------------
|
||||
|
||||
# stateless version
|
||||
proc new_circuit*(ctx: ptr (ptr RLN)): bool {.importc: "new".}
|
||||
proc ffi_extended_key_gen*(): Vec_CFr {.importc: "ffi_extended_key_gen", cdecl.}
|
||||
|
||||
proc new_circuit_from_data*(
|
||||
zkey_buffer: ptr Buffer, graph_buffer: ptr Buffer, ctx: ptr (ptr RLN)
|
||||
): bool {.importc: "new_with_params".}
|
||||
proc ffi_seeded_extended_key_gen*(
|
||||
seed: ptr Vec_uint8
|
||||
): Vec_CFr {.importc: "ffi_seeded_extended_key_gen", cdecl.}
|
||||
|
||||
## creates an instance of rln object as defined by the zerokit RLN lib by passing the required inputs as byte arrays
|
||||
## zkey_buffer contains the bytes read from the .zkey proving key
|
||||
## graph_buffer contains the bytes read from the graph data file
|
||||
## ctx holds the final created rln object
|
||||
## the return bool value indicates the success or failure of the operation
|
||||
# --- Witness construction -------------------------------------------------
|
||||
|
||||
#-------------------------------- Hashing utils -------------------------------------------
|
||||
proc ffi_rln_witness_input_new*(
|
||||
identity_secret: ptr CFr,
|
||||
user_message_limit: ptr CFr,
|
||||
message_id: ptr CFr,
|
||||
path_elements: ptr Vec_CFr,
|
||||
identity_path_index: ptr Vec_uint8,
|
||||
x: ptr CFr,
|
||||
external_nullifier: ptr CFr,
|
||||
): CResultWitnessInputPtrVecU8 {.importc: "ffi_rln_witness_input_new", cdecl.}
|
||||
|
||||
proc sha256*(
|
||||
input_buffer: ptr Buffer, output_buffer: ptr Buffer, is_little_endian: bool
|
||||
): bool {.importc: "hash".}
|
||||
proc ffi_rln_witness_input_free*(
|
||||
witness: ptr FFI_RLNWitnessInput
|
||||
) {.importc: "ffi_rln_witness_input_free", cdecl.}
|
||||
|
||||
## it hashes (sha256) the plain text supplied in inputs_buffer and then maps it to a field element
|
||||
## this proc is used to map arbitrary signals to field element for the sake of proof generation
|
||||
## inputs_buffer holds the hash input as a byte seq
|
||||
## the hash output is generated and populated inside output_buffer
|
||||
## the output_buffer contains 32 bytes hash output
|
||||
proc ffi_rln_partial_witness_input_new*(
|
||||
identity_secret: ptr CFr,
|
||||
user_message_limit: ptr CFr,
|
||||
path_elements: ptr Vec_CFr,
|
||||
identity_path_index: ptr Vec_uint8,
|
||||
): CResultPartialWitnessInputPtrVecU8 {.
|
||||
importc: "ffi_rln_partial_witness_input_new", cdecl
|
||||
.}
|
||||
|
||||
proc poseidon*(
|
||||
input_buffer: ptr Buffer, output_buffer: ptr Buffer, is_little_endian: bool
|
||||
): bool {.importc: "poseidon_hash".}
|
||||
proc ffi_rln_partial_witness_input_free*(
|
||||
witness: ptr FFI_RLNPartialWitnessInput
|
||||
) {.importc: "ffi_rln_partial_witness_input_free", cdecl.}
|
||||
|
||||
## it hashes (poseidon) the plain text supplied in inputs_buffer
|
||||
## this proc is used to compute the identity secret hash, and external nullifier
|
||||
## inputs_buffer holds the hash input as a byte seq
|
||||
## the hash output is generated and populated inside output_buffer
|
||||
## the output_buffer contains 32 bytes hash output
|
||||
# --- Proof generation -----------------------------------------------------
|
||||
# safer-ffi's repr_c::Box<T> lands on the Nim side as `ptr ptr T`. Call sites
|
||||
# pass `addr handle` where `handle` is `ptr T`.
|
||||
|
||||
proc ffi_generate_rln_proof*(
|
||||
rln: ptr ptr RLN, witness: ptr ptr FFI_RLNWitnessInput
|
||||
): CResultProofPtrVecU8 {.importc: "ffi_generate_rln_proof", cdecl.}
|
||||
|
||||
proc ffi_generate_partial_zk_proof*(
|
||||
rln: ptr ptr RLN, partial_witness: ptr ptr FFI_RLNPartialWitnessInput
|
||||
): CResultPartialProofPtrVecU8 {.importc: "ffi_generate_partial_zk_proof", cdecl.}
|
||||
|
||||
proc ffi_finish_rln_proof*(
|
||||
rln: ptr ptr RLN,
|
||||
partial_proof: ptr ptr FFI_RLNPartialProof,
|
||||
witness: ptr ptr FFI_RLNWitnessInput,
|
||||
): CResultProofPtrVecU8 {.importc: "ffi_finish_rln_proof", cdecl.}
|
||||
|
||||
# --- Verification ---------------------------------------------------------
|
||||
|
||||
proc ffi_verify_with_roots*(
|
||||
rln: ptr ptr RLN, proof: ptr ptr FFI_RLNProof, roots: ptr Vec_CFr, x: ptr CFr
|
||||
): CBoolResult {.importc: "ffi_verify_with_roots", cdecl.}
|
||||
|
||||
# --- Proof serialization --------------------------------------------------
|
||||
|
||||
proc ffi_rln_proof_to_bytes_le*(
|
||||
proof: ptr ptr FFI_RLNProof
|
||||
): CResultVecU8VecU8 {.importc: "ffi_rln_proof_to_bytes_le", cdecl.}
|
||||
|
||||
proc ffi_bytes_le_to_rln_proof*(
|
||||
bytes: ptr Vec_uint8
|
||||
): CResultProofPtrVecU8 {.importc: "ffi_bytes_le_to_rln_proof", cdecl.}
|
||||
|
||||
# v2.0.2: construct an RLNProof directly from its field elements (single
|
||||
# message-id variant), avoiding the manual 290-byte wire layout.
|
||||
proc ffi_rln_proof_new*(
|
||||
groth16Bytes: ptr Vec_uint8,
|
||||
root: ptr CFr,
|
||||
externalNullifier: ptr CFr,
|
||||
x: ptr CFr,
|
||||
y: ptr CFr,
|
||||
nullifier: ptr CFr,
|
||||
): CResultProofPtrVecU8 {.importc: "ffi_rln_proof_new", cdecl.}
|
||||
|
||||
proc ffi_rln_proof_free*(p: ptr FFI_RLNProof) {.importc: "ffi_rln_proof_free", cdecl.}
|
||||
|
||||
proc ffi_rln_partial_proof_to_bytes_le*(
|
||||
partial_proof: ptr ptr FFI_RLNPartialProof
|
||||
): CResultVecU8VecU8 {.importc: "ffi_rln_partial_proof_to_bytes_le", cdecl.}
|
||||
|
||||
proc ffi_bytes_le_to_rln_partial_proof*(
|
||||
bytes: ptr Vec_uint8
|
||||
): CResultPartialProofPtrVecU8 {.importc: "ffi_bytes_le_to_rln_partial_proof", cdecl.}
|
||||
|
||||
proc ffi_rln_partial_proof_free*(
|
||||
p: ptr FFI_RLNPartialProof
|
||||
) {.importc: "ffi_rln_partial_proof_free", cdecl.}
|
||||
|
||||
# --- Proof values (extract root / x / y / nullifier from a proof) ---------
|
||||
|
||||
proc ffi_rln_proof_get_values*(
|
||||
proof: ptr ptr FFI_RLNProof
|
||||
): ptr FFI_RLNProofValues {.importc: "ffi_rln_proof_get_values", cdecl.}
|
||||
|
||||
proc ffi_rln_proof_values_get_root*(
|
||||
pv: ptr ptr FFI_RLNProofValues
|
||||
): ptr CFr {.importc: "ffi_rln_proof_values_get_root", cdecl.}
|
||||
|
||||
proc ffi_rln_proof_values_get_x*(
|
||||
pv: ptr ptr FFI_RLNProofValues
|
||||
): ptr CFr {.importc: "ffi_rln_proof_values_get_x", cdecl.}
|
||||
|
||||
proc ffi_rln_proof_values_get_external_nullifier*(
|
||||
pv: ptr ptr FFI_RLNProofValues
|
||||
): ptr CFr {.importc: "ffi_rln_proof_values_get_external_nullifier", cdecl.}
|
||||
|
||||
proc ffi_rln_proof_values_get_y*(
|
||||
pv: ptr ptr FFI_RLNProofValues
|
||||
): CResultCFrPtrVecU8 {.importc: "ffi_rln_proof_values_get_y", cdecl.}
|
||||
|
||||
proc ffi_rln_proof_values_get_nullifier*(
|
||||
pv: ptr ptr FFI_RLNProofValues
|
||||
): CResultCFrPtrVecU8 {.importc: "ffi_rln_proof_values_get_nullifier", cdecl.}
|
||||
|
||||
proc ffi_rln_proof_values_free*(
|
||||
pv: ptr FFI_RLNProofValues
|
||||
) {.importc: "ffi_rln_proof_values_free", cdecl.}
|
||||
|
||||
# --- Slashing -------------------------------------------------------------
|
||||
|
||||
proc ffi_compute_id_secret*(
|
||||
share1_x: ptr CFr, share1_y: ptr CFr, share2_x: ptr CFr, share2_y: ptr CFr
|
||||
): CResultCFrPtrVecU8 {.importc: "ffi_compute_id_secret", cdecl.}
|
||||
|
||||
# --- Primitives: CFr ------------------------------------------------------
|
||||
|
||||
proc ffi_cfr_zero*(): ptr CFr {.importc: "ffi_cfr_zero", cdecl.}
|
||||
|
||||
proc ffi_cfr_to_bytes_le*(
|
||||
cfr: ptr CFr
|
||||
): Vec_uint8 {.importc: "ffi_cfr_to_bytes_le", cdecl.}
|
||||
|
||||
proc ffi_bytes_le_to_cfr*(
|
||||
bytes: ptr Vec_uint8
|
||||
): CResultCFrPtrVecU8 {.importc: "ffi_bytes_le_to_cfr", cdecl.}
|
||||
|
||||
proc ffi_cfr_free*(cfr: ptr CFr) {.importc: "ffi_cfr_free", cdecl.}
|
||||
|
||||
# --- Primitives: Vec_CFr --------------------------------------------------
|
||||
|
||||
proc ffi_vec_cfr_new*(capacity: CSize): Vec_CFr {.importc: "ffi_vec_cfr_new", cdecl.}
|
||||
|
||||
proc ffi_vec_cfr_push*(
|
||||
v: ptr Vec_CFr, cfr: ptr CFr
|
||||
) {.importc: "ffi_vec_cfr_push", cdecl.}
|
||||
|
||||
proc ffi_vec_cfr_len*(v: ptr Vec_CFr): CSize {.importc: "ffi_vec_cfr_len", cdecl.}
|
||||
|
||||
proc ffi_vec_cfr_get*(
|
||||
v: ptr Vec_CFr, i: CSize
|
||||
): ptr CFr {.importc: "ffi_vec_cfr_get", cdecl.}
|
||||
|
||||
proc ffi_vec_cfr_free*(v: Vec_CFr) {.importc: "ffi_vec_cfr_free", cdecl.}
|
||||
|
||||
# --- Primitives: Vec_uint8 ------------------------------------------------
|
||||
|
||||
proc ffi_vec_u8_free*(v: Vec_uint8) {.importc: "ffi_vec_u8_free", cdecl.}
|
||||
|
||||
proc ffi_c_string_free*(s: Vec_uint8) {.importc: "ffi_c_string_free", cdecl.}
|
||||
|
||||
# --- Hash helpers ---------------------------------------------------------
|
||||
|
||||
proc ffi_hash_to_field_le*(
|
||||
input: ptr Vec_uint8
|
||||
): ptr CFr {.importc: "ffi_hash_to_field_le", cdecl.}
|
||||
|
||||
proc ffi_poseidon_hash_pair*(
|
||||
a: ptr CFr, b: ptr CFr
|
||||
): ptr CFr {.importc: "ffi_poseidon_hash_pair", cdecl.}
|
||||
|
||||
# --- Memory-hygiene helpers -------------------------------------------------
|
||||
|
||||
proc hasError*(data: Vec_uint8): bool =
|
||||
not data.dataPtr.isNil
|
||||
|
||||
proc asString*(data: Vec_uint8): string =
|
||||
if data.dataPtr.isNil or data.len == 0:
|
||||
return ""
|
||||
result = newString(int(data.len))
|
||||
copyMem(addr result[0], data.dataPtr, int(data.len))
|
||||
|
||||
proc consumeError*(prefix: string, data: Vec_uint8): string =
|
||||
## Read an error string out of a Rust-owned Vec_uint8 AND free it.
|
||||
let msg = asString(data)
|
||||
if hasError(data):
|
||||
ffi_c_string_free(data)
|
||||
if prefix.len == 0:
|
||||
msg
|
||||
elif msg.len == 0:
|
||||
prefix
|
||||
else:
|
||||
prefix & msg
|
||||
|
||||
proc toVecUint8*(data: openArray[byte]): Vec_uint8 =
|
||||
## Wrap Nim-owned bytes as a Vec_uint8 view. NOTE: the resulting Vec_uint8
|
||||
## must NOT be passed to ffi_vec_u8_free — Nim retains ownership.
|
||||
if data.len == 0:
|
||||
return Vec_uint8(dataPtr: nil, len: 0, cap: 0)
|
||||
Vec_uint8(
|
||||
dataPtr: cast[ptr uint8](unsafeAddr data[0]),
|
||||
len: CSize(data.len),
|
||||
cap: CSize(data.len),
|
||||
)
|
||||
|
||||
proc vecToSeq*(data: Vec_uint8): seq[byte] =
|
||||
result = newSeq[byte](int(data.len))
|
||||
if result.len > 0:
|
||||
copyMem(addr result[0], data.dataPtr, result.len)
|
||||
|
||||
proc seqToFixed32*(data: openArray[byte]): RlnRelayResult[array[32, byte]] =
|
||||
if data.len != FieldElementSize:
|
||||
return err("Expected 32 bytes, got " & $data.len)
|
||||
var output: array[32, byte]
|
||||
copyMem(addr output[0], unsafeAddr data[0], FieldElementSize)
|
||||
ok(output)
|
||||
|
||||
proc cfrToBytesLe*(cfr: ptr CFr): RlnRelayResult[array[32, byte]] =
|
||||
let bytes = ffi_cfr_to_bytes_le(cfr)
|
||||
defer:
|
||||
ffi_vec_u8_free(bytes)
|
||||
if int(bytes.len) != FieldElementSize:
|
||||
return err("Invalid field byte length: " & $bytes.len)
|
||||
seqToFixed32(vecToSeq(bytes))
|
||||
|
||||
proc bytesToCfrLe*(data: openArray[byte]): RlnRelayResult[ptr CFr] =
|
||||
## Allocate a ptr CFr from raw bytes. Caller MUST ffi_cfr_free(x).
|
||||
var vec = toVecUint8(data)
|
||||
let res = ffi_bytes_le_to_cfr(addr vec)
|
||||
if not res.ok.isNil:
|
||||
return ok(res.ok)
|
||||
err(consumeError("Failed to convert bytes to field: ", res.err))
|
||||
|
||||
proc cfrResultToBytes*(
|
||||
res: CResultCFrPtrVecU8, prefix: string
|
||||
): RlnRelayResult[array[32, byte]] =
|
||||
## Consume a CResultCFrPtrVecU8: read bytes if ok, free the CFr, or
|
||||
## propagate the error (also freeing the error string).
|
||||
if res.ok.isNil:
|
||||
return err(consumeError(prefix, res.err))
|
||||
defer:
|
||||
ffi_cfr_free(res.ok)
|
||||
cfrToBytesLe(res.ok)
|
||||
|
||||
proc hashToFieldLe*(data: openArray[byte]): RlnRelayResult[ptr CFr] =
|
||||
## Caller MUST ffi_cfr_free the returned ptr.
|
||||
var vec = toVecUint8(data)
|
||||
let cfr = ffi_hash_to_field_le(addr vec)
|
||||
if cfr.isNil:
|
||||
return err("Failed to hash to field")
|
||||
ok(cfr)
|
||||
|
||||
proc poseidonPairLe*(a, b: openArray[byte]): RlnRelayResult[array[32, byte]] =
|
||||
## Poseidon hash of exactly two 32-byte field elements (little-endian).
|
||||
## zerokit v2 FFI only exposes pair-input Poseidon; unary is not supported.
|
||||
let aPtr = bytesToCfrLe(a).valueOr:
|
||||
return err(error)
|
||||
defer:
|
||||
ffi_cfr_free(aPtr)
|
||||
let bPtr = bytesToCfrLe(b).valueOr:
|
||||
return err(error)
|
||||
defer:
|
||||
ffi_cfr_free(bPtr)
|
||||
let cfr = ffi_poseidon_hash_pair(aPtr, bPtr)
|
||||
if cfr.isNil:
|
||||
return err("Poseidon hash failed")
|
||||
defer:
|
||||
ffi_cfr_free(cfr)
|
||||
cfrToBytesLe(cfr)
|
||||
|
||||
@ -1,140 +1,149 @@
|
||||
import std/json
|
||||
import
|
||||
chronicles,
|
||||
options,
|
||||
eth/keys,
|
||||
stew/[arrayops, byteutils, endians2],
|
||||
stint,
|
||||
results,
|
||||
std/[sequtils, strutils, tables],
|
||||
nimcrypto/keccak as keccak
|
||||
import chronicles, eth/keys, stew/[arrayops, endians2], stint, results
|
||||
|
||||
import ./rln_interface, ../conversion_utils, ../protocol_types, ../protocol_metrics
|
||||
import ../../waku_core, ../../waku_keystore
|
||||
|
||||
{.push raises: [], gcsafe.}
|
||||
|
||||
logScope:
|
||||
topics = "waku rln_relay ffi"
|
||||
|
||||
proc membershipKeyGen*(): RlnRelayResult[IdentityCredential] =
|
||||
## generates a IdentityCredential that can be used for the registration into the rln membership contract
|
||||
## Returns an error if the key generation fails
|
||||
# Forward decl; body defined below.
|
||||
proc generateExternalNullifier*(
|
||||
epoch: Epoch, rlnIdentifier: RlnIdentifier
|
||||
): RlnRelayResult[ExternalNullifier]
|
||||
|
||||
# keysBufferPtr will hold the generated identity tuple i.e., trapdoor, nullifier, secret hash and commitment
|
||||
var
|
||||
keysBuffer: Buffer
|
||||
keysBufferPtr = addr(keysBuffer)
|
||||
done = key_gen(keysBufferPtr, true)
|
||||
proc toRootVec(validRoots: seq[MerkleNode]): RlnRelayResult[Vec_CFr] =
|
||||
## Caller MUST ffi_vec_cfr_free the returned Vec_CFr.
|
||||
var roots = ffi_vec_cfr_new(csize_t(validRoots.len))
|
||||
for root in validRoots:
|
||||
let cfr = bytesToCfrLe(root).valueOr:
|
||||
ffi_vec_cfr_free(roots)
|
||||
return err("failed call to bytesToCfrLe in toRootVec: " & error)
|
||||
ffi_vec_cfr_push(addr roots, cfr)
|
||||
ffi_cfr_free(cfr)
|
||||
ok(roots)
|
||||
|
||||
# check whether the keys are generated successfully
|
||||
if (done == false):
|
||||
return err("error in key generation")
|
||||
proc proofPtrToRateLimitProof(
|
||||
proofPtr: ptr FFI_RLNProof, epoch: Epoch, rlnIdentifier: RlnIdentifier
|
||||
): RlnRelayResult[RateLimitProof] =
|
||||
var proofHandle = proofPtr
|
||||
let proofBytesRes = ffi_rln_proof_to_bytes_le(addr proofHandle)
|
||||
if hasError(proofBytesRes.err):
|
||||
return err(consumeError("Failed to serialize proof: ", proofBytesRes.err))
|
||||
defer:
|
||||
ffi_vec_u8_free(proofBytesRes.ok)
|
||||
|
||||
if (keysBuffer.len != 4 * 32):
|
||||
return err("keysBuffer is of invalid length")
|
||||
let serialized = vecToSeq(proofBytesRes.ok)
|
||||
if serialized.len < RlnProofWireSize:
|
||||
return err("Serialized proof too short: " & $serialized.len)
|
||||
|
||||
var generatedKeys = cast[ptr array[4 * 32, byte]](keysBufferPtr.`ptr`)[]
|
||||
# the public and secret keys together are 64 bytes
|
||||
let proofValues = ffi_rln_proof_get_values(addr proofHandle)
|
||||
if proofValues.isNil():
|
||||
return err("Failed to extract proof values")
|
||||
defer:
|
||||
ffi_rln_proof_values_free(proofValues)
|
||||
|
||||
# TODO define a separate proc to decode the generated keys to the secret and public components
|
||||
var
|
||||
idTrapdoor: array[32, byte]
|
||||
idNullifier: array[32, byte]
|
||||
idSecretHash: array[32, byte]
|
||||
idCommitment: array[32, byte]
|
||||
for (i, x) in idTrapdoor.mpairs:
|
||||
x = generatedKeys[i + 0 * 32]
|
||||
for (i, x) in idNullifier.mpairs:
|
||||
x = generatedKeys[i + 1 * 32]
|
||||
for (i, x) in idSecretHash.mpairs:
|
||||
x = generatedKeys[i + 2 * 32]
|
||||
for (i, x) in idCommitment.mpairs:
|
||||
x = generatedKeys[i + 3 * 32]
|
||||
var output: RateLimitProof
|
||||
output.epoch = epoch
|
||||
output.rlnIdentifier = rlnIdentifier
|
||||
|
||||
var identityCredential = IdentityCredential(
|
||||
idTrapdoor: @idTrapdoor,
|
||||
idNullifier: @idNullifier,
|
||||
idSecretHash: @idSecretHash,
|
||||
idCommitment: @idCommitment,
|
||||
# zkSNARK bytes: skip the leading version byte, take 128.
|
||||
copyMem(addr output.proof[0], unsafeAddr serialized[1], ZksnarkProofSize)
|
||||
|
||||
var pvHandle = proofValues
|
||||
|
||||
let rootPtr = ffi_rln_proof_values_get_root(addr pvHandle)
|
||||
if rootPtr.isNil():
|
||||
return err("Failed to read proof root")
|
||||
defer:
|
||||
ffi_cfr_free(rootPtr)
|
||||
output.merkleRoot = cfrToBytesLe(rootPtr).valueOr:
|
||||
return
|
||||
err("failed call to cfrToBytesLe (root) in proofPtrToRateLimitProof: " & error)
|
||||
|
||||
let xPtr = ffi_rln_proof_values_get_x(addr pvHandle)
|
||||
if xPtr.isNil():
|
||||
return err("Failed to read proof x")
|
||||
defer:
|
||||
ffi_cfr_free(xPtr)
|
||||
output.shareX = cfrToBytesLe(xPtr).valueOr:
|
||||
return
|
||||
err("failed call to cfrToBytesLe (shareX) in proofPtrToRateLimitProof: " & error)
|
||||
|
||||
let yRes = ffi_rln_proof_values_get_y(addr pvHandle)
|
||||
output.shareY = cfrResultToBytes(yRes, "Failed to read proof y: ").valueOr:
|
||||
return err(error)
|
||||
|
||||
let nullifierRes = ffi_rln_proof_values_get_nullifier(addr pvHandle)
|
||||
output.nullifier = cfrResultToBytes(nullifierRes, "Failed to read proof nullifier: ").valueOr:
|
||||
return err(error)
|
||||
|
||||
let extNullPtr = ffi_rln_proof_values_get_external_nullifier(addr pvHandle)
|
||||
if extNullPtr.isNil():
|
||||
return err("Failed to read proof external nullifier")
|
||||
defer:
|
||||
ffi_cfr_free(extNullPtr)
|
||||
output.externalNullifier = cfrToBytesLe(extNullPtr).valueOr:
|
||||
return err(
|
||||
"failed call to cfrToBytesLe (externalNullifier) in proofPtrToRateLimitProof: " &
|
||||
error
|
||||
)
|
||||
|
||||
ok(output)
|
||||
|
||||
proc parseCredentialVec(vec: var Vec_CFr): RlnRelayResult[IdentityCredential] =
|
||||
## Vec_CFr order: idTrapdoor, idNullifier, idSecretHash, idCommitment.
|
||||
if int(ffi_vec_cfr_len(addr vec)) != 4:
|
||||
return err("Unexpected credential element count")
|
||||
|
||||
template readField(idx: int): seq[byte] =
|
||||
let f = ffi_vec_cfr_get(addr vec, csize_t(idx))
|
||||
if f.isNil():
|
||||
return err("Missing credential field from zerokit")
|
||||
let bytes = cfrToBytesLe(f).valueOr:
|
||||
return err("failed call to cfrToBytesLe in parseCredentialVec: " & error)
|
||||
@bytes
|
||||
|
||||
let idTrapdoor = readField(0)
|
||||
let idNullifier = readField(1)
|
||||
let idSecretHash = readField(2)
|
||||
let idCommitment = readField(3)
|
||||
|
||||
return ok(
|
||||
IdentityCredential(
|
||||
idTrapdoor: idTrapdoor,
|
||||
idNullifier: idNullifier,
|
||||
idSecretHash: idSecretHash,
|
||||
idCommitment: idCommitment,
|
||||
)
|
||||
)
|
||||
|
||||
return ok(identityCredential)
|
||||
|
||||
type RlnTreeConfig = ref object of RootObj
|
||||
cache_capacity: int
|
||||
mode: string
|
||||
compression: bool
|
||||
flush_every_ms: int
|
||||
|
||||
type RlnConfig = ref object of RootObj
|
||||
resources_folder: string
|
||||
tree_config: RlnTreeConfig
|
||||
|
||||
proc `%`(c: RlnConfig): JsonNode =
|
||||
## wrapper around the generic JObject constructor.
|
||||
## We don't need to have a separate proc for the tree_config field
|
||||
let tree_config = %{
|
||||
"cache_capacity": %c.tree_config.cache_capacity,
|
||||
"mode": %c.tree_config.mode,
|
||||
"compression": %c.tree_config.compression,
|
||||
"flush_every_ms": %c.tree_config.flush_every_ms,
|
||||
}
|
||||
return %[("resources_folder", %c.resources_folder), ("tree_config", %tree_config)]
|
||||
proc membershipKeyGen*(): RlnRelayResult[IdentityCredential] =
|
||||
var vec = ffi_extended_key_gen()
|
||||
defer:
|
||||
ffi_vec_cfr_free(vec)
|
||||
parseCredentialVec(vec)
|
||||
|
||||
proc createRLNInstanceLocal(): RLNResult =
|
||||
## generates an instance of RLN
|
||||
## An RLN instance supports both zkSNARKs logics and Merkle tree data structure and operations
|
||||
## Returns an error if the instance creation fails
|
||||
|
||||
let rln_config = RlnConfig(
|
||||
resources_folder: "tree_height_/",
|
||||
tree_config: RlnTreeConfig(
|
||||
cache_capacity: 15_000,
|
||||
mode: "high_throughput",
|
||||
compression: false,
|
||||
flush_every_ms: 500,
|
||||
),
|
||||
)
|
||||
|
||||
var serialized_rln_config = $(%rln_config)
|
||||
|
||||
var
|
||||
rlnInstance: ptr RLN
|
||||
merkleDepth: csize_t = uint(20)
|
||||
configBuffer =
|
||||
serialized_rln_config.toOpenArrayByte(0, serialized_rln_config.high).toBuffer()
|
||||
|
||||
# create an instance of RLN
|
||||
let res = new_circuit(merkleDepth, addr configBuffer, addr rlnInstance)
|
||||
# check whether the circuit parameters are generated successfully
|
||||
if (res == false):
|
||||
info "error in parameters generation"
|
||||
return err("error in parameters generation")
|
||||
return ok(rlnInstance)
|
||||
## Creates a stateless RLN instance (no local Merkle tree).
|
||||
let res = ffi_rln_new()
|
||||
if res.ok.isNil():
|
||||
let msg = consumeError("error in parameters generation: ", res.err)
|
||||
info "error in parameters generation", err = msg
|
||||
return err(msg)
|
||||
ok(res.ok)
|
||||
|
||||
proc createRLNInstance*(): RLNResult =
|
||||
## Wraps the rln instance creation for metrics
|
||||
## Returns an error if the instance creation fails
|
||||
## Wraps createRLNInstanceLocal with metrics timing.
|
||||
var res: RLNResult
|
||||
waku_rln_instance_creation_duration_seconds.nanosecondTime:
|
||||
res = createRLNInstanceLocal()
|
||||
return res
|
||||
|
||||
proc poseidon*(data: seq[seq[byte]]): RlnRelayResult[array[32, byte]] =
|
||||
## a thin layer on top of the Nim wrapper of the poseidon hasher
|
||||
var inputBytes = serialize(data)
|
||||
var
|
||||
hashInputBuffer = inputBytes.toBuffer()
|
||||
outputBuffer: Buffer # will holds the hash output
|
||||
|
||||
let hashSuccess = poseidon(addr hashInputBuffer, addr outputBuffer, true)
|
||||
|
||||
# check whether the hash call is done successfully
|
||||
if not hashSuccess:
|
||||
return err("error in poseidon hash")
|
||||
|
||||
let output = cast[ptr array[32, byte]](outputBuffer.`ptr`)[]
|
||||
|
||||
return ok(output)
|
||||
proc poseidon*(left, right: seq[byte]): RlnRelayResult[array[32, byte]] =
|
||||
## Poseidon hash of exactly 2 inputs; zerokit v2 FFI only exposes the pair variant.
|
||||
poseidonPairLe(left, right)
|
||||
|
||||
proc toLeaf*(rateCommitment: RateCommitment): RlnRelayResult[seq[byte]] =
|
||||
let idCommitment = rateCommitment.idCommitment
|
||||
@ -147,7 +156,7 @@ proc toLeaf*(rateCommitment: RateCommitment): RlnRelayResult[seq[byte]] =
|
||||
return err(
|
||||
"could not convert the user message limit to bytes: " & getCurrentExceptionMsg()
|
||||
)
|
||||
let leaf = poseidon(@[@idCommitment, @userMessageLimit]).valueOr:
|
||||
let leaf = poseidon(@idCommitment, @userMessageLimit).valueOr:
|
||||
return err("could not convert the rate commitment to a leaf")
|
||||
var retLeaf = newSeq[byte](leaf.len)
|
||||
for i in 0 ..< leaf.len:
|
||||
@ -165,11 +174,24 @@ proc toLeaves*(rateCommitments: seq[RateCommitment]): RlnRelayResult[seq[seq[byt
|
||||
proc generateExternalNullifier*(
|
||||
epoch: Epoch, rlnIdentifier: RlnIdentifier
|
||||
): RlnRelayResult[ExternalNullifier] =
|
||||
let epochHash = keccak.keccak256.digest(@(epoch))
|
||||
let rlnIdentifierHash = keccak.keccak256.digest(@(rlnIdentifier))
|
||||
let externalNullifier = poseidon(@[@(epochHash), @(rlnIdentifierHash)]).valueOr:
|
||||
return err("Failed to compute external nullifier: " & error)
|
||||
return ok(externalNullifier)
|
||||
## externalNullifier = Poseidon(H(epoch), H(rlnIdentifier)); H = ffi_hash_to_field_le.
|
||||
let epochFr = hashToFieldLe(@epoch).valueOr:
|
||||
return err("Failed to hash epoch to field: " & error)
|
||||
defer:
|
||||
ffi_cfr_free(epochFr)
|
||||
let rlnIdFr = hashToFieldLe(@rlnIdentifier).valueOr:
|
||||
return err("Failed to hash rlnIdentifier to field: " & error)
|
||||
defer:
|
||||
ffi_cfr_free(rlnIdFr)
|
||||
let cfr = ffi_poseidon_hash_pair(epochFr, rlnIdFr)
|
||||
if cfr.isNil():
|
||||
return err("Failed to compute external nullifier")
|
||||
defer:
|
||||
ffi_cfr_free(cfr)
|
||||
cfrToBytesLe(cfr).mapErr(
|
||||
proc(e: string): string =
|
||||
"Failed to serialize external nullifier: " & e
|
||||
)
|
||||
|
||||
proc extractMetadata*(proof: RateLimitProof): RlnRelayResult[ProofMetadata] =
|
||||
let externalNullifier = generateExternalNullifier(proof.epoch, proof.rlnIdentifier).valueOr:
|
||||
@ -182,3 +204,178 @@ proc extractMetadata*(proof: RateLimitProof): RlnRelayResult[ProofMetadata] =
|
||||
externalNullifier: externalNullifier,
|
||||
)
|
||||
)
|
||||
|
||||
proc buildPathElementsVec(
|
||||
pathElements: seq[byte], depth: int
|
||||
): RlnRelayResult[Vec_CFr] =
|
||||
## Caller MUST ffi_vec_cfr_free the returned Vec_CFr.
|
||||
var vec = ffi_vec_cfr_new(csize_t(depth))
|
||||
for i in 0 ..< depth:
|
||||
let start = i * FieldElementSize
|
||||
let element = bytesToCfrLe(
|
||||
pathElements.toOpenArray(start, start + FieldElementSize - 1)
|
||||
).valueOr:
|
||||
ffi_vec_cfr_free(vec)
|
||||
return err(
|
||||
"failed call to bytesToCfrLe (path element) in buildPathElementsVec: " & error
|
||||
)
|
||||
ffi_vec_cfr_push(addr vec, element)
|
||||
ffi_cfr_free(element)
|
||||
ok(vec)
|
||||
|
||||
proc buildWitnessInput(
|
||||
witness: RLNWitnessInput
|
||||
): RlnRelayResult[ptr FFI_RLNWitnessInput] =
|
||||
## ffi_rln_witness_input_new copies all inputs, so the intermediate CFrs/vecs
|
||||
## are freed here. Caller MUST ffi_rln_witness_input_free the returned handle.
|
||||
let depth = witness.identity_path_index.len
|
||||
if witness.path_elements.len != depth * FieldElementSize:
|
||||
return err(
|
||||
"Invalid Merkle path: expected " & $(depth * FieldElementSize) & " bytes for " &
|
||||
$depth & " levels, got " & $witness.path_elements.len
|
||||
)
|
||||
|
||||
var pathElementsVec = buildPathElementsVec(witness.path_elements, depth).valueOr:
|
||||
return err("failed call to buildPathElementsVec in buildWitnessInput: " & error)
|
||||
defer:
|
||||
ffi_vec_cfr_free(pathElementsVec)
|
||||
|
||||
var pathIndexVec = toVecUint8(witness.identity_path_index)
|
||||
|
||||
let identitySecret = bytesToCfrLe(witness.identity_secret).valueOr:
|
||||
return err(
|
||||
"failed call to bytesToCfrLe (identity_secret) in buildWitnessInput: " & error
|
||||
)
|
||||
defer:
|
||||
ffi_cfr_free(identitySecret)
|
||||
let userLimit = bytesToCfrLe(witness.user_message_limit).valueOr:
|
||||
return err(
|
||||
"failed call to bytesToCfrLe (user_message_limit) in buildWitnessInput: " & error
|
||||
)
|
||||
defer:
|
||||
ffi_cfr_free(userLimit)
|
||||
let messageIdFr = bytesToCfrLe(witness.message_id).valueOr:
|
||||
return
|
||||
err("failed call to bytesToCfrLe (message_id) in buildWitnessInput: " & error)
|
||||
defer:
|
||||
ffi_cfr_free(messageIdFr)
|
||||
let xFr = bytesToCfrLe(witness.x).valueOr:
|
||||
return err("failed call to bytesToCfrLe (x) in buildWitnessInput: " & error)
|
||||
defer:
|
||||
ffi_cfr_free(xFr)
|
||||
let externalNullifierFr = bytesToCfrLe(witness.external_nullifier).valueOr:
|
||||
return err(
|
||||
"failed call to bytesToCfrLe (external_nullifier) in buildWitnessInput: " & error
|
||||
)
|
||||
defer:
|
||||
ffi_cfr_free(externalNullifierFr)
|
||||
|
||||
let witnessRes = ffi_rln_witness_input_new(
|
||||
identitySecret,
|
||||
userLimit,
|
||||
messageIdFr,
|
||||
addr pathElementsVec,
|
||||
addr pathIndexVec,
|
||||
xFr,
|
||||
externalNullifierFr,
|
||||
)
|
||||
if witnessRes.ok.isNil():
|
||||
return err(
|
||||
consumeError("Failed to create witness in buildWitnessInput: ", witnessRes.err)
|
||||
)
|
||||
return ok(witnessRes.ok)
|
||||
|
||||
proc generateRlnProofWithWitness*(
|
||||
rlnInstance: ptr RLN,
|
||||
witness: RLNWitnessInput,
|
||||
epoch: Epoch,
|
||||
rlnIdentifier: RlnIdentifier,
|
||||
): RlnRelayResult[RateLimitProof] =
|
||||
let witnessHandle = buildWitnessInput(witness).valueOr:
|
||||
return
|
||||
err("failed call to buildWitnessInput in generateRlnProofWithWitness: " & error)
|
||||
defer:
|
||||
ffi_rln_witness_input_free(witnessHandle)
|
||||
|
||||
var ctx = rlnInstance
|
||||
var wh = witnessHandle
|
||||
let proofRes = ffi_generate_rln_proof(addr ctx, addr wh)
|
||||
if proofRes.ok.isNil():
|
||||
return err(consumeError("Failed to generate RLN proof: ", proofRes.err))
|
||||
defer:
|
||||
ffi_rln_proof_free(proofRes.ok)
|
||||
|
||||
return proofPtrToRateLimitProof(proofRes.ok, epoch, rlnIdentifier)
|
||||
|
||||
proc buildRlnProof(
|
||||
proof: RateLimitProof, externalNullifier: ExternalNullifier
|
||||
): RlnRelayResult[ptr FFI_RLNProof] =
|
||||
## ffi_rln_proof_new copies all inputs, so the intermediate CFrs are freed
|
||||
## here. Caller MUST ffi_rln_proof_free the returned handle.
|
||||
var groth16Vec = toVecUint8(proof.proof)
|
||||
let rootFr = bytesToCfrLe(proof.merkleRoot).valueOr:
|
||||
return err("failed call to bytesToCfrLe (root) in buildRlnProof: " & error)
|
||||
defer:
|
||||
ffi_cfr_free(rootFr)
|
||||
let extNullFr = bytesToCfrLe(externalNullifier).valueOr:
|
||||
return
|
||||
err("failed call to bytesToCfrLe (externalNullifier) in buildRlnProof: " & error)
|
||||
defer:
|
||||
ffi_cfr_free(extNullFr)
|
||||
let shareXFr = bytesToCfrLe(proof.shareX).valueOr:
|
||||
return err("failed call to bytesToCfrLe (shareX) in buildRlnProof: " & error)
|
||||
defer:
|
||||
ffi_cfr_free(shareXFr)
|
||||
let shareYFr = bytesToCfrLe(proof.shareY).valueOr:
|
||||
return err("failed call to bytesToCfrLe (shareY) in buildRlnProof: " & error)
|
||||
defer:
|
||||
ffi_cfr_free(shareYFr)
|
||||
let nullifierFr = bytesToCfrLe(proof.nullifier).valueOr:
|
||||
return err("failed call to bytesToCfrLe (nullifier) in buildRlnProof: " & error)
|
||||
defer:
|
||||
ffi_cfr_free(nullifierFr)
|
||||
|
||||
let proofRes = ffi_rln_proof_new(
|
||||
addr groth16Vec, rootFr, extNullFr, shareXFr, shareYFr, nullifierFr
|
||||
)
|
||||
if proofRes.ok.isNil():
|
||||
return
|
||||
err(consumeError("Failed to build RLN proof in buildRlnProof: ", proofRes.err))
|
||||
return ok(proofRes.ok)
|
||||
|
||||
proc verifyRlnProof*(
|
||||
rlnInstance: ptr RLN,
|
||||
proof: RateLimitProof,
|
||||
signal: openArray[byte],
|
||||
validRoots: seq[MerkleNode],
|
||||
): RlnRelayResult[bool] =
|
||||
if validRoots.len == 0:
|
||||
return err("verifyRlnProof requires at least one valid root (stateless mode)")
|
||||
|
||||
# externalNullifier isn't a protobuf wire field, so a received proof has it
|
||||
# zeroed; recompute from epoch + rlnIdentifier.
|
||||
let externalNullifier = generateExternalNullifier(proof.epoch, proof.rlnIdentifier).valueOr:
|
||||
return err("failed call to generateExternalNullifier in verifyRlnProof: " & error)
|
||||
|
||||
let proofHandlePtr = buildRlnProof(proof, externalNullifier).valueOr:
|
||||
return err("failed call to buildRlnProof in verifyRlnProof: " & error)
|
||||
defer:
|
||||
ffi_rln_proof_free(proofHandlePtr)
|
||||
|
||||
let xFr = hashToFieldLe(signal).valueOr:
|
||||
return err("failed call to hashToFieldLe (signal) in verifyRlnProof: " & error)
|
||||
defer:
|
||||
ffi_cfr_free(xFr)
|
||||
|
||||
var roots = toRootVec(validRoots).valueOr:
|
||||
return err("failed call to toRootVec in verifyRlnProof: " & error)
|
||||
defer:
|
||||
ffi_vec_cfr_free(roots)
|
||||
|
||||
var ctx = rlnInstance
|
||||
var proofHandle = proofHandlePtr
|
||||
let verifyRes = ffi_verify_with_roots(addr ctx, addr proofHandle, addr roots, xFr)
|
||||
# zerokit FFI quirk: err is non-nil for all failures; free it and return the bool.
|
||||
if hasError(verifyRes.err):
|
||||
ffi_c_string_free(verifyRes.err)
|
||||
return ok(verifyRes.ok)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user