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/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/encoder.spec.ts b/src/codec.spec.ts similarity index 59% rename from src/encoder.spec.ts rename to src/codec.spec.ts index a458a74..2e8305c 100644 --- a/src/encoder.spec.ts +++ b/src/codec.spec.ts @@ -1,11 +1,7 @@ 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 "./encoder.js"; +import { RLNDecoder, RLNEncoder } from "./codec.js"; import * as rln from "./index.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/encoder.ts b/src/codec.ts similarity index 78% rename from src/encoder.ts rename to src/codec.ts index 535be68..265f149 100644 --- a/src/encoder.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/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 new file mode 100644 index 0000000..8a7934f --- /dev/null +++ b/src/epoch.ts @@ -0,0 +1,21 @@ +const DefaultEpochUnitSeconds = 10; // the rln-relay epoch length in seconds + +export function dateToEpoch( + timestamp: Date, + epochUnitSeconds: number = DefaultEpochUnitSeconds +): number { + const time = timestamp.getTime(); + return Math.floor(time / 1000 / epochUnitSeconds); +} + +export function epochIntToBytes(epoch: number): Uint8Array { + const bytes = new Uint8Array(32); + const db = new DataView(bytes.buffer); + db.setUint32(0, epoch, true); + return bytes; +} + +export function epochBytesToInt(bytes: Uint8Array): number { + const dv = new DataView(bytes.buffer); + return dv.getUint32(0, true); +} 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"; 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); + } +} diff --git a/src/rln.ts b/src/rln.ts index 1140daf..62b04b5 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, epochIntToBytes } 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 = epochIntToBytes(dateToEpoch(new Date())); } else if (epoch instanceof Date) { - epoch = toEpoch(epoch); + epoch = epochIntToBytes(dateToEpoch(epoch)); } if (epoch.length != 32) throw "invalid epoch";