@lodestar/beacon-node
Version:
A Typescript implementation of the beacon chain
101 lines • 5.81 kB
JavaScript
import { isGloasBeaconBlock, ssz } from "@lodestar/types";
import { toRootHex } from "@lodestar/utils";
import { BlockError, BlockErrorCode } from "../../errors/index.js";
/**
* Assert this chain segment of blocks is linear with slot numbers and hashes,
* and that the provided envelopes are consistent with their respective blocks.
*
*
* For each block:
* - Verifies parent root + slot linearity
* - For gloas: verifies bid.parentBlockHash matches the tracked execution hash; if not, the
* previous FULL envelope is treated as orphaned (segment continues as if previous slot was EMPTY)
* - If an envelope exists for this slot: verifies it references this block's root
* - Advances the tracked execution hash (FULL if envelope present, EMPTY if not)
*/
export function assertLinearChainSegment(config, blocks, payloadEnvelopes, parentBlock) {
const warnings = [];
// Track the expected execution payload block hash through the segment.
// Starts from the known forkchoice parent's execution hash.
// - FULL variant (envelope present for slot): advances to envelope.payload.blockHash
// - EMPTY variant (no envelope for slot): execution hash is unchanged
let currentExecHash = parentBlock?.executionPayloadBlockHash ?? null;
// Checkpoint sync first batch: parent is the anchor PENDING whose executionPayloadBlockHash
// is the inherited parentBlockHash semantic (= grandparent's payload), not its own payload.
// If parent's own payload envelope arrives in this batch, advance currentExecHash to that
// payload's blockHash so the segment validation sees the true EL chain head.
if (parentBlock !== null) {
const parentPayloadInput = payloadEnvelopes?.get(parentBlock.slot);
if (parentPayloadInput?.hasPayloadEnvelope()) {
currentExecHash = parentPayloadInput.getBlockHashHex();
}
}
// Track the execution hash before the last FULL advancement so we can recover
// if the next block reveals that envelope was orphaned.
let prevExecHash = currentExecHash;
// The slot whose envelope last advanced currentExecHash (for warning context).
let lastFullSlot = null;
for (let i = 0; i < blocks.length; i++) {
const block = blocks[i].getBlock();
const slot = block.message.slot;
if (i > 0) {
const prevBlock = blocks[i - 1].getBlock();
// Ensure parent root matches the previous block's root
if (!ssz.Root.equals(config.getForkTypes(prevBlock.message.slot).BeaconBlock.hashTreeRoot(prevBlock.message), block.message.parentRoot)) {
throw new BlockError(block, { code: BlockErrorCode.NON_LINEAR_PARENT_ROOTS });
}
// Ensure slots are strictly increasing
if (slot <= prevBlock.message.slot) {
throw new BlockError(block, { code: BlockErrorCode.NON_LINEAR_SLOTS });
}
}
if (isGloasBeaconBlock(block.message)) {
// Bid check fires only when we have a seeded execution hash. With parentBlock=null the
// chain is seeded mid-segment by the first FULL envelope.
if (currentExecHash !== null) {
// Verify the bid's parentBlockHash matches the tracked execution hash.
// This ensures the block was built on the correct FULL or EMPTY variant of its parent.
const bidParentHash = toRootHex(block.message.body.signedExecutionPayloadBid.message.parentBlockHash);
if (bidParentHash !== currentExecHash) {
// Maybe the previous slot's FULL envelope was orphaned — try falling back.
// If even prevExecHash doesn't match, the segment is non-linear.
if (bidParentHash !== prevExecHash) {
throw new BlockError(block, {
code: BlockErrorCode.PARENT_PAYLOAD_UNKNOWN,
parentRoot: toRootHex(block.message.parentRoot),
parentBlockHash: bidParentHash,
});
}
if (lastFullSlot !== null && payloadEnvelopes !== null) {
const orphanedInput = payloadEnvelopes.get(lastFullSlot);
if (orphanedInput != null) {
warnings.push({ slot: lastFullSlot, payloadEnvelopeInput: orphanedInput });
}
}
currentExecHash = prevExecHash;
}
}
const payloadInput = payloadEnvelopes?.get(slot) ?? null;
const payloadEnvelope = payloadInput?.hasPayloadEnvelope() ? payloadInput.getPayloadEnvelope() : null;
if (payloadEnvelope !== null) {
// Verify the envelope references this block's root
const blockRoot = toRootHex(config.getForkTypes(slot).BeaconBlock.hashTreeRoot(block.message));
const envelopeBlockRoot = toRootHex(payloadEnvelope.message.beaconBlockRoot);
if (blockRoot !== envelopeBlockRoot) {
throw new BlockError(block, {
code: BlockErrorCode.ENVELOPE_BLOCK_ROOT_MISMATCH,
envelopeBlockRoot,
blockRoot,
});
}
// FULL variant: save state before advancing, then advance
prevExecHash = currentExecHash;
lastFullSlot = slot;
currentExecHash = toRootHex(payloadEnvelope.message.payload.blockHash);
}
// EMPTY variant: currentExecHash unchanged
}
}
return { warnings: warnings.length > 0 ? warnings : null };
}
//# sourceMappingURL=chainSegment.js.map