UNPKG

@waku/rln

Version:

RLN (Rate Limiting Nullifier) implementation for Waku

92 lines (89 loc) 3.77 kB
import { sha256 } from '../../../../../node_modules/@noble/hashes/esm/sha256.js'; import '../../../../interfaces/dist/protocols.js'; import '../../../../interfaces/dist/connection_manager.js'; import { DEFAULT_CLUSTER_ID } from '../../../../interfaces/dist/constants.js'; import '../../../../interfaces/dist/health_status.js'; import { concat, utf8ToBytes } from '../../bytes/index.js'; const singleShardInfoToPubsubTopic = (shardInfo) => { if (shardInfo.shard === undefined) throw new Error("Invalid shard"); return `/waku/2/rs/${shardInfo.clusterId ?? DEFAULT_CLUSTER_ID}/${shardInfo.shard}`; }; /** * Given a string, will throw an error if it is not formatted as a valid content topic for autosharding based on https://rfc.vac.dev/spec/51/ * @param contentTopic String to validate * @returns Object with each content topic field as an attribute */ function ensureValidContentTopic(contentTopic) { const parts = contentTopic.split("/"); if (parts.length < 5 || parts.length > 6) { throw Error("Content topic format is invalid"); } // Validate generation field if present let generation = 0; if (parts.length == 6) { generation = parseInt(parts[1]); if (isNaN(generation)) { throw new Error("Invalid generation field in content topic"); } if (generation > 0) { throw new Error("Generation greater than 0 is not supported"); } } // Validate remaining fields const fields = parts.splice(-4); // Validate application field if (fields[0].length == 0) { throw new Error("Application field cannot be empty"); } // Validate version field if (fields[1].length == 0) { throw new Error("Version field cannot be empty"); } // Validate topic name field if (fields[2].length == 0) { throw new Error("Topic name field cannot be empty"); } // Validate encoding field if (fields[3].length == 0) { throw new Error("Encoding field cannot be empty"); } return { generation, application: fields[0], version: fields[1], topicName: fields[2], encoding: fields[3] }; } /** * Given a string, determines which autoshard index to use for its pubsub topic. * Based on the algorithm described in the RFC: https://rfc.vac.dev/spec/51//#algorithm */ function contentTopicToShardIndex(contentTopic, networkShards = 8) { const { application, version } = ensureValidContentTopic(contentTopic); const digest = sha256(concat([utf8ToBytes(application), utf8ToBytes(version)])); const dataview = new DataView(digest.buffer.slice(-8)); return Number(dataview.getBigUint64(0, false) % BigInt(networkShards)); } function contentTopicToPubsubTopic(contentTopic, clusterId = DEFAULT_CLUSTER_ID, networkShards = 8) { if (!contentTopic) { throw Error("Content topic must be specified"); } const shardIndex = contentTopicToShardIndex(contentTopic, networkShards); return `/waku/2/rs/${clusterId}/${shardIndex}`; } /** * Used when creating encoders/decoders to determine which pubsub topic to use */ function determinePubsubTopic(contentTopic, // TODO: make it accept ShardInfo https://github.com/waku-org/js-waku/issues/2086 pubsubTopicShardInfo) { if (typeof pubsubTopicShardInfo == "string") { return pubsubTopicShardInfo; } return pubsubTopicShardInfo?.shard !== undefined ? singleShardInfoToPubsubTopic(pubsubTopicShardInfo) : contentTopicToPubsubTopic(contentTopic, pubsubTopicShardInfo?.clusterId ?? DEFAULT_CLUSTER_ID); } export { contentTopicToPubsubTopic, contentTopicToShardIndex, determinePubsubTopic, ensureValidContentTopic, singleShardInfoToPubsubTopic };