Skip to content

Commit bd983ea

Browse files
committed
feat!: enable encoding of meta field
1 parent 497588b commit bd983ea

14 files changed

+252
-10
lines changed

packages/core/src/lib/message/topic_only_message.ts

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export class TopicOnlyMessage implements IDecodedMessage {
1212
public payload: Uint8Array = new Uint8Array();
1313
public rateLimitProof: undefined;
1414
public timestamp: undefined;
15+
public meta: undefined;
1516
public ephemeral: undefined;
1617

1718
constructor(
@@ -35,6 +36,7 @@ export class TopicOnlyDecoder implements IDecoder<TopicOnlyMessage> {
3536
payload: new Uint8Array(),
3637
rateLimitProof: undefined,
3738
timestamp: undefined,
39+
meta: undefined,
3840
version: undefined,
3941
ephemeral: undefined,
4042
});

packages/core/src/lib/message/version_0.spec.ts

+48
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { IProtoMessage } from "@waku/interfaces";
12
import { expect } from "chai";
23
import fc from "fast-check";
34

@@ -57,4 +58,51 @@ describe("Waku Message version 0", function () {
5758
)
5859
);
5960
});
61+
62+
it("Meta field set when metaSetter is specified", async function () {
63+
await fc.assert(
64+
fc.asyncProperty(
65+
fc.string(),
66+
fc.string(),
67+
fc.uint8Array({ minLength: 1 }),
68+
async (contentTopic, pubSubTopic, payload) => {
69+
// Encode the length of the payload
70+
// Not a relevant real life example
71+
const metaSetter = (
72+
msg: IProtoMessage & { meta: undefined }
73+
): Uint8Array => {
74+
const buffer = new ArrayBuffer(4);
75+
const view = new DataView(buffer);
76+
view.setUint32(0, msg.payload.length, false);
77+
return new Uint8Array(buffer);
78+
};
79+
80+
const encoder = createEncoder({
81+
contentTopic,
82+
ephemeral: true,
83+
metaSetter,
84+
});
85+
const bytes = await encoder.toWire({ payload });
86+
const decoder = createDecoder(contentTopic);
87+
const protoResult = await decoder.fromWireToProtoObj(bytes);
88+
const result = (await decoder.fromProtoObj(
89+
pubSubTopic,
90+
protoResult!
91+
)) as DecodedMessage;
92+
93+
const expectedMeta = metaSetter({
94+
payload,
95+
timestamp: undefined,
96+
contentTopic: "",
97+
ephemeral: undefined,
98+
meta: undefined,
99+
rateLimitProof: undefined,
100+
version: undefined,
101+
});
102+
103+
expect(result.meta).to.deep.eq(expectedMeta);
104+
}
105+
)
106+
);
107+
});
60108
});

packages/core/src/lib/message/version_0.ts

+23-4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { IMetaSetter } from "@waku/interfaces";
12
import type {
23
EncoderOptions,
34
IDecodedMessage,
@@ -50,6 +51,10 @@ export class DecodedMessage implements IDecodedMessage {
5051
}
5152
}
5253

