Repository navigation
fix(beacon): verify a block's signature before it waits for its parent or columns - #629
Draft
MegaRedHand wants to merge 4 commits into
Draft
MegaRedHand wants to merge 4 commits into
MegaRedHand wants to merge 4 commits into
Conversation
…t or columns A beacon block that waits, for its parent's post-state and then for its custody columns, is kept unjudged until what it waits for arrives, and a held one has its columns asked for again every slot until finality evicts it. Mainnet gossip carries altered copies of real blocks (a blob transaction re-encoded after signing, the original signature kept): nobody signed their root and no peer has columns for them. Gossip's queue path checked the signature only against a cached head state, and queued the block anyway when it found none. After a resume nothing caches the head state before the first import, so in that window every parentless block reached the chain actor unchecked, and an altered one sat held for columns until finality. Range sync and by-root fetches never pass through gossip validation at all. - Gossip queues a parentless block only once the head state verifies its signature. One it cannot judge is ignored as `signature_unverified`, and only an accepted or queued block is forwarded, so an overloaded one no longer reaches the actor unjudged. - `precheck_block` against a recent state reports an unknown proposer instead of passing it, so `Ok` always means the signature verified. - The chain actor runs `precheck_block` before parking a block for its parent (signature, against the head state) and before holding it for columns (every rule, against the parent's post-state). - Resuming from the DB caches the head's state, so gossip can judge a parentless block from the first message on.
The previous commit stopped forwarding a block gossip could not verify the signature of, `Overloaded` included, so the "still forwarded" and "either way" passages no longer hold. It also added an ignore reason and a counter nothing documented yet.
…ix/beacon-verify-block-signature-before-waiting # Conflicts: # crates/net/p2p/src/beacon/verdict.rs
…signature-before-waiting
4 tasks
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A beacon block that waits, for its parent's post-state and then for its custody columns, is kept unjudged until what it waits for arrives. A held one also has its columns asked for again every slot until finality evicts it. Mainnet gossip carries altered copies of real blocks (a blob transaction re-encoded after signing, the original signature kept): nobody signed their root, and no peer has columns for them.
lambdaclass/ethlambda_private#38 moved the signature check for a parentless block into gossip, but that check has two gaps:
Store::cached_state, and queues the block unchecked when it finds none. After a resume nothing caches the head state before the first import.What it cost
eth-5, keep-DB restart, 2026-09-25 19:41Z. Slot 15295105 had two gossip blocks from proposer 2128692, 2 ms apart: the altered copy
9b320e3a(917 KB, first) and the realb4a78feb(367 KB). The parent was held for columns, so gossip returnedQueue(ParentNotReady), and with no head state cached yet the altered copy went to the chain actor unchecked. It was held for columns and loggedData column fetch failed after max retries13 times, once per slot.In the first ~45 s after that restart Prometheus shows 3
queue/parent_not_ready, 0accept, and noreject/bad_signatureuntil the first import.Change
Every path a beacon block takes into waiting now requires a verified proposer signature first.
gossip::block::queue_if_signed(wasqueue_unless_forged)Queueunless the signature is shown forgedQueueonly once it verifies; otherwiseIgnore(SignatureUnverified)(newIgnoreReason)precheck_block,Reference::Recent, proposer not in stateOk(())Err(UnknownProposer), soOkalways means the signature verifiedValidated::forwardAccept/Queue, so anOverloadedblock is dropped tooprecheck_before_waiting(Wait::Parent): signature against the head's post-state; on failure alsodiscard_pending_subtreeprecheck_before_waiting(Wait::Columns): every precheck rule against the parent's post-state; returnsImportError::Precheckfetch_initial_beacon_state, both resuming returns)Store::cache_head_state()A fresh checkpoint sync already caches its anchor through
insert_state, so the resume path was the only one missing it.New metric:
lean_beacon_blocks_refused_before_waiting_total{reason, wait}, wherereasonis thePrecheckErrorlabel andwaitisparentorcolumns.Docs (separate commit):
beacon_wire.mdandmetrics.mdno longer say a block is forwarded to the chain actor on every outcome or whenOverloaded. They also now list thesignature_unverifiedignore reason and the new counter.Trade-offs
spawn_stateful_checksnotes that no block was measuredOverloadedon the mainnet followers over the 24 hours before this change.Tests
an_unknown_parent_by_a_proposer_the_head_state_lacks_is_droppedonly_an_accepted_or_queued_block_is_forwardeda_resumed_store_caches_its_head_state_on_requesta_block_whose_parent_was_never_seen_is_dropped_with_no_state_to_check_it(was..._is_queued)a_proposer_missing_from_the_reference_state_is_unknown_to_both_references--lib: blockchain--lib --bins: state-transition, p2p, storage, ethlambdamake lint,cargo fmt --checkNot yet run on a follower.
Known failures (why this is a draft)
releasing_once_every_custody_column_arrives_clears_the_holdprecheck_before_waiting: "the reference block has a post-state"on_block, and the fixture's parent (ZERO) has no state, so the parent-wait precheck runs. The head (ZERO) has no state either, so theexpectpanics.a_childs_fan_out_does_not_livelock_a_held_parentblocks_awaiting_columns.contains_key(&parent_root)is falsefulu_blockis unsigned andbare_state()has no validators, so the columns-wait precheck refuses the block withUnknownProposerinstead of holding it.Neither test is wrong: they hold unsigned blocks, and holding now requires a valid signature. The signing helpers (
secret_key_for,with_validators_at) are#[cfg(test)] pub(crate)inethlambda-state-transition, so the blockchain crate cannot reach them. Options:test_statebehind atest-utilsfeature and give these tests keyed states and signed blocks.precheck_before_waiting. This is smaller, but these tests would then stop covering the precheck path.Moved
beacon-chain-integrationlives on this repo.beacon-chain-integration@c79fabd5, merged into the branch. One conflict, inverdict.rstests: both sides added a test at the same spot, so both are kept.