era: Capella+ support (fixes #4752) (#4853)

Post-Capella, historical roots are computed from historical summaries
instead of being directly stored in the beacon state.

Slightly messy to pass both lists around - this is done to avoid
computing the historical root unnecessarily.
This commit is contained in:
Jacek Sieka 2023-04-24 15:26:28 +02:00 committed by GitHub
parent fbe90dcbea
commit 58b93ccbe0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 60 additions and 22 deletions

View File

@ -238,6 +238,7 @@ proc containsBlock(dag: ChainDAGRef, bid: BlockId): bool =
(bid.slot <= dag.finalizedHead.slot and (bid.slot <= dag.finalizedHead.slot and
getBlockSZ( getBlockSZ(
dag.era, getStateField(dag.headState, historical_roots).asSeq, dag.era, getStateField(dag.headState, historical_roots).asSeq,
dag.headState.historical_summaries().asSeq,
bid.slot, bytes).isOk and bytes.len > 0) bid.slot, bytes).isOk and bytes.len > 0)
proc getBlock*( proc getBlock*(
@ -246,6 +247,7 @@ proc getBlock*(
dag.db.getBlock(bid.root, T) or dag.db.getBlock(bid.root, T) or
getBlock( getBlock(
dag.era, getStateField(dag.headState, historical_roots).asSeq, dag.era, getStateField(dag.headState, historical_roots).asSeq,
dag.headState.historical_summaries().asSeq,
bid.slot, Opt[Eth2Digest].ok(bid.root), T) bid.slot, Opt[Eth2Digest].ok(bid.root), T)
proc getBlockSSZ*(dag: ChainDAGRef, bid: BlockId, bytes: var seq[byte]): bool = proc getBlockSSZ*(dag: ChainDAGRef, bid: BlockId, bytes: var seq[byte]): bool =
@ -256,6 +258,7 @@ proc getBlockSSZ*(dag: ChainDAGRef, bid: BlockId, bytes: var seq[byte]): bool =
(bid.slot <= dag.finalizedHead.slot and (bid.slot <= dag.finalizedHead.slot and
getBlockSSZ( getBlockSSZ(
dag.era, getStateField(dag.headState, historical_roots).asSeq, dag.era, getStateField(dag.headState, historical_roots).asSeq,
dag.headState.historical_summaries().asSeq,
bid.slot, bytes).isOk) bid.slot, bytes).isOk)
proc getBlockSZ*(dag: ChainDAGRef, bid: BlockId, bytes: var seq[byte]): bool = proc getBlockSZ*(dag: ChainDAGRef, bid: BlockId, bytes: var seq[byte]): bool =
@ -268,6 +271,7 @@ proc getBlockSZ*(dag: ChainDAGRef, bid: BlockId, bytes: var seq[byte]): bool =
(bid.slot <= dag.finalizedHead.slot and (bid.slot <= dag.finalizedHead.slot and
getBlockSZ( getBlockSZ(
dag.era, getStateField(dag.headState, historical_roots).asSeq, dag.era, getStateField(dag.headState, historical_roots).asSeq,
dag.headState.historical_summaries().asSeq,
bid.slot, bytes).isOk) bid.slot, bytes).isOk)
proc getForkedBlock*( proc getForkedBlock*(
@ -280,6 +284,7 @@ proc getForkedBlock*(
blck = getBlock(dag, bid, T).valueOr: blck = getBlock(dag, bid, T).valueOr:
getBlock( getBlock(
dag.era, getStateField(dag.headState, historical_roots).asSeq, dag.era, getStateField(dag.headState, historical_roots).asSeq,
dag.headState.historical_summaries().asSeq,
bid.slot, Opt[Eth2Digest].ok(bid.root), T).valueOr: bid.slot, Opt[Eth2Digest].ok(bid.root), T).valueOr:
result.err() result.err()
return return
@ -1152,13 +1157,15 @@ proc init*(T: type ChainDAGRef, cfg: RuntimeConfig, db: BeaconChainDB,
let let
historical_roots = getStateField(dag.headState, historical_roots).asSeq() historical_roots = getStateField(dag.headState, historical_roots).asSeq()
historical_summaries = dag.headState.historical_summaries.asSeq()
var var
blocks = 0 blocks = 0
# Here, we'll build up the slot->root mapping in memory for the range of # Here, we'll build up the slot->root mapping in memory for the range of
# blocks from genesis to backfill, if possible. # blocks from genesis to backfill, if possible.
for bid in dag.era.getBlockIds(historical_roots, Slot(0), Eth2Digest()): for bid in dag.era.getBlockIds(
historical_roots, historical_summaries, Slot(0), Eth2Digest()):
if bid.slot >= dag.backfill.slot: if bid.slot >= dag.backfill.slot:
# If we end up in here, we failed the root comparison just below in # If we end up in here, we failed the root comparison just below in
# an earlier iteration # an earlier iteration
@ -2394,6 +2401,7 @@ proc rebuildIndex*(dag: ChainDAGRef) =
let let
roots = dag.db.loadStateRoots() roots = dag.db.loadStateRoots()
historicalRoots = getStateField(dag.headState, historical_roots).asSeq() historicalRoots = getStateField(dag.headState, historical_roots).asSeq()
historicalSummaries = dag.headState.historical_summaries.asSeq()
var var
canonical = newSeq[Eth2Digest]( canonical = newSeq[Eth2Digest](
@ -2456,7 +2464,8 @@ proc rebuildIndex*(dag: ChainDAGRef) =
# If we can find an era file with this state, use it as an alternative # If we can find an era file with this state, use it as an alternative
# starting point - ignore failures for now # starting point - ignore failures for now
var bytes: seq[byte] var bytes: seq[byte]
if dag.era.getState(historicalRoots, slot, state[]).isOk(): if dag.era.getState(
historicalRoots, historicalSummaries, slot, state[]).isOk():
state_root = getStateRoot(state[]) state_root = getStateRoot(state[])
withState(state[]): dag.db.putState(forkyState) withState(state[]): dag.db.putState(forkyState)

View File

@ -234,7 +234,8 @@ proc verify*(f: EraFile, cfg: RuntimeConfig): Result[Eth2Digest, string] =
ok(getStateRoot(state[])) ok(getStateRoot(state[]))
proc getEraFile( proc getEraFile(
db: EraDB, historical_roots: openArray[Eth2Digest], era: Era): db: EraDB, historical_roots: openArray[Eth2Digest],
historical_summaries: openArray[HistoricalSummary], era: Era):
Result[EraFile, string] = Result[EraFile, string] =
for f in db.files: for f in db.files:
if f.stateIdx.startSlot.era == era: if f.stateIdx.startSlot.era == era:
@ -242,7 +243,8 @@ proc getEraFile(
let let
eraRoot = eraRoot( eraRoot = eraRoot(
db.genesis_validators_root, historical_roots, era).valueOr: db.genesis_validators_root, historical_roots, historical_summaries,
era).valueOr:
return err("Era outside of known history") return err("Era outside of known history")
name = eraFileName(db.cfg, era, eraRoot) name = eraFileName(db.cfg, era, eraRoot)
path = db.path / name path = db.path / name
@ -265,7 +267,8 @@ proc getEraFile(
ok(f) ok(f)
proc getBlockSZ*( proc getBlockSZ*(
db: EraDB, historical_roots: openArray[Eth2Digest], slot: Slot, db: EraDB, historical_roots: openArray[Eth2Digest],
historical_summaries: openArray[HistoricalSummary], slot: Slot,
bytes: var seq[byte]): Result[void, string] = bytes: var seq[byte]): Result[void, string] =
## Get a snappy-frame-compressed version of the block data - may overwrite ## Get a snappy-frame-compressed version of the block data - may overwrite
## `bytes` on error ## `bytes` on error
@ -276,23 +279,26 @@ proc getBlockSZ*(
# Block content for the blocks of an era is found in the file for the _next_ # Block content for the blocks of an era is found in the file for the _next_
# era # era
let let
f = ? db.getEraFile(historical_roots, slot.era + 1) f = ? db.getEraFile(historical_roots, historical_summaries, slot.era + 1)
f.getBlockSZ(slot, bytes) f.getBlockSZ(slot, bytes)
proc getBlockSSZ*( proc getBlockSSZ*(
db: EraDB, historical_roots: openArray[Eth2Digest], slot: Slot, db: EraDB, historical_roots: openArray[Eth2Digest],
historical_summaries: openArray[HistoricalSummary], slot: Slot,
bytes: var seq[byte]): Result[void, string] = bytes: var seq[byte]): Result[void, string] =
let let
f = ? db.getEraFile(historical_roots, slot.era + 1) f = ? db.getEraFile(historical_roots, historical_summaries, slot.era + 1)
f.getBlockSSZ(slot, bytes) f.getBlockSSZ(slot, bytes)
proc getBlock*( proc getBlock*(
db: EraDB, historical_roots: openArray[Eth2Digest], slot: Slot, db: EraDB, historical_roots: openArray[Eth2Digest],
historical_summaries: openArray[HistoricalSummary], slot: Slot,
root: Opt[Eth2Digest], T: type ForkyTrustedSignedBeaconBlock): Opt[T] = root: Opt[Eth2Digest], T: type ForkyTrustedSignedBeaconBlock): Opt[T] =
var tmp: seq[byte] var tmp: seq[byte]
? db.getBlockSSZ(historical_roots, slot, tmp).mapErr(proc(x: auto) = discard) ? db.getBlockSSZ(
historical_roots, historical_summaries, slot, tmp).mapErr(proc(x: auto) = discard)
result.ok(default(T)) result.ok(default(T))
try: try:
@ -303,7 +309,8 @@ proc getBlock*(
result.err() result.err()
proc getStateSZ*( proc getStateSZ*(
db: EraDB, historical_roots: openArray[Eth2Digest], slot: Slot, db: EraDB, historical_roots: openArray[Eth2Digest],
historical_summaries: openArray[HistoricalSummary], slot: Slot,
bytes: var seq[byte]): bytes: var seq[byte]):
Result[void, string] = Result[void, string] =
## Get a snappy-frame-compressed version of the state data - may overwrite ## Get a snappy-frame-compressed version of the state data - may overwrite
@ -313,23 +320,25 @@ proc getStateSZ*(
# Block content for the blocks of an era is found in the file for the _next_ # Block content for the blocks of an era is found in the file for the _next_
# era # era
let let
f = ? db.getEraFile(historical_roots, slot.era) f = ? db.getEraFile(historical_roots, historical_summaries, slot.era)
f.getStateSZ(slot, bytes) f.getStateSZ(slot, bytes)
proc getStateSSZ*( proc getStateSSZ*(
db: EraDB, historical_roots: openArray[Eth2Digest], slot: Slot, db: EraDB, historical_roots: openArray[Eth2Digest],
historical_summaries: openArray[HistoricalSummary], slot: Slot,
bytes: var seq[byte], partial = Opt.none(int)): Result[void, string] = bytes: var seq[byte], partial = Opt.none(int)): Result[void, string] =
let let
f = ? db.getEraFile(historical_roots, slot.era) f = ? db.getEraFile(historical_roots, historical_summaries, slot.era)
f.getStateSSZ(slot, bytes, partial) f.getStateSSZ(slot, bytes, partial)
proc getState*( proc getState*(
db: EraDB, historical_roots: openArray[Eth2Digest], slot: Slot, db: EraDB, historical_roots: openArray[Eth2Digest],
historical_summaries: openArray[HistoricalSummary], slot: Slot,
state: var ForkedHashedBeaconState): Result[void, string] = state: var ForkedHashedBeaconState): Result[void, string] =
var bytes: seq[byte] var bytes: seq[byte]
? db.getStateSSZ(historical_roots, slot, bytes) ? db.getStateSSZ(historical_roots, historical_summaries, slot, bytes)
try: try:
state = readSszForkedHashedBeaconState(db.cfg, slot, bytes) state = readSszForkedHashedBeaconState(db.cfg, slot, bytes)
@ -356,7 +365,8 @@ type
## Needed to process attestations, older to newer ## Needed to process attestations, older to newer
proc getPartialState( proc getPartialState(
db: EraDB, historical_roots: openArray[Eth2Digest], slot: Slot, db: EraDB, historical_roots: openArray[Eth2Digest],
historical_summaries: openArray[HistoricalSummary], slot: Slot,
output: var PartialBeaconState): bool = output: var PartialBeaconState): bool =
static: doAssert isFixedSize(PartialBeaconState) static: doAssert isFixedSize(PartialBeaconState)
const partialBytes = fixedPortionSize(PartialBeaconState) const partialBytes = fixedPortionSize(PartialBeaconState)
@ -366,7 +376,8 @@ proc getPartialState(
# of reading the minimal number of bytes from disk # of reading the minimal number of bytes from disk
var tmp: seq[byte] var tmp: seq[byte]
if (let e = db.getStateSSZ( if (let e = db.getStateSSZ(
historical_roots, slot, tmp, Opt[int].ok(partialBytes)); historical_roots, historical_summaries, slot, tmp,
Opt[int].ok(partialBytes));
e.isErr): e.isErr):
return false return false
@ -379,6 +390,7 @@ proc getPartialState(
iterator getBlockIds*( iterator getBlockIds*(
db: EraDB, historical_roots: openArray[Eth2Digest], db: EraDB, historical_roots: openArray[Eth2Digest],
historical_summaries: openArray[HistoricalSummary],
start_slot: Slot, prev_root: Eth2Digest): BlockId = start_slot: Slot, prev_root: Eth2Digest): BlockId =
## Iterate over block roots starting from the given slot - `prev_root` must ## Iterate over block roots starting from the given slot - `prev_root` must
## point out the last block added to the chain before `start_slot` such that ## point out the last block added to the chain before `start_slot` such that
@ -394,7 +406,8 @@ iterator getBlockIds*(
case db.cfg.consensusForkAtEpoch(slot.epoch) case db.cfg.consensusForkAtEpoch(slot.epoch)
of ConsensusFork.Phase0 .. ConsensusFork.Deneb: of ConsensusFork.Phase0 .. ConsensusFork.Deneb:
let stateSlot = (slot.era() + 1).start_slot() let stateSlot = (slot.era() + 1).start_slot()
if not getPartialState(db, historical_roots, stateSlot, state[]): if not getPartialState(
db, historical_roots, historical_summaries, stateSlot, state[]):
state = nil # No `return` in iterators state = nil # No `return` in iterators
if state == nil: if state == nil:
@ -445,7 +458,7 @@ when isMainModule:
got8191 = false got8191 = false
got8192 = false got8192 = false
got8193 = false got8193 = false
for bid in db.getBlockIds(historical_roots, Slot(0), Eth2Digest()): for bid in db.getBlockIds(historical_roots, [], Slot(0), Eth2Digest()):
if bid.slot == Slot(0): if bid.slot == Slot(0):
doAssert bid.root == Eth2Digest.fromHex( doAssert bid.root == Eth2Digest.fromHex(
"0x4d611d5b93fdab69013a7f0a2f961caca0c853f87cfe9595fe50038163079360") "0x4d611d5b93fdab69013a7f0a2f961caca0c853f87cfe9595fe50038163079360")

View File

@ -1025,3 +1025,11 @@ func toBlockId*(blck: ForkedSignedBeaconBlock |
ForkedMsgTrustedSignedBeaconBlock | ForkedMsgTrustedSignedBeaconBlock |
ForkedTrustedSignedBeaconBlock): BlockId = ForkedTrustedSignedBeaconBlock): BlockId =
withBlck(blck): BlockId(root: blck.root, slot: blck.message.slot) withBlck(blck): BlockId(root: blck.root, slot: blck.message.slot)
func historical_summaries*(state: ForkedHashedBeaconState):
HashList[HistoricalSummary, Limit HISTORICAL_ROOTS_LIMIT] =
withState(state):
when consensusFork >= ConsensusFork.Capella:
forkyState.data.historical_summaries
else:
HashList[HistoricalSummary, Limit HISTORICAL_ROOTS_LIMIT]()

View File

@ -187,6 +187,7 @@ Each era is identified by when it ends. Thus, the genesis era is era `0`, follow
* `era-number` is the number of the _first_ era stored in the file - for example, the genesis era file has number 0 - as a 5-digit 0-filled decimal integer * `era-number` is the number of the _first_ era stored in the file - for example, the genesis era file has number 0 - as a 5-digit 0-filled decimal integer
* `short-era-root` is the first 4 bytes of the last historical root in the _last_ state in the era file, lower-case hex-encoded (8 characters), except the genesis era which instead uses the `genesis_validators_root` field from the genesis state. * `short-era-root` is the first 4 bytes of the last historical root in the _last_ state in the era file, lower-case hex-encoded (8 characters), except the genesis era which instead uses the `genesis_validators_root` field from the genesis state.
* The root is available as `state.historical_roots[era - 1]` except for genesis, which is `state.genesis_validators_root` * The root is available as `state.historical_roots[era - 1]` except for genesis, which is `state.genesis_validators_root`
* Post-Capella, the root must be computed from `state.historical_summaries[era - state.historical_roots.len - 1]`
Era files with multiple eras use the era number of the lowest era stored in the file, and the root of the highest era. Era files with multiple eras use the era number of the lowest era stored in the file, and the root of the highest era.

View File

@ -54,9 +54,15 @@ proc toString(v: IoErrorCode): string =
func eraRoot*( func eraRoot*(
genesis_validators_root: Eth2Digest, genesis_validators_root: Eth2Digest,
historical_roots: openArray[Eth2Digest], era: Era): Opt[Eth2Digest] = historical_roots: openArray[Eth2Digest],
historical_summaries: openArray[HistoricalSummary],
era: Era): Opt[Eth2Digest] =
if era == Era(0): ok(genesis_validators_root) if era == Era(0): ok(genesis_validators_root)
elif era <= historical_roots.lenu64(): ok(historical_roots[int(uint64(era) - 1)]) elif era <= historical_roots.lenu64():
ok(historical_roots[int(uint64(era) - 1)])
elif era <= historical_roots.lenu64() + historical_summaries.lenu64():
ok(hash_tree_root(
historical_summaries[int(uint64(era) - 1) - historical_roots.len()]))
else: err() else: err()
func eraFileName*( func eraFileName*(

View File

@ -528,6 +528,7 @@ proc cmdExportEra(conf: DbConf, cfg: RuntimeConfig) =
eraRoot( eraRoot(
forkyState.data.genesis_validators_root, forkyState.data.genesis_validators_root,
forkyState.data.historical_roots.asSeq, forkyState.data.historical_roots.asSeq,
dag.headState.historical_summaries().asSeq,
era).expect("have era root since we checked slot") era).expect("have era root since we checked slot")
name = eraFileName(cfg, era, eraRoot) name = eraFileName(cfg, era, eraRoot)