feat: migrate to zerokit v2.0.2 (#3868)

This commit is contained in:
Darshan 2026-05-21 17:31:03 +05:30 committed by GitHub
parent c6e448a0ba
commit eb1891dc0e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 889 additions and 545 deletions

View File

@ -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
View File

@ -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"
}
}

View File

@ -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 {

View File

@ -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}"

View File

@ -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,

View File

@ -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.

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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

@ -1 +1 @@
Subproject commit a4bb3feb5054e6fd24827adf204493e6e173437b
Subproject commit 5e64cb8822bee65eed6cf459f95ae72b80c6ba63

View File

@ -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 = [

View File

@ -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] = @[]

View File

@ -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.} =

View File

@ -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,

View File

@ -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)

View File

@ -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)