Skip to content

Commit

Permalink
feat(rln-relay): group manager api
Browse files Browse the repository at this point in the history
  • Loading branch information
rymnc committed Dec 14, 2022
1 parent b38bf15 commit cfca7a6
Show file tree
Hide file tree
Showing 6 changed files with 293 additions and 0 deletions.
6 changes: 6 additions & 0 deletions waku/v2/protocol/waku_rln_relay/group_manager.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import
group_manager/[static, on_chain]

export
static,
on_chain
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# This module contains the GroupManagerBase interface
# The GroupManager is responsible for managing the group state
# It should be used to register new members, and withdraw existing members
# It should also be used to sync the group state with the rest of the group members

type OnRegisterCallback* = proc (registrations: seq[(IDCommitment, MembershipIndex)]) {.async, gcsafe.}
type OnWithdrawCallback* = proc (withdrawals: seq[(IDCommitment, MembershipIndex)]) {.async, gcsafe.}

type
GroupManagerBase*[Config] = ref object of RootObj
idCredentials*: Option[IdentityCredentials]
onRegisterCb: Option[OnRegisterCallback]
onWithdrawCb: Option[OnWithdrawCallback]
config*: Config
rlnInstance: ptr RLN
initialized*: bool

# This method is used to initialize the group manager
# Any initialization logic should be implemented here
method init*(g: GroupManagerBase): Result[void] {.async,base.} =
return err("init method for " & $g.kind & " is not implemented yet")

# This method is used to start the group sync process
# It should be used to sync the group state with the rest of the group members
method startGroupSync*(g: GroupManagerBase): Result[void] {.async,base.} =
return err("startGroupSync method for " & $g.kind & " is not implemented yet")

# This method is used to register a new identity commitment into the merkle tree
# The user may or may not have the identity secret to this commitment
# It should be used when detecting new members in the group, and syncing the group state
method register*(g: GroupManagerBase, idCommitment: IDCommitment): Result[void] {.async,base.} =
return err("register method for " & $g.kind & " is not implemented yet")

# This method is used to register a new identity commitment into the merkle tree
# The user should have the identity secret to this commitment
# It should be used when the user wants to join the group
method register*(g: GroupManagerBase, credentials: IdentityCredentials): Result[void] {.async,base.} =
return err("register method for " & $g.kind & " is not implemented yet")

# This method is used to register a batch of new identity commitments into the merkle tree
# The user may or may not have the identity secret to these commitments
# It should be used when detecting a batch of new members in the group, and syncing the group state
method registerBatch*(g: GroupManagerBase, idCommitments: seq[IDCommitment]): Result[void] {.async,base.} =
return err("registerBatch method for " & $g.kind & " is not implemented yet")

# This method is used to set a callback that will be called when a new identity commitment is registered
# The callback may be called multiple times, and should be used to for any post processing
method onRegister*(g: GroupManagerBase, cb: OnRegisterCallback): Result[void] {.base.} =
g.onRegisterCb = some(cb)
return ok()

# This method is used to withdraw/remove an identity commitment from the merkle tree
# The user should have the identity secret hash to this commitment, by either deriving it, or owning it
method withdraw*(g: GroupManagerBase, identitySecretHash: IdentitySecretHash): Result[void] {.async,base.} =
return err("withdraw method for " & $g.kind & " is not implemented yet")

# This method is used to withdraw/remove a batch of identity commitments from the merkle tree
# The user should have the identity secret hash to these commitments, by either deriving them, or owning them
method withdrawBatch*(g: GroupManagerBase, identitySecretHashes: seq[IdentitySecretHash]): Result[void] {.async,base.} =
return err("withdrawBatch method for " & $g.kind & " is not implemented yet")

# This method is used to set a callback that will be called when an identity commitment is withdrawn
# The callback may be called multiple times, and should be used to for any post processing
method onWithdraw*(g: GroupManagerBase, cb: OnRegisterCallback): Result[void] {.base.} =
g.onWithdrawCb = some(cb)
return ok()
3 changes: 3 additions & 0 deletions waku/v2/protocol/waku_rln_relay/group_manager/on_chain.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import on_chain/group_manager

