Repository navigation
fix: verify the proposer signature before retaining unknown parent gossip blocks #10087
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: unstable
Are you sure you want to change the base?
Changes from all commits
9f460ff
0673498
d4cd0d1
7f976f8
7e4f88b
93b951d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -90,6 +90,22 @@ export async function validateGossipBlock( | |
| throw new BlockGossipError(GossipAction.IGNORE, {code: BlockErrorCode.ALREADY_KNOWN, root: blockRoot}); | ||
| } | ||
|
|
||
| // Authenticate the block before the parent lookup so that only blocks signed by a validator are retained for | ||
| // unknown parent sync, a peer without a validator key is penalized for every block it sends | ||
| // [REJECT] The proposer index is a valid validator index | ||
| if (proposerIndex >= chain.pubkeyCache.size) { | ||
| throw new BlockGossipError(GossipAction.REJECT, { | ||
| code: BlockErrorCode.UNKNOWN_PROPOSER, | ||
| slot: blockSlot, | ||
| root: blockRoot, | ||
| proposerIndex, | ||
| }); | ||
| } | ||
|
|
||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we should check if the validator is active via head state |
||
| // [REJECT] The proposer signature, signed_beacon_block.signature, is valid with respect to the proposer_index pubkey. | ||
| await verifyBlockProposerSignature(chain, signedBlock, blockRoot); | ||
| chain.seenBlockProposers.observeBlockRoot(blockSlot, proposerIndex, blockRoot, signedBlockHeader); | ||
|
|
||
| // [REJECT] The current finalized_checkpoint is an ancestor of block -- i.e. | ||
| // get_ancestor(store, block.parent_root, compute_start_slot_at_epoch(store.finalized_checkpoint.epoch)) == store.finalized_checkpoint.root | ||
| const parentRoot = toRootHex(block.parentRoot); | ||
|
|
@@ -105,6 +121,8 @@ export async function validateGossipBlock( | |
| // descend from the finalized root. | ||
| // (Non-Lighthouse): Since we prune all blocks non-descendant from finalized checking the `db.block` database won't be useful to guard | ||
| // against known bad fork blocks, so we throw PARENT_BLOCK_UNKNOWN for cases (1) and (2) | ||
| // The block is retained for unknown parent sync, count it as this proposer's proposal for the slot | ||
| chain.seenBlockProposers.add(blockSlot, proposerIndex, blockRoot); | ||
|
Comment on lines
+124
to
+125
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When distinct unknown-parent blocks for the same proposer and slot are validated concurrently, every invocation can pass the initial Useful? React with 👍 / 👎.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this seems true |
||
| throw new BlockGossipError(GossipAction.IGNORE, {code: BlockErrorCode.PARENT_BLOCK_UNKNOWN, parentRoot}); | ||
| } | ||
|
|
||
|
|
@@ -122,6 +140,7 @@ export async function validateGossipBlock( | |
| if (isGloasBeaconBlock(block)) { | ||
| const parentBlockHashHex = toRootHex(block.body.signedExecutionPayloadBid.message.parentBlockHash); | ||
| if (chain.forkChoice.getBlockHexAndBlockHash(parentRoot, parentBlockHashHex) === null) { | ||
| chain.seenBlockProposers.add(blockSlot, proposerIndex, blockRoot); | ||
| throw new BlockGossipError(GossipAction.IGNORE, { | ||
| code: BlockErrorCode.PARENT_PAYLOAD_UNKNOWN, | ||
| parentRoot, | ||
|
|
@@ -257,20 +276,6 @@ export async function validateGossipBlock( | |
| } | ||
| } | ||
|
|
||
| // [REJECT] The proposer index is a valid validator index | ||
| if (proposerIndex >= state.validatorCount) { | ||
| throw new BlockGossipError(GossipAction.REJECT, { | ||
| code: BlockErrorCode.UNKNOWN_PROPOSER, | ||
| slot: blockSlot, | ||
| root: blockRoot, | ||
| proposerIndex, | ||
| }); | ||
| } | ||
|
|
||
| // [REJECT] The proposer signature, signed_beacon_block.signature, is valid with respect to the proposer_index pubkey. | ||
| await verifyBlockProposerSignature(chain, signedBlock, blockRoot); | ||
| chain.seenBlockProposers.observeBlockRoot(blockSlot, proposerIndex, blockRoot, signedBlockHeader); | ||
|
|
||
| // [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 | ||
| // shuffling, the block MAY be queued for later processing while proposers for the block's branch are calculated -- | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -244,14 +244,15 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand | |
|
|
||
| // IGNORE means the block is acceptable (e.g. FUTURE_SLOT, ALREADY_KNOWN), just not propagated. | ||
| // Keep the optimistically-added cache entries; they are pruned on finalization. Only REJECT | ||
| // (provably invalid), unexpected errors and repeat proposals that are not imported prune. | ||
| // (provably invalid), unexpected errors and repeat proposals that are not imported remove the entry. | ||
| if (e.action === GossipAction.IGNORE) { | ||
| // Only a signature-verified sibling is imported by the beacon_block handler, any other repeat proposal | ||
| // is dropped from the caches and re-downloaded by sync if it ever becomes relevant. Its signature was not | ||
| // verified, so only its own entry is removed, never its claimed ancestors | ||
| // Only a signature-verified sibling with a known parent is imported by the beacon_block handler, any other | ||
| // repeat proposal is dropped from the caches and re-downloaded by sync if it ever becomes relevant. Only its | ||
| // own entry is removed, never its claimed ancestors, which an unverified block must not be able to evict | ||
| if ( | ||
| e.type.code === BlockErrorCode.REPEAT_PROPOSAL && | ||
| !chain.seenBlockProposers.hasBlockRoot(slot, signedBlock.message.proposerIndex, blockRootHex) | ||
| (!chain.seenBlockProposers.hasBlockRoot(slot, signedBlock.message.proposerIndex, blockRootHex) || | ||
| chain.forkChoice.getBlockHexDefaultStatus(toRootHex(signedBlock.message.parentRoot)) === null) | ||
| ) { | ||
| chain.seenBlockInputCache.remove(blockRootHex); | ||
| if (isForkPostGloas(fork)) { | ||
|
|
@@ -752,7 +753,9 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand | |
| e.type.code === BlockErrorCode.REPEAT_PROPOSAL && | ||
| // Only a signature-verified sibling recorded in the seen cache is imported. The cache holds at most two | ||
| // roots per proposer and slot, which bounds full imports over gossip to one alternate | ||
| chain.seenBlockProposers.hasBlockRoot(signedBlock.message.slot, e.type.proposerIndex, e.type.root) | ||
| chain.seenBlockProposers.hasBlockRoot(signedBlock.message.slot, e.type.proposerIndex, e.type.root) && | ||
| // A sibling with an unknown parent cannot be imported, sync fetches it if its branch becomes relevant | ||
| chain.forkChoice.getBlockHexDefaultStatus(toRootHex(signedBlock.message.parentRoot)) !== null | ||
|
Comment on lines
+756
to
+758
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Under Gloas, the newly recorded first proposal may have produced AGENTS.md reference: AGENTS.md:L206-L208 Useful? React with 👍 / 👎.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. looks also valid |
||
| ) { | ||
| // blockInput was optimistically seeded in validateBeaconBlock and retained on IGNORE | ||
| const blockInput = chain.seenBlockInputCache.get(e.type.root); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If an unknown-parent branch registered and activated a validator in ancestry this node has not processed, a valid current-slot block can have
proposerIndex >= chain.pubkeyCache.size. This branch is exactly what unknown-parent sync is meant to recover, but the new check returns REJECT and penalizes the relaying peer instead of treating the signature as currently unverifiable. Such blocks should be ignored without retention or penalty until the ancestry populates the key cache.Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm, so this can happen if your finalized checkpoint is behind? we discussed this yesterday on discord @wemeetagain but need to be careful with those rejects here