54+
get meta(): Uint8Array | undefined {
55+
return this.proto.meta;
56+
}
57+
5358
get version(): number {
5459
// https://rfc.vac.dev/spec/14/
5560
// > If omitted, the value SHOULD be interpreted as version 0.
@@ -62,7 +67,11 @@ export class DecodedMessage implements IDecodedMessage {
6267
}
6368

6469
export class Encoder implements IEncoder {
65-
constructor(public contentTopic: string, public ephemeral: boolean = false) {}
70+
constructor(
71+
public contentTopic: string,
72+
public ephemeral: boolean = false,
73+
public metaSetter?: IMetaSetter
74+
) {}
6675

6776
async toWire(message: IMessage): Promise<Uint8Array> {
6877
return proto.WakuMessage.encode(await this.toProtoObj(message));
@@ -71,14 +80,22 @@ export class Encoder implements IEncoder {
7180
async toProtoObj(message: IMessage): Promise<IProtoMessage> {
7281
const timestamp = message.timestamp ?? new Date();
7382

74-
return {
83+
const protoMessage = {
7584
payload: message.payload,
7685
version: Version,
7786
contentTopic: this.contentTopic,
7887
timestamp: BigInt(timestamp.valueOf()) * OneMillion,
88+
meta: undefined,
7989
rateLimitProof: message.rateLimitProof,
8090
ephemeral: this.ephemeral,
8191
};
92+
93+
if (this.metaSetter) {
94+
const meta = this.metaSetter(protoMessage);
95+
return { ...protoMessage, meta };
96+
}
97+
98+
return protoMessage;
8299
}
83100
}
84101

@@ -94,8 +111,9 @@ export class Encoder implements IEncoder {
94111
export function createEncoder({
95112
contentTopic,
96113
ephemeral,
114+
metaSetter,
97115
}: EncoderOptions): Encoder {
98-
return new Encoder(contentTopic, ephemeral);
116+
return new Encoder(contentTopic, ephemeral, metaSetter);
99117
}
100118

101119
export class Decoder implements IDecoder<DecodedMessage> {
@@ -109,6 +127,7 @@ export class Decoder implements IDecoder<DecodedMessage> {
109127
contentTopic: protoMessage.contentTopic,
110128
version: protoMessage.version ?? undefined,
111129
timestamp: protoMessage.timestamp ?? undefined,
130+
meta: protoMessage.meta ?? undefined,
112131
rateLimitProof: protoMessage.rateLimitProof ?? undefined,
113132
ephemeral: protoMessage.ephemeral ?? false,
114133
});
@@ -135,7 +154,7 @@ export class Decoder implements IDecoder<DecodedMessage> {
135154
}
136155

137156
/**
138-
* Creates an decoder that decode messages without Waku level encryption.
157+
* Creates a decoder that decode messages without Waku level encryption.
139158
*
140159
* A decoder is used to decode messages from the [14/WAKU2-MESSAGE](https://rfc.vac.dev/spec/14/)
141160
* format when received from the Waku network. The resulting decoder can then be

packages/core/src/lib/to_proto_message.ts

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const EmptyMessage: IProtoMessage = {
66
contentTopic: "",
77
version: undefined,
88
timestamp: undefined,
9+
meta: undefined,
910
rateLimitProof: undefined,
1011
ephemeral: undefined,
1112
};

packages/interfaces/src/message.ts

+11
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export interface IProtoMessage {
1717
contentTopic: string;
1818
version: number | undefined;
1919
timestamp: bigint | undefined;
20+
meta: Uint8Array | undefined;
2021
rateLimitProof: IRateLimitProof | undefined;
2122
ephemeral: boolean | undefined;
2223
}
@@ -30,6 +31,10 @@ export interface IMessage {
3031
rateLimitProof?: IRateLimitProof;
3132
}
3233

34+
export interface IMetaSetter {
35+
(message: IProtoMessage & { meta: undefined }): Uint8Array;
36+
}
37+
3338
export interface EncoderOptions {
3439
/** The content topic to set on outgoing messages. */
3540
contentTopic: string;
@@ -38,6 +43,12 @@ export interface EncoderOptions {
3843
* @defaultValue `false`
3944
*/
4045
ephemeral?: boolean;
46+
/**
47+
* A function called when encoding messages to set the meta field.
48+
* @param IProtoMessage The message encoded for wire, without the meta field.
49+
* If encryption is used, `metaSetter` only accesses _encrypted_ payload.
50+
*/
51+
metaSetter?: IMetaSetter;
4152
}
4253

4354
export interface IEncoder {

packages/message-encryption/src/ecies.spec.ts

+48
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { IProtoMessage } from "@waku/interfaces";
12
import { expect } from "chai";
23
import fc from "fast-check";
34

@@ -81,4 +82,51 @@ describe("Ecies Encryption", function () {
8182
)
8283
);
8384
});
85+
86+
it("Check meta is set [ecies]", async function () {
87+
await fc.assert(
88+
fc.asyncProperty(
89+
fc.string(),
90+
fc.string(),
91+
fc.uint8Array({ minLength: 1 }),
92+
fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }),
93+
async (pubSubTopic, contentTopic, payload, privateKey) => {
94+
const publicKey = getPublicKey(privateKey);
95+
const metaSetter = (
96+
msg: IProtoMessage & { meta: undefined }
97+
): Uint8Array => {
98+
const buffer = new ArrayBuffer(4);
99+
const view = new DataView(buffer);
100+
view.setUint32(0, msg.payload.length, false);
101+
return new Uint8Array(buffer);
102+
};
103+
104+
const encoder = createEncoder({
105+
contentTopic,
106+
publicKey,
107+
metaSetter,
108+
});
109+
const bytes = await encoder.toWire({ payload });
110+
111+
const decoder = createDecoder(contentTopic, privateKey);
112+
const protoResult = await decoder.fromWireToProtoObj(bytes!);
113+
if (!protoResult) throw "Failed to proto decode";
114+
const result = await decoder.fromProtoObj(pubSubTopic, protoResult);
115+
if (!result) throw "Failed to decode";
116+
117+
const expectedMeta = metaSetter({
118+
payload: protoResult.payload,
119+
timestamp: undefined,
120+
contentTopic: "",
121+
ephemeral: undefined,
122+
meta: undefined,
123+
rateLimitProof: undefined,
124+
version: undefined,
125+
});
126+
127+
expect(result.meta).to.deep.equal(expectedMeta);
128+
}
129+
)
130+
);
131+
});
84132
});

