disable state pruning

* fix crash when state root is present but state is missing
* fix state root removal when state is removed
* fix block pool initialization which needs tail state
* remove tail block pruning
  * incomplete - fork states are not pruned
  * incomplete - fork blocks are not pruned
  * incomplete - empty slot states are not pruned
  * unknown - tail/finalized block on empty slot might be incorrect
This commit is contained in:
Jacek Sieka 2020-01-22 13:59:54 +01:00 committed by zah
parent 23b93adfe6
commit 95437e103a
3 changed files with 63 additions and 73 deletions

View File

@ -67,9 +67,6 @@ proc init*(T: type BeaconChainDB, backend: KVStoreRef): BeaconChainDB =
proc putBlock*(db: BeaconChainDB, key: Eth2Digest, value: SignedBeaconBlock) =
db.backend.put(subkey(type value, key), SSZ.encode(value))
proc putHead*(db: BeaconChainDB, key: Eth2Digest) =
db.backend.put(subkey(kHeadBlock), key.data) # TODO head block?
proc putState*(db: BeaconChainDB, key: Eth2Digest, value: BeaconState) =
# TODO prune old states - this is less easy than it seems as we never know
# when or if a particular state will become finalized.
@ -92,8 +89,11 @@ proc delBlock*(db: BeaconChainDB, key: Eth2Digest) =
proc delState*(db: BeaconChainDB, key: Eth2Digest) =
db.backend.del(subkey(BeaconState, key))
proc delStateRoot*(db: BeaconChainDB, root: Eth2Digest, slot: Slot) =
db.backend.del(subkey(root, slot))
proc putHeadBlock*(db: BeaconChainDB, key: Eth2Digest) =
db.backend.put(subkey(kHeadBlock), key.data) # TODO head block?
db.backend.put(subkey(kHeadBlock), key.data)
proc putTailBlock*(db: BeaconChainDB, key: Eth2Digest) =
db.backend.put(subkey(kTailBlock), key.data)

View File

