mirror of
https://github.com/ChainSafe/lodestar.git
synced 2026-01-10 08:08:16 -05:00
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:
@@ -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
|
||||
|
||||
58
.github/workflows/test-sim-merge.yml
vendored
58
.github/workflows/test-sim-merge.yml
vendored
@@ -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
@@ -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`
|
||||
|
||||
@@ -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",
|
||||
};
|
||||
|
||||
|
||||
@@ -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/",
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 [];
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
0xdc6457099f127cf0bac78de8b297df04951281909db4f58b43def7c7151e765d
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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},
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
});
|
||||
@@ -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})},
|
||||
|
||||
@@ -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: [],
|
||||
};
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
@@ -48,7 +48,6 @@ export async function getNetworkForTest(
|
||||
|
||||
const chain = new BeaconChain(
|
||||
{
|
||||
safeSlotsToImportOptimistically: 0,
|
||||
archiveStateEpochFrequency: 0,
|
||||
suggestedFeeRecipient: "",
|
||||
blsVerifyAllMainThread: true,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
@@ -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: {},
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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 */
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user