# beacon_chain
# Copyright (c) 2021-2022 Status Research & Development GmbH
# Licensed and distributed under either of
#   * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT).
#   * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0).
# at your option. This file may not be copied, modified, or distributed except according to those terms.

{.push raises: [Defect].}

# References to `vFuture` refer to the pre-release proposal of the libp2p based
# light client sync protocol. Conflicting release versions are not in use.
# https://github.com/ethereum/consensus-specs/pull/2802

import
  stew/[bitops2, objects],
  datatypes/altair,
  helpers

from ../consensus_object_pools/block_pools_types import BlockError
export block_pools_types.BlockError

# https://github.com/ethereum/consensus-specs/blob/vFuture/specs/altair/sync-protocol.md#initialize_light_client_store
func initialize_light_client_store*(
    trusted_block_root: Eth2Digest,
    bootstrap: altair.LightClientBootstrap
): Result[LightClientStore, BlockError] =
  if hash_tree_root(bootstrap.header) != trusted_block_root:
    return err(BlockError.Invalid)

  if not is_valid_merkle_branch(
      hash_tree_root(bootstrap.current_sync_committee),
      bootstrap.current_sync_committee_branch,
      log2trunc(altair.CURRENT_SYNC_COMMITTEE_INDEX),
      get_subtree_index(altair.CURRENT_SYNC_COMMITTEE_INDEX),
      bootstrap.header.state_root):
    return err(BlockError.Invalid)

  return ok(LightClientStore(
    finalized_header: bootstrap.header,
    current_sync_committee: bootstrap.current_sync_committee,
    optimistic_header: bootstrap.header))

# https://github.com/ethereum/consensus-specs/blob/vFuture/specs/altair/sync-protocol.md#validate_light_client_update
proc validate_light_client_update*(
    store: LightClientStore,
    update: SomeLightClientUpdate,
    current_slot: Slot,
    cfg: RuntimeConfig,
    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 err(BlockError.Invalid)

  # Verify update does not skip a sync committee period
  when update is SomeLightClientUpdateWithFinality:
    if update.attested_header.slot < update.finalized_header.slot:
      return err(BlockError.Invalid)
  if update.signature_slot <= update.attested_header.slot:
    return err(BlockError.Invalid)
  if current_slot < update.signature_slot:
    return err(BlockError.UnviableFork)
  let
    store_period = store.finalized_header.slot.sync_committee_period
    signature_period = update.signature_slot.sync_committee_period
    is_next_sync_committee_known = store.is_next_sync_committee_known
  if is_next_sync_committee_known:
    if signature_period notin [store_period, store_period + 1]:
      return err(BlockError.MissingParent)
  else:
    if signature_period != store_period:
      return err(BlockError.MissingParent)

  # Verify update is relevant
  let attested_period = update.attested_header.slot.sync_committee_period
  when update is SomeLightClientUpdateWithSyncCommittee:
    let is_sync_committee_update = update.is_sync_committee_update
  if update.attested_header.slot <= store.finalized_header.slot:
    when update is SomeLightClientUpdateWithSyncCommittee:
      if is_next_sync_committee_known:
        return err(BlockError.Duplicate)
      if attested_period != store_period or not is_sync_committee_update:
        return err(BlockError.Duplicate)
    else:
      return err(BlockError.Duplicate)

  # Verify that the `finalized_header`, if present, actually is the
  # finalized header saved in the state of the `attested_header`
  when update is SomeLightClientUpdateWithFinality:
    if not update.is_finality_update:
      if not update.finalized_header.isZeroMemory:
        return err(BlockError.Invalid)
    else:
      var finalized_root {.noinit.}: Eth2Digest
      if update.finalized_header.slot != GENESIS_SLOT:
        finalized_root = hash_tree_root(update.finalized_header)
      elif update.finalized_header.isZeroMemory:
        finalized_root.reset()
      else:
        return err(BlockError.Invalid)
      if not is_valid_merkle_branch(
          finalized_root,
          update.finality_branch,
          log2trunc(altair.FINALIZED_ROOT_INDEX),
          get_subtree_index(altair.FINALIZED_ROOT_INDEX),
          update.attested_header.state_root):
        return err(BlockError.Invalid)

  # Verify that the `next_sync_committee`, if present, actually is the
  # next sync committee saved in the state of the `attested_header`
  when update is SomeLightClientUpdateWithSyncCommittee:
    if not is_sync_committee_update:
      if not update.next_sync_committee.isZeroMemory:
        return err(BlockError.Invalid)
    else:
      if attested_period == store_period and is_next_sync_committee_known:
        if update.next_sync_committee != store.next_sync_committee:
          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),
          update.attested_header.state_root):
        return err(BlockError.Invalid)

  # Verify sync committee aggregate signature
  let sync_committee =
    if signature_period == store_period:
      unsafeAddr store.current_sync_committee
    else:
      unsafeAddr store.next_sync_committee
  var participant_pubkeys =
    newSeqOfCap[ValidatorPubKey](num_active_participants)
  for idx, bit in sync_aggregate.sync_committee_bits:
    if bit:
      participant_pubkeys.add(sync_committee.pubkeys.data[idx])
  let
    fork_version = cfg.forkVersionAtEpoch(update.signature_slot.epoch)
    domain = compute_domain(
      DOMAIN_SYNC_COMMITTEE, fork_version, genesis_validators_root)
    signing_root = compute_signing_root(update.attested_header, domain)
  if not blsFastAggregateVerify(
      participant_pubkeys, signing_root.data,
      sync_aggregate.sync_committee_signature):
    return err(BlockError.UnviableFork)

  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: SomeLightClientUpdate): bool =
  var didProgress = false
  let
    store_period = store.finalized_header.slot.sync_committee_period
    finalized_period = update.finalized_header.slot.sync_committee_period
  if not store.is_next_sync_committee_known:
    assert finalized_period == store_period
    when update is SomeLightClientUpdateWithSyncCommittee:
      store.next_sync_committee = update.next_sync_committee
      if store.is_next_sync_committee_known:
        didProgress = true
  elif finalized_period == store_period + 1:
    store.current_sync_committee = store.next_sync_committee
    when update is SomeLightClientUpdateWithSyncCommittee:
      store.next_sync_committee = update.next_sync_committee
    else:
      store.next_sync_committee.reset()
    store.previous_max_active_participants =
      store.current_max_active_participants
    store.current_max_active_participants = 0
    didProgress = true
  if update.finalized_header.slot > store.finalized_header.slot:
    store.finalized_header = update.finalized_header
    if store.finalized_header.slot > store.optimistic_header.slot:
      store.optimistic_header = store.finalized_header
    didProgress = true
  didProgress

