diff --git a/beacon_chain/el/engine_api_conversions.nim b/beacon_chain/el/engine_api_conversions.nim index 996d0ddece..2a78a907f9 100644 --- a/beacon_chain/el/engine_api_conversions.nim +++ b/beacon_chain/el/engine_api_conversions.nim @@ -232,11 +232,11 @@ func asConsensusType*( # The `mapIt` calls below are necessary only because we use different distinct # types for KZG commitments and Blobs in the `web3` and the `deneb` spec types. # Both are defined as `array[N, byte]` under the hood. - blobsBundle: deneb.BlobsBundle( + blobsBundle: fulu.BlobsBundleV2( commitments: KzgCommitments.init( payload.blobsBundle.commitments.mapIt( kzg_abi.KzgCommitment(bytes: it.data))), - proofs: KzgProofs.init( + proofs: KzgProofsV2.init( payload.blobsBundle.proofs.mapIt( kzg_abi.KzgProof(bytes: it.data))), blobs: Blobs.init( diff --git a/beacon_chain/rpc/rest_beacon_api.nim b/beacon_chain/rpc/rest_beacon_api.nim index 0a08497431..6670fec843 100644 --- a/beacon_chain/rpc/rest_beacon_api.nim +++ b/beacon_chain/rpc/rest_beacon_api.nim @@ -1042,7 +1042,8 @@ proc installBeaconApiHandlers*(router: var RestRouter, node: BeaconNode) = doAssert strictVerification notin node.dag.updateFlags return RestApiResponse.jsonError(Http400, InvalidBlockObjectError) - when consensusFork >= ConsensusFork.Deneb: + when consensusFork >= ConsensusFork.Deneb and + consensusFork < ConsensusFork.Fulu: await node.router.routeSignedBeaconBlock( forkyBlck, Opt.some( forkyBlck.create_blob_sidecars(kzg_proofs, blobs)), @@ -1099,7 +1100,8 @@ proc installBeaconApiHandlers*(router: var RestRouter, node: BeaconNode) = doAssert strictVerification notin node.dag.updateFlags return RestApiResponse.jsonError(Http400, InvalidBlockObjectError) - when consensusFork >= ConsensusFork.Deneb: + when consensusFork >= ConsensusFork.Deneb and + consensusFork < ConsensusFork.Fulu: await node.router.routeSignedBeaconBlock( forkyBlck, Opt.some( forkyBlck.create_blob_sidecars(kzg_proofs, blobs)), diff --git a/beacon_chain/spec/datatypes/deneb.nim b/beacon_chain/spec/datatypes/deneb.nim index b5a61cd050..0374236f8a 100644 --- a/beacon_chain/spec/datatypes/deneb.nim +++ b/beacon_chain/spec/datatypes/deneb.nim @@ -108,6 +108,7 @@ type excess_blob_gas*: uint64 # [New in Deneb] # https://github.com/ethereum/consensus-specs/blob/v1.6.0-alpha.0/specs/deneb/validator.md#blobsbundle + # https://github.com/ethereum/builder-specs/blob/ae1d97d080a12bfb7ca248b58fb1fc6b10aed02e/specs/fulu/builder.md#blobsbundle KzgProofs* = List[KzgProof, Limit MAX_BLOB_COMMITMENTS_PER_BLOCK] Blobs* = List[Blob, Limit MAX_BLOB_COMMITMENTS_PER_BLOCK] BlobRoots* = List[Eth2Digest, Limit MAX_BLOB_COMMITMENTS_PER_BLOCK] diff --git a/beacon_chain/spec/datatypes/fulu.nim b/beacon_chain/spec/datatypes/fulu.nim index fd3a214d9e..0aa35f65dc 100644 --- a/beacon_chain/spec/datatypes/fulu.nim +++ b/beacon_chain/spec/datatypes/fulu.nim @@ -123,6 +123,15 @@ type column_index*: ColumnIndex row_index*: RowIndex + # https://github.com/ethereum/builder-specs/blob/ae1d97d080a12bfb7ca248b58fb1fc6b10aed02e/specs/fulu/builder.md#blobsbundle + KzgProofsV2* = List[KzgProof, Limit FIELD_ELEMENTS_PER_EXT_BLOB * MAX_BLOB_COMMITMENTS_PER_BLOCK] + + # https://github.com/ethereum/builder-specs/blob/ae1d97d080a12bfb7ca248b58fb1fc6b10aed02e/specs/fulu/builder.md#blobsbundle + BlobsBundleV2* = object + commitments*: KzgCommitments + proofs*: KzgProofsV2 + blobs*: Blobs + # Not in spec, defined in order to compute custody subnets CgcBits* = BitArray[DATA_COLUMN_SIDECAR_SUBNET_COUNT] @@ -164,7 +173,7 @@ type ExecutionPayloadForSigning* = object executionPayload*: ExecutionPayload blockValue*: Wei - blobsBundle*: BlobsBundle + blobsBundle*: BlobsBundleV2 # [New in Fulu] executionRequests*: seq[seq[byte]] # https://github.com/ethereum/consensus-specs/blob/v1.5.0-alpha.10/specs/deneb/beacon-chain.md#executionpayloadheader @@ -606,7 +615,7 @@ type BlockContents* = object `block`*: BeaconBlock - kzg_proofs*: KzgProofs + kzg_proofs*: KzgProofsV2 blobs*: Blobs func shortLog*(v: DataColumnSidecar): auto = diff --git a/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim b/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim index 092086bbbd..73e6c06af7 100644 --- a/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim +++ b/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim @@ -270,6 +270,7 @@ RestJson.useDefaultSerializationFor( fulu_mev.BlindedBeaconBlock, fulu_mev.BlindedBeaconBlockBody, fulu_mev.BuilderBid, + fulu_mev.ExecutionPayloadAndBlobsBundle, fulu_mev.SignedBlindedBeaconBlock, fulu_mev.SignedBuilderBid, phase0.AggregateAndProof, diff --git a/beacon_chain/spec/eth2_apis/rest_types.nim b/beacon_chain/spec/eth2_apis/rest_types.nim index 8f8fdf643d..7d573a399c 100644 --- a/beacon_chain/spec/eth2_apis/rest_types.nim +++ b/beacon_chain/spec/eth2_apis/rest_types.nim @@ -316,7 +316,7 @@ type FuluSignedBlockContents* = object signed_block*: fulu.SignedBeaconBlock - kzg_proofs*: deneb.KzgProofs + kzg_proofs*: fulu.KzgProofsV2 blobs*: deneb.Blobs RestPublishedSignedBlockContents* = object @@ -550,6 +550,7 @@ type GetHeaderResponseElectra* = DataVersionEnclosedObject[electra_mev.SignedBuilderBid] GetHeaderResponseFulu* = DataVersionEnclosedObject[fulu_mev.SignedBuilderBid] SubmitBlindedBlockResponseElectra* = DataVersionEnclosedObject[electra_mev.ExecutionPayloadAndBlobsBundle] + SubmitBlindedBlockResponseFulu* = DataVersionEnclosedObject[fulu_mev.ExecutionPayloadAndBlobsBundle] RestNodeValidity* {.pure.} = enum valid = "VALID", diff --git a/beacon_chain/spec/forks.nim b/beacon_chain/spec/forks.nim index 143868b23b..83fd8cc28d 100644 --- a/beacon_chain/spec/forks.nim +++ b/beacon_chain/spec/forks.nim @@ -508,7 +508,8 @@ template kind*( fulu.TrustedSignedBeaconBlock | fulu_mev.BlindedBeaconBlock | fulu_mev.SignedBlindedBeaconBlock | - fulu_mev.SignedBuilderBid]): ConsensusFork = + fulu_mev.SignedBuilderBid | + fulu_mev.ExecutionPayloadAndBlobsBundle]): ConsensusFork = ConsensusFork.Fulu template BeaconState*(kind: static ConsensusFork): typedesc = diff --git a/beacon_chain/spec/mev/fulu_mev.nim b/beacon_chain/spec/mev/fulu_mev.nim index 8dd55d822b..83956aac60 100644 --- a/beacon_chain/spec/mev/fulu_mev.nim +++ b/beacon_chain/spec/mev/fulu_mev.nim @@ -13,6 +13,7 @@ from stew/byteutils import to0xHex from ".."/datatypes/phase0 import AttesterSlashing from ".."/datatypes/capella import SignedBLSToExecutionChange from ".."/datatypes/deneb import BlobsBundle, KzgCommitments +from ".."/datatypes/fulu import BlobsBundleV2 from ".."/datatypes/electra import Attestation, AttesterSlashing, ExecutionRequests from ".."/eth2_merkleization import hash_tree_root @@ -69,11 +70,17 @@ type message*: BlindedBeaconBlock signature*: ValidatorSig + # https://github.com/ethereum/builder-specs/blob/ae1d97d080a12bfb7ca248b58fb1fc6b10aed02e/specs/fulu/builder.md#executionpayloadandblobsbundle + ExecutionPayloadAndBlobsBundle* = object + execution_payload*: electra.ExecutionPayload + blobs_bundle*: BlobsBundleV2 # [New in Fulu] + # Not spec, but suggested by spec BlindedExecutionPayloadAndBlobsBundle* = object execution_payload_header*: fulu.ExecutionPayloadHeader blob_kzg_commitments*: KzgCommitments # [New in Deneb] + func shortLog*(v: BlindedBeaconBlock): auto = ( slot: shortLog(v.slot), diff --git a/beacon_chain/spec/state_transition.nim b/beacon_chain/spec/state_transition.nim index fc17087356..1193035b83 100644 --- a/beacon_chain/spec/state_transition.nim +++ b/beacon_chain/spec/state_transition.nim @@ -570,7 +570,7 @@ proc makeBeaconBlockWithRewards*( hash_tree_root(validator_changes.proposer_slashings), hash_tree_root(validator_changes.electra_attester_slashings), hash_tree_root( - List[electra.Attestation, Limit MAX_ATTESTATIONS]( + List[electra.Attestation, Limit MAX_ATTESTATIONS_ELECTRA]( attestations)), hash_tree_root(List[Deposit, Limit MAX_DEPOSITS](deposits)), hash_tree_root(validator_changes.voluntary_exits), diff --git a/beacon_chain/validators/beacon_validators.nim b/beacon_chain/validators/beacon_validators.nim index 226d936db3..f10f81958d 100644 --- a/beacon_chain/validators/beacon_validators.nim +++ b/beacon_chain/validators/beacon_validators.nim @@ -81,6 +81,7 @@ type executionPayloadValue*: Wei consensusBlockValue*: UInt256 blobsBundle*: deneb.BlobsBundle + blobsBundleV2*: fulu.BlobsBundleV2 BuilderBid[SBBB] = object blindedBlckPart*: SBBB @@ -585,15 +586,24 @@ proc makeBeaconBlockForHeadAndSlot*( $error if res.isOk: + let val = res.get() ok(EngineBid( - blck: res.get().blck, + blck: val.blck, executionPayloadValue: payload.blockValue, - consensusBlockValue: res.get().rewards.blockConsensusValue(), - blobsBundle: - when typeof(payload).kind >= ConsensusFork.Deneb: + consensusBlockValue: val.rewards.blockConsensusValue(), + blobsBundle: ( + when typeof(payload).kind >= ConsensusFork.Deneb and + typeof(payload).kind < ConsensusFork.Fulu: payload.blobsBundle else: default(deneb.BlobsBundle) + ), + blobsBundleV2: ( + when typeof(payload).kind >= ConsensusFork.Fulu: + payload.blobsBundle + else: + default(fulu.BlobsBundleV2) + ) )) else: err(res.error) @@ -920,14 +930,45 @@ proc getBuilderBid[ proc proposeBlockMEV( node: BeaconNode, payloadBuilderClient: RestClientRef, blindedBlock: - electra_mev.SignedBlindedBeaconBlock | + electra_mev.SignedBlindedBeaconBlock): + Future[Result[Opt[BlockRef], string]] {.async: (raises: [CancelledError]).} = + let unblindedBlockRef = await node.unblindAndRouteBlockMEV( + payloadBuilderClient, blindedBlock) + if unblindedBlockRef.isOk and unblindedBlockRef.get.isSome: + beacon_blocks_proposed.inc() + return ok(unblindedBlockRef.get) + else: + # unblindedBlockRef.isOk and unblindedBlockRef.get.isNone indicates that + # the block failed to validate and integrate into the DAG, which for the + # purpose of this return value, is equivalent. It's used to drive Beacon + # REST API output. + # + # https://collective.flashbots.net/t/post-mortem-april-3rd-2023-mev-boost-relay-incident-and-related-timing-issue/1540 + # has caused false positives, because + # "A potential mitigation to this attack is to introduce a cutoff timing + # into the proposer's slot whereafter this time (e.g. 3 seconds) the relay + # will no longer return a block to the proposer. Relays began to roll out + # this mitigation in the evening of April 3rd UTC time with a 2 second + # cutoff, and notified other relays to do the same. After receiving + # credible reports of honest validators missing their slots the suggested + # timing cutoff was increased to 3 seconds." + let errMsg = + if unblindedBlockRef.isErr: + unblindedBlockRef.error + else: + "Unblinded block not returned to proposer" + err errMsg + +proc proposeBlockMEV( + node: BeaconNode, payloadBuilderClient: RestClientRef, + blindedBlock: fulu_mev.SignedBlindedBeaconBlock): - Future[Result[BlockRef, string]] {.async: (raises: [CancelledError]).} = + Future[Result[Opt[BlockRef], string]] {.async: (raises: [CancelledError]).} = let unblindedBlockRef = await node.unblindAndRouteBlockMEV( payloadBuilderClient, blindedBlock) - return if unblindedBlockRef.isOk and unblindedBlockRef.get.isSome: + if unblindedBlockRef.isOk: beacon_blocks_proposed.inc() - ok(unblindedBlockRef.get.get) + return ok(Opt.none(BlockRef)) else: # unblindedBlockRef.isOk and unblindedBlockRef.get.isNone indicates that # the block failed to validate and integrate into the DAG, which for the @@ -1090,7 +1131,7 @@ proc proposeBlockAux( head: BlockRef, slot: Slot, randao: ValidatorSig, fork: Fork, genesis_validators_root: Eth2Digest, localBlockValueBoost: uint8 -): Future[BlockRef] {.async: (raises: [CancelledError]).} = +): Future[Opt[BlockRef]] {.async: (raises: [CancelledError]).} = let boostFactor = BoostFactor.init(localBlockValueBoost) graffitiBytes = node.getGraffitiBytes(validator) @@ -1109,7 +1150,7 @@ proc proposeBlockAux( collectedBids.engineBid.value().executionPayloadValue) else: if not collectedBids.engineBid.isSome(): - return head # errors logged in router + return Opt.some(head) # errors logged in router false # There should always be an engine bid, and if payloadBuilderClient exists, @@ -1150,20 +1191,25 @@ proc proposeBlockAux( blindedBlock = (await blindedBlockCheckSlashingAndSign( node, slot, validator, validator_index, collectedBids.builderBid.value().blindedBlckPart)).valueOr: - return head + return Opt.some(head) # Before proposeBlockMEV, can fall back to EL; after, cannot without # risking slashing. maybeUnblindedBlock = await proposeBlockMEV( node, payloadBuilderClient, blindedBlock) - return maybeUnblindedBlock.valueOr: + if maybeUnblindedBlock.isOk(): + if maybeUnblindedBlock.get.isSome(): + return maybeUnblindedBlock.get + else: + return Opt.none(BlockRef) + else: warn "Blinded block proposal incomplete", head = shortLog(head), slot, validator_index, validator = shortLog(validator), err = maybeUnblindedBlock.error, blindedBlck = shortLog(blindedBlock) beacon_block_builder_missed_without_fallback.inc() - return head + return Opt.some(head) let engineBid = collectedBids.engineBid.value() @@ -1184,8 +1230,7 @@ proc proposeBlockAux( validator = validator.pubkey, slot = slot, existingProposal = notSlashable.error - return head - + return Opt.some(head) let signature = block: @@ -1194,24 +1239,24 @@ proc proposeBlockAux( if res.isErr(): warn "Unable to sign block", validator = shortLog(validator), error_msg = res.error() - return head + return Opt.some(head) res.get() signedBlock = consensusFork.SignedBeaconBlock( message: forkyBlck, signature: signature, root: blockRoot) blobsOpt = when consensusFork >= ConsensusFork.Deneb: Opt.some(signedBlock.create_blob_sidecars( - engineBid.blobsBundle.proofs, engineBid.blobsBundle.blobs)) + KzgProofs(engineBid.blobsBundle.proofs), engineBid.blobsBundle.blobs)) else: Opt.none(seq[BlobSidecar]) newBlockRef = ( await node.router.routeSignedBeaconBlock(signedBlock, blobsOpt, checkValidator = false) ).valueOr: - return head # Errors logged in router + return Opt.some(head) # Errors logged in router if newBlockRef.isNone(): - return head # Validation errors logged in router + return Opt.some(head) # Validation errors logged in router notice "Block proposed", blockRoot = shortLog(blockRoot), blck = shortLog(forkyBlck), @@ -1219,7 +1264,7 @@ proc proposeBlockAux( beacon_blocks_proposed.inc() - return newBlockRef.get() + return newBlockRef proc proposeBlock( node: BeaconNode, @@ -1227,7 +1272,7 @@ proc proposeBlock( validator_index: ValidatorIndex, head: BlockRef, slot: Slot -): Future[BlockRef] {.async: (raises: [CancelledError]).} = +): Future[Opt[BlockRef]] {.async: (raises: [CancelledError]).} = if head.slot >= slot: # We should normally not have a head newer than the slot we're proposing for # but this can happen if block proposal is delayed @@ -1235,7 +1280,7 @@ proc proposeBlock( headSlot = shortLog(head.slot), headBlockRoot = shortLog(head.root), slot = shortLog(slot) - return head + return Opt.some(head) let fork = node.dag.forkAtEpoch(slot.epoch) @@ -1246,7 +1291,7 @@ proc proposeBlock( if res.isErr(): warn "Unable to generate randao reveal", validator = shortLog(validator), error_msg = res.error() - return head + return Opt.some(head) res.get() template proposeBlockContinuation(type1, type2: untyped): auto = @@ -1472,21 +1517,21 @@ proc sendSyncCommitteeContributions( node, validator, subcommitteeIdx, head, slot) proc handleProposal(node: BeaconNode, head: BlockRef, slot: Slot): - Future[BlockRef] {.async: (raises: [CancelledError]).} = + Future[Opt[BlockRef]] {.async: (raises: [CancelledError]).} = ## Perform the proposal for the given slot, iff we have a validator attached ## that is supposed to do so, given the shuffling at that slot for the given ## head - to compute the proposer, we need to advance a state to the given ## slot let proposer = node.dag.getProposer(head, slot).valueOr: - return head + return Opt.some(head) proposerKey = node.dag.validatorKey(proposer).get().toPubKey validator = node.getValidatorForDuties(proposer, slot).valueOr: debug "Expecting block proposal", headRoot = shortLog(head.root), slot = shortLog(slot), proposer_index = proposer, proposer = shortLog(proposerKey) - return head + return Opt.some(head) return await proposeBlock(node, validator, proposer, head, slot) @@ -1871,7 +1916,8 @@ proc handleValidatorDuties*(node: BeaconNode, lastSlot, slot: Slot) {.async: (ra node.updateValidators(forkyState.data.validators.asSeq()) let newHead = await handleProposal(node, head, slot) - head = newHead + if newHead.isSome(): + head = newHead.get let # The latest point in time when we'll be sending out attestations @@ -2007,7 +2053,19 @@ proc makeMaybeBlindedBeaconBlockForHeadAndSlotImpl[ResultType]( doAssert engineBid.blck.kind == consensusFork template forkyBlck: untyped = engineBid.blck.forky(consensusFork) - when consensusFork >= ConsensusFork.Deneb: + when consensusFork >= ConsensusFork.Fulu: + doAssert engineBid.blobsBundleV2.commitments == + forkyBlck.body.blob_kzg_commitments + ResultType.ok(( + blck: consensusFork.MaybeBlindedBeaconBlock( + isBlinded: false, + data: consensusFork.BlockContents( + `block`: forkyBlck, + kzg_proofs: KzgProofsV2(engineBid.blobsBundleV2.proofs), + blobs: engineBid.blobsBundleV2.blobs)), + executionValue: Opt.some(engineBid.executionPayloadValue), + consensusValue: Opt.some(engineBid.consensusBlockValue))) + elif consensusFork >= ConsensusFork.Deneb: doAssert engineBid.blobsBundle.commitments == forkyBlck.body.blob_kzg_commitments ResultType.ok(( @@ -2015,7 +2073,7 @@ proc makeMaybeBlindedBeaconBlockForHeadAndSlotImpl[ResultType]( isBlinded: false, data: consensusFork.BlockContents( `block`: forkyBlck, - kzg_proofs: engineBid.blobsBundle.proofs, + kzg_proofs: KzgProofs(engineBid.blobsBundle.proofs), blobs: engineBid.blobsBundle.blobs)), executionValue: Opt.some(engineBid.executionPayloadValue), consensusValue: Opt.some(engineBid.consensusBlockValue))) diff --git a/research/wss_sim.nim b/research/wss_sim.nim index 24fd462e7e..197739cd39 100644 --- a/research/wss_sim.nim +++ b/research/wss_sim.nim @@ -305,7 +305,8 @@ cli do(validatorsDir: string, secretsDir: string, proposerPrivkey).toValidatorSig()) dump(".", signedBlock) - when consensusFork >= ConsensusFork.Deneb: + when consensusFork >= ConsensusFork.Deneb and + consensusFork < ConsensusFork.Fulu: let blobs = signedBlock.create_blob_sidecars( payload.blobsBundle.proofs, payload.blobsBundle.blobs) for blob in blobs: