feat!: enable encoding of `meta` field

This commit is contained in:
fryorcraken.eth 2023-03-10 14:41:07 +11:00
parent 497588bc36
commit bd983ea48e
No known key found for this signature in database
GPG Key ID: A82ED75A8DFC50A4
14 changed files with 252 additions and 10 deletions

View File

@ -12,6 +12,7 @@ export class TopicOnlyMessage implements IDecodedMessage {
public payload: Uint8Array = new Uint8Array();
public rateLimitProof: undefined;
public timestamp: undefined;
public meta: undefined;
public ephemeral: undefined;
constructor(
@ -35,6 +36,7 @@ export class TopicOnlyDecoder implements IDecoder<TopicOnlyMessage> {
payload: new Uint8Array(),
rateLimitProof: undefined,
timestamp: undefined,
meta: undefined,
version: undefined,
ephemeral: undefined,
});

View File

@ -1,3 +1,4 @@
import type { IProtoMessage } from "@waku/interfaces";
import { expect } from "chai";
import fc from "fast-check";
@ -57,4 +58,51 @@ describe("Waku Message version 0", function () {
)
);
});
it("Meta field set when metaSetter is specified", async function () {
await fc.assert(
fc.asyncProperty(
fc.string(),
fc.string(),
fc.uint8Array({ minLength: 1 }),
async (contentTopic, pubSubTopic, payload) => {
// Encode the length of the payload
// Not a relevant real life example
const metaSetter = (
msg: IProtoMessage & { meta: undefined }
): Uint8Array => {
const buffer = new ArrayBuffer(4);
const view = new DataView(buffer);
view.setUint32(0, msg.payload.length, false);
return new Uint8Array(buffer);
};
const encoder = createEncoder({
contentTopic,
ephemeral: true,
metaSetter,
});
const bytes = await encoder.toWire({ payload });
const decoder = createDecoder(contentTopic);
const protoResult = await decoder.fromWireToProtoObj(bytes);
const result = (await decoder.fromProtoObj(
pubSubTopic,
protoResult!
)) as DecodedMessage;
const expectedMeta = metaSetter({
payload,
timestamp: undefined,
contentTopic: "",
ephemeral: undefined,
meta: undefined,
rateLimitProof: undefined,
version: undefined,
});
expect(result.meta).to.deep.eq(expectedMeta);
}
)
);
});
});

View File

