From 48bc7f89a16ca8a107485f8786cf02600d857540 Mon Sep 17 00:00:00 2001 From: matthewkeil Date: Fri, 16 Aug 2024 22:31:06 -0400 Subject: [PATCH 01/18] feat: add fullOrBlindedBlock.ts util --- .../src/util/fullOrBlindedBlock.ts | 272 ++++++++++++++++++ packages/beacon-node/src/util/sszBytes.ts | 2 +- 2 files changed, 273 insertions(+), 1 deletion(-) create mode 100644 packages/beacon-node/src/util/fullOrBlindedBlock.ts diff --git a/packages/beacon-node/src/util/fullOrBlindedBlock.ts b/packages/beacon-node/src/util/fullOrBlindedBlock.ts new file mode 100644 index 000000000000..509a30f77a60 --- /dev/null +++ b/packages/beacon-node/src/util/fullOrBlindedBlock.ts @@ -0,0 +1,272 @@ +import {ChainForkConfig} from "@lodestar/config"; +import { + bellatrix, + capella, + deneb, + ExecutionPayload, + ExecutionPayloadHeader, + SignedBeaconBlock, + SignedBlindedBeaconBlock, +} from "@lodestar/types"; +import {BYTES_PER_LOGS_BLOOM, ForkSeq, ForkName, isForkExecution, 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"; + +export type FullOrBlindedSignedBeaconBlock = SignedBeaconBlock | SignedBlindedBeaconBlock; +/** + * * 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, + block: FullOrBlindedSignedBeaconBlock, + transactionsAndWithdrawals: Partial +): SignedBeaconBlock { + if ( + !isBlinded(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 From 78e5f7d9f8a6be439b9b49b86e6c302e81a955bf Mon Sep 17 00:00:00 2001 From: matthewkeil Date: Fri, 16 Aug 2024 22:31:19 -0400 Subject: [PATCH 02/18] feat: update DB to use full or blinded blocks --- .../beacon-node/src/db/repositories/block.ts | 26 +++++++----- .../src/db/repositories/blockArchive.ts | 42 +++++++++++-------- .../src/db/repositories/blockArchiveIndex.ts | 7 ++-- 3 files changed, 44 insertions(+), 31 deletions(-) diff --git a/packages/beacon-node/src/db/repositories/block.ts b/packages/beacon-node/src/db/repositories/block.ts index b01acb8c2ea8..155855b94b48 100644 --- a/packages/beacon-node/src/db/repositories/block.ts +++ b/packages/beacon-node/src/db/repositories/block.ts @@ -1,7 +1,12 @@ import {ChainForkConfig} from "@lodestar/config"; import {Db, Repository} from "@lodestar/db"; -import {SignedBeaconBlock, ssz} from "@lodestar/types"; -import {getSignedBlockTypeFromBytes} from "../../util/multifork.js"; +import {ssz} from "@lodestar/types"; +import {blindedOrFullBlockHashTreeRoot} from "@lodestar/state-transition"; +import { + FullOrBlindedSignedBeaconBlock, + serializeFullOrBlindedSignedBeaconBlock, + deserializeFullOrBlindedSignedBeaconBlock, +} from "../../util/fullOrBlindedBlock.js"; import {Bucket, getBucketNameByValue} from "../buckets.js"; /** @@ -9,25 +14,26 @@ 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: 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): Uint8Array { + return serializeFullOrBlindedSignedBeaconBlock(this.config, value); } - decodeValue(data: Buffer): SignedBeaconBlock { - return getSignedBlockTypeFromBytes(this.config, data).deserialize(data); + decodeValue(data: Uint8Array): 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..1d9b82333990 100644 --- a/packages/beacon-node/src/db/repositories/blockArchive.ts +++ b/packages/beacon-node/src/db/repositories/blockArchive.ts @@ -3,7 +3,12 @@ import {ChainForkConfig} from "@lodestar/config"; import {Db, Repository, KeyValue, FilterOptions} from "@lodestar/db"; import {Slot, Root, ssz, SignedBeaconBlock} from "@lodestar/types"; import {bytesToInt} from "@lodestar/utils"; -import {getSignedBlockTypeFromBytes} from "../../util/multifork.js"; +import {blindedOrFullBlockHashTreeRoot} from "@lodestar/state-transition"; +import { + FullOrBlindedSignedBeaconBlock, + 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 +26,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: 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 +56,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 +66,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 +90,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 +98,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 +108,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 +120,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 +136,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..3df3b402c881 100644 --- a/packages/beacon-node/src/db/repositories/blockArchiveIndex.ts +++ b/packages/beacon-node/src/db/repositories/blockArchiveIndex.ts @@ -1,7 +1,8 @@ import {Db, encodeKey} from "@lodestar/db"; -import {Slot, Root, ssz, SignedBeaconBlock, SSZTypesFor} from "@lodestar/types"; +import {Slot, Root, ssz, SSZTypesFor} from "@lodestar/types"; import {intToBytes} from "@lodestar/utils"; import {ForkAll} from "@lodestar/params"; +import {FullOrBlindedSignedBeaconBlock} from "../../util/fullOrBlindedBlock.js"; import {Bucket} from "../buckets.js"; export async function storeRootIndex(db: Db, slot: Slot, blockRoot: Root): Promise { @@ -15,13 +16,13 @@ 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))); } -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)); } From d3e61dddb60b40066b975d2f4490f91d79512271 Mon Sep 17 00:00:00 2001 From: matthewkeil Date: Fri, 16 Aug 2024 23:45:28 -0400 Subject: [PATCH 03/18] feat: write blocks to db blinded --- .../src/chain/blocks/writeBlockInputToDb.ts | 12 +++--------- packages/beacon-node/src/db/repositories/block.ts | 10 ++++++++++ 2 files changed, 13 insertions(+), 9 deletions(-) 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/db/repositories/block.ts b/packages/beacon-node/src/db/repositories/block.ts index 155855b94b48..e668c64890ee 100644 --- a/packages/beacon-node/src/db/repositories/block.ts +++ b/packages/beacon-node/src/db/repositories/block.ts @@ -4,6 +4,7 @@ import {ssz} from "@lodestar/types"; import {blindedOrFullBlockHashTreeRoot} from "@lodestar/state-transition"; import { FullOrBlindedSignedBeaconBlock, + blindedOrFullBlockToBlinded, serializeFullOrBlindedSignedBeaconBlock, deserializeFullOrBlindedSignedBeaconBlock, } from "../../util/fullOrBlindedBlock.js"; @@ -36,4 +37,13 @@ export class BlockRepository extends Repository { + throw new Error("cannot .putBinary into BlockRepository. must use .add so can be saved blinded"); + } + + async add(value: FullOrBlindedSignedBeaconBlock): Promise { + return super.add(blindedOrFullBlockToBlinded(value)); + } } From 7aa809de27a90974199b5dd380c5294ee735305e Mon Sep 17 00:00:00 2001 From: matthewkeil Date: Sun, 18 Aug 2024 18:02:42 -0400 Subject: [PATCH 04/18] feat: add blindedOrFullBlockBodyHashTreeRoot to state-transition --- .../util/{blindedBlock.ts => fullOrBlindedBlock.ts} | 11 +++++++++++ packages/state-transition/src/util/index.ts | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) rename packages/state-transition/src/util/{blindedBlock.ts => fullOrBlindedBlock.ts} (90%) diff --git a/packages/state-transition/src/util/blindedBlock.ts b/packages/state-transition/src/util/fullOrBlindedBlock.ts similarity index 90% rename from packages/state-transition/src/util/blindedBlock.ts rename to packages/state-transition/src/util/fullOrBlindedBlock.ts index 2e4e4d590817..87a1bbe6f6f9 100644 --- a/packages/state-transition/src/util/blindedBlock.ts +++ b/packages/state-transition/src/util/fullOrBlindedBlock.ts @@ -30,6 +30,17 @@ export function blindedOrFullBlockHashTreeRoot( config.getForkTypes(blindedOrFull.slot).BeaconBlock.hashTreeRoot(blindedOrFull); } +export function blindedOrFullBlockBodyHashTreeRoot( + config: ChainForkConfig, + blindedOrFull: BeaconBlock | BlindedBeaconBlock +): Root { + return isBlindedBeaconBlock(blindedOrFull) + ? // Blinded + config.getExecutionForkTypes(blindedOrFull.slot).BlindedBeaconBlockBody.hashTreeRoot(blindedOrFull.body) + : // Full + config.getForkTypes(blindedOrFull.slot).BeaconBlockBody.hashTreeRoot(blindedOrFull.body); +} + export function blindedOrFullBlockToHeader( config: ChainForkConfig, blindedOrFull: BeaconBlock | BlindedBeaconBlock 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"; From 1096eb7d98f530cdf9580b8a632ed0961b965223 Mon Sep 17 00:00:00 2001 From: matthewkeil Date: Mon, 19 Aug 2024 01:16:21 -0400 Subject: [PATCH 05/18] feat: add blindedOrFull utils to state-transition --- .../beacon-node/src/db/repositories/block.ts | 5 +- .../src/util/fullOrBlindedBlock.ts | 61 ++------------- .../state-transition/src/util/blockRoot.ts | 9 ++- .../src/util/fullOrBlindedBlock.ts | 76 +++++++++++++++---- 4 files changed, 77 insertions(+), 74 deletions(-) diff --git a/packages/beacon-node/src/db/repositories/block.ts b/packages/beacon-node/src/db/repositories/block.ts index e668c64890ee..23bd8a02b34e 100644 --- a/packages/beacon-node/src/db/repositories/block.ts +++ b/packages/beacon-node/src/db/repositories/block.ts @@ -1,10 +1,9 @@ import {ChainForkConfig} from "@lodestar/config"; import {Db, Repository} from "@lodestar/db"; import {ssz} from "@lodestar/types"; -import {blindedOrFullBlockHashTreeRoot} from "@lodestar/state-transition"; +import {blindedOrFullBlockHashTreeRoot, fullOrBlindedSignedBlockToBlinded} from "@lodestar/state-transition"; import { FullOrBlindedSignedBeaconBlock, - blindedOrFullBlockToBlinded, serializeFullOrBlindedSignedBeaconBlock, deserializeFullOrBlindedSignedBeaconBlock, } from "../../util/fullOrBlindedBlock.js"; @@ -44,6 +43,6 @@ export class BlockRepository extends Repository { - return super.add(blindedOrFullBlockToBlinded(value)); + return super.add(fullOrBlindedSignedBlockToBlinded(this.config, value)); } } diff --git a/packages/beacon-node/src/util/fullOrBlindedBlock.ts b/packages/beacon-node/src/util/fullOrBlindedBlock.ts index 509a30f77a60..1258e0999a35 100644 --- a/packages/beacon-node/src/util/fullOrBlindedBlock.ts +++ b/packages/beacon-node/src/util/fullOrBlindedBlock.ts @@ -1,19 +1,22 @@ import {ChainForkConfig} from "@lodestar/config"; import { + BeaconBlock, bellatrix, + BlindedBeaconBlock, 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 {executionPayloadToPayloadHeader} from "@lodestar/state-transition"; import {ExecutionPayloadBody} from "../execution/engine/types.js"; import {ROOT_SIZE, getSlotFromSignedBeaconBlockSerialized} from "./sszBytes.js"; export type FullOrBlindedSignedBeaconBlock = SignedBeaconBlock | SignedBlindedBeaconBlock; +export type FullOrBlindedBeaconBlock = BeaconBlock | BlindedBeaconBlock; /** * * class SignedBeaconBlock(Container): * message: BeaconBlock [offset - 4 bytes] @@ -126,16 +129,11 @@ export function isBlindedBytes(forkSeq: ForkSeq, blockBytes: Uint8Array): boolea 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)) { + if (isBlindedSignedBeaconBlock(value)) { const type = config.getExecutionForkTypes(value.message.slot).SignedBlindedBeaconBlock; return type.serialize(value); } @@ -157,52 +155,6 @@ export function deserializeFullOrBlindedSignedBeaconBlock( : 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, @@ -242,13 +194,14 @@ function executionPayloadHeaderToPayload( return bellatrixPayloadFields; } +// TODO: (@matthewkeil) not the same as blindedOrFullBlockToFull in state-transition. consider merging? export function blindedOrFullBlockToFull( config: ChainForkConfig, block: FullOrBlindedSignedBeaconBlock, transactionsAndWithdrawals: Partial ): SignedBeaconBlock { if ( - !isBlinded(block) || // already full + !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 ) { 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 index 87a1bbe6f6f9..6ffb09431600 100644 --- a/packages/state-transition/src/util/fullOrBlindedBlock.ts +++ b/packages/state-transition/src/util/fullOrBlindedBlock.ts @@ -1,5 +1,5 @@ import {ChainForkConfig} from "@lodestar/config"; -import {ForkExecution, ForkSeq} from "@lodestar/params"; +import {ForkName, ForkSeq} from "@lodestar/params"; import { Root, isBlindedBeaconBlock, @@ -15,10 +15,22 @@ import { SignedBlindedBeaconBlock, BlindedBeaconBlock, ExecutionPayloadHeader, + isBlindedSignedBeaconBlock, } from "@lodestar/types"; import {executionPayloadToPayloadHeader} from "./execution.js"; +export function blindedOrFullBlockBodyHashTreeRoot( + config: ChainForkConfig, + blindedOrFull: BeaconBlock | BlindedBeaconBlock +): Root { + return isBlindedBeaconBlock(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 @@ -30,15 +42,15 @@ export function blindedOrFullBlockHashTreeRoot( config.getForkTypes(blindedOrFull.slot).BeaconBlock.hashTreeRoot(blindedOrFull); } -export function blindedOrFullBlockBodyHashTreeRoot( +export function blindedOrFullSignedBlockHashTreeRoot( config: ChainForkConfig, - blindedOrFull: BeaconBlock | BlindedBeaconBlock + blindedOrFull: SignedBeaconBlock | SignedBlindedBeaconBlock ): Root { - return isBlindedBeaconBlock(blindedOrFull) + return isBlindedSignedBeaconBlock(blindedOrFull) ? // Blinded - config.getExecutionForkTypes(blindedOrFull.slot).BlindedBeaconBlockBody.hashTreeRoot(blindedOrFull.body) + config.getExecutionForkTypes(blindedOrFull.message.slot).SignedBlindedBeaconBlock.hashTreeRoot(blindedOrFull) : // Full - config.getForkTypes(blindedOrFull.slot).BeaconBlockBody.hashTreeRoot(blindedOrFull.body); + config.getForkTypes(blindedOrFull.message.slot).SignedBeaconBlock.hashTreeRoot(blindedOrFull); } export function blindedOrFullBlockToHeader( @@ -60,23 +72,59 @@ export function blindedOrFullBlockToHeader( }; } -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 fullOrBlindedBlockToBlinded( + config: ChainForkConfig, + block: BeaconBlock | BlindedBeaconBlock +): BlindedBeaconBlock { + const forkSeq = config.getForkSeq(block.slot); + if (isBlindedBeaconBlock(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 signedBeaconBlockToBlinded( +export function fullOrBlindedSignedBlockToBlinded( config: ChainForkConfig, - signedBlock: SignedBeaconBlock + signedBlock: SignedBeaconBlock | SignedBlindedBeaconBlock ): SignedBlindedBeaconBlock { return { - message: beaconBlockToBlinded(config, signedBlock.message), + 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 From a739828431ea5b8e5e1bf588959107701d36f795 Mon Sep 17 00:00:00 2001 From: matthewkeil Date: Mon, 19 Aug 2024 01:23:21 -0400 Subject: [PATCH 06/18] feat: update chain to work with full or blinded blocks --- packages/beacon-node/src/chain/chain.ts | 37 +++++++++++++++++---- packages/beacon-node/src/chain/interface.ts | 19 +++++++---- packages/beacon-node/src/eth1/errors.ts | 5 ++- 3 files changed, 47 insertions(+), 14 deletions(-) diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index ee70231c7d40..dfe2a4180835 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -13,6 +13,7 @@ import { PubkeyIndexMap, EpochShuffling, computeEndSlotAtEpoch, + blindedOrFullBlockHashTreeRoot, } from "@lodestar/state-transition"; import {BeaconConfig} from "@lodestar/config"; import { @@ -32,10 +33,11 @@ import { 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 { @@ -560,9 +564,11 @@ export class BeaconChain implements IBeaconChain { return null; } - async getCanonicalBlockAtSlot( - slot: Slot - ): Promise<{block: SignedBeaconBlock; executionOptimistic: boolean; finalized: boolean} | null> { + async getCanonicalBlockAtSlot(slot: Slot): 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 @@ -582,9 +588,11 @@ export class BeaconChain implements IBeaconChain { return data && {block: data, executionOptimistic: false, finalized: true}; } - async getBlockByRoot( - root: string - ): Promise<{block: SignedBeaconBlock; executionOptimistic: boolean; finalized: boolean} | null> { + async getBlockByRoot(root: string): 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)); @@ -995,6 +1003,21 @@ export class BeaconChain implements IBeaconChain { return {state: blockState, stateId: "block_state_any_epoch", shouldWarn: true}; } + async fullOrBlindedSignedBeaconBlockToFull( + block: SignedBeaconBlock | SignedBlindedBeaconBlock + ): Promise { + if (!isBlindedBeaconBlock(block)) return block; + const blockHash = toHex(blindedOrFullBlockHashTreeRoot(this.config, block.message)); + const [payload] = await this.executionEngine.getPayloadBodiesByHash([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..522e83bd9413 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, @@ -172,15 +173,19 @@ export interface IBeaconChain { * this methods returns blocks in current chain head according to * forkchoice. Works for finalized slots as well */ - getCanonicalBlockAtSlot( - slot: Slot - ): Promise<{block: SignedBeaconBlock; executionOptimistic: boolean; finalized: boolean} | null>; + getCanonicalBlockAtSlot(slot: Slot): 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>; + getBlockByRoot(root: RootHex): Promise<{ + block: SignedBeaconBlock | SignedBlindedBeaconBlock; + executionOptimistic: boolean; + finalized: boolean; + } | null>; getContents(beaconBlock: deneb.BeaconBlock): deneb.Contents; @@ -197,6 +202,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/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 {} From d4215a78ae58909d40b9ff82fe6971c5f5b603f9 Mon Sep 17 00:00:00 2001 From: matthewkeil Date: Mon, 19 Aug 2024 01:23:45 -0400 Subject: [PATCH 07/18] feat: update API to work with full or blinded blocks --- .../src/api/impl/beacon/blocks/index.ts | 20 ++++++-------- .../src/api/impl/beacon/blocks/utils.ts | 11 ++++---- .../beacon-node/src/api/impl/proof/index.ts | 5 +++- .../src/api/impl/validator/index.ts | 26 +++++-------------- 4 files changed, 25 insertions(+), 37 deletions(-) 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..5117751cc8ca 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 {isBlindedBeaconBlock} from "@lodestar/types"; 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 = isBlindedBeaconBlock(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}) { From 4d156eaba02f97973ee085a7044e18ecf19ddd9f Mon Sep 17 00:00:00 2001 From: matthewkeil Date: Mon, 19 Aug 2024 01:49:10 -0400 Subject: [PATCH 08/18] feat: update req/resp to work with full or blinded blocks --- packages/beacon-node/src/chain/chain.ts | 4 +++- .../network/reqresp/handlers/beaconBlocksByRange.ts | 13 ++++++++++--- .../network/reqresp/handlers/beaconBlocksByRoot.ts | 6 +++++- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index dfe2a4180835..6ea0c3b1d1d8 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -1006,7 +1006,9 @@ export class BeaconChain implements IBeaconChain { async fullOrBlindedSignedBeaconBlockToFull( block: SignedBeaconBlock | SignedBlindedBeaconBlock ): Promise { - if (!isBlindedBeaconBlock(block)) return block; + if (!isBlindedBeaconBlock(block)) { + return block; + } const blockHash = toHex(blindedOrFullBlockHashTreeRoot(this.config, block.message)); const [payload] = await this.executionEngine.getPayloadBodiesByHash([blockHash]); if (!payload) { 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), }; } From 378822ee74e62237dd373520b8e0027809ec6d1c Mon Sep 17 00:00:00 2001 From: matthewkeil Date: Mon, 19 Aug 2024 02:24:14 -0400 Subject: [PATCH 09/18] refactor: remove FullOrBlinded union type --- .../beacon-node/src/db/repositories/block.ts | 14 +++---- .../src/db/repositories/blockArchive.ts | 38 ++++++++++--------- .../src/db/repositories/blockArchiveIndex.ts | 18 ++++----- .../src/util/fullOrBlindedBlock.ts | 8 ++-- 4 files changed, 37 insertions(+), 41 deletions(-) diff --git a/packages/beacon-node/src/db/repositories/block.ts b/packages/beacon-node/src/db/repositories/block.ts index 23bd8a02b34e..061a784a2f50 100644 --- a/packages/beacon-node/src/db/repositories/block.ts +++ b/packages/beacon-node/src/db/repositories/block.ts @@ -1,9 +1,8 @@ import {ChainForkConfig} from "@lodestar/config"; import {Db, Repository} from "@lodestar/db"; -import {ssz} from "@lodestar/types"; +import {SignedBeaconBlock, SignedBlindedBeaconBlock, ssz} from "@lodestar/types"; import {blindedOrFullBlockHashTreeRoot, fullOrBlindedSignedBlockToBlinded} from "@lodestar/state-transition"; import { - FullOrBlindedSignedBeaconBlock, serializeFullOrBlindedSignedBeaconBlock, deserializeFullOrBlindedSignedBeaconBlock, } from "../../util/fullOrBlindedBlock.js"; @@ -14,7 +13,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; // Pick some type but won't be used, override below so correct container is used @@ -25,24 +24,25 @@ export class BlockRepository extends Repository { throw new Error("cannot .putBinary into BlockRepository. must use .add so can be saved blinded"); } - async add(value: FullOrBlindedSignedBeaconBlock): Promise { + 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 1d9b82333990..e7785e4ea899 100644 --- a/packages/beacon-node/src/db/repositories/blockArchive.ts +++ b/packages/beacon-node/src/db/repositories/blockArchive.ts @@ -1,11 +1,10 @@ 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 {blindedOrFullBlockHashTreeRoot} from "@lodestar/state-transition"; +import {blindedOrFullBlockHashTreeRoot, fullOrBlindedSignedBlockToBlinded} from "@lodestar/state-transition"; import { - FullOrBlindedSignedBeaconBlock, serializeFullOrBlindedSignedBeaconBlock, deserializeFullOrBlindedSignedBeaconBlock, } from "../../util/fullOrBlindedBlock.js"; @@ -26,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; // Pick some type but won't be used, override below so correct container is used @@ -36,17 +35,17 @@ export class BlockArchiveRepository extends Repository { + async put(key: Slot, value: SignedBeaconBlock | SignedBlindedBeaconBlock): Promise { const blockRoot = blindedOrFullBlockHashTreeRoot(this.config, value.message); const slot = value.message.slot; await Promise.all([ @@ -66,9 +65,11 @@ export class BlockArchiveRepository extends Repository[]): 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 = blindedOrFullBlockHashTreeRoot(this.config, item.value.message); @@ -82,6 +83,7 @@ export class BlockArchiveRepository extends Repository { await Promise.all([ super.batchPutBinary(items), @@ -90,25 +92,25 @@ export class BlockArchiveRepository extends Repository { + 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: FullOrBlindedSignedBeaconBlock[]): 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; @@ -120,13 +122,13 @@ export class BlockArchiveRepository extends Repository { + 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; } @@ -136,7 +138,7 @@ export class BlockArchiveRepository extends Repository) : 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 3df3b402c881..dbdca157e51e 100644 --- a/packages/beacon-node/src/db/repositories/blockArchiveIndex.ts +++ b/packages/beacon-node/src/db/repositories/blockArchiveIndex.ts @@ -1,8 +1,6 @@ import {Db, encodeKey} from "@lodestar/db"; -import {Slot, Root, ssz, SSZTypesFor} from "@lodestar/types"; +import {Slot, Root, SignedBeaconBlock, SignedBlindedBeaconBlock} from "@lodestar/types"; import {intToBytes} from "@lodestar/utils"; -import {ForkAll} from "@lodestar/params"; -import {FullOrBlindedSignedBeaconBlock} from "../../util/fullOrBlindedBlock.js"; import {Bucket} from "../buckets.js"; export async function storeRootIndex(db: Db, slot: Slot, blockRoot: Root): Promise { @@ -13,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: FullOrBlindedSignedBeaconBlock -): 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: FullOrBlindedSignedBeaconBlock): 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/util/fullOrBlindedBlock.ts b/packages/beacon-node/src/util/fullOrBlindedBlock.ts index 1258e0999a35..520d310822e5 100644 --- a/packages/beacon-node/src/util/fullOrBlindedBlock.ts +++ b/packages/beacon-node/src/util/fullOrBlindedBlock.ts @@ -15,8 +15,6 @@ import {BYTES_PER_LOGS_BLOOM, ForkSeq, ForkName, isForkExecution, SYNC_COMMITTEE import {ExecutionPayloadBody} from "../execution/engine/types.js"; import {ROOT_SIZE, getSlotFromSignedBeaconBlockSerialized} from "./sszBytes.js"; -export type FullOrBlindedSignedBeaconBlock = SignedBeaconBlock | SignedBlindedBeaconBlock; -export type FullOrBlindedBeaconBlock = BeaconBlock | BlindedBeaconBlock; /** * * class SignedBeaconBlock(Container): * message: BeaconBlock [offset - 4 bytes] @@ -131,7 +129,7 @@ export function isBlindedBytes(forkSeq: ForkSeq, blockBytes: Uint8Array): boolea export function serializeFullOrBlindedSignedBeaconBlock( config: ChainForkConfig, - value: FullOrBlindedSignedBeaconBlock + value: SignedBeaconBlock | SignedBlindedBeaconBlock ): Uint8Array { if (isBlindedSignedBeaconBlock(value)) { const type = config.getExecutionForkTypes(value.message.slot).SignedBlindedBeaconBlock; @@ -144,7 +142,7 @@ export function serializeFullOrBlindedSignedBeaconBlock( export function deserializeFullOrBlindedSignedBeaconBlock( config: ChainForkConfig, bytes: Buffer | Uint8Array -): FullOrBlindedSignedBeaconBlock { +): SignedBeaconBlock | SignedBlindedBeaconBlock { const slot = getSlotFromSignedBeaconBlockSerialized(bytes); if (slot === null) { throw Error("getSignedBlockTypeFromBytes: invalid bytes"); @@ -197,7 +195,7 @@ function executionPayloadHeaderToPayload( // TODO: (@matthewkeil) not the same as blindedOrFullBlockToFull in state-transition. consider merging? export function blindedOrFullBlockToFull( config: ChainForkConfig, - block: FullOrBlindedSignedBeaconBlock, + block: SignedBeaconBlock | SignedBlindedBeaconBlock, transactionsAndWithdrawals: Partial ): SignedBeaconBlock { if ( From e32e946adb0bfa79bebc17e8d4d032f2ec753987 Mon Sep 17 00:00:00 2001 From: matthewkeil Date: Mon, 19 Aug 2024 02:26:34 -0400 Subject: [PATCH 10/18] chore: lint --- packages/beacon-node/src/util/fullOrBlindedBlock.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/beacon-node/src/util/fullOrBlindedBlock.ts b/packages/beacon-node/src/util/fullOrBlindedBlock.ts index 520d310822e5..74d952a9ce68 100644 --- a/packages/beacon-node/src/util/fullOrBlindedBlock.ts +++ b/packages/beacon-node/src/util/fullOrBlindedBlock.ts @@ -1,8 +1,6 @@ import {ChainForkConfig} from "@lodestar/config"; import { - BeaconBlock, bellatrix, - BlindedBeaconBlock, capella, deneb, ExecutionPayload, From 9e15d8f9748f0f73d993eb605fb17acc3252d60c Mon Sep 17 00:00:00 2001 From: matthewkeil Date: Mon, 19 Aug 2024 23:34:01 -0400 Subject: [PATCH 11/18] fix: debug tests --- .../beacon-node/src/api/impl/proof/index.ts | 4 +-- packages/beacon-node/src/chain/chain.ts | 6 ++--- .../src/util/fullOrBlindedBlock.ts | 2 +- .../src/signatureSets/proposer.ts | 6 ++--- .../src/util/fullOrBlindedBlock.ts | 25 +++++++++++++++---- 5 files changed, 29 insertions(+), 14 deletions(-) diff --git a/packages/beacon-node/src/api/impl/proof/index.ts b/packages/beacon-node/src/api/impl/proof/index.ts index 5117751cc8ca..10c1dd6593a0 100644 --- a/packages/beacon-node/src/api/impl/proof/index.ts +++ b/packages/beacon-node/src/api/impl/proof/index.ts @@ -1,7 +1,7 @@ import {CompactMultiProof, createProof, ProofType} from "@chainsafe/persistent-merkle-tree"; import {routes} from "@lodestar/api"; import {ApplicationMethods} from "@lodestar/api/server"; -import {isBlindedBeaconBlock} from "@lodestar/types"; +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"; @@ -44,7 +44,7 @@ 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 = isBlindedBeaconBlock(block.message) + 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; diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index 6ea0c3b1d1d8..3a15658aadc4 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -14,6 +14,7 @@ import { EpochShuffling, computeEndSlotAtEpoch, blindedOrFullBlockHashTreeRoot, + isBlindedBlock, } from "@lodestar/state-transition"; import {BeaconConfig} from "@lodestar/config"; import { @@ -27,7 +28,6 @@ import { deneb, Wei, bellatrix, - isBlindedBeaconBlock, BeaconBlock, SignedBeaconBlock, ExecutionPayload, @@ -844,7 +844,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 { @@ -1006,7 +1006,7 @@ export class BeaconChain implements IBeaconChain { async fullOrBlindedSignedBeaconBlockToFull( block: SignedBeaconBlock | SignedBlindedBeaconBlock ): Promise { - if (!isBlindedBeaconBlock(block)) { + if (!isBlindedBlock(block)) { return block; } const blockHash = toHex(blindedOrFullBlockHashTreeRoot(this.config, block.message)); diff --git a/packages/beacon-node/src/util/fullOrBlindedBlock.ts b/packages/beacon-node/src/util/fullOrBlindedBlock.ts index 74d952a9ce68..053f3df796c2 100644 --- a/packages/beacon-node/src/util/fullOrBlindedBlock.ts +++ b/packages/beacon-node/src/util/fullOrBlindedBlock.ts @@ -147,7 +147,7 @@ export function deserializeFullOrBlindedSignedBeaconBlock( } return isBlindedBytes(config.getForkSeq(slot), bytes) - ? config.getExecutionForkTypes(slot).SignedBeaconBlock.deserialize(bytes) + ? config.getExecutionForkTypes(slot).SignedBlindedBeaconBlock.deserialize(bytes) : config.getForkTypes(slot).SignedBeaconBlock.deserialize(bytes); } 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/fullOrBlindedBlock.ts b/packages/state-transition/src/util/fullOrBlindedBlock.ts index 6ffb09431600..241d5bf5b225 100644 --- a/packages/state-transition/src/util/fullOrBlindedBlock.ts +++ b/packages/state-transition/src/util/fullOrBlindedBlock.ts @@ -1,8 +1,8 @@ +/* eslint-disable @typescript-eslint/strict-boolean-expressions */ import {ChainForkConfig} from "@lodestar/config"; import {ForkName, ForkSeq} from "@lodestar/params"; import { Root, - isBlindedBeaconBlock, isExecutionPayloadAndBlobsBundle, BeaconBlock, BeaconBlockHeader, @@ -20,11 +20,26 @@ import { 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 isBlindedBeaconBlock(blindedOrFull) + return isBlindedBlock(blindedOrFull) ? // Blinded config.getExecutionForkTypes(blindedOrFull.slot).BlindedBeaconBlockBody.hashTreeRoot(blindedOrFull.body) : // Full @@ -35,7 +50,7 @@ export function blindedOrFullBlockHashTreeRoot( config: ChainForkConfig, blindedOrFull: BeaconBlock | BlindedBeaconBlock ): Root { - return isBlindedBeaconBlock(blindedOrFull) + return isBlindedBlock(blindedOrFull) ? // Blinded config.getExecutionForkTypes(blindedOrFull.slot).BlindedBeaconBlock.hashTreeRoot(blindedOrFull) : // Full @@ -57,7 +72,7 @@ export function blindedOrFullBlockToHeader( config: ChainForkConfig, blindedOrFull: BeaconBlock | BlindedBeaconBlock ): BeaconBlockHeader { - const bodyRoot = isBlindedBeaconBlock(blindedOrFull) + const bodyRoot = isBlindedBlock(blindedOrFull) ? // Blinded config.getExecutionForkTypes(blindedOrFull.slot).BlindedBeaconBlockBody.hashTreeRoot(blindedOrFull.body) : // Full @@ -77,7 +92,7 @@ export function fullOrBlindedBlockToBlinded( block: BeaconBlock | BlindedBeaconBlock ): BlindedBeaconBlock { const forkSeq = config.getForkSeq(block.slot); - if (isBlindedBeaconBlock(block) || forkSeq < ForkSeq.bellatrix) { + if (isBlindedBlock(block) || forkSeq < ForkSeq.bellatrix) { return block as BlindedBeaconBlock; } const blinded: BlindedBeaconBlock = { From 232351762d01b9d6ef49595681a116da3464f747 Mon Sep 17 00:00:00 2001 From: matthewkeil Date: Tue, 20 Aug 2024 00:03:08 -0400 Subject: [PATCH 12/18] fix: bug in headAssertion of sim tests --- .../crucible/assertions/defaults/headAssertion.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) 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")}; }, From c1eff3ef1c741c525fc2f72c055df69fcd780f62 Mon Sep 17 00:00:00 2001 From: matthewkeil Date: Tue, 20 Aug 2024 00:03:32 -0400 Subject: [PATCH 13/18] fix: add extra graceExtraTimeFraction to sim tests --- packages/cli/test/utils/crucible/utils/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/test/utils/crucible/utils/index.ts b/packages/cli/test/utils/crucible/utils/index.ts index 0b6ae1bba1e5..11e69355dda3 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.5, }) * 1000; const ttd = getEstimatedTTD({ From 3e37719f4e0675cb69092c50d62918c466c5fbcb Mon Sep 17 00:00:00 2001 From: matthewkeil Date: Tue, 20 Aug 2024 00:32:28 -0400 Subject: [PATCH 14/18] fix: extend sim test timeout --- packages/cli/test/utils/crucible/utils/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/test/utils/crucible/utils/index.ts b/packages/cli/test/utils/crucible/utils/index.ts index 11e69355dda3..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.5, + graceExtraTimeFraction: 0.8, }) * 1000; const ttd = getEstimatedTTD({ From 0e1ce551e23f715eac366263509c37ad823074a9 Mon Sep 17 00:00:00 2001 From: matthewkeil Date: Tue, 27 Aug 2024 02:39:53 -0400 Subject: [PATCH 15/18] test: add unit test for fullOrBlindedBlock.ts with mocks --- .../src/util/fullOrBlindedBlock.ts | 2 +- packages/beacon-node/test/mocks/block.ts | 95 +++++++++++++++++++ .../test/unit/util/fullOrBlindedBlock.test.ts | 77 +++++++++++++++ 3 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 packages/beacon-node/test/mocks/block.ts create mode 100644 packages/beacon-node/test/unit/util/fullOrBlindedBlock.test.ts diff --git a/packages/beacon-node/src/util/fullOrBlindedBlock.ts b/packages/beacon-node/src/util/fullOrBlindedBlock.ts index 053f3df796c2..fef769703251 100644 --- a/packages/beacon-node/src/util/fullOrBlindedBlock.ts +++ b/packages/beacon-node/src/util/fullOrBlindedBlock.ts @@ -199,7 +199,7 @@ export function blindedOrFullBlockToFull( 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 + (block as unknown as SignedBeaconBlock).message.body.executionPayload?.timestamp === 0 // before merge ) { return block; } 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/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(); + }); + } +}); From c55c282af498f7761b3a0de098d3ec693640c2a5 Mon Sep 17 00:00:00 2001 From: matthewkeil Date: Thu, 5 Sep 2024 23:59:55 -0400 Subject: [PATCH 16/18] chore: lint --- packages/beacon-node/src/chain/chain.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index 3a15658aadc4..5ebeb4a77b4d 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -1010,7 +1010,9 @@ export class BeaconChain implements IBeaconChain { return block; } const blockHash = toHex(blindedOrFullBlockHashTreeRoot(this.config, block.message)); - const [payload] = await this.executionEngine.getPayloadBodiesByHash([blockHash]); + const [payload] = await this.executionEngine.getPayloadBodiesByHash(this.config.getForkName(block.message.slot), [ + blockHash, + ]); if (!payload) { throw new Eth1Error( {code: Eth1ErrorCode.INVALID_PAYLOAD_BODY, blockHash}, From dea3b7d86be89bba1b8f706f7ff9ad583273d769 Mon Sep 17 00:00:00 2001 From: matthewkeil Date: Tue, 10 Sep 2024 04:48:40 -0400 Subject: [PATCH 17/18] test: rough out fixture building functions --- packages/beacon-node/test/fixtures/altair.ts | 28 ++ .../beacon-node/test/fixtures/bellatrix.ts | 36 +++ packages/beacon-node/test/fixtures/capella.ts | 29 ++- packages/beacon-node/test/fixtures/deneb.ts | 34 +++ packages/beacon-node/test/fixtures/electra.ts | 26 ++ packages/beacon-node/test/fixtures/phase0.ts | 240 +++++++++++++++--- packages/beacon-node/test/fixtures/utils.ts | 18 ++ .../test/perf/chain/opPools/opPool.test.ts | 4 +- 8 files changed, 377 insertions(+), 38 deletions(-) create mode 100644 packages/beacon-node/test/fixtures/altair.ts create mode 100644 packages/beacon-node/test/fixtures/bellatrix.ts create mode 100644 packages/beacon-node/test/fixtures/deneb.ts create mode 100644 packages/beacon-node/test/fixtures/electra.ts create mode 100644 packages/beacon-node/test/fixtures/utils.ts 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/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) From 357968a9ecb741435566dd501c188cf8db72d0f8 Mon Sep 17 00:00:00 2001 From: matthewkeil Date: Mon, 16 Sep 2024 15:31:17 +0700 Subject: [PATCH 18/18] feat: add getFull to chain.getBlockBy* --- packages/beacon-node/src/chain/chain.ts | 38 +++++++++++++++++---- packages/beacon-node/src/chain/interface.ts | 10 ++++-- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index 5ebeb4a77b4d..1fa57562b924 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -564,7 +564,10 @@ export class BeaconChain implements IBeaconChain { return null; } - async getCanonicalBlockAtSlot(slot: Slot): Promise<{ + async getCanonicalBlockAtSlot( + slot: Slot, + getFull = true + ): Promise<{ block: SignedBeaconBlock | SignedBlindedBeaconBlock; executionOptimistic: boolean; finalized: boolean; @@ -576,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 @@ -585,10 +592,19 @@ 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<{ + async getBlockByRoot( + root: string, + getFull = true + ): Promise<{ block: SignedBeaconBlock | SignedBlindedBeaconBlock; executionOptimistic: boolean; finalized: boolean; @@ -597,14 +613,24 @@ export class BeaconChain implements IBeaconChain { 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 { diff --git a/packages/beacon-node/src/chain/interface.ts b/packages/beacon-node/src/chain/interface.ts index 522e83bd9413..9ee719e37c49 100644 --- a/packages/beacon-node/src/chain/interface.ts +++ b/packages/beacon-node/src/chain/interface.ts @@ -173,7 +173,10 @@ export interface IBeaconChain { * this methods returns blocks in current chain head according to * forkchoice. Works for finalized slots as well */ - getCanonicalBlockAtSlot(slot: Slot): Promise<{ + getCanonicalBlockAtSlot( + slot: Slot, + getFull?: boolean + ): Promise<{ block: SignedBeaconBlock | SignedBlindedBeaconBlock; executionOptimistic: boolean; finalized: boolean; @@ -181,7 +184,10 @@ export interface IBeaconChain { /** * Get local block by root, does not fetch from the network */ - getBlockByRoot(root: RootHex): Promise<{ + getBlockByRoot( + root: RootHex, + getFull?: boolean + ): Promise<{ block: SignedBeaconBlock | SignedBlindedBeaconBlock; executionOptimistic: boolean; finalized: boolean;