export group_manager
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import
../group_manager_base

type
RlnContractWithSender = Sender[RlnContract]
OnchainGroupManagerConfig* = object
ethClientUrl*: string
ethPrivateKey*: Option[string]
ethContractAddress*: string
ethRpc: Option[web3]
rlnContract: Option[RlnContractWithSender]
membershipFee: Option[Uint256]

OnchainGroupManager* = ref object of GroupManagerBase[OnchainGroupManagerConfig]

template initializedGuard*(g: OnchainGroupManager): untyped =
if not g.initialized:
return err("OnchainGroupManager is not initialized")

method init*(g: OnchainGroupManager): Result[void] {.async.} =
var web3: Web3
var contract: RlnContractWithSender
# check if the Ethereum client is reachable
try:
web3 = some(await newWeb3(ethClientAddress))
except:
return err("could not connect to the Ethereum client")

contract = some(web3.contractSender(RlnContract, g.config.ethContractAddress))

# check if the contract exists by calling a static method
var membershipFee: uint64
try:
membershipFee = await contract.MEMBERSHIP_DEPOSIT()
except:
return err("could not get the membership deposit")

if g.config.ethPrivateKey.isSome():
let pk = g.config.ethPrivateKey.get()
web3.privateKey = pk

g.config.ethRpc = some(web3)
g.config.rlnContract = some(contract)
g.config.membershipFee = some(membershipFee)

g.initialized = true
return ok()

method startGroupSync*(g: OnchainGroupManager): Result[void] {.async.} =
initializedGuard(g)

if g.config.ethPrivateKey.isSome():
# TODO: use register() after generating credentials
debug "registering commitment on contract"
await g.register(g.config.idCredentials)

# TODO: set up the contract event listener and block listener


method register*(g: OnchainGroupManager, idCommitment: IDCommitment): Result[void] {.async.} =
initializedGuard(g)

let memberInserted = g.rlnInstance.insertMember(idCommitment)
if not memberInserted:
return err("Failed to insert member into the merkle tree")

if g.onRegisterCb.isSome():
await g.onRegisterCb.get()(@[idCommitment])

return ok()

method register*(g: OnchainGroupManager, identityCredentials: IdentityCredentials): Result[void] {.async.} =
initializedGuard(g)

# TODO: interact with the contract
let ethRpc = g.config.ethRpc.get()
let rlnContract = g.config.rlnContract.get()
let membershipFee = g.config.membershipFee.get()

let gasPrice = int(await ethRpc.provider.eth_gasPrice()) * 2
let idCommitment = identityCredentials.idCommitment.toUInt256()

var txHash: TxHash
try: # send the registration transaction and check if any error occurs
txHash = await rlnContract.register(pk).send(value = membershipFee, gasPrice = gasPrice)
except ValueError as e:
return err("registration transaction failed: " & e.msg)

let tsReceipt = await ethRpc.getMinedTransactionReceipt(txHash)

# the receipt topic holds the hash of signature of the raised events
# TODO: make this robust. search within the event list for the event
let firstTopic = tsReceipt.logs[0].topics[0]
# the hash of the signature of MemberRegistered(uint256,uint256) event is equal to the following hex value
if firstTopic[0..65] != "0x5a92c2530f207992057b9c3e544108ffce3beda4a63719f316967c49bf6159d2":
return err("invalid event signature hash")

# the arguments of the raised event i.e., MemberRegistered are encoded inside the data field
# data = pk encoded as 256 bits || index encoded as 256 bits
let arguments = tsReceipt.logs[0].data
debug "tx log data", arguments=arguments
let
argumentsBytes = arguments.hexToSeqByte()
# In TX log data, uints are encoded in big endian
eventIndex = UInt256.fromBytesBE(argumentsBytes[32..^1])

# don't handle member insertion into the tree here, it will be handled by the event listener
return ok()

method withdraw*(g: OnchainGroupManager, idCommitment: IDCommitment): Result[void] {.async.} =
initializedGuard(g)

