From 727caf2aef87b7c6394ea6fc3d560a6e129d9a9f Mon Sep 17 00:00:00 2001 From: harkamal Date: Wed, 8 Mar 2023 21:54:14 +0530 Subject: [PATCH 01/10] Setup to dev/test snapsync with sim architecture --- packages/client/test/sim/single-run.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/client/test/sim/single-run.sh b/packages/client/test/sim/single-run.sh index 0f50e469b59..5fb7424af88 100755 --- a/packages/client/test/sim/single-run.sh +++ b/packages/client/test/sim/single-run.sh @@ -24,14 +24,14 @@ 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" + 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 --logLevel debug" ;; 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" + EL_PORT_ARGS="--port 30304 --rpcEnginePort 8552 --rpcPort 8946 --multiaddrs /ip4/127.0.0.1/tcp/50581/ws --bootnodes $elBootnode --logLevel debug" 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" ;; @@ -159,7 +159,7 @@ 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") From e52d23c166b64e091a4a40f80a0aa3a25cb4527c Mon Sep 17 00:00:00 2001 From: harkamal Date: Thu, 9 Mar 2023 23:22:01 +0530 Subject: [PATCH 02/10] modfiy single-run to setup a lodestar<>geth node to snapsync from --- packages/client/test/sim/simutils.ts | 7 +- packages/client/test/sim/single-run.sh | 117 ++++++++++++++++++++-- packages/client/test/sim/snapsync.md | 5 + packages/client/test/sim/snapsync.spec.ts | 94 +++++++++++++++++ 4 files changed, 211 insertions(+), 12 deletions(-) create mode 100644 packages/client/test/sim/snapsync.md create mode 100644 packages/client/test/sim/snapsync.spec.ts diff --git a/packages/client/test/sim/simutils.ts b/packages/client/test/sim/simutils.ts index 15a04f59445..1e8fd8cbdec 100644 --- a/packages/client/test/sim/simutils.ts +++ b/packages/client/test/sim/simutils.ts @@ -136,7 +136,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 +148,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 } }) diff --git a/packages/client/test/sim/single-run.sh b/packages/client/test/sim/single-run.sh index 5fb7424af88..4a851c25911 100755 --- a/packages/client/test/sim/single-run.sh +++ b/packages/client/test/sim/single-run.sh @@ -20,24 +20,86 @@ 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) + ELCLIENT_IMAGE="ethereum/client-go:stable" + echo "ELCLIENT=$ELCLIENT using ELCLIENT_IMAGE=$ELCLIENT_IMAGE" + ;; + *) + 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" + + 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 --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" + ;; + *) + 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 +124,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 +163,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 +188,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" @@ -162,8 +251,18 @@ else 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 +278,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..fc4c584f1a9 --- /dev/null +++ b/packages/client/test/sim/snapsync.md @@ -0,0 +1,5 @@ +### Snapsync sim setup + +```bash +ELCLIENT=geth DATADIR=/usr/app/ethereumjs/packages/client/data npm run tape -- test/sim/snapsync.spec.ts +``` \ 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..4f014d254bc --- /dev/null +++ b/packages/client/test/sim/snapsync.spec.ts @@ -0,0 +1,94 @@ +import { Common } from '@ethereumjs/common' +import { privateToAddress } from '@ethereumjs/util' +import { Client } from 'jayson/promise' +import * as tape from 'tape' + +import { + filterKeywords, + filterOutWords, + runTxHelper, + startNetwork, + waitForELStart, +} from './simutils' + +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 }) + +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) => { + 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') + } + + 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 + } + + t.test('snap sync empty state', async (st) => { + // start client inline here for snap sync, no need for beacon + st.end() + }) + + const blockHashes: string[] = [] + // ------------Sanity checks-------------------------------- + t.test('add some EOA transfers', async (st) => { + await runTx('', '0x3dA33B9A0894b908DdBb00d96399e506515A1009', 1000000n) + let balance = await client.request('eth_getBalance', [ + '0x3dA33B9A0894b908DdBb00d96399e506515A1009', + 'latest', + ]) + st.equal(BigInt(balance.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), 2000000n, 'sent a simple ETH transfer 2x') + const latestBlock = await client.request('eth_getBlockByNumber', ['latest', false]) + blockHashes.push(latestBlock.result.hash) + st.end() + }) + + t.test('snap sync state with EOA states', async (st) => { + // start client inline here for snap sync, no need for beacon + st.end() + }) + + t.test('should reset td', async (st) => { + try { + await teardownCallBack() + st.pass('network cleaned') + } catch (e) { + st.fail('network not cleaned properly') + } + st.end() + }) + + t.end() +}) From 61e458b013ca1f6507c1c0a8e2a77c54a3bfe12c Mon Sep 17 00:00:00 2001 From: harkamal Date: Fri, 10 Mar 2023 17:10:39 +0530 Subject: [PATCH 03/10] setup an ethereumjs inline client and get it to peer with geth --- packages/client/test/sim/simutils.ts | 1 + packages/client/test/sim/single-run.sh | 9 +- packages/client/test/sim/snapsync.md | 26 +++++- packages/client/test/sim/snapsync.spec.ts | 101 +++++++++++++++++++--- 4 files changed, 122 insertions(+), 15 deletions(-) diff --git a/packages/client/test/sim/simutils.ts b/packages/client/test/sim/simutils.ts index 1e8fd8cbdec..adae3d4cd1b 100644 --- a/packages/client/test/sim/simutils.ts +++ b/packages/client/test/sim/simutils.ts @@ -415,5 +415,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 4a851c25911..f16a78be0aa 100755 --- a/packages/client/test/sim/single-run.sh +++ b/packages/client/test/sim/single-run.sh @@ -34,8 +34,13 @@ then 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" + echo "ELCLIENT=$ELCLIENT using ELCLIENT_IMAGE=$ELCLIENT_IMAGE NETWORKID=$NETWORKID" ;; *) echo "ELCLIENT=$ELCLIENT not implemented" @@ -94,7 +99,7 @@ case $MULTIPEER in ;; 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 --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" + 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" ;; *) echo "ELCLIENT=$ELCLIENT not implemented" diff --git a/packages/client/test/sim/snapsync.md b/packages/client/test/sim/snapsync.md index fc4c584f1a9..4656084454d 100644 --- a/packages/client/test/sim/snapsync.md +++ b/packages/client/test/sim/snapsync.md @@ -1,5 +1,29 @@ ### Snapsync sim setup +1. Start external geth client: ```bash -ELCLIENT=geth DATADIR=/usr/app/ethereumjs/packages/client/data npm run tape -- test/sim/snapsync.spec.ts +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 TARGET_PEER=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 +``` + +## Combinations + +Combine 2 & 3 in single step: +```bash +EXTERNAL_RUN=true TARGET_PEER=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 +TARGET_PEER=true SNAP_SYNC=true DATADIR=/usr/app/ethereumjs/packages/client/data npm run tape -- test/sim/snapsync.spec.ts ``` \ No newline at end of file diff --git a/packages/client/test/sim/snapsync.spec.ts b/packages/client/test/sim/snapsync.spec.ts index 4f014d254bc..4ca1493b96a 100644 --- a/packages/client/test/sim/snapsync.spec.ts +++ b/packages/client/test/sim/snapsync.spec.ts @@ -1,8 +1,15 @@ +import { Blockchain, parseGethGenesisState } from '@ethereumjs/blockchain' import { Common } from '@ethereumjs/common' import { privateToAddress } from '@ethereumjs/util' +import debug from 'debug' import { Client } from 'jayson/promise' +import { Level } from 'level' import * as tape from 'tape' +import { EthereumClient } from '../../lib/client' +import { Config } from '../../lib/config' +import { getLogger } from '../../lib/logging' + import { filterKeywords, filterOutWords, @@ -18,12 +25,16 @@ 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 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, @@ -37,6 +48,9 @@ tape('simple mainnet test run', async (t) => { 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) @@ -46,20 +60,26 @@ tape('simple mainnet test run', async (t) => { throw e } - t.test('snap sync empty state', async (st) => { - // start client inline here for snap sync, no need for beacon - st.end() - }) - - const blockHashes: string[] = [] // ------------Sanity checks-------------------------------- - t.test('add some EOA transfers', async (st) => { + t.test('add some EOA transfers', { skip: process.env.TARGET_PEER === 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), 1000000n, 'sent a simple ETH transfer') + 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', @@ -69,19 +89,30 @@ tape('simple mainnet test run', async (t) => { '0x3dA33B9A0894b908DdBb00d96399e506515A1009', 'latest', ]) - st.equal(BigInt(balance.result), 2000000n, 'sent a simple ETH transfer 2x') - const latestBlock = await client.request('eth_getBlockByNumber', ['latest', false]) - blockHashes.push(latestBlock.result.hash) + st.equal( + BigInt(balance.result), + BigInt(startBalance.result) + 2000000n, + 'sent a simple ETH transfer 2x' + ) st.end() }) - t.test('snap sync state with EOA states', async (st) => { + t.test('snap sync state', { skip: process.env.SNAP_SYNC === undefined }, async (st) => { // start client inline here for snap sync, no need for beacon + // eslint-disable-next-line @typescript-eslint/no-use-before-define + ejsClient = await createClient(common, customGenesisState, [nodeInfo.enode]).catch((e) => { + console.log(e) + return null + }) + st.ok(ejsClient !== null, 'ethereumjs client started') st.end() }) t.test('should reset td', async (st) => { try { + if (ejsClient !== null) { + await ejsClient.stop() + } await teardownCallBack() st.pass('network cleaned') } catch (e) { @@ -92,3 +123,49 @@ tape('simple mainnet test run', async (t) => { t.end() }) + +export async function createClient(common: any, customGenesisState: any, bootnodes: any) { + // Turn on `debug` logs, defaults to all client logging + debug.enable('devp2p:*') + const logger = getLogger({ logLevel: 'debug' }) + const datadir = Config.DATADIR_DEFAULT + const config = new Config({ + common, + transports: ['rlpx'], + bootnodes, + multiaddrs: [], + logger, + discDns: false, + discV4: false, + port: 30304, + }) + config.events.setMaxListeners(50) + 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 +} + +process.on('uncaughtException', (err, origin) => { + console.log({ err, origin }) + process.exit() +}) From 09f02267dd917c146ce4114f0e7782d11775a2a2 Mon Sep 17 00:00:00 2001 From: harkamal Date: Fri, 10 Mar 2023 17:25:04 +0530 Subject: [PATCH 04/10] cleanup setup a bit --- packages/client/test/sim/simutils.ts | 33 ++++++++++++++++++++ packages/client/test/sim/snapsync.spec.ts | 37 +++++------------------ 2 files changed, 40 insertions(+), 30 deletions(-) diff --git a/packages/client/test/sim/simutils.ts b/packages/client/test/sim/simutils.ts index adae3d4cd1b..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' @@ -401,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 = [ diff --git a/packages/client/test/sim/snapsync.spec.ts b/packages/client/test/sim/snapsync.spec.ts index 4ca1493b96a..0a0246067fa 100644 --- a/packages/client/test/sim/snapsync.spec.ts +++ b/packages/client/test/sim/snapsync.spec.ts @@ -1,16 +1,15 @@ -import { Blockchain, parseGethGenesisState } from '@ethereumjs/blockchain' +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 { Level } from 'level' import * as tape from 'tape' -import { EthereumClient } from '../../lib/client' import { Config } from '../../lib/config' import { getLogger } from '../../lib/logging' import { + createInlineClient, filterKeywords, filterOutWords, runTxHelper, @@ -18,6 +17,8 @@ import { waitForELStart, } from './simutils' +import type { EthereumClient } from '../../lib/client' + const pkey = Buffer.from('ae557af4ceefda559c924516cabf029bedc36b68109bf8d6183fe96e04121f4e', 'hex') const sender = '0x' + privateToAddress(pkey).toString('hex') const client = Client.http({ port: 8545 }) @@ -100,7 +101,7 @@ tape('simple mainnet test run', async (t) => { t.test('snap sync state', { skip: process.env.SNAP_SYNC === undefined }, async (st) => { // start client inline here for snap sync, no need for beacon // eslint-disable-next-line @typescript-eslint/no-use-before-define - ejsClient = await createClient(common, customGenesisState, [nodeInfo.enode]).catch((e) => { + ejsClient = await createSnapClient(common, customGenesisState, [nodeInfo.enode]).catch((e) => { console.log(e) return null }) @@ -124,11 +125,10 @@ tape('simple mainnet test run', async (t) => { t.end() }) -export async function createClient(common: any, customGenesisState: any, bootnodes: any) { +async function createSnapClient(common: any, customGenesisState: any, bootnodes: any) { // Turn on `debug` logs, defaults to all client logging debug.enable('devp2p:*') const logger = getLogger({ logLevel: 'debug' }) - const datadir = Config.DATADIR_DEFAULT const config = new Config({ common, transports: ['rlpx'], @@ -139,30 +139,7 @@ export async function createClient(common: any, customGenesisState: any, bootnod discV4: false, port: 30304, }) - config.events.setMaxListeners(50) - 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 + return createInlineClient(config, common, customGenesisState) } process.on('uncaughtException', (err, origin) => { From d00614f35ffaf73b47aa2e635b824429b34047d1 Mon Sep 17 00:00:00 2001 From: harkamal Date: Fri, 10 Mar 2023 19:03:25 +0530 Subject: [PATCH 05/10] snapsync run spec --- packages/client/lib/sync/snapsync.ts | 27 +++++++++++++++++++++++ packages/client/test/sim/snapsync.spec.ts | 1 + 2 files changed, 28 insertions(+) diff --git a/packages/client/lib/sync/snapsync.ts b/packages/client/lib/sync/snapsync.ts index 87bffcabb47..2542cc1ba5f 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' @@ -8,6 +10,7 @@ interface SnapSynchronizerOptions extends SynchronizerOptions {} export class SnapSynchronizer extends Synchronizer { public running = false + public finished = false constructor(options: SnapSynchronizerOptions) { super(options) } @@ -75,6 +78,30 @@ export class SnapSynchronizer extends Synchronizer { return result ? result[1][0] : undefined } + /** + * Start synchronizer. + * If passed a block, will initialize sync starting from the block. + */ + async start(): Promise { + if (this.running) return + this.running = true + + const timeout = setTimeout(() => { + this.forceSync = true + }, this.interval * 30) + while (this.running && !this.finished) { + 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/test/sim/snapsync.spec.ts b/packages/client/test/sim/snapsync.spec.ts index 0a0246067fa..de548e901f3 100644 --- a/packages/client/test/sim/snapsync.spec.ts +++ b/packages/client/test/sim/snapsync.spec.ts @@ -138,6 +138,7 @@ async function createSnapClient(common: any, customGenesisState: any, bootnodes: discDns: false, discV4: false, port: 30304, + forceSnapSync: true, }) return createInlineClient(config, common, customGenesisState) } From ad649fd8ae8afa8307c528b085109d92f112aaf3 Mon Sep 17 00:00:00 2001 From: harkamal Date: Fri, 10 Mar 2023 22:58:48 +0530 Subject: [PATCH 06/10] get the snap testdev sim working --- packages/client/lib/sync/sync.ts | 8 ++++-- packages/client/test/sim/snapsync.md | 4 +++ packages/client/test/sim/snapsync.spec.ts | 34 +++++++++++++++++++---- 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/packages/client/lib/sync/sync.ts b/packages/client/lib/sync/sync.ts index a62af0a693a..427201a56e4 100644 --- a/packages/client/lib/sync/sync.ts +++ b/packages/client/lib/sync/sync.ts @@ -47,6 +47,7 @@ export abstract class Synchronizer { // Time (in ms) after which the synced state is reset private SYNCED_STATE_REMOVAL_PERIOD = 60000 private _syncedStatusCheckInterval: NodeJS.Timeout | undefined /* global NodeJS */ + private syncPromise: Promise | null = null /** * Create new node @@ -152,12 +153,13 @@ export abstract class Synchronizer { numAttempts += 1 } - if (!(await this.syncWithPeer(peer))) return false + if (this.syncPromise || !(await this.syncWithPeer(peer))) return false // eslint-disable-next-line no-async-promise-executor - return new Promise(async (resolve, reject) => { + this.syncPromise = new Promise(async (resolve, reject) => { const resolveSync = (height?: number) => { this.clearFetcher() + this.syncPromise = null resolve(true) const heightStr = typeof height === 'number' && height !== 0 ? ` height=${height}` : '' this.config.logger.info(`Finishing up sync with the current fetcher ${heightStr}`) @@ -174,9 +176,11 @@ export abstract class Synchronizer { `Received sync error, stopping sync and clearing fetcher: ${error.message ?? error}` ) this.clearFetcher() + this.syncPromise = null reject(error) } }) + return this.syncPromise } /** diff --git a/packages/client/test/sim/snapsync.md b/packages/client/test/sim/snapsync.md index 4656084454d..1a40e420593 100644 --- a/packages/client/test/sim/snapsync.md +++ b/packages/client/test/sim/snapsync.md @@ -15,6 +15,10 @@ EXTERNAL_RUN=true TARGET_PEER=true DATADIR=/usr/app/ethereumjs/packages/client/ ```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 diff --git a/packages/client/test/sim/snapsync.spec.ts b/packages/client/test/sim/snapsync.spec.ts index de548e901f3..565540dda3b 100644 --- a/packages/client/test/sim/snapsync.spec.ts +++ b/packages/client/test/sim/snapsync.spec.ts @@ -7,6 +7,7 @@ import * as tape from 'tape' import { Config } from '../../lib/config' import { getLogger } from '../../lib/logging' +import { Event } from '../../lib/types' import { createInlineClient, @@ -98,20 +99,36 @@ tape('simple mainnet test run', async (t) => { st.end() }) - t.test('snap sync state', { skip: process.env.SNAP_SYNC === undefined }, async (st) => { + t.test('setup snap sync', { skip: process.env.SNAP_SYNC === undefined }, async (st) => { // start client inline here for snap sync, no need for beacon // eslint-disable-next-line @typescript-eslint/no-use-before-define - ejsClient = await createSnapClient(common, customGenesisState, [nodeInfo.enode]).catch((e) => { + const { ejsInlineClient, peerConnectedPromise } = (await createSnapClient( + common, + customGenesisState, + [nodeInfo.enode] + ).catch((e) => { console.log(e) return null - }) + })) ?? { ejsInlineClient: null, peerConnectedPromise: Promise.reject('Client creation error') } + ejsClient = ejsInlineClient st.ok(ejsClient !== null, 'ethereumjs client started') + 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 reset td', async (st) => { + t.test('should snap sync and finish', async (st) => { try { if (ejsClient !== null) { + // call sync if not has been called yet + await ejsClient.services[0].synchronizer.sync() + // wait on the sync promise to complete if it has been called independently + await ejsClient.services[0].synchronizer['syncPromise'] await ejsClient.stop() } await teardownCallBack() @@ -127,7 +144,7 @@ tape('simple mainnet test run', async (t) => { async function createSnapClient(common: any, customGenesisState: any, bootnodes: any) { // Turn on `debug` logs, defaults to all client logging - debug.enable('devp2p:*') + debug.enable(process.env.DEBUG_SNAP ?? '') const logger = getLogger({ logLevel: 'debug' }) const config = new Config({ common, @@ -140,7 +157,12 @@ async function createSnapClient(common: any, customGenesisState: any, bootnodes: port: 30304, forceSnapSync: true, }) - return createInlineClient(config, common, customGenesisState) + const peerConnectedPromise = new Promise((resolve) => { + config.events.on(Event.PEER_CONNECTED, (peer: any) => resolve(peer)) + }) + + const ejsInlineClient = await createInlineClient(config, common, customGenesisState) + return { ejsInlineClient, peerConnectedPromise } } process.on('uncaughtException', (err, origin) => { From 4099dba459dc03726165afc6ffdb16599d9d46c9 Mon Sep 17 00:00:00 2001 From: harkamal Date: Fri, 10 Mar 2023 23:24:51 +0530 Subject: [PATCH 07/10] finalize the test infra and update usage doc --- packages/client/test/sim/snapsync.md | 23 +++++-- packages/client/test/sim/snapsync.spec.ts | 74 ++++++++++++----------- 2 files changed, 58 insertions(+), 39 deletions(-) diff --git a/packages/client/test/sim/snapsync.md b/packages/client/test/sim/snapsync.md index 1a40e420593..441c458b8c9 100644 --- a/packages/client/test/sim/snapsync.md +++ b/packages/client/test/sim/snapsync.md @@ -8,7 +8,7 @@ NETWORK=mainnet NETWORKID=1337903 ELCLIENT=geth DATADIR=/usr/app/ethereumjs/pac 2. (optional) Add some txs/state to geth ```bash -EXTERNAL_RUN=true TARGET_PEER=true DATADIR=/usr/app/ethereumjs/packages/client/data npm run tape -- test/sim/snapsync.spec.ts +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: @@ -24,10 +24,25 @@ EXTERNAL_RUN=true SNAP_SYNC=true DEBUG_SNAP=client:* DATADIR=/usr/app/ethereumjs Combine 2 & 3 in single step: ```bash -EXTERNAL_RUN=true TARGET_PEER=true SNAP_SYNC=true DATADIR=/usr/app/ethereumjs/packages/client/data npm run tape -- test/sim/snapsync.spec.ts +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 -TARGET_PEER=true SNAP_SYNC=true DATADIR=/usr/app/ethereumjs/packages/client/data npm run tape -- test/sim/snapsync.spec.ts -``` \ No newline at end of file +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 index 565540dda3b..a07b7941cfd 100644 --- a/packages/client/test/sim/snapsync.spec.ts +++ b/packages/client/test/sim/snapsync.spec.ts @@ -63,41 +63,45 @@ tape('simple mainnet test run', async (t) => { } // ------------Sanity checks-------------------------------- - t.test('add some EOA transfers', { skip: process.env.TARGET_PEER === 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( + '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 From ca5fbdd66d73f6449a6bab77007eccf046e85d38 Mon Sep 17 00:00:00 2001 From: harkamal Date: Sun, 12 Mar 2023 15:02:35 +0530 Subject: [PATCH 08/10] enhance coverage --- packages/client/lib/sync/snapsync.ts | 21 ++++++------ packages/client/test/sync/snapsync.spec.ts | 40 +++++++++++++++++++++- 2 files changed, 50 insertions(+), 11 deletions(-) diff --git a/packages/client/lib/sync/snapsync.ts b/packages/client/lib/sync/snapsync.ts index 2542cc1ba5f..23aeff92c0f 100644 --- a/packages/client/lib/sync/snapsync.ts +++ b/packages/client/lib/sync/snapsync.ts @@ -10,7 +10,6 @@ interface SnapSynchronizerOptions extends SynchronizerOptions {} export class SnapSynchronizer extends Synchronizer { public running = false - public finished = false constructor(options: SnapSynchronizerOptions) { super(options) } @@ -36,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 @@ -89,15 +92,13 @@ export class SnapSynchronizer extends Synchronizer { const timeout = setTimeout(() => { this.forceSync = true }, this.interval * 30) - while (this.running && !this.finished) { - 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)) + 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) } 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() }) }) From 3fe114db0986fd30a20f16d53b4055eeceea0b69 Mon Sep 17 00:00:00 2001 From: acolytec3 <17355484+acolytec3@users.noreply.github.com> Date: Tue, 14 Mar 2023 10:44:05 -0400 Subject: [PATCH 09/10] Use geth RPC to connect to ethJS --- packages/client/test/sim/single-run.sh | 2 +- packages/client/test/sim/snapsync.spec.ts | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/client/test/sim/single-run.sh b/packages/client/test/sim/single-run.sh index f16a78be0aa..19a2e3bf635 100755 --- a/packages/client/test/sim/single-run.sh +++ b/packages/client/test/sim/single-run.sh @@ -99,7 +99,7 @@ case $MULTIPEER in ;; 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" + 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" diff --git a/packages/client/test/sim/snapsync.spec.ts b/packages/client/test/sim/snapsync.spec.ts index a07b7941cfd..33bb58e1319 100644 --- a/packages/client/test/sim/snapsync.spec.ts +++ b/packages/client/test/sim/snapsync.spec.ts @@ -19,6 +19,7 @@ import { } 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') @@ -116,6 +117,11 @@ tape('simple mainnet test run', async (t) => { })) ?? { ejsInlineClient: null, peerConnectedPromise: Promise.reject('Client creation error') } ejsClient = ejsInlineClient 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]) From d0766a74b4e7e1becf3f146cc91bf0a37dab1a6c Mon Sep 17 00:00:00 2001 From: harkamal Date: Tue, 14 Mar 2023 22:45:50 +0530 Subject: [PATCH 10/10] refac wait for snap sync completion --- packages/client/lib/sync/snapsync.ts | 1 - packages/client/lib/sync/sync.ts | 10 ++---- packages/client/lib/types.ts | 3 ++ packages/client/test/sim/snapsync.spec.ts | 42 +++++++++++++++-------- 4 files changed, 34 insertions(+), 22 deletions(-) diff --git a/packages/client/lib/sync/snapsync.ts b/packages/client/lib/sync/snapsync.ts index 23aeff92c0f..b6b7372560d 100644 --- a/packages/client/lib/sync/snapsync.ts +++ b/packages/client/lib/sync/snapsync.ts @@ -83,7 +83,6 @@ export class SnapSynchronizer extends Synchronizer { /** * Start synchronizer. - * If passed a block, will initialize sync starting from the block. */ async start(): Promise { if (this.running) return diff --git a/packages/client/lib/sync/sync.ts b/packages/client/lib/sync/sync.ts index 427201a56e4..a593044aada 100644 --- a/packages/client/lib/sync/sync.ts +++ b/packages/client/lib/sync/sync.ts @@ -47,7 +47,6 @@ export abstract class Synchronizer { // Time (in ms) after which the synced state is reset private SYNCED_STATE_REMOVAL_PERIOD = 60000 private _syncedStatusCheckInterval: NodeJS.Timeout | undefined /* global NodeJS */ - private syncPromise: Promise | null = null /** * Create new node @@ -153,13 +152,12 @@ export abstract class Synchronizer { numAttempts += 1 } - if (this.syncPromise || !(await this.syncWithPeer(peer))) return false + if (!(await this.syncWithPeer(peer))) return false // eslint-disable-next-line no-async-promise-executor - this.syncPromise = new Promise(async (resolve, reject) => { + return new Promise(async (resolve, reject) => { const resolveSync = (height?: number) => { this.clearFetcher() - this.syncPromise = null resolve(true) const heightStr = typeof height === 'number' && height !== 0 ? ` height=${height}` : '' this.config.logger.info(`Finishing up sync with the current fetcher ${heightStr}`) @@ -176,11 +174,9 @@ export abstract class Synchronizer { `Received sync error, stopping sync and clearing fetcher: ${error.message ?? error}` ) this.clearFetcher() - this.syncPromise = null reject(error) } }) - return this.syncPromise } /** @@ -198,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/snapsync.spec.ts b/packages/client/test/sim/snapsync.spec.ts index 33bb58e1319..0a518274581 100644 --- a/packages/client/test/sim/snapsync.spec.ts +++ b/packages/client/test/sim/snapsync.spec.ts @@ -30,6 +30,7 @@ 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) @@ -106,16 +107,17 @@ tape('simple mainnet test run', async (t) => { t.test('setup snap sync', { skip: process.env.SNAP_SYNC === undefined }, async (st) => { // start client inline here for snap sync, no need for beacon - // eslint-disable-next-line @typescript-eslint/no-use-before-define - const { ejsInlineClient, peerConnectedPromise } = (await createSnapClient( - common, - customGenesisState, - [nodeInfo.enode] - ).catch((e) => { - console.log(e) - return null - })) ?? { ejsInlineClient: null, peerConnectedPromise: Promise.reject('Client creation error') } + 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 @@ -134,13 +136,22 @@ tape('simple mainnet test run', async (t) => { t.test('should snap sync and finish', async (st) => { try { - if (ejsClient !== null) { + if (ejsClient !== null && snapCompleted !== undefined) { // call sync if not has been called yet - await ejsClient.services[0].synchronizer.sync() + void ejsClient.services[0].synchronizer.sync() // wait on the sync promise to complete if it has been called independently - await ejsClient.services[0].synchronizer['syncPromise'] + 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) { @@ -168,11 +179,14 @@ async function createSnapClient(common: any, customGenesisState: any, bootnodes: forceSnapSync: true, }) const peerConnectedPromise = new Promise((resolve) => { - config.events.on(Event.PEER_CONNECTED, (peer: any) => resolve(peer)) + 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 } + return { ejsInlineClient, peerConnectedPromise, snapSyncCompletedPromise } } process.on('uncaughtException', (err, origin) => {