mirror of
https://github.com/logos-messaging/logos-delivery.git
synced 2026-02-27 21:53:16 +00:00
replaces off-chain manager with service over LEZ
This commit is contained in:
parent
29a40fe486
commit
e66a10bf3e
@ -557,7 +557,8 @@ proc processInput(rfd: AsyncFD, rng: ref HmacDrbgContext) {.async.} =
|
||||
|
||||
(
|
||||
await node.mountMix(
|
||||
conf.clusterId, mixPrivKey, conf.mixnodes, some(conf.rlnUserMessageLimit)
|
||||
conf.clusterId, mixPrivKey, conf.mixnodes, some(conf.rlnUserMessageLimit),
|
||||
conf.rlnServiceUrl,
|
||||
)
|
||||
).isOkOr:
|
||||
error "failed to mount waku mix protocol: ", error = $error
|
||||
|
||||
@ -244,6 +244,13 @@ type
|
||||
name: "rln-user-message-limit"
|
||||
.}: int
|
||||
|
||||
rlnServiceUrl* {.
|
||||
desc:
|
||||
"URL of the external RLN Merkle proof service (required for group manager)",
|
||||
defaultValue: "",
|
||||
name: "rln-service-url"
|
||||
.}: string
|
||||
|
||||
proc parseCmdArg*(T: type MixNodePubInfo, p: string): T =
|
||||
let elements = p.split(":")
|
||||
if elements.len != 2:
|
||||
|
||||
@ -11,6 +11,7 @@ The simulation includes:
|
||||
|
||||
1. A 5-node mixnet where `run_mix_node.sh` is the bootstrap node for the other 4 nodes
|
||||
2. Two chat app instances that publish messages using lightpush protocol over the mixnet
|
||||
3. A local LEZ sequencer, RLN registration program, and a JSON-RPC service for interacting with the program
|
||||
|
||||
### Available Scripts
|
||||
|
||||
@ -35,9 +36,39 @@ source env.sh
|
||||
make wakunode2 chat2mix
|
||||
```
|
||||
|
||||
## RLN Program and Service
|
||||
|
||||
Requires Docker.
|
||||
|
||||
Install Rust and the RISC Zero toolchain:
|
||||
```bash
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
curl -L https://risczero.com/install | bash
|
||||
rzup install
|
||||
```
|
||||
|
||||
Clone the RLN program repo and build the guest programs:
|
||||
```bash
|
||||
git clone -b feat/rln-service git@github.com:logos-co/logos-lez-rln.git
|
||||
cd logos-lez-rln
|
||||
cargo risczero build --manifest-path methods/guest/Cargo.toml
|
||||
```
|
||||
|
||||
Start the local sequencer (runs in foreground):
|
||||
```bash
|
||||
./dev.sh
|
||||
```
|
||||
|
||||
In another terminal, run setup and start the service:
|
||||
```bash
|
||||
source dev/env.sh && cargo run --bin run_setup && cargo run --bin rln_service
|
||||
```
|
||||
|
||||
The service listens on http://127.0.0.1:3001.
|
||||
|
||||
## RLN Spam Protection Setup
|
||||
|
||||
Generate RLN credentials and the shared Merkle tree for all nodes:
|
||||
Generate RLN credentials and register them in the on-chain Merkle tree for all nodes:
|
||||
|
||||
```bash
|
||||
cd simulations/mixnet
|
||||
@ -48,10 +79,10 @@ This script will:
|
||||
|
||||
1. Build and run the `setup_credentials` tool
|
||||
2. Generate RLN credentials for all nodes (5 mix nodes + 2 chat clients)
|
||||
3. Create `rln_tree.db` - the shared Merkle tree with all members
|
||||
4. Create keystore files (`rln_keystore_{peerId}.json`) for each node
|
||||
3. Create keystore files (`rln_keystore_{peerId}.json`) for each node
|
||||
4. Call the RLN service to register each identity
|
||||
|
||||
**Important:** All scripts must be run from this directory (`simulations/mixnet/`) so they can access their credentials and tree file.
|
||||
**Important:** All scripts must be run from this directory (`simulations/mixnet/`) so they can access their credential files.
|
||||
|
||||
To regenerate credentials (e.g., after adding new nodes), run `./build_setup.sh` again - it will clean up old files first.
|
||||
|
||||
@ -77,7 +108,7 @@ Verify RLN spam protection initialized correctly by checking for these logs:
|
||||
```log
|
||||
INF Initializing MixRlnSpamProtection
|
||||
INF MixRlnSpamProtection initialized, waiting for sync
|
||||
DBG Tree loaded from file
|
||||
INF Group manager started
|
||||
INF MixRlnSpamProtection started
|
||||
```
|
||||
|
||||
|
||||
@ -4,8 +4,11 @@ MIXNET_DIR=$(pwd)
|
||||
cd ../..
|
||||
ROOT_DIR=$(pwd)
|
||||
|
||||
# Clean up old files first
|
||||
rm -f "$MIXNET_DIR/rln_tree.db" "$MIXNET_DIR"/rln_keystore_*.json
|
||||
# Source env.sh to get the correct nim with vendor paths
|
||||
source "$ROOT_DIR/env.sh"
|
||||
|
||||
# Clean up old keystore files
|
||||
rm -f "$MIXNET_DIR"/rln_keystore_*.json
|
||||
|
||||
echo "Building and running credentials setup..."
|
||||
# Compile to temp location, then run from mixnet directory
|
||||
@ -17,16 +20,17 @@ nim c -d:release --mm:refc \
|
||||
# Run from mixnet directory so files are created there
|
||||
cd "$MIXNET_DIR"
|
||||
/tmp/setup_credentials_$$
|
||||
RESULT=$?
|
||||
|
||||
# Clean up temp binary
|
||||
rm -f /tmp/setup_credentials_$$
|
||||
|
||||
# Verify output
|
||||
if [ -f "rln_tree.db" ]; then
|
||||
if [ $RESULT -eq 0 ]; then
|
||||
echo ""
|
||||
echo "Tree file ready at: $(pwd)/rln_tree.db"
|
||||
ls -la rln_keystore_*.json 2>/dev/null | wc -l | xargs -I {} echo "Generated {} keystore files"
|
||||
KEYSTORE_COUNT=$(ls -1 rln_keystore_*.json 2>/dev/null | wc -l | tr -d ' ')
|
||||
echo "Generated $KEYSTORE_COUNT keystore files"
|
||||
else
|
||||
echo "Setup failed - rln_tree.db not found"
|
||||
echo "Setup failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
log-level = "TRACE"
|
||||
relay = true
|
||||
mix = true
|
||||
rln-service-url = "http://127.0.0.1:3001"
|
||||
filter = true
|
||||
store = true
|
||||
lightpush = true
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
log-level = "TRACE"
|
||||
relay = true
|
||||
mix = true
|
||||
rln-service-url = "http://127.0.0.1:3001"
|
||||
filter = true
|
||||
store = false
|
||||
lightpush = true
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
log-level = "TRACE"
|
||||
relay = true
|
||||
mix = true
|
||||
rln-service-url = "http://127.0.0.1:3001"
|
||||
filter = true
|
||||
store = false
|
||||
lightpush = true
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
log-level = "TRACE"
|
||||
relay = true
|
||||
mix = true
|
||||
rln-service-url = "http://127.0.0.1:3001"
|
||||
filter = true
|
||||
store = false
|
||||
lightpush = true
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
log-level = "TRACE"
|
||||
relay = true
|
||||
mix = true
|
||||
rln-service-url = "http://127.0.0.1:3001"
|
||||
filter = true
|
||||
store = false
|
||||
lightpush = true
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
../../build/chat2mix --cluster-id=2 --num-shards-in-network=1 --shard=0 --servicenode="/ip4/127.0.0.1/tcp/60001/p2p/16Uiu2HAmPiEs2ozjjJF2iN2Pe2FYeMC9w4caRHKYdLdAfjgbWM6o" --log-level=TRACE --nodekey="cb6fe589db0e5d5b48f7e82d33093e4d9d35456f4aaffc2322c473a173b2ac49" --kad-bootstrap-node="/ip4/127.0.0.1/tcp/60001/p2p/16Uiu2HAmPiEs2ozjjJF2iN2Pe2FYeMC9w4caRHKYdLdAfjgbWM6o" --fleet="none"
|
||||
../../build/chat2mix --cluster-id=2 --num-shards-in-network=1 --shard=0 --servicenode="/ip4/127.0.0.1/tcp/60001/p2p/16Uiu2HAmPiEs2ozjjJF2iN2Pe2FYeMC9w4caRHKYdLdAfjgbWM6o" --log-level=TRACE --nodekey="cb6fe589db0e5d5b48f7e82d33093e4d9d35456f4aaffc2322c473a173b2ac49" --kad-bootstrap-node="/ip4/127.0.0.1/tcp/60001/p2p/16Uiu2HAmPiEs2ozjjJF2iN2Pe2FYeMC9w4caRHKYdLdAfjgbWM6o" --fleet="none" --rln-service-url=http://127.0.0.1:3001
|
||||
#--mixnode="/ip4/127.0.0.1/tcp/60002/p2p/16Uiu2HAmLtKaFaSWDohToWhWUZFLtqzYZGPFuXwKrojFVF6az5UF:9231e86da6432502900a84f867004ce78632ab52cd8e30b1ec322cd795710c2a" --mixnode="/ip4/127.0.0.1/tcp/60003/p2p/16Uiu2HAmTEDHwAziWUSz6ZE23h5vxG2o4Nn7GazhMor4bVuMXTrA:275cd6889e1f29ca48e5b9edb800d1a94f49f13d393a0ecf1a07af753506de6c" --mixnode="/ip4/127.0.0.1/tcp/60004/p2p/16Uiu2HAmPwRKZajXtfb1Qsv45VVfRZgK3ENdfmnqzSrVm3BczF6f:e0ed594a8d506681be075e8e23723478388fb182477f7a469309a25e7076fc18" --mixnode="/ip4/127.0.0.1/tcp/60005/p2p/16Uiu2HAmRhxmCHBYdXt1RibXrjAUNJbduAhzaTHwFCZT4qWnqZAu:8fd7a1a7c19b403d231452a9b1ea40eb1cc76f455d918ef8980e7685f9eeeb1f"
|
||||
|
||||
@ -1 +1 @@
|
||||
../../build/chat2mix --cluster-id=2 --num-shards-in-network=1 --shard=0 --servicenode="/ip4/127.0.0.1/tcp/60001/p2p/16Uiu2HAmPiEs2ozjjJF2iN2Pe2FYeMC9w4caRHKYdLdAfjgbWM6o" --log-level=TRACE --nodekey="35eace7ccb246f20c487e05015ca77273d8ecaed0ed683de3d39bf4f69336feb" --mixnode="/ip4/127.0.0.1/tcp/60002/p2p/16Uiu2HAmLtKaFaSWDohToWhWUZFLtqzYZGPFuXwKrojFVF6az5UF:9231e86da6432502900a84f867004ce78632ab52cd8e30b1ec322cd795710c2a" --mixnode="/ip4/127.0.0.1/tcp/60003/p2p/16Uiu2HAmTEDHwAziWUSz6ZE23h5vxG2o4Nn7GazhMor4bVuMXTrA:275cd6889e1f29ca48e5b9edb800d1a94f49f13d393a0ecf1a07af753506de6c" --mixnode="/ip4/127.0.0.1/tcp/60004/p2p/16Uiu2HAmPwRKZajXtfb1Qsv45VVfRZgK3ENdfmnqzSrVm3BczF6f:e0ed594a8d506681be075e8e23723478388fb182477f7a469309a25e7076fc18" --mixnode="/ip4/127.0.0.1/tcp/60005/p2p/16Uiu2HAmRhxmCHBYdXt1RibXrjAUNJbduAhzaTHwFCZT4qWnqZAu:8fd7a1a7c19b403d231452a9b1ea40eb1cc76f455d918ef8980e7685f9eeeb1f" --mixnode="/ip4/127.0.0.1/tcp/60001/p2p/16Uiu2HAmPiEs2ozjjJF2iN2Pe2FYeMC9w4caRHKYdLdAfjgbWM6o:9d09ce624f76e8f606265edb9cca2b7de9b41772a6d784bddaf92ffa8fba7d2c" --fleet="none"
|
||||
../../build/chat2mix --cluster-id=2 --num-shards-in-network=1 --shard=0 --servicenode="/ip4/127.0.0.1/tcp/60001/p2p/16Uiu2HAmPiEs2ozjjJF2iN2Pe2FYeMC9w4caRHKYdLdAfjgbWM6o" --log-level=TRACE --nodekey="35eace7ccb246f20c487e05015ca77273d8ecaed0ed683de3d39bf4f69336feb" --mixnode="/ip4/127.0.0.1/tcp/60002/p2p/16Uiu2HAmLtKaFaSWDohToWhWUZFLtqzYZGPFuXwKrojFVF6az5UF:9231e86da6432502900a84f867004ce78632ab52cd8e30b1ec322cd795710c2a" --mixnode="/ip4/127.0.0.1/tcp/60003/p2p/16Uiu2HAmTEDHwAziWUSz6ZE23h5vxG2o4Nn7GazhMor4bVuMXTrA:275cd6889e1f29ca48e5b9edb800d1a94f49f13d393a0ecf1a07af753506de6c" --mixnode="/ip4/127.0.0.1/tcp/60004/p2p/16Uiu2HAmPwRKZajXtfb1Qsv45VVfRZgK3ENdfmnqzSrVm3BczF6f:e0ed594a8d506681be075e8e23723478388fb182477f7a469309a25e7076fc18" --mixnode="/ip4/127.0.0.1/tcp/60005/p2p/16Uiu2HAmRhxmCHBYdXt1RibXrjAUNJbduAhzaTHwFCZT4qWnqZAu:8fd7a1a7c19b403d231452a9b1ea40eb1cc76f455d918ef8980e7685f9eeeb1f" --mixnode="/ip4/127.0.0.1/tcp/60001/p2p/16Uiu2HAmPiEs2ozjjJF2iN2Pe2FYeMC9w4caRHKYdLdAfjgbWM6o:9d09ce624f76e8f606265edb9cca2b7de9b41772a6d784bddaf92ffa8fba7d2c" --fleet="none" --rln-service-url=http://127.0.0.1:3001
|
||||
|
||||
BIN
simulations/mixnet/setup_credentials
Executable file
BIN
simulations/mixnet/setup_credentials
Executable file
Binary file not shown.
@ -1,27 +1,26 @@
|
||||
{.push raises: [].}
|
||||
|
||||
## Setup script to generate RLN credentials and shared Merkle tree for mix nodes.
|
||||
## Setup script to generate RLN credentials and register them with the external service.
|
||||
##
|
||||
## This script:
|
||||
## 1. Generates credentials for each node (identified by peer ID)
|
||||
## 2. Registers all credentials in a shared Merkle tree
|
||||
## 3. Saves the tree to rln_tree.db
|
||||
## 4. Saves individual keystores named by peer ID
|
||||
## 2. Registers all credentials with the external RLN service (in parallel)
|
||||
## 3. Saves individual keystores named by peer ID, using the service's leaf index
|
||||
##
|
||||
## Usage: nim c -r setup_credentials.nim
|
||||
|
||||
import std/[os, strformat, options], chronicles, chronos, results
|
||||
import std/[os, strformat, options, json, strutils], chronicles, chronos, results
|
||||
import chronos/apps/http/[httpclient, httpcommon]
|
||||
|
||||
import
|
||||
mix_rln_spam_protection/credentials,
|
||||
mix_rln_spam_protection/group_manager,
|
||||
mix_rln_spam_protection/rln_interface,
|
||||
mix_rln_spam_protection/types
|
||||
|
||||
const
|
||||
KeystorePassword = "mix-rln-password" # Must match protocol.nim
|
||||
DefaultUserMessageLimit = 100'u64 # Network-wide default rate limit
|
||||
SpammerUserMessageLimit = 3'u64 # Lower limit for spammer testing
|
||||
RlnServiceUrl = "http://127.0.0.1:3001"
|
||||
|
||||
# Peer IDs derived from nodekeys in config files
|
||||
# config.toml: nodekey = "f98e3fba96c32e8d1967d460f1b79457380e1a895f7971cecc8528abe733781a"
|
||||
@ -50,8 +49,47 @@ const
|
||||
# chat2mix client 2
|
||||
]
|
||||
|
||||
proc setupCredentialsAndTree() {.async.} =
|
||||
## Generate credentials for all nodes and create a shared tree
|
||||
proc registerWithService(
|
||||
session: HttpSessionRef,
|
||||
address: HttpAddress,
|
||||
idCommitment: IDCommitment,
|
||||
rateLimit: uint64,
|
||||
reqId: int,
|
||||
): Future[int] {.async.} =
|
||||
## Register a credential with the external RLN service.
|
||||
## Returns the leaf index assigned by the service.
|
||||
let commitmentHex = "0x" & idCommitment.toHex()
|
||||
let body = $(%*{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "rln_register",
|
||||
"params": [commitmentHex, rateLimit],
|
||||
"id": reqId,
|
||||
})
|
||||
|
||||
var req: HttpClientRequestRef = nil
|
||||
var res: HttpClientResponseRef = nil
|
||||
try:
|
||||
req = HttpClientRequestRef.post(
|
||||
session, address,
|
||||
body = body.toOpenArrayByte(0, body.len - 1),
|
||||
headers = @[("Content-Type", "application/json")],
|
||||
)
|
||||
res = await req.send()
|
||||
let resBytes = await res.getBodyBytes()
|
||||
let parsed = parseJson(cast[string](resBytes))
|
||||
|
||||
if parsed.hasKey("error"):
|
||||
raise newException(CatchableError, "Service error: " & $parsed["error"])
|
||||
|
||||
return parsed["result"]["leaf_index"].getInt()
|
||||
finally:
|
||||
if req != nil:
|
||||
await req.closeWait()
|
||||
if res != nil:
|
||||
await res.closeWait()
|
||||
|
||||
proc setupCredentials() {.async.} =
|
||||
## Generate credentials, register with external service in parallel, save keystores.
|
||||
|
||||
echo "=== RLN Credentials Setup ==="
|
||||
echo "Generating credentials for ", NodeConfigs.len, " nodes...\n"
|
||||
@ -71,69 +109,61 @@ proc setupCredentialsAndTree() {.async.} =
|
||||
|
||||
echo ""
|
||||
|
||||
# Create a group manager directly to build the tree
|
||||
let rlnInstance = newRLNInstance().valueOr:
|
||||
echo "Failed to create RLN instance: ", error
|
||||
# Register all credentials with the external RLN service in parallel
|
||||
echo "Registering all credentials with external RLN service at ", RlnServiceUrl, " (parallel)..."
|
||||
let session = HttpSessionRef.new()
|
||||
let address = session.getAddress(RlnServiceUrl).valueOr:
|
||||
echo "FATAL: Invalid RLN service URL: ", RlnServiceUrl
|
||||
quit(1)
|
||||
|
||||
let groupManager = newOffchainGroupManager(rlnInstance, "/mix/rln/membership/v1")
|
||||
|
||||
# Initialize the group manager
|
||||
let initRes = await groupManager.init()
|
||||
if initRes.isErr:
|
||||
echo "Failed to initialize group manager: ", initRes.error
|
||||
quit(1)
|
||||
|
||||
# Register all credentials in the tree with their specific rate limits
|
||||
echo "Registering all credentials in the Merkle tree..."
|
||||
var futures: seq[Future[int]]
|
||||
for i, entry in allCredentials:
|
||||
let index = (
|
||||
await groupManager.registerWithLimit(entry.cred.idCommitment, entry.rateLimit)
|
||||
).valueOr:
|
||||
echo "Failed to register credential for ", entry.peerId, ": ", error
|
||||
quit(1)
|
||||
echo " Registered ",
|
||||
entry.peerId, " at index ", index, " (limit: ", entry.rateLimit, ")"
|
||||
futures.add(registerWithService(
|
||||
session, address, entry.cred.idCommitment, entry.rateLimit, i + 1
|
||||
))
|
||||
|
||||
echo ""
|
||||
|
||||
# Save the tree to disk
|
||||
echo "Saving tree to rln_tree.db..."
|
||||
let saveRes = groupManager.saveTreeToFile("rln_tree.db")
|
||||
if saveRes.isErr:
|
||||
echo "Failed to save tree: ", saveRes.error
|
||||
var serviceIndices = newSeq[int](futures.len)
|
||||
try:
|
||||
await allFutures(futures)
|
||||
for i, fut in futures:
|
||||
if fut.failed:
|
||||
raise fut.error
|
||||
serviceIndices[i] = fut.read()
|
||||
echo " Registered ",
|
||||
allCredentials[i].peerId, " at service index ", serviceIndices[i],
|
||||
" (limit: ", allCredentials[i].rateLimit, ")"
|
||||
except CatchableError as e:
|
||||
echo "FATAL: Failed to register with external service: ", e.msg
|
||||
echo " The external RLN service must be running at ", RlnServiceUrl
|
||||
quit(1)
|
||||
echo "Tree saved successfully!"
|
||||
|
||||
await session.closeWait()
|
||||
|
||||
echo ""
|
||||
|
||||
# Save each credential to a keystore file named by peer ID
|
||||
# Save each credential to a keystore file using the service's leaf index
|
||||
echo "Saving keystores..."
|
||||
for i, entry in allCredentials:
|
||||
let keystorePath = &"rln_keystore_{entry.peerId}.json"
|
||||
let membershipIndex = MembershipIndex(serviceIndices[i])
|
||||
|
||||
# Save with membership index and rate limit
|
||||
let saveResult = saveKeystore(
|
||||
entry.cred,
|
||||
KeystorePassword,
|
||||
keystorePath,
|
||||
some(MembershipIndex(i)),
|
||||
some(membershipIndex),
|
||||
some(entry.rateLimit),
|
||||
)
|
||||
if saveResult.isErr:
|
||||
echo "Failed to save keystore for ", entry.peerId, ": ", saveResult.error
|
||||
quit(1)
|
||||
echo " Saved: ", keystorePath, " (limit: ", entry.rateLimit, ")"
|
||||
echo " Saved: ", keystorePath, " (index: ", membershipIndex, ", limit: ", entry.rateLimit, ")"
|
||||
|
||||
echo ""
|
||||
echo "=== Setup Complete ==="
|
||||
echo " Tree file: rln_tree.db (", NodeConfigs.len, " members)"
|
||||
echo " Keystores: rln_keystore_{peerId}.json"
|
||||
echo " Password: ", KeystorePassword
|
||||
echo " Default rate limit: ", DefaultUserMessageLimit
|
||||
echo " Spammer rate limit: ", SpammerUserMessageLimit
|
||||
echo ""
|
||||
echo "Note: All nodes must use the same rln_tree.db file."
|
||||
|
||||
when isMainModule:
|
||||
waitFor setupCredentialsAndTree()
|
||||
waitFor setupCredentials()
|
||||
|
||||
BIN
simulations/mixnet/test_onchain_gm
Executable file
BIN
simulations/mixnet/test_onchain_gm
Executable file
Binary file not shown.
149
simulations/mixnet/test_onchain_gm.nim
Normal file
149
simulations/mixnet/test_onchain_gm.nim
Normal file
@ -0,0 +1,149 @@
|
||||
## Integration test: GroupManager with external RLN service.
|
||||
##
|
||||
## Prerequisites:
|
||||
## - RLN service running at http://127.0.0.1:3001
|
||||
## - Run setup_credentials first to generate keystores
|
||||
##
|
||||
## Usage: nim c -r --mm:refc test_onchain_gm.nim [http://127.0.0.1:3001]
|
||||
|
||||
import std/[os, options]
|
||||
import chronos, results, chronicles
|
||||
|
||||
import
|
||||
mix_rln_spam_protection/types,
|
||||
mix_rln_spam_protection/constants,
|
||||
mix_rln_spam_protection/rln_interface,
|
||||
mix_rln_spam_protection/group_manager,
|
||||
mix_rln_spam_protection/credentials
|
||||
|
||||
# Import the service client from the waku layer
|
||||
import ../../waku/waku_mix/rln_service_client
|
||||
|
||||
const
|
||||
DefaultUrl = "http://127.0.0.1:3001"
|
||||
KeystorePassword = "mix-rln-password"
|
||||
# Use the first node's peer ID for keystore lookup
|
||||
TestPeerId = "16Uiu2HAmPiEs2ozjjJF2iN2Pe2FYeMC9w4caRHKYdLdAfjgbWM6o"
|
||||
|
||||
proc main() {.async.} =
|
||||
let serviceUrl = if paramCount() >= 1: paramStr(1) else: DefaultUrl
|
||||
echo "=== OnchainGroupManager Integration Test ==="
|
||||
echo "Service URL: ", serviceUrl
|
||||
|
||||
# 1. Create RLN instance
|
||||
echo "\n1. Creating RLN instance..."
|
||||
let rlnInstance = newRLNInstance().valueOr:
|
||||
echo "FAIL: ", error
|
||||
quit(1)
|
||||
echo " OK"
|
||||
|
||||
# 2. Create GroupManager
|
||||
echo "\n2. Creating GroupManager..."
|
||||
let gm = newGroupManager(
|
||||
rlnInstance, pollIntervalSeconds = 2.0, userMessageLimit = 100
|
||||
)
|
||||
|
||||
# 3. Set service callbacks
|
||||
echo "\n3. Setting service callbacks..."
|
||||
gm.setFetchLatestRoots(makeFetchLatestRoots(serviceUrl))
|
||||
gm.setFetchMerkleProof(makeFetchMerkleProof(serviceUrl))
|
||||
echo " OK"
|
||||
|
||||
# 4. Initialize
|
||||
echo "\n4. Initializing group manager..."
|
||||
let initRes = await gm.init()
|
||||
if initRes.isErr:
|
||||
echo "FAIL: ", initRes.error
|
||||
quit(1)
|
||||
echo " OK"
|
||||
|
||||
# 5. Load credentials from keystore
|
||||
echo "\n5. Loading credentials..."
|
||||
let keystorePath = "rln_keystore_" & TestPeerId & ".json"
|
||||
if not fileExists(keystorePath):
|
||||
echo "FAIL: Keystore not found at ", keystorePath
|
||||
echo " Run build_setup.sh first"
|
||||
quit(1)
|
||||
|
||||
let (cred, maybeIndex, maybeRateLimit, wasGenerated) =
|
||||
loadOrGenerateCredentials(keystorePath, KeystorePassword).valueOr:
|
||||
echo "FAIL: ", error
|
||||
quit(1)
|
||||
|
||||
echo " Loaded credential from keystore"
|
||||
echo " idCommitment: ", cred.idCommitment.toHex()[0..15], "..."
|
||||
if maybeIndex.isSome:
|
||||
echo " membershipIndex: ", maybeIndex.get()
|
||||
if maybeRateLimit.isSome:
|
||||
echo " userMessageLimit: ", maybeRateLimit.get()
|
||||
|
||||
# 6. Register credentials with the group manager
|
||||
echo "\n6. Registering credentials..."
|
||||
if maybeIndex.isSome:
|
||||
gm.credentials = some(cred)
|
||||
gm.membershipIndex = some(maybeIndex.get())
|
||||
echo " Set credentials with index ", maybeIndex.get()
|
||||
else:
|
||||
echo " No index in keystore, registering..."
|
||||
let regRes = await gm.register(cred)
|
||||
if regRes.isErr:
|
||||
echo "FAIL: ", regRes.error
|
||||
quit(1)
|
||||
echo " Registered at index ", regRes.get()
|
||||
|
||||
# 7. Start the group manager (begins polling)
|
||||
echo "\n7. Starting group manager (polling)..."
|
||||
let startRes = await gm.start()
|
||||
if startRes.isErr:
|
||||
echo "FAIL: ", startRes.error
|
||||
quit(1)
|
||||
echo " OK - polling started"
|
||||
|
||||
# 8. Wait for cached proof to arrive
|
||||
echo "\n8. Waiting for cached proof..."
|
||||
var attempts = 0
|
||||
while attempts < 10:
|
||||
await sleepAsync(chronos.seconds(1))
|
||||
attempts.inc()
|
||||
# Try generating a proof to see if cache is populated
|
||||
let testSignal = @[byte(1), 2, 3, 4]
|
||||
let epoch = currentEpoch()
|
||||
var rlnId: RlnIdentifier
|
||||
let idStr = MixRlnIdentifier
|
||||
let copyLen = min(idStr.len, HashByteSize)
|
||||
if copyLen > 0:
|
||||
copyMem(addr rlnId[0], unsafeAddr idStr[0], copyLen)
|
||||
let proofRes = gm.generateProof(testSignal, epoch, rlnId)
|
||||
if proofRes.isOk:
|
||||
echo " Proof generated after ", attempts, " seconds!"
|
||||
let proof = proofRes.get()
|
||||
echo " merkleRoot: ", proof.merkleRoot.toHex()[0..15], "..."
|
||||
echo " epoch: ", proof.epoch.epochToUint64()
|
||||
echo " nullifier: ", proof.nullifier.toHex()[0..15], "..."
|
||||
|
||||
# 9. Verify the proof
|
||||
echo "\n9. Verifying proof..."
|
||||
# Pass the proof's root as a valid root since our local tree is empty
|
||||
let verifyRes = rlnInstance.verifyRlnProof(
|
||||
proof, rlnId, testSignal, validRoots = @[proof.merkleRoot]
|
||||
)
|
||||
if verifyRes.isOk and verifyRes.get():
|
||||
echo " PASS - proof verified successfully!"
|
||||
else:
|
||||
echo " FAIL - proof verification failed"
|
||||
if verifyRes.isErr:
|
||||
echo " Error: ", verifyRes.error
|
||||
|
||||
# 10. Stop
|
||||
echo "\n10. Stopping..."
|
||||
await gm.stop()
|
||||
echo " Stopped"
|
||||
|
||||
echo "\n=== ALL TESTS PASSED ==="
|
||||
return
|
||||
|
||||
echo " FAIL: No cached proof after ", attempts, " seconds"
|
||||
await gm.stop()
|
||||
quit(1)
|
||||
|
||||
waitFor main()
|
||||
BIN
simulations/mixnet/test_rln_service
Executable file
BIN
simulations/mixnet/test_rln_service
Executable file
Binary file not shown.
71
simulations/mixnet/test_rln_service.nim
Normal file
71
simulations/mixnet/test_rln_service.nim
Normal file
@ -0,0 +1,71 @@
|
||||
## Quick smoke test for the external RLN JSON-RPC service.
|
||||
##
|
||||
## Usage: nim c -r test_rln_service.nim [http://127.0.0.1:3001]
|
||||
|
||||
import std/[httpclient, json, os, strutils]
|
||||
|
||||
const DefaultUrl = "http://127.0.0.1:3001"
|
||||
|
||||
proc jsonRpc(client: HttpClient, url, methodName: string,
|
||||
params: JsonNode = newJArray()): JsonNode =
|
||||
let body = %*{
|
||||
"jsonrpc": "2.0",
|
||||
"method": methodName,
|
||||
"params": params,
|
||||
"id": 1
|
||||
}
|
||||
let resp = client.request(url, httpMethod = HttpPost,
|
||||
body = $body,
|
||||
headers = newHttpHeaders({"Content-Type": "application/json"}))
|
||||
let parsed = parseJson(resp.body)
|
||||
if parsed.hasKey("error"):
|
||||
echo " ERROR: ", parsed["error"]
|
||||
return nil
|
||||
return parsed["result"]
|
||||
|
||||
proc main() =
|
||||
let url = if paramCount() >= 1: paramStr(1) else: DefaultUrl
|
||||
echo "Testing RLN service at ", url
|
||||
let client = newHttpClient()
|
||||
|
||||
# 1. Get root
|
||||
echo "\n--- rln_getRoot ---"
|
||||
let root = client.jsonRpc(url, "rln_getRoot")
|
||||
if root != nil:
|
||||
echo " root: ", root.getStr()
|
||||
|
||||
# 2. Register a dummy identity commitment
|
||||
echo "\n--- rln_register ---"
|
||||
# 32-byte hex commitment (64 hex chars) — just a test value
|
||||
let testCommitment = "0x" & "ab".repeat(32)
|
||||
let testLimit = 100
|
||||
let regResult = client.jsonRpc(url, "rln_register",
|
||||
%*[testCommitment, testLimit])
|
||||
if regResult != nil:
|
||||
echo " result: ", regResult
|
||||
|
||||
# 3. Get root again (should have changed after registration)
|
||||
echo "\n--- rln_getRoot (after register) ---"
|
||||
let root2 = client.jsonRpc(url, "rln_getRoot")
|
||||
if root2 != nil:
|
||||
echo " root: ", root2.getStr()
|
||||
if root != nil:
|
||||
echo " changed: ", root.getStr() != root2.getStr()
|
||||
|
||||
# 4. Get merkle proof for index 0
|
||||
echo "\n--- rln_getMerkleProof(0) ---"
|
||||
let proof = client.jsonRpc(url, "rln_getMerkleProof", %*[0])
|
||||
if proof != nil:
|
||||
if proof.kind == JObject:
|
||||
for key, val in proof.pairs:
|
||||
let s = $val
|
||||
if s.len > 200:
|
||||
echo " ", key, ": ", s[0..196], "..."
|
||||
else:
|
||||
echo " ", key, ": ", s
|
||||
else:
|
||||
echo " result: ", proof
|
||||
|
||||
echo "\nDone."
|
||||
|
||||
main()
|
||||
0
simulations/mixnet/typescript
Normal file
0
simulations/mixnet/typescript
Normal file
@ -621,6 +621,13 @@ with the drawback of consuming some more bandwidth.""",
|
||||
name: "mixnode"
|
||||
.}: seq[MixNodePubInfo]
|
||||
|
||||
rlnServiceUrl* {.
|
||||
desc:
|
||||
"URL of the external RLN Merkle proof service (required for group manager)",
|
||||
defaultValue: "",
|
||||
name: "rln-service-url"
|
||||
.}: string
|
||||
|
||||
# Kademlia Discovery config
|
||||
enableKadDiscovery* {.
|
||||
desc:
|
||||
@ -1021,6 +1028,7 @@ proc toWakuConf*(n: WakuNodeConf): ConfResult[WakuConf] =
|
||||
|
||||
b.mixConf.withEnabled(n.mix)
|
||||
b.mixConf.withMixNodes(n.mixnodes)
|
||||
b.mixConf.withRlnServiceUrl(n.rlnServiceUrl)
|
||||
b.withMix(n.mix)
|
||||
if n.mixkey.isSome():
|
||||
b.mixConf.withMixKey(n.mixkey.get())
|
||||
|
||||
2
vendor/mix-rln-spam-protection-plugin
vendored
2
vendor/mix-rln-spam-protection-plugin
vendored
@ -1 +1 @@
|
||||
Subproject commit b3e3f72932d53fe0959e182fc081fe14c0c2c2f0
|
||||
Subproject commit 38f8ac8d6fcf06afb1dd1f4490887a4ddd8a331a
|
||||
@ -12,6 +12,7 @@ type MixConfBuilder* = object
|
||||
enabled: Option[bool]
|
||||
mixKey: Option[string]
|
||||
mixNodes: seq[MixNodePubInfo]
|
||||
rlnServiceUrl: string
|
||||
|
||||
proc init*(T: type MixConfBuilder): MixConfBuilder =
|
||||
MixConfBuilder()
|
||||
@ -25,19 +26,24 @@ proc withMixKey*(b: var MixConfBuilder, mixKey: string) =
|
||||
proc withMixNodes*(b: var MixConfBuilder, mixNodes: seq[MixNodePubInfo]) =
|
||||
b.mixNodes = mixNodes
|
||||
|
||||
proc withRlnServiceUrl*(b: var MixConfBuilder, url: string) =
|
||||
b.rlnServiceUrl = url
|
||||
|
||||
proc build*(b: MixConfBuilder): Result[Option[MixConf], string] =
|
||||
if not b.enabled.get(false):
|
||||
return ok(none[MixConf]())
|
||||
|
||||
if b.mixKey.isSome():
|
||||
let mixPrivKey = intoCurve25519Key(ncrutils.fromHex(b.mixKey.get()))
|
||||
let mixPubKey = public(mixPrivKey)
|
||||
return ok(some(MixConf(
|
||||
mixKey: mixPrivKey, mixPubKey: mixPubKey, mixNodes: b.mixNodes,
|
||||
rlnServiceUrl: b.rlnServiceUrl,
|
||||
)))
|
||||
else:
|
||||
if b.mixKey.isSome():
|
||||
let mixPrivKey = intoCurve25519Key(ncrutils.fromHex(b.mixKey.get()))
|
||||
let mixPubKey = public(mixPrivKey)
|
||||
return ok(
|
||||
some(MixConf(mixKey: mixPrivKey, mixPubKey: mixPubKey, mixNodes: b.mixNodes))
|
||||
)
|
||||
else:
|
||||
let (mixPrivKey, mixPubKey) = generateKeyPair().valueOr:
|
||||
return err("Generate key pair error: " & $error)
|
||||
return ok(
|
||||
some(MixConf(mixKey: mixPrivKey, mixPubKey: mixPubKey, mixNodes: b.mixNodes))
|
||||
)
|
||||
let (mixPrivKey, mixPubKey) = generateKeyPair().valueOr:
|
||||
return err("Generate key pair error: " & $error)
|
||||
return ok(some(MixConf(
|
||||
mixKey: mixPrivKey, mixPubKey: mixPubKey, mixNodes: b.mixNodes,
|
||||
rlnServiceUrl: b.rlnServiceUrl,
|
||||
)))
|
||||
|
||||
@ -168,7 +168,10 @@ proc setupProtocols(
|
||||
#mount mix
|
||||
if conf.mixConf.isSome():
|
||||
let mixConf = conf.mixConf.get()
|
||||
(await node.mountMix(conf.clusterId, mixConf.mixKey, mixConf.mixnodes)).isOkOr:
|
||||
(await node.mountMix(
|
||||
conf.clusterId, mixConf.mixKey, mixConf.mixnodes,
|
||||
rlnServiceUrl = mixConf.rlnServiceUrl,
|
||||
)).isOkOr:
|
||||
return err("failed to mount waku mix protocol: " & $error)
|
||||
|
||||
# Setup extended kademlia discovery
|
||||
|
||||
@ -51,6 +51,7 @@ type MixConf* = ref object
|
||||
mixKey*: Curve25519Key
|
||||
mixPubKey*: Curve25519Key
|
||||
mixnodes*: seq[MixNodePubInfo]
|
||||
rlnServiceUrl*: string
|
||||
|
||||
type KademliaDiscoveryConf* = object
|
||||
bootstrapNodes*: seq[(PeerId, seq[MultiAddress])]
|
||||
|
||||
@ -327,6 +327,7 @@ proc mountMix*(
|
||||
mixPrivKey: Curve25519Key,
|
||||
mixnodes: seq[MixNodePubInfo],
|
||||
userMessageLimit: Option[int] = none(int),
|
||||
rlnServiceUrl: string = "",
|
||||
): Future[Result[void, string]] {.async.} =
|
||||
info "mounting mix protocol", nodeId = node.info #TODO log the config used
|
||||
|
||||
@ -359,7 +360,7 @@ proc mountMix*(
|
||||
|
||||
node.wakuMix = WakuMix.new(
|
||||
localaddrStr, node.peerManager, clusterId, mixPrivKey, mixnodes, publishMessage,
|
||||
userMessageLimit,
|
||||
userMessageLimit, rlnServiceUrl,
|
||||
).valueOr:
|
||||
error "Waku Mix protocol initialization failed", err = error
|
||||
return
|
||||
|
||||
@ -21,7 +21,8 @@ import
|
||||
waku/node/peer_manager/waku_peer_store,
|
||||
mix_rln_spam_protection,
|
||||
waku/waku_relay,
|
||||
waku/common/nimchronos
|
||||
waku/common/nimchronos,
|
||||
./rln_service_client
|
||||
|
||||
logScope:
|
||||
topics = "waku mix"
|
||||
@ -87,6 +88,7 @@ proc new*(
|
||||
bootnodes: seq[MixNodePubInfo],
|
||||
publishMessage: PublishMessage,
|
||||
userMessageLimit: Option[int] = none(int),
|
||||
rlnServiceUrl: string = "",
|
||||
): WakuMixResult[T] =
|
||||
let mixPubKey = public(mixPrivKey)
|
||||
trace "mixPubKey", mixPubKey = mixPubKey
|
||||
@ -113,6 +115,14 @@ proc new*(
|
||||
let spamProtection = newMixRlnSpamProtection(spamProtectionConfig).valueOr:
|
||||
return err("failed to create spam protection: " & error)
|
||||
|
||||
# Wire up RLN service callbacks
|
||||
if rlnServiceUrl.len == 0:
|
||||
return err("rlnServiceUrl is required for group manager")
|
||||
spamProtection.setMerkleProofCallbacks(
|
||||
makeFetchMerkleProof(rlnServiceUrl),
|
||||
makeFetchLatestRoots(rlnServiceUrl),
|
||||
)
|
||||
|
||||
var m = WakuMix(
|
||||
peerManager: peermgr,
|
||||
clusterId: clusterId,
|
||||
@ -179,22 +189,7 @@ proc handleMessage*(
|
||||
|
||||
let contentTopic = message.contentTopic
|
||||
|
||||
if contentTopic == mix.mixRlnSpamProtection.getMembershipContentTopic():
|
||||
# Handle membership update
|
||||
let res = await mix.mixRlnSpamProtection.handleMembershipUpdate(message.payload)
|
||||
if res.isErr:
|
||||
warn "Failed to handle membership update", error = res.error
|
||||
else:
|
||||
trace "Handled membership update"
|
||||
|
||||
# Persist tree after membership changes (temporary solution)
|
||||
# TODO: Replace with proper persistence strategy (e.g., periodic snapshots)
|
||||
let saveRes = mix.mixRlnSpamProtection.saveTree()
|
||||
if saveRes.isErr:
|
||||
debug "Failed to save tree after membership update", error = saveRes.error
|
||||
else:
|
||||
trace "Saved tree after membership update"
|
||||
elif contentTopic == mix.mixRlnSpamProtection.getProofMetadataContentTopic():
|
||||
if contentTopic == mix.mixRlnSpamProtection.getProofMetadataContentTopic():
|
||||
# Handle proof metadata for network-wide spam detection
|
||||
let res = mix.mixRlnSpamProtection.handleProofMetadata(message.payload)
|
||||
if res.isErr:
|
||||
@ -209,31 +204,6 @@ proc getSpamProtectionContentTopics*(mix: WakuMix): seq[string] =
|
||||
return @[]
|
||||
return mix.mixRlnSpamProtection.getContentTopics()
|
||||
|
||||
proc saveSpamProtectionTree*(mix: WakuMix): Result[void, string] =
|
||||
## Save the spam protection membership tree to disk.
|
||||
## This allows preserving the tree state across restarts.
|
||||
if mix.mixRlnSpamProtection.isNil():
|
||||
return err("Spam protection not initialized")
|
||||
|
||||
mix.mixRlnSpamProtection.saveTree().mapErr(
|
||||
proc(e: string): string =
|
||||
e
|
||||
)
|
||||
|
||||
proc loadSpamProtectionTree*(mix: WakuMix): Result[void, string] =
|
||||
## Load the spam protection membership tree from disk.
|
||||
## Call this before init() to restore tree state from previous runs.
|
||||
## TODO: This is a temporary solution. Ideally nodes should sync tree state
|
||||
## via a store query for historical membership messages or via dedicated
|
||||
## tree sync protocol.
|
||||
if mix.mixRlnSpamProtection.isNil():
|
||||
return err("Spam protection not initialized")
|
||||
|
||||
mix.mixRlnSpamProtection.loadTree().mapErr(
|
||||
proc(e: string): string =
|
||||
e
|
||||
)
|
||||
|
||||
method start*(mix: WakuMix) {.async.} =
|
||||
info "starting waku mix protocol"
|
||||
|
||||
@ -244,23 +214,6 @@ method start*(mix: WakuMix) {.async.} =
|
||||
if initRes.isErr:
|
||||
error "Failed to initialize spam protection", error = initRes.error
|
||||
else:
|
||||
# Load existing tree to sync with other members
|
||||
# This should be done after init() (which loads credentials)
|
||||
# but before registerSelf() (which adds us to the tree)
|
||||
let loadRes = mix.mixRlnSpamProtection.loadTree()
|
||||
if loadRes.isErr:
|
||||
debug "No existing tree found or failed to load, starting fresh",
|
||||
error = loadRes.error
|
||||
else:
|
||||
debug "Loaded existing spam protection membership tree from disk"
|
||||
|
||||
# Restore our credentials to the tree (after tree load, whether it succeeded or not)
|
||||
# This ensures our member is in the tree if we have an index from keystore
|
||||
let restoreRes = mix.mixRlnSpamProtection.restoreCredentialsToTree()
|
||||
if restoreRes.isErr:
|
||||
error "Failed to restore credentials to tree", error = restoreRes.error
|
||||
|
||||
# Set up publish callback (must be before start so registerSelf can use it)
|
||||
mix.setupSpamProtectionCallbacks()
|
||||
|
||||
let startRes = await mix.mixRlnSpamProtection.start()
|
||||
@ -275,13 +228,6 @@ method start*(mix: WakuMix) {.async.} =
|
||||
else:
|
||||
debug "Registered spam protection credentials", index = registerRes.get()
|
||||
|
||||
# Save tree to persist membership state
|
||||
let saveRes = mix.mixRlnSpamProtection.saveTree()
|
||||
if saveRes.isErr:
|
||||
warn "Failed to save spam protection tree", error = saveRes.error
|
||||
else:
|
||||
trace "Saved spam protection tree to disk"
|
||||
|
||||
method stop*(mix: WakuMix) {.async.} =
|
||||
# Stop spam protection
|
||||
if not mix.mixRlnSpamProtection.isNil():
|
||||
|
||||
154
waku/waku_mix/rln_service_client.nim
Normal file
154
waku/waku_mix/rln_service_client.nim
Normal file
@ -0,0 +1,154 @@
|
||||
{.push raises: [].}
|
||||
|
||||
## JSON-RPC client for the external RLN Merkle proof service.
|
||||
##
|
||||
## Provides factory functions that create the callback procs expected by
|
||||
## GroupManager (FetchLatestRootsCallback, FetchMerkleProofCallback).
|
||||
|
||||
import std/[json, strutils]
|
||||
import chronos
|
||||
import chronos/apps/http/[httpclient, httpcommon]
|
||||
import results
|
||||
import stew/byteutils
|
||||
import chronicles
|
||||
|
||||
import mix_rln_spam_protection/types
|
||||
|
||||
logScope:
|
||||
topics = "waku mix rln-service"
|
||||
|
||||
# =============================================================================
|
||||
# Helpers
|
||||
# =============================================================================
|
||||
|
||||
proc jsonRpcCall(
|
||||
session: HttpSessionRef, address: HttpAddress, methodName: string,
|
||||
params: string = "[]",
|
||||
): Future[JsonNode] {.async.} =
|
||||
## Make a JSON-RPC call and return the result field.
|
||||
let body = """{"jsonrpc":"2.0","method":"""" & methodName &
|
||||
"""","params":""" & params & ""","id":1}"""
|
||||
|
||||
var req: HttpClientRequestRef = nil
|
||||
var res: HttpClientResponseRef = nil
|
||||
try:
|
||||
req = HttpClientRequestRef.post(
|
||||
session, address,
|
||||
body = body.toOpenArrayByte(0, body.len - 1),
|
||||
headers = @[("Content-Type", "application/json")],
|
||||
)
|
||||
res = await req.send()
|
||||
let resBytes = await res.getBodyBytes()
|
||||
let parsed = parseJson(string.fromBytes(resBytes))
|
||||
|
||||
if parsed.hasKey("error"):
|
||||
let errMsg = $parsed["error"]
|
||||
raise newException(CatchableError, "JSON-RPC error: " & errMsg)
|
||||
|
||||
if not parsed.hasKey("result"):
|
||||
raise newException(CatchableError, "JSON-RPC response missing 'result'")
|
||||
|
||||
return parsed["result"]
|
||||
finally:
|
||||
if req != nil:
|
||||
await req.closeWait()
|
||||
if res != nil:
|
||||
await res.closeWait()
|
||||
|
||||
proc hexToBytes32(hex: string): RlnResult[array[32, byte]] =
|
||||
## Parse a "0x..." hex string into a 32-byte array.
|
||||
var h = hex
|
||||
if h.startsWith("0x") or h.startsWith("0X"):
|
||||
h = h[2 .. ^1]
|
||||
if h.len != 64:
|
||||
return err("Expected 64 hex chars, got " & $h.len)
|
||||
var output: array[32, byte]
|
||||
for i in 0 ..< 32:
|
||||
try:
|
||||
output[i] = byte(parseHexInt(h[i * 2 .. i * 2 + 1]))
|
||||
except ValueError:
|
||||
return err("Invalid hex at position " & $i)
|
||||
ok(output)
|
||||
|
||||
# =============================================================================
|
||||
# Callback factories
|
||||
# =============================================================================
|
||||
|
||||
proc makeFetchLatestRoots*(
|
||||
serviceUrl: string
|
||||
): FetchLatestRootsCallback =
|
||||
## Create a callback that fetches the latest valid Merkle roots from the
|
||||
## RLN service via rln_getRoots. Returns 1–5 roots, newest first.
|
||||
let session = HttpSessionRef.new()
|
||||
let address = session.getAddress(serviceUrl)
|
||||
if address.isErr:
|
||||
warn "Invalid RLN service URL", url = serviceUrl, error = address.error
|
||||
return proc(): Future[RlnResult[seq[MerkleNode]]] {.async, gcsafe, raises: [].} =
|
||||
return err("Invalid RLN service URL: " & serviceUrl)
|
||||
|
||||
let httpAddress = address.get()
|
||||
|
||||
return proc(): Future[RlnResult[seq[MerkleNode]]] {.async, gcsafe, raises: [].} =
|
||||
try:
|
||||
let resultJson = await jsonRpcCall(session, httpAddress, "rln_getRoots")
|
||||
var roots: seq[MerkleNode]
|
||||
for elem in resultJson:
|
||||
let root = hexToBytes32(elem.getStr()).valueOr:
|
||||
return err("Invalid root hex: " & error)
|
||||
roots.add(MerkleNode(root))
|
||||
return ok(roots)
|
||||
except CatchableError as e:
|
||||
debug "Failed to fetch latest roots", error = e.msg
|
||||
return err("Failed to fetch roots: " & e.msg)
|
||||
|
||||
proc makeFetchMerkleProof*(
|
||||
serviceUrl: string
|
||||
): FetchMerkleProofCallback =
|
||||
## Create a callback that fetches a Merkle proof from the RLN service.
|
||||
let session = HttpSessionRef.new()
|
||||
let address = session.getAddress(serviceUrl)
|
||||
if address.isErr:
|
||||
warn "Invalid RLN service URL", url = serviceUrl, error = address.error
|
||||
return proc(
|
||||
index: MembershipIndex
|
||||
): Future[RlnResult[ExternalMerkleProof]] {.async, gcsafe, raises: [].} =
|
||||
return err("Invalid RLN service URL: " & serviceUrl)
|
||||
|
||||
let httpAddress = address.get()
|
||||
|
||||
return proc(
|
||||
index: MembershipIndex
|
||||
): Future[RlnResult[ExternalMerkleProof]] {.async, gcsafe, raises: [].} =
|
||||
try:
|
||||
let resultJson = await jsonRpcCall(
|
||||
session, httpAddress, "rln_getMerkleProof", "[" & $index & "]"
|
||||
)
|
||||
|
||||
# Parse root
|
||||
let rootHex = resultJson["root"].getStr()
|
||||
let root = hexToBytes32(rootHex).valueOr:
|
||||
return err("Invalid root hex: " & error)
|
||||
|
||||
# Parse pathElements: array of "0x..." hex strings -> concatenated bytes
|
||||
var pathElements: seq[byte]
|
||||
for elem in resultJson["pathElements"]:
|
||||
let elemBytes = hexToBytes32(elem.getStr()).valueOr:
|
||||
return err("Invalid pathElement hex: " & error)
|
||||
for b in elemBytes:
|
||||
pathElements.add(b)
|
||||
|
||||
# Parse identityPathIndex: array of ints -> byte per level
|
||||
var identityPathIndex: seq[byte]
|
||||
for idx in resultJson["identityPathIndex"]:
|
||||
identityPathIndex.add(byte(idx.getInt()))
|
||||
|
||||
return ok(ExternalMerkleProof(
|
||||
pathElements: pathElements,
|
||||
identityPathIndex: identityPathIndex,
|
||||
root: MerkleNode(root),
|
||||
))
|
||||
except CatchableError as e:
|
||||
debug "Failed to fetch Merkle proof", index = index, error = e.msg
|
||||
return err("Failed to fetch Merkle proof: " & e.msg)
|
||||
|
||||
{.pop.}
|
||||
Loading…
x
Reference in New Issue
Block a user