From 2d307e2257b5d7c80fb0aabe1b89d2c21bbe8112 Mon Sep 17 00:00:00 2001 From: Jacek Sieka Date: Wed, 20 Feb 2019 22:42:17 -0600 Subject: [PATCH] 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 --- beacon_chain.nimble | 2 +- beacon_chain/attestation_pool.nim | 7 +- beacon_chain/beacon_chain_db.nim | 105 +++++----- beacon_chain/beacon_node.nim | 311 ++++++++++++++++++++++-------- beacon_chain/fork_choice.nim | 13 +- beacon_chain/spec/beaconstate.nim | 8 + beacon_chain/state_transition.nim | 4 +- research/state_sim.nim | 2 +- tests/simulation/start.sh | 4 +- tests/test_attestation_pool.nim | 35 +++- tests/test_state_transition.nim | 2 +- tests/testutil.nim | 6 - 12 files changed, 336 insertions(+), 163 deletions(-) diff --git a/beacon_chain.nimble b/beacon_chain.nimble index f431e1cbb..4e1095410 100644 --- a/beacon_chain.nimble +++ b/beacon_chain.nimble @@ -15,7 +15,7 @@ bin = @[ requires "nim >= 0.19.0", "eth", "nimcrypto", - "https://github.com/status-im/nim-blscurve#master", + "blscurve", "ranges", "chronicles", "confutils", diff --git a/beacon_chain/attestation_pool.nim b/beacon_chain/attestation_pool.nim index 334329983..b5370c095 100644 --- a/beacon_chain/attestation_pool.nim +++ b/beacon_chain/attestation_pool.nim @@ -200,11 +200,12 @@ proc add*(pool: var AttestationPool, ", startingSlot: " & $humaneSlotNum(pool.startingSlot) if pool.slots.len == 0: - # When receiving the first attestation, we want to avoid adding a lot of - # empty SlotData items, so we'll cheat a bit here + # Because the first attestations may arrive in any order, we'll make sure + # to start counting at the last finalized epoch start slot - anything + # earlier than that is thrown out by the above check info "First attestation!", attestationSlot = $humaneSlotNum(attestationSlot) - pool.startingSlot = attestationSlot + pool.startingSlot = state.finalized_epoch.get_epoch_start_slot() if pool.startingSlot + pool.slots.len.Slot <= attestationSlot: debug "Growing attestation pool", diff --git a/beacon_chain/beacon_chain_db.nim b/beacon_chain/beacon_chain_db.nim index fafc617be..d6902a978 100644 --- a/beacon_chain/beacon_chain_db.nim +++ b/beacon_chain/beacon_chain_db.nim @@ -4,88 +4,71 @@ import spec/[datatypes, digest, crypto], eth/trie/db, ssz - type BeaconChainDB* = ref object backend: TrieDatabaseRef DbKeyKind = enum - kLastFinalizedState + kHashToState kHashToBlock - kSlotToBlockHash - kSlotToState - kHashToValidatorRegistryChangeLog + kHeadBlock -proc lastFinalizedStateKey(): array[1, byte] = - result[0] = byte ord(kLastFinalizedState) + DbTypes = BeaconState | BeaconBlock -proc hashToBlockKey(h: Eth2Digest): array[32 + 1, byte] = - result[0] = byte ord(kHashToBlock) - result[1 .. ^1] = h.data +func subkey(kind: DbKeyKind): array[1, byte] = + result[0] = byte ord(kind) -proc slotToBlockHashKey(s: Slot): array[sizeof(Slot) + 1, byte] = - result[0] = byte ord(kSlotToBlockHash) - copyMem(addr result[1], unsafeAddr(s), sizeof(s)) +func subkey[N: static int](kind: DbKeyKind, key: array[N, byte]): + array[N + 1, byte] = + result[0] = byte ord(kind) + result[1 .. ^1] = key -proc slotToStateKey(s: Slot): array[sizeof(Slot) + 1, byte] = - result[0] = byte ord(kSlotToState) - copyMem(addr result[1], unsafeAddr(s), sizeof(s)) +func subkey(kind: type BeaconState, key: Eth2Digest): auto = + subkey(kHashToState, key.data) -proc hashToValidatorRegistryChangeLogKey(deltaChainTip: Eth2Digest): array[32 + 1, byte] = - result[0] = byte ord(kHashToValidatorRegistryChangeLog) - result[1 .. ^1] = deltaChainTip.data +func subkey(kind: type BeaconBlock, key: Eth2Digest): auto = + subkey(kHashToBlock, key.data) proc init*(T: type BeaconChainDB, backend: TrieDatabaseRef): BeaconChainDB = new result result.backend = backend -proc lastFinalizedState*(db: BeaconChainDB): BeaconState = - let res = db.backend.get(lastFinalizedStateKey()) - if res.len == 0: - raise newException(Exception, "Internal error: Database has no finalized state") - ssz.deserialize(res, BeaconState).get +proc put*(db: BeaconChainDB, key: Eth2Digest, value: BeaconBlock) = + db.backend.put(subkey(type value, key), ssz.serialize(value)) -proc isInitialized*(db: BeaconChainDB): bool = - db.backend.get(lastFinalizedStateKey()).len != 0 +proc putHead*(db: BeaconChainDB, key: Eth2Digest) = + db.backend.put(subkey(kHeadBlock), key.data) # TODO head block? -proc persistState*(db: BeaconChainDB, s: BeaconState) = - if s.slot != GENESIS_SLOT: - # TODO: Verify incoming state slot is higher than lastFinalizedState one - discard - else: - # Make sure we have no states - assert(not db.isInitialized) +proc put*(db: BeaconChainDB, key: Eth2Digest, value: BeaconState) = + db.backend.put(subkey(type value, key), ssz.serialize(value)) - var prevState: BeaconState - if s.slot != GENESIS_SLOT: - 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 +proc put*(db: BeaconChainDB, value: DbTypes) = + db.put(hash_tree_root_final(value), value) - let serializedState = ssz.serialize(s) - # TODO: Consider mapping slots and last pointer to state hashes to avoid - # 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)) +proc get(db: BeaconChainDB, key: auto, T: typedesc): Option[T] = + let res = db.backend.get(key) if res.len != 0: - output = ssz.deserialize(res, BeaconBlock).get - true + ssz.deserialize(res, T) else: - false + none(T) -proc getBlock*(db: BeaconChainDB, hash: Eth2Digest): BeaconBlock = - if not db.getBlock(hash, result): - raise newException(Exception, "Block not found") +# TODO: T: type DbTypes fails with compiler error.. investigate +proc get*(db: BeaconChainDB, key: Eth2Digest, T: type BeaconBlock): Option[T] = + 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)) diff --git a/beacon_chain/beacon_node.nim b/beacon_chain/beacon_node.nim index 796cd02f0..d8df177a0 100644 --- a/beacon_chain/beacon_node.nim +++ b/beacon_chain/beacon_node.nim @@ -17,7 +17,6 @@ type attachedValidators: ValidatorPool attestationPool: AttestationPool mainchainMonitor: MainchainMonitor - lastScheduledEpoch: Epoch headBlock: BeaconBlock headBlockRoot: Eth2Digest blocksChildren: Table[Eth2Digest, seq[Eth2Digest]] @@ -44,6 +43,8 @@ proc ensureNetworkKeys*(dataDir: string): KeyPair = # if necessary return newKeyPair() +proc updateHeadBlock(node: BeaconNode, blck: BeaconBlock) + proc init*(T: type BeaconNode, conf: BeaconNodeConf): T = new result result.config = conf @@ -55,10 +56,24 @@ proc init*(T: type BeaconNode, conf: BeaconNodeConf): T = let trieDB = trieDB newChainDb(string conf.dataDir) result.db = BeaconChainDB.init(trieDB) - if not result.db.isInitialized: - # Use stateSnapshot as genesis - info "Initializing DB" - result.db.persistState(result.config.stateSnapshot.get) + # TODO does it really make sense to load from DB if a state snapshot has been + # specified on command line? potentially, this should be the other way + # around... + 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) @@ -91,11 +106,9 @@ proc connectToNetwork(node: BeaconNode) {.async.} = node.network.startListening() proc sync*(node: BeaconNode): Future[bool] {.async.} = - let persistedState = node.db.lastFinalizedState() - if persistedState.slotDistanceFromNow() > WEAK_SUBJECTVITY_PERIOD.int64: + if node.beaconState.slotDistanceFromNow() > WEAK_SUBJECTVITY_PERIOD.int64: node.beaconState = await obtainTrustedStateSnapshot(node.db) else: - node.beaconState = persistedState var targetSlot = node.beaconState.getSlotFromTime() let t = now() @@ -107,19 +120,26 @@ proc sync*(node: BeaconNode): Future[bool] {.async.} = finalized_epoch = humaneEpochNum(node.beaconState.finalized_epoch), target_slot_epoch = humaneEpochNum(targetSlot.slot_to_epoch) - while node.beaconState.finalized_epoch < targetSlot.slot_to_epoch: - var (peer, changeLog) = await node.network.getValidatorChangeLog( - node.beaconState.validator_registry_delta_chain_tip) + # TODO: sync is called at the beginning of the program, but doing this kind + # of catching up here is wrong - if we fall behind on processing + # 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: - error "Failed to sync with any peer" - return false + # if peer == nil: + # error "Failed to sync with any peer" + # return false - if applyValidatorChangeLog(changeLog, node.beaconState): - node.db.persistState(node.beaconState) - node.db.persistBlock(changeLog.signedBlock) - else: - warn "Ignoring invalid validator change log", sentFrom = peer + # if applyValidatorChangeLog(changeLog, node.beaconState): + # node.db.persistState(node.beaconState) + # node.db.persistBlock(changeLog.signedBlock) + # else: + # warn "Ignoring invalid validator change log", sentFrom = peer return true @@ -256,6 +276,7 @@ proc proposeBlock(node: BeaconNode, info "Block proposed", slot = humaneSlotNum(slot), stateRoot = shortHash(newBlock.state_root), + parentRoot = shortHash(newBlock.parent_root), validator = shortValidatorKey(node, validator.idx), idx = validator.idx @@ -356,88 +377,220 @@ proc scheduleEpochActions(node: BeaconNode, epoch: Epoch) = node, validator, slot, crosslink_committee.shard, crosslink_committee.committee.len, i) - node.lastScheduledEpoch = epoch let 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", fromNow = (at - fastEpochTime()) div 1000, epoch = humaneEpochNum(nextEpoch) addTimer(at) do (p: pointer): - if node.lastScheduledEpoch != nextEpoch: - node.scheduleEpochActions(nextEpoch) + node.scheduleEpochActions(nextEpoch) proc stateNeedsSaving(s: BeaconState): bool = # TODO: Come up with a better predicate logic s.slot mod stateStoragePeriod == 0 -proc processBlocks*(node: BeaconNode) = - node.network.subscribe(topicBeaconBlocks) do (newBlock: BeaconBlock): - let stateSlot = node.beaconState.slot - info "Block received", - slot = humaneSlotNum(newBlock.slot), - stateRoot = shortHash(newBlock.state_root), +proc onAttestation(node: BeaconNode, attestation: Attestation) = + let participants = get_attestation_participants( + node.beaconState, attestation.data, attestation.aggregation_bitfield). + mapIt(shortValidatorKey(node, it)) + + info "Attestation received", + slot = humaneSlotNum(attestation.data.slot), + shard = attestation.data.shard, + signature = shortHash(attestation.aggregate_signature), + participants, + beaconBlockRoot = shortHash(attestation.data.beacon_block_root) + + 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) - # TODO: This should be replaced with the real fork-choice rule - if newBlock.slot <= stateSlot: - debug "Ignoring block" - return + updateHeadBlock(node, node.headBlock) + return - let newBlockRoot = hash_tree_root_final(newBlock) + info "Block received", + slot = humaneSlotNum(blck.slot), + stateRoot = shortHash(blck.state_root), + parentRoot = shortHash(blck.parent_root), + blockRoot = shortHash(blockRoot) - var state = node.beaconState - if stateSlot + 1 < newBlock.slot: - info "Advancing state past slot gap", - blockSlot = humaneSlotNum(newBlock.slot), - stateSlot = humaneSlotNum(stateSlot) + # 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! - for slot in stateSlot + 1 ..< newBlock.slot: - let ok = updateState(state, node.headBlockRoot, none[BeaconBlock](), {}) - doAssert ok + # 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) - let ok = updateState(state, node.headBlockRoot, some(newBlock), {}) - if not ok: - debug "Ignoring non-validating block" - return + # 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) - node.headBlock = newBlock - node.headBlockRoot = newBlockRoot - node.beaconState = state + 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 - if stateNeedsSaving(node.beaconState): - node.db.persistState(node.beaconState) + # 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?? - node.db.persistBlock(newBlock) + updateHeadBlock(node, blck) - # TODO: - # - # 1. Check for missing blocks and obtain them - # - # 2. Apply fork-choice rule (update node.headBlock) - # - # 3. Peform block processing / state recalculation / etc - # + if stateNeedsSaving(node.beaconState): + node.db.put(node.beaconState) - let epoch = newBlock.slot.epoch - if epoch != node.lastScheduledEpoch: - node.scheduleEpochActions(epoch) +proc run*(node: BeaconNode) = + node.network.subscribe(topicBeaconBlocks) do (blck: BeaconBlock): + node.onBeaconBlock(blck) - node.network.subscribe(topicAttestations) do (a: Attestation): - let participants = get_attestation_participants( - node.beaconState, a.data, a.aggregation_bitfield). - mapIt(shortValidatorKey(node, it)) - - info "Attestation received", - slot = humaneSlotNum(a.data.slot), - shard = a.data.shard, - signature = shortHash(a.aggregate_signature), - participants, - beaconBlockRoot = shortHash(a.data.beacon_block_root) - - node.attestationPool.add(a, node.beaconState) + node.network.subscribe(topicAttestations) do (attestation: Attestation): + node.onAttestation(attestation) let epoch = node.beaconState.getSlotFromTime div SLOTS_PER_EPOCH node.scheduleEpochActions(epoch) @@ -470,6 +623,10 @@ when isMainModule: var node = BeaconNode.init config 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() if not waitFor node.sync(): @@ -484,4 +641,4 @@ when isMainModule: SPEC_VERSION node.addLocalValidators() - node.processBlocks() + node.run() diff --git a/beacon_chain/fork_choice.nim b/beacon_chain/fork_choice.nim index 2f1cb2e8d..2b9571df2 100644 --- a/beacon_chain/fork_choice.nim +++ b/beacon_chain/fork_choice.nim @@ -43,13 +43,10 @@ import # nor get_latest_attestation_target # - 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( store: BeaconChainDB, blck: Eth2Digest, slot: Slot): Eth2Digest = ## Find the ancestor with a specific slot number - let blk = store.getBlock(blck) + let blk = store.get(blck, BeaconBlock).get() if blk.slot == slot: blck else: @@ -113,16 +110,16 @@ proc lmdGhost*( while true: # TODO use a O(log N) implementation instead of O(N^2) let children = blocksChildren[head] 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 - let next_slot = store.getBlock(head).slot + 1 + let next_slot = store.get(head, BeaconBlock).get().slot + 1 for child in children: - doAssert store.getBlock(child).slot == next_slot + doAssert store.get(child, BeaconBlock).get().slot == next_slot childVotes.clear() 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) head = childVotes.largest().key diff --git a/beacon_chain/spec/beaconstate.nim b/beacon_chain/spec/beaconstate.nim index 591589fa1..9075fad5f 100644 --- a/beacon_chain/spec/beaconstate.nim +++ b/beacon_chain/spec/beaconstate.nim @@ -225,6 +225,14 @@ func get_initial_beacon_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 func get_block_root*(state: BeaconState, slot: Slot): Eth2Digest = diff --git a/beacon_chain/state_transition.nim b/beacon_chain/state_transition.nim index e99e273cb..df17f7f2f 100644 --- a/beacon_chain/state_transition.nim +++ b/beacon_chain/state_transition.nim @@ -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 if not (blck.slot == state.slot): notice "Unexpected block slot number", - blockSlot = blck.slot, - stateSlot = state.slot + blockSlot = humaneSlotNum(blck.slot), + stateSlot = humaneSlotNum(state.slot) return false # Spec does not have this check explicitly, but requires that this condition diff --git a/research/state_sim.nim b/research/state_sim.nim index 7582aecba..bf7e2b817 100644 --- a/research/state_sim.nim +++ b/research/state_sim.nim @@ -26,7 +26,7 @@ cli do(slots = 1945, flags = if validate: {} else: {skipValidation} genesisState = get_initial_beacon_state( makeInitialDeposits(validators, flags), 0, Eth1Data(), flags) - genesisBlock = makeGenesisBlock(genesisState) + genesisBlock = get_initial_beacon_block(genesisState) var attestations: array[MIN_ATTESTATION_INCLUSION_DELAY, seq[Attestation]] diff --git a/tests/simulation/start.sh b/tests/simulation/start.sh index 4dcd1b37d..078c200bd 100755 --- a/tests/simulation/start.sh +++ b/tests/simulation/start.sh @@ -43,7 +43,7 @@ fi if [ ! -f $SNAPSHOT_FILE ]; then $BEACON_NODE_BIN createChain \ --chainStartupData:$STARTUP_FILE \ - --out:$SNAPSHOT_FILE # --genesisOffset=2 # Delay in seconds + --out:$SNAPSHOT_FILE --genesisOffset=5 # Delay in seconds fi 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 SLEEP="0" else - SLEEP="1" + SLEEP="2" fi # "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'" ) diff --git a/tests/test_attestation_pool.nim b/tests/test_attestation_pool.nim index 7516dc2ad..9a0694731 100644 --- a/tests/test_attestation_pool.nim +++ b/tests/test_attestation_pool.nim @@ -20,7 +20,7 @@ suite "Attestation pool processing": # TODO bls verification is a bit of a bottleneck here genesisState = get_initial_beacon_state( makeInitialDeposits(), 0, Eth1Data(), {skipValidation}) - genesisBlock = makeGenesisBlock(genesisState) + genesisBlock = get_initial_beacon_block(genesisState) genesisRoot = hash_tree_root_final(genesisBlock) test "Can add and retrieve simple attestation": @@ -44,3 +44,36 @@ suite "Attestation pool processing": check: 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 diff --git a/tests/test_state_transition.nim b/tests/test_state_transition.nim index b6300a2de..f25c1772d 100644 --- a/tests/test_state_transition.nim +++ b/tests/test_state_transition.nim @@ -20,7 +20,7 @@ suite "Block processing": # TODO bls verification is a bit of a bottleneck here genesisState = get_initial_beacon_state( makeInitialDeposits(), 0, Eth1Data(), {}) - genesisBlock = makeGenesisBlock(genesisState) + genesisBlock = get_initial_beacon_block(genesisState) test "Passes from genesis state, no block": var diff --git a/tests/testutil.nim b/tests/testutil.nim index 94aa35a1c..966f35767 100644 --- a/tests/testutil.nim +++ b/tests/testutil.nim @@ -61,12 +61,6 @@ func makeInitialDeposits*( for i in 0..