mirror of
https://github.com/status-im/nimbus-eth2.git
synced 2025-02-02 17:53:52 +00:00
initial state replay implementation
* fix initial attestation pool on reordered attestations * simplify db layer api * load head block from database on startup, then load state * significantly changes database format * move subscriptions to separate proc's * implement block replay from historical state * avoid rescheduling epoch actions on block receipt (why?) * make sure genesis block is created and used * relax initial state sim parameters a bit
This commit is contained in:
parent
1d13007627
commit
2d307e2257
@ -15,7 +15,7 @@ bin = @[
|
|||||||
requires "nim >= 0.19.0",
|
requires "nim >= 0.19.0",
|
||||||
"eth",
|
"eth",
|
||||||
"nimcrypto",
|
"nimcrypto",
|
||||||
"https://github.com/status-im/nim-blscurve#master",
|
"blscurve",
|
||||||
"ranges",
|
"ranges",
|
||||||
"chronicles",
|
"chronicles",
|
||||||
"confutils",
|
"confutils",
|
||||||
|
@ -200,11 +200,12 @@ proc add*(pool: var AttestationPool,
|
|||||||
", startingSlot: " & $humaneSlotNum(pool.startingSlot)
|
", startingSlot: " & $humaneSlotNum(pool.startingSlot)
|
||||||
|
|
||||||
if pool.slots.len == 0:
|
if pool.slots.len == 0:
|
||||||
# When receiving the first attestation, we want to avoid adding a lot of
|
# Because the first attestations may arrive in any order, we'll make sure
|
||||||
# empty SlotData items, so we'll cheat a bit here
|
# to start counting at the last finalized epoch start slot - anything
|
||||||
|
# earlier than that is thrown out by the above check
|
||||||
info "First attestation!",
|
info "First attestation!",
|
||||||
attestationSlot = $humaneSlotNum(attestationSlot)
|
attestationSlot = $humaneSlotNum(attestationSlot)
|
||||||
pool.startingSlot = attestationSlot
|
pool.startingSlot = state.finalized_epoch.get_epoch_start_slot()
|
||||||
|
|
||||||
if pool.startingSlot + pool.slots.len.Slot <= attestationSlot:
|
if pool.startingSlot + pool.slots.len.Slot <= attestationSlot:
|
||||||
debug "Growing attestation pool",
|
debug "Growing attestation pool",
|
||||||
|
@ -4,88 +4,71 @@ import
|
|||||||
spec/[datatypes, digest, crypto],
|
spec/[datatypes, digest, crypto],
|
||||||
eth/trie/db, ssz
|
eth/trie/db, ssz
|
||||||
|
|
||||||
|
|
||||||
type
|
type
|
||||||
BeaconChainDB* = ref object
|
BeaconChainDB* = ref object
|
||||||
backend: TrieDatabaseRef
|
backend: TrieDatabaseRef
|
||||||
|
|
||||||
DbKeyKind = enum
|
DbKeyKind = enum
|
||||||
kLastFinalizedState
|
kHashToState
|
||||||
kHashToBlock
|
kHashToBlock
|
||||||
kSlotToBlockHash
|
kHeadBlock
|
||||||
kSlotToState
|
|
||||||
kHashToValidatorRegistryChangeLog
|
|
||||||
|
|
||||||
proc lastFinalizedStateKey(): array[1, byte] =
|
DbTypes = BeaconState | BeaconBlock
|
||||||
result[0] = byte ord(kLastFinalizedState)
|
|
||||||
|
|
||||||
proc hashToBlockKey(h: Eth2Digest): array[32 + 1, byte] =
|
func subkey(kind: DbKeyKind): array[1, byte] =
|
||||||
result[0] = byte ord(kHashToBlock)
|
result[0] = byte ord(kind)
|
||||||
result[1 .. ^1] = h.data
|
|
||||||
|
|
||||||
proc slotToBlockHashKey(s: Slot): array[sizeof(Slot) + 1, byte] =
|
func subkey[N: static int](kind: DbKeyKind, key: array[N, byte]):
|
||||||
result[0] = byte ord(kSlotToBlockHash)
|
array[N + 1, byte] =
|
||||||
copyMem(addr result[1], unsafeAddr(s), sizeof(s))
|
result[0] = byte ord(kind)
|
||||||
|
result[1 .. ^1] = key
|
||||||
|
|
||||||
proc slotToStateKey(s: Slot): array[sizeof(Slot) + 1, byte] =
|
func subkey(kind: type BeaconState, key: Eth2Digest): auto =
|
||||||
result[0] = byte ord(kSlotToState)
|
subkey(kHashToState, key.data)
|
||||||
copyMem(addr result[1], unsafeAddr(s), sizeof(s))
|
|
||||||
|
|
||||||
proc hashToValidatorRegistryChangeLogKey(deltaChainTip: Eth2Digest): array[32 + 1, byte] =
|
func subkey(kind: type BeaconBlock, key: Eth2Digest): auto =
|
||||||
result[0] = byte ord(kHashToValidatorRegistryChangeLog)
|
subkey(kHashToBlock, key.data)
|
||||||
result[1 .. ^1] = deltaChainTip.data
|
|
||||||
|
|
||||||
proc init*(T: type BeaconChainDB, backend: TrieDatabaseRef): BeaconChainDB =
|
proc init*(T: type BeaconChainDB, backend: TrieDatabaseRef): BeaconChainDB =
|
||||||
new result
|
new result
|
||||||
result.backend = backend
|
result.backend = backend
|
||||||
|
|
||||||
proc lastFinalizedState*(db: BeaconChainDB): BeaconState =
|
proc put*(db: BeaconChainDB, key: Eth2Digest, value: BeaconBlock) =
|
||||||
let res = db.backend.get(lastFinalizedStateKey())
|
db.backend.put(subkey(type value, key), ssz.serialize(value))
|
||||||
if res.len == 0:
|
|
||||||
raise newException(Exception, "Internal error: Database has no finalized state")
|
|
||||||
ssz.deserialize(res, BeaconState).get
|
|
||||||
|
|
||||||
proc isInitialized*(db: BeaconChainDB): bool =
|
proc putHead*(db: BeaconChainDB, key: Eth2Digest) =
|
||||||
db.backend.get(lastFinalizedStateKey()).len != 0
|
db.backend.put(subkey(kHeadBlock), key.data) # TODO head block?
|
||||||
|
|
||||||
proc persistState*(db: BeaconChainDB, s: BeaconState) =
|
proc put*(db: BeaconChainDB, key: Eth2Digest, value: BeaconState) =
|
||||||
if s.slot != GENESIS_SLOT:
|
db.backend.put(subkey(type value, key), ssz.serialize(value))
|
||||||
# TODO: Verify incoming state slot is higher than lastFinalizedState one
|
|
||||||
discard
|
|
||||||
else:
|
|
||||||
# Make sure we have no states
|
|
||||||
assert(not db.isInitialized)
|
|
||||||
|
|
||||||
var prevState: BeaconState
|
proc put*(db: BeaconChainDB, value: DbTypes) =
|
||||||
if s.slot != GENESIS_SLOT:
|
db.put(hash_tree_root_final(value), value)
|
||||||
prevState = db.lastFinalizedState()
|
|
||||||
if prevState.validator_registry_delta_chain_tip != s.validator_registry_delta_chain_tip:
|
|
||||||
# Validator registry has changed in the incoming state.
|
|
||||||
# TODO: Save the changelog.
|
|
||||||
discard
|
|
||||||
|
|
||||||
let serializedState = ssz.serialize(s)
|
proc get(db: BeaconChainDB, key: auto, T: typedesc): Option[T] =
|
||||||
# TODO: Consider mapping slots and last pointer to state hashes to avoid
|
let res = db.backend.get(key)
|
||||||
# duplicating in the db
|
|
||||||
db.backend.put(lastFinalizedStateKey(), serializedState)
|
|
||||||
db.backend.put(slotToStateKey(s.slot), serializedState)
|
|
||||||
|
|
||||||
proc persistBlock*(db: BeaconChainDB, b: BeaconBlock) =
|
|
||||||
let blockHash = b.hash_tree_root_final
|
|
||||||
db.backend.put(hashToBlockKey(blockHash), ssz.serialize(b))
|
|
||||||
db.backend.put(slotToBlockHashKey(b.slot), blockHash.data)
|
|
||||||
|
|
||||||
# proc getValidatorChangeLog*(deltaChainTip: Eth2Digest)
|
|
||||||
|
|
||||||
proc getBlock*(db: BeaconChainDB, hash: Eth2Digest, output: var BeaconBlock): bool =
|
|
||||||
let res = db.backend.get(hashToBlockKey(hash))
|
|
||||||
if res.len != 0:
|
if res.len != 0:
|
||||||
output = ssz.deserialize(res, BeaconBlock).get
|
ssz.deserialize(res, T)
|
||||||
true
|
|
||||||
else:
|
else:
|
||||||
false
|
none(T)
|
||||||
|
|
||||||
proc getBlock*(db: BeaconChainDB, hash: Eth2Digest): BeaconBlock =
|
# TODO: T: type DbTypes fails with compiler error.. investigate
|
||||||
if not db.getBlock(hash, result):
|
proc get*(db: BeaconChainDB, key: Eth2Digest, T: type BeaconBlock): Option[T] =
|
||||||
raise newException(Exception, "Block not found")
|
db.get(subkey(T, key), T)
|
||||||
|
|
||||||
|
proc get*(db: BeaconChainDB, key: Eth2Digest, T: type BeaconState): Option[T] =
|
||||||
|
db.get(subkey(T, key), T)
|
||||||
|
|
||||||
|
proc getHead*(db: BeaconChainDB, T: type BeaconBlock): Option[T] =
|
||||||
|
let key = db.backend.get(subkey(kHeadBlock))
|
||||||
|
if key.len == sizeof(Eth2Digest):
|
||||||
|
var tmp: Eth2Digest
|
||||||
|
copyMem(addr tmp, unsafeAddr key[0], sizeof(tmp))
|
||||||
|
|
||||||
|
db.get(tmp, T)
|
||||||
|
else:
|
||||||
|
none(T)
|
||||||
|
|
||||||
|
proc contains*(
|
||||||
|
db: BeaconChainDB, key: Eth2Digest, T: type DbTypes): bool =
|
||||||
|
db.backend.contains(subkey(T, key))
|
||||||
|
@ -17,7 +17,6 @@ type
|
|||||||
attachedValidators: ValidatorPool
|
attachedValidators: ValidatorPool
|
||||||
attestationPool: AttestationPool
|
attestationPool: AttestationPool
|
||||||
mainchainMonitor: MainchainMonitor
|
mainchainMonitor: MainchainMonitor
|
||||||
lastScheduledEpoch: Epoch
|
|
||||||
headBlock: BeaconBlock
|
headBlock: BeaconBlock
|
||||||
headBlockRoot: Eth2Digest
|
headBlockRoot: Eth2Digest
|
||||||
blocksChildren: Table[Eth2Digest, seq[Eth2Digest]]
|
blocksChildren: Table[Eth2Digest, seq[Eth2Digest]]
|
||||||
@ -44,6 +43,8 @@ proc ensureNetworkKeys*(dataDir: string): KeyPair =
|
|||||||
# if necessary
|
# if necessary
|
||||||
return newKeyPair()
|
return newKeyPair()
|
||||||
|
|
||||||
|
proc updateHeadBlock(node: BeaconNode, blck: BeaconBlock)
|
||||||
|
|
||||||
proc init*(T: type BeaconNode, conf: BeaconNodeConf): T =
|
proc init*(T: type BeaconNode, conf: BeaconNodeConf): T =
|
||||||
new result
|
new result
|
||||||
result.config = conf
|
result.config = conf
|
||||||
@ -55,10 +56,24 @@ proc init*(T: type BeaconNode, conf: BeaconNodeConf): T =
|
|||||||
let trieDB = trieDB newChainDb(string conf.dataDir)
|
let trieDB = trieDB newChainDb(string conf.dataDir)
|
||||||
result.db = BeaconChainDB.init(trieDB)
|
result.db = BeaconChainDB.init(trieDB)
|
||||||
|
|
||||||
if not result.db.isInitialized:
|
# TODO does it really make sense to load from DB if a state snapshot has been
|
||||||
# Use stateSnapshot as genesis
|
# specified on command line? potentially, this should be the other way
|
||||||
info "Initializing DB"
|
# around...
|
||||||
result.db.persistState(result.config.stateSnapshot.get)
|
if (let head = result.db.getHead(BeaconBlock) ; head.isSome()):
|
||||||
|
info "Loading head from database",
|
||||||
|
blockSlot = humaneSlotNum(head.get().slot)
|
||||||
|
updateHeadBlock(result, head.get())
|
||||||
|
else:
|
||||||
|
result.beaconState = result.config.stateSnapshot.get()
|
||||||
|
result.headBlock = get_initial_beacon_block(result.beaconState)
|
||||||
|
result.headBlockRoot = hash_tree_root_final(result.headBlock)
|
||||||
|
|
||||||
|
info "Loaded state from snapshot",
|
||||||
|
stateSlot = humaneSlotNum(result.beaconState.slot)
|
||||||
|
result.db.put(result.beaconState)
|
||||||
|
# The genesis block is special in that we have to store it at hash 0 - in
|
||||||
|
# the genesis state, this block has not been applied..
|
||||||
|
result.db.put(result.headBlock)
|
||||||
|
|
||||||
result.keys = ensureNetworkKeys(string conf.dataDir)
|
result.keys = ensureNetworkKeys(string conf.dataDir)
|
||||||
|
|
||||||
@ -91,11 +106,9 @@ proc connectToNetwork(node: BeaconNode) {.async.} =
|
|||||||
node.network.startListening()
|
node.network.startListening()
|
||||||
|
|
||||||
proc sync*(node: BeaconNode): Future[bool] {.async.} =
|
proc sync*(node: BeaconNode): Future[bool] {.async.} =
|
||||||
let persistedState = node.db.lastFinalizedState()
|
if node.beaconState.slotDistanceFromNow() > WEAK_SUBJECTVITY_PERIOD.int64:
|
||||||
if persistedState.slotDistanceFromNow() > WEAK_SUBJECTVITY_PERIOD.int64:
|
|
||||||
node.beaconState = await obtainTrustedStateSnapshot(node.db)
|
node.beaconState = await obtainTrustedStateSnapshot(node.db)
|
||||||
else:
|
else:
|
||||||
node.beaconState = persistedState
|
|
||||||
var targetSlot = node.beaconState.getSlotFromTime()
|
var targetSlot = node.beaconState.getSlotFromTime()
|
||||||
|
|
||||||
let t = now()
|
let t = now()
|
||||||
@ -107,19 +120,26 @@ proc sync*(node: BeaconNode): Future[bool] {.async.} =
|
|||||||
finalized_epoch = humaneEpochNum(node.beaconState.finalized_epoch),
|
finalized_epoch = humaneEpochNum(node.beaconState.finalized_epoch),
|
||||||
target_slot_epoch = humaneEpochNum(targetSlot.slot_to_epoch)
|
target_slot_epoch = humaneEpochNum(targetSlot.slot_to_epoch)
|
||||||
|
|
||||||
while node.beaconState.finalized_epoch < targetSlot.slot_to_epoch:
|
# TODO: sync is called at the beginning of the program, but doing this kind
|
||||||
var (peer, changeLog) = await node.network.getValidatorChangeLog(
|
# of catching up here is wrong - if we fall behind on processing
|
||||||
node.beaconState.validator_registry_delta_chain_tip)
|
# for whatever reason, we want to be safe against the damage that
|
||||||
|
# might cause regardless if we just started or have been running for
|
||||||
|
# long. A classic example where this might happen is when the
|
||||||
|
# computer goes to sleep - when waking up, we'll be in the middle of
|
||||||
|
# processing, but behind everyone else.
|
||||||
|
# while node.beaconState.finalized_epoch < targetSlot.slot_to_epoch:
|
||||||
|
# var (peer, changeLog) = await node.network.getValidatorChangeLog(
|
||||||
|
# node.beaconState.validator_registry_delta_chain_tip)
|
||||||
|
|
||||||
if peer == nil:
|
# if peer == nil:
|
||||||
error "Failed to sync with any peer"
|
# error "Failed to sync with any peer"
|
||||||
return false
|
# return false
|
||||||
|
|
||||||
if applyValidatorChangeLog(changeLog, node.beaconState):
|
# if applyValidatorChangeLog(changeLog, node.beaconState):
|
||||||
node.db.persistState(node.beaconState)
|
# node.db.persistState(node.beaconState)
|
||||||
node.db.persistBlock(changeLog.signedBlock)
|
# node.db.persistBlock(changeLog.signedBlock)
|
||||||
else:
|
# else:
|
||||||
warn "Ignoring invalid validator change log", sentFrom = peer
|
# warn "Ignoring invalid validator change log", sentFrom = peer
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|
||||||
@ -256,6 +276,7 @@ proc proposeBlock(node: BeaconNode,
|
|||||||
info "Block proposed",
|
info "Block proposed",
|
||||||
slot = humaneSlotNum(slot),
|
slot = humaneSlotNum(slot),
|
||||||
stateRoot = shortHash(newBlock.state_root),
|
stateRoot = shortHash(newBlock.state_root),
|
||||||
|
parentRoot = shortHash(newBlock.parent_root),
|
||||||
validator = shortValidatorKey(node, validator.idx),
|
validator = shortValidatorKey(node, validator.idx),
|
||||||
idx = validator.idx
|
idx = validator.idx
|
||||||
|
|
||||||
@ -356,88 +377,220 @@ proc scheduleEpochActions(node: BeaconNode, epoch: Epoch) =
|
|||||||
node, validator, slot, crosslink_committee.shard,
|
node, validator, slot, crosslink_committee.shard,
|
||||||
crosslink_committee.committee.len, i)
|
crosslink_committee.committee.len, i)
|
||||||
|
|
||||||
node.lastScheduledEpoch = epoch
|
|
||||||
let
|
let
|
||||||
nextEpoch = epoch + 1
|
nextEpoch = epoch + 1
|
||||||
at = node.beaconState.slotMiddle(nextEpoch * SLOTS_PER_EPOCH)
|
at = node.beaconState.slotStart(nextEpoch.get_epoch_start_slot())
|
||||||
|
|
||||||
info "Scheduling next epoch update",
|
info "Scheduling next epoch update",
|
||||||
fromNow = (at - fastEpochTime()) div 1000,
|
fromNow = (at - fastEpochTime()) div 1000,
|
||||||
epoch = humaneEpochNum(nextEpoch)
|
epoch = humaneEpochNum(nextEpoch)
|
||||||
|
|
||||||
addTimer(at) do (p: pointer):
|
addTimer(at) do (p: pointer):
|
||||||
if node.lastScheduledEpoch != nextEpoch:
|
|
||||||
node.scheduleEpochActions(nextEpoch)
|
node.scheduleEpochActions(nextEpoch)
|
||||||
|
|
||||||
proc stateNeedsSaving(s: BeaconState): bool =
|
proc stateNeedsSaving(s: BeaconState): bool =
|
||||||
# TODO: Come up with a better predicate logic
|
# TODO: Come up with a better predicate logic
|
||||||
s.slot mod stateStoragePeriod == 0
|
s.slot mod stateStoragePeriod == 0
|
||||||
|
|
||||||
proc processBlocks*(node: BeaconNode) =
|
proc onAttestation(node: BeaconNode, attestation: Attestation) =
|
||||||
node.network.subscribe(topicBeaconBlocks) do (newBlock: BeaconBlock):
|
|
||||||
let stateSlot = node.beaconState.slot
|
|
||||||
info "Block received",
|
|
||||||
slot = humaneSlotNum(newBlock.slot),
|
|
||||||
stateRoot = shortHash(newBlock.state_root),
|
|
||||||
stateSlot = humaneSlotNum(stateSlot)
|
|
||||||
|
|
||||||
# TODO: This should be replaced with the real fork-choice rule
|
|
||||||
if newBlock.slot <= stateSlot:
|
|
||||||
debug "Ignoring block"
|
|
||||||
return
|
|
||||||
|
|
||||||
let newBlockRoot = hash_tree_root_final(newBlock)
|
|
||||||
|
|
||||||
var state = node.beaconState
|
|
||||||
if stateSlot + 1 < newBlock.slot:
|
|
||||||
info "Advancing state past slot gap",
|
|
||||||
blockSlot = humaneSlotNum(newBlock.slot),
|
|
||||||
stateSlot = humaneSlotNum(stateSlot)
|
|
||||||
|
|
||||||
for slot in stateSlot + 1 ..< newBlock.slot:
|
|
||||||
let ok = updateState(state, node.headBlockRoot, none[BeaconBlock](), {})
|
|
||||||
doAssert ok
|
|
||||||
|
|
||||||
let ok = updateState(state, node.headBlockRoot, some(newBlock), {})
|
|
||||||
if not ok:
|
|
||||||
debug "Ignoring non-validating block"
|
|
||||||
return
|
|
||||||
|
|
||||||
node.headBlock = newBlock
|
|
||||||
node.headBlockRoot = newBlockRoot
|
|
||||||
node.beaconState = state
|
|
||||||
|
|
||||||
if stateNeedsSaving(node.beaconState):
|
|
||||||
node.db.persistState(node.beaconState)
|
|
||||||
|
|
||||||
node.db.persistBlock(newBlock)
|
|
||||||
|
|
||||||
# TODO:
|
|
||||||
#
|
|
||||||
# 1. Check for missing blocks and obtain them
|
|
||||||
#
|
|
||||||
# 2. Apply fork-choice rule (update node.headBlock)
|
|
||||||
#
|
|
||||||
# 3. Peform block processing / state recalculation / etc
|
|
||||||
#
|
|
||||||
|
|
||||||
let epoch = newBlock.slot.epoch
|
|
||||||
if epoch != node.lastScheduledEpoch:
|
|
||||||
node.scheduleEpochActions(epoch)
|
|
||||||
|
|
||||||
node.network.subscribe(topicAttestations) do (a: Attestation):
|
|
||||||
let participants = get_attestation_participants(
|
let participants = get_attestation_participants(
|
||||||
node.beaconState, a.data, a.aggregation_bitfield).
|
node.beaconState, attestation.data, attestation.aggregation_bitfield).
|
||||||
mapIt(shortValidatorKey(node, it))
|
mapIt(shortValidatorKey(node, it))
|
||||||
|
|
||||||
info "Attestation received",
|
info "Attestation received",
|
||||||
slot = humaneSlotNum(a.data.slot),
|
slot = humaneSlotNum(attestation.data.slot),
|
||||||
shard = a.data.shard,
|
shard = attestation.data.shard,
|
||||||
signature = shortHash(a.aggregate_signature),
|
signature = shortHash(attestation.aggregate_signature),
|
||||||
participants,
|
participants,
|
||||||
beaconBlockRoot = shortHash(a.data.beacon_block_root)
|
beaconBlockRoot = shortHash(attestation.data.beacon_block_root)
|
||||||
|
|
||||||
node.attestationPool.add(a, node.beaconState)
|
node.attestationPool.add(attestation, node.beaconState)
|
||||||
|
|
||||||
|
if not node.db.contains(attestation.data.beacon_block_root, BeaconBlock):
|
||||||
|
notice "Attestation block root missing",
|
||||||
|
beaconBlockRoot = shortHash(attestation.data.beacon_block_root)
|
||||||
|
# TODO download...
|
||||||
|
|
||||||
|
proc skipSlots(state: var BeaconState, parentRoot: Eth2Digest, nextSlot: Slot) =
|
||||||
|
if state.slot + 1 < nextSlot:
|
||||||
|
info "Advancing state past slot gap",
|
||||||
|
targetSlot = humaneSlotNum(nextSlot),
|
||||||
|
stateSlot = humaneSlotNum(state.slot)
|
||||||
|
|
||||||
|
for slot in state.slot + 1 ..< nextSlot:
|
||||||
|
let ok = updateState(state, parentRoot, none[BeaconBlock](), {})
|
||||||
|
doAssert ok, "Empty block state update should never fail!"
|
||||||
|
|
||||||
|
proc skipAndUpdateState(
|
||||||
|
state: var BeaconState, blck: BeaconBlock, flags: UpdateFlags): bool =
|
||||||
|
skipSlots(state, blck.parent_root, blck.slot)
|
||||||
|
updateState(state, blck.parent_root, some(blck), flags)
|
||||||
|
|
||||||
|
proc updateHeadBlock(node: BeaconNode, blck: BeaconBlock) =
|
||||||
|
# To update the head block, we need to apply it to the state. When things
|
||||||
|
# progress normally, the block we recieve will be a direct child of the
|
||||||
|
# last block we applied to the state:
|
||||||
|
|
||||||
|
if blck.parent_root == node.headBlockRoot:
|
||||||
|
let ok = skipAndUpdateState(node.beaconState, blck, {})
|
||||||
|
doAssert ok, "Nobody is ever going to send a faulty block!"
|
||||||
|
|
||||||
|
node.headBlock = blck
|
||||||
|
node.headBlockRoot = hash_tree_root_final(blck)
|
||||||
|
node.db.putHead(node.headBlockRoot)
|
||||||
|
|
||||||
|
info "Updated head",
|
||||||
|
stateRoot = shortHash(blck.state_root),
|
||||||
|
headBlockRoot = shortHash(node.headBlockRoot),
|
||||||
|
stateSlot = humaneSlotNum(node.beaconState.slot)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
# It appears that the parent root of the proposed new block is different from
|
||||||
|
# what we expected. We will have to rewind the state to a point along the
|
||||||
|
# chain of ancestors of the new block. We will do this by loading each
|
||||||
|
# successive parent block and checking if we can find the corresponding state
|
||||||
|
# in the database.
|
||||||
|
var parents = @[blck]
|
||||||
|
while true:
|
||||||
|
let top = parents[^1]
|
||||||
|
|
||||||
|
# We're looking for the most recent state that we have in the database
|
||||||
|
# that also exists on the ancestor chain.
|
||||||
|
if (let prevState = node.db.get(top.state_root, BeaconState);
|
||||||
|
prevState.isSome()):
|
||||||
|
# Got it!
|
||||||
|
notice "Replaying state transitions",
|
||||||
|
stateSlot = humaneSlotNum(node.beaconState.slot),
|
||||||
|
prevStateSlot = humaneSlotNum(prevState.get().slot)
|
||||||
|
node.beaconState = prevState.get()
|
||||||
|
break
|
||||||
|
|
||||||
|
if top.slot == 0:
|
||||||
|
# We've arrived at the genesis block and still haven't found what we're
|
||||||
|
# looking for. This is very bad - are we receiving blocks from a different
|
||||||
|
# chain? What's going on?
|
||||||
|
# TODO crashing like this is the wrong thing to do, obviously, but
|
||||||
|
# we'll do it anyway just to see if it ever happens - if it does,
|
||||||
|
# it's likely a bug :)
|
||||||
|
error "Couldn't find ancestor state",
|
||||||
|
blockSlot = humaneSlotNum(blck.slot),
|
||||||
|
blockRoot = shortHash(hash_tree_root_final(blck))
|
||||||
|
doAssert false, "Oh noes, we passed big bang!"
|
||||||
|
|
||||||
|
if (let parent = node.db.get(top.parent_root, BeaconBlock); parent.isSome):
|
||||||
|
# We're lucky this time - we found the parent block in the database, so
|
||||||
|
# we put it on the stack and keep looking
|
||||||
|
parents.add(parent.get())
|
||||||
|
else:
|
||||||
|
# We don't have the parent block. This is a bit strange, but may happen
|
||||||
|
# if things are happening seriously out of order or if we're back after
|
||||||
|
# a net split or restart, for example. Once the missing block arrives,
|
||||||
|
# we should retry setting the head block..
|
||||||
|
# TODO implement block sync here
|
||||||
|
# TODO instead of doing block sync here, make sure we are sync already
|
||||||
|
# elsewhere, so as to simplify the logic of finding the block
|
||||||
|
# here..
|
||||||
|
error "Parent missing! Too bad, because sync is also missing :/",
|
||||||
|
parentRoot = shortHash(top.parent_root),
|
||||||
|
blockSlot = humaneSlotNum(top.slot)
|
||||||
|
quit("So long")
|
||||||
|
|
||||||
|
# If we come this far, we found the state root. The last block on the stack
|
||||||
|
# is the one that produced this particular state, so we can pop it
|
||||||
|
# TODO it might be possible to use the latest block hashes from the state to
|
||||||
|
# do this more efficiently.. whatever!
|
||||||
|
discard parents.pop()
|
||||||
|
|
||||||
|
# Time to replay all the blocks between then and now.
|
||||||
|
while parents.len > 0:
|
||||||
|
let last = parents.pop()
|
||||||
|
skipSlots(node.beaconState, last.parent_root, last.slot)
|
||||||
|
|
||||||
|
let ok = updateState(
|
||||||
|
node.beaconState, last.parent_root, some(last),
|
||||||
|
if parents.len == 0: {} else: {skipValidation})
|
||||||
|
|
||||||
|
doAssert(ok)
|
||||||
|
|
||||||
|
doAssert hash_tree_root_final(node.beaconState) == blck.state_root
|
||||||
|
|
||||||
|
node.headBlock = blck
|
||||||
|
node.headBlockRoot = hash_tree_root_final(blck)
|
||||||
|
node.db.putHead(node.headBlockRoot)
|
||||||
|
|
||||||
|
info "Updated head",
|
||||||
|
stateRoot = shortHash(blck.state_root),
|
||||||
|
headBlockRoot = shortHash(node.headBlockRoot),
|
||||||
|
stateSlot = humaneSlotNum(node.beaconState.slot)
|
||||||
|
|
||||||
|
proc onBeaconBlock(node: BeaconNode, blck: BeaconBlock) =
|
||||||
|
let
|
||||||
|
blockRoot = hash_tree_root_final(blck)
|
||||||
|
stateSlot = node.beaconState.slot
|
||||||
|
|
||||||
|
if node.db.contains(blockRoot, BeaconBlock):
|
||||||
|
debug "Block already seen",
|
||||||
|
slot = humaneSlotNum(blck.slot),
|
||||||
|
stateRoot = shortHash(blck.state_root),
|
||||||
|
blockRoot = shortHash(blockRoot),
|
||||||
|
stateSlot = humaneSlotNum(stateSlot)
|
||||||
|
|
||||||
|
updateHeadBlock(node, node.headBlock)
|
||||||
|
return
|
||||||
|
|
||||||
|
info "Block received",
|
||||||
|
slot = humaneSlotNum(blck.slot),
|
||||||
|
stateRoot = shortHash(blck.state_root),
|
||||||
|
parentRoot = shortHash(blck.parent_root),
|
||||||
|
blockRoot = shortHash(blockRoot)
|
||||||
|
|
||||||
|
# TODO we should now validate the block to ensure that it's sane - but the
|
||||||
|
# only way to do that is to apply it to the state... for now, we assume
|
||||||
|
# all blocks are good!
|
||||||
|
|
||||||
|
# The block has been validated and it's not in the database yet - first, let's
|
||||||
|
# store it there, just to be safe
|
||||||
|
node.db.put(blck)
|
||||||
|
|
||||||
|
# Since this is a good block, we should add its attestations in case we missed
|
||||||
|
# any. If everything checks out, this should lead to the fork choice selecting
|
||||||
|
# this particular block as head, eventually (technically, if we have other
|
||||||
|
# attestations, that might not be the case!)
|
||||||
|
for attestation in blck.body.attestations:
|
||||||
|
# TODO attestation pool needs to be taught to deal with overlapping
|
||||||
|
# attestations!
|
||||||
|
discard # node.onAttestation(attestation)
|
||||||
|
|
||||||
|
if blck.slot <= node.beaconState.slot:
|
||||||
|
# This is some old block that we received (perhaps as the result of a sync)
|
||||||
|
# request. At this point, there's not much we can do, except maybe try to
|
||||||
|
# update the state to the head block (this could have failed before due to
|
||||||
|
# missing blocks!)..
|
||||||
|
# TODO figure out what to do - for example, how to resume setting
|
||||||
|
# the head block...
|
||||||
|
return
|
||||||
|
|
||||||
|
# TODO We have a block that is newer than our latest state. What now??
|
||||||
|
# Here, we choose to update our state eagerly, assuming that the block
|
||||||
|
# is the one that the fork choice would have ended up with anyway, but
|
||||||
|
# is this a sane strategy? Technically, we could wait for more
|
||||||
|
# attestations and update the state lazily only when actually needed,
|
||||||
|
# such as when attesting.
|
||||||
|
# TODO Also, should we update to the block we just got, or run the fork
|
||||||
|
# choice at this point??
|
||||||
|
|
||||||
|
updateHeadBlock(node, blck)
|
||||||
|
|
||||||
|
if stateNeedsSaving(node.beaconState):
|
||||||
|
node.db.put(node.beaconState)
|
||||||
|
|
||||||
|
proc run*(node: BeaconNode) =
|
||||||
|
node.network.subscribe(topicBeaconBlocks) do (blck: BeaconBlock):
|
||||||
|
node.onBeaconBlock(blck)
|
||||||
|
|
||||||
|
node.network.subscribe(topicAttestations) do (attestation: Attestation):
|
||||||
|
node.onAttestation(attestation)
|
||||||
|
|
||||||
let epoch = node.beaconState.getSlotFromTime div SLOTS_PER_EPOCH
|
let epoch = node.beaconState.getSlotFromTime div SLOTS_PER_EPOCH
|
||||||
node.scheduleEpochActions(epoch)
|
node.scheduleEpochActions(epoch)
|
||||||
@ -470,6 +623,10 @@ when isMainModule:
|
|||||||
var node = BeaconNode.init config
|
var node = BeaconNode.init config
|
||||||
|
|
||||||
dynamicLogScope(node = node.config.tcpPort - 50000):
|
dynamicLogScope(node = node.config.tcpPort - 50000):
|
||||||
|
# TODO: while it's nice to cheat by waiting for connections here, we
|
||||||
|
# actually need to make this part of normal application flow -
|
||||||
|
# losing all connections might happen at any time and we should be
|
||||||
|
# prepared to handle it.
|
||||||
waitFor node.connectToNetwork()
|
waitFor node.connectToNetwork()
|
||||||
|
|
||||||
if not waitFor node.sync():
|
if not waitFor node.sync():
|
||||||
@ -484,4 +641,4 @@ when isMainModule:
|
|||||||
SPEC_VERSION
|
SPEC_VERSION
|
||||||
|
|
||||||
node.addLocalValidators()
|
node.addLocalValidators()
|
||||||
node.processBlocks()
|
node.run()
|
||||||
|
@ -43,13 +43,10 @@ import
|
|||||||
# nor get_latest_attestation_target
|
# nor get_latest_attestation_target
|
||||||
# - We use block hashes (Eth2Digest) instead of raw blocks where possible
|
# - We use block hashes (Eth2Digest) instead of raw blocks where possible
|
||||||
|
|
||||||
proc get_parent(db: BeaconChainDB, blck: Eth2Digest): Eth2Digest =
|
|
||||||
db.getBlock(blck).parent_root
|
|
||||||
|
|
||||||
proc get_ancestor(
|
proc get_ancestor(
|
||||||
store: BeaconChainDB, blck: Eth2Digest, slot: Slot): Eth2Digest =
|
store: BeaconChainDB, blck: Eth2Digest, slot: Slot): Eth2Digest =
|
||||||
## Find the ancestor with a specific slot number
|
## Find the ancestor with a specific slot number
|
||||||
let blk = store.getBlock(blck)
|
let blk = store.get(blck, BeaconBlock).get()
|
||||||
if blk.slot == slot:
|
if blk.slot == slot:
|
||||||
blck
|
blck
|
||||||
else:
|
else:
|
||||||
@ -113,16 +110,16 @@ proc lmdGhost*(
|
|||||||
while true: # TODO use a O(log N) implementation instead of O(N^2)
|
while true: # TODO use a O(log N) implementation instead of O(N^2)
|
||||||
let children = blocksChildren[head]
|
let children = blocksChildren[head]
|
||||||
if children.len == 0:
|
if children.len == 0:
|
||||||
return store.getBlock(head)
|
return store.get(head, BeaconBlock).get()
|
||||||
|
|
||||||
# For now we assume that all children are direct descendant of the current head
|
# For now we assume that all children are direct descendant of the current head
|
||||||
let next_slot = store.getBlock(head).slot + 1
|
let next_slot = store.get(head, BeaconBlock).get().slot + 1
|
||||||
for child in children:
|
for child in children:
|
||||||
doAssert store.getBlock(child).slot == next_slot
|
doAssert store.get(child, BeaconBlock).get().slot == next_slot
|
||||||
|
|
||||||
childVotes.clear()
|
childVotes.clear()
|
||||||
for target, votes in rawVoteCount.pairs:
|
for target, votes in rawVoteCount.pairs:
|
||||||
if store.getBlock(target).slot >= next_slot:
|
if store.get(target, BeaconBlock).get().slot >= next_slot:
|
||||||
childVotes.inc(store.get_ancestor(target, next_slot), votes)
|
childVotes.inc(store.get_ancestor(target, next_slot), votes)
|
||||||
|
|
||||||
head = childVotes.largest().key
|
head = childVotes.largest().key
|
||||||
|
@ -225,6 +225,14 @@ func get_initial_beacon_state*(
|
|||||||
|
|
||||||
state
|
state
|
||||||
|
|
||||||
|
# TODO candidate for spec?
|
||||||
|
# https://github.com/ethereum/eth2.0-specs/blob/v0.3.0/specs/core/0_beacon-chain.md#on-genesis
|
||||||
|
func get_initial_beacon_block*(state: BeaconState): BeaconBlock =
|
||||||
|
BeaconBlock(
|
||||||
|
slot: GENESIS_SLOT,
|
||||||
|
state_root: Eth2Digest(data: hash_tree_root(state))
|
||||||
|
)
|
||||||
|
|
||||||
# https://github.com/ethereum/eth2.0-specs/blob/v0.3.0/specs/core/0_beacon-chain.md#get_block_root
|
# https://github.com/ethereum/eth2.0-specs/blob/v0.3.0/specs/core/0_beacon-chain.md#get_block_root
|
||||||
func get_block_root*(state: BeaconState,
|
func get_block_root*(state: BeaconState,
|
||||||
slot: Slot): Eth2Digest =
|
slot: Slot): Eth2Digest =
|
||||||
|
@ -372,8 +372,8 @@ proc processBlock(
|
|||||||
# https://github.com/ethereum/eth2.0-specs/blob/v0.2.0/specs/core/0_beacon-chain.md#slot-1
|
# https://github.com/ethereum/eth2.0-specs/blob/v0.2.0/specs/core/0_beacon-chain.md#slot-1
|
||||||
if not (blck.slot == state.slot):
|
if not (blck.slot == state.slot):
|
||||||
notice "Unexpected block slot number",
|
notice "Unexpected block slot number",
|
||||||
blockSlot = blck.slot,
|
blockSlot = humaneSlotNum(blck.slot),
|
||||||
stateSlot = state.slot
|
stateSlot = humaneSlotNum(state.slot)
|
||||||
return false
|
return false
|
||||||
|
|
||||||
# Spec does not have this check explicitly, but requires that this condition
|
# Spec does not have this check explicitly, but requires that this condition
|
||||||
|
@ -26,7 +26,7 @@ cli do(slots = 1945,
|
|||||||
flags = if validate: {} else: {skipValidation}
|
flags = if validate: {} else: {skipValidation}
|
||||||
genesisState = get_initial_beacon_state(
|
genesisState = get_initial_beacon_state(
|
||||||
makeInitialDeposits(validators, flags), 0, Eth1Data(), flags)
|
makeInitialDeposits(validators, flags), 0, Eth1Data(), flags)
|
||||||
genesisBlock = makeGenesisBlock(genesisState)
|
genesisBlock = get_initial_beacon_block(genesisState)
|
||||||
|
|
||||||
var
|
var
|
||||||
attestations: array[MIN_ATTESTATION_INCLUSION_DELAY, seq[Attestation]]
|
attestations: array[MIN_ATTESTATION_INCLUSION_DELAY, seq[Attestation]]
|
||||||
|
@ -43,7 +43,7 @@ fi
|
|||||||
if [ ! -f $SNAPSHOT_FILE ]; then
|
if [ ! -f $SNAPSHOT_FILE ]; then
|
||||||
$BEACON_NODE_BIN createChain \
|
$BEACON_NODE_BIN createChain \
|
||||||
--chainStartupData:$STARTUP_FILE \
|
--chainStartupData:$STARTUP_FILE \
|
||||||
--out:$SNAPSHOT_FILE # --genesisOffset=2 # Delay in seconds
|
--out:$SNAPSHOT_FILE --genesisOffset=5 # Delay in seconds
|
||||||
fi
|
fi
|
||||||
|
|
||||||
MASTER_NODE_ADDRESS_FILE="$SIMULATION_DIR/node-0/beacon_node.address"
|
MASTER_NODE_ADDRESS_FILE="$SIMULATION_DIR/node-0/beacon_node.address"
|
||||||
@ -93,7 +93,7 @@ for i in $(seq 0 9); do
|
|||||||
if [ "$i" = "0" ]; then
|
if [ "$i" = "0" ]; then
|
||||||
SLEEP="0"
|
SLEEP="0"
|
||||||
else
|
else
|
||||||
SLEEP="1"
|
SLEEP="2"
|
||||||
fi
|
fi
|
||||||
# "multitail" closes the corresponding panel when a command exits, so let's make sure it doesn't exit
|
# "multitail" closes the corresponding panel when a command exits, so let's make sure it doesn't exit
|
||||||
COMMANDS+=( " -cT ansi -t 'node #$i' -l 'sleep $SLEEP; $CMD; echo [node execution completed]; while true; do sleep 100; done'" )
|
COMMANDS+=( " -cT ansi -t 'node #$i' -l 'sleep $SLEEP; $CMD; echo [node execution completed]; while true; do sleep 100; done'" )
|
||||||
|
@ -20,7 +20,7 @@ suite "Attestation pool processing":
|
|||||||
# TODO bls verification is a bit of a bottleneck here
|
# TODO bls verification is a bit of a bottleneck here
|
||||||
genesisState = get_initial_beacon_state(
|
genesisState = get_initial_beacon_state(
|
||||||
makeInitialDeposits(), 0, Eth1Data(), {skipValidation})
|
makeInitialDeposits(), 0, Eth1Data(), {skipValidation})
|
||||||
genesisBlock = makeGenesisBlock(genesisState)
|
genesisBlock = get_initial_beacon_block(genesisState)
|
||||||
genesisRoot = hash_tree_root_final(genesisBlock)
|
genesisRoot = hash_tree_root_final(genesisBlock)
|
||||||
|
|
||||||
test "Can add and retrieve simple attestation":
|
test "Can add and retrieve simple attestation":
|
||||||
@ -44,3 +44,36 @@ suite "Attestation pool processing":
|
|||||||
|
|
||||||
check:
|
check:
|
||||||
attestations.len == 1
|
attestations.len == 1
|
||||||
|
|
||||||
|
|
||||||
|
test "Attestations may arrive in any order":
|
||||||
|
var
|
||||||
|
pool = init(AttestationPool, 42)
|
||||||
|
state = genesisState
|
||||||
|
# Slot 0 is a finalized slot - won't be making attestations for it..
|
||||||
|
discard updateState(
|
||||||
|
state, genesisRoot, none(BeaconBlock), {skipValidation})
|
||||||
|
|
||||||
|
let
|
||||||
|
# Create an attestation for slot 1 signed by the only attester we have!
|
||||||
|
crosslink_committees1 = get_crosslink_committees_at_slot(state, state.slot)
|
||||||
|
attestation1 = makeAttestation(
|
||||||
|
state, genesisRoot, crosslink_committees1[0].committee[0])
|
||||||
|
|
||||||
|
discard updateState(
|
||||||
|
state, genesisRoot, none(BeaconBlock), {skipValidation})
|
||||||
|
|
||||||
|
let
|
||||||
|
crosslink_committees2 = get_crosslink_committees_at_slot(state, state.slot)
|
||||||
|
attestation2 = makeAttestation(
|
||||||
|
state, genesisRoot, crosslink_committees2[0].committee[0])
|
||||||
|
|
||||||
|
# test reverse order
|
||||||
|
pool.add(attestation2, state)
|
||||||
|
pool.add(attestation1, state)
|
||||||
|
|
||||||
|
let attestations = pool.getAttestationsForBlock(
|
||||||
|
state, state.slot + MIN_ATTESTATION_INCLUSION_DELAY)
|
||||||
|
|
||||||
|
check:
|
||||||
|
attestations.len == 1
|
||||||
|
@ -20,7 +20,7 @@ suite "Block processing":
|
|||||||
# TODO bls verification is a bit of a bottleneck here
|
# TODO bls verification is a bit of a bottleneck here
|
||||||
genesisState = get_initial_beacon_state(
|
genesisState = get_initial_beacon_state(
|
||||||
makeInitialDeposits(), 0, Eth1Data(), {})
|
makeInitialDeposits(), 0, Eth1Data(), {})
|
||||||
genesisBlock = makeGenesisBlock(genesisState)
|
genesisBlock = get_initial_beacon_block(genesisState)
|
||||||
|
|
||||||
test "Passes from genesis state, no block":
|
test "Passes from genesis state, no block":
|
||||||
var
|
var
|
||||||
|
@ -61,12 +61,6 @@ func makeInitialDeposits*(
|
|||||||
for i in 0..<n.int:
|
for i in 0..<n.int:
|
||||||
result.add makeDeposit(i + 1, flags)
|
result.add makeDeposit(i + 1, flags)
|
||||||
|
|
||||||
func makeGenesisBlock*(state: BeaconState): BeaconBlock =
|
|
||||||
BeaconBlock(
|
|
||||||
slot: GENESIS_SLOT,
|
|
||||||
state_root: Eth2Digest(data: hash_tree_root(state))
|
|
||||||
)
|
|
||||||
|
|
||||||
func getNextBeaconProposerIndex*(state: BeaconState): ValidatorIndex =
|
func getNextBeaconProposerIndex*(state: BeaconState): ValidatorIndex =
|
||||||
# TODO: This is a special version of get_beacon_proposer_index that takes into
|
# TODO: This is a special version of get_beacon_proposer_index that takes into
|
||||||
# account the partial update done at the start of slot processing -
|
# account the partial update done at the start of slot processing -
|
||||||
|
Loading…
x
Reference in New Issue
Block a user