@ -1,3 +1,4 @@
import { IMetaSetter } from "@waku/interfaces";
import type {
EncoderOptions,
IDecodedMessage,
@ -50,6 +51,10 @@ export class DecodedMessage implements IDecodedMessage {
}
}
get meta(): Uint8Array | undefined {
return this.proto.meta;
}
get version(): number {
// https://rfc.vac.dev/spec/14/
// > If omitted, the value SHOULD be interpreted as version 0.
@ -62,7 +67,11 @@ export class DecodedMessage implements IDecodedMessage {
}
export class Encoder implements IEncoder {
constructor(public contentTopic: string, public ephemeral: boolean = false) {}
constructor(
public contentTopic: string,
public ephemeral: boolean = false,
public metaSetter?: IMetaSetter
) {}
async toWire(message: IMessage): Promise<Uint8Array> {
return proto.WakuMessage.encode(await this.toProtoObj(message));
@ -71,14 +80,22 @@ export class Encoder implements IEncoder {
async toProtoObj(message: IMessage): Promise<IProtoMessage> {
const timestamp = message.timestamp ?? new Date();
return {
const protoMessage = {
payload: message.payload,
version: Version,
contentTopic: this.contentTopic,
timestamp: BigInt(timestamp.valueOf()) * OneMillion,
meta: undefined,
rateLimitProof: message.rateLimitProof,
ephemeral: this.ephemeral,
};
if (this.metaSetter) {
const meta = this.metaSetter(protoMessage);
return { ...protoMessage, meta };
}
return protoMessage;
}
}
@ -94,8 +111,9 @@ export class Encoder implements IEncoder {
export function createEncoder({
contentTopic,
ephemeral,
metaSetter,
}: EncoderOptions): Encoder {
return new Encoder(contentTopic, ephemeral);
return new Encoder(contentTopic, ephemeral, metaSetter);
}
export class Decoder implements IDecoder<DecodedMessage> {
@ -109,6 +127,7 @@ export class Decoder implements IDecoder<DecodedMessage> {
contentTopic: protoMessage.contentTopic,
version: protoMessage.version ?? undefined,
timestamp: protoMessage.timestamp ?? undefined,
meta: protoMessage.meta ?? undefined,
rateLimitProof: protoMessage.rateLimitProof ?? undefined,
ephemeral: protoMessage.ephemeral ?? false,
});
@ -135,7 +154,7 @@ export class Decoder implements IDecoder<DecodedMessage> {
}
/**
* Creates an decoder that decode messages without Waku level encryption.
* Creates a decoder that decode messages without Waku level encryption.
*
* A decoder is used to decode messages from the [14/WAKU2-MESSAGE](https://rfc.vac.dev/spec/14/)
* format when received from the Waku network. The resulting decoder can then be

View File

@ -6,6 +6,7 @@ const EmptyMessage: IProtoMessage = {
contentTopic: "",
version: undefined,
timestamp: undefined,
meta: undefined,
rateLimitProof: undefined,
ephemeral: undefined,
};

View File

@ -17,6 +17,7 @@ export interface IProtoMessage {
contentTopic: string;
version: number | undefined;
timestamp: bigint | undefined;
meta: Uint8Array | undefined;
rateLimitProof: IRateLimitProof | undefined;
ephemeral: boolean | undefined;
}
@ -30,6 +31,10 @@ export interface IMessage {
rateLimitProof?: IRateLimitProof;
}
export interface IMetaSetter {
(message: IProtoMessage & { meta: undefined }): Uint8Array;
}
export interface EncoderOptions {
/** The content topic to set on outgoing messages. */
contentTopic: string;
@ -38,6 +43,12 @@ export interface EncoderOptions {
* @defaultValue `false`
*/
ephemeral?: boolean;
/**
* A function called when encoding messages to set the meta field.
* @param IProtoMessage The message encoded for wire, without the meta field.
* If encryption is used, `metaSetter` only accesses _encrypted_ payload.
*/
metaSetter?: IMetaSetter;
}
export interface IEncoder {

View File

@ -1,3 +1,4 @@
import { IProtoMessage } from "@waku/interfaces";
import { expect } from "chai";
import fc from "fast-check";
@ -81,4 +82,51 @@ describe("Ecies Encryption", function () {
)
);
});
it("Check meta is set [ecies]", async function () {
await fc.assert(
fc.asyncProperty(
fc.string(),
fc.string(),
fc.uint8Array({ minLength: 1 }),
fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }),
async (pubSubTopic, contentTopic, payload, privateKey) => {
const publicKey = getPublicKey(privateKey);
const metaSetter = (
msg: IProtoMessage & { meta: undefined }
): Uint8Array => {
const buffer = new ArrayBuffer(4);
const view = new DataView(buffer);
view.setUint32(0, msg.payload.length, false);
return new Uint8Array(buffer);
};
const encoder = createEncoder({
contentTopic,
publicKey,
metaSetter,
});
const bytes = await encoder.toWire({ payload });
const decoder = createDecoder(contentTopic, privateKey);
const protoResult = await decoder.fromWireToProtoObj(bytes!);
if (!protoResult) throw "Failed to proto decode";
const result = await decoder.fromProtoObj(pubSubTopic, protoResult);
if (!result) throw "Failed to decode";
const expectedMeta = metaSetter({
payload: protoResult.payload,
timestamp: undefined,
contentTopic: "",
ephemeral: undefined,
meta: undefined,
rateLimitProof: undefined,
version: undefined,
});
expect(result.meta).to.deep.equal(expectedMeta);
}
)
);
});
});

View File