packages/message-encryption/src/ecies.ts

+20-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Decoder as DecoderV0 } from "@waku/core/lib/message/version_0";
2+
import { IMetaSetter } from "@waku/interfaces";
23
import type {
34
EncoderOptions as BaseEncoderOptions,
45
IDecoder,
@@ -34,7 +35,8 @@ class Encoder implements IEncoder {
3435
public contentTopic: string,
3536
private publicKey: Uint8Array,
3637
private sigPrivKey?: Uint8Array,
37-
public ephemeral: boolean = false
38+
public ephemeral: boolean = false,
39+
public metaSetter?: IMetaSetter
3840
) {}
3941

4042
async toWire(message: IMessage): Promise<Uint8Array | undefined> {
@@ -50,14 +52,22 @@ class Encoder implements IEncoder {
5052

5153
const payload = await encryptAsymmetric(preparedPayload, this.publicKey);
5254

53-
return {
55+
const protoMessage = {
5456
payload,
5557
version: Version,
5658
contentTopic: this.contentTopic,
5759
timestamp: BigInt(timestamp.valueOf()) * OneMillion,
60+
meta: undefined,
5861
rateLimitProof: message.rateLimitProof,
5962
ephemeral: this.ephemeral,
6063
};
64+
65+
if (this.metaSetter) {
66+
const meta = this.metaSetter(protoMessage);
67+
return { ...protoMessage, meta };
68+
}
69+
70+
return protoMessage;
6171
}
6272
}
6373

@@ -85,8 +95,15 @@ export function createEncoder({
8595
publicKey,
8696
sigPrivKey,
8797
ephemeral = false,
98+
metaSetter,
8899
}: EncoderOptions): Encoder {
89-
return new Encoder(contentTopic, publicKey, sigPrivKey, ephemeral);
100+
return new Encoder(
101+
contentTopic,
102+
publicKey,
103+
sigPrivKey,
104+
ephemeral,
105+
metaSetter
106+
);
90107
}
91108

92109
class Decoder extends DecoderV0 implements IDecoder<DecodedMessage> {

packages/message-encryption/src/symmetric.spec.ts

+47
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { IProtoMessage } from "@waku/interfaces";
12
import { expect } from "chai";
23
import fc from "fast-check";
34

@@ -70,4 +71,50 @@ describe("Symmetric Encryption", function () {
7071
)
7172
);
7273
});
74+
75+
it("Check meta is set [symmetric]", async function () {
76+
await fc.assert(
77+
fc.asyncProperty(
78+
fc.string(),
79+
fc.string(),
80+
fc.uint8Array({ minLength: 1 }),
81+
fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }),
82+
async (pubSubTopic, contentTopic, payload, symKey) => {
83+
const metaSetter = (
84+
msg: IProtoMessage & { meta: undefined }
85+
): Uint8Array => {
86+
const buffer = new ArrayBuffer(4);
87+
const view = new DataView(buffer);
88+
view.setUint32(0, msg.payload.length, false);
89+
return new Uint8Array(buffer);
90+
};
91+
92+
const encoder = createEncoder({
93+
contentTopic,
94+
symKey,
95+
metaSetter,
96+
});
97+
const bytes = await encoder.toWire({ payload });
98+
99+
const decoder = createDecoder(contentTopic, symKey);
100+
const protoResult = await decoder.fromWireToProtoObj(bytes!);
101+
if (!protoResult) throw "Failed to proto decode";
102+
const result = await decoder.fromProtoObj(pubSubTopic, protoResult);
103+
if (!result) throw "Failed to decode";
104+
105+
const expectedMeta = metaSetter({
106+
payload: protoResult.payload,
107+
timestamp: undefined,
108+
contentTopic: "",
109+
ephemeral: undefined,
110+
meta: undefined,
111+
rateLimitProof: undefined,
112+
version: undefined,
113+
});
114+
115+
expect(result.meta).to.deep.equal(expectedMeta);
116+
}
117+
)
118+
);
119+
});
73120
});

0 commit comments

Comments
 (0)