diff --git a/AllTests-mainnet.md b/AllTests-mainnet.md index 7be6a5536..b67d018ee 100644 --- a/AllTests-mainnet.md +++ b/AllTests-mainnet.md @@ -54,10 +54,11 @@ OK: 7/7 Fail: 0/7 Skip: 0/7 OK: 5/5 Fail: 0/5 Skip: 0/5 ## BlockRef and helpers [Preset: mainnet] ```diff ++ epochAncestor sanity [Preset: mainnet] OK + get_ancestor sanity [Preset: mainnet] OK + isAncestorOf sanity [Preset: mainnet] OK ``` -OK: 2/2 Fail: 0/2 Skip: 0/2 +OK: 3/3 Fail: 0/3 Skip: 0/3 ## BlockSlot and helpers [Preset: mainnet] ```diff + atSlot sanity [Preset: mainnet] OK @@ -247,4 +248,4 @@ OK: 1/1 Fail: 0/1 Skip: 0/1 OK: 1/1 Fail: 0/1 Skip: 0/1 ---TOTAL--- -OK: 134/141 Fail: 0/141 Skip: 7/141 +OK: 135/142 Fail: 0/142 Skip: 7/142 diff --git a/beacon_chain/beacon_node.nim b/beacon_chain/beacon_node.nim index e4a8183f4..c7b587312 100644 --- a/beacon_chain/beacon_node.nim +++ b/beacon_chain/beacon_node.nim @@ -351,9 +351,8 @@ proc storeBlock( {.gcsafe.}: # TODO: fork choice and quarantine should sync via messages instead of callbacks let blck = node.chainDag.addRawBlock(node.quarantine, signedBlock) do ( blckRef: BlockRef, signedBlock: SignedBeaconBlock, - state: HashedBeaconState): + epochRef: EpochRef, state: HashedBeaconState): # Callback add to fork choice if valid - let epochRef = getEpochInfo(blckRef, state.data) node.attestationPool.addForkChoice( epochRef, blckRef, signedBlock.message, node.beaconClock.now().slotOrZero()) diff --git a/beacon_chain/block_pools/block_pools_types.nim b/beacon_chain/block_pools/block_pools_types.nim index 0a7d0754f..ab83d531e 100644 --- a/beacon_chain/block_pools/block_pools_types.nim +++ b/beacon_chain/block_pools/block_pools_types.nim @@ -157,12 +157,15 @@ type slot*: Slot # TODO could calculate this by walking to root, but.. - epochsInfo*: seq[EpochRef] ##\ + epochRefs*: seq[EpochRef] ##\ ## Cached information about the epochs starting at this block. ## Could be multiple, since blocks could skip slots, but usually, not many ## Even if competing forks happen later during this epoch, potential empty - ## slots beforehand must all be from this fork. getEpochInfo() is the only - ## supported way of accesssing these. + ## slots beforehand must all be from this fork. find/getEpochRef() are the + ## only supported way of accesssing these. + ## In particular, epoch refs are only stored with the last block of the + ## parent epoch - this way, it's easy to find them from any block in the + ## epoch - including when there are forks that skip the epoch slot. BlockData* = object ## Body and graph in one @@ -190,7 +193,7 @@ type OnBlockAdded* = proc( blckRef: BlockRef, blck: SignedBeaconBlock, - state: HashedBeaconState) {.raises: [Defect], gcsafe.} + epochRef: EpochRef, state: HashedBeaconState) {.raises: [Defect], gcsafe.} template validator_keys*(e: EpochRef): untyped = e.validator_key_store[1][] diff --git a/beacon_chain/block_pools/chain_dag.nim b/beacon_chain/block_pools/chain_dag.nim index 1d6a03917..49a9d8518 100644 --- a/beacon_chain/block_pools/chain_dag.nim +++ b/beacon_chain/block_pools/chain_dag.nim @@ -33,21 +33,24 @@ proc putBlock*( dag.db.putBlock(signedBlock) proc updateStateData*( - dag: ChainDAGRef, state: var StateData, bs: BlockSlot) {.gcsafe.} + dag: ChainDAGRef, state: var StateData, bs: BlockSlot, + cache: var StateCache) {.gcsafe.} template withState*( - dag: ChainDAGRef, cache: var StateData, blockSlot: BlockSlot, body: untyped): untyped = - ## Helper template that updates state to a particular BlockSlot - usage of - ## cache is unsafe outside of block. - ## TODO async transformations will lead to a race where cache gets updated + dag: ChainDAGRef, stateData: var StateData, blockSlot: BlockSlot, + body: untyped): untyped = + ## Helper template that updates stateData to a particular BlockSlot - usage of + ## stateData is unsafe outside of block. + ## TODO async transformations will lead to a race where stateData gets updated ## while waiting for future to complete - catch this here somehow? - updateStateData(dag, cache, blockSlot) + var cache {.inject.} = blockSlot.blck.getStateCache(blockSlot.slot.epoch()) + updateStateData(dag, stateData, blockSlot, cache) - template hashedState(): HashedBeaconState {.inject, used.} = cache.data - template state(): BeaconState {.inject, used.} = cache.data.data - template blck(): BlockRef {.inject, used.} = cache.blck - template root(): Eth2Digest {.inject, used.} = cache.data.root + template hashedState(): HashedBeaconState {.inject, used.} = stateData.data + template state(): BeaconState {.inject, used.} = stateData.data.data + template blck(): BlockRef {.inject, used.} = stateData.blck + template root(): Eth2Digest {.inject, used.} = stateData.data.root body @@ -74,7 +77,9 @@ func get_effective_balances*(state: BeaconState): seq[Gwei] = if validator.is_active_validator(epoch): result[i] = validator.effective_balance -proc init*(T: type EpochRef, state: BeaconState, cache: var StateCache, prevEpoch: EpochRef): T = +proc init*( + T: type EpochRef, state: BeaconState, cache: var StateCache, + prevEpoch: EpochRef): T = let epoch = state.get_current_epoch() epochRef = EpochRef( @@ -90,13 +95,35 @@ proc init*(T: type EpochRef, state: BeaconState, cache: var StateCache, prevEpoc epochRef.beacon_proposers[i] = some((idx.get(), state.validators[idx.get].pubkey)) - if prevEpoch != nil and - (prevEpoch.validator_key_store[0] == hash_tree_root(state.validators)): - # Validator sets typically don't change between epochs - a more efficient - # scheme could be devised where parts of the validator key set is reused - # between epochs because in a single history, the validator set only - # grows - this however is a trivially implementable compromise. - epochRef.validator_key_store = prevEpoch.validator_key_store + # Validator sets typically don't change between epochs - a more efficient + # scheme could be devised where parts of the validator key set is reused + # between epochs because in a single history, the validator set only + # grows - this however is a trivially implementable compromise. + + # The validators root is cached in the state, so we can quickly compare + # it to see if it remains unchanged - effective balances in the validator + # information may however result in a different root, even if the public + # keys are the same + + let validators_root = hash_tree_root(state.validators) + + template sameKeys(a: openArray[ValidatorPubKey], b: openArray[Validator]): bool = + if a.len != b.len: + false + else: + block: + var ret = true + for i, key in a: + if key != b[i].pubkey: + ret = false + break + ret + + if prevEpoch != nil and ( + prevEpoch.validator_key_store[0] == hash_tree_root(state.validators) or + sameKeys(prevEpoch.validator_key_store[1][], state.validators.asSeq)): + epochRef.validator_key_store = + (validators_root, prevEpoch.validator_key_store[1]) else: epochRef.validator_key_store = ( hash_tree_root(state.validators), @@ -175,70 +202,51 @@ func atEpochEnd*(blck: BlockRef, epoch: Epoch): BlockSlot = ## Return the BlockSlot corresponding to the last slot in the given epoch atSlot(blck, (epoch + 1).compute_start_slot_at_epoch - 1) -proc getEpochInfo*(blck: BlockRef, state: BeaconState, cache: var StateCache): EpochRef = - # This is the only intended mechanism by which to get an EpochRef - let - state_epoch = state.get_current_epoch() - matching_epochinfo = blck.epochsInfo.filterIt(it.epoch == state_epoch) +func epochAncestor*(blck: BlockRef, epoch: Epoch): BlockSlot = + ## The state transition works by storing information from blocks in a + ## "working" area until the epoch transition, then batching work collected + ## during the epoch. Thus, last block in the ancestor epochs is the block + ## that has an impact on epoch currently considered. + ## + ## This function returns a BlockSlot pointing to that epoch boundary, ie the + ## boundary where the last block has been applied to the state and epoch + ## processing has been done - we will store epoch caches in that particular + ## block so that any block in the dag that needs it can find it easily. In + ## particular, if empty slot processing is done, there may be multiple epoch + ## caches found there. + var blck = blck + while blck.slot.epoch >= epoch and not blck.parent.isNil: + blck = blck.parent - if matching_epochinfo.len == 0: - # When creating an epochref, we can somtimes reuse some of the information - # from an earlier epoch in the same history - if we're processing slots - # only, the epochref of an earlier slot of the same block will be the most - # similar + blck.atEpochStart(epoch) - var prevEpochRefs = blck.epochsInfo.filterIt(it.epoch < state_epoch) - var prevEpochRef: EpochRef = nil # nil ok - if prevEpochRefs.len > 0: - prevEpochRef = prevEpochRefs[^1] - elif state_epoch > 0: - let parent = blck.atEpochEnd((state_epoch - 1)) - if parent.blck != nil and parent.blck.epochsInfo.len > 0: - prevEpochRef = parent.blck.epochsInfo[0] +proc getStateCache*(blck: BlockRef, epoch: Epoch): StateCache = + # When creating a state cache, we want the current and the previous epoch + # information to be preloaded as both of these are used in state transition + # functions - let epochInfo = EpochRef.init(state, cache, prevEpochRef) + var res = StateCache() + template load(e: Epoch) = + let ancestor = blck.epochAncestor(epoch) + for epochRef in ancestor.blck.epochRefs: + if epochRef.epoch == e: + res.shuffled_active_validator_indices[epochRef.epoch] = + epochRef.shuffled_active_validator_indices - # Don't use BlockRef caching as far as the epoch where the active - # validator indices can diverge. - if (compute_activation_exit_epoch(blck.slot.compute_epoch_at_slot) > - state_epoch): - blck.epochsInfo.add(epochInfo) - trace "chain_dag.getEpochInfo: back-filling parent.epochInfo", - state_slot = state.slot - epochInfo - elif matching_epochinfo.len == 1: - matching_epochinfo[0] - else: - raiseAssert "multiple EpochRefs per epoch per BlockRef invalid" + if epochRef.epoch == epoch: + for i, idx in epochRef.beacon_proposers: + res.beacon_proposer_indices[ + epoch.compute_start_slot_at_epoch + i] = + if idx.isSome: some(idx.get()[0]) else: none(ValidatorIndex) -proc getEpochInfo*(blck: BlockRef, state: BeaconState): EpochRef = - # This is the only intended mechanism by which to get an EpochRef - var cache = StateCache() - getEpochInfo(blck, state, cache) + break -proc getEpochCache*(blck: BlockRef, state: BeaconState): StateCache = - var tmp = StateCache() # TODO Resolve circular init issue - let epochInfo = getEpochInfo(blck, state, tmp) - if epochInfo.epoch > 0: - # When doing state transitioning, both the current and previous epochs are - # useful from a cache perspective since attestations may come from either - - # we'll use the last slot from the epoch because it is more likely to - # be filled in already, compared to the first slot where the block might - # be from the epoch before. - let - prevEpochBlck = blck.atEpochEnd(epochInfo.epoch - 1).blck + load(epoch) - for ei in prevEpochBlck.epochsInfo: - if ei.epoch == epochInfo.epoch - 1: - result.shuffled_active_validator_indices[ei.epoch] = - ei.shuffled_active_validator_indices + if epoch > 0: + load(epoch - 1) - result.shuffled_active_validator_indices[state.get_current_epoch()] = - epochInfo.shuffled_active_validator_indices - for i, idx in epochInfo.beacon_proposers: - result.beacon_proposer_indices[ - epochInfo.epoch.compute_start_slot_at_epoch + i] = - if idx.isSome: some(idx.get()[0]) else: none(ValidatorIndex) + res func init(T: type BlockRef, root: Eth2Digest, slot: Slot): BlockRef = BlockRef( @@ -299,24 +307,31 @@ proc init*(T: type ChainDAGRef, headRef = tailRef var - bs = headRef.atSlot(headRef.slot) + cur = headRef.atSlot(headRef.slot) tmpState = (ref StateData)() # Now that we have a head block, we need to find the most recent state that # we have saved in the database - while bs.blck != nil: - let root = db.getStateRoot(bs.blck.root, bs.slot) + while cur.blck != nil: + let root = db.getStateRoot(cur.blck.root, cur.slot) if root.isSome(): # TODO load StateData from BeaconChainDB # We save state root separately for empty slots which means we might # sometimes not find a state even though we saved its state root if db.getState(root.get(), tmpState.data.data, noRollback): tmpState.data.root = root.get() - tmpState.blck = bs.blck + tmpState.blck = cur.blck break - bs = bs.parent() # Iterate slot by slot in case there's a gap! + if cur.blck.parent != nil and + cur.blck.slot.epoch != epoch(cur.blck.parent.slot): + # We store the state of the parent block with the epoch processing applied + # in the database! + cur = cur.blck.parent.atEpochStart(cur.blck.slot.epoch) + else: + # Moves back slot by slot, in case a state for an empty slot was saved + cur = cur.parent if tmpState.blck == nil: warn "No state found in head history, database corrupt?" @@ -324,19 +339,10 @@ proc init*(T: type ChainDAGRef, # would be a good recovery model? raiseAssert "No state found in head history, database corrupt?" - # We presently save states on the epoch boundary - it means that the latest - # state we loaded might be older than head block - nonetheless, it will be - # from the same epoch as the head, thus the finalized and justified slots are - # the same - these only change on epoch boundaries. - let - finalizedHead = headRef.atEpochStart( - tmpState.data.data.finalized_checkpoint.epoch) - let res = ChainDAGRef( blocks: blocks, tail: tailRef, head: headRef, - finalizedHead: finalizedHead, db: db, heads: @[headRef], headState: tmpState[], @@ -351,36 +357,50 @@ proc init*(T: type ChainDAGRef, doAssert res.updateFlags in [{}, {verifyFinalization}] - res.updateStateData(res.headState, headRef.atSlot(headRef.slot)) + var cache: StateCache + res.updateStateData(res.headState, headRef.atSlot(headRef.slot), cache) + # We presently save states on the epoch boundary - it means that the latest + # state we loaded might be older than head block - nonetheless, it will be + # from the same epoch as the head, thus the finalized and justified slots are + # the same - these only change on epoch boundaries. + res.finalizedHead = headRef.atEpochStart( + res.headState.data.data.finalized_checkpoint.epoch) + res.clearanceState = res.headState info "Block dag initialized", head = shortLog(headRef), - finalizedHead = shortLog(finalizedHead), + finalizedHead = shortLog(res.finalizedHead), tail = shortLog(tailRef), totalBlocks = blocks.len res -proc getEpochRef*(dag: ChainDAGRef, blck: BlockRef, epoch: Epoch): EpochRef = - var bs = blck.atEpochEnd(epoch) +proc findEpochRef*(blck: BlockRef, epoch: Epoch): EpochRef = # may return nil! + let ancestor = blck.epochAncestor(epoch) + for epochRef in ancestor.blck.epochRefs: + if epochRef.epoch == epoch: + return epochRef - while true: - # Any block from within the same epoch will carry the same epochinfo, so - # we start at the most recent one - for e in bs.blck.epochsInfo: - if e.epoch == epoch: - beacon_state_data_cache_hits.inc - return e - if bs.slot == epoch.compute_start_slot_at_epoch: - break - bs = bs.parent +proc getEpochRef*(dag: ChainDAGRef, blck: BlockRef, epoch: Epoch): EpochRef = + let epochRef = blck.findEpochRef(epoch) + if epochRef != nil: + beacon_state_data_cache_hits.inc + return epochRef beacon_state_data_cache_misses.inc - dag.withState(dag.tmpState, bs): - var cache = StateCache() - getEpochInfo(blck, state, cache) + let + ancestor = blck.epochAncestor(epoch) + + dag.withState(dag.tmpState, ancestor): + let + prevEpochRef = blck.findEpochRef(epoch - 1) + newEpochRef = EpochRef.init(state, cache, prevEpochRef) + + # TODO consider constraining the number of epochrefs per state + ancestor.blck.epochRefs.add newEpochRef + newEpochRef proc getState( dag: ChainDAGRef, state: var StateData, stateRoot: Eth2Digest, @@ -412,6 +432,10 @@ proc getState(dag: ChainDAGRef, state: var StateData, bs: BlockSlot): bool = if not bs.slot.isEpoch: return false # We only ever save epoch states - no need to hit database + # TODO earlier versions would store the epoch state with a the epoch block + # applied - we generally shouldn't hit the database for such states but + # will do so in a transitionary upgrade period! + if (let stateRoot = dag.db.getStateRoot(bs.blck.root, bs.slot); stateRoot.isSome()): return dag.getState(state, stateRoot.get(), bs.blck) @@ -425,10 +449,15 @@ proc putState*(dag: ChainDAGRef, state: StateData) = # we could easily see a state explosion logScope: pcs = "save_state_at_epoch_start" + # As a policy, we only store epoch boundary states without the epoch block + # (if it exists) applied - the rest can be reconstructed by loading an epoch + # boundary state and applying the missing blocks if not state.data.data.slot.isEpoch: - # As a policy, we only store epoch boundary states - the rest can be - # reconstructed by loading an epoch boundary state and applying the - # missing blocks + trace "Not storing non-epoch state" + return + + if state.data.data.slot <= state.blck.slot: + trace "Not storing epoch state with block already applied" return if dag.db.containsState(state.data.root): @@ -522,9 +551,9 @@ proc advanceSlots( # processing doAssert state.data.data.slot <= slot + var cache = getStateCache(state.blck, state.data.data.slot.epoch) while state.data.data.slot < slot: # Process slots one at a time in case afterUpdate needs to see empty states - var cache = getEpochCache(state.blck, state.data.data) advance_slot(state.data, dag.updateFlags, cache) if save: @@ -540,18 +569,17 @@ proc applyBlock( # `state_transition` can handle empty slots, but we want to potentially save # some of the empty slot states - dag.advanceSlots(state, blck.data.message.slot - 1, save) + dag.advanceSlots(state, blck.data.message.slot, save) var statePtr = unsafeAddr state # safe because `restore` is locally scoped func restore(v: var HashedBeaconState) = doAssert (addr(statePtr.data) == addr v) statePtr[] = dag.headState - var cache = getEpochCache(blck.refs, state.data.data) - + var cache = getStateCache(state.blck, state.data.data.slot.epoch) let ok = state_transition( dag.runtimePreset, state.data, blck.data, - cache, flags + dag.updateFlags, restore) + cache, flags + dag.updateFlags + {slotProcessed}, restore) if ok: state.blck = blck.refs dag.putState(state) @@ -559,7 +587,8 @@ proc applyBlock( ok proc updateStateData*( - dag: ChainDAGRef, state: var StateData, bs: BlockSlot) = + dag: ChainDAGRef, state: var StateData, bs: BlockSlot, + cache: var StateCache) = ## Rewind or advance state such that it matches the given block and slot - ## this may include replaying from an earlier snapshot if blck is on a ## different branch or has advanced to a higher slot number than slot @@ -590,14 +619,24 @@ proc updateStateData*( while not dag.getState(state, cur): # There's no state saved for this particular BlockSlot combination, keep # looking... - if cur.slot == cur.blck.slot: - # This is not an empty slot, so the block will need to be applied to - # eventually reach bs + if cur.blck.parent != nil and + cur.blck.slot.epoch != epoch(cur.blck.parent.slot): + # We store the state of the parent block with the epoch processing applied + # in the database - we'll need to apply the block however! ancestors.add(cur.blck) + cur = cur.blck.parent.atEpochStart(cur.blck.slot.epoch) + else: + if cur.slot == cur.blck.slot: + # This is not an empty slot, so the block will need to be applied to + # eventually reach bs + ancestors.add(cur.blck) - # Moves back slot by slot, in case a state for an empty slot was saved - cur = cur.parent + # Moves back slot by slot, in case a state for an empty slot was saved + cur = cur.parent + let + startSlot = state.data.data.slot + startRoot = state.data.root # Time to replay all the blocks between then and now for i in countdown(ancestors.len - 1, 0): # Because the ancestors are in the database, there's no need to persist them @@ -615,7 +654,11 @@ proc updateStateData*( beacon_state_rewinds.inc() debug "State reloaded from database", - blocks = ancestors.len, stateRoot = shortLog(state.data.root), + blocks = ancestors.len, + slots = state.data.data.slot - startSlot, + stateRoot = shortLog(state.data.root), + stateSlot = state.data.data.slot, + stateRoot = shortLog(startRoot), blck = shortLog(bs) proc loadTailState*(dag: ChainDAGRef): StateData = @@ -662,8 +705,9 @@ proc updateHead*(dag: ChainDAGRef, newHead: BlockRef) = dag.clearanceState.data.data.slot == newHead.slot: assign(dag.headState, dag.clearanceState) else: + var cache = getStateCache(newHead, newHead.slot.epoch()) updateStateData( - dag, dag.headState, newHead.atSlot(newHead.slot)) + dag, dag.headState, newHead.atSlot(newHead.slot), cache) dag.head = newHead @@ -744,7 +788,7 @@ proc updateHead*(dag: ChainDAGRef, newHead: BlockRef) = while tmp != dag.finalizedHead.blck: # leave the epoch cache in the last block of the epoch.. tmp = tmp.parent - tmp.epochsInfo = @[] + tmp.epochRefs = @[] dag.finalizedHead = finalizedHead diff --git a/beacon_chain/block_pools/clearance.nim b/beacon_chain/block_pools/clearance.nim index 4f62a1c31..d6760849c 100644 --- a/beacon_chain/block_pools/clearance.nim +++ b/beacon_chain/block_pools/clearance.nim @@ -8,7 +8,7 @@ {.push raises: [Defect].} import - std/[sequtils, tables], + std/[tables], chronicles, metrics, stew/results, ../extras, @@ -42,13 +42,16 @@ proc addRawBlock*( proc addResolvedBlock( dag: var ChainDAGRef, quarantine: var QuarantineRef, - state: HashedBeaconState, signedBlock: SignedBeaconBlock, + state: var StateData, signedBlock: SignedBeaconBlock, parent: BlockRef, cache: var StateCache, onBlockAdded: OnBlockAdded - ): BlockRef = + ) = # TODO move quarantine processing out of here logScope: pcs = "block_resolution" - doAssert state.data.slot == signedBlock.message.slot, "state must match block" + doAssert state.data.data.slot == signedBlock.message.slot, + "state must match block" + doAssert state.blck.root == signedBlock.message.parent_root, + "the StateData passed into the addResolved function not yet updated!" let blockRoot = signedBlock.root @@ -57,14 +60,12 @@ proc addResolvedBlock( link(parent, blockRef) - if parent.slot.compute_epoch_at_slot() == blockEpoch: - # If the parent and child blocks are from the same epoch, we can reuse - # the epoch cache - but we'll only use the current epoch because the new - # block might have affected what the next epoch looks like - blockRef.epochsInfo = filterIt(parent.epochsInfo, it.epoch == blockEpoch) - else: - # Ensure we collect the epoch info if it's missing - discard getEpochInfo(blockRef, state.data, cache) + var epochRef = blockRef.findEpochRef(blockEpoch) + if epochRef == nil: + let prevEpochRef = blockRef.findEpochRef(blockEpoch - 1) + + epochRef = EpochRef.init(state.data.data, cache, prevEpochRef) + blockRef.epochAncestor(blockEpoch).blck.epochRefs.add epochRef dag.blocks[blockRoot] = blockRef trace "Populating block dag", key = blockRoot, val = blockRef @@ -90,10 +91,12 @@ proc addResolvedBlock( blockRoot = shortLog(blockRoot), heads = dag.heads.len() + state.blck = blockRef + # Notify others of the new block before processing the quarantine, such that # notifications for parents happens before those of the children if onBlockAdded != nil: - onBlockAdded(blockRef, signedBlock, state) + onBlockAdded(blockRef, signedBlock, epochRef, state.data) # Now that we have the new block, we should see if any of the previously # unresolved blocks magically become resolved @@ -115,8 +118,6 @@ proc addResolvedBlock( for v in resolved: discard addRawBlock(dag, quarantine, v, onBlockAdded) - blockRef - proc addRawBlock*( dag: var ChainDAGRef, quarantine: var QuarantineRef, signedBlock: SignedBeaconBlock, @@ -190,8 +191,9 @@ proc addRawBlock*( # TODO if the block is from the future, we should not be resolving it (yet), # but maybe we should use it as a hint that our clock is wrong? + var cache = getStateCache(parent, blck.slot.epoch) updateStateData( - dag, dag.clearanceState, BlockSlot(blck: parent, slot: blck.slot - 1)) + dag, dag.clearanceState, parent.atSlot(blck.slot), cache) let poolPtr = unsafeAddr dag # safe because restore is short-lived @@ -202,21 +204,17 @@ proc addRawBlock*( doAssert v.addr == addr poolPtr.clearanceState.data assign(poolPtr.clearanceState, poolPtr.headState) - var cache = getEpochCache(parent, dag.clearanceState.data.data) if not state_transition(dag.runtimePreset, dag.clearanceState.data, signedBlock, - cache, dag.updateFlags, restore): + cache, dag.updateFlags + {slotProcessed}, restore): notice "Invalid block" return err Invalid # Careful, clearanceState.data has been updated but not blck - we need to # create the BlockRef first! - dag.clearanceState.blck = addResolvedBlock( - dag, quarantine, dag.clearanceState.data, signedBlock, parent, cache, - onBlockAdded - ) - - dag.putState(dag.clearanceState) + addResolvedBlock( + dag, quarantine, dag.clearanceState, signedBlock, parent, cache, + onBlockAdded) return ok dag.clearanceState.blck diff --git a/beacon_chain/extras.nim b/beacon_chain/extras.nim index 736826415..8cd473d1c 100644 --- a/beacon_chain/extras.nim +++ b/beacon_chain/extras.nim @@ -27,5 +27,8 @@ type skipStateRootValidation ##\ ## Skip verification of block state root. verifyFinalization + slotProcessed ##\ + ## Allow blocks to be applied to states with the same slot number as the + ## block which is what happens when `process_block` is called separately UpdateFlags* = set[UpdateFlag] diff --git a/beacon_chain/spec/crypto.nim b/beacon_chain/spec/crypto.nim index 3688a9f9b..3addd123b 100644 --- a/beacon_chain/spec/crypto.nim +++ b/beacon_chain/spec/crypto.nim @@ -79,19 +79,6 @@ type export AggregateSignature -func `==`*(a, b: BlsValue): bool = - if a.kind != b.kind: return false - if a.kind == Real: - return a.blsValue == b.blsValue - else: - return a.blob == b.blob - -template `==`*[N, T](a: BlsValue[N, T], b: T): bool = - a.blsValue == b - -template `==`*[N, T](a: T, b: BlsValue[N, T]): bool = - a == b.blsValue - # API # ---------------------------------------------------------------------- # https://github.com/ethereum/eth2.0-specs/blob/v0.12.2/specs/phase0/beacon-chain.md#bls-signatures @@ -225,10 +212,9 @@ func `$`*(x: ValidatorPrivKey): string = func `$`*(x: BlsValue): string = # The prefix must be short # due to the mechanics of the `shortLog` function. - if x.kind == Real: - x.blsValue.toHex() - else: - "raw: " & x.blob.toHex() + case x.kind + of Real: x.blsValue.toHex() + of OpaqueBlob: "r:" & x.blob.toHex() func toRaw*(x: ValidatorPrivKey): array[32, byte] = # TODO: distinct type - see https://github.com/status-im/nim-blscurve/pull/67 @@ -278,6 +264,20 @@ func fromHex*(T: type BlsCurveType, hexStr: string): BlsResult[T] {.inline.} = except ValueError: err "bls: cannot parse value" +func `==`*(a, b: BlsValue): bool = + # The assumption here is that converting to raw is mostly fast! + case a.kind + of Real: + if a.kind == b.kind: + a.blsValue == b.blsValue + else: + a.toRaw() == b.blob + of OpaqueBlob: + if a.kind == b.kind: + a.blob == b.blob + else: + a.blob == b.toRaw() + # Hashing # ---------------------------------------------------------------------- @@ -348,7 +348,7 @@ func shortLog*(x: BlsValue): string = if x.kind == Real: x.blsValue.exportRaw()[0..3].toHex() else: - "raw: " & x.blob[0..3].toHex() + "r:" & x.blob[0..3].toHex() func shortLog*(x: ValidatorPrivKey): string = ## Logging for raw unwrapped BLS types @@ -369,7 +369,6 @@ func init*(T: typedesc[ValidatorPrivKey], hex: string): T {.noInit, raises: [Val raise (ref ValueError)(msg: $v.error) v[] - # For mainchain monitor func init*(T: typedesc[ValidatorPubKey], data: array[RawPubKeySize, byte]): T {.noInit, raises: [ValueError, Defect].} = let v = T.fromRaw(data) diff --git a/beacon_chain/spec/state_transition.nim b/beacon_chain/spec/state_transition.nim index 51d32633e..2a647095d 100644 --- a/beacon_chain/spec/state_transition.nim +++ b/beacon_chain/spec/state_transition.nim @@ -163,13 +163,14 @@ proc process_slots*(state: var HashedBeaconState, slot: Slot, # slots "automatically" in `state_transition`, perhaps it would be better # to keep a pre-condition that state must be at the right slot already? if not (state.data.slot < slot): - notice( - "Unusual request for a slot in the past", - state_root = shortLog(state.root), - current_slot = state.data.slot, - target_slot = slot - ) - return false + if slotProcessed notin updateFlags or state.data.slot != slot: + notice( + "Unusual request for a slot in the past", + state_root = shortLog(state.root), + current_slot = state.data.slot, + target_slot = slot + ) + return false # Catch up to the target slot var cache = StateCache() diff --git a/beacon_chain/validator_api.nim b/beacon_chain/validator_api.nim index ba1445363..661cf2e2f 100644 --- a/beacon_chain/validator_api.nim +++ b/beacon_chain/validator_api.nim @@ -229,8 +229,6 @@ proc installValidatorApiHandlers*(rpcServer: RpcServer, node: BeaconNode) = stateId: string, epoch: uint64, index: uint64, slot: uint64) -> seq[BeaconStatesCommitteesTuple]: withStateForStateId(stateId): - var cache = StateCache() # TODO is this OK? - proc getCommittee(slot: Slot, index: CommitteeIndex): BeaconStatesCommitteesTuple = let vals = get_beacon_committee(state, slot, index, cache).mapIt(it.uint64) return (index: index.uint64, slot: slot.uint64, validators: vals) diff --git a/beacon_chain/validator_duties.nim b/beacon_chain/validator_duties.nim index 42f1ba283..60275b0f1 100644 --- a/beacon_chain/validator_duties.nim +++ b/beacon_chain/validator_duties.nim @@ -205,7 +205,6 @@ proc makeBeaconBlockForHeadAndSlot*(node: BeaconNode, doAssert v.addr == addr poolPtr.tmpState.data assign(poolPtr.tmpState, poolPtr.headState) - var cache = StateCache() let message = makeBeaconBlock( node.config.runtimePreset, hashedState, @@ -237,9 +236,8 @@ proc proposeSignedBlock*(node: BeaconNode, let newBlockRef = node.chainDag.addRawBlock(node.quarantine, newBlock) do ( blckRef: BlockRef, signedBlock: SignedBeaconBlock, - state: HashedBeaconState): + epochRef: EpochRef, state: HashedBeaconState): # Callback add to fork choice if valid - let epochRef = getEpochInfo(blckRef, state.data) node.attestationPool.addForkChoice( epochRef, blckRef, signedBlock.message, node.beaconClock.now().slotOrZero()) @@ -402,7 +400,6 @@ proc broadcastAggregatedAttestations( let bs = BlockSlot(blck: aggregationHead, slot: aggregationSlot) node.chainDag.withState(node.chainDag.tmpState, bs): - var cache = getEpochCache(aggregationHead, state) let committees_per_slot = get_committee_count_per_slot(state, aggregationSlot.epoch, cache) diff --git a/research/block_sim.nim b/research/block_sim.nim index 3f8a5c962..b68629a7e 100644 --- a/research/block_sim.nim +++ b/research/block_sim.nim @@ -70,7 +70,6 @@ cli do(slots = SLOTS_PER_EPOCH * 6, attestationHead = chainDag.head.atSlot(slot) chainDag.withState(chainDag.tmpState, attestationHead): - var cache = getEpochCache(attestationHead.blck, state) let committees_per_slot = get_committee_count_per_slot(state, slot.epoch, cache) @@ -104,8 +103,6 @@ cli do(slots = SLOTS_PER_EPOCH * 6, head = chainDag.head chainDag.withState(chainDag.tmpState, head.atSlot(slot)): - var cache = StateCache() - let proposerIdx = get_beacon_proposer_index(state, cache).get() privKey = hackPrivKey(state.validators[proposerIdx]) @@ -140,9 +137,8 @@ cli do(slots = SLOTS_PER_EPOCH * 6, let added = chainDag.addRawBlock(quarantine, newBlock) do ( blckRef: BlockRef, signedBlock: SignedBeaconBlock, - state: HashedBeaconState): + epochRef: EpochRef, state: HashedBeaconState): # Callback add to fork choice if valid - let epochRef = getEpochInfo(blckRef, state.data) attPool.addForkChoice(epochRef, blckRef, signedBlock.message, blckRef.slot) blck() = added[] @@ -175,8 +171,9 @@ cli do(slots = SLOTS_PER_EPOCH * 6, if replay: withTimer(timers[tReplay]): + var cache = StateCache() chainDag.updateStateData( - replayState[], chainDag.head.atSlot(Slot(slots))) + replayState[], chainDag.head.atSlot(Slot(slots)), cache) echo "Done!" diff --git a/tests/test_attestation_pool.nim b/tests/test_attestation_pool.nim index 7c7671905..032a26052 100644 --- a/tests/test_attestation_pool.nim +++ b/tests/test_attestation_pool.nim @@ -185,9 +185,8 @@ suiteReport "Attestation pool processing" & preset(): b1 = addTestBlock(state.data, chainDag.tail.root, cache) b1Add = chainDag.addRawBlock(quarantine, b1) do ( blckRef: BlockRef, signedBlock: SignedBeaconBlock, - state: HashedBeaconState): + epochRef: EpochRef, state: HashedBeaconState): # Callback add to fork choice if valid - let epochRef = getEpochInfo(blckRef, state.data) pool[].addForkChoice(epochRef, blckRef, signedBlock.message, blckRef.slot) let head = pool[].selectHead(b1Add[].slot) @@ -199,9 +198,8 @@ suiteReport "Attestation pool processing" & preset(): b2 = addTestBlock(state.data, b1.root, cache) b2Add = chainDag.addRawBlock(quarantine, b2) do ( blckRef: BlockRef, signedBlock: SignedBeaconBlock, - state: HashedBeaconState): + epochRef: EpochRef, state: HashedBeaconState): # Callback add to fork choice if valid - let epochRef = getEpochInfo(blckRef, state.data) pool[].addForkChoice(epochRef, blckRef, signedBlock.message, blckRef.slot) let head2 = pool[].selectHead(b2Add[].slot) @@ -215,9 +213,8 @@ suiteReport "Attestation pool processing" & preset(): b10 = makeTestBlock(state.data, chainDag.tail.root, cache) b10Add = chainDag.addRawBlock(quarantine, b10) do ( blckRef: BlockRef, signedBlock: SignedBeaconBlock, - state: HashedBeaconState): + epochRef: EpochRef, state: HashedBeaconState): # Callback add to fork choice if valid - let epochRef = getEpochInfo(blckRef, state.data) pool[].addForkChoice(epochRef, blckRef, signedBlock.message, blckRef.slot) let head = pool[].selectHead(b10Add[].slot) @@ -231,9 +228,8 @@ suiteReport "Attestation pool processing" & preset(): ) b11Add = chainDag.addRawBlock(quarantine, b11) do ( blckRef: BlockRef, signedBlock: SignedBeaconBlock, - state: HashedBeaconState): + epochRef: EpochRef, state: HashedBeaconState): # Callback add to fork choice if valid - let epochRef = getEpochInfo(blckRef, state.data) pool[].addForkChoice(epochRef, blckRef, signedBlock.message, blckRef.slot) bc1 = get_beacon_committee( @@ -274,9 +270,8 @@ suiteReport "Attestation pool processing" & preset(): b10 = makeTestBlock(state.data, chainDag.tail.root, cache) b10Add = chainDag.addRawBlock(quarantine, b10) do ( blckRef: BlockRef, signedBlock: SignedBeaconBlock, - state: HashedBeaconState): + epochRef: EpochRef, state: HashedBeaconState): # Callback add to fork choice if valid - let epochRef = getEpochInfo(blckRef, state.data) pool[].addForkChoice(epochRef, blckRef, signedBlock.message, blckRef.slot) let head = pool[].selectHead(b10Add[].slot) @@ -289,9 +284,8 @@ suiteReport "Attestation pool processing" & preset(): let b10_clone = b10 # Assumes deep copy let b10Add_clone = chainDag.addRawBlock(quarantine, b10_clone) do ( blckRef: BlockRef, signedBlock: SignedBeaconBlock, - state: HashedBeaconState): + epochRef: EpochRef, state: HashedBeaconState): # Callback add to fork choice if valid - let epochRef = getEpochInfo(blckRef, state.data) pool[].addForkChoice(epochRef, blckRef, signedBlock.message, blckRef.slot) doAssert: b10Add_clone.error == Duplicate @@ -304,9 +298,8 @@ suiteReport "Attestation pool processing" & preset(): b10 = makeTestBlock(state.data, chainDag.tail.root, cache) b10Add = chainDag.addRawBlock(quarantine, b10) do ( blckRef: BlockRef, signedBlock: SignedBeaconBlock, - state: HashedBeaconState): + epochRef: EpochRef, state: HashedBeaconState): # Callback add to fork choice if valid - let epochRef = getEpochInfo(blckRef, state.data) pool[].addForkChoice(epochRef, blckRef, signedBlock.message, blckRef.slot) let head = pool[].selectHead(b10Add[].slot) @@ -339,9 +332,8 @@ suiteReport "Attestation pool processing" & preset(): block_root = new_block.root let blockRef = chainDag.addRawBlock(quarantine, new_block) do ( blckRef: BlockRef, signedBlock: SignedBeaconBlock, - state: HashedBeaconState): + epochRef: EpochRef, state: HashedBeaconState): # Callback add to fork choice if valid - let epochRef = getEpochInfo(blckRef, state.data) pool[].addForkChoice(epochRef, blckRef, signedBlock.message, blckRef.slot) let head = pool[].selectHead(blockRef[].slot) @@ -381,9 +373,8 @@ suiteReport "Attestation pool processing" & preset(): # Add back the old block to ensure we have a duplicate error let b10Add_clone = chainDag.addRawBlock(quarantine, b10_clone) do ( blckRef: BlockRef, signedBlock: SignedBeaconBlock, - state: HashedBeaconState): + epochRef: EpochRef, state: HashedBeaconState): # Callback add to fork choice if valid - let epochRef = getEpochInfo(blckRef, state.data) pool[].addForkChoice(epochRef, blckRef, signedBlock.message, blckRef.slot) doAssert: b10Add_clone.error == Duplicate diff --git a/tests/test_block_pool.nim b/tests/test_block_pool.nim index 07e7f5f2e..de26ab444 100644 --- a/tests/test_block_pool.nim +++ b/tests/test_block_pool.nim @@ -55,6 +55,22 @@ suiteReport "BlockRef and helpers" & preset(): s4.get_ancestor(Slot(3)) == s2 s4.get_ancestor(Slot(4)) == s4 + timedTest "epochAncestor sanity" & preset(): + let + s0 = BlockRef(slot: Slot(0)) + var cur = s0 + for i in 1..SLOTS_PER_EPOCH * 2: + cur = BlockRef(slot: Slot(i), parent: cur) + + let ancestor = cur.epochAncestor(cur.slot.epoch) + + check: + ancestor.slot.epoch == cur.slot.epoch + ancestor.blck != cur # should have selected a parent + + ancestor.blck.epochAncestor(cur.slot.epoch) == ancestor + ancestor.blck.epochAncestor(ancestor.blck.slot.epoch) != ancestor + suiteReport "BlockSlot and helpers" & preset(): timedTest "atSlot sanity" & preset(): let @@ -98,7 +114,6 @@ suiteReport "Block pool processing" & preset(): b1Root = hash_tree_root(b1.message) b2 = addTestBlock(stateData.data, b1Root, cache) b2Root {.used.} = hash_tree_root(b2.message) - timedTest "getRef returns nil for missing blocks": check: dag.getRef(default Eth2Digest) == nil @@ -132,9 +147,10 @@ suiteReport "Block pool processing" & preset(): b2Add[].root == b2Get.get().refs.root dag.heads.len == 1 dag.heads[0] == b2Add[] - # both should have the same epoch ref instance because they're from the - # same epoch - addr(b2Add[].epochsInfo[0][]) == addr(b1Add[].epochsInfo[0][]) + not b1Add[].findEpochRef(b1Add[].slot.epoch).isNil + b1Add[].findEpochRef(b1Add[].slot.epoch) == + b2Add[].findEpochRef(b2Add[].slot.epoch) + b1Add[].findEpochRef(b1Add[].slot.epoch + 1).isNil # Skip one slot to get a gap check: @@ -249,39 +265,40 @@ suiteReport "Block pool processing" & preset(): var tmpState = assignClone(dag.headState) # move to specific block - dag.updateStateData(tmpState[], bs1) + var cache = StateCache() + dag.updateStateData(tmpState[], bs1, cache) check: tmpState.blck == b1Add[] tmpState.data.data.slot == bs1.slot # Skip slots - dag.updateStateData(tmpState[], bs1_3) # skip slots + dag.updateStateData(tmpState[], bs1_3, cache) # skip slots check: tmpState.blck == b1Add[] tmpState.data.data.slot == bs1_3.slot # Move back slots, but not blocks - dag.updateStateData(tmpState[], bs1_3.parent()) + dag.updateStateData(tmpState[], bs1_3.parent(), cache) check: tmpState.blck == b1Add[] tmpState.data.data.slot == bs1_3.parent().slot # Move to different block and slot - dag.updateStateData(tmpState[], bs2_3) + dag.updateStateData(tmpState[], bs2_3, cache) check: tmpState.blck == b2Add[] tmpState.data.data.slot == bs2_3.slot # Move back slot and block - dag.updateStateData(tmpState[], bs1) + dag.updateStateData(tmpState[], bs1, cache) check: tmpState.blck == b1Add[] tmpState.data.data.slot == bs1.slot # Move back to genesis - dag.updateStateData(tmpState[], bs1.parent()) + dag.updateStateData(tmpState[], bs1.parent(), cache) check: tmpState.blck == b1Add[].parent tmpState.data.data.slot == bs1.parent.slot @@ -328,10 +345,12 @@ suiteReport "chain DAG finalization tests" & preset(): # Epochrefs should share validator key set when the validator set is # stable - addr(dag.heads[0].epochsInfo[0].validator_key_store[1][]) == - addr(dag.heads[0].atEpochEnd( - dag.heads[0].slot.compute_epoch_at_slot() - 1). - blck.epochsInfo[0].validator_key_store[1][]) + not dag.heads[0].findEpochRef(dag.heads[0].slot.epoch).isNil + not dag.heads[0].findEpochRef(dag.heads[0].slot.epoch - 1).isNil + dag.heads[0].findEpochRef(dag.heads[0].slot.epoch) != + dag.heads[0].findEpochRef(dag.heads[0].slot.epoch - 1) + dag.heads[0].findEpochRef(dag.heads[0].slot.epoch).validator_key_store[1] == + dag.heads[0].findEpochRef(dag.heads[0].slot.epoch - 1).validator_key_store[1] block: # The late block is a block whose parent was finalized long ago and thus