diff --git a/apps/chat2/config_chat2.nim b/apps/chat2/config_chat2.nim index d3c792515..c4ae74292 100644 --- a/apps/chat2/config_chat2.nim +++ b/apps/chat2/config_chat2.nim @@ -233,9 +233,8 @@ type name: "rln-relay-cred-path" }: string rlnRelayCredIndex* {. - desc: "the index of credentials to use", - defaultValue: 0 - name: "rln-relay-cred-index" }: uint + desc: "the index of the onchain commitment to use", + name: "rln-relay-cred-index" }: Option[uint] rlnRelayDynamic* {. desc: "Enable waku-rln-relay with on-chain dynamic group management: true|false", @@ -297,6 +296,12 @@ proc parseCmdArg*(T: type Port, p: string): T = proc completeCmdArg*(T: type Port, val: string): seq[string] = return @[] +proc parseCmdArg*(T: type Option[uint], p: string): T = + try: + some(parseUint(p)) + except CatchableError: + raise newException(ConfigurationError, "Invalid unsigned integer") + func defaultListenAddress*(conf: Chat2Conf): ValidIpAddress = # TODO: How should we select between IPv4 and IPv6 # Maybe there should be a config option for this. diff --git a/apps/wakunode2/external_config.nim b/apps/wakunode2/external_config.nim index 711ad6808..6bac5c3ff 100644 --- a/apps/wakunode2/external_config.nim +++ b/apps/wakunode2/external_config.nim @@ -146,9 +146,8 @@ type name: "rln-relay-cred-path" }: string rlnRelayCredIndex* {. - desc: "the index of credentials to use", - defaultValue: 0 - name: "rln-relay-membership-index" }: uint + desc: "the index of the onchain commitment to use", + name: "rln-relay-membership-index" }: Option[uint] rlnRelayDynamic* {. desc: "Enable waku-rln-relay with on-chain dynamic group management: true|false", @@ -506,6 +505,12 @@ proc parseCmdArg*(T: type Option[int], p: string): T = except CatchableError: raise newException(ConfigurationError, "Invalid number") +proc parseCmdArg*(T: type Option[uint], p: string): T = + try: + some(parseUint(p)) + except CatchableError: + raise newException(ConfigurationError, "Invalid unsigned integer") + ## Configuration validation let DbUrlRegex = re"^[\w\+]+:\/\/[\w\/\\\.\:\@]+$" diff --git a/tests/test_waku_keystore.nim b/tests/test_waku_keystore.nim index bf870c33d..952f186eb 100644 --- a/tests/test_waku_keystore.nim +++ b/tests/test_waku_keystore.nim @@ -3,7 +3,7 @@ import std/[os, json], chronos, - testutils/unittests + testutils/unittests import ../../waku/waku_keystore, ./testlib/common @@ -123,3 +123,89 @@ procSuite "Credentials test suite": recoveredCredentialsRes.isOk() recoveredCredentialsRes.get() == expectedMembership + test "if the keystore contains only one credential, fetch that irrespective of treeIndex": + + let filepath = "./testAppKeystore.txt" + defer: removeFile(filepath) + + # We generate random identity credentials (inter-value constrains are not enforced, otherwise we need to load e.g. zerokit RLN keygen) + let + idTrapdoor = randomSeqByte(rng[], 32) + idNullifier = randomSeqByte(rng[], 32) + idSecretHash = randomSeqByte(rng[], 32) + idCommitment = randomSeqByte(rng[], 32) + idCredential = IdentityCredential(idTrapdoor: idTrapdoor, idNullifier: idNullifier, idSecretHash: idSecretHash, idCommitment: idCommitment) + + let contract = MembershipContract(chainId: "5", address: "0x0123456789012345678901234567890123456789") + let index = MembershipIndex(1) + let membershipCredential = KeystoreMembership(membershipContract: contract, + treeIndex: index, + identityCredential: idCredential) + + let password = "%m0um0ucoW%" + + let keystoreRes = addMembershipCredentials(path = filepath, + membership = membershipCredential, + password = password, + appInfo = testAppInfo) + + assert(keystoreRes.isOk(), $keystoreRes.error) + + # We test retrieval of credentials. + let expectedMembership = membershipCredential + let membershipQuery = KeystoreMembership(membershipContract: contract) + + let recoveredCredentialsRes = getMembershipCredentials(path = filepath, + password = password, + query = membershipQuery, + appInfo = testAppInfo) + + assert(recoveredCredentialsRes.isOk(), $recoveredCredentialsRes.error) + check: recoveredCredentialsRes.get() == expectedMembership + + test "if the keystore contains multiple credentials, then error out if treeIndex has not been passed in": + let filepath = "./testAppKeystore.txt" + defer: removeFile(filepath) + + # We generate random identity credentials (inter-value constrains are not enforced, otherwise we need to load e.g. zerokit RLN keygen) + let + idTrapdoor = randomSeqByte(rng[], 32) + idNullifier = randomSeqByte(rng[], 32) + idSecretHash = randomSeqByte(rng[], 32) + idCommitment = randomSeqByte(rng[], 32) + idCredential = IdentityCredential(idTrapdoor: idTrapdoor, idNullifier: idNullifier, idSecretHash: idSecretHash, idCommitment: idCommitment) + + # We generate two distinct membership groups + let contract = MembershipContract(chainId: "5", address: "0x0123456789012345678901234567890123456789") + let index = MembershipIndex(1) + var membershipCredential = KeystoreMembership(membershipContract: contract, + treeIndex: index, + identityCredential: idCredential) + + let password = "%m0um0ucoW%" + + let keystoreRes = addMembershipCredentials(path = filepath, + membership = membershipCredential, + password = password, + appInfo = testAppInfo) + + assert(keystoreRes.isOk(), $keystoreRes.error) + + membershipCredential.treeIndex = MembershipIndex(2) + let keystoreRes2 = addMembershipCredentials(path = filepath, + membership = membershipCredential, + password = password, + appInfo = testAppInfo) + assert(keystoreRes2.isOk(), $keystoreRes2.error) + + # We test retrieval of credentials. + let membershipQuery = KeystoreMembership(membershipContract: contract) + + let recoveredCredentialsRes = getMembershipCredentials(path = filepath, + password = password, + query = membershipQuery, + appInfo = testAppInfo) + + check: + recoveredCredentialsRes.isErr() + recoveredCredentialsRes.error.kind == KeystoreCredentialNotFoundError diff --git a/tests/waku_rln_relay/test_waku_rln_relay.nim b/tests/waku_rln_relay/test_waku_rln_relay.nim index f66f8cd04..066aaad33 100644 --- a/tests/waku_rln_relay/test_waku_rln_relay.nim +++ b/tests/waku_rln_relay/test_waku_rln_relay.nim @@ -662,7 +662,7 @@ suite "Waku rln relay": let index = MembershipIndex(5) let rlnConf = WakuRlnConfig(rlnRelayDynamic: false, - rlnRelayCredIndex: index.uint, + rlnRelayCredIndex: some(index), rlnRelayTreePath: genTempPath("rln_tree", "waku_rln_relay_2")) let wakuRlnRelayRes = await WakuRlnRelay.new(rlnConf) require: @@ -714,7 +714,7 @@ suite "Waku rln relay": let index = MembershipIndex(5) let rlnConf = WakuRlnConfig(rlnRelayDynamic: false, - rlnRelayCredIndex: index.uint, + rlnRelayCredIndex: some(index), rlnRelayBandwidthThreshold: 4, rlnRelayTreePath: genTempPath("rln_tree", "waku_rln_relay_3")) let wakuRlnRelayRes = await WakuRlnRelay.new(rlnConf) diff --git a/tests/waku_rln_relay/test_wakunode_rln_relay.nim b/tests/waku_rln_relay/test_wakunode_rln_relay.nim index 48478a785..7a040d0be 100644 --- a/tests/waku_rln_relay/test_wakunode_rln_relay.nim +++ b/tests/waku_rln_relay/test_wakunode_rln_relay.nim @@ -45,7 +45,7 @@ procSuite "WakuNode - RLN relay": # mount rlnrelay in off-chain mode await node1.mountRlnRelay(WakuRlnConfig(rlnRelayDynamic: false, - rlnRelayCredIndex: 1.uint, + rlnRelayCredIndex: some(1.uint), rlnRelayTreePath: genTempPath("rln_tree", "wakunode"), )) @@ -55,7 +55,7 @@ procSuite "WakuNode - RLN relay": await node2.mountRelay(@[DefaultPubsubTopic]) # mount rlnrelay in off-chain mode await node2.mountRlnRelay(WakuRlnConfig(rlnRelayDynamic: false, - rlnRelayCredIndex: 2.uint, + rlnRelayCredIndex: some(2.uint), rlnRelayTreePath: genTempPath("rln_tree", "wakunode_2"), )) @@ -65,7 +65,7 @@ procSuite "WakuNode - RLN relay": await node3.mountRelay(@[DefaultPubsubTopic]) await node3.mountRlnRelay(WakuRlnConfig(rlnRelayDynamic: false, - rlnRelayCredIndex: 3.uint, + rlnRelayCredIndex: some(3.uint), rlnRelayTreePath: genTempPath("rln_tree", "wakunode_3"), )) @@ -126,7 +126,7 @@ procSuite "WakuNode - RLN relay": # mount rlnrelay in off-chain mode for index, node in nodes: await node.mountRlnRelay(WakuRlnConfig(rlnRelayDynamic: false, - rlnRelayCredIndex: index.uint + 1, + rlnRelayCredIndex: some(index.uint + 1), rlnRelayTreePath: genTempPath("rln_tree", "wakunode_" & $(index+1)))) # start them @@ -204,7 +204,7 @@ procSuite "WakuNode - RLN relay": # mount rlnrelay in off-chain mode await node1.mountRlnRelay(WakuRlnConfig(rlnRelayDynamic: false, - rlnRelayCredIndex: 1.uint, + rlnRelayCredIndex: some(1.uint), rlnRelayTreePath: genTempPath("rln_tree", "wakunode_4"), rlnRelayBandwidthThreshold: 0, )) @@ -215,7 +215,7 @@ procSuite "WakuNode - RLN relay": await node2.mountRelay(@[DefaultPubsubTopic]) # mount rlnrelay in off-chain mode await node2.mountRlnRelay(WakuRlnConfig(rlnRelayDynamic: false, - rlnRelayCredIndex: 2.uint, + rlnRelayCredIndex: some(2.uint), rlnRelayTreePath: genTempPath("rln_tree", "wakunode_5"), rlnRelayBandwidthThreshold: 0, )) @@ -226,7 +226,7 @@ procSuite "WakuNode - RLN relay": await node3.mountRelay(@[DefaultPubsubTopic]) await node3.mountRlnRelay(WakuRlnConfig(rlnRelayDynamic: false, - rlnRelayCredIndex: 3.uint, + rlnRelayCredIndex: some(3.uint), rlnRelayTreePath: genTempPath("rln_tree", "wakunode_6"), rlnRelayBandwidthThreshold: 0, )) @@ -306,7 +306,7 @@ procSuite "WakuNode - RLN relay": # mount rlnrelay in off-chain mode await node1.mountRlnRelay(WakuRlnConfig(rlnRelayDynamic: false, - rlnRelayCredIndex: 1.uint, + rlnRelayCredIndex: some(1.uint), rlnRelayTreePath: genTempPath("rln_tree", "wakunode_7"), rlnRelayBandwidthThreshold: 0, )) @@ -318,7 +318,7 @@ procSuite "WakuNode - RLN relay": # mount rlnrelay in off-chain mode await node2.mountRlnRelay(WakuRlnConfig(rlnRelayDynamic: false, - rlnRelayCredIndex: 2.uint, + rlnRelayCredIndex: some(2.uint), rlnRelayTreePath: genTempPath("rln_tree", "wakunode_8"), rlnRelayBandwidthThreshold: 0, )) @@ -330,7 +330,7 @@ procSuite "WakuNode - RLN relay": # mount rlnrelay in off-chain mode await node3.mountRlnRelay(WakuRlnConfig(rlnRelayDynamic: false, - rlnRelayCredIndex: 3.uint, + rlnRelayCredIndex: some(3.uint), rlnRelayTreePath: genTempPath("rln_tree", "wakunode_9"), rlnRelayBandwidthThreshold: 0, )) diff --git a/tests/wakunode_jsonrpc/test_jsonrpc_relay.nim b/tests/wakunode_jsonrpc/test_jsonrpc_relay.nim index 39f1b1a7e..919b4d006 100644 --- a/tests/wakunode_jsonrpc/test_jsonrpc_relay.nim +++ b/tests/wakunode_jsonrpc/test_jsonrpc_relay.nim @@ -106,11 +106,11 @@ suite "Waku v2 JSON-RPC API - Relay": when defined(rln): await srcNode.mountRlnRelay(WakuRlnConfig(rlnRelayDynamic: false, - rlnRelayCredIndex: 1, + rlnRelayCredIndex: some(1.uint), rlnRelayTreePath: genTempPath("rln_tree", "wakunode_1"))) await dstNode.mountRlnRelay(WakuRlnConfig(rlnRelayDynamic: false, - rlnRelayCredIndex: 2, + rlnRelayCredIndex: some(2.uint), rlnRelayTreePath: genTempPath("rln_tree", "wakunode_2"))) await srcNode.connectToNodes(@[dstNode.peerInfo.toRemotePeerInfo()]) diff --git a/tests/wakunode_rest/test_rest_relay.nim b/tests/wakunode_rest/test_rest_relay.nim index 1550004d0..e59fb6899 100644 --- a/tests/wakunode_rest/test_rest_relay.nim +++ b/tests/wakunode_rest/test_rest_relay.nim @@ -188,7 +188,7 @@ suite "Waku v2 Rest API - Relay": await node.mountRelay() when defined(rln): await node.mountRlnRelay(WakuRlnConfig(rlnRelayDynamic: false, - rlnRelayCredIndex: 1, + rlnRelayCredIndex: some(1.uint), rlnRelayTreePath: genTempPath("rln_tree", "wakunode_1"))) # RPC server setup diff --git a/waku/waku_keystore/keystore.nim b/waku/waku_keystore/keystore.nim index c7d491c9c..18fad47c0 100644 --- a/waku/waku_keystore/keystore.nim +++ b/waku/waku_keystore/keystore.nim @@ -4,7 +4,10 @@ else: {.push raises: [].} import - options, json, strutils, + options, + json, + strutils, + sequtils, std/[tables, os] import @@ -144,7 +147,7 @@ proc addMembershipCredentials*(path: string, let encodedMembershipCredential = membership.encode() let keyfileRes = createKeyFileJson(encodedMembershipCredential, password) if keyfileRes.isErr(): - return err(AppKeystoreError(kind: KeystoreCreateKeyfileError, + return err(AppKeystoreError(kind: KeystoreCreateKeyfileError, msg: $keyfileRes.error)) # We add it to the credentials field of the keystore @@ -183,13 +186,24 @@ proc getMembershipCredentials*(path: string, if jsonKeystore.hasKey("credentials"): # We get all credentials in keystore var keystoreCredentials = jsonKeystore["credentials"] - let key = query.hash() - if not keystoreCredentials.hasKey(key): + if keystoreCredentials.len == 0: # error return err(AppKeystoreError(kind: KeystoreCredentialNotFoundError, - msg: "Credential not found in keystore")) + msg: "No credentials found in keystore")) + var keystoreCredential: JsonNode + if keystoreCredentials.len == 1: + keystoreCredential = keystoreCredentials + .getFields() + .values() + .toSeq()[0] + else: + let key = query.hash() + if not keystoreCredentials.hasKey(key): + # error + return err(AppKeystoreError(kind: KeystoreCredentialNotFoundError, + msg: "Credential not found in keystore")) + keystoreCredential = keystoreCredentials[key] - let keystoreCredential = keystoreCredentials[key] let decodedKeyfileRes = decodeKeyFileJson(keystoreCredential, password) if decodedKeyfileRes.isErr(): return err(AppKeystoreError(kind: KeystoreReadKeyfileError, 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 4b7ab58bc..56c82cb27 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 @@ -440,23 +440,23 @@ method init*(g: OnchainGroupManager): Future[void] {.async.} = g.registryContract = some(registryContract) if g.keystorePath.isSome() and g.keystorePassword.isSome(): - if g.membershipIndex.isNone(): - raise newException(CatchableError, "membership index is not set when keystore is provided") - let keystoreQuery = KeystoreMembership( + var keystoreQuery = KeystoreMembership( membershipContract: MembershipContract( chainId: $g.chainId.get(), address: g.ethContractAddress - ), - treeIndex: MembershipIndex(g.membershipIndex.get()), + ) ) + if g.membershipIndex.isSome(): + keystoreQuery.treeIndex = MembershipIndex(g.membershipIndex.get()) waku_rln_membership_credentials_import_duration_seconds.nanosecondTime: let keystoreCredRes = getMembershipCredentials(path = g.keystorePath.get(), password = g.keystorePassword.get(), query = keystoreQuery, appInfo = RLNAppInfo) if keystoreCredRes.isErr(): - raise newException(ValueError, "could not parse the keystore: " & $keystoreCredRes.error) + raise newException(CatchableError, "could not parse the keystore: " & $keystoreCredRes.error) let keystoreCred = keystoreCredRes.get() + g.membershipIndex = some(keystoreCred.treeIndex) # now we check on the contract if the commitment actually has a membership try: let membershipExists = await rlnContract.memberExists(keystoreCred diff --git a/waku/waku_rln_relay/rln_relay.nim b/waku/waku_rln_relay/rln_relay.nim index c9ed26786..e0b65dc04 100644 --- a/waku/waku_rln_relay/rln_relay.nim +++ b/waku/waku_rln_relay/rln_relay.nim @@ -31,7 +31,7 @@ logScope: type WakuRlnConfig* = object rlnRelayDynamic*: bool - rlnRelayCredIndex*: uint + rlnRelayCredIndex*: Option[uint] rlnRelayEthContractAddress*: string rlnRelayEthClientAddress*: string rlnRelayCredPath*: string @@ -374,7 +374,7 @@ proc mount(conf: WakuRlnConfig, raise newException(ValueError, "Static group keys are not valid") groupManager = StaticGroupManager(groupSize: StaticGroupSize, groupKeys: parsedGroupKeysRes.get(), - membershipIndex: some(conf.rlnRelayCredIndex), + membershipIndex: conf.rlnRelayCredIndex, rlnInstance: rlnInstance) # we don't persist credentials in static mode since they exist in ./constants.nim else: @@ -390,7 +390,7 @@ proc mount(conf: WakuRlnConfig, registrationHandler: registrationHandler, keystorePath: rlnRelayCredPath, keystorePassword: rlnRelayCredPassword, - membershipIndex: some(conf.rlnRelayCredIndex)) + membershipIndex: conf.rlnRelayCredIndex) # Initialize the groupManager await groupManager.init() # Start the group sync