Skip to content

Commit 613f4a9

Browse files
authored
accelerate EL sync with LC with --sync-light-client (#4041)
When the BN-embedded LC makes sync progress, pass the corresponding execution block hash to the EL via `engine_forkchoiceUpdatedV1`. This allows the EL to sync to wall slot while the chain DAG is behind. Renamed `--light-client` to `--sync-light-client` for clarity, and `--light-client-trusted-block-root` to `--trusted-block-root` for consistency with `nimbus_light_client`. Note that this does not work well in practice at this time: - Geth sticks to the optimistic sync: "Ignoring payload while snap syncing" (when passing the LC head) "Forkchoice requested unknown head" (when updating to LC head) - Nethermind syncs to LC head but does not report ancestors as VALID, so the main forward sync is still stuck in optimistic mode: "Pre-pivot block, ignored and returned Syncing" To aid EL client teams in fixing those issues, having this available as a hidden option is still useful.
1 parent 2545d1d commit 613f4a9

File tree

11 files changed

+164
-65
lines changed

11 files changed

+164
-65
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,7 @@ define CONNECT_TO_NETWORK_IN_DEV_MODE
383383
--network=$(1) $(3) $(GOERLI_TESTNETS_PARAMS) \
384384
--log-level="DEBUG; TRACE:discv5,networking; REQUIRED:none; DISABLED:none" \
385385
--data-dir=build/data/shared_$(1)_$(NODE_ID) \
386-
--light-client=on \
386+
--sync-light-client=on \
387387
--dump $(NODE_PARAMS)
388388
endef
389389

beacon_chain/beacon_node_light_client.nim

Lines changed: 45 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,15 @@ import
1717
logScope: topics = "beacnde"
1818

1919
func shouldSyncOptimistically*(node: BeaconNode, wallSlot: Slot): bool =
20-
# Check whether light client is used for syncing
21-
let optimisticHeader = node.lightClient.optimisticHeader.valueOr:
22-
return false
23-
24-
# Check whether light client is sufficiently ahead of DAG
25-
const minProgress = 8 * SLOTS_PER_EPOCH # Set arbitrarily
26-
let dagSlot = getStateField(node.dag.headState, slot)
27-
if dagSlot + minProgress > optimisticHeader.slot:
20+
if node.eth1Monitor == nil:
2821
return false
29-
30-
# Check whether light client has synced sufficiently close to wall slot
31-
const maxAge = 2 * SLOTS_PER_EPOCH
32-
if optimisticHeader.slot < max(wallSlot, maxAge.Slot) - maxAge:
22+
let optimisticHeader = node.lightClient.optimisticHeader.valueOr:
3323
return false
3424

35-
true
25+
shouldSyncOptimistically(
26+
optimisticSlot = optimisticHeader.slot,
27+
dagSlot = getStateField(node.dag.headState, slot),
28+
wallSlot = wallSlot)
3629

3730
proc initLightClient*(
3831
node: BeaconNode,
@@ -43,26 +36,52 @@ proc initLightClient*(
4336
genesis_validators_root: Eth2Digest) =
4437
template config(): auto = node.config
4538

46-
# Creating a light client is not dependent on `lightClientEnable`
39+
# Creating a light client is not dependent on `syncLightClient`
4740
# because the light client module also handles gossip subscriptions
4841
# for broadcasting light client data as a server.
4942

5043
let
5144
optimisticHandler = proc(signedBlock: ForkedMsgTrustedSignedBeaconBlock):
5245
Future[void] {.async.} =
53-
debug "New LC optimistic block",
46+
info "New LC optimistic block",
5447
opt = signedBlock.toBlockId(),
5548
dag = node.dag.head.bid,
5649
wallSlot = node.currentSlot
57-
return
50+
withBlck(signedBlock):
51+
when stateFork >= BeaconStateFork.Bellatrix:
52+
if blck.message.is_execution_block:
53+
template payload(): auto = blck.message.body.execution_payload
54+
55+
let eth1Monitor = node.eth1Monitor
56+
if eth1Monitor != nil and not payload.block_hash.isZero:
57+
# engine_newPayloadV1
58+
discard await eth1Monitor.newExecutionPayload(payload)
59+
60+
# Retain optimistic head for other `forkchoiceUpdated` callers.
61+
# May temporarily block `forkchoiceUpdatedV1` calls, e.g., Geth:
62+
# - Refuses `newPayload`: "Ignoring payload while snap syncing"
63+
# - Refuses `fcU`: "Forkchoice requested unknown head"
64+
# Once DAG sync catches up or as new optimistic heads are fetched
65+
# the situation recovers
66+
node.consensusManager[].setOptimisticHead(
67+
blck.toBlockId(), payload.block_hash)
68+
69+
# engine_forkchoiceUpdatedV1
70+
let beaconHead = node.attestationPool[].getBeaconHead(nil)
71+
discard await eth1Monitor.runForkchoiceUpdated(
72+
headBlockRoot = payload.block_hash,
73+
safeBlockRoot = beaconHead.safeExecutionPayloadHash,
74+
finalizedBlockRoot = beaconHead.finalizedExecutionPayloadHash)
75+
else: discard
76+
5877
optimisticProcessor = initOptimisticProcessor(
5978
getBeaconTime, optimisticHandler)
6079

6180
lightClient = createLightClient(
6281
node.network, rng, config, cfg, forkDigests, getBeaconTime,
6382
genesis_validators_root, LightClientFinalizationMode.Strict)
6483

65-
if config.lightClientEnable:
84+
if config.syncLightClient:
6685
proc onFinalizedHeader(
6786
lightClient: LightClient, finalizedHeader: BeaconBlockHeader) =
6887
optimisticProcessor.setFinalizedHeader(finalizedHeader)
@@ -73,18 +92,18 @@ proc initLightClient*(
7392

7493
lightClient.onFinalizedHeader = onFinalizedHeader
7594
lightClient.onOptimisticHeader = onOptimisticHeader
76-
lightClient.trustedBlockRoot = config.lightClientTrustedBlockRoot
95+
lightClient.trustedBlockRoot = config.trustedBlockRoot
7796

78-
elif config.lightClientTrustedBlockRoot.isSome:
79-
warn "Ignoring `lightClientTrustedBlockRoot`, light client not enabled",
80-
lightClientEnable = config.lightClientEnable,
81-
lightClientTrustedBlockRoot = config.lightClientTrustedBlockRoot
97+
elif config.trustedBlockRoot.isSome:
98+
warn "Ignoring `trustedBlockRoot`, light client not enabled",
99+
syncLightClient = config.syncLightClient,
100+
trustedBlockRoot = config.trustedBlockRoot
82101

83102
node.optimisticProcessor = optimisticProcessor
84103
node.lightClient = lightClient
85104

86105
proc startLightClient*(node: BeaconNode) =
87-
if not node.config.lightClientEnable:
106+
if not node.config.syncLightClient:
88107
return
89108

90109
node.lightClient.start()
@@ -94,7 +113,7 @@ proc installLightClientMessageValidators*(node: BeaconNode) =
94113
if node.config.lightClientDataServe:
95114
# Process gossip using both full node and light client
96115
node.processor
97-
elif node.config.lightClientEnable:
116+
elif node.config.syncLightClient:
98117
# Only process gossip using light client
99118
nil
100119
else:
@@ -116,9 +135,9 @@ proc updateLightClientGossipStatus*(
116135
node.lightClient.updateGossipStatus(slot, some isBehind)
117136

118137
proc updateLightClientFromDag*(node: BeaconNode) =
119-
if not node.config.lightClientEnable:
138+
if not node.config.syncLightClient:
120139
return
121-
if node.config.lightClientTrustedBlockRoot.isSome:
140+
if node.config.trustedBlockRoot.isSome:
122141
return
123142

124143
let

beacon_chain/conf.nim

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -284,16 +284,16 @@ type
284284
desc: "Weak subjectivity checkpoint in the format block_root:epoch_number"
285285
name: "weak-subjectivity-checkpoint" .}: Option[Checkpoint]
286286

287-
lightClientEnable* {.
287+
syncLightClient* {.
288288
hidden
289-
desc: "BETA: Accelerate sync using light client."
289+
desc: "Accelerate sync using light client"
290290
defaultValue: false
291-
name: "light-client" .}: bool
291+
name: "sync-light-client" .}: bool
292292

293-
lightClientTrustedBlockRoot* {.
293+
trustedBlockRoot* {.
294294
hidden
295-
desc: "BETA: Recent trusted finalized block root to initialize light client from."
296-
name: "light-client-trusted-block-root" .}: Option[Eth2Digest]
295+
desc: "Recent trusted finalized block root to initialize light client from"
296+
name: "trusted-block-root" .}: Option[Eth2Digest]
297297

298298
finalizedCheckpointState* {.
299299
desc: "SSZ file specifying a recent finalized state"

beacon_chain/consensus_object_pools/attestation_pool.nim

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -740,7 +740,7 @@ type BeaconHead* = object
740740
safeExecutionPayloadHash*, finalizedExecutionPayloadHash*: Eth2Digest
741741

742742
proc getBeaconHead*(
743-
pool: var AttestationPool, headBlock: BlockRef): BeaconHead =
743+
pool: AttestationPool, headBlock: BlockRef): BeaconHead =
744744
let
745745
finalizedExecutionPayloadHash =
746746
pool.dag.loadExecutionBlockRoot(pool.dag.finalizedHead.blck)

beacon_chain/consensus_object_pools/consensus_manager.nim

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ type
5656
# Tracking last proposal forkchoiceUpdated payload information
5757
# ----------------------------------------------------------------
5858
forkchoiceUpdatedInfo*: Opt[ForkchoiceUpdatedInformation]
59+
optimisticHead: tuple[bid: BlockId, execution_block_hash: Eth2Digest]
5960

6061
# Initialization
6162
# ------------------------------------------------------------------------------
@@ -117,6 +118,45 @@ from web3/engine_api_types import
117118

118119
func `$`(h: BlockHash): string = $h.asEth2Digest
119120

121+
func shouldSyncOptimistically*(
122+
optimisticSlot, dagSlot, wallSlot: Slot): bool =
123+
## Determine whether an optimistic execution block hash should be reported
124+
## to the EL client instead of the current head as determined by fork choice.
125+
126+
# Check whether optimistic head is sufficiently ahead of DAG
127+
const minProgress = 8 * SLOTS_PER_EPOCH # Set arbitrarily
128+
if optimisticSlot < dagSlot or optimisticSlot - dagSlot < minProgress:
129+
return false
130+
131+
# Check whether optimistic head has synced sufficiently close to wall slot
132+
const maxAge = 2 * SLOTS_PER_EPOCH # Set arbitrarily
133+
if optimisticSlot < max(wallSlot, maxAge.Slot) - maxAge:
134+
return false
135+
136+
true
137+
138+
func shouldSyncOptimistically*(self: ConsensusManager, wallSlot: Slot): bool =
139+
if self.eth1Monitor == nil:
140+
return false
141+
if self.optimisticHead.execution_block_hash.isZero:
142+
return false
143+
144+
shouldSyncOptimistically(
145+
optimisticSlot = self.optimisticHead.bid.slot,
146+
dagSlot = getStateField(self.dag.headState, slot),
147+
wallSlot = wallSlot)
148+
149+
func optimisticHead*(self: ConsensusManager): BlockId =
150+
self.optimisticHead.bid
151+
152+
func optimisticExecutionPayloadHash*(self: ConsensusManager): Eth2Digest =
153+
self.optimisticHead.execution_block_hash
154+
155+
func setOptimisticHead*(
156+
self: var ConsensusManager,
157+
bid: BlockId, execution_block_hash: Eth2Digest) =
158+
self.optimisticHead = (bid: bid, execution_block_hash: execution_block_hash)
159+
120160
proc runForkchoiceUpdated*(
121161
eth1Monitor: Eth1Monitor,
122162
headBlockRoot, safeBlockRoot, finalizedBlockRoot: Eth2Digest):
@@ -158,6 +198,12 @@ proc runForkchoiceUpdated*(
158198
err = err.msg
159199
return PayloadExecutionStatus.syncing
160200

201+
proc runForkchoiceUpdatedDiscardResult*(
202+
eth1Monitor: Eth1Monitor,
203+
headBlockRoot, safeBlockRoot, finalizedBlockRoot: Eth2Digest) {.async.} =
204+
discard await eth1Monitor.runForkchoiceUpdated(
205+
headBlockRoot, safeBlockRoot, finalizedBlockRoot)
206+
161207
proc updateExecutionClientHead(self: ref ConsensusManager, newHead: BeaconHead)
162208
{.async.} =
163209
if self.eth1Monitor.isNil:

beacon_chain/fork_choice/fork_choice.nim

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -422,7 +422,7 @@ proc get_head*(self: var ForkChoice,
422422
self.checkpoints.proposer_boost_root)
423423

424424
# https://github.com/ethereum/consensus-specs/blob/v1.2.0-rc.3/fork_choice/safe-block.md#get_safe_beacon_block_root
425-
func get_safe_beacon_block_root*(self: var ForkChoice): Eth2Digest =
425+
func get_safe_beacon_block_root*(self: ForkChoice): Eth2Digest =
426426
# Use most recent justified block as a stopgap
427427
self.checkpoints.justified.checkpoint.root
428428

beacon_chain/gossip_processing/block_processor.nim

Lines changed: 43 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@ import
1717
../sszdump
1818

1919
from ../consensus_object_pools/consensus_manager import
20-
ConsensusManager, runForkchoiceUpdated, runProposalForkchoiceUpdated,
21-
updateHead, updateHeadWithExecution
20+
ConsensusManager, optimisticExecutionPayloadHash, runForkchoiceUpdated,
21+
runForkchoiceUpdatedDiscardResult, runProposalForkchoiceUpdated,
22+
shouldSyncOptimistically, updateHead, updateHeadWithExecution
2223
from ../beacon_clock import GetBeaconTimeFn, toFloatSeconds
2324
from ../consensus_object_pools/block_dag import BlockRef, root, slot
2425
from ../consensus_object_pools/block_pools_types import BlockError, EpochRef
@@ -294,25 +295,48 @@ proc storeBlock*(
294295
wallSlot.start_beacon_time)
295296

296297
if newHead.isOk:
297-
let headExecutionPayloadHash =
298-
self.consensusManager.dag.loadExecutionBlockRoot(newHead.get.blck)
299-
if headExecutionPayloadHash.isZero:
300-
# Blocks without execution payloads can't be optimistic.
298+
template eth1Monitor(): auto = self.consensusManager.eth1Monitor
299+
if self.consensusManager[].shouldSyncOptimistically(wallSlot):
300+
# Optimistic head is far in the future; report it as head block to EL.
301+
302+
# Note that the specification allows an EL client to skip fcU processing
303+
# if an update to an ancestor is requested.
304+
# > Client software MAY skip an update of the forkchoice state and MUST
305+
# NOT begin a payload build process if `forkchoiceState.headBlockHash`
306+
# references an ancestor of the head of canonical chain.
307+
# https://github.com/ethereum/execution-apis/blob/v1.0.0-beta.1/src/engine/specification.md#engine_forkchoiceupdatedv1
308+
#
309+
# However, in practice, an EL client may not have completed importing all
310+
# block headers, so may be unaware of a block's ancestor status.
311+
# Therefore, hopping back and forth between the optimistic head and the
312+
# chain DAG head does not work well in practice, e.g., Geth:
313+
# - "Beacon chain gapped" from DAG head to optimistic head,
314+
# - followed by "Beacon chain reorged" from optimistic head back to DAG.
301315
self.consensusManager[].updateHead(newHead.get.blck)
302-
elif not self.consensusManager.dag.is_optimistic newHead.get.blck.root:
303-
# Not `NOT_VALID`; either `VALID` or `INVALIDATED`, but latter wouldn't
304-
# be selected as head, so `VALID`. `forkchoiceUpdated` necessary for EL
305-
# client only.
306-
self.consensusManager[].updateHead(newHead.get.blck)
307-
asyncSpawn self.consensusManager.eth1Monitor.expectValidForkchoiceUpdated(
308-
headExecutionPayloadHash,
309-
newHead.get.safeExecutionPayloadHash,
310-
newHead.get.finalizedExecutionPayloadHash)
311-
312-
# TODO remove redundant fcU in case of proposal
313-
asyncSpawn self.consensusManager.runProposalForkchoiceUpdated()
316+
asyncSpawn eth1Monitor.runForkchoiceUpdatedDiscardResult(
317+
headBlockRoot = self.consensusManager[].optimisticExecutionPayloadHash,
318+
safeBlockRoot = newHead.get.safeExecutionPayloadHash,
319+
finalizedBlockRoot = newHead.get.finalizedExecutionPayloadHash)
314320
else:
315-
asyncSpawn self.consensusManager.updateHeadWithExecution(newHead.get)
321+
let headExecutionPayloadHash =
322+
self.consensusManager.dag.loadExecutionBlockRoot(newHead.get.blck)
323+
if headExecutionPayloadHash.isZero:
324+
# Blocks without execution payloads can't be optimistic.
325+
self.consensusManager[].updateHead(newHead.get.blck)
326+
elif not self.consensusManager.dag.is_optimistic newHead.get.blck.root:
327+
# Not `NOT_VALID`; either `VALID` or `INVALIDATED`, but latter wouldn't
328+
# be selected as head, so `VALID`. `forkchoiceUpdated` necessary for EL
329+
# client only.
330+
self.consensusManager[].updateHead(newHead.get.blck)
331+
asyncSpawn eth1Monitor.expectValidForkchoiceUpdated(
332+
headBlockRoot = headExecutionPayloadHash,
333+
safeBlockRoot = newHead.get.safeExecutionPayloadHash,
334+
finalizedBlockRoot = newHead.get.finalizedExecutionPayloadHash)
335+
336+
# TODO remove redundant fcU in case of proposal
337+
asyncSpawn self.consensusManager.runProposalForkchoiceUpdated()
338+
else:
339+
asyncSpawn self.consensusManager.updateHeadWithExecution(newHead.get)
316340
else:
317341
warn "Head selection failed, using previous head",
318342
head = shortLog(self.consensusManager.dag.head), wallSlot

beacon_chain/nimbus_beacon_node.nim

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1285,13 +1285,21 @@ proc onSlotEnd(node: BeaconNode, slot: Slot) {.async.} =
12851285
# above, this will be done just before the next slot starts
12861286
await node.updateGossipStatus(slot + 1)
12871287

1288-
func syncStatus(node: BeaconNode): string =
1288+
func syncStatus(node: BeaconNode, wallSlot: Slot): string =
12891289
let optimistic_head = node.dag.is_optimistic(node.dag.head.root)
12901290
if node.syncManager.inProgress:
1291-
if optimistic_head:
1292-
node.syncManager.syncStatus & "/opt"
1293-
else:
1294-
node.syncManager.syncStatus
1291+
let
1292+
optimisticSuffix =
1293+
if optimistic_head:
1294+
"/opt"
1295+
else:
1296+
""
1297+
lightClientSuffix =
1298+
if node.consensusManager[].shouldSyncOptimistically(wallSlot):
1299+
" - lc: " & $shortLog(node.consensusManager[].optimisticHead)
1300+
else:
1301+
""
1302+
node.syncManager.syncStatus & optimisticSuffix & lightClientSuffix
12951303
elif node.backfiller.inProgress:
12961304
"backfill: " & node.backfiller.syncStatus
12971305
elif optimistic_head:
@@ -1318,7 +1326,7 @@ proc onSlotStart(node: BeaconNode, wallTime: BeaconTime,
13181326
info "Slot start",
13191327
slot = shortLog(wallSlot),
13201328
epoch = shortLog(wallSlot.epoch),
1321-
sync = node.syncStatus(),
1329+
sync = node.syncStatus(wallSlot),
13221330
peers = len(node.network.peerPool),
13231331
head = shortLog(node.dag.head),
13241332
finalized = shortLog(getStateField(
@@ -1751,7 +1759,7 @@ when not defined(windows):
17511759
formatGwei(node.attachedValidatorBalanceTotal)
17521760

17531761
of "sync_status":
1754-
node.syncStatus()
1762+
node.syncStatus(node.currentSlot)
17551763
else:
17561764
# We ignore typos for now and just render the expression
17571765
# as it was written. TODO: come up with a good way to show

beacon_chain/nimbus_light_client.nim

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ programMain:
8181
if blck.message.is_execution_block:
8282
template payload(): auto = blck.message.body.execution_payload
8383

84-
if eth1Monitor != nil:
84+
if eth1Monitor != nil and not payload.block_hash.isZero:
8585
await eth1Monitor.ensureDataProvider()
8686

8787
# engine_newPayloadV1
@@ -93,7 +93,6 @@ programMain:
9393
safeBlockRoot = payload.block_hash, # stub value
9494
finalizedBlockRoot = ZERO_HASH)
9595
else: discard
96-
return
9796
optimisticProcessor = initOptimisticProcessor(
9897
getBeaconTime, optimisticHandler)
9998

@@ -139,7 +138,8 @@ programMain:
139138
lightClient.trustedBlockRoot = some config.trustedBlockRoot
140139

141140
# Full blocks gossip is required to portably drive an EL client:
142-
# - EL clients may not sync when only driven with `forkChoiceUpdated`
141+
# - EL clients may not sync when only driven with `forkChoiceUpdated`,
142+
# e.g., Geth: "Forkchoice requested unknown head"
143143
# - `newPayload` requires the full `ExecutionPayload` (most of block content)
144144
# - `ExecutionPayload` block root is not available in `BeaconBlockHeader`,
145145
# so won't be exchanged via light client gossip

0 commit comments

Comments
 (0)