# beacon_chain # Copyright (c) 2021 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: [Defect].} import stew/[assign2], chronicles, ../extras, "."/[eth2_merkleization, eth2_ssz_serialization, presets], ./datatypes/[phase0, altair, merge] export extras, phase0, altair, merge, eth2_merkleization, eth2_ssz_serialization, presets # This file contains helpers for dealing with forks - we have two ways we can # deal with forks: # * generics - this means using the static typing and differentiating forks # at compile time - this is preferred in fork-specific code where the fork # is known up-front, for example spec functions. # * variants - this means using a variant object and determining the fork at # runtime - this carries the obvious risk and complexity of dealing with # runtime checking, but is of course needed for external data that may be # of any fork kind. # # For generics, we define `Forky*` type classes that cover "similar" objects # across forks - for variants, they're called `Forked*` instead. # See withXxx and `init` for convenient ways of moving between these two worlds. # A clever programmer would use templates, macros and dark magic to create all # these types and converters :) type BeaconStateFork* {.pure.} = enum Phase0, Altair, Merge ForkyBeaconState* = phase0.BeaconState | altair.BeaconState | merge.BeaconState ForkyHashedBeaconState* = phase0.HashedBeaconState | altair.HashedBeaconState | merge.HashedBeaconState ForkedHashedBeaconState* = object case kind*: BeaconStateFork of BeaconStateFork.Phase0: phase0Data*: phase0.HashedBeaconState of BeaconStateFork.Altair: altairData*: altair.HashedBeaconState of BeaconStateFork.Merge: mergeData*: merge.HashedBeaconState BeaconBlockFork* {.pure.} = enum Phase0 Altair Merge ForkyBeaconBlock* = phase0.BeaconBlock | altair.BeaconBlock | merge.BeaconBlock ForkyTrustedBeaconBlock* = phase0.TrustedBeaconBlock | altair.TrustedBeaconBlock | merge.TrustedBeaconBlock ForkedBeaconBlock* = object case kind*: BeaconBlockFork of BeaconBlockFork.Phase0: phase0Data*: phase0.BeaconBlock of BeaconBlockFork.Altair: altairData*: altair.BeaconBlock of BeaconBlockFork.Merge: mergeData*: merge.BeaconBlock ForkedTrustedBeaconBlock* = object case kind*: BeaconBlockFork of BeaconBlockFork.Phase0: phase0Data*: phase0.TrustedBeaconBlock of BeaconBlockFork.Altair: altairData*: altair.TrustedBeaconBlock of BeaconBlockFork.Merge: mergeData*: merge.TrustedBeaconBlock ForkySignedBeaconBlock* = phase0.SignedBeaconBlock | altair.SignedBeaconBlock | merge.SignedBeaconBlock ForkedSignedBeaconBlock* = object case kind*: BeaconBlockFork of BeaconBlockFork.Phase0: phase0Data*: phase0.SignedBeaconBlock of BeaconBlockFork.Altair: altairData*: altair.SignedBeaconBlock of BeaconBlockFork.Merge: mergeData*: merge.SignedBeaconBlock ForkyTrustedSignedBeaconBlock* = phase0.TrustedSignedBeaconBlock | altair.TrustedSignedBeaconBlock | merge.TrustedSignedBeaconBlock ForkedTrustedSignedBeaconBlock* = object case kind*: BeaconBlockFork of BeaconBlockFork.Phase0: phase0Data*: phase0.TrustedSignedBeaconBlock of BeaconBlockFork.Altair: altairData*: altair.TrustedSignedBeaconBlock of BeaconBlockFork.Merge: mergeData*: merge.TrustedSignedBeaconBlock EpochInfoFork* {.pure.} = enum Phase0 Altair ForkedEpochInfo* = object case kind*: EpochInfoFork of EpochInfoFork.Phase0: phase0Data*: phase0.EpochInfo of EpochInfoFork.Altair: altairData*: altair.EpochInfo ForkyEpochInfo* = phase0.EpochInfo | altair.EpochInfo ForkDigests* = object phase0*: ForkDigest altair*: ForkDigest merge*: ForkDigest template toFork*[T: phase0.BeaconState | phase0.HashedBeaconState]( t: type T): BeaconStateFork = BeaconStateFork.Phase0 template toFork*[T: altair.BeaconState | altair.HashedBeaconState]( t: type T): BeaconStateFork = BeaconStateFork.Altair template toFork*[T: merge.BeaconState | merge.HashedBeaconState]( t: type T): BeaconStateFork = BeaconStateFork.Merge template init*(T: type ForkedHashedBeaconState, data: phase0.HashedBeaconState): T = T(kind: BeaconStateFork.Phase0, phase0Data: data) template init*(T: type ForkedHashedBeaconState, data: altair.HashedBeaconState): T = T(kind: BeaconStateFork.Altair, altairData: data) template init*(T: type ForkedHashedBeaconState, data: merge.HashedBeaconState): T = T(kind: BeaconStateFork.Merge, mergeData: data) template init*(T: type ForkedBeaconBlock, blck: phase0.BeaconBlock): T = T(kind: BeaconBlockFork.Phase0, phase0Data: blck) template init*(T: type ForkedBeaconBlock, blck: altair.BeaconBlock): T = T(kind: BeaconBlockFork.Altair, altairData: blck) template init*(T: type ForkedBeaconBlock, blck: merge.BeaconBlock): T = T(kind: BeaconBlockFork.Merge, mergeData: blck) template init*(T: type ForkedTrustedBeaconBlock, blck: phase0.TrustedBeaconBlock): T = T(kind: BeaconBlockFork.Phase0, phase0Data: blck) template init*(T: type ForkedTrustedBeaconBlock, blck: altair.TrustedBeaconBlock): T = T(kind: BeaconBlockFork.Altair, altairData: blck) template init*(T: type ForkedTrustedBeaconBlock, blck: merge.TrustedBeaconBlock): T = T(kind: BeaconBlockFork.Merge, mergeData: blck) template init*(T: type ForkedSignedBeaconBlock, blck: phase0.SignedBeaconBlock): T = T(kind: BeaconBlockFork.Phase0, phase0Data: blck) template init*(T: type ForkedSignedBeaconBlock, blck: altair.SignedBeaconBlock): T = T(kind: BeaconBlockFork.Altair, altairData: blck) template init*(T: type ForkedSignedBeaconBlock, blck: merge.SignedBeaconBlock): T = T(kind: BeaconBlockFork.Merge, mergeData: blck) template init*(T: type ForkedSignedBeaconBlock, forked: ForkedBeaconBlock, blockRoot: Eth2Digest, signature: ValidatorSig): T = case forked.kind of BeaconBlockFork.Phase0: T(kind: BeaconBlockFork.Phase0, phase0Data: phase0.SignedBeaconBlock(message: forked.phase0Data, root: blockRoot, signature: signature)) of BeaconBlockFork.Altair: T(kind: BeaconBlockFork.Altair, altairData: altair.SignedBeaconBlock(message: forked.altairData, root: blockRoot, signature: signature)) of BeaconBlockFork.Merge: T(kind: BeaconBlockFork.Merge, mergeData: merge.SignedBeaconBlock(message: forked.mergeData, root: blockRoot, signature: signature)) template init*(T: type ForkedTrustedSignedBeaconBlock, blck: phase0.TrustedSignedBeaconBlock): T = T(kind: BeaconBlockFork.Phase0, phase0Data: blck) template init*(T: type ForkedTrustedSignedBeaconBlock, blck: altair.TrustedSignedBeaconBlock): T = T(kind: BeaconBlockFork.Altair, altairData: blck) template init*(T: type ForkedTrustedSignedBeaconBlock, blck: merge.TrustedSignedBeaconBlock): T = T(kind: BeaconBlockFork.Merge, mergeData: blck) template toFork*[T: phase0.TrustedSignedBeaconBlock]( t: type T): BeaconBlockFork = BeaconBlockFork.Phase0 template toFork*[T: altair.TrustedSignedBeaconBlock]( t: type T): BeaconBlockFork = BeaconBlockFork.Altair template toFork*[T: merge.TrustedSignedBeaconBlock]( t: type T): BeaconBlockFork = BeaconBlockFork.Merge template init*(T: type ForkedEpochInfo, info: phase0.EpochInfo): T = T(kind: EpochInfoFork.Phase0, phase0Data: info) template init*(T: type ForkedEpochInfo, info: altair.EpochInfo): T = T(kind: EpochInfoFork.Altair, altairData: info) template withState*(x: ForkedHashedBeaconState, body: untyped): untyped = case x.kind of BeaconStateFork.Merge: const stateFork {.inject.} = BeaconStateFork.Merge template state: untyped {.inject.} = x.mergeData body of BeaconStateFork.Altair: const stateFork {.inject.} = BeaconStateFork.Altair template state: untyped {.inject.} = x.altairData body of BeaconStateFork.Phase0: const stateFork {.inject.} = BeaconStateFork.Phase0 template state: untyped {.inject.} = x.phase0Data body template withEpochInfo*(x: ForkedEpochInfo, body: untyped): untyped = case x.kind of EpochInfoFork.Phase0: template info: untyped {.inject.} = x.phase0Data body of EpochInfoFork.Altair: template info: untyped {.inject.} = x.altairData body template withEpochInfo*( state: phase0.BeaconState, x: var ForkedEpochInfo, body: untyped): untyped = x.kind = EpochInfoFork.Phase0 template info: untyped {.inject.} = x.phase0Data body template withEpochInfo*( state: altair.BeaconState | merge.BeaconState, x: var ForkedEpochInfo, body: untyped): untyped = x.kind = EpochInfoFork.Altair template info: untyped {.inject.} = x.altairData body func assign*(tgt: var ForkedHashedBeaconState, src: ForkedHashedBeaconState) = if tgt.kind == src.kind: case tgt.kind of BeaconStateFork.Merge: assign(tgt.mergeData, src.mergeData): of BeaconStateFork.Altair: assign(tgt.altairData, src.altairData): of BeaconStateFork.Phase0: assign(tgt.phase0Data, src.phase0Data): else: # Ensure case object and discriminator get updated simultaneously, even # with nimOldCaseObjects. This is infrequent. tgt = src template getStateField*(x: ForkedHashedBeaconState, y: untyped): untyped = # The use of `unsafeAddr` avoids excessive copying in certain situations, e.g., # ``` # for index, validator in getStateField(stateData.data, validators).pairs(): # ``` # Without `unsafeAddr`, the `validators` list would be copied to a temporary variable. (case x.kind of BeaconStateFork.Merge: unsafeAddr x.mergeData.data.y of BeaconStateFork.Altair: unsafeAddr x.altairData.data.y of BeaconStateFork.Phase0: unsafeAddr x.phase0Data.data.y)[] func getStateRoot*(x: ForkedHashedBeaconState): Eth2Digest = withState(x): state.root func setStateRoot*(x: var ForkedHashedBeaconState, root: Eth2Digest) = withState(x): state.root = root func stateForkAtEpoch*(cfg: RuntimeConfig, epoch: Epoch): BeaconStateFork = ## Return the current fork for the given epoch. static: doAssert BeaconStateFork.Merge > BeaconStateFork.Altair doAssert BeaconStateFork.Altair > BeaconStateFork.Phase0 doAssert GENESIS_EPOCH == 0 if epoch >= cfg.MERGE_FORK_EPOCH: BeaconStateFork.Merge elif epoch >= cfg.ALTAIR_FORK_EPOCH: BeaconStateFork.Altair else: BeaconStateFork.Phase0 func blockForkAtEpoch*(cfg: RuntimeConfig, epoch: Epoch): BeaconBlockFork = ## Return the current fork for the given epoch. if epoch >= cfg.MERGE_FORK_EPOCH: BeaconBlockFork.Merge elif epoch >= cfg.ALTAIR_FORK_EPOCH: BeaconBlockFork.Altair else: BeaconBlockFork.Phase0 template asSigned*(x: ForkedTrustedSignedBeaconBlock): ForkedSignedBeaconBlock = isomorphicCast[ForkedSignedBeaconBlock](x) template asTrusted*(x: ForkedSignedBeaconBlock): ForkedTrustedSignedBeaconBlock = isomorphicCast[ForkedTrustedSignedBeaconBlock](x) template withBlck*( x: ForkedBeaconBlock | ForkedSignedBeaconBlock | ForkedTrustedSignedBeaconBlock, body: untyped): untyped = case x.kind of BeaconBlockFork.Phase0: const stateFork {.inject.} = BeaconStateFork.Phase0 template blck: untyped {.inject.} = x.phase0Data body of BeaconBlockFork.Altair: const stateFork {.inject.} = BeaconStateFork.Altair template blck: untyped {.inject.} = x.altairData body of BeaconBlockFork.Merge: const stateFork {.inject.} = BeaconStateFork.Merge template blck: untyped {.inject.} = x.mergeData body func proposer_index*(x: ForkedBeaconBlock): uint64 = withBlck(x): blck.proposer_index func hash_tree_root*(x: ForkedBeaconBlock): Eth2Digest = withBlck(x): hash_tree_root(blck) template getForkedBlockField*(x: ForkedSignedBeaconBlock | ForkedTrustedSignedBeaconBlock, y: untyped): untyped = # unsafeAddr avoids a copy of the field in some cases (case x.kind of BeaconBlockFork.Phase0: unsafeAddr x.phase0Data.message.y of BeaconBlockFork.Altair: unsafeAddr x.altairData.message.y of BeaconBlockFork.Merge: unsafeAddr x.mergeData.message.y)[] template signature*(x: ForkedSignedBeaconBlock): ValidatorSig = withBlck(x): blck.signature template signature*(x: ForkedTrustedSignedBeaconBlock): TrustedSig = withBlck(x): blck.signature template root*(x: ForkedSignedBeaconBlock | ForkedTrustedSignedBeaconBlock): Eth2Digest = withBlck(x): blck.root template slot*(x: ForkedSignedBeaconBlock | ForkedTrustedSignedBeaconBlock): Slot = withBlck(x): blck.message.slot template shortLog*(x: ForkedBeaconBlock): auto = withBlck(x): shortLog(blck) template shortLog*(x: ForkedSignedBeaconBlock | ForkedTrustedSignedBeaconBlock): auto = withBlck(x): shortLog(blck) chronicles.formatIt ForkedBeaconBlock: it.shortLog chronicles.formatIt ForkedSignedBeaconBlock: it.shortLog chronicles.formatIt ForkedTrustedSignedBeaconBlock: it.shortLog template withStateAndBlck*( s: ForkedHashedBeaconState, b: ForkedBeaconBlock | ForkedSignedBeaconBlock | ForkedTrustedSignedBeaconBlock, body: untyped): untyped = case s.kind of BeaconStateFork.Merge: const stateFork {.inject.} = BeaconStateFork.Merge template state: untyped {.inject.} = s.mergeData template blck: untyped {.inject.} = b.mergeData body of BeaconStateFork.Altair: const stateFork {.inject.} = BeaconStateFork.Altair template state: untyped {.inject.} = s.altairData template blck: untyped {.inject.} = b.altairData body of BeaconStateFork.Phase0: const stateFork {.inject.} = BeaconStateFork.Phase0 template state: untyped {.inject.} = s.phase0Data template blck: untyped {.inject.} = b.phase0Data body func genesisFork*(cfg: RuntimeConfig): Fork = Fork( previous_version: cfg.GENESIS_FORK_VERSION, current_version: cfg.GENESIS_FORK_VERSION, epoch: GENESIS_EPOCH) func altairFork*(cfg: RuntimeConfig): Fork = Fork( previous_version: cfg.GENESIS_FORK_VERSION, current_version: cfg.ALTAIR_FORK_VERSION, epoch: cfg.ALTAIR_FORK_EPOCH) func mergeFork*(cfg: RuntimeConfig): Fork = # TODO in theory, the altair + merge forks could be in same epoch, so the # previous fork version would be the GENESIS_FORK_VERSION Fork( previous_version: cfg.ALTAIR_FORK_VERSION, current_version: cfg.MERGE_FORK_VERSION, epoch: cfg.MERGE_FORK_EPOCH) proc forkAtEpoch*(cfg: RuntimeConfig, epoch: Epoch): Fork = case cfg.stateForkAtEpoch(epoch) of BeaconStateFork.Merge: cfg.mergeFork of BeaconStateFork.Altair: cfg.altairFork of BeaconStateFork.Phase0: cfg.genesisFork proc forkVersionAtEpoch*(cfg: RuntimeConfig, epoch: Epoch): Version = case cfg.stateForkAtEpoch(epoch) of BeaconStateFork.Merge: cfg.MERGE_FORK_VERSION of BeaconStateFork.Altair: cfg.ALTAIR_FORK_VERSION of BeaconStateFork.Phase0: cfg.GENESIS_FORK_VERSION proc nextForkEpochAtEpoch*(cfg: RuntimeConfig, epoch: Epoch): Epoch = case cfg.stateForkAtEpoch(epoch) of BeaconStateFork.Merge: FAR_FUTURE_EPOCH of BeaconStateFork.Altair: cfg.MERGE_FORK_EPOCH of BeaconStateFork.Phase0: cfg.ALTAIR_FORK_EPOCH func getForkSchedule*(cfg: RuntimeConfig): array[2, Fork] = ## This procedure returns list of known and/or scheduled forks. ## ## This procedure is used by HTTP REST framework and validator client. ## ## NOTE: Update this procedure when new fork will be scheduled. [cfg.genesisFork(), cfg.altairFork()] type # The first few fields of a state, shared across all forks BeaconStateHeader = object genesis_time: uint64 genesis_validators_root: Eth2Digest slot: uint64 func readSszForkedHashedBeaconState*(cfg: RuntimeConfig, data: openArray[byte]): ForkedHashedBeaconState {.raises: [Defect, SszError].} = ## Helper to read a header from bytes when it's not certain what kind of state ## it is - this happens for example when loading an SSZ state from command ## line if data.len() < sizeof(BeaconStateHeader): raise (ref MalformedSszError)(msg: "Not enough data for BeaconState header") let header = SSZ.decode( data.toOpenArray(0, sizeof(BeaconStateHeader) - 1), BeaconStateHeader) # careful - `result` is used, RVO didn't seem to work without # TODO move time helpers somewhere to avoid circular imports result = ForkedHashedBeaconState( kind: cfg.stateForkAtEpoch(Epoch(header.slot div SLOTS_PER_EPOCH))) withState(result): readSszBytes(data, state.data) state.root = hash_tree_root(state.data) type ForkedBeaconBlockHeader = object message*: uint32 # message offset signature*: ValidatorSig slot: Slot # start of BeaconBlock func readSszForkedTrustedSignedBeaconBlock*( cfg: RuntimeConfig, data: openArray[byte]): ForkedTrustedSignedBeaconBlock {.raises: [Defect, SszError].} = ## Helper to read a header from bytes when it's not certain what kind of block ## it is if data.len() < sizeof(BeaconBlockHeader): raise (ref MalformedSszError)(msg: "Not enough data for SignedBeaconBlock header") let header = SSZ.decode( data.toOpenArray(0, sizeof(ForkedBeaconBlockHeader) - 1), ForkedBeaconBlockHeader) # careful - `result` is used, RVO didn't seem to work without # TODO move time helpers somewhere to avoid circular imports result = ForkedTrustedSignedBeaconBlock( kind: cfg.blockForkAtEpoch(Epoch(header.slot div SLOTS_PER_EPOCH))) withBlck(result): readSszBytes(data, blck) func toBeaconBlockFork*(fork: BeaconStateFork): BeaconBlockFork = case fork of BeaconStateFork.Phase0: BeaconBlockFork.Phase0 of BeaconStateFork.Altair: BeaconBlockFork.Altair of BeaconStateFork.Merge: BeaconBlockFork.Merge # https://github.com/ethereum/consensus-specs/blob/v1.1.6/specs/phase0/beacon-chain.md#compute_fork_data_root func compute_fork_data_root*(current_version: Version, genesis_validators_root: Eth2Digest): Eth2Digest = ## Return the 32-byte fork data root for the ``current_version`` and ## ``genesis_validators_root``. ## This is used primarily in signature domains to avoid collisions across ## forks/chains. hash_tree_root(ForkData( current_version: current_version, genesis_validators_root: genesis_validators_root )) # https://github.com/ethereum/consensus-specs/blob/v1.1.6/specs/phase0/beacon-chain.md#compute_fork_digest func compute_fork_digest*(current_version: Version, genesis_validators_root: Eth2Digest): ForkDigest = ## Return the 4-byte fork digest for the ``current_version`` and ## ``genesis_validators_root``. ## This is a digest primarily used for domain separation on the p2p layer. ## 4-bytes suffices for practical separation of forks/chains. array[4, byte](result)[0..3] = compute_fork_data_root( current_version, genesis_validators_root).data.toOpenArray(0, 3) func init*(T: type ForkDigests, cfg: RuntimeConfig, genesisValidatorsRoot: Eth2Digest): T = T( phase0: compute_fork_digest(cfg.GENESIS_FORK_VERSION, genesisValidatorsRoot), altair: compute_fork_digest(cfg.ALTAIR_FORK_VERSION, genesisValidatorsRoot), merge: compute_fork_digest(cfg.MERGE_FORK_VERSION, genesisValidatorsRoot), )