diff --git a/beacon_chain/consensus_object_pools/inclusion_list_pool.nim b/beacon_chain/consensus_object_pools/inclusion_list_pool.nim new file mode 100644 index 0000000000..f43c497244 --- /dev/null +++ b/beacon_chain/consensus_object_pools/inclusion_list_pool.nim @@ -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() \ No newline at end of file diff --git a/beacon_chain/gossip_processing/batch_validation.nim b/beacon_chain/gossip_processing/batch_validation.nim index bd700996da..78233ef2d9 100644 --- a/beacon_chain/gossip_processing/batch_validation.nim +++ b/beacon_chain/gossip_processing/batch_validation.nim @@ -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 @@ -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)) diff --git a/beacon_chain/gossip_processing/gossip_validation.nim b/beacon_chain/gossip_processing/gossip_validation.nim index e0f3e7e851..9f98b7924b 100644 --- a/beacon_chain/gossip_processing/gossip_validation.nim +++ b/beacon_chain/gossip_processing/gossip_validation.nim @@ -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 @@ -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) \ No newline at end of file diff --git a/beacon_chain/spec/datatypes/constants.nim b/beacon_chain/spec/datatypes/constants.nim index 442d67c6ff..4298cc7e11 100644 --- a/beacon_chain/spec/datatypes/constants.nim +++ b/beacon_chain/spec/datatypes/constants.nim @@ -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 diff --git a/beacon_chain/spec/datatypes/focil.nim b/beacon_chain/spec/datatypes/focil.nim index 1f80a75a06..5074cb3202 100644 --- a/beacon_chain/spec/datatypes/focil.nim +++ b/beacon_chain/spec/datatypes/focil.nim @@ -16,9 +16,7 @@ {.experimental: "notnil".} import - std/[sequtils, typetraits], "."/[phase0, base, electra], - chronicles, chronos, json_serialization, ssz_serialization/[merkleization, proofs], @@ -26,9 +24,6 @@ import ../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 @@ -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 diff --git a/beacon_chain/spec/focil_helpers.nim b/beacon_chain/spec/focil_helpers.nim index 3b967489d3..8fea319537 100644 --- a/beacon_chain/spec/focil_helpers.nim +++ b/beacon_chain/spec/focil_helpers.nim @@ -9,7 +9,7 @@ # Uncategorized helper functions from the spec import - std/[algorithm, sequtils], + std/[algorithm], results, eth/p2p/discoveryv5/[node], kzg4844/[kzg], @@ -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 @@ -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..