Merge pull request #324 from status-im/enr-multiaddrs

This commit is contained in:
F 2021-12-07 11:49:38 +11:00 committed by GitHub
commit e211d53d67
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 1376 additions and 3 deletions

View File

@ -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"
]
}

141
package-lock.json generated
View File

@ -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",

View File

@ -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"
},

10
src/lib/enr/constants.ts Normal file
View File

@ -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;

10
src/lib/enr/create.ts Normal file
View File

@ -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);
}

382
src/lib/enr/enr.spec.ts Normal file
View File

@ -0,0 +1,382 @@
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 multiaddrs successfully [shared test vector]', () => {
const txt =
'enr:-QEnuEBEAyErHEfhiQxAVQoWowGTCuEF9fKZtXSd7H_PymHFhGJA3rGAYDVSHKCyJDGRLBGsloNbS8AZF33IVuefjOO6BIJpZIJ2NIJpcIQS39tkim11bHRpYWRkcnO4lgAvNihub2RlLTAxLmRvLWFtczMud2FrdXYyLnRlc3Quc3RhdHVzaW0ubmV0BgG73gMAODcxbm9kZS0wMS5hYy1jbi1ob25na29uZy1jLndha3V2Mi50ZXN0LnN0YXR1c2ltLm5ldAYBu94DACm9A62t7AQL4Ef5ZYZosRpQTzFVAB8jGjf1TER2wH-0zBOe1-MDBNLeA4lzZWNwMjU2azGhAzfsxbxyCkgCqq8WwYsVWH7YkpMLnU2Bw5xJSimxKav-g3VkcIIjKA';
const enr = ENR.decodeTxt(txt);
expect(enr.multiaddrs).to.not.be.undefined;
expect(enr.multiaddrs!.length).to.be.equal(3);
const multiaddrsAsStr = enr.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 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;
});
});
});
});

476
src/lib/enr/enr.ts Normal file
View File

@ -0,0 +1,476 @@
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);
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));
}
}

6
src/lib/enr/index.ts Normal file
View File

@ -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';

View File

@ -0,0 +1,2 @@
export const ERR_TYPE_NOT_IMPLEMENTED = 'Keypair type not implemented';
export const ERR_INVALID_KEYPAIR_TYPE = 'Invalid keypair type';

View File

@ -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);
});
});

View File

@ -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)
);
}

View File

@ -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);
}
}

View File

@ -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);
}
}

11
src/lib/enr/types.ts Normal file
View File

@ -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;

75
src/lib/enr/v4.ts Normal file
View File

@ -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);
}
}

View File

@ -1,7 +1,7 @@
{
"compilerOptions": {
"incremental": true,
"target": "es2017",
"target": "es2020",
"outDir": "build/main",
"rootDir": "src",
"moduleResolution": "node",
@ -41,7 +41,7 @@
// "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */,
// "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */,
"lib": ["es2017", "dom"],
"lib": ["es2020", "dom"],
"types": ["node", "mocha"],
"typeRoots": ["node_modules/@types", "src/types"]
},