set `safe_block_hash` to fork choice justified (#4010)

Implements the fork choice safe block spec, where `safe_block_hash` in
`forkChoiceUpdated` is set to justified (used to be `ZERO_HASH`).
https://github.com/ethereum/consensus-specs/blob/v1.2.0-rc.3/fork_choice/safe-block.md#get_safe_execution_payload_hash
This commit is contained in:
Etan Kissling 2022-08-26 01:34:02 +02:00 committed by GitHub
parent ebfb624557
commit 64972e3c8a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 131 additions and 88 deletions

View File

@ -733,26 +733,55 @@ func getAggregatedAttestation*(pool: var AttestationPool,
res
type BeaconHead* = object
blck*: BlockRef
safeExecutionPayloadHash*, finalizedExecutionPayloadHash*: Eth2Digest
proc getBeaconHead*(
pool: var AttestationPool, headBlock: BlockRef): BeaconHead =
let
finalizedExecutionPayloadHash =
pool.dag.loadExecutionBlockRoot(pool.dag.finalizedHead.blck)
# https://github.com/ethereum/consensus-specs/blob/v1.2.0-rc.3/fork_choice/safe-block.md#get_safe_execution_payload_hash
safeBlockRoot = pool.forkChoice.get_safe_beacon_block_root()
safeBlock = pool.dag.getBlockRef(safeBlockRoot)
safeExecutionPayloadHash =
if safeBlock.isErr:
# Safe block is currently the justified block determined by fork choice.
# If finality already advanced beyond the current justified checkpoint,
# e.g., because we have selected a head that did not yet realize the cp,
# the justified block may end up not having a `BlockRef` anymore.
# Because we know that a different fork already finalized a later point,
# let's just report the finalized execution payload hash instead.
finalizedExecutionPayloadHash
else:
pool.dag.loadExecutionBlockRoot(safeBlock.get)
BeaconHead(
blck: headBlock,
safeExecutionPayloadHash: safeExecutionPayloadHash,
finalizedExecutionPayloadHash: finalizedExecutionPayloadHash)
proc selectOptimisticHead*(
pool: var AttestationPool, wallTime: BeaconTime): Opt[BlockRef] =
pool: var AttestationPool, wallTime: BeaconTime): Opt[BeaconHead] =
## Trigger fork choice and returns the new head block.
## Can return `nil`
# TODO rename this to get_optimistic_head
let newHead = pool.forkChoice.get_head(pool.dag, wallTime)
let newHeadRoot = pool.forkChoice.get_head(pool.dag, wallTime)
if newHeadRoot.isErr:
error "Couldn't select head", err = newHeadRoot.error
return err()
if newHead.isErr:
error "Couldn't select head", err = newHead.error
err()
else:
let ret = pool.dag.getBlockRef(newHead.get())
if ret.isErr():
# This should normally not happen, but if the chain dag and fork choice
# get out of sync, we'll need to try to download the selected head - in
# the meantime, return nil to indicate that no new head was chosen
warn "Fork choice selected unknown head, trying to sync", root = newHead.get()
pool.quarantine[].addMissing(newHead.get())
let headBlock = pool.dag.getBlockRef(newHeadRoot.get()).valueOr:
# This should normally not happen, but if the chain dag and fork choice
# get out of sync, we'll need to try to download the selected head - in
# the meantime, return nil to indicate that no new head was chosen
warn "Fork choice selected unknown head, trying to sync",
root = newHeadRoot.get()
pool.quarantine[].addMissing(newHeadRoot.get())
return err()
ret
ok pool.getBeaconHead(headBlock)
proc prune*(pool: var AttestationPool) =
if (let v = pool.forkChoice.prune(); v.isErr):

View File

@ -118,7 +118,8 @@ from web3/engine_api_types import
func `$`(h: BlockHash): string = $h.asEth2Digest
proc runForkchoiceUpdated*(
eth1Monitor: Eth1Monitor, headBlockRoot, finalizedBlockRoot: Eth2Digest):
eth1Monitor: Eth1Monitor,
headBlockRoot, safeBlockRoot, finalizedBlockRoot: Eth2Digest):
Future[PayloadExecutionStatus] {.async.} =
# Allow finalizedBlockRoot to be 0 to avoid sync deadlocks.
#
@ -138,15 +139,15 @@ proc runForkchoiceUpdated*(
let fcuR = awaitWithTimeout(
forkchoiceUpdated(
eth1Monitor, headBlockRoot, finalizedBlockRoot),
eth1Monitor, headBlockRoot, safeBlockRoot, finalizedBlockRoot),
FORKCHOICEUPDATED_TIMEOUT):
debug "runForkchoiceUpdated: forkchoiceUpdated timed out"
ForkchoiceUpdatedResponse(
payloadStatus: PayloadStatusV1(status: PayloadExecutionStatus.syncing))
payloadStatus: PayloadStatusV1(
status: PayloadExecutionStatus.syncing))
debug "runForkchoiceUpdated: ran forkchoiceUpdated",
headBlockRoot,
finalizedBlockRoot,
headBlockRoot, safeBlockRoot, finalizedBlockRoot,
payloadStatus = $fcuR.payloadStatus.status,
latestValidHash = $fcuR.payloadStatus.latestValidHash,
validationError = $fcuR.payloadStatus.validationError
@ -157,31 +158,32 @@ proc runForkchoiceUpdated*(
err = err.msg
return PayloadExecutionStatus.syncing
proc updateExecutionClientHead(self: ref ConsensusManager, newHead: BlockRef)
proc updateExecutionClientHead(self: ref ConsensusManager, newHead: BeaconHead)
{.async.} =
if self.eth1Monitor.isNil:
return
let executionHeadRoot = self.dag.loadExecutionBlockRoot(newHead)
let headExecutionPayloadHash = self.dag.loadExecutionBlockRoot(newHead.blck)
if executionHeadRoot.isZero:
if headExecutionPayloadHash.isZero:
# Blocks without execution payloads can't be optimistic.
self.dag.markBlockVerified(self.quarantine[], newHead.root)
self.dag.markBlockVerified(self.quarantine[], newHead.blck.root)
return
# Can't use dag.head here because it hasn't been updated yet
let payloadExecutionStatus = await self.eth1Monitor.runForkchoiceUpdated(
executionHeadRoot,
self.dag.loadExecutionBlockRoot(self.dag.finalizedHead.blck))
headExecutionPayloadHash,
newHead.safeExecutionPayloadHash,
newHead.finalizedExecutionPayloadHash)
case payloadExecutionStatus
of PayloadExecutionStatus.valid:
self.dag.markBlockVerified(self.quarantine[], newHead.root)
self.dag.markBlockVerified(self.quarantine[], newHead.blck.root)
of PayloadExecutionStatus.invalid, PayloadExecutionStatus.invalid_block_hash:
self.dag.markBlockInvalid(newHead.root)
self.quarantine[].addUnviable(newHead.root)
self.dag.markBlockInvalid(newHead.blck.root)
self.quarantine[].addUnviable(newHead.blck.root)
of PayloadExecutionStatus.accepted, PayloadExecutionStatus.syncing:
self.dag.optimisticRoots.incl newHead.root
self.dag.optimisticRoots.incl newHead.blck.root
proc updateHead*(self: var ConsensusManager, newHead: BlockRef) =
## Trigger fork choice and update the DAG with the new head block
@ -206,11 +208,11 @@ proc updateHead*(self: var ConsensusManager, wallSlot: Slot) =
head = shortLog(self.dag.head), wallSlot
return
if self.dag.loadExecutionBlockRoot(newHead).isZero:
if self.dag.loadExecutionBlockRoot(newHead.blck).isZero:
# Blocks without execution payloads can't be optimistic.
self.dag.markBlockVerified(self.quarantine[], newHead.root)
self.dag.markBlockVerified(self.quarantine[], newHead.blck.root)
self.updateHead(newHead)
self.updateHead(newHead.blck)
proc checkNextProposer(dag: ChainDAGRef, slot: Slot):
Opt[(ValidatorIndex, ValidatorPubKey)] =
@ -247,9 +249,8 @@ proc runProposalForkchoiceUpdated*(self: ref ConsensusManager) {.async.} =
get_randao_mix(state.data, get_current_epoch(state.data)).data
feeRecipient = self.getFeeRecipient(
nextProposer, validatorIndex, nextSlot.epoch)
headBlockRoot = self.dag.loadExecutionBlockRoot(self.dag.head)
finalizedBlockRoot =
self.dag.loadExecutionBlockRoot(self.dag.finalizedHead.blck)
beaconHead = self.attestationPool[].getBeaconHead(self.dag.head)
headBlockRoot = self.dag.loadExecutionBlockRoot(beaconHead.blck)
if headBlockRoot.isZero:
return
@ -257,8 +258,11 @@ proc runProposalForkchoiceUpdated*(self: ref ConsensusManager) {.async.} =
try:
let fcResult = awaitWithTimeout(
forkchoiceUpdated(
self.eth1Monitor, headBlockRoot, finalizedBlockRoot, timestamp,
randomData, feeRecipient),
self.eth1Monitor,
headBlockRoot,
beaconHead.safeExecutionPayloadHash,
beaconHead.finalizedExecutionPayloadHash,
timestamp, randomData, feeRecipient),
FORKCHOICEUPDATED_TIMEOUT):
debug "runProposalForkchoiceUpdated: forkchoiceUpdated timed out"
ForkchoiceUpdatedResponse(
@ -271,13 +275,14 @@ proc runProposalForkchoiceUpdated*(self: ref ConsensusManager) {.async.} =
self.forkchoiceUpdatedInfo = Opt.some ForkchoiceUpdatedInformation(
payloadId: bellatrix.PayloadID(fcResult.payloadId.get),
headBlockRoot: headBlockRoot,
finalizedBlockRoot: finalizedBlockRoot,
safeBlockRoot: beaconHead.safeExecutionPayloadHash,
finalizedBlockRoot: beaconHead.finalizedExecutionPayloadHash,
timestamp: timestamp,
feeRecipient: feeRecipient)
except CatchableError as err:
error "Engine API fork-choice update failed", err = err.msg
proc updateHeadWithExecution*(self: ref ConsensusManager, newHead: BlockRef)
proc updateHeadWithExecution*(self: ref ConsensusManager, newHead: BeaconHead)
{.async.} =
## Trigger fork choice and update the DAG with the new head block
## This does not automatically prune the DAG after finalization
@ -290,7 +295,7 @@ proc updateHeadWithExecution*(self: ref ConsensusManager, newHead: BlockRef)
# Store the new head in the chain DAG - this may cause epochs to be
# justified and finalized
self.dag.updateHead(newHead, self.quarantine[])
self.dag.updateHead(newHead.blck, self.quarantine[])
# TODO after things stabilize with this, check for upcoming proposal and
# don't bother sending first fcU, but initially, keep both in place

View File

@ -496,7 +496,7 @@ proc newPayload*(p: Eth1Monitor, payload: engine_api.ExecutionPayloadV1):
p.dataProvider.web3.provider.engine_newPayloadV1(payload)
proc forkchoiceUpdated*(p: Eth1Monitor,
headBlock, finalizedBlock: Eth2Digest):
headBlock, safeBlock, finalizedBlock: Eth2Digest):
Future[engine_api.ForkchoiceUpdatedResponse] =
# Eth1 monitor can recycle connections without (external) warning; at least,
# don't crash.
@ -510,17 +510,12 @@ proc forkchoiceUpdated*(p: Eth1Monitor,
p.dataProvider.web3.provider.engine_forkchoiceUpdatedV1(
ForkchoiceStateV1(
headBlockHash: headBlock.asBlockHash,
# https://hackmd.io/@n0ble/kintsugi-spec#Engine-API
# "CL client software MUST use headBlockHash value as a stub for the
# safeBlockHash parameter"
safeBlockHash: headBlock.asBlockHash,
safeBlockHash: safeBlock.asBlockHash,
finalizedBlockHash: finalizedBlock.asBlockHash),
none(engine_api.PayloadAttributesV1))
proc forkchoiceUpdated*(p: Eth1Monitor,
headBlock, finalizedBlock: Eth2Digest,
headBlock, safeBlock, finalizedBlock: Eth2Digest,
timestamp: uint64,
randomData: array[32, byte],
suggestedFeeRecipient: Eth1Address):
@ -537,12 +532,7 @@ proc forkchoiceUpdated*(p: Eth1Monitor,
p.dataProvider.web3.provider.engine_forkchoiceUpdatedV1(
ForkchoiceStateV1(
headBlockHash: headBlock.asBlockHash,
# https://hackmd.io/@n0ble/kintsugi-spec#Engine-API
# "CL client software MUST use headBlockHash value as a stub for the
# safeBlockHash parameter"
safeBlockHash: headBlock.asBlockHash,
safeBlockHash: safeBlock.asBlockHash,
finalizedBlockHash: finalizedBlock.asBlockHash),
some(engine_api.PayloadAttributesV1(
timestamp: Quantity timestamp,

View File

@ -413,6 +413,11 @@ proc get_head*(self: var ForkChoice,
self.checkpoints.justified.balances,
self.checkpoints.proposer_boost_root)
# https://github.com/ethereum/consensus-specs/blob/v1.2.0-rc.3/fork_choice/safe-block.md#get_safe_beacon_block_root
func get_safe_beacon_block_root*(self: var ForkChoice): Eth2Digest =
# Use most recent justified block as a stopgap
self.checkpoints.justified.checkpoint.root
func prune*(
self: var ForkChoiceBackend, finalized_root: Eth2Digest
): FcResult[void] =

View File

@ -173,20 +173,20 @@ from ../eth1/eth1_monitor import
Eth1Monitor, asEngineExecutionPayload, ensureDataProvider, newPayload
proc expectValidForkchoiceUpdated(
eth1Monitor: Eth1Monitor, headBlockRoot, finalizedBlockRoot: Eth2Digest):
Future[void] {.async.} =
eth1Monitor: Eth1Monitor,
headBlockRoot, safeBlockRoot, finalizedBlockRoot: Eth2Digest
): Future[void] {.async.} =
let payloadExecutionStatus =
await eth1Monitor.runForkchoiceUpdated(headBlockRoot, finalizedBlockRoot)
await eth1Monitor.runForkchoiceUpdated(
headBlockRoot, safeBlockRoot, finalizedBlockRoot)
if payloadExecutionStatus != PayloadExecutionStatus.valid:
# Only called when expecting this to be valid because `newPayload` or some
# previous `forkchoiceUpdated` had already marked it as valid.
warn "expectValidForkchoiceUpdate: forkChoiceUpdated not `VALID`",
payloadExecutionStatus,
headBlockRoot,
finalizedBlockRoot
payloadExecutionStatus, headBlockRoot, safeBlockRoot, finalizedBlockRoot
from ../consensus_object_pools/attestation_pool import
addForkChoice, selectOptimisticHead
addForkChoice, selectOptimisticHead, BeaconHead
from ../consensus_object_pools/blockchain_dag import
is_optimistic, loadExecutionBlockRoot, markBlockVerified
from ../consensus_object_pools/block_dag import shortLog
@ -294,20 +294,20 @@ proc storeBlock*(
wallSlot.start_beacon_time)
if newHead.isOk:
let executionHeadRoot =
self.consensusManager.dag.loadExecutionBlockRoot(newHead.get)
if executionHeadRoot.isZero:
let headExecutionPayloadHash =
self.consensusManager.dag.loadExecutionBlockRoot(newHead.get.blck)
if headExecutionPayloadHash.isZero:
# Blocks without execution payloads can't be optimistic.
self.consensusManager[].updateHead(newHead.get)
elif not self.consensusManager.dag.is_optimistic newHead.get.root:
self.consensusManager[].updateHead(newHead.get.blck)
elif not self.consensusManager.dag.is_optimistic newHead.get.blck.root:
# Not `NOT_VALID`; either `VALID` or `INVALIDATED`, but latter wouldn't
# be selected as head, so `VALID`. `forkchoiceUpdated` necessary for EL
# client only.
self.consensusManager[].updateHead(newHead.get)
self.consensusManager[].updateHead(newHead.get.blck)
asyncSpawn self.consensusManager.eth1Monitor.expectValidForkchoiceUpdated(
executionHeadRoot,
self.consensusManager.dag.loadExecutionBlockRoot(
self.consensusManager.dag.finalizedHead.blck))
headExecutionPayloadHash,
newHead.get.safeExecutionPayloadHash,
newHead.get.finalizedExecutionPayloadHash)
# TODO remove redundant fcU in case of proposal
asyncSpawn self.consensusManager.runProposalForkchoiceUpdated()

View File

@ -90,6 +90,7 @@ programMain:
# engine_forkchoiceUpdatedV1
discard await eth1Monitor.runForkchoiceUpdated(
headBlockRoot = payload.block_hash,
safeBlockRoot = payload.block_hash, # stub value
finalizedBlockRoot = ZERO_HASH)
else: discard
return

View File

@ -313,6 +313,7 @@ from web3/engine_api import ForkchoiceUpdatedResponse
# TODO: This copies the entire BeaconState on each call
proc forkchoice_updated(state: bellatrix.BeaconState,
head_block_hash: Eth2Digest,
safe_block_hash: Eth2Digest,
finalized_block_hash: Eth2Digest,
fee_recipient: ethtypes.Address,
execution_engine: Eth1Monitor):
@ -328,8 +329,8 @@ proc forkchoice_updated(state: bellatrix.BeaconState,
try:
awaitWithTimeout(
execution_engine.forkchoiceUpdated(
head_block_hash, finalized_block_hash, timestamp, random.data,
fee_recipient),
head_block_hash, safe_block_hash, finalized_block_hash,
timestamp, random.data, fee_recipient),
FORKCHOICEUPDATED_TIMEOUT):
error "Engine API fork-choice update timed out"
default(ForkchoiceUpdatedResponse)
@ -398,14 +399,15 @@ proc getExecutionPayload(
node.eth1Monitor.terminalBlockHash.get.asEth2Digest
else:
default(Eth2Digest)
executionBlockRoot = node.dag.loadExecutionBlockRoot(node.dag.head)
beaconHead = node.attestationPool[].getBeaconHead(node.dag.head)
executionBlockRoot = node.dag.loadExecutionBlockRoot(beaconHead.blck)
latestHead =
if not executionBlockRoot.isZero:
executionBlockRoot
else:
terminalBlockHash
latestFinalized =
node.dag.loadExecutionBlockRoot(node.dag.finalizedHead.blck)
latestSafe = beaconHead.safeExecutionPayloadHash
latestFinalized = beaconHead.finalizedExecutionPayloadHash
feeRecipient = node.getFeeRecipient(pubkey, validator_index, epoch)
lastFcU = node.consensusManager.forkchoiceUpdatedInfo
timestamp = compute_timestamp_at_slot(
@ -414,14 +416,14 @@ proc getExecutionPayload(
payload_id =
if lastFcU.isSome and
lastFcU.get.headBlockRoot == latestHead and
lastFcU.get.safeBlockRoot == latestSafe and
lastFcU.get.finalizedBlockRoot == latestFinalized and
lastFcU.get.timestamp == timestamp and
lastFcU.get.feeRecipient == feeRecipient:
some bellatrix.PayloadID(lastFcU.get.payloadId)
else:
debug "getExecutionPayload: didn't find payloadId, re-querying",
latestHead,
latestFinalized,
latestHead, latestSafe, latestFinalized,
timestamp,
feeRecipient,
cachedHeadBlockRoot = lastFcU.get.headBlockRoot,
@ -430,7 +432,8 @@ proc getExecutionPayload(
cachedFeeRecipient = lastFcU.get.feeRecipient
(await forkchoice_updated(
proposalState.bellatrixData.data, latestHead, latestFinalized,
proposalState.bellatrixData.data,
latestHead, latestSafe, latestFinalized,
feeRecipient, node.consensusManager.eth1Monitor))
payload = try:
awaitWithTimeout(

View File

@ -416,7 +416,8 @@ suite "Attestation pool processing" & preset():
epochRef, blckRef, unrealized, signedBlock.message,
blckRef.slot.start_beacon_time)
let head = pool[].selectOptimisticHead(b1Add[].slot.start_beacon_time).get()
let head =
pool[].selectOptimisticHead(b1Add[].slot.start_beacon_time).get().blck
check:
head == b1Add[]
@ -430,7 +431,8 @@ suite "Attestation pool processing" & preset():
epochRef, blckRef, unrealized, signedBlock.message,
blckRef.slot.start_beacon_time)
let head2 = pool[].selectOptimisticHead(b2Add[].slot.start_beacon_time).get()
let head2 =
pool[].selectOptimisticHead(b2Add[].slot.start_beacon_time).get().blck
check:
head2 == b2Add[]
@ -447,7 +449,8 @@ suite "Attestation pool processing" & preset():
epochRef, blckRef, unrealized, signedBlock.message,
blckRef.slot.start_beacon_time)
let head = pool[].selectOptimisticHead(b10Add[].slot.start_beacon_time).get()
let head =
pool[].selectOptimisticHead(b10Add[].slot.start_beacon_time).get().blck
check:
head == b10Add[]
@ -475,7 +478,8 @@ suite "Attestation pool processing" & preset():
attestation0, @[bc1[0]], attestation0.loadSig,
attestation0.data.slot.start_beacon_time)
let head2 = pool[].selectOptimisticHead(b10Add[].slot.start_beacon_time).get()
let head2 =
pool[].selectOptimisticHead(b10Add[].slot.start_beacon_time).get().blck
check:
# Single vote for b10 and no votes for b11
@ -488,7 +492,8 @@ suite "Attestation pool processing" & preset():
attestation1, @[bc1[1]], attestation1.loadSig,
attestation1.data.slot.start_beacon_time)
let head3 = pool[].selectOptimisticHead(b10Add[].slot.start_beacon_time).get()
let head3 =
pool[].selectOptimisticHead(b10Add[].slot.start_beacon_time).get().blck
let bigger = if b11.root.data < b10.root.data: b10Add else: b11Add
check:
@ -499,7 +504,8 @@ suite "Attestation pool processing" & preset():
attestation2, @[bc1[2]], attestation2.loadSig,
attestation2.data.slot.start_beacon_time)
let head4 = pool[].selectOptimisticHead(b11Add[].slot.start_beacon_time).get()
let head4 =
pool[].selectOptimisticHead(b11Add[].slot.start_beacon_time).get().blck
check:
# Two votes for b11
@ -517,7 +523,8 @@ suite "Attestation pool processing" & preset():
epochRef, blckRef, unrealized, signedBlock.message,
blckRef.slot.start_beacon_time)
let head = pool[].selectOptimisticHead(b10Add[].slot.start_beacon_time).get()
let head =
pool[].selectOptimisticHead(b10Add[].slot.start_beacon_time).get().blck
check:
head == b10Add[]
@ -550,7 +557,8 @@ suite "Attestation pool processing" & preset():
epochRef, blckRef, unrealized, signedBlock.message,
blckRef.slot.start_beacon_time)
let head = pool[].selectOptimisticHead(b10Add[].slot.start_beacon_time).get()
let head =
pool[].selectOptimisticHead(b10Add[].slot.start_beacon_time).get().blck
doAssert: head == b10Add[]
@ -577,7 +585,9 @@ suite "Attestation pool processing" & preset():
epochRef, blckRef, unrealized, signedBlock.message,
blckRef.slot.start_beacon_time)
let head = pool[].selectOptimisticHead(blockRef[].slot.start_beacon_time).get()
let head =
pool[].selectOptimisticHead(
blockRef[].slot.start_beacon_time).get().blck
doAssert: head == blockRef[]
dag.updateHead(head, quarantine[])
pruneAtFinalization(dag, pool[])