@ -1,4 +1,5 @@
import { Decoder as DecoderV0 } from "@waku/core/lib/message/version_0";
import { IMetaSetter } from "@waku/interfaces";
import type {
EncoderOptions as BaseEncoderOptions,
IDecoder,
@ -34,7 +35,8 @@ class Encoder implements IEncoder {
public contentTopic: string,
private publicKey: Uint8Array,
private sigPrivKey?: Uint8Array,
public ephemeral: boolean = false
public ephemeral: boolean = false,
public metaSetter?: IMetaSetter
) {}
async toWire(message: IMessage): Promise<Uint8Array | undefined> {
@ -50,14 +52,22 @@ class Encoder implements IEncoder {
const payload = await encryptAsymmetric(preparedPayload, this.publicKey);
return {
const protoMessage = {
payload,
version: Version,
contentTopic: this.contentTopic,
timestamp: BigInt(timestamp.valueOf()) * OneMillion,
meta: undefined,
rateLimitProof: message.rateLimitProof,
ephemeral: this.ephemeral,
};
if (this.metaSetter) {
const meta = this.metaSetter(protoMessage);
return { ...protoMessage, meta };
}
return protoMessage;
}
}
@ -85,8 +95,15 @@ export function createEncoder({
publicKey,
sigPrivKey,
ephemeral = false,
metaSetter,
}: EncoderOptions): Encoder {
return new Encoder(contentTopic, publicKey, sigPrivKey, ephemeral);
return new Encoder(
contentTopic,
publicKey,
sigPrivKey,
ephemeral,
metaSetter
);
}
class Decoder extends DecoderV0 implements IDecoder<DecodedMessage> {

View File

@ -1,3 +1,4 @@
import { IProtoMessage } from "@waku/interfaces";
import { expect } from "chai";
import fc from "fast-check";
@ -70,4 +71,50 @@ describe("Symmetric Encryption", function () {
)
);
});
it("Check meta is set [symmetric]", async function () {
await fc.assert(
fc.asyncProperty(
fc.string(),
fc.string(),
fc.uint8Array({ minLength: 1 }),
fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }),
async (pubSubTopic, contentTopic, payload, symKey) => {
const metaSetter = (
msg: IProtoMessage & { meta: undefined }
): Uint8Array => {
const buffer = new ArrayBuffer(4);
const view = new DataView(buffer);
view.setUint32(0, msg.payload.length, false);
return new Uint8Array(buffer);
};
const encoder = createEncoder({
contentTopic,
symKey,
metaSetter,
});
const bytes = await encoder.toWire({ payload });
const decoder = createDecoder(contentTopic, symKey);
const protoResult = await decoder.fromWireToProtoObj(bytes!);
if (!protoResult) throw "Failed to proto decode";
const result = await decoder.fromProtoObj(pubSubTopic, protoResult);
if (!result) throw "Failed to decode";
const expectedMeta = metaSetter({
payload: protoResult.payload,
timestamp: undefined,
contentTopic: "",
ephemeral: undefined,
meta: undefined,
rateLimitProof: undefined,
version: undefined,
});
expect(result.meta).to.deep.equal(expectedMeta);
}
)
);
});
});

View File

