chore: remove merge transition code (#8680)

**Motiviation**

All networks have completed the merge transition and most execution
clients no longer support pre-merge so it's not even possible anymore to
run a network from a genesis before bellatrix, unless you keep it to
phase0/altair only, which still works after this PR is merged.

This code is effectively tech debt, no longer exercised and just gets in
the way when doing refactors.

**Description**

Removes all code related to performing the merge transition. Running the
node pre-merge (CL only mode) is still possible and syncing still works.
Also removed a few CLI flags we added for the merge specifically, those
shouldn't be used anymore. Spec constants like
`TERMINAL_TOTAL_DIFFICULTY` are kept for spec compliance and ssz types
(like `PowBlock`) as well. I had to disable a few spec tests related to
handling the merge block since those code paths are removed.

Closes https://github.com/ChainSafe/lodestar/issues/8661
This commit is contained in:
Nico Flaig
2025-12-12 04:18:23 +01:00
committed by GitHub
parent 1ddbe5d870
commit 889b1c4475
74 changed files with 199 additions and 3119 deletions

View File

@@ -1,7 +1,5 @@
# We use these images during sim and e2e tests
# This is the last version which supports pre/post merge chains in the same network
# All newer versions only work with post merge chains
GETH_DOCKER_IMAGE=ethereum/client-go:v1.16.2
GETH_DOCKER_IMAGE=ethereum/client-go:v1.16.7
# Use either image or local binary for the testing
GETH_BINARY_DIR=
LIGHTHOUSE_DOCKER_IMAGE=ethpandaops/lighthouse:unstable-d235f2c

View File

@@ -1,58 +0,0 @@
name: Sim merge execution/builder tests
concurrency:
# If PR, cancel prev commits. head_ref = source branch name on pull_request, null if push
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
on:
push:
# We intentionally don't run push on feature branches. See PR for rational.
branches: [unstable, stable]
pull_request:
workflow_dispatch:
env:
GETH_IMAGE: ethereum/client-go:v1.10.25
NETHERMIND_IMAGE: nethermind/nethermind:1.14.3
MERGEMOCK_IMAGE: g11tech/mergemock:latest
GETH_WITHDRAWALS_IMAGE: g11tech/geth:withdrawalsfeb8
ETHEREUMJS_WITHDRAWALS_IMAGE: g11tech/ethereumjs:blobs-b6b63
NETHERMIND_WITHDRAWALS_IMAGE: nethermindeth/nethermind:withdrawals_yolo
ETHEREUMJS_BLOBS_IMAGE: g11tech/ethereumjs:blobs-b6b63
jobs:
sim-merge-tests:
name: Sim merge tests
runs-on: buildjet-4vcpu-ubuntu-2204
steps:
- uses: actions/checkout@v4
- uses: "./.github/actions/setup-and-build"
with:
node: 24
- name: Pull Geth
run: docker pull $GETH_IMAGE
- name: Pull Nethermind
run: docker pull $NETHERMIND_IMAGE
- name: Pull mergemock
run: docker pull $MERGEMOCK_IMAGE
- name: Test Lodestar <> mergemock relay
run: yarn test:sim:mergemock
working-directory: packages/beacon-node
env:
EL_BINARY_DIR: ${{ env.MERGEMOCK_IMAGE }}
EL_SCRIPT_DIR: mergemock
LODESTAR_PRESET: mainnet
ENGINE_PORT: 8551
ETH_PORT: 8661
- name: Upload debug log test files
if: ${{ always() }}
uses: actions/upload-artifact@v4
with:
name: debug-test-logs
path: packages/beacon-node/test-logs

File diff suppressed because it is too large Load Diff

View File

@@ -101,4 +101,3 @@ To set up a local testnet with a Post-Merge configuration, you may need to add t
- `--params.ALTAIR_FORK_EPOCH 0`
- `--params.BELLATRIX_FORK_EPOCH 0`
- `--terminal-total-difficulty-override 0`

View File

@@ -16,7 +16,6 @@ describe("beacon / config", () => {
PRESET_BASE: "mainnet",
DEPOSIT_CONTRACT_ADDRESS: "0xff50ed3d0ec03ac01d4c79aad74928bff48a7b2b",
GENESIS_FORK_VERSION: "0x00001020",
TERMINAL_TOTAL_DIFFICULTY: "115792089237316195423570985008687907853269984665640564039457584007913129639936",
MIN_GENESIS_TIME: "1606824000",
};

View File

@@ -103,7 +103,6 @@
"test:unit": "vitest run --project unit --project unit-minimal",
"test:e2e": "vitest run --project e2e --project e2e-mainnet",
"test:sim": "vitest run test/sim/**/*.test.ts",
"test:sim:mergemock": "vitest run test/sim/mergemock.test.ts",
"test:sim:blobs": "vitest run test/sim/4844-interop.test.ts",
"download-spec-tests": "node --loader=ts-node/esm test/spec/downloadTests.ts",
"test:spec:bls": "vitest run --project spec-minimal test/spec/bls/",

View File

@@ -1,4 +1,3 @@
import {ChainForkConfig} from "@lodestar/config";
import {ExecutionStatus, ProtoBlock} from "@lodestar/fork-choice";
import {ForkName, isForkPostFulu} from "@lodestar/params";
import {
@@ -7,8 +6,7 @@ import {
computeEpochAtSlot,
isStateValidatorsNodesPopulated,
} from "@lodestar/state-transition";
import {IndexedAttestation, bellatrix, deneb} from "@lodestar/types";
import {Logger, toRootHex} from "@lodestar/utils";
import {IndexedAttestation, deneb} from "@lodestar/types";
import type {BeaconChain} from "../chain.js";
import {BlockError, BlockErrorCode} from "../errors/index.js";
import {BlockProcessOpts} from "../options.js";
@@ -18,7 +16,6 @@ import {ImportBlockOpts} from "./types.js";
import {DENEB_BLOWFISH_BANNER} from "./utils/blowfishBanner.js";
import {ELECTRA_GIRAFFE_BANNER} from "./utils/giraffeBanner.js";
import {CAPELLA_OWL_BANNER} from "./utils/ownBanner.js";
import {POS_PANDA_MERGE_TRANSITION_BANNER} from "./utils/pandaMergeTransitionBanner.js";
import {FULU_ZEBRA_BANNER} from "./utils/zebraBanner.js";
import {verifyBlocksDataAvailability} from "./verifyBlocksDataAvailability.js";
import {SegmentExecStatus, verifyBlocksExecutionPayload} from "./verifyBlocksExecutionPayloads.js";
@@ -103,7 +100,6 @@ export async function verifyBlocksInEpoch(
: Promise.resolve({
execAborted: null,
executionStatuses: blocks.map((_blk) => ExecutionStatus.Syncing),
mergeBlockFound: null,
} as SegmentExecStatus);
// Store indexed attestations for each block to avoid recomputing them during import
@@ -163,12 +159,6 @@ export async function verifyBlocksInEpoch(
]);
if (opts.verifyOnly !== true) {
if (segmentExecStatus.execAborted === null && segmentExecStatus.mergeBlockFound !== null) {
// merge block found and is fully valid = state transition + signatures + execution payload.
// TODO: Will this banner be logged during syncing?
logOnPowBlock(this.logger, this.config, segmentExecStatus.mergeBlockFound);
}
const fromForkBoundary = this.config.getForkBoundaryAtEpoch(computeEpochAtSlot(parentBlock.slot));
const toForkBoundary = this.config.getForkBoundaryAtEpoch(computeEpochAtSlot(lastBlock.message.slot));
@@ -251,16 +241,3 @@ export async function verifyBlocksInEpoch(
abortController.abort();
}
}
function logOnPowBlock(logger: Logger, config: ChainForkConfig, mergeBlock: bellatrix.BeaconBlock): void {
const mergeBlockHash = toRootHex(config.getForkTypes(mergeBlock.slot).BeaconBlock.hashTreeRoot(mergeBlock));
const mergeExecutionHash = toRootHex(mergeBlock.body.executionPayload.blockHash);
const mergePowHash = toRootHex(mergeBlock.body.executionPayload.parentHash);
logger.info(POS_PANDA_MERGE_TRANSITION_BANNER);
logger.info("Execution transitioning from PoW to PoS!!!");
logger.info("Importing block referencing terminal PoW block", {
blockHash: mergeBlockHash,
executionHash: mergeExecutionHash,
powHash: mergePowHash,
});
}

View File

@@ -6,19 +6,11 @@ import {
LVHValidResponse,
MaybeValidExecutionStatus,
ProtoBlock,
assertValidTerminalPowBlock,
} from "@lodestar/fork-choice";
import {ForkSeq, SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY} from "@lodestar/params";
import {
CachedBeaconStateAllForks,
isExecutionBlockBodyType,
isExecutionEnabled,
isExecutionStateType,
isMergeTransitionBlock as isMergeTransitionBlockFn,
} from "@lodestar/state-transition";
import {Slot, bellatrix, electra} from "@lodestar/types";
import {ForkSeq} from "@lodestar/params";
import {CachedBeaconStateAllForks, isExecutionBlockBodyType, isExecutionStateType} from "@lodestar/state-transition";
import {bellatrix, electra} from "@lodestar/types";
import {ErrorAborted, Logger, toRootHex} from "@lodestar/utils";
import {IEth1ForBlockProduction} from "../../eth1/index.js";
import {ExecutionPayloadStatus, IExecutionEngine} from "../../execution/engine/interface.js";
import {Metrics} from "../../metrics/metrics.js";
import {IClock} from "../../util/clock.js";
@@ -29,7 +21,6 @@ import {IBlockInput} from "./blockInput/types.js";
import {ImportBlockOpts} from "./types.js";
export type VerifyBlockExecutionPayloadModules = {
eth1: IEth1ForBlockProduction;
executionEngine: IExecutionEngine;
clock: IClock;
logger: Logger;
@@ -44,9 +35,8 @@ export type SegmentExecStatus =
execAborted: null;
executionStatuses: MaybeValidExecutionStatus[];
executionTime: number;
mergeBlockFound: bellatrix.BeaconBlock | null;
}
| {execAborted: ExecAbortType; invalidSegmentLVH?: LVHInvalidResponse; mergeBlockFound: null};
| {execAborted: ExecAbortType; invalidSegmentLVH?: LVHInvalidResponse};
type VerifyExecutionErrorResponse =
| {executionStatus: ExecutionStatus.Invalid; lvhResponse: LVHInvalidResponse; execError: BlockError}
@@ -72,7 +62,6 @@ export async function verifyBlocksExecutionPayload(
opts: BlockProcessOpts & ImportBlockOpts
): Promise<SegmentExecStatus> {
const executionStatuses: MaybeValidExecutionStatus[] = [];
let mergeBlockFound: bellatrix.BeaconBlock | null = null;
const recvToValLatency = Date.now() / 1000 - (opts.seenTimestampSec ?? Date.now() / 1000);
const lastBlock = blockInputs.at(-1);
@@ -96,57 +85,9 @@ export async function verifyBlocksExecutionPayload(
// will either validate or prune invalid blocks
//
// We need to track and keep updating if its safe to optimistically import these blocks.
// The following is how we determine for a block if its safe:
//
// (but we need to modify this check for this segment of blocks because it checks if the
// parent of any block imported in forkchoice is post-merge and currently we could only
// have blocks[0]'s parent imported in the chain as this is no longer one by one verify +
// import.)
//
//
// When to import such blocks:
// From: https://github.com/ethereum/consensus-specs/pull/2844
// A block MUST NOT be optimistically imported, unless either of the following
// conditions are met:
//
// 1. Parent of the block has execution
//
// Since with the sync optimizations, the previous block might not have been in the
// forkChoice yet, so the below check could fail for safeSlotsToImportOptimistically
//
// Luckily, we can depend on the preState0 to see if we are already post merge w.r.t
// the blocks we are importing.
//
// Or in other words if
// - block status is syncing
// - and we are not in a post merge world and is parent is not optimistically safe
// - and we are syncing close to the chain head i.e. clock slot
// - and parent is optimistically safe
//
// then throw error
//
//
// - if we haven't yet imported a post merge ancestor in forkchoice i.e.
// - and we are syncing close to the clockSlot, i.e. merge Transition could be underway
//
//
// 2. The current slot (as per the system clock) is at least
// SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY ahead of the slot of the block being
// imported.
// This means that the merge transition could be underway and we can't afford to import
// a block which is not fully validated as it could affect liveliness of the network.
//
//
// For this segment of blocks:
// We are optimistically safe with respect to this entire block segment if:
// - all the blocks are way behind the current slot
// - or we have already imported a post-merge parent of first block of this chain in forkchoice
const currentSlot = chain.clock.currentSlot;
const safeSlotsToImportOptimistically = opts.safeSlotsToImportOptimistically ?? SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY;
let isOptimisticallySafe =
parentBlock.executionStatus !== ExecutionStatus.PreMerge ||
lastBlock.slot + safeSlotsToImportOptimistically < currentSlot;
for (let blockIndex = 0; blockIndex < blockInputs.length; blockIndex++) {
const blockInput = blockInputs[blockIndex];
// If blocks are invalid in consensus the main promise could resolve before this loop ends.
@@ -154,14 +95,7 @@ export async function verifyBlocksExecutionPayload(
if (signal.aborted) {
throw new ErrorAborted("verifyBlockExecutionPayloads");
}
const verifyResponse = await verifyBlockExecutionPayload(
chain,
blockInput,
preState0,
opts,
isOptimisticallySafe,
currentSlot
);
const verifyResponse = await verifyBlockExecutionPayload(chain, blockInput, preState0);
// If execError has happened, then we need to extract the segmentExecStatus and return
if (verifyResponse.execError !== null) {
@@ -170,75 +104,7 @@ export async function verifyBlocksExecutionPayload(
// If we are here then its because executionStatus is one of MaybeValidExecutionStatus
const {executionStatus} = verifyResponse;
// It becomes optimistically safe for following blocks if a post-merge block is deemed fit
// for import. If it would not have been safe verifyBlockExecutionPayload would have
// returned execError and loop would have been aborted
if (executionStatus !== ExecutionStatus.PreMerge) {
isOptimisticallySafe = true;
}
executionStatuses.push(executionStatus);
const blockBody = blockInput.getBlock().message.body;
const isMergeTransitionBlock =
// If the merge block is found, stop the search as the isMergeTransitionBlockFn condition
// will still evaluate to true for the following blocks leading to errors (while syncing)
// as the preState0 still belongs to the pre state of the first block on segment
mergeBlockFound === null &&
isExecutionStateType(preState0) &&
isExecutionBlockBodyType(blockBody) &&
isMergeTransitionBlockFn(preState0, blockBody);
// If this is a merge transition block, check to ensure if it references
// a valid terminal PoW block.
//
// However specs define this check to be run inside forkChoice's onBlock
// (https://github.com/ethereum/consensus-specs/blob/dev/specs/bellatrix/fork-choice.md#on_block)
// but we perform the check here (as inspired from the lighthouse impl)
//
// Reasons:
// 1. If the block is not valid, we should fail early and not wait till
// forkChoice import.
// 2. It makes logical sense to pair it with the block validations and
// deal it with the external services like eth1 tracker here than
// in import block
if (isMergeTransitionBlock) {
const mergeBlock = blockInput.getBlock().message as bellatrix.BeaconBlock;
const mergeBlockHash = toRootHex(chain.config.getForkTypes(mergeBlock.slot).BeaconBlock.hashTreeRoot(mergeBlock));
const powBlockRootHex = toRootHex(mergeBlock.body.executionPayload.parentHash);
const powBlock = await chain.eth1.getPowBlock(powBlockRootHex).catch((error) => {
// Lets just warn the user here, errors if any will be reported on
// `assertValidTerminalPowBlock` checks
chain.logger.warn(
"Error fetching terminal PoW block referred in the merge transition block",
{powBlockHash: powBlockRootHex, mergeBlockHash},
error
);
return null;
});
const powBlockParent =
powBlock &&
(await chain.eth1.getPowBlock(powBlock.parentHash).catch((error) => {
// Lets just warn the user here, errors if any will be reported on
// `assertValidTerminalPowBlock` checks
chain.logger.warn(
"Error fetching parent of the terminal PoW block referred in the merge transition block",
{powBlockParentHash: powBlock.parentHash, powBlock: powBlockRootHex, mergeBlockHash},
error
);
return null;
}));
// executionStatus will never == ExecutionStatus.PreMerge if it's the mergeBlock. But gotta make TS happy =D
if (executionStatus === ExecutionStatus.PreMerge) {
throw Error("Merge block must not have executionStatus == PreMerge");
}
assertValidTerminalPowBlock(chain.config, mergeBlock, {executionStatus, powBlock, powBlockParent});
// Valid execution payload, but may not be in a valid beacon chain block. Delay printing the POS ACTIVATED banner
// to the end of the verify block routine, which confirms that this block is fully valid.
mergeBlockFound = mergeBlock;
}
}
const executionTime = Date.now();
@@ -265,7 +131,6 @@ export async function verifyBlocksExecutionPayload(
execAborted: null,
executionStatuses,
executionTime,
mergeBlockFound,
};
}
@@ -275,28 +140,18 @@ export async function verifyBlocksExecutionPayload(
export async function verifyBlockExecutionPayload(
chain: VerifyBlockExecutionPayloadModules,
blockInput: IBlockInput,
preState0: CachedBeaconStateAllForks,
opts: BlockProcessOpts,
isOptimisticallySafe: boolean,
currentSlot: Slot
preState0: CachedBeaconStateAllForks
): Promise<VerifyBlockExecutionResponse> {
const block = blockInput.getBlock();
/** Not null if execution is enabled */
const executionPayloadEnabled =
isExecutionStateType(preState0) &&
isExecutionBlockBodyType(block.message.body) &&
// Safe to use with a state previous to block's preState. isMergeComplete can only transition from false to true.
// - If preState0 is after merge block: condition is true, and will always be true
// - If preState0 is before merge block: the block could lie but then state transition function will throw above
// It is kinda safe to send non-trusted payloads to the execution client because at most it can trigger sync.
// TODO: If this becomes a problem, do some basic verification beforehand, like checking the proposer signature.
isExecutionEnabled(preState0, block.message)
isExecutionStateType(preState0) && isExecutionBlockBodyType(block.message.body)
? block.message.body.executionPayload
: null;
if (!executionPayloadEnabled) {
// isExecutionEnabled() -> false
return {executionStatus: ExecutionStatus.PreMerge, execError: null} as VerifyBlockExecutionResponse;
// Pre-merge block, no execution payload to verify
return {executionStatus: ExecutionStatus.PreMerge, lvhResponse: undefined, execError: null};
}
// TODO: Handle better notifyNewPayload() returning error is syncing
@@ -343,24 +198,10 @@ export async function verifyBlockExecutionPayload(
}
// Accepted and Syncing have the same treatment, as final validation of block is pending
// Post-merge, we're always safe to optimistically import
case ExecutionPayloadStatus.ACCEPTED:
case ExecutionPayloadStatus.SYNCING: {
// Check if the entire segment was deemed safe or, this block specifically itself if not in
// the safeSlotsToImportOptimistically window of current slot, then we can import else
// we need to throw and not import his block
const safeSlotsToImportOptimistically =
opts.safeSlotsToImportOptimistically ?? SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY;
if (!isOptimisticallySafe && blockInput.slot + safeSlotsToImportOptimistically >= currentSlot) {
const execError = new BlockError(block, {
code: BlockErrorCode.EXECUTION_ENGINE_ERROR,
execStatus: ExecutionPayloadStatus.UNSAFE_OPTIMISTIC_STATUS,
errorMessage: `not safe to import ${execResult.status} payload within ${opts.safeSlotsToImportOptimistically} of currentSlot`,
});
return {executionStatus: null, execError} as VerifyBlockExecutionResponse;
}
case ExecutionPayloadStatus.SYNCING:
return {executionStatus: ExecutionStatus.Syncing, execError: null};
}
// If the block has is not valid, or it referenced an invalid terminal block then the
// block is invalid, however it has no bearing on any forkChoice cleanup

View File

@@ -3,7 +3,7 @@ import {PrivateKey} from "@libp2p/interface";
import {PubkeyIndexMap} from "@chainsafe/pubkey-index-map";
import {CompositeTypeAny, TreeView, Type} from "@chainsafe/ssz";
import {BeaconConfig} from "@lodestar/config";
import {CheckpointWithHex, ExecutionStatus, IForkChoice, ProtoBlock, UpdateHeadOpt} from "@lodestar/fork-choice";
import {CheckpointWithHex, IForkChoice, ProtoBlock, UpdateHeadOpt} from "@lodestar/fork-choice";
import {LoggerNode} from "@lodestar/logger/node";
import {EFFECTIVE_BALANCE_INCREMENT, GENESIS_SLOT, SLOTS_PER_EPOCH, isForkPostElectra} from "@lodestar/params";
import {
@@ -1177,17 +1177,6 @@ export class BeaconChain implements IBeaconChain {
this.seenAggregatedAttestations.prune(epoch);
this.seenBlockAttesters.prune(epoch);
this.beaconProposerCache.prune(epoch);
// Poll for merge block in the background to speed-up block production. Only if:
// - after BELLATRIX_FORK_EPOCH
// - Beacon node synced
// - head state not isMergeTransitionComplete
if (this.config.BELLATRIX_FORK_EPOCH - epoch < 1) {
const head = this.forkChoice.getHead();
if (epoch - computeEpochAtSlot(head.slot) < 5 && head.executionStatus === ExecutionStatus.PreMerge) {
this.eth1.startPollingMergeBlock();
}
}
}
protected onNewHead(head: ProtoBlock): void {

View File

@@ -18,7 +18,6 @@ import {
getBlockRootAtSlot,
getEffectiveBalanceIncrementsZeroInactive,
isExecutionStateType,
isMergeTransitionComplete,
} from "@lodestar/state-transition";
import {Slot, ssz} from "@lodestar/types";
import {Logger, toRootHex} from "@lodestar/utils";
@@ -135,7 +134,7 @@ export function initializeForkChoiceFromFinalizedState(
unrealizedFinalizedEpoch: finalizedCheckpoint.epoch,
unrealizedFinalizedRoot: toRootHex(finalizedCheckpoint.root),
...(isExecutionStateType(state) && isMergeTransitionComplete(state)
...(isExecutionStateType(state)
? {
executionPayloadBlockHash: toRootHex(state.latestExecutionPayloadHeader.blockHash),
executionPayloadNumber: state.latestExecutionPayloadHeader.blockNumber,
@@ -216,7 +215,7 @@ export function initializeForkChoiceFromUnfinalizedState(
unrealizedFinalizedEpoch: finalizedCheckpoint.epoch,
unrealizedFinalizedRoot: toRootHex(finalizedCheckpoint.root),
...(isExecutionStateType(unfinalizedState) && isMergeTransitionComplete(unfinalizedState)
...(isExecutionStateType(unfinalizedState)
? {
executionPayloadBlockHash: toRootHex(unfinalizedState.latestExecutionPayloadHeader.blockHash),
executionPayloadNumber: unfinalizedState.latestExecutionPayloadHeader.blockNumber,

View File

@@ -1,4 +1,3 @@
import {SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY} from "@lodestar/params";
import {defaultOptions as defaultValidatorOptions} from "@lodestar/validator";
import {DEFAULT_ARCHIVE_MODE} from "./archiveStore/constants.js";
import {ArchiveMode, ArchiveStoreOpts} from "./archiveStore/interface.js";
@@ -56,10 +55,6 @@ export type BlockProcessOpts = {
* Will double processing times. Use only for debugging purposes.
*/
disableBlsBatchVerify?: boolean;
/**
* Override SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY
*/
safeSlotsToImportOptimistically?: number;
/**
* Assert progressive balances the same to EpochTransitionCache
*/
@@ -109,7 +104,6 @@ export const defaultChainOptions: IChainOptions = {
proposerBoost: true,
proposerBoostReorg: true,
computeUnrealized: true,
safeSlotsToImportOptimistically: SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY,
suggestedFeeRecipient: defaultValidatorOptions.suggestedFeeRecipient,
serveHistoricalState: false,
assertCorrectProgressiveBalances: false,

View File

@@ -197,7 +197,7 @@ export class PrepareNextSlotScheduler {
this.chain.opts.emitPayloadAttributes === true &&
this.chain.emitter.listenerCount(routes.events.EventType.payloadAttributes)
) {
const data = await getPayloadAttributesForSSE(fork as ForkPostBellatrix, this.chain, {
const data = getPayloadAttributesForSSE(fork as ForkPostBellatrix, this.chain, {
prepareState: updatedPrepareState,
prepareSlot,
parentBlockRoot: fromHex(headRoot),

View File

@@ -17,10 +17,8 @@ import {
CachedBeaconStateCapella,
CachedBeaconStateExecutions,
computeTimeAtSlot,
getCurrentEpoch,
getExpectedWithdrawals,
getRandaoMix,
isMergeTransitionComplete,
} from "@lodestar/state-transition";
import {
BLSPubkey,
@@ -44,12 +42,9 @@ import {
deneb,
electra,
fulu,
ssz,
sszTypesFor,
} from "@lodestar/types";
import {Logger, sleep, toHex, toPubkeyHex, toRootHex} from "@lodestar/utils";
import {ZERO_HASH, ZERO_HASH_HEX} from "../../constants/index.js";
import {IEth1ForBlockProduction} from "../../eth1/index.js";
import {ZERO_HASH_HEX} from "../../constants/index.js";
import {numToQuantity} from "../../eth1/provider/utils.js";
import {
IExecutionBuilder,
@@ -337,14 +332,6 @@ export async function produceBlockBody<T extends BlockType>(
feeRecipient
);
if (prepareRes.isPremerge) {
return {
...prepareRes,
executionPayload: sszTypesFor(fork).ExecutionPayload.defaultValue(),
executionPayloadValue: BigInt(0),
};
}
const {prepType, payloadId} = prepareRes;
Object.assign(logMeta, {executionPayloadPrepType: prepType});
@@ -366,37 +353,14 @@ export async function produceBlockBody<T extends BlockType>(
return {...prepareRes, ...payloadRes};
})().catch((e) => {
// catch payload fetch here, because there is still a recovery path possible if we
// are pre-merge. We don't care the same for builder segment as the execution block
// will takeover if the builder flow was activated and errors
this.metrics?.blockPayload.payloadFetchErrors.inc();
if (!isMergeTransitionComplete(currentState as CachedBeaconStateBellatrix)) {
this.logger?.warn(
"Fetch payload from the execution failed, however since we are still pre-merge proceeding with an empty one.",
{},
e as Error
);
// ok we don't have an execution payload here, so we can assign an empty one
// if pre-merge
return {
isPremerge: true as const,
executionPayload: sszTypesFor(fork).ExecutionPayload.defaultValue(),
executionPayloadValue: BigInt(0),
};
}
// since merge transition is complete, we need a valid payload even if with an
// empty (transactions) one. defaultValue isn't gonna cut it!
throw e;
});
const [engineRes, commonBlockBody] = await Promise.all([enginePromise, commonBlockBodyPromise]);
blockBody = Object.assign({}, commonBlockBody) as AssembledBodyType<BlockType.Blinded>;
if (engineRes.isPremerge) {
(blockBody as BeaconBlockBody<ForkPostBellatrix & ForkPreGloas>).executionPayload = engineRes.executionPayload;
executionPayloadValue = engineRes.executionPayloadValue;
} else {
{
const {prepType, payloadId, executionPayload, blobsBundle, executionRequests} = engineRes;
shouldOverrideBuilder = engineRes.shouldOverrideBuilder;
@@ -504,15 +468,10 @@ export async function produceBlockBody<T extends BlockType>(
}
/**
* Produce ExecutionPayload for pre-merge, merge, and post-merge.
*
* Expects `eth1MergeBlockFinder` to be actively searching for blocks well in advance to being called.
*
* @returns PayloadId = pow block found, null = pow NOT found
* Produce ExecutionPayload for post-merge.
*/
export async function prepareExecutionPayload(
chain: {
eth1: IEth1ForBlockProduction;
executionEngine: IExecutionEngine;
config: ChainForkConfig;
},
@@ -523,14 +482,8 @@ export async function prepareExecutionPayload(
finalizedBlockHash: RootHex,
state: CachedBeaconStateExecutions,
suggestedFeeRecipient: string
): Promise<{isPremerge: true} | {isPremerge: false; prepType: PayloadPreparationType; payloadId: PayloadId}> {
const parentHashRes = await getExecutionPayloadParentHash(chain, state);
if (parentHashRes.isPremerge) {
// Return null only if the execution is pre-merge
return {isPremerge: true};
}
const {parentHash} = parentHashRes;
): Promise<{prepType: PayloadPreparationType; payloadId: PayloadId}> {
const parentHash = state.latestExecutionPayloadHeader.blockHash;
const timestamp = computeTimeAtSlot(chain.config, state.slot, state.genesisTime);
const prevRandao = getRandaoMix(state, state.epochCtx.epoch);
@@ -586,12 +539,11 @@ export async function prepareExecutionPayload(
// We are only returning payloadId here because prepareExecutionPayload is also called from
// prepareNextSlot, which is an advance call to execution engine to start building payload
// Actual payload isn't produced till getPayload is called.
return {isPremerge: false, payloadId, prepType};
return {payloadId, prepType};
}
async function prepareExecutionPayloadHeader(
chain: {
eth1: IEth1ForBlockProduction;
executionBuilder?: IExecutionBuilder;
config: ChainForkConfig;
},
@@ -608,53 +560,13 @@ async function prepareExecutionPayloadHeader(
throw Error("executionBuilder required");
}
const parentHashRes = await getExecutionPayloadParentHash(chain, state);
if (parentHashRes.isPremerge) {
throw Error("External builder disabled pre-merge");
}
const {parentHash} = parentHashRes;
const parentHash = state.latestExecutionPayloadHeader.blockHash;
return chain.executionBuilder.getHeader(fork, state.slot, parentHash, proposerPubKey);
}
export async function getExecutionPayloadParentHash(
chain: {
eth1: IEth1ForBlockProduction;
config: ChainForkConfig;
},
state: CachedBeaconStateExecutions
): Promise<{isPremerge: true} | {isPremerge: false; parentHash: Root}> {
// Use different POW block hash parent for block production based on merge status.
// Returned value of null == using an empty ExecutionPayload value
if (isMergeTransitionComplete(state)) {
// Post-merge, normal payload
return {isPremerge: false, parentHash: state.latestExecutionPayloadHeader.blockHash};
}
if (
!ssz.Root.equals(chain.config.TERMINAL_BLOCK_HASH, ZERO_HASH) &&
getCurrentEpoch(state) < chain.config.TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH
) {
throw new Error(
`InvalidMergeTBH epoch: expected >= ${
chain.config.TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH
}, actual: ${getCurrentEpoch(state)}`
);
}
const terminalPowBlockHash = await chain.eth1.getTerminalPowBlock();
if (terminalPowBlockHash === null) {
// Pre-merge, no prepare payload call is needed
return {isPremerge: true};
}
// Signify merge via producing on top of the last PoW block
return {isPremerge: false, parentHash: terminalPowBlockHash};
}
export async function getPayloadAttributesForSSE(
export function getPayloadAttributesForSSE(
fork: ForkPostBellatrix,
chain: {
eth1: IEth1ForBlockProduction;
config: ChainForkConfig;
},
{
@@ -663,30 +575,23 @@ export async function getPayloadAttributesForSSE(
parentBlockRoot,
feeRecipient,
}: {prepareState: CachedBeaconStateExecutions; prepareSlot: Slot; parentBlockRoot: Root; feeRecipient: string}
): Promise<SSEPayloadAttributes> {
const parentHashRes = await getExecutionPayloadParentHash(chain, prepareState);
if (!parentHashRes.isPremerge) {
const {parentHash} = parentHashRes;
const payloadAttributes = preparePayloadAttributes(fork, chain, {
prepareState,
prepareSlot,
parentBlockRoot,
feeRecipient,
});
const ssePayloadAttributes: SSEPayloadAttributes = {
proposerIndex: prepareState.epochCtx.getBeaconProposer(prepareSlot),
proposalSlot: prepareSlot,
parentBlockNumber: prepareState.latestExecutionPayloadHeader.blockNumber,
parentBlockRoot,
parentBlockHash: parentHash,
payloadAttributes,
};
return ssePayloadAttributes;
}
throw Error("The execution is still pre-merge");
): SSEPayloadAttributes {
const parentHash = prepareState.latestExecutionPayloadHeader.blockHash;
const payloadAttributes = preparePayloadAttributes(fork, chain, {
prepareState,
prepareSlot,
parentBlockRoot,
feeRecipient,
});
const ssePayloadAttributes: SSEPayloadAttributes = {
proposerIndex: prepareState.epochCtx.getBeaconProposer(prepareSlot),
proposalSlot: prepareSlot,
parentBlockNumber: prepareState.latestExecutionPayloadHeader.blockNumber,
parentBlockRoot,
parentBlockHash: parentHash,
payloadAttributes,
};
return ssePayloadAttributes;
}
function preparePayloadAttributes(

View File

@@ -6,7 +6,6 @@ import {
computeTimeAtSlot,
getBlockProposerSignatureSet,
isExecutionBlockBodyType,
isExecutionEnabled,
isExecutionStateType,
} from "@lodestar/state-transition";
import {SignedBeaconBlock, deneb} from "@lodestar/types";
@@ -140,7 +139,7 @@ export async function validateGossipBlock(
if (fork === ForkName.bellatrix) {
if (!isExecutionBlockBodyType(block.body)) throw Error("Not merge block type");
const executionPayload = block.body.executionPayload;
if (isExecutionStateType(blockState) && isExecutionEnabled(blockState, block)) {
if (isExecutionStateType(blockState)) {
const expectedTimestamp = computeTimeAtSlot(config, blockSlot, chain.genesisTime);
if (executionPayload.timestamp !== computeTimeAtSlot(config, blockSlot, chain.genesisTime)) {
throw new BlockGossipError(GossipAction.REJECT, {

View File

@@ -1,328 +0,0 @@
import {ChainConfig} from "@lodestar/config";
import {RootHex} from "@lodestar/types";
import {Logger, pruneSetToMax, toRootHex} from "@lodestar/utils";
import {ZERO_HASH_HEX} from "../constants/index.js";
import {Metrics} from "../metrics/index.js";
import {enumToIndexMap} from "../util/enum.js";
import {EthJsonRpcBlockRaw, IEth1Provider, PowMergeBlock, PowMergeBlockTimestamp, TDProgress} from "./interface.js";
import {dataToRootHex, quantityToBigint, quantityToNum} from "./provider/utils.js";
export enum StatusCode {
STOPPED = "STOPPED",
SEARCHING = "SEARCHING",
FOUND = "FOUND",
}
type Status =
| {code: StatusCode.STOPPED}
| {code: StatusCode.SEARCHING}
| {code: StatusCode.FOUND; mergeBlock: PowMergeBlock};
/** For metrics, index order = declaration order of StatusCode */
const statusCodeIdx = enumToIndexMap(StatusCode);
/**
* Bounds `blocksByHashCache` cache, imposing a max distance between highest and lowest block numbers.
* In case of extreme forking the cache might grow unbounded.
*/
const MAX_CACHE_POW_BLOCKS = 1024;
const MAX_TD_RENDER_VALUE = Number.MAX_SAFE_INTEGER;
export type Eth1MergeBlockTrackerModules = {
config: ChainConfig;
logger: Logger;
signal: AbortSignal;
metrics: Metrics | null;
};
// get_pow_block_at_total_difficulty
/**
* Follows the eth1 chain to find a (or multiple?) merge blocks that cross the threshold of total terminal difficulty
*
* Finding the mergeBlock could be done in demand when proposing pre-merge blocks. However, that would slow block
* production during the weeks between BELLATRIX_EPOCH and TTD.
*/
export class Eth1MergeBlockTracker {
private readonly config: ChainConfig;
private readonly logger: Logger;
private readonly metrics: Metrics | null;
private readonly blocksByHashCache = new Map<RootHex, PowMergeBlock>();
private readonly intervals: NodeJS.Timeout[] = [];
private status: Status;
private latestEth1Block: PowMergeBlockTimestamp | null = null;
private getTerminalPowBlockFromEth1Promise: Promise<PowMergeBlock | null> | null = null;
private readonly safeTDFactor: bigint;
constructor(
{config, logger, signal, metrics}: Eth1MergeBlockTrackerModules,
private readonly eth1Provider: IEth1Provider
) {
this.config = config;
this.logger = logger;
this.metrics = metrics;
this.status = {code: StatusCode.STOPPED};
signal.addEventListener("abort", () => this.close(), {once: true});
this.safeTDFactor = getSafeTDFactor(this.config.TERMINAL_TOTAL_DIFFICULTY);
const scaledTTD = this.config.TERMINAL_TOTAL_DIFFICULTY / this.safeTDFactor;
// Only run metrics if necessary
if (metrics) {
// TTD can't be dynamically changed during execution, register metric once
metrics.eth1.eth1MergeTTD.set(Number(scaledTTD as bigint));
metrics.eth1.eth1MergeTDFactor.set(Number(this.safeTDFactor as bigint));
metrics.eth1.eth1MergeStatus.addCollect(() => {
// Set merge ttd, merge status and merge block status
metrics.eth1.eth1MergeStatus.set(statusCodeIdx[this.status.code]);
if (this.latestEth1Block !== null) {
// Set latestBlock stats
metrics.eth1.eth1LatestBlockNumber.set(this.latestEth1Block.number);
metrics.eth1.eth1LatestBlockTD.set(Number(this.latestEth1Block.totalDifficulty / this.safeTDFactor));
metrics.eth1.eth1LatestBlockTimestamp.set(this.latestEth1Block.timestamp);
}
});
}
}
/**
* Returns the most recent POW block that satisfies the merge block condition
*/
async getTerminalPowBlock(): Promise<PowMergeBlock | null> {
switch (this.status.code) {
case StatusCode.STOPPED:
// If not module is not polling fetch the mergeBlock explicitly
return this.getTerminalPowBlockFromEth1();
case StatusCode.SEARCHING:
// Assume that polling would have found the block
return null;
case StatusCode.FOUND:
return this.status.mergeBlock;
}
}
getTDProgress(): TDProgress | null {
if (this.latestEth1Block === null) {
return this.latestEth1Block;
}
const tdDiff = this.config.TERMINAL_TOTAL_DIFFICULTY - this.latestEth1Block.totalDifficulty;
if (tdDiff > BigInt(0)) {
return {
ttdHit: false,
tdFactor: this.safeTDFactor,
tdDiffScaled: Number((tdDiff / this.safeTDFactor) as bigint),
ttd: this.config.TERMINAL_TOTAL_DIFFICULTY,
td: this.latestEth1Block.totalDifficulty,
timestamp: this.latestEth1Block.timestamp,
};
}
return {
ttdHit: true,
};
}
/**
* Get a POW block by hash checking the local cache first
*/
async getPowBlock(powBlockHash: string): Promise<PowMergeBlock | null> {
// Check cache first
const cachedBlock = this.blocksByHashCache.get(powBlockHash);
if (cachedBlock) {
return cachedBlock;
}
// Fetch from node
const blockRaw = await this.eth1Provider.getBlockByHash(powBlockHash);
if (blockRaw) {
const block = toPowBlock(blockRaw);
this.cacheBlock(block);
return block;
}
return null;
}
/**
* Should only start polling for mergeBlock if:
* - after BELLATRIX_FORK_EPOCH
* - Beacon node synced
* - head state not isMergeTransitionComplete
*/
startPollingMergeBlock(): void {
if (this.status.code !== StatusCode.STOPPED) {
return;
}
this.status = {code: StatusCode.SEARCHING};
this.logger.info("Starting search for terminal POW block", {
TERMINAL_TOTAL_DIFFICULTY: this.config.TERMINAL_TOTAL_DIFFICULTY,
});
const interval = setInterval(() => {
// Preemptively try to find merge block and cache it if found.
// Future callers of getTerminalPowBlock() will re-use the cached found mergeBlock.
this.getTerminalPowBlockFromEth1().catch((e) => {
this.logger.error("Error on findMergeBlock", {}, e as Error);
this.metrics?.eth1.eth1PollMergeBlockErrors.inc();
});
}, this.config.SECONDS_PER_ETH1_BLOCK * 1000);
this.intervals.push(interval);
}
private close(): void {
this.intervals.forEach(clearInterval);
}
private async getTerminalPowBlockFromEth1(): Promise<PowMergeBlock | null> {
if (!this.getTerminalPowBlockFromEth1Promise) {
this.getTerminalPowBlockFromEth1Promise = this.internalGetTerminalPowBlockFromEth1()
.then((mergeBlock) => {
// Persist found merge block here to affect both caller paths:
// - internal searcher
// - external caller if STOPPED
if (mergeBlock && this.status.code !== StatusCode.FOUND) {
if (this.status.code === StatusCode.SEARCHING) {
this.close();
}
this.logger.info("Terminal POW block found!", {
hash: mergeBlock.blockHash,
number: mergeBlock.number,
totalDifficulty: mergeBlock.totalDifficulty,
});
this.status = {code: StatusCode.FOUND, mergeBlock};
this.metrics?.eth1.eth1MergeBlockDetails.set(
{
terminalBlockHash: mergeBlock.blockHash,
// Convert all number/bigints to string labels
terminalBlockNumber: mergeBlock.number.toString(10),
terminalBlockTD: mergeBlock.totalDifficulty.toString(10),
},
1
);
}
return mergeBlock;
})
.finally(() => {
this.getTerminalPowBlockFromEth1Promise = null;
});
} else {
// This should no happen, since getTerminalPowBlockFromEth1() should resolve faster than SECONDS_PER_ETH1_BLOCK.
// else something is wrong: the el-cl comms are two slow, or the backsearch got stuck in a deep search.
this.metrics?.eth1.getTerminalPowBlockPromiseCacheHit.inc();
}
return this.getTerminalPowBlockFromEth1Promise;
}
/**
* **internal** + **unsafe** since it can create multiple backward searches that overload the eth1 client.
* Must be called in a wrapper to ensure that there's only once concurrent call to this fn.
*/
private async internalGetTerminalPowBlockFromEth1(): Promise<PowMergeBlock | null> {
// Search merge block by hash
// Terminal block hash override takes precedence over terminal total difficulty
const terminalBlockHash = toRootHex(this.config.TERMINAL_BLOCK_HASH);
if (terminalBlockHash !== ZERO_HASH_HEX) {
const block = await this.getPowBlock(terminalBlockHash);
if (block) {
return block;
}
// if a TERMINAL_BLOCK_HASH other than ZERO_HASH is configured and we can't find it, return NONE
return null;
}
// Search merge block by TTD
const latestBlockRaw = await this.eth1Provider.getBlockByNumber("latest");
if (!latestBlockRaw) {
throw Error("getBlockByNumber('latest') returned null");
}
let block = toPowBlock(latestBlockRaw);
this.latestEth1Block = {...block, timestamp: quantityToNum(latestBlockRaw.timestamp)};
this.cacheBlock(block);
// This code path to look backwards for the merge block is only necessary if:
// - The network has not yet found the merge block
// - There are descendants of the merge block in the eth1 chain
// For the search below to require more than a few hops, multiple block proposers in a row must fail to detect
// an existing merge block. Such situation is extremely unlikely, so this search is left un-optimized. Since
// this class can start eagerly looking for the merge block when not necessary, startPollingMergeBlock() should
// only be called when there is certainty that a mergeBlock search is necessary.
while (true) {
if (block.totalDifficulty < this.config.TERMINAL_TOTAL_DIFFICULTY) {
// TTD not reached yet
return null;
}
// else block.totalDifficulty >= this.config.TERMINAL_TOTAL_DIFFICULTY
// Potential mergeBlock! Must find the first block that passes TTD
// Allow genesis block to reach TTD https://github.com/ethereum/consensus-specs/pull/2719
if (block.parentHash === ZERO_HASH_HEX) {
return block;
}
const parent = await this.getPowBlock(block.parentHash);
if (!parent) {
throw Error(`Unknown parent of block with TD>TTD ${block.parentHash}`);
}
this.metrics?.eth1.eth1ParentBlocksFetched.inc();
// block.td > TTD && parent.td < TTD => block is mergeBlock
if (parent.totalDifficulty < this.config.TERMINAL_TOTAL_DIFFICULTY) {
// Is terminal total difficulty block AND has verified block -> parent relationship
return block;
}
block = parent;
}
}
private cacheBlock(block: PowMergeBlock): void {
this.blocksByHashCache.set(block.blockHash, block);
pruneSetToMax(this.blocksByHashCache, MAX_CACHE_POW_BLOCKS);
}
}
export function toPowBlock(block: EthJsonRpcBlockRaw): PowMergeBlock {
// Validate untrusted data from API
return {
number: quantityToNum(block.number),
blockHash: dataToRootHex(block.hash),
parentHash: dataToRootHex(block.parentHash),
totalDifficulty: quantityToBigint(block.totalDifficulty),
};
}
/**
* TTD values can be very large, for xDAI > 1e45. So scale down.
* To be good, TTD should be rendered as a number < Number.MAX_TD_RENDER_VALUE ~= 9e15
*/
export function getSafeTDFactor(ttd: bigint): bigint {
const safeIntegerMult = ttd / BigInt(MAX_TD_RENDER_VALUE);
// TTD < MAX_TD_RENDER_VALUE, no need to scale down
if (safeIntegerMult === BigInt(0)) {
return BigInt(1);
}
// Return closest power of 10 to ensure TD < max
const safeIntegerMultDigits = safeIntegerMult.toString(10).length;
return BigInt(10) ** BigInt(safeIntegerMultDigits);
}

View File

@@ -1,9 +1,6 @@
import {CachedBeaconStateAllForks} from "@lodestar/state-transition";
import {Root} from "@lodestar/types";
import {fromHex} from "@lodestar/utils";
import {Eth1DepositDataTracker, Eth1DepositDataTrackerModules} from "./eth1DepositDataTracker.js";
import {Eth1MergeBlockTracker, Eth1MergeBlockTrackerModules} from "./eth1MergeBlockTracker.js";
import {Eth1DataAndDeposits, IEth1ForBlockProduction, IEth1Provider, PowMergeBlock, TDProgress} from "./interface.js";
import {Eth1DataAndDeposits, IEth1ForBlockProduction, IEth1Provider} from "./interface.js";
import {Eth1Options} from "./options.js";
import {Eth1Provider} from "./provider/eth1Provider.js";
export {Eth1Provider};
@@ -23,23 +20,6 @@ export type {IEth1ForBlockProduction, IEth1Provider};
//
// - Fetch ALL deposit events from the deposit contract to build the deposit tree and validate future merkle proofs.
// Then it must follow deposit events at a distance roughly similar to the `ETH1_FOLLOW_DISTANCE` parameter above.
//
// - [New bellatrix]: After BELLATRIX_FORK_EPOCH, it must fetch the block with hash
// `state.eth1_data.block_hash` to compute `terminal_total_difficulty`. Note this may change with
// https://github.com/ethereum/consensus-specs/issues/2603.
//
// - [New bellatrix]: On block production post BELLATRIX_FORK_EPOCH, pre merge, the beacon node must find the merge block
// crossing the `terminal_total_difficulty` boundary and include it in the block. After the merge block production
// will just use `execution_engine.assemble_block` without fetching individual blocks.
//
// - [New bellatrix]: Fork-choice must validate the merge block ensuring it crossed the `terminal_total_difficulty`
// boundary, so it must fetch the POW block referenced in the merge block + its POW parent block.
//
// With the merge the beacon node has to follow the eth1 chain at two distances:
// 1. At `ETH1_FOLLOW_DISTANCE` for eth1Data to be re-org safe
// 2. At the head to get the first merge block, tolerating possible re-orgs
//
// Then both streams of blocks should not be merged since it's harder to guard against re-orgs from (2) to (1).
export function initializeEth1ForBlockProduction(
opts: Eth1Options,
@@ -59,12 +39,8 @@ export function initializeEth1ForBlockProduction(
export class Eth1ForBlockProduction implements IEth1ForBlockProduction {
private readonly eth1DepositDataTracker: Eth1DepositDataTracker | null;
private readonly eth1MergeBlockTracker: Eth1MergeBlockTracker;
constructor(
opts: Eth1Options,
modules: Eth1DepositDataTrackerModules & Eth1MergeBlockTrackerModules & {eth1Provider?: IEth1Provider}
) {
constructor(opts: Eth1Options, modules: Eth1DepositDataTrackerModules & {eth1Provider?: IEth1Provider}) {
const eth1Provider =
modules.eth1Provider ||
new Eth1Provider(
@@ -77,8 +53,6 @@ export class Eth1ForBlockProduction implements IEth1ForBlockProduction {
this.eth1DepositDataTracker = opts.disableEth1DepositDataTracker
? null
: new Eth1DepositDataTracker(opts, modules, eth1Provider);
this.eth1MergeBlockTracker = new Eth1MergeBlockTracker(modules, eth1Provider);
}
async getEth1DataAndDeposits(state: CachedBeaconStateAllForks): Promise<Eth1DataAndDeposits> {
@@ -88,23 +62,6 @@ export class Eth1ForBlockProduction implements IEth1ForBlockProduction {
return this.eth1DepositDataTracker.getEth1DataAndDeposits(state);
}
async getTerminalPowBlock(): Promise<Root | null> {
const block = await this.eth1MergeBlockTracker.getTerminalPowBlock();
return block && fromHex(block.blockHash);
}
getPowBlock(powBlockHash: string): Promise<PowMergeBlock | null> {
return this.eth1MergeBlockTracker.getPowBlock(powBlockHash);
}
getTDProgress(): TDProgress | null {
return this.eth1MergeBlockTracker.getTDProgress();
}
startPollingMergeBlock(): void {
this.eth1MergeBlockTracker.startPollingMergeBlock();
}
isPollingEth1Data(): boolean {
return this.eth1DepositDataTracker?.isPollingEth1Data() ?? false;
}
@@ -127,30 +84,10 @@ export class Eth1ForBlockProductionDisabled implements IEth1ForBlockProduction {
return {eth1Data: state.eth1Data, deposits: []};
}
/**
* Will miss the oportunity to propose the merge block but will still produce valid blocks
*/
async getTerminalPowBlock(): Promise<Root | null> {
return null;
}
/** Will not be able to validate the merge block */
async getPowBlock(_powBlockHash: string): Promise<PowMergeBlock | null> {
throw Error("eth1 must be enabled to verify merge block");
}
getTDProgress(): TDProgress | null {
return null;
}
isPollingEth1Data(): boolean {
return false;
}
startPollingMergeBlock(): void {
// Ignore
}
stopPollingEth1Data(): void {
// Ignore
}

View File

@@ -1,6 +1,6 @@
import {BeaconConfig} from "@lodestar/config";
import {CachedBeaconStateAllForks} from "@lodestar/state-transition";
import {Root, RootHex, phase0} from "@lodestar/types";
import {phase0} from "@lodestar/types";
export type EthJsonRpcBlockRaw = {
/** the block number. null when its pending block. `"0x1b4"` */
@@ -47,22 +47,6 @@ export type Eth1DataAndDeposits = {
export interface IEth1ForBlockProduction {
getEth1DataAndDeposits(state: CachedBeaconStateAllForks): Promise<Eth1DataAndDeposits>;
/** Returns the most recent POW block that satisfies the merge block condition */
getTerminalPowBlock(): Promise<Root | null>;
/** Get a POW block by hash checking the local cache first */
getPowBlock(powBlockHash: string): Promise<PowMergeBlock | null>;
/** Get current TD progress for log notifier */
getTDProgress(): TDProgress | null;
/**
* Should only start polling for mergeBlock if:
* - after BELLATRIX_FORK_EPOCH
* - Beacon node synced
* - head state not isMergeTransitionComplete
*/
startPollingMergeBlock(): void;
isPollingEth1Data(): boolean;
/**
@@ -78,34 +62,6 @@ export type Eth1Block = {
timestamp: number;
};
export type PowMergeBlock = {
number: number;
blockHash: RootHex;
parentHash: RootHex;
totalDifficulty: bigint;
};
export type PowMergeBlockTimestamp = PowMergeBlock & {
/** in seconds */
timestamp: number;
};
export type TDProgress =
| {
ttdHit: false;
/** Power of ten by which tdDiffScaled is scaled down */
tdFactor: bigint;
/** (TERMINAL_TOTAL_DIFFICULTY - block.totalDifficulty) / tdFactor */
tdDiffScaled: number;
/** TERMINAL_TOTAL_DIFFICULTY */
ttd: bigint;
/** totalDifficulty of latest fetched eth1 block */
td: bigint;
/** timestamp in sec of latest fetched eth1 block */
timestamp: number;
}
| {ttdHit: true};
export type BatchDepositEvents = {
depositEvents: phase0.DepositEvent[];
blockNumber: number;

View File

@@ -194,15 +194,12 @@ export class ExecutionEngineHttp implements IExecutionEngine {
* 1. {status: INVALID_BLOCK_HASH, latestValidHash: null, validationError:
* errorMessage | null} if the blockHash validation has failed
*
* 2. {status: INVALID_TERMINAL_BLOCK, latestValidHash: null, validationError:
* errorMessage | null} if terminal block conditions are not satisfied
*
* 3. {status: SYNCING, latestValidHash: null, validationError: null} if the payload
* 2. {status: SYNCING, latestValidHash: null, validationError: null} if the payload
* extends the canonical chain and requisite data for its validation is missing
* with the payload status obtained from the Payload validation process if the payload
* has been fully validated while processing the call
*
* 4. {status: ACCEPTED, latestValidHash: null, validationError: null} if the
* 3. {status: ACCEPTED, latestValidHash: null, validationError: null} if the
* following conditions are met:
* i) the blockHash of the payload is valid
* ii) the payload doesn't extend the canonical chain
@@ -330,16 +327,11 @@ export class ExecutionEngineHttp implements IExecutionEngine {
* errorMessage | null}, payloadId: null}
* obtained from the Payload validation process if the payload is deemed INVALID
*
* 3. {payloadStatus: {status: INVALID_TERMINAL_BLOCK, latestValidHash: null,
* validationError: errorMessage | null}, payloadId: null}
* either obtained from the Payload validation process or as a result of validating a
* PoW block referenced by forkchoiceState.headBlockHash
*
* 4. {payloadStatus: {status: VALID, latestValidHash: forkchoiceState.headBlockHash,
* 3. {payloadStatus: {status: VALID, latestValidHash: forkchoiceState.headBlockHash,
* validationError: null}, payloadId: null}
* if the payload is deemed VALID and a build process hasn't been started
*
* 5. {payloadStatus: {status: VALID, latestValidHash: forkchoiceState.headBlockHash,
* 4. {payloadStatus: {status: VALID, latestValidHash: forkchoiceState.headBlockHash,
* validationError: null}, payloadId: buildProcessId}
* if the payload is deemed VALID and the build process has begun.
*

View File

@@ -9,7 +9,7 @@ import {
ForkSeq,
} from "@lodestar/params";
import {ExecutionPayload, RootHex, bellatrix, deneb, ssz} from "@lodestar/types";
import {fromHex, toHex, toRootHex} from "@lodestar/utils";
import {fromHex, toRootHex} from "@lodestar/utils";
import {ZERO_HASH_HEX} from "../../constants/index.js";
import {quantityToNum} from "../../eth1/provider/utils.js";
import {INTEROP_BLOCK_HASH} from "../../node/utils/interop/state.js";
@@ -70,7 +70,7 @@ export class ExecutionEngineMockBackend implements JsonRpcBackend {
finalizedBlockHash = ZERO_HASH_HEX;
readonly payloadIdCache = new PayloadIdCache();
/** Known valid blocks, both pre-merge and post-merge */
/** Known valid blocks */
private readonly validBlocks = new Map<RootHex, ExecutionBlock>();
/** Preparing payloads to be retrieved via engine_getPayloadV1 */
private readonly preparingPayloads = new Map<number, PreparedPayload>();
@@ -135,18 +135,6 @@ export class ExecutionEngineMockBackend implements JsonRpcBackend {
return [] as ExecutionPayloadBodyRpc[];
}
/**
* Mock manipulator to add more known blocks to this mock.
*/
addPowBlock(powBlock: bellatrix.PowBlock): void {
this.validBlocks.set(toHex(powBlock.blockHash), {
parentHash: toHex(powBlock.parentHash),
blockHash: toHex(powBlock.blockHash),
timestamp: 0,
blockNumber: 0,
});
}
/**
* Mock manipulator to add predefined responses before execution engine client calls
*/
@@ -258,7 +246,7 @@ export class ExecutionEngineMockBackend implements JsonRpcBackend {
// section of the EIP. Additionally, if this validation fails, client software MUST NOT update the forkchoice
// state and MUST NOT begin a payload build process.
//
// > TODO
// > N/A: All networks have completed the merge transition
// 4. Before updating the forkchoice state, client software MUST ensure the validity of the payload referenced by
// forkchoiceState.headBlockHash, and MAY validate the payload while processing the call. The validation process

View File

@@ -1668,58 +1668,6 @@ export function createLodestarMetrics(
name: "lodestar_eth1_logs_batch_size_dynamic",
help: "Dynamic batch size to fetch deposit logs",
}),
// Merge Search info
eth1MergeStatus: register.gauge({
name: "lodestar_eth1_merge_status",
help: "Eth1 Merge Status 0 PRE_MERGE 1 SEARCHING 2 FOUND 3 POST_MERGE",
}),
eth1MergeTDFactor: register.gauge({
name: "lodestar_eth1_merge_td_factor",
help: "TTD set for the merge",
}),
eth1MergeTTD: register.gauge({
name: "lodestar_eth1_merge_ttd",
help: "TTD set for the merge scaled down by td_factor",
}),
eth1PollMergeBlockErrors: register.gauge({
name: "lodestar_eth1_poll_merge_block_errors_total",
help: "Total count of errors polling merge block",
}),
getTerminalPowBlockPromiseCacheHit: register.gauge({
name: "lodestar_eth1_get_terminal_pow_block_promise_cache_hit_total",
help: "Total count of skipped runs in poll merge block, because a previous promise existed",
}),
eth1ParentBlocksFetched: register.gauge({
name: "lodestar_eth1_parent_blocks_fetched_total",
help: "Total count of parent blocks fetched searching for merge block",
}),
// Latest block details
eth1LatestBlockTD: register.gauge({
name: "lodestar_eth1_latest_block_ttd",
help: "Eth1 latest Block td scaled down by td_factor",
}),
eth1LatestBlockNumber: register.gauge({
name: "lodestar_eth1_latest_block_number",
help: "Eth1 latest block number",
}),
eth1LatestBlockTimestamp: register.gauge({
name: "lodestar_eth1_latest_block_timestamp",
help: "Eth1 latest block timestamp",
}),
// Merge details
eth1MergeBlockDetails: register.gauge<{
terminalBlockHash: string;
terminalBlockNumber: string;
terminalBlockTD: string;
}>({
name: "lodestar_eth1_merge_block_details",
help: "If found then 1 with terminal block details",
labelNames: ["terminalBlockHash", "terminalBlockNumber", "terminalBlockTD"],
}),
},
eth1HttpClient: {

View File

@@ -6,7 +6,6 @@ import {
computeEpochAtSlot,
computeStartSlotAtEpoch,
isExecutionCachedStateType,
isMergeTransitionComplete,
} from "@lodestar/state-transition";
import {Epoch} from "@lodestar/types";
import {ErrorAborted, Logger, prettyBytes, prettyBytesShort, sleep} from "@lodestar/utils";
@@ -36,7 +35,6 @@ export async function runNodeNotifier(modules: NodeNotifierModules): Promise<voi
const {network, chain, sync, config, logger, signal} = modules;
const headSlotTimeSeries = new TimeSeries({maxPoints: 10});
const tdTimeSeries = new TimeSeries({maxPoints: 50});
const SLOTS_PER_SYNC_COMMITTEE_PERIOD = SLOTS_PER_EPOCH * EPOCHS_PER_SYNC_COMMITTEE_PERIOD;
let hasLowPeerCount = false;
@@ -87,21 +85,6 @@ export async function runNodeNotifier(modules: NodeNotifierModules): Promise<voi
const executionInfo = getHeadExecutionInfo(config, clockEpoch, headState, headInfo);
const finalizedCheckpointRow = `finalized: ${prettyBytes(finalizedRoot)}:${finalizedEpoch}`;
// Log in TD progress in separate line to not clutter regular status update.
// This line will only exist between BELLATRIX_FORK_EPOCH and TTD, a window of some days / weeks max.
// Notifier log lines must be kept at a reasonable max width otherwise it's very hard to read
const tdProgress = chain.eth1.getTDProgress();
if (tdProgress !== null && !tdProgress.ttdHit) {
tdTimeSeries.addPoint(tdProgress.tdDiffScaled, tdProgress.timestamp);
const timestampTDD = tdTimeSeries.computeY0Point();
// It is possible to get ttd estimate with an error at imminent merge
const secToTTD = Math.max(Math.floor(timestampTDD - Date.now() / 1000), 0);
const timeLeft = Number.isFinite(secToTTD) ? prettyTimeDiffSec(secToTTD) : "?";
logger.info(`TTD in ${timeLeft} current TD ${tdProgress.td} / ${tdProgress.ttd}`);
}
let nodeState: string[];
switch (sync.state) {
case SyncState.SyncingFinalized:
@@ -188,18 +171,13 @@ function getHeadExecutionInfo(
// Add execution status to notifier only if head is on/post bellatrix
if (isExecutionCachedStateType(headState)) {
if (isMergeTransitionComplete(headState)) {
const executionPayloadHashInfo =
headInfo.executionStatus !== ExecutionStatus.PreMerge ? headInfo.executionPayloadBlockHash : "empty";
const executionPayloadNumberInfo =
headInfo.executionStatus !== ExecutionStatus.PreMerge ? headInfo.executionPayloadNumber : NaN;
return [
`exec-block: ${executionStatusStr}(${executionPayloadNumberInfo} ${prettyBytesShort(
executionPayloadHashInfo
)})`,
];
}
return [`exec-block: ${executionStatusStr}`];
const executionPayloadHashInfo =
headInfo.executionStatus !== ExecutionStatus.PreMerge ? headInfo.executionPayloadBlockHash : "empty";
const executionPayloadNumberInfo =
headInfo.executionStatus !== ExecutionStatus.PreMerge ? headInfo.executionPayloadNumber : NaN;
return [
`exec-block: ${executionStatusStr}(${executionPayloadNumberInfo} ${prettyBytesShort(executionPayloadHashInfo)})`,
];
}
return [];

View File

@@ -1,148 +0,0 @@
import {afterEach, beforeAll, beforeEach, describe, expect, it} from "vitest";
import {fromHexString} from "@chainsafe/ssz";
import {ChainConfig} from "@lodestar/config";
import {sleep} from "@lodestar/utils";
import {ZERO_HASH} from "../../../src/constants/index.js";
import {Eth1MergeBlockTracker, StatusCode} from "../../../src/eth1/eth1MergeBlockTracker.js";
import {Eth1Options} from "../../../src/eth1/options.js";
import {quantityToBigint} from "../../../src/eth1/provider/utils.js";
import {Eth1Provider, IEth1Provider} from "../../../src/index.js";
import {getGoerliRpcUrl} from "../../testParams.js";
import {testLogger} from "../../utils/logger.js";
// This test is constantly failing. We must unblock PR so this issue is a TODO to debug it and re-enable latter.
// It's OKAY to disable temporarily since this functionality is tested indirectly by the sim merge tests.
// See https://github.com/ChainSafe/lodestar/issues/4197
// https://github.com/ChainSafe/lodestar/issues/5967
describe.skip("eth1 / Eth1MergeBlockTracker", () => {
const logger = testLogger();
function getConfig(ttd: bigint): ChainConfig {
return {
// Set time units to 1s to make the test faster
SECONDS_PER_ETH1_BLOCK: 1,
SLOT_DURATION_MS: 1000,
DEPOSIT_CONTRACT_ADDRESS: Buffer.alloc(32, 0),
TERMINAL_TOTAL_DIFFICULTY: ttd,
TERMINAL_BLOCK_HASH: ZERO_HASH,
} as Partial<ChainConfig> as ChainConfig;
}
const eth1Config = {DEPOSIT_CONTRACT_ADDRESS: ZERO_HASH};
// Compute lazily since getGoerliRpcUrl() throws if GOERLI_RPC_URL is not set
let eth1Options: Eth1Options;
beforeAll(() => {
eth1Options = {
enabled: true,
providerUrls: [getGoerliRpcUrl()],
depositContractDeployBlock: 0,
unsafeAllowDepositDataOverwrite: false,
};
});
let controller: AbortController;
beforeEach(() => {
controller = new AbortController();
});
afterEach(() => controller.abort());
it("Should find terminal pow block through TERMINAL_BLOCK_HASH", async () => {
const eth1Provider = new Eth1Provider(eth1Config, eth1Options, controller.signal);
const latestBlock = await eth1Provider.getBlockByNumber("latest");
if (!latestBlock) throw Error("No latestBlock");
const terminalTotalDifficulty = quantityToBigint(latestBlock.totalDifficulty) - BigInt(1000);
const config = getConfig(terminalTotalDifficulty);
config.TERMINAL_BLOCK_HASH = fromHexString(latestBlock.hash);
const eth1MergeBlockTracker = new Eth1MergeBlockTracker(
{
config,
logger,
signal: controller.signal,
metrics: null,
},
eth1Provider as IEth1Provider
);
// Wait for Eth1MergeBlockTracker to find at least one merge block
while (!controller.signal.aborted) {
if (await eth1MergeBlockTracker.getTerminalPowBlock()) break;
await sleep(500, controller.signal);
}
// Status should acknowlege merge block is found
expect(eth1MergeBlockTracker["status"]).toBe(StatusCode.FOUND);
// Given the total difficulty offset the block that has TTD is the `difficultyOffset`nth block
const mergeBlock = await eth1MergeBlockTracker.getTerminalPowBlock();
if (!mergeBlock) throw Error("terminal pow block not found");
expect(mergeBlock.totalDifficulty).toBe(quantityToBigint(latestBlock.totalDifficulty));
});
it("Should find merge block polling future 'latest' blocks", async () => {
const eth1Provider = new Eth1Provider(eth1Config, eth1Options, controller.signal);
const latestBlock = await eth1Provider.getBlockByNumber("latest");
if (!latestBlock) throw Error("No latestBlock");
// Set TTD to current totalDifficulty + 1, so the next block is the merge block
const terminalTotalDifficulty = quantityToBigint(latestBlock.totalDifficulty) + BigInt(1);
const eth1MergeBlockTracker = new Eth1MergeBlockTracker(
{
config: getConfig(terminalTotalDifficulty),
logger,
signal: controller.signal,
metrics: null,
},
eth1Provider as IEth1Provider
);
// Wait for Eth1MergeBlockTracker to find at least one merge block
while (!controller.signal.aborted) {
if (await eth1MergeBlockTracker.getTerminalPowBlock()) break;
await sleep(500, controller.signal);
}
// Status should acknowlege merge block is found
expect(eth1MergeBlockTracker["status"]).toBe(StatusCode.FOUND);
// Given the total difficulty offset the block that has TTD is the `difficultyOffset`nth block
const mergeBlock = await eth1MergeBlockTracker.getTerminalPowBlock();
if (!mergeBlock) throw Error("mergeBlock not found");
// "mergeBlock.totalDifficulty is not >= TTD"
expect(mergeBlock.totalDifficulty).toBeGreaterThanOrEqual(terminalTotalDifficulty);
});
it("Should find merge block fetching past blocks", async () => {
const eth1Provider = new Eth1Provider(eth1Config, eth1Options, controller.signal);
const latestBlock = await eth1Provider.getBlockByNumber("latest");
if (!latestBlock) throw Error("No latestBlock");
// Set TTD to current totalDifficulty + 1, so the previous block is the merge block
const terminalTotalDifficulty = quantityToBigint(latestBlock.totalDifficulty) - BigInt(1);
const eth1MergeBlockTracker = new Eth1MergeBlockTracker(
{
config: getConfig(terminalTotalDifficulty),
logger,
signal: controller.signal,
metrics: null,
},
eth1Provider as IEth1Provider
);
// Wait for Eth1MergeBlockTracker to find at least one merge block
while (!controller.signal.aborted) {
if (await eth1MergeBlockTracker.getTerminalPowBlock()) break;
await sleep(500, controller.signal);
}
// Status should acknowlege merge block is found
expect(eth1MergeBlockTracker["status"]).toBe(StatusCode.FOUND);
// Given the total difficulty offset the block that has TTD is the `difficultyOffset`nth block
const mergeBlock = await eth1MergeBlockTracker.getTerminalPowBlock();
if (!mergeBlock) throw Error("mergeBlock not found");
// "mergeBlock.totalDifficulty is not >= TTD"
expect(mergeBlock.totalDifficulty).toBeGreaterThanOrEqual(terminalTotalDifficulty);
});
});

View File

@@ -3,7 +3,6 @@ import {afterAll, beforeAll, bench, describe} from "@chainsafe/benchmark";
import {fromHexString} from "@chainsafe/ssz";
import {config} from "@lodestar/config/default";
import {LevelDbController} from "@lodestar/db/controller/level";
import {SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY} from "@lodestar/params";
import {CachedBeaconStateAltair} from "@lodestar/state-transition";
import {defaultOptions as defaultValidatorOptions} from "@lodestar/validator";
import {generatePerfTestCachedStateAltair} from "../../../../../state-transition/test/perf/util.js";
@@ -31,7 +30,6 @@ describe("produceBlockBody", () => {
proposerBoost: true,
proposerBoostReorg: true,
computeUnrealized: false,
safeSlotsToImportOptimistically: SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY,
disableArchiveOnCheckpoint: true,
suggestedFeeRecipient: defaultValidatorOptions.suggestedFeeRecipient,
skipCreateStateCacheIfAvailable: true,

View File

@@ -2,7 +2,7 @@ import {generateKeyPair} from "@libp2p/crypto/keys";
import {afterAll, beforeAll, bench, describe, setBenchOpts} from "@chainsafe/benchmark";
import {config} from "@lodestar/config/default";
import {LevelDbController} from "@lodestar/db/controller/level";
import {SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY, SLOTS_PER_EPOCH} from "@lodestar/params";
import {SLOTS_PER_EPOCH} from "@lodestar/params";
import {sleep, toHex} from "@lodestar/utils";
import {defaultOptions as defaultValidatorOptions} from "@lodestar/validator";
import {rangeSyncTest} from "../../../../state-transition/test/perf/params.js";
@@ -82,7 +82,6 @@ describe.skip("verify+import blocks - range sync perf test", () => {
proposerBoost: true,
proposerBoostReorg: true,
computeUnrealized: false,
safeSlotsToImportOptimistically: SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY,
disableArchiveOnCheckpoint: true,
suggestedFeeRecipient: defaultValidatorOptions.suggestedFeeRecipient,
skipCreateStateCacheIfAvailable: true,

View File

@@ -2,7 +2,8 @@
scriptDir=$(dirname $0)
currentDir=$(pwd)
elDir=$scriptDir/besu
. $scriptDir/common-setup.sh
. $elDir/common-setup.sh
$EL_BINARY_DIR/besu --engine-rpc-enabled --rpc-http-enabled --rpc-http-api ADMIN,ETH,MINER,NET --rpc-http-port $ETH_PORT --engine-rpc-port $ENGINE_PORT --engine-jwt-secret $currentDir/$DATA_DIR/jwtsecret --data-path $DATA_DIR --data-storage-format BONSAI --genesis-file $DATA_DIR/genesis.json

View File

@@ -6,11 +6,11 @@ echo $EL_BINARY_DIR
echo $JWT_SECRET_HEX
echo $TEMPLATE_FILE
echo $scriptDir
echo $elDir
echo $currentDir
env TTD=$TTD envsubst < $scriptDir/$TEMPLATE_FILE > $DATA_DIR/genesis.json
env TTD=$TTD envsubst < $elDir/$TEMPLATE_FILE > $DATA_DIR/genesis.json
echo "45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8" > $DATA_DIR/sk.json
echo "12345678" > $DATA_DIR/password.txt
pubKey="0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b"

View File

@@ -2,7 +2,8 @@
scriptDir=$(dirname $0)
currentDir=$(pwd)
elDir=$scriptDir/besudocker
. $scriptDir/common-setup.sh
. $elDir/common-setup.sh
docker run --rm -u $(id -u ${USER}):$(id -g ${USER}) --name custom-execution -p $ETH_PORT:$ETH_PORT -p $ENGINE_PORT:$ENGINE_PORT -v $currentDir/$DATA_DIR:/data $EL_BINARY_DIR --engine-rpc-enabled --rpc-http-enabled --rpc-http-api ADMIN,ETH,MINER,NET --rpc-http-port $ETH_PORT --engine-rpc-port $ENGINE_PORT --engine-jwt-secret /data/jwtsecret --data-path /data/besu --data-storage-format BONSAI --genesis-file /data/genesis.json

View File

@@ -6,11 +6,11 @@ echo $EL_BINARY_DIR
echo $JWT_SECRET_HEX
echo $TEMPLATE_FILE
echo $scriptDir
echo $elDir
echo $currentDir
env TTD=$TTD envsubst < $scriptDir/$TEMPLATE_FILE > $DATA_DIR/genesis.json
env TTD=$TTD envsubst < $elDir/$TEMPLATE_FILE > $DATA_DIR/genesis.json
echo "45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8" > $DATA_DIR/sk.json
echo "12345678" > $DATA_DIR/password.txt
pubKey="0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b"

View File

@@ -2,7 +2,8 @@
scriptDir=$(dirname $0)
currentDir=$(pwd)
elDir=$scriptDir/ethereumjsdocker
. $scriptDir/common-setup.sh
. $elDir/common-setup.sh
docker run --rm -u $(id -u ${USER}):$(id -g ${USER}) --name custom-execution --network host -v $currentDir/$DATA_DIR:/data $EL_BINARY_DIR --dataDir /data/ethereumjs --gethGenesis /data/genesis.json --rpc --rpcEngineAddr 0.0.0.0 --rpcAddr 0.0.0.0 --rpcEngine --jwt-secret /data/jwtsecret --logLevel debug --isSingleNode

View File

@@ -6,11 +6,11 @@ echo $EL_BINARY_DIR
echo $JWT_SECRET_HEX
echo $TEMPLATE_FILE
echo $scriptDir
echo $elDir
echo $currentDir
env TTD=$TTD envsubst < $scriptDir/$TEMPLATE_FILE > $DATA_DIR/genesis.json
env TTD=$TTD envsubst < $elDir/$TEMPLATE_FILE > $DATA_DIR/genesis.json
echo "45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8" > $DATA_DIR/sk.json
echo "12345678" > $DATA_DIR/password.txt
pubKey="0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b"

View File

@@ -2,7 +2,8 @@
scriptDir=$(dirname $0)
currentDir=$(pwd)
elDir=$scriptDir/geth
. $scriptDir/common-setup.sh
. $elDir/common-setup.sh
$EL_BINARY_DIR/geth --http -http.api "engine,net,eth,miner" --http.port $ETH_PORT --authrpc.port $ENGINE_PORT --authrpc.jwtsecret $currentDir/$DATA_DIR/jwtsecret --datadir $DATA_DIR --allow-insecure-unlock --unlock $pubKey --password $DATA_DIR/password.txt --syncmode full

View File

@@ -5,11 +5,11 @@ echo $DATA_DIR
echo $EL_BINARY_DIR
echo $JWT_SECRET_HEX
echo $scriptDir
echo $elDir
echo $currentDir
env TTD=$TTD envsubst < $scriptDir/genesisPre.tmpl > $DATA_DIR/genesis.json
env TTD=$TTD envsubst < $elDir/genesisPre.tmpl > $DATA_DIR/genesis.json
echo "45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8" > $DATA_DIR/sk.json
echo "12345678" > $DATA_DIR/password.txt
pubKey="0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b"

View File

@@ -1,8 +0,0 @@
#!/bin/bash -x
scriptDir=$(dirname $0)
currentDir=$(pwd)
. $scriptDir/common-setup.sh
$EL_BINARY_DIR/geth --http -http.api "engine,net,eth,miner" --http.port $ETH_PORT --authrpc.port $ENGINE_PORT --authrpc.jwtsecret $currentDir/$DATA_DIR/jwtsecret --datadir $DATA_DIR --allow-insecure-unlock --unlock $pubKey --password $DATA_DIR/password.txt --nodiscover --mine --syncmode full

View File

@@ -2,7 +2,8 @@
scriptDir=$(dirname $0)
currentDir=$(pwd)
elDir=$scriptDir/gethdocker
. $scriptDir/common-setup.sh
. $elDir/common-setup.sh
docker run --rm -u $(id -u ${USER}):$(id -g ${USER}) --name custom-execution --network host -v $currentDir/$DATA_DIR:/data $EL_BINARY_DIR --http -http.api "engine,net,eth,miner" --http.port $ETH_PORT --authrpc.port $ENGINE_PORT --authrpc.jwtsecret /data/jwtsecret --allow-insecure-unlock --unlock $pubKey --password /data/password.txt --datadir /data/geth --syncmode full

View File

@@ -6,11 +6,11 @@ echo $EL_BINARY_DIR
echo $JWT_SECRET_HEX
echo $TEMPLATE_FILE
echo $scriptDir
echo $elDir
echo $currentDir
env TTD=$TTD envsubst < $scriptDir/$TEMPLATE_FILE > $DATA_DIR/genesis.json
env TTD=$TTD envsubst < $elDir/$TEMPLATE_FILE > $DATA_DIR/genesis.json
echo "45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8" > $DATA_DIR/sk.json
echo "12345678" > $DATA_DIR/password.txt
pubKey="0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b"

View File

@@ -1,9 +0,0 @@
#!/bin/bash -x
scriptDir=$(dirname $0)
currentDir=$(pwd)
. $scriptDir/common-setup.sh
# EL_BINARY_DIR refers to the local docker image build from kiln/gethdocker folder
docker run --rm -u $(id -u ${USER}):$(id -g ${USER}) --name custom-execution --network host -v $currentDir/$DATA_DIR:/data $EL_BINARY_DIR --http -http.api "engine,net,eth,miner" --http.port $ETH_PORT --authrpc.port $ENGINE_PORT --authrpc.jwtsecret /data/jwtsecret --allow-insecure-unlock --unlock $pubKey --password /data/password.txt --datadir /data/geth --nodiscover --mine --syncmode full

View File

@@ -1,19 +0,0 @@
#!/bin/bash -x
echo $TTD
echo $DATA_DIR
echo $EL_BINARY_DIR
echo $JWT_SECRET_HEX
echo $scriptDir
echo $currentDir
env TTD=$TTD envsubst < $scriptDir/genesisPre.tmpl > $DATA_DIR/genesis.json
echo "45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8" > $DATA_DIR/sk.json
echo "12345678" > $DATA_DIR/password.txt
pubKey="0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b"
# echo a hex encoded 256 bit secret into a file, however remove leading 0x as mergemock doesnt like it
echo $JWT_SECRET_HEX | sed 's/0x//' > $DATA_DIR/jwtsecret
docker rm -f custom-execution

View File

@@ -1,36 +0,0 @@
{
"config": {
"chainId": 1,
"homesteadBlock": 0,
"eip150Block": 0,
"eip150Hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"eip155Block": 0,
"eip158Block": 0,
"byzantiumBlock": 0,
"constantinopleBlock": 0,
"petersburgBlock": 0,
"istanbulBlock": 0,
"muirGlacierBlock": 0,
"berlinBlock": 0,
"londonBlock": 0,
"clique": {
"period": 5,
"epoch": 30000
},
"terminalTotalDifficulty": ${TTD}
},
"nonce": "0x42",
"timestamp": "0x0",
"extraData": "0x0000000000000000000000000000000000000000000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"gasLimit": "0x1c9c380",
"difficulty": "0x0",
"mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"coinbase": "0x0000000000000000000000000000000000000000",
"alloc": {
"0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": {"balance": "0x6d6172697573766477000000"}
},
"number": "0x0",
"gasUsed": "0x0",
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"baseFeePerGas": "0x7"
}

View File

@@ -1 +0,0 @@
0xdc6457099f127cf0bac78de8b297df04951281909db4f58b43def7c7151e765d

View File

@@ -1,9 +0,0 @@
#!/bin/bash -x
scriptDir=$(dirname $0)
currentDir=$(pwd)
. $scriptDir/common-setup.sh
# if we don't provide any datadir merge mock stores data in memory which is fine by us
docker run --rm -u $(id -u ${USER}):$(id -g ${USER}) --name custom-execution --network host -v $currentDir/$DATA_DIR/genesis.json:/usr/app/genesis.json -v $currentDir/$DATA_DIR/jwtsecret:/usr/app/jwt.hex $EL_BINARY_DIR relay --listen-addr 127.0.0.1:$ETH_PORT --engine-listen-addr 127.0.0.1:$ENGINE_PORT --log.level debug --genesis-validators-root 0x3e8bd71d9925794b4f5e8623e15094ea6edc0fd206e3551e13dd2d10e08fbaba

View File

@@ -2,8 +2,9 @@
scriptDir=$(dirname $0)
currentDir=$(pwd)
elDir=$scriptDir/nethermind
. $scriptDir/common-setup.sh
. $elDir/common-setup.sh
cd $EL_BINARY_DIR
dotnet run -c Release -- --config themerge_kiln_testvectors --Merge.TerminalTotalDifficulty $TTD --JsonRpc.JwtSecretFile $currentDir/$DATA_DIR/jwtsecret --JsonRpc.Enabled true --JsonRpc.Host 0.0.0.0 --JsonRpc.AdditionalRpcUrls "http://localhost:$ETH_PORT|http|net;eth;subscribe;engine;web3;client|no-auth,http://localhost:$ENGINE_PORT|http|eth;engine" --Sync.SnapSync false

View File

@@ -1,9 +0,0 @@
#!/bin/bash -x
scriptDir=$(dirname $0)
currentDir=$(pwd)
. $scriptDir/common-setup.sh
cd $EL_BINARY_DIR
dotnet run -c Release -- --config themerge_kiln_m2 --Merge.TerminalTotalDifficulty $TTD --JsonRpc.JwtSecretFile $currentDir/$DATA_DIR/jwtsecret --Merge.Enabled true --Init.DiagnosticMode=None --JsonRpc.Enabled true --JsonRpc.Host 0.0.0.0 --JsonRpc.AdditionalRpcUrls "http://localhost:$ETH_PORT|http|net;eth;subscribe;engine;web3;client|no-auth,http://localhost:$ENGINE_PORT|http|eth;engine" --Sync.SnapSync false

View File

@@ -2,8 +2,9 @@
scriptDir=$(dirname $0)
currentDir=$(pwd)
elDir=$scriptDir/netherminddocker
. $scriptDir/common-setup.sh
. $elDir/common-setup.sh
if [ "$TEMPLATE_FILE" == "genesisPostWithdraw.tmpl" ]
then

View File

@@ -5,7 +5,7 @@ echo $DATA_DIR
echo $EL_BINARY_DIR
echo $JWT_SECRET_HEX
echo $scriptDir
echo $elDir
echo $currentDir
# echo a hex encoded 256 bit secret into a file

View File

@@ -1,10 +0,0 @@
#!/bin/bash -x
scriptDir=$(dirname $0)
currentDir=$(pwd)
. $scriptDir/common-setup.sh
echo "sleeping for 10 seconds..."
docker run --rm --network host --name custom-execution -v $currentDir/$DATA_DIR:/data $EL_BINARY_DIR --datadir /data/nethermind --config themerge_kiln_m2 --Merge.TerminalTotalDifficulty $TTD --JsonRpc.JwtSecretFile /data/jwtsecret --Merge.Enabled true --Init.DiagnosticMode=None --JsonRpc.Enabled true --JsonRpc.Host 0.0.0.0 --JsonRpc.AdditionalRpcUrls "http://localhost:$ETH_PORT|http|net;eth;subscribe;engine;web3;client|no-auth,http://localhost:$ENGINE_PORT|http|eth;engine" --Sync.SnapSync false

View File

@@ -20,7 +20,7 @@ import {TestLoggerOpts, testLogger} from "../utils/logger.js";
import {getDevBeaconNode} from "../utils/node/beacon.js";
import {simTestInfoTracker} from "../utils/node/simTest.js";
import {getAndInitDevValidators} from "../utils/node/validator.js";
import {ELClient, ELStartMode, runEL, sendRawTransactionBig} from "../utils/runEl.js";
import {ELClient, runEL, sendRawTransactionBig} from "../utils/runEl.js";
import {logFilesDir} from "./params.js";
import {shell} from "./shell.js";
@@ -67,7 +67,7 @@ describe("executionEngine / ExecutionEngineHttp", () => {
it("Send and get payloads with depositRequests to/from EL", async () => {
const {elClient, tearDownCallBack} = await runEL(
{...elSetupConfig, mode: ELStartMode.PostMerge, genesisTemplate: "electra.tmpl"},
{...elSetupConfig, genesisTemplate: "electra.tmpl"},
{...elRunOptions, ttd: BigInt(0)},
controller.signal
);
@@ -232,7 +232,7 @@ describe("executionEngine / ExecutionEngineHttp", () => {
it.skip("Post-merge, run for a few blocks", async () => {
console.log("\n\nPost-merge, run for a few blocks\n\n");
const {elClient, tearDownCallBack} = await runEL(
{...elSetupConfig, mode: ELStartMode.PostMerge, genesisTemplate: "electra.tmpl"},
{...elSetupConfig, genesisTemplate: "electra.tmpl"},
{...elRunOptions, ttd: BigInt(0)},
controller.signal
);
@@ -259,7 +259,7 @@ describe("executionEngine / ExecutionEngineHttp", () => {
electraEpoch: Epoch;
testName: string;
}): Promise<void> {
const {genesisBlockHash, ttd, engineRpcUrl, ethRpcUrl} = elClient;
const {genesisBlockHash, engineRpcUrl, ethRpcUrl} = elClient;
const validatorClientCount = 1;
const validatorsPerClient = 32;
@@ -306,7 +306,6 @@ describe("executionEngine / ExecutionEngineHttp", () => {
CAPELLA_FORK_EPOCH: 0,
DENEB_FORK_EPOCH: 0,
ELECTRA_FORK_EPOCH: electraEpoch,
TERMINAL_TOTAL_DIFFICULTY: ttd,
},
options: {
api: {rest: {enabled: true} as BeaconRestApiServerOpts},

View File

@@ -1,273 +0,0 @@
import fs from "node:fs";
import {afterAll, afterEach, describe, it, vi} from "vitest";
import {fromHexString, toHexString} from "@chainsafe/ssz";
import {routes} from "@lodestar/api";
import {ChainConfig} from "@lodestar/config";
import {TimestampFormatCode} from "@lodestar/logger";
import {SLOTS_PER_EPOCH} from "@lodestar/params";
import {Epoch, SignedBeaconBlock, bellatrix} from "@lodestar/types";
import {LogLevel, sleep} from "@lodestar/utils";
import {ValidatorProposerConfig} from "@lodestar/validator";
import {BeaconRestApiServerOpts} from "../../src/api/index.js";
import {ZERO_HASH} from "../../src/constants/index.js";
import {BuilderStatus} from "../../src/execution/builder/http.js";
import {Eth1Provider} from "../../src/index.js";
import {ClockEvent} from "../../src/util/clock.js";
import {TestLoggerOpts, testLogger} from "../utils/logger.js";
import {getDevBeaconNode} from "../utils/node/beacon.js";
import {simTestInfoTracker} from "../utils/node/simTest.js";
import {getAndInitDevValidators} from "../utils/node/validator.js";
import {ELClient, ELStartMode, runEL} from "../utils/runEl.js";
import {logFilesDir} from "./params.js";
import {shell} from "./shell.js";
// NOTE: How to run
// EL_BINARY_DIR=g11tech/mergemock:latest EL_SCRIPT_DIR=mergemock LODESTAR_PRESET=mainnet ETH_PORT=8661 ENGINE_PORT=8551 yarn vitest run test/sim/mergemock.test.ts
// ```
const jwtSecretHex = "0xdc6457099f127cf0bac78de8b297df04951281909db4f58b43def7c7151e765d";
describe("executionEngine / ExecutionEngineHttp", () => {
if (!process.env.EL_BINARY_DIR || !process.env.EL_SCRIPT_DIR) {
throw Error(
`EL ENV must be provided, EL_BINARY_DIR: ${process.env.EL_BINARY_DIR}, EL_SCRIPT_DIR: ${process.env.EL_SCRIPT_DIR}`
);
}
vi.setConfig({testTimeout: 10 * 60 * 1000});
const dataPath = fs.mkdtempSync("lodestar-test-mergemock");
const elSetupConfig = {
elScriptDir: process.env.EL_SCRIPT_DIR,
elBinaryDir: process.env.EL_BINARY_DIR,
};
const elRunOptions = {
dataPath,
jwtSecretHex,
enginePort: parseInt(process.env.ENGINE_PORT ?? "8551"),
ethPort: parseInt(process.env.ETH_PORT ?? "8545"),
};
const controller = new AbortController();
afterAll(async () => {
controller?.abort();
await shell(`sudo rm -rf ${dataPath}`);
});
const afterEachCallbacks: (() => Promise<void> | void)[] = [];
afterEach(async () => {
while (afterEachCallbacks.length > 0) {
const callback = afterEachCallbacks.pop();
if (callback) await callback();
}
});
it("Test builder flow", async () => {
console.log("\n\nPost-merge, run for a few blocks\n\n");
const {elClient, tearDownCallBack} = await runEL(
{...elSetupConfig, mode: ELStartMode.PostMerge},
{...elRunOptions, ttd: BigInt(0)},
controller.signal
);
afterEachCallbacks.push(() => tearDownCallBack());
await runNodeWithEL({
elClient,
bellatrixEpoch: 0,
testName: "post-merge",
});
});
type RunOpts = {elClient: ELClient; bellatrixEpoch: Epoch; testName: string};
async function runNodeWithEL({elClient, bellatrixEpoch, testName}: RunOpts): Promise<void> {
const {genesisBlockHash, ttd, engineRpcUrl, ethRpcUrl} = elClient;
const validatorClientCount = 1;
const validatorsPerClient = 32;
const testParams: Pick<ChainConfig, "SLOT_DURATION_MS"> = {
SLOT_DURATION_MS: 2000,
};
// Should reach justification in 6 epochs max.
// Merge block happens at epoch 2 slot 4. Then 4 epochs to finalize
const expectedEpochsToFinish = 1;
// 1 epoch of margin of error
const epochsOfMargin = 1;
const timeoutSetupMargin = 30 * 1000; // Give extra 30 seconds of margin
// We only expect builder blocks since `builderalways` is configured
// In a perfect run expected builder = 32, expected engine = 0
// keeping 4 missed slots and 4 engine blocks due to fallback as margin
const expectedBuilderBlocks = 28;
const maximumEngineBlocks = 4;
// All assertions are tracked w.r.t. fee recipient by attaching different fee recipient to
// execution and builder
const feeRecipientLocal = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
const feeRecipientEngine = "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
const feeRecipientMevBoost = "0xcccccccccccccccccccccccccccccccccccccccc";
// delay a bit so regular sync sees it's up to date and sync is completed from the beginning
const genesisSlotsDelay = 8;
const timeout =
((epochsOfMargin + expectedEpochsToFinish) * SLOTS_PER_EPOCH + genesisSlotsDelay) * testParams.SLOT_DURATION_MS;
vi.setConfig({testTimeout: timeout + 2 * timeoutSetupMargin});
const genesisTime = Math.floor(Date.now() / 1000) + genesisSlotsDelay * (testParams.SLOT_DURATION_MS / 1000);
const testLoggerOpts: TestLoggerOpts = {
level: LogLevel.info,
file: {
filepath: `${logFilesDir}/mergemock-${testName}.log`,
level: LogLevel.debug,
},
timestampFormat: {
format: TimestampFormatCode.EpochSlot,
genesisTime,
slotsPerEpoch: SLOTS_PER_EPOCH,
secondsPerSlot: testParams.SLOT_DURATION_MS / 1000,
},
};
const loggerNodeA = testLogger("Node-A", testLoggerOpts);
const bn = await getDevBeaconNode({
params: {
...testParams,
ALTAIR_FORK_EPOCH: 0,
BELLATRIX_FORK_EPOCH: bellatrixEpoch,
TERMINAL_TOTAL_DIFFICULTY: ttd,
},
options: {
api: {rest: {enabled: true} as BeaconRestApiServerOpts},
sync: {isSingleNode: true},
network: {allowPublishToZeroPeers: true, discv5: null},
// Now eth deposit/merge tracker methods directly available on engine endpoints
eth1: {enabled: false, providerUrls: [engineRpcUrl], jwtSecretHex},
executionEngine: {urls: [engineRpcUrl], jwtSecretHex},
executionBuilder: {
url: ethRpcUrl,
enabled: true,
issueLocalFcUWithFeeRecipient: feeRecipientMevBoost,
allowedFaults: 8,
faultInspectionWindow: 32,
},
chain: {suggestedFeeRecipient: feeRecipientLocal},
},
validatorCount: validatorClientCount * validatorsPerClient,
logger: loggerNodeA,
genesisTime,
eth1BlockHash: fromHexString(genesisBlockHash),
});
if (!bn.chain.executionBuilder) {
throw Error("executionBuilder should have been initialized");
}
// Enable builder by default, else because of circuit breaker we always start it with disabled
bn.chain.executionBuilder.updateStatus(BuilderStatus.enabled);
afterEachCallbacks.push(async () => {
await bn.close();
await sleep(1000);
});
const stopInfoTracker = simTestInfoTracker(bn, loggerNodeA);
const valProposerConfig = {
defaultConfig: {
graffiti: "default graffiti",
strictFeeRecipientCheck: true,
feeRecipient: feeRecipientEngine,
builder: {
gasLimit: 60000000,
selection: routes.validator.BuilderSelection.BuilderAlways,
},
},
} as ValidatorProposerConfig;
const {validators} = await getAndInitDevValidators({
logPrefix: "mergemock",
node: bn,
validatorsPerClient,
validatorClientCount,
startIndex: 0,
// At least one sim test must use the REST API for beacon <-> validator comms
useRestApi: true,
testLoggerOpts,
valProposerConfig,
});
afterEachCallbacks.push(async () => {
await Promise.all(validators.map((v) => v.close()));
});
let engineBlocks = 0;
let builderBlocks = 0;
await new Promise<void>((resolve, _reject) => {
bn.chain.emitter.on(routes.events.EventType.block, async (blockData) => {
const {data: fullOrBlindedBlock} = (await bn.api.beacon.getBlockV2({blockId: blockData.block})) as {
data: SignedBeaconBlock;
};
if (fullOrBlindedBlock !== undefined) {
const blockFeeRecipient = toHexString(
(fullOrBlindedBlock as bellatrix.SignedBeaconBlock).message.body.executionPayload.feeRecipient
);
if (blockFeeRecipient === feeRecipientMevBoost) {
builderBlocks++;
} else {
engineBlocks++;
}
}
});
bn.chain.clock.on(ClockEvent.epoch, (epoch) => {
// Resolve only if the finalized checkpoint includes execution payload
if (epoch >= expectedEpochsToFinish) {
console.log("\nGot event epoch, stopping validators and nodes\n");
resolve();
}
});
});
// Stop chain and un-subscribe events so the execution engine won't update it's head
// Allow some time to broadcast finalized events and complete the importBlock routine
await Promise.all(validators.map((v) => v.close()));
await bn.close();
await sleep(500);
if (bn.chain.beaconProposerCache.get(1) !== feeRecipientEngine) {
throw Error("Invalid feeRecipient set at BN");
}
// Assertions to make sure the end state is good
// 1. The proper head is set
const rpc = new Eth1Provider({DEPOSIT_CONTRACT_ADDRESS: ZERO_HASH}, {providerUrls: [engineRpcUrl], jwtSecretHex});
const consensusHead = bn.chain.forkChoice.getHead();
const executionHeadBlock = await rpc.getBlockByNumber("latest");
if (!executionHeadBlock) throw Error("Execution has not head block");
if (consensusHead.executionPayloadBlockHash !== executionHeadBlock.hash) {
throw Error(
"Consensus head not equal to execution head: " +
JSON.stringify({
executionHeadBlockHash: executionHeadBlock.hash,
consensusHeadExecutionPayloadBlockHash: consensusHead.executionPayloadBlockHash,
consensusHeadSlot: consensusHead.slot,
})
);
}
// 2. builder blocks are as expected
if (builderBlocks < expectedBuilderBlocks) {
throw Error(`Incorrect builderBlocks=${builderBlocks} (expected=${expectedBuilderBlocks})`);
}
// 3. engine blocks do not exceed max limit
if (engineBlocks > maximumEngineBlocks) {
throw Error(`Incorrect engineBlocks=${engineBlocks} (limit=${maximumEngineBlocks})`);
}
// wait for 1 slot to print current epoch stats
await sleep(1 * bn.config.SLOT_DURATION_MS);
stopInfoTracker();
console.log("\n\nDone\n\n");
}
});

View File

@@ -21,7 +21,6 @@ import {
BeaconBlock,
RootHex,
SignedBeaconBlock,
bellatrix,
deneb,
fulu,
ssz,
@@ -40,7 +39,6 @@ import {defaultChainOptions} from "../../../src/chain/options.js";
import {validateBlockDataColumnSidecars} from "../../../src/chain/validation/dataColumnSidecar.js";
import {ZERO_HASH_HEX} from "../../../src/constants/constants.js";
import {Eth1ForBlockProductionDisabled} from "../../../src/eth1/index.js";
import {PowMergeBlock} from "../../../src/eth1/interface.js";
import {ExecutionPayloadStatus} from "../../../src/execution/engine/interface.js";
import {ExecutionEngineMockBackend} from "../../../src/execution/engine/mock.js";
import {getExecutionEngineFromBackend} from "../../../src/execution/index.js";
@@ -61,7 +59,6 @@ const ANCHOR_BLOCK_FILE_NAME = "anchor_block";
const BLOCK_FILE_NAME = "^(block)_([0-9a-zA-Z]+)$";
const BLOBS_FILE_NAME = "^(blobs)_([0-9a-zA-Z]+)$";
const COLUMN_FILE_NAME = "^(column)_([0-9a-zA-Z]+)$";
const POW_BLOCK_FILE_NAME = "^(pow_block)_([0-9a-zA-Z]+)$";
const ATTESTATION_FILE_NAME = "^(attestation)_([0-9a-zA-Z])+$";
const ATTESTER_SLASHING_FILE_NAME = "^(attester_slashing)_([0-9a-zA-Z])+$";
@@ -80,7 +77,7 @@ const forkChoiceTest =
/** This is to track test's tickTime to be used in proposer boost */
let tickTime = 0;
const clock = new ClockStopped(currentSlot);
const eth1 = new Eth1ForBlockProductionMock();
const eth1 = new Eth1ForBlockProductionDisabled();
const executionEngineBackend = new ExecutionEngineMockBackend({
onlyPredefinedResponses: opts.onlyPredefinedResponses,
genesisBlockHash: isExecutionStateType(anchorState)
@@ -333,23 +330,6 @@ const forkChoiceTest =
}
}
// **on_merge_block execution**
// Adds PowBlock data which is required for executing on_block(store, block).
// The file is located in the same folder (see below). PowBlocks should be used as return values for
// get_pow_block(hash: Hash32) -> PowBlock function if hashes match.
else if (isPowBlock(step)) {
const powBlock = testcase.powBlocks.get(step.pow_block);
if (!powBlock) throw Error(`pow_block ${step.pow_block} not found`);
logger.debug(`Step ${i}/${stepsLen} pow_block`, {
blockHash: toHexString(powBlock.blockHash),
parentHash: toHexString(powBlock.parentHash),
});
// Register PowBlock for `get_pow_block(hash: Hash32)` calls in verifyBlock
eth1.addPowBlock(powBlock);
// Register PowBlock to allow validation in execution engine
executionEngineBackend.addPowBlock(powBlock);
}
// Optional step for optimistic sync tests.
else if (isOnPayloadInfoStep(step)) {
logger.debug(`Step ${i}/${stepsLen} payload_status`, {blockHash: step.block_hash});
@@ -455,7 +435,6 @@ const forkChoiceTest =
[BLOCK_FILE_NAME]: ssz[fork].SignedBeaconBlock,
[BLOBS_FILE_NAME]: ssz.deneb.Blobs,
[COLUMN_FILE_NAME]: ssz.fulu.DataColumnSidecar,
[POW_BLOCK_FILE_NAME]: ssz.bellatrix.PowBlock,
[ATTESTATION_FILE_NAME]: sszTypesFor(fork).Attestation,
[ATTESTER_SLASHING_FILE_NAME]: sszTypesFor(fork).AttesterSlashing,
},
@@ -464,7 +443,6 @@ const forkChoiceTest =
const blocks = new Map<string, SignedBeaconBlock>();
const blobs = new Map<string, deneb.Blobs>();
const columns = new Map<string, fulu.DataColumnSidecar>();
const powBlocks = new Map<string, bellatrix.PowBlock>();
const attestations = new Map<string, Attestation>();
const attesterSlashings = new Map<string, AttesterSlashing>();
for (const key in t) {
@@ -482,10 +460,6 @@ const forkChoiceTest =
if (columnMatch) {
columns.set(key, t[key]);
}
const powBlockMatch = key.match(POW_BLOCK_FILE_NAME);
if (powBlockMatch) {
powBlocks.set(key, t[key]);
}
const attMatch = key.match(ATTESTATION_FILE_NAME);
if (attMatch) {
attestations.set(key, t[key]);
@@ -503,7 +477,6 @@ const forkChoiceTest =
blocks,
blobs,
columns,
powBlocks,
attestations,
attesterSlashings,
};
@@ -530,7 +503,7 @@ function toSpecTestCheckpoint(checkpoint: CheckpointWithHex): SpecTestCheckpoint
};
}
type Step = OnTick | OnAttestation | OnAttesterSlashing | OnBlock | OnPowBlock | OnPayloadInfo | Checks;
type Step = OnTick | OnAttestation | OnAttesterSlashing | OnBlock | OnPayloadInfo | Checks;
type SpecTestCheckpoint = {epoch: bigint; root: string};
@@ -571,15 +544,6 @@ type OnBlock = {
valid?: number;
};
/** Optional step for optimistic sync tests. */
type OnPowBlock = {
/**
* the name of the `pow_block_<32-byte-root>.ssz_snappy` file. To
* execute `on_pow_block(store, block)`
*/
pow_block: string;
};
type OnPayloadInfo = {
/** Encoded 32-byte value of payload's block hash. */
block_hash: string;
@@ -622,7 +586,6 @@ type ForkChoiceTestCase = {
blocks: Map<string, SignedBeaconBlock>;
blobs: Map<string, deneb.Blobs>;
columns: Map<string, fulu.DataColumnSidecar>;
powBlocks: Map<string, bellatrix.PowBlock>;
attestations: Map<string, Attestation>;
attesterSlashings: Map<string, AttesterSlashing>;
};
@@ -643,10 +606,6 @@ function isBlock(step: Step): step is OnBlock {
return typeof (step as OnBlock).block === "string";
}
function isPowBlock(step: Step): step is OnPowBlock {
return typeof (step as OnPowBlock).pow_block === "string";
}
function isOnPayloadInfoStep(step: Step): step is OnPayloadInfo {
return typeof (step as OnPayloadInfo).block_hash === "string";
}
@@ -655,25 +614,6 @@ function isCheck(step: Step): step is Checks {
return typeof (step as Checks).checks === "object";
}
// Extend Eth1ForBlockProductionDisabled to not have to re-implement new methods
class Eth1ForBlockProductionMock extends Eth1ForBlockProductionDisabled {
private items = new Map<string, PowMergeBlock>();
async getPowBlock(powBlockHash: string): Promise<PowMergeBlock | null> {
return this.items.get(powBlockHash) ?? null;
}
addPowBlock(powBlock: bellatrix.PowBlock): void {
this.items.set(toHexString(powBlock.blockHash), {
// not used by verifyBlock()
number: 0,
blockHash: toHexString(powBlock.blockHash),
parentHash: toHexString(powBlock.parentHash),
totalDifficulty: powBlock.totalDifficulty,
});
}
}
specTestIterator(path.join(ethereumConsensusSpecsTests.outputDir, "tests", ACTIVE_PRESET), {
fork_choice: {type: RunnerType.default, fn: forkChoiceTest({onlyPredefinedResponses: false})},
sync: {type: RunnerType.default, fn: forkChoiceTest({onlyPredefinedResponses: true})},

View File

@@ -60,10 +60,13 @@ const coveredTestRunners = [
// ```
export const defaultSkipOpts: SkipOpts = {
skippedForks: ["eip7805"],
// TODO: capella
// BeaconBlockBody proof in lightclient is the new addition in v1.3.0-rc.2-hotfix
// Skip them for now to enable subsequently
skippedTestSuites: [
// Merge transition tests are skipped because we no longer support performing the merge transition.
// All networks have already completed the merge, so this code path is no longer needed.
/^bellatrix\/fork_choice\/on_merge_block\/.*/,
// TODO: capella
// BeaconBlockBody proof in lightclient is the new addition in v1.3.0-rc.2-hotfix
// Skip them for now to enable subsequently
/^capella\/light_client\/single_merkle_proof\/BeaconBlockBody.*/,
/^deneb\/light_client\/single_merkle_proof\/BeaconBlockBody.*/,
/^electra\/light_client\/single_merkle_proof\/BeaconBlockBody.*/,
@@ -72,7 +75,12 @@ export const defaultSkipOpts: SkipOpts = {
/^gloas\/(finality|fork_choice|networking|sanity|transition)\/.*$/,
/^gloas\/ssz_static\/ForkChoiceNode.*$/,
],
skippedTests: [],
skippedTests: [
// These tests validate "first payload" scenarios where is_execution_enabled was false pre-merge.
// Since we removed merge transition support, these code paths no longer exist.
/^bellatrix\/operations\/execution_payload\/.+\/bad_parent_hash_first_payload$/,
/^bellatrix\/sanity\/blocks\/.+\/is_execution_enabled_false$/,
],
skippedRunners: [],
};

View File

@@ -1,256 +0,0 @@
import {afterEach, beforeEach, describe, expect, it} from "vitest";
import {toHexString} from "@chainsafe/ssz";
import {ChainConfig} from "@lodestar/config";
import {sleep} from "@lodestar/utils";
import {ZERO_HASH} from "../../../src/constants/index.js";
import {Eth1MergeBlockTracker, StatusCode, toPowBlock} from "../../../src/eth1/eth1MergeBlockTracker.js";
import {Eth1ProviderState, EthJsonRpcBlockRaw} from "../../../src/eth1/interface.js";
import {IEth1Provider} from "../../../src/index.js";
import {testLogger} from "../../utils/logger.js";
describe("eth1 / Eth1MergeBlockTracker", () => {
const logger = testLogger();
const terminalTotalDifficulty = 1000;
let config: ChainConfig;
let controller: AbortController;
beforeEach(() => {
controller = new AbortController();
config = {
// Set time units to 0 to make the test as fast as possible
SECONDS_PER_ETH1_BLOCK: 0,
SLOT_DURATION_MS: 0,
// Hardcode TTD to a low value
TERMINAL_TOTAL_DIFFICULTY: BigInt(terminalTotalDifficulty),
TERMINAL_BLOCK_HASH: ZERO_HASH,
} as Partial<ChainConfig> as ChainConfig;
});
afterEach(() => controller.abort());
it("Should find terminal pow block through TERMINAL_BLOCK_HASH", async () => {
config.TERMINAL_BLOCK_HASH = Buffer.alloc(32, 1);
const block: EthJsonRpcBlockRaw = {
number: toHex(10),
hash: toRootHex(11),
parentHash: toRootHex(10),
totalDifficulty: toHex(100),
timestamp: "0x0",
};
const terminalPowBlock = toPowBlock(block);
const eth1Provider: IEth1Provider = {
deployBlock: 0,
getBlockNumber: async () => 0,
getBlockByNumber: async () => {
throw Error("Not implemented");
},
getBlockByHash: async (blockHashHex): Promise<EthJsonRpcBlockRaw | null> => {
return blockHashHex === toHexString(config.TERMINAL_BLOCK_HASH) ? block : null;
},
getBlocksByNumber: async (): Promise<any> => {
throw Error("Not implemented");
},
getDepositEvents: async (): Promise<any> => {
throw Error("Not implemented");
},
validateContract: async (): Promise<any> => {
throw Error("Not implemented");
},
getState: () => Eth1ProviderState.ONLINE,
};
const eth1MergeBlockTracker = new Eth1MergeBlockTracker(
{
config,
logger,
signal: controller.signal,
metrics: null,
},
eth1Provider
);
eth1MergeBlockTracker.startPollingMergeBlock();
// Wait for Eth1MergeBlockTracker to find at least one merge block
while (!controller.signal.aborted) {
if (await eth1MergeBlockTracker.getTerminalPowBlock()) break;
await sleep(10, controller.signal);
}
// Status should acknowlege merge block is found
expect(eth1MergeBlockTracker["status"].code).toBe(StatusCode.FOUND);
// Given the total difficulty offset the block that has TTD is the `difficultyOffset`nth block
expect(await eth1MergeBlockTracker.getTerminalPowBlock()).toEqual(terminalPowBlock);
});
it("Should find terminal pow block polling future 'latest' blocks", async () => {
// Set current network totalDifficulty to behind terminalTotalDifficulty by 5.
// Then on each call to getBlockByNumber("latest") increase totalDifficulty by 1.
const numOfBlocks = 5;
const difficulty = 1;
let latestBlockPointer = 0;
const blocks: EthJsonRpcBlockRaw[] = [];
const blocksByHash = new Map<string, EthJsonRpcBlockRaw>();
for (let i = 0; i < numOfBlocks + 1; i++) {
const block: EthJsonRpcBlockRaw = {
number: toHex(i),
hash: toRootHex(i + 1),
parentHash: toRootHex(i),
// Latest block is under TTD, so past block search is stopped
totalDifficulty: toHex(terminalTotalDifficulty - numOfBlocks * difficulty + i * difficulty),
timestamp: "0x0",
};
blocks.push(block);
}
const eth1Provider: IEth1Provider = {
deployBlock: 0,
getBlockNumber: async () => 0,
getBlockByNumber: async (blockNumber) => {
// On each call simulate that the eth1 chain advances 1 block with +1 totalDifficulty
if (blockNumber === "latest") {
if (latestBlockPointer >= blocks.length) {
throw Error("Fetched too many blocks");
}
return blocks[latestBlockPointer++];
}
return blocks[blockNumber];
},
getBlockByHash: async (blockHashHex) => blocksByHash.get(blockHashHex) ?? null,
getBlocksByNumber: async (): Promise<any> => {
throw Error("Not implemented");
},
getDepositEvents: async (): Promise<any> => {
throw Error("Not implemented");
},
validateContract: async (): Promise<any> => {
throw Error("Not implemented");
},
getState: () => Eth1ProviderState.ONLINE,
};
await runFindMergeBlockTest(eth1Provider, blocks.at(-1) as EthJsonRpcBlockRaw);
});
it("Should find terminal pow block fetching past blocks", async () => {
// Set current network totalDifficulty to behind terminalTotalDifficulty by 5.
// Then on each call to getBlockByNumber("latest") increase totalDifficulty by 1.
const numOfBlocks = 5;
const difficulty = 1;
const ttdOffset = 1 * difficulty;
const hashOffset = 100;
const blocks: EthJsonRpcBlockRaw[] = [];
for (let i = 0; i < numOfBlocks * 2; i++) {
const block: EthJsonRpcBlockRaw = {
number: toHex(hashOffset + i),
hash: toRootHex(hashOffset + i + 1),
parentHash: toRootHex(hashOffset + i),
// Latest block is under TTD, so past block search is stopped
totalDifficulty: toHex(terminalTotalDifficulty + i * difficulty - ttdOffset),
timestamp: "0x0",
};
blocks.push(block);
}
// Before last block (with ttdOffset = 1) is the merge block
const expectedMergeBlock = blocks[ttdOffset];
const eth1Provider = mockEth1ProviderFromBlocks(blocks);
await runFindMergeBlockTest(eth1Provider, expectedMergeBlock);
});
it("Should find terminal pow block fetching past blocks till genesis", async () => {
// There's no block with TD < TTD, searcher should stop at genesis block
const numOfBlocks = 5;
const difficulty = 1;
const blocks: EthJsonRpcBlockRaw[] = [];
for (let i = 0; i < numOfBlocks * 2; i++) {
const block: EthJsonRpcBlockRaw = {
number: toHex(i),
hash: toRootHex(i + 1),
parentHash: toRootHex(i),
// Latest block is under TTD, so past block search is stopped
totalDifficulty: toHex(terminalTotalDifficulty + i * difficulty + 1),
timestamp: "0x0",
};
blocks.push(block);
}
// Merge block must be genesis block
const expectedMergeBlock = blocks[0];
const eth1Provider = mockEth1ProviderFromBlocks(blocks);
await runFindMergeBlockTest(eth1Provider, expectedMergeBlock);
});
function mockEth1ProviderFromBlocks(blocks: EthJsonRpcBlockRaw[]): IEth1Provider {
const blocksByHash = new Map<string, EthJsonRpcBlockRaw>();
for (const block of blocks) {
blocksByHash.set(block.hash, block);
}
return {
deployBlock: 0,
getBlockNumber: async () => 0,
getBlockByNumber: async (blockNumber) => {
// Always return the same block with totalDifficulty > TTD and unknown parent
if (blockNumber === "latest") return blocks.at(-1) as EthJsonRpcBlockRaw;
return blocks[blockNumber];
},
getBlockByHash: async (blockHashHex) => blocksByHash.get(blockHashHex) ?? null,
getBlocksByNumber: async (from, to) => blocks.slice(from, to),
getDepositEvents: async (): Promise<any> => {
throw Error("Not implemented");
},
validateContract: async (): Promise<any> => {
throw Error("Not implemented");
},
getState: () => Eth1ProviderState.ONLINE,
};
}
async function runFindMergeBlockTest(
eth1Provider: IEth1Provider,
expectedMergeBlock: EthJsonRpcBlockRaw
): Promise<void> {
const eth1MergeBlockTracker = new Eth1MergeBlockTracker(
{
config,
logger,
signal: controller.signal,
metrics: null,
},
eth1Provider
);
eth1MergeBlockTracker.startPollingMergeBlock();
// Wait for Eth1MergeBlockTracker to find at least one merge block
while (!controller.signal.aborted) {
if (await eth1MergeBlockTracker.getTerminalPowBlock()) break;
await sleep(10, controller.signal);
}
// Status should acknowlege merge block is found
expect(eth1MergeBlockTracker["status"].code).toBe(StatusCode.FOUND);
// Given the total difficulty offset the block that has TTD is the `difficultyOffset`nth block
expect(await eth1MergeBlockTracker.getTerminalPowBlock()).toEqual(toPowBlock(expectedMergeBlock));
}
});
function toHex(num: number | bigint): string {
return "0x" + num.toString(16);
}
function toRootHex(num: number): string {
return "0x" + num.toString(16).padStart(64, "0");
}

View File

@@ -48,7 +48,6 @@ export async function getNetworkForTest(
const chain = new BeaconChain(
{
safeSlotsToImportOptimistically: 0,
archiveStateEpochFrequency: 0,
suggestedFeeRecipient: "",
blsVerifyAllMainThread: true,

View File

@@ -9,16 +9,10 @@ import {shell} from "../sim/shell.js";
let txRpcId = 1;
export enum ELStartMode {
PreMerge = "pre-merge",
PostMerge = "post-merge",
}
export type ELSetupConfig = {mode: ELStartMode; elScriptDir: string; elBinaryDir: string; genesisTemplate?: string};
export type ELSetupConfig = {elScriptDir: string; elBinaryDir: string; genesisTemplate?: string};
export type ELRunOptions = {ttd: bigint; dataPath: string; jwtSecretHex: string; enginePort: number; ethPort: number};
export type ELClient = {
genesisBlockHash: string;
ttd: bigint;
engineRpcUrl: string;
ethRpcUrl: string;
network: string;
@@ -26,7 +20,7 @@ export type ELClient = {
};
/**
* A util function to start an EL in a "pre-merge" or "post-merge" mode using an `elScriptDir` setup
* A util function to start an EL using an `elScriptDir` setup
* scripts folder in packages/beacon-node/test/scripts/el-interop.
*
* Returns an ELRunConfig after starting the EL, which can be used to initialize the genesis
@@ -34,11 +28,11 @@ export type ELClient = {
*/
export async function runEL(
{mode, elScriptDir, elBinaryDir, genesisTemplate: template}: ELSetupConfig,
{elScriptDir, elBinaryDir, genesisTemplate: template}: ELSetupConfig,
{ttd, dataPath, jwtSecretHex, enginePort, ethPort}: ELRunOptions,
signal: AbortSignal
): Promise<{elClient: ELClient; tearDownCallBack: () => Promise<void>}> {
const network = `${elScriptDir}/${mode}`;
const network = `${elScriptDir}`;
const ethRpcUrl = `http://127.0.0.1:${ethPort}`;
const engineRpcUrl = `http://127.0.0.1:${enginePort}`;
const genesisTemplate = template ?? "genesisPre.tmpl";

View File

@@ -6,13 +6,7 @@ import {
createChainForkConfig,
} from "@lodestar/config";
import {NetworkName, getNetworkBeaconParams} from "../networks/index.js";
import {
GlobalArgs,
ITerminalPowArgs,
defaultNetwork,
parseBeaconParamsArgs,
parseTerminalPowArgs,
} from "../options/index.js";
import {GlobalArgs, defaultNetwork, parseBeaconParamsArgs} from "../options/index.js";
import {readFile} from "../util/index.js";
import {IBeaconParamsUnparsed} from "./types.js";
@@ -44,7 +38,6 @@ export function getBeaconParamsFromArgs(args: GlobalArgs): ChainConfig {
paramsFile: args.paramsFile,
additionalParamsCli: {
...parseBeaconParamsArgs(args as IBeaconParamsUnparsed),
...parseTerminalPowArgs(args as ITerminalPowArgs),
},
});
}

View File

@@ -23,7 +23,6 @@ export type ChainArgs = {
"chain.computeUnrealized"?: boolean;
"chain.assertCorrectProgressiveBalances"?: boolean;
"chain.maxSkipSlots"?: number;
"safe-slots-to-import-optimistically": number;
emitPayloadAttributes?: boolean;
broadcastValidationStrictness?: string;
"chain.minSameMessageSignatureSetsToBatch"?: number;
@@ -63,7 +62,6 @@ export function parseArgs(args: ChainArgs): IBeaconNodeOptions["chain"] {
computeUnrealized: args["chain.computeUnrealized"],
assertCorrectProgressiveBalances: args["chain.assertCorrectProgressiveBalances"],
maxSkipSlots: args["chain.maxSkipSlots"],
safeSlotsToImportOptimistically: args["safe-slots-to-import-optimistically"],
emitPayloadAttributes: args.emitPayloadAttributes,
broadcastValidationStrictness: args.broadcastValidationStrictness,
minSameMessageSignatureSetsToBatch:
@@ -227,15 +225,6 @@ Will double processing times. Use only for debugging purposes.",
group: "chain",
},
"safe-slots-to-import-optimistically": {
hidden: true,
type: "number",
description:
"Slots from current (clock) slot till which its safe to import a block optimistically if the merge is not justified yet.",
default: defaultOptions.chain.safeSlotsToImportOptimistically,
group: "chain",
},
"chain.archiveStateEpochFrequency": {
description: "Minimum number of epochs between archived states",
default: defaultOptions.chain.archiveStateEpochFrequency,

View File

@@ -1,4 +1,4 @@
import {ChainConfig, chainConfigTypes} from "@lodestar/config";
import {chainConfigTypes} from "@lodestar/config";
import {CliCommandOptions, CliOptionDefinition} from "@lodestar/utils";
import {IBeaconParamsUnparsed} from "../config/types.js";
import {ObjectKeys} from "../util/index.js";
@@ -7,12 +7,7 @@ import {ObjectKeys} from "../util/index.js";
// If an arbitrary key notation is used, it removes type safety on most of this CLI arg parsing code.
// Params will be parsed from an args object assuming to contain the required keys
export type ITerminalPowArgs = {
"terminal-total-difficulty-override"?: string;
"terminal-block-hash-override"?: string;
"terminal-block-hash-epoch-override"?: string;
};
export type IParamsArgs = Record<never, never> & ITerminalPowArgs;
export type IParamsArgs = Record<never, never>;
const getArgKey = (key: keyof IBeaconParamsUnparsed): string => `params.${key}`;
@@ -24,7 +19,7 @@ export function parseBeaconParamsArgs(args: Record<string, string | number>): IB
}, {});
}
const paramsOptionsByName = ObjectKeys(chainConfigTypes).reduce(
export const paramsOptions: CliCommandOptions<IParamsArgs> = ObjectKeys(chainConfigTypes).reduce(
(options: Record<string, CliOptionDefinition>, key): Record<string, CliOptionDefinition> => {
options[getArgKey(key)] = {
hidden: true,
@@ -35,38 +30,3 @@ const paramsOptionsByName = ObjectKeys(chainConfigTypes).reduce(
},
{}
);
const terminalArgsToParamsMap: {[K in keyof ITerminalPowArgs]: keyof ChainConfig} = {
"terminal-total-difficulty-override": "TERMINAL_TOTAL_DIFFICULTY",
"terminal-block-hash-override": "TERMINAL_BLOCK_HASH",
"terminal-block-hash-epoch-override": "TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH",
};
export function parseTerminalPowArgs(args: ITerminalPowArgs): IBeaconParamsUnparsed {
const parsedArgs = ObjectKeys(terminalArgsToParamsMap).reduce((beaconParams: Partial<IBeaconParamsUnparsed>, key) => {
const paramOption = terminalArgsToParamsMap[key];
const value = args[key];
if (paramOption != null && value != null) beaconParams[paramOption] = value;
return beaconParams;
}, {});
return parsedArgs;
}
export const paramsOptions: CliCommandOptions<IParamsArgs> = {
...paramsOptionsByName,
"terminal-total-difficulty-override": {
description: "Terminal PoW block TTD override",
type: "string",
},
"terminal-block-hash-override": {
description: "Terminal PoW block hash override",
type: "string",
},
"terminal-block-hash-epoch-override": {
description: "Terminal PoW block hash override activation epoch",
type: "string",
},
};

View File

@@ -2,7 +2,6 @@ import path from "node:path";
import {createAccountBalanceAssertion} from "../utils/crucible/assertions/accountBalanceAssertion.js";
import {createExecutionHeadAssertion} from "../utils/crucible/assertions/executionHeadAssertion.js";
import {createForkAssertion} from "../utils/crucible/assertions/forkAssertion.js";
import {mergeAssertion} from "../utils/crucible/assertions/mergeAssertion.js";
import {nodeAssertion} from "../utils/crucible/assertions/nodeAssertion.js";
import {createWithdrawalAssertions} from "../utils/crucible/assertions/withdrawalsAssertion.js";
import {BeaconClient, ExecutionClient, Match, ValidatorClient} from "../utils/crucible/interfaces.js";
@@ -116,14 +115,6 @@ env.tracker.register({
},
});
env.tracker.register({
...mergeAssertion,
match: ({slot}) => {
// Check at the end of bellatrix fork, merge should happen by then
return slot === env.clock.getLastSlotOfEpoch(bellatrixForkEpoch) ? Match.Assert | Match.Remove : Match.None;
},
});
env.tracker.register(
createAccountBalanceAssertion({
address: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",

View File

@@ -33,7 +33,6 @@ describe("options / beaconNodeOptions", () => {
suggestedFeeRecipient: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"chain.assertCorrectProgressiveBalances": true,
"chain.maxSkipSlots": 100,
"safe-slots-to-import-optimistically": 256,
"chain.archiveStateEpochFrequency": 1024,
"chain.minSameMessageSignatureSetsToBatch": 32,
"chain.maxShufflingCacheEpochs": 100,
@@ -139,7 +138,6 @@ describe("options / beaconNodeOptions", () => {
preaggregateSlotDistance: 1,
attDataCacheSlotDistance: 2,
computeUnrealized: true,
safeSlotsToImportOptimistically: 256,
suggestedFeeRecipient: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
assertCorrectProgressiveBalances: true,
maxSkipSlots: 100,

View File

@@ -1,21 +0,0 @@
import {BeaconStateAllForks, isExecutionStateType, isMergeTransitionComplete} from "@lodestar/state-transition";
import {Assertion, AssertionResult} from "../interfaces.js";
import {neverMatcher} from "./matchers.js";
export const mergeAssertion: Assertion<"merge", string> = {
id: "merge",
// Include into particular test with custom condition
match: neverMatcher,
async assert({node}) {
const errors: AssertionResult[] = [];
const res = await node.beacon.api.debug.getStateV2({stateId: "head"});
const state = res.value() as unknown as BeaconStateAllForks;
if (!(isExecutionStateType(state) && isMergeTransitionComplete(state))) {
errors.push("Node has not yet completed the merged transition");
}
return errors;
},
};

View File

@@ -3,7 +3,7 @@ import path from "node:path";
import {Web3} from "web3";
import {fetch} from "@lodestar/utils";
import {EL_GENESIS_PASSWORD, EL_GENESIS_SECRET_KEY, SHARED_JWT_SECRET, SIM_ENV_NETWORK_ID} from "../../constants.js";
import {ExecutionClient, ExecutionNodeGenerator, ExecutionStartMode, JobOptions, RunnerType} from "../../interfaces.js";
import {ExecutionClient, ExecutionNodeGenerator, JobOptions, RunnerType} from "../../interfaces.js";
import {getNodeMountedPaths} from "../../utils/paths.js";
import {getNodePorts} from "../../utils/ports.js";
import {registerWeb3JsPlugins} from "../../web3js/plugins/index.js";
@@ -13,7 +13,7 @@ export const generateGethNode: ExecutionNodeGenerator<ExecutionClient.Geth> = (o
throw new Error("GETH_BINARY_DIR or GETH_DOCKER_IMAGE must be provided");
}
const {id, mode, ttd, address, mining, clientOptions, nodeIndex} = opts;
const {id, ttd, address, mining, clientOptions, nodeIndex} = opts;
const ports = getNodePorts(nodeIndex);
const isDocker = !!process.env.GETH_DOCKER_IMAGE;
@@ -136,7 +136,6 @@ export const generateGethNode: ExecutionNodeGenerator<ExecutionClient.Geth> = (o
"--verbosity",
"5",
...(mining ? ["--mine"] : []),
...(mode === ExecutionStartMode.PreMerge ? ["--nodiscover"] : []),
...clientOptions,
],
env: {},

View File

@@ -6,7 +6,6 @@ import {
ExecutionGeneratorOptions,
ExecutionGenesisOptions,
ExecutionNode,
ExecutionStartMode,
} from "../../interfaces.js";
import {getGethGenesisBlock} from "../../utils/executionGenesis.js";
import {getEstimatedForkTime} from "../../utils/index.js";
@@ -57,16 +56,13 @@ export async function createExecutionNode<E extends ExecutionClient>(
...options,
...genesisOptions,
id: elId,
mode:
options.mode ??
(forkConfig.BELLATRIX_FORK_EPOCH > 0 ? ExecutionStartMode.PreMerge : ExecutionStartMode.PostMerge),
address: runner.getNextIp(),
mining: options.mining ?? false,
};
await ensureDirectories(opts.paths);
await writeFile(opts.paths.jwtsecretFilePath, SHARED_JWT_SECRET);
await writeFile(opts.paths.genesisFilePath, JSON.stringify(getGethGenesisBlock(opts.mode, genesisOptions)));
await writeFile(opts.paths.genesisFilePath, JSON.stringify(getGethGenesisBlock(genesisOptions)));
switch (client) {
case ExecutionClient.Mock: {

View File

@@ -16,7 +16,6 @@ export const generateNethermindNode: ExecutionNodeGenerator<ExecutionClient.Neth
const {
id,
mode,
ttd,
address,
mining,
@@ -59,7 +58,7 @@ export const generateNethermindNode: ExecutionNodeGenerator<ExecutionClient.Neth
await writeFile(
chainSpecPath,
JSON.stringify(
getNethermindChainSpec(mode, {
getNethermindChainSpec({
ttd,
cliqueSealingPeriod,
shanghaiTime,

View File

@@ -45,11 +45,6 @@ export enum ExecutionClient {
Nethermind = "execution-nethermind",
}
export enum ExecutionStartMode {
PreMerge = "pre-merge",
PostMerge = "post-merge",
}
export type BeaconClientsOptions = {
[BeaconClient.Lodestar]: Partial<BeaconArgs & GlobalArgs>;
[BeaconClient.Lighthouse]: Record<string, unknown>;
@@ -137,7 +132,6 @@ export interface ExecutionGenesisOptions<E extends ExecutionClient = ExecutionCl
export interface ExecutionGeneratorOptions<E extends ExecutionClient = ExecutionClient>
extends ExecutionGenesisOptions<E>,
GeneratorOptions {
mode: ExecutionStartMode;
mining: boolean;
paths: ExecutionPaths;
clientOptions: ExecutionClientsOptions[E];

View File

@@ -1,10 +1,7 @@
import {SIM_ENV_CHAIN_ID, SIM_ENV_NETWORK_ID} from "../constants.js";
import {Eth1GenesisBlock, ExecutionGenesisOptions, ExecutionStartMode} from "../interfaces.js";
import {Eth1GenesisBlock, ExecutionGenesisOptions} from "../interfaces.js";
export const getGethGenesisBlock = (
mode: ExecutionStartMode,
options: ExecutionGenesisOptions
): Record<string, unknown> => {
export const getGethGenesisBlock = (options: ExecutionGenesisOptions): Record<string, unknown> => {
const {ttd, cliqueSealingPeriod, shanghaiTime, genesisTime, cancunTime, pragueTime} = options;
const genesis = {
@@ -63,20 +60,12 @@ export const getGethGenesisBlock = (
baseFeePerGas: "0x0",
};
if (mode === ExecutionStartMode.PreMerge) {
return genesis;
}
// TODO: Figure out PostMerge genesis later
return genesis;
};
export const getNethermindChainSpec = (
mode: ExecutionStartMode,
options: ExecutionGenesisOptions
): Record<string, unknown> => {
export const getNethermindChainSpec = (options: ExecutionGenesisOptions): Record<string, unknown> => {
const {ttd, shanghaiTime} = options;
const genesis = getGethGenesisBlock(mode, options) as Eth1GenesisBlock;
const genesis = getGethGenesisBlock(options) as Eth1GenesisBlock;
return {
name: "simulation-dev",

View File

@@ -15,8 +15,11 @@ export type ChainConfig = {
CONFIG_NAME: string;
// Transition
/** @deprecated All networks have completed the merge transition */
TERMINAL_TOTAL_DIFFICULTY: bigint;
/** @deprecated All networks have completed the merge transition */
TERMINAL_BLOCK_HASH: Uint8Array;
/** @deprecated All networks have completed the merge transition */
TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH: number;
// Genesis

View File

@@ -1,4 +1,4 @@
import {ChainConfig, ChainForkConfig} from "@lodestar/config";
import {ChainForkConfig} from "@lodestar/config";
import {SLOTS_PER_EPOCH, SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params";
import {
CachedBeaconStateAllForks,
@@ -10,7 +10,6 @@ import {
computeStartSlotAtEpoch,
getAttesterSlashableIndices,
isExecutionBlockBodyType,
isExecutionEnabled,
isExecutionStateType,
} from "@lodestar/state-transition";
import {computeUnrealizedCheckpoints} from "@lodestar/state-transition/epoch";
@@ -23,7 +22,6 @@ import {
RootHex,
Slot,
ValidatorIndex,
bellatrix,
phase0,
ssz,
} from "@lodestar/types";
@@ -49,7 +47,6 @@ import {
EpochDifference,
IForkChoice,
NotReorgedReason,
PowBlockHex,
ShouldOverrideForkChoiceUpdateResult,
} from "./interface.js";
import {CheckpointWithHex, IForkChoiceStore, JustifiedBalances, toCheckpointWithHex} from "./store.js";
@@ -656,16 +653,6 @@ export class ForkChoice implements IForkChoice {
this.proposerBoostRoot = blockRootHex;
}
// As per specs, we should be validating here the terminal conditions of
// the PoW if this were a merge transition block.
// (https://github.com/ethereum/consensus-specs/blob/dev/specs/bellatrix/fork-choice.md#on_block)
//
// However this check has been moved to the `verifyBlockStateTransition` in
// `packages/beacon-node/src/chain/blocks/verifyBlock.ts` as:
//
// 1. Its prudent to fail fast and not try importing a block in forkChoice.
// 2. Also the data to run such a validation is readily available there.
const justifiedCheckpoint = toCheckpointWithHex(state.currentJustifiedCheckpoint);
const finalizedCheckpoint = toCheckpointWithHex(state.finalizedCheckpoint);
const stateJustifiedEpoch = justifiedCheckpoint.epoch;
@@ -754,7 +741,7 @@ export class ForkChoice implements IForkChoice {
unrealizedFinalizedEpoch: unrealizedFinalizedCheckpoint.epoch,
unrealizedFinalizedRoot: unrealizedFinalizedCheckpoint.rootHex,
...(isExecutionBlockBodyType(block.body) && isExecutionStateType(state) && isExecutionEnabled(state, block)
...(isExecutionBlockBodyType(block.body) && isExecutionStateType(state)
? {
executionPayloadBlockHash: toRootHex(block.body.executionPayload.blockHash),
executionPayloadNumber: block.body.executionPayload.blockNumber,
@@ -1609,65 +1596,6 @@ export class ForkChoice implements IForkChoice {
}
}
/**
* This function checks the terminal pow conditions on the merge block as
* specified in the config either via TTD or TBH. This function is part of
* forkChoice because if the merge block was previously imported as syncing
* and the EL eventually signals it catching up via validateLatestHash
* the specs mandates validating terminal conditions on the previously
* imported merge block.
*/
export function assertValidTerminalPowBlock(
config: ChainConfig,
block: bellatrix.BeaconBlock,
preCachedData: {
executionStatus: ExecutionStatus.Syncing | ExecutionStatus.Valid;
powBlock?: PowBlockHex | null;
powBlockParent?: PowBlockHex | null;
}
): void {
if (!ssz.Root.equals(config.TERMINAL_BLOCK_HASH, ZERO_HASH)) {
if (computeEpochAtSlot(block.slot) < config.TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH)
throw Error(`Terminal block activation epoch ${config.TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH} not reached`);
// powBock.blockHash is hex, so we just pick the corresponding root
if (!ssz.Root.equals(block.body.executionPayload.parentHash, config.TERMINAL_BLOCK_HASH))
throw new Error(
`Invalid terminal block hash, expected: ${toRootHex(config.TERMINAL_BLOCK_HASH)}, actual: ${toRootHex(
block.body.executionPayload.parentHash
)}`
);
} else {
// If no TERMINAL_BLOCK_HASH override, check ttd
// Delay powBlock checks if the payload execution status is unknown because of
// syncing response in notifyNewPayload call while verifying
if (preCachedData?.executionStatus === ExecutionStatus.Syncing) return;
const {powBlock, powBlockParent} = preCachedData;
if (!powBlock) throw Error("onBlock preCachedData must include powBlock");
// if powBlock is genesis don't assert powBlockParent
if (!powBlockParent && powBlock.parentHash !== HEX_ZERO_HASH)
throw Error("onBlock preCachedData must include powBlockParent");
const isTotalDifficultyReached = powBlock.totalDifficulty >= config.TERMINAL_TOTAL_DIFFICULTY;
// If we don't have powBlockParent here, powBlock is the genesis and as we would have errored above
// we can mark isParentTotalDifficultyValid as valid
const isParentTotalDifficultyValid =
!powBlockParent || powBlockParent.totalDifficulty < config.TERMINAL_TOTAL_DIFFICULTY;
if (!isTotalDifficultyReached) {
throw Error(
`Invalid terminal POW block: total difficulty not reached expected >= ${config.TERMINAL_TOTAL_DIFFICULTY}, actual = ${powBlock.totalDifficulty}`
);
}
if (!isParentTotalDifficultyValid) {
throw Error(
`Invalid terminal POW block parent: expected < ${config.TERMINAL_TOTAL_DIFFICULTY}, actual = ${powBlockParent.totalDifficulty}`
);
}
}
}
// Approximate https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/fork-choice.md#calculate_committee_fraction
// Calculates proposer boost score when committeePercent = config.PROPOSER_SCORE_BOOST
export function getCommitteeFraction(

View File

@@ -246,10 +246,3 @@ export interface IForkChoice {
*/
getDependentRoot(block: ProtoBlock, atEpochDiff: EpochDifference): RootHex;
}
/** Same to the PowBlock but we want RootHex to work with forkchoice conveniently */
export type PowBlockHex = {
blockHash: RootHex;
parentHash: RootHex;
totalDifficulty: bigint;
};

View File

@@ -6,14 +6,13 @@ export {
type InvalidBlock,
InvalidBlockCode,
} from "./forkChoice/errors.js";
export {ForkChoice, type ForkChoiceOpts, UpdateHeadOpt, assertValidTerminalPowBlock} from "./forkChoice/forkChoice.js";
export {ForkChoice, type ForkChoiceOpts, UpdateHeadOpt} from "./forkChoice/forkChoice.js";
export {
type AncestorResult,
AncestorStatus,
EpochDifference,
type IForkChoice,
NotReorgedReason,
type PowBlockHex,
} from "./forkChoice/interface.js";
export * from "./forkChoice/safeBlocks.js";
export {

View File

@@ -1,71 +0,0 @@
import {describe, expect, it} from "vitest";
import {createChainForkConfig} from "@lodestar/config";
import {ssz} from "@lodestar/types";
import {ExecutionStatus, assertValidTerminalPowBlock} from "../../../src/index.js";
describe("assertValidTerminalPowBlock", () => {
const config = createChainForkConfig({TERMINAL_TOTAL_DIFFICULTY: BigInt(10)});
const block = ssz.bellatrix.BeaconBlock.defaultValue();
const executionStatus = ExecutionStatus.Valid;
it("should accept ttd >= genesis block as terminal without powBlockParent", () => {
const powBlock = {
blockHash: "0x" + "ab".repeat(32),
// genesis powBlock will have zero parent hash
parentHash: "0x" + "00".repeat(32),
totalDifficulty: BigInt(10),
};
expect(() =>
assertValidTerminalPowBlock(config, block, {executionStatus, powBlockParent: null, powBlock})
).not.toThrow();
});
it("should require powBlockParent if powBlock not genesis", () => {
const powBlock = {
blockHash: "0x" + "ab".repeat(32),
// genesis powBlock will have non zero parent hash
parentHash: "0x" + "01".repeat(32),
totalDifficulty: BigInt(10),
};
expect(() =>
assertValidTerminalPowBlock(config, block, {executionStatus, powBlockParent: null, powBlock})
).toThrow();
});
it("should require powBlock >= ttd", () => {
const powBlock = {
blockHash: "0x" + "ab".repeat(32),
// genesis powBlock will have non zero parent hash
parentHash: "0x" + "01".repeat(32),
totalDifficulty: BigInt(9),
};
expect(() =>
assertValidTerminalPowBlock(config, block, {executionStatus, powBlockParent: powBlock, powBlock})
).toThrow();
});
it("should require powBlockParent < ttd", () => {
const powBlock = {
blockHash: "0x" + "ab".repeat(32),
// genesis powBlock will have non zero parent hash
parentHash: "0x" + "01".repeat(32),
totalDifficulty: BigInt(10),
};
expect(() =>
assertValidTerminalPowBlock(config, block, {executionStatus, powBlockParent: powBlock, powBlock})
).toThrow();
});
it("should accept powBlockParent < ttd and powBlock >= ttd", () => {
const powBlock = {
blockHash: "0x" + "ab".repeat(32),
// genesis powBlock will have non zero parent hash
parentHash: "0x" + "01".repeat(32),
totalDifficulty: BigInt(10),
};
const powBlockParent = {
...powBlock,
totalDifficulty: BigInt(9),
};
expect(() => assertValidTerminalPowBlock(config, block, {executionStatus, powBlockParent, powBlock})).not.toThrow();
});
});

View File

@@ -271,6 +271,7 @@ export const MAX_REQUEST_LIGHT_CLIENT_COMMITTEE_HASHES = 128;
/**
* Optimistic sync
* @deprecated All networks have completed the merge transition, blocks are always safe to import optimistically.
*/
export const SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY = 128;
/** @deprecated */

View File

@@ -7,7 +7,7 @@ import {
CachedBeaconStateCapella,
CachedBeaconStateGloas,
} from "../types.js";
import {getFullOrBlindedPayload, isExecutionEnabled} from "../util/execution.js";
import {getFullOrBlindedPayload} from "../util/execution.js";
import {BlockExternalData, DataAvailabilityStatus} from "./externalData.js";
import {processBlobKzgCommitments} from "./processBlobKzgCommitments.js";
import {processBlockHeader} from "./processBlockHeader.js";
@@ -67,11 +67,7 @@ export function processBlock(
// The call to the process_execution_payload must happen before the call to the process_randao as the former depends
// on the randao_mix computed with the reveal of the previous block.
// TODO GLOAS: We call processExecutionPayload somewhere else post-gloas
if (
fork >= ForkSeq.bellatrix &&
fork < ForkSeq.gloas &&
isExecutionEnabled(state as CachedBeaconStateBellatrix, block)
) {
if (fork >= ForkSeq.bellatrix && fork < ForkSeq.gloas) {
processExecutionPayload(fork, state as CachedBeaconStateBellatrix, block.body, externalData);
}

View File

@@ -3,11 +3,7 @@ import {ForkName, ForkSeq, isForkPostDeneb} from "@lodestar/params";
import {BeaconBlockBody, BlindedBeaconBlockBody, deneb, isExecutionPayload} from "@lodestar/types";
import {toHex, toRootHex} from "@lodestar/utils";
import {CachedBeaconStateBellatrix, CachedBeaconStateCapella} from "../types.js";
import {
executionPayloadToPayloadHeader,
getFullOrBlindedPayloadFromBody,
isMergeTransitionComplete,
} from "../util/execution.js";
import {executionPayloadToPayloadHeader, getFullOrBlindedPayloadFromBody} from "../util/execution.js";
import {computeEpochAtSlot, computeTimeAtSlot, getRandaoMix} from "../util/index.js";
import {BlockExternalData, ExecutionPayloadStatus} from "./externalData.js";
@@ -21,15 +17,13 @@ export function processExecutionPayload(
const forkName = ForkName[ForkSeq[fork] as ForkName];
// Verify consistency of the parent hash, block number, base fee per gas and gas limit
// with respect to the previous execution payload header
if (isMergeTransitionComplete(state)) {
const {latestExecutionPayloadHeader} = state;
if (!byteArrayEquals(payload.parentHash, latestExecutionPayloadHeader.blockHash)) {
throw Error(
`Invalid execution payload parentHash ${toRootHex(payload.parentHash)} latest blockHash ${toRootHex(
latestExecutionPayloadHeader.blockHash
)}`
);
}
const {latestExecutionPayloadHeader} = state;
if (!byteArrayEquals(payload.parentHash, latestExecutionPayloadHeader.blockHash)) {
throw Error(
`Invalid execution payload parentHash ${toRootHex(payload.parentHash)} latest blockHash ${toRootHex(
latestExecutionPayloadHeader.blockHash
)}`
);
}
// Verify random

View File

@@ -2,7 +2,6 @@ import {ForkName, ForkPostBellatrix, ForkPreGloas, ForkSeq} from "@lodestar/para
import {
BeaconBlock,
BeaconBlockBody,
BlindedBeaconBlock,
BlindedBeaconBlockBody,
ExecutionPayload,
ExecutionPayloadHeader,
@@ -10,75 +9,16 @@ import {
capella,
deneb,
isBlindedBeaconBlockBody,
isExecutionPayload,
ssz,
} from "@lodestar/types";
import {
BeaconStateAllForks,
BeaconStateBellatrix,
BeaconStateCapella,
BeaconStateExecutions,
CachedBeaconStateAllForks,
CachedBeaconStateExecutions,
} from "../types.js";
/**
* Execution enabled = merge is done.
* When (A) state has execution data OR (B) block has execution data
*/
export function isExecutionEnabled(state: BeaconStateExecutions, block: BeaconBlock | BlindedBeaconBlock): boolean {
if (isMergeTransitionComplete(state)) {
return true;
}
// Throws if not post-bellatrix block. A fork-guard in isExecutionEnabled() prevents this from happening
const payload = getFullOrBlindedPayload(block);
// Note: spec says to check all payload is zero-ed. However a state-root cannot be zero for any non-empty payload
// TODO: Consider comparing with the payload root if this assumption is not correct.
// return !byteArrayEquals(payload.stateRoot, ZERO_HASH);
// UPDATE: stateRoot comparision should have been enough with zero hash, but spec tests were failing
// Revisit this later to fix specs and make this efficient
return isExecutionPayload(payload)
? !ssz.bellatrix.ExecutionPayload.equals(payload, ssz.bellatrix.ExecutionPayload.defaultValue())
: !ssz.bellatrix.ExecutionPayloadHeader.equals(
state.latestExecutionPayloadHeader,
// TODO: Performance
ssz.bellatrix.ExecutionPayloadHeader.defaultValue()
);
}
/**
* Merge block is the SINGLE block that transitions from POW to POS.
* state has no execution data AND this block has execution data
*/
export function isMergeTransitionBlock(state: BeaconStateExecutions, body: bellatrix.BeaconBlockBody): boolean {
return (
!isMergeTransitionComplete(state) &&
!ssz.bellatrix.ExecutionPayload.equals(body.executionPayload, ssz.bellatrix.ExecutionPayload.defaultValue())
);
}
/**
* Merge is complete when the state includes execution layer data:
* state.latestExecutionPayloadHeader NOT EMPTY
*/
export function isMergeTransitionComplete(state: BeaconStateExecutions): boolean {
if (!isCapellaStateType(state)) {
return !ssz.bellatrix.ExecutionPayloadHeader.equals(
(state as BeaconStateBellatrix).latestExecutionPayloadHeader,
// TODO: Performance
ssz.bellatrix.ExecutionPayloadHeader.defaultValue()
);
}
return !ssz.capella.ExecutionPayloadHeader.equals(
state.latestExecutionPayloadHeader,
// TODO: Performance
ssz.capella.ExecutionPayloadHeader.defaultValue()
);
}
/** Type guard for bellatrix.BeaconState */
export function isExecutionStateType(state: BeaconStateAllForks): state is BeaconStateExecutions {
return (state as BeaconStateExecutions).latestExecutionPayloadHeader !== undefined;

View File

@@ -9,6 +9,7 @@ export type BeaconBlockBody = ValueOf<typeof ssz.BeaconBlockBody>;
export type BeaconBlock = ValueOf<typeof ssz.BeaconBlock>;
export type SignedBeaconBlock = ValueOf<typeof ssz.SignedBeaconBlock>;
export type BeaconState = ValueOf<typeof ssz.BeaconState>;
/** @deprecated */
export type PowBlock = ValueOf<typeof ssz.PowBlock>;
export type BlindedBeaconBlockBody = ValueOf<typeof ssz.BlindedBeaconBlockBody>;

View File

@@ -110,8 +110,8 @@ function getSpecCriticalParams(localConfig: ChainConfig): Record<keyof ConfigWit
PRESET_BASE: false, // Not relevant, each preset value is checked below
CONFIG_NAME: false, // Arbitrary string, not relevant
// validator client behaviour does not change with this parameters, so it's not concerned about them.
// However, with the override ttd flag, the validator and beacon could be out of sync and prevent it from running.
// Deprecated - All networks have completed the merge transition
TERMINAL_TOTAL_DIFFICULTY: false,
TERMINAL_BLOCK_HASH: false,
TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH: false,