mirror of https://github.com/waku-org/js-waku.git
Add ENR with multiaddr
Imported from https://github.com/D4nte/discv5/pull/1 Replaced bcrypto backend with secp256k1.
This commit is contained in:
parent
96353fef0f
commit
01c66f7e89
|
@ -18,10 +18,13 @@
|
|||
"dependabot",
|
||||
"dingpu",
|
||||
"Dlazy",
|
||||
"dnsaddr",
|
||||
"Dout",
|
||||
"Dscore",
|
||||
"ecies",
|
||||
"editorconfig",
|
||||
"ENR",
|
||||
"ENRs",
|
||||
"ephem",
|
||||
"esnext",
|
||||
"ethersproject",
|
||||
|
@ -53,6 +56,7 @@
|
|||
"multiaddrs",
|
||||
"multicodecs",
|
||||
"mplex",
|
||||
"multihashes",
|
||||
"muxed",
|
||||
"muxer",
|
||||
"mvps",
|
||||
|
@ -82,6 +86,7 @@
|
|||
"untracked",
|
||||
"upgrader",
|
||||
"vacp",
|
||||
"varint",
|
||||
"waku",
|
||||
"wakuv",
|
||||
"wakunode",
|
||||
|
@ -99,6 +104,7 @@
|
|||
"node_modules/**",
|
||||
"build",
|
||||
"gen",
|
||||
"proto"
|
||||
"proto",
|
||||
"*.spec.ts"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -5,11 +5,14 @@
|
|||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "js-waku",
|
||||
"version": "0.14.2",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@chainsafe/libp2p-noise": "^4.1.1",
|
||||
"axios": "^0.21.1",
|
||||
"base64url": "^3.0.1",
|
||||
"bigint-buffer": "^1.1.5",
|
||||
"debug": "^4.3.1",
|
||||
"ecies-geth": "^1.5.2",
|
||||
"it-concat": "^2.0.0",
|
||||
|
@ -21,7 +24,9 @@
|
|||
"libp2p-mplex": "^0.10.4",
|
||||
"libp2p-websockets": "^0.16.1",
|
||||
"multiaddr": "^10.0.1",
|
||||
"multihashes": "^4.0.3",
|
||||
"protobufjs": "^6.8.8",
|
||||
"rlp": "^2.2.7",
|
||||
"secp256k1": "^4.0.2",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
|
@ -34,6 +39,7 @@
|
|||
"@types/secp256k1": "^4.0.2",
|
||||
"@types/tail": "^2.0.0",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"@types/varint": "^6.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.0.1",
|
||||
"@typescript-eslint/parser": "^4.0.1",
|
||||
"app-root-path": "^3.0.0",
|
||||
|
@ -69,6 +75,7 @@
|
|||
"typedoc": "^0.22.10",
|
||||
"typedoc-plugin-no-inherit": "^1.3.1",
|
||||
"typescript": "^4.0.2",
|
||||
"varint": "^6.0.0",
|
||||
"webpack": "^5.58.1",
|
||||
"webpack-cli": "^4.9.0"
|
||||
},
|
||||
|
@ -3031,6 +3038,15 @@
|
|||
"integrity": "sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/varint": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/varint/-/varint-6.0.0.tgz",
|
||||
"integrity": "sha512-2jBazyxGl4644tvu3VAez8UA/AtrcEetT9HOeAbqZ/vAcRVL/ZDFQjSS7rkWusU5cyONQVUz+nwwrNZdMva4ow==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/yargs": {
|
||||
"version": "16.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz",
|
||||
|
@ -4118,6 +4134,14 @@
|
|||
"node": "^4.5.0 || >= 5.9"
|
||||
}
|
||||
},
|
||||
"node_modules/base64url": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz",
|
||||
"integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bcrypt-pbkdf": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
|
||||
|
@ -4135,6 +4159,18 @@
|
|||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/bigint-buffer": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/bigint-buffer/-/bigint-buffer-1.1.5.tgz",
|
||||
"integrity": "sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"bindings": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bignumber.js": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.1.tgz",
|
||||
|
@ -11331,6 +11367,33 @@
|
|||
"resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.4.6.tgz",
|
||||
"integrity": "sha512-ngZRO82P7mPvw/3gu5NQ2QiUJGYTS0LAxvQnEAlWCJakvn7YpK2VAd9JWM5oosYUeqoVbkylH/FsqRc4fc2+ag=="
|
||||
},
|
||||
"node_modules/multihashes": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/multihashes/-/multihashes-4.0.3.tgz",
|
||||
"integrity": "sha512-0AhMH7Iu95XjDLxIeuCOOE4t9+vQZsACyKZ9Fxw2pcsRmlX4iCn1mby0hS0bb+nQOVpdQYWPpnyusw4da5RPhA==",
|
||||
"dependencies": {
|
||||
"multibase": "^4.0.1",
|
||||
"uint8arrays": "^3.0.0",
|
||||
"varint": "^5.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0",
|
||||
"npm": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/multihashes/node_modules/uint8arrays": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.0.0.tgz",
|
||||
"integrity": "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==",
|
||||
"dependencies": {
|
||||
"multiformats": "^9.4.2"
|
||||
}
|
||||
},
|
||||
"node_modules/multihashes/node_modules/varint": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/varint/-/varint-5.0.2.tgz",
|
||||
"integrity": "sha512-lKxKYG6H03yCZUpAGOPOsMcGxd1RHCu1iKvEHYDPmTyq2HueGhD73ssNBqqQWfvYs04G9iUFRvmAVLW20Jw6ow=="
|
||||
},
|
||||
"node_modules/multistream-select": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/multistream-select/-/multistream-select-2.0.0.tgz",
|
||||
|
@ -13886,6 +13949,22 @@
|
|||
"inherits": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/rlp": {
|
||||
"version": "2.2.7",
|
||||
"resolved": "https://registry.npmjs.org/rlp/-/rlp-2.2.7.tgz",
|
||||
"integrity": "sha512-d5gdPmgQ0Z+AklL2NVXr/IoSjNZFfTVvQWzL/AM2AOcSzYP2xjlb0AC8YyCLc41MSNf6P6QVtjgPdmVtzb+4lQ==",
|
||||
"dependencies": {
|
||||
"bn.js": "^5.2.0"
|
||||
},
|
||||
"bin": {
|
||||
"rlp": "bin/rlp"
|
||||
}
|
||||
},
|
||||
"node_modules/rlp/node_modules/bn.js": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz",
|
||||
"integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw=="
|
||||
},
|
||||
"node_modules/rsvp": {
|
||||
"version": "4.8.5",
|
||||
"resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz",
|
||||
|
@ -19548,6 +19627,15 @@
|
|||
"integrity": "sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/varint": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/varint/-/varint-6.0.0.tgz",
|
||||
"integrity": "sha512-2jBazyxGl4644tvu3VAez8UA/AtrcEetT9HOeAbqZ/vAcRVL/ZDFQjSS7rkWusU5cyONQVUz+nwwrNZdMva4ow==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/yargs": {
|
||||
"version": "16.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz",
|
||||
|
@ -20391,6 +20479,11 @@
|
|||
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
|
||||
"dev": true
|
||||
},
|
||||
"base64url": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz",
|
||||
"integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A=="
|
||||
},
|
||||
"bcrypt-pbkdf": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
|
||||
|
@ -20405,6 +20498,14 @@
|
|||
"integrity": "sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w==",
|
||||
"peer": true
|
||||
},
|
||||
"bigint-buffer": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/bigint-buffer/-/bigint-buffer-1.1.5.tgz",
|
||||
"integrity": "sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA==",
|
||||
"requires": {
|
||||
"bindings": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"bignumber.js": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.1.tgz",
|
||||
|
@ -26172,6 +26273,31 @@
|
|||
"resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.4.6.tgz",
|
||||
"integrity": "sha512-ngZRO82P7mPvw/3gu5NQ2QiUJGYTS0LAxvQnEAlWCJakvn7YpK2VAd9JWM5oosYUeqoVbkylH/FsqRc4fc2+ag=="
|
||||
},
|
||||
"multihashes": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/multihashes/-/multihashes-4.0.3.tgz",
|
||||
"integrity": "sha512-0AhMH7Iu95XjDLxIeuCOOE4t9+vQZsACyKZ9Fxw2pcsRmlX4iCn1mby0hS0bb+nQOVpdQYWPpnyusw4da5RPhA==",
|
||||
"requires": {
|
||||
"multibase": "^4.0.1",
|
||||
"uint8arrays": "^3.0.0",
|
||||
"varint": "^5.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"uint8arrays": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.0.0.tgz",
|
||||
"integrity": "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==",
|
||||
"requires": {
|
||||
"multiformats": "^9.4.2"
|
||||
}
|
||||
},
|
||||
"varint": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/varint/-/varint-5.0.2.tgz",
|
||||
"integrity": "sha512-lKxKYG6H03yCZUpAGOPOsMcGxd1RHCu1iKvEHYDPmTyq2HueGhD73ssNBqqQWfvYs04G9iUFRvmAVLW20Jw6ow=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"multistream-select": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/multistream-select/-/multistream-select-2.0.0.tgz",
|
||||
|
@ -28124,6 +28250,21 @@
|
|||
"inherits": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"rlp": {
|
||||
"version": "2.2.7",
|
||||
"resolved": "https://registry.npmjs.org/rlp/-/rlp-2.2.7.tgz",
|
||||
"integrity": "sha512-d5gdPmgQ0Z+AklL2NVXr/IoSjNZFfTVvQWzL/AM2AOcSzYP2xjlb0AC8YyCLc41MSNf6P6QVtjgPdmVtzb+4lQ==",
|
||||
"requires": {
|
||||
"bn.js": "^5.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"bn.js": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz",
|
||||
"integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"rsvp": {
|
||||
"version": "4.8.5",
|
||||
"resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz",
|
||||
|
|
|
@ -55,6 +55,8 @@
|
|||
"dependencies": {
|
||||
"@chainsafe/libp2p-noise": "^4.1.1",
|
||||
"axios": "^0.21.1",
|
||||
"base64url": "^3.0.1",
|
||||
"bigint-buffer": "^1.1.5",
|
||||
"debug": "^4.3.1",
|
||||
"ecies-geth": "^1.5.2",
|
||||
"it-concat": "^2.0.0",
|
||||
|
@ -66,7 +68,9 @@
|
|||
"libp2p-mplex": "^0.10.4",
|
||||
"libp2p-websockets": "^0.16.1",
|
||||
"multiaddr": "^10.0.1",
|
||||
"multihashes": "^4.0.3",
|
||||
"protobufjs": "^6.8.8",
|
||||
"rlp": "^2.2.7",
|
||||
"secp256k1": "^4.0.2",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
|
@ -79,6 +83,7 @@
|
|||
"@types/secp256k1": "^4.0.2",
|
||||
"@types/tail": "^2.0.0",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"@types/varint": "^6.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.0.1",
|
||||
"@typescript-eslint/parser": "^4.0.1",
|
||||
"app-root-path": "^3.0.0",
|
||||
|
@ -114,6 +119,7 @@
|
|||
"typedoc": "^0.22.10",
|
||||
"typedoc-plugin-no-inherit": "^1.3.1",
|
||||
"typescript": "^4.0.2",
|
||||
"varint": "^6.0.0",
|
||||
"webpack": "^5.58.1",
|
||||
"webpack-cli": "^4.9.0"
|
||||
},
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
// Maximum encoded size of an ENR
|
||||
export const MAX_RECORD_SIZE = 300;
|
||||
|
||||
export const ERR_INVALID_ID = 'Invalid record id';
|
||||
|
||||
export const ERR_NO_SIGNATURE = 'No valid signature found';
|
||||
|
||||
// The maximum length of byte size of a multiaddr to encode in the `multiaddr` field
|
||||
// The size is a big endian 16-bit unsigned integer
|
||||
export const MULTIADDR_LENGTH_SIZE = 2;
|
|
@ -0,0 +1,10 @@
|
|||
import { bufToHex } from '../utils';
|
||||
|
||||
import { NodeId } from './types';
|
||||
|
||||
export function createNodeId(buffer: Buffer): NodeId {
|
||||
if (buffer.length !== 32) {
|
||||
throw new Error('NodeId must be 32 bytes in length');
|
||||
}
|
||||
return bufToHex(buffer);
|
||||
}
|
|
@ -0,0 +1,363 @@
|
|||
import { assert, expect } from 'chai';
|
||||
import { Multiaddr } from 'multiaddr';
|
||||
import PeerId from 'peer-id';
|
||||
|
||||
import { bufToHex } from '../utils';
|
||||
|
||||
import { ERR_INVALID_ID } from './constants';
|
||||
import { ENR } from './enr';
|
||||
import { createKeypairFromPeerId } from './keypair';
|
||||
|
||||
import { v4 } from './index';
|
||||
|
||||
describe('ENR', function () {
|
||||
describe('decodeTxt', () => {
|
||||
it('should encodeTxt and decodeTxt', async () => {
|
||||
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.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 txt = enr.encodeTxt(keypair.privateKey);
|
||||
expect(txt.slice(0, 4)).to.be.equal('enr:');
|
||||
const enr2 = ENR.decodeTxt(txt);
|
||||
expect(bufToHex(enr2.signature as Buffer)).to.be.equal(
|
||||
bufToHex(enr.signature as Buffer)
|
||||
);
|
||||
const multiaddr = enr2.getLocationMultiaddr('udp')!;
|
||||
expect(multiaddr.toString()).to.be.equal('/ip4/18.223.219.100/udp/9000');
|
||||
expect(enr2.multiaddrs).to.not.be.undefined;
|
||||
expect(enr2.multiaddrs!.length).to.be.equal(3);
|
||||
const multiaddrsAsStr = enr2.multiaddrs!.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'
|
||||
);
|
||||
});
|
||||
|
||||
it('should decode valid enr successfully', () => {
|
||||
const txt =
|
||||
'enr:-Ku4QMh15cIjmnq-co5S3tYaNXxDzKTgj0ufusA-QfZ66EWHNsULt2kb0eTHoo1Dkjvvf6CAHDS1Di-htjiPFZzaIPcLh2F0dG5ldHOIAAAAAAAAAACEZXRoMpD2d10HAAABE________x8AgmlkgnY0gmlwhHZFkMSJc2VjcDI1NmsxoQIWSDEWdHwdEA3Lw2B_byeFQOINTZ0GdtF9DBjes6JqtIN1ZHCCIyg';
|
||||
const enr = ENR.decodeTxt(txt);
|
||||
const eth2 = enr.get('eth2') as Buffer;
|
||||
expect(eth2).to.not.be.undefined;
|
||||
expect(bufToHex(eth2)).to.be.equal('f6775d0700000113ffffffffffff1f00');
|
||||
});
|
||||
|
||||
it('should decode valid enr with tcp successfully', async () => {
|
||||
const txt =
|
||||
'enr:-IS4QAmC_o1PMi5DbR4Bh4oHVyQunZblg4bTaottPtBodAhJZvxVlWW-4rXITPNg4mwJ8cW__D9FBDc9N4mdhyMqB-EBgmlkgnY0gmlwhIbRi9KJc2VjcDI1NmsxoQOevTdO6jvv3fRruxguKR-3Ge4bcFsLeAIWEDjrfaigNoN0Y3CCdl8';
|
||||
const enr = ENR.decodeTxt(txt);
|
||||
expect(enr.tcp).to.not.be.undefined;
|
||||
expect(enr.tcp).to.be.equal(30303);
|
||||
expect(enr.ip).to.not.be.undefined;
|
||||
expect(enr.ip).to.be.equal('134.209.139.210');
|
||||
expect(enr.publicKey).to.not.be.undefined;
|
||||
const peerId = await enr.peerId();
|
||||
expect(peerId.toB58String()).to.be.equal(
|
||||
'16Uiu2HAmPLe7Mzm8TsYUubgCAW1aJoeFScxrLj8ppHFivPo97bUZ'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error - no id', () => {
|
||||
try {
|
||||
const txt = Buffer.from(
|
||||
'656e723a2d435972595a62404b574342526c4179357a7a61445a584a42476b636e68344d486342465a6e75584e467264764a6a5830346a527a6a7a',
|
||||
'hex'
|
||||
).toString();
|
||||
ENR.decodeTxt(txt);
|
||||
assert.fail('Expect error here');
|
||||
} catch (err) {
|
||||
expect(err.message).to.be.equal(ERR_INVALID_ID);
|
||||
}
|
||||
});
|
||||
|
||||
it('should throw error - no public key', () => {
|
||||
try {
|
||||
const txt =
|
||||
'enr:-IS4QJ2d11eu6dC7E7LoXeLMgMP3kom1u3SE8esFSWvaHoo0dP1jg8O3-nx9ht-EO3CmG7L6OkHcMmoIh00IYWB92QABgmlkgnY0gmlwhH8AAAGJc2d11eu6dCsxoQIB_c-jQMOXsbjWkbN-kj99H57gfId5pfb4wa1qxwV4CIN1ZHCCIyk';
|
||||
ENR.decodeTxt(txt);
|
||||
assert.fail('Expect error here');
|
||||
} catch (err) {
|
||||
expect(err.message).to.be.equal('Failed to verify enr: No public key');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('verify', () => {
|
||||
it('should throw error - no id', () => {
|
||||
try {
|
||||
const enr = new ENR({}, BigInt(0), Buffer.alloc(0));
|
||||
enr.verify(Buffer.alloc(0), Buffer.alloc(0));
|
||||
assert.fail('Expect error here');
|
||||
} catch (err) {
|
||||
expect(err.message).to.be.equal(ERR_INVALID_ID);
|
||||
}
|
||||
});
|
||||
|
||||
it('should throw error - invalid id', () => {
|
||||
try {
|
||||
const enr = new ENR(
|
||||
{ id: Buffer.from('v3') },
|
||||
BigInt(0),
|
||||
Buffer.alloc(0)
|
||||
);
|
||||
enr.verify(Buffer.alloc(0), Buffer.alloc(0));
|
||||
assert.fail('Expect error here');
|
||||
} catch (err) {
|
||||
expect(err.message).to.be.equal(ERR_INVALID_ID);
|
||||
}
|
||||
});
|
||||
|
||||
it('should throw error - no public key', () => {
|
||||
try {
|
||||
const enr = new ENR(
|
||||
{ id: Buffer.from('v4') },
|
||||
BigInt(0),
|
||||
Buffer.alloc(0)
|
||||
);
|
||||
enr.verify(Buffer.alloc(0), Buffer.alloc(0));
|
||||
assert.fail('Expect error here');
|
||||
} catch (err) {
|
||||
expect(err.message).to.be.equal('Failed to verify enr: No public key');
|
||||
}
|
||||
});
|
||||
|
||||
it('should return false', () => {
|
||||
const txt =
|
||||
'enr:-Ku4QMh15cIjmnq-co5S3tYaNXxDzKTgj0ufusA-QfZ66EWHNsULt2kb0eTHoo1Dkjvvf6CAHDS1Di-htjiPFZzaIPcLh2F0dG5ldHOIAAAAAAAAAACEZXRoMpD2d10HAAABE________x8AgmlkgnY0gmlwhHZFkMSJc2VjcDI1NmsxoQIWSDEWdHwdEA3Lw2B_byeFQOINTZ0GdtF9DBjes6JqtIN1ZHCCIyg';
|
||||
const enr = ENR.decodeTxt(txt);
|
||||
// should have id and public key inside ENR
|
||||
expect(enr.verify(Buffer.alloc(32), Buffer.alloc(64))).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('Fuzzing testcases', () => {
|
||||
it('should throw error in invalid signature', () => {
|
||||
const buf = Buffer.from(
|
||||
'656e723a2d4b7634514147774f54385374716d7749354c486149796d494f346f6f464b664e6b456a576130663150384f73456c67426832496a622d4772445f2d623957346b6350466377796e354845516d526371584e716470566f3168656f42683246306447356c64484f494141414141414141414143455a58526f4d704141414141414141414141505f5f5f5f5f5f5f5f5f5f676d6c6b676e5930676d6c7768424c663232534a6332566a634449314e6d73786f514a78436e4536765f7832656b67595f756f45317274777a76477934306d7139654436365866485042576749494e315a48437f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f434436410d0a',
|
||||
'hex'
|
||||
).toString();
|
||||
try {
|
||||
ENR.decodeTxt(buf);
|
||||
} catch (e) {
|
||||
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 (e) {
|
||||
expect(e.message).to.equal(
|
||||
'Decoded ENR invalid sequence number: must be a byte array'
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Static tests', () => {
|
||||
let privateKey: Buffer;
|
||||
let record: ENR;
|
||||
|
||||
beforeEach(() => {
|
||||
const seq = 1n;
|
||||
privateKey = Buffer.from(
|
||||
'b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291',
|
||||
'hex'
|
||||
);
|
||||
record = ENR.createV4(v4.publicKey(privateKey));
|
||||
record.set('ip', Buffer.from('7f000001', 'hex'));
|
||||
record.set('udp', Buffer.from((30303).toString(16), 'hex'));
|
||||
record.seq = seq;
|
||||
});
|
||||
|
||||
it('should properly compute the node id', () => {
|
||||
expect(record.nodeId).to.equal(
|
||||
'a448f24c6d18e575453db13171562b71999873db5b286df957af199ec94617f7'
|
||||
);
|
||||
});
|
||||
|
||||
it('should encode/decode to RLP encoding', () => {
|
||||
const decoded = ENR.decode(record.encode(privateKey));
|
||||
expect(decoded).to.deep.equal(record);
|
||||
});
|
||||
|
||||
it('should encode/decode to text encoding', () => {
|
||||
// spec enr https://eips.ethereum.org/EIPS/eip-778
|
||||
const testTxt =
|
||||
'enr:-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOonrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8';
|
||||
const decoded = ENR.decodeTxt(testTxt);
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Multiformats support', () => {
|
||||
let privateKey: Buffer;
|
||||
let record: ENR;
|
||||
|
||||
beforeEach(() => {
|
||||
privateKey = Buffer.from(
|
||||
'b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291',
|
||||
'hex'
|
||||
);
|
||||
record = ENR.createV4(v4.publicKey(privateKey));
|
||||
});
|
||||
|
||||
it('should get / set UDP multiaddr', () => {
|
||||
const multi0 = new Multiaddr('/ip4/127.0.0.1/udp/30303');
|
||||
const tuples0 = multi0.tuples();
|
||||
|
||||
if (!tuples0[0][1] || !tuples0[1][1]) {
|
||||
throw new Error('invalid multiaddr');
|
||||
}
|
||||
// set underlying records
|
||||
record.set('ip', tuples0[0][1]);
|
||||
record.set('udp', tuples0[1][1]);
|
||||
// and get the multiaddr
|
||||
expect(record.getLocationMultiaddr('udp')!.toString()).to.equal(
|
||||
multi0.toString()
|
||||
);
|
||||
// set the multiaddr
|
||||
const multi1 = new Multiaddr('/ip4/0.0.0.0/udp/30300');
|
||||
record.setLocationMultiaddr(multi1);
|
||||
// and get the multiaddr
|
||||
expect(record.getLocationMultiaddr('udp')!.toString()).to.equal(
|
||||
multi1.toString()
|
||||
);
|
||||
// and get the underlying records
|
||||
const tuples1 = multi1.tuples();
|
||||
expect(record.get('ip')).to.deep.equal(tuples1[0][1]);
|
||||
expect(record.get('udp')).to.deep.equal(tuples1[1][1]);
|
||||
});
|
||||
|
||||
it('should get / set TCP multiaddr', () => {
|
||||
const multi0 = new Multiaddr('/ip4/127.0.0.1/tcp/30303');
|
||||
const tuples0 = multi0.tuples();
|
||||
|
||||
if (!tuples0[0][1] || !tuples0[1][1]) {
|
||||
throw new Error('invalid multiaddr');
|
||||
}
|
||||
|
||||
// set underlying records
|
||||
record.set('ip', tuples0[0][1]);
|
||||
record.set('tcp', tuples0[1][1]);
|
||||
// and get the multiaddr
|
||||
expect(record.getLocationMultiaddr('tcp')!.toString()).to.equal(
|
||||
multi0.toString()
|
||||
);
|
||||
// set the multiaddr
|
||||
const multi1 = new Multiaddr('/ip4/0.0.0.0/tcp/30300');
|
||||
record.setLocationMultiaddr(multi1);
|
||||
// and get the multiaddr
|
||||
expect(record.getLocationMultiaddr('tcp')!.toString()).to.equal(
|
||||
multi1.toString()
|
||||
);
|
||||
// and get the underlying records
|
||||
const tuples1 = multi1.tuples();
|
||||
expect(record.get('ip')).to.deep.equal(tuples1[0][1]);
|
||||
expect(record.get('tcp')).to.deep.equal(tuples1[1][1]);
|
||||
});
|
||||
|
||||
describe('location multiaddr', async () => {
|
||||
const ip4 = '127.0.0.1';
|
||||
const ip6 = '::1';
|
||||
const tcp = 8080;
|
||||
const udp = 8080;
|
||||
|
||||
const peerId = await PeerId.create({ keyType: 'secp256k1' });
|
||||
const enr = ENR.createFromPeerId(peerId);
|
||||
enr.ip = ip4;
|
||||
enr.ip6 = ip6;
|
||||
enr.tcp = tcp;
|
||||
enr.udp = udp;
|
||||
enr.tcp6 = tcp;
|
||||
enr.udp6 = udp;
|
||||
|
||||
it('should properly create location multiaddrs - udp4', () => {
|
||||
expect(enr.getLocationMultiaddr('udp4')).to.deep.equal(
|
||||
new Multiaddr(`/ip4/${ip4}/udp/${udp}`)
|
||||
);
|
||||
});
|
||||
|
||||
it('should properly create location multiaddrs - tcp4', () => {
|
||||
expect(enr.getLocationMultiaddr('tcp4')).to.deep.equal(
|
||||
new Multiaddr(`/ip4/${ip4}/tcp/${tcp}`)
|
||||
);
|
||||
});
|
||||
|
||||
it('should properly create location multiaddrs - udp6', () => {
|
||||
expect(enr.getLocationMultiaddr('udp6')).to.deep.equal(
|
||||
new Multiaddr(`/ip6/${ip6}/udp/${udp}`)
|
||||
);
|
||||
});
|
||||
|
||||
it('should properly create location multiaddrs - tcp6', () => {
|
||||
expect(enr.getLocationMultiaddr('tcp6')).to.deep.equal(
|
||||
new Multiaddr(`/ip6/${ip6}/tcp/${tcp}`)
|
||||
);
|
||||
});
|
||||
|
||||
it('should properly create location multiaddrs - udp', () => {
|
||||
// default to ip4
|
||||
expect(enr.getLocationMultiaddr('udp')).to.deep.equal(
|
||||
new Multiaddr(`/ip4/${ip4}/udp/${udp}`)
|
||||
);
|
||||
// if ip6 is set, use it
|
||||
enr.ip = undefined;
|
||||
expect(enr.getLocationMultiaddr('udp')).to.deep.equal(
|
||||
new Multiaddr(`/ip6/${ip6}/udp/${udp}`)
|
||||
);
|
||||
// if ip6 does not exist, use ip4
|
||||
enr.ip6 = undefined;
|
||||
enr.ip = ip4;
|
||||
expect(enr.getLocationMultiaddr('udp')).to.deep.equal(
|
||||
new Multiaddr(`/ip4/${ip4}/udp/${udp}`)
|
||||
);
|
||||
enr.ip6 = ip6;
|
||||
});
|
||||
|
||||
it('should properly create location multiaddrs - tcp', () => {
|
||||
// default to ip4
|
||||
expect(enr.getLocationMultiaddr('tcp')).to.deep.equal(
|
||||
new Multiaddr(`/ip4/${ip4}/tcp/${tcp}`)
|
||||
);
|
||||
// if ip6 is set, use it
|
||||
enr.ip = undefined;
|
||||
expect(enr.getLocationMultiaddr('tcp')).to.deep.equal(
|
||||
new Multiaddr(`/ip6/${ip6}/tcp/${tcp}`)
|
||||
);
|
||||
// if ip6 does not exist, use ip4
|
||||
enr.ip6 = undefined;
|
||||
enr.ip = ip4;
|
||||
expect(enr.getLocationMultiaddr('tcp')).to.deep.equal(
|
||||
new Multiaddr(`/ip4/${ip4}/tcp/${tcp}`)
|
||||
);
|
||||
enr.ip6 = ip6;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,477 @@
|
|||
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
|
||||
import muConvert from 'multiaddr/src/convert';
|
||||
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 {
|
||||
createKeypair,
|
||||
createKeypairFromPeerId,
|
||||
createPeerIdFromKeypair,
|
||||
IKeypair,
|
||||
KeypairType,
|
||||
} from './keypair';
|
||||
import { ENRKey, ENRValue, NodeId, SequenceNumber } from './types';
|
||||
import * as v4 from './v4';
|
||||
|
||||
export class ENR extends Map<ENRKey, ENRValue> {
|
||||
public seq: SequenceNumber;
|
||||
public signature: Buffer | null;
|
||||
|
||||
constructor(
|
||||
kvs: Record<ENRKey, ENRValue> = {},
|
||||
seq: SequenceNumber = 1n,
|
||||
signature: Buffer | null = null
|
||||
) {
|
||||
super(Object.entries(kvs));
|
||||
this.seq = seq;
|
||||
this.signature = signature;
|
||||
}
|
||||
|
||||
static createV4(publicKey: Buffer, kvs: Record<ENRKey, ENRValue> = {}): ENR {
|
||||
return new ENR({
|
||||
...kvs,
|
||||
id: Buffer.from('v4'),
|
||||
secp256k1: publicKey,
|
||||
});
|
||||
}
|
||||
|
||||
static createFromPeerId(
|
||||
peerId: PeerId,
|
||||
kvs: Record<ENRKey, ENRValue> = {}
|
||||
): ENR {
|
||||
const keypair = createKeypairFromPeerId(peerId);
|
||||
switch (keypair.type) {
|
||||
case KeypairType.secp256k1:
|
||||
return ENR.createV4(keypair.publicKey, kvs);
|
||||
default:
|
||||
throw new Error();
|
||||
}
|
||||
}
|
||||
|
||||
static decodeFromValues(decoded: Buffer[]): ENR {
|
||||
if (!Array.isArray(decoded)) {
|
||||
throw new Error('Decoded ENR must be an array');
|
||||
}
|
||||
if (decoded.length % 2 !== 0) {
|
||||
throw new Error('Decoded ENR must have an even number of elements');
|
||||
}
|
||||
const [signature, seq, ...kvs] = decoded;
|
||||
if (!signature || Array.isArray(signature)) {
|
||||
throw new Error('Decoded ENR invalid signature: must be a byte array');
|
||||
}
|
||||
if (!seq || Array.isArray(seq)) {
|
||||
throw new Error(
|
||||
'Decoded ENR invalid sequence number: must be a byte array'
|
||||
);
|
||||
}
|
||||
const obj: Record<ENRKey, ENRValue> = {};
|
||||
for (let i = 0; i < kvs.length; i += 2) {
|
||||
obj[kvs[i].toString()] = Buffer.from(kvs[i + 1]);
|
||||
}
|
||||
const enr = new ENR(obj, toBigIntBE(seq), signature);
|
||||
console.log('signature', signature.length, signature);
|
||||
|
||||
if (!enr.verify(RLP.encode([seq, ...kvs]), signature)) {
|
||||
throw new Error('Unable to verify ENR signature');
|
||||
}
|
||||
return enr;
|
||||
}
|
||||
|
||||
static decode(encoded: Buffer): ENR {
|
||||
const decoded = RLP.decode(encoded) as unknown as Buffer[];
|
||||
return ENR.decodeFromValues(decoded);
|
||||
}
|
||||
|
||||
static decodeTxt(encoded: string): ENR {
|
||||
if (!encoded.startsWith('enr:')) {
|
||||
throw new Error("string encoded ENR must start with 'enr:'");
|
||||
}
|
||||
return ENR.decode(base64url.toBuffer(encoded.slice(4)));
|
||||
}
|
||||
|
||||
set(k: ENRKey, v: ENRValue): this {
|
||||
this.signature = null;
|
||||
this.seq++;
|
||||
return super.set(k, v);
|
||||
}
|
||||
|
||||
get id(): string {
|
||||
const id = this.get('id') as Buffer;
|
||||
if (!id) throw new Error('id not found.');
|
||||
return id.toString('utf8');
|
||||
}
|
||||
|
||||
get keypairType(): KeypairType {
|
||||
switch (this.id) {
|
||||
case 'v4':
|
||||
return KeypairType.secp256k1;
|
||||
default:
|
||||
throw new Error(ERR_INVALID_ID);
|
||||
}
|
||||
}
|
||||
|
||||
get publicKey(): Buffer {
|
||||
switch (this.id) {
|
||||
case 'v4':
|
||||
return this.get('secp256k1') as Buffer;
|
||||
default:
|
||||
throw new Error(ERR_INVALID_ID);
|
||||
}
|
||||
}
|
||||
|
||||
get keypair(): IKeypair {
|
||||
return createKeypair(this.keypairType, undefined, this.publicKey);
|
||||
}
|
||||
|
||||
async peerId(): Promise<PeerId> {
|
||||
return createPeerIdFromKeypair(this.keypair);
|
||||
}
|
||||
|
||||
get nodeId(): NodeId {
|
||||
switch (this.id) {
|
||||
case 'v4':
|
||||
return v4.nodeId(this.publicKey);
|
||||
default:
|
||||
throw new Error(ERR_INVALID_ID);
|
||||
}
|
||||
}
|
||||
|
||||
get ip(): string | undefined {
|
||||
const raw = this.get('ip');
|
||||
if (raw) {
|
||||
return muConvert.toString(protocols.names.ip4.code, raw) as string;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
set ip(ip: string | undefined) {
|
||||
if (ip) {
|
||||
this.set('ip', muConvert.toBytes(protocols.names.ip4.code, ip));
|
||||
} else {
|
||||
this.delete('ip');
|
||||
}
|
||||
}
|
||||
|
||||
get tcp(): number | undefined {
|
||||
const raw = this.get('tcp');
|
||||
if (raw) {
|
||||
return Number(muConvert.toString(protocols.names.tcp.code, raw));
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
set tcp(port: number | undefined) {
|
||||
if (port === undefined) {
|
||||
this.delete('tcp');
|
||||
} else {
|
||||
this.set('tcp', muConvert.toBytes(protocols.names.tcp.code, port));
|
||||
}
|
||||
}
|
||||
|
||||
get udp(): number | undefined {
|
||||
const raw = this.get('udp');
|
||||
if (raw) {
|
||||
return Number(muConvert.toString(protocols.names.udp.code, raw));
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
set udp(port: number | undefined) {
|
||||
if (port === undefined) {
|
||||
this.delete('udp');
|
||||
} else {
|
||||
this.set('udp', muConvert.toBytes(protocols.names.udp.code, port));
|
||||
}
|
||||
}
|
||||
|
||||
get ip6(): string | undefined {
|
||||
const raw = this.get('ip6');
|
||||
if (raw) {
|
||||
return muConvert.toString(protocols.names.ip6.code, raw) as string;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
set ip6(ip: string | undefined) {
|
||||
if (ip) {
|
||||
this.set('ip6', muConvert.toBytes(protocols.names.ip6.code, ip));
|
||||
} else {
|
||||
this.delete('ip6');
|
||||
}
|
||||
}
|
||||
|
||||
get tcp6(): number | undefined {
|
||||
const raw = this.get('tcp6');
|
||||
if (raw) {
|
||||
return Number(muConvert.toString(protocols.names.tcp.code, raw));
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
set tcp6(port: number | undefined) {
|
||||
if (port === undefined) {
|
||||
this.delete('tcp6');
|
||||
} else {
|
||||
this.set('tcp6', muConvert.toBytes(protocols.names.tcp.code, port));
|
||||
}
|
||||
}
|
||||
|
||||
get udp6(): number | undefined {
|
||||
const raw = this.get('udp6');
|
||||
if (raw) {
|
||||
return Number(muConvert.toString(protocols.names.udp.code, raw));
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
set udp6(port: number | undefined) {
|
||||
if (port === undefined) {
|
||||
this.delete('udp6');
|
||||
} else {
|
||||
this.set('udp6', muConvert.toBytes(protocols.names.udp.code, port));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the `multiaddrs` field from ENR.
|
||||
*
|
||||
* This field is used to store multiaddresses that cannot be stored with the current ENR pre-defined keys.
|
||||
* These can be a multiaddresses that include encapsulation (e.g. wss) or do not use `ip4` nor `ip6` for the host
|
||||
* address (e.g. `dns4`, `dnsaddr`, etc)..
|
||||
*
|
||||
* If the peer information only contains information that can be represented with the ENR pre-defined keys
|
||||
* (ip, tcp, etc) then the usage of [[getLocationMultiaddr]] should be preferred.
|
||||
*
|
||||
* The multiaddresses stored in this field are expected to be location multiaddresses, ie, peer id less.
|
||||
*/
|
||||
get multiaddrs(): Multiaddr[] | undefined {
|
||||
const raw = this.get('multiaddrs');
|
||||
|
||||
if (raw) {
|
||||
const multiaddrs = [];
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the `multiaddrs` field on the ENR.
|
||||
*
|
||||
* This field is used to store multiaddresses that cannot be stored with the current ENR pre-defined keys.
|
||||
* These can be a multiaddresses that include encapsulation (e.g. wss) or do not use `ip4` nor `ip6` for the host
|
||||
* address (e.g. `dns4`, `dnsaddr`, etc)..
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
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]);
|
||||
});
|
||||
|
||||
this.set('multiaddrs', multiaddrsBuf);
|
||||
}
|
||||
}
|
||||
|
||||
getLocationMultiaddr(
|
||||
protocol: 'udp' | 'udp4' | 'udp6' | 'tcp' | 'tcp4' | 'tcp6'
|
||||
): Multiaddr | undefined {
|
||||
if (protocol === 'udp') {
|
||||
return (
|
||||
this.getLocationMultiaddr('udp4') || this.getLocationMultiaddr('udp6')
|
||||
);
|
||||
}
|
||||
if (protocol === 'tcp') {
|
||||
return (
|
||||
this.getLocationMultiaddr('tcp4') || this.getLocationMultiaddr('tcp6')
|
||||
);
|
||||
}
|
||||
const isIpv6 = protocol.endsWith('6');
|
||||
const ipVal = this.get(isIpv6 ? 'ip6' : 'ip');
|
||||
if (!ipVal) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const isUdp = protocol.startsWith('udp');
|
||||
const isTcp = protocol.startsWith('tcp');
|
||||
let protoName, protoVal;
|
||||
if (isUdp) {
|
||||
protoName = 'udp';
|
||||
protoVal = isIpv6 ? this.get('udp6') : this.get('udp');
|
||||
} else if (isTcp) {
|
||||
protoName = 'tcp';
|
||||
protoVal = isIpv6 ? this.get('tcp6') : this.get('tcp');
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
if (!protoVal) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Create raw multiaddr buffer
|
||||
// multiaddr length is:
|
||||
// 1 byte for the ip protocol (ip4 or ip6)
|
||||
// N bytes for the ip address
|
||||
// 1 or 2 bytes for the protocol as buffer (tcp or udp)
|
||||
// 2 bytes for the port
|
||||
const ipMa = protocols.names[isIpv6 ? 'ip6' : 'ip4'];
|
||||
const ipByteLen = ipMa.size / 8;
|
||||
const protoMa = protocols.names[protoName];
|
||||
const protoBuf = varintEncode(protoMa.code);
|
||||
const maBuf = new Uint8Array(3 + ipByteLen + protoBuf.length);
|
||||
maBuf[0] = ipMa.code;
|
||||
maBuf.set(ipVal, 1);
|
||||
maBuf.set(protoBuf, 1 + ipByteLen);
|
||||
maBuf.set(protoVal, 1 + ipByteLen + protoBuf.length);
|
||||
|
||||
return new Multiaddr(maBuf);
|
||||
}
|
||||
setLocationMultiaddr(multiaddr: Multiaddr): void {
|
||||
const protoNames = multiaddr.protoNames();
|
||||
if (
|
||||
protoNames.length !== 2 &&
|
||||
protoNames[1] !== 'udp' &&
|
||||
protoNames[1] !== 'tcp'
|
||||
) {
|
||||
throw new Error('Invalid multiaddr');
|
||||
}
|
||||
const tuples = multiaddr.tuples();
|
||||
if (!tuples[0][1] || !tuples[1][1]) {
|
||||
throw new Error('Invalid multiaddr');
|
||||
}
|
||||
|
||||
// IPv4
|
||||
if (tuples[0][0] === 4) {
|
||||
this.set('ip', tuples[0][1]);
|
||||
this.set(protoNames[1], tuples[1][1]);
|
||||
} else {
|
||||
this.set('ip6', tuples[0][1]);
|
||||
this.set(protoNames[1] + '6', tuples[1][1]);
|
||||
}
|
||||
}
|
||||
|
||||
async getFullMultiaddr(
|
||||
protocol: 'udp' | 'udp4' | 'udp6' | 'tcp' | 'tcp4' | 'tcp6'
|
||||
): Promise<Multiaddr | undefined> {
|
||||
const locationMultiaddr = this.getLocationMultiaddr(protocol);
|
||||
if (locationMultiaddr) {
|
||||
const peerId = await this.peerId();
|
||||
return locationMultiaddr.encapsulate(`/p2p/${peerId.toB58String()}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
verify(data: Buffer, signature: Buffer): boolean {
|
||||
if (!this.get('id') || this.id !== 'v4') {
|
||||
throw new Error(ERR_INVALID_ID);
|
||||
}
|
||||
if (!this.publicKey) {
|
||||
throw new Error('Failed to verify enr: No public key');
|
||||
}
|
||||
return v4.verify(this.publicKey, data, signature);
|
||||
}
|
||||
|
||||
sign(data: Buffer, privateKey: Buffer): Buffer {
|
||||
switch (this.id) {
|
||||
case 'v4':
|
||||
this.signature = v4.sign(privateKey, data);
|
||||
break;
|
||||
default:
|
||||
throw new Error(ERR_INVALID_ID);
|
||||
}
|
||||
return this.signature;
|
||||
}
|
||||
|
||||
encodeToValues(privateKey?: Buffer): (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))
|
||||
.map((k) => [k, this.get(k)] as [ENRKey, ENRValue])
|
||||
.flat();
|
||||
content.unshift(Number(this.seq));
|
||||
if (privateKey) {
|
||||
content.unshift(this.sign(RLP.encode(content), privateKey));
|
||||
} else {
|
||||
if (!this.signature) {
|
||||
throw new Error(ERR_NO_SIGNATURE);
|
||||
}
|
||||
content.unshift(this.signature);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
encode(privateKey?: Buffer): Buffer {
|
||||
const encoded = RLP.encode(this.encodeToValues(privateKey));
|
||||
if (encoded.length >= MAX_RECORD_SIZE) {
|
||||
throw new Error('ENR must be less than 300 bytes');
|
||||
}
|
||||
return encoded;
|
||||
}
|
||||
|
||||
encodeTxt(privateKey?: Buffer): string {
|
||||
return 'enr:' + base64url.encode(this.encode(privateKey));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import * as v4Crypto from './v4';
|
||||
export const v4 = v4Crypto;
|
||||
export * from './constants';
|
||||
export * from './enr';
|
||||
export * from './types';
|
||||
export * from './create';
|
|
@ -0,0 +1,2 @@
|
|||
export const ERR_TYPE_NOT_IMPLEMENTED = 'Keypair type not implemented';
|
||||
export const ERR_INVALID_KEYPAIR_TYPE = 'Invalid keypair type';
|
|
@ -0,0 +1,40 @@
|
|||
import { expect } from 'chai';
|
||||
import { keys } from 'libp2p-crypto';
|
||||
import PeerId from 'peer-id';
|
||||
|
||||
import {
|
||||
AbstractKeypair,
|
||||
createPeerIdFromKeypair,
|
||||
generateKeypair,
|
||||
KeypairType,
|
||||
} from './index';
|
||||
|
||||
const { supportedKeys } = keys;
|
||||
|
||||
describe('createPeerIdFromKeypair', function () {
|
||||
it('should properly create a PeerId from a secp256k1 keypair with private key', async function () {
|
||||
const keypair = await generateKeypair(KeypairType.secp256k1);
|
||||
const privKey = new supportedKeys.secp256k1.Secp256k1PrivateKey(
|
||||
keypair.privateKey,
|
||||
keypair.publicKey
|
||||
);
|
||||
|
||||
const expectedPeerId = await PeerId.createFromPrivKey(privKey.bytes);
|
||||
const actualPeerId = await createPeerIdFromKeypair(keypair);
|
||||
|
||||
expect(actualPeerId).to.be.deep.equal(expectedPeerId);
|
||||
});
|
||||
|
||||
it('should properly create a PeerId from a secp256k1 keypair without private key', async function () {
|
||||
const keypair = await generateKeypair(KeypairType.secp256k1);
|
||||
delete (keypair as AbstractKeypair)._privateKey;
|
||||
const pubKey = new supportedKeys.secp256k1.Secp256k1PublicKey(
|
||||
keypair.publicKey
|
||||
);
|
||||
|
||||
const expectedPeerId = await PeerId.createFromPubKey(pubKey.bytes);
|
||||
const actualPeerId = await createPeerIdFromKeypair(keypair);
|
||||
|
||||
expect(actualPeerId).to.be.deep.equal(expectedPeerId);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,68 @@
|
|||
import { keys } from 'libp2p-crypto';
|
||||
import mh from 'multihashes';
|
||||
import PeerId from 'peer-id';
|
||||
|
||||
const { keysPBM, supportedKeys } = keys;
|
||||
|
||||
import { ERR_TYPE_NOT_IMPLEMENTED } from './constants';
|
||||
import { Secp256k1Keypair } from './secp256k1';
|
||||
import { IKeypair, KeypairType } from './types';
|
||||
|
||||
export * from './types';
|
||||
export * from './secp256k1';
|
||||
|
||||
export async function generateKeypair(type: KeypairType): Promise<IKeypair> {
|
||||
switch (type) {
|
||||
case KeypairType.secp256k1:
|
||||
return await Secp256k1Keypair.generate();
|
||||
default:
|
||||
throw new Error(ERR_TYPE_NOT_IMPLEMENTED);
|
||||
}
|
||||
}
|
||||
|
||||
export function createKeypair(
|
||||
type: KeypairType,
|
||||
privateKey?: Buffer,
|
||||
publicKey?: Buffer
|
||||
): IKeypair {
|
||||
switch (type) {
|
||||
case KeypairType.secp256k1:
|
||||
return new Secp256k1Keypair(privateKey, publicKey);
|
||||
default:
|
||||
throw new Error(ERR_TYPE_NOT_IMPLEMENTED);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createPeerIdFromKeypair(
|
||||
keypair: IKeypair
|
||||
): Promise<PeerId> {
|
||||
switch (keypair.type) {
|
||||
case KeypairType.secp256k1: {
|
||||
// manually create a peer id to avoid expensive ops
|
||||
const privKey = keypair.hasPrivateKey()
|
||||
? new supportedKeys.secp256k1.Secp256k1PrivateKey(
|
||||
keypair.privateKey,
|
||||
keypair.publicKey
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const pubKey = new supportedKeys.secp256k1.Secp256k1PublicKey(
|
||||
keypair.publicKey
|
||||
);
|
||||
const id = mh.encode(pubKey.bytes, 'identity');
|
||||
return new PeerId(id, privKey, pubKey);
|
||||
}
|
||||
default:
|
||||
throw new Error(ERR_TYPE_NOT_IMPLEMENTED);
|
||||
}
|
||||
}
|
||||
|
||||
export function createKeypairFromPeerId(peerId: PeerId): IKeypair {
|
||||
// pub/private key bytes from peer-id are encoded in protobuf format
|
||||
const pub = keysPBM.PublicKey.decode(peerId.pubKey.bytes);
|
||||
return createKeypair(
|
||||
pub.Type as KeypairType,
|
||||
peerId.privKey ? Buffer.from(peerId.privKey.marshal()) : undefined,
|
||||
Buffer.from(pub.Data)
|
||||
);
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
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 {
|
||||
if (publicKey.length === 64) {
|
||||
publicKey = Buffer.concat([Buffer.from([4]), publicKey]);
|
||||
}
|
||||
return Buffer.from(secp256k1.publicKeyConvert(publicKey, true));
|
||||
}
|
||||
|
||||
export function secp256k1PublicKeyToFull(publicKey: Uint8Array): Buffer {
|
||||
if (publicKey.length === 64) {
|
||||
return Buffer.concat([Buffer.from([4]), publicKey]);
|
||||
}
|
||||
return Buffer.from(secp256k1.publicKeyConvert(publicKey, false));
|
||||
}
|
||||
|
||||
export function secp256k1PublicKeyToRaw(publicKey: Uint8Array): Buffer {
|
||||
return Buffer.from(secp256k1.publicKeyConvert(publicKey, false).slice(1));
|
||||
}
|
||||
|
||||
export const Secp256k1Keypair: IKeypairClass = class Secp256k1Keypair
|
||||
extends AbstractKeypair
|
||||
implements IKeypair
|
||||
{
|
||||
readonly type: KeypairType;
|
||||
|
||||
constructor(privateKey?: Buffer, publicKey?: Buffer) {
|
||||
let pub = publicKey;
|
||||
if (pub) {
|
||||
pub = secp256k1PublicKeyToCompressed(pub);
|
||||
}
|
||||
super(privateKey, pub);
|
||||
this.type = KeypairType.secp256k1;
|
||||
}
|
||||
|
||||
static async generate(): Promise<Secp256k1Keypair> {
|
||||
const privateKey = Buffer.from(await randomBytes(32));
|
||||
const publicKey = secp256k1.publicKeyCreate(privateKey);
|
||||
return new Secp256k1Keypair(privateKey, Buffer.from(publicKey));
|
||||
}
|
||||
|
||||
privateKeyVerify(key = this._privateKey): boolean {
|
||||
if (key) {
|
||||
return secp256k1.privateKeyVerify(key);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
publicKeyVerify(key = this._publicKey): boolean {
|
||||
if (key) {
|
||||
return secp256k1.publicKeyVerify(key);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
sign(msg: Buffer): Buffer {
|
||||
const { signature, recid } = secp256k1.ecdsaSign(msg, this.privateKey);
|
||||
return Buffer.concat([signature, Buffer.from([recid])]);
|
||||
}
|
||||
|
||||
verify(msg: Buffer, sig: Buffer): boolean {
|
||||
return secp256k1.ecdsaVerify(sig, msg, this.publicKey);
|
||||
}
|
||||
};
|
||||
|
||||
function randomBytes(length: number): Uint8Array {
|
||||
if (typeof window !== 'undefined' && window && window.crypto) {
|
||||
const array = new Uint8Array(length);
|
||||
window.crypto.getRandomValues(array);
|
||||
return array;
|
||||
} else {
|
||||
return crypto.randomBytes(length);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
export enum KeypairType {
|
||||
rsa = 0,
|
||||
ed25519 = 1,
|
||||
secp256k1 = 2,
|
||||
}
|
||||
|
||||
export interface IKeypair {
|
||||
type: KeypairType;
|
||||
privateKey: Buffer;
|
||||
publicKey: Buffer;
|
||||
privateKeyVerify(): boolean;
|
||||
publicKeyVerify(): boolean;
|
||||
sign(msg: Buffer): Buffer;
|
||||
verify(msg: Buffer, sig: Buffer): boolean;
|
||||
hasPrivateKey(): boolean;
|
||||
}
|
||||
|
||||
export interface IKeypairClass {
|
||||
new (privateKey?: Buffer, publicKey?: Buffer): IKeypair;
|
||||
generate(): Promise<IKeypair>;
|
||||
}
|
||||
|
||||
export abstract class AbstractKeypair {
|
||||
_privateKey?: Buffer;
|
||||
readonly _publicKey?: Buffer;
|
||||
|
||||
constructor(privateKey?: Buffer, publicKey?: Buffer) {
|
||||
if ((this._privateKey = privateKey) && !this.privateKeyVerify()) {
|
||||
throw new Error('Invalid private key');
|
||||
}
|
||||
if ((this._publicKey = publicKey) && !this.publicKeyVerify()) {
|
||||
throw new Error('Invalid private key');
|
||||
}
|
||||
}
|
||||
|
||||
get privateKey(): Buffer {
|
||||
if (!this._privateKey) {
|
||||
throw new Error();
|
||||
}
|
||||
return this._privateKey;
|
||||
}
|
||||
|
||||
get publicKey(): Buffer {
|
||||
if (!this._publicKey) {
|
||||
throw new Error();
|
||||
}
|
||||
return this._publicKey;
|
||||
}
|
||||
|
||||
privateKeyVerify(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
publicKeyVerify(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
hasPrivateKey(): boolean {
|
||||
return Boolean(this._privateKey);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
// Custom and aliased types for ENRs
|
||||
|
||||
/**
|
||||
* We represent NodeId as a hex string, since node equality is used very heavily
|
||||
* and it is convenient to index data by NodeId
|
||||
*/
|
||||
export type NodeId = string;
|
||||
export type SequenceNumber = bigint;
|
||||
|
||||
export type ENRKey = string;
|
||||
export type ENRValue = Uint8Array;
|
|
@ -0,0 +1,75 @@
|
|||
import { Buffer } from 'buffer';
|
||||
import crypto from 'crypto';
|
||||
|
||||
import { keccak256 } from 'js-sha3';
|
||||
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 async function createPrivateKey(): Promise<Buffer> {
|
||||
return Buffer.from(await randomBytes(32));
|
||||
}
|
||||
|
||||
export function publicKey(privKey: Uint8Array): Buffer {
|
||||
return Buffer.from(secp256k1.publicKeyCreate(privKey));
|
||||
}
|
||||
|
||||
export function sign(privKey: Uint8Array, msg: Uint8Array): Buffer {
|
||||
const { signature } = secp256k1.ecdsaSign(hash(msg), privKey);
|
||||
return Buffer.from(signature);
|
||||
}
|
||||
|
||||
export function verify(pubKey: Buffer, msg: Buffer, sig: Buffer): boolean {
|
||||
// Remove the recovery id if present (byte #65)
|
||||
return secp256k1.ecdsaVerify(sig.slice(0, 64), hash(msg), pubKey);
|
||||
}
|
||||
|
||||
export function nodeId(pubKey: Uint8Array): NodeId {
|
||||
const uncompressedPubkey = secp256k1.publicKeyConvert(pubKey, false);
|
||||
|
||||
return createNodeId(hash(uncompressedPubkey.slice(1)));
|
||||
}
|
||||
|
||||
export class ENRKeyPair {
|
||||
public constructor(
|
||||
public readonly nodeId: NodeId,
|
||||
public readonly privateKey: Buffer,
|
||||
public readonly publicKey: Buffer
|
||||
) {}
|
||||
|
||||
public static async create(privateKey?: Buffer): Promise<ENRKeyPair> {
|
||||
if (privateKey) {
|
||||
if (!secp256k1.privateKeyVerify(privateKey)) {
|
||||
throw new Error('Invalid private key');
|
||||
}
|
||||
}
|
||||
const _privateKey = privateKey || (await createPrivateKey());
|
||||
const _publicKey = publicKey(_privateKey);
|
||||
const _nodeId = nodeId(_publicKey);
|
||||
|
||||
return new ENRKeyPair(_nodeId, _privateKey, _publicKey);
|
||||
}
|
||||
|
||||
public sign(msg: Buffer): Buffer {
|
||||
return sign(this.privateKey, msg);
|
||||
}
|
||||
|
||||
public verify(msg: Buffer, sig: Buffer): boolean {
|
||||
return verify(this.publicKey, msg, sig);
|
||||
}
|
||||
}
|
||||
|
||||
function randomBytes(length: number): Uint8Array {
|
||||
if (typeof window !== 'undefined' && window && window.crypto) {
|
||||
const array = new Uint8Array(length);
|
||||
window.crypto.getRandomValues(array);
|
||||
return array;
|
||||
} else {
|
||||
return crypto.randomBytes(length);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue