@waku/rln
Version:
RLN (Rate Limiting Nullifier) implementation for Waku
92 lines (89 loc) • 3.77 kB
JavaScript
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 };