diff --git a/packages/beacon-node/test/e2e/api/impl/beacon/node/endpoints.test.ts b/packages/beacon-node/test/e2e/api/impl/beacon/node/endpoints.test.ts index e1c2da029d23..bcff1724b38e 100644 --- a/packages/beacon-node/test/e2e/api/impl/beacon/node/endpoints.test.ts +++ b/packages/beacon-node/test/e2e/api/impl/beacon/node/endpoints.test.ts @@ -62,55 +62,54 @@ describe("beacon node api", function () { expect(res.response.data.elOffline).toEqual(false); }); - it( - "should return 'el_offline' as 'true' when EL not available", - async () => { - const portElOffline = 9597; - const bnElOffline = await getDevBeaconNode({ - params: { - ...chainConfigDef, - // eslint-disable-next-line @typescript-eslint/naming-convention - ALTAIR_FORK_EPOCH: 0, - // eslint-disable-next-line @typescript-eslint/naming-convention - BELLATRIX_FORK_EPOCH: 0, - }, - options: { - sync: {isSingleNode: true}, - network: {allowPublishToZeroPeers: true}, - executionEngine: {mode: "http", urls: ["http://not-available-engine:9999"]}, - api: { - rest: { - enabled: true, - port: portElOffline, - }, + // To make the code review easy for code block below + /* prettier-ignore */ + it("should return 'el_offline' as 'true' when EL not available", async () => { + const portElOffline = 9597; + const bnElOffline = await getDevBeaconNode({ + params: { + ...chainConfigDef, + // eslint-disable-next-line @typescript-eslint/naming-convention + ALTAIR_FORK_EPOCH: 0, + // eslint-disable-next-line @typescript-eslint/naming-convention + BELLATRIX_FORK_EPOCH: 0, + }, + options: { + sync: {isSingleNode: true}, + network: {allowPublishToZeroPeers: true}, + executionEngine: {mode: "http", urls: ["http://not-available-engine:9999"]}, + api: { + rest: { + enabled: true, + port: portElOffline, }, - chain: {blsVerifyAllMainThread: true}, }, - validatorCount: 5, - logger: testLogger("Node-EL-Offline", {level: LogLevel.info}), - }); - const clientElOffline = getClient({baseUrl: `http://127.0.0.1:${portElOffline}`}, {config}); - // To make BN communicate with EL, it needs to produce some blocks and for that need validators - const {validators} = await getAndInitDevValidators({ - node: bnElOffline, - validatorClientCount: 1, - validatorsPerClient: validatorCount, - startIndex: 0, - }); - - // Give node sometime to communicate with EL - await sleep(chainConfigDef.SECONDS_PER_SLOT * 2 * 1000); - - const res = await clientElOffline.node.getSyncingStatus(); - ApiError.assert(res); - - expect(res.response.data.elOffline).toEqual(true); - - await Promise.all(validators.map((v) => v.close())); - await bnElOffline.close(); - }, - {timeout: 60_000} - ); + chain: {blsVerifyAllMainThread: true}, + }, + validatorCount: 5, + logger: testLogger("Node-EL-Offline", {level: LogLevel.info}), + }); + const clientElOffline = getClient({baseUrl: `http://127.0.0.1:${portElOffline}`}, {config}); + // To make BN communicate with EL, it needs to produce some blocks and for that need validators + const {validators} = await getAndInitDevValidators({ + node: bnElOffline, + validatorClientCount: 1, + validatorsPerClient: validatorCount, + startIndex: 0, + }); + + // Give node sometime to communicate with EL + await sleep(chainConfigDef.SECONDS_PER_SLOT * 2 * 1000); + + const res = await clientElOffline.node.getSyncingStatus(); + ApiError.assert(res); + + expect(res.response.data.elOffline).toEqual(true); + + await Promise.all(validators.map((v) => v.close())); + await bnElOffline.close(); + }, + {timeout: 60_000}); }); describe("getHealth", () => { diff --git a/packages/beacon-node/test/e2e/api/lodestar/lodestar.test.ts b/packages/beacon-node/test/e2e/api/lodestar/lodestar.test.ts index 27db4c6fe117..4bb8f76ef39a 100644 --- a/packages/beacon-node/test/e2e/api/lodestar/lodestar.test.ts +++ b/packages/beacon-node/test/e2e/api/lodestar/lodestar.test.ts @@ -74,62 +74,61 @@ describe("api / impl / validator", function () { }); }); - it( - "Should return only for previous, current and next epoch", - async function () { - const chainConfig: ChainConfig = {...chainConfigDef, SECONDS_PER_SLOT, ALTAIR_FORK_EPOCH}; - const genesisValidatorsRoot = Buffer.alloc(32, 0xaa); - const config = createBeaconConfig(chainConfig, genesisValidatorsRoot); - - const testLoggerOpts: TestLoggerOpts = {level: LogLevel.info}; - const loggerNodeA = testLogger("Node-A", testLoggerOpts); - - bn = await getDevBeaconNode({ - params: testParams, - options: { - sync: {isSingleNode: true}, - api: {rest: {enabled: true, api: ["lodestar"], port: restPort}}, - chain: {blsVerifyAllMainThread: true}, - }, - validatorCount, - logger: loggerNodeA, - }); - - await waitForEvent(bn.chain.clock, ClockEvent.epoch, timeout); // wait for epoch 1 - await waitForEvent(bn.chain.clock, ClockEvent.epoch, timeout); // wait for epoch 2 - - bn.chain.seenBlockProposers.add(bn.chain.clock.currentEpoch, 1); - - const client = getClient({baseUrl: `http://127.0.0.1:${restPort}`}, {config}); - - const currentEpoch = bn.chain.clock.currentEpoch; - const nextEpoch = currentEpoch + 1; - const previousEpoch = currentEpoch - 1; - - // current epoch is fine - await expect(client.validator.getLiveness(currentEpoch, [1])).resolves.toBeDefined(); - // next epoch is fine - await expect(client.validator.getLiveness(nextEpoch, [1])).resolves.toBeDefined(); - // previous epoch is fine - await expect(client.validator.getLiveness(previousEpoch, [1])).resolves.toBeDefined(); - // more than next epoch is not fine - const res1 = await client.validator.getLiveness(currentEpoch + 2, [1]); - expect(res1.ok).toBe(false); - expect(res1.error?.message).toEqual( - expect.stringContaining( - `Request epoch ${currentEpoch + 2} is more than one epoch before or after the current epoch ${currentEpoch}` - ) - ); - // more than previous epoch is not fine - const res2 = await client.validator.getLiveness(currentEpoch - 2, [1]); - expect(res2.ok).toBe(false); - expect(res2.error?.message).toEqual( - expect.stringContaining( - `Request epoch ${currentEpoch - 2} is more than one epoch before or after the current epoch ${currentEpoch}` - ) - ); - }, - {timeout: 60_000} - ); + // To make the code review easy for code block below + /* prettier-ignore */ + it("Should return only for previous, current and next epoch", async function () { + const chainConfig: ChainConfig = {...chainConfigDef, SECONDS_PER_SLOT, ALTAIR_FORK_EPOCH}; + const genesisValidatorsRoot = Buffer.alloc(32, 0xaa); + const config = createBeaconConfig(chainConfig, genesisValidatorsRoot); + + const testLoggerOpts: TestLoggerOpts = {level: LogLevel.info}; + const loggerNodeA = testLogger("Node-A", testLoggerOpts); + + bn = await getDevBeaconNode({ + params: testParams, + options: { + sync: {isSingleNode: true}, + api: {rest: {enabled: true, api: ["lodestar"], port: restPort}}, + chain: {blsVerifyAllMainThread: true}, + }, + validatorCount, + logger: loggerNodeA, + }); + + await waitForEvent(bn.chain.clock, ClockEvent.epoch, timeout); // wait for epoch 1 + await waitForEvent(bn.chain.clock, ClockEvent.epoch, timeout); // wait for epoch 2 + + bn.chain.seenBlockProposers.add(bn.chain.clock.currentEpoch, 1); + + const client = getClient({baseUrl: `http://127.0.0.1:${restPort}`}, {config}); + + const currentEpoch = bn.chain.clock.currentEpoch; + const nextEpoch = currentEpoch + 1; + const previousEpoch = currentEpoch - 1; + + // current epoch is fine + await expect(client.validator.getLiveness(currentEpoch, [1])).resolves.toBeDefined(); + // next epoch is fine + await expect(client.validator.getLiveness(nextEpoch, [1])).resolves.toBeDefined(); + // previous epoch is fine + await expect(client.validator.getLiveness(previousEpoch, [1])).resolves.toBeDefined(); + // more than next epoch is not fine + const res1 = await client.validator.getLiveness(currentEpoch + 2, [1]); + expect(res1.ok).toBe(false); + expect(res1.error?.message).toEqual( + expect.stringContaining( + `Request epoch ${currentEpoch + 2} is more than one epoch before or after the current epoch ${currentEpoch}` + ) + ); + // more than previous epoch is not fine + const res2 = await client.validator.getLiveness(currentEpoch - 2, [1]); + expect(res2.ok).toBe(false); + expect(res2.error?.message).toEqual( + expect.stringContaining( + `Request epoch ${currentEpoch - 2} is more than one epoch before or after the current epoch ${currentEpoch}` + ) + ); + }, + {timeout: 60_000}); }); }); diff --git a/packages/beacon-node/test/e2e/chain/lightclient.test.ts b/packages/beacon-node/test/e2e/chain/lightclient.test.ts index 97c530a83c86..7e63f851db05 100644 --- a/packages/beacon-node/test/e2e/chain/lightclient.test.ts +++ b/packages/beacon-node/test/e2e/chain/lightclient.test.ts @@ -14,172 +14,170 @@ import {getDevBeaconNode} from "../../utils/node/beacon.js"; import {getAndInitDevValidators} from "../../utils/node/validator.js"; import {HeadEventData} from "../../../src/chain/index.js"; -describe( - "chain / lightclient", - function () { - /** - * Max distance between beacon node head and lightclient head - * If SECONDS_PER_SLOT === 1, there should be some margin for slow blocks, - * 4 = 4 sec should be good enough. - */ - const maxLcHeadTrackingDiffSlots = 4; - const validatorCount = 8; - const validatorClientCount = 4; - // Reduced from 3 to 1, so test can complete in 10 epoch vs 27 epoch - const targetSyncCommittee = 1; - /** N sync committee periods + 1 epoch of margin */ - const finalizedEpochToReach = targetSyncCommittee * EPOCHS_PER_SYNC_COMMITTEE_PERIOD + 1; - /** Given 100% participation the fastest epoch to reach finalization is +2 epochs. -1 for margin */ - const targetSlotToReach = computeStartSlotAtEpoch(finalizedEpochToReach + 2) - 1; - const restPort = 9000; - - const testParams: Pick = { - /* eslint-disable @typescript-eslint/naming-convention */ - SECONDS_PER_SLOT: 1, - ALTAIR_FORK_EPOCH: 0, +// To make the code review easy for code block below +/* prettier-ignore */ +describe("chain / lightclient", function () { + /** + * Max distance between beacon node head and lightclient head + * If SECONDS_PER_SLOT === 1, there should be some margin for slow blocks, + * 4 = 4 sec should be good enough. + */ + const maxLcHeadTrackingDiffSlots = 4; + const validatorCount = 8; + const validatorClientCount = 4; + // Reduced from 3 to 1, so test can complete in 10 epoch vs 27 epoch + const targetSyncCommittee = 1; + /** N sync committee periods + 1 epoch of margin */ + const finalizedEpochToReach = targetSyncCommittee * EPOCHS_PER_SYNC_COMMITTEE_PERIOD + 1; + /** Given 100% participation the fastest epoch to reach finalization is +2 epochs. -1 for margin */ + const targetSlotToReach = computeStartSlotAtEpoch(finalizedEpochToReach + 2) - 1; + const restPort = 9000; + + const testParams: Pick = { + /* eslint-disable @typescript-eslint/naming-convention */ + SECONDS_PER_SLOT: 1, + ALTAIR_FORK_EPOCH: 0, + }; + + const afterEachCallbacks: (() => Promise | void)[] = []; + afterEach(async () => { + while (afterEachCallbacks.length > 0) { + const callback = afterEachCallbacks.pop(); + if (callback) await callback(); + } + }); + + it("Lightclient track head on server configuration", async function () { + // delay a bit so regular sync sees it's up to date and sync is completed from the beginning + // also delay to allow bls workers to be transpiled/initialized + const genesisSlotsDelay = 7; + const genesisTime = Math.floor(Date.now() / 1000) + genesisSlotsDelay * testParams.SECONDS_PER_SLOT; + + const testLoggerOpts: TestLoggerOpts = { + level: LogLevel.info, + timestampFormat: { + format: TimestampFormatCode.EpochSlot, + genesisTime, + slotsPerEpoch: SLOTS_PER_EPOCH, + secondsPerSlot: testParams.SECONDS_PER_SLOT, + }, }; - const afterEachCallbacks: (() => Promise | void)[] = []; - afterEach(async () => { - while (afterEachCallbacks.length > 0) { - const callback = afterEachCallbacks.pop(); - if (callback) await callback(); - } + const loggerNodeA = testLogger("Node", testLoggerOpts); + const loggerLC = testLogger("LC", {...testLoggerOpts, level: LogLevel.debug}); + + const bn = await getDevBeaconNode({ + params: testParams, + options: { + sync: {isSingleNode: true}, + network: {allowPublishToZeroPeers: true}, + api: {rest: {enabled: true, api: ["lightclient", "proof"], port: restPort, address: "localhost"}}, + chain: {blsVerifyAllMainThread: true}, + }, + validatorCount: validatorCount * validatorClientCount, + genesisTime, + logger: loggerNodeA, }); - it("Lightclient track head on server configuration", async function () { - // delay a bit so regular sync sees it's up to date and sync is completed from the beginning - // also delay to allow bls workers to be transpiled/initialized - const genesisSlotsDelay = 7; - const genesisTime = Math.floor(Date.now() / 1000) + genesisSlotsDelay * testParams.SECONDS_PER_SLOT; - - const testLoggerOpts: TestLoggerOpts = { - level: LogLevel.info, - timestampFormat: { - format: TimestampFormatCode.EpochSlot, - genesisTime, - slotsPerEpoch: SLOTS_PER_EPOCH, - secondsPerSlot: testParams.SECONDS_PER_SLOT, - }, - }; - - const loggerNodeA = testLogger("Node", testLoggerOpts); - const loggerLC = testLogger("LC", {...testLoggerOpts, level: LogLevel.debug}); - - const bn = await getDevBeaconNode({ - params: testParams, - options: { - sync: {isSingleNode: true}, - network: {allowPublishToZeroPeers: true}, - api: {rest: {enabled: true, api: ["lightclient", "proof"], port: restPort, address: "localhost"}}, - chain: {blsVerifyAllMainThread: true}, - }, - validatorCount: validatorCount * validatorClientCount, - genesisTime, - logger: loggerNodeA, - }); + afterEachCallbacks.push(async () => { + await bn.close(); + }); - afterEachCallbacks.push(async () => { - await bn.close(); - }); + const {validators} = await getAndInitDevValidators({ + node: bn, + validatorsPerClient: validatorCount, + validatorClientCount, + startIndex: 0, + useRestApi: false, + testLoggerOpts: {...testLoggerOpts, level: LogLevel.error}, + }); - const {validators} = await getAndInitDevValidators({ - node: bn, - validatorsPerClient: validatorCount, - validatorClientCount, - startIndex: 0, - useRestApi: false, - testLoggerOpts: {...testLoggerOpts, level: LogLevel.error}, + afterEachCallbacks.push(async () => { + await Promise.all(validators.map((v) => v.close())); + }); + + // This promise chain does: + // 1. Wait for the beacon node to emit one head that has a snapshot associated to it + // 2. Initialize lightclient from that head block root + // 3. Start lightclient to track head + // 4. On every new beacon node head, check that the lightclient is following closely + // - If too far behind error the test + // - If beacon node reaches the finality slot, resolve test + const promiseUntilHead = new Promise((resolve) => { + bn.chain.emitter.on(routes.events.EventType.head, async (head) => { + // Wait for the second slot so syncCommitteeWitness is available + if (head.slot > 2) { + resolve(head); + } + }); + }).then(async (head) => { + // Initialize lightclient + loggerLC.info("Initializing lightclient", {slot: head.slot}); + const api = getClient({baseUrl: `http://localhost:${restPort}`}, {config: bn.config}); + const lightclient = await Lightclient.initializeFromCheckpointRoot({ + config: bn.config, + logger: loggerLC, + transport: new LightClientRestTransport(api), + genesisData: { + genesisTime: bn.chain.genesisTime, + genesisValidatorsRoot: bn.chain.genesisValidatorsRoot, + }, + checkpointRoot: fromHexString(head.block), }); afterEachCallbacks.push(async () => { - await Promise.all(validators.map((v) => v.close())); + lightclient.stop(); }); - // This promise chain does: - // 1. Wait for the beacon node to emit one head that has a snapshot associated to it - // 2. Initialize lightclient from that head block root - // 3. Start lightclient to track head - // 4. On every new beacon node head, check that the lightclient is following closely - // - If too far behind error the test - // - If beacon node reaches the finality slot, resolve test - const promiseUntilHead = new Promise((resolve) => { - bn.chain.emitter.on(routes.events.EventType.head, async (head) => { - // Wait for the second slot so syncCommitteeWitness is available - if (head.slot > 2) { - resolve(head); - } - }); - }).then(async (head) => { - // Initialize lightclient - loggerLC.info("Initializing lightclient", {slot: head.slot}); - const api = getClient({baseUrl: `http://localhost:${restPort}`}, {config: bn.config}); - const lightclient = await Lightclient.initializeFromCheckpointRoot({ - config: bn.config, - logger: loggerLC, - transport: new LightClientRestTransport(api), - genesisData: { - genesisTime: bn.chain.genesisTime, - genesisValidatorsRoot: bn.chain.genesisValidatorsRoot, - }, - checkpointRoot: fromHexString(head.block), - }); + loggerLC.info("Initialized lightclient", {headSlot: lightclient.getHead().beacon.slot}); + lightclient.start(); - afterEachCallbacks.push(async () => { - lightclient.stop(); - }); + return new Promise((resolve, reject) => { + bn.chain.emitter.on(routes.events.EventType.head, async (head) => { + try { + // Test fetching proofs + const {proof, header} = await getHeadStateProof(lightclient, api, [["latestBlockHeader", "bodyRoot"]]); + const stateRootHex = toHexString(header.beacon.stateRoot); + const lcHeadState = bn.chain.regen.getStateSync(stateRootHex); + if (!lcHeadState) { + throw Error(`LC head state not in cache ${stateRootHex}`); + } - loggerLC.info("Initialized lightclient", {headSlot: lightclient.getHead().beacon.slot}); - lightclient.start(); - - return new Promise((resolve, reject) => { - bn.chain.emitter.on(routes.events.EventType.head, async (head) => { - try { - // Test fetching proofs - const {proof, header} = await getHeadStateProof(lightclient, api, [["latestBlockHeader", "bodyRoot"]]); - const stateRootHex = toHexString(header.beacon.stateRoot); - const lcHeadState = bn.chain.regen.getStateSync(stateRootHex); - if (!lcHeadState) { - throw Error(`LC head state not in cache ${stateRootHex}`); - } - - const stateLcFromProof = ssz.altair.BeaconState.createFromProof(proof, header.beacon.stateRoot); - expect(toHexString(stateLcFromProof.latestBlockHeader.bodyRoot)).toBe( - toHexString(lcHeadState.latestBlockHeader.bodyRoot) - ); - - // Stop test if reached target head slot - const lcHeadSlot = lightclient.getHead().beacon.slot; - if (head.slot - lcHeadSlot > maxLcHeadTrackingDiffSlots) { - throw Error(`Lightclient head ${lcHeadSlot} is too far behind the beacon node ${head.slot}`); - } else if (head.slot > targetSlotToReach) { - resolve(); - } - } catch (e) { - reject(e); + const stateLcFromProof = ssz.altair.BeaconState.createFromProof(proof, header.beacon.stateRoot); + expect(toHexString(stateLcFromProof.latestBlockHeader.bodyRoot)).toBe( + toHexString(lcHeadState.latestBlockHeader.bodyRoot) + ); + + // Stop test if reached target head slot + const lcHeadSlot = lightclient.getHead().beacon.slot; + if (head.slot - lcHeadSlot > maxLcHeadTrackingDiffSlots) { + throw Error(`Lightclient head ${lcHeadSlot} is too far behind the beacon node ${head.slot}`); + } else if (head.slot > targetSlotToReach) { + resolve(); } - }); + } catch (e) { + reject(e); + } }); }); + }); - const promiseTillFinalization = new Promise((resolve) => { - bn.chain.emitter.on(routes.events.EventType.finalizedCheckpoint, (checkpoint) => { - loggerNodeA.info("Node A emitted finalized checkpoint event", {epoch: checkpoint.epoch}); - if (checkpoint.epoch >= finalizedEpochToReach) { - resolve(); - } - }); + const promiseTillFinalization = new Promise((resolve) => { + bn.chain.emitter.on(routes.events.EventType.finalizedCheckpoint, (checkpoint) => { + loggerNodeA.info("Node A emitted finalized checkpoint event", {epoch: checkpoint.epoch}); + if (checkpoint.epoch >= finalizedEpochToReach) { + resolve(); + } }); + }); - await Promise.all([promiseUntilHead, promiseTillFinalization]); + await Promise.all([promiseUntilHead, promiseTillFinalization]); - const headSummary = bn.chain.forkChoice.getHead(); - const head = await bn.db.block.get(fromHexString(headSummary.blockRoot)); - if (!head) throw Error("First beacon node has no head block"); - }); - }, - {timeout: 600_000} -); + const headSummary = bn.chain.forkChoice.getHead(); + const head = await bn.db.block.get(fromHexString(headSummary.blockRoot)); + if (!head) throw Error("First beacon node has no head block"); + }); +}, {timeout: 600_000}); // TODO: Re-incorporate for REST-only light-client async function getHeadStateProof( diff --git a/packages/beacon-node/test/e2e/eth1/jsonRpcHttpClient.test.ts b/packages/beacon-node/test/e2e/eth1/jsonRpcHttpClient.test.ts index ccaef5b63982..4508607ab57f 100644 --- a/packages/beacon-node/test/e2e/eth1/jsonRpcHttpClient.test.ts +++ b/packages/beacon-node/test/e2e/eth1/jsonRpcHttpClient.test.ts @@ -7,347 +7,343 @@ import {JsonRpcHttpClient} from "../../../src/eth1/provider/jsonRpcHttpClient.js import {getGoerliRpcUrl} from "../../testParams.js"; import {RpcPayload} from "../../../src/eth1/interface.js"; -describe( - "eth1 / jsonRpcHttpClient", - function () { - const port = 36421; - const noMethodError = {code: -32601, message: "Method not found"}; - const notInSpecError = "JSON RPC Error not in spec"; - const randomHex = crypto.randomBytes(32).toString("hex"); - - const testCases: { - id: string; - url?: string; - payload?: RpcPayload; - requestListener?: http.RequestListener; - abort?: true; - timeout?: number; - error: any; - errorCode?: string; - }[] = [ - // // NOTE: This DNS query is very expensive, all cache miss. So it can timeout the tests and cause false positives - // { - // id: "Bad domain", - // url: `https://${randomHex}.com`, - // error: "getaddrinfo ENOTFOUND", - // }, - { - id: "Bad subdomain", - // Use random bytes to ensure no collisions - url: `https://${randomHex}.infura.io`, - error: "", - errorCode: "ENOTFOUND", +// To make the code review easy for code block below +/* prettier-ignore */ +describe("eth1 / jsonRpcHttpClient", function () { + const port = 36421; + const noMethodError = {code: -32601, message: "Method not found"}; + const notInSpecError = "JSON RPC Error not in spec"; + const randomHex = crypto.randomBytes(32).toString("hex"); + + const testCases: { + id: string; + url?: string; + payload?: RpcPayload; + requestListener?: http.RequestListener; + abort?: true; + timeout?: number; + error: any; + errorCode?: string; + }[] = [ + // // NOTE: This DNS query is very expensive, all cache miss. So it can timeout the tests and cause false positives + // { + // id: "Bad domain", + // url: `https://${randomHex}.com`, + // error: "getaddrinfo ENOTFOUND", + // }, + { + id: "Bad subdomain", + // Use random bytes to ensure no collisions + url: `https://${randomHex}.infura.io`, + error: "", + errorCode: "ENOTFOUND", + }, + { + id: "Bad port", + url: `http://localhost:${port + 1}`, + requestListener: (req, res) => res.end(), + error: "", + errorCode: "ECONNREFUSED", + }, + { + id: "Not a JSON RPC endpoint", + requestListener: (req, res) => { + res.setHeader("Content-Type", "text/html"); + res.end(""); }, - { - id: "Bad port", - url: `http://localhost:${port + 1}`, - requestListener: (req, res) => res.end(), - error: "", - errorCode: "ECONNREFUSED", - }, - { - id: "Not a JSON RPC endpoint", - requestListener: (req, res) => { - res.setHeader("Content-Type", "text/html"); - res.end(""); - }, - error: "Error parsing JSON", + error: "Error parsing JSON", + }, + { + id: "Endpoint returns HTTP error", + requestListener: (req, res) => { + res.statusCode = 404; + res.end(); }, - { - id: "Endpoint returns HTTP error", - requestListener: (req, res) => { - res.statusCode = 404; - res.end(); - }, - error: "Not Found", + error: "Not Found", + }, + { + id: "RPC payload with error", + requestListener: (req, res) => { + res.setHeader("Content-Type", "application/json"); + res.end(JSON.stringify({jsonrpc: "2.0", id: 83, error: noMethodError})); }, - { - id: "RPC payload with error", - requestListener: (req, res) => { - res.setHeader("Content-Type", "application/json"); - res.end(JSON.stringify({jsonrpc: "2.0", id: 83, error: noMethodError})); - }, - error: noMethodError.message, + error: noMethodError.message, + }, + { + id: "RPC payload with non-spec error: error has no message", + requestListener: (req, res) => { + res.setHeader("Content-Type", "application/json"); + res.end(JSON.stringify({jsonrpc: "2.0", id: 83, error: {code: noMethodError.code}})); }, - { - id: "RPC payload with non-spec error: error has no message", - requestListener: (req, res) => { - res.setHeader("Content-Type", "application/json"); - res.end(JSON.stringify({jsonrpc: "2.0", id: 83, error: {code: noMethodError.code}})); - }, - error: noMethodError.message, + error: noMethodError.message, + }, + { + id: "RPC payload with non-spec error: error is a string", + requestListener: (req, res) => { + res.setHeader("Content-Type", "application/json"); + res.end(JSON.stringify({jsonrpc: "2.0", id: 83, error: notInSpecError})); }, - { - id: "RPC payload with non-spec error: error is a string", - requestListener: (req, res) => { - res.setHeader("Content-Type", "application/json"); - res.end(JSON.stringify({jsonrpc: "2.0", id: 83, error: notInSpecError})); - }, - error: notInSpecError, + error: notInSpecError, + }, + { + id: "RPC payload with no result", + requestListener: (req, res) => { + res.setHeader("Content-Type", "application/json"); + res.end(JSON.stringify({jsonrpc: "2.0", id: 83})); }, - { - id: "RPC payload with no result", - requestListener: (req, res) => { - res.setHeader("Content-Type", "application/json"); - res.end(JSON.stringify({jsonrpc: "2.0", id: 83})); - }, - error: "no result", + error: "no result", + }, + { + id: "Aborted request", + abort: true, + requestListener: () => { + // leave the request open until aborted }, - { - id: "Aborted request", - abort: true, - requestListener: () => { - // leave the request open until aborted - }, - error: "Aborted request", - }, - { - id: "Timeout request", - timeout: 1, - requestListener: () => { - // leave the request open until timeout - }, - error: "Timeout request", + error: "Aborted request", + }, + { + id: "Timeout request", + timeout: 1, + requestListener: () => { + // leave the request open until timeout }, - ]; - - const afterHooks: (() => Promise)[] = []; - - afterEach(async function () { - while (afterHooks.length) { - const afterHook = afterHooks.pop(); - if (afterHook) await afterHook(); - } - }); + error: "Timeout request", + }, + ]; - for (const testCase of testCases) { - const {id, requestListener, abort, timeout} = testCase; - let {url, payload} = testCase; - - it(id, async function () { - if (requestListener) { - if (!url) url = `http://localhost:${port}`; - - const server = http.createServer(requestListener); - await new Promise((resolve) => server.listen(port, resolve)); - afterHooks.push( - () => - new Promise((resolve, reject) => - server.close((err) => { - if (err) reject(err); - else resolve(); - }) - ) - ); - } + const afterHooks: (() => Promise)[] = []; - if (!url) url = getGoerliRpcUrl(); - if (!payload) payload = {method: "no-method", params: []}; - - const controller = new AbortController(); - if (abort) setTimeout(() => controller.abort(), 50); - const eth1JsonRpcClient = new JsonRpcHttpClient([url], {signal: controller.signal}); - - try { - await eth1JsonRpcClient.fetch(payload, {timeout}); - } catch (error) { - if (testCase.errorCode) { - expect((error as FetchError).code).toBe(testCase.errorCode); - } else { - expect((error as Error).message).toEqual(expect.stringContaining(testCase.error)); - } - } - expect.assertions(1); - }); + afterEach(async function () { + while (afterHooks.length) { + const afterHook = afterHooks.pop(); + if (afterHook) await afterHook(); } - }, - {timeout: 10_000} -); - -describe( - "eth1 / jsonRpcHttpClient - with retries", - function () { - const port = 36421; - const noMethodError = {code: -32601, message: "Method not found"}; - const afterHooks: (() => Promise)[] = []; - - afterEach(async function () { - while (afterHooks.length) { - const afterHook = afterHooks.pop(); - if (afterHook) - await afterHook().catch((e: Error) => { - // eslint-disable-next-line no-console - console.error("Error in afterEach hook", e); - }); + }); + + for (const testCase of testCases) { + const {id, requestListener, abort, timeout} = testCase; + let {url, payload} = testCase; + + it(id, async function () { + if (requestListener) { + if (!url) url = `http://localhost:${port}`; + + const server = http.createServer(requestListener); + await new Promise((resolve) => server.listen(port, resolve)); + afterHooks.push( + () => + new Promise((resolve, reject) => + server.close((err) => { + if (err) reject(err); + else resolve(); + }) + ) + ); } - }); - - it("should retry ENOTFOUND", async function () { - let retryCount = 0; - const url = "https://goerli.fake-website.io"; - const payload = {method: "get", params: []}; - const retryAttempts = 2; + if (!url) url = getGoerliRpcUrl(); + if (!payload) payload = {method: "no-method", params: []}; const controller = new AbortController(); + if (abort) setTimeout(() => controller.abort(), 50); const eth1JsonRpcClient = new JsonRpcHttpClient([url], {signal: controller.signal}); - await expect( - eth1JsonRpcClient.fetchWithRetries(payload, { - retryAttempts, - shouldRetry: () => { - // using the shouldRetry function to keep tab of the retried requests - retryCount++; - return true; - }, - }) - ).rejects.toThrow("getaddrinfo ENOTFOUND"); - expect(retryCount).toBe(retryAttempts); - }); - - it("should retry ECONNREFUSED", async function () { - let retryCount = 0; - - const url = `http://localhost:${port + 1}`; - const payload = {method: "get", params: []}; - const retryAttempts = 2; - const controller = new AbortController(); - const eth1JsonRpcClient = new JsonRpcHttpClient([url], {signal: controller.signal}); - await expect( - eth1JsonRpcClient.fetchWithRetries(payload, { - retryAttempts, - shouldRetry: () => { - // using the shouldRetry function to keep tab of the retried requests - retryCount++; - return true; - }, - }) - ).rejects.toThrow(expect.objectContaining({code: "ECONNREFUSED"})); - expect(retryCount).toBe(retryAttempts); + try { + await eth1JsonRpcClient.fetch(payload, {timeout}); + } catch (error) { + if (testCase.errorCode) { + expect((error as FetchError).code).toBe(testCase.errorCode); + } else { + expect((error as Error).message).toEqual(expect.stringContaining(testCase.error)); + } + } + expect.assertions(1); }); - - it("should retry 404", async function () { - let retryCount = 0; - - const server = http.createServer((req, res) => { - retryCount++; - res.statusCode = 404; - res.end(); - }); - - await new Promise((resolve) => server.listen(port, resolve)); - afterHooks.push( - () => - new Promise((resolve, reject) => - server.close((err) => { - if (err) reject(err); - else resolve(); - }) - ) - ); - - const url = `http://localhost:${port}`; - const payload = {method: "get", params: []}; - const retryAttempts = 2; - - const controller = new AbortController(); - const eth1JsonRpcClient = new JsonRpcHttpClient([url], {signal: controller.signal}); - await expect(eth1JsonRpcClient.fetchWithRetries(payload, {retryAttempts})).rejects.toThrow("Not Found"); - expect(retryCount).toBe(retryAttempts); + } +}, {timeout: 10_000}); + +// To make the code review easy for code block below +/* prettier-ignore */ +describe("eth1 / jsonRpcHttpClient - with retries", function () { + const port = 36421; + const noMethodError = {code: -32601, message: "Method not found"}; + const afterHooks: (() => Promise)[] = []; + + afterEach(async function () { + while (afterHooks.length) { + const afterHook = afterHooks.pop(); + if (afterHook) + await afterHook().catch((e: Error) => { + // eslint-disable-next-line no-console + console.error("Error in afterEach hook", e); + }); + } + }); + + it("should retry ENOTFOUND", async function () { + let retryCount = 0; + + const url = "https://goerli.fake-website.io"; + const payload = {method: "get", params: []}; + const retryAttempts = 2; + + const controller = new AbortController(); + const eth1JsonRpcClient = new JsonRpcHttpClient([url], {signal: controller.signal}); + await expect( + eth1JsonRpcClient.fetchWithRetries(payload, { + retryAttempts, + shouldRetry: () => { + // using the shouldRetry function to keep tab of the retried requests + retryCount++; + return true; + }, + }) + ).rejects.toThrow("getaddrinfo ENOTFOUND"); + expect(retryCount).toBe(retryAttempts); + }); + + it("should retry ECONNREFUSED", async function () { + let retryCount = 0; + + const url = `http://localhost:${port + 1}`; + const payload = {method: "get", params: []}; + const retryAttempts = 2; + + const controller = new AbortController(); + const eth1JsonRpcClient = new JsonRpcHttpClient([url], {signal: controller.signal}); + await expect( + eth1JsonRpcClient.fetchWithRetries(payload, { + retryAttempts, + shouldRetry: () => { + // using the shouldRetry function to keep tab of the retried requests + retryCount++; + return true; + }, + }) + ).rejects.toThrow(expect.objectContaining({code: "ECONNREFUSED"})); + expect(retryCount).toBe(retryAttempts); + }); + + it("should retry 404", async function () { + let retryCount = 0; + + const server = http.createServer((req, res) => { + retryCount++; + res.statusCode = 404; + res.end(); }); - it("should retry timeout", async function () { - let retryCount = 0; - - const server = http.createServer(async () => { - retryCount++; - }); - - await new Promise((resolve) => server.listen(port, resolve)); - afterHooks.push( - () => - new Promise((resolve, reject) => - server.close((err) => { - if (err) reject(err); - else resolve(); - }) - ) - ); - // it's observed that immediate request after the server started end up ECONNRESET - await sleep(100); - - const url = `http://localhost:${port}`; - const payload = {method: "get", params: []}; - const retryAttempts = 2; - const timeout = 2000; - - const controller = new AbortController(); - const eth1JsonRpcClient = new JsonRpcHttpClient([url], {signal: controller.signal}); - await expect(eth1JsonRpcClient.fetchWithRetries(payload, {retryAttempts, timeout})).rejects.toThrow( - "Timeout request" - ); - expect(retryCount).toBe(retryAttempts); + await new Promise((resolve) => server.listen(port, resolve)); + afterHooks.push( + () => + new Promise((resolve, reject) => + server.close((err) => { + if (err) reject(err); + else resolve(); + }) + ) + ); + + const url = `http://localhost:${port}`; + const payload = {method: "get", params: []}; + const retryAttempts = 2; + + const controller = new AbortController(); + const eth1JsonRpcClient = new JsonRpcHttpClient([url], {signal: controller.signal}); + await expect(eth1JsonRpcClient.fetchWithRetries(payload, {retryAttempts})).rejects.toThrow("Not Found"); + expect(retryCount).toBe(retryAttempts); + }); + + it("should retry timeout", async function () { + let retryCount = 0; + + const server = http.createServer(async () => { + retryCount++; }); - it("should retry aborted", async function () { - let retryCount = 0; - const server = http.createServer(() => { - retryCount++; - // leave the request open until timeout - }); - - await new Promise((resolve) => server.listen(port, resolve)); - afterHooks.push( - () => - new Promise((resolve, reject) => - server.close((err) => { - if (err) reject(err); - else resolve(); - }) - ) - ); - - const url = `http://localhost:${port}`; - const payload = {method: "get", params: []}; - const retryAttempts = 2; - const timeout = 2000; - - const controller = new AbortController(); - setTimeout(() => controller.abort(), 50); - const eth1JsonRpcClient = new JsonRpcHttpClient([url], {signal: controller.signal}); - await expect(eth1JsonRpcClient.fetchWithRetries(payload, {retryAttempts, timeout})).rejects.toThrow("Aborted"); - expect(retryCount).toBe(1); + await new Promise((resolve) => server.listen(port, resolve)); + afterHooks.push( + () => + new Promise((resolve, reject) => + server.close((err) => { + if (err) reject(err); + else resolve(); + }) + ) + ); + // it's observed that immediate request after the server started end up ECONNRESET + await sleep(100); + + const url = `http://localhost:${port}`; + const payload = {method: "get", params: []}; + const retryAttempts = 2; + const timeout = 2000; + + const controller = new AbortController(); + const eth1JsonRpcClient = new JsonRpcHttpClient([url], {signal: controller.signal}); + await expect(eth1JsonRpcClient.fetchWithRetries(payload, {retryAttempts, timeout})).rejects.toThrow( + "Timeout request" + ); + expect(retryCount).toBe(retryAttempts); + }); + + it("should retry aborted", async function () { + let retryCount = 0; + const server = http.createServer(() => { + retryCount++; + // leave the request open until timeout }); - it("should not retry payload error", async function () { - let retryCount = 0; - - const server = http.createServer((req, res) => { - retryCount++; - res.setHeader("Content-Type", "application/json"); - res.end(JSON.stringify({jsonrpc: "2.0", id: 83, error: noMethodError})); - }); - - await new Promise((resolve) => server.listen(port, resolve)); - afterHooks.push( - () => - new Promise((resolve, reject) => - server.close((err) => { - if (err) reject(err); - else resolve(); - }) - ) - ); - - const url = `http://localhost:${port}`; - const payload = {method: "get", params: []}; - const retryAttempts = 2; - - const controller = new AbortController(); - const eth1JsonRpcClient = new JsonRpcHttpClient([url], {signal: controller.signal}); - await expect(eth1JsonRpcClient.fetchWithRetries(payload, {retryAttempts})).rejects.toThrow("Method not found"); - expect(retryCount).toBe(1); + await new Promise((resolve) => server.listen(port, resolve)); + afterHooks.push( + () => + new Promise((resolve, reject) => + server.close((err) => { + if (err) reject(err); + else resolve(); + }) + ) + ); + + const url = `http://localhost:${port}`; + const payload = {method: "get", params: []}; + const retryAttempts = 2; + const timeout = 2000; + + const controller = new AbortController(); + setTimeout(() => controller.abort(), 50); + const eth1JsonRpcClient = new JsonRpcHttpClient([url], {signal: controller.signal}); + await expect(eth1JsonRpcClient.fetchWithRetries(payload, {retryAttempts, timeout})).rejects.toThrow("Aborted"); + expect(retryCount).toBe(1); + }); + + it("should not retry payload error", async function () { + let retryCount = 0; + + const server = http.createServer((req, res) => { + retryCount++; + res.setHeader("Content-Type", "application/json"); + res.end(JSON.stringify({jsonrpc: "2.0", id: 83, error: noMethodError})); }); - }, - {timeout: 10_000} -); + + await new Promise((resolve) => server.listen(port, resolve)); + afterHooks.push( + () => + new Promise((resolve, reject) => + server.close((err) => { + if (err) reject(err); + else resolve(); + }) + ) + ); + + const url = `http://localhost:${port}`; + const payload = {method: "get", params: []}; + const retryAttempts = 2; + + const controller = new AbortController(); + const eth1JsonRpcClient = new JsonRpcHttpClient([url], {signal: controller.signal}); + await expect(eth1JsonRpcClient.fetchWithRetries(payload, {retryAttempts})).rejects.toThrow("Method not found"); + expect(retryCount).toBe(1); + }); +}, {timeout: 10_000}); diff --git a/packages/beacon-node/test/e2e/sync/unknownBlockSync.test.ts b/packages/beacon-node/test/e2e/sync/unknownBlockSync.test.ts index bdeb8d42efbe..06c51715da27 100644 --- a/packages/beacon-node/test/e2e/sync/unknownBlockSync.test.ts +++ b/packages/beacon-node/test/e2e/sync/unknownBlockSync.test.ts @@ -17,137 +17,135 @@ import {testLogger, LogLevel, TestLoggerOpts} from "../../utils/logger.js"; import {BlockError, BlockErrorCode} from "../../../src/chain/errors/index.js"; import {BlockSource, getBlockInput} from "../../../src/chain/blocks/types.js"; -describe( - "sync / unknown block sync", - function () { - const validatorCount = 8; - const testParams: Pick = { - // eslint-disable-next-line @typescript-eslint/naming-convention - SECONDS_PER_SLOT: 2, - }; - - const afterEachCallbacks: (() => Promise | void)[] = []; - afterEach(async () => { - while (afterEachCallbacks.length > 0) { - const callback = afterEachCallbacks.pop(); - if (callback) await callback(); - } - }); +// To make the code review easy for code block below +/* prettier-ignore */ +describe("sync / unknown block sync", function () { + const validatorCount = 8; + const testParams: Pick = { + // eslint-disable-next-line @typescript-eslint/naming-convention + SECONDS_PER_SLOT: 2, + }; + + const afterEachCallbacks: (() => Promise | void)[] = []; + afterEach(async () => { + while (afterEachCallbacks.length > 0) { + const callback = afterEachCallbacks.pop(); + if (callback) await callback(); + } + }); + + const testCases: {id: string; event: NetworkEvent}[] = [ + { + id: "should do an unknown block parent sync from another BN", + event: NetworkEvent.unknownBlockParent, + }, + { + id: "should do an unknown block sync from another BN", + event: NetworkEvent.unknownBlock, + }, + ]; + + for (const {id, event} of testCases) { + it(id, async function () { + // the node needs time to transpile/initialize bls worker threads + const genesisSlotsDelay = 7; + const genesisTime = Math.floor(Date.now() / 1000) + genesisSlotsDelay * testParams.SECONDS_PER_SLOT; + const testLoggerOpts: TestLoggerOpts = { + level: LogLevel.info, + timestampFormat: { + format: TimestampFormatCode.EpochSlot, + genesisTime, + slotsPerEpoch: SLOTS_PER_EPOCH, + secondsPerSlot: testParams.SECONDS_PER_SLOT, + }, + }; + + const loggerNodeA = testLogger("Node-A", testLoggerOpts); + const loggerNodeB = testLogger("Node-B", testLoggerOpts); + + const bn = await getDevBeaconNode({ + params: testParams, + options: { + sync: {isSingleNode: true}, + network: {allowPublishToZeroPeers: true}, + chain: {blsVerifyAllMainThread: true}, + }, + validatorCount, + logger: loggerNodeA, + }); - const testCases: {id: string; event: NetworkEvent}[] = [ - { - id: "should do an unknown block parent sync from another BN", - event: NetworkEvent.unknownBlockParent, - }, - { - id: "should do an unknown block sync from another BN", - event: NetworkEvent.unknownBlock, - }, - ]; - - for (const {id, event} of testCases) { - it(id, async function () { - // the node needs time to transpile/initialize bls worker threads - const genesisSlotsDelay = 7; - const genesisTime = Math.floor(Date.now() / 1000) + genesisSlotsDelay * testParams.SECONDS_PER_SLOT; - const testLoggerOpts: TestLoggerOpts = { - level: LogLevel.info, - timestampFormat: { - format: TimestampFormatCode.EpochSlot, - genesisTime, - slotsPerEpoch: SLOTS_PER_EPOCH, - secondsPerSlot: testParams.SECONDS_PER_SLOT, - }, - }; - - const loggerNodeA = testLogger("Node-A", testLoggerOpts); - const loggerNodeB = testLogger("Node-B", testLoggerOpts); - - const bn = await getDevBeaconNode({ - params: testParams, - options: { - sync: {isSingleNode: true}, - network: {allowPublishToZeroPeers: true}, - chain: {blsVerifyAllMainThread: true}, - }, - validatorCount, - logger: loggerNodeA, - }); - - afterEachCallbacks.push(() => bn.close()); - - const {validators} = await getAndInitDevValidators({ - node: bn, - validatorsPerClient: validatorCount, - validatorClientCount: 1, - startIndex: 0, - useRestApi: false, - testLoggerOpts, - }); - - afterEachCallbacks.push(() => Promise.all(validators.map((v) => v.close()))); - - // stop bn after validators - afterEachCallbacks.push(() => bn.close()); - - await waitForEvent(bn.chain.emitter, ChainEvent.checkpoint, 240000); - loggerNodeA.info("Node A emitted checkpoint event"); - - const bn2 = await getDevBeaconNode({ - params: testParams, - options: { - api: {rest: {enabled: false}}, - sync: {disableRangeSync: true}, - chain: {blsVerifyAllMainThread: true}, - }, - validatorCount, - genesisTime: bn.chain.getHeadState().genesisTime, - logger: loggerNodeB, - }); - - afterEachCallbacks.push(() => bn2.close()); - - const headSummary = bn.chain.forkChoice.getHead(); - const head = await bn.db.block.get(fromHexString(headSummary.blockRoot)); - if (!head) throw Error("First beacon node has no head block"); - const waitForSynced = waitForEvent( - bn2.chain.emitter, - routes.events.EventType.head, - 100000, - ({block}) => block === headSummary.blockRoot - ); - - await connect(bn2.network, bn.network); - const headInput = getBlockInput.preDeneb(config, head, BlockSource.gossip, null); - - switch (event) { - case NetworkEvent.unknownBlockParent: - await bn2.chain.processBlock(headInput).catch((e) => { - if (e instanceof BlockError && e.type.code === BlockErrorCode.PARENT_UNKNOWN) { - // Expected - bn2.network.events.emit(NetworkEvent.unknownBlockParent, { - blockInput: headInput, - peer: bn2.network.peerId.toString(), - }); - } else { - throw e; - } - }); - break; - case NetworkEvent.unknownBlock: - bn2.network.events.emit(NetworkEvent.unknownBlock, { - rootHex: headSummary.blockRoot, - peer: bn2.network.peerId.toString(), - }); - break; - default: - throw Error("Unknown event type"); - } - - // Wait for NODE-A head to be processed in NODE-B without range sync - await waitForSynced; + afterEachCallbacks.push(() => bn.close()); + + const {validators} = await getAndInitDevValidators({ + node: bn, + validatorsPerClient: validatorCount, + validatorClientCount: 1, + startIndex: 0, + useRestApi: false, + testLoggerOpts, }); - } - }, - {timeout: 30_000} -); + + afterEachCallbacks.push(() => Promise.all(validators.map((v) => v.close()))); + + // stop bn after validators + afterEachCallbacks.push(() => bn.close()); + + await waitForEvent(bn.chain.emitter, ChainEvent.checkpoint, 240000); + loggerNodeA.info("Node A emitted checkpoint event"); + + const bn2 = await getDevBeaconNode({ + params: testParams, + options: { + api: {rest: {enabled: false}}, + sync: {disableRangeSync: true}, + chain: {blsVerifyAllMainThread: true}, + }, + validatorCount, + genesisTime: bn.chain.getHeadState().genesisTime, + logger: loggerNodeB, + }); + + afterEachCallbacks.push(() => bn2.close()); + + const headSummary = bn.chain.forkChoice.getHead(); + const head = await bn.db.block.get(fromHexString(headSummary.blockRoot)); + if (!head) throw Error("First beacon node has no head block"); + const waitForSynced = waitForEvent( + bn2.chain.emitter, + routes.events.EventType.head, + 100000, + ({block}) => block === headSummary.blockRoot + ); + + await connect(bn2.network, bn.network); + const headInput = getBlockInput.preDeneb(config, head, BlockSource.gossip, null); + + switch (event) { + case NetworkEvent.unknownBlockParent: + await bn2.chain.processBlock(headInput).catch((e) => { + if (e instanceof BlockError && e.type.code === BlockErrorCode.PARENT_UNKNOWN) { + // Expected + bn2.network.events.emit(NetworkEvent.unknownBlockParent, { + blockInput: headInput, + peer: bn2.network.peerId.toString(), + }); + } else { + throw e; + } + }); + break; + case NetworkEvent.unknownBlock: + bn2.network.events.emit(NetworkEvent.unknownBlock, { + rootHex: headSummary.blockRoot, + peer: bn2.network.peerId.toString(), + }); + break; + default: + throw Error("Unknown event type"); + } + + // Wait for NODE-A head to be processed in NODE-B without range sync + await waitForSynced; + }); + } +}, {timeout: 30_000});