Skip to content
87 changes: 87 additions & 0 deletions beacon_chain/consensus_object_pools/inclusion_list_pool.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# beacon_chain
# Copyright (c) 2024-2025 Status Research & Development GmbH
# Licensed and distributed under either of
# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT).
# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0).
# at your option. This file may not be copied, modified, or distributed except according to those terms.

{.push raises: [].}

import
# Standard libraries
std/[deques, sets],
# Internal
../spec/datatypes/[base, focil],
../spec/[helpers, state_transition_block],
"."/[blockchain_dag]

export base, deques, blockchain_dag, focil

const
INCLUSION_LISTS_BOUND = 1024'u64 # Reasonable bound for inclusion lists

type
OnInclusionListCallback =
proc(data: SignedInclusionList) {.gcsafe, raises: [].}

InclusionListPool* = object
## The inclusion list pool tracks signed inclusion lists that could be
## added to a proposed block.

inclusion_lists*: Deque[SignedInclusionList] ## \
## Not a function of chain DAG branch; just used as a FIFO queue for blocks

prior_seen_inclusion_list_validators: HashSet[uint64] ## \
## Records validator indices that have already submitted inclusion lists
## to prevent duplicate processing

dag*: ChainDAGRef
onInclusionListReceived*: OnInclusionListCallback

func init*(T: type InclusionListPool, dag: ChainDAGRef,
onInclusionList: OnInclusionListCallback = nil): T =
## Initialize an InclusionListPool from the dag `headState`
T(
inclusion_lists:
initDeque[SignedInclusionList](initialSize = INCLUSION_LISTS_BOUND.int),
dag: dag,
onInclusionListReceived: onInclusionList)

func addInclusionListMessage(
subpool: var Deque[SignedInclusionList],
seenpool: var HashSet[uint64],
inclusionList: SignedInclusionList,
bound: static[uint64]) =
## Add an inclusion list message to the pool, maintaining bounds
while subpool.lenu64 >= bound:
seenpool.excl subpool.popFirst().message.validator_index.uint64

subpool.addLast(inclusionList)
doAssert subpool.lenu64 <= bound

func isSeen*(pool: InclusionListPool, msg: SignedInclusionList): bool =
## Check if we've already seen an inclusion list from this validator
msg.message.validator_index.uint64 in pool.prior_seen_inclusion_list_validators

proc addMessage*(pool: var InclusionListPool, msg: SignedInclusionList) =
## Add an inclusion list message to the pool
pool.prior_seen_inclusion_list_validators.incl(
msg.message.validator_index.uint64)

addInclusionListMessage(
pool.inclusion_lists, pool.prior_seen_inclusion_list_validators, msg, INCLUSION_LISTS_BOUND)

# Send notification about new inclusion list via callback
if not(isNil(pool.onInclusionListReceived)):
pool.onInclusionListReceived(msg)

func getInclusionLists*(pool: InclusionListPool): seq[SignedInclusionList] =
## Get all inclusion lists in the pool
result = newSeq[SignedInclusionList](pool.inclusion_lists.len)
for i, inclusionList in pool.inclusion_lists:
result[i] = inclusionList

func clear*(pool: var InclusionListPool) =
## Clear all inclusion lists from the pool
pool.inclusion_lists.clear()
pool.prior_seen_inclusion_list_validators.clear()
28 changes: 27 additions & 1 deletion beacon_chain/gossip_processing/batch_validation.nim
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import
# Status
chronicles, chronos, chronos/threadsync,
../spec/signatures_batch,
../consensus_object_pools/[blockchain_dag, spec_cache]
../consensus_object_pools/[blockchain_dag, spec_cache],
../spec/datatypes/focil

export signatures_batch, blockchain_dag

Expand Down Expand Up @@ -577,3 +578,28 @@ proc scheduleBlsToExecutionChangeCheck*(
pubkey, sig)

ok((fut, sig))

proc scheduleInclusionListCheck*(
batchCrypto: ref BatchCrypto,
fork: Fork,
message: InclusionList,
pubkey: CookedPubKey,
signature: ValidatorSig):
Result[tuple[fut: FutureBatchResult, sig: CookedSig], cstring] =
## Schedule crypto verification of an inclusion list signature
##
## The buffer is processed:
## - when eager processing is enabled and the batch is full
## - otherwise after 10ms (BatchAttAccumTime)
##
## This returns an error if crypto sanity checks failed
## and a future with the deferred check otherwise.

let
sig = signature.load().valueOr:
return err("InclusionList: cannot load signature")
fut = batchCrypto.verifySoon("scheduleInclusionListCheck"):
inclusion_list_signature_set(
fork, batchCrypto[].genesis_validators_root, message, pubkey, sig)

