diff --git a/.github/workflows/test-sim-merge.yml b/.github/workflows/test-sim-merge.yml index ad79bc2c0035..e6c00b4ebdb8 100644 --- a/.github/workflows/test-sim-merge.yml +++ b/.github/workflows/test-sim-merge.yml @@ -58,18 +58,30 @@ jobs: - name: Pull Nethermind run: docker pull $NETHERMIND_IMAGE - - name: Pull mergemock - run: docker pull $MERGEMOCK_IMAGE - - - name: Test Lodestar <> mergemock relay - run: yarn test:sim:mergemock + - name: Test Lodestar <> Nethermind interop + run: yarn test:sim:merge-interop working-directory: packages/beacon-node env: - EL_BINARY_DIR: ${{ env.MERGEMOCK_IMAGE }} - EL_SCRIPT_DIR: mergemock - LODESTAR_PRESET: mainnet + EL_BINARY_DIR: ${{ env.NETHERMIND_IMAGE }} + EL_SCRIPT_DIR: netherminddocker ENGINE_PORT: 8551 - ETH_PORT: 8661 + ETH_PORT: 8545 + + # This container is pre-shanghai and does not support enginer_getPayloadBodyV2 + # for blinding/unblinding. Re-enable when we have a newer build. + # + # - name: Pull mergemock + # run: docker pull $MERGEMOCK_IMAGE + + # - name: Test Lodestar <> mergemock relay + # run: yarn test:sim:mergemock + # working-directory: packages/beacon-node + # env: + # EL_BINARY_DIR: ${{ env.MERGEMOCK_IMAGE }} + # EL_SCRIPT_DIR: mergemock + # LODESTAR_PRESET: mainnet + # ENGINE_PORT: 8551 + # ETH_PORT: 8661 - name: Upload debug log test files if: ${{ always() }} 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 177f58aebb95..b72079238069 100644 --- a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts @@ -281,6 +281,7 @@ export function getBeaconBlockApi({ return { async getBlockHeaders({slot, parentRoot}) { + // TODO: (matthewkeil) Make this code BlindedOrFull block aware // TODO - SLOW CODE: This code seems like it could be improved // If one block in the response contains an optimistic block, mark the entire response as optimistic 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 f0d243967c22..2c73d40f31f0 100644 --- a/packages/beacon-node/src/api/impl/beacon/blocks/utils.ts +++ b/packages/beacon-node/src/api/impl/beacon/blocks/utils.ts @@ -1,23 +1,38 @@ import {routes} from "@lodestar/api"; -import {blockToHeader} from "@lodestar/state-transition"; import {ChainForkConfig} from "@lodestar/config"; import {SignedBeaconBlock} from "@lodestar/types"; import {GENESIS_SLOT} from "../../../../constants/index.js"; import {ApiError, ValidationError} from "../../errors.js"; import {IBeaconChain} from "../../../../chain/interface.js"; import {rootHexRegex} from "../../../../eth1/provider/utils.js"; +import {isBlinded} from "../../../../util/fullOrBlindedBlock.js"; export function toBeaconHeaderResponse( config: ChainForkConfig, block: SignedBeaconBlock, canonical = false ): routes.beacon.BlockHeaderResponse { + // need to have ts-ignore below to pull type here so it only happens once and + // gets used twice + const types = isBlinded(block) + ? config.getExecutionForkTypes(block.message.slot) + : config.getForkTypes(block.message.slot); return { - root: config.getForkTypes(block.message.slot).BeaconBlock.hashTreeRoot(block.message), + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + root: types.BeaconBlock.hashTreeRoot(block.message), canonical, header: { - message: blockToHeader(config, block.message), signature: block.signature, + message: { + stateRoot: block.message.stateRoot, + proposerIndex: block.message.proposerIndex, + slot: block.message.slot, + parentRoot: block.message.parentRoot, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + bodyRoot: types.BeaconBlockBody.hashTreeRoot(block.message.body), + }, }, }; } diff --git a/packages/beacon-node/src/chain/blocks/writeBlockInputToDb.ts b/packages/beacon-node/src/chain/blocks/writeBlockInputToDb.ts index b0f5ab159591..ade6fa1914b2 100644 --- a/packages/beacon-node/src/chain/blocks/writeBlockInputToDb.ts +++ b/packages/beacon-node/src/chain/blocks/writeBlockInputToDb.ts @@ -1,5 +1,6 @@ import {toHex} from "@lodestar/utils"; import {BeaconChain} from "../chain.js"; +import {blindedOrFullBlockToBlinded} from "../../util/fullOrBlindedBlock.js"; import {BlockInput, BlockInputType} from "./types.js"; /** @@ -13,17 +14,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 = toHex(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(blindedOrFullBlockToBlinded(this.config, 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 a6912d952b68..1faa43769c9d 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -29,6 +29,7 @@ import { isBlindedBeaconBlock, BeaconBlock, SignedBeaconBlock, + FullOrBlindedSignedBeaconBlock, ExecutionPayload, BlindedBeaconBlock, BlindedBeaconBlockBody, @@ -47,6 +48,14 @@ 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 { + blindedOrFullBlockToFull, + deserializeFullOrBlindedSignedBeaconBlock, + getEth1BlockHashFromSerializedBlock, + serializeFullOrBlindedSignedBeaconBlock, +} from "../util/fullOrBlindedBlock.js"; +import {ExecutionPayloadBody} from "../execution/engine/types.js"; +import {Eth1Error, Eth1ErrorCode} from "../eth1/errors.js"; import {BlockProcessor, ImportBlockOpts} from "./blocks/index.js"; import {ChainEventEmitter, ChainEvent} from "./emitter.js"; import { @@ -510,7 +519,11 @@ export class BeaconChain implements IBeaconChain { if (block) { const data = await this.db.block.get(fromHexString(block.blockRoot)); if (data) { - return {block: data, executionOptimistic: isOptimisticBlock(block), finalized: false}; + return { + block: await this.blindedOrFullBlockToFull(data), + executionOptimistic: isOptimisticBlock(block), + finalized: false, + }; } } // A non-finalized slot expected to be found in the hot db, could be archived during @@ -519,7 +532,7 @@ export class BeaconChain implements IBeaconChain { } const data = await this.db.blockArchive.get(slot); - return data && {block: data, executionOptimistic: false, finalized: true}; + return data && {block: await this.blindedOrFullBlockToFull(data), executionOptimistic: false, finalized: true}; } async getBlockByRoot( @@ -529,7 +542,11 @@ export class BeaconChain implements IBeaconChain { if (block) { const data = await this.db.block.get(fromHexString(root)); if (data) { - return {block: data, executionOptimistic: isOptimisticBlock(block), finalized: false}; + return { + block: await this.blindedOrFullBlockToFull(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 @@ -574,6 +591,28 @@ export class BeaconChain implements IBeaconChain { return this.produceBlockWrapper(BlockType.Blinded, blockAttributes); } + async blindedOrFullBlockToFull(block: FullOrBlindedSignedBeaconBlock): Promise { + const info = this.config.getForkInfo(block.message.slot); + return blindedOrFullBlockToFull( + this.config, + info.seq, + block, + await this.getTransactionsAndWithdrawals(info.seq, toHexString(block.message.body.eth1Data.blockHash)) + ); + } + + async blindedOrFullBlockToFullBytes(forkSeq: ForkSeq, block: Uint8Array): Promise { + return serializeFullOrBlindedSignedBeaconBlock( + this.config, + blindedOrFullBlockToFull( + this.config, + forkSeq, + deserializeFullOrBlindedSignedBeaconBlock(this.config, block), + await this.getTransactionsAndWithdrawals(forkSeq, toHexString(getEth1BlockHashFromSerializedBlock(block))) + ) + ); + } + async produceBlockWrapper( blockType: T, { @@ -803,6 +842,23 @@ export class BeaconChain implements IBeaconChain { } } + private async getTransactionsAndWithdrawals( + forkSeq: ForkSeq, + blockHash: string + ): Promise> { + if (forkSeq < ForkSeq.bellatrix) { + return {}; + } + const [payload] = await this.executionEngine.getPayloadBodiesByHash([blockHash]); + if (!payload) { + throw new Eth1Error( + {code: Eth1ErrorCode.INVALID_PAYLOAD_BODY, blockHash}, + "payload body not found by eth1 engine" + ); + } + return payload; + } + /** * Regenerate state for attestation verification, this does not happen with default chain option of maxSkipSlots = 32 . * However, need to handle just in case. Lodestar doesn't support multiple regen state requests for attestation verification diff --git a/packages/beacon-node/src/chain/interface.ts b/packages/beacon-node/src/chain/interface.ts index e412d8e8aafa..f7ca72f34731 100644 --- a/packages/beacon-node/src/chain/interface.ts +++ b/packages/beacon-node/src/chain/interface.ts @@ -14,6 +14,7 @@ import { BeaconBlock, ExecutionPayload, SignedBeaconBlock, + FullOrBlindedSignedBeaconBlock, BlindedBeaconBlock, } from "@lodestar/types"; import { @@ -26,6 +27,7 @@ import { import {BeaconConfig} from "@lodestar/config"; import {Logger} from "@lodestar/utils"; import {CheckpointWithHex, IForkChoice, ProtoBlock} from "@lodestar/fork-choice"; +import {ForkSeq} from "@lodestar/params"; import {IEth1ForBlockProduction} from "../eth1/index.js"; import {IExecutionEngine, IExecutionBuilder} from "../execution/index.js"; import {Metrics} from "../metrics/metrics.js"; @@ -187,6 +189,9 @@ export interface IBeaconChain { consensusBlockValue: Wei; }>; + blindedOrFullBlockToFull(block: FullOrBlindedSignedBeaconBlock): Promise; + blindedOrFullBlockToFullBytes(forkSeq: ForkSeq, block: Uint8Array): 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..c23ff24c93f2 100644 --- a/packages/beacon-node/src/db/repositories/block.ts +++ b/packages/beacon-node/src/db/repositories/block.ts @@ -1,6 +1,11 @@ import {ChainForkConfig} from "@lodestar/config"; import {Db, Repository} from "@lodestar/db"; -import {SignedBeaconBlock, ssz} from "@lodestar/types"; +import {ssz, FullOrBlindedSignedBeaconBlock} from "@lodestar/types"; +import {blindedOrFullBlockHashTreeRoot} from "@lodestar/state-transition"; +import { + deserializeFullOrBlindedSignedBeaconBlock, + serializeFullOrBlindedSignedBeaconBlock, +} from "../../util/fullOrBlindedBlock.js"; import {getSignedBlockTypeFromBytes} from "../../util/multifork.js"; import {Bucket, getBucketNameByValue} from "../buckets.js"; @@ -9,7 +14,7 @@ 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 @@ -19,15 +24,15 @@ export class BlockRepository extends Repository { /** * Id is hashTreeRoot of unsigned BeaconBlock */ - getId(value: SignedBeaconBlock): Uint8Array { - return this.config.getForkTypes(value.message.slot).BeaconBlock.hashTreeRoot(value.message); + getId(value: FullOrBlindedSignedBeaconBlock): 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: FullOrBlindedSignedBeaconBlock): Buffer { + return serializeFullOrBlindedSignedBeaconBlock(this.config, value) as Buffer; } - decodeValue(data: Buffer): SignedBeaconBlock { - return getSignedBlockTypeFromBytes(this.config, data).deserialize(data); + decodeValue(data: Buffer): FullOrBlindedSignedBeaconBlock { + return deserializeFullOrBlindedSignedBeaconBlock(this.config, data); } } diff --git a/packages/beacon-node/src/db/repositories/blockArchive.ts b/packages/beacon-node/src/db/repositories/blockArchive.ts index 15c07f552b21..17e23f75b478 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, FullOrBlindedSignedBeaconBlock} from "@lodestar/types"; import {bytesToInt} from "@lodestar/utils"; -import {getSignedBlockTypeFromBytes} from "../../util/multifork.js"; +import {blindedOrFullBlockHashTreeRoot} from "@lodestar/state-transition"; +import { + deserializeFullOrBlindedSignedBeaconBlock, + serializeFullOrBlindedSignedBeaconBlock, +} 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,7 +25,7 @@ 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 @@ -30,17 +34,17 @@ export class BlockArchiveRepository extends Repository // Overrides for multi-fork - encodeValue(value: SignedBeaconBlock): Uint8Array { - return this.config.getForkTypes(value.message.slot).SignedBeaconBlock.serialize(value); + encodeValue(value: FullOrBlindedSignedBeaconBlock): Uint8Array { + return serializeFullOrBlindedSignedBeaconBlock(this.config, value); } - decodeValue(data: Uint8Array): SignedBeaconBlock { - return getSignedBlockTypeFromBytes(this.config, data).deserialize(data); + decodeValue(data: Uint8Array): FullOrBlindedSignedBeaconBlock { + return deserializeFullOrBlindedSignedBeaconBlock(this.config, data); } // Handle key as slot - getId(value: SignedBeaconBlock): Slot { + getId(value: FullOrBlindedSignedBeaconBlock): Slot { return value.message.slot; } @@ -50,8 +54,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: FullOrBlindedSignedBeaconBlock): Promise { + const blockRoot = blindedOrFullBlockHashTreeRoot(this.config, value.message); const slot = value.message.slot; await Promise.all([ super.put(key, value), @@ -60,12 +64,12 @@ export class BlockArchiveRepository extends Repository ]); } - async batchPut(items: KeyValue[]): Promise { + async batchPut(items: KeyValue[]): Promise { await Promise.all([ super.batchPut(items), 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) => { @@ -84,7 +88,7 @@ export class BlockArchiveRepository extends Repository ]); } - async remove(value: SignedBeaconBlock): Promise { + async remove(value: FullOrBlindedSignedBeaconBlock): Promise { await Promise.all([ super.remove(value), deleteRootIndex(this.db, this.config.getForkTypes(value.message.slot).SignedBeaconBlock, value), @@ -92,7 +96,7 @@ export class BlockArchiveRepository extends Repository ]); } - async batchRemove(values: SignedBeaconBlock[]): Promise { + async batchRemove(values: FullOrBlindedSignedBeaconBlock[]): Promise { await Promise.all([ super.batchRemove(values), Array.from(values).map((value) => @@ -102,7 +106,7 @@ export class BlockArchiveRepository extends Repository ]); } - 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 +118,13 @@ export class BlockArchiveRepository extends Repository } } - async values(opts?: BlockFilterOptions): Promise { + async values(opts?: BlockFilterOptions): Promise { 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 +134,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..8bc22b9795af 100644 --- a/packages/beacon-node/src/db/repositories/blockArchiveIndex.ts +++ b/packages/beacon-node/src/db/repositories/blockArchiveIndex.ts @@ -1,5 +1,5 @@ import {Db, encodeKey} from "@lodestar/db"; -import {Slot, Root, ssz, SignedBeaconBlock, SSZTypesFor} from "@lodestar/types"; +import {Slot, Root, FullOrBlindedSignedBeaconBlock, SSZTypesFor} from "@lodestar/types"; import {intToBytes} from "@lodestar/utils"; import {ForkAll} from "@lodestar/params"; import {Bucket} from "../buckets.js"; @@ -15,13 +15,12 @@ export async function storeParentRootIndex(db: Db, slot: Slot, parentRoot: Root) export async function deleteRootIndex( db: Db, signedBeaconBlockType: SSZTypesFor, - block: SignedBeaconBlock + block: FullOrBlindedSignedBeaconBlock ): Promise { - const beaconBlockType = (signedBeaconBlockType as typeof ssz.phase0.SignedBeaconBlock).fields["message"]; - return db.delete(getRootIndexKey(beaconBlockType.hashTreeRoot(block.message))); + return db.delete(getRootIndexKey(signedBeaconBlockType.hashTreeRoot(block.message))); } -export async function deleteParentRootIndex(db: Db, block: SignedBeaconBlock): Promise { +export async function deleteParentRootIndex(db: Db, block: FullOrBlindedSignedBeaconBlock): 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/execution/engine/mock.ts b/packages/beacon-node/src/execution/engine/mock.ts index 5779713435a5..e0a0fa756e07 100644 --- a/packages/beacon-node/src/execution/engine/mock.ts +++ b/packages/beacon-node/src/execution/engine/mock.ts @@ -102,7 +102,7 @@ export class ExecutionEngineMockBackend implements JsonRpcBackend { private getPayloadBodiesByHash( _blockHex: EngineApiRpcParamTypes["engine_getPayloadBodiesByHashV1"][0] ): EngineApiRpcReturnTypes["engine_getPayloadBodiesByHashV1"] { - return [] as ExecutionPayloadBodyRpc[]; + return [{transactions: [], withdrawals: []}] as ExecutionPayloadBodyRpc[]; } private getPayloadBodiesByRange( diff --git a/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRange.ts b/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRange.ts index d1046db9651d..16efb8354985 100644 --- a/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRange.ts +++ b/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRange.ts @@ -23,9 +23,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 {name, seq} = chain.config.getForkInfo(finalized.decodeKey(key)); + yield { - data: value, - fork: chain.config.getForkName(finalized.decodeKey(key)), + data: await chain.blindedOrFullBlockToFullBytes(seq, value), + fork: name, }; } } @@ -54,9 +56,11 @@ export async function* onBeaconBlocksByRange( throw new ResponseError(RespStatus.SERVER_ERROR, `No item for root ${block.blockRoot} slot ${block.slot}`); } + const {name, seq} = chain.config.getForkInfo(block.slot); + yield { - data: blockBytes, - fork: chain.config.getForkName(block.slot), + data: await chain.blindedOrFullBlockToFullBytes(seq, blockBytes), + fork: name, }; } diff --git a/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRoot.ts b/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRoot.ts index 0ed0e6a2d185..bf3b928a5a3f 100644 --- a/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRoot.ts +++ b/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRoot.ts @@ -38,9 +38,11 @@ export async function* onBeaconBlocksByRoot( slot = slotFromBytes; } + const {name, seq} = chain.config.getForkInfo(slot); + yield { - data: blockBytes, - fork: chain.config.getForkName(slot), + data: await chain.blindedOrFullBlockToFullBytes(seq, blockBytes), + fork: name, }; } } diff --git a/packages/beacon-node/src/util/bytes.ts b/packages/beacon-node/src/util/bytes.ts index 358693d7c261..365c6171274b 100644 --- a/packages/beacon-node/src/util/bytes.ts +++ b/packages/beacon-node/src/util/bytes.ts @@ -9,3 +9,17 @@ export function byteArrayEquals(a: Uint8Array | Root, b: Uint8Array | Root): boo } return true; } + +export function byteArrayEqualsThrowBadIndexes(a: Uint8Array | Root, b: Uint8Array | Root): boolean { + if (a.length !== b.length) { + throw new Error(`byteArrayEquals: length mismatch: ${a.length} !== ${b.length}`); + } + const invalidBytes: number[] = []; + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) invalidBytes.push(i); + } + if (invalidBytes.length > 0) { + throw new Error(`byteArrayEquals: mismatch at indexes ${invalidBytes.join(", ")}`); + } + return true; +} diff --git a/packages/beacon-node/src/util/fullOrBlindedBlock.ts b/packages/beacon-node/src/util/fullOrBlindedBlock.ts new file mode 100644 index 000000000000..045cd851d418 --- /dev/null +++ b/packages/beacon-node/src/util/fullOrBlindedBlock.ts @@ -0,0 +1,273 @@ +import {ChainForkConfig} from "@lodestar/config"; +import { + bellatrix, + capella, + deneb, + ExecutionPayload, + ExecutionPayloadHeader, + SignedBeaconBlock, + SignedBlindedBeaconBlock, + FullOrBlindedSignedBeaconBlock, +} from "@lodestar/types"; +import {BYTES_PER_LOGS_BLOOM, ForkSeq, SYNC_COMMITTEE_SIZE} from "@lodestar/params"; +import {executionPayloadToPayloadHeader} from "@lodestar/state-transition"; +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; +} + +// same as isBlindedSignedBeaconBlock but without type narrowing +export function isBlinded(block: FullOrBlindedSignedBeaconBlock): block is SignedBlindedBeaconBlock { + return (block as bellatrix.SignedBlindedBeaconBlock).message.body.executionPayloadHeader !== undefined; +} + +export function serializeFullOrBlindedSignedBeaconBlock( + config: ChainForkConfig, + value: FullOrBlindedSignedBeaconBlock +): Uint8Array { + if (isBlinded(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 +): FullOrBlindedSignedBeaconBlock { + const slot = getSlotFromSignedBeaconBlockSerialized(bytes); + if (slot === null) { + throw Error("getSignedBlockTypeFromBytes: invalid bytes"); + } + + return isBlindedBytes(config.getForkSeq(slot), bytes) + ? config.getExecutionForkTypes(slot).SignedBeaconBlock.deserialize(bytes) + : config.getForkTypes(slot).SignedBeaconBlock.deserialize(bytes); +} + +export function blindedOrFullBlockToBlinded( + config: ChainForkConfig, + block: FullOrBlindedSignedBeaconBlock +): SignedBlindedBeaconBlock { + const forkSeq = config.getForkSeq(block.message.slot); + if (isBlinded(block) || forkSeq < ForkSeq.bellatrix) { + return block as SignedBlindedBeaconBlock; + } + + const blinded = { + signature: block.signature, + message: { + ...block.message, + body: { + randaoReveal: block.message.body.randaoReveal, + eth1Data: block.message.body.eth1Data, + graffiti: block.message.body.graffiti, + proposerSlashings: block.message.body.proposerSlashings, + attesterSlashings: block.message.body.attesterSlashings, + attestations: block.message.body.attestations, + deposits: block.message.body.deposits, + voluntaryExits: block.message.body.voluntaryExits, + syncAggregate: (block.message.body as bellatrix.BeaconBlockBody).syncAggregate, + executionPayloadHeader: executionPayloadToPayloadHeader( + forkSeq, + (block.message.body as deneb.BeaconBlockBody).executionPayload + ), + }, + }, + }; + + if (forkSeq >= ForkSeq.capella) { + (blinded as capella.SignedBlindedBeaconBlock).message.body.blsToExecutionChanges = ( + block as capella.SignedBeaconBlock + ).message.body.blsToExecutionChanges; + } + + if (forkSeq >= ForkSeq.deneb) { + (blinded as deneb.SignedBlindedBeaconBlock).message.body.blobKzgCommitments = ( + block as deneb.SignedBeaconBlock + ).message.body.blobKzgCommitments; + } + + return blinded; +} + +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; +} + +export function blindedOrFullBlockToFull( + config: ChainForkConfig, + forkSeq: ForkSeq, + block: FullOrBlindedSignedBeaconBlock, + transactionsAndWithdrawals: Partial +): SignedBeaconBlock { + if ( + !isBlinded(block) || // already full + forkSeq < ForkSeq.bellatrix || // no execution payload + (block.message as bellatrix.BeaconBlock).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( + forkSeq, + (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 802b9a266ab1..dfbff2edb4d7 100644 --- a/packages/beacon-node/src/util/sszBytes.ts +++ b/packages/beacon-node/src/util/sszBytes.ts @@ -18,9 +18,9 @@ export type AttDataBase64 = string; // source: Checkpoint - data 40 // target: Checkpoint - data 40 -const VARIABLE_FIELD_OFFSET = 4; +export 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; const SIGNATURE_SIZE = 96; diff --git a/packages/beacon-node/test/mocks/__fixtures__/blindedBlock.bellatrix.ssz b/packages/beacon-node/test/mocks/__fixtures__/blindedBlock.bellatrix.ssz new file mode 100644 index 000000000000..fe7d06c447a0 Binary files /dev/null and b/packages/beacon-node/test/mocks/__fixtures__/blindedBlock.bellatrix.ssz differ diff --git a/packages/beacon-node/test/mocks/__fixtures__/blindedBlock.capella.ssz b/packages/beacon-node/test/mocks/__fixtures__/blindedBlock.capella.ssz new file mode 100644 index 000000000000..d252f25a46a4 Binary files /dev/null and b/packages/beacon-node/test/mocks/__fixtures__/blindedBlock.capella.ssz differ diff --git a/packages/beacon-node/test/mocks/__fixtures__/blindedBlock.deneb.ssz b/packages/beacon-node/test/mocks/__fixtures__/blindedBlock.deneb.ssz new file mode 100644 index 000000000000..90b6aaff9a60 Binary files /dev/null and b/packages/beacon-node/test/mocks/__fixtures__/blindedBlock.deneb.ssz differ diff --git a/packages/beacon-node/test/mocks/__fixtures__/block.altair.ssz b/packages/beacon-node/test/mocks/__fixtures__/block.altair.ssz new file mode 100644 index 000000000000..94ac6248be2f Binary files /dev/null and b/packages/beacon-node/test/mocks/__fixtures__/block.altair.ssz differ diff --git a/packages/beacon-node/test/mocks/__fixtures__/block.bellatrix.ssz b/packages/beacon-node/test/mocks/__fixtures__/block.bellatrix.ssz new file mode 100644 index 000000000000..272691ded36f Binary files /dev/null and b/packages/beacon-node/test/mocks/__fixtures__/block.bellatrix.ssz differ diff --git a/packages/beacon-node/test/mocks/__fixtures__/block.capella.ssz b/packages/beacon-node/test/mocks/__fixtures__/block.capella.ssz new file mode 100644 index 000000000000..056a78e1a929 Binary files /dev/null and b/packages/beacon-node/test/mocks/__fixtures__/block.capella.ssz differ diff --git a/packages/beacon-node/test/mocks/__fixtures__/block.deneb.ssz b/packages/beacon-node/test/mocks/__fixtures__/block.deneb.ssz new file mode 100644 index 000000000000..4490a31f2233 Binary files /dev/null and b/packages/beacon-node/test/mocks/__fixtures__/block.deneb.ssz differ diff --git a/packages/beacon-node/test/mocks/__fixtures__/block.phase0.ssz b/packages/beacon-node/test/mocks/__fixtures__/block.phase0.ssz new file mode 100644 index 000000000000..940bea5ca146 Binary files /dev/null and b/packages/beacon-node/test/mocks/__fixtures__/block.phase0.ssz differ diff --git a/packages/beacon-node/test/mocks/block.ts b/packages/beacon-node/test/mocks/block.ts new file mode 100644 index 000000000000..2afba7ca0515 --- /dev/null +++ b/packages/beacon-node/test/mocks/block.ts @@ -0,0 +1,96 @@ +import fs from "node:fs"; +import {ssz, allForks} from "@lodestar/types"; +import {ForkInfo, createChainForkConfig, defaultChainConfig} from "@lodestar/config"; +import {mainnetChainConfig} from "@lodestar/config/configs"; + +const directory = "./__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, + // mainnet DENEB_FORK_EPOCH is Infinity at time of writing this + DENEB_FORK_EPOCH: mainnetChainConfig.CAPELLA_FORK_EPOCH * 2 * 4, +}); +/* eslint-enable @typescript-eslint/naming-convention */ + +const loadSerialized = (filename: string): Buffer => + fs.readFileSync(new URL(directory.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: allForks.SignedBeaconBlock; + fullSerialized: Uint8Array; + blinded?: allForks.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/unit/util/fullOrBlindedBlock.test.ts b/packages/beacon-node/test/unit/util/fullOrBlindedBlock.test.ts new file mode 100644 index 000000000000..d8154da5a3cf --- /dev/null +++ b/packages/beacon-node/test/unit/util/fullOrBlindedBlock.test.ts @@ -0,0 +1,105 @@ +import {describe, it, expect} from "vitest"; +import {ForkInfo} from "@lodestar/config"; +import {allForks, capella} from "@lodestar/types"; +import {isForkExecution} from "@lodestar/params"; +import { + blindedOrFullBlockToBlinded, + 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, allForks.FullOrBlindedSignedBeaconBlock, 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 ${fullOrBlinded} ${name} blocks`, () => { + expect(isBlindedBytes(seq, block)).toEqual(isForkExecution(name) && fullOrBlinded === "blinded"); + }); + } +}); + +describe("serializeFullOrBlindedSignedBeaconBlock", () => { + for (const [fullOrBlinded, {name}, block, expected] of fullOrBlindedBlocks) { + it(`should serialize ${fullOrBlinded} ${name} block`, () => { + const serialized = serializeFullOrBlindedSignedBeaconBlock(chainConfig, block); + expect(byteArrayEquals(serialized, expected)).toBeTruthy(); + }); + } +}); + +describe("deserializeFullOrBlindedSignedBeaconBlock", () => { + for (const [fullOrBlinded, {name}, block, serialized] of fullOrBlindedBlocks) { + it(`should deserialize ${fullOrBlinded} ${name} block`, () => { + const deserialized = deserializeFullOrBlindedSignedBeaconBlock(chainConfig, serialized); + const type = + isForkExecution(name) && fullOrBlinded === "blinded" + ? chainConfig.getBlindedForkTypes(block.message.slot).SignedBeaconBlock + : chainConfig.getForkTypes(block.message.slot).SignedBeaconBlock; + expect(type.equals(deserialized as any, block as any)).toBeTruthy(); + }); + } +}); + +describe("blindedOrFullBlockToBlinded", function () { + for (const { + forkInfo: {name}, + full, + blinded, + } of mockBlocks) { + it(`should convert full ${name} to blinded block`, () => { + const result = blindedOrFullBlockToBlinded(chainConfig, full); + const isExecution = isForkExecution(name); + const isEqual = isExecution + ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + chainConfig.getBlindedForkTypes(full.message.slot).SignedBeaconBlock.equals(result, blinded!) + : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + chainConfig.getForkTypes(full.message.slot).SignedBeaconBlock.equals(result, isExecution ? blinded! : full); + expect(isEqual).toBeTruthy(); + }); + if (!blinded) continue; + it(`should convert blinded ${name} to blinded block`, () => { + const result = blindedOrFullBlockToBlinded(chainConfig, blinded); + const isEqual = isForkExecution(name) + ? chainConfig.getBlindedForkTypes(full.message.slot).SignedBeaconBlock.equals(result, blinded) + : chainConfig.getForkTypes(full.message.slot).SignedBeaconBlock.equals(result, blinded); + expect(isEqual).toBeTruthy(); + }); + } +}); + +describe("blindedOrFullBlockToFull", function () { + for (const { + forkInfo: {name, seq}, + full, + blinded, + } of mockBlocks) { + const transactionsAndWithdrawals = { + transactions: (full as capella.SignedBeaconBlock).message.body.executionPayload?.transactions ?? [], + withdrawals: (full as capella.SignedBeaconBlock).message.body.executionPayload?.withdrawals ?? [], + }; + it(`should convert full ${name} to full block`, () => { + const result = blindedOrFullBlockToFull(chainConfig, seq, full, transactionsAndWithdrawals); + expect(chainConfig.getForkTypes(full.message.slot).SignedBeaconBlock.equals(result, full)).toBeTruthy(); + }); + if (!blinded) continue; + it(`should convert blinded ${name} to full block`, () => { + const result = blindedOrFullBlockToFull(chainConfig, seq, blinded, transactionsAndWithdrawals); + expect(chainConfig.getForkTypes(full.message.slot).SignedBeaconBlock.equals(result, full)).toBeTruthy(); + }); + } +}); diff --git a/packages/cli/test/utils/crucible/simulation.ts b/packages/cli/test/utils/crucible/simulation.ts index c6e58095474a..21b09d981a81 100644 --- a/packages/cli/test/utils/crucible/simulation.ts +++ b/packages/cli/test/utils/crucible/simulation.ts @@ -285,6 +285,7 @@ export class Simulation { genesisState: this.genesisState, engineUrls, paths: getNodePaths({id, logsDir: this.options.logsDir, client: beaconType, root: this.options.rootDir}), + engineMock: executionType === ExecutionClient.Mock, }); if (keys.type === "no-keys") { diff --git a/packages/types/src/bellatrix/types.ts b/packages/types/src/bellatrix/types.ts index dcd45d7c97ea..fdfc8863acf6 100644 --- a/packages/types/src/bellatrix/types.ts +++ b/packages/types/src/bellatrix/types.ts @@ -22,3 +22,4 @@ export type SignedBuilderBid = ValueOf; export type SSEPayloadAttributes = ValueOf; export type FullOrBlindedExecutionPayload = ExecutionPayload | ExecutionPayloadHeader; +export type FullOrBlindedSignedBeaconBlock = SignedBeaconBlock | SignedBlindedBeaconBlock; diff --git a/packages/types/src/capella/types.ts b/packages/types/src/capella/types.ts index 386f96ecd280..8e8065c6111c 100644 --- a/packages/types/src/capella/types.ts +++ b/packages/types/src/capella/types.ts @@ -20,6 +20,7 @@ export type BlindedBeaconBlock = ValueOf; export type SignedBlindedBeaconBlock = ValueOf; export type FullOrBlindedExecutionPayload = ExecutionPayload | ExecutionPayloadHeader; +export type FullOrBlindedSignedBeaconBlock = SignedBeaconBlock | SignedBlindedBeaconBlock; export type BuilderBid = ValueOf; export type SignedBuilderBid = ValueOf; diff --git a/packages/types/src/deneb/types.ts b/packages/types/src/deneb/types.ts index 9a901c9a1a81..41cd5094009a 100644 --- a/packages/types/src/deneb/types.ts +++ b/packages/types/src/deneb/types.ts @@ -36,6 +36,7 @@ export type BlindedBeaconBlock = ValueOf; export type SignedBlindedBeaconBlock = ValueOf; export type FullOrBlindedExecutionPayload = ExecutionPayload | ExecutionPayloadHeader; +export type FullOrBlindedSignedBeaconBlock = SignedBeaconBlock | SignedBlindedBeaconBlock; export type BuilderBid = ValueOf; export type SignedBuilderBid = ValueOf; diff --git a/packages/types/src/types.ts b/packages/types/src/types.ts index 46641d55667e..2f32339ae12c 100644 --- a/packages/types/src/types.ts +++ b/packages/types/src/types.ts @@ -1,4 +1,12 @@ -import {ForkAll, ForkBlobs, ForkExecution, ForkLightClient, ForkName, ForkPreBlobs} from "@lodestar/params"; +import { + ForkAll, + ForkBlobs, + ForkExecution, + ForkLightClient, + ForkName, + ForkPreBlobs, + ForkPreExecution, +} from "@lodestar/params"; import {ts as phase0} from "./phase0/index.js"; import {ts as altair} from "./altair/index.js"; import {ts as bellatrix} from "./bellatrix/index.js"; @@ -69,6 +77,7 @@ type TypesByFork = { BlindedBeaconBlock: bellatrix.BlindedBeaconBlock; BlindedBeaconBlockBody: bellatrix.BlindedBeaconBlockBody; SignedBlindedBeaconBlock: bellatrix.SignedBlindedBeaconBlock; + FullOrBlindedSignedBeaconBlock: bellatrix.FullOrBlindedSignedBeaconBlock; ExecutionPayload: bellatrix.ExecutionPayload; ExecutionPayloadHeader: bellatrix.ExecutionPayloadHeader; BuilderBid: bellatrix.BuilderBid; @@ -94,6 +103,7 @@ type TypesByFork = { BlindedBeaconBlock: capella.BlindedBeaconBlock; BlindedBeaconBlockBody: capella.BlindedBeaconBlockBody; SignedBlindedBeaconBlock: capella.SignedBlindedBeaconBlock; + FullOrBlindedSignedBeaconBlock: capella.FullOrBlindedSignedBeaconBlock; ExecutionPayload: capella.ExecutionPayload; ExecutionPayloadHeader: capella.ExecutionPayloadHeader; BuilderBid: capella.BuilderBid; @@ -119,6 +129,7 @@ type TypesByFork = { BlindedBeaconBlock: deneb.BlindedBeaconBlock; BlindedBeaconBlockBody: deneb.BlindedBeaconBlockBody; SignedBlindedBeaconBlock: deneb.SignedBlindedBeaconBlock; + FullOrBlindedSignedBeaconBlock: deneb.FullOrBlindedSignedBeaconBlock; ExecutionPayload: deneb.ExecutionPayload; ExecutionPayloadHeader: deneb.ExecutionPayloadHeader; BuilderBid: deneb.BuilderBid; @@ -151,6 +162,9 @@ export type BlindedBeaconBlock = TypesB export type SignedBeaconBlock = TypesByFork[F]["SignedBeaconBlock"]; export type SignedBlindedBeaconBlock = TypesByFork[F]["SignedBlindedBeaconBlock"]; +export type FullOrBlindedSignedBeaconBlock = + | SignedBeaconBlock + | SignedBlindedBeaconBlock; export type BeaconBlockBody = TypesByFork[F]["BeaconBlockBody"]; export type BlindedBeaconBlockBody = TypesByFork[F]["BlindedBeaconBlockBody"];