diff --git a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts index 65e7b9373a22..6bbfbc4bb7a6 100644 --- a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts @@ -4,9 +4,10 @@ import { computeEpochAtSlot, computeTimeAtSlot, reconstructFullBlockOrContents, - signedBeaconBlockToBlinded, + blindedOrFullBlockHashTreeRoot, + fullOrBlindedSignedBlockToBlinded, } from "@lodestar/state-transition"; -import {ForkExecution, SLOTS_PER_HISTORICAL_ROOT, isForkExecution, isForkPostElectra} from "@lodestar/params"; +import {SLOTS_PER_HISTORICAL_ROOT, isForkExecution, isForkPostElectra} from "@lodestar/params"; import {sleep, fromHex, toRootHex} from "@lodestar/utils"; import { deneb, @@ -331,15 +332,12 @@ export function getBeaconBlockApi({ if (slot > headSlot) { return {data: [], meta: {executionOptimistic: false, finalized: false}}; } - const canonicalBlock = await chain.getCanonicalBlockAtSlot(slot); // skip slot if (!canonicalBlock) { return {data: [], meta: {executionOptimistic: false, finalized: false}}; } - const canonicalRoot = config - .getForkTypes(canonicalBlock.block.message.slot) - .BeaconBlock.hashTreeRoot(canonicalBlock.block.message); + const canonicalRoot = blindedOrFullBlockHashTreeRoot(config, canonicalBlock.block.message); result.push(toBeaconHeaderResponse(config, canonicalBlock.block, true)); if (!canonicalBlock.finalized) { finalized = false; @@ -381,7 +379,7 @@ export function getBeaconBlockApi({ async getBlockV2({blockId}) { const {block, executionOptimistic, finalized} = await getBlockResponse(chain, blockId); return { - data: block, + data: await chain.fullOrBlindedSignedBeaconBlockToFull(block), meta: { executionOptimistic, finalized, @@ -394,9 +392,7 @@ export function getBeaconBlockApi({ const {block, executionOptimistic, finalized} = await getBlockResponse(chain, blockId); const fork = config.getForkName(block.message.slot); return { - data: isForkExecution(fork) - ? signedBeaconBlockToBlinded(config, block as SignedBeaconBlock) - : block, + data: isForkExecution(fork) ? fullOrBlindedSignedBlockToBlinded(config, block) : block, meta: { executionOptimistic, finalized, @@ -464,7 +460,7 @@ export function getBeaconBlockApi({ // Slow path const {block, executionOptimistic, finalized} = await getBlockResponse(chain, blockId); return { - data: {root: config.getForkTypes(block.message.slot).BeaconBlock.hashTreeRoot(block.message)}, + data: {root: blindedOrFullBlockHashTreeRoot(config, block.message)}, meta: {executionOptimistic, finalized}, }; }, @@ -482,7 +478,7 @@ export function getBeaconBlockApi({ async getBlobSidecars({blockId, indices}) { const {block, executionOptimistic, finalized} = await getBlockResponse(chain, blockId); - const blockRoot = config.getForkTypes(block.message.slot).BeaconBlock.hashTreeRoot(block.message); + const blockRoot = blindedOrFullBlockHashTreeRoot(config, block.message); let {blobSidecars} = (await db.blobSidecars.get(blockRoot)) ?? {}; if (!blobSidecars) { diff --git a/packages/beacon-node/src/api/impl/beacon/blocks/utils.ts b/packages/beacon-node/src/api/impl/beacon/blocks/utils.ts index fe4fc5ca3dc0..703ceca83117 100644 --- a/packages/beacon-node/src/api/impl/beacon/blocks/utils.ts +++ b/packages/beacon-node/src/api/impl/beacon/blocks/utils.ts @@ -1,7 +1,7 @@ import {routes} from "@lodestar/api"; -import {blockToHeader} from "@lodestar/state-transition"; +import {blockToHeader, blindedOrFullBlockHashTreeRoot} from "@lodestar/state-transition"; import {ChainForkConfig} from "@lodestar/config"; -import {RootHex, SignedBeaconBlock, Slot} from "@lodestar/types"; +import {RootHex, SignedBeaconBlock, SignedBlindedBeaconBlock, Slot} from "@lodestar/types"; import {IForkChoice} from "@lodestar/fork-choice"; import {GENESIS_SLOT} from "../../../../constants/index.js"; import {ApiError, ValidationError} from "../../errors.js"; @@ -10,11 +10,12 @@ import {rootHexRegex} from "../../../../eth1/provider/utils.js"; export function toBeaconHeaderResponse( config: ChainForkConfig, - block: SignedBeaconBlock, + block: SignedBeaconBlock | SignedBlindedBeaconBlock, canonical = false ): routes.beacon.BlockHeaderResponse { + const root = blindedOrFullBlockHashTreeRoot(config, block.message); return { - root: config.getForkTypes(block.message.slot).BeaconBlock.hashTreeRoot(block.message), + root, canonical, header: { message: blockToHeader(config, block.message), @@ -59,7 +60,7 @@ export function resolveBlockId(forkChoice: IForkChoice, blockId: routes.beacon.B export async function getBlockResponse( chain: IBeaconChain, blockId: routes.beacon.BlockId -): Promise<{block: SignedBeaconBlock; executionOptimistic: boolean; finalized: boolean}> { +): Promise<{block: SignedBeaconBlock | SignedBlindedBeaconBlock; executionOptimistic: boolean; finalized: boolean}> { const rootOrSlot = resolveBlockId(chain.forkChoice, blockId); const res = diff --git a/packages/beacon-node/src/api/impl/proof/index.ts b/packages/beacon-node/src/api/impl/proof/index.ts index 9e1a33940225..10c1dd6593a0 100644 --- a/packages/beacon-node/src/api/impl/proof/index.ts +++ b/packages/beacon-node/src/api/impl/proof/index.ts @@ -1,6 +1,7 @@ import {CompactMultiProof, createProof, ProofType} from "@chainsafe/persistent-merkle-tree"; import {routes} from "@lodestar/api"; import {ApplicationMethods} from "@lodestar/api/server"; +import {isBlindedBlock} from "@lodestar/state-transition"; import {ApiModules} from "../types.js"; import {getStateResponse} from "../beacon/state/utils.js"; import {getBlockResponse} from "../beacon/blocks/utils.js"; @@ -43,7 +44,9 @@ export function getProofApi( const {block} = await getBlockResponse(chain, blockId); // Commit any changes before computing the state root. In normal cases the state should have no changes here - const blockNode = config.getForkTypes(block.message.slot).BeaconBlock.toView(block.message).node; + const blockNode = isBlindedBlock(block.message) + ? config.getExecutionForkTypes(block.message.slot).BlindedBeaconBlock.toView(block.message).node + : config.getForkTypes(block.message.slot).BeaconBlock.toView(block.message).node; const proof = createProof(blockNode, {type: ProofType.compactMulti, descriptor}); diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index a17c1418809e..ed003922a2f2 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -8,7 +8,8 @@ import { getBlockRootAtSlot, computeEpochAtSlot, getCurrentSlot, - beaconBlockToBlinded, + blindedOrFullSignedBlockHashTreeRoot, + fullOrBlindedBlockToBlinded, } from "@lodestar/state-transition"; import { GENESIS_SLOT, @@ -33,7 +34,6 @@ import { ProducedBlockSource, bellatrix, BLSSignature, - isBlindedBeaconBlock, isBlockContents, phase0, Wei, @@ -135,12 +135,9 @@ export function getValidatorApi( if (state.slot < SLOTS_PER_HISTORICAL_ROOT) { genesisBlockRoot = state.blockRoots.get(0); } - const blockRes = await chain.getCanonicalBlockAtSlot(GENESIS_SLOT); if (blockRes) { - genesisBlockRoot = config - .getForkTypes(blockRes.block.message.slot) - .SignedBeaconBlock.hashTreeRoot(blockRes.block); + genesisBlockRoot = blindedOrFullSignedBlockHashTreeRoot(config, blockRes.block); } } @@ -767,13 +764,13 @@ export function getValidatorApi( } else { if (isBlockContents(data)) { const {block} = data; - const blindedBlock = beaconBlockToBlinded(config, block as BeaconBlock); + const blindedBlock = fullOrBlindedBlockToBlinded(config, block); return { data: blindedBlock, meta: {...meta, executionPayloadBlinded: true}, }; } else { - const blindedBlock = beaconBlockToBlinded(config, data as BeaconBlock); + const blindedBlock = fullOrBlindedBlockToBlinded(config, data); return { data: blindedBlock, meta: {...meta, executionPayloadBlinded: true}, @@ -790,17 +787,8 @@ export function getValidatorApi( if (!isForkExecution(version)) { throw Error(`Invalid fork=${version} for produceBlindedBlock`); } - - if (isBlockContents(data)) { - const {block} = data; - const blindedBlock = beaconBlockToBlinded(config, block as BeaconBlock); - return {data: blindedBlock, meta: {version}}; - } else if (isBlindedBeaconBlock(data)) { - return {data, meta: {version}}; - } else { - const blindedBlock = beaconBlockToBlinded(config, data as BeaconBlock); - return {data: blindedBlock, meta: {version}}; - } + const blindedBlock = fullOrBlindedBlockToBlinded(config, isBlockContents(data) ? data.block : data); + return {data: blindedBlock, meta: {version}}; }, async produceAttestationData({committeeIndex, slot}) { diff --git a/packages/beacon-node/src/chain/blocks/writeBlockInputToDb.ts b/packages/beacon-node/src/chain/blocks/writeBlockInputToDb.ts index 89cf7ddc7556..9d5557558648 100644 --- a/packages/beacon-node/src/chain/blocks/writeBlockInputToDb.ts +++ b/packages/beacon-node/src/chain/blocks/writeBlockInputToDb.ts @@ -13,17 +13,11 @@ export async function writeBlockInputToDb(this: BeaconChain, blocksInput: BlockI const fnPromises: Promise[] = []; for (const blockInput of blocksInput) { - const {block, blockBytes} = blockInput; + const {block} = blockInput; const blockRoot = this.config.getForkTypes(block.message.slot).BeaconBlock.hashTreeRoot(block.message); const blockRootHex = toRootHex(blockRoot); - if (blockBytes) { - // skip serializing data if we already have it - this.metrics?.importBlock.persistBlockWithSerializedDataCount.inc(); - fnPromises.push(this.db.block.putBinary(this.db.block.getId(block), blockBytes)); - } else { - this.metrics?.importBlock.persistBlockNoSerializedDataCount.inc(); - fnPromises.push(this.db.block.add(block)); - } + this.metrics?.importBlock.persistBlockNoSerializedDataCount.inc(); + fnPromises.push(this.db.block.add(block)); this.logger.debug("Persist block to hot DB", { slot: block.message.slot, root: blockRootHex, diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index ee70231c7d40..1fa57562b924 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -13,6 +13,8 @@ import { PubkeyIndexMap, EpochShuffling, computeEndSlotAtEpoch, + blindedOrFullBlockHashTreeRoot, + isBlindedBlock, } from "@lodestar/state-transition"; import {BeaconConfig} from "@lodestar/config"; import { @@ -26,16 +28,16 @@ import { deneb, Wei, bellatrix, - isBlindedBeaconBlock, BeaconBlock, SignedBeaconBlock, ExecutionPayload, BlindedBeaconBlock, BlindedBeaconBlockBody, + SignedBlindedBeaconBlock, } from "@lodestar/types"; import {CheckpointWithHex, ExecutionStatus, IForkChoice, ProtoBlock, UpdateHeadOpt} from "@lodestar/fork-choice"; import {ProcessShutdownCallback} from "@lodestar/validator"; -import {Logger, fromHex, gweiToWei, isErrorAborted, pruneSetToMax, sleep, toRootHex} from "@lodestar/utils"; +import {Logger, fromHex, toHex, gweiToWei, isErrorAborted, pruneSetToMax, sleep, toRootHex} from "@lodestar/utils"; import {ForkSeq, GENESIS_SLOT, SLOTS_PER_EPOCH} from "@lodestar/params"; import {GENESIS_EPOCH, ZERO_HASH} from "../constants/index.js"; @@ -47,6 +49,8 @@ import {Clock, ClockEvent, IClock} from "../util/clock.js"; import {ensureDir, writeIfNotExist} from "../util/file.js"; import {isOptimisticBlock} from "../util/forkChoice.js"; import {BufferPool} from "../util/bufferPool.js"; +import {Eth1Error, Eth1ErrorCode} from "../eth1/errors.js"; +import {blindedOrFullBlockToFull} from "../util/fullOrBlindedBlock.js"; import {BlockProcessor, ImportBlockOpts} from "./blocks/index.js"; import {ChainEventEmitter, ChainEvent} from "./emitter.js"; import { @@ -561,8 +565,13 @@ export class BeaconChain implements IBeaconChain { } async getCanonicalBlockAtSlot( - slot: Slot - ): Promise<{block: SignedBeaconBlock; executionOptimistic: boolean; finalized: boolean} | null> { + slot: Slot, + getFull = true + ): Promise<{ + block: SignedBeaconBlock | SignedBlindedBeaconBlock; + executionOptimistic: boolean; + finalized: boolean; + } | null> { const finalizedBlock = this.forkChoice.getFinalizedBlock(); if (slot > finalizedBlock.slot) { // Unfinalized slot, attempt to find in fork-choice @@ -570,7 +579,11 @@ export class BeaconChain implements IBeaconChain { if (block) { const data = await this.db.block.get(fromHex(block.blockRoot)); if (data) { - return {block: data, executionOptimistic: isOptimisticBlock(block), finalized: false}; + return { + block: getFull ? await this.fullOrBlindedSignedBeaconBlockToFull(data) : data, + executionOptimistic: isOptimisticBlock(block), + finalized: false, + }; } } // A non-finalized slot expected to be found in the hot db, could be archived during @@ -579,24 +592,45 @@ export class BeaconChain implements IBeaconChain { } const data = await this.db.blockArchive.get(slot); - return data && {block: data, executionOptimistic: false, finalized: true}; + return ( + data && { + block: getFull ? await this.fullOrBlindedSignedBeaconBlockToFull(data) : data, + executionOptimistic: false, + finalized: true, + } + ); } async getBlockByRoot( - root: string - ): Promise<{block: SignedBeaconBlock; executionOptimistic: boolean; finalized: boolean} | null> { + root: string, + getFull = true + ): Promise<{ + block: SignedBeaconBlock | SignedBlindedBeaconBlock; + executionOptimistic: boolean; + finalized: boolean; + } | null> { const block = this.forkChoice.getBlockHex(root); if (block) { const data = await this.db.block.get(fromHex(root)); if (data) { - return {block: data, executionOptimistic: isOptimisticBlock(block), finalized: false}; + return { + block: getFull ? await this.fullOrBlindedSignedBeaconBlockToFull(data) : data, + executionOptimistic: isOptimisticBlock(block), + finalized: false, + }; } // If block is not found in hot db, try cold db since there could be an archive cycle happening // TODO: Add a lock to the archiver to have deterministic behavior on where are blocks } const data = await this.db.blockArchive.getByRoot(fromHex(root)); - return data && {block: data, executionOptimistic: false, finalized: true}; + return ( + data && { + block: getFull ? await this.fullOrBlindedSignedBeaconBlockToFull(data) : data, + executionOptimistic: false, + finalized: true, + } + ); } async produceCommonBlockBody(blockAttributes: BlockAttributes): Promise { @@ -836,7 +870,7 @@ export class BeaconChain implements IBeaconChain { persistBlock(data: BeaconBlock | BlindedBeaconBlock, suffix?: string): void { const slot = data.slot; - if (isBlindedBeaconBlock(data)) { + if (isBlindedBlock(data)) { const sszType = this.config.getExecutionForkTypes(slot).BlindedBeaconBlock; void this.persistSszObject("BlindedBeaconBlock", sszType.serialize(data), sszType.hashTreeRoot(data), suffix); } else { @@ -995,6 +1029,25 @@ export class BeaconChain implements IBeaconChain { return {state: blockState, stateId: "block_state_any_epoch", shouldWarn: true}; } + async fullOrBlindedSignedBeaconBlockToFull( + block: SignedBeaconBlock | SignedBlindedBeaconBlock + ): Promise { + if (!isBlindedBlock(block)) { + return block; + } + const blockHash = toHex(blindedOrFullBlockHashTreeRoot(this.config, block.message)); + const [payload] = await this.executionEngine.getPayloadBodiesByHash(this.config.getForkName(block.message.slot), [ + blockHash, + ]); + if (!payload) { + throw new Eth1Error( + {code: Eth1ErrorCode.INVALID_PAYLOAD_BODY, blockHash}, + `Execution PayloadBody not found by eth1 engine for ${blockHash}` + ); + } + return blindedOrFullBlockToFull(this.config, block, payload); + } + private async persistSszObject( typeName: string, bytes: Uint8Array, diff --git a/packages/beacon-node/src/chain/interface.ts b/packages/beacon-node/src/chain/interface.ts index 5185662eaa4f..9ee719e37c49 100644 --- a/packages/beacon-node/src/chain/interface.ts +++ b/packages/beacon-node/src/chain/interface.ts @@ -15,6 +15,7 @@ import { ExecutionPayload, SignedBeaconBlock, BlindedBeaconBlock, + SignedBlindedBeaconBlock, } from "@lodestar/types"; import { BeaconStateAllForks, @@ -173,14 +174,24 @@ export interface IBeaconChain { * forkchoice. Works for finalized slots as well */ getCanonicalBlockAtSlot( - slot: Slot - ): Promise<{block: SignedBeaconBlock; executionOptimistic: boolean; finalized: boolean} | null>; + slot: Slot, + getFull?: boolean + ): Promise<{ + block: SignedBeaconBlock | SignedBlindedBeaconBlock; + executionOptimistic: boolean; + finalized: boolean; + } | null>; /** * Get local block by root, does not fetch from the network */ getBlockByRoot( - root: RootHex - ): Promise<{block: SignedBeaconBlock; executionOptimistic: boolean; finalized: boolean} | null>; + root: RootHex, + getFull?: boolean + ): Promise<{ + block: SignedBeaconBlock | SignedBlindedBeaconBlock; + executionOptimistic: boolean; + finalized: boolean; + } | null>; getContents(beaconBlock: deneb.BeaconBlock): deneb.Contents; @@ -197,6 +208,8 @@ export interface IBeaconChain { consensusBlockValue: Wei; }>; + fullOrBlindedSignedBeaconBlockToFull(block: SignedBeaconBlock | SignedBlindedBeaconBlock): Promise; + /** Process a block until complete */ processBlock(block: BlockInput, opts?: ImportBlockOpts): Promise; /** Process a chain of blocks until complete */ diff --git a/packages/beacon-node/src/db/repositories/block.ts b/packages/beacon-node/src/db/repositories/block.ts index b01acb8c2ea8..061a784a2f50 100644 --- a/packages/beacon-node/src/db/repositories/block.ts +++ b/packages/beacon-node/src/db/repositories/block.ts @@ -1,7 +1,11 @@ import {ChainForkConfig} from "@lodestar/config"; import {Db, Repository} from "@lodestar/db"; -import {SignedBeaconBlock, ssz} from "@lodestar/types"; -import {getSignedBlockTypeFromBytes} from "../../util/multifork.js"; +import {SignedBeaconBlock, SignedBlindedBeaconBlock, ssz} from "@lodestar/types"; +import {blindedOrFullBlockHashTreeRoot, fullOrBlindedSignedBlockToBlinded} from "@lodestar/state-transition"; +import { + serializeFullOrBlindedSignedBeaconBlock, + deserializeFullOrBlindedSignedBeaconBlock, +} from "../../util/fullOrBlindedBlock.js"; import {Bucket, getBucketNameByValue} from "../buckets.js"; /** @@ -9,25 +13,36 @@ import {Bucket, getBucketNameByValue} from "../buckets.js"; * * Used to store unfinalized blocks */ -export class BlockRepository extends Repository { +export class BlockRepository extends Repository { constructor(config: ChainForkConfig, db: Db) { const bucket = Bucket.allForks_block; - const type = ssz.phase0.SignedBeaconBlock; // Pick some type but won't be used + // Pick some type but won't be used, override below so correct container is used + const type = ssz.phase0.SignedBeaconBlock; super(config, db, bucket, type, getBucketNameByValue(bucket)); } /** * Id is hashTreeRoot of unsigned BeaconBlock */ - getId(value: SignedBeaconBlock): Uint8Array { - return this.config.getForkTypes(value.message.slot).BeaconBlock.hashTreeRoot(value.message); + getId(value: SignedBeaconBlock | SignedBlindedBeaconBlock): Uint8Array { + return blindedOrFullBlockHashTreeRoot(this.config, value.message); } - encodeValue(value: SignedBeaconBlock): Buffer { - return this.config.getForkTypes(value.message.slot).SignedBeaconBlock.serialize(value) as Buffer; + encodeValue(value: SignedBeaconBlock | SignedBlindedBeaconBlock): Uint8Array { + return serializeFullOrBlindedSignedBeaconBlock(this.config, value); } - decodeValue(data: Buffer): SignedBeaconBlock { - return getSignedBlockTypeFromBytes(this.config, data).deserialize(data); + decodeValue(data: Uint8Array): SignedBeaconBlock | SignedBlindedBeaconBlock { + return deserializeFullOrBlindedSignedBeaconBlock(this.config, data); + } + + // TODO: (@matthewkeil) should this throw or should we allow puts of binary blocks? + // eslint-disable-next-line @typescript-eslint/naming-convention + async putBinary(_: Uint8Array, __: Uint8Array): Promise { + throw new Error("cannot .putBinary into BlockRepository. must use .add so can be saved blinded"); + } + + async add(value: SignedBeaconBlock | SignedBlindedBeaconBlock): Promise { + return super.add(fullOrBlindedSignedBlockToBlinded(this.config, value)); } } diff --git a/packages/beacon-node/src/db/repositories/blockArchive.ts b/packages/beacon-node/src/db/repositories/blockArchive.ts index 15c07f552b21..e7785e4ea899 100644 --- a/packages/beacon-node/src/db/repositories/blockArchive.ts +++ b/packages/beacon-node/src/db/repositories/blockArchive.ts @@ -1,9 +1,13 @@ import all from "it-all"; import {ChainForkConfig} from "@lodestar/config"; import {Db, Repository, KeyValue, FilterOptions} from "@lodestar/db"; -import {Slot, Root, ssz, SignedBeaconBlock} from "@lodestar/types"; +import {Slot, Root, ssz, SignedBeaconBlock, SignedBlindedBeaconBlock} from "@lodestar/types"; import {bytesToInt} from "@lodestar/utils"; -import {getSignedBlockTypeFromBytes} from "../../util/multifork.js"; +import {blindedOrFullBlockHashTreeRoot, fullOrBlindedSignedBlockToBlinded} from "@lodestar/state-transition"; +import { + serializeFullOrBlindedSignedBeaconBlock, + deserializeFullOrBlindedSignedBeaconBlock, +} from "../../util/fullOrBlindedBlock.js"; import {Bucket, getBucketNameByValue} from "../buckets.js"; import {getRootIndexKey, getParentRootIndexKey} from "./blockArchiveIndex.js"; import {deleteParentRootIndex, deleteRootIndex, storeParentRootIndex, storeRootIndex} from "./blockArchiveIndex.js"; @@ -21,26 +25,27 @@ export type BlockArchiveBatchPutBinaryItem = KeyValue & { /** * Stores finalized blocks. Block slot is identifier. */ -export class BlockArchiveRepository extends Repository { +export class BlockArchiveRepository extends Repository { constructor(config: ChainForkConfig, db: Db) { const bucket = Bucket.allForks_blockArchive; - const type = ssz.phase0.SignedBeaconBlock; // Pick some type but won't be used + // Pick some type but won't be used, override below so correct container is used + const type = ssz.phase0.SignedBeaconBlock; super(config, db, bucket, type, getBucketNameByValue(bucket)); } // Overrides for multi-fork - encodeValue(value: SignedBeaconBlock): Uint8Array { - return this.config.getForkTypes(value.message.slot).SignedBeaconBlock.serialize(value); + encodeValue(value: SignedBeaconBlock | SignedBlindedBeaconBlock): Uint8Array { + return serializeFullOrBlindedSignedBeaconBlock(this.config, value); } - decodeValue(data: Uint8Array): SignedBeaconBlock { - return getSignedBlockTypeFromBytes(this.config, data).deserialize(data); + decodeValue(data: Uint8Array): SignedBeaconBlock | SignedBlindedBeaconBlock { + return deserializeFullOrBlindedSignedBeaconBlock(this.config, data); } // Handle key as slot - getId(value: SignedBeaconBlock): Slot { + getId(value: SignedBeaconBlock | SignedBlindedBeaconBlock): Slot { return value.message.slot; } @@ -50,8 +55,8 @@ export class BlockArchiveRepository extends Repository // Overrides to index - async put(key: Slot, value: SignedBeaconBlock): Promise { - const blockRoot = this.config.getForkTypes(value.message.slot).BeaconBlock.hashTreeRoot(value.message); + async put(key: Slot, value: SignedBeaconBlock | SignedBlindedBeaconBlock): Promise { + const blockRoot = blindedOrFullBlockHashTreeRoot(this.config, value.message); const slot = value.message.slot; await Promise.all([ super.put(key, value), @@ -60,12 +65,14 @@ export class BlockArchiveRepository extends Repository ]); } - async batchPut(items: KeyValue[]): Promise { + async batchPut(items: KeyValue[]): Promise { await Promise.all([ - super.batchPut(items), + super.batchPut( + items.map(({key, value}) => ({key, value: fullOrBlindedSignedBlockToBlinded(this.config, value)})) + ), Array.from(items).map((item) => { const slot = item.value.message.slot; - const blockRoot = this.config.getForkTypes(slot).BeaconBlock.hashTreeRoot(item.value.message); + const blockRoot = blindedOrFullBlockHashTreeRoot(this.config, item.value.message); return storeRootIndex(this.db, slot, blockRoot); }), Array.from(items).map((item) => { @@ -76,6 +83,7 @@ export class BlockArchiveRepository extends Repository ]); } + // TODO: (@matthewkeil) should this throw or should we allow puts of binary blocks? async batchPutBinary(items: BlockArchiveBatchPutBinaryItem[]): Promise { await Promise.all([ super.batchPutBinary(items), @@ -84,25 +92,25 @@ export class BlockArchiveRepository extends Repository ]); } - async remove(value: SignedBeaconBlock): Promise { + async remove(value: SignedBeaconBlock | SignedBlindedBeaconBlock): Promise { await Promise.all([ super.remove(value), - deleteRootIndex(this.db, this.config.getForkTypes(value.message.slot).SignedBeaconBlock, value), + deleteRootIndex(this.db, blindedOrFullBlockHashTreeRoot(this.config, value.message)), deleteParentRootIndex(this.db, value), ]); } - async batchRemove(values: SignedBeaconBlock[]): Promise { + async batchRemove(values: (SignedBeaconBlock | SignedBlindedBeaconBlock)[]): Promise { await Promise.all([ super.batchRemove(values), Array.from(values).map((value) => - deleteRootIndex(this.db, this.config.getForkTypes(value.message.slot).SignedBeaconBlock, value) + deleteRootIndex(this.db, blindedOrFullBlockHashTreeRoot(this.config, value.message)) ), Array.from(values).map((value) => deleteParentRootIndex(this.db, value)), ]); } - async *valuesStream(opts?: BlockFilterOptions): AsyncIterable { + async *valuesStream(opts?: BlockFilterOptions): AsyncIterable { const firstSlot = this.getFirstSlot(opts); const valuesStream = super.valuesStream(opts); const step = (opts && opts.step) ?? 1; @@ -114,13 +122,13 @@ export class BlockArchiveRepository extends Repository } } - async values(opts?: BlockFilterOptions): Promise { + async values(opts?: BlockFilterOptions): Promise<(SignedBeaconBlock | SignedBlindedBeaconBlock)[]> { return all(this.valuesStream(opts)); } // INDEX - async getByRoot(root: Root): Promise { + async getByRoot(root: Root): Promise { const slot = await this.getSlotByRoot(root); return slot !== null ? this.get(slot) : null; } @@ -130,7 +138,7 @@ export class BlockArchiveRepository extends Repository return slot !== null ? ({key: slot, value: await this.getBinary(slot)} as KeyValue) : null; } - async getByParentRoot(root: Root): Promise { + async getByParentRoot(root: Root): Promise { const slot = await this.getSlotByParentRoot(root); return slot !== null ? this.get(slot) : null; } diff --git a/packages/beacon-node/src/db/repositories/blockArchiveIndex.ts b/packages/beacon-node/src/db/repositories/blockArchiveIndex.ts index 797142d09db7..dbdca157e51e 100644 --- a/packages/beacon-node/src/db/repositories/blockArchiveIndex.ts +++ b/packages/beacon-node/src/db/repositories/blockArchiveIndex.ts @@ -1,7 +1,6 @@ import {Db, encodeKey} from "@lodestar/db"; -import {Slot, Root, ssz, SignedBeaconBlock, SSZTypesFor} from "@lodestar/types"; +import {Slot, Root, SignedBeaconBlock, SignedBlindedBeaconBlock} from "@lodestar/types"; import {intToBytes} from "@lodestar/utils"; -import {ForkAll} from "@lodestar/params"; import {Bucket} from "../buckets.js"; export async function storeRootIndex(db: Db, slot: Slot, blockRoot: Root): Promise { @@ -12,16 +11,14 @@ export async function storeParentRootIndex(db: Db, slot: Slot, parentRoot: Root) return db.put(getParentRootIndexKey(parentRoot), intToBytes(slot, 8, "be")); } -export async function deleteRootIndex( - db: Db, - signedBeaconBlockType: SSZTypesFor, - block: SignedBeaconBlock -): Promise { - const beaconBlockType = (signedBeaconBlockType as typeof ssz.phase0.SignedBeaconBlock).fields["message"]; - return db.delete(getRootIndexKey(beaconBlockType.hashTreeRoot(block.message))); +export async function deleteRootIndex(db: Db, blockRoot: Uint8Array): Promise { + return db.delete(getRootIndexKey(blockRoot)); } -export async function deleteParentRootIndex(db: Db, block: SignedBeaconBlock): Promise { +export async function deleteParentRootIndex( + db: Db, + block: SignedBeaconBlock | SignedBlindedBeaconBlock +): Promise { return db.delete(getParentRootIndexKey(block.message.parentRoot)); } diff --git a/packages/beacon-node/src/eth1/errors.ts b/packages/beacon-node/src/eth1/errors.ts index 914a5448ade3..40e7b52f574f 100644 --- a/packages/beacon-node/src/eth1/errors.ts +++ b/packages/beacon-node/src/eth1/errors.ts @@ -23,6 +23,8 @@ export enum Eth1ErrorCode { NON_CONSECUTIVE_LOGS = "ETH1_ERROR_NON_CONSECUTIVE_LOGS", /** Expected a deposit log in the db for the index, missing log implies a corrupted db */ MISSING_DEPOSIT_LOG = "ETH1_ERROR_MISSING_DEPOSIT_LOG", + /** Expected transactions or withdrawals for un-blinding block from db before serving */ + INVALID_PAYLOAD_BODY = "ETH1_ERROR_INVALID_PAYLOAD_BODY", } export type Eth1ErrorType = @@ -35,6 +37,7 @@ export type Eth1ErrorType = | {code: Eth1ErrorCode.NOT_ENOUGH_DEPOSIT_ROOTS; index: number; treeLength: number} | {code: Eth1ErrorCode.DUPLICATE_DISTINCT_LOG; newIndex: number; lastLogIndex: number} | {code: Eth1ErrorCode.NON_CONSECUTIVE_LOGS; newIndex: number; lastLogIndex: number} - | {code: Eth1ErrorCode.MISSING_DEPOSIT_LOG; newIndex: number; lastLogIndex: number}; + | {code: Eth1ErrorCode.MISSING_DEPOSIT_LOG; newIndex: number; lastLogIndex: number} + | {code: Eth1ErrorCode.INVALID_PAYLOAD_BODY; blockHash: string}; export class Eth1Error extends LodestarError {} diff --git a/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRange.ts b/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRange.ts index d1046db9651d..e758e8f1a41c 100644 --- a/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRange.ts +++ b/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRange.ts @@ -4,6 +4,7 @@ import {deneb, phase0} from "@lodestar/types"; import {fromHex} from "@lodestar/utils"; import {IBeaconChain} from "../../../chain/index.js"; import {IBeaconDb} from "../../../db/index.js"; +import {deserializeFullOrBlindedSignedBeaconBlock} from "../../../util/fullOrBlindedBlock.js"; // TODO: Unit test @@ -23,8 +24,11 @@ export async function* onBeaconBlocksByRange( if (startSlot <= finalizedSlot) { // Chain of blobs won't change for await (const {key, value} of finalized.binaryEntriesStream({gte: startSlot, lt: endSlot})) { + const fullBlock = await chain.fullOrBlindedSignedBeaconBlockToFull( + deserializeFullOrBlindedSignedBeaconBlock(chain.config, value) + ); yield { - data: value, + data: chain.config.getForkTypes(fullBlock.message.slot).SignedBeaconBlock.serialize(fullBlock), fork: chain.config.getForkName(finalized.decodeKey(key)), }; } @@ -54,9 +58,12 @@ export async function* onBeaconBlocksByRange( throw new ResponseError(RespStatus.SERVER_ERROR, `No item for root ${block.blockRoot} slot ${block.slot}`); } + const fullBlock = await chain.fullOrBlindedSignedBeaconBlockToFull( + deserializeFullOrBlindedSignedBeaconBlock(chain.config, blockBytes) + ); yield { - data: blockBytes, - fork: chain.config.getForkName(block.slot), + data: chain.config.getForkTypes(fullBlock.message.slot).SignedBeaconBlock.serialize(fullBlock), + fork: chain.config.getForkName(fullBlock.message.slot), }; } diff --git a/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRoot.ts b/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRoot.ts index 36d90256276e..c1863c1b0c0c 100644 --- a/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRoot.ts +++ b/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRoot.ts @@ -4,6 +4,7 @@ import {toRootHex} from "@lodestar/utils"; import {IBeaconChain} from "../../../chain/index.js"; import {IBeaconDb} from "../../../db/index.js"; import {getSlotFromSignedBeaconBlockSerialized} from "../../../util/sszBytes.js"; +import {deserializeFullOrBlindedSignedBeaconBlock} from "../../../util/fullOrBlindedBlock.js"; export async function* onBeaconBlocksByRoot( requestBody: phase0.BeaconBlocksByRootRequest, @@ -38,8 +39,11 @@ export async function* onBeaconBlocksByRoot( slot = slotFromBytes; } + const block = await chain.fullOrBlindedSignedBeaconBlockToFull( + deserializeFullOrBlindedSignedBeaconBlock(chain.config, blockBytes) + ); yield { - data: blockBytes, + data: chain.config.getForkTypes(slot).SignedBeaconBlock.serialize(block), fork: chain.config.getForkName(slot), }; } diff --git a/packages/beacon-node/src/util/fullOrBlindedBlock.ts b/packages/beacon-node/src/util/fullOrBlindedBlock.ts new file mode 100644 index 000000000000..fef769703251 --- /dev/null +++ b/packages/beacon-node/src/util/fullOrBlindedBlock.ts @@ -0,0 +1,221 @@ +import {ChainForkConfig} from "@lodestar/config"; +import { + bellatrix, + capella, + deneb, + ExecutionPayload, + ExecutionPayloadHeader, + isBlindedSignedBeaconBlock, + SignedBeaconBlock, + SignedBlindedBeaconBlock, +} from "@lodestar/types"; +import {BYTES_PER_LOGS_BLOOM, ForkSeq, ForkName, isForkExecution, SYNC_COMMITTEE_SIZE} from "@lodestar/params"; +import {ExecutionPayloadBody} from "../execution/engine/types.js"; +import {ROOT_SIZE, getSlotFromSignedBeaconBlockSerialized} from "./sszBytes.js"; + +/** + * * class SignedBeaconBlock(Container): + * message: BeaconBlock [offset - 4 bytes] + * signature: BLSSignature [fixed - 96 bytes] + */ +const SIGNED_BEACON_BLOCK_FIXED_LENGTH = 4 + 96; +/** + * class BeaconBlock(Container) or class BlindedBeaconBlock(Container): + * slot: Slot [fixed - 8 bytes] + * proposer_index: ValidatorIndex [fixed - 8 bytes] + * parent_root: Root [fixed - 32 bytes] + * state_root: Root [fixed - 32 bytes] + * body: MaybeBlindBeaconBlockBody [offset - 4 bytes] + */ +const BEACON_BLOCK_FIXED_LENGTH = 8 + 8 + 32 + 32 + 4; +/** + * class BeaconBlockBody(Container) or class BlindedBeaconBlockBody(Container): + * + * Phase 0: + * randaoReveal: [fixed - 96 bytes] + * eth1Data: [Container] + * depositRoot: [fixed - 32 bytes] + * depositCount: [fixed - 8 bytes] + * blockHash: [fixed - 32 bytes] + * graffiti: [fixed - 32 bytes] + * proposerSlashings: [offset - 4 bytes] + * attesterSlashings: [offset - 4 bytes] + * attestations: [offset - 4 bytes] + * deposits: [offset - 4 bytes] + * voluntaryExits: [offset - 4 bytes] + * + * Altair: + * syncCommitteeBits: [fixed - 4 or 64 bytes] (pull from params) + * syncCommitteeSignature: [fixed - 96 bytes] + * + * Bellatrix: + * executionPayload: [offset - 4 bytes] + * + * Capella: + * blsToExecutionChanges [offset - 4 bytes] + * + * Deneb: + * blobKzgCommitments [offset - 4 bytes] + */ + +const LOCATION_OF_ETH1_BLOCK_HASH = 96 + 32 + 8; +export function getEth1BlockHashFromSerializedBlock(block: Uint8Array): Uint8Array { + const firstByte = SIGNED_BEACON_BLOCK_FIXED_LENGTH + BEACON_BLOCK_FIXED_LENGTH + LOCATION_OF_ETH1_BLOCK_HASH; + return block.slice(firstByte, firstByte + ROOT_SIZE); +} + +const LOCATION_OF_EXECUTION_PAYLOAD_OFFSET = + LOCATION_OF_ETH1_BLOCK_HASH + 32 + 32 + 4 + 4 + 4 + 4 + 4 + SYNC_COMMITTEE_SIZE / 8 + 96; + +/** + * class ExecutionPayload(Container) or class ExecutionPayloadHeader(Container) + * parentHash: [fixed - 32 bytes] + * feeRecipient: [fixed - 20 bytes] + * stateRoot: [fixed - 32 bytes] + * receiptsRoot: [fixed - 32 bytes] + * logsBloom: [fixed - 256 bytes] (pull from params) + * prevRandao: [fixed - 32 bytes] + * blockNumber: [fixed - 8 bytes] + * gasLimit: [fixed - 8 bytes] + * gasUsed: [fixed - 8 bytes] + * timestamp: [fixed - 8 bytes] + * extraData: [offset - 4 bytes] + * baseFeePerGas: [fixed - 32 bytes] + * blockHash: [fixed - 32 bytes] + * ------------------------------------------------ + * transactions: [offset - 4 bytes] + * - or - + * transactionsRoot: [fixed - 32 bytes] + * + * Capella: + * withdrawals: [offset - 4 bytes] + * - or - + * withdrawalsRoot: [fixed - 32 bytes] + * ------------------------------------------------ + * Deneb: + * dataGasUsed: [fixed - 8 bytes] + * excessDataGas: [fixed - 8 bytes] + */ + +const LOCATION_OF_EXTRA_DATA_OFFSET_WITHIN_EXECUTION_PAYLOAD = + 32 + 20 + 32 + 32 + BYTES_PER_LOGS_BLOOM + 32 + 8 + 8 + 8 + 8; + +export function isBlindedBytes(forkSeq: ForkSeq, blockBytes: Uint8Array): boolean { + if (forkSeq < ForkSeq.bellatrix) { + return false; + } + + const dv = new DataView(blockBytes.buffer, blockBytes.byteOffset, blockBytes.byteLength); + + // read the executionPayload offset, encoded as offset from start of BeaconBlockBody and compensate with the fixed + // data length of the SignedBeaconBlock and BeaconBlock to get absolute offset from start of bytes + const readExecutionPayloadOffsetAt = + LOCATION_OF_EXECUTION_PAYLOAD_OFFSET + SIGNED_BEACON_BLOCK_FIXED_LENGTH + BEACON_BLOCK_FIXED_LENGTH; + const executionPayloadOffset = + dv.getUint32(readExecutionPayloadOffsetAt, true) + SIGNED_BEACON_BLOCK_FIXED_LENGTH + BEACON_BLOCK_FIXED_LENGTH; + + // read the extraData offset, encoded as offset from start of ExecutionPayload and compensate with absolute offset of + // executionPayload to get location of first byte of extraData + const readExtraDataOffsetAt = LOCATION_OF_EXTRA_DATA_OFFSET_WITHIN_EXECUTION_PAYLOAD + executionPayloadOffset; + const firstByte = dv.getUint32(readExtraDataOffsetAt, true) + executionPayloadOffset; + + // compare first byte of extraData with location of the offset of the extraData. In full blocks the distance between + // the offset and first byte is at maximum 4 + 32 + 32 + 4 + 4 + 8 + 8 = 92. In blinded blocks the distance at minimum + // is 4 + 32 + 32 + 4 + 4 + 32 = 108. Therefore if the distance is 93 or greater it must be blinded + return firstByte - readExtraDataOffsetAt > 92; +} + +export function serializeFullOrBlindedSignedBeaconBlock( + config: ChainForkConfig, + value: SignedBeaconBlock | SignedBlindedBeaconBlock +): Uint8Array { + if (isBlindedSignedBeaconBlock(value)) { + const type = config.getExecutionForkTypes(value.message.slot).SignedBlindedBeaconBlock; + return type.serialize(value); + } + const type = config.getForkTypes(value.message.slot).SignedBeaconBlock; + return type.serialize(value); +} + +export function deserializeFullOrBlindedSignedBeaconBlock( + config: ChainForkConfig, + bytes: Buffer | Uint8Array +): SignedBeaconBlock | SignedBlindedBeaconBlock { + const slot = getSlotFromSignedBeaconBlockSerialized(bytes); + if (slot === null) { + throw Error("getSignedBlockTypeFromBytes: invalid bytes"); + } + + return isBlindedBytes(config.getForkSeq(slot), bytes) + ? config.getExecutionForkTypes(slot).SignedBlindedBeaconBlock.deserialize(bytes) + : config.getForkTypes(slot).SignedBeaconBlock.deserialize(bytes); +} + +function executionPayloadHeaderToPayload( + forkSeq: ForkSeq, + header: ExecutionPayloadHeader, + {transactions, withdrawals}: Partial +): ExecutionPayload { + const bellatrixPayloadFields: ExecutionPayload = { + parentHash: header.parentHash, + feeRecipient: header.feeRecipient, + stateRoot: header.stateRoot, + receiptsRoot: header.receiptsRoot, + logsBloom: header.logsBloom, + prevRandao: header.prevRandao, + blockNumber: header.blockNumber, + gasLimit: header.gasLimit, + gasUsed: header.gasUsed, + timestamp: header.timestamp, + extraData: header.extraData, + baseFeePerGas: header.baseFeePerGas, + blockHash: header.blockHash, + transactions: transactions ?? [], + }; + + if (forkSeq >= ForkSeq.capella) { + (bellatrixPayloadFields as capella.ExecutionPayload).withdrawals = withdrawals ?? []; + } + + if (forkSeq >= ForkSeq.deneb) { + // https://github.com/ethereum/consensus-specs/blob/dev/specs/eip4844/beacon-chain.md#process_execution_payload + (bellatrixPayloadFields as deneb.ExecutionPayload).blobGasUsed = ( + header as deneb.ExecutionPayloadHeader + ).blobGasUsed; + (bellatrixPayloadFields as deneb.ExecutionPayload).excessBlobGas = ( + header as deneb.ExecutionPayloadHeader + ).excessBlobGas; + } + + return bellatrixPayloadFields; +} + +// TODO: (@matthewkeil) not the same as blindedOrFullBlockToFull in state-transition. consider merging? +export function blindedOrFullBlockToFull( + config: ChainForkConfig, + block: SignedBeaconBlock | SignedBlindedBeaconBlock, + transactionsAndWithdrawals: Partial +): SignedBeaconBlock { + if ( + !isBlindedSignedBeaconBlock(block) || // already full + !isForkExecution(config.getForkName(block.message.slot)) || // no execution payload + (block as unknown as SignedBeaconBlock).message.body.executionPayload?.timestamp === 0 // before merge + ) { + return block; + } + + return config.getForkTypes(block.message.slot).SignedBeaconBlock.clone({ + signature: block.signature, + message: { + ...block.message, + body: { + ...block.message.body, + executionPayload: executionPayloadHeaderToPayload( + config.getForkSeq(block.message.slot), + (block.message.body as bellatrix.BlindedBeaconBlockBody).executionPayloadHeader, + transactionsAndWithdrawals + ), + }, + }, + }); +} diff --git a/packages/beacon-node/src/util/sszBytes.ts b/packages/beacon-node/src/util/sszBytes.ts index c27df1a0fbf3..f54b9be4de04 100644 --- a/packages/beacon-node/src/util/sszBytes.ts +++ b/packages/beacon-node/src/util/sszBytes.ts @@ -37,7 +37,7 @@ export type CommitteeBitsBase64 = string; const VARIABLE_FIELD_OFFSET = 4; const ATTESTATION_BEACON_BLOCK_ROOT_OFFSET = VARIABLE_FIELD_OFFSET + 8 + 8; -const ROOT_SIZE = 32; +export const ROOT_SIZE = 32; const SLOT_SIZE = 8; const ATTESTATION_DATA_SIZE = 128; // MAX_COMMITTEES_PER_SLOT is in bit, need to convert to byte diff --git a/packages/beacon-node/test/fixtures/altair.ts b/packages/beacon-node/test/fixtures/altair.ts new file mode 100644 index 000000000000..19b5bad859f1 --- /dev/null +++ b/packages/beacon-node/test/fixtures/altair.ts @@ -0,0 +1,28 @@ +import {CachedBeaconStateAllForks} from "@lodestar/state-transition"; +import {ssz, altair} from "@lodestar/types"; +import {BlockGenerationOptionsPhase0, generatePhase0BeaconBlocks} from "./phase0.js"; +import {generateSignature} from "./utils.js"; + +export function generateSyncAggregate( + state: CachedBeaconStateAllForks, + block: altair.BeaconBlock +): altair.SyncAggregate { + return { + syncCommitteeBits: ssz.altair.SyncCommitteeBits.defaultValue(), + syncCommitteeSignature: generateSignature(), + }; +} + +export interface BlockGenerationOptionsAltair extends BlockGenerationOptionsPhase0 {} + +export function generateAltairBeaconBlocks( + state: CachedBeaconStateAllForks, + count: number, + opts?: BlockGenerationOptionsAltair +): altair.BeaconBlock[] { + const blocks = generatePhase0BeaconBlocks(state, count, opts) as altair.BeaconBlock[]; + for (const block of blocks) { + block.body.syncAggregate = generateSyncAggregate(state, block); + } + return blocks; +} diff --git a/packages/beacon-node/test/fixtures/bellatrix.ts b/packages/beacon-node/test/fixtures/bellatrix.ts new file mode 100644 index 000000000000..e698c36b108a --- /dev/null +++ b/packages/beacon-node/test/fixtures/bellatrix.ts @@ -0,0 +1,36 @@ +import {ssz, bellatrix} from "@lodestar/types"; +import {CachedBeaconStateAllForks} from "@lodestar/state-transition"; +import {BlockGenerationOptionsAltair, generateAltairBeaconBlocks} from "./altair.js"; + +export function generateBellatrixExecutionPayload(): bellatrix.ExecutionPayload { + return { + baseFeePerGas: BigInt(0), + blockHash: new Uint8Array(), + blockNumber: 0, + extraData: new Uint8Array(), + feeRecipient: new Uint8Array(), + gasLimit: 0, + gasUsed: 0, + logsBloom: new Uint8Array(), + parentHash: new Uint8Array(), + prevRandao: new Uint8Array(), + receiptsRoot: new Uint8Array(), + stateRoot: new Uint8Array(), + timestamp: 0, + transactions: [ssz.bellatrix.Transaction.defaultValue()], + }; +} + +export interface BlockGenerationOptionsBellatrix extends BlockGenerationOptionsAltair {} + +export function generateBellatrixBeaconBlocks( + state: CachedBeaconStateAllForks, + count: number, + opts?: BlockGenerationOptionsBellatrix +): bellatrix.BeaconBlock[] { + const blocks = generateAltairBeaconBlocks(state, count, opts) as bellatrix.BeaconBlock[]; + for (const block of blocks) { + block.body.executionPayload = generateBellatrixExecutionPayload(); + } + return blocks; +} diff --git a/packages/beacon-node/test/fixtures/capella.ts b/packages/beacon-node/test/fixtures/capella.ts index fe9b0206efb1..55e191d73f88 100644 --- a/packages/beacon-node/test/fixtures/capella.ts +++ b/packages/beacon-node/test/fixtures/capella.ts @@ -1,8 +1,16 @@ -import {CachedBeaconStateAltair} from "@lodestar/state-transition"; -import {capella} from "@lodestar/types"; +import {CachedBeaconStateAllForks} from "@lodestar/state-transition"; +import {ssz, bellatrix, capella} from "@lodestar/types"; +import {BlockGenerationOptionsBellatrix, generateBellatrixBeaconBlocks} from "./bellatrix.js"; + +export function generateCapellaExecutionPayload(payload: bellatrix.ExecutionPayload): capella.ExecutionPayload { + return { + ...payload, + withdrawals: [ssz.capella.Withdrawal.defaultValue()], + }; +} export function generateBlsToExecutionChanges( - state: CachedBeaconStateAltair, + state: CachedBeaconStateAllForks, count: number ): capella.SignedBLSToExecutionChange[] { const result: capella.SignedBLSToExecutionChange[] = []; @@ -22,3 +30,18 @@ export function generateBlsToExecutionChanges( return result; } + +export interface BlockGenerationOptionsCapella extends BlockGenerationOptionsBellatrix {} + +export function generateCapellaBeaconBlocks( + state: CachedBeaconStateAllForks, + count: number, + opts?: BlockGenerationOptionsCapella +): capella.BeaconBlock[] { + const blocks = generateBellatrixBeaconBlocks(state, count, opts) as capella.BeaconBlock[]; + for (const block of blocks) { + block.body.executionPayload = generateCapellaExecutionPayload(block.body.executionPayload); + block.body.blsToExecutionChanges = generateBlsToExecutionChanges(state, count); + } + return blocks; +} diff --git a/packages/beacon-node/test/fixtures/deneb.ts b/packages/beacon-node/test/fixtures/deneb.ts new file mode 100644 index 000000000000..37c78b4d2a58 --- /dev/null +++ b/packages/beacon-node/test/fixtures/deneb.ts @@ -0,0 +1,34 @@ +import crypto from "node:crypto"; +import {CachedBeaconStateAllForks} from "@lodestar/state-transition"; +import {capella, deneb} from "@lodestar/types"; +import {MAX_BLOBS_PER_BLOCK} from "@lodestar/params"; +import {generateCapellaBeaconBlocks, BlockGenerationOptionsCapella} from "./capella.js"; + +export function generateDenebExecutionPayload(payload: capella.ExecutionPayload): deneb.ExecutionPayload { + return { + ...payload, + blobGasUsed: BigInt(0), + excessBlobGas: BigInt(0), + }; +} + +export function generateKzgCommitments(count: number): deneb.BlobKzgCommitments { + return Array.from({length: count}, () => Uint8Array.from(crypto.randomBytes(48))); +} + +export interface BlockGenerationOptionsDeneb extends BlockGenerationOptionsCapella { + numKzgCommitments: number; +} + +export function generateDenebBeaconBlocks( + state: CachedBeaconStateAllForks, + count: number, + opts?: BlockGenerationOptionsDeneb +): capella.BeaconBlock[] { + const blocks = generateCapellaBeaconBlocks(state, count, opts) as deneb.BeaconBlock[]; + for (const block of blocks) { + block.body.executionPayload = generateDenebExecutionPayload(block.body.executionPayload); + block.body.blobKzgCommitments = generateKzgCommitments(opts?.numKzgCommitments ?? MAX_BLOBS_PER_BLOCK); + } + return blocks; +} diff --git a/packages/beacon-node/test/fixtures/electra.ts b/packages/beacon-node/test/fixtures/electra.ts new file mode 100644 index 000000000000..167a2908afb4 --- /dev/null +++ b/packages/beacon-node/test/fixtures/electra.ts @@ -0,0 +1,26 @@ +import {deneb, electra} from "@lodestar/types"; +import {CachedBeaconStateAllForks} from "@lodestar/state-transition"; +import {BlockGenerationOptionsDeneb, generateDenebBeaconBlocks} from "./deneb.js"; + +export function generateElectraExecutionPayload(payload: deneb.ExecutionPayload): electra.ExecutionPayload { + return { + ...payload, + depositRequests: [], + withdrawalRequests: [], + consolidationRequests: [], + }; +} + +export interface BlockGenerationOptionsElectra extends BlockGenerationOptionsDeneb {} + +export function generateElectraBeaconBlocks( + state: CachedBeaconStateAllForks, + count: number, + opts?: BlockGenerationOptionsElectra +): electra.BeaconBlock[] { + const blocks = generateDenebBeaconBlocks(state, count, opts) as electra.BeaconBlock[]; + for (const block of blocks) { + block.body.executionPayload = generateElectraExecutionPayload(block.body.executionPayload); + } + return blocks; +} diff --git a/packages/beacon-node/test/fixtures/phase0.ts b/packages/beacon-node/test/fixtures/phase0.ts index 56b419a824e7..5bdc77888f8e 100644 --- a/packages/beacon-node/test/fixtures/phase0.ts +++ b/packages/beacon-node/test/fixtures/phase0.ts @@ -1,14 +1,84 @@ -import {SLOTS_PER_EPOCH} from "@lodestar/params"; +import crypto from "node:crypto"; import { - CachedBeaconStateAltair, + MAX_ATTESTER_SLASHINGS, + MAX_DEPOSITS, + MAX_PROPOSER_SLASHINGS, + MAX_VOLUNTARY_EXITS, + SLOTS_PER_EPOCH, +} from "@lodestar/params"; +import { + CachedBeaconStateAllForks, computeEpochAtSlot, computeStartSlotAtEpoch, getBlockRootAtSlot, + getRandaoMix, } from "@lodestar/state-transition"; -import {phase0} from "@lodestar/types"; +import {phase0, ssz} from "@lodestar/types"; +import {getDefaultGraffiti} from "../../src/util/graffiti.js"; +import {getLodestarClientVersion} from "../../src/util/metadata.js"; +import {getDepositsWithProofs} from "../../src/eth1/utils/deposits.js"; +import {generateKey, generateSignature, signContainer} from "./utils.js"; + +export function generateAttestationData( + state: CachedBeaconStateAllForks, + committeeIndex: number +): phase0.AttestationData { + const slot = state.slot; + const epoch = computeEpochAtSlot(slot); + return { + slot, + index: committeeIndex, + beaconBlockRoot: getBlockRootAtSlot(state, slot), + source: { + epoch: state.currentJustifiedCheckpoint.epoch, + root: state.currentJustifiedCheckpoint.root, + }, + target: { + epoch: epoch, + root: getBlockRootAtSlot(state, computeStartSlotAtEpoch(epoch)), + }, + }; +} + +export function generateAttestation( + state: CachedBeaconStateAllForks, + committeeIndex: number, + indexed: T +): T extends true ? phase0.IndexedAttestation : phase0.Attestation { + const slot = state.slot; + const attestation = { + data: generateAttestationData(state, committeeIndex), + signature: generateSignature(), + } as unknown as T extends true ? phase0.IndexedAttestation : phase0.Attestation; + + if (indexed) { + (attestation as phase0.IndexedAttestation).attestingIndices = Array.from( + state.epochCtx.getBeaconCommittee(slot, committeeIndex) + ); + } else { + // TODO: (@matthewkeil) add some mock data here so its not all zeros + (attestation as phase0.Attestation).aggregationBits = ssz.phase0.CommitteeBits.defaultValue(); + } + + return attestation; +} + +export function generateAttestations(state: CachedBeaconStateAllForks): phase0.Attestation[] { + const epoch = computeEpochAtSlot(state.slot); + const committeeCount = state.epochCtx.getCommitteeCountPerSlot(epoch); + const attestations: phase0.Attestation[] = []; + for (let committeeIndex = 0; committeeIndex < committeeCount; committeeIndex++) { + attestations.push( + ...Array.from({length: state.epochCtx.getBeaconCommittee(state.slot, committeeIndex).length}, () => + generateAttestation(state, committeeIndex, false) + ) + ); + } + return attestations; +} export function generateIndexedAttestations( - state: CachedBeaconStateAltair, + state: CachedBeaconStateAllForks, count: number ): phase0.IndexedAttestation[] { const result: phase0.IndexedAttestation[] = []; @@ -19,24 +89,7 @@ export function generateIndexedAttestations( const committeeCount = state.epochCtx.getCommitteeCountPerSlot(epoch); for (let committeeIndex = 0; committeeIndex < committeeCount; committeeIndex++) { - result.push({ - attestingIndices: Array.from(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), - }); - + result.push(generateAttestation(state, committeeIndex, true)); if (result.length >= count) return result; } } @@ -44,7 +97,10 @@ export function generateIndexedAttestations( return result; } -export function generateBeaconBlockHeader(state: CachedBeaconStateAltair, count: number): phase0.BeaconBlockHeader[] { +export function generateBeaconBlockHeaders( + state: CachedBeaconStateAllForks, + count: number +): phase0.BeaconBlockHeader[] { const headers: phase0.BeaconBlockHeader[] = []; for (let i = 1; i <= count; i++) { @@ -67,28 +123,28 @@ export function generateBeaconBlockHeader(state: CachedBeaconStateAltair, count: return headers; } -export function generateSignedBeaconBlockHeader( - state: CachedBeaconStateAltair, +export function generateSignedBeaconBlockHeaders( + state: CachedBeaconStateAllForks, count: number ): phase0.SignedBeaconBlockHeader[] { - const headers = generateBeaconBlockHeader(state, count); - - return headers.map((header) => ({ - message: header, - signature: Buffer.alloc(96), - })); + return generateBeaconBlockHeaders(state, count).map(signContainer); } -export function generateVoluntaryExits(state: CachedBeaconStateAltair, count: number): phase0.SignedVoluntaryExit[] { +export function generateVoluntaryExits( + state: CachedBeaconStateAllForks, + count: number = MAX_VOLUNTARY_EXITS +): phase0.SignedVoluntaryExit[] { const result: phase0.SignedVoluntaryExit[] = []; + if (count > MAX_VOLUNTARY_EXITS) count = MAX_VOLUNTARY_EXITS; + for (const validatorIndex of state.epochCtx.proposers) { result.push({ message: { epoch: state.currentJustifiedCheckpoint.epoch, validatorIndex, }, - signature: Buffer.alloc(96), + signature: generateSignature(), }); if (result.length >= count) return result; @@ -96,3 +152,121 @@ export function generateVoluntaryExits(state: CachedBeaconStateAltair, count: nu return result; } + +export function generateAttesterSlashings(attestations: phase0.IndexedAttestation[]): phase0.AttesterSlashing[] { + const slashings: phase0.AttesterSlashing[] = []; + for (const attestation of attestations) { + slashings.push({ + attestation1: ssz.phase0.IndexedAttestationBigint.fromJson(ssz.phase0.IndexedAttestation.toJson(attestation)), + attestation2: ssz.phase0.IndexedAttestationBigint.fromJson(ssz.phase0.IndexedAttestation.toJson(attestation)), + }); + + if (slashings.length >= MAX_ATTESTER_SLASHINGS) { + return slashings; + } + } + return slashings; +} + +export function generateProposerSlashings(blockHeaders: phase0.SignedBeaconBlockHeader[]): phase0.ProposerSlashing[] { + const slashings: phase0.ProposerSlashing[] = []; + for (const blockHeader of blockHeaders) { + const signedHeader2 = ssz.phase0.SignedBeaconBlockHeaderBigint.fromJson( + ssz.phase0.SignedBeaconBlockHeader.toJson(blockHeader) + ); + signedHeader2.message.bodyRoot = crypto.randomBytes(32); + + slashings.push({ + signedHeader1: ssz.phase0.SignedBeaconBlockHeaderBigint.fromJson( + ssz.phase0.SignedBeaconBlockHeader.toJson(blockHeader) + ), + signedHeader2, + }); + + if (slashings.length >= MAX_PROPOSER_SLASHINGS) { + return slashings; + } + } + return slashings; +} + +export function generateDepositEvents(count: number = MAX_DEPOSITS): phase0.DepositEvent[] { + const deposits: phase0.DepositEvent[] = []; + if (count > MAX_DEPOSITS) count = MAX_DEPOSITS; + for (let i = 0; i < count; i++) { + deposits.push({ + blockNumber: 1, + index: 1, + depositData: { + pubkey: generateKey(), + amount: 32 * 10 ** 9, + withdrawalCredentials: Buffer.alloc(32, 0x77), + signature: generateSignature(), + }, + }); + } + return deposits; +} + +export function generateDeposits(state: CachedBeaconStateAllForks, count: number = MAX_DEPOSITS): phase0.Deposit[] { + const depositEvents = generateDepositEvents(count); + // TODO: (@matthewkeil) how do you set the deposit root as root node? + const depositRootTree = ssz.phase0.DepositDataRootList.toViewDU([state.eth1Data.depositRoot]); + return getDepositsWithProofs(depositEvents, depositRootTree, state.eth1Data); +} + +export interface BlockGenerationOptionsPhase0 { + numAttesterSlashings?: number; + numProposerSlashings?: number; + numVoluntaryExits?: number; + numDeposits?: number; +} + +export function generatePhase0BeaconBlocks( + state: CachedBeaconStateAllForks, + count: number, + opts?: BlockGenerationOptionsPhase0 +): phase0.BeaconBlock[] { + const headers = generateBeaconBlockHeaders(state, count); + const attesterSlashings: phase0.AttesterSlashing[] = []; + const proposerSlashings: phase0.ProposerSlashing[] = []; + + if (opts?.numProposerSlashings !== undefined) { + if (opts.numProposerSlashings > headers.length) { + opts.numProposerSlashings = headers.length; + } + proposerSlashings.push( + ...generateProposerSlashings(headers.slice(0, opts.numProposerSlashings).map(signContainer)) + ); + } + + if (opts?.numAttesterSlashings !== undefined) { + const indexedAttestations = generateIndexedAttestations(state, opts.numAttesterSlashings); + attesterSlashings.push(...generateAttesterSlashings(indexedAttestations)); + } + + const blocks: phase0.BeaconBlock[] = []; + for (const header of headers) { + // @ts-expect-error can delete + delete header.bodyRoot; + const block: phase0.BeaconBlock = { + ...header, + body: { + eth1Data: { + blockHash: state.eth1Data.blockHash, + depositCount: state.eth1Data.depositCount, + depositRoot: state.eth1Data.depositRoot, + }, + graffiti: Uint8Array.from(Buffer.from(getDefaultGraffiti(getLodestarClientVersion(), null, {}), "utf8")), + randaoReveal: getRandaoMix(state, state.epochCtx.epoch), + attestations: generateAttestations(state), + attesterSlashings, + deposits: generateDeposits(state, opts?.numDeposits), + proposerSlashings, + voluntaryExits: generateVoluntaryExits(state, opts?.numVoluntaryExits), + }, + }; + blocks.push(block); + } + return blocks; +} diff --git a/packages/beacon-node/test/fixtures/utils.ts b/packages/beacon-node/test/fixtures/utils.ts new file mode 100644 index 000000000000..33b182692579 --- /dev/null +++ b/packages/beacon-node/test/fixtures/utils.ts @@ -0,0 +1,18 @@ +export function generateKey(): Buffer { + return Buffer.alloc(48, 0xaa); +} + +export function generateSignature(): Buffer { + return Buffer.alloc(96, 0xaa); +} + +export interface SignedContainer { + message: T; + signature: Uint8Array; +} +export function signContainer(container: T): SignedContainer { + return { + message: container, + signature: generateSignature(), + }; +} diff --git a/packages/beacon-node/test/mocks/block.ts b/packages/beacon-node/test/mocks/block.ts new file mode 100644 index 000000000000..d44b973cc4cd --- /dev/null +++ b/packages/beacon-node/test/mocks/block.ts @@ -0,0 +1,95 @@ +import fs from "node:fs"; +import {ssz, SignedBlindedBeaconBlock, SignedBeaconBlock} from "@lodestar/types"; +import {ForkInfo, createChainForkConfig, defaultChainConfig} from "@lodestar/config"; +import {mainnetChainConfig} from "@lodestar/config/configs"; + +const fixturesDirectory = "./__fixtures__/"; + +/* eslint-disable @typescript-eslint/naming-convention */ +// export this chainConfig for use in tests that consume the mock blocks +// +// slots / epoch is 8 vs 32 so need to make epoch transition 4 times larger to match slot numbers in mocks +// that were taken from mainnet +export const chainConfig = createChainForkConfig({ + ...defaultChainConfig, + ALTAIR_FORK_EPOCH: mainnetChainConfig.ALTAIR_FORK_EPOCH * 4, + BELLATRIX_FORK_EPOCH: mainnetChainConfig.BELLATRIX_FORK_EPOCH * 4, + CAPELLA_FORK_EPOCH: mainnetChainConfig.CAPELLA_FORK_EPOCH * 4, + DENEB_FORK_EPOCH: mainnetChainConfig.DENEB_FORK_EPOCH * 4, +}); +/* eslint-enable @typescript-eslint/naming-convention */ + +const loadSerialized = (filename: string): Buffer => + fs.readFileSync(new URL(fixturesDirectory.concat(filename), import.meta.url)); + +// NOTE: these mocks were slightly modified so that they would serialize/deserialize with LODESTAR_PRESET=minimal +// and in particular the sync_committee_bits were shortened to match the minimal preset. All other conversion is handled +// via the slots/epoch adjustment above. +export const phase0SerializedSignedBeaconBlock = loadSerialized("block.phase0.ssz"); +export const altairSerializedSignedBeaconBlock = loadSerialized("block.altair.ssz"); +export const bellatrixSerializedSignedBeaconBlock = loadSerialized("block.bellatrix.ssz"); +export const capellaSerializedSignedBeaconBlock = loadSerialized("block.capella.ssz"); +export const denebSerializedSignedBeaconBlock = loadSerialized("block.deneb.ssz"); +export const bellatrixSerializedSignedBlindedBeaconBlock = loadSerialized("blindedBlock.bellatrix.ssz"); +export const capellaSerializedSignedBlindedBeaconBlock = loadSerialized("blindedBlock.capella.ssz"); +export const denebSerializedSignedBlindedBeaconBlock = loadSerialized("blindedBlock.deneb.ssz"); + +export const phase0SignedBeaconBlock = ssz.phase0.SignedBeaconBlock.deserialize(phase0SerializedSignedBeaconBlock); +export const altairSignedBeaconBlock = ssz.altair.SignedBeaconBlock.deserialize(altairSerializedSignedBeaconBlock); +export const bellatrixSignedBeaconBlock = ssz.bellatrix.SignedBeaconBlock.deserialize( + bellatrixSerializedSignedBeaconBlock +); +export const capellaSignedBeaconBlock = ssz.capella.SignedBeaconBlock.deserialize(capellaSerializedSignedBeaconBlock); +export const denebSignedBeaconBlock = ssz.deneb.SignedBeaconBlock.deserialize(denebSerializedSignedBeaconBlock); + +export const bellatrixSignedBlindedBeaconBlock = ssz.bellatrix.SignedBlindedBeaconBlock.deserialize( + bellatrixSerializedSignedBlindedBeaconBlock +); +export const capellaSignedBlindedBeaconBlock = ssz.capella.SignedBlindedBeaconBlock.deserialize( + capellaSerializedSignedBlindedBeaconBlock +); +export const denebSignedBlindedBeaconBlock = ssz.deneb.SignedBlindedBeaconBlock.deserialize( + denebSerializedSignedBlindedBeaconBlock +); + +interface MockBlock { + forkInfo: ForkInfo; + full: SignedBeaconBlock; + fullSerialized: Uint8Array; + blinded?: SignedBlindedBeaconBlock; + blindedSerialized?: Uint8Array; +} + +export const mockBlocks: MockBlock[] = [ + { + forkInfo: chainConfig.getForkInfo(phase0SignedBeaconBlock.message.slot), + full: phase0SignedBeaconBlock, + fullSerialized: phase0SerializedSignedBeaconBlock, + }, + { + forkInfo: chainConfig.getForkInfo(altairSignedBeaconBlock.message.slot), + full: altairSignedBeaconBlock, + fullSerialized: altairSerializedSignedBeaconBlock, + }, + { + forkInfo: chainConfig.getForkInfo(bellatrixSignedBeaconBlock.message.slot), + full: bellatrixSignedBeaconBlock, + fullSerialized: bellatrixSerializedSignedBeaconBlock, + blinded: bellatrixSignedBlindedBeaconBlock, + blindedSerialized: bellatrixSerializedSignedBlindedBeaconBlock, + }, + { + forkInfo: chainConfig.getForkInfo(capellaSignedBeaconBlock.message.slot), + full: capellaSignedBeaconBlock, + fullSerialized: capellaSerializedSignedBeaconBlock, + blinded: capellaSignedBlindedBeaconBlock, + blindedSerialized: capellaSerializedSignedBlindedBeaconBlock, + }, + { + forkInfo: chainConfig.getForkInfo(denebSignedBeaconBlock.message.slot), + full: denebSignedBeaconBlock, + fullSerialized: denebSerializedSignedBeaconBlock, + blinded: denebSignedBlindedBeaconBlock, + blindedSerialized: denebSerializedSignedBlindedBeaconBlock, + }, +]; diff --git a/packages/beacon-node/test/perf/chain/opPools/opPool.test.ts b/packages/beacon-node/test/perf/chain/opPools/opPool.test.ts index 6e420f0e1011..c5016171b289 100644 --- a/packages/beacon-node/test/perf/chain/opPools/opPool.test.ts +++ b/packages/beacon-node/test/perf/chain/opPools/opPool.test.ts @@ -13,7 +13,7 @@ import {OpPool} from "../../../../src/chain/opPools/opPool.js"; import {generateBlsToExecutionChanges} from "../../../fixtures/capella.js"; import { generateIndexedAttestations, - generateSignedBeaconBlockHeader, + generateSignedBeaconBlockHeaders, generateVoluntaryExits, } from "../../../fixtures/phase0.js"; import {BlockType} from "../../../../src/chain/interface.js"; @@ -74,7 +74,7 @@ function fillAttesterSlashing(pool: OpPool, state: CachedBeaconStateAltair, coun } function fillProposerSlashing(pool: OpPool, state: CachedBeaconStateAltair, count: number): OpPool { - for (const blockHeader of generateSignedBeaconBlockHeader(state, count)) { + for (const blockHeader of generateSignedBeaconBlockHeaders(state, count)) { pool.insertProposerSlashing({ signedHeader1: ssz.phase0.SignedBeaconBlockHeaderBigint.fromJson( ssz.phase0.SignedBeaconBlockHeader.toJson(blockHeader) diff --git a/packages/beacon-node/test/unit/util/fullOrBlindedBlock.test.ts b/packages/beacon-node/test/unit/util/fullOrBlindedBlock.test.ts new file mode 100644 index 000000000000..f8b62af8abeb --- /dev/null +++ b/packages/beacon-node/test/unit/util/fullOrBlindedBlock.test.ts @@ -0,0 +1,77 @@ +import {describe, it, expect} from "vitest"; +import {ForkInfo} from "@lodestar/config"; +import {SignedBlindedBeaconBlock, SignedBeaconBlock} from "@lodestar/types"; +import {ForkWithdrawals, isForkExecution} from "@lodestar/params"; +import { + blindedOrFullBlockToFull, + deserializeFullOrBlindedSignedBeaconBlock, + isBlindedBytes, + serializeFullOrBlindedSignedBeaconBlock, +} from "../../../src/util/fullOrBlindedBlock.js"; +import {chainConfig, mockBlocks} from "../../mocks/block.js"; +import {byteArrayEquals} from "../../../src/util/bytes.js"; + +type FullOrBlind = "full" | "blinded"; +type FullOrBlindBlock = [FullOrBlind, ForkInfo, SignedBlindedBeaconBlock | SignedBeaconBlock, Uint8Array]; + +const fullOrBlindedBlocks = Object.values(mockBlocks) + .map(({forkInfo, full, fullSerialized, blinded, blindedSerialized}) => { + const fullOrBlindBlock: FullOrBlindBlock[] = [["full", forkInfo, full, fullSerialized]]; + if (blinded && blindedSerialized) { + fullOrBlindBlock.push(["blinded", forkInfo, blinded, blindedSerialized]); + } + return fullOrBlindBlock; + }) + .flat(); + +describe("isBlindedBytes", () => { + for (const [fullOrBlinded, {seq, name}, , block] of fullOrBlindedBlocks) { + it(`should return ${fullOrBlinded === "blinded"} for ${name} ${fullOrBlinded} blocks`, () => { + expect(isBlindedBytes(seq, block)).toEqual(isForkExecution(name) && fullOrBlinded === "blinded"); + }); + } +}); + +describe("serializeFullOrBlindedSignedBeaconBlock", () => { + for (const [fullOrBlinded, {name}, block, expected] of fullOrBlindedBlocks) { + it(`should serialize ${name} ${fullOrBlinded} block`, () => { + const serialized = serializeFullOrBlindedSignedBeaconBlock(chainConfig, block); + expect(byteArrayEquals(serialized, expected)).toBeTruthy(); + }); + } +}); + +describe("deserializeFullOrBlindedSignedBeaconBlock", () => { + for (const [fullOrBlinded, {name}, block, serialized] of fullOrBlindedBlocks) { + it(`should deserialize ${name} ${fullOrBlinded} block`, () => { + const deserialized = deserializeFullOrBlindedSignedBeaconBlock(chainConfig, serialized); + const type = + isForkExecution(name) && fullOrBlinded === "blinded" + ? chainConfig.getExecutionForkTypes(block.message.slot).SignedBlindedBeaconBlock + : chainConfig.getForkTypes(block.message.slot).SignedBeaconBlock; + expect(type.equals(deserialized as any, block as any)).toBeTruthy(); + }); + } +}); + +describe("blindedOrFullBlockToFull", function () { + for (const { + forkInfo: {name}, + full, + blinded, + } of mockBlocks) { + const transactionsAndWithdrawals = { + transactions: (full as SignedBeaconBlock).message.body.executionPayload?.transactions ?? [], + withdrawals: (full as SignedBeaconBlock).message.body.executionPayload?.withdrawals ?? [], + }; + it(`should convert ${name} full to full block`, () => { + const result = blindedOrFullBlockToFull(chainConfig, full, transactionsAndWithdrawals); + expect(chainConfig.getForkTypes(full.message.slot).SignedBeaconBlock.equals(result, full)).toBeTruthy(); + }); + if (!blinded) continue; + it(`should convert ${name} blinded to full block`, () => { + const result = blindedOrFullBlockToFull(chainConfig, blinded, transactionsAndWithdrawals); + expect(chainConfig.getForkTypes(full.message.slot).SignedBeaconBlock.equals(result, full)).toBeTruthy(); + }); + } +}); diff --git a/packages/cli/test/utils/crucible/assertions/defaults/headAssertion.ts b/packages/cli/test/utils/crucible/assertions/defaults/headAssertion.ts index 6464067d5d7d..709c5612e284 100644 --- a/packages/cli/test/utils/crucible/assertions/defaults/headAssertion.ts +++ b/packages/cli/test/utils/crucible/assertions/defaults/headAssertion.ts @@ -46,7 +46,16 @@ export const headAssertion: Assertion<"head", HeadSummary> = { */ const result = [`Slot,${nodes.map((n) => n.beacon.id).join(", ")}`]; for (let s = 1; s <= slot; s++) { - result.push(`${s}, ${nodes.map((n) => store[n.beacon.id][s].blockRoot ?? "-").join(",")}`); + result.push( + `${s}, ${nodes + .map((n) => { + const nodeStore = store[n.beacon.id]; + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + const desiredSlot = nodeStore ? nodeStore[s] : undefined; + return desiredSlot ? desiredSlot.blockRoot : "not found"; + }) + .join(",")}` + ); } return {"headAssertion.csv": result.join("\n")}; }, diff --git a/packages/cli/test/utils/crucible/utils/index.ts b/packages/cli/test/utils/crucible/utils/index.ts index 0b6ae1bba1e5..73faa549fc9f 100644 --- a/packages/cli/test/utils/crucible/utils/index.ts +++ b/packages/cli/test/utils/crucible/utils/index.ts @@ -62,7 +62,7 @@ export function defineSimTestConfig( secondsPerSlot: SIM_TESTS_SECONDS_PER_SLOT, runTill: opts.runTillEpoch, // After adding Nethermind its took longer to complete - graceExtraTimeFraction: 0.3, + graceExtraTimeFraction: 0.8, }) * 1000; const ttd = getEstimatedTTD({ diff --git a/packages/state-transition/src/signatureSets/proposer.ts b/packages/state-transition/src/signatureSets/proposer.ts index e5ae7fd1f6f1..24e22c608a60 100644 --- a/packages/state-transition/src/signatureSets/proposer.ts +++ b/packages/state-transition/src/signatureSets/proposer.ts @@ -1,6 +1,6 @@ import {DOMAIN_BEACON_PROPOSER} from "@lodestar/params"; -import {SignedBeaconBlock, SignedBlindedBeaconBlock, isBlindedBeaconBlock, phase0, ssz} from "@lodestar/types"; -import {computeSigningRoot} from "../util/index.js"; +import {SignedBeaconBlock, SignedBlindedBeaconBlock, phase0, ssz} from "@lodestar/types"; +import {computeSigningRoot, isBlindedBlock} from "../util/index.js"; import {ISignatureSet, SignatureSetType, verifySignatureSet} from "../util/signatureSets.js"; import {CachedBeaconStateAllForks} from "../types.js"; @@ -19,7 +19,7 @@ export function getBlockProposerSignatureSet( const {config, epochCtx} = state; const domain = config.getDomain(state.slot, DOMAIN_BEACON_PROPOSER, signedBlock.message.slot); - const blockType = isBlindedBeaconBlock(signedBlock.message) + const blockType = isBlindedBlock(signedBlock.message) ? config.getExecutionForkTypes(signedBlock.message.slot).BlindedBeaconBlock : config.getForkTypes(signedBlock.message.slot).BeaconBlock; diff --git a/packages/state-transition/src/util/blindedBlock.ts b/packages/state-transition/src/util/blindedBlock.ts deleted file mode 100644 index 2e4e4d590817..000000000000 --- a/packages/state-transition/src/util/blindedBlock.ts +++ /dev/null @@ -1,126 +0,0 @@ -import {ChainForkConfig} from "@lodestar/config"; -import {ForkExecution, ForkSeq} from "@lodestar/params"; -import { - Root, - isBlindedBeaconBlock, - isExecutionPayloadAndBlobsBundle, - BeaconBlock, - BeaconBlockHeader, - SignedBeaconBlock, - ExecutionPayload, - ExecutionPayloadAndBlobsBundle, - BlobsBundle, - SignedBeaconBlockOrContents, - Contents, - SignedBlindedBeaconBlock, - BlindedBeaconBlock, - ExecutionPayloadHeader, -} from "@lodestar/types"; - -import {executionPayloadToPayloadHeader} from "./execution.js"; - -export function blindedOrFullBlockHashTreeRoot( - config: ChainForkConfig, - blindedOrFull: BeaconBlock | BlindedBeaconBlock -): Root { - return isBlindedBeaconBlock(blindedOrFull) - ? // Blinded - config.getExecutionForkTypes(blindedOrFull.slot).BlindedBeaconBlock.hashTreeRoot(blindedOrFull) - : // Full - config.getForkTypes(blindedOrFull.slot).BeaconBlock.hashTreeRoot(blindedOrFull); -} - -export function blindedOrFullBlockToHeader( - config: ChainForkConfig, - blindedOrFull: BeaconBlock | BlindedBeaconBlock -): BeaconBlockHeader { - const bodyRoot = isBlindedBeaconBlock(blindedOrFull) - ? // Blinded - config.getExecutionForkTypes(blindedOrFull.slot).BlindedBeaconBlockBody.hashTreeRoot(blindedOrFull.body) - : // Full - config.getForkTypes(blindedOrFull.slot).BeaconBlockBody.hashTreeRoot(blindedOrFull.body); - - return { - slot: blindedOrFull.slot, - proposerIndex: blindedOrFull.proposerIndex, - parentRoot: blindedOrFull.parentRoot, - stateRoot: blindedOrFull.stateRoot, - bodyRoot, - }; -} - -export function beaconBlockToBlinded(config: ChainForkConfig, block: BeaconBlock): BlindedBeaconBlock { - const fork = config.getForkName(block.slot); - const executionPayloadHeader = executionPayloadToPayloadHeader(ForkSeq[fork], block.body.executionPayload); - const blindedBlock: BlindedBeaconBlock = {...block, body: {...block.body, executionPayloadHeader}}; - return blindedBlock; -} - -export function signedBeaconBlockToBlinded( - config: ChainForkConfig, - signedBlock: SignedBeaconBlock -): SignedBlindedBeaconBlock { - return { - message: beaconBlockToBlinded(config, signedBlock.message), - signature: signedBlock.signature, - }; -} - -export function signedBlindedBlockToFull( - signedBlindedBlock: SignedBlindedBeaconBlock, - executionPayload: ExecutionPayload | null -): SignedBeaconBlock { - const signedBlock = { - ...signedBlindedBlock, - message: { - ...signedBlindedBlock.message, - body: { - ...signedBlindedBlock.message.body, - // state transition doesn't handle null value for executionPayload in pre-bellatrix blocks - executionPayload: executionPayload ?? undefined, - }, - }, - } as SignedBeaconBlock; - - // state transition can't seem to handle executionPayloadHeader presense in merge block - // so just delete the extra field we don't require - delete (signedBlock.message.body as {executionPayloadHeader?: ExecutionPayloadHeader}).executionPayloadHeader; - return signedBlock; -} - -export function parseExecutionPayloadAndBlobsBundle(data: ExecutionPayload | ExecutionPayloadAndBlobsBundle): { - executionPayload: ExecutionPayload; - blobsBundle: BlobsBundle | null; -} { - if (isExecutionPayloadAndBlobsBundle(data)) { - return data; - } else { - return { - executionPayload: data, - blobsBundle: null, - }; - } -} - -export function reconstructFullBlockOrContents( - signedBlindedBlock: SignedBlindedBeaconBlock, - { - executionPayload, - contents, - }: { - executionPayload: ExecutionPayload | null; - contents: Contents | null; - } -): SignedBeaconBlockOrContents { - const signedBlock = signedBlindedBlockToFull(signedBlindedBlock, executionPayload); - - if (contents !== null) { - if (executionPayload === null) { - throw Error("Missing locally produced executionPayload for deneb+ publishBlindedBlock"); - } - - return {signedBlock, ...contents} as SignedBeaconBlockOrContents; - } else { - return signedBlock as SignedBeaconBlockOrContents; - } -} diff --git a/packages/state-transition/src/util/blockRoot.ts b/packages/state-transition/src/util/blockRoot.ts index 54d96885e675..f4143467396c 100644 --- a/packages/state-transition/src/util/blockRoot.ts +++ b/packages/state-transition/src/util/blockRoot.ts @@ -6,12 +6,14 @@ import { SignedBeaconBlock, BeaconBlockHeader, SignedBeaconBlockHeader, + BlindedBeaconBlock, } from "@lodestar/types"; import {ChainForkConfig} from "@lodestar/config"; import {SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params"; import {ZERO_HASH} from "../constants/index.js"; import {BeaconStateAllForks} from "../types.js"; import {computeStartSlotAtEpoch} from "./epoch.js"; +import {blindedOrFullBlockBodyHashTreeRoot} from "./fullOrBlindedBlock.js"; /** * Return the block root at a recent [[slot]]. @@ -47,15 +49,16 @@ export function getTemporaryBlockHeader(config: ChainForkConfig, block: BeaconBl } /** - * Receives a BeaconBlock, and produces the corresponding BeaconBlockHeader. + * Receives a FullOrBlindedBeaconBlock, and produces the corresponding BeaconBlockHeader. */ -export function blockToHeader(config: ChainForkConfig, block: BeaconBlock): BeaconBlockHeader { +export function blockToHeader(config: ChainForkConfig, block: BeaconBlock | BlindedBeaconBlock): BeaconBlockHeader { + const bodyRoot = blindedOrFullBlockBodyHashTreeRoot(config, block); return { stateRoot: block.stateRoot, proposerIndex: block.proposerIndex, slot: block.slot, parentRoot: block.parentRoot, - bodyRoot: config.getForkTypes(block.slot).BeaconBlockBody.hashTreeRoot(block.body), + bodyRoot, }; } diff --git a/packages/state-transition/src/util/fullOrBlindedBlock.ts b/packages/state-transition/src/util/fullOrBlindedBlock.ts new file mode 100644 index 000000000000..241d5bf5b225 --- /dev/null +++ b/packages/state-transition/src/util/fullOrBlindedBlock.ts @@ -0,0 +1,200 @@ +/* eslint-disable @typescript-eslint/strict-boolean-expressions */ +import {ChainForkConfig} from "@lodestar/config"; +import {ForkName, ForkSeq} from "@lodestar/params"; +import { + Root, + isExecutionPayloadAndBlobsBundle, + BeaconBlock, + BeaconBlockHeader, + SignedBeaconBlock, + ExecutionPayload, + ExecutionPayloadAndBlobsBundle, + BlobsBundle, + SignedBeaconBlockOrContents, + Contents, + SignedBlindedBeaconBlock, + BlindedBeaconBlock, + ExecutionPayloadHeader, + isBlindedSignedBeaconBlock, +} from "@lodestar/types"; + +import {executionPayloadToPayloadHeader} from "./execution.js"; + +export function isSignedBlock( + block: BeaconBlock | BlindedBeaconBlock | SignedBeaconBlock | SignedBlindedBeaconBlock +): block is SignedBeaconBlock | SignedBlindedBeaconBlock { + return !!(block as SignedBeaconBlock).signature; +} + +export function isBlindedBlock( + block: BeaconBlock | BlindedBeaconBlock | SignedBeaconBlock | SignedBlindedBeaconBlock +): block is BlindedBeaconBlock | SignedBlindedBeaconBlock { + if (isSignedBlock(block)) { + return !!(block as SignedBlindedBeaconBlock).message.body.executionPayloadHeader; + } + return !!(block as BlindedBeaconBlock).body.executionPayloadHeader; +} + +export function blindedOrFullBlockBodyHashTreeRoot( + config: ChainForkConfig, + blindedOrFull: BeaconBlock | BlindedBeaconBlock +): Root { + return isBlindedBlock(blindedOrFull) + ? // Blinded + config.getExecutionForkTypes(blindedOrFull.slot).BlindedBeaconBlockBody.hashTreeRoot(blindedOrFull.body) + : // Full + config.getForkTypes(blindedOrFull.slot).BeaconBlockBody.hashTreeRoot(blindedOrFull.body); +} + +export function blindedOrFullBlockHashTreeRoot( + config: ChainForkConfig, + blindedOrFull: BeaconBlock | BlindedBeaconBlock +): Root { + return isBlindedBlock(blindedOrFull) + ? // Blinded + config.getExecutionForkTypes(blindedOrFull.slot).BlindedBeaconBlock.hashTreeRoot(blindedOrFull) + : // Full + config.getForkTypes(blindedOrFull.slot).BeaconBlock.hashTreeRoot(blindedOrFull); +} + +export function blindedOrFullSignedBlockHashTreeRoot( + config: ChainForkConfig, + blindedOrFull: SignedBeaconBlock | SignedBlindedBeaconBlock +): Root { + return isBlindedSignedBeaconBlock(blindedOrFull) + ? // Blinded + config.getExecutionForkTypes(blindedOrFull.message.slot).SignedBlindedBeaconBlock.hashTreeRoot(blindedOrFull) + : // Full + config.getForkTypes(blindedOrFull.message.slot).SignedBeaconBlock.hashTreeRoot(blindedOrFull); +} + +export function blindedOrFullBlockToHeader( + config: ChainForkConfig, + blindedOrFull: BeaconBlock | BlindedBeaconBlock +): BeaconBlockHeader { + const bodyRoot = isBlindedBlock(blindedOrFull) + ? // Blinded + config.getExecutionForkTypes(blindedOrFull.slot).BlindedBeaconBlockBody.hashTreeRoot(blindedOrFull.body) + : // Full + config.getForkTypes(blindedOrFull.slot).BeaconBlockBody.hashTreeRoot(blindedOrFull.body); + + return { + slot: blindedOrFull.slot, + proposerIndex: blindedOrFull.proposerIndex, + parentRoot: blindedOrFull.parentRoot, + stateRoot: blindedOrFull.stateRoot, + bodyRoot, + }; +} + +export function fullOrBlindedBlockToBlinded( + config: ChainForkConfig, + block: BeaconBlock | BlindedBeaconBlock +): BlindedBeaconBlock { + const forkSeq = config.getForkSeq(block.slot); + if (isBlindedBlock(block) || forkSeq < ForkSeq.bellatrix) { + return block as BlindedBeaconBlock; + } + const blinded: BlindedBeaconBlock = { + ...block, + body: { + randaoReveal: block.body.randaoReveal, + eth1Data: block.body.eth1Data, + graffiti: block.body.graffiti, + proposerSlashings: block.body.proposerSlashings, + attesterSlashings: block.body.attesterSlashings, + attestations: block.body.attestations, + deposits: block.body.deposits, + voluntaryExits: block.body.voluntaryExits, + syncAggregate: (block as BeaconBlock).body.syncAggregate, + executionPayloadHeader: executionPayloadToPayloadHeader( + forkSeq, + (block as BeaconBlock).body.executionPayload + ), + }, + }; + + if (forkSeq >= ForkSeq.capella) { + (blinded as BlindedBeaconBlock).body.blsToExecutionChanges = ( + block as BeaconBlock + ).body.blsToExecutionChanges; + } + + if (forkSeq >= ForkSeq.deneb) { + (blinded as BlindedBeaconBlock).body.blobKzgCommitments = ( + block as BeaconBlock + ).body.blobKzgCommitments; + } + + return blinded; +} + +export function fullOrBlindedSignedBlockToBlinded( + config: ChainForkConfig, + signedBlock: SignedBeaconBlock | SignedBlindedBeaconBlock +): SignedBlindedBeaconBlock { + return { + message: fullOrBlindedBlockToBlinded(config, signedBlock.message), + signature: signedBlock.signature, + }; +} + +// TODO: (@matthewkeil) not the same as blindedOrFullBlockToFull in beacon-node. consider merging? +export function signedBlindedBlockToFull( + signedBlindedBlock: SignedBlindedBeaconBlock, + executionPayload: ExecutionPayload | null +): SignedBeaconBlock { + const signedBlock = { + ...signedBlindedBlock, + message: { + ...signedBlindedBlock.message, + body: { + ...signedBlindedBlock.message.body, + // state transition doesn't handle null value for executionPayload in pre-bellatrix blocks + executionPayload: executionPayload ?? undefined, + }, + }, + } as SignedBeaconBlock; + + // state transition can't seem to handle executionPayloadHeader presense in merge block + // so just delete the extra field we don't require + delete (signedBlock.message.body as {executionPayloadHeader?: ExecutionPayloadHeader}).executionPayloadHeader; + return signedBlock; +} + +export function parseExecutionPayloadAndBlobsBundle(data: ExecutionPayload | ExecutionPayloadAndBlobsBundle): { + executionPayload: ExecutionPayload; + blobsBundle: BlobsBundle | null; +} { + if (isExecutionPayloadAndBlobsBundle(data)) { + return data; + } else { + return { + executionPayload: data, + blobsBundle: null, + }; + } +} + +export function reconstructFullBlockOrContents( + signedBlindedBlock: SignedBlindedBeaconBlock, + { + executionPayload, + contents, + }: { + executionPayload: ExecutionPayload | null; + contents: Contents | null; + } +): SignedBeaconBlockOrContents { + const signedBlock = signedBlindedBlockToFull(signedBlindedBlock, executionPayload); + + if (contents !== null) { + if (executionPayload === null) { + throw Error("Missing locally produced executionPayload for deneb+ publishBlindedBlock"); + } + + return {signedBlock, ...contents} as SignedBeaconBlockOrContents; + } else { + return signedBlock as SignedBeaconBlockOrContents; + } +} diff --git a/packages/state-transition/src/util/index.ts b/packages/state-transition/src/util/index.ts index 9b9916f1d49e..90d18407c0a7 100644 --- a/packages/state-transition/src/util/index.ts +++ b/packages/state-transition/src/util/index.ts @@ -3,7 +3,7 @@ export * from "./array.js"; export * from "./attestation.js"; export * from "./attesterStatus.js"; export * from "./balance.js"; -export * from "./blindedBlock.js"; +export * from "./fullOrBlindedBlock.js"; export * from "./capella.js"; export * from "./execution.js"; export * from "./blockRoot.js";