From 26e9bc4b15e8d04fe6af718934aee602da5d0a16 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Mon, 10 Jun 2024 19:39:07 +0100 Subject: [PATCH] fix: add workaround to fix fetching state from checkpointz (#6874) --- packages/api/src/index.ts | 1 + packages/api/src/utils/client/request.ts | 4 +- packages/api/src/utils/headers.ts | 39 ++++++------- packages/api/test/unit/utils/headers.test.ts | 58 +++++++++++++++++++- packages/cli/src/networks/index.ts | 27 ++++++--- 5 files changed, 100 insertions(+), 29 deletions(-) diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index a93fc83d4628..a9a3c075c6a9 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -2,6 +2,7 @@ export * from "./beacon/index.js"; export {HttpStatusCode} from "./utils/httpStatusCode.js"; export {WireFormat} from "./utils/wireFormat.js"; +export {HttpHeader, MediaType} from "./utils/headers.js"; export type {HttpErrorCodes, HttpSuccessCodes} from "./utils/httpStatusCode.js"; export {ApiResponse, HttpClient, FetchError, isFetchError, fetch, defaultInit} from "./utils/client/index.js"; export type {ApiRequestInit} from "./utils/client/request.js"; diff --git a/packages/api/src/utils/client/request.ts b/packages/api/src/utils/client/request.ts index f8b41ca4d706..9c36ad111972 100644 --- a/packages/api/src/utils/client/request.ts +++ b/packages/api/src/utils/client/request.ts @@ -43,7 +43,7 @@ export function createApiRequest( args: E["args"], init: ApiRequestInitRequired ): Request { - const headers = new Headers(init.headers); + const headers = new Headers(); let req: E["request"]; @@ -102,7 +102,7 @@ export function createApiRequest( return new Request(url, { ...init, method: definition.method, - headers: mergeHeaders(headers, req.headers), + headers: mergeHeaders(headers, req.headers, init.headers), body: req.body as BodyInit, }); } diff --git a/packages/api/src/utils/headers.ts b/packages/api/src/utils/headers.ts index 5e39e5765958..0646bb109fbb 100644 --- a/packages/api/src/utils/headers.ts +++ b/packages/api/src/utils/headers.ts @@ -90,28 +90,29 @@ export function setAuthorizationHeader(url: URL, headers: Headers, {bearerToken} } } -export function mergeHeaders(a: HeadersInit | undefined, b: HeadersInit | undefined): Headers { - if (!a) { - return new Headers(b); - } - const headers = new Headers(a); - if (!b) { - return headers; - } - if (Array.isArray(b)) { - for (const [key, value] of b) { - headers.set(key, value); - } - } else if (b instanceof Headers) { - for (const [key, value] of b as unknown as Iterable<[string, string]>) { - headers.set(key, value); +export function mergeHeaders(...headersList: (HeadersInit | undefined)[]): Headers { + const mergedHeaders = new Headers(); + + for (const headers of headersList) { + if (!headers) { + continue; } - } else { - for (const [key, value] of Object.entries(b)) { - headers.set(key, value); + if (Array.isArray(headers)) { + for (const [key, value] of headers) { + mergedHeaders.set(key, value); + } + } else if (headers instanceof Headers) { + for (const [key, value] of headers as unknown as Iterable<[string, string]>) { + mergedHeaders.set(key, value); + } + } else { + for (const [key, value] of Object.entries(headers)) { + mergedHeaders.set(key, value); + } } } - return headers; + + return mergedHeaders; } /** diff --git a/packages/api/test/unit/utils/headers.test.ts b/packages/api/test/unit/utils/headers.test.ts index c909fe59499b..c3bcf6bc79c2 100644 --- a/packages/api/test/unit/utils/headers.test.ts +++ b/packages/api/test/unit/utils/headers.test.ts @@ -1,5 +1,5 @@ import {describe, it, expect} from "vitest"; -import {MediaType, SUPPORTED_MEDIA_TYPES, parseAcceptHeader} from "../../../src/utils/headers.js"; +import {MediaType, SUPPORTED_MEDIA_TYPES, mergeHeaders, parseAcceptHeader} from "../../../src/utils/headers.js"; describe("utils / headers", () => { describe("parseAcceptHeader", () => { @@ -32,4 +32,60 @@ describe("utils / headers", () => { expect(parseAcceptHeader(header, SUPPORTED_MEDIA_TYPES)).toBe(expected); }); }); + + describe("mergeHeaders", () => { + const testCases: {id: string; input: (HeadersInit | undefined)[]; expected: Headers}[] = [ + { + id: "empty headers", + input: [{}, [], new Headers()], + expected: new Headers(), + }, + { + id: "undefined headers", + input: [undefined, undefined], + expected: new Headers(), + }, + { + id: "different headers", + input: [{a: "1"}, {b: "2"}], + expected: new Headers({a: "1", b: "2"}), + }, + { + id: "override on single header", + input: [{a: "1"}, {b: "2"}, {a: "3"}], + expected: new Headers({a: "3", b: "2"}), + }, + { + id: "multiple overrides on same header", + input: [{a: "1"}, {b: "2"}, {a: "3"}, {a: "4"}], + expected: new Headers({a: "4", b: "2"}), + }, + { + id: "multiple overrides on different headers", + input: [{a: "1"}, {b: "2"}, {b: "3"}, {a: "4"}, {c: "5"}], + expected: new Headers({a: "4", b: "3", c: "5"}), + }, + { + id: "headers from array into plain object", + input: [{a: "1"}, [["b", "2"]]], + expected: new Headers({a: "1", b: "2"}), + }, + { + id: "headers from plain object into array", + input: [[["a", "1"]], {b: "2"}], + expected: new Headers({a: "1", b: "2"}), + }, + { + id: "headers from all input types", + input: [[["a", "1"]], {b: "2"}, new Headers({c: "3"}), {d: "4"}], + expected: new Headers({a: "1", b: "2", c: "3", d: "4"}), + }, + ]; + + for (const {id, input, expected} of testCases) { + it(`should correctly merge ${id}`, () => { + expect(mergeHeaders(...input)).toEqual(expected); + }); + } + }); }); diff --git a/packages/cli/src/networks/index.ts b/packages/cli/src/networks/index.ts index 90ba2411e883..2d605335b0e8 100644 --- a/packages/cli/src/networks/index.ts +++ b/packages/cli/src/networks/index.ts @@ -2,10 +2,11 @@ import fs from "node:fs"; import got from "got"; import {ENR} from "@chainsafe/enr"; import {SLOTS_PER_EPOCH} from "@lodestar/params"; -import {WireFormat, getClient} from "@lodestar/api"; +import {HttpHeader, MediaType, WireFormat, getClient} from "@lodestar/api"; +import {getStateTypeFromBytes} from "@lodestar/beacon-node"; import {ChainConfig, ChainForkConfig} from "@lodestar/config"; import {Checkpoint} from "@lodestar/types/phase0"; -import {Slot, ssz} from "@lodestar/types"; +import {Slot} from "@lodestar/types"; import {fromHex, callFnWhenAwait, Logger} from "@lodestar/utils"; import {BeaconStateAllForks, getLatestBlockRoot, computeCheckpointEpochAtStateSlot} from "@lodestar/state-transition"; import {parseBootnodesFile} from "../util/format.js"; @@ -156,18 +157,30 @@ export async function fetchWeakSubjectivityState( } // getStateV2 should be available for all forks including phase0 - const getStatePromise = api.debug.getStateV2({stateId}, {responseWireFormat: WireFormat.ssz}); - - const {stateBytes, fork} = await callFnWhenAwait( + const getStatePromise = api.debug.getStateV2( + {stateId}, + { + responseWireFormat: WireFormat.ssz, + headers: { + // Set Accept header explicitly to fix Checkpointz incompatibility + // See https://github.com/ethpandaops/checkpointz/issues/165 + [HttpHeader.Accept]: MediaType.ssz, + }, + } + ); + + const stateBytes = await callFnWhenAwait( getStatePromise, () => logger.info("Download in progress, please wait..."), GET_STATE_LOG_INTERVAL ).then((res) => { - return {stateBytes: res.ssz(), fork: res.meta().version}; + return res.ssz(); }); logger.info("Download completed", {stateId}); - const wsState = ssz.allForks[fork].BeaconState.deserializeToViewDU(stateBytes); + // It should not be required to get fork type from bytes but Checkpointz does not return + // Eth-Consensus-Version header, see https://github.com/ethpandaops/checkpointz/issues/164 + const wsState = getStateTypeFromBytes(config, stateBytes).deserializeToViewDU(stateBytes); return { wsState,