mirror of https://github.com/waku-org/js-waku.git
Reduce Buffer usage in ENR module (#522)
This commit is contained in:
parent
9931011c93
commit
297d65ce03
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"extension": ["ts"],
|
||||
"spec": "src/**/*.spec.ts",
|
||||
"require": ["ts-node/register", "isomorphic-fetch"],
|
||||
"require": ["ts-node/register", "isomorphic-fetch", "jsdom-global/register"],
|
||||
"exit": true
|
||||
}
|
||||
|
|
|
@ -20,7 +20,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
### Removed
|
||||
|
||||
- axios dependency in favour of fetch.
|
||||
- `axios` dependency in favour of fetch.
|
||||
- `base64url` and `bigint-buffer` dependencies.
|
||||
|
||||
## [0.16.0] - 2022-01-31
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -51,15 +51,13 @@
|
|||
"doc:cname": "echo 'js-waku.wakuconnect.dev' > build/docs/CNAME",
|
||||
"doc:examples": "mkdir -p build/docs/examples",
|
||||
"deploy": "node ci/deploy.js",
|
||||
"reset-hard": "git clean -dfx && git reset --hard && npm i && npm run build && for d in examples/*; do (cd $d; npm i); done"
|
||||
"reset-hard": "git clean -dfx && git reset --hard && npm i && npm run build && for d in examples/*/; do (cd $d; npm i); done"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"dependencies": {
|
||||
"@chainsafe/libp2p-noise": "^5.0.0",
|
||||
"base64url": "^3.0.1",
|
||||
"bigint-buffer": "^1.1.5",
|
||||
"debug": "^4.3.1",
|
||||
"dns-query": "^0.8.0",
|
||||
"ecies-geth": "^1.5.2",
|
||||
|
@ -107,6 +105,8 @@
|
|||
"fast-check": "^2.14.0",
|
||||
"gh-pages": "^3.2.3",
|
||||
"isomorphic-fetch": "^3.0.0",
|
||||
"jsdom": "^19.0.0",
|
||||
"jsdom-global": "^3.0.2",
|
||||
"karma": "^6.3.12",
|
||||
"karma-chrome-launcher": "^3.1.0",
|
||||
"karma-env-preprocessor": "^0.1.1",
|
||||
|
|
|
@ -3,6 +3,8 @@ export * as discovery from "./lib/discovery";
|
|||
|
||||
export * as enr from "./lib/enr";
|
||||
|
||||
export * as utf8 from "./lib/utf8";
|
||||
|
||||
export * as utils from "./lib/utils";
|
||||
|
||||
export * as waku from "./lib/waku";
|
||||
|
|
|
@ -183,10 +183,7 @@ describe("DNS Node Discovery [live data]", function () {
|
|||
const maxQuantity = 3;
|
||||
|
||||
before(function () {
|
||||
if (
|
||||
process.env.CI ||
|
||||
(typeof window !== "undefined" && window?.__env__?.CI)
|
||||
) {
|
||||
if (process.env.CI || window?.__env__?.CI) {
|
||||
this.skip();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import assert from "assert";
|
||||
|
||||
import base64url from "base64url";
|
||||
import * as base32 from "hi-base32";
|
||||
import { ecdsaVerify } from "secp256k1";
|
||||
|
||||
import { ENR } from "../enr";
|
||||
import { keccak256Buf } from "../utils";
|
||||
import { base64ToBytes, keccak256Buf } from "../utils";
|
||||
|
||||
export type ENRRootValues = {
|
||||
eRoot: string;
|
||||
|
@ -43,15 +42,12 @@ export class ENRTree {
|
|||
// (Trailing recovery bit must be trimmed to pass `ecdsaVerify` method)
|
||||
const signedComponent = root.split(" sig")[0];
|
||||
const signedComponentBuffer = Buffer.from(signedComponent);
|
||||
const signatureBuffer = base64url
|
||||
.toBuffer(rootValues.signature)
|
||||
.slice(0, 64);
|
||||
const keyBuffer = Buffer.from(decodedPublicKey);
|
||||
const signatureBuffer = base64ToBytes(rootValues.signature).slice(0, 64);
|
||||
|
||||
const isVerified = ecdsaVerify(
|
||||
signatureBuffer,
|
||||
keccak256Buf(signedComponentBuffer),
|
||||
keyBuffer
|
||||
new Uint8Array(decodedPublicKey)
|
||||
);
|
||||
|
||||
assert(isVerified, "Unable to verify ENRTree root signature");
|
||||
|
|
|
@ -45,10 +45,7 @@ describe("Discovery", () => {
|
|||
|
||||
describe("Discovery [live data]", function () {
|
||||
before(function () {
|
||||
if (
|
||||
process.env.CI ||
|
||||
(typeof window !== "undefined" && window?.__env__?.CI)
|
||||
) {
|
||||
if (process.env.CI || window?.__env__?.CI) {
|
||||
this.skip();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -2,9 +2,9 @@ import { bytesToHex } from "../utils";
|
|||
|
||||
import { NodeId } from "./types";
|
||||
|
||||
export function createNodeId(buffer: Buffer): NodeId {
|
||||
if (buffer.length !== 32) {
|
||||
export function createNodeId(bytes: Uint8Array): NodeId {
|
||||
if (bytes.length !== 32) {
|
||||
throw new Error("NodeId must be 32 bytes in length");
|
||||
}
|
||||
return bytesToHex(buffer);
|
||||
return bytesToHex(bytes);
|
||||
}
|
||||
|
|
|
@ -28,8 +28,8 @@ describe("ENR", function () {
|
|||
"/onion3/vww6ybal4bd7szmgncyruucpgfkqahzddi37ktceo3ah7ngmcopnpyyd:1234/wss"
|
||||
),
|
||||
];
|
||||
const txt = enr.encodeTxt(keypair.privateKey);
|
||||
expect(txt.slice(0, 4)).to.be.equal("enr:");
|
||||
const txt = await enr.encodeTxt(keypair.privateKey);
|
||||
|
||||
const enr2 = ENR.decodeTxt(txt);
|
||||
expect(bytesToHex(enr2.signature as Buffer)).to.be.equal(
|
||||
bytesToHex(enr.signature as Buffer)
|
||||
|
@ -87,17 +87,21 @@ describe("ENR", function () {
|
|||
expect(enr.ip).to.not.be.undefined;
|
||||
expect(enr.ip).to.be.equal("134.209.139.210");
|
||||
expect(enr.publicKey).to.not.be.undefined;
|
||||
expect(enr.peerId.toB58String()).to.be.equal(
|
||||
expect(enr.peerId?.toB58String()).to.be.equal(
|
||||
"16Uiu2HAmPLe7Mzm8TsYUubgCAW1aJoeFScxrLj8ppHFivPo97bUZ"
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw error - no id", () => {
|
||||
it("should throw error - no id", async () => {
|
||||
try {
|
||||
const txt = Buffer.from(
|
||||
"656e723a2d435972595a62404b574342526c4179357a7a61445a584a42476b636e68344d486342465a6e75584e467264764a6a5830346a527a6a7a",
|
||||
"hex"
|
||||
).toString();
|
||||
const peerId = await PeerId.create({ keyType: "secp256k1" });
|
||||
const enr = ENR.createFromPeerId(peerId);
|
||||
const keypair = createKeypairFromPeerId(peerId);
|
||||
enr.setLocationMultiaddr(new Multiaddr("/ip4/18.223.219.100/udp/9000"));
|
||||
|
||||
enr.set("id", new Uint8Array([0]));
|
||||
const txt = await enr.encodeTxt(keypair.privateKey);
|
||||
|
||||
ENR.decodeTxt(txt);
|
||||
assert.fail("Expect error here");
|
||||
} catch (err: unknown) {
|
||||
|
@ -170,37 +174,6 @@ describe("ENR", function () {
|
|||
});
|
||||
});
|
||||
|
||||
describe("Fuzzing testcases", () => {
|
||||
it("should throw error in invalid signature", () => {
|
||||
const buf = Buffer.from(
|
||||
"656e723a2d4b7634514147774f54385374716d7749354c486149796d494f346f6f464b664e6b456a576130663150384f73456c67426832496a622d4772445f2d623957346b6350466377796e354845516d526371584e716470566f3168656f42683246306447356c64484f494141414141414141414143455a58526f4d704141414141414141414141505f5f5f5f5f5f5f5f5f5f676d6c6b676e5930676d6c7768424c663232534a6332566a634449314e6d73786f514a78436e4536765f7832656b67595f756f45317274777a76477934306d7139654436365866485042576749494e315a48437f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f434436410d0a",
|
||||
"hex"
|
||||
).toString();
|
||||
try {
|
||||
ENR.decodeTxt(buf);
|
||||
} catch (err: unknown) {
|
||||
const e = err as Error;
|
||||
expect(e.message).to.equal(
|
||||
"Decoded ENR invalid signature: must be a byte array"
|
||||
);
|
||||
}
|
||||
});
|
||||
it("should throw error in invalid sequence number", () => {
|
||||
const buf = Buffer.from(
|
||||
"656e723a2d495334514b6b33ff583945717841337838334162436979416e537550444d764b353264433530486d31584744643574457951684d3356634a4c2d5062446b44673541507a5f706f76763022d48dcf992d5379716b306e616e636f4e572d656e7263713042676d6c6b676e5930676d6c77684838414141474a6332566a634449314e6d73786f514d31453579557370397638516a397476335a575843766146427672504e647a384b5049314e68576651577a494e315a4843434239410a",
|
||||
"hex"
|
||||
).toString();
|
||||
try {
|
||||
ENR.decodeTxt(buf);
|
||||
} catch (err: unknown) {
|
||||
const e = err as Error;
|
||||
expect(e.message).to.equal(
|
||||
"Decoded ENR invalid sequence number: must be a byte array"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Static tests", () => {
|
||||
let privateKey: Buffer;
|
||||
let record: ENR;
|
||||
|
@ -212,9 +185,10 @@ describe("ENR", function () {
|
|||
"hex"
|
||||
);
|
||||
record = ENR.createV4(v4.publicKey(privateKey));
|
||||
record.set("ip", Buffer.from("7f000001", "hex"));
|
||||
record.set("udp", Buffer.from((30303).toString(16), "hex"));
|
||||
record.setLocationMultiaddr(new Multiaddr("/ip4/127.0.0.1/udp/30303"));
|
||||
record.seq = seq;
|
||||
// To set signature
|
||||
record.encode(privateKey);
|
||||
});
|
||||
|
||||
it("should properly compute the node id", () => {
|
||||
|
@ -228,7 +202,7 @@ describe("ENR", function () {
|
|||
expect(decoded).to.deep.equal(record);
|
||||
});
|
||||
|
||||
it("should encode/decode to text encoding", () => {
|
||||
it("should encode/decode to text encoding", async () => {
|
||||
// spec enr https://eips.ethereum.org/EIPS/eip-778
|
||||
const testTxt =
|
||||
"enr:-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOonrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8";
|
||||
|
@ -236,7 +210,8 @@ describe("ENR", function () {
|
|||
expect(decoded.udp).to.be.equal(30303);
|
||||
expect(decoded.ip).to.be.equal("127.0.0.1");
|
||||
expect(decoded).to.deep.equal(record);
|
||||
expect(record.encodeTxt(privateKey)).to.equal(testTxt);
|
||||
const recordTxt = await record.encodeTxt(privateKey);
|
||||
expect(recordTxt).to.equal(testTxt);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import base64url from "base64url";
|
||||
import { toBigIntBE } from "bigint-buffer";
|
||||
import { Multiaddr, protocols } from "multiaddr";
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: No types available
|
||||
|
@ -8,12 +6,10 @@ import PeerId from "peer-id";
|
|||
import * as RLP from "rlp";
|
||||
import { encode as varintEncode } from "varint";
|
||||
|
||||
import {
|
||||
ERR_INVALID_ID,
|
||||
ERR_NO_SIGNATURE,
|
||||
MAX_RECORD_SIZE,
|
||||
MULTIADDR_LENGTH_SIZE,
|
||||
} from "./constants";
|
||||
import { bytesToUtf8, utf8ToBytes } from "../utf8";
|
||||
import { base64ToBytes, bytesToBase64, bytesToHex } from "../utils";
|
||||
|
||||
import { ERR_INVALID_ID, ERR_NO_SIGNATURE, MAX_RECORD_SIZE } from "./constants";
|
||||
import {
|
||||
createKeypair,
|
||||
createKeypairFromPeerId,
|
||||
|
@ -21,28 +17,32 @@ import {
|
|||
IKeypair,
|
||||
KeypairType,
|
||||
} from "./keypair";
|
||||
import { decodeMultiaddrs, encodeMultiaddrs } from "./multiaddrs_codec";
|
||||
import { ENRKey, ENRValue, NodeId, SequenceNumber } from "./types";
|
||||
import * as v4 from "./v4";
|
||||
|
||||
export class ENR extends Map<ENRKey, ENRValue> {
|
||||
public static readonly RECORD_PREFIX = "enr:";
|
||||
public seq: SequenceNumber;
|
||||
public signature: Buffer | null;
|
||||
public signature: Uint8Array | null;
|
||||
|
||||
constructor(
|
||||
kvs: Record<ENRKey, ENRValue> = {},
|
||||
seq: SequenceNumber = 1n,
|
||||
signature: Buffer | null = null
|
||||
signature: Uint8Array | null = null
|
||||
) {
|
||||
super(Object.entries(kvs));
|
||||
this.seq = seq;
|
||||
this.signature = signature;
|
||||
}
|
||||
|
||||
static createV4(publicKey: Buffer, kvs: Record<ENRKey, ENRValue> = {}): ENR {
|
||||
static createV4(
|
||||
publicKey: Uint8Array,
|
||||
kvs: Record<ENRKey, ENRValue> = {}
|
||||
): ENR {
|
||||
return new ENR({
|
||||
...kvs,
|
||||
id: Buffer.from("v4"),
|
||||
id: utf8ToBytes("v4"),
|
||||
secp256k1: publicKey,
|
||||
});
|
||||
}
|
||||
|
@ -78,9 +78,13 @@ export class ENR extends Map<ENRKey, ENRValue> {
|
|||
}
|
||||
const obj: Record<ENRKey, ENRValue> = {};
|
||||
for (let i = 0; i < kvs.length; i += 2) {
|
||||
obj[kvs[i].toString()] = Buffer.from(kvs[i + 1]);
|
||||
obj[kvs[i].toString()] = new Uint8Array(kvs[i + 1]);
|
||||
}
|
||||
const enr = new ENR(obj, toBigIntBE(seq), signature);
|
||||
const enr = new ENR(
|
||||
obj,
|
||||
BigInt("0x" + bytesToHex(seq)),
|
||||
new Uint8Array(signature)
|
||||
);
|
||||
|
||||
if (!enr.verify(RLP.encode([seq, ...kvs]), signature)) {
|
||||
throw new Error("Unable to verify ENR signature");
|
||||
|
@ -88,7 +92,7 @@ export class ENR extends Map<ENRKey, ENRValue> {
|
|||
return enr;
|
||||
}
|
||||
|
||||
static decode(encoded: Buffer): ENR {
|
||||
static decode(encoded: Uint8Array): ENR {
|
||||
const decoded = RLP.decode(encoded) as unknown as Buffer[];
|
||||
return ENR.decodeFromValues(decoded);
|
||||
}
|
||||
|
@ -99,7 +103,7 @@ export class ENR extends Map<ENRKey, ENRValue> {
|
|||
`"string encoded ENR must start with '${this.RECORD_PREFIX}'`
|
||||
);
|
||||
}
|
||||
return ENR.decode(base64url.toBuffer(encoded.slice(4)));
|
||||
return ENR.decode(base64ToBytes(encoded.slice(4)));
|
||||
}
|
||||
|
||||
set(k: ENRKey, v: ENRValue): this {
|
||||
|
@ -109,9 +113,9 @@ export class ENR extends Map<ENRKey, ENRValue> {
|
|||
}
|
||||
|
||||
get id(): string {
|
||||
const id = this.get("id") as Buffer;
|
||||
const id = this.get("id");
|
||||
if (!id) throw new Error("id not found.");
|
||||
return id.toString("utf8");
|
||||
return bytesToUtf8(id);
|
||||
}
|
||||
|
||||
get keypairType(): KeypairType {
|
||||
|
@ -123,27 +127,31 @@ export class ENR extends Map<ENRKey, ENRValue> {
|
|||
}
|
||||
}
|
||||
|
||||
get publicKey(): Buffer {
|
||||
get publicKey(): Uint8Array | undefined {
|
||||
switch (this.id) {
|
||||
case "v4":
|
||||
return this.get("secp256k1") as Buffer;
|
||||
return this.get("secp256k1");
|
||||
default:
|
||||
throw new Error(ERR_INVALID_ID);
|
||||
}
|
||||
}
|
||||
|
||||
get keypair(): IKeypair {
|
||||
return createKeypair(this.keypairType, undefined, this.publicKey);
|
||||
get keypair(): IKeypair | undefined {
|
||||
if (this.publicKey) {
|
||||
const publicKey = this.publicKey;
|
||||
return createKeypair(this.keypairType, undefined, publicKey);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
get peerId(): PeerId {
|
||||
return createPeerIdFromKeypair(this.keypair);
|
||||
get peerId(): PeerId | undefined {
|
||||
return this.keypair ? createPeerIdFromKeypair(this.keypair) : undefined;
|
||||
}
|
||||
|
||||
get nodeId(): NodeId {
|
||||
get nodeId(): NodeId | undefined {
|
||||
switch (this.id) {
|
||||
case "v4":
|
||||
return v4.nodeId(this.publicKey);
|
||||
return this.publicKey ? v4.nodeId(this.publicKey) : undefined;
|
||||
default:
|
||||
throw new Error(ERR_INVALID_ID);
|
||||
}
|
||||
|
@ -266,32 +274,9 @@ export class ENR extends Map<ENRKey, ENRValue> {
|
|||
get multiaddrs(): Multiaddr[] | undefined {
|
||||
const raw = this.get("multiaddrs");
|
||||
|
||||
if (raw) {
|
||||
const multiaddrs = [];
|
||||
if (raw) return decodeMultiaddrs(raw);
|
||||
|
||||
try {
|
||||
let index = 0;
|
||||
|
||||
while (index < raw.length) {
|
||||
const sizeBytes = raw.slice(index, index + 2);
|
||||
const size = Buffer.from(sizeBytes).readUInt16BE(0);
|
||||
|
||||
const multiaddrBytes = raw.slice(
|
||||
index + MULTIADDR_LENGTH_SIZE,
|
||||
index + size + MULTIADDR_LENGTH_SIZE
|
||||
);
|
||||
const multiaddr = new Multiaddr(multiaddrBytes);
|
||||
|
||||
multiaddrs.push(multiaddr);
|
||||
index += size + MULTIADDR_LENGTH_SIZE;
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error("Invalid value in multiaddrs field");
|
||||
}
|
||||
return multiaddrs;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -303,37 +288,14 @@ export class ENR extends Map<ENRKey, ENRValue> {
|
|||
*
|
||||
* If the peer information only contains information that can be represented with the ENR pre-defined keys
|
||||
* (ip, tcp, etc) then the usage of [[setLocationMultiaddr]] should be preferred.
|
||||
*
|
||||
* The multiaddresses stored in this field must to be location multiaddresses, ie, peer id less.
|
||||
* The multiaddresses stored in this field must be location multiaddresses,
|
||||
* ie, without a peer id.
|
||||
*/
|
||||
set multiaddrs(multiaddrs: Multiaddr[] | undefined) {
|
||||
if (multiaddrs === undefined) {
|
||||
this.delete("multiaddrs");
|
||||
} else {
|
||||
let multiaddrsBuf = Buffer.from([]);
|
||||
|
||||
multiaddrs.forEach((multiaddr) => {
|
||||
if (multiaddr.getPeerId())
|
||||
throw new Error("`multiaddr` field MUST not contain peer id");
|
||||
|
||||
const bytes = multiaddr.bytes;
|
||||
|
||||
let buf = Buffer.alloc(2);
|
||||
|
||||
// Prepend the size of the next entry
|
||||
const written = buf.writeUInt16BE(bytes.length, 0);
|
||||
|
||||
if (written !== MULTIADDR_LENGTH_SIZE) {
|
||||
throw new Error(
|
||||
`Internal error: unsigned 16-bit integer was not written in ${MULTIADDR_LENGTH_SIZE} bytes`
|
||||
);
|
||||
}
|
||||
|
||||
buf = Buffer.concat([buf, bytes]);
|
||||
|
||||
multiaddrsBuf = Buffer.concat([multiaddrsBuf, buf]);
|
||||
});
|
||||
|
||||
const multiaddrsBuf = encodeMultiaddrs(multiaddrs);
|
||||
this.set("multiaddrs", multiaddrsBuf);
|
||||
}
|
||||
}
|
||||
|
@ -427,9 +389,13 @@ export class ENR extends Map<ENRKey, ENRValue> {
|
|||
getFullMultiaddr(
|
||||
protocol: "udp" | "udp4" | "udp6" | "tcp" | "tcp4" | "tcp6"
|
||||
): Multiaddr | undefined {
|
||||
const locationMultiaddr = this.getLocationMultiaddr(protocol);
|
||||
if (locationMultiaddr) {
|
||||
return locationMultiaddr.encapsulate(`/p2p/${this.peerId.toB58String()}`);
|
||||
if (this.peerId) {
|
||||
const locationMultiaddr = this.getLocationMultiaddr(protocol);
|
||||
if (locationMultiaddr) {
|
||||
return locationMultiaddr.encapsulate(
|
||||
`/p2p/${this.peerId.toB58String()}`
|
||||
);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
@ -438,15 +404,16 @@ export class ENR extends Map<ENRKey, ENRValue> {
|
|||
* Returns the full multiaddrs from the `multiaddrs` ENR field.
|
||||
*/
|
||||
getFullMultiaddrs(): Multiaddr[] {
|
||||
if (this.multiaddrs) {
|
||||
if (this.peerId && this.multiaddrs) {
|
||||
const peerId = this.peerId;
|
||||
return this.multiaddrs.map((ma) => {
|
||||
return ma.encapsulate(`/p2p/${this.peerId.toB58String()}`);
|
||||
return ma.encapsulate(`/p2p/${peerId.toB58String()}`);
|
||||
});
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
verify(data: Buffer, signature: Buffer): boolean {
|
||||
verify(data: Uint8Array, signature: Uint8Array): boolean {
|
||||
if (!this.get("id") || this.id !== "v4") {
|
||||
throw new Error(ERR_INVALID_ID);
|
||||
}
|
||||
|
@ -456,7 +423,7 @@ export class ENR extends Map<ENRKey, ENRValue> {
|
|||
return v4.verify(this.publicKey, data, signature);
|
||||
}
|
||||
|
||||
sign(data: Buffer, privateKey: Buffer): Buffer {
|
||||
sign(data: Uint8Array, privateKey: Uint8Array): Uint8Array {
|
||||
switch (this.id) {
|
||||
case "v4":
|
||||
this.signature = v4.sign(privateKey, data);
|
||||
|
@ -467,7 +434,7 @@ export class ENR extends Map<ENRKey, ENRValue> {
|
|||
return this.signature;
|
||||
}
|
||||
|
||||
encodeToValues(privateKey?: Buffer): (ENRKey | ENRValue | number)[] {
|
||||
encodeToValues(privateKey?: Uint8Array): (ENRKey | ENRValue | number)[] {
|
||||
// sort keys and flatten into [k, v, k, v, ...]
|
||||
const content: Array<ENRKey | ENRValue | number> = Array.from(this.keys())
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
|
@ -485,7 +452,7 @@ export class ENR extends Map<ENRKey, ENRValue> {
|
|||
return content;
|
||||
}
|
||||
|
||||
encode(privateKey?: Buffer): Buffer {
|
||||
encode(privateKey?: Uint8Array): Uint8Array {
|
||||
const encoded = RLP.encode(this.encodeToValues(privateKey));
|
||||
if (encoded.length >= MAX_RECORD_SIZE) {
|
||||
throw new Error("ENR must be less than 300 bytes");
|
||||
|
@ -493,7 +460,7 @@ export class ENR extends Map<ENRKey, ENRValue> {
|
|||
return encoded;
|
||||
}
|
||||
|
||||
encodeTxt(privateKey?: Buffer): string {
|
||||
return ENR.RECORD_PREFIX + base64url.encode(this.encode(privateKey));
|
||||
async encodeTxt(privateKey?: Uint8Array): Promise<string> {
|
||||
return ENR.RECORD_PREFIX + (await bytesToBase64(this.encode(privateKey)));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,8 +22,8 @@ export async function generateKeypair(type: KeypairType): Promise<IKeypair> {
|
|||
|
||||
export function createKeypair(
|
||||
type: KeypairType,
|
||||
privateKey?: Buffer,
|
||||
publicKey?: Buffer
|
||||
privateKey?: Uint8Array,
|
||||
publicKey?: Uint8Array
|
||||
): IKeypair {
|
||||
switch (type) {
|
||||
case KeypairType.secp256k1:
|
||||
|
|
|
@ -1,25 +1,26 @@
|
|||
import { Buffer } from "buffer";
|
||||
import crypto from "crypto";
|
||||
|
||||
import * as secp256k1 from "secp256k1";
|
||||
|
||||
import { AbstractKeypair, IKeypair, IKeypairClass, KeypairType } from "./types";
|
||||
|
||||
export function secp256k1PublicKeyToCompressed(publicKey: Uint8Array): Buffer {
|
||||
export function secp256k1PublicKeyToCompressed(
|
||||
publicKey: Uint8Array
|
||||
): Uint8Array {
|
||||
if (publicKey.length === 64) {
|
||||
publicKey = Buffer.concat([Buffer.from([4]), publicKey]);
|
||||
}
|
||||
return Buffer.from(secp256k1.publicKeyConvert(publicKey, true));
|
||||
}
|
||||
|
||||
export function secp256k1PublicKeyToFull(publicKey: Uint8Array): Buffer {
|
||||
export function secp256k1PublicKeyToFull(publicKey: Uint8Array): Uint8Array {
|
||||
if (publicKey.length === 64) {
|
||||
return Buffer.concat([Buffer.from([4]), publicKey]);
|
||||
}
|
||||
return Buffer.from(secp256k1.publicKeyConvert(publicKey, false));
|
||||
}
|
||||
|
||||
export function secp256k1PublicKeyToRaw(publicKey: Uint8Array): Buffer {
|
||||
export function secp256k1PublicKeyToRaw(publicKey: Uint8Array): Uint8Array {
|
||||
return Buffer.from(secp256k1.publicKeyConvert(publicKey, false).slice(1));
|
||||
}
|
||||
|
||||
|
@ -29,7 +30,7 @@ export const Secp256k1Keypair: IKeypairClass = class Secp256k1Keypair
|
|||
{
|
||||
readonly type: KeypairType;
|
||||
|
||||
constructor(privateKey?: Buffer, publicKey?: Buffer) {
|
||||
constructor(privateKey?: Uint8Array, publicKey?: Uint8Array) {
|
||||
let pub = publicKey;
|
||||
if (pub) {
|
||||
pub = secp256k1PublicKeyToCompressed(pub);
|
||||
|
@ -58,12 +59,12 @@ export const Secp256k1Keypair: IKeypairClass = class Secp256k1Keypair
|
|||
return true;
|
||||
}
|
||||
|
||||
sign(msg: Buffer): Buffer {
|
||||
sign(msg: Uint8Array): Uint8Array {
|
||||
const { signature, recid } = secp256k1.ecdsaSign(msg, this.privateKey);
|
||||
return Buffer.concat([signature, Buffer.from([recid])]);
|
||||
}
|
||||
|
||||
verify(msg: Buffer, sig: Buffer): boolean {
|
||||
verify(msg: Uint8Array, sig: Uint8Array): boolean {
|
||||
return secp256k1.ecdsaVerify(sig, msg, this.publicKey);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -6,25 +6,25 @@ export enum KeypairType {
|
|||
|
||||
export interface IKeypair {
|
||||
type: KeypairType;
|
||||
privateKey: Buffer;
|
||||
publicKey: Buffer;
|
||||
privateKey: Uint8Array;
|
||||
publicKey: Uint8Array;
|
||||
privateKeyVerify(): boolean;
|
||||
publicKeyVerify(): boolean;
|
||||
sign(msg: Buffer): Buffer;
|
||||
verify(msg: Buffer, sig: Buffer): boolean;
|
||||
sign(msg: Uint8Array): Uint8Array;
|
||||
verify(msg: Uint8Array, sig: Uint8Array): boolean;
|
||||
hasPrivateKey(): boolean;
|
||||
}
|
||||
|
||||
export interface IKeypairClass {
|
||||
new (privateKey?: Buffer, publicKey?: Buffer): IKeypair;
|
||||
new (privateKey?: Uint8Array, publicKey?: Uint8Array): IKeypair;
|
||||
generate(): Promise<IKeypair>;
|
||||
}
|
||||
|
||||
export abstract class AbstractKeypair {
|
||||
_privateKey?: Buffer;
|
||||
readonly _publicKey?: Buffer;
|
||||
_privateKey?: Uint8Array;
|
||||
readonly _publicKey?: Uint8Array;
|
||||
|
||||
constructor(privateKey?: Buffer, publicKey?: Buffer) {
|
||||
constructor(privateKey?: Uint8Array, publicKey?: Uint8Array) {
|
||||
if ((this._privateKey = privateKey) && !this.privateKeyVerify()) {
|
||||
throw new Error("Invalid private key");
|
||||
}
|
||||
|
@ -33,14 +33,14 @@ export abstract class AbstractKeypair {
|
|||
}
|
||||
}
|
||||
|
||||
get privateKey(): Buffer {
|
||||
get privateKey(): Uint8Array {
|
||||
if (!this._privateKey) {
|
||||
throw new Error();
|
||||
}
|
||||
return this._privateKey;
|
||||
}
|
||||
|
||||
get publicKey(): Buffer {
|
||||
get publicKey(): Uint8Array {
|
||||
if (!this._publicKey) {
|
||||
throw new Error();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
import { expect } from "chai";
|
||||
import { Multiaddr } from "multiaddr";
|
||||
|
||||
import { decodeMultiaddrs, encodeMultiaddrs } from "./multiaddrs_codec";
|
||||
|
||||
describe("ENR multiaddrs codec", function () {
|
||||
it("Sample", async () => {
|
||||
const multiaddrs = [
|
||||
new Multiaddr(
|
||||
"/dns4/node-01.do-ams3.wakuv2.test.statusim.net/tcp/443/wss"
|
||||
),
|
||||
new Multiaddr(
|
||||
"/dns6/node-01.ac-cn-hongkong-c.wakuv2.test.statusim.net/tcp/443/wss"
|
||||
),
|
||||
new Multiaddr(
|
||||
"/onion3/vww6ybal4bd7szmgncyruucpgfkqahzddi37ktceo3ah7ngmcopnpyyd:1234/wss"
|
||||
),
|
||||
];
|
||||
|
||||
const bytes = encodeMultiaddrs(multiaddrs);
|
||||
const result = decodeMultiaddrs(bytes);
|
||||
|
||||
const multiaddrsAsStr = result.map((ma) => ma.toString());
|
||||
expect(multiaddrsAsStr).to.include(
|
||||
"/dns4/node-01.do-ams3.wakuv2.test.statusim.net/tcp/443/wss"
|
||||
);
|
||||
expect(multiaddrsAsStr).to.include(
|
||||
"/dns6/node-01.ac-cn-hongkong-c.wakuv2.test.statusim.net/tcp/443/wss"
|
||||
);
|
||||
expect(multiaddrsAsStr).to.include(
|
||||
"/onion3/vww6ybal4bd7szmgncyruucpgfkqahzddi37ktceo3ah7ngmcopnpyyd:1234/wss"
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,50 @@
|
|||
import { Multiaddr } from "multiaddr";
|
||||
|
||||
import { MULTIADDR_LENGTH_SIZE } from "./constants";
|
||||
|
||||
export function decodeMultiaddrs(bytes: Uint8Array): Multiaddr[] {
|
||||
const multiaddrs = [];
|
||||
|
||||
let index = 0;
|
||||
|
||||
while (index < bytes.length) {
|
||||
const sizeDataView = new DataView(
|
||||
bytes.buffer,
|
||||
index,
|
||||
MULTIADDR_LENGTH_SIZE
|
||||
);
|
||||
const size = sizeDataView.getUint16(0);
|
||||
index += MULTIADDR_LENGTH_SIZE;
|
||||
|
||||
const multiaddrBytes = bytes.slice(index, index + size);
|
||||
index += size;
|
||||
|
||||
const multiaddr = new Multiaddr(multiaddrBytes);
|
||||
multiaddrs.push(multiaddr);
|
||||
}
|
||||
return multiaddrs;
|
||||
}
|
||||
|
||||
export function encodeMultiaddrs(multiaddrs: Multiaddr[]): Uint8Array {
|
||||
const totalLength = multiaddrs.reduce(
|
||||
(acc, ma) => acc + MULTIADDR_LENGTH_SIZE + ma.bytes.length,
|
||||
0
|
||||
);
|
||||
const bytes = new Uint8Array(totalLength);
|
||||
const dataView = new DataView(bytes.buffer);
|
||||
|
||||
let index = 0;
|
||||
multiaddrs.forEach((multiaddr) => {
|
||||
if (multiaddr.getPeerId())
|
||||
throw new Error("`multiaddr` field MUST not contain peer id");
|
||||
|
||||
// Prepend the size of the next entry
|
||||
dataView.setUint16(index, multiaddr.bytes.length);
|
||||
index += MULTIADDR_LENGTH_SIZE;
|
||||
|
||||
bytes.set(multiaddr.bytes, index);
|
||||
index += multiaddr.bytes.length;
|
||||
});
|
||||
|
||||
return bytes;
|
||||
}
|
|
@ -6,24 +6,28 @@ import * as secp256k1 from "secp256k1";
|
|||
import { createNodeId } from "./create";
|
||||
import { NodeId } from "./types";
|
||||
|
||||
export function hash(input: Uint8Array): Buffer {
|
||||
return Buffer.from(keccak256.arrayBuffer(input));
|
||||
export function hash(input: Uint8Array): Uint8Array {
|
||||
return new Uint8Array(keccak256.arrayBuffer(input));
|
||||
}
|
||||
|
||||
export async function createPrivateKey(): Promise<Buffer> {
|
||||
return Buffer.from(await randomBytes(32));
|
||||
export async function createPrivateKey(): Promise<Uint8Array> {
|
||||
return new Uint8Array(await randomBytes(32));
|
||||
}
|
||||
|
||||
export function publicKey(privKey: Uint8Array): Buffer {
|
||||
return Buffer.from(secp256k1.publicKeyCreate(privKey));
|
||||
export function publicKey(privKey: Uint8Array): Uint8Array {
|
||||
return new Uint8Array(secp256k1.publicKeyCreate(privKey));
|
||||
}
|
||||
|
||||
export function sign(privKey: Uint8Array, msg: Uint8Array): Buffer {
|
||||
export function sign(privKey: Uint8Array, msg: Uint8Array): Uint8Array {
|
||||
const { signature } = secp256k1.ecdsaSign(hash(msg), privKey);
|
||||
return Buffer.from(signature);
|
||||
return new Uint8Array(signature);
|
||||
}
|
||||
|
||||
export function verify(pubKey: Buffer, msg: Buffer, sig: Buffer): boolean {
|
||||
export function verify(
|
||||
pubKey: Uint8Array,
|
||||
msg: Uint8Array,
|
||||
sig: Uint8Array
|
||||
): boolean {
|
||||
// Remove the recovery id if present (byte #65)
|
||||
return secp256k1.ecdsaVerify(sig.slice(0, 64), hash(msg), pubKey);
|
||||
}
|
||||
|
@ -37,11 +41,11 @@ export function nodeId(pubKey: Uint8Array): NodeId {
|
|||
export class ENRKeyPair {
|
||||
public constructor(
|
||||
public readonly nodeId: NodeId,
|
||||
public readonly privateKey: Buffer,
|
||||
public readonly publicKey: Buffer
|
||||
public readonly privateKey: Uint8Array,
|
||||
public readonly publicKey: Uint8Array
|
||||
) {}
|
||||
|
||||
public static async create(privateKey?: Buffer): Promise<ENRKeyPair> {
|
||||
public static async create(privateKey?: Uint8Array): Promise<ENRKeyPair> {
|
||||
if (privateKey) {
|
||||
if (!secp256k1.privateKeyVerify(privateKey)) {
|
||||
throw new Error("Invalid private key");
|
||||
|
@ -54,11 +58,11 @@ export class ENRKeyPair {
|
|||
return new ENRKeyPair(_nodeId, _privateKey, _publicKey);
|
||||
}
|
||||
|
||||
public sign(msg: Buffer): Buffer {
|
||||
public sign(msg: Uint8Array): Uint8Array {
|
||||
return sign(this.privateKey, msg);
|
||||
}
|
||||
|
||||
public verify(msg: Buffer, sig: Buffer): boolean {
|
||||
public verify(msg: Uint8Array, sig: Uint8Array): boolean {
|
||||
return verify(this.publicKey, msg, sig);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
/**
|
||||
* Decode bytes to utf-8 string.
|
||||
*/
|
||||
// Thanks https://gist.github.com/pascaldekloe/62546103a1576803dade9269ccf76330
|
||||
export function bytesToUtf8(bytes: Uint8Array): string {
|
||||
let i = 0,
|
||||
|
@ -41,3 +44,42 @@ export function bytesToUtf8(bytes: Uint8Array): string {
|
|||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode utf-8 string to byte array
|
||||
*/
|
||||
// Thanks https://gist.github.com/pascaldekloe/62546103a1576803dade9269ccf76330
|
||||
export function utf8ToBytes(s: string): Uint8Array {
|
||||
let i = 0;
|
||||
const bytes = new Uint8Array(s.length * 4);
|
||||
for (let ci = 0; ci != s.length; ci++) {
|
||||
let c = s.charCodeAt(ci);
|
||||
if (c < 128) {
|
||||
bytes[i++] = c;
|
||||
continue;
|
||||
}
|
||||
if (c < 2048) {
|
||||
bytes[i++] = (c >> 6) | 192;
|
||||
} else {
|
||||
if (c > 0xd7ff && c < 0xdc00) {
|
||||
if (++ci >= s.length)
|
||||
throw new Error("UTF-8 encode: incomplete surrogate pair");
|
||||
const c2 = s.charCodeAt(ci);
|
||||
if (c2 < 0xdc00 || c2 > 0xdfff)
|
||||
throw new Error(
|
||||
"UTF-8 encode: second surrogate character 0x" +
|
||||
c2.toString(16) +
|
||||
" at index " +
|
||||
ci +
|
||||
" out of range"
|
||||
);
|
||||
c = 0x10000 + ((c & 0x03ff) << 10) + (c2 & 0x03ff);
|
||||
bytes[i++] = (c >> 18) | 240;
|
||||
bytes[i++] = ((c >> 12) & 63) | 128;
|
||||
} else bytes[i++] = (c >> 12) | 224;
|
||||
bytes[i++] = ((c >> 6) & 63) | 128;
|
||||
}
|
||||
bytes[i++] = (c & 63) | 128;
|
||||
}
|
||||
return bytes.subarray(0, i);
|
||||
}
|
|
@ -81,3 +81,49 @@ export function equalByteArrays(
|
|||
export function keccak256Buf(message: Message): Buffer {
|
||||
return Buffer.from(keccak256.arrayBuffer(message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert base64 string to byte array.
|
||||
*/
|
||||
export function base64ToBytes(base64: string): Uint8Array {
|
||||
const e = new Map<string, number>();
|
||||
|
||||
const len = base64.length;
|
||||
const res = [];
|
||||
const A = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
||||
for (let i = 0; i < 64; i++) {
|
||||
e.set(A.charAt(i), i);
|
||||
}
|
||||
e.set("+", 62);
|
||||
e.set("/", 63);
|
||||
|
||||
let b = 0,
|
||||
l = 0,
|
||||
a;
|
||||
for (let i = 0; i < len; i++) {
|
||||
const c = e.get(base64.charAt(i));
|
||||
if (c === undefined)
|
||||
throw new Error(`Invalid base64 character ${base64.charAt(i)}`);
|
||||
b = (b << 6) + c;
|
||||
l += 6;
|
||||
while (l >= 8) {
|
||||
((a = (b >>> (l -= 8)) & 0xff) || i < len - 2) && res.push(a);
|
||||
}
|
||||
}
|
||||
return new Uint8Array(res);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert byte array to base64 string.
|
||||
*/
|
||||
export async function bytesToBase64(bytes: Uint8Array): Promise<string> {
|
||||
const base64url: string = await new Promise((r) => {
|
||||
const reader = new window.FileReader();
|
||||
reader.onload = (): void => r(reader.result as string);
|
||||
reader.readAsDataURL(new Blob([bytes]));
|
||||
});
|
||||
const base64 = base64url.split(",", 2)[1];
|
||||
// We want URL and Filename Safe base64: https://datatracker.ietf.org/doc/html/rfc4648#section-5
|
||||
// Without trailing padding
|
||||
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
||||
}
|
||||
|
|
|
@ -19,10 +19,7 @@ describe("Waku Dial", function () {
|
|||
});
|
||||
|
||||
before(function () {
|
||||
if (
|
||||
process.env.CI ||
|
||||
(typeof window !== "undefined" && window?.__env__?.CI)
|
||||
) {
|
||||
if (process.env.CI || window?.__env__?.CI) {
|
||||
this.skip();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -2,13 +2,13 @@ import { expect } from "chai";
|
|||
import debug from "debug";
|
||||
|
||||
import {
|
||||
bytesToUtf8,
|
||||
makeLogFileName,
|
||||
NimWaku,
|
||||
NOISE_KEY_1,
|
||||
WakuRelayMessage,
|
||||
} from "../../test_utils";
|
||||
import { delay } from "../../test_utils/delay";
|
||||
import { bytesToUtf8 } from "../utf8";
|
||||
import { hexToBytes } from "../utils";
|
||||
import { Protocols, Waku } from "../waku";
|
||||
|
||||
|
|
|
@ -9,4 +9,3 @@ export * from "./async_fs";
|
|||
export * from "./constants";
|
||||
export * from "./log_file";
|
||||
export * from "./nim_waku";
|
||||
export * from "./utf8";
|
||||
|
|
|
@ -335,7 +335,6 @@ export class NimWaku {
|
|||
return { peerId: this.peerId, multiaddrWithId: this.multiaddrWithId };
|
||||
}
|
||||
const res = await this.info();
|
||||
console.log(res);
|
||||
this.multiaddrWithId = res.listenAddresses
|
||||
.map((ma) => multiaddr(ma))
|
||||
.find((ma) => ma.protoNames().includes("ws"));
|
||||
|
|
Loading…
Reference in New Issue