nimbus-eth2/beacon_chain/consensus_object_pools/block_clearance_light_clien...

307 lines
10 KiB
Nim

# beacon_chain
# Copyright (c) 2019-2022 Status Research & Development GmbH
# Licensed and distributed under either of
# * MIT license (license terms in the root directory or at http://opensource.org/licenses/MIT).
# * Apache v2 license (license terms in the root directory or at http://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].}
import
std/[deques, math],
chronicles,
../spec/forks,
../beacon_chain_db,
./block_pools_types
export forks, block_pools_types
logScope:
topics = "clearance"
# Clearance (light client)
# ---------------------------------------------
#
# This module validates blocks obtained using the light client sync protocol.
# Those blocks are considered trusted by delegating the full verification to a
# supermajority (> 2/3) of the corresponding sync committee (512 members).
# The validated blocks are downloaded in backwards order into a `deque`.
#
# If the sync committee is trusted, expensive verification already done by the
# sync committee may be skipped:
# - BLS signatures (except the outer block signature not covered by `root`)
# - Verifying whether the state transition function applies
# - `ExecutionPayload` verification
# - `state_root` computation and verification
type LCBlocks* = object
maxSlots: int # max cache.len
cache: Deque[ref ForkedMsgTrustedSignedBeaconBlock] # by slots descending
headSlot: Slot # matches cache[0].slot once block is downloaded
backfill: BeaconBlockSummary # next expected block
finalizedBid: BlockId
func initLCBlocks*(maxSlots: int): LCBlocks =
LCBlocks(
maxSlots: maxSlots,
cache: initDeque[ref ForkedMsgTrustedSignedBeaconBlock](
nextPowerOfTwo(maxSlots)),
headSlot: FAR_FUTURE_SLOT)
func getHeadSlot*(lcBlocks: LCBlocks): Slot =
lcBlocks.headSlot
func getFinalizedSlot*(lcBlocks: LCBlocks): Slot =
lcBlocks.finalizedBid.slot
func getFrontfillSlot*(lcBlocks: LCBlocks): Slot =
lcBlocks.headSlot + 1 - lcBlocks.cache.lenu64
func getBackfillSlot*(lcBlocks: LCBlocks): Slot =
if lcBlocks.backfill.slot != FAR_FUTURE_SLOT:
max(lcBlocks.backfill.slot, lcBlocks.getFrontfillSlot())
else:
lcBlocks.headSlot + 1
func getBackfillRoot*(lcBlocks: LCBlocks): Option[Eth2Digest] =
if lcBlocks.headSlot == FAR_FUTURE_SLOT:
none(Eth2Digest)
elif lcBlocks.backfill.slot < lcBlocks.getFrontfillSlot():
none(Eth2Digest)
else:
some lcBlocks.backfill.parent_root
func getCacheIndex(lcBlocks: LCBlocks, slot: Slot): uint64 =
if slot < lcBlocks.headSlot and lcBlocks.headSlot != FAR_FUTURE_SLOT:
lcBlocks.headSlot - slot
else:
0
func getBlockAtSlot*(
lcBlocks: LCBlocks, slot: Slot): Opt[ForkedMsgTrustedSignedBeaconBlock] =
if slot < lcBlocks.backfill.slot:
return err()
let index = lcBlocks.getCacheIndex(slot)
if index >= lcBlocks.cache.lenu64:
return err()
let existing = lcBlocks.cache[index]
if existing == nil:
return err()
return ok existing[]
func getLatestBlockThroughSlot*(
lcBlocks: LCBlocks, maxSlot: Slot): Opt[ForkedMsgTrustedSignedBeaconBlock] =
if maxSlot < lcBlocks.backfill.slot:
return err()
let startIndex = lcBlocks.getCacheIndex(maxSlot)
for i in startIndex ..< lcBlocks.cache.lenu64:
let blck = lcBlocks.cache[i]
if blck != nil:
return ok blck[]
err()
proc processBlock(
lcBlocks: var LCBlocks,
signedBlock: ForkySignedBeaconBlock,
isNewBlock = true): Result[void, BlockError] =
logScope:
headSlot = lcBlocks.headSlot
backfill = (lcBlocks.backfill.slot, shortLog(lcBlocks.backfill.parent_root))
blck = shortLog(signedBlock.toBlockId())
let startTick = Moment.now()
template blck(): untyped = signedBlock.message
template blockRoot(): untyped = signedBlock.root
if blck.slot > lcBlocks.headSlot:
debug "LC block too new"
return err(BlockError.Duplicate)
# Handle head block
if lcBlocks.backfill.slot == FAR_FUTURE_SLOT:
if blck.slot < lcBlocks.headSlot:
if isNewBlock:
debug "Head LC block skipped"
return err(BlockError.MissingParent)
if blockRoot != lcBlocks.backfill.parent_root:
if isNewBlock:
debug "Head LC block from unviable fork"
return err(BlockError.UnviableFork)
const index = 0'u64 # Head block is always mapped to index 0 (never empty)
if index >= lcBlocks.cache.lenu64:
lcBlocks.backfill.slot = blck.slot
debug "Final head LC block"
return ok()
lcBlocks.backfill = blck.toBeaconBlockSummary()
let existing = lcBlocks.cache[index]
if existing != nil:
if blockRoot == existing[].root:
if isNewBlock:
debug "Head LC block already known"
return ok()
warn "Head LC block reorg", existing = existing[]
lcBlocks.cache[index] =
newClone ForkedMsgTrustedSignedBeaconBlock.init(
signedBlock.asMsgTrusted())
debug "Head LC block cached", cacheDur = Moment.now() - startTick
return ok()
# Handle duplicate block
if blck.slot >= lcBlocks.getBackfillSlot():
let index = lcBlocks.getCacheIndex(blck.slot)
doAssert index < lcBlocks.cache.lenu64
let existing = lcBlocks.cache[index]
if existing == nil:
debug "Duplicate LC block for empty slot"
return err(BlockError.UnviableFork)
doAssert blck.slot == existing[].slot
if blockRoot != existing[].root:
debug "Duplicate LC block from unviable fork"
return err(BlockError.UnviableFork)
debug "Duplicate LC block"
return err(BlockError.Duplicate)
# Handle new block
if blck.slot > lcBlocks.backfill.slot:
debug "LC block for empty slot"
return err(BlockError.UnviableFork)
if blockRoot != lcBlocks.backfill.parent_root:
if blck.slot == lcBlocks.backfill.slot:
debug "Final LC block from unviable fork"
return err(BlockError.UnviableFork)
if isNewBlock:
debug "LC block does not match expected backfill root"
return err(BlockError.MissingParent)
if blck.slot == lcBlocks.backfill.slot:
debug "Duplicate final LC block"
return err(BlockError.Duplicate)
let
previousIndex = lcBlocks.getCacheIndex(lcBlocks.backfill.slot)
index = lcBlocks.getCacheIndex(blck.slot)
for i in previousIndex + 1 ..< min(index, lcBlocks.cache.lenu64):
let existing = lcBlocks.cache[i]
if existing != nil:
warn "LC block reorg to empty", existing = existing[]
lcBlocks.cache[i] = nil
if index >= lcBlocks.cache.lenu64:
lcBlocks.backfill.slot = blck.slot
debug "Final LC block"
return ok()
lcBlocks.backfill = blck.toBeaconBlockSummary()
let existing = lcBlocks.cache[index]
if existing != nil:
if blockRoot == existing[].root:
if isNewBlock:
debug "LC block already known"
return ok()
warn "LC block reorg", existing = existing[]
lcBlocks.cache[index] =
newClone ForkedMsgTrustedSignedBeaconBlock.init(
signedBlock.asMsgTrusted())
debug "LC block cached", cacheDur = Moment.now() - startTick
ok()
proc setHeadBid*(lcBlocks: var LCBlocks, headBid: BlockId) =
debug "New LC head block", headBid
if lcBlocks.maxSlots == 0:
discard
elif lcBlocks.headSlot == FAR_FUTURE_SLOT or
headBid.slot >= lcBlocks.headSlot + lcBlocks.maxSlots.uint64 or (
lcBlocks.headSlot - lcBlocks.cache.lenu64 != FAR_FUTURE_SLOT and
headBid.slot <= lcBlocks.headSlot - lcBlocks.cache.lenu64):
lcBlocks.cache.clear()
for i in 0 ..< min(headBid.slot + 1, lcBlocks.maxSlots.Slot).int:
lcBlocks.cache.addLast(nil)
elif headBid.slot > lcBlocks.headSlot:
let numNewSlots = headBid.slot - lcBlocks.headSlot
doAssert numNewSlots <= lcBlocks.maxSlots.uint64
if numNewSlots > lcBlocks.maxSlots.uint64 - lcBlocks.cache.lenu64:
lcBlocks.cache.shrink(
fromLast = numNewSlots.int + lcBlocks.cache.len - lcBlocks.maxSlots)
for i in 0 ..< numNewSlots:
lcBlocks.cache.addFirst(nil)
else:
lcBlocks.cache.shrink(fromFirst = (lcBlocks.headSlot - headBid.slot).int)
let startLen = lcBlocks.cache.len
for i in startLen ..< min(headBid.slot + 1, lcBlocks.maxSlots.Slot).int:
lcBlocks.cache.addLast(nil)
lcBlocks.headSlot = headBid.slot
lcBlocks.backfill.slot = FAR_FUTURE_SLOT
lcBlocks.backfill.parent_root = headBid.root
for i in 0 ..< lcBlocks.cache.len:
let existing = lcBlocks.cache[i]
if existing != nil:
let res =
withBlck(existing[]):
lcBlocks.processBlock(blck.asSigned(), isNewBlock = false)
if res.isErr:
break
proc setFinalizedBid*(lcBlocks: var LCBlocks, finalizedBid: BlockId) =
if finalizedBid.slot > lcBlocks.headSlot:
lcBlocks.setHeadBid(finalizedBid)
if finalizedBid != lcBlocks.finalizedBid:
debug "New LC finalized block", finalizedBid
lcBlocks.finalizedBid = finalizedBid
if finalizedBid.slot <= lcBlocks.headSlot and
finalizedBid.slot >= lcBlocks.getBackfillSlot:
let index = lcBlocks.getCacheIndex(finalizedBid.slot)
doAssert index < lcBlocks.cache.lenu64
let existing = lcBlocks.cache[index]
if existing == nil or finalizedBid.root != existing[].root:
if existing != nil:
error "Finalized LC block reorg", existing = existing[]
else:
error "Finalized LC block reorg"
lcBlocks.cache.clear()
lcBlocks.backfill.reset()
lcBlocks.headSlot.reset()
lcBlocks.setHeadBid(finalizedBid)
proc addBlock*(
lcBlocks: var LCBlocks,
signedBlock: ForkedSignedBeaconBlock): Result[void, BlockError] =
let oldBackfillSlot = lcBlocks.getBackfillSlot()
withBlck(signedBlock):
? lcBlocks.processBlock(blck)
if oldBackfillSlot > lcBlocks.finalizedBid.slot and
lcBlocks.getBackfillSlot() <= lcBlocks.finalizedBid.slot:
if signedBlock.slot != lcBlocks.finalizedBid.slot or
signedBlock.root != lcBlocks.finalizedBid.root:
error "LC finalized block from unviable fork"
lcBlocks.setFinalizedBid(lcBlocks.finalizedBid)
return err(BlockError.UnviableFork)
let slot = signedBlock.slot
for i in lcBlocks.getCacheIndex(slot) + 1 ..< lcBlocks.cache.lenu64:
let existing = lcBlocks.cache[i]
if existing != nil:
let res =
withBlck(existing[]):
lcBlocks.processBlock(blck.asSigned(), isNewBlock = false)
if res.isErr:
break
ok()