@lodestar/beacon-node
Version:
A Typescript implementation of the beacon chain
622 lines • 35.5 kB
JavaScript
import { EventEmitter } from "node:events";
import { blockToHeader, computeAnchorCheckpoint } from "@lodestar/state-transition";
import { ssz } from "@lodestar/types";
import { ErrorAborted, sleep, toRootHex } from "@lodestar/utils";
import { SLOTS_PER_EPOCH } from "@lodestar/params";
import { GENESIS_SLOT, ZERO_HASH } from "../../constants/index.js";
import { NetworkEvent, PeerAction } from "../../network/index.js";
import { byteArrayEquals } from "../../util/bytes.js";
import { ItTrigger } from "../../util/itTrigger.js";
import { shuffleOne } from "../../util/shuffle.js";
import { BackfillSyncError, BackfillSyncErrorCode } from "./errors.js";
import { verifyBlockProposerSignature, verifyBlockSequence } from "./verify.js";
/**
* Timeout in ms to take a break from reading a backfillBatchSize from db, as just yielding
* to sync loop gives hardly any.
*/
const DB_READ_BREATHER_TIMEOUT = 1000;
export var BackfillSyncEvent;
(function (BackfillSyncEvent) {
BackfillSyncEvent["completed"] = "BackfillSync-completed";
})(BackfillSyncEvent || (BackfillSyncEvent = {}));
export var BackfillSyncMethod;
(function (BackfillSyncMethod) {
BackfillSyncMethod["database"] = "database";
BackfillSyncMethod["backfilled_ranges"] = "backfilled_ranges";
BackfillSyncMethod["rangesync"] = "rangesync";
BackfillSyncMethod["blockbyroot"] = "blockbyroot";
})(BackfillSyncMethod || (BackfillSyncMethod = {}));
export var BackfillSyncStatus;
(function (BackfillSyncStatus) {
BackfillSyncStatus["pending"] = "pending";
BackfillSyncStatus["syncing"] = "syncing";
BackfillSyncStatus["completed"] = "completed";
BackfillSyncStatus["aborted"] = "aborted";
})(BackfillSyncStatus || (BackfillSyncStatus = {}));
/** Map a SyncState to an integer for rendering in Grafana */
const syncStatus = {
[BackfillSyncStatus.aborted]: 0,
[BackfillSyncStatus.pending]: 1,
[BackfillSyncStatus.syncing]: 2,
[BackfillSyncStatus.completed]: 3,
};
export class BackfillSync extends EventEmitter {
constructor(opts, modules) {
super();
this.wsValidated = false;
this.processor = new ItTrigger();
this.peers = new Set();
this.status = BackfillSyncStatus.pending;
this.addPeer = (data) => {
const requiredSlot = this.syncAnchor.lastBackSyncedBlock?.slot ?? this.backfillStartFromSlot;
this.logger.debug("Add peer", { peerhead: data.status.headSlot, requiredSlot });
if (data.status.headSlot >= requiredSlot) {
this.peers.add(data.peer);
this.processor.trigger();
}
};
this.removePeer = (data) => {
this.peers.delete(data.peer);
};
this.syncAnchor = modules.syncAnchor;
this.backfillStartFromSlot = modules.backfillStartFromSlot;
this.backfillRangeWrittenSlot = modules.backfillRangeWrittenSlot;
this.prevFinalizedCheckpointBlock = modules.prevFinalizedCheckpointBlock;
this.wsCheckpointHeader = modules.wsCheckpointHeader;
this.chain = modules.chain;
this.network = modules.network;
this.db = modules.db;
this.config = modules.config;
this.logger = modules.logger;
this.metrics = modules.metrics;
this.opts = opts;
this.network.events.on(NetworkEvent.peerConnected, this.addPeer);
this.network.events.on(NetworkEvent.peerDisconnected, this.removePeer);
this.signal = modules.signal;
this.sync()
.then((oldestSlotSynced) => {
if (this.status !== BackfillSyncStatus.completed) {
throw new ErrorAborted(`Invalid BackfillSyncStatus at the completion of sync loop status=${this.status}`);
}
this.emit(BackfillSyncEvent.completed, oldestSlotSynced);
this.logger.info("BackfillSync completed", { oldestSlotSynced });
// Sync completed, unsubscribe listeners and don't run the processor again.
// Backfill is never necessary again until the node shuts down
this.close();
})
.catch((e) => {
this.logger.error("BackfillSync processor error", e);
this.status = BackfillSyncStatus.aborted;
this.close();
});
const metrics = this.metrics;
if (metrics) {
metrics.backfillSync.status.addCollect(() => metrics.backfillSync.status.set(syncStatus[this.status]));
metrics.backfillSync.backfilledTillSlot.addCollect(() => metrics.backfillSync.backfilledTillSlot.set(this.syncAnchor.lastBackSyncedBlock?.slot ?? this.backfillStartFromSlot));
metrics.backfillSync.prevFinOrWsSlot.addCollect(() => metrics.backfillSync.prevFinOrWsSlot.set(Math.max(this.prevFinalizedCheckpointBlock.slot, GENESIS_SLOT)));
}
}
/**
* Use the root of the anchorState of the beacon node as the starting point of the
* backfill sync with its expected slot to be anchorState.slot, which will be
* validated once the block is resolved in the backfill sync.
*
* NOTE: init here is quite light involving couple of
*
* 1. db keys lookup in stateArchive/backfilledRanges
* 2. computing root(s) for anchorBlockRoot and prevFinalizedCheckpointBlock
*
* The way we initialize beacon node, wsCheckpoint's slot is always <= anchorSlot
* If:
* the root belonging to wsCheckpoint is in the DB, we need to verify linkage to it
* i.e. it becomes our first prevFinalizedCheckpointBlock
* Else
* we initialize prevFinalizedCheckpointBlock from the last stored db finalized state
* for verification and when we go below its epoch we just check if a correct block
* corresponding to wsCheckpoint root was stored.
*
* and then we continue going back and verifying the next unconnected previous finalized
* or wsCheckpoints identifiable as the keys of backfill sync.
*/
static async init(opts, modules) {
const { config, anchorState, db, wsCheckpoint, logger } = modules;
const { checkpoint: anchorCp } = computeAnchorCheckpoint(config, anchorState);
const anchorSlot = anchorState.latestBlockHeader.slot;
const syncAnchor = {
anchorBlock: null,
anchorBlockRoot: anchorCp.root,
anchorSlot,
lastBackSyncedBlock: null,
};
// Load the previous written to slot for the key backfillStartFromSlot
// in backfilledRanges
const backfillStartFromSlot = anchorSlot;
const backfillRangeWrittenSlot = await db.backfilledRanges.get(backfillStartFromSlot);
const previousBackfilledRanges = await db.backfilledRanges.entries({
lte: backfillStartFromSlot,
});
modules.logger.info("Initializing from Checkpoint", {
root: toRootHex(anchorCp.root),
epoch: anchorCp.epoch,
backfillStartFromSlot,
previousBackfilledRanges: JSON.stringify(previousBackfilledRanges),
});
// wsCheckpointHeader is where the checkpoint can actually be validated
const wsCheckpointHeader = wsCheckpoint
? { root: wsCheckpoint.root, slot: wsCheckpoint.epoch * SLOTS_PER_EPOCH }
: null;
// Load a previous finalized or wsCheckpoint slot from DB below anchorSlot
const prevFinalizedCheckpointBlock = await extractPreviousFinOrWsCheckpoint(config, db, anchorSlot, logger);
return new BackfillSync(opts, {
syncAnchor,
backfillStartFromSlot,
backfillRangeWrittenSlot,
wsCheckpointHeader,
prevFinalizedCheckpointBlock,
...modules,
});
}
/** Throw / return all AsyncGenerators */
close() {
this.network.events.off(NetworkEvent.peerConnected, this.addPeer);
this.network.events.off(NetworkEvent.peerDisconnected, this.removePeer);
this.processor.end(new ErrorAborted("BackfillSync"));
}
/**
* @returns Returns oldestSlotSynced
*/
async sync() {
this.processor.trigger();
for await (const _ of this.processor) {
if (this.status === BackfillSyncStatus.aborted) {
/** Break out of sync loop and throw error */
break;
}
this.status = BackfillSyncStatus.syncing;
// 1. We should always have either anchorBlock or anchorBlockRoot, they are the
// anchor points for this round of the sync
// 2. Check and validate if we have reached prevFinalizedCheckpointBlock
// On success Update prevFinalizedCheckpointBlock to check the *next* previous
// 3. Validate Checkpoint as part of DB block tree if we have backfilled
// before the checkpoint
// 4. Exit the sync if backfilled till genesis
//
// 5. Check if we can jump back from available backfill sequence, if found yield and
// recontinue from top making checks
// 7. Check and read batchSize from DB, if found yield and recontinue from top
// 8. If not in DB, and if peer available
// a) Either fetch blockByRoot if only anchorBlockRoot is set, which could be because
// i) its the unavailable root of the very first block to start off sync
// ii) its parent of lastBackSyncedBlock and there was an issue in establishing
// linear sequence in syncRange as there could be one or more
// skipped/orphaned slots
// between the parent we want to fetch and lastBackSyncedBlock
// b) read previous batchSize blocks from network assuming most likely those blocks
// form a linear anchored chain with anchorBlock. If not, try fetching the
// parent of
// the anchorBlock via strategy a) as it could be multiple skipped/orphaned slots
// behind
if (this.syncAnchor.lastBackSyncedBlock != null) {
// If after a previous sync round:
// lastBackSyncedBlock.slot < prevFinalizedCheckpointBlock.slot
// then it means the prevFinalizedCheckpoint block has been missed because in each
// round we backfill new blocks till (if the batchSize allows):
// lastBackSyncedBlock.slot <= prevFinalizedCheckpointBlock.slot
if (this.syncAnchor.lastBackSyncedBlock.slot < this.prevFinalizedCheckpointBlock.slot) {
this.logger.error(`Backfilled till ${this.syncAnchor.lastBackSyncedBlock.slot} but not found previous saved finalized or wsCheckpoint with root=${toRootHex(this.prevFinalizedCheckpointBlock.root)}, slot=${this.prevFinalizedCheckpointBlock.slot}`);
// Break sync loop and throw error
break;
}
if (this.syncAnchor.lastBackSyncedBlock.slot === this.prevFinalizedCheckpointBlock.slot) {
// Okay! we backfilled successfully till prevFinalizedCheckpointBlock
if (!byteArrayEquals(this.syncAnchor.lastBackSyncedBlock.root, this.prevFinalizedCheckpointBlock.root)) {
this.logger.error(`Invalid root synced at a previous finalized or wsCheckpoint, slot=${this.prevFinalizedCheckpointBlock.slot}: expected=${toRootHex(this.prevFinalizedCheckpointBlock.root)}, actual=${toRootHex(this.syncAnchor.lastBackSyncedBlock.root)}`);
// Break sync loop and throw error
break;
}
this.logger.verbose("Validated current prevFinalizedCheckpointBlock", {
root: toRootHex(this.prevFinalizedCheckpointBlock.root),
slot: this.prevFinalizedCheckpointBlock.slot,
});
// 1. If this is not a genesis block save this block in DB as this wasn't saved
// earlier pending validation. Genesis block will be saved with extra validation
// before returning from the sync.
//
// 2. Load another previous saved finalized or wsCheckpoint which has not
// been validated yet. These are the keys of backfill ranges as each
// range denotes
// a validated connected segment having the slots of previous wsCheckpoint
// or finalized as keys
if (this.syncAnchor.lastBackSyncedBlock.slot !== GENESIS_SLOT) {
await this.db.blockArchive.put(this.syncAnchor.lastBackSyncedBlock.slot, this.syncAnchor.lastBackSyncedBlock.block);
}
this.prevFinalizedCheckpointBlock = await extractPreviousFinOrWsCheckpoint(this.config, this.db, this.syncAnchor.lastBackSyncedBlock.slot, this.logger);
}
if (this.syncAnchor.lastBackSyncedBlock.slot === GENESIS_SLOT) {
if (!byteArrayEquals(this.syncAnchor.lastBackSyncedBlock.block.message.parentRoot, ZERO_HASH)) {
Error(`Invalid Gensis Block with non zero parentRoot=${toRootHex(this.syncAnchor.lastBackSyncedBlock.block.message.parentRoot)}`);
}
await this.db.blockArchive.put(GENESIS_SLOT, this.syncAnchor.lastBackSyncedBlock.block);
}
if (this.wsCheckpointHeader && !this.wsValidated) {
await this.checkIfCheckpointSyncedAndValidate();
}
if (this.backfillRangeWrittenSlot === null ||
this.syncAnchor.lastBackSyncedBlock.slot < this.backfillRangeWrittenSlot) {
this.backfillRangeWrittenSlot = this.syncAnchor.lastBackSyncedBlock.slot;
await this.db.backfilledRanges.put(this.backfillStartFromSlot, this.backfillRangeWrittenSlot);
this.logger.debug(`Updated the backfill range from=${this.backfillStartFromSlot} till=${this.backfillRangeWrittenSlot}`);
}
if (this.syncAnchor.lastBackSyncedBlock.slot === GENESIS_SLOT) {
this.logger.verbose("Successfully synced to genesis.");
this.status = BackfillSyncStatus.completed;
return GENESIS_SLOT;
}
const foundValidSeq = await this.checkUpdateFromBackfillSequences();
if (foundValidSeq) {
// Go back to top and do checks till
this.processor.trigger();
continue;
}
}
try {
const foundBlocks = await this.fastBackfillDb();
if (foundBlocks) {
this.processor.trigger();
continue;
}
}
catch (e) {
this.logger.error("Error while reading from DB", {}, e);
// Break sync loop and throw error
break;
}
// Try the network if nothing found in DB
const peer = shuffleOne(Array.from(this.peers.values()));
if (!peer) {
this.status = BackfillSyncStatus.pending;
this.logger.debug("No peers yet");
continue;
}
try {
if (!this.syncAnchor.anchorBlock) {
await this.syncBlockByRoot(peer, this.syncAnchor.anchorBlockRoot);
// Go back and make the checks in case this block could be at or
// behind prevFinalizedCheckpointBlock
}
else {
await this.syncRange(peer);
// Go back and make the checks in case the lastbackSyncedBlock could be at or
// behind prevFinalizedCheckpointBlock
}
}
catch (e) {
this.metrics?.backfillSync.errors.inc();
this.logger.error("Sync error", {}, e);
if (e instanceof BackfillSyncError) {
switch (e.type.code) {
case BackfillSyncErrorCode.INTERNAL_ERROR:
// Break it out of the loop and throw error
this.status = BackfillSyncStatus.aborted;
break;
case BackfillSyncErrorCode.NOT_ANCHORED:
// biome-ignore lint/suspicious/noFallthroughSwitchClause: We need fall-through behavior here
case BackfillSyncErrorCode.NOT_LINEAR:
// Lets try to jump directly to the parent of this anchorBlock as previous
// (segment) of blocks could be orphaned/missed
if (this.syncAnchor.anchorBlock) {
this.syncAnchor = {
anchorBlock: null,
anchorBlockRoot: this.syncAnchor.anchorBlock.message.parentRoot,
anchorSlot: null,
lastBackSyncedBlock: this.syncAnchor.lastBackSyncedBlock,
};
}
// falls through
case BackfillSyncErrorCode.INVALID_SIGNATURE:
this.network.reportPeer(peer, PeerAction.LowToleranceError, "BadSyncBlocks");
}
}
}
finally {
if (this.status !== BackfillSyncStatus.aborted)
this.processor.trigger();
}
}
throw new ErrorAborted("BackfillSync");
}
/**
* Ensure that any weak subjectivity checkpoint provided in past with respect
* the initialization point is the same block tree as the DB once backfill
*/
async checkIfCheckpointSyncedAndValidate() {
if (this.syncAnchor.lastBackSyncedBlock == null) {
throw Error("Invalid lastBackSyncedBlock for checkpoint validation");
}
if (this.wsCheckpointHeader == null) {
throw Error("Invalid null checkpoint for validation");
}
if (this.wsValidated)
return;
if (this.wsCheckpointHeader.slot >= this.syncAnchor.lastBackSyncedBlock.slot) {
// Checkpoint root should be in db now , in case there are string of orphaned/missed
// slots before/leading up to checkpoint, the block just backsynced before the
// wsCheckpointHeader.slot will have the checkpoint root
const wsDbCheckpointBlock = await this.db.blockArchive.getByRoot(this.wsCheckpointHeader.root);
if (!wsDbCheckpointBlock ||
// The only validation we can do here is that wsDbCheckpointBlock is found at/before
// wsCheckpoint's epoch as there could be orphaned/missed slots all the way
// from wsDbCheckpointBlock's slot to the wsCheckpoint's epoch
// TODO: one can verify the child of wsDbCheckpointBlock is at
// slot > wsCheckpointHeader
// Note: next epoch is at wsCheckpointHeader.slot + SLOTS_PER_EPOCH
wsDbCheckpointBlock.message.slot >= this.wsCheckpointHeader.slot + SLOTS_PER_EPOCH)
// TODO: explode and stop the entire node
throw new Error(`InvalidWsCheckpoint root=${toRootHex(this.wsCheckpointHeader.root)}, epoch=${this.wsCheckpointHeader.slot / SLOTS_PER_EPOCH}, ${wsDbCheckpointBlock
? "found at epoch=" + Math.floor(wsDbCheckpointBlock?.message.slot / SLOTS_PER_EPOCH)
: "not found"}`);
this.logger.info("wsCheckpoint validated!", {
root: toRootHex(this.wsCheckpointHeader.root),
epoch: this.wsCheckpointHeader.slot / SLOTS_PER_EPOCH,
});
this.wsValidated = true;
}
}
async checkUpdateFromBackfillSequences() {
if (this.syncAnchor.lastBackSyncedBlock === null) {
throw Error("Backfill ranges can only be used once we have a valid lastBackSyncedBlock as a pivot point");
}
let validSequence = false;
if (this.syncAnchor.lastBackSyncedBlock.slot === null)
return validSequence;
const lastBackSyncedSlot = this.syncAnchor.lastBackSyncedBlock.slot;
const filteredSeqs = await this.db.backfilledRanges.entries({
gte: lastBackSyncedSlot,
});
if (filteredSeqs.length > 0) {
const jumpBackTo = Math.min(...filteredSeqs.map(({ value: justToSlot }) => justToSlot));
if (jumpBackTo < lastBackSyncedSlot) {
validSequence = true;
const anchorBlock = await this.db.blockArchive.get(jumpBackTo);
if (!anchorBlock) {
validSequence = false;
this.logger.warn(`Invalid backfill sequence: expected a block at ${jumpBackTo} in blockArchive, ignoring the sequence`);
}
if (anchorBlock && validSequence && this.prevFinalizedCheckpointBlock.slot >= jumpBackTo) {
this.logger.debug(`Found a sequence going back to ${jumpBackTo} before the previous finalized or wsCheckpoint`, { slot: this.prevFinalizedCheckpointBlock.slot });
// Everything saved in db between a backfilled range is a connected sequence
// we only need to check if prevFinalizedCheckpointBlock is in db
const prevBackfillCpBlock = await this.db.blockArchive.getByRoot(this.prevFinalizedCheckpointBlock.root);
if (prevBackfillCpBlock != null &&
this.prevFinalizedCheckpointBlock.slot === prevBackfillCpBlock.message.slot) {
this.logger.verbose("Validated current prevFinalizedCheckpointBlock", {
root: toRootHex(this.prevFinalizedCheckpointBlock.root),
slot: prevBackfillCpBlock.message.slot,
});
}
else {
validSequence = false;
this.logger.warn(`Invalid backfill sequence: previous finalized or checkpoint block root=${toRootHex(this.prevFinalizedCheckpointBlock.root)}, slot=${this.prevFinalizedCheckpointBlock.slot} ${prevBackfillCpBlock ? "found at slot=" + prevBackfillCpBlock.message.slot : "not found"}, ignoring the sequence`);
}
}
if (anchorBlock && validSequence) {
// Update the current sequence in DB as we will be cleaning up previous sequences
await this.db.backfilledRanges.put(this.backfillStartFromSlot, jumpBackTo);
this.backfillRangeWrittenSlot = jumpBackTo;
this.logger.verbose(`Jumped and updated the backfilled range ${this.backfillStartFromSlot}, ${this.backfillRangeWrittenSlot}`, { jumpBackTo });
const anchorBlockHeader = blockToHeader(this.config, anchorBlock.message);
const anchorBlockRoot = ssz.phase0.BeaconBlockHeader.hashTreeRoot(anchorBlockHeader);
this.syncAnchor = {
anchorBlock,
anchorBlockRoot,
anchorSlot: jumpBackTo,
lastBackSyncedBlock: { root: anchorBlockRoot, slot: jumpBackTo, block: anchorBlock },
};
if (this.prevFinalizedCheckpointBlock.slot >= jumpBackTo) {
// prevFinalizedCheckpointBlock must have been validated, update to a
// new unverified
// finalized or wsCheckpoint behind the new lastBackSyncedBlock
this.prevFinalizedCheckpointBlock = await extractPreviousFinOrWsCheckpoint(this.config, this.db, jumpBackTo, this.logger);
}
this.metrics?.backfillSync.totalBlocks.inc({ method: BackfillSyncMethod.backfilled_ranges }, lastBackSyncedSlot - jumpBackTo);
}
}
}
// Only delete < backfillStartFromSlot, the keys greater than this would be cleaned
// up by the archival process of forward sync
const cleanupSeqs = filteredSeqs.filter((entry) => entry.key < this.backfillStartFromSlot);
if (cleanupSeqs.length > 0) {
await this.db.backfilledRanges.batchDelete(cleanupSeqs.map((entry) => entry.key));
this.logger.debug(`Cleaned up the old sequences between ${this.backfillStartFromSlot},${toRootHex(this.syncAnchor.lastBackSyncedBlock.root)}`, { cleanupSeqs: JSON.stringify(cleanupSeqs) });
}
return validSequence;
}
async fastBackfillDb() {
// Block of this anchorBlockRoot can't be behind the prevFinalizedCheckpointBlock
// as prevFinalizedCheckpointBlock can't be skipped
let anchorBlockRoot;
let expectedSlot = null;
if (this.syncAnchor.anchorBlock) {
anchorBlockRoot = this.syncAnchor.anchorBlock.message.parentRoot;
}
else {
anchorBlockRoot = this.syncAnchor.anchorBlockRoot;
expectedSlot = this.syncAnchor.anchorSlot;
}
let anchorBlock = await this.db.blockArchive.getByRoot(anchorBlockRoot);
if (!anchorBlock)
return false;
if (expectedSlot !== null && anchorBlock.message.slot !== expectedSlot)
throw Error(`Invalid slot of anchorBlock read from DB with root=${toRootHex(anchorBlockRoot)}, expected=${expectedSlot}, actual=${anchorBlock.message.slot}`);
// If possible, read back till anchorBlock > this.prevFinalizedCheckpointBlock
let parentBlock, backCount = 1;
let isPrevFinWsConfirmedAnchorParent = false;
while (backCount !== this.opts.backfillBatchSize &&
// biome-ignore lint/suspicious/noAssignInExpressions: <explanation>
(parentBlock = await this.db.blockArchive.getByRoot(anchorBlock.message.parentRoot))) {
// Before moving anchorBlock back, we need check for prevFinalizedCheckpointBlock
if (anchorBlock.message.slot < this.prevFinalizedCheckpointBlock.slot) {
throw Error(`Skipped a prevFinalizedCheckpointBlock with slot=${toRootHex(this.prevFinalizedCheckpointBlock.root)}, root=${toRootHex(this.prevFinalizedCheckpointBlock.root)}`);
}
if (anchorBlock.message.slot === this.prevFinalizedCheckpointBlock.slot) {
if (!isPrevFinWsConfirmedAnchorParent &&
!byteArrayEquals(anchorBlockRoot, this.prevFinalizedCheckpointBlock.root)) {
throw Error(`Invalid root for prevFinalizedCheckpointBlock at slot=${this.prevFinalizedCheckpointBlock.slot}, expected=${toRootHex(this.prevFinalizedCheckpointBlock.root)}, found=${toRootHex(anchorBlockRoot)}`);
}
// If the new parentBlock is just one slot back, we can safely assign
// prevFinalizedCheckpointBlock with the parentBlock and skip root
// validation in next iteration. Else we need to extract
// prevFinalizedCheckpointBlock
if (parentBlock.message.slot === anchorBlock.message.slot - 1) {
this.prevFinalizedCheckpointBlock = { root: anchorBlock.message.parentRoot, slot: parentBlock.message.slot };
isPrevFinWsConfirmedAnchorParent = true;
}
else {
// Extract new prevFinalizedCheckpointBlock below anchorBlock
this.prevFinalizedCheckpointBlock = await extractPreviousFinOrWsCheckpoint(this.config, this.db, anchorBlock.message.slot, this.logger);
isPrevFinWsConfirmedAnchorParent = false;
}
}
anchorBlockRoot = anchorBlock.message.parentRoot;
anchorBlock = parentBlock;
backCount++;
}
this.syncAnchor = {
anchorBlock,
anchorBlockRoot,
anchorSlot: anchorBlock.message.slot,
lastBackSyncedBlock: { root: anchorBlockRoot, slot: anchorBlock.message.slot, block: anchorBlock },
};
this.metrics?.backfillSync.totalBlocks.inc({ method: BackfillSyncMethod.database }, backCount);
this.logger.verbose(`Read ${backCount} blocks from DB till `, {
slot: anchorBlock.message.slot,
});
if (backCount >= this.opts.backfillBatchSize) {
// We should sleep as there seems to be more that can be read from db but yielding to
// the sync loop hardly gives any breather to the beacon node
await sleep(DB_READ_BREATHER_TIMEOUT, this.signal);
}
return true;
}
async syncBlockByRoot(peer, anchorBlockRoot) {
const res = await this.network.sendBeaconBlocksByRoot(peer, [anchorBlockRoot]);
if (res.length < 1)
throw new Error("InvalidBlockSyncedFromPeer");
const anchorBlock = res[0];
// GENESIS_SLOT doesn't has valid signature
if (anchorBlock.data.message.slot === GENESIS_SLOT)
return;
await verifyBlockProposerSignature(this.chain.bls, this.chain.getHeadState(), [anchorBlock]);
// We can write to the disk if this is ahead of prevFinalizedCheckpointBlock otherwise
// we will need to go make checks on the top of sync loop before writing as it might
// override prevFinalizedCheckpointBlock
if (this.prevFinalizedCheckpointBlock.slot < anchorBlock.data.message.slot)
await this.db.blockArchive.putBinary(anchorBlock.data.message.slot, anchorBlock.bytes);
this.syncAnchor = {
anchorBlock: anchorBlock.data,
anchorBlockRoot,
anchorSlot: anchorBlock.data.message.slot,
lastBackSyncedBlock: { root: anchorBlockRoot, slot: anchorBlock.data.message.slot, block: anchorBlock.data },
};
this.metrics?.backfillSync.totalBlocks.inc({ method: BackfillSyncMethod.blockbyroot });
this.logger.verbose("Fetched new anchorBlock", {
root: toRootHex(anchorBlockRoot),
slot: anchorBlock.data.message.slot,
});
return;
}
async syncRange(peer) {
if (!this.syncAnchor.anchorBlock) {
throw Error("Invalid anchorBlock null for syncRange");
}
const toSlot = this.syncAnchor.anchorBlock.message.slot;
const fromSlot = Math.max(toSlot - this.opts.backfillBatchSize, this.prevFinalizedCheckpointBlock.slot, GENESIS_SLOT);
const blocks = await this.network.sendBeaconBlocksByRange(peer, {
startSlot: fromSlot,
count: toSlot - fromSlot,
step: 1,
});
const anchorParentRoot = this.syncAnchor.anchorBlock.message.parentRoot;
if (blocks.length === 0) {
// Lets just directly try to jump to anchorParentRoot
this.syncAnchor = {
anchorBlock: null,
anchorBlockRoot: anchorParentRoot,
anchorSlot: null,
lastBackSyncedBlock: this.syncAnchor.lastBackSyncedBlock,
};
return;
}
const { nextAnchor, verifiedBlocks, error } = verifyBlockSequence(this.config, blocks, anchorParentRoot);
// If any of the block's proposer signature fail, we can't trust this peer at all
if (verifiedBlocks.length > 0) {
await verifyBlockProposerSignature(this.chain.bls, this.chain.getHeadState(), verifiedBlocks);
// This is bad, like super bad. Abort the backfill
if (!nextAnchor)
throw new BackfillSyncError({
code: BackfillSyncErrorCode.INTERNAL_ERROR,
reason: "Invalid verifyBlockSequence result",
});
// Verified blocks are in reverse order with the nextAnchor being the smallest slot
// if nextAnchor is on the same slot as prevFinalizedCheckpointBlock, we can't save
// it before returning to top of sync loop for validation
const blocksToPut = nextAnchor.slot > this.prevFinalizedCheckpointBlock.slot
? verifiedBlocks
: verifiedBlocks.slice(0, verifiedBlocks.length - 1);
await this.db.blockArchive.batchPutBinary(blocksToPut.map((block) => ({
key: block.data.message.slot,
value: block.bytes,
slot: block.data.message.slot,
blockRoot: this.config.getForkTypes(block.data.message.slot).BeaconBlock.hashTreeRoot(block.data.message),
parentRoot: block.data.message.parentRoot,
})));
this.metrics?.backfillSync.totalBlocks.inc({ method: BackfillSyncMethod.rangesync }, verifiedBlocks.length);
}
// If nextAnchor provided, found some linear anchored blocks
if (nextAnchor !== null) {
this.syncAnchor = {
anchorBlock: nextAnchor.block,
anchorBlockRoot: nextAnchor.root,
anchorSlot: nextAnchor.slot,
lastBackSyncedBlock: nextAnchor,
};
this.logger.verbose(`syncRange discovered ${verifiedBlocks.length} valid blocks`, {
backfilled: this.syncAnchor.lastBackSyncedBlock.slot,
});
}
if (error != null)
throw new BackfillSyncError({ code: error });
}
}
async function extractPreviousFinOrWsCheckpoint(config, db, belowSlot, logger) {
// Anything below genesis block is just zero hash
if (belowSlot <= GENESIS_SLOT)
return { root: ZERO_HASH, slot: belowSlot - 1 };
// To extract the next prevFinalizedCheckpointBlock, we just need to look back in DB
// Any saved previous finalized or ws checkpoint, will also have a corresponding block
// saved in DB, as we make sure of that
// 1. When we archive new finalized state and blocks
// 2. When we backfill from a wsCheckpoint
const nextPrevFinOrWsBlock = (await db.blockArchive.values({
lt: belowSlot,
reverse: true,
limit: 1,
}))[0];
let prevFinalizedCheckpointBlock;
if (nextPrevFinOrWsBlock != null) {
const header = blockToHeader(config, nextPrevFinOrWsBlock.message);
const root = ssz.phase0.BeaconBlockHeader.hashTreeRoot(header);
prevFinalizedCheckpointBlock = { root, slot: nextPrevFinOrWsBlock.message.slot };
logger?.debug("Extracted new prevFinalizedCheckpointBlock as potential previous finalized or wsCheckpoint", {
root: toRootHex(prevFinalizedCheckpointBlock.root),
slot: prevFinalizedCheckpointBlock.slot,
});
}
else {
// GENESIS_SLOT -1 is the placeholder for parentHash of the genesis block
// which should always be ZERO_HASH.
prevFinalizedCheckpointBlock = { root: ZERO_HASH, slot: GENESIS_SLOT - 1 };
}
return prevFinalizedCheckpointBlock;
}
//# sourceMappingURL=backfill.js.map