diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bd23981a7cfe..27dd70031c7f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,6 +2,9 @@ name: Tests on: [pull_request, push] +env: + GOERLI_RPC_DEFAULT_URL: https://goerli.infura.io/v3/84842078b09946638c03157f83405213 + jobs: tests-main: name: Tests @@ -53,6 +56,6 @@ jobs: - name: E2e tests run: yarn test:e2e env: - GOERLI_RPC_URL: ${{ secrets.GOERLI_RPC_URL }} + GOERLI_RPC_URL: ${{ secrets.GOERLI_RPC_URL!=0 && secrets.GOERLI_RPC_URL || env.GOERLI_RPC_DEFAULT_URL }} - name: README check run: yarn run check-readme diff --git a/packages/fork-choice/src/forkChoice/forkChoice.ts b/packages/fork-choice/src/forkChoice/forkChoice.ts index 109955808a36..f001380a8807 100644 --- a/packages/fork-choice/src/forkChoice/forkChoice.ts +++ b/packages/fork-choice/src/forkChoice/forkChoice.ts @@ -11,7 +11,7 @@ import { import {IChainConfig, IChainForkConfig} from "@chainsafe/lodestar-config"; import {computeDeltas} from "../protoArray/computeDeltas"; -import {HEX_ZERO_HASH, IVoteTracker, IProtoBlock} from "../protoArray/interface"; +import {HEX_ZERO_HASH, IVoteTracker, IProtoBlock, ExecutionStatus} from "../protoArray/interface"; import {ProtoArray} from "../protoArray/protoArray"; import {IForkChoiceMetrics} from "../metrics"; @@ -296,9 +296,8 @@ export class ForkChoice implements IForkChoice { } if ( - merge.isMergeStateType(state) && - merge.isMergeBlockBodyType(block.body) && - merge.isMergeBlock(state, block.body) + preCachedData?.isMergeBlock || + (merge.isMergeStateType(state) && merge.isMergeBlockBodyType(block.body) && merge.isMergeBlock(state, block.body)) ) assertValidTerminalPowBlock(this.config, (block as unknown) as merge.BeaconBlock, preCachedData); @@ -368,13 +367,20 @@ export class ForkChoice implements IForkChoice { parentRoot: parentRootHex, targetRoot: toHexString(targetRoot), stateRoot: toHexString(block.stateRoot), - executionPayloadBlockHash: merge.isMergeBlockBodyType(block.body) - ? toHexString(block.body.executionPayload.blockHash) - : null, + justifiedEpoch: stateJustifiedEpoch, justifiedRoot: toHexString(state.currentJustifiedCheckpoint.root), finalizedEpoch: finalizedCheckpoint.epoch, finalizedRoot: toHexString(state.finalizedCheckpoint.root), + + ...(merge.isMergeBlockBodyType(block.body) && + merge.isMergeStateType(state) && + merge.isExecutionEnabled(state, block.body) + ? { + executionPayloadBlockHash: toHexString(block.body.executionPayload.blockHash), + executionStatus: this.getPostMergeExecStatus(preCachedData), + } + : {executionPayloadBlockHash: null, executionStatus: this.getPreMergeExecStatus(preCachedData)}), }); } @@ -629,6 +635,35 @@ export class ForkChoice implements IForkChoice { return newNode.slot - commonAncestor.slot; } + /** + * Optimistic sync validate till validated latest hash, invalidate any decendant branch if invalidate till hash provided + * TODO: implementation: + * 1. verify is_merge_block if the mergeblock has not yet been validated + * 2. Throw critical error and exit if a block in finalized chain gets invalidated + */ + validateLatestHash(_latestValidHash: RootHex, _invalidateTillHash: RootHex | null): void { + // Silently ignore for now if all calls were valid + return; + } + + private getPreMergeExecStatus(preCachedData?: OnBlockPrecachedData): ExecutionStatus.PreMerge { + const executionStatus = preCachedData?.executionStatus || ExecutionStatus.PreMerge; + if (executionStatus !== ExecutionStatus.PreMerge) + throw Error(`Invalid pre-merge execution status: expected: ${ExecutionStatus.PreMerge}, got ${executionStatus}`); + return executionStatus; + } + + private getPostMergeExecStatus( + preCachedData?: OnBlockPrecachedData + ): ExecutionStatus.Valid | ExecutionStatus.Syncing { + const executionStatus = preCachedData?.executionStatus || ExecutionStatus.Syncing; + if (executionStatus === ExecutionStatus.PreMerge) + throw Error( + `Invalid post-merge execution status: expected: ${ExecutionStatus.Syncing} or ${ExecutionStatus.Valid} , got ${executionStatus}` + ); + return executionStatus; + } + private updateJustified(justifiedCheckpoint: CheckpointWithHex, justifiedBalances: number[]): void { this.synced = false; this.justifiedBalances = justifiedBalances; @@ -922,6 +957,10 @@ function assertValidTerminalPowBlock( ); } else { // If no TERMINAL_BLOCK_HASH override, check ttd + + // Delay powBlock checks if the payload execution status is unknown because of syncing response in executePayload call while verifying + if (preCachedData?.executionStatus === ExecutionStatus.Syncing) return; + const {powBlock, powBlockParent} = preCachedData || {}; if (!powBlock) throw Error("onBlock preCachedData must include powBlock"); if (!powBlockParent) throw Error("onBlock preCachedData must include powBlock"); diff --git a/packages/fork-choice/src/forkChoice/interface.ts b/packages/fork-choice/src/forkChoice/interface.ts index ebfdae2ed4c9..e2b1b093da52 100644 --- a/packages/fork-choice/src/forkChoice/interface.ts +++ b/packages/fork-choice/src/forkChoice/interface.ts @@ -1,5 +1,5 @@ import {Epoch, Slot, ValidatorIndex, phase0, allForks, Root, RootHex} from "@chainsafe/lodestar-types"; -import {IProtoBlock} from "../protoArray/interface"; +import {IProtoBlock, ExecutionStatus} from "../protoArray/interface"; import {CheckpointWithHex} from "./store"; export type CheckpointHex = { @@ -132,6 +132,10 @@ export interface IForkChoice { getBlockSummariesAtSlot(slot: Slot): IProtoBlock[]; /** Returns the distance of common ancestor of nodes to newNode. Returns null if newNode is descendant of prevNode */ getCommonAncestorDistance(prevBlock: IProtoBlock, newBlock: IProtoBlock): number | null; + /** + * Optimistic sync validate till validated latest hash, invalidate any decendant branch if invalidated branch decendant provided + */ + validateLatestHash(latestValidHash: RootHex, invalidateTillHash: RootHex | null): void; } /** Same to the PowBlock but we want RootHex to work with forkchoice conveniently */ @@ -150,14 +154,19 @@ export type OnBlockPrecachedData = { * powBlock = getPowBlock((block as merge.BeaconBlock).body.executionPayload.parentHash) * ``` */ - powBlock?: PowBlockHex; + powBlock?: PowBlockHex | null; /** * POW chain block's block parent, from getPowBlock() `eth_getBlockByHash` JSON RPC endpoint * ```ts * const powParent = getPowBlock(powBlock.parentHash); * ``` */ - powBlockParent?: PowBlockHex; + powBlockParent?: PowBlockHex | null; + /** + * Optimistic sync fields + */ + isMergeBlock?: boolean; + executionStatus?: ExecutionStatus; }; export interface ILatestMessage { diff --git a/packages/fork-choice/src/index.ts b/packages/fork-choice/src/index.ts index 1590503838cc..c4c75b6fb2f0 100644 --- a/packages/fork-choice/src/index.ts +++ b/packages/fork-choice/src/index.ts @@ -1,5 +1,5 @@ export {ProtoArray} from "./protoArray/protoArray"; -export {IProtoBlock, IProtoNode} from "./protoArray/interface"; +export {IProtoBlock, IProtoNode, ExecutionStatus} from "./protoArray/interface"; export {ForkChoice} from "./forkChoice/forkChoice"; export { diff --git a/packages/fork-choice/src/protoArray/interface.ts b/packages/fork-choice/src/protoArray/interface.ts index 47cf36bc0d49..41156aaff50c 100644 --- a/packages/fork-choice/src/protoArray/interface.ts +++ b/packages/fork-choice/src/protoArray/interface.ts @@ -13,12 +13,22 @@ export interface IVoteTracker { nextEpoch: Epoch; } +export enum ExecutionStatus { + Valid = "Valid", + Syncing = "Syncing", + PreMerge = "PreMerge", +} + +type BlockExecution = + | {executionPayloadBlockHash: RootHex; executionStatus: ExecutionStatus.Valid | ExecutionStatus.Syncing} + | {executionPayloadBlockHash: null; executionStatus: ExecutionStatus.PreMerge}; /** * A block that is to be applied to the fork choice * * A simplified version of BeaconBlock */ -export interface IProtoBlock { + +export type IProtoBlock = BlockExecution & { /** * The slot is not necessary for ProtoArray, * it just exists so external components can easily query the block slot. @@ -39,25 +49,20 @@ export interface IProtoBlock { * it also just exists for upstream components (namely attestation verification) */ targetRoot: RootHex; - /** - * `executionPayloadBlockHash` is not necessary for ProtoArray either. - * Here to do ExecutionEngine.notify_forkchoice_updated() easier. - * TODO: Check with other teams if this is the optimal strategy - */ - executionPayloadBlockHash: RootHex | null; + justifiedEpoch: Epoch; justifiedRoot: RootHex; finalizedEpoch: Epoch; finalizedRoot: RootHex; -} +}; /** * A block root with additional metadata required to form a DAG * with vote weights and best blocks stored as metadata */ -export interface IProtoNode extends IProtoBlock { +export type IProtoNode = IProtoBlock & { parent?: number; weight: number; bestChild?: number; bestDescendant?: number; -} +}; diff --git a/packages/fork-choice/src/protoArray/protoArray.ts b/packages/fork-choice/src/protoArray/protoArray.ts index e7e6d98a7b90..1bdd8c5a559f 100644 --- a/packages/fork-choice/src/protoArray/protoArray.ts +++ b/packages/fork-choice/src/protoArray/protoArray.ts @@ -50,7 +50,7 @@ export class ProtoArray { ...block, // We are using the blockROot as the targetRoot, since it always lies on an epoch boundary targetRoot: block.blockRoot, - }); + } as IProtoBlock); return protoArray; } diff --git a/packages/fork-choice/test/unit/forkChoice/forkChoice.test.ts b/packages/fork-choice/test/unit/forkChoice/forkChoice.test.ts index aeb2a8752026..4fd938fe0e1a 100644 --- a/packages/fork-choice/test/unit/forkChoice/forkChoice.test.ts +++ b/packages/fork-choice/test/unit/forkChoice/forkChoice.test.ts @@ -1,4 +1,4 @@ -import {ForkChoice, IForkChoiceStore, IProtoBlock, ProtoArray} from "../../../src"; +import {ForkChoice, IForkChoiceStore, IProtoBlock, ProtoArray, ExecutionStatus} from "../../../src"; import {config} from "@chainsafe/lodestar-config/default"; import {expect} from "chai"; import {fromHexString} from "@chainsafe/ssz"; @@ -18,12 +18,15 @@ describe("Forkchoice", function () { stateRoot, parentRoot, blockRoot: finalizedRoot, - executionPayloadBlockHash: null, + justifiedEpoch: genesisEpoch, justifiedRoot: genesisRoot, finalizedEpoch: genesisEpoch, finalizedRoot: genesisRoot, - }); + + executionPayloadBlockHash: null, + executionStatus: ExecutionStatus.PreMerge, + } as Omit); // Add block that is a finalized descendant. const block: IProtoBlock = { @@ -32,11 +35,14 @@ describe("Forkchoice", function () { parentRoot: finalizedRoot, stateRoot, targetRoot: finalizedRoot, - executionPayloadBlockHash: null, + justifiedEpoch: genesisEpoch, justifiedRoot: genesisRoot, finalizedEpoch: genesisEpoch, finalizedRoot: genesisRoot, + + executionPayloadBlockHash: null, + executionStatus: ExecutionStatus.PreMerge, }; const fcStore: IForkChoiceStore = { diff --git a/packages/fork-choice/test/unit/protoArray/getCommonAncestor.test.ts b/packages/fork-choice/test/unit/protoArray/getCommonAncestor.test.ts index 1ddf7cb1fda8..147387bd0a48 100644 --- a/packages/fork-choice/test/unit/protoArray/getCommonAncestor.test.ts +++ b/packages/fork-choice/test/unit/protoArray/getCommonAncestor.test.ts @@ -1,5 +1,5 @@ import {expect} from "chai"; -import {ProtoArray} from "../../../src"; +import {ProtoArray, ExecutionStatus} from "../../../src"; describe("getCommonAncestor", () => { const blocks: {slot: number; root: string; parent: string}[] = [ @@ -29,11 +29,13 @@ describe("getCommonAncestor", () => { stateRoot: "-", parentRoot: "-", blockRoot: "0", - executionPayloadBlockHash: null, + justifiedEpoch: 0, justifiedRoot: "-", finalizedEpoch: 0, finalizedRoot: "-", + + ...{executionPayloadBlockHash: null, executionStatus: ExecutionStatus.PreMerge}, }); for (const block of blocks) { @@ -43,11 +45,13 @@ describe("getCommonAncestor", () => { parentRoot: block.parent, stateRoot: "-", targetRoot: "-", - executionPayloadBlockHash: null, + justifiedEpoch: 0, justifiedRoot: "-", finalizedEpoch: 0, finalizedRoot: "-", + + ...{executionPayloadBlockHash: null, executionStatus: ExecutionStatus.PreMerge}, }); } diff --git a/packages/fork-choice/test/unit/protoArray/protoArray.test.ts b/packages/fork-choice/test/unit/protoArray/protoArray.test.ts index 4b6c9d62bd1e..057d8aebd827 100644 --- a/packages/fork-choice/test/unit/protoArray/protoArray.test.ts +++ b/packages/fork-choice/test/unit/protoArray/protoArray.test.ts @@ -1,7 +1,7 @@ import {RootHex} from "@chainsafe/lodestar-types"; import {expect} from "chai"; -import {ProtoArray} from "../../../src"; +import {ProtoArray, ExecutionStatus} from "../../../src"; describe("ProtoArray", () => { it("finalized descendant", () => { @@ -19,11 +19,13 @@ describe("ProtoArray", () => { stateRoot, parentRoot, blockRoot: finalizedRoot, - executionPayloadBlockHash: null, + justifiedEpoch: genesisEpoch, justifiedRoot: stateRoot, finalizedEpoch: genesisEpoch, finalizedRoot: stateRoot, + + ...{executionPayloadBlockHash: null, executionStatus: ExecutionStatus.PreMerge}, }); // Add block that is a finalized descendant. @@ -33,11 +35,13 @@ describe("ProtoArray", () => { parentRoot: finalizedRoot, stateRoot, targetRoot: finalizedRoot, - executionPayloadBlockHash: null, + justifiedEpoch: genesisEpoch, justifiedRoot: stateRoot, finalizedEpoch: genesisEpoch, finalizedRoot: stateRoot, + + ...{executionPayloadBlockHash: null, executionStatus: ExecutionStatus.PreMerge}, }); // Add block that is *not* a finalized descendant. @@ -47,11 +51,13 @@ describe("ProtoArray", () => { parentRoot: unknown, stateRoot, targetRoot: finalizedRoot, - executionPayloadBlockHash: null, + justifiedEpoch: genesisEpoch, justifiedRoot: stateRoot, finalizedEpoch: genesisEpoch, finalizedRoot: stateRoot, + + ...{executionPayloadBlockHash: null, executionStatus: ExecutionStatus.PreMerge}, }); // ancestorRoot, descendantRoot, isDescendant diff --git a/packages/lodestar/src/api/impl/validator/index.ts b/packages/lodestar/src/api/impl/validator/index.ts index e7f1b4878f25..ff0ba10fa6c1 100644 --- a/packages/lodestar/src/api/impl/validator/index.ts +++ b/packages/lodestar/src/api/impl/validator/index.ts @@ -15,6 +15,8 @@ import { SYNC_COMMITTEE_SUBNET_COUNT, } from "@chainsafe/lodestar-params"; import {allForks, Root, Slot, ValidatorIndex, ssz} from "@chainsafe/lodestar-types"; +import {ExecutionStatus} from "@chainsafe/lodestar-fork-choice"; + import {assembleBlock} from "../../../chain/factory/block"; import {AttestationError, AttestationErrorCode, GossipAction, SyncCommitteeError} from "../../../chain/errors"; import {validateGossipAggregateAndProof} from "../../../chain/validation"; @@ -133,6 +135,38 @@ export function getValidatorApi({chain, config, logger, metrics, network, sync}: } } + /** + * Post merge, the CL and EL could be out of step in the sync, and could result in + * Syncing status of the chain head. To be precise: + * 1. CL could be ahead of the EL, with the validity of head payload not yet verified + * 2. CL could be on an invalid chain of execution blocks with a non-existent + * or non-available parent that never syncs up + * + * Both the above scenarios could be problematic and hence validator shouldn't participate + * or weigh its vote on a head till it resolves to a Valid execution status. + * Following activities should be skipped on an Optimistic head (with Syncing status): + * 1. Attestation if targetRoot is optimistic + * 2. SyncCommitteeContribution if if the root for which to produce contribution is Optimistic. + * 3. ProduceBlock if the parentRoot (chain's current head is optimistic). However this doesn't + * need to be checked/aborted here as assembleBody would call EL's api for the latest + * executionStatus of the parentRoot. If still not validated, produceBlock will throw error. + * + * TODO/PENDING: SyncCommitteeSignatures should also be aborted, the best way to address this + * is still in flux and will be updated as and when other CL's figure this out. + */ + + function notOnOptimisticBlockRoot(beaconBlockRoot: Root): void { + const protoBeaconBlock = chain.forkChoice.getBlock(beaconBlockRoot); + if (!protoBeaconBlock) { + throw new ApiError(400, "Block not in forkChoice"); + } + + if (protoBeaconBlock.executionStatus === ExecutionStatus.Syncing) + throw new NodeIsSyncing( + `Block's execution payload not yet validated, executionPayloadBlockHash=${protoBeaconBlock.executionPayloadBlockHash}` + ); + } + const produceBlock: routes.validator.Api["produceBlockV2"] = async function produceBlock( slot, randaoReveal, @@ -195,6 +229,10 @@ export function getValidatorApi({chain, config, logger, metrics, network, sync}: headBlockRoot : getBlockRootAtSlot(headState, targetSlot); + // Check the execution status as validator shouldn't vote on an optimistic head + // Check on target is sufficient as a valid target would imply a valid source + notOnOptimisticBlockRoot(targetRoot); + // To get the correct source we must get a state in the same epoch as the attestation's epoch. // An epoch transition may change state.currentJustifiedCheckpoint const attEpochState = @@ -226,6 +264,9 @@ export function getValidatorApi({chain, config, logger, metrics, network, sync}: * @param beaconBlockRoot The block root for which to produce the contribution. */ async produceSyncCommitteeContribution(slot, subcommitteeIndex, beaconBlockRoot) { + // Check the execution status as validator shouldn't contribute on an optimistic head + notOnOptimisticBlockRoot(beaconBlockRoot); + const contribution = chain.syncCommitteeMessagePool.getContribution(subcommitteeIndex, slot, beaconBlockRoot); if (!contribution) throw new ApiError(500, "No contribution available"); return {data: contribution}; diff --git a/packages/lodestar/src/chain/blocks/importBlock.ts b/packages/lodestar/src/chain/blocks/importBlock.ts index 62eac8668464..fab1802e4e46 100644 --- a/packages/lodestar/src/chain/blocks/importBlock.ts +++ b/packages/lodestar/src/chain/blocks/importBlock.ts @@ -9,7 +9,7 @@ import { altair, computeEpochAtSlot, } from "@chainsafe/lodestar-beacon-state-transition"; -import {IForkChoice, OnBlockPrecachedData} from "@chainsafe/lodestar-fork-choice"; +import {IForkChoice, OnBlockPrecachedData, ExecutionStatus} from "@chainsafe/lodestar-fork-choice"; import {ILogger} from "@chainsafe/lodestar-utils"; import {IChainForkConfig} from "@chainsafe/lodestar-config"; import {IMetrics} from "../../metrics"; @@ -59,8 +59,7 @@ export type ImportBlockModules = { * - Send events after everything is done */ export async function importBlock(chain: ImportBlockModules, fullyVerifiedBlock: FullyVerifiedBlock): Promise { - const {block, postState, parentBlock, skipImportingAttestations} = fullyVerifiedBlock; - + const {block, postState, parentBlock, skipImportingAttestations, executionStatus} = fullyVerifiedBlock; const pendingEvents = new PendingEvents(chain.emitter); // - Observe attestations @@ -80,7 +79,7 @@ export async function importBlock(chain: ImportBlockModules, fullyVerifiedBlock: // current justified checkpoint should be prev epoch or current epoch if it's just updated // it should always have epochBalances there bc it's a checkpoint state, ie got through processEpoch const justifiedCheckpoint = postState.currentJustifiedCheckpoint; - const onBlockPrecachedData: OnBlockPrecachedData = {}; + const onBlockPrecachedData: OnBlockPrecachedData = {executionStatus}; if (justifiedCheckpoint.epoch > chain.forkChoice.getJustifiedCheckpoint().epoch) { const state = getStateForJustifiedBalances(chain, postState, block); onBlockPrecachedData.justifiedBalances = getEffectiveBalances(state); @@ -94,12 +93,15 @@ export async function importBlock(chain: ImportBlockModules, fullyVerifiedBlock: // pow_block = get_pow_block(block.body.execution_payload.parent_hash) const powBlockRootHex = toHexString(block.message.body.executionPayload.parentHash); const powBlock = await chain.eth1.getPowBlock(powBlockRootHex); - if (!powBlock) throw Error(`merge block parent POW block not found ${powBlockRootHex}`); + if (!powBlock && executionStatus !== ExecutionStatus.Syncing) + throw Error(`merge block parent POW block not found ${powBlockRootHex}`); // pow_parent = get_pow_block(pow_block.parent_hash) - const powBlockParent = await chain.eth1.getPowBlock(powBlock.parentHash); - if (!powBlockParent) throw Error(`merge block parent's parent POW block not found ${powBlock.parentHash}`); + const powBlockParent = powBlock && (await chain.eth1.getPowBlock(powBlock.parentHash)); + if (powBlock && !powBlockParent) + throw Error(`merge block parent's parent POW block not found ${powBlock.parentHash}`); onBlockPrecachedData.powBlock = powBlock; onBlockPrecachedData.powBlockParent = powBlockParent; + onBlockPrecachedData.isMergeBlock = true; } const prevFinalizedEpoch = chain.forkChoice.getFinalizedCheckpoint().epoch; diff --git a/packages/lodestar/src/chain/blocks/types.ts b/packages/lodestar/src/chain/blocks/types.ts index b5f68d6dff2a..6d873dafca7f 100644 --- a/packages/lodestar/src/chain/blocks/types.ts +++ b/packages/lodestar/src/chain/blocks/types.ts @@ -1,5 +1,5 @@ import {CachedBeaconState} from "@chainsafe/lodestar-beacon-state-transition"; -import {IProtoBlock} from "@chainsafe/lodestar-fork-choice"; +import {IProtoBlock, ExecutionStatus} from "@chainsafe/lodestar-fork-choice"; import {allForks} from "@chainsafe/lodestar-types"; export type FullyVerifiedBlockFlags = { @@ -19,6 +19,10 @@ export type FullyVerifiedBlockFlags = { * Used by range sync. */ ignoreIfFinalized?: boolean; + /** + * If the execution payload couldnt be verified because of EL syncing status, used in optimistic sync or for merge block + */ + executionStatus?: ExecutionStatus; }; export type PartiallyVerifiedBlockFlags = FullyVerifiedBlockFlags & { diff --git a/packages/lodestar/src/chain/blocks/verifyBlock.ts b/packages/lodestar/src/chain/blocks/verifyBlock.ts index b88604b27ad6..6452c40f7ece 100644 --- a/packages/lodestar/src/chain/blocks/verifyBlock.ts +++ b/packages/lodestar/src/chain/blocks/verifyBlock.ts @@ -1,7 +1,7 @@ import {ssz} from "@chainsafe/lodestar-types"; import {CachedBeaconState, computeStartSlotAtEpoch, allForks, merge} from "@chainsafe/lodestar-beacon-state-transition"; import {toHexString} from "@chainsafe/ssz"; -import {IForkChoice, IProtoBlock} from "@chainsafe/lodestar-fork-choice"; +import {IForkChoice, IProtoBlock, ExecutionStatus} from "@chainsafe/lodestar-fork-choice"; import {IChainForkConfig} from "@chainsafe/lodestar-config"; import {ILogger} from "@chainsafe/lodestar-utils"; import {IMetrics} from "../../metrics"; @@ -36,13 +36,14 @@ export async function verifyBlock( ): Promise { const parentBlock = verifyBlockSanityChecks(chain, partiallyVerifiedBlock); - const postState = await verifyBlockStateTransition(chain, partiallyVerifiedBlock, opts); + const {postState, executionStatus} = await verifyBlockStateTransition(chain, partiallyVerifiedBlock, opts); return { block: partiallyVerifiedBlock.block, postState, parentBlock, skipImportingAttestations: partiallyVerifiedBlock.skipImportingAttestations, + executionStatus, }; } @@ -113,7 +114,7 @@ export async function verifyBlockStateTransition( chain: VerifyBlockModules, partiallyVerifiedBlock: PartiallyVerifiedBlock, opts: BlockProcessOpts -): Promise> { +): Promise<{postState: CachedBeaconState; executionStatus: ExecutionStatus}> { const {block, validProposerSignature, validSignatures} = partiallyVerifiedBlock; // TODO: Skip in process chain segment @@ -161,9 +162,10 @@ export async function verifyBlockStateTransition( } } + let executionStatus = ExecutionStatus.PreMerge; if (executionPayloadEnabled) { // TODO: Handle better executePayload() returning error is syncing - const status = await chain.executionEngine.executePayload( + const execResult = await chain.executionEngine.executePayload( // executionPayload must be serialized as JSON and the TreeBacked structure breaks the baseFeePerGas serializer // For clarity and since it's needed anyway, just send the struct representation at this level such that // executePayload() can expect a regular JS object. @@ -171,28 +173,37 @@ export async function verifyBlockStateTransition( executionPayloadEnabled.valueOf() as typeof executionPayloadEnabled ); - switch (status) { + switch (execResult.status) { case ExecutePayloadStatus.VALID: + executionStatus = ExecutionStatus.Valid; + chain.forkChoice.validateLatestHash(execResult.latestValidHash, null); break; // OK - case ExecutePayloadStatus.INVALID: + case ExecutePayloadStatus.INVALID: { + // If the parentRoot is not same as latestValidHash, then the branch from latestValidHash + // to parentRoot needs to be invalidated + const parentHashHex = toHexString(block.message.parentRoot); + chain.forkChoice.validateLatestHash( + execResult.latestValidHash, + parentHashHex !== execResult.latestValidHash ? parentHashHex : null + ); throw new BlockError(block, {code: BlockErrorCode.EXECUTION_PAYLOAD_NOT_VALID}); + } case ExecutePayloadStatus.SYNCING: - // It's okay to ignore SYNCING status because: - // - We MUST verify execution payloads of blocks we attest - // - We are NOT REQUIRED to check the execution payload of blocks we don't attest - // When EL syncs from genesis to a chain post-merge, it doesn't know what the head, CL knows. However, we - // must verify (complete this fn) and import a block to sync. Since we are syncing we only need to verify - // consensus and trust that whatever the chain agrees is valid, is valid; no need to verify. When we - // verify consensus up to the head we notify forkchoice update head and then EL can sync to our head. At that - // point regular EL sync kicks in and it does verify the execution payload (EL blocks). If after syncing EL - // gets to an invalid payload or we can prepare payloads on what we consider the head that's a critical error - // - // TODO: Exit with critical error if we can't prepare payloads on top of what we consider head. - if (partiallyVerifiedBlock.fromRangeSync) { - break; - } else { - throw new BlockError(block, {code: BlockErrorCode.EXECUTION_ENGINE_SYNCING}); + // It's okay to ignore SYNCING status as EL could switch into syncing + // 1. On intial startup/restart + // 2. When some reorg might have occured and EL doesn't has a parent root + // (observed on devnets) + // 3. Because of some unavailable (and potentially invalid) root but there is no way + // of knowing if this is invalid/unavailable. For unavailable block, some proposer + // will (sooner or later) build on the available parent head which will + // eventually win in fork-choice as other validators vote on VALID blocks. + // Once EL catches up again and respond VALID, the fork choice will be updated which + // will either validate or prune invalid blocks + executionStatus = ExecutionStatus.Syncing; + if (execResult.latestValidHash) { + chain.forkChoice.validateLatestHash(execResult.latestValidHash, null); } + break; } } @@ -201,5 +212,5 @@ export async function verifyBlockStateTransition( throw new BlockError(block, {code: BlockErrorCode.INVALID_STATE_ROOT, preState, postState}); } - return postState; + return {postState, executionStatus}; } diff --git a/packages/lodestar/src/chain/factory/block/body.ts b/packages/lodestar/src/chain/factory/block/body.ts index 7f559c8cb60f..726d6a5a4130 100644 --- a/packages/lodestar/src/chain/factory/block/body.ts +++ b/packages/lodestar/src/chain/factory/block/body.ts @@ -97,14 +97,20 @@ export async function assembleBody( let executionPayload: merge.ExecutionPayload | null = null; try { + // prepareExecutionPayload will throw error via notifyForkchoiceUpdate if + // the EL returns Syncing on this request to prepare a payload const payloadId = await prepareExecutionPayload( chain, finalizedBlockHash ?? ZERO_HASH_HEX, currentState as CachedBeaconState, feeRecipient ); - if (payloadId) executionPayload = await chain.executionEngine.getPayload(payloadId); + executionPayload = await chain.executionEngine.getPayload(payloadId); } catch (e) { + // 1. If Merge is complete, then fail hard i.e. can't produce block + // 2. Otherwise this was going to be mergeBlock, just propose a pre-merge block with + // empty execution and keep the chain going + if (merge.isMergeComplete(currentState as CachedBeaconState)) throw e; logger?.warn("Failed to produce execution payload", {}, e as Error); } @@ -127,7 +133,7 @@ async function prepareExecutionPayload( finalizedBlockHash: RootHex, state: CachedBeaconState, suggestedFeeRecipient: ExecutionAddress -): Promise { +): Promise { // Use different POW block hash parent for block production based on merge status. // Returned value of null == using an empty ExecutionPayload value let parentHash: Root; @@ -136,11 +142,15 @@ async function prepareExecutionPayload( !ssz.Root.equals(chain.config.TERMINAL_BLOCK_HASH, ZERO_HASH) && getCurrentEpoch(state) < chain.config.TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH ) - return null; + throw new Error( + `InvalidMergeTBH epoch: expected >= ${ + chain.config.TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH + }, actual: ${getCurrentEpoch(state)}` + ); const terminalPowBlockHash = chain.eth1.getTerminalPowBlock(); if (terminalPowBlockHash === null) { // Pre-merge, no prepare payload call is needed - return null; + throw new Error("InvalidTerminalPow: terminal pow block not found yet"); } else { // Signify merge via producing on top of the last PoW block parentHash = terminalPowBlockHash; @@ -152,11 +162,13 @@ async function prepareExecutionPayload( const timestamp = computeTimeAtSlot(chain.config, state.slot, state.genesisTime); const random = getRandaoMix(state, state.currentShuffling.epoch); - return await chain.executionEngine.notifyForkchoiceUpdate(parentHash, finalizedBlockHash, { + const payloadId = await chain.executionEngine.notifyForkchoiceUpdate(parentHash, finalizedBlockHash, { timestamp, random, suggestedFeeRecipient, }); + if (!payloadId) throw new Error("InvalidPayloadId: Null"); + return payloadId; } /** process_sync_committee_contributions is implemented in syncCommitteeContribution.getSyncAggregate */ diff --git a/packages/lodestar/src/chain/forkChoice/index.ts b/packages/lodestar/src/chain/forkChoice/index.ts index cc36e8dd0ee3..18882a7cc527 100644 --- a/packages/lodestar/src/chain/forkChoice/index.ts +++ b/packages/lodestar/src/chain/forkChoice/index.ts @@ -5,13 +5,14 @@ import {toHexString} from "@chainsafe/ssz"; import {allForks, Slot} from "@chainsafe/lodestar-types"; import {IChainForkConfig} from "@chainsafe/lodestar-config"; -import {ForkChoice, ProtoArray, ForkChoiceStore} from "@chainsafe/lodestar-fork-choice"; +import {ForkChoice, ProtoArray, ForkChoiceStore, ExecutionStatus} from "@chainsafe/lodestar-fork-choice"; import {getEffectiveBalances, CachedBeaconState, merge} from "@chainsafe/lodestar-beacon-state-transition"; import {computeAnchorCheckpoint} from "../initState"; import {ChainEventEmitter} from "../emitter"; import {IMetrics} from "../../metrics"; import {ChainEvent} from "../emitter"; +import {GENESIS_SLOT} from "../../constants"; export type ForkChoiceOpts = { terminalTotalDifficulty?: bigint; @@ -53,14 +54,18 @@ export function initializeForkChoice( parentRoot: toHexString(blockHeader.parentRoot), stateRoot: toHexString(blockHeader.stateRoot), blockRoot: toHexString(checkpoint.root), - // TODO: Review if correct after merge interop - executionPayloadBlockHash: merge.isMergeStateType(state) - ? toHexString(state.latestExecutionPayloadHeader.blockHash) - : null, + justifiedEpoch: justifiedCheckpoint.epoch, justifiedRoot: toHexString(justifiedCheckpoint.root), finalizedEpoch: finalizedCheckpoint.epoch, finalizedRoot: toHexString(finalizedCheckpoint.root), + + ...(merge.isMergeStateType(state) && merge.isMergeComplete(state) + ? { + executionPayloadBlockHash: toHexString(state.latestExecutionPayloadHeader.blockHash), + executionStatus: blockHeader.slot === GENESIS_SLOT ? ExecutionStatus.Valid : ExecutionStatus.Syncing, + } + : {executionPayloadBlockHash: null, executionStatus: ExecutionStatus.PreMerge}), }), justifiedBalances, diff --git a/packages/lodestar/src/executionEngine/http.ts b/packages/lodestar/src/executionEngine/http.ts index 2cbbe0beb59d..3b0ae817f37e 100644 --- a/packages/lodestar/src/executionEngine/http.ts +++ b/packages/lodestar/src/executionEngine/http.ts @@ -1,5 +1,7 @@ import {AbortSignal} from "@chainsafe/abort-controller"; import {merge, RootHex, Root} from "@chainsafe/lodestar-types"; +import {BYTES_PER_LOGS_BLOOM} from "@chainsafe/lodestar-params"; + import {JsonRpcHttpClient} from "../eth1/provider/jsonRpcHttpClient"; import { bytesToData, @@ -13,13 +15,13 @@ import { import {IJsonRpcHttpClient} from "../eth1/provider/jsonRpcHttpClient"; import { ExecutePayloadStatus, + ExecutePayloadResponse, ForkChoiceUpdateStatus, IExecutionEngine, PayloadId, PayloadAttributes, ApiPayloadAttributes, } from "./interface"; -import {BYTES_PER_LOGS_BLOOM} from "@chainsafe/lodestar-params"; export type ExecutionEngineHttpOpts = { urls: string[]; @@ -63,23 +65,27 @@ export class ExecutionEngineHttp implements IExecutionEngine { * 6. If the parent block is a PoW block as per EIP-3675 definition, then all missing dependencies of the payload MUST be pulled from the network and validated accordingly. The call MUST be responded according to the validity of the payload and the chain of its ancestors. * If the parent block is a PoS block as per EIP-3675 definition, then the call MAY be responded with SYNCING status and sync process SHOULD be initiated accordingly. */ - async executePayload(executionPayload: merge.ExecutionPayload): Promise { + async executePayload(executionPayload: merge.ExecutionPayload): Promise { const method = "engine_executePayloadV1"; - const {status} = await this.rpc.fetch< + const serializedExecutionPayload = serializeExecutionPayload(executionPayload); + const {status, latestValidHash} = await this.rpc.fetch< EngineApiRpcReturnTypes[typeof method], EngineApiRpcParamTypes[typeof method] >({ method, - params: [serializeExecutionPayload(executionPayload)], + params: [serializedExecutionPayload], }); // Validate status is known const statusEnum = ExecutePayloadStatus[status]; if (statusEnum === undefined) { - throw Error(`Unknown status ${status}`); + throw Error(`Invalid EL status on executePayload: ${status}`); + } + if (statusEnum !== ExecutePayloadStatus.SYNCING && latestValidHash == null) { + throw Error(`Invalid latestValidHash for ${status}`); } - return statusEnum; + return {status, latestValidHash}; } /** @@ -180,7 +186,7 @@ type EngineApiRpcReturnTypes = { * Object - Response object: * - status: String - the result of the payload execution: */ - engine_executePayloadV1: {status: ExecutePayloadStatus}; + engine_executePayloadV1: {status: ExecutePayloadStatus; latestValidHash: DATA}; engine_consensusValidated: void; engine_forkchoiceUpdatedV1: {status: ForkChoiceUpdateStatus; payloadId: QUANTITY}; /** diff --git a/packages/lodestar/src/executionEngine/interface.ts b/packages/lodestar/src/executionEngine/interface.ts index ea92c166eb78..b44b20643a10 100644 --- a/packages/lodestar/src/executionEngine/interface.ts +++ b/packages/lodestar/src/executionEngine/interface.ts @@ -1,5 +1,6 @@ import {merge, Root, RootHex} from "@chainsafe/lodestar-types"; import {ByteVector} from "@chainsafe/ssz"; + import {DATA, QUANTITY} from "../eth1/provider/utils"; // An execution engine can produce a payload id anywhere the the uint64 range // Since we do no processing with this id, we have no need to deserialize it @@ -14,6 +15,10 @@ export enum ExecutePayloadStatus { SYNCING = "SYNCING", } +export type ExecutePayloadResponse = + | {status: ExecutePayloadStatus.SYNCING; latestValidHash: RootHex | null} + | {status: ExecutePayloadStatus.VALID | ExecutePayloadStatus.INVALID; latestValidHash: RootHex}; + export enum ForkChoiceUpdateStatus { /** given payload is valid */ SUCCESS = "SUCCESS", @@ -51,7 +56,7 @@ export interface IExecutionEngine { * * Should be called in advance before, after or in parallel to block processing */ - executePayload(executionPayload: merge.ExecutionPayload): Promise; + executePayload(executionPayload: merge.ExecutionPayload): Promise; /** * Signal fork choice updates diff --git a/packages/lodestar/src/executionEngine/mock.ts b/packages/lodestar/src/executionEngine/mock.ts index 5dce758253b3..9aed18e35133 100644 --- a/packages/lodestar/src/executionEngine/mock.ts +++ b/packages/lodestar/src/executionEngine/mock.ts @@ -2,7 +2,13 @@ import crypto from "crypto"; import {merge, RootHex, Root} from "@chainsafe/lodestar-types"; import {toHexString} from "@chainsafe/ssz"; import {ZERO_HASH, ZERO_HASH_HEX} from "../constants"; -import {ExecutePayloadStatus, IExecutionEngine, PayloadId, PayloadAttributes} from "./interface"; +import { + ExecutePayloadStatus, + ExecutePayloadResponse, + IExecutionEngine, + PayloadId, + PayloadAttributes, +} from "./interface"; import {BYTES_PER_LOGS_BLOOM} from "@chainsafe/lodestar-params"; const INTEROP_GAS_LIMIT = 30e6; @@ -52,14 +58,14 @@ export class ExecutionEngineMock implements IExecutionEngine { * 6. If the parent block is a PoW block as per EIP-3675 definition, then all missing dependencies of the payload MUST be pulled from the network and validated accordingly. The call MUST be responded according to the validity of the payload and the chain of its ancestors. * If the parent block is a PoS block as per EIP-3675 definition, then the call MAY be responded with SYNCING status and sync process SHOULD be initiated accordingly. */ - async executePayload(executionPayload: merge.ExecutionPayload): Promise { + async executePayload(executionPayload: merge.ExecutionPayload): Promise { // Only validate that parent is known if (!this.knownBlocks.has(toHexString(executionPayload.parentHash))) { - return ExecutePayloadStatus.INVALID; + return {status: ExecutePayloadStatus.INVALID, latestValidHash: this.headBlockRoot}; } this.knownBlocks.set(toHexString(executionPayload.blockHash), executionPayload); - return ExecutePayloadStatus.VALID; + return {status: ExecutePayloadStatus.VALID, latestValidHash: toHexString(executionPayload.blockHash)}; } /** diff --git a/packages/lodestar/src/node/notifier.ts b/packages/lodestar/src/node/notifier.ts index 19c8c3cf4322..93232dc4e154 100644 --- a/packages/lodestar/src/node/notifier.ts +++ b/packages/lodestar/src/node/notifier.ts @@ -2,7 +2,7 @@ import {IBeaconConfig} from "@chainsafe/lodestar-config"; import {ErrorAborted, ILogger, sleep, prettyBytes} from "@chainsafe/lodestar-utils"; import {AbortSignal} from "@chainsafe/abort-controller"; import {EPOCHS_PER_SYNC_COMMITTEE_PERIOD, SLOTS_PER_EPOCH} from "@chainsafe/lodestar-params"; -import {computeEpochAtSlot} from "@chainsafe/lodestar-beacon-state-transition"; +import {computeEpochAtSlot, merge} from "@chainsafe/lodestar-beacon-state-transition"; import {IBeaconChain} from "../chain"; import {INetwork} from "../network"; import {IBeaconSync, SyncState} from "../sync"; @@ -60,6 +60,15 @@ export async function runNodeNotifier({ const peersRow = `peers: ${connectedPeerCount}`; const finalizedCheckpointRow = `finalized: ${prettyBytes(finalizedRoot)}:${finalizedEpoch}`; const headRow = `head: ${headInfo.slot} ${prettyBytes(headInfo.blockRoot)}`; + const isMergeComplete = merge.isMergeStateType(headState) && merge.isMergeComplete(headState); + const mergeInfo = isMergeComplete + ? [ + `execution: ${headInfo.executionStatus.toLowerCase()}(${prettyBytes( + headInfo.executionPayloadBlockHash ?? "empty" + )})`, + ] + : []; + // Give info about empty slots if head < clock const skippedSlots = clockSlot - headInfo.slot; const clockSlotRow = `slot: ${clockSlot}` + (skippedSlots > 0 ? ` (skipped ${skippedSlots})` : ""); @@ -79,6 +88,7 @@ export async function runNodeNotifier({ `${slotsPerSecond.toPrecision(3)} slots/s`, clockSlotRow, headRow, + ...mergeInfo, finalizedCheckpointRow, peersRow, ]; @@ -87,13 +97,13 @@ export async function runNodeNotifier({ case SyncState.Synced: { // Synced - clock - head - finalized - peers - nodeState = ["Synced", clockSlotRow, headRow, finalizedCheckpointRow, peersRow]; + nodeState = ["Synced", clockSlotRow, headRow, ...mergeInfo, finalizedCheckpointRow, peersRow]; break; } case SyncState.Stalled: { // Searching peers - peers - head - finalized - clock - nodeState = ["Searching peers", peersRow, clockSlotRow, headRow, finalizedCheckpointRow]; + nodeState = ["Searching peers", peersRow, clockSlotRow, headRow, ...mergeInfo, finalizedCheckpointRow]; } } logger.info(nodeState.join(" - ")); diff --git a/packages/lodestar/test/sim/merge-interop.test.ts b/packages/lodestar/test/sim/merge-interop.test.ts index 5316235d8647..2f7bc46e531f 100644 --- a/packages/lodestar/test/sim/merge-interop.test.ts +++ b/packages/lodestar/test/sim/merge-interop.test.ts @@ -10,6 +10,7 @@ import {IChainConfig} from "@chainsafe/lodestar-config"; import {Epoch} from "@chainsafe/lodestar-types"; import {merge} from "@chainsafe/lodestar-beacon-state-transition"; +import {ExecutePayloadStatus} from "../../src/executionEngine/interface"; import {ExecutionEngineHttp} from "../../src/executionEngine/http"; import {shell} from "./shell"; import {ChainEvent} from "../../src/chain"; @@ -191,7 +192,7 @@ describe("executionEngine / ExecutionEngineHttp", function () { **/ const payloadResult = await executionEngine.executePayload(payload); - if (!payloadResult) { + if (payloadResult.status !== ExecutePayloadStatus.VALID) { throw Error("getPayload returned payload that executePayload deems invalid"); } diff --git a/packages/lodestar/test/unit/api/impl/beacon/blocks/utils.test.ts b/packages/lodestar/test/unit/api/impl/beacon/blocks/utils.test.ts index 219243022f46..b12bc23461ea 100644 --- a/packages/lodestar/test/unit/api/impl/beacon/blocks/utils.test.ts +++ b/packages/lodestar/test/unit/api/impl/beacon/blocks/utils.test.ts @@ -1,5 +1,5 @@ import {SinonStubbedInstance} from "sinon"; -import {ForkChoice, IProtoBlock} from "@chainsafe/lodestar-fork-choice"; +import {ForkChoice, IProtoBlock, ExecutionStatus} from "@chainsafe/lodestar-fork-choice"; import {resolveBlockId} from "../../../../../../src/api/impl/beacon/blocks/utils"; import {expect, use} from "chai"; import {toHexString} from "@chainsafe/ssz"; @@ -32,11 +32,13 @@ describe("block api utils", function () { parentRoot: expectedRootHex, targetRoot: expectedRootHex, stateRoot: expectedRootHex, - executionPayloadBlockHash: null, + finalizedEpoch: 0, finalizedRoot: expectedRootHex, justifiedEpoch: 0, justifiedRoot: expectedRootHex, + + ...{executionPayloadBlockHash: null, executionStatus: ExecutionStatus.PreMerge}, }; }); diff --git a/packages/lodestar/test/unit/executionEngine/http.test.ts b/packages/lodestar/test/unit/executionEngine/http.test.ts index e0fceceefa0e..34f00cb46519 100644 --- a/packages/lodestar/test/unit/executionEngine/http.test.ts +++ b/packages/lodestar/test/unit/executionEngine/http.test.ts @@ -102,9 +102,13 @@ describe("ExecutionEngine / http", () => { }, ], }; - returnValue = {jsonrpc: "2.0", id: 67, result: {status: "VALID"}}; + returnValue = { + jsonrpc: "2.0", + id: 67, + result: {status: "VALID", latestValidHash: "0xb084c10440f05f5a23a55d1d7ebcb1b3892935fb56f23cdc9a7f42c348eed174"}, + }; - const status = await executionEngine.executePayload(parseExecutionPayload(request.params[0])); + const {status} = await executionEngine.executePayload(parseExecutionPayload(request.params[0])); expect(status).to.equal("VALID", "Wrong returned execute payload result"); expect(reqJsonRpcPayload).to.deep.equal(request, "Wrong request JSON RPC payload"); diff --git a/packages/lodestar/test/utils/block.ts b/packages/lodestar/test/utils/block.ts index 6fc4aa1c2968..eb13d4004c66 100644 --- a/packages/lodestar/test/utils/block.ts +++ b/packages/lodestar/test/utils/block.ts @@ -3,7 +3,7 @@ import {config as defaultConfig} from "@chainsafe/lodestar-config/default"; import {IChainForkConfig} from "@chainsafe/lodestar-config"; import {allForks, phase0} from "@chainsafe/lodestar-types"; import {List} from "@chainsafe/ssz"; -import {IProtoBlock} from "@chainsafe/lodestar-fork-choice"; +import {IProtoBlock, ExecutionStatus} from "@chainsafe/lodestar-fork-choice"; import {isPlainObject} from "@chainsafe/lodestar-utils"; import {RecursivePartial} from "@chainsafe/lodestar-utils"; import deepmerge from "deepmerge"; @@ -96,11 +96,13 @@ export function generateEmptyProtoBlock(): IProtoBlock { parentRoot: rootHex, stateRoot: rootHex, targetRoot: rootHex, - executionPayloadBlockHash: null, + justifiedEpoch: 0, justifiedRoot: rootHex, finalizedEpoch: 0, finalizedRoot: rootHex, + + ...{executionPayloadBlockHash: null, executionStatus: ExecutionStatus.PreMerge}, }; } diff --git a/packages/lodestar/test/utils/mocks/chain/chain.ts b/packages/lodestar/test/utils/mocks/chain/chain.ts index 3cb11514358a..24c3f987a18e 100644 --- a/packages/lodestar/test/utils/mocks/chain/chain.ts +++ b/packages/lodestar/test/utils/mocks/chain/chain.ts @@ -6,7 +6,7 @@ import {allForks, Number64, Root, Slot, ssz, Uint16, Uint64} from "@chainsafe/lo import {IBeaconConfig} from "@chainsafe/lodestar-config"; import {CachedBeaconState, createCachedBeaconState} from "@chainsafe/lodestar-beacon-state-transition"; import {phase0} from "@chainsafe/lodestar-beacon-state-transition"; -import {CheckpointWithHex, IForkChoice, IProtoBlock} from "@chainsafe/lodestar-fork-choice"; +import {CheckpointWithHex, IForkChoice, IProtoBlock, ExecutionStatus} from "@chainsafe/lodestar-fork-choice"; import {ChainEventEmitter, IBeaconChain} from "../../../../src/chain"; import {IBeaconClock} from "../../../../src/chain/clock/interface"; @@ -177,11 +177,13 @@ function mockForkChoice(): IForkChoice { parentRoot: rootHex, stateRoot: rootHex, targetRoot: rootHex, - executionPayloadBlockHash: null, + justifiedEpoch: 0, justifiedRoot: rootHex, finalizedEpoch: 0, finalizedRoot: rootHex, + + ...{executionPayloadBlockHash: null, executionStatus: ExecutionStatus.PreMerge}, }; const checkpoint: CheckpointWithHex = {epoch: 0, root, rootHex}; @@ -218,5 +220,6 @@ function mockForkChoice(): IForkChoice { getBlockSummariesByParentRoot: () => [block], getBlockSummariesAtSlot: () => [block], getCommonAncestorDistance: () => null, + validateLatestHash: () => {}, }; } diff --git a/packages/lodestar/test/utils/validationData/attestation.ts b/packages/lodestar/test/utils/validationData/attestation.ts index 8d7091121936..20a9d2470132 100644 --- a/packages/lodestar/test/utils/validationData/attestation.ts +++ b/packages/lodestar/test/utils/validationData/attestation.ts @@ -5,7 +5,7 @@ import { computeSigningRoot, computeStartSlotAtEpoch, } from "@chainsafe/lodestar-beacon-state-transition"; -import {IProtoBlock, IForkChoice} from "@chainsafe/lodestar-fork-choice"; +import {IProtoBlock, IForkChoice, ExecutionStatus} from "@chainsafe/lodestar-fork-choice"; import {DOMAIN_BEACON_ATTESTER} from "@chainsafe/lodestar-params"; import {phase0, Slot, ssz} from "@chainsafe/lodestar-types"; import {IBeaconChain} from "../../../src/chain"; @@ -59,11 +59,13 @@ export function getAttestationValidData( parentRoot: ZERO_HASH_HEX, stateRoot: ZERO_HASH_HEX, targetRoot: toHexString(targetRoot), - executionPayloadBlockHash: null, + justifiedEpoch: 0, justifiedRoot: ZERO_HASH_HEX, finalizedEpoch: 0, finalizedRoot: ZERO_HASH_HEX, + + ...{executionPayloadBlockHash: null, executionStatus: ExecutionStatus.PreMerge}, }; const forkChoice = ({ getBlock: (root) => {