diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index bd802e83bc69..b5fa586c7a8b 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -17,6 +17,7 @@ on: branches: - stable - unstable + workflow_dispatch: jobs: run: diff --git a/.wordlist.txt b/.wordlist.txt index 83d2bd51aa73..b7cff203f57c 100644 --- a/.wordlist.txt +++ b/.wordlist.txt @@ -77,6 +77,7 @@ config configs const constantish +coreutils cors cryptographic dApp diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index eaa110a60467..b5990d2eabbf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,6 +7,13 @@ Thanks for your interest in contributing to Lodestar. It's people like you that - :gear: [NodeJS](https://nodejs.org/) (LTS) - :toolbox: [Yarn](https://yarnpkg.com/) +### MacOS Specifics + +When using MacOS, there are a couple of extra prerequisites that are required. + +- python +- coreutils (e.g. via `brew install coreutils`) + ## Getting Started - :gear: Run `yarn` to install dependencies. diff --git a/dashboards/lodestar_block_production.json b/dashboards/lodestar_block_production.json index 7bb0b4a2db7e..b999e47a33d4 100644 --- a/dashboards/lodestar_block_production.json +++ b/dashboards/lodestar_block_production.json @@ -53,6 +53,358 @@ ], "liveNow": false, "panels": [ + { + "type": "timeseries", + "title": "Full block production avg time with steps", + "gridPos": { + "x": 0, + "y": 1, + "w": 12, + "h": 8 + }, + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "id": 546, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "refId": "proposerSlashing", + "expr": "rate(beacon_block_production_execution_steps_seconds{step=\"proposerSlashing\"}[$rate_interval])\n/\nrate(beacon_block_production_execution_steps_seconds{step=\"proposerSlashing\"}[$rate_interval])", + "range": true, + "instant": false, + "hide": false, + "editorMode": "code", + "legendFormat": "{{step}}", + "exemplar": false + }, + { + "refId": "attesterSlashings", + "expr": "rate(beacon_block_production_execution_steps_seconds{step=\"attesterSlashings\"}[$rate_interval])\n/\nrate(beacon_block_production_execution_steps_seconds{step=\"attesterSlashings\"}[$rate_interval])", + "range": true, + "instant": false, + "datasource": { + "uid": "${DS_PROMETHEUS}", + "type": "prometheus" + }, + "hide": false, + "editorMode": "code", + "legendFormat": "{{step}}" + }, + { + "refId": "voluntaryExits", + "expr": "rate(beacon_block_production_execution_steps_seconds{step=\"voluntaryExits\"}[$rate_interval])\n/\nrate(beacon_block_production_execution_steps_seconds{step=\"voluntaryExits\"}[$rate_interval])", + "range": true, + "instant": false, + "datasource": { + "uid": "${DS_PROMETHEUS}", + "type": "prometheus" + }, + "hide": false, + "editorMode": "code", + "legendFormat": "{{step}}" + }, + { + "refId": "blsToExecutionChanges", + "expr": "rate(beacon_block_production_execution_steps_seconds{step=\"blsToExecutionChanges\"}[$rate_interval])\n/\nrate(beacon_block_production_execution_steps_seconds{step=\"blsToExecutionChanges\"}[$rate_interval])", + "range": true, + "instant": false, + "datasource": { + "uid": "${DS_PROMETHEUS}", + "type": "prometheus" + }, + "hide": false, + "editorMode": "code", + "legendFormat": "{{step}}" + }, + { + "refId": "attestations", + "expr": "rate(beacon_block_production_execution_steps_seconds{step=\"attestations\"}[$rate_interval])\n/\nrate(beacon_block_production_execution_steps_seconds{step=\"attestations\"}[$rate_interval])", + "range": true, + "instant": false, + "datasource": { + "uid": "${DS_PROMETHEUS}", + "type": "prometheus" + }, + "hide": false, + "editorMode": "code", + "legendFormat": "{{step}}" + }, + { + "refId": "eth1DataAndDeposits", + "expr": "rate(beacon_block_production_execution_steps_seconds{step=\"eth1DataAndDeposits\"}[$rate_interval])\n/\nrate(beacon_block_production_execution_steps_seconds{step=\"eth1DataAndDeposits\"}[$rate_interval])", + "range": true, + "instant": false, + "datasource": { + "uid": "${DS_PROMETHEUS}", + "type": "prometheus" + }, + "hide": false, + "editorMode": "code", + "legendFormat": "{{step}}" + }, + { + "refId": "syncAggregate", + "expr": "rate(beacon_block_production_execution_steps_seconds{step=\"syncAggregate\"}[$rate_interval])\n/\nrate(beacon_block_production_execution_steps_seconds{step=\"syncAggregate\"}[$rate_interval])", + "range": true, + "instant": false, + "datasource": { + "uid": "${DS_PROMETHEUS}", + "type": "prometheus" + }, + "hide": false, + "editorMode": "code", + "legendFormat": "{{step}}" + }, + { + "refId": "executionPayload", + "expr": "rate(beacon_block_production_execution_steps_seconds{step=\"executionPayload\"}[$rate_interval])\n/\nrate(beacon_block_production_execution_steps_seconds{step=\"executionPayload\"}[$rate_interval])", + "range": true, + "instant": false, + "datasource": { + "uid": "${DS_PROMETHEUS}", + "type": "prometheus" + }, + "hide": false, + "editorMode": "code", + "legendFormat": "{{step}}" + } + ], + "options": { + "tooltip": { + "mode": "multi", + "sort": "none" + }, + "legend": { + "showLegend": true, + "displayMode": "list", + "placement": "bottom", + "calcs": [] + } + }, + "fieldConfig": { + "defaults": { + "custom": { + "drawStyle": "line", + "lineInterpolation": "linear", + "barAlignment": 0, + "lineWidth": 1, + "fillOpacity": 30, + "gradientMode": "opacity", + "spanNulls": false, + "insertNulls": false, + "showPoints": "auto", + "pointSize": 5, + "stacking": { + "mode": "normal", + "group": "A" + }, + "axisPlacement": "auto", + "axisLabel": "", + "axisColorMode": "text", + "scaleDistribution": { + "type": "linear" + }, + "axisCenteredZero": false, + "hideFrom": { + "tooltip": false, + "viz": false, + "legend": false + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "color": { + "mode": "palette-classic" + }, + "mappings": [], + "unit": "s" + }, + "overrides": [] + }, + "transformations": [] + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "custom": { + "drawStyle": "line", + "lineInterpolation": "linear", + "barAlignment": 0, + "lineWidth": 1, + "fillOpacity": 30, + "gradientMode": "opacity", + "spanNulls": false, + "insertNulls": false, + "showPoints": "auto", + "pointSize": 5, + "stacking": { + "mode": "normal", + "group": "A" + }, + "axisPlacement": "auto", + "axisLabel": "", + "axisColorMode": "text", + "scaleDistribution": { + "type": "linear" + }, + "axisCenteredZero": false, + "hideFrom": { + "tooltip": false, + "viz": false, + "legend": false + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "color": { + "mode": "palette-classic" + }, + "mappings": [], + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "x": 12, + "y": 1, + "w": 12, + "h": 8 + }, + "id": 547, + "options": { + "tooltip": { + "mode": "multi", + "sort": "none" + }, + "legend": { + "showLegend": true, + "displayMode": "list", + "placement": "bottom", + "calcs": [] + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "refId": "proposerSlashing", + "expr": "rate(beacon_block_production_builder_steps_seconds{step=\"proposerSlashing\"}[$rate_interval])\n/\nrate(beacon_block_production_builder_steps_seconds{step=\"proposerSlashing\"}[$rate_interval])", + "range": true, + "instant": false, + "hide": false, + "editorMode": "code", + "legendFormat": "{{step}}", + "exemplar": false + }, + { + "refId": "attesterSlashings", + "expr": "rate(beacon_block_production_builder_steps_seconds{step=\"attesterSlashings\"}[$rate_interval])\n/\nrate(beacon_block_production_builder_steps_seconds{step=\"attesterSlashings\"}[$rate_interval])", + "range": true, + "instant": false, + "datasource": { + "uid": "${DS_PROMETHEUS}", + "type": "prometheus" + }, + "hide": false, + "editorMode": "code", + "legendFormat": "{{step}}" + }, + { + "refId": "voluntaryExits", + "expr": "rate(beacon_block_production_builder_steps_seconds{step=\"voluntaryExits\"}[$rate_interval])\n/\nrate(beacon_block_production_builder_steps_seconds{step=\"voluntaryExits\"}[$rate_interval])", + "range": true, + "instant": false, + "datasource": { + "uid": "${DS_PROMETHEUS}", + "type": "prometheus" + }, + "hide": false, + "editorMode": "code", + "legendFormat": "{{step}}" + }, + { + "refId": "blsToExecutionChanges", + "expr": "rate(beacon_block_production_builder_steps_seconds{step=\"blsToExecutionChanges\"}[$rate_interval])\n/\nrate(beacon_block_production_builder_steps_seconds{step=\"blsToExecutionChanges\"}[$rate_interval])", + "range": true, + "instant": false, + "datasource": { + "uid": "${DS_PROMETHEUS}", + "type": "prometheus" + }, + "hide": false, + "editorMode": "code", + "legendFormat": "{{step}}" + }, + { + "refId": "attestations", + "expr": "rate(beacon_block_production_builder_steps_seconds{step=\"attestations\"}[$rate_interval])\n/\nrate(beacon_block_production_builder_steps_seconds{step=\"attestations\"}[$rate_interval])", + "range": true, + "instant": false, + "datasource": { + "uid": "${DS_PROMETHEUS}", + "type": "prometheus" + }, + "hide": false, + "editorMode": "code", + "legendFormat": "{{step}}" + }, + { + "refId": "eth1DataAndDeposits", + "expr": "rate(beacon_block_production_builder_steps_seconds{step=\"eth1DataAndDeposits\"}[$rate_interval])\n/\nrate(beacon_block_production_builder_steps_seconds{step=\"eth1DataAndDeposits\"}[$rate_interval])", + "range": true, + "instant": false, + "datasource": { + "uid": "${DS_PROMETHEUS}", + "type": "prometheus" + }, + "hide": false, + "editorMode": "code", + "legendFormat": "{{step}}" + }, + { + "refId": "syncAggregate", + "expr": "rate(beacon_block_production_builder_steps_seconds{step=\"syncAggregate\"}[$rate_interval])\n/\nrate(beacon_block_production_builder_steps_seconds{step=\"syncAggregate\"}[$rate_interval])", + "range": true, + "instant": false, + "datasource": { + "uid": "${DS_PROMETHEUS}", + "type": "prometheus" + }, + "hide": false, + "editorMode": "code", + "legendFormat": "{{step}}" + }, + { + "refId": "executionPayload", + "expr": "rate(beacon_block_production_builder_steps_seconds{step=\"executionPayload\"}[$rate_interval])\n/\nrate(beacon_block_production_builder_steps_seconds{step=\"executionPayload\"}[$rate_interval])", + "range": true, + "instant": false, + "datasource": { + "uid": "${DS_PROMETHEUS}", + "type": "prometheus" + }, + "hide": false, + "editorMode": "code", + "legendFormat": "{{step}}" + } + ], + "title": "Blinded block production avg time with steps", + "type": "timeseries", + "transformations": [] + }, { "collapsed": false, "datasource": { @@ -309,7 +661,7 @@ "expr": "rate(beacon_block_production_seconds_sum[$rate_interval])\n/\nrate(beacon_block_production_seconds_count[$rate_interval])", "format": "heatmap", "interval": "", - "legendFormat": "time", + "legendFormat": "{{instance}} - {{source}}", "refId": "A" } ], diff --git a/packages/api/src/utils/client/httpClient.ts b/packages/api/src/utils/client/httpClient.ts index cc91d8dc8136..b3e123de6ab4 100644 --- a/packages/api/src/utils/client/httpClient.ts +++ b/packages/api/src/utils/client/httpClient.ts @@ -49,7 +49,8 @@ export class HttpClient implements IHttpClient { private readonly urlsScore: number[]; get baseUrl(): string { - return this.urlsInits[0].baseUrl; + // Don't leak username/password to caller + return new URL(this.urlsInits[0].baseUrl).origin; } constructor(opts: HttpClientOptions, {logger, metrics}: HttpClientModules = {}) { diff --git a/packages/api/src/utils/headers.ts b/packages/api/src/utils/headers.ts index 2c1b764037da..5308dcd9a5bc 100644 --- a/packages/api/src/utils/headers.ts +++ b/packages/api/src/utils/headers.ts @@ -90,7 +90,7 @@ export function setAuthorizationHeader(url: URL, headers: Headers, {bearerToken} } if (url.username || url.password) { if (!headers.has("Authorization")) { - headers.set("Authorization", `Basic ${toBase64(`${url.username}:${url.password}`)}`); + headers.set("Authorization", `Basic ${toBase64(decodeURIComponent(`${url.username}:${url.password}`))}`); } // Remove the username and password from the URL url.username = ""; diff --git a/packages/api/test/unit/client/httpClient.test.ts b/packages/api/test/unit/client/httpClient.test.ts index be6006d9e375..85dd1106b996 100644 --- a/packages/api/test/unit/client/httpClient.test.ts +++ b/packages/api/test/unit/client/httpClient.test.ts @@ -1,7 +1,7 @@ import {IncomingMessage} from "node:http"; import {expect} from "chai"; import fastify, {RouteOptions} from "fastify"; -import {ErrorAborted, TimeoutError} from "@lodestar/utils"; +import {ErrorAborted, TimeoutError, toBase64} from "@lodestar/utils"; import {HttpClient, HttpError} from "../../../src/utils/client/index.js"; import {HttpStatusCode} from "../../../src/utils/client/httpStatusCode.js"; @@ -151,6 +151,39 @@ describe("httpClient json client", () => { await httpClient.json(testRoute); }); + it("should not URI-encode user credentials in Authorization header", async () => { + // Semi exhaustive set of characters that RFC-3986 allows in the userinfo portion of a URI + // Notably absent is `%`. See comment on isValidHttpUrl(). + const username = "A1-._~!$'&\"()*+,;="; + const password = "b2-._~!$'&\"()*+,;="; + let {baseUrl} = await getServer({ + ...testRoute, + handler: async (req) => { + expect(req.headers.authorization).to.equal(`Basic ${toBase64(`${username}:${password}`)}`); + return {}; + }, + }); + // Since `new URL()` is what URI-encodes, we have to do string manipulation to set the username/password + // First validate the assumption that the URL starts with http:// + expect(baseUrl.indexOf("http://")).to.equal(0); + // We avoid using baseUrl.replace() because it treats $ as a special character + baseUrl = `http://${username}:${password}@${baseUrl.substring("http://".length)}`; + + const httpClient = new HttpClient({baseUrl: baseUrl}); + + await httpClient.json(testRoute); + }); + + it("should not leak user credentials in baseUrl getter", () => { + const url = new URL("http://localhost"); + url.username = "user"; + url.password = "password"; + const httpClient = new HttpClient({baseUrl: url.toString()}); + + expect(httpClient.baseUrl.includes(url.username)).to.be.false; + expect(httpClient.baseUrl.includes(url.password)).to.be.false; + }); + it("should handle aborting request with timeout", async () => { const {baseUrl} = await getServer({ ...testRoute, diff --git a/packages/api/test/unit/client/httpClientOptions.test.ts b/packages/api/test/unit/client/httpClientOptions.test.ts index 1bad7c69a406..0409a41f5aa9 100644 --- a/packages/api/test/unit/client/httpClientOptions.test.ts +++ b/packages/api/test/unit/client/httpClientOptions.test.ts @@ -83,4 +83,8 @@ describe("HTTPClient options", () => { it("Throw if invalid value in urls option", () => { expect(() => new HttpClient({urls: ["invalid"]})).to.throw(Error); }); + + it("Throw if invalid username/password", () => { + expect(() => new HttpClient({baseUrl: "http://hasa%:%can'tbedecoded@localhost"})).to.throw(Error); + }); }); diff --git a/packages/beacon-node/package.json b/packages/beacon-node/package.json index 083cc2410df1..c8cff7cbf28c 100644 --- a/packages/beacon-node/package.json +++ b/packages/beacon-node/package.json @@ -77,10 +77,10 @@ "lint:fix": "yarn run lint --fix", "pretest": "yarn run check-types", "test": "yarn test:unit && yarn test:e2e", - "test:unit:minimal": "vitest --run --dir test/unit/ --coverage", + "test:unit:minimal": "vitest --run --segfaultRetry 3 --dir test/unit/ --coverage", "test:unit:mainnet": "LODESTAR_PRESET=mainnet nyc --cache-dir .nyc_output/.cache -e .ts mocha 'test/unit-mainnet/**/*.test.ts'", "test:unit": "yarn test:unit:minimal && yarn test:unit:mainnet", - "test:e2e": "LODESTAR_PRESET=minimal vitest --run --single-thread --dir test/e2e", + "test:e2e": "LODESTAR_PRESET=minimal vitest --run --segfaultRetry 3 --single-thread --dir test/e2e", "test:sim": "mocha 'test/sim/**/*.test.ts'", "test:sim:merge-interop": "mocha 'test/sim/merge-interop.test.ts'", "test:sim:mergemock": "mocha 'test/sim/mergemock.test.ts'", diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index decab5be5c3b..d9c6906229b7 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -72,7 +72,7 @@ const SYNC_TOLERANCE_EPOCHS = 1; * Cutoff time to wait for execution and builder block production apis to resolve * Post this time, race execution and builder to pick whatever resolves first * - * Emprically the builder block resolves in ~1.5+ seconds, and executon should resolve <1 sec. + * Empirically the builder block resolves in ~1.5+ seconds, and execution should resolve <1 sec. * So lowering the cutoff to 2 sec from 3 seconds to publish faster for successful proposal * as proposals post 4 seconds into the slot seems to be not being included */ @@ -437,7 +437,7 @@ export function getValidatorApi({ chain.executionBuilder !== undefined && builderSelection !== routes.validator.BuilderSelection.ExecutionOnly; - logger.verbose("produceBlockV3 assembling block", { + logger.verbose("Assembling block with produceBlockV3 ", { fork, builderSelection, slot, diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index 3c86a7aaa2d4..3464aad8b673 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -866,7 +866,7 @@ export class BeaconChain implements IBeaconChain { this.metrics?.blockProductionCaches.producedBlockRoot.set(this.producedBlockRoot.size); pruneSetToMax(this.producedBlindedBlockRoot, this.opts.maxCachedProducedRoots ?? DEFAULT_MAX_CACHED_PRODUCED_ROOTS); - this.metrics?.blockProductionCaches.producedBlindedBlockRoot.set(this.producedBlockRoot.size); + this.metrics?.blockProductionCaches.producedBlindedBlockRoot.set(this.producedBlindedBlockRoot.size); if (this.config.getForkSeq(slot) >= ForkSeq.deneb) { pruneSetToMax( diff --git a/packages/beacon-node/src/chain/opPools/opPool.ts b/packages/beacon-node/src/chain/opPools/opPool.ts index c058404834f0..cee8d0614c30 100644 --- a/packages/beacon-node/src/chain/opPools/opPool.ts +++ b/packages/beacon-node/src/chain/opPools/opPool.ts @@ -17,6 +17,8 @@ import { import {Epoch, phase0, capella, ssz, ValidatorIndex} from "@lodestar/types"; import {IBeaconDb} from "../../db/index.js"; import {SignedBLSToExecutionChangeVersioned} from "../../util/types.js"; +import {BlockType} from "../interface.js"; +import {Metrics} from "../../metrics/metrics.js"; import {isValidBlsToExecutionChangeForBlockInclusion} from "./utils.js"; type HexRoot = string; @@ -165,7 +167,9 @@ export class OpPool { * slashings included earlier in the block. */ getSlashingsAndExits( - state: CachedBeaconStateAllForks + state: CachedBeaconStateAllForks, + blockType: BlockType, + metrics: Metrics | null ): [ phase0.AttesterSlashing[], phase0.ProposerSlashing[], @@ -178,6 +182,12 @@ export class OpPool { const toBeSlashedIndices = new Set(); const proposerSlashings: phase0.ProposerSlashing[] = []; + const stepsMetrics = + blockType === BlockType.Full + ? metrics?.executionBlockProductionTimeSteps + : metrics?.builderBlockProductionTimeSteps; + + const endProposerSlashing = stepsMetrics?.startTimer(); for (const proposerSlashing of this.proposerSlashings.values()) { const index = proposerSlashing.signedHeader1.message.proposerIndex; const validator = state.validators.getReadonly(index); @@ -190,19 +200,23 @@ export class OpPool { } } } + endProposerSlashing?.({ + step: "proposerSlashing", + }); + const endAttesterSlashings = stepsMetrics?.startTimer(); const attesterSlashings: phase0.AttesterSlashing[] = []; attesterSlashing: for (const attesterSlashing of this.attesterSlashings.values()) { /** Indices slashable in this attester slashing */ const slashableIndices = new Set(); for (let i = 0; i < attesterSlashing.intersectingIndices.length; i++) { const index = attesterSlashing.intersectingIndices[i]; - const validator = state.validators.getReadonly(index); - // If we already have a slashing for this index, we can continue on to the next slashing if (toBeSlashedIndices.has(index)) { continue attesterSlashing; } + + const validator = state.validators.getReadonly(index); if (isSlashableAtEpoch(validator, stateEpoch)) { slashableIndices.add(index); } @@ -220,7 +234,11 @@ export class OpPool { } } } + endAttesterSlashings?.({ + step: "attesterSlashings", + }); + const endVoluntaryExits = stepsMetrics?.startTimer(); const voluntaryExits: phase0.SignedVoluntaryExit[] = []; for (const voluntaryExit of this.voluntaryExits.values()) { if ( @@ -237,7 +255,11 @@ export class OpPool { } } } + endVoluntaryExits?.({ + step: "voluntaryExits", + }); + const endBlsToExecutionChanges = stepsMetrics?.startTimer(); const blsToExecutionChanges: capella.SignedBLSToExecutionChange[] = []; for (const blsToExecutionChange of this.blsToExecutionChanges.values()) { if (isValidBlsToExecutionChangeForBlockInclusion(state, blsToExecutionChange.data)) { @@ -247,6 +269,9 @@ export class OpPool { } } } + endBlsToExecutionChanges?.({ + step: "blsToExecutionChanges", + }); return [attesterSlashings, proposerSlashings, voluntaryExits, blsToExecutionChanges]; } diff --git a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts index acefbbf765a1..1c522c54a93d 100644 --- a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts +++ b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts @@ -123,10 +123,25 @@ export async function produceBlockBody( // } // } + const stepsMetrics = + blockType === BlockType.Full + ? this.metrics?.executionBlockProductionTimeSteps + : this.metrics?.builderBlockProductionTimeSteps; + const [attesterSlashings, proposerSlashings, voluntaryExits, blsToExecutionChanges] = - this.opPool.getSlashingsAndExits(currentState); + this.opPool.getSlashingsAndExits(currentState, blockType, this.metrics); + + const endAttestations = stepsMetrics?.startTimer(); const attestations = this.aggregatedAttestationPool.getAttestationsForBlock(this.forkChoice, currentState); + endAttestations?.({ + step: "attestations", + }); + + const endEth1DataAndDeposits = stepsMetrics?.startTimer(); const {eth1Data, deposits} = await this.eth1.getEth1DataAndDeposits(currentState); + endEth1DataAndDeposits?.({ + step: "eth1DataAndDeposits", + }); const blockBody: phase0.BeaconBlockBody = { randaoReveal, @@ -141,6 +156,7 @@ export async function produceBlockBody( const blockEpoch = computeEpochAtSlot(blockSlot); + const endSyncAggregate = stepsMetrics?.startTimer(); if (blockEpoch >= this.config.ALTAIR_FORK_EPOCH) { const syncAggregate = this.syncContributionAndProofPool.getAggregate(parentSlot, parentBlockRoot); this.metrics?.production.producedSyncAggregateParticipants.observe( @@ -148,6 +164,9 @@ export async function produceBlockBody( ); (blockBody as altair.BeaconBlockBody).syncAggregate = syncAggregate; } + endSyncAggregate?.({ + step: "syncAggregate", + }); Object.assign(logMeta, { attestations: attestations.length, @@ -157,6 +176,7 @@ export async function produceBlockBody( proposerSlashings: proposerSlashings.length, }); + const endExecutionPayload = stepsMetrics?.startTimer(); if (isForkExecution(fork)) { const safeBlockHash = this.forkChoice.getJustifiedBlock().executionPayloadBlockHash ?? ZERO_HASH_HEX; const finalizedBlockHash = this.forkChoice.getFinalizedBlock().executionPayloadBlockHash ?? ZERO_HASH_HEX; @@ -359,6 +379,9 @@ export async function produceBlockBody( blobsResult = {type: BlobsResultType.preDeneb}; executionPayloadValue = BigInt(0); } + endExecutionPayload?.({ + step: "executionPayload", + }); if (ForkSeq[fork] >= ForkSeq.capella) { // TODO: blsToExecutionChanges should be passed in the produceBlock call diff --git a/packages/beacon-node/src/metrics/metrics/beacon.ts b/packages/beacon-node/src/metrics/metrics/beacon.ts index 2b763599f6e1..8d9094f19a25 100644 --- a/packages/beacon-node/src/metrics/metrics/beacon.ts +++ b/packages/beacon-node/src/metrics/metrics/beacon.ts @@ -121,6 +121,38 @@ export function createBeaconMetrics(register: RegistryMetricCreator) { buckets: [0.1, 1, 2, 4, 10], labelNames: ["source"], }), + executionBlockProductionTimeSteps: register.histogram<"step">({ + name: "beacon_block_production_execution_steps_seconds", + help: "Detailed steps runtime of execution block production", + buckets: [0.01, 0.1, 0.2, 0.5, 1], + /** + * - proposerSlashing + * - attesterSlashings + * - voluntaryExits + * - blsToExecutionChanges + * - attestations + * - eth1DataAndDeposits + * - syncAggregate + * - executionPayload + */ + labelNames: ["step"], + }), + builderBlockProductionTimeSteps: register.histogram<"step">({ + name: "beacon_block_production_builder_steps_seconds", + help: "Detailed steps runtime of builder block production", + buckets: [0.01, 0.1, 0.2, 0.5, 1], + /** + * - proposerSlashing + * - attesterSlashings + * - voluntaryExits + * - blsToExecutionChanges + * - attestations + * - eth1DataAndDeposits + * - syncAggregate + * - executionPayload + */ + labelNames: ["step"], + }), blockProductionRequests: register.gauge<"source">({ name: "beacon_block_production_requests_total", help: "Count of all block production requests", diff --git a/packages/beacon-node/src/sync/options.ts b/packages/beacon-node/src/sync/options.ts index 52cd288e2b4b..7afd624b9d2f 100644 --- a/packages/beacon-node/src/sync/options.ts +++ b/packages/beacon-node/src/sync/options.ts @@ -1,3 +1,5 @@ +import {SLOTS_PER_EPOCH} from "@lodestar/params"; + export type SyncOptions = { /** * Allow node to consider itself synced without being connected to a peer. @@ -22,6 +24,16 @@ export type SyncOptions = { backfillBatchSize: number; /** For testing only, MAX_PENDING_BLOCKS by default */ maxPendingBlocks?: number; + + /** + * The number of slots ahead of us that is allowed before starting a RangeSync + * If a peer is within this tolerance (forwards or backwards), it is treated as a fully sync'd peer. + * + * This means that we consider ourselves synced (and hence subscribe to all subnets and block + * gossip if no peers are further than this range ahead of us that we have not already downloaded + * blocks for. + */ + slotImportTolerance?: number; }; export const defaultSyncOptions: SyncOptions = { @@ -29,4 +41,5 @@ export const defaultSyncOptions: SyncOptions = { disableProcessAsChainSegment: false, /** By default skip the backfill sync */ backfillBatchSize: 0, + slotImportTolerance: SLOTS_PER_EPOCH, }; diff --git a/packages/beacon-node/src/sync/sync.ts b/packages/beacon-node/src/sync/sync.ts index 9cf7ba9ff716..f7492c57da38 100644 --- a/packages/beacon-node/src/sync/sync.ts +++ b/packages/beacon-node/src/sync/sync.ts @@ -28,15 +28,6 @@ export class BeaconSync implements IBeaconSync { /** For metrics only */ private readonly peerSyncType = new Map(); - - /** - * The number of slots ahead of us that is allowed before starting a RangeSync - * If a peer is within this tolerance (forwards or backwards), it is treated as a fully sync'd peer. - * - * This means that we consider ourselves synced (and hence subscribe to all subnets and block - * gossip if no peers are further than this range ahead of us that we have not already downloaded - * blocks for. - */ private readonly slotImportTolerance: Slot; constructor(opts: SyncOptions, modules: SyncModules) { @@ -48,7 +39,7 @@ export class BeaconSync implements IBeaconSync { this.logger = logger; this.rangeSync = new RangeSync(modules, opts); this.unknownBlockSync = new UnknownBlockSync(config, network, chain, logger, metrics, opts); - this.slotImportTolerance = SLOTS_PER_EPOCH; + this.slotImportTolerance = opts.slotImportTolerance ?? SLOTS_PER_EPOCH; // Subscribe to RangeSync completing a SyncChain and recompute sync state if (!opts.disableRangeSync) { @@ -241,7 +232,7 @@ export class BeaconSync implements IBeaconSync { } } - // If we stopped being synced and falled significantly behind, stop gossip + // If we stopped being synced and fallen significantly behind, stop gossip else if (state !== SyncState.Synced) { const syncDiff = this.chain.clock.currentSlot - this.chain.forkChoice.getHead().slot; if (syncDiff > this.slotImportTolerance * 2) { diff --git a/packages/beacon-node/test/fixtures/capella.ts b/packages/beacon-node/test/fixtures/capella.ts new file mode 100644 index 000000000000..fe9b0206efb1 --- /dev/null +++ b/packages/beacon-node/test/fixtures/capella.ts @@ -0,0 +1,24 @@ +import {CachedBeaconStateAltair} from "@lodestar/state-transition"; +import {capella} from "@lodestar/types"; + +export function generateBlsToExecutionChanges( + state: CachedBeaconStateAltair, + count: number +): capella.SignedBLSToExecutionChange[] { + const result: capella.SignedBLSToExecutionChange[] = []; + + for (const validatorIndex of state.epochCtx.proposers) { + result.push({ + message: { + fromBlsPubkey: state.epochCtx.index2pubkey[validatorIndex].toBytes(), + toExecutionAddress: Buffer.alloc(20), + validatorIndex, + }, + signature: Buffer.alloc(96), + }); + + if (result.length >= count) return result; + } + + return result; +} diff --git a/packages/beacon-node/test/fixtures/phase0.ts b/packages/beacon-node/test/fixtures/phase0.ts new file mode 100644 index 000000000000..a273f55e967d --- /dev/null +++ b/packages/beacon-node/test/fixtures/phase0.ts @@ -0,0 +1,98 @@ +import {SLOTS_PER_EPOCH} from "@lodestar/params"; +import { + CachedBeaconStateAltair, + computeEpochAtSlot, + computeStartSlotAtEpoch, + getBlockRootAtSlot, +} from "@lodestar/state-transition"; +import {phase0} from "@lodestar/types"; + +export function generateIndexedAttestations( + state: CachedBeaconStateAltair, + count: number +): phase0.IndexedAttestation[] { + const result: phase0.IndexedAttestation[] = []; + + for (let epochSlot = 0; epochSlot < SLOTS_PER_EPOCH; epochSlot++) { + const slot = state.slot - 1 - epochSlot; + const epoch = computeEpochAtSlot(slot); + const committeeCount = state.epochCtx.getCommitteeCountPerSlot(epoch); + + for (let committeeIndex = 0; committeeIndex < committeeCount; committeeIndex++) { + result.push({ + attestingIndices: state.epochCtx.getBeaconCommittee(slot, committeeIndex), + data: { + slot: slot, + index: committeeIndex, + beaconBlockRoot: getBlockRootAtSlot(state, slot), + source: { + epoch: state.currentJustifiedCheckpoint.epoch, + root: state.currentJustifiedCheckpoint.root, + }, + target: { + epoch: epoch, + root: getBlockRootAtSlot(state, computeStartSlotAtEpoch(epoch)), + }, + }, + signature: Buffer.alloc(96), + }); + + if (result.length >= count) return result; + } + } + + return result; +} + +export function generateBeaconBlockHeader(state: CachedBeaconStateAltair, count: number): phase0.BeaconBlockHeader[] { + const headers: phase0.BeaconBlockHeader[] = []; + + for (let i = 1; i <= count; i++) { + const slot = state.slot - i; + const epoch = computeEpochAtSlot(slot); + const epochStartSlot = computeStartSlotAtEpoch(epoch); + const parentRoot = getBlockRootAtSlot(state, slot - 1); + const stateRoot = getBlockRootAtSlot(state, epochStartSlot); + const bodyRoot = getBlockRootAtSlot(state, epochStartSlot + 1); + const header: phase0.BeaconBlockHeader = { + slot, + proposerIndex: state.epochCtx.proposers[slot % SLOTS_PER_EPOCH], + parentRoot, + stateRoot, + bodyRoot, + }; + + headers.push(header); + } + return headers; +} + +export function generateSignedBeaconBlockHeader( + state: CachedBeaconStateAltair, + count: number +): phase0.SignedBeaconBlockHeader[] { + const headers = generateBeaconBlockHeader(state, count); + + return headers.map((header) => ({ + message: header, + signature: Buffer.alloc(96), + })); +} + +export function generateVoluntaryExits(state: CachedBeaconStateAltair, count: number): phase0.SignedVoluntaryExit[] { + const result: phase0.SignedVoluntaryExit[] = []; + + for (const validatorIndex of state.epochCtx.proposers) { + result.push({ + message: { + epoch: state.currentJustifiedCheckpoint.epoch, + validatorIndex, + }, + signature: Buffer.alloc(96), + }); + + if (result.length >= count) return result; + } + + return result; +} diff --git a/packages/beacon-node/test/perf/chain/opPools/opPool.test.ts b/packages/beacon-node/test/perf/chain/opPools/opPool.test.ts new file mode 100644 index 000000000000..6e420f0e1011 --- /dev/null +++ b/packages/beacon-node/test/perf/chain/opPools/opPool.test.ts @@ -0,0 +1,107 @@ +import {itBench} from "@dapplion/benchmark"; +import { + MAX_ATTESTER_SLASHINGS, + MAX_BLS_TO_EXECUTION_CHANGES, + MAX_PROPOSER_SLASHINGS, + MAX_VOLUNTARY_EXITS, +} from "@lodestar/params"; +import {CachedBeaconStateAltair} from "@lodestar/state-transition"; +import {ssz} from "@lodestar/types"; +// eslint-disable-next-line import/no-relative-packages +import {generatePerfTestCachedStateAltair} from "../../../../../state-transition/test/perf/util.js"; +import {OpPool} from "../../../../src/chain/opPools/opPool.js"; +import {generateBlsToExecutionChanges} from "../../../fixtures/capella.js"; +import { + generateIndexedAttestations, + generateSignedBeaconBlockHeader, + generateVoluntaryExits, +} from "../../../fixtures/phase0.js"; +import {BlockType} from "../../../../src/chain/interface.js"; + +describe("opPool", () => { + let originalState: CachedBeaconStateAltair; + + before(function () { + this.timeout(2 * 60 * 1000); // Generating the states for the first time is very slow + + originalState = generatePerfTestCachedStateAltair({goBackOneSlot: true}); + }); + + itBench({ + id: "getSlashingsAndExits - default max", + beforeEach: () => { + const pool = new OpPool(); + fillAttesterSlashing(pool, originalState, MAX_ATTESTER_SLASHINGS); + fillProposerSlashing(pool, originalState, MAX_PROPOSER_SLASHINGS); + fillVoluntaryExits(pool, originalState, MAX_VOLUNTARY_EXITS); + fillBlsToExecutionChanges(pool, originalState, MAX_BLS_TO_EXECUTION_CHANGES); + + return pool; + }, + fn: (pool) => { + pool.getSlashingsAndExits(originalState, BlockType.Full, null); + }, + }); + + itBench({ + id: "getSlashingsAndExits - 2k", + beforeEach: () => { + const pool = new OpPool(); + const maxItemsInPool = 2_000; + + fillAttesterSlashing(pool, originalState, maxItemsInPool); + fillProposerSlashing(pool, originalState, maxItemsInPool); + fillVoluntaryExits(pool, originalState, maxItemsInPool); + fillBlsToExecutionChanges(pool, originalState, maxItemsInPool); + + return pool; + }, + fn: (pool) => { + pool.getSlashingsAndExits(originalState, BlockType.Full, null); + }, + }); +}); + +function fillAttesterSlashing(pool: OpPool, state: CachedBeaconStateAltair, count: number): OpPool { + for (const attestation of generateIndexedAttestations(state, count)) { + pool.insertAttesterSlashing({ + attestation1: ssz.phase0.IndexedAttestationBigint.fromJson(ssz.phase0.IndexedAttestation.toJson(attestation)), + attestation2: ssz.phase0.IndexedAttestationBigint.fromJson(ssz.phase0.IndexedAttestation.toJson(attestation)), + }); + } + + return pool; +} + +function fillProposerSlashing(pool: OpPool, state: CachedBeaconStateAltair, count: number): OpPool { + for (const blockHeader of generateSignedBeaconBlockHeader(state, count)) { + pool.insertProposerSlashing({ + signedHeader1: ssz.phase0.SignedBeaconBlockHeaderBigint.fromJson( + ssz.phase0.SignedBeaconBlockHeader.toJson(blockHeader) + ), + signedHeader2: ssz.phase0.SignedBeaconBlockHeaderBigint.fromJson( + ssz.phase0.SignedBeaconBlockHeader.toJson(blockHeader) + ), + }); + } + + return pool; +} + +function fillVoluntaryExits(pool: OpPool, state: CachedBeaconStateAltair, count: number): OpPool { + for (const exit of generateVoluntaryExits(state, count)) { + pool.insertVoluntaryExit(exit); + } + + return pool; +} + +// This does not set the `withdrawalCredentials` for the validator +// So it will be in the pool but not returned from `getSlashingsAndExits` +function fillBlsToExecutionChanges(pool: OpPool, state: CachedBeaconStateAltair, count: number): OpPool { + for (const blsToExecution of generateBlsToExecutionChanges(state, count)) { + pool.insertBlsToExecutionChange(blsToExecution); + } + + return pool; +} diff --git a/packages/beacon-node/test/perf/chain/produceBlock/produceBlockBody.test.ts b/packages/beacon-node/test/perf/chain/produceBlock/produceBlockBody.test.ts new file mode 100644 index 000000000000..dbe86c2c5868 --- /dev/null +++ b/packages/beacon-node/test/perf/chain/produceBlock/produceBlockBody.test.ts @@ -0,0 +1,86 @@ +import {fromHexString} from "@chainsafe/ssz"; +import {itBench} from "@dapplion/benchmark"; +import {config} from "@lodestar/config/default"; +import {LevelDbController} from "@lodestar/db"; +import {SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY} from "@lodestar/params"; +import {defaultOptions as defaultValidatorOptions} from "@lodestar/validator"; +import {CachedBeaconStateAltair} from "@lodestar/state-transition"; +// eslint-disable-next-line import/no-relative-packages +import {generatePerfTestCachedStateAltair} from "../../../../../state-transition/test/perf/util.js"; +import {BeaconChain} from "../../../../src/chain/index.js"; +import {BlockType, produceBlockBody} from "../../../../src/chain/produceBlock/produceBlockBody.js"; +import {Eth1ForBlockProductionDisabled} from "../../../../src/eth1/index.js"; +import {ExecutionEngineDisabled} from "../../../../src/execution/engine/index.js"; +import {BeaconDb} from "../../../../src/index.js"; +import {testLogger} from "../../../utils/logger.js"; + +const logger = testLogger(); + +describe("produceBlockBody", () => { + const stateOg = generatePerfTestCachedStateAltair({goBackOneSlot: false}); + + let db: BeaconDb; + let chain: BeaconChain; + let state: CachedBeaconStateAltair; + + before(async () => { + db = new BeaconDb(config, await LevelDbController.create({name: ".tmpdb"}, {logger})); + state = stateOg.clone(); + chain = new BeaconChain( + { + proposerBoostEnabled: true, + computeUnrealized: false, + safeSlotsToImportOptimistically: SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY, + disableArchiveOnCheckpoint: true, + suggestedFeeRecipient: defaultValidatorOptions.suggestedFeeRecipient, + skipCreateStateCacheIfAvailable: true, + archiveStateEpochFrequency: 1024, + minSameMessageSignatureSetsToBatch: 32, + }, + { + config: state.config, + db, + logger, + // eslint-disable-next-line @typescript-eslint/no-empty-function + processShutdownCallback: () => {}, + metrics: null, + anchorState: state, + eth1: new Eth1ForBlockProductionDisabled(), + executionEngine: new ExecutionEngineDisabled(), + } + ); + }); + + after(async () => { + // If before blocks fail, db won't be declared + if (db !== undefined) await db.close(); + if (chain !== undefined) await chain.close(); + }); + + itBench({ + id: "proposeBlockBody type=full, size=empty", + minRuns: 5, + maxMs: Infinity, + timeoutBench: 60 * 1000, + beforeEach: async () => { + const head = chain.forkChoice.getHead(); + const proposerIndex = state.epochCtx.getBeaconProposer(state.slot); + const proposerPubKey = state.epochCtx.index2pubkey[proposerIndex].toBytes(); + + return {chain, state, head, proposerIndex, proposerPubKey}; + }, + fn: async ({chain, state, head, proposerIndex, proposerPubKey}) => { + const slot = state.slot; + + await produceBlockBody.call(chain, BlockType.Full, state, { + parentSlot: slot, + slot: slot + 1, + graffiti: Buffer.alloc(32), + randaoReveal: Buffer.alloc(96), + parentBlockRoot: fromHexString(head.blockRoot), + proposerIndex, + proposerPubKey, + }); + }, + }); +}); diff --git a/packages/cli/src/networks/gnosis.ts b/packages/cli/src/networks/gnosis.ts index b9d23bb274c1..4abe2a17b53d 100644 --- a/packages/cli/src/networks/gnosis.ts +++ b/packages/cli/src/networks/gnosis.ts @@ -19,4 +19,5 @@ export const bootEnrs = [ "enr:-Ly4QLKgv5M2D4DYJgo6s4NG_K4zu4sk5HOLCfGCdtgoezsbfRbfGpQ4iSd31M88ec3DHA5FWVbkgIas9EaJeXia0nwBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpCCS-QxAgAAZP__________gmlkgnY0gmlwhI1eYRaJc2VjcDI1NmsxoQLpK_A47iNBkVjka9Mde1F-Kie-R0sq97MCNKCxt2HwOIhzeW5jbmV0cwCDdGNwgiMog3VkcIIjKA", "enr:-Ly4QF_0qvji6xqXrhQEhwJR1W9h5dXV7ZjVCN_NlosKxcgZW6emAfB_KXxEiPgKr_-CZG8CWvTiojEohG1ewF7P368Bh2F0dG5ldHOIAAAAAAAAAACEZXRoMpCCS-QxAgAAZP__________gmlkgnY0gmlwhI1eYUqJc2VjcDI1NmsxoQIpNRUT6llrXqEbjkAodsZOyWv8fxQkyQtSvH4sg2D7n4hzeW5jbmV0cwCDdGNwgiMog3VkcIIjKA", "enr:-Ly4QCD5D99p36WafgTSxB6kY7D2V1ca71C49J4VWI2c8UZCCPYBvNRWiv0-HxOcbpuUdwPVhyWQCYm1yq2ZH0ukCbQBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpCCS-QxAgAAZP__________gmlkgnY0gmlwhI1eYVSJc2VjcDI1NmsxoQJJMSV8iSZ8zvkgbi8cjIGEUVJeekLqT0LQha_co-siT4hzeW5jbmV0cwCDdGNwgiMog3VkcIIjKA", + "enr:-KK4QKXJq1QOVWuJAGige4uaT8LRPQGCVRf3lH3pxjaVScMRUfFW1eiiaz8RwOAYvw33D4EX-uASGJ5QVqVCqwccxa-Bi4RldGgykCGm-DYDAABk__________-CaWSCdjSCaXCEM0QnzolzZWNwMjU2azGhAhNvrRkpuK4MWTf3WqiOXSOePL8Zc-wKVpZ9FQx_BDadg3RjcIIjKIN1ZHCCIyg", ]; diff --git a/packages/cli/src/options/beaconNodeOptions/sync.ts b/packages/cli/src/options/beaconNodeOptions/sync.ts index 6a28338adad3..7130b835b987 100644 --- a/packages/cli/src/options/beaconNodeOptions/sync.ts +++ b/packages/cli/src/options/beaconNodeOptions/sync.ts @@ -6,6 +6,7 @@ export type SyncArgs = { "sync.disableProcessAsChainSegment"?: boolean; "sync.disableRangeSync"?: boolean; "sync.backfillBatchSize"?: number; + "sync.slotImportTolerance"?: number; }; export function parseArgs(args: SyncArgs): IBeaconNodeOptions["sync"] { @@ -14,6 +15,7 @@ export function parseArgs(args: SyncArgs): IBeaconNodeOptions["sync"] { disableProcessAsChainSegment: args["sync.disableProcessAsChainSegment"], backfillBatchSize: args["sync.backfillBatchSize"] ?? defaultOptions.sync.backfillBatchSize, disableRangeSync: args["sync.disableRangeSync"], + slotImportTolerance: args["sync.slotImportTolerance"] ?? defaultOptions.sync.slotImportTolerance, }; } @@ -36,6 +38,14 @@ Use only for local networks with a single node, can be dangerous in regular netw group: "sync", }, + "sync.slotImportTolerance": { + hidden: true, + type: "number", + description: "Number of slot tolerance to trigger range sync and to measure if node is synced.", + defaultDescription: String(defaultOptions.sync.slotImportTolerance), + group: "sync", + }, + "sync.disableProcessAsChainSegment": { hidden: true, type: "boolean", diff --git a/packages/cli/test/sim/multi_fork.test.ts b/packages/cli/test/sim/multi_fork.test.ts index 0ac8d18ed055..1888195a4745 100644 --- a/packages/cli/test/sim/multi_fork.test.ts +++ b/packages/cli/test/sim/multi_fork.test.ts @@ -199,19 +199,29 @@ await checkpointSync.execution.job.stop(); // Unknown block sync // ======================================================== +const headForUnknownBlockSync = await env.nodes[0].beacon.api.beacon.getBlockV2("head"); +ApiError.assert(headForUnknownBlockSync); const unknownBlockSync = await env.createNodePair({ id: "unknown-block-sync-node", beacon: { type: BeaconClient.Lodestar, - options: {clientOptions: {"network.allowPublishToZeroPeers": true, "sync.disableRangeSync": true}}, + options: { + clientOptions: { + "network.allowPublishToZeroPeers": true, + "sync.disableRangeSync": true, + // unknownBlockSync node start when other nodes are multiple epoch ahead and + // unknown block sync can work only if the gap is maximum `slotImportTolerance * 2` + // default value for slotImportTolerance is one epoch, so if gap is more than 2 epoch + // unknown block sync will not work. So why we have to increase it for tests. + "sync.slotImportTolerance": headForUnknownBlockSync.response.data.message.slot / 2 + 2, + }, + }, }, execution: ExecutionClient.Geth, keysCount: 0, }); await unknownBlockSync.execution.job.start(); await unknownBlockSync.beacon.job.start(); -const headForUnknownBlockSync = await env.nodes[0].beacon.api.beacon.getBlockV2("head"); -ApiError.assert(headForUnknownBlockSync); await connectNewNode(unknownBlockSync, env.nodes); // Wait for EL node to start and sync diff --git a/packages/cli/test/unit/options/beaconNodeOptions.test.ts b/packages/cli/test/unit/options/beaconNodeOptions.test.ts index 5e9491ab1436..f3a887ffea30 100644 --- a/packages/cli/test/unit/options/beaconNodeOptions.test.ts +++ b/packages/cli/test/unit/options/beaconNodeOptions.test.ts @@ -206,6 +206,7 @@ describe("options / beaconNodeOptions", () => { }, sync: { isSingleNode: true, + slotImportTolerance: 32, disableProcessAsChainSegment: true, backfillBatchSize: 64, disableRangeSync: false, diff --git a/packages/db/test/unit/controller/level.test.ts b/packages/db/test/unit/controller/level.test.ts index 38d8274ebb22..768ef3a39006 100644 --- a/packages/db/test/unit/controller/level.test.ts +++ b/packages/db/test/unit/controller/level.test.ts @@ -144,7 +144,8 @@ describe("LevelDB controller", () => { return "gdu"; } } catch { - /* no-op */ + /* eslint-disable no-console */ + console.error("Cannot find gdu command, falling back to du"); } } return "du"; diff --git a/packages/state-transition/test/perf/util.ts b/packages/state-transition/test/perf/util.ts index 46faf11c50f1..4df6746ea938 100644 --- a/packages/state-transition/test/perf/util.ts +++ b/packages/state-transition/test/perf/util.ts @@ -211,6 +211,10 @@ export function cachedStateAltairPopulateCaches(state: CachedBeaconStateAltair): state.inactivityScores.getAll(); } +/** + * Warning: This function has side effects on the cached state + * The order in which the caches are populated is important and can cause stable tests to fail. + */ export function generatePerfTestCachedStateAltair(opts?: { goBackOneSlot: boolean; vc?: number; diff --git a/packages/utils/src/validation.ts b/packages/utils/src/validation.ts index 0b47a70e2340..ed8b88c912e8 100644 --- a/packages/utils/src/validation.ts +++ b/packages/utils/src/validation.ts @@ -2,6 +2,16 @@ export function isValidHttpUrl(urlStr: string): boolean { let url; try { url = new URL(urlStr); + + // `new URL` encodes the username/password with the userinfo percent-encode set. + // This means the `%` character is not encoded, but others are (such as `=`). + // If a username/password contain a `%`, they will not be able to be decoded. + // + // Make sure that we can successfully decode the username and password here. + // + // Unfortunately this means we don't accept every character supported by RFC-3986. + decodeURIComponent(url.username); + decodeURIComponent(url.password); } catch (_) { return false; }