Introduce slot->BlockRef mapping for finalized chain (#3144)
* Introduce slot->BlockRef mapping for finalized chain The finalized chain is linear, thus we can use a seq to lookup blocks by slot number. Here, we introduce such a seq, even though in the future, it should likely be backed by a database structure instead, or, more likely, a flat era file with a flat lookup index. This dramatically speeds up requests by slot, such as those coming from the REST interface or GetBlocksByRange, as these are currently served by a linear iteration from head. * fix REST block requests to not return blocks from an earlier slot when the given slot is empty * fix StateId interpretation such that it doesn't treat state roots as block roots * don't load full block from database just to return its root
This commit is contained in:
parent
850eece949
commit
89d6a1b403
|
@ -92,7 +92,6 @@ OK: 5/5 Fail: 0/5 Skip: 0/5
|
||||||
OK: 1/1 Fail: 0/1 Skip: 0/1
|
OK: 1/1 Fail: 0/1 Skip: 0/1
|
||||||
## BlockRef and helpers [Preset: mainnet]
|
## BlockRef and helpers [Preset: mainnet]
|
||||||
```diff
|
```diff
|
||||||
+ epochAncestor sanity [Preset: mainnet] OK
|
|
||||||
+ get_ancestor sanity [Preset: mainnet] OK
|
+ get_ancestor sanity [Preset: mainnet] OK
|
||||||
+ isAncestorOf sanity [Preset: mainnet] OK
|
+ isAncestorOf sanity [Preset: mainnet] OK
|
||||||
```
|
```
|
||||||
|
|
|
@ -89,6 +89,10 @@ type
|
||||||
## Directed acyclic graph of blocks pointing back to a finalized block on the chain we're
|
## Directed acyclic graph of blocks pointing back to a finalized block on the chain we're
|
||||||
## interested in - we call that block the tail
|
## interested in - we call that block the tail
|
||||||
|
|
||||||
|
finalizedBlocks*: seq[BlockRef] ##\
|
||||||
|
## Slot -> BlockRef mapping for the canonical chain - use getBlockBySlot
|
||||||
|
## to access, generally
|
||||||
|
|
||||||
genesis*: BlockRef ##\
|
genesis*: BlockRef ##\
|
||||||
## The genesis block of the network
|
## The genesis block of the network
|
||||||
|
|
||||||
|
|
|
@ -137,6 +137,8 @@ func validatorKey*(
|
||||||
## non-head branch)!
|
## non-head branch)!
|
||||||
validatorKey(epochRef.dag, index)
|
validatorKey(epochRef.dag, index)
|
||||||
|
|
||||||
|
func epochAncestor*(dag: ChainDAGRef, blck: BlockRef, epoch: Epoch): EpochKey
|
||||||
|
|
||||||
func init*(
|
func init*(
|
||||||
T: type EpochRef, dag: ChainDAGRef, state: StateData,
|
T: type EpochRef, dag: ChainDAGRef, state: StateData,
|
||||||
cache: var StateCache): T =
|
cache: var StateCache): T =
|
||||||
|
@ -144,7 +146,7 @@ func init*(
|
||||||
epoch = state.data.get_current_epoch()
|
epoch = state.data.get_current_epoch()
|
||||||
epochRef = EpochRef(
|
epochRef = EpochRef(
|
||||||
dag: dag, # This gives access to the validator pubkeys through an EpochRef
|
dag: dag, # This gives access to the validator pubkeys through an EpochRef
|
||||||
key: state.blck.epochAncestor(epoch),
|
key: epochAncestor(dag, state.blck, epoch),
|
||||||
eth1_data: getStateField(state.data, eth1_data),
|
eth1_data: getStateField(state.data, eth1_data),
|
||||||
eth1_deposit_index: getStateField(state.data, eth1_deposit_index),
|
eth1_deposit_index: getStateField(state.data, eth1_deposit_index),
|
||||||
current_justified_checkpoint:
|
current_justified_checkpoint:
|
||||||
|
@ -251,7 +253,21 @@ func atEpochStart*(blck: BlockRef, epoch: Epoch): BlockSlot =
|
||||||
## Return the BlockSlot corresponding to the first slot in the given epoch
|
## Return the BlockSlot corresponding to the first slot in the given epoch
|
||||||
atSlot(blck, epoch.compute_start_slot_at_epoch)
|
atSlot(blck, epoch.compute_start_slot_at_epoch)
|
||||||
|
|
||||||
func epochAncestor*(blck: BlockRef, epoch: Epoch): EpochKey =
|
func getBlockBySlot*(dag: ChainDAGRef, slot: Slot): BlockSlot =
|
||||||
|
## Retrieve the canonical block at the given slot, or the last block that
|
||||||
|
## comes before - similar to atSlot, but without the linear scan
|
||||||
|
if slot > dag.finalizedHead.slot:
|
||||||
|
return dag.head.atSlot(slot) # Linear iteration is the fastest we have
|
||||||
|
|
||||||
|
var tmp = slot.int
|
||||||
|
while true:
|
||||||
|
if dag.finalizedBlocks[tmp] != nil:
|
||||||
|
return dag.finalizedBlocks[tmp].atSlot(slot)
|
||||||
|
if tmp == 0:
|
||||||
|
raiseAssert "At least the genesis block should be available!"
|
||||||
|
tmp = tmp - 1
|
||||||
|
|
||||||
|
func epochAncestor*(dag: ChainDAGRef, blck: BlockRef, epoch: Epoch): EpochKey =
|
||||||
## The state transition works by storing information from blocks in a
|
## The state transition works by storing information from blocks in a
|
||||||
## "working" area until the epoch transition, then batching work collected
|
## "working" area until the epoch transition, then batching work collected
|
||||||
## during the epoch. Thus, last block in the ancestor epochs is the block
|
## during the epoch. Thus, last block in the ancestor epochs is the block
|
||||||
|
@ -260,15 +276,17 @@ func epochAncestor*(blck: BlockRef, epoch: Epoch): EpochKey =
|
||||||
## This function returns a BlockSlot pointing to that epoch boundary, ie the
|
## This function returns a BlockSlot pointing to that epoch boundary, ie the
|
||||||
## boundary where the last block has been applied to the state and epoch
|
## boundary where the last block has been applied to the state and epoch
|
||||||
## processing has been done.
|
## processing has been done.
|
||||||
var blck = blck
|
let blck =
|
||||||
while blck.slot.epoch >= epoch and not blck.parent.isNil:
|
if epoch == GENESIS_EPOCH:
|
||||||
blck = blck.parent
|
dag.genesis
|
||||||
|
else:
|
||||||
|
dag.getBlockBySlot(compute_start_slot_at_epoch(epoch) - 1).blck
|
||||||
|
|
||||||
EpochKey(epoch: epoch, blck: blck)
|
EpochKey(epoch: epoch, blck: blck)
|
||||||
|
|
||||||
func findEpochRef*(
|
func findEpochRef*(
|
||||||
dag: ChainDAGRef, blck: BlockRef, epoch: Epoch): EpochRef = # may return nil!
|
dag: ChainDAGRef, blck: BlockRef, epoch: Epoch): EpochRef = # may return nil!
|
||||||
let ancestor = blck.epochAncestor(epoch)
|
let ancestor = epochAncestor(dag, blck, epoch)
|
||||||
doAssert ancestor.blck != nil
|
doAssert ancestor.blck != nil
|
||||||
for i in 0..<dag.epochRefs.len:
|
for i in 0..<dag.epochRefs.len:
|
||||||
if dag.epochRefs[i] != nil and dag.epochRefs[i].key == ancestor:
|
if dag.epochRefs[i] != nil and dag.epochRefs[i].key == ancestor:
|
||||||
|
@ -540,6 +558,13 @@ proc init*(T: type ChainDAGRef, cfg: RuntimeConfig, db: BeaconChainDB,
|
||||||
tailRef.slot.epoch)
|
tailRef.slot.epoch)
|
||||||
dag.finalizedHead = headRef.atEpochStart(finalizedEpoch)
|
dag.finalizedHead = headRef.atEpochStart(finalizedEpoch)
|
||||||
|
|
||||||
|
block:
|
||||||
|
dag.finalizedBlocks.setLen(dag.finalizedHead.slot.int + 1)
|
||||||
|
var tmp = dag.finalizedHead.blck
|
||||||
|
while not isNil(tmp):
|
||||||
|
dag.finalizedBlocks[tmp.slot.int] = tmp
|
||||||
|
tmp = tmp.parent
|
||||||
|
|
||||||
dag.clearanceState = dag.headState
|
dag.clearanceState = dag.headState
|
||||||
|
|
||||||
# Pruning metadata
|
# Pruning metadata
|
||||||
|
@ -605,7 +630,7 @@ proc getEpochRef*(dag: ChainDAGRef, blck: BlockRef, epoch: Epoch): EpochRef =
|
||||||
beacon_state_data_cache_misses.inc
|
beacon_state_data_cache_misses.inc
|
||||||
|
|
||||||
let
|
let
|
||||||
ancestor = blck.epochAncestor(epoch)
|
ancestor = epochAncestor(dag, blck, epoch)
|
||||||
|
|
||||||
dag.withState(
|
dag.withState(
|
||||||
dag.epochRefState, ancestor.blck.atEpochStart(ancestor.epoch)):
|
dag.epochRefState, ancestor.blck.atEpochStart(ancestor.epoch)):
|
||||||
|
@ -724,7 +749,7 @@ func getBlockRange*(
|
||||||
endSlot = startSlot + extraBlocks * skipStep
|
endSlot = startSlot + extraBlocks * skipStep
|
||||||
|
|
||||||
var
|
var
|
||||||
b = dag.head.atSlot(endSlot)
|
b = dag.getBlockBySlot(endSlot)
|
||||||
o = output.len
|
o = output.len
|
||||||
|
|
||||||
# Process all blocks that follow the start block (may be zero blocks)
|
# Process all blocks that follow the start block (may be zero blocks)
|
||||||
|
@ -743,11 +768,6 @@ func getBlockRange*(
|
||||||
|
|
||||||
o # Return the index of the first non-nil item in the output
|
o # Return the index of the first non-nil item in the output
|
||||||
|
|
||||||
func getBlockBySlot*(dag: ChainDAGRef, slot: Slot): BlockSlot =
|
|
||||||
## Retrieves the first block in the current canonical chain
|
|
||||||
## with slot number less or equal to `slot`.
|
|
||||||
dag.head.atSlot(slot)
|
|
||||||
|
|
||||||
proc getForkedBlock*(dag: ChainDAGRef, blck: BlockRef): ForkedTrustedSignedBeaconBlock =
|
proc getForkedBlock*(dag: ChainDAGRef, blck: BlockRef): ForkedTrustedSignedBeaconBlock =
|
||||||
case dag.cfg.blockForkAtEpoch(blck.slot.epoch)
|
case dag.cfg.blockForkAtEpoch(blck.slot.epoch)
|
||||||
of BeaconBlockFork.Phase0:
|
of BeaconBlockFork.Phase0:
|
||||||
|
@ -1318,6 +1338,16 @@ proc updateHead*(
|
||||||
finalized = shortLog(getStateField(
|
finalized = shortLog(getStateField(
|
||||||
dag.headState.data, finalized_checkpoint))
|
dag.headState.data, finalized_checkpoint))
|
||||||
|
|
||||||
|
block:
|
||||||
|
# Update `dag.finalizedBlocks` with all newly finalized blocks (those
|
||||||
|
# newer than the previous finalized head), then update `dag.finalizedHead`
|
||||||
|
|
||||||
|
dag.finalizedBlocks.setLen(finalizedHead.slot.int + 1)
|
||||||
|
var tmp = finalizedHead.blck
|
||||||
|
while not isNil(tmp) and tmp.slot >= dag.finalizedHead.slot:
|
||||||
|
dag.finalizedBlocks[tmp.slot.int] = tmp
|
||||||
|
tmp = tmp.parent
|
||||||
|
|
||||||
dag.finalizedHead = finalizedHead
|
dag.finalizedHead = finalizedHead
|
||||||
|
|
||||||
beacon_finalized_epoch.set(getStateField(
|
beacon_finalized_epoch.set(getStateField(
|
||||||
|
|
|
@ -686,20 +686,16 @@ proc installBeaconApiHandlers*(router: var RestRouter, node: BeaconNode) =
|
||||||
$rroot.error())
|
$rroot.error())
|
||||||
return RestApiResponse.jsonError(Http500, NoImplementationError)
|
return RestApiResponse.jsonError(Http500, NoImplementationError)
|
||||||
|
|
||||||
let bdata =
|
let blck =
|
||||||
block:
|
block:
|
||||||
let head =
|
let res = node.getCurrentBlock(qslot)
|
||||||
block:
|
|
||||||
let res = node.getCurrentHead(qslot)
|
|
||||||
if res.isErr():
|
if res.isErr():
|
||||||
return RestApiResponse.jsonError(Http404, SlotNotFoundError,
|
return RestApiResponse.jsonError(Http404, BlockNotFoundError,
|
||||||
$res.error())
|
$res.error())
|
||||||
res.get()
|
res.get()
|
||||||
let blockSlot = head.atSlot(qslot)
|
|
||||||
if isNil(blockSlot.blck):
|
|
||||||
return RestApiResponse.jsonError(Http404, BlockNotFoundError)
|
|
||||||
node.dag.get(blockSlot.blck)
|
|
||||||
|
|
||||||
|
|
||||||
|
let bdata = node.dag.get(blck)
|
||||||
return
|
return
|
||||||
withBlck(bdata.data):
|
withBlck(bdata.data):
|
||||||
RestApiResponse.jsonResponse(
|
RestApiResponse.jsonResponse(
|
||||||
|
@ -871,18 +867,16 @@ proc installBeaconApiHandlers*(router: var RestRouter, node: BeaconNode) =
|
||||||
# https://ethereum.github.io/beacon-APIs/#/Beacon/getBlockRoot
|
# https://ethereum.github.io/beacon-APIs/#/Beacon/getBlockRoot
|
||||||
router.api(MethodGet, "/api/eth/v1/beacon/blocks/{block_id}/root") do (
|
router.api(MethodGet, "/api/eth/v1/beacon/blocks/{block_id}/root") do (
|
||||||
block_id: BlockIdent) -> RestApiResponse:
|
block_id: BlockIdent) -> RestApiResponse:
|
||||||
let bdata =
|
let blck =
|
||||||
block:
|
block:
|
||||||
if block_id.isErr():
|
if block_id.isErr():
|
||||||
return RestApiResponse.jsonError(Http400, InvalidBlockIdValueError,
|
return RestApiResponse.jsonError(Http400, InvalidBlockIdValueError,
|
||||||
$block_id.error())
|
$block_id.error())
|
||||||
let res = node.getBlockDataFromBlockIdent(block_id.get())
|
let res = node.getBlockRef(block_id.get())
|
||||||
if res.isErr():
|
if res.isErr():
|
||||||
return RestApiResponse.jsonError(Http404, BlockNotFoundError)
|
return RestApiResponse.jsonError(Http404, BlockNotFoundError)
|
||||||
res.get()
|
res.get()
|
||||||
return
|
return RestApiResponse.jsonResponse((root: blck.root))
|
||||||
withBlck(bdata.data):
|
|
||||||
RestApiResponse.jsonResponse((root: blck.root))
|
|
||||||
|
|
||||||
# https://ethereum.github.io/beacon-APIs/#/Beacon/getBlockAttestations
|
# https://ethereum.github.io/beacon-APIs/#/Beacon/getBlockAttestations
|
||||||
router.api(MethodGet,
|
router.api(MethodGet,
|
||||||
|
|
|
@ -40,8 +40,22 @@ proc validate(key: string, value: string): int =
|
||||||
else:
|
else:
|
||||||
1
|
1
|
||||||
|
|
||||||
proc getCurrentHead*(node: BeaconNode,
|
func getCurrentSlot*(node: BeaconNode, slot: Slot):
|
||||||
slot: Slot): Result[BlockRef, cstring] =
|
Result[Slot, cstring] =
|
||||||
|
if slot <= (node.dag.head.slot + (SLOTS_PER_EPOCH * 2)):
|
||||||
|
ok(slot)
|
||||||
|
else:
|
||||||
|
err("Requesting slot too far ahead of the current head")
|
||||||
|
|
||||||
|
func getCurrentBlock*(node: BeaconNode, slot: Slot):
|
||||||
|
Result[BlockRef, cstring] =
|
||||||
|
let bs = node.dag.getBlockBySlot(? node.getCurrentSlot(slot))
|
||||||
|
if bs.slot == bs.blck.slot:
|
||||||
|
ok(bs.blck)
|
||||||
|
else:
|
||||||
|
err("Block not found")
|
||||||
|
|
||||||
|
proc getCurrentHead*(node: BeaconNode, slot: Slot): Result[BlockRef, cstring] =
|
||||||
let res = node.dag.head
|
let res = node.dag.head
|
||||||
# if not(node.isSynced(res)):
|
# if not(node.isSynced(res)):
|
||||||
# return err("Cannot fulfill request until node is synced")
|
# return err("Cannot fulfill request until node is synced")
|
||||||
|
@ -62,16 +76,13 @@ proc getBlockSlot*(node: BeaconNode,
|
||||||
stateIdent: StateIdent): Result[BlockSlot, cstring] =
|
stateIdent: StateIdent): Result[BlockSlot, cstring] =
|
||||||
case stateIdent.kind
|
case stateIdent.kind
|
||||||
of StateQueryKind.Slot:
|
of StateQueryKind.Slot:
|
||||||
let head = ? getCurrentHead(node, stateIdent.slot)
|
ok(node.dag.getBlockBySlot(? node.getCurrentSlot(stateIdent.slot)))
|
||||||
let bslot = head.atSlot(stateIdent.slot)
|
|
||||||
if isNil(bslot.blck):
|
|
||||||
return err("Block not found")
|
|
||||||
ok(bslot)
|
|
||||||
of StateQueryKind.Root:
|
of StateQueryKind.Root:
|
||||||
let blckRef = node.dag.getRef(stateIdent.root)
|
if stateIdent.root == getStateRoot(node.dag.headState.data):
|
||||||
if isNil(blckRef):
|
ok(node.dag.headState.blck.toBlockSlot())
|
||||||
return err("Block not found")
|
else:
|
||||||
ok(blckRef.toBlockSlot())
|
# We don't have a state root -> BlockSlot mapping
|
||||||
|
err("State not found")
|
||||||
of StateQueryKind.Named:
|
of StateQueryKind.Named:
|
||||||
case stateIdent.value
|
case stateIdent.value
|
||||||
of StateIdentType.Head:
|
of StateIdentType.Head:
|
||||||
|
@ -84,28 +95,29 @@ proc getBlockSlot*(node: BeaconNode,
|
||||||
ok(node.dag.head.atEpochStart(getStateField(
|
ok(node.dag.head.atEpochStart(getStateField(
|
||||||
node.dag.headState.data, current_justified_checkpoint).epoch))
|
node.dag.headState.data, current_justified_checkpoint).epoch))
|
||||||
|
|
||||||
proc getBlockDataFromBlockIdent*(node: BeaconNode,
|
proc getBlockRef*(node: BeaconNode,
|
||||||
id: BlockIdent): Result[BlockData, cstring] =
|
id: BlockIdent): Result[BlockRef, cstring] =
|
||||||
case id.kind
|
case id.kind
|
||||||
of BlockQueryKind.Named:
|
of BlockQueryKind.Named:
|
||||||
case id.value
|
case id.value
|
||||||
of BlockIdentType.Head:
|
of BlockIdentType.Head:
|
||||||
ok(node.dag.get(node.dag.head))
|
ok(node.dag.head)
|
||||||
of BlockIdentType.Genesis:
|
of BlockIdentType.Genesis:
|
||||||
ok(node.dag.getGenesisBlockData())
|
ok(node.dag.genesis)
|
||||||
of BlockIdentType.Finalized:
|
of BlockIdentType.Finalized:
|
||||||
ok(node.dag.get(node.dag.finalizedHead.blck))
|
ok(node.dag.finalizedHead.blck)
|
||||||
of BlockQueryKind.Root:
|
of BlockQueryKind.Root:
|
||||||
let res = node.dag.get(id.root)
|
let res = node.dag.getRef(id.root)
|
||||||
if res.isNone():
|
if isNil(res):
|
||||||
return err("Block not found")
|
err("Block not found")
|
||||||
ok(res.get())
|
else:
|
||||||
|
ok(res)
|
||||||
of BlockQueryKind.Slot:
|
of BlockQueryKind.Slot:
|
||||||
let head = ? node.getCurrentHead(id.slot)
|
node.getCurrentBlock(id.slot)
|
||||||
let blockSlot = head.atSlot(id.slot)
|
|
||||||
if isNil(blockSlot.blck):
|
proc getBlockDataFromBlockIdent*(node: BeaconNode,
|
||||||
return err("Block not found")
|
id: BlockIdent): Result[BlockData, cstring] =
|
||||||
ok(node.dag.get(blockSlot.blck))
|
ok(node.dag.get(? node.getBlockRef(id)))
|
||||||
|
|
||||||
template withStateForBlockSlot*(node: BeaconNode,
|
template withStateForBlockSlot*(node: BeaconNode,
|
||||||
blockSlot: BlockSlot, body: untyped): untyped =
|
blockSlot: BlockSlot, body: untyped): untyped =
|
||||||
|
|
|
@ -259,7 +259,7 @@ proc installValidatorApiHandlers*(router: var RestRouter, node: BeaconNode) =
|
||||||
# in order to compute the sync committee for the epoch. See the following
|
# in order to compute the sync committee for the epoch. See the following
|
||||||
# discussion for more details:
|
# discussion for more details:
|
||||||
# https://github.com/status-im/nimbus-eth2/pull/3133#pullrequestreview-817184693
|
# https://github.com/status-im/nimbus-eth2/pull/3133#pullrequestreview-817184693
|
||||||
node.withStateForBlockSlot(node.dag.head.atSlot(earliestSlotInQSyncPeriod)):
|
node.withStateForBlockSlot(node.dag.getBlockBySlot(earliestSlotInQSyncPeriod)):
|
||||||
let res = withState(stateData().data):
|
let res = withState(stateData().data):
|
||||||
when stateFork >= BeaconStateFork.Altair:
|
when stateFork >= BeaconStateFork.Altair:
|
||||||
produceResponse(indexList,
|
produceResponse(indexList,
|
||||||
|
|
|
@ -68,22 +68,6 @@ suite "BlockRef and helpers" & preset():
|
||||||
s4.get_ancestor(Slot(3)) == s2
|
s4.get_ancestor(Slot(3)) == s2
|
||||||
s4.get_ancestor(Slot(4)) == s4
|
s4.get_ancestor(Slot(4)) == s4
|
||||||
|
|
||||||
test "epochAncestor sanity" & preset():
|
|
||||||
let
|
|
||||||
s0 = BlockRef(slot: Slot(0))
|
|
||||||
var cur = s0
|
|
||||||
for i in 1..SLOTS_PER_EPOCH * 2:
|
|
||||||
cur = BlockRef(slot: Slot(i), parent: cur)
|
|
||||||
|
|
||||||
let ancestor = cur.epochAncestor(cur.slot.epoch)
|
|
||||||
|
|
||||||
check:
|
|
||||||
ancestor.epoch == cur.slot.epoch
|
|
||||||
ancestor.blck != cur # should have selected a parent
|
|
||||||
|
|
||||||
ancestor.blck.epochAncestor(cur.slot.epoch) == ancestor
|
|
||||||
ancestor.blck.epochAncestor(ancestor.blck.slot.epoch) != ancestor
|
|
||||||
|
|
||||||
suite "BlockSlot and helpers" & preset():
|
suite "BlockSlot and helpers" & preset():
|
||||||
test "atSlot sanity" & preset():
|
test "atSlot sanity" & preset():
|
||||||
let
|
let
|
||||||
|
|
Loading…
Reference in New Issue