diff --git a/packages/api/src/beacon/server/beacon.ts b/packages/api/src/beacon/server/beacon.ts index da5a0997a0d8..c71decd1ac8e 100644 --- a/packages/api/src/beacon/server/beacon.ts +++ b/packages/api/src/beacon/server/beacon.ts @@ -1,4 +1,5 @@ import {ChainForkConfig} from "@lodestar/config"; +import {ssz} from "@lodestar/types"; import {Api, ReqTypes, routesData, getReturnTypes, getReqSerializers} from "../routes/beacon/index.js"; import {ServerRoutes, getGenericJsonServer} from "../../utils/server/index.js"; import {ServerApi} from "../../interfaces.js"; @@ -30,15 +31,35 @@ export function getRoutes(config: ChainForkConfig, api: ServerApi): ServerR }, getBlockV2: { ...serverRoutes.getBlockV2, - handler: async (req) => { + handler: async (req, res) => { const response = await api.getBlockV2(...reqSerializers.getBlockV2.parseReq(req)); if (response instanceof Uint8Array) { + const slot = extractSlotFromBlockBytes(response); + const version = config.getForkName(slot); + void res.header("Eth-Consensus-Version", version); // Fastify 3.x.x will automatically add header `Content-Type: application/octet-stream` if Buffer return Buffer.from(response); } else { + void res.header("Eth-Consensus-Version", response.version); return returnTypes.getBlockV2.toJson(response); } }, }, }; } + +function extractSlotFromBlockBytes(block: Uint8Array): number { + const {signature} = ssz.phase0.SignedBeaconBlock.fields; + /** + * class SignedBeaconBlock(Container): + * message: BeaconBlock [offset - 4 bytes] + * signature: BLSSignature [fixed - 96 bytes] + * + * class BeaconBlock(Container): + * slot: Slot [fixed - 8 bytes] + * ... + */ + const offset = 4 + signature.lengthBytes; + const bytes = block.subarray(offset, offset + ssz.Slot.byteLength); + return ssz.Slot.deserialize(bytes); +} diff --git a/packages/api/src/beacon/server/debug.ts b/packages/api/src/beacon/server/debug.ts index 7dd767d4a2fd..fd9de661edd2 100644 --- a/packages/api/src/beacon/server/debug.ts +++ b/packages/api/src/beacon/server/debug.ts @@ -1,4 +1,5 @@ import {ChainForkConfig} from "@lodestar/config"; +import {ssz} from "@lodestar/types"; import {Api, ReqTypes, routesData, getReturnTypes, getReqSerializers} from "../routes/debug.js"; import {ServerRoutes, getGenericJsonServer} from "../../utils/server/index.js"; import {ServerApi} from "../../interfaces.js"; @@ -31,15 +32,33 @@ export function getRoutes(config: ChainForkConfig, api: ServerApi): ServerR }, getStateV2: { ...serverRoutes.getStateV2, - handler: async (req) => { + handler: async (req, res) => { const response = await api.getStateV2(...reqSerializers.getStateV2.parseReq(req)); if (response instanceof Uint8Array) { + const slot = extractSlotFromStateBytes(response); + const version = config.getForkName(slot); + void res.header("Eth-Consensus-Version", version); // Fastify 3.x.x will automatically add header `Content-Type: application/octet-stream` if Buffer return Buffer.from(response); } else { + void res.header("Eth-Consensus-Version", response.version); return returnTypes.getStateV2.toJson(response); } }, }, }; } + +function extractSlotFromStateBytes(state: Uint8Array): number { + const {genesisTime, genesisValidatorsRoot} = ssz.phase0.BeaconState.fields; + /** + * class BeaconState(Container): + * genesisTime: BeaconBlock [offset - 4 bytes] + * genesisValidatorsRoot: BLSSignature [fixed - 96 bytes] + * slot: Slot [fixed - 8 bytes] + * ... + */ + const offset = genesisTime.byteLength + genesisValidatorsRoot.lengthBytes; + const bytes = state.subarray(offset, offset + ssz.Slot.byteLength); + return ssz.Slot.deserialize(bytes); +}