Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
317e04b
fix: reject payload envelopes for proposer equivocations
nflaig Aug 3, 2026
b4fad04
fix: track gloas proposals after execution verification
nflaig Aug 3, 2026
348df93
feat: validate block proposer equivocations
nflaig Aug 3, 2026
4b289d0
Merge branch 'unstable' into nflaig/reject-equivocating-payload-envel…
nflaig Aug 4, 2026
9d018e8
Merge branch 'unstable' into nflaig/reject-equivocating-payload-envel…
nflaig Aug 4, 2026
198bdf5
Merge branch 'unstable' into nflaig/reject-equivocating-payload-envel…
nflaig Aug 5, 2026
2cb6ade
refine proposer equivocation validation
nflaig Aug 5, 2026
5702042
Merge branch 'unstable' into nflaig/reject-equivocating-payload-envel…
nflaig Aug 5, 2026
8902551
fix: log proposer equivocation rejections as warnings
nflaig Aug 5, 2026
10522f4
Restore finalized slot guard for seen block proposers
nflaig Aug 5, 2026
c448521
clarify seen block proposer cache invariants
nflaig Aug 5, 2026
589d073
apply repeat proposal race check across forks
nflaig Aug 5, 2026
f80e52d
remove periods from single sentence comments
nflaig Aug 5, 2026
f3805fb
document proposer block root limit
nflaig Aug 5, 2026
5ba5e9a
use jsdoc for proposer block root limit
nflaig Aug 5, 2026
84329fa
clarify proposer equivocation evidence wording
nflaig Aug 5, 2026
fcca661
clarify equivocation test descriptions
nflaig Aug 5, 2026
5a1661d
qualify equivocation validation tests
nflaig Aug 5, 2026
14f8b44
group tests by broadcast validation strategy
nflaig Aug 5, 2026
8607cf0
group repeat proposal validation tests
nflaig Aug 5, 2026
3fd4d10
share repeat proposal tests across forks
nflaig Aug 5, 2026
6654ca6
simplify repeat proposal validation tests
nflaig Aug 5, 2026
8a55739
construct test chain with gloas config
nflaig Aug 5, 2026
64a632f
use consistent repeat proposal test setup
nflaig Aug 5, 2026
76461b1
verify local proposer signatures for consensus validation
nflaig Aug 5, 2026
2c2d723
clarify local block signature validation
nflaig Aug 5, 2026
7360da2
refine local signature validation comment
nflaig Aug 5, 2026
308d3af
restore generic signed block test type
nflaig Aug 5, 2026
0154c58
Merge branch 'unstable' into nflaig/reject-equivocating-payload-envel…
nflaig Aug 6, 2026
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
76 changes: 60 additions & 16 deletions packages/beacon-node/src/api/impl/beacon/blocks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ import {
ProduceFullGloas,
} from "../../../../chain/produceBlock/index.js";
import {RegenCaller} from "../../../../chain/regen/index.js";
import {validateGossipBlock} from "../../../../chain/validation/block.js";
import {validateGossipBlock, verifyBlockProposerSignature} from "../../../../chain/validation/block.js";
import {validateApiExecutionPayloadBid} from "../../../../chain/validation/executionPayloadBid.js";
import {validateApiExecutionPayloadEnvelope} from "../../../../chain/validation/executionPayloadEnvelope.js";
import {OpSource} from "../../../../chain/validatorMonitor.js";
Expand Down Expand Up @@ -268,7 +268,6 @@ export function getBeaconBlockApi({
await verifyBlocksInEpoch.call(chain as BeaconChain, parentBlock, [blockForImport], null, {
...opts,
verifyOnly: true,
skipVerifyBlockSignatures: true,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

consensus validation should make sure the block has valid signatures, this seems like a bug to skip verification in this case

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.

this becomes a dead flag after this PR, noone specify it anymore, maybe we should remove it?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

what you mean by dead? passing broadcast_validation=consensus is still valid option, the additional equivocation check is mostly used by builders, I don't see a reason a proposer itself would use it, I remember we have used consensus in the past to avoid broadcasting a invalid block, although I think using gossip (the default) should be best for most proposers so there is low overhead

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

ah I think I misunderstood what you mean, it's about removing skipVerifyBlockSignatures, let's do that, I am not sure why it was added

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

seems like it was introduced in #6149 but this doesn't seem right, based on what other clients do, when broadcast_validation=consensus is passed, we should validate these as well

the spec explicitly says this too

image

including validation of all signatures and blocks fields

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

skipVerifyExecutionPayload: true,
seenTimestampSec,
});
Expand All @@ -285,12 +284,26 @@ export function getBeaconBlockApi({

chain.logger.debug("Consensus validated while publishing block", valLogMeta);

if (broadcastValidation === routes.beacon.BroadcastValidation.consensusAndEquivocation) {
const message = `Equivocation checks not yet implemented for broadcastValidation=${broadcastValidation}`;
if (chain.opts.broadcastValidationStrictness === "error") {
throw Error(message);
// Non-local blocks had their proposer and block-body signatures checked by verifyBlocksInEpoch above
// Locally produced blocks already passed block production validation, so only their proposer signature is unchecked
// Verify that signature and observe the block root here
if (blockLocallyProduced) {
try {
await verifyBlockProposerSignature(chain, signedBlock, blockRoot);
Comment thread
nflaig marked this conversation as resolved.
chain.seenBlockProposers.observeBlockRoot(slot, signedBlock.message.proposerIndex, blockRoot);
} catch (e) {
chain.logger.error(
"Proposer signature validation failed while publishing the block",
valLogMeta,
e as Error
);
chain.persistInvalidSszValue(
chain.config.getForkTypes(slot).SignedBeaconBlock,
signedBlock,
"api_reject_consensus_failure"
);
throw e;
}
chain.logger.warn(message, valLogMeta);
}
break;
}
Expand Down Expand Up @@ -318,6 +331,25 @@ export function getBeaconBlockApi({
await sleep(msToBlockSlot);
}

if (broadcastValidation === routes.beacon.BroadcastValidation.consensusAndEquivocation) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject equivocating blinded Fulu blocks

When a validator calls /eth/v2/beacon/blinded_blocks on Fulu with broadcast_validation=consensus_and_equivocation, publishBlindedBlockV2 takes the isForkPostFulu(fork) branch and calls submitBlindedBlockToBuilder directly, so it never reaches this new publishBlockV2 equivocation gate. That can still submit a blinded block to the builder even when seenBlockProposers already has a conflicting root for the same slot and proposer; add the same proposer-signature and getConflictingBlockRoots check before the direct submit path.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

this is pointless for blinded publishing, we submit the block to the builder anyways and broadcast_validation=consensus_and_equivocation is not something proposers use, it's meant to be used by builders to protect them from unbundling attacks

const conflictingRoots = chain.seenBlockProposers.getConflictingBlockRoots(
slot,
signedBlock.message.proposerIndex,
blockRoot
);
if (conflictingRoots.length > 0) {
chain.logger.warn("Not publishing block due to proposer equivocation", {
...valLogMeta,
conflictingRoots: conflictingRoots.join(", "),
});
throw new ApiError(
400,
`Block is a proposer equivocation, conflicting block roots: ${conflictingRoots.join(", ")}`
);
}
chain.logger.debug("Equivocation validated while publishing the block", valLogMeta);
}

// TODO: Validate block
const delaySec = seenTimestampSec - computeTimeAtSlot(config, slot, chain.genesisTime);
metrics?.gossipBlock.elapsedTimeTillReceived.observe({source: OpSource.api}, delaySec);
Expand Down Expand Up @@ -775,15 +807,6 @@ export function getBeaconBlockApi({
throw new ApiError(400, (error as Error).message);
}
chain.logger.debug("Consensus validated while publishing execution payload envelope", valLogMeta);

// TODO GLOAS: check the block is not a proposer equivocation before publishing the envelope
if (broadcastValidation === routes.beacon.BroadcastValidation.consensusAndEquivocation) {
const message = `Equivocation checks not yet implemented for broadcastValidation=${broadcastValidation}`;
if (chain.opts.broadcastValidationStrictness === "error") {
throw Error(message);
}
chain.logger.warn(message, valLogMeta);
}
break;
}

Expand Down Expand Up @@ -886,6 +909,27 @@ export function getBeaconBlockApi({
await sleep(msToBlockSlot);
}

// Keep this as the final async validation before publishing. A conflicting block may be observed while the
// envelope, blob data, or slot timing is being validated above.
if (broadcastValidation === routes.beacon.BroadcastValidation.consensusAndEquivocation) {
const conflictingRoots = chain.seenBlockProposers.getConflictingBlockRoots(
slot,
payloadInput.proposerIndex,
blockRootHex
);
if (conflictingRoots.length > 0) {
chain.logger.warn("Not publishing execution payload envelope due to proposer equivocation", {
...valLogMeta,
conflictingRoots: conflictingRoots.join(", "),
});
throw new ApiError(
400,
`Block of execution payload envelope is a proposer equivocation, conflicting block roots: ${conflictingRoots.join(", ")}`
);
}
chain.logger.debug("Equivocation validated while publishing execution payload envelope", valLogMeta);
}

if (payloadInput.hasPayloadEnvelope()) {
// The envelope may have been added while this request was being validated, e.g. via gossip
chain.logger.debug("Execution payload envelope already added during publishing", valLogMeta);
Expand Down
7 changes: 7 additions & 0 deletions packages/beacon-node/src/chain/blocks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,13 @@ export async function processBlocks(
throw segmentExecStatus.execAborted.execError;
}

if (opts.skipVerifyBlockSignatures !== true) {
for (const blockInput of relevantBlocks) {
const block = blockInput.getBlock().message;
this.seenBlockProposers.add(block.slot, block.proposerIndex);
}
}

const {executionStatuses} = segmentExecStatus;
const verifiedBlocksBySlot = new Map<Slot, FullyVerifiedBlock>();
for (let i = 0; i < relevantBlocks.length; i++) {
Expand Down
10 changes: 10 additions & 0 deletions packages/beacon-node/src/chain/blocks/verifyBlock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {ExecutionStatus, ProtoBlock} from "@lodestar/fork-choice";
import {ForkName, ForkSeq, isForkPostFulu} from "@lodestar/params";
import {DataAvailabilityStatus, IBeaconStateView, computeEpochAtSlot} from "@lodestar/state-transition";
import {IndexedAttestation, Slot, deneb} from "@lodestar/types";
import {toRootHex} from "@lodestar/utils";
import {getBlobKzgCommitments} from "../../util/dataColumns.js";
import type {BeaconChain} from "../chain.js";
import {BlockError, BlockErrorCode} from "../errors/index.js";
Expand Down Expand Up @@ -204,6 +205,15 @@ export async function verifyBlocksInEpoch(
// maybe chain with the above verifyBlocksSignatures()
]);

if (opts.skipVerifyBlockSignatures !== true) {
for (const block of blocks) {
const blockRoot = toRootHex(
this.config.getForkTypes(block.message.slot).BeaconBlock.hashTreeRoot(block.message)

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.

we should be able to get block root from BlockInput instead

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

);
this.seenBlockProposers.observeBlockRoot(block.message.slot, block.message.proposerIndex, blockRoot);
}
}

if (opts.verifyOnly !== true) {
const fromForkBoundary = this.config.getForkBoundaryAtEpoch(computeEpochAtSlot(parentBlock.slot));
const toForkBoundary = this.config.getForkBoundaryAtEpoch(computeEpochAtSlot(lastBlock.message.slot));
Expand Down
51 changes: 46 additions & 5 deletions packages/beacon-node/src/chain/seenCache/seenBlockProposers.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,57 @@
import {computeStartSlotAtEpoch} from "@lodestar/state-transition";
import {Epoch, Slot, ValidatorIndex} from "@lodestar/types";
import {Epoch, RootHex, Slot, ValidatorIndex} from "@lodestar/types";
import {MapDef} from "@lodestar/utils";

/** Two distinct block roots signed by the same proposer for the same slot are sufficient to establish an equivocation */
const MAX_BLOCK_ROOTS_PER_PROPOSAL = 2;

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.

this name is confusing to me, it feels like allowed block roots per proposal
I think the constant should be 1, rename it to something like MAX_ALLOWED_BLOCK_ROOTS_PER_PROPOSAL = 1, and change all logics accordingly
or name it different way: MIN_EQUIVOCATION_BLOCK_ROOTS_PER_PROPOSAL = 2 without changing the below logic

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I like MIN_EQUIVOCATION_BLOCK_ROOTS_PER_PROPOSAL, agree the name isn't entirely clear, the jsdoc helps though

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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


/**
* Keeps a cache to filter block proposals from the same validator in the same slot.
*
* This cache is not bounded and for extremely long periods of non-finality it can grow a lot. However it's practically
* limited by the possible shufflings in those epochs, and the stored data is very cheap
* Block roots with a signature verified against the block's proposer index are tracked separately from proposals
* accepted by gossip validation or block import. A root from a block signed by its declared proposer is potential
* equivocation evidence, but the block may still fail other validation. Such a block must not mark the proposal as
* known, since that would cause a later valid block for the same slot and proposer to be ignored as a repeat proposal.
*
* The cache is pruned on finalization and bounds the number of roots stored per proposer and slot
*/
export class SeenBlockProposers {
private readonly proposerIndexesBySlot = new MapDef<Slot, Set<ValidatorIndex>>(() => new Set<ValidatorIndex>());
private finalizedSlot: Epoch = 0;
private readonly blockRootsBySlot = new MapDef<Slot, MapDef<ValidatorIndex, Set<RootHex>>>(
Comment thread
nflaig marked this conversation as resolved.
() => new MapDef<ValidatorIndex, Set<RootHex>>(() => new Set<RootHex>())
);
private finalizedSlot: Slot = 0;

isKnown(blockSlot: Slot, proposerIndex: ValidatorIndex): boolean {
return this.proposerIndexesBySlot.get(blockSlot)?.has(proposerIndex) === true;
}

hasBlockRoot(blockSlot: Slot, proposerIndex: ValidatorIndex, blockRoot: RootHex): boolean {
return this.blockRootsBySlot.get(blockSlot)?.get(proposerIndex)?.has(blockRoot) === true;
}

isEquivocating(blockSlot: Slot, proposerIndex: ValidatorIndex): boolean {
return (this.blockRootsBySlot.get(blockSlot)?.get(proposerIndex)?.size ?? 0) >= MAX_BLOCK_ROOTS_PER_PROPOSAL;
}

getConflictingBlockRoots(blockSlot: Slot, proposerIndex: ValidatorIndex, blockRoot: RootHex): RootHex[] {
const roots = this.blockRootsBySlot.get(blockSlot)?.get(proposerIndex);
return roots === undefined ? [] : Array.from(roots).filter((root) => root !== blockRoot);
}

/** Record a block only after its proposer signature has been verified */
observeBlockRoot(blockSlot: Slot, proposerIndex: ValidatorIndex, blockRoot: RootHex): void {
if (blockSlot < this.finalizedSlot) {
throw Error(`blockSlot ${blockSlot} < finalizedSlot ${this.finalizedSlot}`);
}

const blockRoots = this.blockRootsBySlot.getOrDefault(blockSlot).getOrDefault(proposerIndex);
if (blockRoots.size < MAX_BLOCK_ROOTS_PER_PROPOSAL) {
blockRoots.add(blockRoot);
}
}

/** Mark a block as known from gossip or another block import path */
add(blockSlot: Slot, proposerIndex: ValidatorIndex): void {
if (blockSlot < this.finalizedSlot) {
throw Error(`blockSlot ${blockSlot} < finalizedSlot ${this.finalizedSlot}`);
Expand All @@ -31,9 +67,14 @@ export class SeenBlockProposers {
this.proposerIndexesBySlot.delete(slot);
}
}
for (const slot of this.blockRootsBySlot.keys()) {
if (slot < finalizedSlot) {
this.blockRootsBySlot.delete(slot);
}
}
}

seenAtEpoch(epoch: Slot, index: ValidatorIndex): boolean {
seenAtEpoch(epoch: Epoch, index: ValidatorIndex): boolean {
const fromSlot = computeStartSlotAtEpoch(epoch);
const toSlot = computeStartSlotAtEpoch(epoch + 1);

Expand Down
54 changes: 36 additions & 18 deletions packages/beacon-node/src/chain/validation/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
isExecutionBlockBodyType,
isStatePostBellatrix,
} from "@lodestar/state-transition";
import {SignedBeaconBlock, deneb, gloas, isGloasBeaconBlock} from "@lodestar/types";
import {RootHex, SignedBeaconBlock, deneb, gloas, isGloasBeaconBlock} from "@lodestar/types";
import {byteArrayEquals, sleep, toRootHex} from "@lodestar/utils";
import {BlockErrorCode, BlockGossipError, GossipAction} from "../errors/index.js";
import {IBeaconChain} from "../interface.js";
Expand Down Expand Up @@ -85,7 +85,12 @@ export async function validateGossipBlock(

// [IGNORE] The block is the first block with valid signature received for the proposer for the slot, signed_beacon_block.message.slot.
const proposerIndex = block.proposerIndex;
const hasBlockRoot = chain.seenBlockProposers.hasBlockRoot(blockSlot, proposerIndex, blockRoot);
if (chain.seenBlockProposers.isKnown(blockSlot, proposerIndex)) {
if (!hasBlockRoot && !chain.seenBlockProposers.isEquivocating(blockSlot, proposerIndex)) {
await verifyBlockProposerSignature(chain, signedBlock, blockRoot, {verifyOnMainThread: false});
chain.seenBlockProposers.observeBlockRoot(blockSlot, proposerIndex, blockRoot);
}
throw new BlockGossipError(GossipAction.IGNORE, {code: BlockErrorCode.REPEAT_PROPOSAL, proposerIndex});

@nflaig nflaig Aug 4, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This returns immediately after verifying the proposer signature, so a conflicting block never reaches normal processing or fork choice. #9233's shouldApplyProposerBoost() scans the ProtoArray for the timely same-proposer block, so it cannot observe equivocations received through the normal gossip path. Can we separate the gossip result from local handling here: still return IGNORE for propagation, but continue full validation locally and, if the block passes consensus, DA, and execution validation, import it into fork choice using its original receive time? A signature-valid but otherwise-invalid conflict should remain only in SeenBlockProposers.

(written by codex)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

we can handle that either in #9233 if this PR is merged first, or completely separate as a follow-up

}

Expand Down Expand Up @@ -270,18 +275,8 @@ export async function validateGossipBlock(
}

// [REJECT] The proposer signature, signed_beacon_block.signature, is valid with respect to the proposer_index pubkey.
if (!chain.seenBlockInputCache.isVerifiedProposerSignature(blockSlot, blockRoot, signedBlock.signature)) {
const signatureSet = getBlockProposerSignatureSet(chain.config, signedBlock);
// Don't batch so verification is not delayed
if (!(await chain.bls.verifySignatureSets([signatureSet], {verifyOnMainThread: true}))) {
throw new BlockGossipError(GossipAction.REJECT, {
code: BlockErrorCode.PROPOSAL_SIGNATURE_INVALID,
blockSlot,
});
}

chain.seenBlockInputCache.markVerifiedProposerSignature(blockSlot, blockRoot, signedBlock.signature);
}
await verifyBlockProposerSignature(chain, signedBlock, blockRoot);
chain.seenBlockProposers.observeBlockRoot(blockSlot, proposerIndex, blockRoot);

// [REJECT] The block is proposed by the expected proposer_index for the block's slot in the context of the current
// shuffling (defined by parent_root/slot). If the proposer_index cannot immediately be verified against the expected
Expand All @@ -291,11 +286,6 @@ export async function validateGossipBlock(
throw new BlockGossipError(GossipAction.REJECT, {code: BlockErrorCode.INCORRECT_PROPOSER, proposerIndex});
}

// Check again in case there two blocks are processed concurrently
if (chain.seenBlockProposers.isKnown(blockSlot, proposerIndex)) {
throw new BlockGossipError(GossipAction.IGNORE, {code: BlockErrorCode.REPEAT_PROPOSAL, proposerIndex});
}

// Simple implementation of a pending block queue. Keeping the block here recycles the queue logic, and keeps the
// gossip validation promise without any extra infrastructure.
// Do the sleep at the end, since regen and signature validation can already take longer than `msToBlockSlot`.
Expand All @@ -305,7 +295,35 @@ export async function validateGossipBlock(
await sleep(msToBlockSlot);
}

// Check again after all async validation and the early-block delay so concurrent proposals cannot both pass
if (chain.seenBlockProposers.isKnown(blockSlot, proposerIndex)) {
throw new BlockGossipError(GossipAction.IGNORE, {code: BlockErrorCode.REPEAT_PROPOSAL, proposerIndex});
}

chain.seenBlockProposers.add(blockSlot, proposerIndex);

return {skippedSlots};
}

export async function verifyBlockProposerSignature(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

verifyBlockProposerSignature has a naming collision with

export async function verifyBlockProposerSignature(

No need to change it I guess but it is good to know.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

oh that other one is in backfill that code isn't actively used right now, but the function here is specifically designed for gossip validation

the other function also takes blocks (array of blocks) so the name should be verifyBlockProposerSignatures (plural)

If I would change anything, I would probably delete that other function, or rename it. I do think keeping as is for now is fine.

good catch though

chain: IBeaconChain,
signedBlock: SignedBeaconBlock,
blockRoot: RootHex,
opts: {verifyOnMainThread?: boolean} = {}
): Promise<void> {
const blockSlot = signedBlock.message.slot;
if (chain.seenBlockInputCache.isVerifiedProposerSignature(blockSlot, blockRoot, signedBlock.signature)) {
return;
}

const signatureSet = getBlockProposerSignatureSet(chain.config, signedBlock);
// Don't batch so verification is not delayed
if (!(await chain.bls.verifySignatureSets([signatureSet], {verifyOnMainThread: opts.verifyOnMainThread ?? true}))) {
throw new BlockGossipError(GossipAction.REJECT, {
code: BlockErrorCode.PROPOSAL_SIGNATURE_INVALID,
blockSlot,
});
}

chain.seenBlockInputCache.markVerifiedProposerSignature(blockSlot, blockRoot, signedBlock.signature);
}
Loading
Loading