diff --git a/AllTests-mainnet.md b/AllTests-mainnet.md index 7b4d8f1ab6..1fc63000fc 100644 --- a/AllTests-mainnet.md +++ b/AllTests-mainnet.md @@ -135,6 +135,7 @@ AllTests-mainnet ## Block processor [Preset: mainnet] ```diff + Invalidate block root [Preset: mainnet] OK ++ Process a block from each fork (without blobs) [Preset: mainnet] OK + Reverse order block add & get [Preset: mainnet] OK ``` ## Block quarantine diff --git a/beacon_chain/gossip_processing/block_processor.nim b/beacon_chain/gossip_processing/block_processor.nim index 95f8f42375..fab7728103 100644 --- a/beacon_chain/gossip_processing/block_processor.nim +++ b/beacon_chain/gossip_processing/block_processor.nim @@ -499,9 +499,6 @@ proc verifyPayload( # There are no `blob_kzg_commitments` before Deneb to compare against discard - if signedBlock.root in self.invalidBlockRoots: - returnWithError "Block root treated as invalid via config", $signedBlock.root - ok OptimisticStatus.notValidated else: ok OptimisticStatus.valid @@ -580,6 +577,12 @@ proc storeBlock( chronos.nanoseconds((slotTime - wallTime).nanoseconds) deadline = sleepAsync(deadlineTime) + if signedBlock.root in self.invalidBlockRoots: + warn "Block root treated as invalid via config", + blck = shortLog(signedBlock.message), + blockRoot = shortLog(signedBlock.root) + return err(VerifierError.Invalid) + # We have to be careful that there exists only one in-flight entry point # for adding blocks or the checks performed in `checkHeadBlock` might # be invalidated (ie a block could be added while we wait for EL response diff --git a/beacon_chain/spec/helpers.nim b/beacon_chain/spec/helpers.nim index dc9c65c851..32a560d07f 100644 --- a/beacon_chain/spec/helpers.nim +++ b/beacon_chain/spec/helpers.nim @@ -535,19 +535,36 @@ func toExecutionBlockHeader( requestsHash : requestsHash) # EIP-7685 func compute_execution_block_hash*( - body: ForkyBeaconBlockBody, - parentRoot: Eth2Digest): Eth2Digest = - when typeof(body).kind >= ConsensusFork.Electra: - body.execution_payload.toExecutionBlockHeader( - Opt.some parentRoot, Opt.some body.execution_requests.computeRequestsHash()) - .computeRlpHash().to(Eth2Digest) - elif typeof(body).kind >= ConsensusFork.Deneb: - body.execution_payload.toExecutionBlockHeader( - Opt.some parentRoot) - .computeRlpHash().to(Eth2Digest) + consensusFork: static ConsensusFork, + payload: ForkyExecutionPayload, + parentRoot: Eth2Digest, + requestsHash = Opt.none(EthHash32), +): Eth2Digest = + let header = + when consensusFork >= ConsensusFork.Electra: + payload.toExecutionBlockHeader(Opt.some parentRoot, requestsHash) + elif consensusFork >= ConsensusFork.Deneb: + payload.toExecutionBlockHeader(Opt.some parentRoot) + else: + payload.toExecutionBlockHeader(Opt.none(Eth2Digest)) + + header.computeRlpHash().to(Eth2Digest) + +func compute_execution_block_hash*( + body: ForkyBeaconBlockBody, parentRoot: Eth2Digest +): Eth2Digest = + const consensusFork = typeof(body).kind + when consensusFork >= ConsensusFork.Electra: + compute_execution_block_hash( + consensusFork, + body.execution_payload, + parentRoot, + Opt.some body.execution_requests.computeRequestsHash(), + ) else: - body.execution_payload.toExecutionBlockHeader(Opt.none(Eth2Digest)) - .computeRlpHash().to(Eth2Digest) + compute_execution_block_hash( + consensusFork, body.execution_payload, parentRoot + ) func compute_execution_block_hash*(blck: ForkyBeaconBlock): Eth2Digest = blck.body.compute_execution_block_hash(blck.parent_root) diff --git a/tests/test_block_processor.nim b/tests/test_block_processor.nim index 87caedd41c..624abb270b 100644 --- a/tests/test_block_processor.nim +++ b/tests/test_block_processor.nim @@ -18,9 +18,10 @@ import ../beacon_chain/gossip_processing/block_processor, ../beacon_chain/consensus_object_pools/[ attestation_pool, blockchain_dag, blob_quarantine, block_quarantine, - block_clearance, consensus_manager], + block_clearance, consensus_manager, + ], ../beacon_chain/el/el_manager, - ./testutil, ./testdbutil, ./testblockutil + ./[testblockutil, testdbutil, testutil] from chronos/unittest2/asynctests import asyncTest from ../beacon_chain/spec/eth2_apis/dynamic_fee_recipients import @@ -40,43 +41,57 @@ suite "Block processor" & preset(): var res = defaultRuntimeConfig res.ALTAIR_FORK_EPOCH = GENESIS_EPOCH res.BELLATRIX_FORK_EPOCH = GENESIS_EPOCH + res.CAPELLA_FORK_EPOCH = Epoch(1) + res.DENEB_FORK_EPOCH = Epoch(2) + res.ELECTRA_FORK_EPOCH = Epoch(3) + res.FULU_FORK_EPOCH = Epoch(4) res db = cfg.makeTestDB(SLOTS_PER_EPOCH) validatorMonitor = newClone(ValidatorMonitor.init(cfg.timeParams)) dag = init(ChainDAGRef, cfg, db, validatorMonitor, {}) - var taskpool = Taskpool.new() quarantine = newClone(Quarantine.init(cfg)) blobQuarantine = newClone(BlobQuarantine()) dataColumnQuarantine = newClone(ColumnQuarantine()) attestationPool = newClone(AttestationPool.init(dag, quarantine)) elManager = new ELManager # TODO: initialise this properly - actionTracker: ActionTracker + actionTracker = default(ActionTracker) consensusManager = ConsensusManager.new( - dag, attestationPool, quarantine, elManager, actionTracker, - newClone(DynamicFeeRecipientsStore.init()), "", - Opt.some default(Eth1Address), defaultGasLimit) + dag, + attestationPool, + quarantine, + elManager, + actionTracker, + newClone(DynamicFeeRecipientsStore.init()), + "", + Opt.some default(Eth1Address), + defaultGasLimit, + ) state = newClone(dag.headState) - cache: StateCache - info: ForkedEpochInfo - cfg.process_slots( - state[], cfg.lastPremergeSlotInTestCfg, cache, info, {}).expect("OK") - var - b1 = addTestBlock(state[], cache, cfg = cfg).bellatrixData - b2 = addTestBlock(state[], cache, cfg = cfg).bellatrixData getTimeFn = proc(): BeaconTime = - b2.message.slot.start_beacon_time(cfg.timeParams) + getStateField(state[], slot).start_beacon_time(cfg.timeParams) batchVerifier = BatchVerifier.new(rng, taskpool) - processor = BlockProcessor.new( - false, "", "", batchVerifier, consensusManager, - validatorMonitor, blobQuarantine, dataColumnQuarantine, getTimeFn) + var + cache: StateCache + info: ForkedEpochInfo + + cfg.process_slots(state[], cfg.lastPremergeSlotInTestCfg, cache, info, {}).expect( + "OK" + ) asyncTest "Reverse order block add & get" & preset(): - let missing = await processor.addBlock(MsgSource.gossip, b2, noSidecars) + let + processor = BlockProcessor.new( + false, "", "", batchVerifier, consensusManager, validatorMonitor, + blobQuarantine, dataColumnQuarantine, getTimeFn, + ) + b1 = addTestBlock(state[], cache, cfg = cfg).bellatrixData + b2 = addTestBlock(state[], cache, cfg = cfg).bellatrixData - check: missing.error == VerifierError.MissingParent + missing = await processor.addBlock(MsgSource.gossip, b2, noSidecars) check: + missing.error == VerifierError.MissingParent not dag.containsForkBlock(b2.root) # Unresolved, shouldn't show up FetchRecord(root: b1.root) in quarantine[].checkMissing(32) @@ -94,8 +109,7 @@ suite "Block processor" & preset(): while processor[].hasBlocks(): poll() - let - b2Get = dag.getBlockRef(b2.root) + let b2Get = dag.getBlockRef(b2.root) check: b2Get.isSome() @@ -126,6 +140,8 @@ suite "Block processor" & preset(): asyncTest "Invalidate block root" & preset(): let + b1 = addTestBlock(state[], cache, cfg = cfg).bellatrixData + b2 = addTestBlock(state[], cache, cfg = cfg).bellatrixData processor = BlockProcessor.new( false, "", "", batchVerifier, consensusManager, validatorMonitor, blobQuarantine, dataColumnQuarantine, @@ -156,3 +172,30 @@ suite "Block processor" & preset(): res == Result[void, VerifierError].err VerifierError.Invalid dag.containsForkBlock(b1.root) not dag.containsForkBlock(b2.root) + + asyncTest "Process a block from each fork (without blobs)" & preset(): + let processor = BlockProcessor.new( + false, "", "", batchVerifier, consensusManager, validatorMonitor, blobQuarantine, + dataColumnQuarantine, getTimeFn, + ) + + debugGloasComment "TODO testing" + for consensusFork in ConsensusFork.Bellatrix .. ConsensusFork.Fulu: + process_slots( + cfg, + state[], + max( + getStateField(state[], slot) + 1, + cfg.consensusForkEpoch(consensusFork).start_slot, + ), + cache, + info, + {}, + ) + .expect("OK") + + withState(state[]): + let b0 = addTestEngineBlock(cfg, consensusFork, forkyState, cache) + discard await processor.addBlock( + MsgSource.gossip, b0.blck, b0.blobsBundle.toSidecarsOpt(consensusFork) + ) diff --git a/tests/testblockutil.nim b/tests/testblockutil.nim index a2e3ed41b5..7941e81d01 100644 --- a/tests/testblockutil.nim +++ b/tests/testblockutil.nim @@ -10,11 +10,13 @@ import chronicles, stew/endians2, + eth/common/headers_rlp, ../beacon_chain/consensus_object_pools/sync_committee_msg_pool, ../beacon_chain/el/engine_api_conversions, - ../beacon_chain/spec/datatypes/bellatrix, ../beacon_chain/spec/[ - beaconstate, helpers, keystore, signatures, state_transition, validator] + beaconstate, helpers, keystore, forks, signatures, state_transition, validator] + +from kzg4844 import KzgCommitment, KzgProof # TODO remove this dependency from std/random import rand @@ -27,6 +29,18 @@ const MockPrivKeys* = MockPrivKeysT() MockPubKeys* = MockPubKeysT() +type + BlobsBundle = object + # TODO the fulu BlobsBundle uses an ugly hack to get deneb compatibility + # which defeats the purpose of using distinct types to begin with.. + commitments*: seq[kzg4844.KzgCommitment] + proofs*: seq[kzg4844.KzgProof] + blobs*: seq[Blob] + + EngineBlock[BB: ForkySignedBeaconBlock] = object + blck*: BB + blobsBundle*: BlobsBundle + # https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.5/tests/core/pyspec/eth2spec/test/helpers/keys.py func `[]`*(sk: MockPrivKeysT, index: ValidatorIndex|uint64): ValidatorPrivKey = var bytes = (index.uint64 + 1'u64).toBytesLE() # Consistent with EF tests @@ -88,92 +102,94 @@ proc makeInitialDeposits*( ) func signBlock( - fork: Fork, genesis_validators_root: Eth2Digest, blck: ForkyBeaconBlock, - privKey: ValidatorPrivKey, flags: UpdateFlags = {}): ForkedSignedBeaconBlock = + fork: Fork, + genesis_validators_root: Eth2Digest, + blck: ForkyBeaconBlock, + privKey: ValidatorPrivKey, + flags: UpdateFlags, +): auto = let slot = blck.slot root = hash_tree_root(blck) signature = if skipBlsValidation notin flags: - get_block_signature( - fork, genesis_validators_root, slot, root, privKey).toValidatorSig() + get_block_signature(fork, genesis_validators_root, slot, root, privKey) + .toValidatorSig() else: ValidatorSig() - ForkedSignedBeaconBlock.init(ForkedBeaconBlock.init(blck), root, signature) + ForkyBeaconBlock.kind.SignedBeaconBlock(message: blck, signature: signature, root: root) from eth/eip1559 import EIP1559_INITIAL_BASE_FEE, calcEip1599BaseFee from eth/common/eth_types import EMPTY_ROOT_HASH, GasInt -func build_empty_merge_execution_payload( +func makeExecutionPayloadForSigning*( cfg: RuntimeConfig, - state: bellatrix.BeaconState): bellatrix.ExecutionPayloadForSigning = - ## Assuming a pre-state of the same slot, build a valid ExecutionPayload - ## without any transactions from a non-merged block. - - doAssert not is_merge_transition_complete(state) + consensusFork: static ConsensusFork, + state: ForkyBeaconState, + blobsBundle: BlobsBundle, +): consensusFork.ExecutionPayloadForSigning = + ## Construct an execution payload that is sufficiently valid to pass consensus + ## validations (without necessarily making sense on the execution side, which + ## requires execution state) - in Bellatrix, it _should_ be EL-valid as well! let + merged = is_merge_transition_complete(state) latest = state.latest_execution_payload_header timestamp = cfg.timeParams.compute_timestamp_at_slot(state, state.slot) randao_mix = get_randao_mix(state, get_current_epoch(state)) + base_fee = + if merged: + calcEip1599BaseFee(latest.gas_limit, latest.gas_used, latest.base_fee_per_gas) + else: + EIP1559_INITIAL_BASE_FEE - var payload = bellatrix.ExecutionPayload( + var eps = default(consensusFork.ExecutionPayloadForSigning) + var payload = typeof(eps.executionPayload)( parent_hash: latest.block_hash, - state_root: latest.state_root, # no changes to the state + fee_recipient: default(Eth1Address), + state_root: latest.state_root, receipts_root: EMPTY_ROOT_HASH.asEth2Digest, block_number: latest.block_number + 1, prev_randao: randao_mix, - gas_limit: 30000000, # retain same limit + gas_limit: if merged: latest.gas_limit else: 30000000, gas_used: 0, # empty block, 0 gas timestamp: timestamp, - base_fee_per_gas: EIP1559_INITIAL_BASE_FEE) + base_fee_per_gas: base_fee, + ) - payload.block_hash = compute_execution_block_hash(bellatrix.BeaconBlock(body: - bellatrix.BeaconBlockBody(execution_payload: payload))) + let parent_root = state.latest_block_root(default(Eth2Digest)) + payload.block_hash = + when consensusFork >= ConsensusFork.Electra: + # TODO execution request hash + compute_execution_block_hash( + consensusFork, payload, parent_root, Opt.some default(Hash32) + ) + else: + compute_execution_block_hash( + consensusFork, payload, parent_root, Opt.none Hash32 + ) - bellatrix.ExecutionPayloadForSigning(executionPayload: payload, - blockValue: Wei.zero) + when consensusFork >= ConsensusFork.Capella: + payload.withdrawals = + List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD](get_expected_withdrawals(state)) -func build_empty_execution_payload( - cfg: RuntimeConfig, - state: bellatrix.BeaconState, - feeRecipient: Eth1Address): bellatrix.ExecutionPayloadForSigning = - ## Assuming a pre-state of the same slot, build a valid ExecutionPayload - ## without any transactions. - let - latest = state.latest_execution_payload_header - timestamp = cfg.timeParams.compute_timestamp_at_slot(state, state.slot) - randao_mix = get_randao_mix(state, get_current_epoch(state)) - base_fee = calcEip1599BaseFee(latest.gas_limit, - latest.gas_used, - latest.base_fee_per_gas) - - var payload = bellatrix.ExecutionPayloadForSigning( - executionPayload: bellatrix.ExecutionPayload( - parent_hash: latest.block_hash, - fee_recipient: feeRecipient, - state_root: latest.state_root, # no changes to the state - receipts_root: EMPTY_ROOT_HASH.asEth2Digest, - block_number: latest.block_number + 1, - prev_randao: randao_mix, - gas_limit: latest.gas_limit, # retain same limit - gas_used: 0, # empty block, 0 gas - timestamp: timestamp, - base_fee_per_gas: base_fee), - blockValue: Wei.zero) - - payload.executionPayload.block_hash = - bellatrix.BeaconBlock(body: bellatrix.BeaconBlockBody(execution_payload: - payload.executionPayload)).compute_execution_block_hash() - - payload + eps.executionPayload = payload + + when consensusFork == ConsensusFork.Fulu: + eps.blobsBundle = fulu.BlobsBundle() + elif consensusFork in ConsensusFork.Deneb..ConsensusFork.Electra: + eps.blobsBundle = deneb.BlobsBundle() + + eps func lastPremergeSlotInTestCfg*(cfg: RuntimeConfig): Slot = # Merge shortly after Bellatrix cfg.BELLATRIX_FORK_EPOCH.start_slot + 10 -proc addTestBlock*( - state: var ForkedHashedBeaconState, +proc addTestEngineBlock*( + cfg: RuntimeConfig, + consensusFork: static ConsensusFork, + state: var ForkyHashedBeaconState, cache: var StateCache, eth1_data: Eth1Data = Eth1Data(), attestations: seq[phase0.Attestation] = newSeq[phase0.Attestation](), @@ -182,74 +198,107 @@ proc addTestBlock*( sync_aggregate: SyncAggregate = SyncAggregate.init(), graffiti: GraffitiBytes = default(GraffitiBytes), flags: set[UpdateFlag] = {}, - nextSlot: bool = true, - cfg: RuntimeConfig = defaultRuntimeConfig): ForkedSignedBeaconBlock = +): EngineBlock[consensusFork.SignedBeaconBlock] = # Create and add a block to state - state will advance by one slot! - if nextSlot: - var info = ForkedEpochInfo() - process_slots( - cfg, state, getStateField(state, slot) + 1, cache, info, flags).expect( - "can advance 1") - let - proposer_index = get_beacon_proposer_index( - state, cache, getStateField(state, slot)).expect("valid proposer index") + proposer_index = get_beacon_proposer_index(state.data, cache, state.data.slot) + .expect("valid proposer index") privKey = MockPrivKeys[proposer_index] randao_reveal = if skipBlsValidation notin flags: get_epoch_signature( - getStateField(state, fork), - getStateField(state, genesis_validators_root), - getStateField(state, slot).epoch, privKey).toValidatorSig() + state.data.fork, state.data.genesis_validators_root, state.data.slot.epoch, + privKey, + ) + .toValidatorSig() else: ValidatorSig() - withState(state): - let execution_payload = - when consensusFork > ConsensusFork.Bellatrix: - default(consensusFork.ExecutionPayloadForSigning) - elif consensusFork == ConsensusFork.Bellatrix: - if cfg.CAPELLA_FORK_EPOCH != FAR_FUTURE_EPOCH: - # Can't keep correctly doing this once Capella happens, but LVH search - # test relies on merging. So, merge only if no Capella transition. - default(bellatrix.ExecutionPayloadForSigning) - else: - if forkyState.data.slot > cfg.lastPremergeSlotInTestCfg: - if is_merge_transition_complete(forkyState.data): - const feeRecipient = default(Eth1Address) - cfg.build_empty_execution_payload(forkyState.data, feeRecipient) - else: - cfg.build_empty_merge_execution_payload(forkyState.data) - else: - default(bellatrix.ExecutionPayloadForSigning) - else: - default(bellatrix.ExecutionPayloadForSigning) - - let message = makeBeaconBlock( - cfg, - consensusFork, - forkyState, - cache, - proposer_index, - randao_reveal, + eth1_data = # Keep deposit counts internally consistent. Eth1Data( deposit_root: eth1_data.deposit_root, - deposit_count: forkyState.data.eth1_deposit_index + deposits.lenu64, - block_hash: eth1_data.block_hash), - graffiti, - when consensusFork >= ConsensusFork.Electra: - electraAttestations + deposit_count: state.data.eth1_deposit_index + deposits.lenu64, + block_hash: eth1_data.block_hash, + ) + blobs = BlobsBundle() + + eps = + when consensusFork >= ConsensusFork.Gloas: + debugGloasComment "" + default(gloas.ExecutionPayloadForSigning) + elif consensusFork >= ConsensusFork.Bellatrix: + if state.data.slot > cfg.lastPremergeSlotInTestCfg: + makeExecutionPayloadForSigning(cfg, consensusFork, state.data, blobs) + else: + default(consensusFork.ExecutionPayloadForSigning) else: + default(bellatrix.ExecutionPayloadForSigning) + attestations = + when consensusFork >= ConsensusFork.Electra: electraAttestations else: attestations + message = makeBeaconBlock( + cfg, + consensusFork, + state, + cache, + proposer_index, + randao_reveal, + eth1_data, + graffiti, attestations, - deposits, - BeaconBlockValidatorChanges(), - sync_aggregate, - execution_payload, - verificationFlags = {skipBlsValidation}).expect("block") - - signBlock( - forkyState.data.fork, forkyState.data.genesis_validators_root, message, privKey, flags) + deposits, + BeaconBlockValidatorChanges(), + sync_aggregate, + eps, + verificationFlags = {skipBlsValidation}, + ) + .expect("block") + + EngineBlock[consensusFork.SignedBeaconBlock]( + blck: signBlock( + state.data.fork, state.data.genesis_validators_root, message, privKey, flags + ) + ) + +template toSidecarsOpt*( + blobsBundle: BlobsBundle, consensusFork: static ConsensusFork +): untyped = + # TODO actually construct sidecars.. + when consensusFork >= ConsensusFork.Gloas: + Opt.some(default(gloas.DataColumnSidecars)) + elif consensusFork >= ConsensusFork.Fulu: + Opt.some(default(fulu.DataColumnSidecars)) + elif consensusFork >= ConsensusFork.Deneb: + Opt.some(default(BlobSidecars)) + else: + noSidecars + +proc addTestBlock*( + state: var ForkedHashedBeaconState, + cache: var StateCache, + eth1_data: Eth1Data = Eth1Data(), + attestations: seq[phase0.Attestation] = newSeq[phase0.Attestation](), + electraAttestations: seq[electra.Attestation] = newSeq[electra.Attestation](), + deposits: seq[Deposit] = newSeq[Deposit](), + sync_aggregate: SyncAggregate = SyncAggregate.init(), + graffiti: GraffitiBytes = default(GraffitiBytes), + flags: set[UpdateFlag] = {}, + nextSlot: bool = true, + cfg: RuntimeConfig = defaultRuntimeConfig): ForkedSignedBeaconBlock = + # Create and add a block to state - state will advance by one slot! + if nextSlot: + var info = ForkedEpochInfo() + process_slots( + cfg, state, getStateField(state, slot) + 1, cache, info, flags).expect( + "can advance 1") + + withState(state): + ForkedSignedBeaconBlock.init( + addTestEngineBlock( + cfg, consensusFork, forkyState, cache, eth1_data, attestations, + electraAttestations, deposits, sync_aggregate, graffiti, flags, + ).blck + ) proc makeTestBlock*( state: ForkedHashedBeaconState,