Merge pull request #24 from waku-org/message-helpers

This commit is contained in:
fryorcraken.eth 2022-09-28 14:39:18 +10:00 committed by GitHub
commit b5f0cbb3dd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 145 additions and 80 deletions

7
package-lock.json generated
View File

@ -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"

View File

@ -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",

39
src/byte_utils.ts Normal file
View File

@ -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;
}

View File

@ -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;
});
});

View File

@ -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<Message> {
public contentTopic: string;
export class RLNDecoder<T extends Message> implements Decoder<RlnMessage<T>> {
constructor(private rlnInstance: RLNInstance, private decoder: Decoder<T>) {}
constructor(private decoder: Decoder<Message>) {
this.contentTopic = decoder.contentTopic;
get contentTopic(): string {
return this.decoder.contentTopic;
}
decodeProto(bytes: Uint8Array): Promise<ProtoMessage | undefined> {
@ -66,12 +67,10 @@ export class RLNDecoder implements Decoder<Message> {
return Promise.resolve(protoMessage);
}
async decode(proto: ProtoMessage): Promise<Message | undefined> {
const msg = await this.decoder.decode(proto);
if (msg) {
msg.rateLimitProof = proto.rateLimitProof;
}
return msg;
async decode(proto: ProtoMessage): Promise<RlnMessage<T> | undefined> {
const msg: T | undefined = await this.decoder.decode(proto);
if (!msg) return;
return new RlnMessage(this.rlnInstance, msg, proto.rateLimitProof);
}
}

17
src/epoch.spec.ts Normal file
View File

@ -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());
})
);
});
});

21
src/epoch.ts Normal file
View File

@ -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);
}

View File

@ -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";

37
src/message.ts Normal file
View File

@ -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<T extends Message> 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);
}
}

View File

@ -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<RateLimitProof> {
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";