error and progress codes for light client sync (#3490)
When syncing as a light client, different behaviour is needed to handle the various ways how errors may occur. The existing logic for blocks can also be applied to light client objects: - `Invalid`: Malformed object that is clearly an error by its producer. - `MissingParent`: More data is needed to decide applicability. - `UnviableFork`: Object may be valid but will never apply on this fork. - `Duplicate`: No errors were encountered but the object was not useful.
This commit is contained in:
parent
276762958e
commit
29e5a4a752
|
@ -16,6 +16,8 @@ import
|
|||
datatypes/altair,
|
||||
helpers
|
||||
|
||||
from ../consensus_object_pools/block_pools_types import BlockError
|
||||
|
||||
func period_contains_fork_version(
|
||||
cfg: RuntimeConfig,
|
||||
period: SyncCommitteePeriod,
|
||||
|
@ -67,9 +69,9 @@ func get_safety_threshold(store: LightClientStore): uint64 =
|
|||
func initialize_light_client_store*(
|
||||
trusted_block_root: Eth2Digest,
|
||||
bootstrap: altair.LightClientBootstrap
|
||||
): Opt[LightClientStore] =
|
||||
): Result[LightClientStore, BlockError] =
|
||||
if hash_tree_root(bootstrap.header) != trusted_block_root:
|
||||
return err()
|
||||
return err(BlockError.Invalid)
|
||||
|
||||
if not is_valid_merkle_branch(
|
||||
hash_tree_root(bootstrap.current_sync_committee),
|
||||
|
@ -77,7 +79,7 @@ func initialize_light_client_store*(
|
|||
log2trunc(altair.CURRENT_SYNC_COMMITTEE_INDEX),
|
||||
get_subtree_index(altair.CURRENT_SYNC_COMMITTEE_INDEX),
|
||||
bootstrap.header.state_root):
|
||||
return err()
|
||||
return err(BlockError.Invalid)
|
||||
|
||||
return ok(LightClientStore(
|
||||
finalized_header: bootstrap.header,
|
||||
|
@ -90,37 +92,39 @@ proc validate_light_client_update*(
|
|||
update: altair.LightClientUpdate,
|
||||
current_slot: Slot,
|
||||
cfg: RuntimeConfig,
|
||||
genesis_validators_root: Eth2Digest): bool =
|
||||
genesis_validators_root: Eth2Digest): Result[void, BlockError] =
|
||||
# Verify sync committee has sufficient participants
|
||||
template sync_aggregate(): auto = update.sync_aggregate
|
||||
template sync_committee_bits(): auto = sync_aggregate.sync_committee_bits
|
||||
let num_active_participants = countOnes(sync_committee_bits).uint64
|
||||
if num_active_participants < MIN_SYNC_COMMITTEE_PARTICIPANTS:
|
||||
return false
|
||||
return err(BlockError.Invalid)
|
||||
|
||||
# Determine update header
|
||||
template attested_header(): auto = update.attested_header
|
||||
if current_slot < attested_header.slot:
|
||||
return false
|
||||
return err(BlockError.UnviableFork)
|
||||
let active_header = get_active_header(update)
|
||||
if attested_header.slot < active_header.slot:
|
||||
return false
|
||||
return err(BlockError.Invalid)
|
||||
|
||||
# Verify update is relevant
|
||||
let is_next_sync_committee_known = not store.next_sync_committee.isZeroMemory
|
||||
if is_next_sync_committee_known:
|
||||
if active_header.slot < store.finalized_header.slot:
|
||||
return false
|
||||
return err(BlockError.Duplicate)
|
||||
if active_header.slot == store.finalized_header.slot:
|
||||
if attested_header.slot <= store.optimistic_header.slot:
|
||||
return false
|
||||
return err(BlockError.Duplicate)
|
||||
|
||||
# Verify update does not skip a sync committee period
|
||||
let
|
||||
finalized_period = store.finalized_header.slot.sync_committee_period
|
||||
update_period = active_header.slot.sync_committee_period
|
||||
if update_period notin [finalized_period, finalized_period + 1]:
|
||||
return false
|
||||
if update_period < finalized_period:
|
||||
return err(BlockError.Duplicate)
|
||||
if update_period > finalized_period + 1:
|
||||
return err(BlockError.MissingParent)
|
||||
let
|
||||
is_signed_by_next_sync_committee =
|
||||
update.next_sync_committee.isZeroMemory
|
||||
|
@ -131,24 +135,24 @@ proc validate_light_client_update*(
|
|||
update_period
|
||||
current_period = current_slot.sync_committee_period
|
||||
if current_period < signature_period:
|
||||
return false
|
||||
return err(BlockError.UnviableFork)
|
||||
if is_next_sync_committee_known:
|
||||
if signature_period notin [finalized_period, finalized_period + 1]:
|
||||
return false
|
||||
return err(BlockError.MissingParent)
|
||||
else:
|
||||
if signature_period != finalized_period:
|
||||
return false
|
||||
return err(BlockError.MissingParent)
|
||||
|
||||
# Verify fork version is acceptable
|
||||
let fork_version = update.fork_version
|
||||
if not cfg.period_contains_fork_version(signature_period, fork_version):
|
||||
return false
|
||||
return err(BlockError.UnviableFork)
|
||||
|
||||
# Verify that the `finalized_header`, if present, actually is the finalized
|
||||
# header saved in the state of the `attested_header`
|
||||
if not update.is_finality_update:
|
||||
if not update.finality_branch.isZeroMemory:
|
||||
return false
|
||||
return err(BlockError.Invalid)
|
||||
else:
|
||||
if not is_valid_merkle_branch(
|
||||
hash_tree_root(update.finalized_header),
|
||||
|
@ -156,24 +160,24 @@ proc validate_light_client_update*(
|
|||
log2trunc(altair.FINALIZED_ROOT_INDEX),
|
||||
get_subtree_index(altair.FINALIZED_ROOT_INDEX),
|
||||
update.attested_header.state_root):
|
||||
return false
|
||||
return err(BlockError.Invalid)
|
||||
|
||||
# Verify that the `next_sync_committee`, if present, actually is the
|
||||
# next sync committee saved in the state of the `active_header`
|
||||
if is_signed_by_next_sync_committee:
|
||||
if not update.next_sync_committee_branch.isZeroMemory:
|
||||
return false
|
||||
return err(BlockError.Invalid)
|
||||
else:
|
||||
if update_period == finalized_period and is_next_sync_committee_known:
|
||||
if update.next_sync_committee != store.next_sync_committee:
|
||||
return false
|
||||
return err(BlockError.UnviableFork)
|
||||
if not is_valid_merkle_branch(
|
||||
hash_tree_root(update.next_sync_committee),
|
||||
update.next_sync_committee_branch,
|
||||
log2trunc(altair.NEXT_SYNC_COMMITTEE_INDEX),
|
||||
get_subtree_index(altair.NEXT_SYNC_COMMITTEE_INDEX),
|
||||
active_header.state_root):
|
||||
return false
|
||||
return err(BlockError.Invalid)
|
||||
|
||||
# Verify sync committee aggregate signature
|
||||
let sync_committee =
|
||||
|
@ -193,9 +197,9 @@ proc validate_light_client_update*(
|
|||
if not blsFastAggregateVerify(
|
||||
participant_pubkeys, signing_root.data,
|
||||
sync_aggregate.sync_committee_signature):
|
||||
return false
|
||||
return err(BlockError.Invalid)
|
||||
|
||||
true
|
||||
ok()
|
||||
|
||||
# https://github.com/ethereum/consensus-specs/blob/vFuture/specs/altair/sync-protocol.md#validate_optimistic_light_client_update
|
||||
proc validate_optimistic_light_client_update*(
|
||||
|
@ -203,30 +207,32 @@ proc validate_optimistic_light_client_update*(
|
|||
optimistic_update: OptimisticLightClientUpdate,
|
||||
current_slot: Slot,
|
||||
cfg: RuntimeConfig,
|
||||
genesis_validators_root: Eth2Digest): bool =
|
||||
genesis_validators_root: Eth2Digest): Result[void, BlockError] =
|
||||
# Verify sync committee has sufficient participants
|
||||
template sync_aggregate(): auto = optimistic_update.sync_aggregate
|
||||
template sync_committee_bits(): auto = sync_aggregate.sync_committee_bits
|
||||
let num_active_participants = countOnes(sync_committee_bits).uint64
|
||||
if num_active_participants < MIN_SYNC_COMMITTEE_PARTICIPANTS:
|
||||
return false
|
||||
return err(BlockError.Invalid)
|
||||
|
||||
# Determine update header
|
||||
template attested_header(): auto = optimistic_update.attested_header
|
||||
if current_slot < attested_header.slot:
|
||||
return false
|
||||
return err(BlockError.Invalid)
|
||||
template active_header(): auto = attested_header
|
||||
|
||||
# Verify update is relevant
|
||||
if attested_header.slot <= store.optimistic_header.slot:
|
||||
return false
|
||||
return err(BlockError.Duplicate)
|
||||
|
||||
# Verify update does not skip a sync committee period
|
||||
let
|
||||
finalized_period = store.finalized_header.slot.sync_committee_period
|
||||
update_period = active_header.slot.sync_committee_period
|
||||
if update_period notin [finalized_period, finalized_period + 1]:
|
||||
return false
|
||||
if update_period < finalized_period:
|
||||
return err(BlockError.Duplicate)
|
||||
if update_period > finalized_period + 1:
|
||||
return err(BlockError.MissingParent)
|
||||
let
|
||||
is_signed_by_next_sync_committee =
|
||||
optimistic_update.is_signed_by_next_sync_committee
|
||||
|
@ -237,19 +243,19 @@ proc validate_optimistic_light_client_update*(
|
|||
update_period
|
||||
current_period = current_slot.sync_committee_period
|
||||
if current_period < signature_period:
|
||||
return false
|
||||
return err(BlockError.Invalid)
|
||||
let is_next_sync_committee_known = not store.next_sync_committee.isZeroMemory
|
||||
if is_next_sync_committee_known:
|
||||
if signature_period notin [finalized_period, finalized_period + 1]:
|
||||
return false
|
||||
return err(BlockError.MissingParent)
|
||||
else:
|
||||
if signature_period != finalized_period:
|
||||
return false
|
||||
return err(BlockError.MissingParent)
|
||||
|
||||
# Verify fork version is acceptable
|
||||
let fork_version = optimistic_update.fork_version
|
||||
if not cfg.period_contains_fork_version(signature_period, fork_version):
|
||||
return false
|
||||
return err(BlockError.UnviableFork)
|
||||
|
||||
# Verify sync committee aggregate signature
|
||||
let sync_committee =
|
||||
|
@ -269,14 +275,15 @@ proc validate_optimistic_light_client_update*(
|
|||
if not blsFastAggregateVerify(
|
||||
participant_pubkeys, signing_root.data,
|
||||
sync_aggregate.sync_committee_signature):
|
||||
return false
|
||||
return err(BlockError.Invalid)
|
||||
|
||||
true
|
||||
ok()
|
||||
|
||||
# https://github.com/ethereum/consensus-specs/blob/vFuture/specs/altair/sync-protocol.md#apply_light_client_update
|
||||
func apply_light_client_update(
|
||||
store: var LightClientStore,
|
||||
update: altair.LightClientUpdate) =
|
||||
update: altair.LightClientUpdate): bool =
|
||||
var didProgress = false
|
||||
let
|
||||
active_header = get_active_header(update)
|
||||
finalized_period = store.finalized_header.slot.sync_committee_period
|
||||
|
@ -284,6 +291,7 @@ func apply_light_client_update(
|
|||
if store.next_sync_committee.isZeroMemory:
|
||||
assert update_period == finalized_period
|
||||
store.next_sync_committee = update.next_sync_committee
|
||||
didProgress = true
|
||||
elif update_period == finalized_period + 1:
|
||||
store.previous_max_active_participants =
|
||||
store.current_max_active_participants
|
||||
|
@ -291,29 +299,52 @@ func apply_light_client_update(
|
|||
store.current_sync_committee = store.next_sync_committee
|
||||
store.next_sync_committee = update.next_sync_committee
|
||||
assert not store.next_sync_committee.isZeroMemory
|
||||
didProgress = true
|
||||
if active_header.slot > store.finalized_header.slot:
|
||||
store.finalized_header = active_header
|
||||
didProgress = true
|
||||
didProgress
|
||||
|
||||
# https://github.com/ethereum/consensus-specs/blob/vFuture/specs/altair/sync-protocol.md#apply_optimistic_light_client_header
|
||||
func apply_optimistic_light_client_header(
|
||||
store: var LightClientStore,
|
||||
attested_header: BeaconBlockHeader,
|
||||
num_active_participants: uint64) =
|
||||
num_active_participants: uint64): bool =
|
||||
var didProgress = false
|
||||
|
||||
if store.current_max_active_participants < num_active_participants:
|
||||
store.current_max_active_participants = num_active_participants
|
||||
|
||||
if num_active_participants > get_safety_threshold(store) and
|
||||
attested_header.slot > store.optimistic_header.slot:
|
||||
store.optimistic_header = attested_header
|
||||
didProgress = true
|
||||
|
||||
didProgress
|
||||
|
||||
# https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/altair/sync-protocol.md#process_slot_for_light_client_store
|
||||
type
|
||||
ProcessSlotForLightClientStoreResult* = enum
|
||||
NoUpdate,
|
||||
UpdatedWithoutSupermajority,
|
||||
UpdatedWithoutFinalityProof
|
||||
|
||||
func process_slot_for_light_client_store*(
|
||||
store: var LightClientStore,
|
||||
current_slot: Slot) =
|
||||
current_slot: Slot): ProcessSlotForLightClientStoreResult {.discardable.} =
|
||||
var res = NoUpdate
|
||||
if store.best_valid_update.isSome and
|
||||
current_slot > store.finalized_header.slot + UPDATE_TIMEOUT:
|
||||
apply_light_client_update(store, store.best_valid_update.get)
|
||||
template sync_aggregate(): auto = store.best_valid_update.get.sync_aggregate
|
||||
template sync_committee_bits(): auto = sync_aggregate.sync_committee_bits
|
||||
let num_active_participants = countOnes(sync_committee_bits).uint64
|
||||
if apply_light_client_update(store, store.best_valid_update.get):
|
||||
if num_active_participants * 3 < static(sync_committee_bits.len * 2):
|
||||
res = UpdatedWithoutSupermajority
|
||||
else:
|
||||
res = UpdatedWithoutFinalityProof
|
||||
store.best_valid_update = none(altair.LightClientUpdate)
|
||||
res
|
||||
|
||||
# https://github.com/ethereum/consensus-specs/blob/vFuture/specs/altair/sync-protocol.md#process_light_client_update
|
||||
proc process_light_client_update*(
|
||||
|
@ -322,18 +353,20 @@ proc process_light_client_update*(
|
|||
current_slot: Slot,
|
||||
cfg: RuntimeConfig,
|
||||
genesis_validators_root: Eth2Digest,
|
||||
allowForceUpdate = true): bool =
|
||||
if not validate_light_client_update(
|
||||
store, update, current_slot, cfg, genesis_validators_root):
|
||||
return false
|
||||
allowForceUpdate = true): Result[void, BlockError] =
|
||||
? validate_light_client_update(
|
||||
store, update, current_slot, cfg, genesis_validators_root)
|
||||
|
||||
var didProgress = false
|
||||
|
||||
template sync_aggregate(): auto = update.sync_aggregate
|
||||
template sync_committee_bits(): auto = sync_aggregate.sync_committee_bits
|
||||
let num_active_participants = countOnes(sync_committee_bits).uint64
|
||||
|
||||
# Update the optimistic header
|
||||
apply_optimistic_light_client_header(
|
||||
store, update.attested_header, num_active_participants)
|
||||
if apply_optimistic_light_client_header(
|
||||
store, update.attested_header, num_active_participants):
|
||||
didProgress = true
|
||||
|
||||
# Update the best update in case we have to force-update to it
|
||||
# if the timeout elapses
|
||||
|
@ -346,19 +379,27 @@ proc process_light_client_update*(
|
|||
countOnes(best_sync_aggregate.sync_committee_bits).uint64
|
||||
if num_active_participants > best_active_participants:
|
||||
store.best_valid_update = some(update)
|
||||
didProgress = true
|
||||
|
||||
# Update finalized header
|
||||
if num_active_participants * 3 >= static(sync_committee_bits.len * 2) and
|
||||
update.is_finality_update:
|
||||
# Normal update through 2/3 threshold
|
||||
apply_light_client_update(store, update)
|
||||
if apply_light_client_update(store, update):
|
||||
didProgress = true
|
||||
store.best_valid_update = none(altair.LightClientUpdate)
|
||||
else:
|
||||
if allowForceUpdate:
|
||||
# Force-update to best update if the timeout elapsed
|
||||
process_slot_for_light_client_store(store, current_slot)
|
||||
case process_slot_for_light_client_store(store, current_slot)
|
||||
of UpdatedWithoutSupermajority, UpdatedWithoutFinalityProof:
|
||||
didProgress = true
|
||||
of NoUpdate: discard
|
||||
|
||||
true
|
||||
if not didProgress:
|
||||
err(BlockError.Duplicate)
|
||||
else:
|
||||
ok()
|
||||
|
||||
# https://github.com/ethereum/consensus-specs/blob/vFuture/specs/altair/sync-protocol.md#process_light_client_update
|
||||
proc process_optimistic_light_client_update*(
|
||||
|
@ -366,17 +407,22 @@ proc process_optimistic_light_client_update*(
|
|||
optimistic_update: OptimisticLightClientUpdate,
|
||||
current_slot: Slot,
|
||||
cfg: RuntimeConfig,
|
||||
genesis_validators_root: Eth2Digest): bool =
|
||||
if not validate_optimistic_light_client_update(
|
||||
store, optimistic_update, current_slot, cfg, genesis_validators_root):
|
||||
return false
|
||||
genesis_validators_root: Eth2Digest): Result[void, BlockError] =
|
||||
? validate_optimistic_light_client_update(
|
||||
store, optimistic_update, current_slot, cfg, genesis_validators_root)
|
||||
|
||||
var didProgress = false
|
||||
|
||||
template sync_aggregate(): auto = optimistic_update.sync_aggregate
|
||||
template sync_committee_bits(): auto = sync_aggregate.sync_committee_bits
|
||||
let num_active_participants = countOnes(sync_committee_bits).uint64
|
||||
|
||||
# Update the optimistic header
|
||||
apply_optimistic_light_client_header(
|
||||
store, optimistic_update.attested_header, num_active_participants)
|
||||
if apply_optimistic_light_client_header(
|
||||
store, optimistic_update.attested_header, num_active_participants):
|
||||
didProgress = true
|
||||
|
||||
true
|
||||
if not didProgress:
|
||||
err(BlockError.Duplicate)
|
||||
else:
|
||||
ok()
|
||||
|
|
|
@ -151,7 +151,7 @@ suite "EF - Altair - Unittests - Sync protocol" & preset():
|
|||
store, update, signature_slot, cfg, state.genesis_validators_root)
|
||||
|
||||
check:
|
||||
res
|
||||
res.isOk
|
||||
store.current_max_active_participants > 0
|
||||
store.optimistic_header == update.attested_header
|
||||
store.finalized_header == pre_store_finalized_header
|
||||
|
@ -214,7 +214,7 @@ suite "EF - Altair - Unittests - Sync protocol" & preset():
|
|||
store, update, signature_slot, cfg, state.genesis_validators_root)
|
||||
|
||||
check:
|
||||
res
|
||||
res.isOk
|
||||
store.current_max_active_participants > 0
|
||||
store.optimistic_header == update.attested_header
|
||||
store.finalized_header == pre_store_finalized_header
|
||||
|
@ -277,7 +277,7 @@ suite "EF - Altair - Unittests - Sync protocol" & preset():
|
|||
store, update, signature_slot, cfg, state.genesis_validators_root)
|
||||
|
||||
check:
|
||||
res
|
||||
res.isOk
|
||||
store.previous_max_active_participants > 0
|
||||
store.optimistic_header == update.attested_header
|
||||
store.finalized_header == update.attested_header
|
||||
|
@ -370,7 +370,7 @@ suite "EF - Altair - Unittests - Sync protocol" & preset():
|
|||
store, update, signature_slot, cfg, state.genesis_validators_root)
|
||||
|
||||
check:
|
||||
res
|
||||
res.isOk
|
||||
store.current_max_active_participants > 0
|
||||
store.optimistic_header == update.attested_header
|
||||
store.finalized_header == update.finalized_header
|
||||
|
|
|
@ -121,12 +121,12 @@ proc runTest(identifier: string) =
|
|||
let res = process_light_client_update(
|
||||
store, step.update, step.current_slot,
|
||||
cfg, genesis_validators_root)
|
||||
check res
|
||||
check res.isOk
|
||||
of TestStepKind.ProcessOptimisticUpdate:
|
||||
let res = process_optimistic_light_client_update(
|
||||
store, step.optimistic_update, step.current_slot,
|
||||
cfg, genesis_validators_root)
|
||||
check res
|
||||
check res.isOk
|
||||
|
||||
check:
|
||||
store.finalized_header == expected_finalized_header
|
||||
|
|
|
@ -128,7 +128,7 @@ suite "Light client" & preset():
|
|||
check bootstrap.isSome
|
||||
var storeRes = initialize_light_client_store(
|
||||
trusted_block_root, bootstrap.get)
|
||||
check storeRes.isSome
|
||||
check storeRes.isOk
|
||||
template store(): auto = storeRes.get
|
||||
|
||||
# Sync to latest sync committee period
|
||||
|
@ -146,7 +146,7 @@ suite "Light client" & preset():
|
|||
check:
|
||||
bestUpdate.isSome
|
||||
bestUpdate.get.finalized_header.slot.sync_committee_period == period
|
||||
res
|
||||
res.isOk
|
||||
store.finalized_header == bestUpdate.get.finalized_header
|
||||
inc numIterations
|
||||
if numIterations > 20: doAssert false # Avoid endless loop on test failure
|
||||
|
@ -159,7 +159,7 @@ suite "Light client" & preset():
|
|||
check:
|
||||
latestUpdate.isSome
|
||||
latestUpdate.get.attested_header.slot == dag.headState.blck.parent.slot
|
||||
res
|
||||
res.isOk
|
||||
store.finalized_header == latestUpdate.get.finalized_header
|
||||
store.optimistic_header == latestUpdate.get.attested_header
|
||||
|
||||
|
|
Loading…
Reference in New Issue