From cfca7a6af77a674ed5a3f68b1bc1a2d9db8e2524 Mon Sep 17 00:00:00 2001 From: rymnc <43716372+rymnc@users.noreply.github.com> Date: Wed, 14 Dec 2022 18:47:30 +0530 Subject: [PATCH] feat(rln-relay): group manager api --- .../protocol/waku_rln_relay/group_manager.nim | 6 + .../group_manager/group_manager_base.nim | 66 ++++++++++ .../waku_rln_relay/group_manager/on_chain.nim | 3 + .../group_manager/on_chain/group_manager.nim | 118 ++++++++++++++++++ .../waku_rln_relay/group_manager/static.nim | 3 + .../group_manager/static/group_manager.nim | 97 ++++++++++++++ 6 files changed, 293 insertions(+) create mode 100644 waku/v2/protocol/waku_rln_relay/group_manager.nim create mode 100644 waku/v2/protocol/waku_rln_relay/group_manager/group_manager_base.nim create mode 100644 waku/v2/protocol/waku_rln_relay/group_manager/on_chain.nim create mode 100644 waku/v2/protocol/waku_rln_relay/group_manager/on_chain/group_manager.nim create mode 100644 waku/v2/protocol/waku_rln_relay/group_manager/static.nim create mode 100644 waku/v2/protocol/waku_rln_relay/group_manager/static/group_manager.nim diff --git a/waku/v2/protocol/waku_rln_relay/group_manager.nim b/waku/v2/protocol/waku_rln_relay/group_manager.nim new file mode 100644 index 0000000000..1e1a17b488 --- /dev/null +++ b/waku/v2/protocol/waku_rln_relay/group_manager.nim @@ -0,0 +1,6 @@ +import + group_manager/[static, on_chain] + +export + static, + on_chain \ No newline at end of file diff --git a/waku/v2/protocol/waku_rln_relay/group_manager/group_manager_base.nim b/waku/v2/protocol/waku_rln_relay/group_manager/group_manager_base.nim new file mode 100644 index 0000000000..432e6eac6f --- /dev/null +++ b/waku/v2/protocol/waku_rln_relay/group_manager/group_manager_base.nim @@ -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() diff --git a/waku/v2/protocol/waku_rln_relay/group_manager/on_chain.nim b/waku/v2/protocol/waku_rln_relay/group_manager/on_chain.nim new file mode 100644 index 0000000000..e6a900f973 --- /dev/null +++ b/waku/v2/protocol/waku_rln_relay/group_manager/on_chain.nim @@ -0,0 +1,3 @@ +import on_chain/group_manager + +export group_manager \ No newline at end of file diff --git a/waku/v2/protocol/waku_rln_relay/group_manager/on_chain/group_manager.nim b/waku/v2/protocol/waku_rln_relay/group_manager/on_chain/group_manager.nim new file mode 100644 index 0000000000..1036ba7ed2 --- /dev/null +++ b/waku/v2/protocol/waku_rln_relay/group_manager/on_chain/group_manager.nim @@ -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 \ No newline at end of file diff --git a/waku/v2/protocol/waku_rln_relay/group_manager/static.nim b/waku/v2/protocol/waku_rln_relay/group_manager/static.nim new file mode 100644 index 0000000000..64a0f8ec3a --- /dev/null +++ b/waku/v2/protocol/waku_rln_relay/group_manager/static.nim @@ -0,0 +1,3 @@ +import static/group_manager + +export group_manager \ No newline at end of file diff --git a/waku/v2/protocol/waku_rln_relay/group_manager/static/group_manager.nim b/waku/v2/protocol/waku_rln_relay/group_manager/static/group_manager.nim new file mode 100644 index 0000000000..070add17c0 --- /dev/null +++ b/waku/v2/protocol/waku_rln_relay/group_manager/static/group_manager.nim @@ -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() \ No newline at end of file