@ -4,6 +4,7 @@ import type {
IDecoder,
IEncoder,
IMessage,
IMetaSetter,
IProtoMessage,
} from "@waku/interfaces";
import { WakuMessage } from "@waku/proto";
@ -29,7 +30,8 @@ class Encoder implements IEncoder {
public contentTopic: string,
private symKey: Uint8Array,
private sigPrivKey?: Uint8Array,
public ephemeral: boolean = false
public ephemeral: boolean = false,
public metaSetter?: IMetaSetter
) {}
async toWire(message: IMessage): Promise<Uint8Array | undefined> {
@ -44,14 +46,23 @@ class Encoder implements IEncoder {
const preparedPayload = await preCipher(message.payload, this.sigPrivKey);
const payload = await encryptSymmetric(preparedPayload, this.symKey);
return {
const protoMessage = {
payload,
version: Version,
contentTopic: this.contentTopic,
timestamp: BigInt(timestamp.valueOf()) * OneMillion,
meta: undefined,
rateLimitProof: message.rateLimitProof,
ephemeral: this.ephemeral,
};
if (this.metaSetter) {
const meta = this.metaSetter(protoMessage);
return { ...protoMessage, meta };
}
return protoMessage;
}
}
@ -80,8 +91,9 @@ export function createEncoder({
symKey,
sigPrivKey,
ephemeral = false,
metaSetter,
}: EncoderOptions): Encoder {
return new Encoder(contentTopic, symKey, sigPrivKey, ephemeral);
return new Encoder(contentTopic, symKey, sigPrivKey, ephemeral, metaSetter);
}
class Decoder extends DecoderV0 implements IDecoder<DecodedMessage> {

View File

@ -430,6 +430,7 @@ export interface WakuMessage {
contentTopic: string;
version?: number;
timestamp?: bigint;
meta?: Uint8Array;
rateLimitProof?: RateLimitProof;
ephemeral?: boolean;
}
@ -465,6 +466,11 @@ export namespace WakuMessage {
w.sint64(obj.timestamp);
}
if (obj.meta != null) {
w.uint32(90);
w.bytes(obj.meta);
}
if (obj.rateLimitProof != null) {
w.uint32(170);
RateLimitProof.codec().encode(obj.rateLimitProof, w);
@ -503,6 +509,9 @@ export namespace WakuMessage {
case 10:
obj.timestamp = reader.sint64();
break;
case 11:
obj.meta = reader.bytes();
break;
case 21:
obj.rateLimitProof = RateLimitProof.codec().decode(
reader,

View File

@ -362,6 +362,7 @@ export interface WakuMessage {
contentTopic: string;
version?: number;
timestamp?: bigint;
meta?: Uint8Array;
rateLimitProof?: RateLimitProof;
ephemeral?: boolean;
}
@ -397,6 +398,11 @@ export namespace WakuMessage {
w.sint64(obj.timestamp);
}
if (obj.meta != null) {
w.uint32(90);
w.bytes(obj.meta);
}
if (obj.rateLimitProof != null) {
w.uint32(170);
RateLimitProof.codec().encode(obj.rateLimitProof, w);
@ -435,6 +441,9 @@ export namespace WakuMessage {
case 10:
obj.timestamp = reader.sint64();
break;
case 11:
obj.meta = reader.bytes();
break;
case 21:
obj.rateLimitProof = RateLimitProof.codec().decode(
reader,

View File

@ -17,6 +17,7 @@ message WakuMessage {
string content_topic = 2;
optional uint32 version = 3;
optional sint64 timestamp = 10;
optional bytes meta = 11;
optional RateLimitProof rate_limit_proof = 21;
optional bool ephemeral = 31;
}

View File

@ -134,6 +134,7 @@ export interface WakuMessage {
contentTopic: string;
version?: number;
timestamp?: bigint;
meta?: Uint8Array;
rateLimitProof?: RateLimitProof;
ephemeral?: boolean;
}
@ -169,6 +170,11 @@ export namespace WakuMessage {
w.sint64(obj.timestamp);
}
if (obj.meta != null) {
w.uint32(90);
w.bytes(obj.meta);
}
if (obj.rateLimitProof != null) {
w.uint32(170);
RateLimitProof.codec().encode(obj.rateLimitProof, w);
@ -207,6 +213,9 @@ export namespace WakuMessage {
case 10:
obj.timestamp = reader.sint64();
break;
case 11:
obj.meta = reader.bytes();
break;
case 21:
obj.rateLimitProof = RateLimitProof.codec().decode(
reader,

View File

@ -676,6 +676,7 @@ export interface WakuMessage {
contentTopic: string;
version?: number;
timestamp?: bigint;
meta?: Uint8Array;
rateLimitProof?: RateLimitProof;
ephemeral?: boolean;
}
@ -711,6 +712,11 @@ export namespace WakuMessage {
w.sint64(obj.timestamp);
}
if (obj.meta != null) {
w.uint32(90);
w.bytes(obj.meta);
}
if (obj.rateLimitProof != null) {
w.uint32(170);
RateLimitProof.codec().encode(obj.rateLimitProof, w);
@ -749,6 +755,9 @@ export namespace WakuMessage {
case 10:
obj.timestamp = reader.sint64();
break;
case 11:
obj.meta = reader.bytes();
break;
case 21:
obj.rateLimitProof = RateLimitProof.codec().decode(
reader,