nimbus-eth2/beacon_chain/spec/beacon_time.nim

284 lines
12 KiB
Nim
Raw Normal View History

# beacon_chain
# Copyright (c) 2018-2024 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.
{.push raises: [].}
import
std/[hashes, typetraits],
chronicles,
chronos/timer,
json_serialization,
./presets
export hashes, timer, json_serialization, presets
# A collection of time units that permeate the spec - common to all of them is
# that they expressed relative to the genesis of the chain at varying
# granularities:
#
# * BeaconTime - nanoseconds since genesis
# * Slot - SLOTS_PER_SECOND seconds since genesis
# * Epoch - EPOCHS_PER_SLOT slots since genesis
# * SyncCommitteePeriod - EPOCHS_PER_SYNC_COMMITTEE_PERIOD epochs since genesis
type
BeaconTime* = object
## A point in time, relative to the genesis of the chain
##
## Implemented as nanoseconds since genesis - negative means before
## the chain started.
ns_since_genesis*: int64
TimeDiff* = object
nanoseconds*: int64
## Difference between two points in time with nanosecond granularity
## Can be negative (unlike timer.Duration)
const
# Earlier spec versions had these at a different slot
GENESIS_SLOT* = Slot(0)
GENESIS_EPOCH* = Epoch(0) # compute_epoch_at_slot(GENESIS_SLOT)
# https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.7/specs/phase0/fork-choice.md#constant
INTERVALS_PER_SLOT* = 3
FAR_FUTURE_BEACON_TIME* = BeaconTime(ns_since_genesis: int64.high())
NANOSECONDS_PER_SLOT* = SECONDS_PER_SLOT * 1_000_000_000'u64
era: load blocks and states (#3394) * era: load blocks and states Era files contain finalized history and can be thought of as an alternative source for block and state data that allows clients to avoid syncing this information from the P2P network - the P2P network is then used to "top up" the client with the most recent data. They can be freely shared in the community via whatever means (http, torrent, etc) and serve as a permanent cold store of consensus data (and, after the merge, execution data) for history buffs and bean counters alike. This PR gently introduces support for loading blocks and states in two cases: block requests from rest/p2p and frontfilling when doing checkpoint sync. The era files are used as a secondary source if the information is not found in the database - compared to the database, there are a few key differences: * the database stores the block indexed by block root while the era file indexes by slot - the former is used only in rest, while the latter is used both by p2p and rest. * when loading blocks from era files, the root is no longer trivially available - if it is needed, it must either be computed (slow) or cached (messy) - the good news is that for p2p requests, it is not needed * in era files, "framed" snappy encoding is used while in the database we store unframed snappy - for p2p2 requests, the latter requires recompression while the former could avoid it * front-filling is the process of using era files to replace backfilling - in theory this front-filling could happen from any block and front-fills with gaps could also be entertained, but our backfilling algorithm cannot take advantage of this because there's no (simple) way to tell it to "skip" a range. * front-filling, as implemented, is a bit slow (10s to load mainnet): we load the full BeaconState for every era to grab the roots of the blocks - it would be better to partially load the state - as such, it would also be good to be able to partially decompress snappy blobs * lookups from REST via root are served by first looking up a block summary in the database, then using the slot to load the block data from the era file - however, there needs to be an option to create the summary table from era files to fully support historical queries To test this, `ncli_db` has an era file exporter: the files it creates should be placed in an `era` folder next to `db` in the data directory. What's interesting in particular about this setup is that `db` remains as the source of truth for security purposes - it stores the latest synced head root which in turn determines where a node "starts" its consensus participation - the era directory however can be freely shared between nodes / people without any (significant) security implications, assuming the era files are consistent / not broken. There's lots of future improvements to be had: * we can drop the in-memory `BlockRef` index almost entirely - at this point, resident memory usage of Nimbus should drop to a cool 500-600 mb * we could serve era files via REST trivially: this would drop backfill times to whatever time it takes to download the files - unlike the current implementation that downloads block by block, downloading an era at a time almost entirely cuts out request overhead * we can "reasonably" recreate detailed state history from almost any point in time, turning an O(slot) process into O(1) effectively - we'll still need caches and indices to do this with sufficient efficiency for the rest api, but at least it cuts the whole process down to minutes instead of hours, for arbitrary points in time * CI: ignore failures with Nim-1.6 (temporary) * test fixes Co-authored-by: Ștefan Talpalaru <stefantalpalaru@yahoo.com>
2022-03-23 08:58:17 +00:00
template ethTimeUnit*(typ: type) {.dirty.} =
func `+`*(x: typ, y: uint64): typ {.borrow.}
func `-`*(x: typ, y: uint64): typ {.borrow.}
func `-`*(x: uint64, y: typ): typ {.borrow.}
# Not closed over type in question (Slot or Epoch)
func `mod`*(x: typ, y: uint64): uint64 {.borrow.}
func `div`*(x: typ, y: uint64): uint64 {.borrow.}
func `div`*(x: uint64, y: typ): uint64 {.borrow.}
func `-`*(x: typ, y: typ): uint64 {.borrow.}
iterator countdown*(a, b: typ, step: Positive = 1): typ =
# otherwise we use the signed version that breaks at the boundary
for i in countdown(distinctBase(a), distinctBase(b), step):
yield typ(i)
func `*`*(x: typ, y: uint64): uint64 {.borrow.}
func `+=`*(x: var typ, y: typ) {.borrow.}
func `+=`*(x: var typ, y: uint64) {.borrow.}
func `-=`*(x: var typ, y: typ) {.borrow.}
func `-=`*(x: var typ, y: uint64) {.borrow.}
# Comparison operators
func `<`*(x: typ, y: typ): bool {.borrow.}
func `<`*(x: typ, y: uint64): bool {.borrow.}
func `<`*(x: uint64, y: typ): bool {.borrow.}
func `<=`*(x: typ, y: typ): bool {.borrow.}
func `<=`*(x: typ, y: uint64): bool {.borrow.}
func `<=`*(x: uint64, y: typ): bool {.borrow.}
func `==`*(x: typ, y: typ): bool {.borrow.}
func `==`*(x: typ, y: uint64): bool {.borrow.}
func `==`*(x: uint64, y: typ): bool {.borrow.}
# Nim integration
func `$`*(x: typ): string {.borrow.}
func hash*(x: typ): Hash {.borrow.}
template asUInt64*(v: typ): uint64 = distinctBase(v)
template shortLog*(v: typ): auto = distinctBase(v)
# Serialization
proc writeValue*(writer: var JsonWriter, value: typ) {.raises: [IOError].} =
writeValue(writer, uint64 value)
proc readValue*(reader: var JsonReader, value: var typ)
{.raises: [IOError, SerializationError].} =
value = typ reader.readValue(uint64)
ethTimeUnit Slot
ethTimeUnit Epoch
ethTimeUnit SyncCommitteePeriod
template `<`*(a, b: BeaconTime): bool = a.ns_since_genesis < b.ns_since_genesis
template `<=`*(a, b: BeaconTime): bool = a.ns_since_genesis <= b.ns_since_genesis
template `<`*(a, b: TimeDiff): bool = a.nanoseconds < b.nanoseconds
template `<=`*(a, b: TimeDiff): bool = a.nanoseconds <= b.nanoseconds
template `<`*(a: TimeDiff, b: Duration): bool = a.nanoseconds < b.nanoseconds
func toSlot*(t: BeaconTime): tuple[afterGenesis: bool, slot: Slot] =
if t == FAR_FUTURE_BEACON_TIME:
(true, FAR_FUTURE_SLOT)
elif t.ns_since_genesis >= 0:
(true, Slot(uint64(t.ns_since_genesis) div NANOSECONDS_PER_SLOT))
else:
(false, Slot(uint64(-t.ns_since_genesis) div NANOSECONDS_PER_SLOT))
template `+`*(t: BeaconTime, offset: Duration | TimeDiff): BeaconTime =
BeaconTime(ns_since_genesis: t.ns_since_genesis + offset.nanoseconds)
template `-`*(t: BeaconTime, offset: Duration | TimeDiff): BeaconTime =
BeaconTime(ns_since_genesis: t.ns_since_genesis - offset.nanoseconds)
template `-`*(a, b: BeaconTime): TimeDiff =
TimeDiff(nanoseconds: a.ns_since_genesis - b.ns_since_genesis)
template `+`*(a: TimeDiff, b: Duration): TimeDiff =
TimeDiff(nanoseconds: a.nanoseconds + b.nanoseconds)
const
# Offsets from the start of the slot to when the corresponding message should
# be sent
# https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.7/specs/phase0/validator.md#attesting
attestationSlotOffset* = TimeDiff(nanoseconds:
NANOSECONDS_PER_SLOT.int64 div INTERVALS_PER_SLOT)
# https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.7/specs/phase0/validator.md#broadcast-aggregate
aggregateSlotOffset* = TimeDiff(nanoseconds:
NANOSECONDS_PER_SLOT.int64 * 2 div INTERVALS_PER_SLOT)
# https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.7/specs/altair/validator.md#prepare-sync-committee-message
syncCommitteeMessageSlotOffset* = TimeDiff(nanoseconds:
NANOSECONDS_PER_SLOT.int64 div INTERVALS_PER_SLOT)
# https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.7/specs/altair/validator.md#broadcast-sync-committee-contribution
syncContributionSlotOffset* = TimeDiff(nanoseconds:
NANOSECONDS_PER_SLOT.int64 * 2 div INTERVALS_PER_SLOT)
# https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.7/specs/altair/light-client/p2p-interface.md#sync-committee
lightClientFinalityUpdateSlotOffset* = TimeDiff(nanoseconds:
NANOSECONDS_PER_SLOT.int64 div INTERVALS_PER_SLOT)
# https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.7/specs/altair/light-client/p2p-interface.md#sync-committee
lightClientOptimisticUpdateSlotOffset* = TimeDiff(nanoseconds:
NANOSECONDS_PER_SLOT.int64 div INTERVALS_PER_SLOT)
func toFloatSeconds*(t: TimeDiff): float =
float(t.nanoseconds) / 1_000_000_000.0
func start_beacon_time*(s: Slot): BeaconTime =
# The point in time that a slot begins
const maxSlot = Slot(
uint64(FAR_FUTURE_BEACON_TIME.ns_since_genesis) div NANOSECONDS_PER_SLOT)
if s > maxSlot: FAR_FUTURE_BEACON_TIME
else: BeaconTime(ns_since_genesis: int64(uint64(s) * NANOSECONDS_PER_SLOT))
func block_deadline*(s: Slot): BeaconTime =
s.start_beacon_time
func attestation_deadline*(s: Slot): BeaconTime =
s.start_beacon_time + attestationSlotOffset
func aggregate_deadline*(s: Slot): BeaconTime =
s.start_beacon_time + aggregateSlotOffset
func sync_committee_message_deadline*(s: Slot): BeaconTime =
s.start_beacon_time + syncCommitteeMessageSlotOffset
func sync_contribution_deadline*(s: Slot): BeaconTime =
s.start_beacon_time + syncContributionSlotOffset
func light_client_finality_update_time*(s: Slot): BeaconTime =
s.start_beacon_time + lightClientFinalityUpdateSlotOffset
func light_client_optimistic_update_time*(s: Slot): BeaconTime =
s.start_beacon_time + lightClientOptimisticUpdateSlotOffset
func slotOrZero*(time: BeaconTime): Slot =
let exSlot = time.toSlot
if exSlot.afterGenesis: exSlot.slot
else: Slot(0)
# https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.6/specs/phase0/beacon-chain.md#compute_epoch_at_slot
func epoch*(slot: Slot): Epoch = # aka compute_epoch_at_slot
## Return the epoch number at ``slot``.
if slot == FAR_FUTURE_SLOT: FAR_FUTURE_EPOCH
else: Epoch(slot div SLOTS_PER_EPOCH)
# https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.7/specs/phase0/fork-choice.md#compute_slots_since_epoch_start
func since_epoch_start*(slot: Slot): uint64 = # aka compute_slots_since_epoch_start
## How many slots since the beginning of the epoch (`[0..SLOTS_PER_EPOCH-1]`)
(slot mod SLOTS_PER_EPOCH)
template is_epoch*(slot: Slot): bool =
slot.since_epoch_start == 0
# https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.7/specs/phase0/beacon-chain.md#compute_start_slot_at_epoch
func start_slot*(epoch: Epoch): Slot = # aka compute_start_slot_at_epoch
## Return the start slot of ``epoch``.
const maxEpoch = Epoch(FAR_FUTURE_SLOT div SLOTS_PER_EPOCH)
if epoch >= maxEpoch: FAR_FUTURE_SLOT
else: Slot(epoch * SLOTS_PER_EPOCH)
# https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.6/specs/phase0/beacon-chain.md#get_previous_epoch
func get_previous_epoch*(current_epoch: Epoch): Epoch =
## Return the previous epoch (unless the current epoch is ``GENESIS_EPOCH``).
if current_epoch == GENESIS_EPOCH:
current_epoch
else:
current_epoch - 1
iterator slots*(epoch: Epoch): Slot =
let start_slot = start_slot(epoch)
for slot in start_slot ..< start_slot + SLOTS_PER_EPOCH:
yield slot
# https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.7/specs/altair/validator.md#sync-committee
template sync_committee_period*(epoch: Epoch): SyncCommitteePeriod =
if epoch == FAR_FUTURE_EPOCH: FAR_FUTURE_PERIOD
else: SyncCommitteePeriod(epoch div EPOCHS_PER_SYNC_COMMITTEE_PERIOD)
template sync_committee_period*(slot: Slot): SyncCommitteePeriod =
if slot == FAR_FUTURE_SLOT: FAR_FUTURE_PERIOD
else: SyncCommitteePeriod(slot div SLOTS_PER_SYNC_COMMITTEE_PERIOD)
func since_sync_committee_period_start*(slot: Slot): uint64 =
## How many slots since the beginning of the epoch (`[0..SLOTS_PER_SYNC_COMMITTEE_PERIOD-1]`)
(slot mod SLOTS_PER_SYNC_COMMITTEE_PERIOD)
func since_sync_committee_period_start*(epoch: Epoch): uint64 =
## How many slots since the beginning of the epoch (`[0..EPOCHS_PER_SYNC_COMMITTEE_PERIOD-1]`)
(epoch mod EPOCHS_PER_SYNC_COMMITTEE_PERIOD)
template is_sync_committee_period*(slot: Slot): bool =
slot.since_sync_committee_period_start() == 0
template is_sync_committee_period*(epoch: Epoch): bool =
epoch.since_sync_committee_period_start() == 0
template start_epoch*(period: SyncCommitteePeriod): Epoch =
## Return the start epoch of ``period``.
const maxPeriod = SyncCommitteePeriod(
FAR_FUTURE_EPOCH div EPOCHS_PER_SYNC_COMMITTEE_PERIOD)
if period >= maxPeriod: FAR_FUTURE_EPOCH
else: Epoch(period * EPOCHS_PER_SYNC_COMMITTEE_PERIOD)
template start_slot*(period: SyncCommitteePeriod): Slot =
## Return the start slot of ``period``.
const maxPeriod = SyncCommitteePeriod(
FAR_FUTURE_SLOT div SLOTS_PER_SYNC_COMMITTEE_PERIOD)
if period >= maxPeriod: FAR_FUTURE_SLOT
else: Slot(period * SLOTS_PER_SYNC_COMMITTEE_PERIOD)
2023-05-16 08:21:52 +00:00
func proposer_dependent_slot*(epoch: Epoch): Slot =
if epoch >= 1: epoch.start_slot() - 1 else: Slot(0)
func attester_dependent_slot*(epoch: Epoch): Slot =
if epoch >= 2: (epoch - 1).start_slot() - 1 else: Slot(0)
2022-01-17 19:12:36 +00:00
func `$`*(t: BeaconTime): string =
if t.ns_since_genesis >= 0:
$(timer.nanoseconds(t.ns_since_genesis))
else:
"-" & $(timer.nanoseconds(-t.ns_since_genesis))
2022-01-17 19:12:36 +00:00
func `$`*(t: TimeDiff): string =
if t.nanoseconds >= 0:
$(timer.nanoseconds(t.nanoseconds))
else:
"-" & $(timer.nanoseconds(-t.nanoseconds))
2022-01-17 19:12:36 +00:00
func shortLog*(t: BeaconTime | TimeDiff): string = $t
chronicles.formatIt BeaconTime: it.shortLog
chronicles.formatIt TimeDiff: it.shortLog
chronicles.formatIt Slot: it.shortLog
chronicles.formatIt Epoch: it.shortLog
chronicles.formatIt SyncCommitteePeriod: it.shortLog
const defaultSyncHorizon* =
(uint64(10*60) + SECONDS_PER_SLOT - 1) div SECONDS_PER_SLOT