# https://github.com/ethereum/consensus-specs/blob/vFuture/specs/altair/sync-protocol.md#try_light_client_store_force_update
type
  ForceUpdateResult* = enum
    NoUpdate,
    DidUpdateWithoutSupermajority,
    DidUpdateWithoutFinality

func try_light_client_store_force_update*(
    store: var LightClientStore,
    current_slot: Slot): ForceUpdateResult {.discardable.} =
  var res = NoUpdate
  if store.best_valid_update.isSome and
      current_slot > store.finalized_header.slot + UPDATE_TIMEOUT:
    # Forced best update when the update timeout has elapsed
    template best(): auto = store.best_valid_update.get
    if best.finalized_header.slot <= store.finalized_header.slot:
      best.finalized_header = best.attested_header
    if apply_light_client_update(store, best):
      template sync_aggregate(): auto = best.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 * 3 < static(sync_committee_bits.len * 2):
        res = DidUpdateWithoutSupermajority
      else:
        res = DidUpdateWithoutFinality
    store.best_valid_update.reset()
  res

# https://github.com/ethereum/consensus-specs/blob/vFuture/specs/altair/sync-protocol.md#process_light_client_update
proc process_light_client_update*(
    store: var LightClientStore,
    update: SomeLightClientUpdate,
    current_slot: Slot,
    cfg: RuntimeConfig,
    genesis_validators_root: Eth2Digest): Result[void, BlockError] =
  ? validate_light_client_update(
    store, update, current_slot, cfg, genesis_validators_root)

  var didProgress = false

  # Update the best update in case we have to force-update to it
  # if the timeout elapses
  if store.best_valid_update.isNone or
      is_better_update(update, store.best_valid_update.get):
    store.best_valid_update = some(update.toFull)
    didProgress = true

  # Track the maximum number of active participants in the committee signatures
  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 > store.current_max_active_participants:
    store.current_max_active_participants = num_active_participants

  # Update the optimistic header
  if num_active_participants > get_safety_threshold(store) and
      update.attested_header.slot > store.optimistic_header.slot:
    store.optimistic_header = update.attested_header
    didProgress = true

  # Update finalized header
  when update is SomeLightClientUpdateWithFinality:
    if num_active_participants * 3 >= static(sync_committee_bits.len * 2):
      var improvesFinality =
        update.finalized_header.slot > store.finalized_header.slot
      when update is SomeLightClientUpdateWithSyncCommittee:
        if not improvesFinality and not store.is_next_sync_committee_known:
          improvesFinality =
            update.is_sync_committee_update and update.is_finality_update and
            update.finalized_header.slot.sync_committee_period ==
            update.attested_header.slot.sync_committee_period
      if improvesFinality:
        # Normal update through 2/3 threshold
        if apply_light_client_update(store, update):
          didProgress = true
        store.best_valid_update.reset()

  if not didProgress:
    return err(BlockError.Duplicate)
  ok()