Skip to content

draft impl for EIP-7805's p2p interface and inclusion list pool. #7290

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

Draft
wants to merge 13 commits into
base: focil
Choose a base branch
from
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