add initial block pool (#139)

* implement in-memory block graph
* store tail block in database
* resolve unknown parents by syncing them from peers
* introduce concept of resolved blocks and attestations - those that
follow minimal protocol rules
* update state head lazily
* log more stuff
* shortHash -> shortLog
* start 9/10 beacon nodes by default, last can be started manually
* see also #134
* fix start.sh epoch length
This commit is contained in:
Jacek Sieka 2019-02-28 15:21:29 -06:00 committed by GitHub
parent 20f99db058
commit 125231d321
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 986 additions and 396 deletions

View File

@ -2,7 +2,7 @@ import
deques, options, sequtils, tables,
chronicles,
./spec/[beaconstate, datatypes, crypto, digest, helpers, validator], extras,
./beacon_chain_db, ./ssz
./beacon_chain_db, ./ssz, ./block_pool
type
Validation* = object
@ -25,6 +25,7 @@ type
AttestationEntry* = object
data*: AttestationData
blck: BlockRef
validations*: seq[Validation] ## \
## Instead of aggregating the signatures eagerly, we simply dump them in
## this seq and aggregate only when needed
@ -39,6 +40,10 @@ type
## TODO this could be a Table[AttestationData, seq[Validation] or something
## less naive
UnresolvedAttestation* = object
attestation: Attestation
tries: int
AttestationPool* = object
## The attestation pool keeps all attestations that are known to the
## client - each attestation counts as votes towards the fork choice
@ -54,12 +59,21 @@ type
## Generally, we keep attestations only until a slot has been finalized -
## after that, they may no longer affect fork choice.
proc init*(T: type AttestationPool, dummy: int): T =
result.slots = initDeque[SlotData]()
blockPool: BlockPool
unresolved: Table[Eth2Digest, UnresolvedAttestation]
proc init*(T: type AttestationPool, blockPool: BlockPool): T =
T(
slots: initDeque[SlotData](),
blockPool: blockPool,
unresolved: initTable[Eth2Digest, UnresolvedAttestation]()
)
proc overlaps(a, b: seq[byte]): bool =
for i in 0..<a.len:
if (a[i] and b[i]) > 0'u8: return true
if (a[i] and b[i]) > 0'u8:
return true
proc combineBitfield(tgt: var seq[byte], src: seq[byte]) =
for i in 0 ..< tgt.len:
@ -179,13 +193,10 @@ proc validate(
true
proc add*(pool: var AttestationPool,
attestation: Attestation,
state: BeaconState) =
if not validate(state, attestation, {skipValidation}): return
proc slotIndex(
pool: var AttestationPool, state: BeaconState, attestationSlot: Slot): int =
## Grow and garbage collect pool, returning the deque index of the slot
let
attestationSlot = attestation.data.slot
# We keep a sliding window of attestations, roughly from the last finalized
# epoch to now, because these are the attestations that may affect the voting
# outcome. Some of these attestations will already have been added to blocks,
@ -227,8 +238,20 @@ proc add*(pool: var AttestationPool,
pool.slots.popFirst()
pool.startingSlot += 1
int(attestationSlot - pool.startingSlot)
proc add*(pool: var AttestationPool,
state: BeaconState,
attestation: Attestation) =
if not validate(state, attestation, {skipValidation}):
return
# TODO inefficient data structures..
let
slotData = addr pool.slots[attestationSlot - pool.startingSlot]
attestationSlot = attestation.data.slot
idx = pool.slotIndex(state, attestationSlot)
slotData = addr pool.slots[idx]
validation = Validation(
aggregation_bitfield: attestation.aggregation_bitfield,
custody_bitfield: attestation.custody_bitfield,
@ -256,18 +279,43 @@ proc add*(pool: var AttestationPool,
if not found:
a.validations.add(validation)
info "Attestation resolved",
slot = humaneSlotNum(attestation.data.slot),
shard = attestation.data.shard,
beaconBlockRoot = shortLog(attestation.data.beacon_block_root),
justifiedEpoch = humaneEpochNum(attestation.data.justified_epoch),
justifiedBlockRoot = shortLog(attestation.data.justified_block_root),
signature = shortLog(attestation.aggregate_signature),
validations = a.validations.len() # TODO popcount of union
found = true
break
if not found:
slotData.attestations.add(AttestationEntry(
data: attestation.data,
validations: @[validation]
))
if (let blck = pool.blockPool.getOrResolve(
attestation.data.beacon_block_root); blck != nil):
slotData.attestations.add(AttestationEntry(
data: attestation.data,
blck: blck,
validations: @[validation]
))
info "Attestation resolved",
slot = humaneSlotNum(attestation.data.slot),
shard = attestation.data.shard,
beaconBlockRoot = shortLog(attestation.data.beacon_block_root),
justifiedEpoch = humaneEpochNum(attestation.data.justified_epoch),
justifiedBlockRoot = shortLog(attestation.data.justified_block_root),
signature = shortLog(attestation.aggregate_signature),
validations = 1
else:
pool.unresolved[attestation.data.beacon_block_root] =
UnresolvedAttestation(
attestation: attestation,
)
proc getAttestationsForBlock*(pool: AttestationPool,
lastState: BeaconState,
newBlockSlot: Slot): seq[Attestation] =
if newBlockSlot - GENESIS_SLOT < MIN_ATTESTATION_INCLUSION_DELAY:
debug "Too early for attestations",
@ -321,3 +369,23 @@ proc getAttestationsForBlock*(pool: AttestationPool,
if result.len >= MAX_ATTESTATIONS:
return
proc resolve*(pool: var AttestationPool, state: BeaconState) =
var done: seq[Eth2Digest]
var resolved: seq[Attestation]
for k, v in pool.unresolved.mpairs():
if v.tries > 8 or v.attestation.data.slot < pool.startingSlot:
done.add(k)
else:
if pool.blockPool.get(k).isSome():
resolved.add(v.attestation)
done.add(k)
else:
inc v.tries
for k in done:
pool.unresolved.del(k)
for a in resolved:
pool.add(state, a)

View File

@ -11,7 +11,8 @@ type
DbKeyKind = enum
kHashToState
kHashToBlock
kHeadBlock
kHeadBlock # Pointer to the most recent block seen
kTailBlock # Pointer to the earliest finalized block
func subkey(kind: DbKeyKind): array[1, byte] =
result[0] = byte ord(kind)
@ -31,12 +32,6 @@ proc init*(T: type BeaconChainDB, backend: TrieDatabaseRef): BeaconChainDB =
new result
result.backend = backend
proc putBlock*(db: BeaconChainDB, key: Eth2Digest, value: BeaconBlock) =
db.backend.put(subkey(type value, key), ssz.serialize(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
# TODO: it might be necessary to introduce the concept of a "last finalized
@ -52,11 +47,20 @@ proc putState*(db: BeaconChainDB, key: Eth2Digest, value: BeaconState) =
# significant (days), meaning replay might be expensive.
db.backend.put(subkey(type value, key), ssz.serialize(value))
proc putState*(db: BeaconChainDB, value: BeaconState) =
db.putState(hash_tree_root_final(value), value)
proc putBlock*(db: BeaconChainDB, key: Eth2Digest, value: BeaconBlock) =
db.backend.put(subkey(type value, key), ssz.serialize(value))
proc putBlock*(db: BeaconChainDB, value: BeaconBlock) =
db.putBlock(hash_tree_root_final(value), value)
proc putState*(db: BeaconChainDB, value: BeaconState) =
db.putState(hash_tree_root_final(value), value)
proc putHeadBlock*(db: BeaconChainDB, key: Eth2Digest) =
db.backend.put(subkey(kHeadBlock), key.data) # TODO head block?
proc putTailBlock*(db: BeaconChainDB, key: Eth2Digest) =
db.backend.put(subkey(kTailBlock), key.data)
proc get(db: BeaconChainDB, key: auto, T: typedesc): Option[T] =
let res = db.backend.get(key)
@ -71,15 +75,11 @@ proc getBlock*(db: BeaconChainDB, key: Eth2Digest): Option[BeaconBlock] =
proc getState*(db: BeaconChainDB, key: Eth2Digest): Option[BeaconState] =
db.get(subkey(BeaconState, key), BeaconState)
proc getHead*(db: BeaconChainDB): Option[BeaconBlock] =
let key = db.backend.get(subkey(kHeadBlock))
if key.len == sizeof(Eth2Digest):
var tmp: Eth2Digest
copyMem(addr tmp, unsafeAddr key[0], sizeof(tmp))
proc getHeadBlock*(db: BeaconChainDB): Option[Eth2Digest] =
db.get(subkey(kHeadBlock), Eth2Digest)
db.getBlock(tmp)
else:
none(BeaconBlock)
proc getTailBlock*(db: BeaconChainDB): Option[Eth2Digest] =
db.get(subkey(kTailBlock), Eth2Digest)
proc containsBlock*(
db: BeaconChainDB, key: Eth2Digest): bool =
@ -89,24 +89,15 @@ proc containsState*(
db: BeaconChainDB, key: Eth2Digest): bool =
db.backend.contains(subkey(BeaconBlock, key))
proc getAncestors*(
db: BeaconChainDB, blck: BeaconBlock,
predicate: proc(blck: BeaconBlock): bool = nil): seq[BeaconBlock] =
iterator getAncestors*(db: BeaconChainDB, root: Eth2Digest):
tuple[root: Eth2Digest, blck: BeaconBlock] =
## Load a chain of ancestors for blck - returns a list of blocks with the
## oldest block last (blck will be at result[0]).
##
## The search will go on until the ancestor cannot be found (or slot 0) or
## the predicate returns true (you found what you were looking for) - the list
## will include the last block as well
## TODO maybe turn into iterator? or add iterator also?
## The search will go on until the ancestor cannot be found.
result = @[blck]
var root = root
while (let blck = db.getBlock(root); blck.isSome()):
yield (root, blck.get())
while result[^1].slot > 0.Slot:
let parent = db.getBlock(result[^1].parent_root)
if parent.isNone(): break
result.add parent.get()
if predicate != nil and predicate(parent.get()): break
root = blck.get().parent_root

View File

@ -3,23 +3,22 @@ import
chronos, chronicles, confutils, eth/[p2p, keys],
spec/[datatypes, digest, crypto, beaconstate, helpers, validator], conf, time,
state_transition, fork_choice, ssz, beacon_chain_db, validator_pool, extras,
attestation_pool,
attestation_pool, block_pool,
mainchain_monitor, sync_protocol, gossipsub_protocol, trusted_state_snapshots,
eth/trie/db, eth/trie/backends/rocksdb_backend
type
BeaconNode* = ref object
beaconState*: BeaconState
network*: EthereumNode
db*: BeaconChainDB
config*: BeaconNodeConf
keys*: KeyPair
attachedValidators: ValidatorPool
blockPool: BlockPool
state: StateData
attestationPool: AttestationPool
mainchainMonitor: MainchainMonitor
headBlock: BeaconBlock
headBlockRoot: Eth2Digest
blocksChildren: Table[Eth2Digest, seq[Eth2Digest]]
potentialHeads: seq[Eth2Digest]
const
version = "v0.1" # TODO: read this from the nimble file
@ -27,14 +26,13 @@ const
topicBeaconBlocks = "ethereum/2.1/beacon_chain/blocks"
topicAttestations = "ethereum/2.1/beacon_chain/attestations"
stateStoragePeriod = SLOTS_PER_EPOCH.uint64 * 10 # Save states once per this number of slots. TODO: Find a good number.
func shortHash(x: auto): string =
($x)[0..7]
topicfetchBlocks = "ethereum/2.1/beacon_chain/fetch"
func shortValidatorKey(node: BeaconNode, validatorIdx: int): string =
($node.beaconState.validator_registry[validatorIdx].pubkey)[0..7]
($node.state.data.validator_registry[validatorIdx].pubkey)[0..7]
func slotStart(node: BeaconNode, slot: Slot): Timestamp =
node.state.data.slotStart(slot)
proc ensureNetworkKeys*(dataDir: string): KeyPair =
# TODO:
@ -43,37 +41,41 @@ 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
result.attachedValidators = ValidatorPool.init
init result.attestationPool, 42 # TODO compile failure without the dummy int??
init result.mainchainMonitor, "", Port(0) # TODO: specify geth address and port
let trieDB = trieDB newChainDb(string conf.dataDir)
result.db = BeaconChainDB.init(trieDB)
# TODO this is problably not the right place to ensure that db is sane..
# 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(); 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.putState(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.putBlock(result.headBlock)
let headBlock = result.db.getHeadBlock()
if headBlock.isNone():
let
tailState = result.config.stateSnapshot.get()
tailBlock = get_initial_beacon_block(tailState)
blockRoot = hash_tree_root_final(tailBlock)
notice "Creating new database from snapshot",
blockRoot = shortLog(blockRoot),
stateRoot = shortLog(tailBlock.state_root),
fork = tailState.fork,
validators = tailState.validator_registry.len()
result.db.putState(tailState)
result.db.putBlock(tailBlock)
result.db.putTailBlock(blockRoot)
result.db.putHeadBlock(blockRoot)
result.blockPool = BlockPool.init(result.db)
result.attestationPool = AttestationPool.init(result.blockPool)
result.keys = ensureNetworkKeys(string conf.dataDir)
@ -82,7 +84,14 @@ proc init*(T: type BeaconNode, conf: BeaconNodeConf): T =
address.tcpPort = Port(conf.tcpPort)
address.udpPort = Port(conf.udpPort)
result.network = newEthereumNode(result.keys, address, 0, nil, clientId, minPeers = 1)
result.network =
newEthereumNode(result.keys, address, 0, nil, clientId, minPeers = 1)
let
head = result.blockPool.get(result.db.getHeadBlock().get())
result.state = result.blockPool.loadTailState()
result.blockPool.updateState(result.state, head.get().refs)
writeFile(string(conf.dataDir) / "beacon_node.address",
$result.network.listeningAddress)
@ -105,18 +114,24 @@ proc connectToNetwork(node: BeaconNode) {.async.} =
await node.network.connectToNetwork(bootstrapNodes)
proc sync*(node: BeaconNode): Future[bool] {.async.} =
if node.beaconState.slotDistanceFromNow() > WEAK_SUBJECTVITY_PERIOD.int64:
node.beaconState = await obtainTrustedStateSnapshot(node.db)
if node.state.data.slotDistanceFromNow() > WEAK_SUBJECTVITY_PERIOD.int64:
# node.state.data = await obtainTrustedStateSnapshot(node.db)
return false
else:
var targetSlot = node.beaconState.getSlotFromTime()
# TODO waiting for genesis should probably be moved elsewhere.. it has
# little to do with syncing..
let t = now()
if t < node.beaconState.genesisTime * 1000:
await sleepAsync int(node.beaconState.genesisTime * 1000 - t)
if t < node.state.data.genesis_time * 1000:
notice "Waiting for genesis",
fromNow = int(node.state.data.genesis_time * 1000 - t) div 1000
await sleepAsync int(node.state.data.genesis_time * 1000 - t)
let
targetSlot = node.state.data.getSlotFromTime()
# TODO: change this to a full sync / block download
info "Syncing state from remote peers",
finalized_epoch = humaneEpochNum(node.beaconState.finalized_epoch),
finalized_epoch = humaneEpochNum(node.state.data.finalized_epoch),
target_slot_epoch = humaneEpochNum(targetSlot.slot_to_epoch)
# TODO: sync is called at the beginning of the program, but doing this kind
@ -126,6 +141,8 @@ proc sync*(node: BeaconNode): Future[bool] {.async.} =
# 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.
# TOOD we now detect during epoch scheduling if we're very far behind -
# that would potentially be a good place to run the sync (?)
# while node.beaconState.finalized_epoch < targetSlot.slot_to_epoch:
# var (peer, changeLog) = await node.network.getValidatorChangeLog(
# node.beaconState.validator_registry_delta_chain_tip)
@ -156,7 +173,7 @@ proc addLocalValidators*(node: BeaconNode) =
privKey = validator.privKey
pubKey = privKey.pubKey()
let idx = node.beaconState.validator_registry.findIt(it.pubKey == pubKey)
let idx = node.state.data.validator_registry.findIt(it.pubKey == pubKey)
if idx == -1:
warn "Validator not in registry", pubKey
else:
@ -167,9 +184,51 @@ proc addLocalValidators*(node: BeaconNode) =
info "Local validators attached ", count = node.attachedValidators.count
proc getAttachedValidator(node: BeaconNode, idx: int): AttachedValidator =
let validatorKey = node.beaconState.validator_registry[idx].pubkey
let validatorKey = node.state.data.validator_registry[idx].pubkey
return node.attachedValidators.getValidator(validatorKey)
proc updateHead(node: BeaconNode) =
# TODO placeholder logic for running the fork choice
var
head = node.state.blck
headSlot = node.state.data.slot
# LRB fork choice - latest resolved block :)
for ph in node.potentialHeads:
let blck = node.blockPool.get(ph)
if blck.isNone():
continue
if blck.get().data.slot >= headSlot:
head = blck.get().refs
headSlot = blck.get().data.slot
node.potentialHeads.setLen(0)
if head.root == node.state.blck.root:
debug "No new head found",
stateRoot = shortLog(node.state.root),
blockRoot = shortLog(node.state.blck.root),
stateSlot = humaneSlotNum(node.state.data.slot)
return
node.blockPool.updateState(node.state, head)
# TODO this should probably be in blockpool, but what if updateState is
# called with a non-head block?
node.db.putHeadBlock(node.state.blck.root)
# TODO we should save the state every now and then, but which state do we
# save? When we receive a block and process it, the state from a
# particular epoch may become finalized - but we no longer have it!
# One thing that would work would be to replay from some earlier
# state (the tail?) to the new finalized state, then save that. Another
# option would be to simply save every epoch start state, and eventually
# point it out as it becomes finalized..
info "Updated head",
stateRoot = shortLog(node.state.root),
headBlockRoot = shortLog(node.state.blck.root),
stateSlot = humaneSlotNum(node.state.data.slot)
proc makeAttestation(node: BeaconNode,
validator: AttachedValidator,
slot: Slot,
@ -179,17 +238,24 @@ proc makeAttestation(node: BeaconNode,
doAssert node != nil
doAssert validator != nil
var state = node.beaconState
# It's time to make an attestation. To do so, we must determine what we
# consider to be the head block - this is done by the fork choice rule.
# TODO this lazy update of the head is good because it delays head resolution
# until the very latest moment - on the other hand, if it takes long, the
# attestation might be late!
node.updateHead()
if state.slot < slot:
info "Filling slot gap for attestation",
slot = humaneSlotNum(slot),
stateSlot = humaneSlotNum(state.slot)
# Check pending attestations - maybe we found some blocks for them
node.attestationPool.resolve(node.state.data)
for s in state.slot ..< slot:
let ok = updateState(
state, node.headBlockRoot, none[BeaconBlock](), {skipValidation})
doAssert ok
# It might be that the latest block we found is an old one - if this is the
# case, we need to fast-forward the state
# TODO maybe this is not necessary? We just use the justified epoch from the
# state - investigate if it can change (and maybe restructure the state
# update code so it becomes obvious... this would require moving away
# from the huge state object)
var state = node.state.data
skipSlots(state, node.state.blck.root, slot)
let
justifiedBlockRoot =
@ -198,7 +264,7 @@ proc makeAttestation(node: BeaconNode,
attestationData = AttestationData(
slot: slot,
shard: shard,
beacon_block_root: node.headBlockRoot,
beacon_block_root: node.state.blck.root,
epoch_boundary_root: Eth2Digest(), # TODO
shard_block_root: Eth2Digest(), # TODO
latest_crosslink: Crosslink(epoch: state.latest_crosslinks[shard].epoch),
@ -218,47 +284,51 @@ proc makeAttestation(node: BeaconNode,
custody_bitfield: newSeq[byte](participationBitfield.len)
)
# TODO what are we waiting for here? broadcast should never block, and never
# fail...
await node.network.broadcast(topicAttestations, attestation)
info "Attestation sent",
slot = humaneSlotNum(attestationData.slot),
shard = attestationData.shard,
validator = shortValidatorKey(node, validator.idx),
signature = shortHash(validatorSignature),
beaconBlockRoot = shortHash(attestationData.beacon_block_root)
signature = shortLog(validatorSignature),
beaconBlockRoot = shortLog(attestationData.beacon_block_root)
proc proposeBlock(node: BeaconNode,
validator: AttachedValidator,
slot: Slot) {.async.} =
doAssert node != nil
doAssert validator != nil
doAssert validator.idx < node.beaconState.validator_registry.len
doAssert validator.idx < node.state.data.validator_registry.len
var state = node.beaconState
# To propose a block, we should know what the head is, because that's what
# we'll be building the next block upon..
node.updateHead()
if state.slot + 1 < slot:
info "Filling slot gap for block proposal",
slot = humaneSlotNum(slot),
stateSlot = humaneSlotNum(state.slot)
# To create a block, we'll first apply a partial block to the state, skipping
# some validations.
# TODO technically, we could leave the state with the new block applied here,
# though it works this way as well because eventually we'll receive the
# block through broadcast.. to apply or not to apply permantently, that
# is the question...
var state = node.state.data
for s in state.slot + 1 ..< slot:
let ok = updateState(
state, node.headBlockRoot, none[BeaconBlock](), {skipValidation})
doAssert ok
skipSlots(state, node.state.blck.root, slot - 1)
var blockBody = BeaconBlockBody(
attestations: node.attestationPool.getAttestationsForBlock(state, slot))
attestations: node.attestationPool.getAttestationsForBlock(slot))
var newBlock = BeaconBlock(
slot: slot,
parent_root: node.headBlockRoot,
parent_root: node.state.blck.root,
randao_reveal: validator.genRandaoReveal(state, state.slot),
eth1_data: node.mainchainMonitor.getBeaconBlockRef(),
signature: ValidatorSig(), # we need the rest of the block first!
body: blockBody)
let ok =
updateState(state, node.headBlockRoot, some(newBlock), {skipValidation})
updateState(state, node.state.blck.root, some(newBlock), {skipValidation})
doAssert ok # TODO: err, could this fail somehow?
newBlock.state_root = Eth2Digest(data: hash_tree_root(state))
@ -270,12 +340,14 @@ proc proposeBlock(node: BeaconNode,
newBlock.signature = await validator.signBlockProposal(state.fork, signedData)
# TODO what are we waiting for here? broadcast should never block, and never
# fail...
await node.network.broadcast(topicBeaconBlocks, newBlock)
info "Block proposed",
slot = humaneSlotNum(slot),
stateRoot = shortHash(newBlock.state_root),
parentRoot = shortHash(newBlock.parent_root),
stateRoot = shortLog(newBlock.state_root),
parentRoot = shortLog(newBlock.parent_root),
validator = shortValidatorKey(node, validator.idx),
idx = validator.idx
@ -289,7 +361,7 @@ proc scheduleBlockProposal(node: BeaconNode,
doAssert validator != nil
let
at = node.beaconState.slotStart(slot)
at = node.slotStart(slot)
now = fastEpochTime()
if now > at:
@ -320,7 +392,7 @@ proc scheduleAttestation(node: BeaconNode,
doAssert validator != nil
let
at = node.beaconState.slotStart(slot)
at = node.slotStart(slot)
now = fastEpochTime()
if now > at:
@ -342,14 +414,58 @@ proc scheduleEpochActions(node: BeaconNode, epoch: Epoch) =
## attestations from our attached validators.
doAssert node != nil
doAssert epoch >= GENESIS_EPOCH,
"Epoch: " & $epoch & ", humane epoch: " & $humaneSlotNum(epoch)
"Epoch: " & $epoch & ", humane epoch: " & $humaneEpochNum(epoch)
debug "Scheduling epoch actions", epoch = humaneEpochNum(epoch)
debug "Scheduling epoch actions",
epoch = humaneEpochNum(epoch),
stateEpoch = humaneEpochNum(node.state.data.slot.slot_to_epoch())
# TODO: this copy of the state shouldn't be necessary, but please
# see the comments in `get_beacon_proposer_index`
var nextState = node.beaconState
# Sanity check - verify that the current head block is not too far behind
if node.state.data.slot.slot_to_epoch() + 1 < epoch:
# Normally, we update the head state lazily, just before making an
# attestation. However, if we skip scheduling attestations, we'll never
# run the head update - thus we make an attempt now:
node.updateHead()
if node.state.data.slot.slot_to_epoch() + 1 < epoch:
# We're still behind!
#
# There's a few ways this can happen:
#
# * we receive no attestations or blocks for an extended period of time
# * all the attestations we receive are bogus - maybe we're connected to
# the wrong network?
# * we just started and still haven't synced
#
# TODO make an effort to find other nodes and sync? A worst case scenario
# here is that the network stalls because nobody is sending out
# attestations because nobody is scheduling them, in a vicious
# circle
# TODO diagnose the various scenarios and do something smart...
let
expectedSlot = node.state.data.getSlotFromTime()
nextSlot = expectedSlot + 1
at = node.slotStart(nextSlot)
notice "Delaying epoch scheduling, head too old - scheduling new attempt",
stateSlot = humaneSlotNum(node.state.data.slot),
expectedEpoch = humaneEpochNum(epoch),
expectedSlot = humaneSlotNum(expectedSlot),
fromNow = (at - fastEpochTime()) div 1000
addTimer(at) do (p: pointer):
node.scheduleEpochActions(nextSlot.slot_to_epoch())
return
# TODO: is this necessary with the new shuffling?
# see get_beacon_proposer_index
var nextState = node.state.data
skipSlots(nextState, node.state.blck.root, epoch.get_epoch_start_slot())
# TODO we don't need to do anything at slot 0 - what about slots we missed
# if we got delayed above?
let start = if epoch == GENESIS_EPOCH: 1.uint64 else: 0.uint64
for i in start ..< SLOTS_PER_EPOCH:
@ -378,7 +494,7 @@ proc scheduleEpochActions(node: BeaconNode, epoch: Epoch) =
let
nextEpoch = epoch + 1
at = node.beaconState.slotStart(nextEpoch.get_epoch_start_slot())
at = node.slotStart(nextEpoch.get_epoch_start_slot())
info "Scheduling next epoch update",
fromNow = (at - fastEpochTime()) div 1000,
@ -387,198 +503,94 @@ proc scheduleEpochActions(node: BeaconNode, epoch: Epoch) =
addTimer(at) do (p: pointer):
node.scheduleEpochActions(nextEpoch)
proc stateNeedsSaving(s: BeaconState): bool =
# TODO: Come up with a better predicate logic
s.slot mod stateStoragePeriod == 0
proc fetchBlocks(node: BeaconNode, roots: seq[Eth2Digest]) =
if roots.len == 0: return
# TODO shouldn't send to all!
# TODO should never fail - asyncCheck is wrong here..
asyncCheck node.network.broadcast(topicfetchBlocks, roots)
proc onFetchBlocks(node: BeaconNode, roots: seq[Eth2Digest]) =
# TODO placeholder logic for block recovery
debug "fetchBlocks received",
roots = roots.len
for root in roots:
if (let blck = node.db.getBlock(root); blck.isSome()):
# TODO should never fail - asyncCheck is wrong here..
# TODO should obviously not spam, but rather send it back to the requester
asyncCheck node.network.broadcast(topicBeaconBlocks, blck.get())
proc scheduleSlotStartActions(node: BeaconNode, slot: Slot) =
# TODO in this setup, we retry fetching blocks at the beginning of every slot,
# hoping that we'll get some before it's time to attest or propose - is
# there a better time to do this?
let missingBlocks = node.blockPool.checkUnresolved()
node.fetchBlocks(missingBlocks)
let
nextSlot = slot + 1
at = node.slotStart(nextSlot)
info "Scheduling next slot start action block",
fromNow = (at - fastEpochTime()) div 1000,
slot = humaneSlotNum(nextSlot)
addTimer(at) do (p: pointer):
node.scheduleSlotStartActions(nextSlot)
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",
# We received an attestation from the network but don't know much about it
# yet - in particular, we haven't verified that it belongs to particular chain
# we're on, or that it follows the rules of the protocol
debug "Attestation received",
slot = humaneSlotNum(attestation.data.slot),
shard = attestation.data.shard,
signature = shortHash(attestation.aggregate_signature),
participants,
beaconBlockRoot = shortHash(attestation.data.beacon_block_root)
beaconBlockRoot = shortLog(attestation.data.beacon_block_root),
justifiedEpoch = humaneEpochNum(attestation.data.justified_epoch),
justifiedBlockRoot = shortLog(attestation.data.justified_block_root),
signature = shortLog(attestation.aggregate_signature)
node.attestationPool.add(attestation, node.beaconState)
node.attestationPool.add(node.state.data, attestation)
if not node.db.containsBlock(attestation.data.beacon_block_root):
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.
let
ancestors = node.db.getAncestors(blck) do (bb: BeaconBlock) -> bool:
node.db.containsState(bb.state_root)
ancestor = ancestors[^1]
# Several things can happen, but the most common one should be that we found
# a beacon state
if (let state = node.db.getState(ancestor.state_root); state.isSome()):
# Got it!
notice "Replaying state transitions",
stateSlot = humaneSlotNum(node.beaconState.slot),
prevStateSlot = humaneSlotNum(state.get().slot)
node.beaconState = state.get()
elif ancestor.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!"
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(ancestor.parent_root),
blockSlot = humaneSlotNum(ancestor.slot)
doAssert false, "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!
# Time to replay all the blocks between then and now. We skip the one because
# it's the one that we found the state with, and it has already been
# applied
for i in countdown(ancestors.len - 2, 0):
let last = ancestors[i]
skipSlots(node.beaconState, last.parent_root, last.slot)
# TODO technically, we should be storing states here, because we're now
# going down a different fork
let ok = updateState(
node.beaconState, last.parent_root, some(last),
if ancestors.len == 0: {} else: {skipValidation})
doAssert(ok)
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)
if attestation.data.beacon_block_root notin node.potentialHeads:
node.potentialHeads.add attestation.data.beacon_block_root
proc onBeaconBlock(node: BeaconNode, blck: BeaconBlock) =
let
blockRoot = hash_tree_root_final(blck)
stateSlot = node.beaconState.slot
if node.db.containsBlock(blockRoot):
debug "Block already seen",
slot = humaneSlotNum(blck.slot),
stateRoot = shortHash(blck.state_root),
blockRoot = shortHash(blockRoot),
stateSlot = humaneSlotNum(stateSlot)
return
info "Block received",
# We received a block but don't know much about it yet - in particular, we
# don't know if it's part of the chain we're currently building.
let blockRoot = hash_tree_root_final(blck)
debug "Block received",
blockRoot = shortLog(blockRoot),
slot = humaneSlotNum(blck.slot),
stateRoot = shortHash(blck.state_root),
parentRoot = shortHash(blck.parent_root),
blockRoot = shortHash(blockRoot)
stateRoot = shortLog(blck.state_root),
parentRoot = shortLog(blck.parent_root),
signature = shortLog(blck.signature),
proposer_slashings = blck.body.proposer_slashings.len,
attester_slashings = blck.body.attester_slashings.len,
attestations = blck.body.attestations.len,
deposits = blck.body.deposits.len,
voluntary_exits = blck.body.voluntary_exits.len,
transfers = blck.body.transfers.len
# 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!
if not node.blockPool.add(blockRoot, blck):
# TODO the fact that add returns a bool that causes the parent block to be
# pre-emptively fetched is quite ugly - fix.
node.fetchBlocks(@[blck.parent_root])
# 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.putBlock(blck)
# Delay updating the head until the latest moment possible - this makes it
# more likely that we've managed to resolve the block, in case of
# irregularities
if blockRoot notin node.potentialHeads:
node.potentialHeads.add blockRoot
# 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!)
# The block we received contains attestations, and we might not yet know about
# all of them. Let's add them to the attestation pool - in case they block
# is not yet resolved, neither will the attestations be!
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.putState(node.beaconState)
proc run*(node: BeaconNode) =
node.network.subscribe(topicBeaconBlocks) do (blck: BeaconBlock):
node.onBeaconBlock(blck)
@ -586,8 +598,13 @@ proc run*(node: BeaconNode) =
node.network.subscribe(topicAttestations) do (attestation: Attestation):
node.onAttestation(attestation)
let epoch = node.beaconState.getSlotFromTime div SLOTS_PER_EPOCH
node.scheduleEpochActions(epoch)
node.network.subscribe(topicfetchBlocks) do (roots: seq[Eth2Digest]):
node.onFetchBlocks(roots)
let nowSlot = node.state.data.getSlotFromTime()
node.scheduleEpochActions(nowSlot.slot_to_epoch())
node.scheduleSlotStartActions(nowSlot)
runForever()
@ -627,8 +644,8 @@ when isMainModule:
quit 1
info "Starting beacon node",
slotsSinceFinalization = node.beaconState.slotDistanceFromNow(),
stateSlot = humaneSlotNum(node.beaconState.slot),
slotsSinceFinalization = node.state.data.slotDistanceFromNow(),
stateSlot = humaneSlotNum(node.state.data.slot),
SHARD_COUNT,
SLOTS_PER_EPOCH,
SECONDS_PER_SLOT,

366
beacon_chain/block_pool.nim Normal file
View File

@ -0,0 +1,366 @@
import
bitops, chronicles, options, sequtils, sets, tables,
ssz, beacon_chain_db, state_transition, extras,
spec/[crypto, datatypes, digest]
type
BlockPool* = ref object
## Pool of blocks responsible for keeping a graph of resolved blocks as well
## as candidates that may yet become part of that graph.
## Currently, this type works as a facade to the BeaconChainDB, making
## assumptions about the block composition therein.
##
## The general idea here is that blocks known to us are divided into two
## camps - unresolved and resolved. When we start the chain, we have a
## genesis state that serves as the root of the graph we're interested in.
## Every block that belongs to that chain will have a path to that block -
## conversely, blocks that do not are not interesting to us.
##
## As the chain progresses, some states become finalized as part of the
## consensus process. One way to think of that is that the blocks that
## come before them are no longer relevant, and the finalized state
## is the new genesis from which we build. Thus, instead of tracing a path
## to genesis, we can trace a path to any finalized block that follows - we
## call the oldest such block a tail block.
##
## It's important to note that blocks may arrive in any order due to
## chainging network conditions - we counter this by buffering unresolved
## blocks for some time while trying to establish a path.
##
## Once a path is established, the block becomes resolved. We store the
## graph in memory, in the form of BlockRef objects. This is also when
## we forward the block for storage in the database
##
## TODO evaluate the split of responsibilities between the two
## TODO prune the graph as tail moves
pending*: Table[Eth2Digest, BeaconBlock] ##\
## Blocks that have passed validation but that we lack a link back to tail
## for - when we receive a "missing link", we can use this data to build
## an entire branch
unresolved*: Table[Eth2Digest, UnresolvedBlock] ##\
## Roots of blocks that we would like to have (either parent_root of
## unresolved blocks or block roots of attestations)
blocks*: Table[Eth2Digest, BlockRef] ##\
## Tree of blocks pointing back to a finalized block on the chain we're
## interested in - we call that block the tail
tail*: BlockData ##\
## The earliest finalized block we know about
db*: BeaconChainDB
UnresolvedBlock = object
tries*: int
BlockRef* = ref object {.acyclic.}
## Node in object graph guaranteed to lead back to tail block, and to have
## a corresponding entry in database.
## Block graph should form a tree - in particular, there are no cycles.
root*: Eth2Digest ##\
## Root that can be used to retrieve block data from database
parent*: BlockRef ##\
## Not nil, except for the tail
children*: seq[BlockRef]
BlockData* = object
## Body and graph in one
data*: BeaconBlock
refs*: BlockRef
StateData* = object
data*: BeaconState
root*: Eth2Digest ##\
## Root of above data (cache)
blck*: BlockRef ##\
## The block associated with the state found in data - in particular,
## blck.state_root == root
proc link(parent, child: BlockRef) =
doAssert (not (parent.root == Eth2Digest() or child.root == Eth2Digest())),
"blocks missing root!"
doAssert parent.root != child.root, "self-references not allowed"
child.parent = parent
parent.children.add(child)
proc init*(T: type BlockPool, db: BeaconChainDB): BlockPool =
# TODO we require that the db contains both a head and a tail block -
# asserting here doesn't seem like the right way to go about it however..
# TODO head is updated outside of block pool but read here - ugly.
let
tail = db.getTailBlock()
head = db.getHeadBlock()
doAssert tail.isSome(), "Missing tail block, database corrupt?"
doAssert head.isSome(), "Missing head block, database corrupt?"
let
headRoot = head.get()
tailRoot = tail.get()
tailRef = BlockRef(root: tailRoot)
var blocks = {tailRef.root: tailRef}.toTable()
if headRoot != tailRoot:
var curRef: BlockRef
for root, _ in db.getAncestors(headRoot):
if root == tailRef.root:
link(tailRef, curRef)
curRef = curRef.parent
break
if curRef == nil:
curRef = BlockRef(root: root)
else:
link(BlockRef(root: root), curRef)
curRef = curRef.parent
blocks[curRef.root] = curRef
doAssert curRef == tailRef,
"head block does not lead to tail, database corrupt?"
BlockPool(
pending: initTable[Eth2Digest, BeaconBlock](),
unresolved: initTable[Eth2Digest, UnresolvedBlock](),
blocks: blocks,
tail: BlockData(
data: db.getBlock(tailRef.root).get(),
refs: tailRef,
),
db: db
)
proc add*(pool: var BlockPool, blockRoot: Eth2Digest, blck: BeaconBlock): bool =
## return false indicates that the block parent was missing and should be
## fetched
## TODO reevaluate this API - it's pretty ugly with the bool return
doAssert blockRoot == hash_tree_root_final(blck)
# Already seen this block??
if blockRoot in pool.blocks:
debug "Block already exists",
slot = humaneSlotNum(blck.slot),
stateRoot = shortLog(blck.state_root),
parentRoot = shortLog(blck.parent_root),
blockRoot = shortLog(blockRoot)
return true
# The tail block points to a cutoff time beyond which we don't store blocks -
# if we receive a block with an earlier slot, there's no hope of ever
# resolving it
if blck.slot <= pool.tail.data.slot:
debug "Old block, dropping",
slot = humaneSlotNum(blck.slot),
tailSlot = humaneSlotNum(pool.tail.data.slot),
stateRoot = shortLog(blck.state_root),
parentRoot = shortLog(blck.parent_root),
blockRoot = shortLog(blockRoot)
return true
# 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!
let parent = pool.blocks.getOrDefault(blck.parent_root)
if parent != nil:
# The block is resolved, nothing more to do!
let blockRef = BlockRef(
root: blockRoot
)
link(parent, blockRef)
pool.blocks[blockRoot] = blockRef
# The block might have been in either of these - we don't want any more
# work done on its behalf
pool.unresolved.del(blockRoot)
pool.pending.del(blockRoot)
# Resolved blocks should be stored in database
pool.db.putBlock(blockRoot, blck)
info "Block resolved",
blockRoot = shortLog(blockRoot),
slot = humaneSlotNum(blck.slot),
stateRoot = shortLog(blck.state_root),
parentRoot = shortLog(blck.parent_root),
signature = shortLog(blck.signature),
proposer_slashings = blck.body.proposer_slashings.len,
attester_slashings = blck.body.attester_slashings.len,
attestations = blck.body.attestations.len,
deposits = blck.body.deposits.len,
voluntary_exits = blck.body.voluntary_exits.len,
transfers = blck.body.transfers.len
# Now that we have the new block, we should see if any of the previously
# unresolved blocks magically become resolved
# TODO there are more efficient ways of doing this, that also don't risk
# running out of stack etc
let retries = pool.pending
for k, v in retries:
discard pool.add(k, v)
return true
# TODO possibly, it makes sense to check the database - that would allow sync
# to simply fill up the database with random blocks the other clients
# think are useful - but, it would also risk filling the database with
# junk that's not part of the block graph
if blck.parent_root in pool.unresolved:
return true
# This is an unresolved block - put it on the unresolved list for now...
debug "Unresolved block",
slot = humaneSlotNum(blck.slot),
stateRoot = shortLog(blck.state_root),
parentRoot = shortLog(blck.parent_root),
blockRoot = shortLog(blockRoot)
pool.unresolved[blck.parent_root] = UnresolvedBlock()
pool.pending[blockRoot] = blck
false
proc get*(pool: BlockPool, blck: BlockRef): BlockData =
## Retrieve the associated block body of a block reference
doAssert (not blck.isNil), "Trying to get nil BlockRef"
let data = pool.db.getBlock(blck.root)
doAssert data.isSome, "BlockRef without backing data, database corrupt?"
BlockData(data: data.get(), refs: blck)
proc get*(pool: BlockPool, root: Eth2Digest): Option[BlockData] =
## Retrieve a resolved block reference and its associated body, if available
let refs = pool.blocks.getOrDefault(root)
if not refs.isNil:
some(pool.get(refs))
else:
none(BlockData)
proc getOrResolve*(pool: var BlockPool, root: Eth2Digest): BlockRef =
## Fetch a block ref, or nil if not found (will be added to list of
## blocks-to-resolve)
result = pool.blocks.getOrDefault(root)
if result.isNil:
pool.unresolved[root] = UnresolvedBlock()
proc checkUnresolved*(pool: var BlockPool): seq[Eth2Digest] =
## Return a list of blocks that we should try to resolve from other client -
## to be called periodically but not too often (once per slot?)
var done: seq[Eth2Digest]
for k, v in pool.unresolved.mpairs():
if v.tries > 8:
done.add(k)
else:
inc v.tries
for k in done:
pool.unresolved.del(k)
# simple (simplistic?) exponential backoff for retries..
for k, v in pool.unresolved.pairs():
if v.tries.popcount() == 1:
result.add(k)
proc skipAndUpdateState(
state: var BeaconState, blck: BeaconBlock, flags: UpdateFlags): bool =
skipSlots(state, blck.parent_root, blck.slot - 1)
updateState(state, blck.parent_root, some(blck), flags)
proc updateState*(
pool: BlockPool, state: var StateData, blck: BlockRef) =
if state.blck.root == blck.root:
return # State already at the right spot
# TODO this blockref should never be created, since we trace every blockref
# back to the tail block
doAssert (not blck.parent.isNil), "trying to apply genesis block!"
var ancestors = @[pool.get(blck)]
# Common case: blck points to a block that is one step ahead of state
if state.blck.root == blck.parent.root:
let ok = skipAndUpdateState(state.data, ancestors[0].data, {skipValidation})
doAssert ok, "Blocks in database should never fail to apply.."
state.blck = blck
state.root = ancestors[0].data.state_root
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.
while not ancestors[^1].refs.parent.isNil:
let parent = pool.get(ancestors[^1].refs.parent)
ancestors.add parent
if pool.db.containsState(parent.data.state_root): break
let
ancestor = ancestors[^1]
ancestorState = pool.db.getState(ancestor.data.state_root)
if ancestorState.isNone():
# TODO this should only happen if the database is corrupt - we walked the
# list of parent blocks and couldn't find a corresponding state in the
# database, which should never happen (at least we should have the
# tail state in there!)
error "Couldn't find ancestor state or block parent missing!",
blockRoot = shortLog(blck.root)
doAssert false, "Oh noes, we passed big bang!"
notice "Replaying state transitions",
stateSlot = humaneSlotNum(state.data.slot),
prevStateSlot = humaneSlotNum(ancestorState.get().slot),
ancestors = ancestors.len
state.data = ancestorState.get()
# 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!
# Time to replay all the blocks between then and now. We skip the one because
# it's the one that we found the state with, and it has already been
# applied
for i in countdown(ancestors.len - 2, 0):
let last = ancestors[i]
skipSlots(state.data, last.data.parent_root, last.data.slot - 1)
# TODO technically, we should be adding states to the database here because
# we're going down a different fork..
let ok = updateState(
state.data, last.data.parent_root, some(last.data), {skipValidation})
doAssert(ok)
state.blck = blck
state.root = ancestors[0].data.state_root
proc loadTailState*(pool: BlockPool): StateData =
## Load the state associated with the current tail in the pool
StateData(
data: pool.db.getState(pool.tail.data.state_root).get(),
root: pool.tail.data.state_root,
blck: pool.tail.refs
)

View File

@ -366,14 +366,16 @@ proc checkAttestation*(
if not (attestation.data.slot <= state.slot - MIN_ATTESTATION_INCLUSION_DELAY):
warn("Attestation too new",
attestation_slot = attestation.data.slot, state_slot = state.slot)
attestation_slot = humaneSlotNum(attestation.data.slot),
state_slot = humaneSlotNum(state.slot))
return
# Can't underflow, because GENESIS_SLOT > MIN_ATTESTATION_INCLUSION_DELAY
if not (state.slot - MIN_ATTESTATION_INCLUSION_DELAY <
attestation.data.slot + SLOTS_PER_EPOCH):
warn("Attestation too old",
attestation_slot = attestation.data.slot, state_slot = state.slot)
attestation_slot = humaneSlotNum(attestation.data.slot),
state_slot = humaneSlotNum(state.slot))
return
let expected_justified_epoch =
@ -384,8 +386,9 @@ proc checkAttestation*(
if not (attestation.data.justified_epoch == expected_justified_epoch):
warn("Unexpected justified epoch",
attestation_justified_epoch = attestation.data.justified_epoch,
expected_justified_epoch)
attestation_justified_epoch =
humaneEpochNum(attestation.data.justified_epoch),
expected_justified_epoch = humaneEpochNum(expected_justified_epoch))
return
let expected_justified_block_root =

View File

@ -59,6 +59,9 @@ type
ValidatorSig* = blscurve.Signature
ValidatorPKI* = ValidatorPrivKey|ValidatorPubKey|ValidatorSig
func shortLog*(x: ValidatorPKI): string =
($x)[0..7]
template hash*(k: ValidatorPubKey|ValidatorPrivKey): Hash =
hash(k.getBytes())

View File

@ -31,6 +31,9 @@ type
Eth2Digest* = MDigest[32 * 8] ## `hash32` from spec
Eth2Hash* = blake2_512 ## Context for hash function
func shortLog*(x: Eth2Digest): string =
($x)[0..7]
func eth2hash*(v: openArray[byte]): Eth2Digest =
var tmp = Eth2Hash.digest v
copyMem(result.data.addr, tmp.addr, sizeof(result))

View File

@ -269,6 +269,7 @@ func get_beacon_proposer_index*(state: BeaconState, slot: Slot): ValidatorIndex
# TODO this index is invalid outside of the block state transition function
# because presently, `state.slot += 1` happens before this function
# is called - see also testutil.getNextBeaconProposerIndex
# TODO is the above still true? the shuffling has changed since it was written
let (first_committee, _) = get_crosslink_committees_at_slot(state, slot)[0]
let idx = int(slot mod uint64(first_committee.len))
first_committee[idx]

View File

@ -994,6 +994,16 @@ proc updateState*(state: var BeaconState, previous_block_root: Eth2Digest,
processEpoch(state)
true
proc skipSlots*(state: var BeaconState, parentRoot: Eth2Digest, slot: Slot) =
if state.slot < slot:
info "Advancing state past slot gap",
targetSlot = humaneSlotNum(slot),
stateSlot = humaneSlotNum(state.slot)
while state.slot < slot:
let ok = updateState(state, parentRoot, none[BeaconBlock](), {})
doAssert ok, "Empty block state update should never fail!"
# TODO document this:
# Jacek Sieka

View File

@ -6,7 +6,8 @@ import
proc stateSize(deposits: int, maxContent = false) =
var state = get_genesis_beacon_state(
makeInitialDeposits(deposits), 0, Eth1Data(), {skipValidation})
makeInitialDeposits(
deposits, {skipValidation}), 0, Eth1Data(), {skipValidation})
if maxContent:
# TODO verify this is correct, but generally we collect up to two epochs

View File

@ -10,6 +10,7 @@ import
./test_beacon_chain_db,
./test_beacon_node,
./test_beaconstate,
./test_block_pool,
./test_helpers,
./test_ssz,
./test_state_transition,

View File

@ -1,2 +1,3 @@
data/
startup/

29
tests/simulation/run_node.sh Executable file
View File

@ -0,0 +1,29 @@
#!/bin/bash
set -eux
. $(dirname $0)/vars.sh
BOOTSTRAP_NODES_FLAG="--bootstrapNodesFile:$MASTER_NODE_ADDRESS_FILE"
if [[ "$1" == "0" ]]; then
BOOTSTRAP_NODES_FLAG=""
fi
DATA_DIR=$SIMULATION_DIR/node-${1}
$BEACON_NODE_BIN \
--dataDir:$DATA_DIR \
--validator:$STARTUP_DIR/validator-${1}1.json \
--validator:$STARTUP_DIR/validator-${1}2.json \
--validator:$STARTUP_DIR/validator-${1}3.json \
--validator:$STARTUP_DIR/validator-${1}4.json \
--validator:$STARTUP_DIR/validator-${1}5.json \
--validator:$STARTUP_DIR/validator-${1}6.json \
--validator:$STARTUP_DIR/validator-${1}7.json \
--validator:$STARTUP_DIR/validator-${1}8.json \
--validator:$STARTUP_DIR/validator-${1}9.json \
--tcpPort:5000${1} \
--udpPort:5000${1} \
--stateSnapshot:$SNAPSHOT_FILE \
$BOOTSTRAP_NODES_FLAG

View File

@ -5,44 +5,35 @@ set -eux
# Kill children on ctrl-c
trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM EXIT
# Read in variables
. $(dirname $0)/vars.sh
# Set a default value for the env vars usually supplied by nimbus Makefile
: ${SKIP_BUILDS:=""}
: ${BUILD_OUTPUTS_DIR:="./build"}
NUMBER_OF_VALIDATORS=99
cd $(dirname "$0")
PWD_CMD="pwd"
# get native Windows paths on Mingw
uname | grep -qi mingw && PWD_CMD="pwd -W"
SIMULATION_DIR="$($PWD_CMD)/data"
cd $SIM_ROOT
mkdir -p "$SIMULATION_DIR"
mkdir -p "$STARTUP_DIR"
STARTUP_FILE="$SIMULATION_DIR/startup.json"
SNAPSHOT_FILE="$SIMULATION_DIR/state_snapshot.json"
cd $(git rev-parse --show-toplevel)
ROOT_DIR="$($PWD_CMD)"
cd $GIT_ROOT
mkdir -p $BUILD_OUTPUTS_DIR
BEACON_NODE_BIN=$BUILD_OUTPUTS_DIR/beacon_node
VALIDATOR_KEYGEN_BIN=$BUILD_OUTPUTS_DIR/validator_keygen
# Run with "SHARD_COUNT=4 ./start.sh" to change these
DEFS="-d:SHARD_COUNT=${SHARD_COUNT:-4} " # Spec default: 1024
DEFS+="-d:EPOCH_LENGTH=${EPOCH_LENGTH:-8} " # Spec default: 64
DEFS+="-d:SLOTS_PER_EPOCH=${SLOTS_PER_EPOCH:-8} " # Spec default: 64
DEFS+="-d:SECONDS_PER_SLOT=${SECONDS_PER_SLOT:-6} " # Spec default: 6
if [[ -z "$SKIP_BUILDS" ]]; then
nim c -o:"$VALIDATOR_KEYGEN_BIN" $DEFS -d:release beacon_chain/validator_keygen
nim c -o:"$BEACON_NODE_BIN" $DEFS --opt:speed beacon_chain/beacon_node
if [ ! -f $STARTUP_FILE ]; then
if [[ -z "$SKIP_BUILDS" ]]; then
nim c -o:"$VALIDATOR_KEYGEN_BIN" $DEFS -d:release beacon_chain/validator_keygen
fi
$VALIDATOR_KEYGEN_BIN --validators=$NUMBER_OF_VALIDATORS --outputDir="$STARTUP_DIR"
fi
if [ ! -f $STARTUP_FILE ]; then
$VALIDATOR_KEYGEN_BIN --validators=$NUMBER_OF_VALIDATORS --outputDir="$SIMULATION_DIR"
if [[ -z "$SKIP_BUILDS" ]]; then
nim c -o:"$BEACON_NODE_BIN" $DEFS --opt:speed beacon_chain/beacon_node
fi
if [ ! -f $SNAPSHOT_FILE ]; then
@ -51,8 +42,6 @@ if [ ! -f $SNAPSHOT_FILE ]; then
--out:$SNAPSHOT_FILE --genesisOffset=5 # Delay in seconds
fi
MASTER_NODE_ADDRESS_FILE="$SIMULATION_DIR/node-0/beacon_node.address"
# Delete any leftover address files from a previous session
if [ -f $MASTER_NODE_ADDRESS_FILE ]; then
rm $MASTER_NODE_ADDRESS_FILE
@ -64,11 +53,11 @@ USE_MULTITAIL="${USE_MULTITAIL:-no}" # make it an opt-in
type "$MULTITAIL" &>/dev/null || USE_MULTITAIL="no"
COMMANDS=()
for i in $(seq 0 9); do
for i in $(seq 0 8); do
BOOTSTRAP_NODES_FLAG="--bootstrapNodesFile:$MASTER_NODE_ADDRESS_FILE"
if [[ "$i" == "0" ]]; then
BOOTSTRAP_NODES_FLAG=""
sleep 0
elif [ "$USE_MULTITAIL" = "no" ]; then
# Wait for the master node to write out its address file
while [ ! -f $MASTER_NODE_ADDRESS_FILE ]; do
@ -76,23 +65,7 @@ for i in $(seq 0 9); do
done
fi
DATA_DIR=$SIMULATION_DIR/node-$i
CMD="$BEACON_NODE_BIN \
--dataDir:\"$DATA_DIR\" \
--validator:\"$SIMULATION_DIR/validator-${i}1.json\" \
--validator:\"$SIMULATION_DIR/validator-${i}2.json\" \
--validator:\"$SIMULATION_DIR/validator-${i}3.json\" \
--validator:\"$SIMULATION_DIR/validator-${i}4.json\" \
--validator:\"$SIMULATION_DIR/validator-${i}5.json\" \
--validator:\"$SIMULATION_DIR/validator-${i}6.json\" \
--validator:\"$SIMULATION_DIR/validator-${i}7.json\" \
--validator:\"$SIMULATION_DIR/validator-${i}8.json\" \
--validator:\"$SIMULATION_DIR/validator-${i}9.json\" \
--tcpPort:5000$i \
--udpPort:5000$i \
--stateSnapshot:\"$SNAPSHOT_FILE\" \
$BOOTSTRAP_NODES_FLAG"
CMD="$SIM_ROOT/run_node.sh $i"
if [ "$USE_MULTITAIL" != "no" ]; then
if [ "$i" = "0" ]; then
@ -108,8 +81,7 @@ for i in $(seq 0 9); do
done
if [ "$USE_MULTITAIL" != "no" ]; then
eval $MULTITAIL -s 2 -M 0 -x \"beacon chain simulation\" "${COMMANDS[@]}"
eval $MULTITAIL -s 3 -M 0 -x \"Nimbus beacon chain\" "${COMMANDS[@]}"
else
wait # Stop when all nodes have gone down
fi

22
tests/simulation/vars.sh Normal file
View File

@ -0,0 +1,22 @@
#!/bin/bash
PWD_CMD="pwd"
# get native Windows paths on Mingw
uname | grep -qi mingw && PWD_CMD="pwd -W"
cd $(dirname $0)
SIM_ROOT="$($PWD_CMD)"
cd $(git rev-parse --show-toplevel)
GIT_ROOT="$($PWD_CMD)"
# Set a default value for the env vars usually supplied by nimbus Makefile
: ${SKIP_BUILDS:=""}
: ${BUILD_OUTPUTS_DIR:="$GIT_ROOT/build"}
SIMULATION_DIR="$SIM_ROOT/data"
STARTUP_DIR="$SIM_ROOT/startup"
STARTUP_FILE="$STARTUP_DIR/startup.json"
SNAPSHOT_FILE="$SIMULATION_DIR/state_snapshot.json"
BEACON_NODE_BIN=$BUILD_OUTPUTS_DIR/beacon_node
VALIDATOR_KEYGEN_BIN=$BUILD_OUTPUTS_DIR/validator_keygen
MASTER_NODE_ADDRESS_FILE="$SIMULATION_DIR/node-0/beacon_node.address"

View File

@ -9,38 +9,40 @@ import
options, sequtils, unittest,
./testutil,
../beacon_chain/spec/[beaconstate, crypto, datatypes, digest, helpers, validator],
../beacon_chain/[attestation_pool, extras, state_transition, ssz]
../beacon_chain/[attestation_pool, block_pool, extras, state_transition, ssz]
suite "Attestation pool processing":
## For now just test that we can compile and execute block processing with
## mock data.
let
# Genesis state with minimal number of deposits
# TODO bls verification is a bit of a bottleneck here
genesisState = get_genesis_beacon_state(
makeInitialDeposits(), 0, Eth1Data(), {skipValidation})
genesisBlock = get_initial_beacon_block(genesisState)
genesisRoot = hash_tree_root_final(genesisBlock)
# Genesis state with minimal number of deposits
var
genState = get_genesis_beacon_state(
makeInitialDeposits(flags = {skipValidation}), 0, Eth1Data(),
{skipValidation})
genBlock = get_initial_beacon_block(genState)
blockPool = BlockPool.init(makeTestDB(genState, genBlock))
test "Can add and retrieve simple attestation":
var
pool = init(AttestationPool, 42)
state = genesisState
pool = AttestationPool.init(blockPool)
state = blockPool.loadTailState()
# Slot 0 is a finalized slot - won't be making attestations for it..
discard updateState(
state, genesisRoot, none(BeaconBlock), {skipValidation})
state.data, state.blck.root, none(BeaconBlock), {skipValidation})
let
# Create an attestation for slot 1 signed by the only attester we have!
crosslink_committees = get_crosslink_committees_at_slot(state, state.slot)
crosslink_committees =
get_crosslink_committees_at_slot(state.data, state.data.slot)
attestation = makeAttestation(
state, genesisRoot, crosslink_committees[0].committee[0])
state.data, state.blck.root, crosslink_committees[0].committee[0])
pool.add(attestation, state)
pool.add(state.data, attestation)
let attestations = pool.getAttestationsForBlock(
state, state.slot + MIN_ATTESTATION_INCLUSION_DELAY)
state.data.slot + MIN_ATTESTATION_INCLUSION_DELAY)
check:
attestations.len == 1
@ -48,32 +50,34 @@ suite "Attestation pool processing":
test "Attestations may arrive in any order":
var
pool = init(AttestationPool, 42)
state = genesisState
pool = AttestationPool.init(blockPool)
state = blockPool.loadTailState()
# Slot 0 is a finalized slot - won't be making attestations for it..
discard updateState(
state, genesisRoot, none(BeaconBlock), {skipValidation})
state.data, state.blck.root, 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)
crosslink_committees1 =
get_crosslink_committees_at_slot(state.data, state.data.slot)
attestation1 = makeAttestation(
state, genesisRoot, crosslink_committees1[0].committee[0])
state.data, state.blck.root, crosslink_committees1[0].committee[0])
discard updateState(
state, genesisRoot, none(BeaconBlock), {skipValidation})
state.data, state.blck.root, none(BeaconBlock), {skipValidation})
let
crosslink_committees2 = get_crosslink_committees_at_slot(state, state.slot)
crosslink_committees2 =
get_crosslink_committees_at_slot(state.data, state.data.slot)
attestation2 = makeAttestation(
state, genesisRoot, crosslink_committees2[0].committee[0])
state.data, state.blck.root, crosslink_committees2[0].committee[0])
# test reverse order
pool.add(attestation2, state)
pool.add(attestation1, state)
pool.add(state.data, attestation2)
pool.add(state.data, attestation1)
let attestations = pool.getAttestationsForBlock(
state, state.slot + MIN_ATTESTATION_INCLUSION_DELAY)
state.data.slot + MIN_ATTESTATION_INCLUSION_DELAY)
check:
attestations.len == 1

View File

@ -5,7 +5,7 @@
# * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
# at your option. This file may not be copied, modified, or distributed except according to those terms.
import options, unittest, strutils, eth/trie/[db],
import options, unittest, sequtils, strutils, eth/trie/[db],
../beacon_chain/[beacon_chain_db, ssz],
../beacon_chain/spec/[datatypes, digest, crypto]
@ -26,28 +26,26 @@ suite "Beacon chain DB":
let
a0 = BeaconBlock(slot: 0)
a1 = BeaconBlock(slot: 1, parent_root: hash_tree_root_final(a0))
a2 = BeaconBlock(slot: 2, parent_root: hash_tree_root_final(a1))
a0r = hash_tree_root_final(a0)
a1 = BeaconBlock(slot: 1, parent_root: a0r)
a1r = hash_tree_root_final(a1)
a2 = BeaconBlock(slot: 2, parent_root: a1r)
a2r = hash_tree_root_final(a2)
# TODO check completely kills compile times here
doAssert db.getAncestors(a0) == [a0]
doAssert db.getAncestors(a2) == [a2]
doAssert toSeq(db.getAncestors(a0r)) == []
doAssert toSeq(db.getAncestors(a2r)) == []
db.putBlock(a2)
doAssert db.getAncestors(a0) == [a0]
doAssert db.getAncestors(a2) == [a2]
doAssert toSeq(db.getAncestors(a0r)) == []
doAssert toSeq(db.getAncestors(a2r)) == [(a2r, a2)]
db.putBlock(a1)
doAssert db.getAncestors(a0) == [a0]
doAssert db.getAncestors(a2) == [a2, a1]
doAssert toSeq(db.getAncestors(a0r)) == []
doAssert toSeq(db.getAncestors(a2r)) == [(a2r, a2), (a1r, a1)]
db.putBlock(a0)
doAssert db.getAncestors(a0) == [a0]
doAssert db.getAncestors(a2) == [a2, a1, a0]
let tmp = db.getAncestors(a2) do (b: BeaconBlock) -> bool:
b.slot == 1
doAssert tmp == [a2, a1]
doAssert toSeq(db.getAncestors(a0r)) == [(a0r, a0)]
doAssert toSeq(db.getAncestors(a2r)) == [(a2r, a2), (a1r, a1), (a0r, a0)]

89
tests/test_block_pool.nim Normal file
View File

@ -0,0 +1,89 @@
# beacon_chain
# Copyright (c) 2018 Status Research & Development GmbH
# Licensed and distributed under either of
# * MIT license (license terms in the root directory or at http://opensource.org/licenses/MIT).
# * Apache v2 license (license terms in the root directory or at http://www.apache.org/licenses/LICENSE-2.0).
# at your option. This file may not be copied, modified, or distributed except according to those terms.
import
options, sequtils, unittest,
./testutil,
../beacon_chain/spec/[beaconstate, crypto, datatypes, digest, helpers, validator],
../beacon_chain/[block_pool, beacon_chain_db, extras, state_transition, ssz]
suite "Block pool processing":
var
genState = get_genesis_beacon_state(
makeInitialDeposits(flags = {skipValidation}), 0, Eth1Data(),
{skipValidation})
genBlock = get_initial_beacon_block(genState)
test "loadTailState gets genesis block on first load":
var
pool = BlockPool.init(makeTestDB(genState, genBlock))
state = pool.loadTailState()
b0 = pool.get(state.blck.root)
check:
state.data.slot == GENESIS_SLOT
b0.isSome()
test "Simple block add&get":
var
pool = BlockPool.init(makeTestDB(genState, genBlock))
state = pool.loadTailState()
let
b1 = makeBlock(state.data, state.blck.root, BeaconBlockBody())
b1Root = hash_tree_root_final(b1)
# TODO the return value is ugly here, need to fix and test..
discard pool.add(b1Root, b1)
let b1Ref = pool.get(b1Root)
check:
b1Ref.isSome()
b1Ref.get().refs.root == b1Root
test "Reverse order block add & get":
var
db = makeTestDB(genState, genBlock)
pool = BlockPool.init(db)
state = pool.loadTailState()
let
b1 = addBlock(
state.data, state.blck.root, BeaconBlockBody(), {skipValidation})
b1Root = hash_tree_root_final(b1)
b2 = addBlock(state.data, b1Root, BeaconBlockBody(), {skipValidation})
b2Root = hash_tree_root_final(b2)
discard pool.add(b2Root, b2)
check:
pool.get(b2Root).isNone() # Unresolved, shouldn't show up
b1Root in pool.checkUnresolved()
discard pool.add(b1Root, b1)
let
b1r = pool.get(b1Root)
b2r = pool.get(b2Root)
check:
b1r.isSome()
b2r.isSome()
b1r.get().refs.children[0] == b2r.get().refs
b2r.get().refs.parent == b1r.get().refs
db.putHeadBlock(b2Root)
# check that init also reloads block graph
var
pool2 = BlockPool.init(db)
check:
pool2.get(b1Root).isSome()
pool2.get(b2Root).isSome()

View File

@ -7,7 +7,8 @@
import
options, sequtils,
../beacon_chain/[extras, ssz, state_transition, validator_pool],
eth/trie/[db],
../beacon_chain/[beacon_chain_db, extras, ssz, state_transition, validator_pool],
../beacon_chain/spec/[beaconstate, crypto, datatypes, digest, helpers, validator]
func makeValidatorPrivKey(i: int): ValidatorPrivKey =
@ -201,3 +202,13 @@ proc makeAttestation*(
aggregate_signature: sig,
custody_bitfield: repeat(0'u8, ceil_div8(sac.committee.len))
)
proc makeTestDB*(tailState: BeaconState, tailBlock: BeaconBlock): BeaconChainDB =
let
tailRoot = hash_tree_root_final(tailBlock)
result = init(BeaconChainDB, newMemoryDB())
result.putState(tailState)
result.putBlock(tailBlock)
result.putTailBlock(tailRoot)
result.putHeadBlock(tailRoot)