feat: implement consensus_and_equivocation for blocks and payload envelopes - #9757
Conversation
Performance Report✔️ no performance regression detected Full benchmark results
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## unstable #9757 +/- ##
============================================
- Coverage 52.57% 52.57% -0.01%
============================================
Files 848 848
Lines 60132 60124 -8
Branches 4429 4426 -3
============================================
- Hits 31614 31608 -6
+ Misses 28459 28457 -2
Partials 59 59 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 317e04bab2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| await verifyBlockProposerSignature(chain, signedBlock, blockRoot); | ||
| chain.seenBlockProposers.observeBlockRoot(blockSlot, proposerIndex, blockRoot); | ||
| } | ||
| throw new BlockGossipError(GossipAction.IGNORE, {code: BlockErrorCode.REPEAT_PROPOSAL, proposerIndex}); |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
we can handle that either in #9233 if this PR is merged first, or completely separate as a follow-up
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 308d3afa78
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| await sleep(msToBlockSlot); | ||
| } | ||
|
|
||
| if (broadcastValidation === routes.beacon.BroadcastValidation.consensusAndEquivocation) { |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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
| return {skippedSlots}; | ||
| } | ||
|
|
||
| export async function verifyBlockProposerSignature( |
There was a problem hiding this comment.
verifyBlockProposerSignature has a naming collision with
No need to change it I guess but it is good to know.
There was a problem hiding this comment.
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
Follow-up to #9757 that produces proposer slashings from observed proposer equivocations - store the signed block header alongside each observed block root, the root is derived from the header so no extra hashing - build a proposer slashing from the two conflicting headers when an equivocation is observed via gossip or block import - fully validate the slashing, add it to the local op pool for block inclusion and publish it on gossip - do not produce slashings from blocks only observed via the beacon api so an unpublished block cannot leak equivocation evidence - add focused seen cache and gossip validation tests Verified on glamsterdam-devnet-7, a slashing was produced and broadcast 137ms after an adversarial equivocation was published and the validator got slashed
| if (opts.skipVerifyBlockSignatures !== true) { | ||
| for (const block of blocks) { | ||
| const blockRoot = toRootHex( | ||
| this.config.getForkTypes(block.message.slot).BeaconBlock.hashTreeRoot(block.message) |
There was a problem hiding this comment.
we should be able to get block root from BlockInput instead
There was a problem hiding this comment.
| 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; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
I like MIN_EQUIVOCATION_BLOCK_ROOTS_PER_PROPOSAL, agree the name isn't entirely clear, the jsdoc helps though
There was a problem hiding this comment.
Follow-up to #9757 and #9787 based on @twoeths' comments - remove `skipVerifyBlockSignatures` and always verify block signatures - reuse the block input root when recording observed proposals - defer proposer slashing production to the next event loop - log both conflicting block header roots - clarify the equivocation root-count constant name
|
🎉 This PR is included in v1.46.0 🎉 |
**Motivation** - in `lodestar-geth-1` of `glamsterdam-devnet-7`, we saw 2 blocks of the same slot. The 2nd block got ignored, we ended up having to use UnknownBlockInput to download it again. And the 2nd block ended up being the canonical block - a continuation of #9757 **Description** - process `REPEAT_PROPOSAL` block, but don't publish it to the network (to conform to the spec) part of #9799 **AI Assistance Disclosure** - created with the help of Claude --------- Co-authored-by: twoeths <twoeths@users.noreply.github.com>
Follow-up to #9595 that implements
broadcast_validation=consensus_and_equivocationfor block and execution payload envelope publishingpublishBlockV2before broadcastconsensusorconsensus_and_equivocationvalidation