ok((fut, sig))
91 changes: 88 additions & 3 deletions beacon_chain/gossip_processing/gossip_validation.nim
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@ import
results,
kzg4844/[kzg, kzg_abi],
stew/byteutils,
ssz_serialization/types as sszTypes,
# Internals
../spec/[
beaconstate, state_transition_block, forks,
helpers, network, signatures, peerdas_helpers],
beaconstate, state_transition_block, forks, datatypes/focil,
helpers, network, signatures, peerdas_helpers, focil_helpers],
../consensus_object_pools/[
attestation_pool, blockchain_dag, blob_quarantine, block_quarantine,
data_column_quarantine, spec_cache, light_client_pool, sync_committee_msg_pool,
validator_change_pool],
validator_change_pool, inclusion_list_pool],
".."/[beacon_clock],
./batch_validation

Expand Down Expand Up @@ -1893,3 +1894,87 @@ proc validateLightClientOptimisticUpdate*(

pool.latestForwardedOptimisticSlot = attested_slot
ok()

# https://github.com/ethereum/consensus-specs/blob/dev/specs/_features/eip7805/p2p-interface.md#global-topics
proc validateInclusionList*(
pool: var InclusionListPool, dag: ChainDAGRef,
batchCrypto: ref BatchCrypto,
signed_inclusion_list: SignedInclusionList,
wallTime: BeaconTime, checkSignature: bool):
Future[Result[CookedSig, ValidationError]] {.async: (raises: [CancelledError]).} =
## Validate a signed inclusion list according to the EIP-7805 specification

template message: untyped = signed_inclusion_list.message

# [REJECT] The size of message.transactions is within upperbound MAX_BYTES_PER_INCLUSION_LIST.
var totalSize: uint64 = 0
for transaction in message.transactions:
totalSize += uint64(transaction.len)
if totalSize > MAX_BYTES_PER_INCLUSION_LIST:
return dag.checkedReject("InclusionList: transactions size exceeds MAX_BYTES_PER_INCLUSION_LIST")

# [REJECT] The slot message.slot is equal to the previous or current slot.
let currentSlot = wallTime.slotOrZero
if not (message.slot == currentSlot or message.slot == currentSlot - 1):
return dag.checkedReject("InclusionList: slot must be current or previous slot")

# [IGNORE] The slot message.slot is equal to the current slot, or it is equal to the previous slot and the current time is less than ATTESTATION_DEADLINE seconds into the slot.
if message.slot == currentSlot - 1:
let slotStartTime = message.slot.start_beacon_time()
let currentTime = wallTime
if currentTime >= slotStartTime + ATTESTATION_DEADLINE:
return errIgnore("InclusionList: previous slot inclusion list received after deadline")

# [IGNORE] The inclusion_list_committee for slot message.slot on the current branch corresponds to message.inclusion_list_committee_root, as determined by hash_tree_root(inclusion_list_committee) == message.inclusion_list_committee_root.
withState(dag.headState):
let committee = resolve_inclusion_list_committee(forkyState.data, message.slot)
# Note: We need to convert the HashSet to a sequence for hash_tree_root
var committeeList: List[uint64, Limit INCLUSION_LIST_COMMITTEE_SIZE]
for validator in committee:
if not committeeList.add(validator):
raiseAssert "Committee list overflowed its maximum size"
let committeeRoot = hash_tree_root(committeeList)
if committeeRoot != message.inclusion_list_committee_root:
return errIgnore("InclusionList: inclusion list committee root mismatch")

# [REJECT] The validator index message.validator_index is within the inclusion_list_committee corresponding to message.inclusion_list_committee_root.
withState(dag.headState):
let committee = resolve_inclusion_list_committee(forkyState.data, message.slot)
if message.validator_index notin committee:
return dag.checkedReject("InclusionList: validator not in inclusion list committee")

# [IGNORE] The message is either the first or second valid message received from the validator with index message.validator_index.
if pool.isSeen(signed_inclusion_list):
return errIgnore("InclusionList: already received inclusion list from this validator")

# [REJECT] The signature of inclusion_list.signature is valid with respect to the validator index.
let sig =
if checkSignature:
withState(dag.headState):
let
pubkey = dag.validatorKey(message.validator_index).valueOr:
return dag.checkedReject("InclusionList: invalid validator index")
let deferredCrypto = batchCrypto.scheduleInclusionListCheck(
dag.forkAtEpoch(message.slot.epoch),
message, pubkey, signed_inclusion_list.signature)
if deferredCrypto.isErr():
return dag.checkedReject(deferredCrypto.error)

let (cryptoFut, sig) = deferredCrypto.get()
# Await the crypto check
let x = (await cryptoFut)
case x
of BatchResult.Invalid:
return dag.checkedReject("InclusionList: invalid signature")
of BatchResult.Timeout:
return errIgnore("InclusionList: timeout checking signature")
of BatchResult.Valid:
sig # keep going only in this case
else:
signed_inclusion_list.signature.load().valueOr:
return dag.checkedReject("InclusionList: unable to load signature")

# Add the inclusion list to the pool
pool.addMessage(signed_inclusion_list)

ok(sig)
3 changes: 3 additions & 0 deletions beacon_chain/spec/datatypes/constants.nim
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,6 @@ const
DEPOSIT_REQUEST_TYPE* = 0x00'u8
WITHDRAWAL_REQUEST_TYPE* = 0x01'u8
CONSOLIDATION_REQUEST_TYPE* = 0x02'u8

# https://github.com/ethereum/consensus-specs/blob/dev/specs/_features/eip7805/p2p-interface.md#configuration
MAX_REQUEST_INCLUSION_LIST*: uint64 = 16 # 2**4
17 changes: 6 additions & 11 deletions beacon_chain/spec/datatypes/focil.nim
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,14 @@
{.experimental: "notnil".}

import
std/[sequtils, typetraits],
"."/[phase0, base, electra],
chronicles,
chronos,
json_serialization,
ssz_serialization/[merkleization, proofs],
ssz_serialization/types as sszTypes,
../digest,
kzg4844/[kzg, kzg_abi]

from std/strutils import join
from stew/bitops2 import log2trunc
from stew/byteutils import to0xHex
from ./altair import
EpochParticipationFlags, InactivityScores, SyncAggregate, SyncCommittee,
TrustedSyncAggregate, SyncnetBits, num_active_participants
Expand All @@ -47,21 +42,21 @@ const
# https://github.com/ethereum/consensus-specs/blob/v1.6.0-alpha.2/specs/_features/eip7805/beacon-chain.md#preset
INCLUSION_LIST_COMMITTEE_SIZE* = 16'u64
# https://github.com/ethereum/consensus-specs/blob/v1.6.0-alpha.2/specs/_features/eip7805/fork-choice.md#time-parameters
VIEW_FREEZE_DEADLINE* = (SECONDS_PER_SLOT * 2 div 3 + 1).seconds
VIEW_FREEZE_DEADLINE* = chronos.seconds((SECONDS_PER_SLOT * 2 div 3 + 1).int64)
# https://github.com/ethereum/consensus-specs/blob/v1.6.0-alpha.2/specs/_features/eip7805/p2p-interface.md#configuration
ATTESTATION_DEADLINE* = (SECONDS_PER_SLOT div 3).seconds
ATTESTATION_DEADLINE* = chronos.seconds((SECONDS_PER_SLOT div 3).int64)
MAX_REQUEST_INCLUSION_LIST* = 16'u64
MAX_BYTES_PER_INCLUSION_LIST* = 8192'u64
# https://github.com/ethereum/consensus-specs/blob/v1.6.0-alpha.2/specs/_features/eip7805/validator.md#configuration
PROPOSER_INCLUSION_LIST_CUT_OFF = (SECONDS_PER_SLOT - 1).seconds
PROPOSER_INCLUSION_LIST_CUT_OFF = chronos.seconds((SECONDS_PER_SLOT - 1).int64)

type
# https://github.com/ethereum/consensus-specs/blob/v1.6.0-alpha.2/specs/_features/eip7805/beacon-chain.md#inclusionlist
InclusionList* = object
slot*: Slot
validator_index*: ValidatorIndex
inclusion_list_committee_root: Eth2Digest
transactions: List[Transaction, MAX_TRANSACTIONS_PER_PAYLOAD]
validator_index*: uint64
inclusion_list_committee_root*: Eth2Digest
transactions*: List[Transaction, MAX_TRANSACTIONS_PER_PAYLOAD]

# https://github.com/ethereum/consensus-specs/blob/v1.6.0-alpha.2/specs/_features/eip7805/beacon-chain.md#signedinclusionlist
SignedInclusionList* = object
Expand Down
21 changes: 9 additions & 12 deletions beacon_chain/spec/focil_helpers.nim
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

# Uncategorized helper functions from the spec
import
std/[algorithm, sequtils],
std/[algorithm],
results,
eth/p2p/discoveryv5/[node],
kzg4844/[kzg],
Expand All @@ -22,7 +22,7 @@ import
./datatypes/[fulu, focil]

# https://github.com/ethereum/consensus-specs/blob/v1.6.0-alpha.2/specs/_features/eip7805/beacon-chain.md#new-is_valid_inclusion_list_signature
func verify_inclusion_list_signature*(
func is_valid_inclusion_list_signature*(
state: ForkyBeaconState,
signed_inclusion_list: SignedInclusionList): bool =
## Check if the `signed_inclusion_list` has a valid signature
Expand All @@ -34,31 +34,31 @@ func verify_inclusion_list_signature*(
message.slot.epoch())
signing_root =
compute_signing_root(message, domain)
blsVerify(pubkey, signing_root.data, signature)
blsVerify(pubkey, signing_root.data, signed_inclusion_list.signature)

# https://github.com/ethereum/consensus-specs/blob/v1.6.0-alpha.2/specs/_features/eip7805/beacon-chain.md#new-get_inclusion_list_committee
func resolve_inclusion_list_committee*(
state: ForkyBeaconState,
slot: Slot): HashSet[ValidatorIndex] =
slot: Slot): HashSet[uint64] =
## Return the inclusion list committee for the given slot
let
seed = get_seed(state, slot.epoch(), DOMAIN_INCLUSION_LIST_COMMITTEE)
indices =
get_active_validator_indices(state, epoch)
get_active_validator_indices(state, slot.epoch())

start = (slot mod SLOTS_PER_EPOCH) * INCLUSION_LIST_COMMITTEE_SIZE
end_i = start + INCLUSION_LIST_COMMITTEE_SIZE
seq_len {.inject.} = indices.lenu64

var res: HashSet[ValidatorIndex]
var res: HashSet[uint64]
for i in 0..<INCLUSION_LIST_COMMITTEE_SIZE:
let
shuffledIdx = compute_shuffled_index(
((start + i) mod seq_len).asUInt64,
(start + i) mod seq_len,
seq_len,
seed)

res.incl indices[shuffledIdx]
res.incl uint64(indices[shuffledIdx])

res

Expand All @@ -72,13 +72,10 @@ func get_inclusion_committee_assignment*(
## Returns None if no assignment is found.
let
next_epoch = Epoch(state.slot.epoch() + 1)
start_slot = epoch.start_slot()

doAssert epoch <= nextEpoch

for epochSlot in epoch.slots():
for slot in epoch.slots():
let
slot = Slot(epochSlot + start_slot)
committee = resolve_inclusion_list_committee(state, slot)
if validator_index in committee:
return Opt.som(slot)
Expand Down
6 changes: 6 additions & 0 deletions beacon_chain/spec/network.nim
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@ export base
const
# https://github.com/ethereum/consensus-specs/blob/v1.5.0-alpha.10/specs/phase0/p2p-interface.md#topics-and-messages
# https://github.com/ethereum/consensus-specs/blob/v1.5.0-alpha.9/specs/capella/p2p-interface.md#topics-and-messages
# https://github.com/ethereum/consensus-specs/blob/dev/specs/_features/eip7805/p2p-interface.md#topics-and-messages
topicBeaconBlocksSuffix = "beacon_block/ssz_snappy"
topicVoluntaryExitsSuffix = "voluntary_exit/ssz_snappy"
topicProposerSlashingsSuffix = "proposer_slashing/ssz_snappy"
topicAttesterSlashingsSuffix = "attester_slashing/ssz_snappy"
topicAggregateAndProofsSuffix = "beacon_aggregate_and_proof/ssz_snappy"
topicBlsToExecutionChangeSuffix = "bls_to_execution_change/ssz_snappy"
topicInclusionListSuffix = "inclusion_list/ssz_snappy"

const
# The spec now includes this as a bare uint64 as `RESP_TIMEOUT`
Expand Down Expand Up @@ -68,6 +70,10 @@ func getAggregateAndProofsTopic*(forkDigest: ForkDigest): string =
func getBlsToExecutionChangeTopic*(forkDigest: ForkDigest): string =
eth2Prefix(forkDigest) & topicBlsToExecutionChangeSuffix

# https://github.com/ethereum/consensus-specs/blob/dev/specs/_features/eip7805/p2p-interface.md#topics-and-messages
func getInclusionListTopic*(forkDigest: ForkDigest): string =
eth2Prefix(forkDigest) & topicInclusionListSuffix

# https://github.com/ethereum/consensus-specs/blob/v1.5.0-beta.2/specs/phase0/validator.md#broadcast-attestation
func compute_subnet_for_attestation*(
committees_per_slot: uint64, slot: Slot, committee_index: CommitteeIndex):
Expand Down
Loading
Loading