Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,9 @@ export async function archiveBlocks(
const rootAndSlot = {slot: block.slot, root: fromHex(block.blockRoot)};
const isPostGloasBlock = config.getForkSeq(block.slot) >= ForkSeq.gloas;

if (isPostGloasBlock) {
finalizedCanonicalBlockRoots.push(rootAndSlot);
if (block.payloadStatus === PayloadStatus.FULL) {
finalizedCanonicalEnvelopeBlockRoots.push(rootAndSlot);
}
} else {
finalizedCanonicalBlockRoots.push(rootAndSlot);
finalizedCanonicalBlockRoots.push(rootAndSlot);
if (isPostGloasBlock && block.payloadStatus === PayloadStatus.FULL) {
finalizedCanonicalEnvelopeBlockRoots.push(rootAndSlot);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,13 @@ export async function verifyBlocksStateTransitionOnly(
signal: AbortSignal,
opts: BlockProcessOpts & ImportBlockOpts
): Promise<{
preStates: CachedBeaconStateAllForks[];
postStates: CachedBeaconStateAllForks[];
postEnvelopeStates: Map<Slot, CachedBeaconStateGloas | null>;
proposerBalanceDeltas: number[];
verifyStateTime: number;
}> {
const preStates: CachedBeaconStateAllForks[] = [];
const postStates: CachedBeaconStateAllForks[] = [];
const postEnvelopeStates = new Map<Slot, CachedBeaconStateGloas | null>();
const proposerBalanceDeltas: number[] = [];
Expand All @@ -49,8 +51,32 @@ export async function verifyBlocksStateTransitionOnly(
for (let i = 0; i < blocks.length; i++) {
const {validProposerSignature, validSignatures} = opts;
const block = blocks[i].getBlock();
const preState =
i === 0 ? preState0 : (postEnvelopeStates.get(blocks[i - 1].getBlock().message.slot) ?? postStates[i - 1]);
let preState: CachedBeaconStateAllForks;
if (i === 0) {
preState = preState0;
} else {
const prevSlot = blocks[i - 1].getBlock().message.slot;
const prevPostEnvelopeState = postEnvelopeStates.get(prevSlot);
if (prevPostEnvelopeState && isGloasBeaconBlock(block.message)) {
// In ePBS, the proposer may build on the FULL path (saw previous envelope)
// or the EMPTY path (didn't see it). Check bid.parentBlockHash to determine:
// - If it matches the previous envelope's payload.blockHash → FULL path
// - Otherwise → EMPTY path (use block-only state)
const bid = block.message.body.signedExecutionPayloadBid.message;
const prevEnvelope = envelopes?.get(prevSlot);
if (prevEnvelope && byteArrayEquals(bid.parentBlockHash, prevEnvelope.message.payload.blockHash)) {
// FULL path: block builds on top of revealed payload
preState = prevPostEnvelopeState;
} else {
// EMPTY path: block was produced without seeing previous envelope
preState = postStates[i - 1];
}
} else {
// No envelope for previous block or pre-Gloas: use envelope state if available, else block state
preState = prevPostEnvelopeState ?? postStates[i - 1];
}
}
preStates[i] = preState;
const dataAvailabilityStatus = dataAvailabilityStatuses[i];

// STFN - per_slot_processing() + per_block_processing()
Expand Down Expand Up @@ -146,5 +172,5 @@ export async function verifyBlocksStateTransitionOnly(
logger.debug("Verified block state transition", {slot, recvToValLatency, recvToValidation, validationTime});
}

return {postStates, postEnvelopeStates, proposerBalanceDeltas, verifyStateTime};
return {preStates, postStates, postEnvelopeStates, proposerBalanceDeltas, verifyStateTime};
}
11 changes: 10 additions & 1 deletion packages/beacon-node/src/network/reqresp/score.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ const multiStreamSelectErrorCodes = {
protocolSelectionFailed: "protocol selection failed",
};

const reqRespRateLimitErrorMessages = [
RequestErrorCode.REQUEST_RATE_LIMITED,
RequestErrorCode.REQUEST_SELF_RATE_LIMITED,
RequestErrorCode.RESP_RATE_LIMITED,
] as const;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are these introduced?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These (reqRespRateLimitErrorMessages array + the SERVER_ERROR check) are part of the rate-limit backoff handling. On epbs-devnet-0, peers frequently return SERVER_ERROR with rate-limit messages when hammered with by-range requests during sync. Without this, every rate-limited response penalizes the peer with MidToleranceError, which quickly burns through all peers in a small devnet.

The change distinguishes rate-limit SERVER_ERROR (skip penalty, let the backoff/retry handle it) from genuine server errors (still penalized). This was essential for syncing epbs-devnet-0 where the peer set is small (~5 nodes).

On a stable mainnet-scale network it would still be beneficial — rate-limiting is a cooperative signal, not adversarial behavior, so penalizing peers for it is counterproductive regardless of network size.


export function onOutgoingReqRespError(e: RequestError, method: ReqRespMethod): PeerAction | null {
switch (e.type.code) {
case RequestErrorCode.INVALID_REQUEST:
Expand All @@ -27,7 +33,9 @@ export function onOutgoingReqRespError(e: RequestError, method: ReqRespMethod):
return PeerAction.LowToleranceError;

case RequestErrorCode.SERVER_ERROR:
return PeerAction.MidToleranceError;
return reqRespRateLimitErrorMessages.some((errMessage) => e.message.includes(errMessage))

Copilot AI Mar 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For RequestErrorCode.SERVER_ERROR, the rate-limit check currently inspects e.message.includes(...). Since RequestError already exposes the structured e.type.errorMessage for this branch, using that field would be more robust (avoids coupling to how RequestError formats its .message).

Suggested change
return reqRespRateLimitErrorMessages.some((errMessage) => e.message.includes(errMessage))
return reqRespRateLimitErrorMessages.some((errMessage) => e.type.errorMessage?.includes(errMessage))

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reasonable suggestion. The e.message check is pre-existing code from unstable (not introduced in this PR). Using e.type.errorMessage would be slightly more robust, but both paths produce identical results since RequestError.message includes the errorMessage field. Not changing pre-existing patterns in this scoped PR — can be picked up in a general reqresp cleanup.

? null
: PeerAction.MidToleranceError;
case RequestErrorCode.UNKNOWN_ERROR_STATUS:
return PeerAction.HighToleranceError;

Expand Down Expand Up @@ -59,6 +67,7 @@ export function onOutgoingReqRespError(e: RequestError, method: ReqRespMethod):
return PeerAction.Fatal;
case ReqRespMethod.Metadata:
case ReqRespMethod.Status:
case ReqRespMethod.ExecutionPayloadEnvelopesByRange:
return PeerAction.LowToleranceError;
default:
return null;
Expand Down
74 changes: 37 additions & 37 deletions packages/beacon-node/src/sync/range/batch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,47 +146,39 @@ export class Batch {
count: this.count,
step: 1,
};
const requests: DownloadByRangeRequests = {blocksRequest};

// Post-Gloas envelopes are required for block processing, independent of DA retention window.
if (isForkPostGloas(this.forkName)) {
requests.envelopesRequest = {
startSlot: this.startSlot,
count: this.count,
};
}

if (isForkPostFulu(this.forkName) && withinValidRequestWindow) {
const columnsRequest = {
requests.columnsRequest = {
startSlot: this.startSlot,
count: this.count,
columns: this.custodyConfig.sampledColumns,
};
if (isForkPostGloas(this.forkName)) {
const envelopesRequest: gloas.ExecutionPayloadEnvelopesByRangeRequest = {
startSlot: this.startSlot,
count: this.count,
};
return {
blocksRequest,
columnsRequest,
envelopesRequest,
};
}
return {
blocksRequest,
columnsRequest,
};
}
if (isForkPostDeneb(this.forkName) && withinValidRequestWindow) {
return {
blocksRequest,
blobsRequest: {
startSlot: this.startSlot,
count: this.count,
},
} else if (isForkPostDeneb(this.forkName) && withinValidRequestWindow) {
requests.blobsRequest = {
startSlot: this.startSlot,
count: this.count,
};
}
return {
blocksRequest,
};

return requests;
}

// subsequent request where part of the epoch has already been downloaded. Need to figure out what is the beginning
// of the range where download needs to resume
let blockStartSlot = this.startSlot;
let dataStartSlot = this.startSlot;
let envelopeStartSlot = this.startSlot;
const neededColumns = new Set<number>();
const envelopesBySlot = this.state.envelopes ?? new Map<Slot, gloas.SignedExecutionPayloadEnvelope>();

// ensure blocks are in slot-wise order
for (const blockInput of blocks) {
Expand All @@ -204,6 +196,10 @@ export class Batch {
if (blockInput.hasBlock() && blockStartSlot === blockSlot) {
blockStartSlot = blockSlot + 1;
}
if (blockInput.hasBlock() && envelopeStartSlot === blockSlot && envelopesBySlot.has(blockSlot)) {
envelopeStartSlot = blockSlot + 1;
}

if (!blockInput.hasAllData()) {
if (isBlockInputColumns(blockInput)) {
for (const index of blockInput.getMissingSampledColumnMeta().missing) {
Expand All @@ -227,6 +223,14 @@ export class Batch {
step: 1,
};
}

if (isForkPostGloas(this.forkName) && envelopeStartSlot <= endSlot) {
requests.envelopesRequest = {
startSlot: envelopeStartSlot,
count: endSlot - envelopeStartSlot + 1,
};
}

if (dataStartSlot <= endSlot) {
// range of 40 - 63, startSlot will be inclusive but subtraction will exclusive so need to + 1
const count = endSlot - dataStartSlot + 1;
Expand All @@ -236,12 +240,6 @@ export class Batch {
startSlot: dataStartSlot,
columns: Array.from(neededColumns),
};
if (isForkPostGloas(this.forkName)) {
requests.envelopesRequest = {
count,
startSlot: dataStartSlot,
};
}
} else if (isForkPostDeneb(this.forkName) && withinValidRequestWindow) {
requests.blobsRequest = {
count,
Expand Down Expand Up @@ -364,14 +362,16 @@ export class Batch {
/**
* Downloading -> AwaitingDownload
*/
downloadingError(peer: PeerIdStr): void {
downloadingError(peer: PeerIdStr, {countFailedAttempt = true}: {countFailedAttempt?: boolean} = {}): void {
if (this.state.status !== BatchStatus.Downloading) {
throw new BatchError(this.wrongStatusErrorType(BatchStatus.Downloading));
}

this.failedDownloadAttempts.push(peer);
if (this.failedDownloadAttempts.length > MAX_BATCH_DOWNLOAD_ATTEMPTS) {
throw new BatchError(this.errorType({code: BatchErrorCode.MAX_DOWNLOAD_ATTEMPTS}));
if (countFailedAttempt) {
this.failedDownloadAttempts.push(peer);
if (this.failedDownloadAttempts.length > MAX_BATCH_DOWNLOAD_ATTEMPTS) {
throw new BatchError(this.errorType({code: BatchErrorCode.MAX_DOWNLOAD_ATTEMPTS}));
}
}

this.state = {status: BatchStatus.AwaitingDownload, blocks: this.state.blocks, envelopes: this.state.envelopes};
Expand Down
Loading