Skip to content

Implement gossip verification of inclusion lists - #10182

Merged
mergify[bot] merged 16 commits into
sigp:unstablefrom
rahulbarmann:focil/inclusion-list-gossip-verification
Oct 12, 2026
Merged

mergify[bot] merged 16 commits into
sigp:unstablefrom
rahulbarmann:focil/inclusion-list-gossip-verification

Conversation

@rahulbarmann

@rahulbarmann rahulbarmann commented Oct 1, 2026 •

Copy link
Copy Markdown

Issue Addressed

Part of FOCIL (EIP-7805). Follow-up to #9846, which wired up the inclusion_list topic but ignored every message.

Proposed Changes

  • Add a new inclusion_list_verification module that runs the validate_inclusion_list_gossip checks in the order defined by the spec, with unit tests in tests.rs.
  • Make the gossip handler add verified lists to the store and decide whether to propagate based on the store result. This means concurrent workers can't end up forwarding a third list from the same validator.
  • Re-key the store's first-or-second count by (dependent_root, validator), following consensus-specs#5605.
  • Make InclusionListStore::process_inclusion_list take a GossipVerifiedInclusionList instead of a raw list and timeliness flag.
  • Change inclusion_list_signature_set to take the genesis_validators_root instead of a state.
  • Add ChainSpec::get_inclusion_list_due for the timeliness check.

Additional Info

The transaction bounds check is copied from #10008 and get_inclusion_list_due from conache#8, so whichever PR merges second will need a small rebase.

@rahulbarmann
rahulbarmann requested a review from jxs as a code owner October 1, 2026 01:57
@eserilev eserilev added the focil Fork choice enforced inclusion lists label Oct 1, 2026
@eserilev

eserilev commented Oct 7, 2026

Copy link
Copy Markdown
Member

tests claude thinks we might want to add:

inclusion_list_verification/tests.rs

  1. Edge of the clock window. future_slot (line 194) and past_slot (line 210) test only slots outside the allowed window. Add tests where the clock is a few ms before slot N+1 and a list for N+1 is accepted, and a few ms after slot N and a list for N is accepted but marked not timely. These cover the MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance.
  2. Committee on a fork. The committee() helper (line 72) uses the head block root, and all tests build one straight chain. So a bug that took the committee from the head state instead of the dependent root's state would pass. Add a test with two branches whose committees differ. Sign a list from the non-head branch's committee. It has to pass, and a list from the head's committee has to fail.
  3. A failed check does not count. Send a list with a bad signature, then two valid lists from the same validator. Both valid lists have to pass. The count is allowed to increase only after a list passes every check.
  4. First Heze epoch. Add a test for a slot in HEZE_FORK_EPOCH. The dependent state is still Gloas there. Check that the committee and the signature domain (Heze fork version) are correct.

network_beacon_processor/tests.rs
5. Equivocation mapping. The new test (test_gossip_inclusion_list_propagation_follows_the_store_outcome) checks only the first message (Accept) and an exact duplicate (Ignore). Add a test where a different second list from the same validator gives Equivocating and Accept, so it is forwarded. A third list has to give SubsequentEquivocation and Ignore. Also check that a REJECT result applies a peer penalty.

@rahulbarmann

Copy link
Copy Markdown
Author

@eserilev added all of these, thanks!

@conache

conache commented Oct 8, 2026

Copy link
Copy Markdown

Adding one more case we might want to add a test for - from claude too:

To be added in inclusion_list_verification/tests.rs.

Possible dependent block at the boundary. The [IGNORE] check requires a child strictly after dependent_slot (child.slot > dependent_slot), but no test fails if it becomes >=. Add a test where the dependent root R is not the head and its only child sits exactly at dependent_slot. A list with dependent_root = R has to be ignored with InvalidDependentRoot, since on that branch the child, not R, is the latest block at or before dependent_slot.

"Successfully verified gossip inclusion list"
);

// TODO(heze): emit the inclusion_list SSE event

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

PR #10228 which enabled inclusion_list SSE event to be emitted was merged in the focil devnet branches so can those changes be incorporated in this PR itself or is the plan to introduce it in a subsequent PR when this PR is merged?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

good call, thanks! pulled it in here

Comment on lines +154 to +162
let current_slot = ctx
.slot_clock
.now()
.ok_or(InclusionListVerificationError::UnableToReadSlot)?;
let is_timely = slot == current_slot
&& ctx
.slot_clock
.millis_from_current_slot_start()
.is_some_and(|time_into_slot| time_into_slot < ctx.spec.get_inclusion_list_due());

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 are reading the slot clock twice, this could cause a race condition. it's on on the order of a microsecond, so not realistic tbh. but still i think its cleaner to read the slot clock once

something like:

let now = ctx
    .slot_clock
    .now_duration()
    .ok_or(InclusionListVerificationError::UnableToReadSlot)?;
let current_slot = ctx
    .slot_clock
    .slot_of(now)
    .ok_or(InclusionListVerificationError::UnableToReadSlot)?;

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

that makes sense, thanks! switched to reading it once

validator_counts: HashMap<u64, usize>,
/// Count of valid inclusion lists seen per dependent root and validator, for the
/// first-or-second rule.
validator_counts: HashMap<(DependentRoot, u64), usize>,

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.

i'm wondering if validator_counts is even necessary? could we just check equivocators directly?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

good point, it was always the same as being in equivocators, so removed it

InvalidTransactions(InclusionListTransactionsError),
/// The block with root `dependent_root` has not been seen.
DependentRootUnknown { dependent_root: Hash256 },
/// The block with root `dependent_root` is not before the start of the lookahead epoch.

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.

nit:

// The block with root dependent_root is after dependent_slot.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

done f6c430e

}
}

let now = ctx

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.

I think we need to get the actual seen_timestamp (like when we actually first saw the IL). if we calculate it here we are ignoring the time that the IL sat in the beacon processor/got requeued etc

you can get that info in router.rs and pass it down to here

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

done, it's now passed down from the router and used for the timeliness check

}
}

pub fn verify_inclusion_list_for_gossip(

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.

can we add gossip metrics for IL's? we do a similar thing for payload atteestations.

we can add a counter for

  • IL processing requests
  • IL processing sucecess
  • IL verification errors

also maybe a metric for equivocations

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

added the request, success and per-type error counters, plus one for equivocations

@rahulbarmann
rahulbarmann requested a review from eserilev October 9, 2026 19:39

@eserilev eserilev left a comment

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.

nice, lgtm!

@eserilev eserilev added the ready-for-merge This PR is ready to merge. label Oct 12, 2026
@mergify

mergify Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Queued — the merge queue status continues in this comment ↓.

@mergify mergify Bot added the queued label Oct 12, 2026
@mergify

mergify Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Merge Queue Status

This pull request spent 30 minutes 10 seconds in the queue, including 28 minutes 19 seconds running CI.

Required conditions to merge

@mergify
mergify Bot merged commit f83aeef into sigp:unstable Oct 12, 2026
41 checks passed
@mergify mergify Bot removed the queued label Oct 12, 2026
conache added a commit to conache/lighthouse that referenced this pull request Oct 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

focil Fork choice enforced inclusion lists heze ready-for-merge This PR is ready to merge.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants