Merge pull request #234 from status-im/fix-doc

This commit is contained in:
Franck Royer 2021-07-16 11:58:01 +10:00 committed by GitHub
commit 228cdab89b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 305 additions and 87 deletions

View File

@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- Examples (web-chat): New `/fleet` command to switch connection between Status prod and test fleets.
- Export `generatePrivateKey` and `getPublicKey` directly from the root.
- Usage of the encryption and signature APIs to the readme.
### Changed
- **Breaking**: Renamed `WakuRelay.(add|delete)PrivateDecryptionKey` to `WakuRelay.(add|delete)DecryptionKey` to make it clearer that it accepts both symmetric keys and asymmetric private keys.
### Fix
- Align `WakuMessage` readme example with actual code behaviour.
## [0.8.0] - 2021-07-15
### Added

214
README.md
View File

@ -10,20 +10,17 @@ Install `js-waku` package:
npm install js-waku
```
Start a waku node:
### Start a waku node
```javascript
```ts
import { Waku } from 'js-waku';
const waku = await Waku.create();
```
Connect to a new peer:
```javascript
import { multiaddr } from 'multiaddr';
import PeerId from 'peer-id';
### Connect to a new peer
```ts
// Directly dial a new peer
await waku.dial('/dns4/node-01.do-ams3.wakuv2.test.statusim.net/tcp/443/wss/p2p/16Uiu2HAmPLe7Mzm8TsYUubgCAW1aJoeFScxrLj8ppHFivPo97bUZ');
@ -36,17 +33,18 @@ waku.addPeerToAddressBook(
You can also use `getStatusFleetNodes` to connect to nodes run by Status:
```javascript
```ts
import { getStatusFleetNodes } from 'js-waku';
const nodes = await getStatusFleetNodes();
await Promise.all(
nodes.map((addr) => {
return waku.dial(addr);
})
);
getStatusFleetNodes().then((nodes) => {
nodes.forEach((addr) => {
waku.dial(addr);
});
});
```
### Listen for messages
The `contentTopic` is a metadata `string` that allows categorization of messages on the waku network.
Depending on your use case, you can either create one (or several) new `contentTopic`(s) or look at the [RFCs](https://rfc.vac.dev/) and use an existing `contentTopic`.
See the [Waku v2 Topic Usage Recommendations](https://rfc.vac.dev/spec/23/) for more details.
@ -54,7 +52,7 @@ See the [Waku v2 Topic Usage Recommendations](https://rfc.vac.dev/spec/23/) for
For example, if you were to use a new `contentTopic` such as `/my-cool-app/1/my-use-case/proto`,
here is how to listen to new messages received via [Waku v2 Relay](https://rfc.vac.dev/spec/11/):
```javascript
```ts
waku.relay.addObserver((msg) => {
console.log("Message received:", msg.payloadAsUtf8)
}, ["/my-cool-app/1/my-use-case/proto"]);
@ -62,37 +60,199 @@ waku.relay.addObserver((msg) => {
The examples chat apps currently use content topic `"/toy-chat/2/huilong/proto"`.
Send a message on the waku relay network:
### Send messages
```javascript
There are two ways to send messages:
#### Waku Relay
[Waku Relay](https://rfc.vac.dev/spec/11/) is the most decentralized option,
peer receiving your messages are unlikely to know whether you are the originator or simply forwarding them.
However, it does not give you any delivery information.
```ts
import { WakuMessage } from 'js-waku';
const msg = WakuMessage.fromUtf8String("Here is a message!", "/my-cool-app/1/my-use-case/proto")
const msg = await WakuMessage.fromUtf8String("Here is a message!", { contentTopic: "/my-cool-app/1/my-use-case/proto" })
await waku.relay.send(msg);
```
The [Waku v2 Store protocol](https://rfc.vac.dev/spec/13/) enables full nodes to store messages received via relay
and clients to retrieve them (e.g. after resuming connectivity).
#### Waku Light Push
[Waku Light Push](https://rfc.vac.dev/spec/19/) gives you confirmation that the light push server node has
received your message.
However, it means that said node knows you are the originator of the message.
It cannot guarantee that the node will forward the message.
```ts
const ack = await waku.lightPush.push(message);
if (!ack?.isSuccess) {
// Message was not sent
}
```
### Retrieve archived messages
The [Waku v2 Store protocol](https://rfc.vac.dev/spec/13/) enables more permanent nodes to store messages received via relay
and ephemeral clients to retrieve them (e.g. mobile phone resuming connectivity).
The protocol implements pagination meaning that it may take several queries to retrieve all messages.
Query a waku store peer to check historical messages:
```javascript
// Process messages once they are all retrieved:
const messages = await waku.store.queryHistory(storePeerId, ["my-cool-app"]);
```ts
// Process messages once they are all retrieved
const messages = await waku.store.queryHistory({ contentTopics: ["/my-cool-app/1/my-use-case/proto"] });
messages.forEach((msg) => {
console.log("Message retrieved:", msg.payloadAsUtf8)
})
// Or, pass a callback function to be executed as pages are received:
waku.store.queryHistory(storePeerId, ["my-cool-app"],
(messages) => {
waku.store.queryHistory({
contentTopics: ["/my-cool-app/1/my-use-case/proto"],
callback: (messages) => {
messages.forEach((msg) => {
console.log("Message retrieved:", msg.payloadAsUtf8)
})
console.log("Message retrieved:", msg.payloadAsUtf8);
});
}
});
```
## Encryption & Signature
With js-waku, you can:
- Encrypt messages over the wire using public/private key pair (asymmetric encryption),
- Encrypt messages over the wire using a unique key to both encrypt and decrypt (symmetric encryption),
- Sign and verify your waku messages (must use encryption, compatible with both symmetric and asymmetric).
### Cryptographic Libraries
A quick note on the cryptographic libraries used as it is a not a straightforward affair:
- Asymmetric encryption:
Uses [ecies-geth](https://github.com/cyrildever/ecies-geth/)
which in turns uses [SubtleCrypto](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto) Web API (browser),
[secp256k1](https://www.npmjs.com/package/secp256k1) (native binding for node)
or [elliptic](https://www.npmjs.com/package/elliptic) (pure JS if none of the other libraries are available).
- Symmetric encryption:
Uses [SubtleCrypto](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto) Web API (browser)
or [NodeJS' crypto](https://nodejs.org/api/crypto.html) module.
### Create new keys
Asymmetric private keys and symmetric keys are expected to be 32 bytes arrays.
```ts
import { generatePrivateKey, getPublicKey } from 'js-waku';
// Asymmetric
const privateKey = generatePrivateKey();
const publicKey = getPublicKey(privateKey);
// Symmetric
const symKey = generatePrivateKey();
```
### Encrypt Waku Messages
To encrypt your waku messages, simply pass the encryption key when creating it:
```ts
import { WakuMessage } from 'js-waku';
// Asymmetric
const message = await WakuMessage.fromBytes(payload, {
contentTopic: myAppContentTopic,
encPublicKey: publicKey,
});
// Symmetric
const message = await WakuMessage.fromBytes(payload, {
contentTopic: myAppContentTopic,
symKey: symKey,
});
```
### Decrypt Waku Messages
#### Waku Relay
If you expect to receive encrypted messages then simply add private decryption key(s) to `WakuRelay`.
Waku Relay will attempt to decrypt incoming messages with each keys, both for symmetric and asymmetric encryption.
Messages that are successfully decrypted (or received in clear) will be passed to the observers, other messages will be omitted.
```ts
// Asymmetric
waku.relay.addDecryptionKey(privateKey);
// Symmetric
waku.relay.addDecryptionKey(symKey);
// Then add the observer
waku.relay.addObserver(callback, [contentTopic]);
```
Keys can be removed using `WakuMessage.deleteDecryptionKey`.
#### Waku Store
```ts
const messages = await waku.store.queryHistory({
contentTopics: [],
decryptionKeys: [privateKey, symKey],
});
```
Similarly to relay, only decrypted or clear messages will be returned.
### Sign Waku Messages
As per version 1`s [specs](https://rfc.vac.dev/spec/26/), signatures are only included in encrypted messages.
In the case where your app does not need encryption then you could use symmetric encryption with a trivial key, I intend to dig [more on the subject](https://github.com/status-im/js-waku/issues/74#issuecomment-880440186) and come back with recommendation and examples.
Signature keys can be generated the same way asymmetric keys for encryption are:
```ts
import { generatePrivateKey, getPublicKey, WakuMessage } from 'js-waku';
const signPrivateKey = generatePrivateKey();
// Asymmetric Encryption
const message = await WakuMessage.fromBytes(payload, {
contentTopic: myAppContentTopic,
encPublicKey: recipientPublicKey,
sigPrivKey: signPrivateKey
});
// Symmetric Encryption
const message = await WakuMessage.fromBytes(payload, {
contentTopic: myAppContentTopic,
encPublicKey: symKey,
sigPrivKey: signPrivateKey
});
```
### Verify Waku Message signatures
Two fields are available on `WakuMessage` regarding signatures:
- `signaturePublicKey`: If the message is signed, it holds the public key of the signature,
- `signature`: If the message is signed, it holds the actual signature.
Thus, if you expect messages to be signed by Alice,
you can simply compare `WakuMessage.signaturePublicKey` with Alice's public key.
As comparing hex string can lead to issues (is the `0x` prefix present?),
simply use helper function `equalByteArrays`.
```ts
import { equalByteArrays } from 'js-waku/lib/utils';
const sigPubKey = wakuMessage.signaturePublicKey;
const isSignedByAlice = sigPubKey && equalByteArrays(sigPubKey, alicePublicKey);
```
## More documentation
Find more [examples](#examples) below
or checkout the latest `main` branch documentation at [https://status-im.github.io/js-waku/docs/](https://status-im.github.io/js-waku/docs/).

View File

@ -128,13 +128,13 @@ function App() {
if (!waku) return;
if (!ethDmKeyPair) return;
waku.relay.addDecryptionPrivateKey(ethDmKeyPair.privateKey);
waku.relay.addDecryptionKey(ethDmKeyPair.privateKey);
return function cleanUp() {
if (!waku) return;
if (!ethDmKeyPair) return;
waku.relay.deleteDecryptionPrivateKey(ethDmKeyPair.privateKey);
waku.relay.deleteDecryptionKey(ethDmKeyPair.privateKey);
};
}, [waku, ethDmKeyPair]);

View File

@ -4,10 +4,7 @@ import { ethers } from 'ethers';
import { Signer } from '@ethersproject/abstract-signer';
import { PublicKeyMessage } from './messaging/wire';
import { hexToBuf, equalByteArrays, bufToHex } from 'js-waku/lib/utils';
import {
generatePrivateKey,
getPublicKey,
} from 'js-waku/lib/waku_message/version_1';
import { generatePrivateKey, getPublicKey } from 'js-waku';
export interface KeyPair {
privateKey: Uint8Array;

View File

@ -74,28 +74,27 @@ async function retrieveStoreMessages(
}
export default function App() {
let [newMessages, setNewMessages] = useState<Message[]>([]);
let [archivedMessages, setArchivedMessages] = useState<Message[]>([]);
let [stateWaku, setWaku] = useState<Waku | undefined>(undefined);
let [nick, setNick] = useState<string>(() => {
const [newMessages, setNewMessages] = useState<Message[]>([]);
const [archivedMessages, setArchivedMessages] = useState<Message[]>([]);
const [waku, setWaku] = useState<Waku | undefined>(undefined);
const [nick, setNick] = useState<string>(() => {
const persistedNick = window.localStorage.getItem('nick');
return persistedNick !== null ? persistedNick : generate();
});
const [fleetEnv, setFleetEnv] = useState<Environment>(defaultFleetEnv);
useEffect(() => {
localStorage.setItem('nick', nick);
}, [nick]);
useEffect(() => {
if (stateWaku) return;
initWaku(setWaku)
initWaku(fleetEnv, setWaku)
.then(() => console.log('Waku init done'))
.catch((e) => console.log('Waku init failed ', e));
}, [stateWaku]);
}, [fleetEnv]);
useEffect(() => {
if (!stateWaku) return;
if (!waku) return;
const handleRelayMessage = (wakuMsg: WakuMessage) => {
console.log('Message received: ', wakuMsg);
@ -105,23 +104,25 @@ export default function App() {
}
};
stateWaku.relay.addObserver(handleRelayMessage, [ChatContentTopic]);
waku.relay.addObserver(handleRelayMessage, [ChatContentTopic]);
return;
}, [stateWaku]);
return function cleanUp() {
waku?.relay.deleteObserver(handleRelayMessage, [ChatContentTopic]);
};
}, [waku]);
useEffect(() => {
if (!stateWaku) return;
if (!waku) return;
const handleProtocolChange = async (
waku: Waku,
_waku: Waku,
{ peerId, protocols }: { peerId: PeerId; protocols: string[] }
) => {
if (protocols.includes(StoreCodec)) {
console.log(`${peerId.toB58String()}: retrieving archived messages}`);
try {
const length = await retrieveStoreMessages(
waku,
_waku,
peerId,
setArchivedMessages
);
@ -135,36 +136,38 @@ export default function App() {
}
};
stateWaku.libp2p.peerStore.on(
waku.libp2p.peerStore.on(
'change:protocols',
handleProtocolChange.bind({}, stateWaku)
handleProtocolChange.bind({}, waku)
);
// To clean up listener when component unmounts
return () => {
stateWaku?.libp2p.peerStore.removeListener(
return function cleanUp() {
waku?.libp2p.peerStore.removeListener(
'change:protocols',
handleProtocolChange.bind({}, stateWaku)
handleProtocolChange.bind({}, waku)
);
};
}, [stateWaku]);
}, [waku]);
return (
<div
className="chat-app"
style={{ height: '100vh', width: '100vw', overflow: 'hidden' }}
>
<WakuContext.Provider value={{ waku: stateWaku }}>
<WakuContext.Provider value={{ waku: waku }}>
<ThemeProvider theme={themes}>
<Room
nick={nick}
newMessages={newMessages}
archivedMessages={archivedMessages}
fleetEnv={fleetEnv}
commandHandler={(input: string) => {
const { command, response } = handleCommand(
input,
stateWaku,
setNick
waku,
setNick,
fleetEnv,
setFleetEnv
);
const commandMessages = response.map((msg) => {
return Message.fromUtf8String(command, msg);
@ -178,7 +181,7 @@ export default function App() {
);
}
async function initWaku(setter: (waku: Waku) => void) {
async function initWaku(fleetEnv: Environment, setter: (waku: Waku) => void) {
try {
const waku = await Waku.create({
libp2p: {
@ -193,7 +196,7 @@ async function initWaku(setter: (waku: Waku) => void) {
setter(waku);
const nodes = await getNodes();
const nodes = await getStatusFleetNodes(fleetEnv);
await Promise.all(
nodes.map((addr) => {
return waku.dial(addr);
@ -204,11 +207,11 @@ async function initWaku(setter: (waku: Waku) => void) {
}
}
function getNodes() {
function defaultFleetEnv() {
// Works with react-scripts
if (process?.env?.NODE_ENV === 'development') {
return getStatusFleetNodes(Environment.Test);
return Environment.Test;
} else {
return getStatusFleetNodes(Environment.Prod);
return Environment.Prod;
}
}

View File

@ -1,4 +1,4 @@
import { ChatMessage, WakuMessage } from 'js-waku';
import { ChatMessage, Environment, WakuMessage } from 'js-waku';
import { ChatContentTopic } from './App';
import ChatList from './ChatList';
import MessageInput from './MessageInput';
@ -11,17 +11,28 @@ interface Props {
archivedMessages: Message[];
commandHandler: (cmd: string) => void;
nick: string;
fleetEnv: Environment;
}
export default function Room(props: Props) {
const { waku } = useWaku();
let relayPeers = 0;
let storePeers = 0;
if (waku) {
relayPeers = waku.relay.getPeers().size;
storePeers = waku.store.peers.length;
}
return (
<div
className="chat-container"
style={{ height: '98vh', display: 'flex', flexDirection: 'column' }}
>
<TitleBar title="Waku v2 chat app" />
<TitleBar
leftIcons={`Peers: ${relayPeers} relay, ${storePeers} store. Fleet: ${props.fleetEnv}`}
title="Waku v2 chat app"
/>
<ChatList
newMessages={props.newMessages}
archivedMessages={props.archivedMessages}

View File

@ -1,12 +1,13 @@
import { multiaddr } from 'multiaddr';
import PeerId from 'peer-id';
import { Waku } from 'js-waku';
import { Environment, Waku } from 'js-waku';
function help(): string[] {
return [
'/nick <nickname>: set a new nickname',
'/info: some information about the node',
'/connect <Multiaddr>: connect to the given peer',
'/fleet <prod|test>: connect to this fleet; beware it restarts waku node.',
'/help: Display this help',
];
}
@ -22,11 +23,14 @@ function nick(
return [`New nick: ${nick}`];
}
function info(waku: Waku | undefined): string[] {
function info(waku: Waku | undefined, fleetEnv: Environment): string[] {
if (!waku) {
return ['Waku node is starting'];
}
return [`PeerId: ${waku.libp2p.peerId.toB58String()}`];
return [
`PeerId: ${waku.libp2p.peerId.toB58String()}`,
`Fleet environment: ${fleetEnv}`,
];
}
function connect(peer: string | undefined, waku: Waku | undefined): string[] {
@ -78,6 +82,28 @@ function peers(waku: Waku | undefined): string[] {
return response;
}
function fleet(
newFleetEnv: string | undefined,
currFleetEnv: Environment,
setFleetEnv: (fleetEnv: Environment) => void
): string[] {
switch (newFleetEnv) {
case Environment.Test:
setFleetEnv(newFleetEnv);
break;
case Environment.Prod:
setFleetEnv(newFleetEnv);
break;
default:
return [
`Incorrect values, acceptable values are ${Environment.Test}, ${Environment.Prod}`,
`Current fleet environment is ${currFleetEnv}`,
];
}
return [`New fleet Environment: ${newFleetEnv}`];
}
function connections(waku: Waku | undefined): string[] {
if (!waku) {
return ['Waku node is starting'];
@ -107,7 +133,9 @@ function connections(waku: Waku | undefined): string[] {
export default function handleCommand(
input: string,
waku: Waku | undefined,
setNick: (nick: string) => void
setNick: (nick: string) => void,
currFleetEnv: Environment,
setFleetEnv: (fleetEnv: Environment) => void
): { command: string; response: string[] } {
let response: string[] = [];
const args = parseInput(input);
@ -120,7 +148,7 @@ export default function handleCommand(
nick(args.shift(), setNick).map((str) => response.push(str));
break;
case '/info':
info(waku).map((str) => response.push(str));
info(waku, currFleetEnv).map((str) => response.push(str));
break;
case '/connect':
connect(args.shift(), waku).map((str) => response.push(str));
@ -131,6 +159,11 @@ export default function handleCommand(
case '/connections':
connections(waku).map((str) => response.push(str));
break;
case '/fleet':
fleet(args.shift(), currFleetEnv, setFleetEnv).map((str) =>
response.push(str)
);
break;
default:
response.push(`Unknown Command '${command}'`);
}

View File

@ -3,8 +3,11 @@ export { getStatusFleetNodes, Environment, Protocol } from './lib/discover';
export * as utils from './lib/utils';
export { Waku } from './lib/waku';
export { WakuMessage } from './lib/waku_message';
export { generatePrivateKey, getPublicKey } from './lib/waku_message/version_1';
export { ChatMessage } from './lib/chat_message';
export {

View File

@ -40,7 +40,7 @@ export interface CreateOptions {
* Set keep alive frequency in seconds: Waku will send a ping request to each peer
* after the set number of seconds. Set to 0 to disable the keep alive feature
*
* @default 10
* @default 0
*/
keepAlive?: number;
/**

View File

@ -62,7 +62,7 @@ describe('Waku Message: Node only', function () {
const privateKey = generatePrivateKey();
waku.relay.addDecryptionPrivateKey(privateKey);
waku.relay.addDecryptionKey(privateKey);
const receivedMsgPromise: Promise<WakuMessage> = new Promise(
(resolve) => {
@ -118,7 +118,7 @@ describe('Waku Message: Node only', function () {
const symKey = generatePrivateKey();
waku.relay.addDecryptionPrivateKey(symKey);
waku.relay.addDecryptionKey(symKey);
const receivedMsgPromise: Promise<WakuMessage> = new Promise(
(resolve) => {

View File

@ -67,7 +67,7 @@ export class WakuRelay extends Gossipsub {
/**
* Decryption private keys to use to attempt decryption of incoming messages.
*/
public decPrivateKeys: Set<Uint8Array>;
public decryptionKeys: Set<Uint8Array>;
/**
* observers called when receiving new message.
@ -91,7 +91,7 @@ export class WakuRelay extends Gossipsub {
this.heartbeat = new RelayHeartbeat(this);
this.observers = {};
this.decPrivateKeys = new Set();
this.decryptionKeys = new Set();
const multicodecs = [constants.RelayCodec];
@ -124,21 +124,21 @@ export class WakuRelay extends Gossipsub {
}
/**
* Register a decryption private key to attempt decryption of messages of
* the given content topic. This can either be a private key for asymmetric
* encryption or a symmetric key. Waku relay will attempt to decrypt messages
* using both methods.
* Register a decryption private key or symmetric key to attempt decryption
* of messages received on the given content topic. This can either be a
* private key for asymmetric encryption or a symmetric key. Waku relay will
* attempt to decrypt messages using both methods.
*/
addDecryptionPrivateKey(privateKey: Uint8Array): void {
this.decPrivateKeys.add(privateKey);
addDecryptionKey(privateKey: Uint8Array): void {
this.decryptionKeys.add(privateKey);
}
/**
* Delete a decryption private key to attempt decryption of messages of
* the given content topic.
* Delete a decryption key to attempt decryption of messages received on the
* given content topic.
*/
deleteDecryptionPrivateKey(privateKey: Uint8Array): void {
this.decPrivateKeys.delete(privateKey);
deleteDecryptionKey(privateKey: Uint8Array): void {
this.decryptionKeys.delete(privateKey);
}
/**
@ -210,7 +210,7 @@ export class WakuRelay extends Gossipsub {
subscribe(pubsubTopic: string): void {
this.on(pubsubTopic, (event) => {
dbg(`Message received on ${pubsubTopic}`);
WakuMessage.decode(event.data, Array.from(this.decPrivateKeys))
WakuMessage.decode(event.data, Array.from(this.decryptionKeys))
.then((wakuMsg) => {
if (!wakuMsg) {
dbg('Failed to decode Waku Message');