mirror of https://github.com/waku-org/js-waku.git
feat: make message encoding more generic
This commit is contained in:
parent
96e8d0ebd5
commit
b2c7e4185f
|
@ -3,6 +3,8 @@
|
||||||
"$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/master/cspell.schema.json",
|
"$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/master/cspell.schema.json",
|
||||||
"language": "en",
|
"language": "en",
|
||||||
"words": [
|
"words": [
|
||||||
|
"abortable",
|
||||||
|
"asym",
|
||||||
"backoff",
|
"backoff",
|
||||||
"backoffs",
|
"backoffs",
|
||||||
"bitjson",
|
"bitjson",
|
||||||
|
|
|
@ -10,12 +10,14 @@ module.exports = [
|
||||||
import: {
|
import: {
|
||||||
"./bundle/lib/create_waku.js": "{ createLightNode }",
|
"./bundle/lib/create_waku.js": "{ createLightNode }",
|
||||||
"./bundle/lib/wait_for_remote_peer.js": "{ waitForRemotePeer }",
|
"./bundle/lib/wait_for_remote_peer.js": "{ waitForRemotePeer }",
|
||||||
|
"./bundle/lib/waku_message/version_0.js":
|
||||||
|
"{ MessageV0, DecoderV0, EncoderV0 }",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Asymmetric, symmetric encryption and signature",
|
name: "Asymmetric, symmetric encryption and signature",
|
||||||
path: "bundle/index.js",
|
path: "bundle/lib/waku_message/version_1.js",
|
||||||
import: "{ WakuMessage }",
|
import: "{ MessageV1, AsymEncoder, AsymDecoder, SymEncoder, SymDecoder }",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "DNS discovery",
|
name: "DNS discovery",
|
||||||
|
|
|
@ -17,6 +17,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
- `queryCallbackOnPromise`'s return value has been simplified to `Promise<void>`.
|
- `queryCallbackOnPromise`'s return value has been simplified to `Promise<void>`.
|
||||||
- doc: clarified behaviour of `WakuStore` query functions.
|
- doc: clarified behaviour of `WakuStore` query functions.
|
||||||
|
- Waku message encoding and decoding is more generic, to enable upcoming feature such as [RLN](https://rfc.vac.dev/spec/17/) & [Noise](https://rfc.vac.dev/spec/43/);
|
||||||
|
it also enables separating the `version_1` module out to reduce bundle size and improve cross-platform compatibility when not used.
|
||||||
|
- Due to the change above, all APIs that handle messages have changed to receive a `Decoder` or `Encoder`.
|
||||||
|
|
||||||
|
### Deleted
|
||||||
|
|
||||||
|
- `WakuMessage` class in favour of the `Message`, `Encoder`, `Decoder` interfaces and `EncoderV0`, `AsymEncoder`, `SymEncoder` (and related decoders).
|
||||||
|
|
||||||
## [0.28.0] - 2022-09-16
|
## [0.28.0] - 2022-09-16
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,14 @@
|
||||||
"./lib/wait_for_remote_peer": {
|
"./lib/wait_for_remote_peer": {
|
||||||
"types": "./dist/lib/wait_for_remote_peer.d.ts",
|
"types": "./dist/lib/wait_for_remote_peer.d.ts",
|
||||||
"import": "./dist/lib/wait_for_remote_peer.js"
|
"import": "./dist/lib/wait_for_remote_peer.js"
|
||||||
|
},
|
||||||
|
"./lib/waku_message/version_0": {
|
||||||
|
"types": "./dist/lib/waku_message/version_0.d.ts",
|
||||||
|
"import": "./dist/lib/waku_message/version_0.js"
|
||||||
|
},
|
||||||
|
"./lib/waku_message/version_1": {
|
||||||
|
"types": "./dist/lib/waku_message/version_1.d.ts",
|
||||||
|
"import": "./dist/lib/waku_message/version_1.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"typesVersions": {
|
"typesVersions": {
|
||||||
|
|
|
@ -10,6 +10,8 @@ export default {
|
||||||
"lib/peer_discovery_static_list": "dist/lib/peer_discovery_static_list.js",
|
"lib/peer_discovery_static_list": "dist/lib/peer_discovery_static_list.js",
|
||||||
"lib/predefined_bootstrap_nodes": "dist/lib/predefined_bootstrap_nodes.js",
|
"lib/predefined_bootstrap_nodes": "dist/lib/predefined_bootstrap_nodes.js",
|
||||||
"lib/wait_for_remote_peer": "dist/lib/wait_for_remote_peer.js",
|
"lib/wait_for_remote_peer": "dist/lib/wait_for_remote_peer.js",
|
||||||
|
"lib/waku_message/version_0": "dist/lib/waku_message/version_0.js",
|
||||||
|
"lib/waku_message/version_1": "dist/lib/waku_message/version_1.js",
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
dir: "bundle",
|
dir: "bundle",
|
||||||
|
|
|
@ -15,9 +15,6 @@ export * as proto_message from "./proto/message";
|
||||||
export * as waku from "./lib/waku";
|
export * as waku from "./lib/waku";
|
||||||
export { WakuNode, Protocols } from "./lib/waku";
|
export { WakuNode, Protocols } from "./lib/waku";
|
||||||
|
|
||||||
export * as waku_message from "./lib/waku_message";
|
|
||||||
export { WakuMessage } from "./lib/waku_message";
|
|
||||||
|
|
||||||
export * as waku_filter from "./lib/waku_filter";
|
export * as waku_filter from "./lib/waku_filter";
|
||||||
export { WakuFilter } from "./lib/waku_filter";
|
export { WakuFilter } from "./lib/waku_filter";
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
export function groupByContentTopic<T extends { contentTopic: string }>(
|
||||||
|
values: T[]
|
||||||
|
): Map<string, Array<T>> {
|
||||||
|
const groupedDecoders = new Map();
|
||||||
|
values.forEach((value) => {
|
||||||
|
let decs = groupedDecoders.get(value.contentTopic);
|
||||||
|
if (!decs) {
|
||||||
|
groupedDecoders.set(value.contentTopic, []);
|
||||||
|
decs = groupedDecoders.get(value.contentTopic);
|
||||||
|
}
|
||||||
|
decs.push(value);
|
||||||
|
});
|
||||||
|
return groupedDecoders;
|
||||||
|
}
|
|
@ -6,7 +6,6 @@ import type { Libp2p } from "libp2p";
|
||||||
import type { Protocols } from "./waku";
|
import type { Protocols } from "./waku";
|
||||||
import type { WakuFilter } from "./waku_filter";
|
import type { WakuFilter } from "./waku_filter";
|
||||||
import type { WakuLightPush } from "./waku_light_push";
|
import type { WakuLightPush } from "./waku_light_push";
|
||||||
import type { DecryptionMethod } from "./waku_message";
|
|
||||||
import type { WakuRelay } from "./waku_relay";
|
import type { WakuRelay } from "./waku_relay";
|
||||||
import type { WakuStore } from "./waku_store";
|
import type { WakuStore } from "./waku_store";
|
||||||
|
|
||||||
|
@ -29,13 +28,6 @@ export interface Waku {
|
||||||
stop(): Promise<void>;
|
stop(): Promise<void>;
|
||||||
|
|
||||||
isStarted(): boolean;
|
isStarted(): boolean;
|
||||||
|
|
||||||
addDecryptionKey(
|
|
||||||
key: Uint8Array | string,
|
|
||||||
options?: { method?: DecryptionMethod; contentTopics?: string[] }
|
|
||||||
): void;
|
|
||||||
|
|
||||||
deleteDecryptionKey(key: Uint8Array | string): void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WakuLight extends Waku {
|
export interface WakuLight extends Waku {
|
||||||
|
@ -58,3 +50,28 @@ export interface WakuFull extends Waku {
|
||||||
filter: WakuFilter;
|
filter: WakuFilter;
|
||||||
lightPush: WakuLightPush;
|
lightPush: WakuLightPush;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ProtoMessage {
|
||||||
|
payload?: Uint8Array;
|
||||||
|
contentTopic?: string;
|
||||||
|
version?: number;
|
||||||
|
timestamp?: bigint;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Message {
|
||||||
|
payload?: Uint8Array;
|
||||||
|
contentTopic?: string;
|
||||||
|
timestamp?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Encoder {
|
||||||
|
contentTopic: string;
|
||||||
|
encode: (message: Message) => Promise<Uint8Array | undefined>;
|
||||||
|
encodeProto: (message: Message) => Promise<ProtoMessage | undefined>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Decoder {
|
||||||
|
contentTopic: string;
|
||||||
|
decodeProto: (bytes: Uint8Array) => Promise<ProtoMessage | undefined>;
|
||||||
|
decode: (proto: ProtoMessage) => Promise<Message | undefined>;
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { expect } from "chai";
|
||||||
|
|
||||||
|
import { pushOrInitMapSet } from "./push_or_init_map";
|
||||||
|
|
||||||
|
describe("pushOrInitMapSet", () => {
|
||||||
|
it("Init the array if not present", () => {
|
||||||
|
const map = new Map();
|
||||||
|
const key = "key";
|
||||||
|
const value = "value";
|
||||||
|
|
||||||
|
pushOrInitMapSet(map, key, value);
|
||||||
|
|
||||||
|
expect(map.get(key)).to.deep.eq(new Set([value]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Push to array if already present", () => {
|
||||||
|
const map = new Map();
|
||||||
|
const key = "key";
|
||||||
|
const value1 = "value1";
|
||||||
|
const value2 = "value2";
|
||||||
|
|
||||||
|
pushOrInitMapSet(map, key, value1);
|
||||||
|
pushOrInitMapSet(map, key, value2);
|
||||||
|
|
||||||
|
expect(map.get(key)).to.deep.eq(new Set([value1, value2]));
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,13 @@
|
||||||
|
export function pushOrInitMapSet<K, V>(
|
||||||
|
map: Map<K, Set<V>>,
|
||||||
|
key: K,
|
||||||
|
newValue: V
|
||||||
|
): void {
|
||||||
|
let arr = map.get(key);
|
||||||
|
if (typeof arr === "undefined") {
|
||||||
|
map.set(key, new Set());
|
||||||
|
arr = map.get(key) as Set<V>;
|
||||||
|
}
|
||||||
|
|
||||||
|
arr.add(newValue);
|
||||||
|
}
|
|
@ -10,11 +10,12 @@ import {
|
||||||
|
|
||||||
import { createLightNode, createPrivacyNode } from "./create_waku";
|
import { createLightNode, createPrivacyNode } from "./create_waku";
|
||||||
import { generateSymmetricKey } from "./crypto";
|
import { generateSymmetricKey } from "./crypto";
|
||||||
import type { Waku, WakuLight, WakuPrivacy } from "./interfaces";
|
import type { Message, Waku, WakuLight, WakuPrivacy } from "./interfaces";
|
||||||
import { PeerDiscoveryStaticPeers } from "./peer_discovery_static_list";
|
import { PeerDiscoveryStaticPeers } from "./peer_discovery_static_list";
|
||||||
|
import { bytesToUtf8, utf8ToBytes } from "./utils";
|
||||||
import { waitForRemotePeer } from "./wait_for_remote_peer";
|
import { waitForRemotePeer } from "./wait_for_remote_peer";
|
||||||
import { Protocols } from "./waku";
|
import { Protocols } from "./waku";
|
||||||
import { WakuMessage } from "./waku_message";
|
import { SymDecoder, SymEncoder } from "./waku_message/version_1.js";
|
||||||
|
|
||||||
const TestContentTopic = "/test/1/waku/utf8";
|
const TestContentTopic = "/test/1/waku/utf8";
|
||||||
|
|
||||||
|
@ -158,31 +159,26 @@ describe("Decryption Keys", () => {
|
||||||
this.timeout(10000);
|
this.timeout(10000);
|
||||||
|
|
||||||
const symKey = generateSymmetricKey();
|
const symKey = generateSymmetricKey();
|
||||||
|
const decoder = new SymDecoder(TestContentTopic, symKey);
|
||||||
|
|
||||||
waku2.addDecryptionKey(symKey);
|
const encoder = new SymEncoder(TestContentTopic, symKey);
|
||||||
|
|
||||||
const messageText = "Message is encrypted";
|
const messageText = "Message is encrypted";
|
||||||
const messageTimestamp = new Date("1995-12-17T03:24:00");
|
const messageTimestamp = new Date("1995-12-17T03:24:00");
|
||||||
const message = await WakuMessage.fromUtf8String(
|
const message = {
|
||||||
messageText,
|
payload: utf8ToBytes(messageText),
|
||||||
TestContentTopic,
|
|
||||||
{
|
|
||||||
timestamp: messageTimestamp,
|
timestamp: messageTimestamp,
|
||||||
symKey,
|
};
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const receivedMsgPromise: Promise<WakuMessage> = new Promise((resolve) => {
|
const receivedMsgPromise: Promise<Message> = new Promise((resolve) => {
|
||||||
waku2.relay.addObserver(resolve);
|
waku2.relay.addObserver(decoder, resolve);
|
||||||
});
|
});
|
||||||
|
|
||||||
await waku1.relay.send(message);
|
await waku1.relay.send(encoder, message);
|
||||||
|
|
||||||
const receivedMsg = await receivedMsgPromise;
|
const receivedMsg = await receivedMsgPromise;
|
||||||
|
|
||||||
expect(receivedMsg.contentTopic).to.eq(message.contentTopic);
|
expect(receivedMsg.contentTopic).to.eq(TestContentTopic);
|
||||||
expect(receivedMsg.version).to.eq(message.version);
|
expect(bytesToUtf8(receivedMsg.payload!)).to.eq(messageText);
|
||||||
expect(receivedMsg.payloadAsUtf8).to.eq(messageText);
|
|
||||||
expect(receivedMsg.timestamp?.valueOf()).to.eq(messageTimestamp.valueOf());
|
expect(receivedMsg.timestamp?.valueOf()).to.eq(messageTimestamp.valueOf());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -10,7 +10,7 @@ import type { Libp2p } from "libp2p";
|
||||||
import { Waku } from "./interfaces";
|
import { Waku } from "./interfaces";
|
||||||
import { FilterCodec, WakuFilter } from "./waku_filter";
|
import { FilterCodec, WakuFilter } from "./waku_filter";
|
||||||
import { LightPushCodec, WakuLightPush } from "./waku_light_push";
|
import { LightPushCodec, WakuLightPush } from "./waku_light_push";
|
||||||
import { DecryptionMethod, WakuMessage } from "./waku_message";
|
import { EncoderV0 } from "./waku_message/version_0";
|
||||||
import { WakuRelay } from "./waku_relay";
|
import { WakuRelay } from "./waku_relay";
|
||||||
import { RelayCodecs, RelayPingContentTopic } from "./waku_relay/constants";
|
import { RelayCodecs, RelayPingContentTopic } from "./waku_relay/constants";
|
||||||
import * as relayConstants from "./waku_relay/constants";
|
import * as relayConstants from "./waku_relay/constants";
|
||||||
|
@ -43,7 +43,6 @@ export interface WakuOptions {
|
||||||
* @default {@link DefaultRelayKeepAliveValueSecs}
|
* @default {@link DefaultRelayKeepAliveValueSecs}
|
||||||
*/
|
*/
|
||||||
relayKeepAlive?: number;
|
relayKeepAlive?: number;
|
||||||
decryptionKeys?: Array<Uint8Array | string>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class WakuNode implements Waku {
|
export class WakuNode implements Waku {
|
||||||
|
@ -110,10 +109,6 @@ export class WakuNode implements Waku {
|
||||||
libp2p.connectionManager.addEventListener("peer:disconnect", (evt) => {
|
libp2p.connectionManager.addEventListener("peer:disconnect", (evt) => {
|
||||||
this.stopKeepAlive(evt.detail.remotePeer);
|
this.stopKeepAlive(evt.detail.remotePeer);
|
||||||
});
|
});
|
||||||
|
|
||||||
options?.decryptionKeys?.forEach((key) => {
|
|
||||||
this.addDecryptionKey(key);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -183,34 +178,6 @@ export class WakuNode implements Waku {
|
||||||
return this.libp2p.isStarted();
|
return this.libp2p.isStarted();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a decryption key to attempt decryption of messages received via
|
|
||||||
* { @link WakuRelay } and { @link WakuStore }. This can either be a private key for
|
|
||||||
* asymmetric encryption or a symmetric key.
|
|
||||||
*
|
|
||||||
* Strings must be in hex format.
|
|
||||||
*/
|
|
||||||
addDecryptionKey(
|
|
||||||
key: Uint8Array | string,
|
|
||||||
options?: { method?: DecryptionMethod; contentTopics?: string[] }
|
|
||||||
): void {
|
|
||||||
if (this.relay) this.relay.addDecryptionKey(key, options);
|
|
||||||
if (this.store) this.store.addDecryptionKey(key, options);
|
|
||||||
if (this.filter) this.filter.addDecryptionKey(key, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a decryption key that was used to attempt decryption of messages
|
|
||||||
* received via { @link WakuRelay } or { @link WakuStore }.
|
|
||||||
*
|
|
||||||
* Strings must be in hex format.
|
|
||||||
*/
|
|
||||||
deleteDecryptionKey(key: Uint8Array | string): void {
|
|
||||||
if (this.relay) this.relay.deleteDecryptionKey(key);
|
|
||||||
if (this.store) this.store.deleteDecryptionKey(key);
|
|
||||||
if (this.filter) this.filter.deleteDecryptionKey(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the local multiaddr with peer id on which libp2p is listening.
|
* Return the local multiaddr with peer id on which libp2p is listening.
|
||||||
*
|
*
|
||||||
|
@ -246,11 +213,12 @@ export class WakuNode implements Waku {
|
||||||
|
|
||||||
const relay = this.relay;
|
const relay = this.relay;
|
||||||
if (relay && relayPeriodSecs !== 0) {
|
if (relay && relayPeriodSecs !== 0) {
|
||||||
|
const encoder = new EncoderV0(RelayPingContentTopic);
|
||||||
this.relayKeepAliveTimers[peerIdStr] = setInterval(() => {
|
this.relayKeepAliveTimers[peerIdStr] = setInterval(() => {
|
||||||
log("Sending Waku Relay ping message");
|
log("Sending Waku Relay ping message");
|
||||||
WakuMessage.fromBytes(new Uint8Array(), RelayPingContentTopic).then(
|
relay
|
||||||
(wakuMsg) => relay.send(wakuMsg)
|
.send(encoder, { payload: new Uint8Array() })
|
||||||
);
|
.catch((e) => log("Failed to send relay ping", e));
|
||||||
}, relayPeriodSecs * 1000);
|
}, relayPeriodSecs * 1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,10 +4,11 @@ import debug from "debug";
|
||||||
import { makeLogFileName, NOISE_KEY_1, Nwaku } from "../../test_utils";
|
import { makeLogFileName, NOISE_KEY_1, Nwaku } from "../../test_utils";
|
||||||
import { delay } from "../../test_utils/delay";
|
import { delay } from "../../test_utils/delay";
|
||||||
import { createFullNode } from "../create_waku";
|
import { createFullNode } from "../create_waku";
|
||||||
import type { WakuFull } from "../interfaces";
|
import type { Message, WakuFull } from "../interfaces";
|
||||||
|
import { bytesToUtf8, utf8ToBytes } from "../utils";
|
||||||
import { waitForRemotePeer } from "../wait_for_remote_peer";
|
import { waitForRemotePeer } from "../wait_for_remote_peer";
|
||||||
import { Protocols } from "../waku";
|
import { Protocols } from "../waku";
|
||||||
import { WakuMessage } from "../waku_message";
|
import { DecoderV0, EncoderV0 } from "../waku_message/version_0";
|
||||||
|
|
||||||
const log = debug("waku:test");
|
const log = debug("waku:test");
|
||||||
|
|
||||||
|
@ -40,22 +41,25 @@ describe("Waku Filter", () => {
|
||||||
|
|
||||||
let messageCount = 0;
|
let messageCount = 0;
|
||||||
const messageText = "Filtering works!";
|
const messageText = "Filtering works!";
|
||||||
const callback = (msg: WakuMessage): void => {
|
const message = { payload: utf8ToBytes(messageText) };
|
||||||
|
|
||||||
|
const callback = (msg: Message): void => {
|
||||||
log("Got a message");
|
log("Got a message");
|
||||||
messageCount++;
|
messageCount++;
|
||||||
expect(msg.contentTopic).to.eq(TestContentTopic);
|
expect(msg.contentTopic).to.eq(TestContentTopic);
|
||||||
expect(msg.payloadAsUtf8).to.eq(messageText);
|
expect(bytesToUtf8(msg.payload!)).to.eq(messageText);
|
||||||
};
|
};
|
||||||
await waku.filter.subscribe(callback, [TestContentTopic]);
|
|
||||||
|
const decoder = new DecoderV0(TestContentTopic);
|
||||||
|
|
||||||
|
await waku.filter.subscribe([decoder], callback);
|
||||||
// As the filter protocol does not cater for an ack of subscription
|
// As the filter protocol does not cater for an ack of subscription
|
||||||
// we cannot know whether the subscription happened. Something we want to
|
// we cannot know whether the subscription happened. Something we want to
|
||||||
// correct in future versions of the protocol.
|
// correct in future versions of the protocol.
|
||||||
await delay(200);
|
await delay(200);
|
||||||
const message = await WakuMessage.fromUtf8String(
|
|
||||||
messageText,
|
const encoder = new EncoderV0(TestContentTopic);
|
||||||
TestContentTopic
|
await waku.lightPush.push(encoder, message);
|
||||||
);
|
|
||||||
await waku.lightPush.push(message);
|
|
||||||
while (messageCount === 0) {
|
while (messageCount === 0) {
|
||||||
await delay(250);
|
await delay(250);
|
||||||
}
|
}
|
||||||
|
@ -66,21 +70,21 @@ describe("Waku Filter", () => {
|
||||||
this.timeout(10000);
|
this.timeout(10000);
|
||||||
|
|
||||||
let messageCount = 0;
|
let messageCount = 0;
|
||||||
const callback = (msg: WakuMessage): void => {
|
const callback = (msg: Message): void => {
|
||||||
messageCount++;
|
messageCount++;
|
||||||
expect(msg.contentTopic).to.eq(TestContentTopic);
|
expect(msg.contentTopic).to.eq(TestContentTopic);
|
||||||
};
|
};
|
||||||
await waku.filter.subscribe(callback, [TestContentTopic]);
|
const decoder = new DecoderV0(TestContentTopic);
|
||||||
|
await waku.filter.subscribe([decoder], callback);
|
||||||
|
|
||||||
await delay(200);
|
await delay(200);
|
||||||
await waku.lightPush.push(
|
const encoder = new EncoderV0(TestContentTopic);
|
||||||
await WakuMessage.fromUtf8String("Filtering works!", TestContentTopic)
|
await waku.lightPush.push(encoder, {
|
||||||
);
|
payload: utf8ToBytes("Filtering works!"),
|
||||||
await waku.lightPush.push(
|
});
|
||||||
await WakuMessage.fromUtf8String(
|
await waku.lightPush.push(encoder, {
|
||||||
"Filtering still works!",
|
payload: utf8ToBytes("Filtering still works!"),
|
||||||
TestContentTopic
|
});
|
||||||
)
|
|
||||||
);
|
|
||||||
while (messageCount < 2) {
|
while (messageCount < 2) {
|
||||||
await delay(250);
|
await delay(250);
|
||||||
}
|
}
|
||||||
|
@ -92,25 +96,21 @@ describe("Waku Filter", () => {
|
||||||
const callback = (): void => {
|
const callback = (): void => {
|
||||||
messageCount++;
|
messageCount++;
|
||||||
};
|
};
|
||||||
const unsubscribe = await waku.filter.subscribe(callback, [
|
const decoder = new DecoderV0(TestContentTopic);
|
||||||
TestContentTopic,
|
const unsubscribe = await waku.filter.subscribe([decoder], callback);
|
||||||
]);
|
|
||||||
|
const encoder = new EncoderV0(TestContentTopic);
|
||||||
|
|
||||||
await delay(200);
|
await delay(200);
|
||||||
await waku.lightPush.push(
|
await waku.lightPush.push(encoder, {
|
||||||
await WakuMessage.fromUtf8String(
|
payload: utf8ToBytes("This should be received"),
|
||||||
"This should be received",
|
});
|
||||||
TestContentTopic
|
|
||||||
)
|
|
||||||
);
|
|
||||||
await delay(100);
|
await delay(100);
|
||||||
await unsubscribe();
|
await unsubscribe();
|
||||||
await delay(200);
|
await delay(200);
|
||||||
await waku.lightPush.push(
|
await waku.lightPush.push(encoder, {
|
||||||
await WakuMessage.fromUtf8String(
|
payload: utf8ToBytes("This should not be received"),
|
||||||
"This should not be received",
|
});
|
||||||
TestContentTopic
|
|
||||||
)
|
|
||||||
);
|
|
||||||
await delay(100);
|
await delay(100);
|
||||||
expect(messageCount).to.eq(1);
|
expect(messageCount).to.eq(1);
|
||||||
});
|
});
|
||||||
|
|
|
@ -10,14 +10,14 @@ import type { Libp2p } from "libp2p";
|
||||||
|
|
||||||
import { WakuMessage as WakuMessageProto } from "../../proto/message";
|
import { WakuMessage as WakuMessageProto } from "../../proto/message";
|
||||||
import { DefaultPubSubTopic } from "../constants";
|
import { DefaultPubSubTopic } from "../constants";
|
||||||
|
import { groupByContentTopic } from "../group_by";
|
||||||
|
import { Decoder, Message } from "../interfaces";
|
||||||
import { selectConnection } from "../select_connection";
|
import { selectConnection } from "../select_connection";
|
||||||
import {
|
import {
|
||||||
getPeersForProtocol,
|
getPeersForProtocol,
|
||||||
selectPeerForProtocol,
|
selectPeerForProtocol,
|
||||||
selectRandomPeer,
|
selectRandomPeer,
|
||||||
} from "../select_peer";
|
} from "../select_peer";
|
||||||
import { hexToBytes } from "../utils";
|
|
||||||
import { DecryptionMethod, WakuMessage } from "../waku_message";
|
|
||||||
|
|
||||||
import { ContentFilter, FilterRPC } from "./filter_rpc";
|
import { ContentFilter, FilterRPC } from "./filter_rpc";
|
||||||
export { ContentFilter };
|
export { ContentFilter };
|
||||||
|
@ -49,7 +49,7 @@ export type FilterSubscriptionOpts = {
|
||||||
peerId?: PeerId;
|
peerId?: PeerId;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FilterCallback = (msg: WakuMessage) => void | Promise<void>;
|
export type FilterCallback = (msg: Message) => void | Promise<void>;
|
||||||
|
|
||||||
export type UnsubscribeFunction = () => Promise<void>;
|
export type UnsubscribeFunction = () => Promise<void>;
|
||||||
|
|
||||||
|
@ -63,14 +63,14 @@ export type UnsubscribeFunction = () => Promise<void>;
|
||||||
export class WakuFilter {
|
export class WakuFilter {
|
||||||
pubSubTopic: string;
|
pubSubTopic: string;
|
||||||
private subscriptions: Map<string, FilterCallback>;
|
private subscriptions: Map<string, FilterCallback>;
|
||||||
public decryptionKeys: Map<
|
public decoders: Map<
|
||||||
Uint8Array,
|
string, // content topic
|
||||||
{ method?: DecryptionMethod; contentTopics?: string[] }
|
Set<Decoder>
|
||||||
>;
|
>;
|
||||||
|
|
||||||
constructor(public libp2p: Libp2p, options?: CreateOptions) {
|
constructor(public libp2p: Libp2p, options?: CreateOptions) {
|
||||||
this.subscriptions = new Map();
|
this.subscriptions = new Map();
|
||||||
this.decryptionKeys = new Map();
|
this.decoders = new Map();
|
||||||
this.pubSubTopic = options?.pubSubTopic ?? DefaultPubSubTopic;
|
this.pubSubTopic = options?.pubSubTopic ?? DefaultPubSubTopic;
|
||||||
this.libp2p
|
this.libp2p
|
||||||
.handle(FilterCodec, this.onRequest.bind(this))
|
.handle(FilterCodec, this.onRequest.bind(this))
|
||||||
|
@ -78,17 +78,21 @@ export class WakuFilter {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param contentTopics Array of ContentTopics to subscribe to. If empty, no messages will be returned from the filter.
|
* @param decoders Array of Decoders to use to decode messages, it also specifies the content topics.
|
||||||
* @param callback A function that will be called on each message returned by the filter.
|
* @param callback A function that will be called on each message returned by the filter.
|
||||||
* @param opts The FilterSubscriptionOpts used to narrow which messages are returned, and which peer to connect to.
|
* @param opts The FilterSubscriptionOpts used to narrow which messages are returned, and which peer to connect to.
|
||||||
* @returns Unsubscribe function that can be used to end the subscription.
|
* @returns Unsubscribe function that can be used to end the subscription.
|
||||||
*/
|
*/
|
||||||
async subscribe(
|
async subscribe(
|
||||||
|
decoders: Decoder[],
|
||||||
callback: FilterCallback,
|
callback: FilterCallback,
|
||||||
contentTopics: string[],
|
|
||||||
opts?: FilterSubscriptionOpts
|
opts?: FilterSubscriptionOpts
|
||||||
): Promise<UnsubscribeFunction> {
|
): Promise<UnsubscribeFunction> {
|
||||||
const topic = opts?.pubsubTopic ?? this.pubSubTopic;
|
const topic = opts?.pubsubTopic ?? this.pubSubTopic;
|
||||||
|
|
||||||
|
const groupedDecoders = groupByContentTopic(decoders);
|
||||||
|
const contentTopics = Array.from(groupedDecoders.keys());
|
||||||
|
|
||||||
const contentFilters = contentTopics.map((contentTopic) => ({
|
const contentFilters = contentTopics.map((contentTopic) => ({
|
||||||
contentTopic,
|
contentTopic,
|
||||||
}));
|
}));
|
||||||
|
@ -130,11 +134,13 @@ export class WakuFilter {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.addDecoders(groupedDecoders);
|
||||||
this.addCallback(requestId, callback);
|
this.addCallback(requestId, callback);
|
||||||
|
|
||||||
return async () => {
|
return async () => {
|
||||||
await this.unsubscribe(topic, contentFilters, requestId, peer);
|
await this.unsubscribe(topic, contentFilters, requestId, peer);
|
||||||
this.removeCallback(requestId);
|
this.deleteDecoders(groupedDecoders);
|
||||||
|
this.deleteCallback(requestId);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,23 +177,35 @@ export class WakuFilter {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const decryptionParams = Array.from(this.decryptionKeys).map(
|
for (const protoMessage of messages) {
|
||||||
([key, { method, contentTopics }]) => {
|
const contentTopic = protoMessage.contentTopic;
|
||||||
return {
|
if (!contentTopic) {
|
||||||
key,
|
log("Message has no content topic, skipping");
|
||||||
method,
|
return;
|
||||||
contentTopics,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
for (const message of messages) {
|
const decoders = this.decoders.get(contentTopic);
|
||||||
const decoded = await WakuMessage.decodeProto(message, decryptionParams);
|
if (!decoders) {
|
||||||
|
log("No decoder for", contentTopic);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let msg: Message | undefined;
|
||||||
|
// We don't want to wait for decoding failure, just attempt to decode
|
||||||
|
// all messages and do the call back on the one that works
|
||||||
|
// noinspection ES6MissingAwait
|
||||||
|
decoders.forEach(async (dec) => {
|
||||||
|
if (msg) return;
|
||||||
|
const decoded = await dec.decode(protoMessage);
|
||||||
if (!decoded) {
|
if (!decoded) {
|
||||||
log("Not able to decode message");
|
log("Not able to decode message");
|
||||||
continue;
|
return;
|
||||||
}
|
}
|
||||||
callback(decoded);
|
// This is just to prevent more decoding attempt
|
||||||
|
// TODO: Could be better if we were to abort promises
|
||||||
|
msg = decoded;
|
||||||
|
await callback(decoded);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -195,10 +213,32 @@ export class WakuFilter {
|
||||||
this.subscriptions.set(requestId, callback);
|
this.subscriptions.set(requestId, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
private removeCallback(requestId: string): void {
|
private deleteCallback(requestId: string): void {
|
||||||
this.subscriptions.delete(requestId);
|
this.subscriptions.delete(requestId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private addDecoders(decoders: Map<string, Array<Decoder>>): void {
|
||||||
|
decoders.forEach((decoders, contentTopic) => {
|
||||||
|
const currDecs = this.decoders.get(contentTopic);
|
||||||
|
if (!currDecs) {
|
||||||
|
this.decoders.set(contentTopic, new Set(decoders));
|
||||||
|
} else {
|
||||||
|
this.decoders.set(contentTopic, new Set([...currDecs, ...decoders]));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private deleteDecoders(decoders: Map<string, Array<Decoder>>): void {
|
||||||
|
decoders.forEach((decoders, contentTopic) => {
|
||||||
|
const currDecs = this.decoders.get(contentTopic);
|
||||||
|
if (currDecs) {
|
||||||
|
decoders.forEach((dec) => {
|
||||||
|
currDecs.delete(dec);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private async unsubscribe(
|
private async unsubscribe(
|
||||||
topic: string,
|
topic: string,
|
||||||
contentFilters: ContentFilter[],
|
contentFilters: ContentFilter[],
|
||||||
|
@ -243,30 +283,6 @@ export class WakuFilter {
|
||||||
return res.peer;
|
return res.peer;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a decryption key to attempt decryption of messages received in any
|
|
||||||
* subsequent { @link subscribe } call. This can either be a private key for
|
|
||||||
* asymmetric encryption or a symmetric key. { @link WakuStore } will attempt to
|
|
||||||
* decrypt messages using both methods.
|
|
||||||
*
|
|
||||||
* Strings must be in hex format.
|
|
||||||
*/
|
|
||||||
addDecryptionKey(
|
|
||||||
key: Uint8Array | string,
|
|
||||||
options?: { method?: DecryptionMethod; contentTopics?: string[] }
|
|
||||||
): void {
|
|
||||||
this.decryptionKeys.set(hexToBytes(key), options ?? {});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a decryption key so that it cannot be used in future { @link subscribe } calls
|
|
||||||
*
|
|
||||||
* Strings must be in hex format.
|
|
||||||
*/
|
|
||||||
deleteDecryptionKey(key: Uint8Array | string): void {
|
|
||||||
this.decryptionKeys.delete(hexToBytes(key));
|
|
||||||
}
|
|
||||||
|
|
||||||
async peers(): Promise<Peer[]> {
|
async peers(): Promise<Peer[]> {
|
||||||
return getPeersForProtocol(this.libp2p.peerStore, [FilterCodec]);
|
return getPeersForProtocol(this.libp2p.peerStore, [FilterCodec]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,10 +10,10 @@ import {
|
||||||
import { delay } from "../../test_utils/delay";
|
import { delay } from "../../test_utils/delay";
|
||||||
import { createFullNode } from "../create_waku";
|
import { createFullNode } from "../create_waku";
|
||||||
import type { WakuFull } from "../interfaces";
|
import type { WakuFull } from "../interfaces";
|
||||||
import { bytesToUtf8 } from "../utils";
|
import { bytesToUtf8, utf8ToBytes } from "../utils";
|
||||||
import { waitForRemotePeer } from "../wait_for_remote_peer";
|
import { waitForRemotePeer } from "../wait_for_remote_peer";
|
||||||
import { Protocols } from "../waku";
|
import { Protocols } from "../waku";
|
||||||
import { WakuMessage } from "../waku_message";
|
import { EncoderV0 } from "../waku_message/version_0";
|
||||||
|
|
||||||
const log = debug("waku:test:lightpush");
|
const log = debug("waku:test:lightpush");
|
||||||
|
|
||||||
|
@ -42,12 +42,11 @@ describe("Waku Light Push [node only]", () => {
|
||||||
await waitForRemotePeer(waku, [Protocols.LightPush]);
|
await waitForRemotePeer(waku, [Protocols.LightPush]);
|
||||||
|
|
||||||
const messageText = "Light Push works!";
|
const messageText = "Light Push works!";
|
||||||
const message = await WakuMessage.fromUtf8String(
|
const encoder = new EncoderV0(TestContentTopic);
|
||||||
messageText,
|
|
||||||
TestContentTopic
|
|
||||||
);
|
|
||||||
|
|
||||||
const pushResponse = await waku.lightPush.push(message);
|
const pushResponse = await waku.lightPush.push(encoder, {
|
||||||
|
payload: utf8ToBytes(messageText),
|
||||||
|
});
|
||||||
expect(pushResponse?.isSuccess).to.be.true;
|
expect(pushResponse?.isSuccess).to.be.true;
|
||||||
|
|
||||||
let msgs: MessageRpcResponse[] = [];
|
let msgs: MessageRpcResponse[] = [];
|
||||||
|
@ -57,8 +56,7 @@ describe("Waku Light Push [node only]", () => {
|
||||||
msgs = await nwaku.messages();
|
msgs = await nwaku.messages();
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(msgs[0].contentTopic).to.equal(message.contentTopic);
|
expect(msgs[0].contentTopic).to.equal(TestContentTopic);
|
||||||
expect(msgs[0].version).to.equal(message.version);
|
|
||||||
expect(bytesToUtf8(new Uint8Array(msgs[0].payload))).to.equal(messageText);
|
expect(bytesToUtf8(new Uint8Array(msgs[0].payload))).to.equal(messageText);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -81,16 +79,17 @@ describe("Waku Light Push [node only]", () => {
|
||||||
const nimPeerId = await nwaku.getPeerId();
|
const nimPeerId = await nwaku.getPeerId();
|
||||||
|
|
||||||
const messageText = "Light Push works!";
|
const messageText = "Light Push works!";
|
||||||
const message = await WakuMessage.fromUtf8String(
|
const encoder = new EncoderV0(TestContentTopic);
|
||||||
messageText,
|
|
||||||
TestContentTopic
|
|
||||||
);
|
|
||||||
|
|
||||||
log("Send message via lightpush");
|
log("Send message via lightpush");
|
||||||
const pushResponse = await waku.lightPush.push(message, {
|
const pushResponse = await waku.lightPush.push(
|
||||||
|
encoder,
|
||||||
|
{ payload: utf8ToBytes(messageText) },
|
||||||
|
{
|
||||||
peerId: nimPeerId,
|
peerId: nimPeerId,
|
||||||
pubSubTopic: customPubSubTopic,
|
pubSubTopic: customPubSubTopic,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
log("Ack received", pushResponse);
|
log("Ack received", pushResponse);
|
||||||
expect(pushResponse?.isSuccess).to.be.true;
|
expect(pushResponse?.isSuccess).to.be.true;
|
||||||
|
|
||||||
|
@ -102,8 +101,7 @@ describe("Waku Light Push [node only]", () => {
|
||||||
msgs = await nwaku.messages(customPubSubTopic);
|
msgs = await nwaku.messages(customPubSubTopic);
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(msgs[0].contentTopic).to.equal(message.contentTopic);
|
expect(msgs[0].contentTopic).to.equal(TestContentTopic);
|
||||||
expect(msgs[0].version).to.equal(message.version);
|
|
||||||
expect(bytesToUtf8(new Uint8Array(msgs[0].payload))!).to.equal(messageText);
|
expect(bytesToUtf8(new Uint8Array(msgs[0].payload))!).to.equal(messageText);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -9,13 +9,13 @@ import { Uint8ArrayList } from "uint8arraylist";
|
||||||
|
|
||||||
import { PushResponse } from "../../proto/light_push";
|
import { PushResponse } from "../../proto/light_push";
|
||||||
import { DefaultPubSubTopic } from "../constants";
|
import { DefaultPubSubTopic } from "../constants";
|
||||||
|
import { Encoder, Message } from "../interfaces";
|
||||||
import { selectConnection } from "../select_connection";
|
import { selectConnection } from "../select_connection";
|
||||||
import {
|
import {
|
||||||
getPeersForProtocol,
|
getPeersForProtocol,
|
||||||
selectPeerForProtocol,
|
selectPeerForProtocol,
|
||||||
selectRandomPeer,
|
selectRandomPeer,
|
||||||
} from "../select_peer";
|
} from "../select_peer";
|
||||||
import { WakuMessage } from "../waku_message";
|
|
||||||
|
|
||||||
import { PushRPC } from "./push_rpc";
|
import { PushRPC } from "./push_rpc";
|
||||||
|
|
||||||
|
@ -52,9 +52,12 @@ export class WakuLightPush {
|
||||||
}
|
}
|
||||||
|
|
||||||
async push(
|
async push(
|
||||||
message: WakuMessage,
|
encoder: Encoder,
|
||||||
|
message: Message,
|
||||||
opts?: PushOptions
|
opts?: PushOptions
|
||||||
): Promise<PushResponse | null> {
|
): Promise<PushResponse | undefined> {
|
||||||
|
const pubSubTopic = opts?.pubSubTopic ? opts.pubSubTopic : this.pubSubTopic;
|
||||||
|
|
||||||
const res = await selectPeerForProtocol(
|
const res = await selectPeerForProtocol(
|
||||||
this.libp2p.peerStore,
|
this.libp2p.peerStore,
|
||||||
[LightPushCodec],
|
[LightPushCodec],
|
||||||
|
@ -73,10 +76,12 @@ export class WakuLightPush {
|
||||||
|
|
||||||
const stream = await connection.newStream(LightPushCodec);
|
const stream = await connection.newStream(LightPushCodec);
|
||||||
try {
|
try {
|
||||||
const pubSubTopic = opts?.pubSubTopic
|
const protoMessage = await encoder.encodeProto(message);
|
||||||
? opts.pubSubTopic
|
if (!protoMessage) {
|
||||||
: this.pubSubTopic;
|
log("Failed to encode to protoMessage, aborting push");
|
||||||
const query = PushRPC.createRequest(message, pubSubTopic);
|
return;
|
||||||
|
}
|
||||||
|
const query = PushRPC.createRequest(protoMessage, pubSubTopic);
|
||||||
const res = await pipe(
|
const res = await pipe(
|
||||||
[query.encode()],
|
[query.encode()],
|
||||||
lp.encode(),
|
lp.encode(),
|
||||||
|
@ -94,7 +99,7 @@ export class WakuLightPush {
|
||||||
|
|
||||||
if (!response) {
|
if (!response) {
|
||||||
log("No response in PushRPC");
|
log("No response in PushRPC");
|
||||||
return null;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
|
@ -104,7 +109,7 @@ export class WakuLightPush {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log("Failed to send waku light push request", err);
|
log("Failed to send waku light push request", err);
|
||||||
}
|
}
|
||||||
return null;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -2,16 +2,18 @@ import type { Uint8ArrayList } from "uint8arraylist";
|
||||||
import { v4 as uuid } from "uuid";
|
import { v4 as uuid } from "uuid";
|
||||||
|
|
||||||
import * as proto from "../../proto/light_push";
|
import * as proto from "../../proto/light_push";
|
||||||
import { WakuMessage } from "../waku_message";
|
|
||||||
|
|
||||||
export class PushRPC {
|
export class PushRPC {
|
||||||
public constructor(public proto: proto.PushRPC) {}
|
public constructor(public proto: proto.PushRPC) {}
|
||||||
|
|
||||||
static createRequest(message: WakuMessage, pubSubTopic: string): PushRPC {
|
static createRequest(
|
||||||
|
message: proto.WakuMessage,
|
||||||
|
pubSubTopic: string
|
||||||
|
): PushRPC {
|
||||||
return new PushRPC({
|
return new PushRPC({
|
||||||
requestId: uuid(),
|
requestId: uuid(),
|
||||||
request: {
|
request: {
|
||||||
message: message.proto,
|
message: message,
|
||||||
pubSubTopic: pubSubTopic,
|
pubSubTopic: pubSubTopic,
|
||||||
},
|
},
|
||||||
response: undefined,
|
response: undefined,
|
||||||
|
|
|
@ -1,191 +0,0 @@
|
||||||
import { expect } from "chai";
|
|
||||||
import debug from "debug";
|
|
||||||
|
|
||||||
import {
|
|
||||||
makeLogFileName,
|
|
||||||
MessageRpcQuery,
|
|
||||||
MessageRpcResponseHex,
|
|
||||||
NOISE_KEY_1,
|
|
||||||
Nwaku,
|
|
||||||
} from "../../test_utils";
|
|
||||||
import { delay } from "../../test_utils/delay";
|
|
||||||
import { createPrivacyNode } from "../create_waku";
|
|
||||||
import {
|
|
||||||
generatePrivateKey,
|
|
||||||
generateSymmetricKey,
|
|
||||||
getPublicKey,
|
|
||||||
} from "../crypto";
|
|
||||||
import type { WakuPrivacy } from "../interfaces";
|
|
||||||
import { bytesToHex, bytesToUtf8, hexToBytes, utf8ToBytes } from "../utils";
|
|
||||||
import { waitForRemotePeer } from "../wait_for_remote_peer";
|
|
||||||
import { Protocols } from "../waku";
|
|
||||||
|
|
||||||
import { DecryptionMethod, WakuMessage } from "./index";
|
|
||||||
|
|
||||||
const log = debug("waku:test:message");
|
|
||||||
|
|
||||||
const TestContentTopic = "/test/1/waku-message/utf8";
|
|
||||||
|
|
||||||
describe("Waku Message [node only]", function () {
|
|
||||||
describe("Interop: nwaku", function () {
|
|
||||||
let waku: WakuPrivacy;
|
|
||||||
let nwaku: Nwaku;
|
|
||||||
|
|
||||||
beforeEach(async function () {
|
|
||||||
this.timeout(30_000);
|
|
||||||
waku = await createPrivacyNode({
|
|
||||||
staticNoiseKey: NOISE_KEY_1,
|
|
||||||
});
|
|
||||||
await waku.start();
|
|
||||||
|
|
||||||
nwaku = new Nwaku(makeLogFileName(this));
|
|
||||||
log("Starting nwaku node");
|
|
||||||
await nwaku.start({ rpcPrivate: true });
|
|
||||||
|
|
||||||
log("Dialing to nwaku node");
|
|
||||||
await waku.dial(await nwaku.getMultiaddrWithId());
|
|
||||||
log("Wait for remote peer");
|
|
||||||
await waitForRemotePeer(waku, [Protocols.Relay]);
|
|
||||||
log("Remote peer ready");
|
|
||||||
// As this test uses the nwaku RPC API, we somehow often face
|
|
||||||
// Race conditions where the nwaku node does not have the js-waku
|
|
||||||
// Node in its relay mesh just yet.
|
|
||||||
await delay(500);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(async function () {
|
|
||||||
!!nwaku && nwaku.stop();
|
|
||||||
!!waku && waku.stop().catch((e) => console.log("Waku failed to stop", e));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Decrypts nwaku message [asymmetric, no signature]", async function () {
|
|
||||||
this.timeout(5000);
|
|
||||||
|
|
||||||
const messageText = "Here is an encrypted message.";
|
|
||||||
const message: MessageRpcQuery = {
|
|
||||||
contentTopic: TestContentTopic,
|
|
||||||
payload: bytesToHex(utf8ToBytes(messageText)),
|
|
||||||
};
|
|
||||||
|
|
||||||
const privateKey = generatePrivateKey();
|
|
||||||
|
|
||||||
waku.relay.addDecryptionKey(privateKey, {
|
|
||||||
method: DecryptionMethod.Asymmetric,
|
|
||||||
});
|
|
||||||
|
|
||||||
const receivedMsgPromise: Promise<WakuMessage> = new Promise(
|
|
||||||
(resolve) => {
|
|
||||||
waku.relay.addObserver(resolve);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const publicKey = getPublicKey(privateKey);
|
|
||||||
log("Post message");
|
|
||||||
const res = await nwaku.postAsymmetricMessage(message, publicKey);
|
|
||||||
expect(res).to.be.true;
|
|
||||||
|
|
||||||
const receivedMsg = await receivedMsgPromise;
|
|
||||||
|
|
||||||
expect(receivedMsg.contentTopic).to.eq(message.contentTopic);
|
|
||||||
expect(receivedMsg.version).to.eq(1);
|
|
||||||
expect(receivedMsg.payloadAsUtf8).to.eq(messageText);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Encrypts message for nwaku [asymmetric, no signature]", async function () {
|
|
||||||
this.timeout(5000);
|
|
||||||
|
|
||||||
log("Ask nwaku to generate asymmetric key pair");
|
|
||||||
const keyPair = await nwaku.getAsymmetricKeyPair();
|
|
||||||
const privateKey = hexToBytes(keyPair.privateKey);
|
|
||||||
const publicKey = hexToBytes(keyPair.publicKey);
|
|
||||||
|
|
||||||
const messageText = "This is a message I am going to encrypt";
|
|
||||||
log("Encrypt message");
|
|
||||||
const message = await WakuMessage.fromUtf8String(
|
|
||||||
messageText,
|
|
||||||
TestContentTopic,
|
|
||||||
{
|
|
||||||
encPublicKey: publicKey,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
log("Send message over relay");
|
|
||||||
await waku.relay.send(message);
|
|
||||||
|
|
||||||
let msgs: MessageRpcResponseHex[] = [];
|
|
||||||
|
|
||||||
while (msgs.length === 0) {
|
|
||||||
log("Wait for message to be seen by nwaku");
|
|
||||||
await delay(200);
|
|
||||||
msgs = await nwaku.getAsymmetricMessages(privateKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
log("Check message content");
|
|
||||||
expect(msgs[0].contentTopic).to.equal(message.contentTopic);
|
|
||||||
expect(bytesToUtf8(hexToBytes(msgs[0].payload))).to.equal(messageText);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Decrypts nwaku message [symmetric, no signature]", async function () {
|
|
||||||
this.timeout(5000);
|
|
||||||
|
|
||||||
const messageText = "Here is a message encrypted in a symmetric manner.";
|
|
||||||
const message: MessageRpcQuery = {
|
|
||||||
contentTopic: TestContentTopic,
|
|
||||||
payload: bytesToHex(utf8ToBytes(messageText)),
|
|
||||||
};
|
|
||||||
|
|
||||||
log("Generate symmetric key");
|
|
||||||
const symKey = generateSymmetricKey();
|
|
||||||
|
|
||||||
waku.relay.addDecryptionKey(symKey, {
|
|
||||||
method: DecryptionMethod.Symmetric,
|
|
||||||
});
|
|
||||||
|
|
||||||
const receivedMsgPromise: Promise<WakuMessage> = new Promise(
|
|
||||||
(resolve) => {
|
|
||||||
waku.relay.addObserver(resolve);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
log("Post message using nwaku");
|
|
||||||
await nwaku.postSymmetricMessage(message, symKey);
|
|
||||||
log("Wait for message to be received by js-waku");
|
|
||||||
const receivedMsg = await receivedMsgPromise;
|
|
||||||
log("Message received by js-waku");
|
|
||||||
|
|
||||||
expect(receivedMsg.contentTopic).to.eq(message.contentTopic);
|
|
||||||
expect(receivedMsg.version).to.eq(1);
|
|
||||||
expect(receivedMsg.payloadAsUtf8).to.eq(messageText);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Encrypts message for nwaku [symmetric, no signature]", async function () {
|
|
||||||
this.timeout(5000);
|
|
||||||
|
|
||||||
log("Getting symmetric key from nwaku");
|
|
||||||
const symKey = await nwaku.getSymmetricKey();
|
|
||||||
log("Encrypting message with js-waku");
|
|
||||||
const messageText =
|
|
||||||
"This is a message I am going to encrypt with a symmetric key";
|
|
||||||
const message = await WakuMessage.fromUtf8String(
|
|
||||||
messageText,
|
|
||||||
TestContentTopic,
|
|
||||||
{
|
|
||||||
symKey: symKey,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
log("Sending message over relay");
|
|
||||||
await waku.relay.send(message);
|
|
||||||
|
|
||||||
let msgs: MessageRpcResponseHex[] = [];
|
|
||||||
|
|
||||||
while (msgs.length === 0) {
|
|
||||||
await delay(200);
|
|
||||||
log("Getting messages from nwaku");
|
|
||||||
msgs = await nwaku.getSymmetricMessages(symKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(msgs[0].contentTopic).to.equal(message.contentTopic);
|
|
||||||
expect(bytesToUtf8(hexToBytes(msgs[0].payload))).to.equal(messageText);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,138 +0,0 @@
|
||||||
import { expect } from "chai";
|
|
||||||
import fc from "fast-check";
|
|
||||||
|
|
||||||
import { getPublicKey } from "../crypto";
|
|
||||||
|
|
||||||
import { WakuMessage } from "./index";
|
|
||||||
|
|
||||||
const TestContentTopic = "/test/1/waku-message/utf8";
|
|
||||||
|
|
||||||
describe("Waku Message: Browser & Node", function () {
|
|
||||||
it("Waku message round trip binary serialization [clear]", async function () {
|
|
||||||
await fc.assert(
|
|
||||||
fc.asyncProperty(fc.string(), async (s) => {
|
|
||||||
const msg = await WakuMessage.fromUtf8String(s, TestContentTopic);
|
|
||||||
const binary = msg.encode();
|
|
||||||
const actual = await WakuMessage.decode(binary);
|
|
||||||
|
|
||||||
expect(actual).to.deep.equal(msg);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Payload to utf-8", async function () {
|
|
||||||
await fc.assert(
|
|
||||||
fc.asyncProperty(fc.string(), async (s) => {
|
|
||||||
const msg = await WakuMessage.fromUtf8String(s, TestContentTopic);
|
|
||||||
const utf8 = msg.payloadAsUtf8;
|
|
||||||
|
|
||||||
return utf8 === s;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Waku message round trip binary encryption [asymmetric, no signature]", async function () {
|
|
||||||
await fc.assert(
|
|
||||||
fc.asyncProperty(
|
|
||||||
fc.uint8Array({ minLength: 1 }),
|
|
||||||
fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }),
|
|
||||||
async (payload, key) => {
|
|
||||||
const publicKey = getPublicKey(key);
|
|
||||||
|
|
||||||
const msg = await WakuMessage.fromBytes(payload, TestContentTopic, {
|
|
||||||
encPublicKey: publicKey,
|
|
||||||
});
|
|
||||||
|
|
||||||
const wireBytes = msg.encode();
|
|
||||||
const actual = await WakuMessage.decode(wireBytes, [{ key }]);
|
|
||||||
|
|
||||||
expect(actual?.payload).to.deep.equal(payload);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Waku message round trip binary encryption [asymmetric, signature]", async function () {
|
|
||||||
this.timeout(4000);
|
|
||||||
|
|
||||||
await fc.assert(
|
|
||||||
fc.asyncProperty(
|
|
||||||
fc.uint8Array({ minLength: 1 }),
|
|
||||||
fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }),
|
|
||||||
fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }),
|
|
||||||
async (payload, sigPrivKey, encPrivKey) => {
|
|
||||||
const sigPubKey = getPublicKey(sigPrivKey);
|
|
||||||
const encPubKey = getPublicKey(encPrivKey);
|
|
||||||
|
|
||||||
const msg = await WakuMessage.fromBytes(payload, TestContentTopic, {
|
|
||||||
encPublicKey: encPubKey,
|
|
||||||
sigPrivKey: sigPrivKey,
|
|
||||||
});
|
|
||||||
|
|
||||||
const wireBytes = msg.encode();
|
|
||||||
const actual = await WakuMessage.decode(wireBytes, [
|
|
||||||
{ key: encPrivKey },
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(actual?.payload).to.deep.equal(payload);
|
|
||||||
expect(actual?.signaturePublicKey).to.deep.equal(sigPubKey);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Waku message round trip binary encryption [symmetric, no signature]", async function () {
|
|
||||||
await fc.assert(
|
|
||||||
fc.asyncProperty(
|
|
||||||
fc.uint8Array({ minLength: 1 }),
|
|
||||||
fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }),
|
|
||||||
async (payload, key) => {
|
|
||||||
const msg = await WakuMessage.fromBytes(payload, TestContentTopic, {
|
|
||||||
symKey: key,
|
|
||||||
});
|
|
||||||
|
|
||||||
const wireBytes = msg.encode();
|
|
||||||
const actual = await WakuMessage.decode(wireBytes, [{ key }]);
|
|
||||||
|
|
||||||
expect(actual?.payload).to.deep.equal(payload);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Waku message round trip binary encryption [symmetric, signature]", async function () {
|
|
||||||
await fc.assert(
|
|
||||||
fc.asyncProperty(
|
|
||||||
fc.uint8Array({ minLength: 1 }),
|
|
||||||
fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }),
|
|
||||||
fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }),
|
|
||||||
async (payload, sigPrivKey, symKey) => {
|
|
||||||
const sigPubKey = getPublicKey(sigPrivKey);
|
|
||||||
|
|
||||||
const msg = await WakuMessage.fromBytes(payload, TestContentTopic, {
|
|
||||||
symKey: symKey,
|
|
||||||
sigPrivKey: sigPrivKey,
|
|
||||||
});
|
|
||||||
|
|
||||||
const wireBytes = msg.encode();
|
|
||||||
const actual = await WakuMessage.decode(wireBytes, [{ key: symKey }]);
|
|
||||||
|
|
||||||
expect(actual?.payload).to.deep.equal(payload);
|
|
||||||
expect(actual?.signaturePublicKey).to.deep.equal(sigPubKey);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Waku message round trip utf-8 including emojis", async function () {
|
|
||||||
const messageText = "😁🤣🥧🤦👩🎓";
|
|
||||||
const wakuMessage = await WakuMessage.fromUtf8String(
|
|
||||||
messageText,
|
|
||||||
TestContentTopic
|
|
||||||
);
|
|
||||||
|
|
||||||
const decodedText = wakuMessage.payloadAsUtf8;
|
|
||||||
|
|
||||||
expect(decodedText).to.eq(messageText);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,312 +0,0 @@
|
||||||
import debug from "debug";
|
|
||||||
|
|
||||||
import * as proto from "../../proto/message";
|
|
||||||
import { bytesToUtf8, utf8ToBytes } from "../utils";
|
|
||||||
|
|
||||||
import * as version_1 from "./version_1";
|
|
||||||
|
|
||||||
const DefaultVersion = 0;
|
|
||||||
const log = debug("waku:message");
|
|
||||||
const OneMillion = BigInt(1_000_000);
|
|
||||||
|
|
||||||
export enum DecryptionMethod {
|
|
||||||
Asymmetric = "asymmetric",
|
|
||||||
Symmetric = "symmetric",
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Options {
|
|
||||||
/**
|
|
||||||
* Timestamp to set on the message, defaults to now if not passed.
|
|
||||||
*/
|
|
||||||
timestamp?: Date;
|
|
||||||
/**
|
|
||||||
* Public Key to use to encrypt the messages using ECIES (Asymmetric Encryption).
|
|
||||||
*
|
|
||||||
* @throws if both `encPublicKey` and `symKey` are passed
|
|
||||||
*/
|
|
||||||
encPublicKey?: Uint8Array | string;
|
|
||||||
/**
|
|
||||||
* Key to use to encrypt the messages using AES (Symmetric Encryption).
|
|
||||||
*
|
|
||||||
* @throws if both `encPublicKey` and `symKey` are passed
|
|
||||||
*/
|
|
||||||
symKey?: Uint8Array | string;
|
|
||||||
/**
|
|
||||||
* Private key to use to sign the message, either `encPublicKey` or `symKey` must be provided as only
|
|
||||||
* encrypted messages are signed.
|
|
||||||
*/
|
|
||||||
sigPrivKey?: Uint8Array;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DecryptionParams {
|
|
||||||
key: Uint8Array;
|
|
||||||
method?: DecryptionMethod;
|
|
||||||
contentTopics?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export class WakuMessage {
|
|
||||||
private constructor(
|
|
||||||
public proto: proto.WakuMessage,
|
|
||||||
private _signaturePublicKey?: Uint8Array,
|
|
||||||
private _signature?: Uint8Array
|
|
||||||
) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create Message with an utf-8 string as payload.
|
|
||||||
*/
|
|
||||||
static async fromUtf8String(
|
|
||||||
utf8: string,
|
|
||||||
contentTopic: string,
|
|
||||||
opts?: Options
|
|
||||||
): Promise<WakuMessage> {
|
|
||||||
const payload = utf8ToBytes(utf8);
|
|
||||||
return WakuMessage.fromBytes(payload, contentTopic, opts);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a Waku Message with the given payload.
|
|
||||||
*
|
|
||||||
* By default, the payload is kept clear (version 0).
|
|
||||||
* If `opts.encPublicKey` is passed, the payload is encrypted using
|
|
||||||
* asymmetric encryption (version 1).
|
|
||||||
*
|
|
||||||
* If `opts.sigPrivKey` is passed and version 1 is used, the payload is signed
|
|
||||||
* before encryption.
|
|
||||||
*
|
|
||||||
* @throws if both `opts.encPublicKey` and `opt.symKey` are passed
|
|
||||||
*/
|
|
||||||
static async fromBytes(
|
|
||||||
payload: Uint8Array,
|
|
||||||
contentTopic: string,
|
|
||||||
opts?: Options
|
|
||||||
): Promise<WakuMessage> {
|
|
||||||
const { timestamp, encPublicKey, symKey, sigPrivKey } = Object.assign(
|
|
||||||
{ timestamp: new Date() },
|
|
||||||
opts ? opts : {}
|
|
||||||
);
|
|
||||||
|
|
||||||
let _payload = payload;
|
|
||||||
let version = DefaultVersion;
|
|
||||||
let sig;
|
|
||||||
|
|
||||||
if (encPublicKey && symKey) {
|
|
||||||
throw "Pass either `encPublicKey` or `symKey`, not both.";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (encPublicKey) {
|
|
||||||
const enc = await version_1.clearEncode(_payload, sigPrivKey);
|
|
||||||
_payload = await version_1.encryptAsymmetric(enc.payload, encPublicKey);
|
|
||||||
sig = enc.sig;
|
|
||||||
version = 1;
|
|
||||||
} else if (symKey) {
|
|
||||||
const enc = await version_1.clearEncode(_payload, sigPrivKey);
|
|
||||||
_payload = await version_1.encryptSymmetric(enc.payload, symKey);
|
|
||||||
sig = enc.sig;
|
|
||||||
version = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new WakuMessage(
|
|
||||||
{
|
|
||||||
payload: _payload,
|
|
||||||
timestampDeprecated: timestamp.valueOf() / 1000,
|
|
||||||
// milliseconds 10^-3 to nanoseconds 10^-9
|
|
||||||
timestamp: BigInt(timestamp.valueOf()) * OneMillion,
|
|
||||||
version,
|
|
||||||
contentTopic,
|
|
||||||
},
|
|
||||||
sig?.publicKey,
|
|
||||||
sig?.signature
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decode a byte array into Waku Message.
|
|
||||||
*
|
|
||||||
* @params bytes The message encoded using protobuf as defined in [14/WAKU2-MESSAGE](https://rfc.vac.dev/spec/14/).
|
|
||||||
* @params decryptionKeys If the payload is encrypted (version = 1), then the
|
|
||||||
* keys are used to attempt decryption of the message. The passed key can either
|
|
||||||
* be asymmetric private keys or symmetric keys, both method are tried for each
|
|
||||||
* key until the message is decrypted or combinations are run out.
|
|
||||||
*/
|
|
||||||
static async decode(
|
|
||||||
bytes: Uint8Array,
|
|
||||||
decryptionParams?: DecryptionParams[]
|
|
||||||
): Promise<WakuMessage | undefined> {
|
|
||||||
const protoBuf = proto.WakuMessage.decode(bytes);
|
|
||||||
|
|
||||||
return WakuMessage.decodeProto(protoBuf, decryptionParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decode and decrypt Waku Message Protobuf Object into Waku Message.
|
|
||||||
*
|
|
||||||
* @params protoBuf The message to decode and decrypt.
|
|
||||||
* @params decryptionParams If the payload is encrypted (version = 1), then the
|
|
||||||
* keys are used to attempt decryption of the message. The passed key can either
|
|
||||||
* be asymmetric private keys or symmetric keys, both method are tried for each
|
|
||||||
* key until the message is decrypted or combinations are run out.
|
|
||||||
*/
|
|
||||||
static async decodeProto(
|
|
||||||
protoBuf: proto.WakuMessage,
|
|
||||||
decryptionParams?: DecryptionParams[]
|
|
||||||
): Promise<WakuMessage | undefined> {
|
|
||||||
const payload = protoBuf.payload;
|
|
||||||
|
|
||||||
let signaturePublicKey;
|
|
||||||
let signature;
|
|
||||||
if (protoBuf.version === 1 && payload) {
|
|
||||||
if (decryptionParams === undefined) {
|
|
||||||
log("Payload is encrypted but no private keys have been provided.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns a bunch of `undefined` and hopefully one decrypted result
|
|
||||||
const allResults = await Promise.all(
|
|
||||||
decryptionParams.map(async ({ key, method, contentTopics }) => {
|
|
||||||
if (
|
|
||||||
!contentTopics ||
|
|
||||||
(protoBuf.contentTopic &&
|
|
||||||
contentTopics.includes(protoBuf.contentTopic))
|
|
||||||
) {
|
|
||||||
switch (method) {
|
|
||||||
case DecryptionMethod.Asymmetric:
|
|
||||||
try {
|
|
||||||
return await version_1.decryptAsymmetric(payload, key);
|
|
||||||
} catch (e) {
|
|
||||||
log(
|
|
||||||
"Failed to decrypt message using asymmetric encryption despite decryption method being specified",
|
|
||||||
e
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
case DecryptionMethod.Symmetric:
|
|
||||||
try {
|
|
||||||
return await version_1.decryptSymmetric(payload, key);
|
|
||||||
} catch (e) {
|
|
||||||
log(
|
|
||||||
"Failed to decrypt message using asymmetric encryption despite decryption method being specified",
|
|
||||||
e
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
try {
|
|
||||||
return await version_1.decryptSymmetric(payload, key);
|
|
||||||
} catch (e) {
|
|
||||||
log(
|
|
||||||
"Failed to decrypt message using symmetric encryption",
|
|
||||||
e
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
return await version_1.decryptAsymmetric(payload, key);
|
|
||||||
} catch (e) {
|
|
||||||
log(
|
|
||||||
"Failed to decrypt message using asymmetric encryption",
|
|
||||||
e
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// No key available for this content topic
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const isDefined = (dec: Uint8Array | undefined): dec is Uint8Array => {
|
|
||||||
return !!dec;
|
|
||||||
};
|
|
||||||
|
|
||||||
const decodedResults = allResults.filter(isDefined);
|
|
||||||
|
|
||||||
if (decodedResults.length === 0) {
|
|
||||||
log("Failed to decrypt payload.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const dec = decodedResults[0];
|
|
||||||
|
|
||||||
const res = await version_1.clearDecode(dec);
|
|
||||||
if (!res) {
|
|
||||||
log("Failed to decode payload.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Object.assign(protoBuf, { payload: res.payload });
|
|
||||||
signaturePublicKey = res.sig?.publicKey;
|
|
||||||
signature = res.sig?.signature;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new WakuMessage(protoBuf, signaturePublicKey, signature);
|
|
||||||
}
|
|
||||||
|
|
||||||
encode(): Uint8Array {
|
|
||||||
return proto.WakuMessage.encode(this.proto);
|
|
||||||
}
|
|
||||||
|
|
||||||
get payloadAsUtf8(): string {
|
|
||||||
if (!this.payload) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return bytesToUtf8(this.payload);
|
|
||||||
} catch (e) {
|
|
||||||
log("Could not decode byte as UTF-8", e);
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get payload(): Uint8Array | undefined {
|
|
||||||
if (this.proto.payload) {
|
|
||||||
return new Uint8Array(this.proto.payload);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
get contentTopic(): string | undefined {
|
|
||||||
return this.proto.contentTopic;
|
|
||||||
}
|
|
||||||
|
|
||||||
get version(): number {
|
|
||||||
// https://github.com/status-im/js-waku/issues/921
|
|
||||||
return this.proto.version ?? 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
get timestamp(): Date | undefined {
|
|
||||||
// In the case we receive a value that is bigger than JS's max number,
|
|
||||||
// we catch the error and return undefined.
|
|
||||||
try {
|
|
||||||
if (this.proto.timestamp) {
|
|
||||||
// nanoseconds 10^-9 to milliseconds 10^-3
|
|
||||||
const timestamp = this.proto.timestamp / OneMillion;
|
|
||||||
return new Date(Number(timestamp));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.proto.timestampDeprecated) {
|
|
||||||
return new Date(this.proto.timestampDeprecated * 1000);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The public key used to sign the message.
|
|
||||||
*
|
|
||||||
* MAY be present if the message is version 1.
|
|
||||||
*/
|
|
||||||
get signaturePublicKey(): Uint8Array | undefined {
|
|
||||||
return this._signaturePublicKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The signature of the message.
|
|
||||||
*
|
|
||||||
* MAY be present if the message is version 1.
|
|
||||||
*/
|
|
||||||
get signature(): Uint8Array | undefined {
|
|
||||||
return this._signature;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { expect } from "chai";
|
||||||
|
import fc from "fast-check";
|
||||||
|
|
||||||
|
import { DecoderV0, EncoderV0, MessageV0 } from "./version_0";
|
||||||
|
|
||||||
|
const TestContentTopic = "/test/1/waku-message/utf8";
|
||||||
|
|
||||||
|
describe("Waku Message version 0", function () {
|
||||||
|
it("Round trip binary serialization", async function () {
|
||||||
|
await fc.assert(
|
||||||
|
fc.asyncProperty(fc.uint8Array({ minLength: 1 }), async (payload) => {
|
||||||
|
const encoder = new EncoderV0(TestContentTopic);
|
||||||
|
const bytes = await encoder.encode({ payload });
|
||||||
|
const decoder = new DecoderV0(TestContentTopic);
|
||||||
|
const protoResult = await decoder.decodeProto(bytes);
|
||||||
|
const result = (await decoder.decode(protoResult!)) as MessageV0;
|
||||||
|
|
||||||
|
expect(result.contentTopic).to.eq(TestContentTopic);
|
||||||
|
expect(result.version).to.eq(0);
|
||||||
|
expect(result.payload).to.deep.eq(payload);
|
||||||
|
expect(result.timestamp).to.not.be.undefined;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,105 @@
|
||||||
|
import debug from "debug";
|
||||||
|
|
||||||
|
import * as proto from "../../proto/message";
|
||||||
|
import { Decoder, Message, ProtoMessage } from "../interfaces";
|
||||||
|
import { Encoder } from "../interfaces";
|
||||||
|
|
||||||
|
const log = debug("waku:message:version-0");
|
||||||
|
|
||||||
|
const OneMillion = BigInt(1_000_000);
|
||||||
|
export const Version = 0;
|
||||||
|
|
||||||
|
export class MessageV0 implements Message {
|
||||||
|
constructor(private proto: proto.WakuMessage) {}
|
||||||
|
|
||||||
|
get _rawPayload(): Uint8Array | undefined {
|
||||||
|
if (this.proto.payload) {
|
||||||
|
return new Uint8Array(this.proto.payload);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
get payload(): Uint8Array | undefined {
|
||||||
|
return this._rawPayload;
|
||||||
|
}
|
||||||
|
|
||||||
|
get contentTopic(): string | undefined {
|
||||||
|
return this.proto.contentTopic;
|
||||||
|
}
|
||||||
|
|
||||||
|
get _rawTimestamp(): bigint | undefined {
|
||||||
|
return this.proto.timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
get timestamp(): Date | undefined {
|
||||||
|
// In the case we receive a value that is bigger than JS's max number,
|
||||||
|
// we catch the error and return undefined.
|
||||||
|
try {
|
||||||
|
if (this.proto.timestamp) {
|
||||||
|
// nanoseconds 10^-9 to milliseconds 10^-3
|
||||||
|
const timestamp = this.proto.timestamp / OneMillion;
|
||||||
|
return new Date(Number(timestamp));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.proto.timestampDeprecated) {
|
||||||
|
return new Date(this.proto.timestampDeprecated * 1000);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
get version(): number {
|
||||||
|
// https://github.com/status-im/js-waku/issues/921
|
||||||
|
return this.proto.version ?? 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EncoderV0 implements Encoder {
|
||||||
|
constructor(public contentTopic: string) {}
|
||||||
|
|
||||||
|
async encode(message: Message): Promise<Uint8Array> {
|
||||||
|
return proto.WakuMessage.encode(await this.encodeProto(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
async encodeProto(message: Message): Promise<ProtoMessage> {
|
||||||
|
const timestamp = message.timestamp ?? new Date();
|
||||||
|
|
||||||
|
return {
|
||||||
|
payload: message.payload,
|
||||||
|
version: Version,
|
||||||
|
contentTopic: message.contentTopic ?? this.contentTopic,
|
||||||
|
timestamp: BigInt(timestamp.valueOf()) * OneMillion,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DecoderV0 implements Decoder {
|
||||||
|
constructor(public contentTopic: string) {}
|
||||||
|
|
||||||
|
decodeProto(bytes: Uint8Array): Promise<ProtoMessage | undefined> {
|
||||||
|
const protoMessage = proto.WakuMessage.decode(bytes);
|
||||||
|
log("Message decoded", protoMessage);
|
||||||
|
return Promise.resolve(protoMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
async decode(proto: ProtoMessage): Promise<Message | undefined> {
|
||||||
|
// https://github.com/status-im/js-waku/issues/921
|
||||||
|
if (proto.version === undefined) {
|
||||||
|
proto.version = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (proto.version !== Version) {
|
||||||
|
log(
|
||||||
|
"Failed to decode due to incorrect version, expected:",
|
||||||
|
Version,
|
||||||
|
", actual:",
|
||||||
|
proto.version
|
||||||
|
);
|
||||||
|
return Promise.resolve(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new MessageV0(proto);
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,46 +2,140 @@ import { expect } from "chai";
|
||||||
import fc from "fast-check";
|
import fc from "fast-check";
|
||||||
|
|
||||||
import { getPublicKey } from "../crypto";
|
import { getPublicKey } from "../crypto";
|
||||||
import { bytesToHex } from "../utils";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
clearDecode,
|
AsymDecoder,
|
||||||
clearEncode,
|
AsymEncoder,
|
||||||
decryptAsymmetric,
|
decryptAsymmetric,
|
||||||
decryptSymmetric,
|
decryptSymmetric,
|
||||||
encryptAsymmetric,
|
encryptAsymmetric,
|
||||||
encryptSymmetric,
|
encryptSymmetric,
|
||||||
|
postCipher,
|
||||||
|
preCipher,
|
||||||
|
SymDecoder,
|
||||||
|
SymEncoder,
|
||||||
} from "./version_1";
|
} from "./version_1";
|
||||||
|
|
||||||
describe("Waku Message Version 1", function () {
|
const TestContentTopic = "/test/1/waku-message/utf8";
|
||||||
it("Sign & Recover", function () {
|
|
||||||
fc.assert(
|
describe("Waku Message version 1", function () {
|
||||||
|
it("Round trip binary encryption [asymmetric, no signature]", async function () {
|
||||||
|
await fc.assert(
|
||||||
fc.asyncProperty(
|
fc.asyncProperty(
|
||||||
fc.uint8Array(),
|
fc.uint8Array({ minLength: 1 }),
|
||||||
fc.uint8Array({ minLength: 32, maxLength: 32 }),
|
fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }),
|
||||||
async (message, privKey) => {
|
async (payload, privateKey) => {
|
||||||
const enc = await clearEncode(message, privKey);
|
const publicKey = getPublicKey(privateKey);
|
||||||
const res = clearDecode(enc.payload);
|
|
||||||
|
|
||||||
const pubKey = getPublicKey(privKey);
|
const encoder = new AsymEncoder(TestContentTopic, publicKey);
|
||||||
|
const bytes = await encoder.encode({ payload });
|
||||||
|
|
||||||
expect(res?.payload).deep.equal(
|
const decoder = new AsymDecoder(TestContentTopic, privateKey);
|
||||||
message,
|
const protoResult = await decoder.decodeProto(bytes!);
|
||||||
"Payload was not encrypted then decrypted correctly"
|
if (!protoResult) throw "Failed to proto decode";
|
||||||
);
|
const result = await decoder.decode(protoResult);
|
||||||
expect(res?.sig?.publicKey).deep.equal(
|
if (!result) throw "Failed to decode";
|
||||||
pubKey,
|
|
||||||
"signature Public key was not recovered from encrypted then decrypted signature"
|
expect(result.contentTopic).to.equal(TestContentTopic);
|
||||||
);
|
expect(result.version).to.equal(1);
|
||||||
expect(enc?.sig?.publicKey).deep.equal(
|
expect(result?.payload).to.deep.equal(payload);
|
||||||
pubKey,
|
expect(result.signature).to.be.undefined;
|
||||||
"Incorrect signature public key was returned when signing the payload"
|
expect(result.signaturePublicKey).to.be.undefined;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("R trip binary encryption [asymmetric, signature]", async function () {
|
||||||
|
this.timeout(4000);
|
||||||
|
|
||||||
|
await fc.assert(
|
||||||
|
fc.asyncProperty(
|
||||||
|
fc.uint8Array({ minLength: 1 }),
|
||||||
|
fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }),
|
||||||
|
fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }),
|
||||||
|
async (payload, alicePrivateKey, bobPrivateKey) => {
|
||||||
|
const alicePublicKey = getPublicKey(alicePrivateKey);
|
||||||
|
const bobPublicKey = getPublicKey(bobPrivateKey);
|
||||||
|
|
||||||
|
const encoder = new AsymEncoder(
|
||||||
|
TestContentTopic,
|
||||||
|
bobPublicKey,
|
||||||
|
alicePrivateKey
|
||||||
|
);
|
||||||
|
const bytes = await encoder.encode({ payload });
|
||||||
|
|
||||||
|
const decoder = new AsymDecoder(TestContentTopic, bobPrivateKey);
|
||||||
|
const protoResult = await decoder.decodeProto(bytes!);
|
||||||
|
if (!protoResult) throw "Failed to proto decode";
|
||||||
|
const result = await decoder.decode(protoResult);
|
||||||
|
if (!result) throw "Failed to decode";
|
||||||
|
|
||||||
|
expect(result.contentTopic).to.equal(TestContentTopic);
|
||||||
|
expect(result.version).to.equal(1);
|
||||||
|
expect(result?.payload).to.deep.equal(payload);
|
||||||
|
expect(result.signature).to.not.be.undefined;
|
||||||
|
expect(result.signaturePublicKey).to.deep.eq(alicePublicKey);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Round trip binary encryption [symmetric, no signature]", async function () {
|
||||||
|
await fc.assert(
|
||||||
|
fc.asyncProperty(
|
||||||
|
fc.uint8Array({ minLength: 1 }),
|
||||||
|
fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }),
|
||||||
|
async (payload, symKey) => {
|
||||||
|
const encoder = new SymEncoder(TestContentTopic, symKey);
|
||||||
|
const bytes = await encoder.encode({ payload });
|
||||||
|
|
||||||
|
const decoder = new SymDecoder(TestContentTopic, symKey);
|
||||||
|
const protoResult = await decoder.decodeProto(bytes!);
|
||||||
|
if (!protoResult) throw "Failed to proto decode";
|
||||||
|
const result = await decoder.decode(protoResult);
|
||||||
|
if (!result) throw "Failed to decode";
|
||||||
|
|
||||||
|
expect(result.contentTopic).to.equal(TestContentTopic);
|
||||||
|
expect(result.version).to.equal(1);
|
||||||
|
expect(result?.payload).to.deep.equal(payload);
|
||||||
|
expect(result.signature).to.be.undefined;
|
||||||
|
expect(result.signaturePublicKey).to.be.undefined;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Round trip binary encryption [symmetric, signature]", async function () {
|
||||||
|
await fc.assert(
|
||||||
|
fc.asyncProperty(
|
||||||
|
fc.uint8Array({ minLength: 1 }),
|
||||||
|
fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }),
|
||||||
|
fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }),
|
||||||
|
async (payload, sigPrivKey, symKey) => {
|
||||||
|
const sigPubKey = getPublicKey(sigPrivKey);
|
||||||
|
|
||||||
|
const encoder = new SymEncoder(TestContentTopic, symKey, sigPrivKey);
|
||||||
|
const bytes = await encoder.encode({ payload });
|
||||||
|
|
||||||
|
const decoder = new SymDecoder(TestContentTopic, symKey);
|
||||||
|
const protoResult = await decoder.decodeProto(bytes!);
|
||||||
|
if (!protoResult) throw "Failed to proto decode";
|
||||||
|
const result = await decoder.decode(protoResult);
|
||||||
|
if (!result) throw "Failed to decode";
|
||||||
|
|
||||||
|
expect(result.contentTopic).to.equal(TestContentTopic);
|
||||||
|
expect(result.version).to.equal(1);
|
||||||
|
expect(result?.payload).to.deep.equal(payload);
|
||||||
|
expect(result.signature).to.not.be.undefined;
|
||||||
|
expect(result.signaturePublicKey).to.deep.eq(sigPubKey);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Encryption helpers", () => {
|
||||||
it("Asymmetric encrypt & decrypt", async function () {
|
it("Asymmetric encrypt & decrypt", async function () {
|
||||||
await fc.assert(
|
await fc.assert(
|
||||||
fc.asyncProperty(
|
fc.asyncProperty(
|
||||||
|
@ -74,14 +168,41 @@ describe("Waku Message Version 1", function () {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Clear encode and decode", async function () {
|
it("pre and post cipher", async function () {
|
||||||
await fc.assert(
|
await fc.assert(
|
||||||
fc.asyncProperty(fc.uint8Array(), async (payload) => {
|
fc.asyncProperty(fc.uint8Array(), async (message) => {
|
||||||
const enc = await clearEncode(payload);
|
const enc = await preCipher(message);
|
||||||
const dec = clearDecode(enc.payload);
|
const res = postCipher(enc);
|
||||||
if (!dec?.payload) throw "payload missing";
|
|
||||||
expect(bytesToHex(dec?.payload)).to.eq(bytesToHex(payload));
|
expect(res?.payload).deep.equal(
|
||||||
|
message,
|
||||||
|
"Payload was not encrypted then decrypted correctly"
|
||||||
|
);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("Sign & Recover", async function () {
|
||||||
|
await fc.assert(
|
||||||
|
fc.asyncProperty(
|
||||||
|
fc.uint8Array(),
|
||||||
|
fc.uint8Array({ minLength: 32, maxLength: 32 }),
|
||||||
|
async (message, sigPrivKey) => {
|
||||||
|
const sigPubKey = getPublicKey(sigPrivKey);
|
||||||
|
|
||||||
|
const enc = await preCipher(message, sigPrivKey);
|
||||||
|
const res = postCipher(enc);
|
||||||
|
|
||||||
|
expect(res?.payload).deep.equal(
|
||||||
|
message,
|
||||||
|
"Payload was not encrypted then decrypted correctly"
|
||||||
|
);
|
||||||
|
expect(res?.sig?.publicKey).deep.equal(
|
||||||
|
sigPubKey,
|
||||||
|
"signature Public key was not recovered from encrypted then decrypted signature"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,100 +1,225 @@
|
||||||
import * as secp from "@noble/secp256k1";
|
import * as secp from "@noble/secp256k1";
|
||||||
|
import debug from "debug";
|
||||||
|
|
||||||
|
import * as proto from "../../proto/message";
|
||||||
import { keccak256, randomBytes, sign } from "../crypto";
|
import { keccak256, randomBytes, sign } from "../crypto";
|
||||||
|
import { Decoder, Encoder, Message, ProtoMessage } from "../interfaces";
|
||||||
import { concat, hexToBytes } from "../utils";
|
import { concat, hexToBytes } from "../utils";
|
||||||
|
|
||||||
import { Symmetric } from "./constants";
|
import { Symmetric } from "./constants";
|
||||||
import * as ecies from "./ecies";
|
import * as ecies from "./ecies";
|
||||||
import * as symmetric from "./symmetric";
|
import * as symmetric from "./symmetric";
|
||||||
|
import { DecoderV0, MessageV0 } from "./version_0";
|
||||||
|
|
||||||
|
const log = debug("waku:message:version-1");
|
||||||
|
|
||||||
const FlagsLength = 1;
|
const FlagsLength = 1;
|
||||||
const FlagMask = 3; // 0011
|
const FlagMask = 3; // 0011
|
||||||
const IsSignedMask = 4; // 0100
|
const IsSignedMask = 4; // 0100
|
||||||
const PaddingTarget = 256;
|
const PaddingTarget = 256;
|
||||||
const SignatureLength = 65;
|
const SignatureLength = 65;
|
||||||
|
const OneMillion = BigInt(1_000_000);
|
||||||
|
|
||||||
|
export const Version = 1;
|
||||||
|
|
||||||
export type Signature = {
|
export type Signature = {
|
||||||
signature: Uint8Array;
|
signature: Uint8Array;
|
||||||
publicKey: Uint8Array | undefined;
|
publicKey: Uint8Array | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
export class MessageV1 extends MessageV0 implements Message {
|
||||||
* Encode the payload pre-encryption.
|
private readonly _decodedPayload: Uint8Array;
|
||||||
*
|
|
||||||
* @internal
|
|
||||||
* @param messagePayload: The payload to include in the message
|
|
||||||
* @param sigPrivKey: If set, a signature using this private key is added.
|
|
||||||
* @returns The encoded payload, ready for encryption using {@link encryptAsymmetric}
|
|
||||||
* or {@link encryptSymmetric}.
|
|
||||||
*/
|
|
||||||
export async function clearEncode(
|
|
||||||
messagePayload: Uint8Array,
|
|
||||||
sigPrivKey?: Uint8Array
|
|
||||||
): Promise<{ payload: Uint8Array; sig?: Signature }> {
|
|
||||||
let envelope = new Uint8Array([0]); // No flags
|
|
||||||
envelope = addPayloadSizeField(envelope, messagePayload);
|
|
||||||
envelope = concat([envelope, messagePayload]);
|
|
||||||
|
|
||||||
// Calculate padding:
|
constructor(
|
||||||
let rawSize =
|
proto: proto.WakuMessage,
|
||||||
FlagsLength +
|
decodedPayload: Uint8Array,
|
||||||
computeSizeOfPayloadSizeField(messagePayload) +
|
public signature?: Uint8Array,
|
||||||
messagePayload.length;
|
public signaturePublicKey?: Uint8Array
|
||||||
|
) {
|
||||||
if (sigPrivKey) {
|
super(proto);
|
||||||
rawSize += SignatureLength;
|
this._decodedPayload = decodedPayload;
|
||||||
}
|
}
|
||||||
|
|
||||||
const remainder = rawSize % PaddingTarget;
|
get payload(): Uint8Array {
|
||||||
const paddingSize = PaddingTarget - remainder;
|
return this._decodedPayload;
|
||||||
const pad = randomBytes(paddingSize);
|
|
||||||
|
|
||||||
if (!validateDataIntegrity(pad, paddingSize)) {
|
|
||||||
throw new Error("failed to generate random padding of size " + paddingSize);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
envelope = concat([envelope, pad]);
|
|
||||||
let sig;
|
|
||||||
if (sigPrivKey) {
|
|
||||||
envelope[0] |= IsSignedMask;
|
|
||||||
const hash = keccak256(envelope);
|
|
||||||
const bytesSignature = await sign(hash, sigPrivKey);
|
|
||||||
envelope = concat([envelope, bytesSignature]);
|
|
||||||
sig = {
|
|
||||||
signature: bytesSignature,
|
|
||||||
publicKey: secp.getPublicKey(sigPrivKey, false),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return { payload: envelope, sig };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export class AsymEncoder implements Encoder {
|
||||||
* Decode a decrypted payload.
|
constructor(
|
||||||
*
|
public contentTopic: string,
|
||||||
* @internal
|
private publicKey: Uint8Array,
|
||||||
*/
|
private sigPrivKey?: Uint8Array
|
||||||
export function clearDecode(
|
) {}
|
||||||
message: Uint8Array
|
|
||||||
): { payload: Uint8Array; sig?: Signature } | undefined {
|
|
||||||
const sizeOfPayloadSizeField = getSizeOfPayloadSizeField(message);
|
|
||||||
if (sizeOfPayloadSizeField === 0) return;
|
|
||||||
|
|
||||||
const payloadSize = getPayloadSize(message, sizeOfPayloadSizeField);
|
async encode(message: Message): Promise<Uint8Array | undefined> {
|
||||||
const payloadStart = 1 + sizeOfPayloadSizeField;
|
const protoMessage = await this.encodeProto(message);
|
||||||
const payload = message.slice(payloadStart, payloadStart + payloadSize);
|
if (!protoMessage) return;
|
||||||
|
|
||||||
const isSigned = isMessageSigned(message);
|
return proto.WakuMessage.encode(protoMessage);
|
||||||
|
|
||||||
let sig;
|
|
||||||
if (isSigned) {
|
|
||||||
const signature = getSignature(message);
|
|
||||||
const hash = getHash(message, isSigned);
|
|
||||||
const publicKey = ecRecoverPubKey(hash, signature);
|
|
||||||
sig = { signature, publicKey };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { payload, sig };
|
async encodeProto(message: Message): Promise<ProtoMessage | undefined> {
|
||||||
|
const timestamp = message.timestamp ?? new Date();
|
||||||
|
if (!message.payload) {
|
||||||
|
log("No payload to encrypt, skipping: ", message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const preparedPayload = await preCipher(message.payload, this.sigPrivKey);
|
||||||
|
|
||||||
|
const payload = await encryptAsymmetric(preparedPayload, this.publicKey);
|
||||||
|
|
||||||
|
return {
|
||||||
|
payload,
|
||||||
|
version: Version,
|
||||||
|
contentTopic: this.contentTopic,
|
||||||
|
timestamp: BigInt(timestamp.valueOf()) * OneMillion,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SymEncoder implements Encoder {
|
||||||
|
constructor(
|
||||||
|
public contentTopic: string,
|
||||||
|
private symKey: Uint8Array,
|
||||||
|
private sigPrivKey?: Uint8Array
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async encode(message: Message): Promise<Uint8Array | undefined> {
|
||||||
|
const protoMessage = await this.encodeProto(message);
|
||||||
|
if (!protoMessage) return;
|
||||||
|
|
||||||
|
return proto.WakuMessage.encode(protoMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
async encodeProto(message: Message): Promise<ProtoMessage | undefined> {
|
||||||
|
const timestamp = message.timestamp ?? new Date();
|
||||||
|
if (!message.payload) {
|
||||||
|
log("No payload to encrypt, skipping: ", message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const preparedPayload = await preCipher(message.payload, this.sigPrivKey);
|
||||||
|
|
||||||
|
const payload = await encryptSymmetric(preparedPayload, this.symKey);
|
||||||
|
return {
|
||||||
|
payload,
|
||||||
|
version: Version,
|
||||||
|
contentTopic: this.contentTopic,
|
||||||
|
timestamp: BigInt(timestamp.valueOf()) * OneMillion,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AsymDecoder extends DecoderV0 implements Decoder {
|
||||||
|
constructor(contentTopic: string, private privateKey: Uint8Array) {
|
||||||
|
super(contentTopic);
|
||||||
|
}
|
||||||
|
|
||||||
|
async decode(protoMessage: ProtoMessage): Promise<MessageV1 | undefined> {
|
||||||
|
const cipherPayload = protoMessage.payload;
|
||||||
|
|
||||||
|
if (protoMessage.version !== Version) {
|
||||||
|
log(
|
||||||
|
"Failed to decrypt due to incorrect version, expected:",
|
||||||
|
Version,
|
||||||
|
", actual:",
|
||||||
|
protoMessage.version
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let payload;
|
||||||
|
if (!cipherPayload) {
|
||||||
|
log(`No payload to decrypt for contentTopic ${this.contentTopic}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
payload = await decryptAsymmetric(cipherPayload, this.privateKey);
|
||||||
|
} catch (e) {
|
||||||
|
log(
|
||||||
|
`Failed to decrypt message using asymmetric decryption for contentTopic: ${this.contentTopic}`,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!payload) {
|
||||||
|
log(`Failed to decrypt payload for contentTopic ${this.contentTopic}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await postCipher(payload);
|
||||||
|
|
||||||
|
if (!res) {
|
||||||
|
log(`Failed to decode payload for contentTopic ${this.contentTopic}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log("Message decrypted", protoMessage);
|
||||||
|
return new MessageV1(
|
||||||
|
protoMessage,
|
||||||
|
res.payload,
|
||||||
|
res.sig?.signature,
|
||||||
|
res.sig?.publicKey
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SymDecoder extends DecoderV0 implements Decoder {
|
||||||
|
constructor(contentTopic: string, private symKey: Uint8Array) {
|
||||||
|
super(contentTopic);
|
||||||
|
}
|
||||||
|
|
||||||
|
async decode(protoMessage: ProtoMessage): Promise<MessageV1 | undefined> {
|
||||||
|
const cipherPayload = protoMessage.payload;
|
||||||
|
|
||||||
|
if (protoMessage.version !== Version) {
|
||||||
|
log(
|
||||||
|
"Failed to decrypt due to incorrect version, expected:",
|
||||||
|
Version,
|
||||||
|
", actual:",
|
||||||
|
protoMessage.version
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let payload;
|
||||||
|
if (!cipherPayload) {
|
||||||
|
log(`No payload to decrypt for contentTopic ${this.contentTopic}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
payload = await decryptSymmetric(cipherPayload, this.symKey);
|
||||||
|
} catch (e) {
|
||||||
|
log(
|
||||||
|
`Failed to decrypt message using asymmetric decryption for contentTopic: ${this.contentTopic}`,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!payload) {
|
||||||
|
log(`Failed to decrypt payload for contentTopic ${this.contentTopic}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await postCipher(payload);
|
||||||
|
|
||||||
|
if (!res) {
|
||||||
|
log(`Failed to decode payload for contentTopic ${this.contentTopic}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log("Message decrypted", protoMessage);
|
||||||
|
return new MessageV1(
|
||||||
|
protoMessage,
|
||||||
|
res.payload,
|
||||||
|
res.sig?.signature,
|
||||||
|
res.sig?.publicKey
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSizeOfPayloadSizeField(message: Uint8Array): number {
|
function getSizeOfPayloadSizeField(message: Uint8Array): number {
|
||||||
|
@ -246,12 +371,77 @@ function ecRecoverPubKey(
|
||||||
const recovery = recoveryDataView.getUint8(0);
|
const recovery = recoveryDataView.getUint8(0);
|
||||||
const _signature = secp.Signature.fromCompact(signature.slice(0, 64));
|
const _signature = secp.Signature.fromCompact(signature.slice(0, 64));
|
||||||
|
|
||||||
return secp.recoverPublicKey(
|
return secp.recoverPublicKey(messageHash, _signature, recovery, false);
|
||||||
messageHash,
|
}
|
||||||
_signature,
|
|
||||||
recovery,
|
/**
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
* Prepare the payload pre-encryption.
|
||||||
// @ts-ignore: compressed: false
|
*
|
||||||
false
|
* @internal
|
||||||
);
|
* @returns The encoded payload, ready for encryption using {@link encryptAsymmetric}
|
||||||
|
* or {@link encryptSymmetric}.
|
||||||
|
*/
|
||||||
|
export async function preCipher(
|
||||||
|
messagePayload: Uint8Array,
|
||||||
|
sigPrivKey?: Uint8Array
|
||||||
|
): Promise<Uint8Array> {
|
||||||
|
let envelope = new Uint8Array([0]); // No flags
|
||||||
|
envelope = addPayloadSizeField(envelope, messagePayload);
|
||||||
|
envelope = concat([envelope, messagePayload]);
|
||||||
|
|
||||||
|
// Calculate padding:
|
||||||
|
let rawSize =
|
||||||
|
FlagsLength +
|
||||||
|
computeSizeOfPayloadSizeField(messagePayload) +
|
||||||
|
messagePayload.length;
|
||||||
|
|
||||||
|
if (sigPrivKey) {
|
||||||
|
rawSize += SignatureLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
const remainder = rawSize % PaddingTarget;
|
||||||
|
const paddingSize = PaddingTarget - remainder;
|
||||||
|
const pad = randomBytes(paddingSize);
|
||||||
|
|
||||||
|
if (!validateDataIntegrity(pad, paddingSize)) {
|
||||||
|
throw new Error("failed to generate random padding of size " + paddingSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
envelope = concat([envelope, pad]);
|
||||||
|
if (sigPrivKey) {
|
||||||
|
envelope[0] |= IsSignedMask;
|
||||||
|
const hash = keccak256(envelope);
|
||||||
|
const bytesSignature = await sign(hash, sigPrivKey);
|
||||||
|
envelope = concat([envelope, bytesSignature]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return envelope;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode a decrypted payload.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export function postCipher(
|
||||||
|
message: Uint8Array
|
||||||
|
): { payload: Uint8Array; sig?: Signature } | undefined {
|
||||||
|
const sizeOfPayloadSizeField = getSizeOfPayloadSizeField(message);
|
||||||
|
if (sizeOfPayloadSizeField === 0) return;
|
||||||
|
|
||||||
|
const payloadSize = getPayloadSize(message, sizeOfPayloadSizeField);
|
||||||
|
const payloadStart = 1 + sizeOfPayloadSizeField;
|
||||||
|
const payload = message.slice(payloadStart, payloadStart + payloadSize);
|
||||||
|
|
||||||
|
const isSigned = isMessageSigned(message);
|
||||||
|
|
||||||
|
let sig;
|
||||||
|
if (isSigned) {
|
||||||
|
const signature = getSignature(message);
|
||||||
|
const hash = getHash(message, isSigned);
|
||||||
|
const publicKey = ecRecoverPubKey(hash, signature);
|
||||||
|
sig = { signature, publicKey };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { payload, sig };
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,15 +18,23 @@ import {
|
||||||
generateSymmetricKey,
|
generateSymmetricKey,
|
||||||
getPublicKey,
|
getPublicKey,
|
||||||
} from "../crypto";
|
} from "../crypto";
|
||||||
import type { WakuPrivacy } from "../interfaces";
|
import type { Message, WakuPrivacy } from "../interfaces";
|
||||||
import { bytesToUtf8, utf8ToBytes } from "../utils";
|
import { bytesToUtf8, utf8ToBytes } from "../utils";
|
||||||
import { waitForRemotePeer } from "../wait_for_remote_peer";
|
import { waitForRemotePeer } from "../wait_for_remote_peer";
|
||||||
import { Protocols } from "../waku";
|
import { Protocols } from "../waku";
|
||||||
import { DecryptionMethod, WakuMessage } from "../waku_message";
|
import { DecoderV0, EncoderV0, MessageV0 } from "../waku_message/version_0.js";
|
||||||
|
import {
|
||||||
|
AsymDecoder,
|
||||||
|
AsymEncoder,
|
||||||
|
SymDecoder,
|
||||||
|
SymEncoder,
|
||||||
|
} from "../waku_message/version_1.js";
|
||||||
|
|
||||||
const log = debug("waku:test");
|
const log = debug("waku:test");
|
||||||
|
|
||||||
const TestContentTopic = "/test/1/waku-relay/utf8";
|
const TestContentTopic = "/test/1/waku-relay/utf8";
|
||||||
|
const TestEncoder = new EncoderV0(TestContentTopic);
|
||||||
|
const TestDecoder = new DecoderV0(TestContentTopic);
|
||||||
|
|
||||||
describe("Waku Relay [node only]", () => {
|
describe("Waku Relay [node only]", () => {
|
||||||
// Node needed as we don't have a way to connect 2 js waku
|
// Node needed as we don't have a way to connect 2 js waku
|
||||||
|
@ -100,27 +108,21 @@ describe("Waku Relay [node only]", () => {
|
||||||
|
|
||||||
const messageText = "JS to JS communication works";
|
const messageText = "JS to JS communication works";
|
||||||
const messageTimestamp = new Date("1995-12-17T03:24:00");
|
const messageTimestamp = new Date("1995-12-17T03:24:00");
|
||||||
const message = await WakuMessage.fromUtf8String(
|
const message = {
|
||||||
messageText,
|
payload: utf8ToBytes(messageText),
|
||||||
TestContentTopic,
|
|
||||||
{
|
|
||||||
timestamp: messageTimestamp,
|
timestamp: messageTimestamp,
|
||||||
}
|
};
|
||||||
);
|
|
||||||
|
|
||||||
const receivedMsgPromise: Promise<WakuMessage> = new Promise(
|
const receivedMsgPromise: Promise<Message> = new Promise((resolve) => {
|
||||||
(resolve) => {
|
waku2.relay.addObserver(TestDecoder, resolve);
|
||||||
waku2.relay.addObserver(resolve);
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
await waku1.relay.send(message);
|
await waku1.relay.send(TestEncoder, message);
|
||||||
|
|
||||||
const receivedMsg = await receivedMsgPromise;
|
const receivedMsg = await receivedMsgPromise;
|
||||||
|
|
||||||
expect(receivedMsg.contentTopic).to.eq(message.contentTopic);
|
expect(receivedMsg.contentTopic).to.eq(TestContentTopic);
|
||||||
expect(receivedMsg.version).to.eq(message.version);
|
expect(bytesToUtf8(receivedMsg.payload!)).to.eq(messageText);
|
||||||
expect(receivedMsg.payloadAsUtf8).to.eq(messageText);
|
|
||||||
expect(receivedMsg.timestamp?.valueOf()).to.eq(
|
expect(receivedMsg.timestamp?.valueOf()).to.eq(
|
||||||
messageTimestamp.valueOf()
|
messageTimestamp.valueOf()
|
||||||
);
|
);
|
||||||
|
@ -131,108 +133,83 @@ describe("Waku Relay [node only]", () => {
|
||||||
|
|
||||||
const fooMessageText = "Published on content topic foo";
|
const fooMessageText = "Published on content topic foo";
|
||||||
const barMessageText = "Published on content topic bar";
|
const barMessageText = "Published on content topic bar";
|
||||||
const fooMessage = await WakuMessage.fromUtf8String(
|
|
||||||
fooMessageText,
|
|
||||||
"foo"
|
|
||||||
);
|
|
||||||
const barMessage = await WakuMessage.fromUtf8String(
|
|
||||||
barMessageText,
|
|
||||||
"bar"
|
|
||||||
);
|
|
||||||
|
|
||||||
const receivedBarMsgPromise: Promise<WakuMessage> = new Promise(
|
const fooContentTopic = "foo";
|
||||||
(resolve) => {
|
const barContentTopic = "bar";
|
||||||
waku2.relay.addObserver(resolve, ["bar"]);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const allMessages: WakuMessage[] = [];
|
const fooEncoder = new EncoderV0(fooContentTopic);
|
||||||
waku2.relay.addObserver((wakuMsg) => {
|
const barEncoder = new EncoderV0(barContentTopic);
|
||||||
allMessages.push(wakuMsg);
|
|
||||||
|
const fooDecoder = new DecoderV0(fooContentTopic);
|
||||||
|
const barDecoder = new DecoderV0(barContentTopic);
|
||||||
|
|
||||||
|
const fooMessages: Message[] = [];
|
||||||
|
waku2.relay.addObserver(fooDecoder, (msg) => {
|
||||||
|
fooMessages.push(msg);
|
||||||
});
|
});
|
||||||
|
|
||||||
await waku1.relay.send(fooMessage);
|
const barMessages: Message[] = [];
|
||||||
await waku1.relay.send(barMessage);
|
waku2.relay.addObserver(barDecoder, (msg) => {
|
||||||
|
barMessages.push(msg);
|
||||||
|
});
|
||||||
|
|
||||||
const receivedBarMsg = await receivedBarMsgPromise;
|
await waku1.relay.send(barEncoder, {
|
||||||
|
payload: utf8ToBytes(barMessageText),
|
||||||
|
});
|
||||||
|
await waku1.relay.send(fooEncoder, {
|
||||||
|
payload: utf8ToBytes(fooMessageText),
|
||||||
|
});
|
||||||
|
|
||||||
expect(receivedBarMsg.contentTopic).to.eq(barMessage.contentTopic);
|
await delay(200);
|
||||||
expect(receivedBarMsg.version).to.eq(barMessage.version);
|
|
||||||
expect(receivedBarMsg.payloadAsUtf8).to.eq(barMessageText);
|
expect(fooMessages[0].contentTopic).to.eq(fooContentTopic);
|
||||||
expect(allMessages.length).to.eq(2);
|
expect(bytesToUtf8(fooMessages[0].payload!)).to.eq(fooMessageText);
|
||||||
expect(allMessages[0].contentTopic).to.eq(fooMessage.contentTopic);
|
|
||||||
expect(allMessages[0].version).to.eq(fooMessage.version);
|
expect(barMessages[0].contentTopic).to.eq(barContentTopic);
|
||||||
expect(allMessages[0].payloadAsUtf8).to.eq(fooMessageText);
|
expect(bytesToUtf8(barMessages[0].payload!)).to.eq(barMessageText);
|
||||||
expect(allMessages[1].contentTopic).to.eq(barMessage.contentTopic);
|
|
||||||
expect(allMessages[1].version).to.eq(barMessage.version);
|
expect(fooMessages.length).to.eq(1);
|
||||||
expect(allMessages[1].payloadAsUtf8).to.eq(barMessageText);
|
expect(barMessages.length).to.eq(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Decrypt messages", async function () {
|
it("Decrypt messages", async function () {
|
||||||
this.timeout(10000);
|
this.timeout(10000);
|
||||||
|
|
||||||
const encryptedAsymmetricMessageText =
|
const asymText = "This message is encrypted using asymmetric";
|
||||||
"This message is encrypted using asymmetric";
|
const asymTopic = "/test/1/asymmetric/proto";
|
||||||
const encryptedAsymmetricContentTopic = "/test/1/asymmetric/proto";
|
const symText = "This message is encrypted using symmetric encryption";
|
||||||
const encryptedSymmetricMessageText =
|
const symTopic = "/test/1/symmetric/proto";
|
||||||
"This message is encrypted using symmetric encryption";
|
|
||||||
const encryptedSymmetricContentTopic = "/test/1/symmetric/proto";
|
|
||||||
|
|
||||||
const privateKey = generatePrivateKey();
|
const privateKey = generatePrivateKey();
|
||||||
const symKey = generateSymmetricKey();
|
const symKey = generateSymmetricKey();
|
||||||
const publicKey = getPublicKey(privateKey);
|
const publicKey = getPublicKey(privateKey);
|
||||||
|
|
||||||
const [encryptedAsymmetricMessage, encryptedSymmetricMessage] =
|
const asymEncoder = new AsymEncoder(asymTopic, publicKey);
|
||||||
await Promise.all([
|
const symEncoder = new SymEncoder(symTopic, symKey);
|
||||||
WakuMessage.fromUtf8String(
|
|
||||||
encryptedAsymmetricMessageText,
|
|
||||||
encryptedAsymmetricContentTopic,
|
|
||||||
{
|
|
||||||
encPublicKey: publicKey,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
WakuMessage.fromUtf8String(
|
|
||||||
encryptedSymmetricMessageText,
|
|
||||||
encryptedSymmetricContentTopic,
|
|
||||||
{
|
|
||||||
symKey: symKey,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
|
|
||||||
waku2.addDecryptionKey(privateKey, {
|
const asymDecoder = new AsymDecoder(asymTopic, privateKey);
|
||||||
contentTopics: [encryptedAsymmetricContentTopic],
|
const symDecoder = new SymDecoder(symTopic, symKey);
|
||||||
method: DecryptionMethod.Asymmetric,
|
|
||||||
});
|
|
||||||
waku2.addDecryptionKey(symKey, {
|
|
||||||
contentTopics: [encryptedSymmetricContentTopic],
|
|
||||||
method: DecryptionMethod.Symmetric,
|
|
||||||
});
|
|
||||||
|
|
||||||
const msgs: WakuMessage[] = [];
|
const msgs: Message[] = [];
|
||||||
waku2.relay.addObserver((wakuMsg) => {
|
waku2.relay.addObserver(asymDecoder, (wakuMsg) => {
|
||||||
|
msgs.push(wakuMsg);
|
||||||
|
});
|
||||||
|
waku2.relay.addObserver(symDecoder, (wakuMsg) => {
|
||||||
msgs.push(wakuMsg);
|
msgs.push(wakuMsg);
|
||||||
});
|
});
|
||||||
|
|
||||||
await waku1.relay.send(encryptedAsymmetricMessage);
|
await waku1.relay.send(asymEncoder, { payload: utf8ToBytes(asymText) });
|
||||||
await delay(200);
|
await delay(200);
|
||||||
await waku1.relay.send(encryptedSymmetricMessage);
|
await waku1.relay.send(symEncoder, { payload: utf8ToBytes(symText) });
|
||||||
|
|
||||||
while (msgs.length < 2) {
|
while (msgs.length < 2) {
|
||||||
await delay(200);
|
await delay(200);
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(msgs.length).to.eq(2);
|
expect(msgs[0].contentTopic).to.eq(asymTopic);
|
||||||
expect(msgs[0].contentTopic).to.eq(
|
expect(bytesToUtf8(msgs[0].payload!)).to.eq(asymText);
|
||||||
encryptedAsymmetricMessage.contentTopic
|
expect(msgs[1].contentTopic).to.eq(symTopic);
|
||||||
);
|
expect(bytesToUtf8(msgs[1].payload!)).to.eq(symText);
|
||||||
expect(msgs[0].version).to.eq(encryptedAsymmetricMessage.version);
|
|
||||||
expect(msgs[0].payloadAsUtf8).to.eq(encryptedAsymmetricMessageText);
|
|
||||||
expect(msgs[1].contentTopic).to.eq(
|
|
||||||
encryptedSymmetricMessage.contentTopic
|
|
||||||
);
|
|
||||||
expect(msgs[1].version).to.eq(encryptedSymmetricMessage.version);
|
|
||||||
expect(msgs[1].payloadAsUtf8).to.eq(encryptedSymmetricMessageText);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Delete observer", async function () {
|
it("Delete observer", async function () {
|
||||||
|
@ -240,22 +217,23 @@ describe("Waku Relay [node only]", () => {
|
||||||
|
|
||||||
const messageText =
|
const messageText =
|
||||||
"Published on content topic with added then deleted observer";
|
"Published on content topic with added then deleted observer";
|
||||||
const message = await WakuMessage.fromUtf8String(
|
|
||||||
messageText,
|
const contentTopic = "added-then-deleted-observer";
|
||||||
"added-then-deleted-observer"
|
|
||||||
);
|
|
||||||
|
|
||||||
// The promise **fails** if we receive a message on this observer.
|
// The promise **fails** if we receive a message on this observer.
|
||||||
const receivedMsgPromise: Promise<WakuMessage> = new Promise(
|
const receivedMsgPromise: Promise<Message> = new Promise(
|
||||||
(resolve, reject) => {
|
(resolve, reject) => {
|
||||||
const deleteObserver = waku2.relay.addObserver(reject, [
|
const deleteObserver = waku2.relay.addObserver(
|
||||||
"added-then-deleted-observer",
|
new DecoderV0(contentTopic),
|
||||||
]);
|
reject
|
||||||
|
);
|
||||||
deleteObserver();
|
deleteObserver();
|
||||||
setTimeout(resolve, 500);
|
setTimeout(resolve, 500);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
await waku1.relay.send(message);
|
await waku1.relay.send(new EncoderV0(contentTopic), {
|
||||||
|
payload: utf8ToBytes(messageText),
|
||||||
|
});
|
||||||
|
|
||||||
await receivedMsgPromise;
|
await receivedMsgPromise;
|
||||||
// If it does not throw then we are good.
|
// If it does not throw then we are good.
|
||||||
|
@ -312,32 +290,30 @@ describe("Waku Relay [node only]", () => {
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const messageText = "Communicating using a custom pubsub topic";
|
const messageText = "Communicating using a custom pubsub topic";
|
||||||
const message = await WakuMessage.fromUtf8String(
|
|
||||||
messageText,
|
|
||||||
TestContentTopic
|
|
||||||
);
|
|
||||||
|
|
||||||
const waku2ReceivedMsgPromise: Promise<WakuMessage> = new Promise(
|
const waku2ReceivedMsgPromise: Promise<Message> = new Promise(
|
||||||
(resolve) => {
|
(resolve) => {
|
||||||
waku2.relay.addObserver(resolve);
|
waku2.relay.addObserver(TestDecoder, resolve);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// The promise **fails** if we receive a message on the default
|
// The promise **fails** if we receive a message on the default
|
||||||
// pubsub topic.
|
// pubsub topic.
|
||||||
const waku3NoMsgPromise: Promise<WakuMessage> = new Promise(
|
const waku3NoMsgPromise: Promise<Message> = new Promise(
|
||||||
(resolve, reject) => {
|
(resolve, reject) => {
|
||||||
waku3.relay.addObserver(reject);
|
waku3.relay.addObserver(TestDecoder, reject);
|
||||||
setTimeout(resolve, 1000);
|
setTimeout(resolve, 1000);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
await waku1.relay.send(message);
|
await waku1.relay.send(TestEncoder, {
|
||||||
|
payload: utf8ToBytes(messageText),
|
||||||
|
});
|
||||||
|
|
||||||
const waku2ReceivedMsg = await waku2ReceivedMsgPromise;
|
const waku2ReceivedMsg = await waku2ReceivedMsgPromise;
|
||||||
await waku3NoMsgPromise;
|
await waku3NoMsgPromise;
|
||||||
|
|
||||||
expect(waku2ReceivedMsg.payloadAsUtf8).to.eq(messageText);
|
expect(bytesToUtf8(waku2ReceivedMsg.payload!)).to.eq(messageText);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -382,12 +358,7 @@ describe("Waku Relay [node only]", () => {
|
||||||
this.timeout(30000);
|
this.timeout(30000);
|
||||||
|
|
||||||
const messageText = "This is a message";
|
const messageText = "This is a message";
|
||||||
const message = await WakuMessage.fromUtf8String(
|
await waku.relay.send(TestEncoder, { payload: utf8ToBytes(messageText) });
|
||||||
messageText,
|
|
||||||
TestContentTopic
|
|
||||||
);
|
|
||||||
await delay(1000);
|
|
||||||
await waku.relay.send(message);
|
|
||||||
|
|
||||||
let msgs: MessageRpcResponse[] = [];
|
let msgs: MessageRpcResponse[] = [];
|
||||||
|
|
||||||
|
@ -397,8 +368,8 @@ describe("Waku Relay [node only]", () => {
|
||||||
msgs = await nwaku.messages();
|
msgs = await nwaku.messages();
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(msgs[0].contentTopic).to.equal(message.contentTopic);
|
expect(msgs[0].contentTopic).to.equal(TestContentTopic);
|
||||||
expect(msgs[0].version).to.equal(message.version);
|
expect(msgs[0].version).to.equal(0);
|
||||||
expect(bytesToUtf8(new Uint8Array(msgs[0].payload))).to.equal(
|
expect(bytesToUtf8(new Uint8Array(msgs[0].payload))).to.equal(
|
||||||
messageText
|
messageText
|
||||||
);
|
);
|
||||||
|
@ -408,24 +379,25 @@ describe("Waku Relay [node only]", () => {
|
||||||
await delay(200);
|
await delay(200);
|
||||||
|
|
||||||
const messageText = "Here is another message.";
|
const messageText = "Here is another message.";
|
||||||
const message = {
|
|
||||||
payload: utf8ToBytes(messageText),
|
|
||||||
contentTopic: TestContentTopic,
|
|
||||||
};
|
|
||||||
|
|
||||||
const receivedMsgPromise: Promise<WakuMessage> = new Promise(
|
const receivedMsgPromise: Promise<MessageV0> = new Promise((resolve) => {
|
||||||
(resolve) => {
|
waku.relay.addObserver(TestDecoder, (msg) =>
|
||||||
waku.relay.addObserver(resolve);
|
resolve(msg as unknown as MessageV0)
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
||||||
await nwaku.sendMessage(Nwaku.toMessageRpcQuery(message));
|
await nwaku.sendMessage(
|
||||||
|
Nwaku.toMessageRpcQuery({
|
||||||
|
contentTopic: TestContentTopic,
|
||||||
|
payload: utf8ToBytes(messageText),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const receivedMsg = await receivedMsgPromise;
|
const receivedMsg = await receivedMsgPromise;
|
||||||
|
|
||||||
expect(receivedMsg.contentTopic).to.eq(message.contentTopic);
|
expect(receivedMsg.contentTopic).to.eq(TestContentTopic);
|
||||||
expect(receivedMsg.version).to.eq(0);
|
expect(receivedMsg.version).to.eq(0);
|
||||||
expect(receivedMsg.payloadAsUtf8).to.eq(messageText);
|
expect(bytesToUtf8(receivedMsg.payload!)).to.eq(messageText);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe.skip("Two nodes connected to nwaku", function () {
|
describe.skip("Two nodes connected to nwaku", function () {
|
||||||
|
@ -475,22 +447,19 @@ describe("Waku Relay [node only]", () => {
|
||||||
expect(waku2.libp2p.peerStore.has(waku1.libp2p.peerId)).to.be.false;
|
expect(waku2.libp2p.peerStore.has(waku1.libp2p.peerId)).to.be.false;
|
||||||
|
|
||||||
const msgStr = "Hello there!";
|
const msgStr = "Hello there!";
|
||||||
const message = await WakuMessage.fromUtf8String(
|
const message = { payload: utf8ToBytes(msgStr) };
|
||||||
msgStr,
|
|
||||||
TestContentTopic
|
|
||||||
);
|
|
||||||
|
|
||||||
const waku2ReceivedMsgPromise: Promise<WakuMessage> = new Promise(
|
const waku2ReceivedMsgPromise: Promise<Message> = new Promise(
|
||||||
(resolve) => {
|
(resolve) => {
|
||||||
waku2.relay.addObserver(resolve);
|
waku2.relay.addObserver(TestDecoder, resolve);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
await waku1.relay.send(message);
|
await waku1.relay.send(TestEncoder, message);
|
||||||
console.log("Waiting for message");
|
console.log("Waiting for message");
|
||||||
const waku2ReceivedMsg = await waku2ReceivedMsgPromise;
|
const waku2ReceivedMsg = await waku2ReceivedMsgPromise;
|
||||||
|
|
||||||
expect(waku2ReceivedMsg.payloadAsUtf8).to.eq(msgStr);
|
expect(waku2ReceivedMsg.payload).to.eq(msgStr);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,16 +8,20 @@ import {
|
||||||
TopicStr,
|
TopicStr,
|
||||||
} from "@chainsafe/libp2p-gossipsub/dist/src/types";
|
} from "@chainsafe/libp2p-gossipsub/dist/src/types";
|
||||||
import { SignaturePolicy } from "@chainsafe/libp2p-gossipsub/types";
|
import { SignaturePolicy } from "@chainsafe/libp2p-gossipsub/types";
|
||||||
|
import { PublishResult } from "@libp2p/interface-pubsub";
|
||||||
import debug from "debug";
|
import debug from "debug";
|
||||||
|
|
||||||
import { DefaultPubSubTopic } from "../constants";
|
import { DefaultPubSubTopic } from "../constants";
|
||||||
import { hexToBytes } from "../utils";
|
import { Decoder, Encoder, Message } from "../interfaces";
|
||||||
import { DecryptionMethod, WakuMessage } from "../waku_message";
|
import { pushOrInitMapSet } from "../push_or_init_map";
|
||||||
|
import { DecoderV0 } from "../waku_message/version_0";
|
||||||
|
|
||||||
import * as constants from "./constants";
|
import * as constants from "./constants";
|
||||||
|
|
||||||
const log = debug("waku:relay");
|
const log = debug("waku:relay");
|
||||||
|
|
||||||
|
export type Callback = (msg: Message) => void;
|
||||||
|
|
||||||
export type CreateOptions = {
|
export type CreateOptions = {
|
||||||
/**
|
/**
|
||||||
* The PubSub Topic to use. Defaults to {@link DefaultPubSubTopic}.
|
* The PubSub Topic to use. Defaults to {@link DefaultPubSubTopic}.
|
||||||
|
@ -33,7 +37,6 @@ export type CreateOptions = {
|
||||||
* @default {@link DefaultPubSubTopic}
|
* @default {@link DefaultPubSubTopic}
|
||||||
*/
|
*/
|
||||||
pubSubTopic?: string;
|
pubSubTopic?: string;
|
||||||
decryptionKeys?: Array<Uint8Array | string>;
|
|
||||||
} & GossipsubOpts;
|
} & GossipsubOpts;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -46,18 +49,11 @@ export class WakuRelay extends GossipSub {
|
||||||
pubSubTopic: string;
|
pubSubTopic: string;
|
||||||
public static multicodec: string = constants.RelayCodecs[0];
|
public static multicodec: string = constants.RelayCodecs[0];
|
||||||
|
|
||||||
public decryptionKeys: Map<
|
|
||||||
Uint8Array,
|
|
||||||
{ method?: DecryptionMethod; contentTopics?: string[] }
|
|
||||||
>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* observers called when receiving new message.
|
* observers called when receiving new message.
|
||||||
* Observers under key `""` are always called.
|
* Observers under key `""` are always called.
|
||||||
*/
|
*/
|
||||||
public observers: {
|
public observers: Map<string, Set<{ decoder: Decoder; callback: Callback }>>;
|
||||||
[contentTopic: string]: Set<(message: WakuMessage) => void>;
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(options?: Partial<CreateOptions>) {
|
constructor(options?: Partial<CreateOptions>) {
|
||||||
options = Object.assign(options ?? {}, {
|
options = Object.assign(options ?? {}, {
|
||||||
|
@ -68,14 +64,9 @@ export class WakuRelay extends GossipSub {
|
||||||
super(options);
|
super(options);
|
||||||
this.multicodecs = constants.RelayCodecs;
|
this.multicodecs = constants.RelayCodecs;
|
||||||
|
|
||||||
this.observers = {};
|
this.observers = new Map();
|
||||||
this.decryptionKeys = new Map();
|
|
||||||
|
|
||||||
this.pubSubTopic = options?.pubSubTopic ?? DefaultPubSubTopic;
|
this.pubSubTopic = options?.pubSubTopic ?? DefaultPubSubTopic;
|
||||||
|
|
||||||
options?.decryptionKeys?.forEach((key) => {
|
|
||||||
this.addDecryptionKey(key);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -92,102 +83,39 @@ export class WakuRelay extends GossipSub {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send Waku message.
|
* Send Waku message.
|
||||||
*
|
|
||||||
* @param {WakuMessage} message
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
*/
|
||||||
public async send(message: WakuMessage): Promise<void> {
|
public async send(
|
||||||
const msg = message.encode();
|
encoder: Encoder,
|
||||||
await this.publish(this.pubSubTopic, msg);
|
message: Message
|
||||||
|
): Promise<PublishResult> {
|
||||||
|
const msg = await encoder.encode(message);
|
||||||
|
if (!msg) {
|
||||||
|
log("Failed to encode message, aborting publish");
|
||||||
|
return { recipients: [] };
|
||||||
|
}
|
||||||
|
return this.publish(this.pubSubTopic, msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a decryption key to attempt decryption of received messages.
|
* Add an observer and associated Decoder to process incoming messages on a given content topic.
|
||||||
* This can either be a private key for asymmetric encryption or a symmetric
|
|
||||||
* key. `WakuRelay` will attempt to decrypt messages using both methods.
|
|
||||||
*
|
*
|
||||||
* Strings must be in hex format.
|
|
||||||
*/
|
|
||||||
addDecryptionKey(
|
|
||||||
key: Uint8Array | string,
|
|
||||||
options?: { method?: DecryptionMethod; contentTopics?: string[] }
|
|
||||||
): void {
|
|
||||||
this.decryptionKeys.set(hexToBytes(key), options ?? {});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a decryption key that was used to attempt decryption of received
|
|
||||||
* messages.
|
|
||||||
*
|
|
||||||
* Strings must be in hex format.
|
|
||||||
*/
|
|
||||||
deleteDecryptionKey(key: Uint8Array | string): void {
|
|
||||||
this.decryptionKeys.delete(hexToBytes(key));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register an observer of new messages received via waku relay
|
|
||||||
*
|
|
||||||
* @param callback called when a new message is received via waku relay
|
|
||||||
* @param contentTopics Content Topics for which the callback with be called,
|
|
||||||
* all of them if undefined, [] or ["",..] is passed.
|
|
||||||
* @returns Function to delete the observer
|
* @returns Function to delete the observer
|
||||||
*/
|
*/
|
||||||
addObserver(
|
addObserver(decoder: Decoder, callback: Callback): () => void {
|
||||||
callback: (message: WakuMessage) => void,
|
const observer = {
|
||||||
contentTopics: string[] = []
|
decoder,
|
||||||
): () => void {
|
callback,
|
||||||
if (contentTopics.length === 0) {
|
};
|
||||||
if (!this.observers[""]) {
|
pushOrInitMapSet(this.observers, decoder.contentTopic, observer);
|
||||||
this.observers[""] = new Set();
|
|
||||||
}
|
|
||||||
this.observers[""].add(callback);
|
|
||||||
} else {
|
|
||||||
contentTopics.forEach((contentTopic) => {
|
|
||||||
if (!this.observers[contentTopic]) {
|
|
||||||
this.observers[contentTopic] = new Set();
|
|
||||||
}
|
|
||||||
this.observers[contentTopic].add(callback);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (contentTopics.length === 0) {
|
const observers = this.observers.get(decoder.contentTopic);
|
||||||
if (this.observers[""]) {
|
if (observers) {
|
||||||
this.observers[""].delete(callback);
|
observers.delete(observer);
|
||||||
}
|
|
||||||
} else {
|
|
||||||
contentTopics.forEach((contentTopic) => {
|
|
||||||
if (this.observers[contentTopic]) {
|
|
||||||
this.observers[contentTopic].delete(callback);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove an observer of new messages received via waku relay.
|
|
||||||
* Useful to ensure the same observer is not registered several time
|
|
||||||
* (e.g when loading React components)
|
|
||||||
*/
|
|
||||||
deleteObserver(
|
|
||||||
callback: (message: WakuMessage) => void,
|
|
||||||
contentTopics: string[] = []
|
|
||||||
): void {
|
|
||||||
if (contentTopics.length === 0) {
|
|
||||||
if (this.observers[""]) {
|
|
||||||
this.observers[""].delete(callback);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
contentTopics.forEach((contentTopic) => {
|
|
||||||
if (this.observers[contentTopic]) {
|
|
||||||
this.observers[contentTopic].delete(callback);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscribe to a pubsub topic and start emitting Waku messages to observers.
|
* Subscribe to a pubsub topic and start emitting Waku messages to observers.
|
||||||
*
|
*
|
||||||
|
@ -196,43 +124,37 @@ export class WakuRelay extends GossipSub {
|
||||||
subscribe(pubSubTopic: string): void {
|
subscribe(pubSubTopic: string): void {
|
||||||
this.addEventListener(
|
this.addEventListener(
|
||||||
"gossipsub:message",
|
"gossipsub:message",
|
||||||
(event: CustomEvent<GossipsubMessage>) => {
|
async (event: CustomEvent<GossipsubMessage>) => {
|
||||||
if (event.detail.msg.topic === pubSubTopic) {
|
if (event.detail.msg.topic !== pubSubTopic) return;
|
||||||
const decryptionParams = Array.from(this.decryptionKeys).map(
|
|
||||||
([key, { method, contentTopics }]) => {
|
|
||||||
return {
|
|
||||||
key,
|
|
||||||
method,
|
|
||||||
contentTopics,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
log(`Message received on ${pubSubTopic}`);
|
log(`Message received on ${pubSubTopic}`);
|
||||||
WakuMessage.decode(event.detail.msg.data, decryptionParams)
|
|
||||||
.then((wakuMsg) => {
|
const decoderV0 = new DecoderV0("");
|
||||||
if (!wakuMsg) {
|
// TODO: User might want to decide what decoder should be used (e.g. for RLN)
|
||||||
log("Failed to decode Waku Message");
|
const protoMsg = await decoderV0.decodeProto(event.detail.msg.data);
|
||||||
|
if (!protoMsg) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const contentTopic = protoMsg.contentTopic;
|
||||||
|
|
||||||
|
if (typeof contentTopic === "undefined") {
|
||||||
|
log("Message does not have a content topic, skipping");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.observers[""]) {
|
const observers = this.observers.get(contentTopic);
|
||||||
this.observers[""].forEach((callbackFn) => {
|
if (!observers) {
|
||||||
callbackFn(wakuMsg);
|
return;
|
||||||
});
|
|
||||||
}
|
|
||||||
if (wakuMsg.contentTopic) {
|
|
||||||
if (this.observers[wakuMsg.contentTopic]) {
|
|
||||||
this.observers[wakuMsg.contentTopic].forEach((callbackFn) => {
|
|
||||||
callbackFn(wakuMsg);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
await Promise.all(
|
||||||
|
Array.from(observers).map(async ({ decoder, callback }) => {
|
||||||
|
const msg = await decoder.decode(protoMsg);
|
||||||
|
if (msg) {
|
||||||
|
callback(msg);
|
||||||
|
} else {
|
||||||
|
log("Failed to decode messages on", contentTopic);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
);
|
||||||
log("Failed to decode Waku Message", e);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -13,17 +13,25 @@ import {
|
||||||
generateSymmetricKey,
|
generateSymmetricKey,
|
||||||
getPublicKey,
|
getPublicKey,
|
||||||
} from "../crypto";
|
} from "../crypto";
|
||||||
import type { WakuFull } from "../interfaces";
|
import type { Message, WakuFull } from "../interfaces";
|
||||||
import { utf8ToBytes } from "../utils";
|
import { bytesToUtf8, utf8ToBytes } from "../utils";
|
||||||
import { waitForRemotePeer } from "../wait_for_remote_peer";
|
import { waitForRemotePeer } from "../wait_for_remote_peer";
|
||||||
import { Protocols } from "../waku";
|
import { Protocols } from "../waku";
|
||||||
import { DecryptionMethod, WakuMessage } from "../waku_message";
|
import { DecoderV0, EncoderV0 } from "../waku_message/version_0.js";
|
||||||
|
import {
|
||||||
|
AsymDecoder,
|
||||||
|
AsymEncoder,
|
||||||
|
SymDecoder,
|
||||||
|
SymEncoder,
|
||||||
|
} from "../waku_message/version_1.js";
|
||||||
|
|
||||||
import { PageDirection } from "./history_rpc";
|
import { PageDirection } from "./history_rpc";
|
||||||
|
|
||||||
const log = debug("waku:test:store");
|
const log = debug("waku:test:store");
|
||||||
|
|
||||||
const TestContentTopic = "/test/1/waku-store/utf8";
|
const TestContentTopic = "/test/1/waku-store/utf8";
|
||||||
|
const TestEncoder = new EncoderV0(TestContentTopic);
|
||||||
|
const TestDecoder = new DecoderV0(TestContentTopic);
|
||||||
|
|
||||||
describe("Waku Store", () => {
|
describe("Waku Store", () => {
|
||||||
let waku: WakuFull;
|
let waku: WakuFull;
|
||||||
|
@ -62,9 +70,9 @@ describe("Waku Store", () => {
|
||||||
await waku.dial(await nwaku.getMultiaddrWithId());
|
await waku.dial(await nwaku.getMultiaddrWithId());
|
||||||
await waitForRemotePeer(waku, [Protocols.Store]);
|
await waitForRemotePeer(waku, [Protocols.Store]);
|
||||||
|
|
||||||
const messages: WakuMessage[] = [];
|
const messages: Message[] = [];
|
||||||
let promises: Promise<void>[] = [];
|
let promises: Promise<void>[] = [];
|
||||||
for await (const msgPromises of waku.store.queryGenerator([])) {
|
for await (const msgPromises of waku.store.queryGenerator(TestDecoder)) {
|
||||||
const _promises = msgPromises.map(async (promise) => {
|
const _promises = msgPromises.map(async (promise) => {
|
||||||
const msg = await promise;
|
const msg = await promise;
|
||||||
if (msg) {
|
if (msg) {
|
||||||
|
@ -78,7 +86,7 @@ describe("Waku Store", () => {
|
||||||
|
|
||||||
expect(messages?.length).eq(totalMsgs);
|
expect(messages?.length).eq(totalMsgs);
|
||||||
const result = messages?.findIndex((msg) => {
|
const result = messages?.findIndex((msg) => {
|
||||||
return msg.payloadAsUtf8 === "Message 0";
|
return bytesToUtf8(msg.payload!) === "Message 0";
|
||||||
});
|
});
|
||||||
expect(result).to.not.eq(-1);
|
expect(result).to.not.eq(-1);
|
||||||
});
|
});
|
||||||
|
@ -93,9 +101,9 @@ describe("Waku Store", () => {
|
||||||
await waku.dial(await nwaku.getMultiaddrWithId());
|
await waku.dial(await nwaku.getMultiaddrWithId());
|
||||||
await waitForRemotePeer(waku, [Protocols.Store]);
|
await waitForRemotePeer(waku, [Protocols.Store]);
|
||||||
|
|
||||||
const messages: WakuMessage[] = [];
|
const messages: Message[] = [];
|
||||||
let promises: Promise<void>[] = [];
|
let promises: Promise<void>[] = [];
|
||||||
for await (const msgPromises of waku.store.queryGenerator([])) {
|
for await (const msgPromises of waku.store.queryGenerator(TestDecoder)) {
|
||||||
const _promises = msgPromises.map(async (promise) => {
|
const _promises = msgPromises.map(async (promise) => {
|
||||||
const msg = await promise;
|
const msg = await promise;
|
||||||
if (msg) {
|
if (msg) {
|
||||||
|
@ -133,8 +141,8 @@ describe("Waku Store", () => {
|
||||||
await waku.dial(await nwaku.getMultiaddrWithId());
|
await waku.dial(await nwaku.getMultiaddrWithId());
|
||||||
await waitForRemotePeer(waku, [Protocols.Store]);
|
await waitForRemotePeer(waku, [Protocols.Store]);
|
||||||
|
|
||||||
const messages: WakuMessage[] = [];
|
const messages: Message[] = [];
|
||||||
await waku.store.queryCallbackOnPromise([], async (msgPromise) => {
|
await waku.store.queryCallbackOnPromise(TestDecoder, async (msgPromise) => {
|
||||||
const msg = await msgPromise;
|
const msg = await msgPromise;
|
||||||
if (msg) {
|
if (msg) {
|
||||||
messages.push(msg);
|
messages.push(msg);
|
||||||
|
@ -143,7 +151,7 @@ describe("Waku Store", () => {
|
||||||
|
|
||||||
expect(messages?.length).eq(totalMsgs);
|
expect(messages?.length).eq(totalMsgs);
|
||||||
const result = messages?.findIndex((msg) => {
|
const result = messages?.findIndex((msg) => {
|
||||||
return msg.payloadAsUtf8 === "Message 0";
|
return bytesToUtf8(msg.payload!) === "Message 0";
|
||||||
});
|
});
|
||||||
expect(result).to.not.eq(-1);
|
expect(result).to.not.eq(-1);
|
||||||
});
|
});
|
||||||
|
@ -172,9 +180,9 @@ describe("Waku Store", () => {
|
||||||
await waitForRemotePeer(waku, [Protocols.Store]);
|
await waitForRemotePeer(waku, [Protocols.Store]);
|
||||||
|
|
||||||
const desiredMsgs = 14;
|
const desiredMsgs = 14;
|
||||||
const messages: WakuMessage[] = [];
|
const messages: Message[] = [];
|
||||||
await waku.store.queryCallbackOnPromise(
|
await waku.store.queryCallbackOnPromise(
|
||||||
[],
|
TestDecoder,
|
||||||
async (msgPromise) => {
|
async (msgPromise) => {
|
||||||
const msg = await msgPromise;
|
const msg = await msgPromise;
|
||||||
if (msg) {
|
if (msg) {
|
||||||
|
@ -210,9 +218,9 @@ describe("Waku Store", () => {
|
||||||
await waku.dial(await nwaku.getMultiaddrWithId());
|
await waku.dial(await nwaku.getMultiaddrWithId());
|
||||||
await waitForRemotePeer(waku, [Protocols.Store]);
|
await waitForRemotePeer(waku, [Protocols.Store]);
|
||||||
|
|
||||||
const messages: WakuMessage[] = [];
|
const messages: Message[] = [];
|
||||||
await waku.store.queryOrderedCallback(
|
await waku.store.queryOrderedCallback(
|
||||||
[],
|
TestDecoder,
|
||||||
async (msg) => {
|
async (msg) => {
|
||||||
messages.push(msg);
|
messages.push(msg);
|
||||||
},
|
},
|
||||||
|
@ -225,7 +233,7 @@ describe("Waku Store", () => {
|
||||||
for (let index = 0; index < totalMsgs; index++) {
|
for (let index = 0; index < totalMsgs; index++) {
|
||||||
expect(
|
expect(
|
||||||
messages?.findIndex((msg) => {
|
messages?.findIndex((msg) => {
|
||||||
return msg.payloadAsUtf8 === `Message ${index}`;
|
return bytesToUtf8(msg.payload!) === `Message ${index}`;
|
||||||
})
|
})
|
||||||
).to.eq(index);
|
).to.eq(index);
|
||||||
}
|
}
|
||||||
|
@ -253,9 +261,9 @@ describe("Waku Store", () => {
|
||||||
await waku.dial(await nwaku.getMultiaddrWithId());
|
await waku.dial(await nwaku.getMultiaddrWithId());
|
||||||
await waitForRemotePeer(waku, [Protocols.Store]);
|
await waitForRemotePeer(waku, [Protocols.Store]);
|
||||||
|
|
||||||
let messages: WakuMessage[] = [];
|
let messages: Message[] = [];
|
||||||
await waku.store.queryOrderedCallback(
|
await waku.store.queryOrderedCallback(
|
||||||
[],
|
TestDecoder,
|
||||||
async (msg) => {
|
async (msg) => {
|
||||||
messages.push(msg);
|
messages.push(msg);
|
||||||
},
|
},
|
||||||
|
@ -270,7 +278,7 @@ describe("Waku Store", () => {
|
||||||
for (let index = 0; index < totalMsgs; index++) {
|
for (let index = 0; index < totalMsgs; index++) {
|
||||||
expect(
|
expect(
|
||||||
messages?.findIndex((msg) => {
|
messages?.findIndex((msg) => {
|
||||||
return msg.payloadAsUtf8 === `Message ${index}`;
|
return bytesToUtf8(msg.payload!) === `Message ${index}`;
|
||||||
})
|
})
|
||||||
).to.eq(index);
|
).to.eq(index);
|
||||||
}
|
}
|
||||||
|
@ -279,60 +287,45 @@ describe("Waku Store", () => {
|
||||||
it("Generator, with asymmetric & symmetric encrypted messages", async function () {
|
it("Generator, with asymmetric & symmetric encrypted messages", async function () {
|
||||||
this.timeout(15_000);
|
this.timeout(15_000);
|
||||||
|
|
||||||
const encryptedAsymmetricMessageText =
|
const asymText = "This message is encrypted for me using asymmetric";
|
||||||
"This message is encrypted for me using asymmetric";
|
const asymTopic = "/test/1/asymmetric/proto";
|
||||||
const encryptedAsymmetricContentTopic = "/test/1/asymmetric/proto";
|
const symText =
|
||||||
const encryptedSymmetricMessageText =
|
|
||||||
"This message is encrypted for me using symmetric encryption";
|
"This message is encrypted for me using symmetric encryption";
|
||||||
const encryptedSymmetricContentTopic = "/test/1/symmetric/proto";
|
const symTopic = "/test/1/symmetric/proto";
|
||||||
const clearMessageText =
|
const clearText = "This is a clear text message for everyone to read";
|
||||||
"This is a clear text message for everyone to read";
|
const otherText =
|
||||||
const otherEncMessageText =
|
|
||||||
"This message is not for and I must not be able to read it";
|
"This message is not for and I must not be able to read it";
|
||||||
|
|
||||||
|
const timestamp = new Date();
|
||||||
|
|
||||||
|
const asymMsg = { payload: utf8ToBytes(asymText), timestamp };
|
||||||
|
const symMsg = {
|
||||||
|
payload: utf8ToBytes(symText),
|
||||||
|
timestamp: new Date(timestamp.valueOf() + 1),
|
||||||
|
};
|
||||||
|
const clearMsg = {
|
||||||
|
payload: utf8ToBytes(clearText),
|
||||||
|
timestamp: new Date(timestamp.valueOf() + 2),
|
||||||
|
};
|
||||||
|
const otherMsg = {
|
||||||
|
payload: utf8ToBytes(otherText),
|
||||||
|
timestamp: new Date(timestamp.valueOf() + 3),
|
||||||
|
};
|
||||||
|
|
||||||
const privateKey = generatePrivateKey();
|
const privateKey = generatePrivateKey();
|
||||||
const symKey = generateSymmetricKey();
|
const symKey = generateSymmetricKey();
|
||||||
const publicKey = getPublicKey(privateKey);
|
const publicKey = getPublicKey(privateKey);
|
||||||
|
|
||||||
const timestamp = new Date();
|
const asymEncoder = new AsymEncoder(asymTopic, publicKey);
|
||||||
const [
|
const symEncoder = new SymEncoder(symTopic, symKey);
|
||||||
encryptedAsymmetricMessage,
|
|
||||||
encryptedSymmetricMessage,
|
|
||||||
clearMessage,
|
|
||||||
otherEncMessage,
|
|
||||||
] = await Promise.all([
|
|
||||||
WakuMessage.fromUtf8String(
|
|
||||||
encryptedAsymmetricMessageText,
|
|
||||||
encryptedAsymmetricContentTopic,
|
|
||||||
{
|
|
||||||
encPublicKey: publicKey,
|
|
||||||
timestamp,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
WakuMessage.fromUtf8String(
|
|
||||||
encryptedSymmetricMessageText,
|
|
||||||
encryptedSymmetricContentTopic,
|
|
||||||
{
|
|
||||||
symKey: symKey,
|
|
||||||
timestamp: new Date(timestamp.valueOf() + 1),
|
|
||||||
}
|
|
||||||
),
|
|
||||||
WakuMessage.fromUtf8String(
|
|
||||||
clearMessageText,
|
|
||||||
encryptedAsymmetricContentTopic,
|
|
||||||
{ timestamp: new Date(timestamp.valueOf() + 2) }
|
|
||||||
),
|
|
||||||
WakuMessage.fromUtf8String(
|
|
||||||
otherEncMessageText,
|
|
||||||
encryptedSymmetricContentTopic,
|
|
||||||
{
|
|
||||||
encPublicKey: getPublicKey(generatePrivateKey()),
|
|
||||||
timestamp: new Date(timestamp.valueOf() + 3),
|
|
||||||
}
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
|
|
||||||
log("Messages have been encrypted");
|
const otherEncoder = new AsymEncoder(
|
||||||
|
TestContentTopic,
|
||||||
|
getPublicKey(generatePrivateKey())
|
||||||
|
);
|
||||||
|
|
||||||
|
const asymDecoder = new AsymDecoder(asymTopic, privateKey);
|
||||||
|
const symDecoder = new SymDecoder(symTopic, symKey);
|
||||||
|
|
||||||
const [waku1, waku2, nimWakuMultiaddr] = await Promise.all([
|
const [waku1, waku2, nimWakuMultiaddr] = await Promise.all([
|
||||||
createFullNode({
|
createFullNode({
|
||||||
|
@ -357,25 +350,18 @@ describe("Waku Store", () => {
|
||||||
|
|
||||||
log("Sending messages using light push");
|
log("Sending messages using light push");
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
waku1.lightPush.push(encryptedAsymmetricMessage),
|
waku1.lightPush.push(asymEncoder, asymMsg),
|
||||||
waku1.lightPush.push(encryptedSymmetricMessage),
|
waku1.lightPush.push(symEncoder, symMsg),
|
||||||
waku1.lightPush.push(otherEncMessage),
|
waku1.lightPush.push(otherEncoder, otherMsg),
|
||||||
waku1.lightPush.push(clearMessage),
|
waku1.lightPush.push(TestEncoder, clearMsg),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await waitForRemotePeer(waku2, [Protocols.Store]);
|
await waitForRemotePeer(waku2, [Protocols.Store]);
|
||||||
|
|
||||||
waku2.addDecryptionKey(symKey, {
|
const messages: Message[] = [];
|
||||||
contentTopics: [encryptedSymmetricContentTopic],
|
|
||||||
method: DecryptionMethod.Symmetric,
|
|
||||||
});
|
|
||||||
|
|
||||||
const messages: WakuMessage[] = [];
|
|
||||||
log("Retrieve messages from store");
|
log("Retrieve messages from store");
|
||||||
|
|
||||||
for await (const msgPromises of waku2.store.queryGenerator([], {
|
for await (const msgPromises of waku2.store.queryGenerator(asymDecoder)) {
|
||||||
decryptionParams: [{ key: privateKey }],
|
|
||||||
})) {
|
|
||||||
for (const promise of msgPromises) {
|
for (const promise of msgPromises) {
|
||||||
const msg = await promise;
|
const msg = await promise;
|
||||||
if (msg) {
|
if (msg) {
|
||||||
|
@ -384,24 +370,29 @@ describe("Waku Store", () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(messages?.length).eq(3);
|
for await (const msgPromises of waku2.store.queryGenerator(symDecoder)) {
|
||||||
if (!messages) throw "Length was tested";
|
for (const promise of msgPromises) {
|
||||||
// Messages are ordered from oldest to latest within a page (1 page query)
|
const msg = await promise;
|
||||||
expect(messages[0].payloadAsUtf8).to.eq(encryptedAsymmetricMessageText);
|
if (msg) {
|
||||||
expect(messages[1].payloadAsUtf8).to.eq(encryptedSymmetricMessageText);
|
messages.push(msg);
|
||||||
expect(messages[2].payloadAsUtf8).to.eq(clearMessageText);
|
|
||||||
|
|
||||||
for (const text of [
|
|
||||||
encryptedAsymmetricMessageText,
|
|
||||||
encryptedSymmetricMessageText,
|
|
||||||
clearMessageText,
|
|
||||||
]) {
|
|
||||||
expect(
|
|
||||||
messages?.findIndex((msg) => {
|
|
||||||
return msg.payloadAsUtf8 === text;
|
|
||||||
})
|
|
||||||
).to.not.eq(-1);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for await (const msgPromises of waku2.store.queryGenerator(TestDecoder)) {
|
||||||
|
for (const promise of msgPromises) {
|
||||||
|
const msg = await promise;
|
||||||
|
if (msg) {
|
||||||
|
messages.push(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Messages are ordered from oldest to latest within a page (1 page query)
|
||||||
|
expect(bytesToUtf8(messages[0].payload!)).to.eq(asymText);
|
||||||
|
expect(bytesToUtf8(messages[1].payload!)).to.eq(symText);
|
||||||
|
expect(bytesToUtf8(messages[2].payload!)).to.eq(clearText);
|
||||||
|
expect(messages?.length).eq(3);
|
||||||
|
|
||||||
!!waku1 && waku1.stop().catch((e) => console.log("Waku failed to stop", e));
|
!!waku1 && waku1.stop().catch((e) => console.log("Waku failed to stop", e));
|
||||||
!!waku2 && waku2.stop().catch((e) => console.log("Waku failed to stop", e));
|
!!waku2 && waku2.stop().catch((e) => console.log("Waku failed to stop", e));
|
||||||
|
@ -450,9 +441,9 @@ describe("Waku Store", () => {
|
||||||
|
|
||||||
const nwakuPeerId = await nwaku.getPeerId();
|
const nwakuPeerId = await nwaku.getPeerId();
|
||||||
|
|
||||||
const firstMessages: WakuMessage[] = [];
|
const firstMessages: Message[] = [];
|
||||||
await waku.store.queryOrderedCallback(
|
await waku.store.queryOrderedCallback(
|
||||||
[],
|
TestDecoder,
|
||||||
(msg) => {
|
(msg) => {
|
||||||
if (msg) {
|
if (msg) {
|
||||||
firstMessages.push(msg);
|
firstMessages.push(msg);
|
||||||
|
@ -464,9 +455,9 @@ describe("Waku Store", () => {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const bothMessages: WakuMessage[] = [];
|
const bothMessages: Message[] = [];
|
||||||
await waku.store.queryOrderedCallback(
|
await waku.store.queryOrderedCallback(
|
||||||
[],
|
TestDecoder,
|
||||||
async (msg) => {
|
async (msg) => {
|
||||||
bothMessages.push(msg);
|
bothMessages.push(msg);
|
||||||
},
|
},
|
||||||
|
@ -481,7 +472,7 @@ describe("Waku Store", () => {
|
||||||
|
|
||||||
expect(firstMessages?.length).eq(1);
|
expect(firstMessages?.length).eq(1);
|
||||||
|
|
||||||
expect(firstMessages[0]?.payloadAsUtf8).eq("Message 0");
|
expect(bytesToUtf8(firstMessages[0].payload!)).eq("Message 0");
|
||||||
|
|
||||||
expect(bothMessages?.length).eq(2);
|
expect(bothMessages?.length).eq(2);
|
||||||
});
|
});
|
||||||
|
@ -531,9 +522,9 @@ describe("Waku Store, custom pubsub topic", () => {
|
||||||
await waku.dial(await nwaku.getMultiaddrWithId());
|
await waku.dial(await nwaku.getMultiaddrWithId());
|
||||||
await waitForRemotePeer(waku, [Protocols.Store]);
|
await waitForRemotePeer(waku, [Protocols.Store]);
|
||||||
|
|
||||||
const messages: WakuMessage[] = [];
|
const messages: Message[] = [];
|
||||||
let promises: Promise<void>[] = [];
|
let promises: Promise<void>[] = [];
|
||||||
for await (const msgPromises of waku.store.queryGenerator([])) {
|
for await (const msgPromises of waku.store.queryGenerator(TestDecoder)) {
|
||||||
const _promises = msgPromises.map(async (promise) => {
|
const _promises = msgPromises.map(async (promise) => {
|
||||||
const msg = await promise;
|
const msg = await promise;
|
||||||
if (msg) {
|
if (msg) {
|
||||||
|
@ -547,7 +538,7 @@ describe("Waku Store, custom pubsub topic", () => {
|
||||||
|
|
||||||
expect(messages?.length).eq(totalMsgs);
|
expect(messages?.length).eq(totalMsgs);
|
||||||
const result = messages?.findIndex((msg) => {
|
const result = messages?.findIndex((msg) => {
|
||||||
return msg.payloadAsUtf8 === "Message 0";
|
return bytesToUtf8(msg.payload!) === "Message 0";
|
||||||
});
|
});
|
||||||
expect(result).to.not.eq(-1);
|
expect(result).to.not.eq(-1);
|
||||||
});
|
});
|
||||||
|
|
|
@ -11,14 +11,9 @@ import { Uint8ArrayList } from "uint8arraylist";
|
||||||
import * as protoV2Beta4 from "../../proto/store_v2beta4";
|
import * as protoV2Beta4 from "../../proto/store_v2beta4";
|
||||||
import { HistoryResponse } from "../../proto/store_v2beta4";
|
import { HistoryResponse } from "../../proto/store_v2beta4";
|
||||||
import { DefaultPubSubTopic, StoreCodecs } from "../constants";
|
import { DefaultPubSubTopic, StoreCodecs } from "../constants";
|
||||||
|
import { Decoder, Message } from "../interfaces";
|
||||||
import { selectConnection } from "../select_connection";
|
import { selectConnection } from "../select_connection";
|
||||||
import { getPeersForProtocol, selectPeerForProtocol } from "../select_peer";
|
import { getPeersForProtocol, selectPeerForProtocol } from "../select_peer";
|
||||||
import { hexToBytes } from "../utils";
|
|
||||||
import {
|
|
||||||
DecryptionMethod,
|
|
||||||
DecryptionParams,
|
|
||||||
WakuMessage,
|
|
||||||
} from "../waku_message";
|
|
||||||
|
|
||||||
import { HistoryRPC, PageDirection, Params } from "./history_rpc";
|
import { HistoryRPC, PageDirection, Params } from "./history_rpc";
|
||||||
|
|
||||||
|
@ -78,13 +73,6 @@ export interface QueryOptions {
|
||||||
* Retrieve messages with a timestamp within the provided values.
|
* Retrieve messages with a timestamp within the provided values.
|
||||||
*/
|
*/
|
||||||
timeFilter?: TimeFilter;
|
timeFilter?: TimeFilter;
|
||||||
/**
|
|
||||||
* Keys that will be used to decrypt messages.
|
|
||||||
*
|
|
||||||
* It can be Asymmetric Private Keys and Symmetric Keys in the same array,
|
|
||||||
* all keys will be tried with both methods.
|
|
||||||
*/
|
|
||||||
decryptionParams?: DecryptionParams[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -94,15 +82,9 @@ export interface QueryOptions {
|
||||||
*/
|
*/
|
||||||
export class WakuStore {
|
export class WakuStore {
|
||||||
pubSubTopic: string;
|
pubSubTopic: string;
|
||||||
public decryptionKeys: Map<
|
|
||||||
Uint8Array,
|
|
||||||
{ method?: DecryptionMethod; contentTopics?: string[] }
|
|
||||||
>;
|
|
||||||
|
|
||||||
constructor(public libp2p: Libp2p, options?: CreateOptions) {
|
constructor(public libp2p: Libp2p, options?: CreateOptions) {
|
||||||
this.pubSubTopic = options?.pubSubTopic ?? DefaultPubSubTopic;
|
this.pubSubTopic = options?.pubSubTopic ?? DefaultPubSubTopic;
|
||||||
|
|
||||||
this.decryptionKeys = new Map();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -122,14 +104,12 @@ export class WakuStore {
|
||||||
* or if an error is encountered when processing the reply.
|
* or if an error is encountered when processing the reply.
|
||||||
*/
|
*/
|
||||||
async queryOrderedCallback(
|
async queryOrderedCallback(
|
||||||
contentTopics: string[],
|
decoder: Decoder,
|
||||||
callback: (
|
callback: (message: Message) => Promise<void | boolean> | boolean | void,
|
||||||
message: WakuMessage
|
|
||||||
) => Promise<void | boolean> | boolean | void,
|
|
||||||
options?: QueryOptions
|
options?: QueryOptions
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const abort = false;
|
const abort = false;
|
||||||
for await (const promises of this.queryGenerator(contentTopics, options)) {
|
for await (const promises of this.queryGenerator(decoder, options)) {
|
||||||
if (abort) break;
|
if (abort) break;
|
||||||
let messages = await Promise.all(promises);
|
let messages = await Promise.all(promises);
|
||||||
|
|
||||||
|
@ -172,15 +152,15 @@ export class WakuStore {
|
||||||
* or if an error is encountered when processing the reply.
|
* or if an error is encountered when processing the reply.
|
||||||
*/
|
*/
|
||||||
async queryCallbackOnPromise(
|
async queryCallbackOnPromise(
|
||||||
contentTopics: string[],
|
decoder: Decoder,
|
||||||
callback: (
|
callback: (
|
||||||
message: Promise<WakuMessage | undefined>
|
message: Promise<Message | undefined>
|
||||||
) => Promise<void | boolean> | boolean | void,
|
) => Promise<void | boolean> | boolean | void,
|
||||||
options?: QueryOptions
|
options?: QueryOptions
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
let abort = false;
|
let abort = false;
|
||||||
let promises: Promise<void>[] = [];
|
let promises: Promise<void>[] = [];
|
||||||
for await (const page of this.queryGenerator(contentTopics, options)) {
|
for await (const page of this.queryGenerator(decoder, options)) {
|
||||||
const _promises = page.map(async (msg) => {
|
const _promises = page.map(async (msg) => {
|
||||||
if (!abort) {
|
if (!abort) {
|
||||||
abort = Boolean(await callback(msg));
|
abort = Boolean(await callback(msg));
|
||||||
|
@ -209,9 +189,9 @@ export class WakuStore {
|
||||||
* or if an error is encountered when processing the reply.
|
* or if an error is encountered when processing the reply.
|
||||||
*/
|
*/
|
||||||
async *queryGenerator(
|
async *queryGenerator(
|
||||||
contentTopics: string[],
|
decoder: Decoder,
|
||||||
options?: QueryOptions
|
options?: QueryOptions
|
||||||
): AsyncGenerator<Promise<WakuMessage | undefined>[]> {
|
): AsyncGenerator<Promise<Message | undefined>[]> {
|
||||||
let startTime, endTime;
|
let startTime, endTime;
|
||||||
|
|
||||||
if (options?.timeFilter) {
|
if (options?.timeFilter) {
|
||||||
|
@ -219,6 +199,8 @@ export class WakuStore {
|
||||||
endTime = options.timeFilter.endTime;
|
endTime = options.timeFilter.endTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const contentTopic = decoder.contentTopic;
|
||||||
|
|
||||||
const queryOpts = Object.assign(
|
const queryOpts = Object.assign(
|
||||||
{
|
{
|
||||||
pubSubTopic: this.pubSubTopic,
|
pubSubTopic: this.pubSubTopic,
|
||||||
|
@ -226,7 +208,7 @@ export class WakuStore {
|
||||||
pageSize: DefaultPageSize,
|
pageSize: DefaultPageSize,
|
||||||
},
|
},
|
||||||
options,
|
options,
|
||||||
{ contentTopics, startTime, endTime }
|
{ contentTopics: [contentTopic], startTime, endTime }
|
||||||
);
|
);
|
||||||
|
|
||||||
log("Querying history with the following options", {
|
log("Querying history with the following options", {
|
||||||
|
@ -250,57 +232,16 @@ export class WakuStore {
|
||||||
|
|
||||||
if (!connection) throw "Failed to get a connection to the peer";
|
if (!connection) throw "Failed to get a connection to the peer";
|
||||||
|
|
||||||
let decryptionParams: DecryptionParams[] = [];
|
|
||||||
|
|
||||||
this.decryptionKeys.forEach(({ method, contentTopics }, key) => {
|
|
||||||
decryptionParams.push({
|
|
||||||
key,
|
|
||||||
method,
|
|
||||||
contentTopics,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add the decryption keys passed to this function against the
|
|
||||||
// content topics also passed to this function.
|
|
||||||
if (options?.decryptionParams) {
|
|
||||||
decryptionParams = decryptionParams.concat(options.decryptionParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
for await (const messages of paginate(
|
for await (const messages of paginate(
|
||||||
connection,
|
connection,
|
||||||
protocol,
|
protocol,
|
||||||
queryOpts,
|
queryOpts,
|
||||||
decryptionParams
|
decoder
|
||||||
)) {
|
)) {
|
||||||
yield messages;
|
yield messages;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a decryption key to attempt decryption of messages received in any
|
|
||||||
* subsequent query call. This can either be a private key for
|
|
||||||
* asymmetric encryption or a symmetric key. { @link WakuStore } will attempt to
|
|
||||||
* decrypt messages using both methods.
|
|
||||||
*
|
|
||||||
* Strings must be in hex format.
|
|
||||||
*/
|
|
||||||
addDecryptionKey(
|
|
||||||
key: Uint8Array | string,
|
|
||||||
options?: { method?: DecryptionMethod; contentTopics?: string[] }
|
|
||||||
): void {
|
|
||||||
this.decryptionKeys.set(hexToBytes(key), options ?? {});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**cursorV2Beta4
|
|
||||||
* Delete a decryption key that was used to attempt decryption of messages
|
|
||||||
* received in subsequent query calls.
|
|
||||||
*
|
|
||||||
* Strings must be in hex format.
|
|
||||||
*/
|
|
||||||
deleteDecryptionKey(key: Uint8Array | string): void {
|
|
||||||
this.decryptionKeys.delete(hexToBytes(key));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns known peers from the address book (`libp2p.peerStore`) that support
|
* Returns known peers from the address book (`libp2p.peerStore`) that support
|
||||||
* store protocol. Waku may or may not be currently connected to these peers.
|
* store protocol. Waku may or may not be currently connected to these peers.
|
||||||
|
@ -319,8 +260,8 @@ async function* paginate(
|
||||||
connection: Connection,
|
connection: Connection,
|
||||||
protocol: string,
|
protocol: string,
|
||||||
queryOpts: Params,
|
queryOpts: Params,
|
||||||
decryptionParams: DecryptionParams[]
|
decoder: Decoder
|
||||||
): AsyncGenerator<Promise<WakuMessage | undefined>[]> {
|
): AsyncGenerator<Promise<Message | undefined>[]> {
|
||||||
let cursor = undefined;
|
let cursor = undefined;
|
||||||
while (true) {
|
while (true) {
|
||||||
queryOpts = Object.assign(queryOpts, { cursor });
|
queryOpts = Object.assign(queryOpts, { cursor });
|
||||||
|
@ -373,9 +314,7 @@ async function* paginate(
|
||||||
|
|
||||||
log(`${response.messages.length} messages retrieved from store`);
|
log(`${response.messages.length} messages retrieved from store`);
|
||||||
|
|
||||||
yield response.messages.map((protoMsg) =>
|
yield response.messages.map((protoMsg) => decoder.decode(protoMsg));
|
||||||
WakuMessage.decodeProto(protoMsg, decryptionParams)
|
|
||||||
);
|
|
||||||
|
|
||||||
cursor = response.pagingInfo?.cursor;
|
cursor = response.pagingInfo?.cursor;
|
||||||
if (typeof cursor === "undefined") {
|
if (typeof cursor === "undefined") {
|
||||||
|
@ -401,7 +340,7 @@ async function* paginate(
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isWakuMessageDefined = (
|
export const isWakuMessageDefined = (
|
||||||
msg: WakuMessage | undefined
|
msg: Message | undefined
|
||||||
): msg is WakuMessage => {
|
): msg is Message => {
|
||||||
return !!msg;
|
return !!msg;
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,7 +6,9 @@
|
||||||
"./src/lib/peer_discovery_dns.ts",
|
"./src/lib/peer_discovery_dns.ts",
|
||||||
"./src/lib/peer_discovery_static_list.ts",
|
"./src/lib/peer_discovery_static_list.ts",
|
||||||
"./src/lib/predefined_bootstrap_nodes.ts",
|
"./src/lib/predefined_bootstrap_nodes.ts",
|
||||||
"./src/lib/wait_for_remote_peer.ts"
|
"./src/lib/wait_for_remote_peer.ts",
|
||||||
|
"./src/lib/waku_message/version_0.ts",
|
||||||
|
"./src/lib/waku_message/version_1.ts"
|
||||||
],
|
],
|
||||||
"out": "build/docs",
|
"out": "build/docs",
|
||||||
"exclude": ["**/*.spec.ts"],
|
"exclude": ["**/*.spec.ts"],
|
||||||
|
|
Loading…
Reference in New Issue