Context
logos-delivery currently ends up with two unrelated RLN keystore formats on disk:
-
Mix keystore (this plugin, src/mix_rln_spam_protection/credentials.nim, default rln_keystore.json from constants.nim):
{
"keystore": [
{
"version": 1,
"salt": "…",
"iv": "…",
"ciphertext": "…",
"membershipIndex": 5,
"userMessageLimit": 100
}
]
}
PBKDF2-HMAC-SHA256 (100k iters) → AES-256-CBC, no MAC; membershipIndex / userMessageLimit stored as plaintext metadata; effectively single-credential (only entries[0] is ever read).
-
Relay RLN keystore (nwaku waku_keystore AppKeystore format, Ethereum-keyfile style):
{
"application": "waku-rln-relay",
"appIdentifier": "01234567890abcdef",
"version": "0.2",
"credentials": {
"<membership-hash>": { "crypto": { "cipher": "aes-128-ctr", "kdf": "pbkdf2", "mac": "…", "...": "…" } }
}
}
Decrypted payload is a KeystoreMembership: membershipContract {chainId, address}, treeIndex, userMessageLimit, identityCredential — a superset of the mix entry. Has MAC integrity, 1M KDF iterations, encrypts everything, and supports multiple memberships per file.
The underlying credential is identical in both (BN254 RLN, idTrapdoor ‖ idNullifier ‖ idSecretHash ‖ idCommitment, 4×32 bytes), so unification is a serialization/plumbing exercise, not a crypto change.
Proposal
Adopt the AppKeystore container format in this plugin so a node can keep a single keystore file holding both relay and mix memberships.
Benefits beyond unification:
- Adds MAC integrity verification (missing today)
- Encrypts
membershipIndex / userMessageLimit instead of leaking them as plaintext
- Multi-membership support comes for free
Scope
File-level unification only. Relay and mix memberships remain separate credentials — we are NOT registering one identity in both groups (a shared idCommitment would publicly link the node's relay and mix identities; that is a separate, deliberate decision if ever).
Open points
- Discriminating mix vs relay entries. Mix memberships are off-chain (registered via relay broadcast), so
membershipContract has no natural value. Options: sentinel values (chainId: "", address: "mix"), or a distinct application value (e.g. "mix-rln") as a separate app-keystore document in the same file. Note: nwaku's loadAppKeystore currently errors on application mismatch instead of skipping to the next document — the multi-app-per-file route needs that changed to skip.
- Where the code lives. The plugin is standalone and shouldn't depend on logos-delivery. Either extract
waku_keystore into a shared nimble package, or reimplement the (small, well-specified) keyfile read/write here.
- Config plumbing in logos-delivery.
waku_mix/protocol.nim currently hardcodes rln_keystore_<peerId>.json and password "mix-rln-password" — should become config-driven (mirroring --rln-relay-cred-path / --rln-relay-cred-password), keeping the per-peerId scheme only for simulations/mixnet/.
- Migration. Detect old format (
"keystore" key vs "application" key), decrypt, re-encrypt as a KeystoreMembership entry.
Context
logos-delivery currently ends up with two unrelated RLN keystore formats on disk:
Mix keystore (this plugin,
src/mix_rln_spam_protection/credentials.nim, defaultrln_keystore.jsonfromconstants.nim):{ "keystore": [ { "version": 1, "salt": "…", "iv": "…", "ciphertext": "…", "membershipIndex": 5, "userMessageLimit": 100 } ] }PBKDF2-HMAC-SHA256 (100k iters) → AES-256-CBC, no MAC;
membershipIndex/userMessageLimitstored as plaintext metadata; effectively single-credential (onlyentries[0]is ever read).Relay RLN keystore (nwaku
waku_keystoreAppKeystore format, Ethereum-keyfile style):{ "application": "waku-rln-relay", "appIdentifier": "01234567890abcdef", "version": "0.2", "credentials": { "<membership-hash>": { "crypto": { "cipher": "aes-128-ctr", "kdf": "pbkdf2", "mac": "…", "...": "…" } } } }Decrypted payload is a
KeystoreMembership:membershipContract {chainId, address},treeIndex,userMessageLimit,identityCredential— a superset of the mix entry. Has MAC integrity, 1M KDF iterations, encrypts everything, and supports multiple memberships per file.The underlying credential is identical in both (BN254 RLN,
idTrapdoor ‖ idNullifier ‖ idSecretHash ‖ idCommitment, 4×32 bytes), so unification is a serialization/plumbing exercise, not a crypto change.Proposal
Adopt the AppKeystore container format in this plugin so a node can keep a single keystore file holding both relay and mix memberships.
Benefits beyond unification:
membershipIndex/userMessageLimitinstead of leaking them as plaintextScope
File-level unification only. Relay and mix memberships remain separate credentials — we are NOT registering one identity in both groups (a shared
idCommitmentwould publicly link the node's relay and mix identities; that is a separate, deliberate decision if ever).Open points
membershipContracthas no natural value. Options: sentinel values (chainId: "",address: "mix"), or a distinctapplicationvalue (e.g."mix-rln") as a separate app-keystore document in the same file. Note: nwaku'sloadAppKeystorecurrently errors on application mismatch instead of skipping to the next document — the multi-app-per-file route needs that changed to skip.waku_keystoreinto a shared nimble package, or reimplement the (small, well-specified) keyfile read/write here.waku_mix/protocol.nimcurrently hardcodesrln_keystore_<peerId>.jsonand password"mix-rln-password"— should become config-driven (mirroring--rln-relay-cred-path/--rln-relay-cred-password), keeping the per-peerId scheme only forsimulations/mixnet/."keystore"key vs"application"key), decrypt, re-encrypt as aKeystoreMembershipentry.