From 84803d77e58bae67ccbc362437360cac34b46261 Mon Sep 17 00:00:00 2001 From: "fryorcraken.eth" Date: Wed, 28 Sep 2022 12:54:04 +1000 Subject: [PATCH 1/5] refactor: more appropriate name --- src/{encoder.spec.ts => codec.spec.ts} | 2 +- src/{encoder.ts => codec.ts} | 0 src/index.ts | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename src/{encoder.spec.ts => codec.spec.ts} (95%) rename src/{encoder.ts => codec.ts} (100%) diff --git a/src/encoder.spec.ts b/src/codec.spec.ts similarity index 95% rename from src/encoder.spec.ts rename to src/codec.spec.ts index a458a74..5cf2544 100644 --- a/src/encoder.spec.ts +++ b/src/codec.spec.ts @@ -5,7 +5,7 @@ import { MessageV0, } from "js-waku/lib/waku_message/version_0"; -import { RLNDecoder, RLNEncoder } from "./encoder.js"; +import { RLNDecoder, RLNEncoder } from "./codec.js"; import * as rln from "./index.js"; diff --git a/src/encoder.ts b/src/codec.ts similarity index 100% rename from src/encoder.ts rename to src/codec.ts diff --git a/src/index.ts b/src/index.ts index 22eadb9..77fa214 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import { RLNDecoder, RLNEncoder } from "./encoder.js"; +import { RLNDecoder, RLNEncoder } from "./codec.js"; import type { Proof, RLNInstance } from "./rln.js"; import { MembershipKey } from "./rln.js"; From 4463c4afabc09b2cd09f1ea64b0fa59957457954 Mon Sep 17 00:00:00 2001 From: "fryorcraken.eth" Date: Wed, 28 Sep 2022 13:05:10 +1000 Subject: [PATCH 2/5] refactor: extract helper functions --- src/byte_utils.ts | 39 +++++++++++++++++++++++++++++++++ src/epoch.ts | 11 ++++++++++ src/rln.ts | 56 ++++------------------------------------------- 3 files changed, 54 insertions(+), 52 deletions(-) create mode 100644 src/byte_utils.ts create mode 100644 src/epoch.ts diff --git a/src/byte_utils.ts b/src/byte_utils.ts new file mode 100644 index 0000000..99c9671 --- /dev/null +++ b/src/byte_utils.ts @@ -0,0 +1,39 @@ +// Adapted from https://github.com/feross/buffer + +function checkInt( + buf: Uint8Array, + value: number, + offset: number, + ext: number, + max: number, + min: number +): void { + if (value > max || value < min) + throw new RangeError('"value" argument is out of bounds'); + if (offset + ext > buf.length) throw new RangeError("Index out of range"); +} + +export function writeUIntLE( + buf: Uint8Array, + value: number, + offset: number, + byteLength: number, + noAssert?: boolean +): Uint8Array { + value = +value; + offset = offset >>> 0; + byteLength = byteLength >>> 0; + if (!noAssert) { + const maxBytes = Math.pow(2, 8 * byteLength) - 1; + checkInt(buf, value, offset, byteLength, maxBytes, 0); + } + + let mul = 1; + let i = 0; + buf[offset] = value & 0xff; + while (++i < byteLength && (mul *= 0x100)) { + buf[offset + i] = (value / mul) & 0xff; + } + + return buf; +} diff --git a/src/epoch.ts b/src/epoch.ts new file mode 100644 index 0000000..3a14bd2 --- /dev/null +++ b/src/epoch.ts @@ -0,0 +1,11 @@ +import { writeUIntLE } from "./byte_utils.js"; + +const DefaultEpochUnitSeconds = 10; // the rln-relay epoch length in seconds + +export function dateToEpoch( + timestamp: Date, + epochUnitSeconds: number = DefaultEpochUnitSeconds +): Uint8Array { + const unix = Math.floor(timestamp.getTime() / 1000 / epochUnitSeconds); + return writeUIntLE(new Uint8Array(32), unix, 0, 8); +} diff --git a/src/rln.ts b/src/rln.ts index 1140daf..d82931d 100644 --- a/src/rln.ts +++ b/src/rln.ts @@ -1,6 +1,8 @@ import init, * as zerokitRLN from "@waku/zerokit-rln-wasm"; import { RateLimitProof } from "js-waku/lib/interfaces"; +import { writeUIntLE } from "./byte_utils.js"; +import { dateToEpoch } from "./epoch.js"; import verificationKey from "./resources/verification_key.js"; import * as wc from "./witness_calculator.js"; import { WitnessCalculator } from "./witness_calculator.js"; @@ -67,56 +69,6 @@ export class MembershipKey { } } -// Adapted from https://github.com/feross/buffer - -function checkInt( - buf: Uint8Array, - value: number, - offset: number, - ext: number, - max: number, - min: number -): void { - if (value > max || value < min) - throw new RangeError('"value" argument is out of bounds'); - if (offset + ext > buf.length) throw new RangeError("Index out of range"); -} - -const writeUIntLE = function writeUIntLE( - buf: Uint8Array, - value: number, - offset: number, - byteLength: number, - noAssert?: boolean -): Uint8Array { - value = +value; - offset = offset >>> 0; - byteLength = byteLength >>> 0; - if (!noAssert) { - const maxBytes = Math.pow(2, 8 * byteLength) - 1; - checkInt(buf, value, offset, byteLength, maxBytes, 0); - } - - let mul = 1; - let i = 0; - buf[offset] = value & 0xff; - while (++i < byteLength && (mul *= 0x100)) { - buf[offset + i] = (value / mul) & 0xff; - } - - return buf; -}; - -const DefaultEpochUnitSeconds = 10; // the rln-relay epoch length in seconds - -export function toEpoch( - timestamp: Date, - epochUnitSeconds: number = DefaultEpochUnitSeconds -): Uint8Array { - const unix = Math.floor(timestamp.getTime() / 1000 / epochUnitSeconds); - return writeUIntLE(new Uint8Array(32), unix, 0, 8); -} - const proofOffset = 128; const rootOffset = proofOffset + 32; const epochOffset = rootOffset + 32; @@ -200,9 +152,9 @@ export class RLNInstance { idKey: Uint8Array ): Promise { if (epoch == undefined) { - epoch = toEpoch(new Date()); + epoch = dateToEpoch(new Date()); } else if (epoch instanceof Date) { - epoch = toEpoch(epoch); + epoch = dateToEpoch(epoch); } if (epoch.length != 32) throw "invalid epoch"; From 50c30f133242f7bd93a4b8d5b7f78b4ddb4b6dd0 Mon Sep 17 00:00:00 2001 From: "fryorcraken.eth" Date: Wed, 28 Sep 2022 13:51:53 +1000 Subject: [PATCH 3/5] feat: decode epoch to int --- package-lock.json | 7 +++++-- package.json | 2 +- src/epoch.spec.ts | 17 +++++++++++++++++ src/epoch.ts | 15 ++++++++++++--- src/rln.ts | 6 +++--- 5 files changed, 38 insertions(+), 9 deletions(-) create mode 100644 src/epoch.spec.ts diff --git a/package-lock.json b/package-lock.json index 2ea0e1d..d464eaa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,7 +36,7 @@ "eslint-plugin-functional": "^4.0.2", "eslint-plugin-import": "^2.25.3", "eslint-plugin-prettier": "^4.0.0", - "fast-check": "^2.14.0", + "fast-check": "^2.25.0", "gh-pages": "^3.2.3", "husky": "^7.0.4", "ignore-loader": "^0.1.2", @@ -5052,8 +5052,9 @@ }, "node_modules/fast-check": { "version": "2.25.0", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-2.25.0.tgz", + "integrity": "sha512-wRUT2KD2lAmT75WNIJIHECawoUUMHM0I5jrlLXGtGeqmPL8jl/EldUDjY1VCp6fDY8yflyfUeIOsOBrIbIiArg==", "dev": true, - "license": "MIT", "dependencies": { "pure-rand": "^5.0.1" }, @@ -14684,6 +14685,8 @@ }, "fast-check": { "version": "2.25.0", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-2.25.0.tgz", + "integrity": "sha512-wRUT2KD2lAmT75WNIJIHECawoUUMHM0I5jrlLXGtGeqmPL8jl/EldUDjY1VCp6fDY8yflyfUeIOsOBrIbIiArg==", "dev": true, "requires": { "pure-rand": "^5.0.1" diff --git a/package.json b/package.json index 5f2cd3c..e2b93ba 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "eslint-plugin-functional": "^4.0.2", "eslint-plugin-import": "^2.25.3", "eslint-plugin-prettier": "^4.0.0", - "fast-check": "^2.14.0", + "fast-check": "^2.25.0", "gh-pages": "^3.2.3", "husky": "^7.0.4", "ignore-loader": "^0.1.2", diff --git a/src/epoch.spec.ts b/src/epoch.spec.ts new file mode 100644 index 0000000..ac06245 --- /dev/null +++ b/src/epoch.spec.ts @@ -0,0 +1,17 @@ +import { expect } from "chai"; +import fc from "fast-check"; + +import { epochBytesToInt, epochIntToBytes } from "./epoch.js"; + +describe("epoch serialization", () => { + it("Round trip", async function () { + await fc.assert( + fc.asyncProperty(fc.integer({ min: 0 }), async (date) => { + const bytes = epochIntToBytes(date); + const _date = epochBytesToInt(bytes); + + expect(_date.valueOf()).to.eq(date.valueOf()); + }) + ); + }); +}); diff --git a/src/epoch.ts b/src/epoch.ts index 3a14bd2..b87c975 100644 --- a/src/epoch.ts +++ b/src/epoch.ts @@ -5,7 +5,16 @@ const DefaultEpochUnitSeconds = 10; // the rln-relay epoch length in seconds export function dateToEpoch( timestamp: Date, epochUnitSeconds: number = DefaultEpochUnitSeconds -): Uint8Array { - const unix = Math.floor(timestamp.getTime() / 1000 / epochUnitSeconds); - return writeUIntLE(new Uint8Array(32), unix, 0, 8); +): number { + const time = timestamp.getTime(); + return Math.floor(time / 1000 / epochUnitSeconds); +} + +export function epochIntToBytes(epoch: number): Uint8Array { + return writeUIntLE(new Uint8Array(32), epoch, 0, 8); +} + +export function epochBytesToInt(bytes: Uint8Array): number { + const dv = new DataView(bytes.buffer); + return dv.getUint32(0, true); } diff --git a/src/rln.ts b/src/rln.ts index d82931d..62b04b5 100644 --- a/src/rln.ts +++ b/src/rln.ts @@ -2,7 +2,7 @@ import init, * as zerokitRLN from "@waku/zerokit-rln-wasm"; import { RateLimitProof } from "js-waku/lib/interfaces"; import { writeUIntLE } from "./byte_utils.js"; -import { dateToEpoch } from "./epoch.js"; +import { dateToEpoch, epochIntToBytes } from "./epoch.js"; import verificationKey from "./resources/verification_key.js"; import * as wc from "./witness_calculator.js"; import { WitnessCalculator } from "./witness_calculator.js"; @@ -152,9 +152,9 @@ export class RLNInstance { idKey: Uint8Array ): Promise { if (epoch == undefined) { - epoch = dateToEpoch(new Date()); + epoch = epochIntToBytes(dateToEpoch(new Date())); } else if (epoch instanceof Date) { - epoch = dateToEpoch(epoch); + epoch = epochIntToBytes(dateToEpoch(epoch)); } if (epoch.length != 32) throw "invalid epoch"; From b0b9e1e46b641bec3e4f5b54293fbba1a2c2f492 Mon Sep 17 00:00:00 2001 From: "fryorcraken.eth" Date: Wed, 28 Sep 2022 13:53:47 +1000 Subject: [PATCH 4/5] refactor: use standard JS lib --- src/epoch.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/epoch.ts b/src/epoch.ts index b87c975..8a7934f 100644 --- a/src/epoch.ts +++ b/src/epoch.ts @@ -1,5 +1,3 @@ -import { writeUIntLE } from "./byte_utils.js"; - const DefaultEpochUnitSeconds = 10; // the rln-relay epoch length in seconds export function dateToEpoch( @@ -11,7 +9,10 @@ export function dateToEpoch( } export function epochIntToBytes(epoch: number): Uint8Array { - return writeUIntLE(new Uint8Array(32), epoch, 0, 8); + const bytes = new Uint8Array(32); + const db = new DataView(bytes.buffer); + db.setUint32(0, epoch, true); + return bytes; } export function epochBytesToInt(bytes: Uint8Array): number { From 382794d25fdb0e774ed39d338cc43608c8f7a6b9 Mon Sep 17 00:00:00 2001 From: "fryorcraken.eth" Date: Wed, 28 Sep 2022 14:23:10 +1000 Subject: [PATCH 5/5] feat: add RlnMessage for handy helpers --- src/codec.spec.ts | 23 ++++++++++------------- src/codec.ts | 19 +++++++++---------- src/message.ts | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 23 deletions(-) create mode 100644 src/message.ts diff --git a/src/codec.spec.ts b/src/codec.spec.ts index 5cf2544..2e8305c 100644 --- a/src/codec.spec.ts +++ b/src/codec.spec.ts @@ -1,9 +1,5 @@ import { expect } from "chai"; -import { - DecoderV0, - EncoderV0, - MessageV0, -} from "js-waku/lib/waku_message/version_0"; +import { DecoderV0, EncoderV0 } from "js-waku/lib/waku_message/version_0"; import { RLNDecoder, RLNEncoder } from "./codec.js"; @@ -26,7 +22,10 @@ describe("js-rln: encoder", () => { index, memKeys ); - const rlnDecoder = new RLNDecoder(new DecoderV0(TestContentTopic)); + const rlnDecoder = new RLNDecoder( + rlnInstance, + new DecoderV0(TestContentTopic) + ); const bytes = await rlnEncoder.encode({ payload }); const protoResult = await rlnDecoder.decodeProto(bytes!); @@ -34,13 +33,11 @@ describe("js-rln: encoder", () => { const msg = (await rlnDecoder.decode(protoResult!))!; // Validate proof - const verifResult = rlnInstance.verifyProof(msg.rateLimitProof!); - expect(verifResult).to.be.true; + expect(msg.verify()).to.be.true; - const msgV0 = msg as MessageV0; - expect(msgV0.contentTopic).to.eq(TestContentTopic); - expect(msgV0.version).to.eq(0); - expect(msgV0.payload).to.deep.eq(payload); - expect(msgV0.timestamp).to.not.be.undefined; + expect(msg.contentTopic).to.eq(TestContentTopic); + expect(msg.msg.version).to.eq(0); + expect(msg.payload).to.deep.eq(payload); + expect(msg.timestamp).to.not.be.undefined; }); }); diff --git a/src/codec.ts b/src/codec.ts index 535be68..265f149 100644 --- a/src/codec.ts +++ b/src/codec.ts @@ -7,6 +7,7 @@ import { ProtoMessage, } from "js-waku/lib/interfaces"; +import { RlnMessage } from "./message.js"; import { MembershipKey, RLNInstance } from "./rln.js"; const log = debug("waku:message:rln-encoder"); @@ -53,11 +54,11 @@ export class RLNEncoder implements Encoder { } } -export class RLNDecoder implements Decoder { - public contentTopic: string; +export class RLNDecoder implements Decoder> { + constructor(private rlnInstance: RLNInstance, private decoder: Decoder) {} - constructor(private decoder: Decoder) { - this.contentTopic = decoder.contentTopic; + get contentTopic(): string { + return this.decoder.contentTopic; } decodeProto(bytes: Uint8Array): Promise { @@ -66,12 +67,10 @@ export class RLNDecoder implements Decoder { return Promise.resolve(protoMessage); } - async decode(proto: ProtoMessage): Promise { - const msg = await this.decoder.decode(proto); - if (msg) { - msg.rateLimitProof = proto.rateLimitProof; - } - return msg; + async decode(proto: ProtoMessage): Promise | undefined> { + const msg: T | undefined = await this.decoder.decode(proto); + if (!msg) return; + return new RlnMessage(this.rlnInstance, msg, proto.rateLimitProof); } } diff --git a/src/message.ts b/src/message.ts new file mode 100644 index 0000000..ab6d45a --- /dev/null +++ b/src/message.ts @@ -0,0 +1,37 @@ +import { Message, RateLimitProof } from "js-waku/lib/interfaces"; + +import { epochBytesToInt } from "./epoch.js"; +import { RLNInstance } from "./rln.js"; + +export class RlnMessage implements Message { + constructor( + public rlnInstance: RLNInstance, + public msg: T, + public rateLimitProof?: RateLimitProof + ) {} + + public verify(): boolean | undefined { + return this.rateLimitProof + ? this.rlnInstance.verifyProof(this.rateLimitProof) + : undefined; + } + + get payload(): Uint8Array | undefined { + return this.msg.payload; + } + + get contentTopic(): string | undefined { + return this.msg.contentTopic; + } + + get timestamp(): Date | undefined { + return this.msg.timestamp; + } + + get epoch(): number | undefined { + const bytes = this.msg.rateLimitProof?.epoch; + if (!bytes) return; + + return epochBytesToInt(bytes); + } +}