@ -582,12 +582,13 @@ proc maybePutState(pool: BlockPool, state: HashedBeaconState, blck: BlockRef) =
# TODO we save state at every epoch start but never remove them - we also
# potentially save multiple states per slot if reorgs happen, meaning
# we could easily see a state explosion
# TODO this is out of sync with epoch def now, I think -- (slot + 1) mod foo.
logScope: pcs = "save_state_at_epoch_start"
if state.data.slot mod SLOTS_PER_EPOCH == 0:
if not pool.db.containsState(state.root):
info "Storing state",
blockRoot = shortLog(blck.root),
blockSlot = shortLog(blck.slot),
stateSlot = shortLog(state.data.slot),
stateRoot = shortLog(state.root),
cat = "caching"
@ -613,6 +614,14 @@ proc rewindState(pool: BlockPool, state: var StateData, bs: BlockSlot):
var
stateRoot = pool.db.getStateRoot(bs.blck.root, bs.slot)
curBs = bs
# TODO this can happen when state root is saved but state is gone - this would
# indicate a corrupt database, but since we're not atomically
# writing and deleting state+root mappings in a single transaction, it's
# likely to happen and we guard against it here.
if stateRoot.isSome() and not pool.db.containsState(stateRoot.get()):
stateRoot = none(type(stateRoot.get()))
while stateRoot.isNone():
let parBs = curBs.parent()
if parBs.blck.isNil:
@ -635,6 +644,8 @@ proc rewindState(pool: BlockPool, state: var StateData, bs: BlockSlot):
# tail state in there!)
error "Couldn't find ancestor state root!",
blockRoot = shortLog(bs.blck.root),
blockSlot = shortLog(bs.blck.slot),
slot = shortLog(bs.slot),
cat = "crash"
doAssert false, "Oh noes, we passed big bang!"
@ -649,6 +660,8 @@ proc rewindState(pool: BlockPool, state: var StateData, bs: BlockSlot):
# tail state in there!)
error "Couldn't find ancestor state or block parent missing!",
blockRoot = shortLog(bs.blck.root),
blockSlot = shortLog(bs.blck.slot),
slot = shortLog(bs.slot),
cat = "crash"
doAssert false, "Oh noes, we passed big bang!"
@ -717,46 +730,11 @@ proc loadTailState*(pool: BlockPool): StateData =
blck: pool.tail
)
proc delBlockAndState(pool: BlockPool, blockRoot: Eth2Digest) =
if (let blk = pool.db.getBlock(blockRoot); blk.isSome):
pool.db.delState(blk.get.message.stateRoot)
pool.db.delBlock(blockRoot)
proc delFinalizedStateIfNeeded(pool: BlockPool, b: BlockRef) =
# Delete finalized state for block `b` from the database, that doesn't need
# to be kept for replaying.
# TODO: Currently the protocol doesn't provide a way to request states,
# so we don't need any of the finalized states, and thus remove all of them
# (except the most recent)
if (let blk = pool.db.getBlock(b.root); blk.isSome):
pool.db.delState(blk.get.message.stateRoot)
proc setTailBlock(pool: BlockPool, newTail: BlockRef) =
## Advance tail block, pruning all the states and blocks with older slots
doAssert pool.tail.isAncestorOf(newTail),
"tail blocks should have linear ancestry to previous tail"
var b = newTail
while b != pool.tail:
doAssert b.slot > pool.tail.slot
doAssert b.parent != nil
b = b.parent
pool.delBlockAndState(b.root)
doAssert b.children.len == 1, "chain to old tail should be finalized"
b.children.setLen(0)
b.parent = nil
pool.blocks.del(b.root)
pool.db.putTailBlock(newTail.root)
pool.tail = newTail
newTail.parent = nil
info "Tail block updated",
slot = newTail.slot,
root = shortLog(newTail.root)
proc delState(pool: BlockPool, bs: BlockSlot) =
# Delete state state and mapping for a particular block+slot
if (let root = pool.db.getStateRoot(bs.blck.root, bs.slot); root.isSome()):
pool.db.delState(root.get())
pool.db.delStateRoot(bs.blck.root, bs.slot)
proc updateHead*(pool: BlockPool, newHead: BlockRef) =
## Update what we consider to be the current head, as given by the fork
@ -828,25 +806,44 @@ proc updateHead*(pool: BlockPool, newHead: BlockRef) =
"Block graph should always lead to a finalized block"
if finalizedHead != pool.finalizedHead:
var cur = finalizedHead.blck
while cur != pool.finalizedHead.blck:
# Finalization means that we choose a single chain as the canonical one -
# it also means we're no longer interested in any branches from that chain
# up to the finalization point.
# The new finalized head should not be cleaned! We start at its parent and
# clean everything including the old finalized head.
cur = cur.parent
block: # Remove states, walking slot by slot
discard
# TODO this is very aggressive - in theory all our operations start at
# the finalized block so all states before that can be wiped..
# TODO this is disabled for now because the logic for initializing the
# block pool and potentially a few other places depend on certain
# states (like the tail state) being present. It's also problematic
# because it is not clear what happens when tail and finalized states
# happen on an empty slot..
# var cur = finalizedHead
# while cur != pool.finalizedHead:
# cur = cur.parent
# pool.delState(cur)
pool.delFinalizedStateIfNeeded(cur)
block: # Clean up block refs, walking block by block
var cur = finalizedHead.blck
while cur != pool.finalizedHead.blck:
# Finalization means that we choose a single chain as the canonical one -
# it also means we're no longer interested in any branches from that chain
# up to the finalization point.
# The new finalized head should not be cleaned! We start at its parent and
# clean everything including the old finalized head.
cur = cur.parent
# TODO what about attestations? we need to drop those too, though they
# *should* be pretty harmless
if cur.parent != nil: # This happens for the genesis / tail block
for child in cur.parent.children:
if child != cur:
pool.blocks.del(child.root)
pool.delBlockAndState(child.root)
cur.parent.children = @[cur]
# TODO what about attestations? we need to drop those too, though they
# *should* be pretty harmless
if cur.parent != nil: # This happens for the genesis / tail block
for child in cur.parent.children:
if child != cur:
# TODO also remove states associated with the unviable forks!
# TODO the easiest thing to do here would probably be to use
# pool.heads to find unviable heads, then walk those chains
# and remove everything.. currently, if there's a child with
# children of its own, those children will not be pruned
# correctly from the database
pool.blocks.del(child.root)
pool.db.delBlock(child.root)
cur.parent.children = @[cur]
pool.finalizedHead = finalizedHead
@ -866,13 +863,7 @@ proc updateHead*(pool: BlockPool, newHead: BlockRef) =
heads = pool.heads.len,
cat = "fork_choice"
# Calculate new tail block and set it
# New tail should be WEAK_SUBJECTIVITY_PERIOD * 2 older than finalizedHead
const tailSlotInterval = WEAK_SUBJECTVITY_PERIOD * 2
if finalizedEpochStartSlot - GENESIS_SLOT > tailSlotInterval:
let tailSlot = finalizedEpochStartSlot - tailSlotInterval
let newTail = finalizedHead.blck.findAncestorBySlot(tailSlot)
pool.setTailBlock(newTail.blck)
# TODO prune everything before weak subjectivity period
func latestJustifiedBlock*(pool: BlockPool): BlockSlot =
## Return the most recent block that is justified and at least as recent
@ -932,5 +923,4 @@ proc preInit*(
db.putBlock(signedBlock)
db.putTailBlock(blockRoot)
db.putHeadBlock(blockRoot)
db.putStateRoot(
blockRoot, signedBlock.message.slot, signedBlock.message.state_root)
db.putStateRoot(blockRoot, state.slot, signedBlock.message.state_root)

View File

@ -201,7 +201,7 @@ when const_preset == "minimal": # Too much stack space used on mainnet
BeaconBlockBody())
discard pool.add(hash_tree_root(blck.message), blck)
for i in 0 ..< (SLOTS_PER_EPOCH * 4):
for i in 0 ..< (SLOTS_PER_EPOCH * 6):
if i == 1:
# There are 2 heads now because of the fork at slot 1
check:
@ -219,5 +219,5 @@ when const_preset == "minimal": # Too much stack space used on mainnet
check:
pool.heads.len() == 1
pool.head.justified.slot.compute_epoch_at_slot() == 3
pool.head.justified.slot.compute_epoch_at_slot() == 5
pool.tail.children.len == 1