# TODO: after slashing is enabled on the contract

method withdrawBatch*(g: OnchainGroupManager, idCommitments: seq[IDCommitment]): Result[void] {.async.} =
initializedGuard(g)

# TODO: after slashing is enabled on the contract
3 changes: 3 additions & 0 deletions waku/v2/protocol/waku_rln_relay/group_manager/static.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import static/group_manager

export group_manager
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import
../group_manager_base

type
StaticGroupManagerConfig* = object
rawGroupKeys*: seq[string]
groupKeys*: Option[seq[IdentityCredentials]]
groupSize*: uint
membershipIndex*: MembershipIndex

StaticGroupManager* = ref object of GroupManagerBase[StaticGroupManagerConfig]

template initializedGuard*(g: StaticGroupManager): untyped =
if not g.initialized:
return err("StaticGroupManager is not initialized")

method init*(g: StaticGroupManager): Result[void] =
let
rawGroupKeys = g.config.rawGroupKeys
groupSize = g.config.groupSize
membershipIndex = g.config.membershipIndex

if membershipIndex < MembershipIndex(0) or membershipIndex >= MembershipIndex(groupSize):
return err("Invalid membership index. Must be within 0 and " & $(groupSize - 1) & "but was " & $membershipIndex)

let parsedGroupKeys = rawGroupKeys.map(parseGroupKey)
if parsedGroupKeys.anyIt(it.isErr):
return err("Invalid group key: " & $parsedGroupKeys.findIt(it.isErr).getErr())

g.config.groupKeys = some(parsedGroupKeys.mapIt(it.get()))
g.idCredentials = g.config.groupKeys[membershipIndex]

# Seed the received commitments into the merkle tree
let membersInserted = g.rlnInstance.insertMembers(g.config.groupKeys.mapIt(it.idCommitment))
if not membersInserted:
return err("Failed to insert members into the merkle tree")

g.initialized = true
return ok()

method startGroupSync*(g: StaticGroupManager): Result[void] {.async.} =
initializedGuard(g)
# No-op
return ok()

method register*(g: StaticGroupManager, idCommitment: IDCommitment): Result[void] {.async.} =
initializedGuard(g)

let memberInserted = g.rlnInstance.insertMember(idCommitment)
if not memberInserted:
return err("Failed to insert member into the merkle tree")

if g.onRegisterCb.isSome():
await g.onRegisterCb.get()(@[idCommitment])

return ok()


method registerBatch*(g: StaticGroupManager, idCommitments: seq[IDCommitment]): Result[void] {.async.} =
initializedGuard(g)

let membersInserted = g.rlnInstance.insertMembers(idCommitments)
if not membersInserted:
return err("Failed to insert members into the merkle tree")

if g.onRegisterCb.isSome():
await g.onRegisterCb.get()(idCommitments)

return ok()

method withdraw*(g: StaticGroupManager, idSecretHash: IdentitySecretHash): Result[void] {.async.} =
initializedGuard(g)

let groupKeys = g.config.groupKeys.get()
let idCommitment = groupKeys.findIt(it.idSecretHash == idSecretHash).get().idCommitment
let memberRemoved = g.rlnInstance.removeMember(idCommitment)
if not memberRemoved:
return err("Failed to remove member from the merkle tree")

if g.onWithdrawCb.isSome():
await g.onWithdrawCb.get()(@[idCommitment])

return ok()

method withdrawBatch*(g: StaticGroupManager, idSecretHashes: seq[IdentitySecretHash]): Result[void] {.async.} =
initializedGuard(g)

let groupKeys = g.config.groupKeys.get()
let identityCommitments = idSecretHashes.map(groupKeys.findIt(it.idSecretHash == idSecretHash).get().idCommitment)
let membersRemoved = g.rlnInstance.removeMembers(idCommitments)
if not membersRemoved:
return err("Failed to remove members from the merkle tree")

if g.onWithdrawCb.isSome():
await g.onWithdrawCb.get()(idCommitments)

return ok()

0 comments on commit cfca7a6

Please sign in to comment.