diff --git a/packages/client/lib/rpc/modules/engine.ts b/packages/client/lib/rpc/modules/engine.ts index a9488b4af21..d12b11a6a2b 100644 --- a/packages/client/lib/rpc/modules/engine.ts +++ b/packages/client/lib/rpc/modules/engine.ts @@ -590,8 +590,10 @@ export class Engine { blocks.push(block) + let lastBlock: Block try { for (const [i, block] of blocks.entries()) { + lastBlock = block const root = (i > 0 ? blocks[i - 1] : await this.chain.getBlock(block.header.parentHash)) .header.stateRoot await this.execution.runWithoutSetHead({ @@ -605,6 +607,14 @@ export class Engine { this.config.logger.error(validationError) const latestValidHash = await validHash(block.header.parentHash, this.chain) const response = { status: Status.INVALID, latestValidHash, validationError } + try { + await this.chain.blockchain.delBlock(lastBlock!.hash()) + // eslint-disable-next-line no-empty + } catch {} + try { + await this.service.beaconSync?.skeleton.deleteBlock(lastBlock!) + // eslint-disable-next-line no-empty + } catch {} return response } diff --git a/packages/client/lib/sync/skeleton.ts b/packages/client/lib/sync/skeleton.ts index 391467c3ac7..34589ca4f40 100644 --- a/packages/client/lib/sync/skeleton.ts +++ b/packages/client/lib/sync/skeleton.ts @@ -788,7 +788,7 @@ export class Skeleton extends MetaDBManager { /** * Deletes a skeleton block from the db by number */ - private async deleteBlock(block: Block): Promise { + async deleteBlock(block: Block): Promise { try { await this.delete(DBKey.SkeletonBlock, bigIntToBuffer(block.header.number)) await this.delete(DBKey.SkeletonBlockHashToNumber, block.hash()) diff --git a/packages/client/lib/sync/snapsync.ts b/packages/client/lib/sync/snapsync.ts index 87bffcabb47..b6b7372560d 100644 --- a/packages/client/lib/sync/snapsync.ts +++ b/packages/client/lib/sync/snapsync.ts @@ -1,3 +1,5 @@ +import { Event } from '../types' + import { AccountFetcher } from './fetcher' import { Synchronizer } from './sync' @@ -33,7 +35,11 @@ export class SnapSynchronizer extends Synchronizer { /** * Open synchronizer. Must be called before sync() is called */ - async open(): Promise {} + async open(): Promise { + await super.open() + await this.chain.open() + await this.pool.open() + } /** * Returns true if peer can be used for syncing @@ -75,6 +81,27 @@ export class SnapSynchronizer extends Synchronizer { return result ? result[1][0] : undefined } + /** + * Start synchronizer. + */ + async start(): Promise { + if (this.running) return + this.running = true + + const timeout = setTimeout(() => { + this.forceSync = true + }, this.interval * 30) + try { + await this.sync() + } catch (error: any) { + this.config.logger.error(`Snap sync error: ${error.message}`) + this.config.events.emit(Event.SYNC_ERROR, error) + } + await new Promise((resolve) => setTimeout(resolve, this.interval)) + this.running = false + clearTimeout(timeout) + } + /** * Called from `sync()` to sync blocks and state from peer starting from current height. * @param peer remote peer to sync with diff --git a/packages/client/lib/sync/sync.ts b/packages/client/lib/sync/sync.ts index a62af0a693a..a593044aada 100644 --- a/packages/client/lib/sync/sync.ts +++ b/packages/client/lib/sync/sync.ts @@ -194,10 +194,10 @@ export abstract class Synchronizer { * Stop synchronizer. */ async stop(): Promise { + this.clearFetcher() if (!this.running) { return false } - this.clearFetcher() clearInterval(this._syncedStatusCheckInterval as NodeJS.Timeout) await new Promise((resolve) => setTimeout(resolve, this.interval)) this.running = false diff --git a/packages/client/lib/types.ts b/packages/client/lib/types.ts index 696fb1ac825..ea9121b1e75 100644 --- a/packages/client/lib/types.ts +++ b/packages/client/lib/types.ts @@ -22,6 +22,7 @@ export enum Event { SYNC_SYNCHRONIZED = 'sync:synchronized', SYNC_ERROR = 'sync:error', SYNC_FETCHER_ERROR = 'sync:fetcher:error', + SYNC_SNAPSYNC_COMPLETE = 'sync:snapsync:complete', PEER_CONNECTED = 'peer:connected', PEER_DISCONNECTED = 'peer:disconnected', PEER_ERROR = 'peer:error', @@ -40,6 +41,7 @@ export interface EventParams { [Event.SYNC_FETCHED_BLOCKS]: [blocks: Block[]] [Event.SYNC_FETCHED_HEADERS]: [headers: BlockHeader[]] [Event.SYNC_SYNCHRONIZED]: [chainHeight: bigint] + [Event.SYNC_SNAPSYNC_COMPLETE]: [stateRoot: Uint8Array] [Event.SYNC_ERROR]: [syncError: Error] [Event.SYNC_FETCHER_ERROR]: [fetchError: Error, task: any, peer: Peer | null | undefined] [Event.PEER_CONNECTED]: [connectedPeer: Peer] @@ -67,6 +69,7 @@ export type EventBusType = EventBus & EventBus & EventBus & EventBus & + EventBus & EventBus & EventBus & EventBus & diff --git a/packages/client/test/sim/simutils.ts b/packages/client/test/sim/simutils.ts index 15a04f59445..24e885132fb 100644 --- a/packages/client/test/sim/simutils.ts +++ b/packages/client/test/sim/simutils.ts @@ -1,3 +1,4 @@ +import { Blockchain } from '@ethereumjs/blockchain' import { BlobEIP4844Transaction, FeeMarketEIP1559Transaction, initKZG } from '@ethereumjs/tx' import { blobsToCommitments, @@ -8,9 +9,13 @@ import { Address } from '@ethereumjs/util' import * as kzg from 'c-kzg' import { randomBytes } from 'crypto' import * as fs from 'fs/promises' +import { Level } from 'level' import { execSync, spawn } from 'node:child_process' import * as net from 'node:net' +import { EthereumClient } from '../../lib/client' +import { Config } from '../../lib/config' + import type { Common } from '@ethereumjs/common' import type { ChildProcessWithoutNullStreams } from 'child_process' import type { Client } from 'jayson/promise' @@ -136,7 +141,7 @@ export function runNetwork( console.log('') lastPrintedDot = false } - process.stdout.write(`${runProcPrefix}:el<>cl: ${runProc.pid}: ${str}`) // str already contains a new line. console.log adds a new line + process.stdout.write(`data:${runProcPrefix}: ${runProc.pid}: ${str}`) // str already contains a new line. console.log adds a new line } else { if (str.includes('Synchronized')) { process.stdout.write('.') @@ -148,9 +153,10 @@ export function runNetwork( }) runProc.stderr.on('data', (chunk) => { const str = Buffer.from(chunk).toString('utf8') + const filterStr = filterKeywords.reduce((acc, next) => acc || str.includes(next), false) const filterOutStr = filterOutWords.reduce((acc, next) => acc || str.includes(next), false) - if (!filterOutStr) { - process.stderr.write(`${runProcPrefix}:el<>cl: ${runProc.pid}: ${str}`) // str already contains a new line. console.log adds a new line + if (filterStr && !filterOutStr) { + process.stderr.write(`stderr:${runProcPrefix}: ${runProc.pid}: ${str}`) // str already contains a new line. console.log adds a new line } }) @@ -400,6 +406,34 @@ export const runBlobTxsFromFile = async (client: Client, path: string) => { return txnHashes } +export async function createInlineClient(config: any, common: any, customGenesisState: any) { + config.events.setMaxListeners(50) + const datadir = Config.DATADIR_DEFAULT + const chainDB = new Level( + `${datadir}/${common.chainName()}/chainDB` + ) + const stateDB = new Level( + `${datadir}/${common.chainName()}/stateDB` + ) + const metaDB = new Level( + `${datadir}/${common.chainName()}/metaDB` + ) + + const blockchain = await Blockchain.create({ + db: chainDB, + genesisState: customGenesisState, + common: config.chainCommon, + hardforkByHeadBlockNumber: true, + validateBlocks: true, + validateConsensus: false, + }) + config.chainCommon.setForkHashes(blockchain.genesisBlock.hash()) + const inlineClient = await EthereumClient.create({ config, blockchain, chainDB, stateDB, metaDB }) + await inlineClient.open() + await inlineClient.start() + return inlineClient +} + // To minimise noise on the spec run, selective filteration is applied to let the important events // of the testnet log to show up in the spec log export const filterKeywords = [ @@ -414,5 +448,6 @@ export const filterKeywords = [ 'pid', 'Synced - slot: 0 -', 'TxPool started', + 'number=0', ] export const filterOutWords = ['duties', 'Low peer count', 'MaxListenersExceededWarning'] diff --git a/packages/client/test/sim/single-run.sh b/packages/client/test/sim/single-run.sh index 0f50e469b59..19a2e3bf635 100755 --- a/packages/client/test/sim/single-run.sh +++ b/packages/client/test/sim/single-run.sh @@ -20,24 +20,91 @@ then exit; fi; +if [ ! -n "$JWT_SECRET" ] +then + JWT_SECRET="0xdc6457099f127cf0bac78de8b297df04951281909db4f58b43def7c7151e765d" +fi; + +if [ -n "$ELCLIENT" ] +then + if [ ! -n "$ELCLIENT_IMAGE" ] + then + case $ELCLIENT in + ethereumjs) + echo "ELCLIENT=$ELCLIENT using local ethereumjs binary from packages/client" + ;; + geth) + if [ ! -n "$NETWORKID" ] + then + echo "geth requires NETWORKID to be passed in env, exiting..." + exit; + fi; + ELCLIENT_IMAGE="ethereum/client-go:stable" + echo "ELCLIENT=$ELCLIENT using ELCLIENT_IMAGE=$ELCLIENT_IMAGE NETWORKID=$NETWORKID" + ;; + *) + echo "ELCLIENT=$ELCLIENT not implemented" + esac + fi +else + ELCLIENT="ethereumjs" + echo "ELCLIENT=$ELCLIENT using local ethereumjs binary from packages/client" +fi; + case $MULTIPEER in syncpeer) echo "setting up to run as a sync only peer to peer1 (bootnode)..." DATADIR="$DATADIR/syncpeer" - EL_PORT_ARGS="--port 30305 --rpcEnginePort 8553 --rpcport 8947 --multiaddrs /ip4/127.0.0.1/tcp/50582/ws --logLevel debug" - CL_PORT_ARGS="--genesisValidators 8 --enr.tcp 9002 --port 9002 --execution.urls http://localhost:8553 --rest.port 9598 --server http://localhost:9598 --network.connectToDiscv5Bootnodes true" + + case $ELCLIENT in + ethereumjs) + EL_PORT_ARGS="--port 30305 --rpcEnginePort 8553 --rpcPort 8947 --multiaddrs /ip4/127.0.0.1/tcp/50582/ws --logLevel debug" + ;; + geth) + echo "syncpeer args not yet implemented for geth, exiting..." + exit; + ;; + *) + echo "ELCLIENT=$ELCLIENT not implemented" + esac + + CL_PORT_ARGS="--genesisValidators 8 --enr.tcp 9002 --port 9002 --execution.urls http://localhost:8553 --rest.port 9598 --server http://localhost:9598 --network.connectToDiscv5Bootnodes true --logLevel debug" ;; - peer2 ) + peer2) echo "setting up peer2 to run with peer1 (bootnode)..." DATADIR="$DATADIR/peer2" - EL_PORT_ARGS="--port 30304 --rpcEnginePort 8552 --rpcport 8946 --multiaddrs /ip4/127.0.0.1/tcp/50581/ws --bootnodes $elBootnode --logLevel debug" + + case $ELCLIENT in + ethereumjs) + EL_PORT_ARGS="--port 30304 --rpcEnginePort 8552 --rpcPort 8946 --multiaddrs /ip4/127.0.0.1/tcp/50581/ws --bootnodes $elBootnode --logLevel debug" + ;; + geth) + echo "peer2 args not yet implemented for geth, exiting..." + exit; + ;; + *) + echo "ELCLIENT=$ELCLIENT not implemented" + esac + CL_PORT_ARGS="--genesisValidators 8 --startValidators 4..7 --enr.tcp 9001 --port 9001 --execution.urls http://localhost:8552 --rest.port 9597 --server http://localhost:9597 --network.connectToDiscv5Bootnodes true --bootnodes $bootEnrs" ;; * ) DATADIR="$DATADIR/peer1" - EL_PORT_ARGS="--isSingleNode --extIP 127.0.0.1 --logLevel debug" + + case $ELCLIENT in + ethereumjs) + EL_PORT_ARGS="--isSingleNode --extIP 127.0.0.1 --logLevel debug" + ;; + geth) + # geth will be mounted in docker with DATADIR to /data + EL_PORT_ARGS="--datadir /data/geth --authrpc.jwtsecret /data/jwtsecret --http --http.api engine,net,eth,web3,debug,admin --http.corsdomain \"*\" --http.port 8545 --http.addr 0.0.0.0 --http.vhosts \"*\" --authrpc.addr 0.0.0.0 --authrpc.vhosts \"*\" --authrpc.port=8551 --syncmode full --networkid $NETWORKID --nodiscover" + ;; + *) + echo "ELCLIENT=$ELCLIENT not implemented" + esac + CL_PORT_ARGS="--enr.ip 127.0.0.1 --enr.tcp 9000 --enr.udp 9000" if [ ! -n "$MULTIPEER" ] then @@ -62,11 +129,22 @@ fi; # clean these folders as old data can cause issues sudo rm -rf $DATADIR/ethereumjs +sudo rm -rf $DATADIR/geth sudo rm -rf $DATADIR/lodestar # these two commands will harmlessly fail if folders exists mkdir $DATADIR/ethereumjs +mkdir $DATADIR/geth mkdir $DATADIR/lodestar +echo "$JWT_SECRET" > $DATADIR/jwtsecret + +# additional step for setting geth genesis now that we have datadir +if [ "$ELCLIENT" == "geth" ] +then + setupCmd="docker run --rm -v $scriptDir/configs:/config -v $DATADIR/geth:/data $ELCLIENT_IMAGE --datadir /data init /config/$NETWORK.json" + echo "$setupCmd" + $setupCmd +fi; run_cmd(){ execCmd=$1; @@ -90,7 +168,12 @@ cleanup() { then ejsPidBySearch=$(ps x | grep "ts-node bin/cli.ts --dataDir $DATADIR/ethereumjs" | grep -v grep | awk '{print $1}') echo "cleaning ethereumjs pid:${ejsPid} ejsPidBySearch:${ejsPidBySearch}..." - kill $ejsPidBySearch + if [ -n "$ELCLIENT_IMAGE" ] + then + docker rm execution${MULTIPEER} -f + else + kill $ejsPidBySearch + fi; fi; if [ -n "$lodePid" ] then @@ -110,7 +193,18 @@ cleanup() { if [ "$MULTIPEER" == "peer1" ] then - ejsCmd="npm run client:start -- --dataDir $DATADIR/ethereumjs --gethGenesis $scriptDir/configs/$NETWORK.json --rpc --rpcEngine --rpcEngineAuth false $EL_PORT_ARGS" + case $ELCLIENT in + ethereumjs) + ejsCmd="npm run client:start -- --dataDir $DATADIR/ethereumjs --gethGenesis $scriptDir/configs/$NETWORK.json --rpc --rpcEngine --rpcEngineAuth false $EL_PORT_ARGS" + ;; + geth) + # geth will be mounted in docker with DATADIR to /data + ejsCmd="docker run --rm --name execution${MULTIPEER} -v $DATADIR:/data --network host $ELCLIENT_IMAGE $EL_PORT_ARGS" + ;; + *) + echo "ELCLIENT=$ELCLIENT not implemented" + esac + run_cmd "$ejsCmd" ejsPid=$! echo "ejsPid: $ejsPid" @@ -159,11 +253,21 @@ else EL_PORT_ARGS="$EL_PORT_ARGS --bootnodes $elBootnode" CL_PORT_ARGS="$CL_PORT_ARGS --bootnodes $bootEnrs" - GENESIS_HASH=$(cat "$origDataDir/geneisHash") + GENESIS_HASH=$(cat "$origDataDir/genesisHash") genTime=$(cat "$origDataDir/genesisTime") + case $ELCLIENT in + ethereumjs) + ejsCmd="npm run client:start -- --dataDir $DATADIR/ethereumjs --gethGenesis $scriptDir/configs/$NETWORK.json --rpc --rpcEngine --rpcEngineAuth false $EL_PORT_ARGS" + ;; + geth) + echo "peer2/syncpeer args not yet implemented for geth, exiting..." + exit; + ;; + *) + echo "ELCLIENT=$ELCLIENT not implemented" + esac - ejsCmd="npm run client:start -- --dataDir $DATADIR/ethereumjs --gethGenesis $scriptDir/configs/$NETWORK.json --rpc --rpcEngine --rpcEngineAuth false $EL_PORT_ARGS" run_cmd "$ejsCmd" ejsPid=$! echo "ejsPid: $ejsPid" @@ -179,9 +283,9 @@ then then LODE_IMAGE="chainsafe/lodestar:latest" fi; - lodeCmd="docker run --rm --name beacon${MULTIPEER} -v $DATADIR:/data --network host $LODE_IMAGE dev --dataDir /data/lodestar $CL_PORT_ARGS" + lodeCmd="docker run --rm --name beacon${MULTIPEER} -v $DATADIR:/data --network host $LODE_IMAGE dev --dataDir /data/lodestar --jwt-secret /data/jwtsecret $CL_PORT_ARGS" else - lodeCmd="$LODE_BINARY dev --dataDir $DATADIR/lodestar $CL_PORT_ARGS" + lodeCmd="$LODE_BINARY dev --dataDir $DATADIR/lodestar --jwt-secret $DATADIR/jwtsecret $CL_PORT_ARGS" fi; run_cmd "$lodeCmd" lodePid=$! diff --git a/packages/client/test/sim/snapsync.md b/packages/client/test/sim/snapsync.md new file mode 100644 index 00000000000..441c458b8c9 --- /dev/null +++ b/packages/client/test/sim/snapsync.md @@ -0,0 +1,48 @@ +### Snapsync sim setup + +1. Start external geth client: +```bash +NETWORK=mainnet NETWORKID=1337903 ELCLIENT=geth DATADIR=/usr/app/ethereumjs/packages/client/data test +/sim/single-run.sh +``` + +2. (optional) Add some txs/state to geth +```bash +EXTERNAL_RUN=true ADD_EOA_STATE=true DATADIR=/usr/app/ethereumjs/packages/client/data npm run tape -- test/sim/snapsync.spec.ts +``` + +3. Run snap sync: +```bash +EXTERNAL_RUN=true SNAP_SYNC=true DATADIR=/usr/app/ethereumjs/packages/client/data npm run tape -- test/sim/snapsync.spec.ts +``` +you may add `DEBUG_SNAP=client:*` to see client fetcher snap sync debug logs i.e. +```bash +EXTERNAL_RUN=true SNAP_SYNC=true DEBUG_SNAP=client:* DATADIR=/usr/app/ethereumjs/packages/client/data npm run tape -- test/sim/snapsync.spec.ts +``` + +## Combinations + +Combine 2 & 3 in single step: +```bash +EXTERNAL_RUN=true ADD_EOA_STATE=true SNAP_SYNC=true DATADIR=/usr/app/ethereumjs/packages/client/data npm run tape -- test/sim/snapsync.spec.ts +``` + +Combine 1, 2, 3 in single step +```bash +NETWORK=mainnet NETWORKID=1337903 ELCLIENT=geth ADD_EOA_STATE=true SNAP_SYNC=true DEBUG_SNAP=client:* DATADIR=/usr/app/ethereumjs/packages/client/data npm run tape -- test/sim/snapsync.spec.ts +``` + +### Fully combined scenarios + +1. Test syncing genesis state from geth: +```bash +NETWORK=mainnet NETWORKID=1337903 ELCLIENT=geth SNAP_SYNC=true DEBUG_SNAP=client:* DATADIR=/usr/app/ethereumjs/packages/client/data npm run tape -- test/sim/snapsync.spec.ts +``` + +2. Add some EOA account states to geth (just add `ADD_EOA_STATE=true` flag to the command) +```bash +NETWORK=mainnet NETWORKID=1337903 ELCLIENT=geth ADD_EOA_STATE=true SNAP_SYNC=true DEBUG_SNAP=client:* DATADIR=/usr/app/ethereumjs/packages/client/data npm run tape -- test/sim/snapsync.spec.ts +``` + +3. Add EOA as well as some contract states (just add `ADD_CONTRACT_STATE=true` flag to the command) +TBD \ No newline at end of file diff --git a/packages/client/test/sim/snapsync.spec.ts b/packages/client/test/sim/snapsync.spec.ts new file mode 100644 index 00000000000..0a518274581 --- /dev/null +++ b/packages/client/test/sim/snapsync.spec.ts @@ -0,0 +1,195 @@ +import { parseGethGenesisState } from '@ethereumjs/blockchain' +import { Common } from '@ethereumjs/common' +import { privateToAddress } from '@ethereumjs/util' +import debug from 'debug' +import { Client } from 'jayson/promise' +import * as tape from 'tape' + +import { Config } from '../../lib/config' +import { getLogger } from '../../lib/logging' +import { Event } from '../../lib/types' + +import { + createInlineClient, + filterKeywords, + filterOutWords, + runTxHelper, + startNetwork, + waitForELStart, +} from './simutils' + +import type { EthereumClient } from '../../lib/client' +import type { RlpxServer } from '../../lib/net/server' + +const pkey = Buffer.from('ae557af4ceefda559c924516cabf029bedc36b68109bf8d6183fe96e04121f4e', 'hex') +const sender = '0x' + privateToAddress(pkey).toString('hex') +const client = Client.http({ port: 8545 }) + +const network = 'mainnet' +const networkJson = require(`./configs/${network}.json`) +const common = Common.fromGethGenesis(networkJson, { chain: network }) +const customGenesisState = parseGethGenesisState(networkJson) +let ejsClient: EthereumClient | null = null +let snapCompleted: Promise | undefined = undefined + +export async function runTx(data: string, to?: string, value?: bigint) { + return runTxHelper({ client, common, sender, pkey }, data, to, value) +} + +tape('simple mainnet test run', async (t) => { + // Better add it as a option in startnetwork + process.env.NETWORKID = `${common.networkId()}` + const { teardownCallBack, result } = await startNetwork(network, client, { + filterKeywords, + filterOutWords, + externalRun: process.env.EXTERNAL_RUN, + withPeer: process.env.WITH_PEER, + }) + + if (result.includes('Geth')) { + t.pass('connected to Geth') + } else { + t.fail('connected to wrong client') + } + + const nodeInfo = (await client.request('admin_nodeInfo', [])).result + t.ok(nodeInfo.enode !== undefined, 'fetched enode for peering') + + console.log(`Waiting for network to start...`) + try { + await waitForELStart(client) + t.pass('geth<>lodestar started successfully') + } catch (e) { + t.fail('geth<>lodestar failed to start') + throw e + } + + // ------------Sanity checks-------------------------------- + t.test( + 'add some EOA transfers', + { skip: process.env.ADD_EOA_STATE === undefined }, + async (st) => { + const startBalance = await client.request('eth_getBalance', [ + '0x3dA33B9A0894b908DdBb00d96399e506515A1009', + 'latest', + ]) + st.ok( + startBalance.result !== undefined, + `fetched 0x3dA33B9A0894b908DdBb00d96399e506515A1009 balance=${startBalance.result}` + ) + await runTx('', '0x3dA33B9A0894b908DdBb00d96399e506515A1009', 1000000n) + let balance = await client.request('eth_getBalance', [ + '0x3dA33B9A0894b908DdBb00d96399e506515A1009', + 'latest', + ]) + st.equal( + BigInt(balance.result), + BigInt(startBalance.result) + 1000000n, + 'sent a simple ETH transfer' + ) + await runTx('', '0x3dA33B9A0894b908DdBb00d96399e506515A1009', 1000000n) + balance = await client.request('eth_getBalance', [ + '0x3dA33B9A0894b908DdBb00d96399e506515A1009', + 'latest', + ]) + balance = await client.request('eth_getBalance', [ + '0x3dA33B9A0894b908DdBb00d96399e506515A1009', + 'latest', + ]) + st.equal( + BigInt(balance.result), + BigInt(startBalance.result) + 2000000n, + 'sent a simple ETH transfer 2x' + ) + st.end() + } + ) + + t.test('setup snap sync', { skip: process.env.SNAP_SYNC === undefined }, async (st) => { + // start client inline here for snap sync, no need for beacon + const { ejsInlineClient, peerConnectedPromise, snapSyncCompletedPromise } = + // eslint-disable-next-line @typescript-eslint/no-use-before-define + (await createSnapClient(common, customGenesisState, [nodeInfo.enode]).catch((e) => { + console.log(e) + return null + })) ?? { + ejsInlineClient: null, + peerConnectedPromise: Promise.reject('Client creation error'), + } + ejsClient = ejsInlineClient + snapCompleted = snapSyncCompletedPromise + st.ok(ejsClient !== null, 'ethereumjs client started') + + const enode = (ejsClient!.server('rlpx') as RlpxServer)!.getRlpxInfo().enode + const res = await client.request('admin_addPeer', [enode]) + st.equal(res.result, true, 'successfully requested Geth add EthereumJS as peer') + + const peerConnectTimeout = new Promise((_resolve, reject) => setTimeout(reject, 10000)) + try { + await Promise.race([peerConnectedPromise, peerConnectTimeout]) + st.pass('connected to geth peer') + } catch (e) { + st.fail('could not connect to geth peer in 10 seconds') + } + st.end() + }) + + t.test('should snap sync and finish', async (st) => { + try { + if (ejsClient !== null && snapCompleted !== undefined) { + // call sync if not has been called yet + void ejsClient.services[0].synchronizer.sync() + // wait on the sync promise to complete if it has been called independently + const snapSyncTimeout = new Promise((_resolve, reject) => setTimeout(reject, 40000)) + try { + await Promise.race([snapCompleted, snapSyncTimeout]) + st.pass('completed snap sync') + } catch (e) { + st.fail('could not complete snap sync in 40 seconds') + } + await ejsClient.stop() + } else { + st.fail('ethereumjs client not setup properly for snap sync') + } + + await teardownCallBack() + st.pass('network cleaned') + } catch (e) { + st.fail('network not cleaned properly') + } + st.end() + }) + + t.end() +}) + +async function createSnapClient(common: any, customGenesisState: any, bootnodes: any) { + // Turn on `debug` logs, defaults to all client logging + debug.enable(process.env.DEBUG_SNAP ?? '') + const logger = getLogger({ logLevel: 'debug' }) + const config = new Config({ + common, + transports: ['rlpx'], + bootnodes, + multiaddrs: [], + logger, + discDns: false, + discV4: false, + port: 30304, + forceSnapSync: true, + }) + const peerConnectedPromise = new Promise((resolve) => { + config.events.once(Event.PEER_CONNECTED, (peer: any) => resolve(peer)) + }) + const snapSyncCompletedPromise = new Promise((resolve) => { + config.events.once(Event.SYNC_SNAPSYNC_COMPLETE, (stateRoot: any) => resolve(stateRoot)) + }) + + const ejsInlineClient = await createInlineClient(config, common, customGenesisState) + return { ejsInlineClient, peerConnectedPromise, snapSyncCompletedPromise } +} + +process.on('uncaughtException', (err, origin) => { + console.log({ err, origin }) + process.exit() +}) diff --git a/packages/client/test/sync/snapsync.spec.ts b/packages/client/test/sync/snapsync.spec.ts index 623c5c6b900..f5800d4f1d1 100644 --- a/packages/client/test/sync/snapsync.spec.ts +++ b/packages/client/test/sync/snapsync.spec.ts @@ -9,9 +9,32 @@ tape('[SnapSynchronizer]', async (t) => { class PeerPool { open() {} close() {} + idle() {} + ban(_peer: any) {} + peers: any[] + + constructor(_opts = undefined) { + this.peers = [] + } } PeerPool.prototype.open = td.func() PeerPool.prototype.close = td.func() + PeerPool.prototype.idle = td.func() + class AccountFetcher { + first: bigint + count: bigint + constructor(opts: any) { + this.first = opts.first + this.count = opts.count + } + fetch() {} + clear() {} + destroy() {} + } + AccountFetcher.prototype.fetch = td.func() + AccountFetcher.prototype.clear = td.func() + AccountFetcher.prototype.destroy = td.func() + td.replace('../../lib/sync/fetcher', { AccountFetcher }) const { SnapSynchronizer } = await import('../../lib/sync/snapsync') @@ -24,6 +47,20 @@ tape('[SnapSynchronizer]', async (t) => { t.end() }) + t.test('should open', async (t) => { + const config = new Config({ transports: [] }) + const pool = new PeerPool() as any + const chain = await Chain.create({ config }) + const sync = new SnapSynchronizer({ config, pool, chain }) + ;(sync as any).pool.open = td.func() + ;(sync as any).pool.peers = [] + td.when((sync as any).pool.open()).thenResolve(null) + await sync.open() + t.pass('opened') + await sync.close() + t.end() + }) + t.test('should find best', async (t) => { const config = new Config({ transports: [] }) const pool = new PeerPool() as any @@ -34,7 +71,6 @@ tape('[SnapSynchronizer]', async (t) => { pool, chain, }) - ;(sync as any).running = true ;(sync as any).chain = { blocks: { height: 1 } } const getBlockHeaders1 = td.func() td.when(getBlockHeaders1(td.matchers.anything())).thenReturn([ @@ -61,6 +97,8 @@ tape('[SnapSynchronizer]', async (t) => { ;(sync as any).pool = { peers } ;(sync as any).forceSync = true t.equal(await sync.best(), peers[1], 'found best') + await sync.start() + t.end() }) })