@ethereumjs/devp2p
Version:
A JavaScript implementation of ÐΞVp2p
183 lines • 6.1 kB
JavaScript
import { RLP } from '@ethereumjs/rlp';
import { EthereumJSErrorWithoutCode, bigIntToBytes, bytesToHex, bytesToInt, bytesToUtf8, concatBytes, intToBytes, setLengthLeft, } from '@ethereumjs/util';
import debugDefault from 'debug';
import { keccak256 } from 'ethereum-cryptography/keccak.js';
import { ecdsaRecover } from 'ethereum-cryptography/secp256k1-compat.js';
import { assertEq, ipToBytes, ipToString, isV4Format, isV6Format, unstrictDecode } from "../util.js";
import { secp256k1 } from 'ethereum-cryptography/secp256k1';
const debug = debugDefault('devp2p:dpt:server');
function getTimestamp() {
return (Date.now() / 1000) | 0;
}
const timestamp = {
encode(value = getTimestamp() + 60) {
const bytes = new Uint8Array(4);
new DataView(bytes.buffer).setUint32(0, value);
return bytes;
},
decode(bytes) {
if (bytes.length !== 4)
throw new RangeError(`Invalid timestamp bytes :${bytesToHex(bytes)}`);
return new DataView(bytes.buffer).getUint32(0);
},
};
const address = {
encode(value) {
if (isV4Format(value))
return ipToBytes(value);
if (isV6Format(value))
return ipToBytes(value);
throw EthereumJSErrorWithoutCode(`Invalid address: ${value}`);
},
decode(bytes) {
if (bytes.length === 4)
return ipToString(bytes);
if (bytes.length === 16)
return ipToString(bytes);
const str = bytesToUtf8(bytes);
if (isV4Format(str) || isV6Format(str))
return str;
// also can be host, but skip it right now (because need async function for resolve)
throw EthereumJSErrorWithoutCode(`Invalid address bytes: ${bytesToHex(bytes)}`);
},
};
const port = {
encode(value) {
if (value === null)
return new Uint8Array();
if (value >>> 16 > 0)
throw new RangeError(`Invalid port: ${value}`);
return Uint8Array.from([(value >>> 8) & 0xff, (value >>> 0) & 0xff]);
},
decode(bytes) {
if (bytes.length === 0)
return null;
return bytesToInt(bytes);
},
};
const endpoint = {
encode(obj) {
return [
address.encode(obj.address),
port.encode(obj.udpPort ?? null),
port.encode(obj.tcpPort ?? null),
];
},
decode(payload) {
return {
address: address.decode(payload[0]),
udpPort: port.decode(payload[1]),
tcpPort: port.decode(payload[2]),
};
},
};
const ping = {
encode(obj) {
return [
intToBytes(obj.version),
endpoint.encode(obj.from),
endpoint.encode(obj.to),
timestamp.encode(obj.timestamp),
];
},
decode(payload) {
return {
version: bytesToInt(payload[0]),
from: endpoint.decode(payload[1]),
to: endpoint.decode(payload[2]),
timestamp: timestamp.decode(payload[3]),
};
},
};
const pong = {
encode(obj) {
return [endpoint.encode(obj.to), obj.hash, timestamp.encode(obj.timestamp)];
},
decode(payload) {
return {
to: endpoint.decode(payload[0]),
hash: payload[1],
timestamp: timestamp.decode(payload[2]),
};
},
};
const findneighbours = {
encode(obj) {
return [obj.id, timestamp.encode(obj.timestamp)];
},
decode(payload) {
return {
id: payload[0],
timestamp: timestamp.decode(payload[1]),
};
},
};
const neighbours = {
encode(obj) {
return [
obj.peers.map((peer) => endpoint.encode(peer).concat(peer.id)),
timestamp.encode(obj.timestamp),
];
},
decode(payload) {
return {
peers: payload[0].map((data) => {
return { endpoint: endpoint.decode(data), id: data[3] }; // hack for id
}),
timestamp: timestamp.decode(payload[1]),
};
},
};
const messages = {
ping,
pong,
findneighbours,
neighbours,
};
const types = {
byName: {
ping: 0x01,
pong: 0x02,
findneighbours: 0x03,
neighbours: 0x04,
},
byType: {
0x01: 'ping',
0x02: 'pong',
0x03: 'findneighbours',
0x04: 'neighbours',
},
};
// [0, 32) data hash
// [32, 96) signature
// 96 recoveryId
// 97 type
// [98, length) data
export function encode(typename, data, privateKey, common) {
const type = types.byName[typename];
if (type === undefined)
throw EthereumJSErrorWithoutCode(`Invalid typename: ${typename}`);
const encodedMsg = messages[typename].encode(data);
const typedata = concatBytes(Uint8Array.from([type]), RLP.encode(encodedMsg));
const sighash = (common?.customCrypto.keccak256 ?? keccak256)(typedata);
const sig = (common?.customCrypto.ecsign ?? secp256k1.sign)(sighash, privateKey);
const hashdata = concatBytes(setLengthLeft(bigIntToBytes(sig.r), 32), setLengthLeft(bigIntToBytes(sig.s), 32), Uint8Array.from([sig.recovery]), typedata);
const hash = (common?.customCrypto.keccak256 ?? keccak256)(hashdata);
return concatBytes(hash, hashdata);
}
export function decode(bytes, common) {
const hash = (common?.customCrypto.keccak256 ?? keccak256)(bytes.subarray(32));
assertEq(bytes.subarray(0, 32), hash, 'Hash verification failed', debug);
const typedata = bytes.subarray(97);
const type = typedata[0];
const typename = types.byType[type];
if (typename === undefined)
throw EthereumJSErrorWithoutCode(`Invalid type: ${type}`);
const data = messages[typename].decode(unstrictDecode(typedata.subarray(1)));
const sighash = (common?.customCrypto.keccak256 ?? keccak256)(typedata);
const signature = bytes.subarray(32, 96);
const recoverId = bytes[96];
const publicKey = (common?.customCrypto.ecdsaRecover ?? ecdsaRecover)(signature, recoverId, sighash, false);
return { typename, data, publicKey };
}
//# sourceMappingURL=message.js.map