mirror of
https://github.com/status-im/nimbus-eth2.git
synced 2025-02-23 11:48:33 +00:00
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:
parent
20f99db058
commit
125231d321
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
366
beacon_chain/block_pool.nim
Normal 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
|
||||
)
|
@ -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 =
|
||||
|
@ -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())
|
||||
|
||||
|
@ -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))
|
||||
|
@ -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]
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -10,6 +10,7 @@ import
|
||||
./test_beacon_chain_db,
|
||||
./test_beacon_node,
|
||||
./test_beaconstate,
|
||||
./test_block_pool,
|
||||
./test_helpers,
|
||||
./test_ssz,
|
||||
./test_state_transition,
|
||||
|
1
tests/simulation/.gitignore
vendored
1
tests/simulation/.gitignore
vendored
@ -1,2 +1,3 @@
|
||||
data/
|
||||
startup/
|
||||
|
||||
|
29
tests/simulation/run_node.sh
Executable file
29
tests/simulation/run_node.sh
Executable 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
|
@ -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
22
tests/simulation/vars.sh
Normal 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"
|
@ -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
|
||||
|
@ -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
89
tests/test_block_pool.nim
Normal 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()
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user