Skip to content

Commit 1125960

Browse files
feat: use stateless rln (#9)
* feat(rln): replace pmtree backend with Nim IMT for stateless zerokit Move Merkle tree state from zerokit's pmtree-ft to a pure-Nim IncrementalMerkleTree (depth-20 Poseidon). Lets the plugin build against zerokit --features stateless and share a single archive with relay -- eliminates the dual-archive symbol collision after zerokit's v0.9->v2.0 FFI rename. Architecture matches zerokit's documented stateless pattern ("stateless RLN and separate Merkle tree", rln-cli/README.md). Byte-parity with pmtree locked down by a 61-op fixture captured from the default-features build, plus delete/sparse-setLeaf/idempotency/ out-of-capacity coverage. * chore(nimble): bump nim-libp2p to d4cd68b91b Aligns with nim-libp2p-mix's libp2p pin so the diamond dependency (waku -> {libp2p, nim-libp2p-mix -> libp2p}) resolves to a single libp2p source. d4cd68b91b is 14 commits past b16873f07, which means the boringssl URL+range form is in place -- nimble vnext SAT can solve without --solver:legacy. No code changes; plugin only uses libp2p/protobuf/minprotobuf and libp2p/varint, neither of which changed in the e1bbda4f6..d4cd68b91b window. All 29 plugin tests still green. * chore(nimble): bump nim-libp2p-mix pin to 99d2ca0 nim-libp2p-mix#f24cd25a (its master tip) still pins libp2p#7e72c0d6, which uses the pre-fix URL+tag form for boringssl and triggers the nimble vnext SAT bug at any downstream consumer that also requires modern libp2p (URL+range form). 99d2ca0 (chore/bump-libp2p-master tip on origin) bumps that pin to libp2p#d4cd68b91b -- same as our pin in the prior commit -- so the constraint graph is consistent end-to-end. Carries PR #10's Rng-type migration; does not affect this plugin (only libp2p_mix/spam_protection is imported and its API surface is unchanged). All 29 plugin tests green against the new pin. * chore(stateless-rln): drop pmtree-parity scaffolding, keep IMT regression test capture_golden.nim and the pmtree-derived golden fixture served as the one-time migration acceptance criterion; capture_golden.nim only compiled against non-stateless zerokit and is now uncompilable in the plugin's build. The IMT is internal-only, so pmtree parity is no longer a load-bearing invariant — what we need is regression protection for the Nim IMT itself, which the renamed test still provides. * chore(nimble): bump libp2p to release/v2.0.0 tip + libp2p-mix to v2.0 bump - libp2p pin: d4cd68b91b → c43199378 (release/v2.0.0 tip; 3 patch commits past the v2.0.0 bump). SHA-pinned because vacp2p/nim-libp2p has not yet published a v2.0.0 git tag. - libp2p-mix pin: 99d2ca07 → a32af1a4 (chore/bump-libp2p-v2.0.0 tip after the v2.0.0 adaptation commit). - nim minimum: 2.0.0 → 2.2.4 to match v2.0.0's transitive requirement. Zero plugin code changes needed: libp2p v2.0.0's API churn (LPProtocol stream limits, Switch service lifecycle, sink changes, etc.) does not affect the plugin (no LPProtocol subclassing, no Service subclassing, no Connection subclassing). Verified compile-link against the new libp2p-mix. * chore(nimble): bump libp2p_mix pin to PR #14 HEAD Brings in the move-semantics propagation + lockfile-as-build-artefact cleanup. Pinned to the PR HEAD (50c4ab4) until logos-co/nim-libp2p-mix#14 and #12 merge to master. waku.nimble pins the same SHA to keep the diamond dep collapsed to a single libp2p_mix source. * fix(nimble): correct libp2p_mix SHA to actual PR #14 tip Previous commit (78995fc) pinned a fabricated SHA suffix (50c4ab43904baa3...) that doesn't exist on the remote. The actual tip of experiment/drop-nimble-lock is 50c4ab4fa788a33eb12a0a2cecaa708873352b58. Both `git ls-remote origin experiment/drop-nimble-lock` and the cold-cache build failure confirmed this. waku.nimble pin will mirror this corrected SHA. * refactor: rename newMixRlnSpamProtection to MixRlnSpamProtection.new * feat: make PublishCallback return Result and track broadcast futures (#11) Previously PublishCallback returned Future[void], so publish errors were silently dropped, and the proof-metadata broadcast in the synchronous verifyProof method was fired via an untracked asyncSpawn. - PublishCallback now returns Future[Result[void, string]] so callers can handle publish failures. - group_manager membership broadcasts handle the returned error (warn). - verifyProof is a sync method (overrides a sync base), so it cannot await; its proof-metadata broadcast now runs as a tracked background future (pruned when finished, cancelled in stop()) instead of asyncSpawn. - update createLoggingPublishCallback and doc examples accordingly. Addresses review feedback on logos-delivery#3931.
1 parent 6a5567d commit 1125960

13 files changed

Lines changed: 1209 additions & 365 deletions

README.md

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,15 @@ This plugin provides:
2222
│ - verifyProof(proof, bindingData) → bool │
2323
│ - proofSize() → 301 bytes (protobuf-encoded) │
2424
├─────────────────────────────────────────────────────────────────┤
25-
│ RLN Core (zerokit v2.0.2 FFI)
25+
│ RLN Core (zerokit v2.0.2 FFI, --features stateless)
2626
│ - Proof generation/verification (RLN-v2 format) │
2727
│ - Partial-proof caching for faster steady-state sends │
28-
│ - Merkle tree operations │
2928
│ - Poseidon hash, secret recovery │
3029
├─────────────────────────────────────────────────────────────────┤
30+
│ IncrementalMerkleTree (pure Nim, src/.../merkle_tree.nim) │
31+
│ - depth-20 Poseidon Merkle tree, byte-parity with pmtree │
32+
│ - set/get/delete leaves, root, inclusion proofs │
33+
├─────────────────────────────────────────────────────────────────┤
3134
│ OffchainGroupManager │
3235
│ - Membership tree (depth 20, ~1M members) │
3336
│ - Credential management │
@@ -48,21 +51,23 @@ This plugin provides:
4851

4952
### Zerokit Library (librln)
5053

51-
This plugin requires the zerokit RLN library (v2.0.2) for proof generation and verification.
54+
This plugin requires the zerokit RLN library (v2.0.2) for Poseidon hashing,
55+
witness construction, and Groth16 prove/verify.
5256

53-
The plugin keeps a local Merkle tree (offchain membership), so zerokit must be
54-
built with default features — do **not** enable the `stateless` cargo feature.
57+
The Merkle tree of memberships lives on the **Nim side** (see
58+
`src/mix_rln_spam_protection/merkle_tree.nim`), so the consuming application
59+
**must** build zerokit with `--features stateless` to avoid carrying a
60+
second zerokit archive alongside relay's. The plugin will not link against
61+
a default-features (pmtree-ft) build — `ffi_set_next_leaf`, `ffi_get_root`,
62+
`ffi_get_merkle_proof`, etc. are no longer referenced.
5563

5664
```bash
57-
# Option 1: Build from source
65+
# Build from source
5866
git clone https://github.com/vacp2p/zerokit
5967
cd zerokit
6068
git checkout v2.0.2
61-
cargo build --release -p rln
69+
cargo build --release -p rln --no-default-features --features stateless
6270
cp target/release/librln.a /path/to/your/project/
63-
64-
# Option 2: Download prebuilt
65-
# https://github.com/vacp2p/zerokit/releases/tag/v2.0.2
6671
```
6772

6873
## Installation
@@ -75,7 +80,7 @@ requires "mix_rln_spam_protection >= 0.1.0"
7580

7681
### Dependencies
7782

78-
- [zerokit-rln](https://github.com/vacp2p/zerokit) v2.0.2 - RLN proving library (static linking, default features — no `stateless`)
83+
- [zerokit-rln](https://github.com/vacp2p/zerokit) v2.0.2 - RLN proving library (static linking, **`--no-default-features --features stateless`**)
7984
- nim >= 2.0.0
8085
- chronos, results, chronicles, nimcrypto
8186

@@ -93,7 +98,7 @@ config.keystorePassword = "my-secure-password"
9398
# config.proofMetadataContentTopic = "/my-app/rln/metadata/v1"
9499
95100
# Create plugin
96-
let plugin = newMixRlnSpamProtection(config).valueOr:
101+
let plugin = MixRlnSpamProtection.new(config).valueOr:
97102
echo "Failed to create plugin: ", error
98103
return
99104

mix_rln_spam_protection.nimble

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ license = "MIT OR Apache-2.0"
66
srcDir = "src"
77

88
# Dependencies
9-
requires "nim >= 2.0.0"
9+
requires "nim >= 2.2.4"
1010
requires "results >= 0.4.0"
1111
requires "stew >= 0.4.2"
1212
requires "chronicles >= 0.11.0"
@@ -15,11 +15,21 @@ requires "nimcrypto >= 0.6.0"
1515
requires "secp256k1 >= 0.5.0"
1616
requires "json_serialization >= 0.2.0"
1717

18-
# nim-libp2p — used directly for protobuf/minprotobuf and varint
19-
requires "https://github.com/vacp2p/nim-libp2p.git#e1bbda4f6"
18+
# nim-libp2p — used directly for protobuf/minprotobuf and varint. Pinned to
19+
# the same commit nim-libp2p-mix uses so the diamond dep resolves to a single
20+
# libp2p source. c43199378 is the release/v2.0.0 tip (3 patch commits past
21+
# the v2.0.0 bump). SHA-pinned because vacp2p/nim-libp2p has not yet
22+
# published a v2.0.0 git tag.
23+
requires "https://github.com/vacp2p/nim-libp2p.git#c43199378f46d0aaf61be1cad1ee1d63e8f665d6"
2024

21-
# libp2p_mix — extracted into its own repo; previously libp2p/protocols/mix
22-
requires "https://github.com/logos-co/nim-libp2p-mix.git#f24cd25a54e2156d66b497f6d576aab3ccfc8fa6"
25+
# libp2p_mix — extracted into its own repo; previously libp2p/protocols/mix.
26+
# Tip of experiment/drop-nimble-lock (currently PR #14, stacked on top of
27+
# chore/bump-libp2p-v2.0.0). Carries the v2.0.0 bump + sink overrides +
28+
# AddressConfidence.Infinite + the deeper move-semantics propagation +
29+
# the lockfile-as-build-artefact cleanup. Pinned to the PR HEAD until the
30+
# stack lands on master; waku.nimble pins the same SHA to keep the diamond
31+
# dep collapsed to one libp2p_mix source.
32+
requires "https://github.com/logos-co/nim-libp2p-mix.git#50c4ab4fa788a33eb12a0a2cecaa708873352b58"
2333

2434
# Tasks
2535
task test, "Run tests":

src/mix_rln_spam_protection.nim

Lines changed: 9 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,15 @@
2222
## config.keystorePassword = "my-password"
2323
##
2424
## # Create and initialize plugin
25-
## let plugin = newMixRlnSpamProtection(config).get()
25+
## let plugin = MixRlnSpamProtection.new(config).get()
2626
## await plugin.init()
2727
##
2828
## # Set up logos-messaging integration
29-
## plugin.setPublishCallback(proc(topic: string, data: seq[byte]) {.async.} =
29+
## plugin.setPublishCallback(proc(
30+
## topic: string, data: seq[byte]
31+
## ): Future[Result[void, string]] {.async.} =
3032
## await logosMessaging.publish(topic, data)
33+
## ok()
3134
## )
3235
##
3336
## # Start the plugin
@@ -71,26 +74,10 @@
7174

7275
import
7376
./mix_rln_spam_protection/[
74-
types,
75-
constants,
76-
protobuf,
77-
codec,
78-
rln_interface,
79-
group_manager,
80-
nullifier_log,
81-
spam_protection,
82-
coordination,
83-
credentials
77+
types, constants, protobuf, codec, rln_interface, group_manager, nullifier_log,
78+
spam_protection, coordination, credentials,
8479
]
8580

8681
export
87-
types,
88-
constants,
89-
protobuf,
90-
codec,
91-
spam_protection,
92-
coordination,
93-
credentials,
94-
group_manager,
95-
nullifier_log,
96-
rln_interface
82+
types, constants, protobuf, codec, spam_protection, coordination, credentials,
83+
group_manager, nullifier_log, rln_interface

src/mix_rln_spam_protection/coordination.nim

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ logScope:
2525
type
2626
# Subscription handler types
2727
MembershipSubscriptionHandler* = proc(update: MembershipUpdate) {.gcsafe, raises: [].}
28-
MetadataSubscriptionHandler* = proc(metadata: ProofMetadataBroadcast) {.gcsafe, raises: [].}
28+
MetadataSubscriptionHandler* =
29+
proc(metadata: ProofMetadataBroadcast) {.gcsafe, raises: [].}
2930

3031
# Coordination layer wrapper
3132
CoordinationLayer* = ref object
@@ -45,7 +46,7 @@ proc newCoordinationLayer*(spamProtection: MixRlnSpamProtection): CoordinationLa
4546
spamProtection: spamProtection,
4647
publishCallback: none(PublishCallback),
4748
onMembershipUpdate: none(MembershipSubscriptionHandler),
48-
onProofMetadata: none(MetadataSubscriptionHandler)
49+
onProofMetadata: none(MetadataSubscriptionHandler),
4950
)
5051

5152
proc setPublishCallback*(cl: CoordinationLayer, callback: PublishCallback) =
@@ -54,18 +55,20 @@ proc setPublishCallback*(cl: CoordinationLayer, callback: PublishCallback) =
5455
cl.publishCallback = some(callback)
5556
cl.spamProtection.setPublishCallback(callback)
5657

57-
proc setMembershipUpdateHandler*(cl: CoordinationLayer, handler: MembershipSubscriptionHandler) =
58+
proc setMembershipUpdateHandler*(
59+
cl: CoordinationLayer, handler: MembershipSubscriptionHandler
60+
) =
5861
## Set additional handler for membership updates (for custom processing).
5962
cl.onMembershipUpdate = some(handler)
6063

61-
proc setProofMetadataHandler*(cl: CoordinationLayer, handler: MetadataSubscriptionHandler) =
64+
proc setProofMetadataHandler*(
65+
cl: CoordinationLayer, handler: MetadataSubscriptionHandler
66+
) =
6267
## Set additional handler for proof metadata (for custom processing).
6368
cl.onProofMetadata = some(handler)
6469

6570
proc handleIncomingMessage*(
66-
cl: CoordinationLayer,
67-
contentTopic: string,
68-
data: seq[byte]
71+
cl: CoordinationLayer, contentTopic: string, data: seq[byte]
6972
): Future[RlnResult[void]] {.async.} =
7073
## Route incoming messages from logos-messaging to appropriate handlers.
7174
##
@@ -85,7 +88,6 @@ proc handleIncomingMessage*(
8588
let update = MembershipUpdate.decode(data).valueOr:
8689
return err("Failed to decode update for handler: " & $error)
8790
cl.onMembershipUpdate.get()(update)
88-
8991
elif contentTopic == metadataTopic:
9092
# Handle proof metadata
9193
let metadataResult = cl.spamProtection.handleProofMetadata(data)
@@ -97,7 +99,6 @@ proc handleIncomingMessage*(
9799
let metadata = ProofMetadataBroadcast.decode(data).valueOr:
98100
return err("Failed to decode metadata for handler: " & $error)
99101
cl.onProofMetadata.get()(metadata)
100-
101102
else:
102103
return err("Unknown content topic: " & contentTopic)
103104

@@ -112,12 +113,14 @@ proc getDefaultContentTopics*(): seq[string] =
112113
@[MembershipContentTopic, ProofMetadataContentTopic]
113114

114115
# Helper function for building a logos-messaging subscription filter
115-
proc buildSubscriptionFilter*(cl: CoordinationLayer): seq[tuple[contentTopic: string, handler: string]] =
116+
proc buildSubscriptionFilter*(
117+
cl: CoordinationLayer
118+
): seq[tuple[contentTopic: string, handler: string]] =
116119
## Build a subscription filter for logos-messaging.
117120
## Returns tuples of (contentTopic, handlerName) for documentation.
118121
@[
119122
(cl.spamProtection.getMembershipContentTopic(), "handleMembershipUpdate"),
120-
(cl.spamProtection.getProofMetadataContentTopic(), "handleProofMetadata")
123+
(cl.spamProtection.getProofMetadataContentTopic(), "handleProofMetadata"),
121124
]
122125

123126
# Integration example documentation
@@ -133,15 +136,18 @@ var config = defaultConfig()
133136
# config.membershipContentTopic = "/my-app/rln/membership/v1"
134137
# config.proofMetadataContentTopic = "/my-app/rln/metadata/v1"
135138
136-
let spamProtection = newMixRlnSpamProtection(config).get()
139+
let spamProtection = MixRlnSpamProtection.new(config).get()
137140
await spamProtection.init()
138141
139142
# Create coordination layer
140143
let coordination = newCoordinationLayer(spamProtection)
141144
142145
# Wire up publish callback to logos-messaging
143-
coordination.setPublishCallback(proc(topic: string, data: seq[byte]) {.async.} =
146+
coordination.setPublishCallback(proc(
147+
topic: string, data: seq[byte]
148+
): Future[Result[void, string]] {.async.} =
144149
await logosMessaging.publish(topic, data)
150+
ok()
145151
)
146152
147153
# Subscribe to RLN topics (uses configured content topics)
@@ -167,20 +173,25 @@ let mixProto = MixProtocol.new(
167173
# Utility for creating a simple publish callback that logs (for testing)
168174
proc createLoggingPublishCallback*(): PublishCallback =
169175
## Create a publish callback that just logs messages (for testing).
170-
proc callback(contentTopic: string, data: seq[byte]): Future[void] {.async, gcsafe.} =
176+
proc callback(
177+
contentTopic: string, data: seq[byte]
178+
): Future[Result[void, string]] {.async, gcsafe.} =
171179
debug "Would publish to coordination layer",
172-
topic = contentTopic,
173-
dataLen = data.len
180+
topic = contentTopic, dataLen = data.len
181+
ok()
182+
174183
return callback
175184

176185
# Utility for printing membership update in human-readable form
177186
proc formatMembershipUpdate*(update: MembershipUpdate): string =
178187
## Format a membership update for logging/display.
179-
let actionStr = case update.action
188+
let actionStr =
189+
case update.action
180190
of MembershipAction.Add: "ADD"
181191
of MembershipAction.Remove: "REMOVE"
182192

183-
result = actionStr & " member at index " & $update.index &
184-
" (idCommitment: " & update.idCommitment[0..7].toHex() & "...)"
193+
result =
194+
actionStr & " member at index " & $update.index & " (idCommitment: " &
195+
update.idCommitment[0 .. 7].toHex() & "...)"
185196

186197
# Note: toHex is imported from types module via spam_protection

0 commit comments

Comments
 (0)