291 lines
8.9 KiB
Nim
291 lines
8.9 KiB
Nim
# Copyright (c) 2018-2022 Status Research & Development GmbH
|
|
# Licensed and distributed under either of
|
|
# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT).
|
|
# * Apache v2 license (license terms in the root directory or at https://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
|
|
std/os,
|
|
chronicles,
|
|
stew/results,
|
|
snappy/framing,
|
|
../ncli/e2store,
|
|
./spec/datatypes/[altair, bellatrix, phase0],
|
|
./spec/forks,
|
|
./consensus_object_pools/block_dag # TODO move to somewhere else to avoid circular deps
|
|
|
|
export results, forks, e2store
|
|
|
|
type
|
|
EraFile = ref object
|
|
handle: IoHandle
|
|
stateIdx: Index
|
|
blockIdx: Index
|
|
|
|
EraDB* = ref object
|
|
## The Era database manages a collection of era files that together make up
|
|
## a linear history of beacon chain data.
|
|
cfg: RuntimeConfig
|
|
path: string
|
|
genesis_validators_root: Eth2Digest
|
|
|
|
files: seq[EraFile]
|
|
|
|
proc getEraFile(
|
|
db: EraDB, historical_roots: openArray[Eth2Digest], era: Era):
|
|
Result[EraFile, string] =
|
|
for f in db.files:
|
|
if f.stateIdx.startSlot.era == era:
|
|
return ok(f)
|
|
|
|
if db.files.len > 16:
|
|
discard closeFile(db.files[0].handle)
|
|
db.files.delete(0)
|
|
|
|
if era.uint64 > historical_roots.lenu64():
|
|
return err("Era outside of known history")
|
|
|
|
let
|
|
name = eraFileName(db.cfg, db.genesis_validators_root, historical_roots, era)
|
|
|
|
var
|
|
f = Opt[IoHandle].ok(? openFile(db.path / name, {OpenFlags.Read}).mapErr(ioErrorMsg))
|
|
|
|
defer:
|
|
if f.isSome(): discard closeFile(f[])
|
|
|
|
# Indices can be found at the end of each era file - we only support
|
|
# single-era files for now
|
|
? f[].setFilePos(0, SeekPosition.SeekEnd).mapErr(ioErrorMsg)
|
|
|
|
# Last in the file is the state index
|
|
let
|
|
stateIdxPos = ? f[].findIndexStartOffset()
|
|
? f[].setFilePos(stateIdxPos, SeekPosition.SeekCurrent).mapErr(ioErrorMsg)
|
|
|
|
let
|
|
stateIdx = ? f[].readIndex()
|
|
if stateIdx.offsets.len() != 1:
|
|
return err("State index length invalid")
|
|
|
|
? f[].setFilePos(stateIdxPos, SeekPosition.SeekCurrent).mapErr(ioErrorMsg)
|
|
|
|
# The genesis era file does not contain a block index
|
|
let blockIdx = if stateIdx.startSlot > 0:
|
|
let
|
|
blockIdxPos = ? f[].findIndexStartOffset()
|
|
? f[].setFilePos(blockIdxPos, SeekPosition.SeekCurrent).mapErr(ioErrorMsg)
|
|
let idx = ? f[].readIndex()
|
|
if idx.offsets.lenu64() != SLOTS_PER_HISTORICAL_ROOT:
|
|
return err("Block index length invalid")
|
|
|
|
idx
|
|
else:
|
|
Index()
|
|
|
|
let res = EraFile(handle: f[], stateIdx: stateIdx, blockIdx: blockIdx)
|
|
reset(f)
|
|
|
|
db.files.add(res)
|
|
ok(res)
|
|
|
|
proc getBlockSZ*(
|
|
db: EraDB, historical_roots: openArray[Eth2Digest], slot: Slot, bytes: var seq[byte]):
|
|
Result[void, string] =
|
|
## Get a snappy-frame-compressed version of the block data - may overwrite
|
|
## `bytes` on error
|
|
|
|
# Block content for the blocks of an era is found in the file for the _next_
|
|
# era
|
|
let
|
|
f = ? db.getEraFile(historical_roots, slot.era + 1)
|
|
pos = f[].blockIdx.offsets[slot - f[].blockIdx.startSlot]
|
|
|
|
if pos == 0:
|
|
return err("No block at given slot")
|
|
|
|
? f.handle.setFilePos(pos, SeekPosition.SeekBegin).mapErr(ioErrorMsg)
|
|
|
|
let header = ? f.handle.readRecord(bytes)
|
|
if header.typ != SnappyBeaconBlock:
|
|
return err("Invalid era file: didn't find block at index position")
|
|
|
|
ok()
|
|
|
|
proc getBlockSSZ*(
|
|
db: EraDB, historical_roots: openArray[Eth2Digest], slot: Slot,
|
|
bytes: var seq[byte]): Result[void, string] =
|
|
var tmp: seq[byte]
|
|
? db.getBlockSZ(historical_roots, slot, tmp)
|
|
|
|
try:
|
|
bytes = framingFormatUncompress(tmp)
|
|
ok()
|
|
except CatchableError as exc:
|
|
err(exc.msg)
|
|
|
|
proc getBlock*(
|
|
db: EraDB, historical_roots: openArray[Eth2Digest], slot: Slot,
|
|
root: Opt[Eth2Digest], T: type ForkyTrustedSignedBeaconBlock): Opt[T] =
|
|
var tmp: seq[byte]
|
|
? db.getBlockSSZ(historical_roots, slot, tmp).mapErr(proc(x: auto) = discard)
|
|
|
|
result.ok(default(T))
|
|
try:
|
|
readSszBytes(tmp, result.get(), updateRoot = root.isNone)
|
|
if root.isSome():
|
|
result.get().root = root.get()
|
|
except CatchableError as exc:
|
|
result.err()
|
|
|
|
proc getStateSZ*(
|
|
db: EraDB, historical_roots: openArray[Eth2Digest], slot: Slot,
|
|
bytes: var seq[byte]):
|
|
Result[void, string] =
|
|
## Get a snappy-frame-compressed version of the state data - may overwrite
|
|
## `bytes` on error
|
|
## https://github.com/google/snappy/blob/8dd58a519f79f0742d4c68fbccb2aed2ddb651e8/framing_format.txt#L34
|
|
|
|
# Block content for the blocks of an era is found in the file for the _next_
|
|
# era
|
|
let
|
|
f = ? db.getEraFile(historical_roots, slot.era)
|
|
|
|
if f.stateIdx.startSlot != slot:
|
|
return err("State not found in era file")
|
|
|
|
let pos = f.stateIdx.offsets[0]
|
|
if pos == 0:
|
|
return err("No state at given slot")
|
|
|
|
? f.handle.setFilePos(pos, SeekPosition.SeekBegin).mapErr(ioErrorMsg)
|
|
|
|
let header = ? f.handle.readRecord(bytes)
|
|
if header.typ != SnappyBeaconState:
|
|
return err("Invalid era file: didn't find state at index position")
|
|
|
|
ok()
|
|
|
|
proc getStateSSZ*(
|
|
db: EraDB, historical_roots: openArray[Eth2Digest], slot: Slot,
|
|
bytes: var seq[byte]): Result[void, string] =
|
|
var tmp: seq[byte]
|
|
? db.getStateSZ(historical_roots, slot, tmp)
|
|
|
|
try:
|
|
bytes = framingFormatUncompress(tmp)
|
|
ok()
|
|
except CatchableError as exc:
|
|
err(exc.msg)
|
|
|
|
type
|
|
PartialBeaconState = object
|
|
# The first bytes of a beacon state object are (for now) shared between all
|
|
# forks - we exploit this to speed up loading
|
|
|
|
# Versioning
|
|
genesis_time*: uint64
|
|
genesis_validators_root*: Eth2Digest
|
|
slot*: Slot
|
|
fork*: Fork
|
|
|
|
# History
|
|
latest_block_header*: BeaconBlockHeader ##\
|
|
## `latest_block_header.state_root == ZERO_HASH` temporarily
|
|
|
|
block_roots*: HashArray[Limit SLOTS_PER_HISTORICAL_ROOT, Eth2Digest] ##\
|
|
## Needed to process attestations, older to newer
|
|
|
|
proc getPartialState(
|
|
db: EraDB, historical_roots: openArray[Eth2Digest], slot: Slot,
|
|
output: var PartialBeaconState): bool =
|
|
# TODO don't read all bytes: we only need a few, and shouldn't decompress the
|
|
# rest - our snappy impl is very slow, in part to the crc32 check it
|
|
# performs
|
|
var tmp: seq[byte]
|
|
if (let e = db.getStateSSZ(historical_roots, slot, tmp); e.isErr):
|
|
debugecho e.error()
|
|
return false
|
|
|
|
static: doAssert isFixedSize(PartialBeaconState)
|
|
const partialBytes = fixedPortionSize(PartialBeaconState)
|
|
|
|
try:
|
|
readSszBytes(tmp.toOpenArray(0, partialBytes - 1), output)
|
|
true
|
|
except CatchableError as exc:
|
|
# TODO log?
|
|
false
|
|
|
|
iterator getBlockIds*(
|
|
db: EraDB, historical_roots: openArray[Eth2Digest], era: Era): BlockId =
|
|
# The state from which we load block roots is stored in the file corresponding
|
|
# to the "next" era
|
|
let fileEra = era + 1
|
|
|
|
var
|
|
state = (ref PartialBeaconState)() # avoid stack overflow
|
|
|
|
# `case` ensures we're on a fork for which the `PartialBeaconState`
|
|
# definition is consistent
|
|
case db.cfg.stateForkAtEpoch(fileEra.start_slot().epoch)
|
|
of BeaconStateFork.Phase0, BeaconStateFork.Altair, BeaconStateFork.Bellatrix:
|
|
if not getPartialState(db, historical_roots, fileEra.start_slot(), state[]):
|
|
state = nil # No `return` in iterators
|
|
|
|
if state != nil:
|
|
var
|
|
slot = era.start_slot()
|
|
for root in state[].block_roots:
|
|
yield BlockId(root: root, slot: slot)
|
|
slot += 1
|
|
|
|
proc new*(
|
|
T: type EraDB, cfg: RuntimeConfig, path: string,
|
|
genesis_validators_root: Eth2Digest): EraDB =
|
|
EraDb(cfg: cfg, path: path, genesis_validators_root: genesis_validators_root)
|
|
|
|
when isMainModule:
|
|
# Testing EraDB gets messy because of the large amounts of data involved:
|
|
# this snippet contains some sanity checks for mainnet at least
|
|
|
|
import
|
|
os,
|
|
stew/arrayops
|
|
|
|
let
|
|
dbPath =
|
|
if os.paramCount() == 1: os.paramStr(1)
|
|
else: "era"
|
|
|
|
db = EraDB.new(
|
|
defaultRuntimeConfig, dbPath,
|
|
Eth2Digest(
|
|
data: array[32, byte].initCopyFrom([byte 0x4b, 0x36, 0x3d, 0xb9])))
|
|
historical_roots = [
|
|
Eth2Digest(
|
|
data: array[32, byte].initCopyFrom([byte 0x40, 0xcf, 0x2f, 0x3c]))]
|
|
|
|
var got8191 = false
|
|
var slot4: Eth2Digest
|
|
for bid in db.getBlockIds(historical_roots, Era(0)):
|
|
if bid.slot == Slot(1):
|
|
doAssert bid.root == Eth2Digest.fromHex(
|
|
"0xbacd20f09da907734434f052bd4c9503aa16bab1960e89ea20610d08d064481c")
|
|
elif bid.slot == Slot(4):
|
|
slot4 = bid.root
|
|
elif bid.slot == Slot(5) and bid.root != slot4:
|
|
raiseAssert "this slot was skipped, should have same root"
|
|
elif bid.slot == Slot(8191):
|
|
doAssert bid.root == Eth2Digest.fromHex(
|
|
"0x48ea23af46320b0290eae668b0c3e6ae3e0534270f897db0e83a57f51a22baca")
|
|
got8191 = true
|
|
|
|
doAssert db.getBlock(
|
|
historical_roots, Slot(1), Opt[Eth2Digest].err(),
|
|
phase0.TrustedSignedBeaconBlock).get().root ==
|
|
Eth2Digest.fromHex(
|
|
"0xbacd20f09da907734434f052bd4c9503aa16bab1960e89ea20610d08d064481c")
|
|
|
|
doAssert got8191
|