From d4a3627b0945fe1441262ef3b7345c34e5cc6d6d Mon Sep 17 00:00:00 2001 From: holgerd77 Date: Wed, 1 Jul 2020 13:49:06 +0200 Subject: [PATCH] Added support for Eth64 protocol version --- examples/peer-communication-les.ts | 2 +- examples/peer-communication.ts | 4 +- package.json | 2 +- src/eth/index.ts | 110 +++++++++++++++++++++++++---- src/rlpx/peer.ts | 2 +- src/rlpx/rlpx.ts | 2 +- test/integration/eth-simulator.ts | 77 +++++++++++++++----- test/integration/les-simulator.ts | 2 +- test/integration/util.ts | 4 +- 9 files changed, 164 insertions(+), 41 deletions(-) diff --git a/examples/peer-communication-les.ts b/examples/peer-communication-les.ts index 8657f81..53888b1 100644 --- a/examples/peer-communication-les.ts +++ b/examples/peer-communication-les.ts @@ -17,7 +17,7 @@ const GENESIS_HASH = Buffer.from( 'hex', ) -const Common = require('ethereumjs-common').default +const Common = require('@ethereumjs/common').default const common = new Common('rinkeby') const bootstrapNodes = common.bootstrapNodes() const BOOTNODES = bootstrapNodes.map((node: any) => { diff --git a/examples/peer-communication.ts b/examples/peer-communication.ts index 4507e24..b2fd0c9 100644 --- a/examples/peer-communication.ts +++ b/examples/peer-communication.ts @@ -11,7 +11,7 @@ import rlp from 'rlp-encoding' const PRIVATE_KEY = randomBytes(32) -const Common = require('ethereumjs-common').default +const Common = require('@ethereumjs/common').default const common = new Common('mainnet') const bootstrapNodes = common.bootstrapNodes() const BOOTNODES = bootstrapNodes.map((node: any) => { @@ -61,7 +61,7 @@ dpt.on('error', err => console.error(chalk.red(`DPT error: ${err}`))) const rlpx = new devp2p.RLPx(PRIVATE_KEY, { dpt: dpt, maxPeers: 25, - capabilities: [devp2p.ETH.eth63, devp2p.ETH.eth62], + capabilities: [devp2p.ETH.eth64], common: common, remoteClientIdFilter: REMOTE_CLIENTID_FILTER, listenPort: null, diff --git a/package.json b/package.json index 711a4ac..6d75fc6 100644 --- a/package.json +++ b/package.json @@ -53,10 +53,10 @@ "test": "node_modules/tape/bin/tape -r ts-node/register ./test/index.ts" }, "dependencies": { + "@ethereumjs/common": "file:ethereumjs-common-1.5.1.tgz", "babel-runtime": "^6.11.6", "bl": "^1.1.2", "debug": "^2.2.0", - "ethereumjs-common": "^1.5.1", "inherits": "^2.0.1", "ip": "^1.1.3", "k-bucket": "^3.2.1", diff --git a/src/eth/index.ts b/src/eth/index.ts index a1aeb17..2ac504f 100644 --- a/src/eth/index.ts +++ b/src/eth/index.ts @@ -1,3 +1,4 @@ +import assert from 'assert' import { EventEmitter } from 'events' import rlp from 'rlp-encoding' import ms from 'ms' @@ -18,6 +19,12 @@ export class ETH extends EventEmitter { _statusTimeoutId: NodeJS.Timeout _send: SendMethod + // Eth64 + _hardfork: string = 'chainstart' + _latestBlock: number = 0 + _forkHash: string = '' + _nextForkBlock: number = 0 + constructor(version: number, peer: Peer, send: SendMethod) { super() @@ -30,10 +37,24 @@ export class ETH extends EventEmitter { this._statusTimeoutId = setTimeout(() => { this._peer.disconnect(DISCONNECT_REASONS.TIMEOUT) }, ms('5s')) + + // Set forkHash and nextForkBlock + if (this._version >= 64) { + const c = this._peer._common + this._hardfork = c.hardfork() ? (c.hardfork() as string) : this._hardfork + // Set latestBlock minimally to start block of fork to have some more + // accurate basis if no latestBlock is provided along status send + this._latestBlock = c.hardforkBlock(this._hardfork) + this._forkHash = c.forkHash(this._hardfork) + // Next fork block number or 0 if none available + const nextForkBlock = c.nextHardforkBlock(this._hardfork) + this._nextForkBlock = nextForkBlock ? nextForkBlock : 0 + } } static eth62 = { name: 'eth', version: 62, length: 8, constructor: ETH } static eth63 = { name: 'eth', version: 63, length: 17, constructor: ETH } + static eth64 = { name: 'eth', version: 64, length: 29, constructor: ETH } _handleMessage(code: ETH.MESSAGE_CODES, data: any) { const payload = rlp.decode(data) @@ -80,6 +101,42 @@ export class ETH extends EventEmitter { this.emit('message', code, payload) } + /** + * Eth 64 Fork ID validation (EIP-2124) + * @param forkId Remote fork ID + */ + _validateForkId(forkId: Buffer[]) { + const c = this._peer._common + + const peerForkHash =`0x${forkId[0].toString('hex')}` + const peerNextFork = buffer2int(forkId[1]) + + if (this._forkHash === peerForkHash) { + if (peerNextFork) { + if (this._latestBlock >= peerNextFork) { + const msg = 'Remote is advertising a future fork that passed locally' + debug(msg) + throw new assert.AssertionError({ message: msg }) + } + + } + } + const peerFork: any = c.hardforkForForkHash(peerForkHash) + if (peerFork === null) { + const msg = 'Unknown fork hash' + debug(msg) + throw new assert.AssertionError({ message: msg }) + } + + if (!c.hardforkGteHardfork(peerFork.name, this._hardfork)) { + if (peerNextFork === null || c.nextHardforkBlock(peerFork.name) !== peerNextFork) { + const msg = 'Outdated fork status, remote needs software update' + debug(msg) + throw new assert.AssertionError({ message: msg }) + } + } + } + _handleStatus(): void { if (this._status === null || this._peerStatus === null) return clearTimeout(this._statusTimeoutId) @@ -88,26 +145,48 @@ export class ETH extends EventEmitter { assertEq(this._status[1], this._peerStatus[1], 'NetworkId mismatch', debug) assertEq(this._status[4], this._peerStatus[4], 'Genesis block mismatch', debug) - this.emit('status', { + let status: any = { networkId: this._peerStatus[1], td: Buffer.from(this._peerStatus[2]), bestHash: Buffer.from(this._peerStatus[3]), genesisHash: Buffer.from(this._peerStatus[4]), - }) + } + + if (this._version >= 64) { + assertEq(this._peerStatus[5].length, 2, 'Incorrect forkId msg format', debug) + this._validateForkId(this._peerStatus[5] as Buffer[]) + console.log(`Successful Eth64 validation with ${this._peer._socket.remoteAddress}`) + status['forkId'] = this._peerStatus[5] + } + + this.emit('status', status) } getVersion() { return this._version } + _forkHashFromForkId(forkId: Buffer): string { + return `0x${forkId.toString('hex')}` + } + + _nextForkFromForkId(forkId: Buffer): number { + return buffer2int(forkId) + } + _getStatusString(status: ETH.StatusMsg) { - let sStr = `[V:${buffer2int(status[0])}, NID:${buffer2int(status[1])}, TD:${buffer2int( - status[2], + let sStr = `[V:${buffer2int(status[0] as Buffer)}, NID:${buffer2int(status[1] as Buffer)}, TD:${buffer2int( + status[2] as Buffer, )}` sStr += `, BestH:${formatLogId(status[3].toString('hex'), verbose)}, GenH:${formatLogId( status[4].toString('hex'), verbose, - )}]` + )}` + if (this._version >= 64) { + sStr += `, ForkHash: 0x${(status[5][0] as Buffer).toString('hex')}` + sStr += `, ForkNext: ${buffer2int(status[5][1] as Buffer)}` + } + sStr += `]` return sStr } @@ -120,6 +199,17 @@ export class ETH extends EventEmitter { status.bestHash, status.genesisHash, ] + if (this._version >= 64) { + if (status.latestBlock) { + if (status.latestBlock < this._latestBlock) { + throw new Error('latest block provided is not matching the HF setting of the Common instance (Rlpx)') + } + this._latestBlock = status.latestBlock + } + const forkHashB = Buffer.from(this._forkHash.substr(2), 'hex') + const nextForkB = Buffer.from(this._nextForkBlock.toString(16), 'hex') + this._status.push([forkHashB, nextForkB]) + } debug( `Send STATUS message to ${this._peer._socket.remoteAddress}:${ @@ -171,20 +261,14 @@ export class ETH extends EventEmitter { } export namespace ETH { - export type StatusMsg = { - 0: Buffer - 1: Buffer - 2: Buffer - 3: Buffer - 4: Buffer - length: 5 - } + export interface StatusMsg extends Array {} export type StatusOpts = { version: number // networkId: number td: Buffer bestHash: Buffer + latestBlock?: number genesisHash: Buffer } diff --git a/src/rlpx/peer.ts b/src/rlpx/peer.ts index f9a73ea..5f852d8 100644 --- a/src/rlpx/peer.ts +++ b/src/rlpx/peer.ts @@ -3,7 +3,7 @@ import { debug as createDebugLogger } from 'debug' import { EventEmitter } from 'events' import ms from 'ms' import rlp from 'rlp-encoding' -import Common from 'ethereumjs-common' +import Common from '@ethereumjs/common' import { ECIES } from './ecies' import { ETH, LES } from '../' import { int2buffer, buffer2int, formatLogData } from '../util' diff --git a/src/rlpx/rlpx.ts b/src/rlpx/rlpx.ts index f44f837..5e68296 100644 --- a/src/rlpx/rlpx.ts +++ b/src/rlpx/rlpx.ts @@ -5,7 +5,7 @@ import { publicKeyCreate } from 'secp256k1' import { EventEmitter } from 'events' import { debug as createDebugLogger } from 'debug' import LRUCache from 'lru-cache' -import Common from 'ethereumjs-common' +import Common from '@ethereumjs/common' // note: relative path only valid in .js file in dist const { version: pVersion } = require('../../package.json') import { pk2id, createDeferred, formatLogId } from '../util' diff --git a/test/integration/eth-simulator.ts b/test/integration/eth-simulator.ts index 98bc618..c76e85a 100644 --- a/test/integration/eth-simulator.ts +++ b/test/integration/eth-simulator.ts @@ -1,7 +1,7 @@ import test from 'tape' import * as devp2p from '../../src' import * as util from './util' -import Common from 'ethereumjs-common' +import Common from '@ethereumjs/common' const GENESIS_TD = 17179869184 const GENESIS_HASH = Buffer.from( @@ -9,7 +9,7 @@ const GENESIS_HASH = Buffer.from( 'hex', ) -var capabilities = [devp2p.ETH.eth63, devp2p.ETH.eth62] +const capabilities = [devp2p.ETH.eth63, devp2p.ETH.eth62] const status = { td: devp2p.int2buffer(GENESIS_TD), @@ -64,12 +64,12 @@ test('ETH: send status message (Genesis block mismatch)', async t => { util.twoPeerMsgExchange(t, opts, capabilities) }) -test('ETH: send allowed eth63', async t => { +function sendWithProtocolVersion(t: test.Test, version: number, cap?: Object) { let opts: any = {} opts.status0 = Object.assign({}, status) opts.status1 = Object.assign({}, status) opts.onOnceStatus0 = function(rlpxs: any, eth: any) { - t.equal(eth.getVersion(), 63, 'should use eth63 as protocol version') + t.equal(eth.getVersion(), version, `should use eth${version} as protocol version`) eth.sendMessage(devp2p.ETH.MESSAGE_CODES.NEW_BLOCK_HASHES, [437000, 1, 0, 0]) t.pass('should send NEW_BLOCK_HASHES message') } @@ -80,26 +80,65 @@ test('ETH: send allowed eth63', async t => { t.end() } } - util.twoPeerMsgExchange(t, opts, capabilities) + util.twoPeerMsgExchange(t, opts, cap) +} + +test('ETH: should use latest protocol version on default', async t => { + sendWithProtocolVersion(t, 64) }) -test('ETH: send allowed eth62', async t => { - let cap = [devp2p.ETH.eth62] +test('ETH: should work with allowed eth64', async t => { + sendWithProtocolVersion(t, 64) +}) + +test('ETH -> Eth64 -> sendStatus(): should throw on non-matching latest block provided', async t => { + const cap = [devp2p.ETH.eth64] + const common = new Common('mainnet', 'byzantium') + let status0: any = Object.assign({}, status) + status0['latestBlock'] = 100000 // lower than Byzantium fork block 4370000 + + const rlpxs = util.initTwoPeerRLPXSetup(null, cap, common) + rlpxs[0].on('peer:added', function(peer: any) { + const protocol = peer.getProtocols()[0] + t.throws(() => { protocol.sendStatus(status0) }, /latest block provided is not matching the HF setting/) + util.destroyRLPXs(rlpxs) + t.end() + }) +}) + +test('ETH -> Eth64 -> ForkId validation 1a)', async t => { let opts: any = {} - opts.status0 = Object.assign({}, status) + const cap = [devp2p.ETH.eth64] + const common = new Common('mainnet', 'byzantium') + let status0: any = Object.assign({}, status) + // Take a latest block > next mainnet fork block (constantinople) + // to trigger validation condition + status0['latestBlock'] = 9069000 + opts.status0 = status0 opts.status1 = Object.assign({}, status) - opts.onOnceStatus0 = function(rlpxs: any, eth: any) { - eth.sendMessage(devp2p.ETH.MESSAGE_CODES.NEW_BLOCK_HASHES, [437000, 1, 0, 0]) - t.pass('should send NEW_BLOCK_HASHES message') - } - opts.onOnMsg1 = function(rlpxs: any, eth: any, code: any, payload: any) { - if (code === devp2p.ETH.MESSAGE_CODES.NEW_BLOCK_HASHES) { - t.pass('should receive NEW_BLOCK_HASHES message') - util.destroyRLPXs(rlpxs) - t.end() - } + opts.onPeerError0 = function(err: Error, rlpxs: any) { + const msg = 'Remote is advertising a future fork that passed locally' + t.equal(err.message, msg, `should emit error: ${msg}`) + util.destroyRLPXs(rlpxs) + t.end() } - util.twoPeerMsgExchange(t, opts, cap) + + util.twoPeerMsgExchange(t, opts, cap, common) +}) + +test('ETH: should work with allowed eth63', async t => { + let cap = [devp2p.ETH.eth63] + sendWithProtocolVersion(t, 63, cap) +}) + +test('ETH: should work with allowed eth63', async t => { + let cap = [devp2p.ETH.eth63] + sendWithProtocolVersion(t, 63, cap) +}) + +test('ETH: work with allowed eth62', async t => { + let cap = [devp2p.ETH.eth62] + sendWithProtocolVersion(t, 62, cap) }) test('ETH: send not-allowed eth62', async t => { diff --git a/test/integration/les-simulator.ts b/test/integration/les-simulator.ts index 49e1429..c58ebdb 100644 --- a/test/integration/les-simulator.ts +++ b/test/integration/les-simulator.ts @@ -1,5 +1,5 @@ import test from 'tape' -import Common from 'ethereumjs-common' +import Common from '@ethereumjs/common' import * as devp2p from '../../src' import * as util from './util' diff --git a/test/integration/util.ts b/test/integration/util.ts index 426bb60..7e46d16 100644 --- a/test/integration/util.ts +++ b/test/integration/util.ts @@ -1,7 +1,7 @@ import { Test } from 'tape' import { DPT, ETH, RLPx, genPrivateKey } from '../../src' -import Common from 'ethereumjs-common' +import Common from '@ethereumjs/common' export const localhost = '127.0.0.1' export const basePort = 30306 @@ -43,7 +43,7 @@ export function getTestRLPXs( ) { const rlpxs = [] if (!capabilities) { - capabilities = [ETH.eth63, ETH.eth62] + capabilities = [ETH.eth64, ETH.eth63, ETH.eth62] } if (!common) { common = new Common('mainnet')