From eb1891dc0e470dfa9612158728298b358077b907 Mon Sep 17 00:00:00 2001 From: Darshan <35736874+darshankabariya@users.noreply.github.com> Date: Thu, 21 May 2026 17:31:03 +0530 Subject: [PATCH] feat: migrate to zerokit v2.0.2 (#3868) --- Makefile | 2 +- flake.lock | 6 +- flake.nix | 38 +- scripts/build_rln.sh | 12 +- .../waku_lightpush_legacy/lightpush_utils.nim | 5 +- tests/waku_rln_relay/rln/buffer_utils.nim | 15 +- .../waku_rln_relay/rln/test_rln_interface.nim | 37 +- tests/waku_rln_relay/rln/test_wrappers.nim | 117 +---- .../test_rln_group_manager_onchain.nim | 86 +++- tests/waku_rln_relay/test_waku_rln_relay.nim | 45 +- vendor/zerokit | 2 +- waku/waku_rln_relay/constants.nim | 2 - waku/waku_rln_relay/conversion_utils.nim | 42 -- .../group_manager/on_chain/group_manager.nim | 104 +--- waku/waku_rln_relay/protocol_metrics.nim | 2 +- waku/waku_rln_relay/rln/rln_interface.nim | 484 +++++++++++++----- waku/waku_rln_relay/rln/wrappers.nim | 435 +++++++++++----- 17 files changed, 889 insertions(+), 545 deletions(-) diff --git a/Makefile b/Makefile index be9e14027..f147c5e7e 100644 --- a/Makefile +++ b/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 diff --git a/flake.lock b/flake.lock index 9b5db728d..8d0db9269 100644 --- a/flake.lock +++ b/flake.lock @@ -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" } } diff --git a/flake.nix b/flake.nix index b99eff6cd..8012fc970 100644 --- a/flake.nix +++ b/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 { diff --git a/scripts/build_rln.sh b/scripts/build_rln.sh index b36ebe807..35b5b8953 100755 --- a/scripts/build_rln.sh +++ b/scripts/build_rln.sh @@ -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}" diff --git a/tests/waku_lightpush_legacy/lightpush_utils.nim b/tests/waku_lightpush_legacy/lightpush_utils.nim index 11c4bf929..d5602173a 100644 --- a/tests/waku_lightpush_legacy/lightpush_utils.nim +++ b/tests/waku_lightpush_legacy/lightpush_utils.nim @@ -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, diff --git a/tests/waku_rln_relay/rln/buffer_utils.nim b/tests/waku_rln_relay/rln/buffer_utils.nim index e38cc5c17..a5ef921f1 100644 --- a/tests/waku_rln_relay/rln/buffer_utils.nim +++ b/tests/waku_rln_relay/rln/buffer_utils.nim @@ -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. diff --git a/tests/waku_rln_relay/rln/test_rln_interface.nim b/tests/waku_rln_relay/rln/test_rln_interface.nim index 7aedf587f..7b8ea3878 100644 --- a/tests/waku_rln_relay/rln/test_rln_interface.nim +++ b/tests/waku_rln_relay/rln/test_rln_interface.nim @@ -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() diff --git a/tests/waku_rln_relay/rln/test_wrappers.nim b/tests/waku_rln_relay/rln/test_wrappers.nim index 29e24aae5..8cd9251c0 100644 --- a/tests/waku_rln_relay/rln/test_wrappers.nim +++ b/tests/waku_rln_relay/rln/test_wrappers.nim @@ -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 diff --git a/tests/waku_rln_relay/test_rln_group_manager_onchain.nim b/tests/waku_rln_relay/test_rln_group_manager_onchain.nim index 29da94129..6b5b81532 100644 --- a/tests/waku_rln_relay/test_rln_group_manager_onchain.nim +++ b/tests/waku_rln_relay/test_rln_group_manager_onchain.nim @@ -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 diff --git a/tests/waku_rln_relay/test_waku_rln_relay.nim b/tests/waku_rln_relay/test_waku_rln_relay.nim index 099226b76..7694b8112 100644 --- a/tests/waku_rln_relay/test_waku_rln_relay.nim +++ b/tests/waku_rln_relay/test_waku_rln_relay.nim @@ -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": diff --git a/vendor/zerokit b/vendor/zerokit index a4bb3feb5..5e64cb882 160000 --- a/vendor/zerokit +++ b/vendor/zerokit @@ -1 +1 @@ -Subproject commit a4bb3feb5054e6fd24827adf204493e6e173437b +Subproject commit 5e64cb8822bee65eed6cf459f95ae72b80c6ba63 diff --git a/waku/waku_rln_relay/constants.nim b/waku/waku_rln_relay/constants.nim index 3e4757537..8532abaaa 100644 --- a/waku/waku_rln_relay/constants.nim +++ b/waku/waku_rln_relay/constants.nim @@ -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 = [ diff --git a/waku/waku_rln_relay/conversion_utils.nim b/waku/waku_rln_relay/conversion_utils.nim index 4a168ebeb..fc130621b 100644 --- a/waku/waku_rln_relay/conversion_utils.nim +++ b/waku/waku_rln_relay/conversion_utils.nim @@ -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 ] - 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] = @[] diff --git a/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim b/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim index 38c533029..02317a056 100644 --- a/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim +++ b/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim @@ -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.} = diff --git a/waku/waku_rln_relay/protocol_metrics.nim b/waku/waku_rln_relay/protocol_metrics.nim index 1551f022e..2cea329fe 100644 --- a/waku/waku_rln_relay/protocol_metrics.nim +++ b/waku/waku_rln_relay/protocol_metrics.nim @@ -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, diff --git a/waku/waku_rln_relay/rln/rln_interface.nim b/waku/waku_rln_relay/rln/rln_interface.nim index 0bb0ef6b0..612d1a2cc 100644 --- a/waku/waku_rln_relay/rln/rln_interface.nim +++ b/waku/waku_rln_relay/rln/rln_interface.nim @@ -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 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 ] -## 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 ] -## 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> | identity_path_index> | 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> | identity_path_index> | 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 ] -## 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 ] -## 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 ] -## 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 ] -## 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> | identity_path_index> | x<32> | external_nullifier<32> ] -## rln-v1 -## input_buffer is serialized as input_data as [ id_key<32> | path_elements> | identity_path_index> | 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 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) diff --git a/waku/waku_rln_relay/rln/wrappers.nim b/waku/waku_rln_relay/rln/wrappers.nim index f6f001d70..4fc8c1542 100644 --- a/waku/waku_rln_relay/rln/wrappers.nim +++ b/waku/waku_rln_relay/rln/wrappers.nim @@ -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)