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

View File

@ -1,3 +1,4 @@
import type { IProtoMessage } from "@waku/interfaces";
import { expect } from "chai"; import { expect } from "chai";
import fc from "fast-check"; 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 { import type {
EncoderOptions, EncoderOptions,
IDecodedMessage, IDecodedMessage,
@ -50,6 +51,10 @@ export class DecodedMessage implements IDecodedMessage {
} }
} }
get meta(): Uint8Array | undefined {
return this.proto.meta;
}
get version(): number { get version(): number {
// https://rfc.vac.dev/spec/14/ // https://rfc.vac.dev/spec/14/
// > If omitted, the value SHOULD be interpreted as version 0. // > If omitted, the value SHOULD be interpreted as version 0.
@ -62,7 +67,11 @@ export class DecodedMessage implements IDecodedMessage {
} }
export class Encoder implements IEncoder { 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> { async toWire(message: IMessage): Promise<Uint8Array> {
return proto.WakuMessage.encode(await this.toProtoObj(message)); return proto.WakuMessage.encode(await this.toProtoObj(message));
@ -71,14 +80,22 @@ export class Encoder implements IEncoder {
async toProtoObj(message: IMessage): Promise<IProtoMessage> { async toProtoObj(message: IMessage): Promise<IProtoMessage> {
const timestamp = message.timestamp ?? new Date(); const timestamp = message.timestamp ?? new Date();
return { const protoMessage = {
payload: message.payload, payload: message.payload,
version: Version, version: Version,
contentTopic: this.contentTopic, contentTopic: this.contentTopic,
timestamp: BigInt(timestamp.valueOf()) * OneMillion, timestamp: BigInt(timestamp.valueOf()) * OneMillion,
meta: undefined,
rateLimitProof: message.rateLimitProof, rateLimitProof: message.rateLimitProof,
ephemeral: this.ephemeral, 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({ export function createEncoder({
contentTopic, contentTopic,
ephemeral, ephemeral,
metaSetter,
}: EncoderOptions): Encoder { }: EncoderOptions): Encoder {
return new Encoder(contentTopic, ephemeral); return new Encoder(contentTopic, ephemeral, metaSetter);
} }
export class Decoder implements IDecoder<DecodedMessage> { export class Decoder implements IDecoder<DecodedMessage> {
@ -109,6 +127,7 @@ export class Decoder implements IDecoder<DecodedMessage> {
contentTopic: protoMessage.contentTopic, contentTopic: protoMessage.contentTopic,
version: protoMessage.version ?? undefined, version: protoMessage.version ?? undefined,
timestamp: protoMessage.timestamp ?? undefined, timestamp: protoMessage.timestamp ?? undefined,
meta: protoMessage.meta ?? undefined,
rateLimitProof: protoMessage.rateLimitProof ?? undefined, rateLimitProof: protoMessage.rateLimitProof ?? undefined,
ephemeral: protoMessage.ephemeral ?? false, 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/) * 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 * format when received from the Waku network. The resulting decoder can then be

View File

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

View File

@ -17,6 +17,7 @@ export interface IProtoMessage {
contentTopic: string; contentTopic: string;
version: number | undefined; version: number | undefined;
timestamp: bigint | undefined; timestamp: bigint | undefined;
meta: Uint8Array | undefined;
rateLimitProof: IRateLimitProof | undefined; rateLimitProof: IRateLimitProof | undefined;
ephemeral: boolean | undefined; ephemeral: boolean | undefined;
} }
@ -30,6 +31,10 @@ export interface IMessage {
rateLimitProof?: IRateLimitProof; rateLimitProof?: IRateLimitProof;
} }
export interface IMetaSetter {
(message: IProtoMessage & { meta: undefined }): Uint8Array;
}
export interface EncoderOptions { export interface EncoderOptions {
/** The content topic to set on outgoing messages. */ /** The content topic to set on outgoing messages. */
contentTopic: string; contentTopic: string;
@ -38,6 +43,12 @@ export interface EncoderOptions {
* @defaultValue `false` * @defaultValue `false`
*/ */
ephemeral?: boolean; 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 { export interface IEncoder {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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