nimbus-eth2/beacon_chain/libnimbus_lc/libnimbus_lc.nim

1977 lines
72 KiB
Nim

# beacon_chain
# Copyright (c) 2023 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/[json, sequtils, times],
stew/saturation_arith,
eth/common/[eth_types_rlp, transaction],
eth/keys,
eth/p2p/discoveryv5/random2,
eth/rlp,
eth/trie/[db, hexary],
json_rpc/jsonmarshal,
secp256k1,
web3/ethtypes,
../el/el_manager,
../spec/eth2_apis/[eth2_rest_serialization, rest_light_client_calls],
../spec/[helpers, light_client_sync],
../sync/light_client_sync_helpers,
../beacon_clock
{.pragma: exported, cdecl, exportc, dynlib, raises: [].}
{.pragma: exportedConst, exportc, dynlib.}
proc toUnmanagedPtr[T](x: ref T): ptr T =
GC_ref(x)
addr x[]
func asRef[T](x: ptr T): ref T =
cast[ref T](x)
proc destroy[T](x: ptr T) =
x[].reset()
GC_unref(asRef(x))
proc ETHRandomNumberCreate(): ptr HmacDrbgContext {.exported.} =
## Creates a new cryptographically secure random number generator.
##
## * The cryptographically secure random number generator must be destroyed
## with `ETHRandomNumberDestroy` once no longer needed, to release memory.
##
## Returns:
## * Pointer to an initialized cryptographically secure random number
## generator context - If successful.
## * `NULL` - If an error occurred.
HmacDrbgContext.new().toUnmanagedPtr()
proc ETHRandomNumberDestroy(rng: ptr HmacDrbgContext) {.exported.} =
## Destroys a cryptographically secure random number generator.
##
## * The cryptographically secure random number generator
## must no longer be used after destruction.
##
## Parameters:
## * `rng` - Cryptographically secure random number generator.
rng.destroy()
proc ETHConsensusConfigCreateFromYaml(
configFileContent: cstring): ptr RuntimeConfig {.exported.} =
## Creates a new Ethereum Consensus Layer network configuration
## based on the given `config.yaml` file content from an
## Ethereum network definition.
##
## * The Ethereum Consensus Layer network configuration must be destroyed with
## `ETHConsensusConfigDestroy` once no longer needed, to release memory.
##
## Parameters:
## * `configFileContent` - `config.yaml` file content. NULL-terminated.
##
## Returns:
## * Pointer to an initialized Ethereum Consensus Layer network configuration
## based on the given `config.yaml` file content - If successful.
## * `NULL` - If the given `config.yaml` is malformed or incompatible.
##
## See:
## * https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.1/configs/README.md
let cfg = RuntimeConfig.new()
try:
cfg[] = readRuntimeConfig($configFileContent, "config.yaml")[0]
except IOError, PresetFileError, PresetIncompatibleError:
return nil
cfg.toUnmanagedPtr()
proc ETHConsensusConfigDestroy(cfg: ptr RuntimeConfig) {.exported.} =
## Destroys an Ethereum Consensus Layer network configuration.
##
## * The Ethereum Consensus Layer network configuration
## must no longer be used after destruction.
##
## Parameters:
## * `cfg` - Ethereum Consensus Layer network configuration.
cfg.destroy()
func ETHConsensusConfigGetConsensusVersionAtEpoch(
cfg: ptr RuntimeConfig, epoch: cint): cstring {.exported.} =
## Returns the expected `Eth-Consensus-Version` for a given `epoch`.
##
## * The returned `Eth-Consensus-Version` is statically allocated.
## It must neither be released nor written to.
##
## Parameters:
## * `cfg` - Ethereum Consensus Layer network configuration.
## * `epoch` - Epoch number for which to obtain `Eth-Consensus-Version`
##
## Returns:
## * Expected `Eth-Consensus-Version` for the given `epoch`. NULL-terminated.
##
## See:
## * https://github.com/ethereum/beacon-APIs/blob/v2.4.1/beacon-node-oapi.yaml#L419
withConsensusFork(cfg[].consensusForkAtEpoch(epoch.Epoch)):
const consensusVersion: cstring = consensusFork.toString()
consensusVersion
proc ETHBeaconStateCreateFromSsz(
cfg: ptr RuntimeConfig,
consensusVersion: cstring,
sszBytes: ptr UncheckedArray[byte],
numSszBytes: cint): ptr ForkedHashedBeaconState {.exported.} =
## Creates a new beacon state based on its SSZ encoded representation.
##
## * The beacon state must be destroyed with `ETHBeaconStateDestroy`
## once no longer needed, to release memory.
##
## * When loading a `genesis.ssz` file from an Ethereum network definition,
## use `ETHConsensusConfigGetConsensusVersionAtEpoch` with `epoch = 0`
## to determine the correct `consensusVersion`.
##
## Parameters:
## * `cfg` - Ethereum Consensus Layer network configuration.
## * `consensusVersion` - `Eth-Consensus-Version` for the given `sszBytes`.
## * `sszBytes` - Buffer with SSZ encoded beacon state representation.
## * `numSszBytes` - Length of buffer.
##
## Returns:
## * Pointer to an initialized beacon state based on the given SSZ encoded
## representation - If successful.
## * `NULL` - If the given `sszBytes` is malformed.
##
## See:
## * https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.1/specs/phase0/beacon-chain.md#beaconstate
## * https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.1/specs/altair/beacon-chain.md#beaconstate
## * https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.1/specs/bellatrix/beacon-chain.md#beaconstate
## * https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.1/specs/capella/beacon-chain.md#beaconstate
## * https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.1/configs/README.md
let
consensusFork = decodeEthConsensusVersion($consensusVersion).valueOr:
return nil
state = ForkedHashedBeaconState.new()
try:
state[] = consensusFork.readSszForkedHashedBeaconState(
sszBytes.toOpenArray(0, numSszBytes - 1))
except SszError:
return nil
withState(state[]):
if cfg[].consensusForkAtEpoch(forkyState.data.slot.epoch) != state.kind:
return nil
state.toUnmanagedPtr()
proc ETHBeaconStateDestroy(state: ptr ForkedHashedBeaconState) {.exported.} =
## Destroys a beacon state.
##
## * The beacon state must no longer be used after destruction.
##
## Parameters:
## * `state` - Beacon state.
state.destroy()
proc ETHBeaconStateCopyGenesisValidatorsRoot(
state: ptr ForkedHashedBeaconState): ptr Eth2Digest {.exported.} =
## Copies the `genesis_validators_root` field from a beacon state.
##
## * The genesis validators root must be destroyed with `ETHRootDestroy`
## once no longer needed, to release memory.
##
## Parameters:
## * `state` - Beacon state.
##
## Returns:
## * Pointer to a copy of the given beacon state's genesis validators root.
let genesisValRoot = Eth2Digest.new()
genesisValRoot[] = getStateField(state[], genesis_validators_root)
genesisValRoot.toUnmanagedPtr()
proc ETHRootDestroy(root: ptr Eth2Digest) {.exported.} =
## Destroys a Merkle root.
##
## * The Merkle root must no longer be used after destruction.
##
## Parameters:
## * `root` - Merkle root.
##
## See:
## * https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.1/specs/phase0/beacon-chain.md#custom-types
root.destroy()
proc ETHForkDigestsCreateFromState(
cfg: ptr RuntimeConfig,
state: ptr ForkedHashedBeaconState): ptr ForkDigests {.exported.} =
## Creates a fork digests cache for a given beacon state.
##
## * The fork digests cache must be destroyed with `ETHForkDigestsDestroy`
## once no longer needed, to release memory.
##
## Parameters:
## * `cfg` - Ethereum Consensus Layer network configuration.
## * `state` - Beacon state.
##
## Returns:
## * Pointer to an initialized fork digests cache based on the beacon state.
##
## See:
## * https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.1/specs/phase0/beacon-chain.md#compute_fork_digest
let forkDigests = ForkDigests.new()
forkDigests[] = ForkDigests.init(
cfg[], getStateField(state[], genesis_validators_root))
forkDigests.toUnmanagedPtr()
proc ETHForkDigestsDestroy(forkDigests: ptr ForkDigests) {.exported.} =
## Destroys a fork digests cache.
##
## * The fork digests cache must no longer be used after destruction.
##
## Parameters:
## * `forkDigests` - Fork digests cache.
forkDigests.destroy()
proc ETHBeaconClockCreateFromState(
state: ptr ForkedHashedBeaconState): ptr BeaconClock {.exported.} =
## Creates a beacon clock for a given beacon state's `genesis_time` field.
##
## * The beacon clock must be destroyed with `ETHBeaconClockDestroy`
## once no longer needed, to release memory.
##
## Parameters:
## * `state` - Beacon state.
##
## Returns:
## * Pointer to an initialized beacon clock based on the beacon state.
let beaconClock = BeaconClock.new()
beaconClock[] = BeaconClock.init(getStateField(state[], genesis_time))
beaconClock.toUnmanagedPtr()
proc ETHBeaconClockDestroy(beaconClock: ptr BeaconClock) {.exported.} =
## Destroys a beacon clock.
##
## * The beacon clock must no longer be used after destruction.
##
## Parameters:
## * `beaconClock` - Beacon clock.
beaconClock.destroy()
proc ETHBeaconClockGetSlot(beaconClock: ptr BeaconClock): cint {.exported.} =
## Indicates the slot number for the current wall clock time.
##
## Parameters:
## * `beaconClock` - Beacon clock.
##
## Returns:
## * Slot number for the current wall clock time - If genesis has occurred.
## * `0` - If genesis is still pending.
##
## See:
## * https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.1/specs/phase0/beacon-chain.md#custom-types
beaconClock[].now().slotOrZero().cint
const lcDataFork = LightClientDataFork.high
proc ETHLightClientStoreCreateFromBootstrap(
cfg: ptr RuntimeConfig,
trustedBlockRoot: ptr Eth2Digest,
mediaType: cstring,
consensusVersion: cstring,
bootstrapBytes: ptr UncheckedArray[byte],
numBootstrapBytes: cint
): ptr lcDataFork.LightClientStore {.exported.} =
## Creates a light client store from light client bootstrap data.
## The light client store is the primary object for syncing with
## an Ethereum network.
##
## * To create a light client store, the Ethereum network definition
## including the fork schedule, `genesis_time` and `genesis_validators_root`
## must be known. Furthermore, a beacon block root must be assumed trusted.
## The trusted block root should be within the weak subjectivity period,
## and its root should be from a finalized `Checkpoint`.
##
## * The REST `/eth/v1/beacon/light_client/bootstrap/{block_root}` beacon API
## may be used to obtain light client bootstrap data for a given
## trusted block root. Setting the `Accept: application/octet-stream`
## HTTP header in the request selects the more compact SSZ representation.
##
## * After creating a light client store, `ETHLightClientStoreGetNextSyncTask`
## may be used to determine what further REST beacon API requests to perform
## for keeping the light client store in sync with the Ethereum network.
##
## * Once synced the REST `/eth/v1/events?topics=light_client_finality_update`
## `&topics=light_client_optimistic_update` beacon API provides the most
## recent light client data. Data from this endpoint is always JSON encoded
## and may be processed with `ETHLightClientStoreProcessFinalityUpdate` and
## `ETHLightClientStoreProcessOptimisticUpdate`.
##
## * The light client store must be destroyed with
## `ETHLightClientStoreDestroy` once no longer needed, to release memory.
##
## Parameters:
## * `cfg` - Ethereum Consensus Layer network configuration.
## * `trustedBlockRoot` - Trusted block root.
## * `mediaType` - HTTP `Content-Type` associated with `bootstrapBytes`;
## `application/json` for JSON, `application/octet-stream` for SSZ.
## * `consensusVersion` - HTTP `Eth-Consensus-Version` response header
## associated with `bootstrapBytes`.
## * `bootstrapBytes` - Buffer with encoded light client bootstrap data.
## * `numBootstrapBytes` - Length of buffer.
##
## Returns:
## * Pointer to an initialized light client store based on the given
## light client bootstrap data - If successful.
## * `NULL` - If the given `bootstrapBytes` is malformed or incompatible.
##
## See:
## * https://ethereum.github.io/beacon-APIs/?urls.primaryName=v2.4.1#/Beacon/getLightClientBootstrap
## * https://ethereum.github.io/beacon-APIs/?urls.primaryName=v2.4.1#/Events/eventstream
## * https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.1/specs/altair/light-client/light-client.md
## * https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.1/specs/phase0/weak-subjectivity.md#weak-subjectivity-period
let
mediaType = MediaType.init($mediaType)
consensusFork = decodeEthConsensusVersion($consensusVersion).valueOr:
return nil
var bootstrap =
try:
ForkedLightClientBootstrap.decodeHttpLightClientObject(
bootstrapBytes.toOpenArray(0, numBootstrapBytes - 1),
mediaType, consensusFork, cfg[])
except RestError:
return nil
doAssert bootstrap.kind > LightClientDataFork.None
bootstrap.migrateToDataFork(lcDataFork)
let store = lcDataFork.LightClientStore.new()
store[] = initialize_light_client_store(
trustedBlockRoot[], bootstrap.forky(lcDataFork), cfg[]).valueOr:
return nil
store.toUnmanagedPtr()
proc ETHLightClientStoreDestroy(
store: ptr lcDataFork.LightClientStore) {.exported.} =
## Destroys a light client store.
##
## * The light client store must no longer be used after destruction.
##
## Parameters:
## * `store` - Light client store.
store.destroy()
let
## Sync task to fulfill using `/eth/v1/beacon/light_client/updates`.
kETHLcSyncKind_UpdatesByRange {.exportedConst.} =
LcSyncKind.UpdatesByRange.cint
## Sync task to fulfill using `/eth/v1/beacon/light_client/finality_update`.
kETHLcSyncKind_FinalityUpdate {.exportedConst.} =
LcSyncKind.FinalityUpdate.cint
## Sync task to fulfill using `/eth/v1/beacon/light_client/optimistic_update`.
kETHLcSyncKind_OptimisticUpdate {.exportedConst.} =
LcSyncKind.OptimisticUpdate.cint
proc ETHLightClientStoreGetNextSyncTask(
store: ptr lcDataFork.LightClientStore,
beaconClock: ptr BeaconClock,
startPeriod #[out]#: ptr cint,
count #[out]#: ptr cint): cint {.exported.} =
## Obtains the next task for keeping a light client store in sync
## with the Ethereum network.
##
## * When using the REST beacon API to fulfill a sync task, setting the
## `Accept: application/octet-stream` HTTP header in the request
## selects the more compact SSZ representation.
##
## * After fetching the requested light client data and processing it with the
## appropriate handler, `ETHLightClientStoreGetMillisecondsToNextSyncTask`
## may be used to obtain a delay until a new sync task becomes available.
## Once the delay is reached, call `ETHLightClientStoreGetNextSyncTask`
## again to obtain the next sync task.
##
## * Once synced the REST `/eth/v1/events?topics=light_client_finality_update`
## `&topics=light_client_optimistic_update` beacon API provides the most
## recent light client data. Data from this endpoint is always JSON encoded
## and may be processed with `ETHLightClientStoreProcessFinalityUpdate` and
## `ETHLightClientStoreProcessOptimisticUpdate`. Events may be processed at
## any time and do not require re-computing the delay until next sync task
## with `ETHLightClientStoreGetMillisecondsToNextSyncTask`.
##
## Parameters:
## * `store` - Light client store.
## * `beaconClock` - Beacon clock.
## * `startPeriod` [out] - `start_period` query parameter, if applicable.
## * `count` [out] - `count` query parameter, if applicable.
##
## Returns:
## * `kETHLcSyncKind_UpdatesByRange` - If the next sync task is fulfillable
## using REST `/eth/v1/beacon/light_client/updates` beacon API.
## The `startPeriod` and `count` parameters are filled, and to be passed to
## `/eth/v1/beacon/light_client/updates?start_period={startPeriod}`
## `&count={count}`.
## Process the response with `ETHLightClientStoreProcessUpdatesByRange`.
## * `kETHLcSyncKind_FinalityUpdate` - If the next sync task is fulfillable
## using REST `/eth/v1/beacon/light_client/finality_update` beacon API.
## Process the response with `ETHLightClientStoreProcessFinalityUpdate`.
## The `startPeriod` and `count` parameters are unused for this sync task.
## * `kETHLcSyncKind_OptimisticUpdate` - If the next sync task is fulfillable
## using REST `/eth/v1/beacon/light_client/optimistic_update` beacon API.
## Process the response with `ETHLightClientStoreProcessOptimisticUpdate`.
## The `startPeriod` and `count` parameters are unused for this sync task.
##
## See:
## * https://ethereum.github.io/beacon-APIs/?urls.primaryName=v2.4.1#/Beacon/getLightClientUpdatesByRange
## * https://ethereum.github.io/beacon-APIs/?urls.primaryName=v2.4.1#/Beacon/getLightClientFinalityUpdate
## * https://ethereum.github.io/beacon-APIs/?urls.primaryName=v2.4.1#/Beacon/getLightClientOptimisticUpdate
## * https://ethereum.github.io/beacon-APIs/?urls.primaryName=v2.4.1#/Events/eventstream
let syncTask = nextLightClientSyncTask(
current = beaconClock[].now().slotOrZero().sync_committee_period,
finalized = store[].finalized_header.beacon.slot.sync_committee_period,
optimistic = store[].optimistic_header.beacon.slot.sync_committee_period,
isNextSyncCommitteeKnown = store[].is_next_sync_committee_known)
case syncTask.kind
of LcSyncKind.UpdatesByRange:
startPeriod[] = syncTask.startPeriod.cint
count[] = syncTask.count.cint
of LcSyncKind.FinalityUpdate:
startPeriod[] = 0
count[] = 0
of LcSyncKind.OptimisticUpdate:
startPeriod[] = 0
count[] = 0
syncTask.kind.cint
proc ETHLightClientStoreGetMillisecondsToNextSyncTask(
store: ptr lcDataFork.LightClientStore,
rng: ptr HmacDrbgContext,
beaconClock: ptr BeaconClock,
latestProcessResult: cint): cint {.exported.} =
## Indicates the delay until a new light client sync task becomes available.
## Once the delay is reached, call `ETHLightClientStoreGetNextSyncTask`
## to obtain the next sync task.
##
## Parameters:
## * `store` - Light client store.
## * `rng` - Cryptographically secure random number generator.
## * `beaconClock` - Beacon clock.
## * `latestProcessResult` - Latest sync task processing result, i.e.,
## the return value of `ETHLightClientStoreProcessUpdatesByRange`,
## `ETHLightClientStoreProcessFinalityUpdate`, or
## `ETHLightClientStoreProcessOptimisticUpdate`, for latest task.
## If the data for the sync task could not be fetched, set to `1`.
##
## Returns:
## * Number of milliseconds until `ETHLightClientStoreGetNextSyncTask`
## should be called again to obtain the next light client sync task.
asRef(rng).nextLcSyncTaskDelay(
wallTime = beaconClock[].now(),
finalized = store[].finalized_header.beacon.slot.sync_committee_period,
optimistic = store[].optimistic_header.beacon.slot.sync_committee_period,
isNextSyncCommitteeKnown = store[].is_next_sync_committee_known,
didLatestSyncTaskProgress = (latestProcessResult == 0)).milliseconds.cint
proc ETHLightClientStoreProcessUpdatesByRange(
store: ptr lcDataFork.LightClientStore,
cfg: ptr RuntimeConfig,
forkDigests: ptr ForkDigests,
genesisValRoot: ptr Eth2Digest,
beaconClock: ptr BeaconClock,
startPeriod: cint,
count: cint,
mediaType: cstring,
updatesBytes: ptr UncheckedArray[byte],
numUpdatesBytes: cint): cint {.exported.} =
## Processes light client update data.
##
## * This processes the response data for a sync task of kind
## `kETHLcSyncKind_UpdatesByRange`, as indicated by
## `ETHLightClientStoreGetNextSyncTask`. After processing, call
## `ETHLightClientStoreGetMillisecondsToNextSyncTask` to obtain a delay
## until a new sync task becomes available.
##
## Parameters:
## * `store` - Light client store.
## * `cfg` - Ethereum Consensus Layer network configuration.
## * `forkDigests` - Fork digests cache.
## * `genesisValRoot` - Genesis validators root.
## * `beaconClock` - Beacon clock.
## * `startPeriod` - `startPeriod` parameter associated with the sync task.
## * `count` - `count` parameter associated with the sync task.
## * `mediaType` - HTTP `Content-Type` associated with `updatesBytes`;
## `application/json` for JSON, `application/octet-stream` for SSZ.
## * `updatesBytes` - Buffer with encoded light client update data.
## * `numUpdatesBytes` - Length of buffer.
##
## Returns:
## * `0` - If the given `updatesBytes` is valid and sync did progress.
## * `1` - If the given `updatesBytes` is malformed or incompatible.
## * `2` - If the given `updatesBytes` did not advance sync progress.
##
## See:
## * https://ethereum.github.io/beacon-APIs/?urls.primaryName=v2.4.1#/Beacon/getLightClientUpdatesByRange
let
wallTime = beaconClock[].now()
currentSlot = wallTime.slotOrZero()
mediaType = MediaType.init($mediaType)
var updates =
try:
seq[ForkedLightClientUpdate].decodeHttpLightClientObjects(
updatesBytes.toOpenArray(0, numUpdatesBytes - 1),
mediaType, cfg[], asRef(forkDigests))
except RestError:
return 1
let e = updates.checkLightClientUpdates(
startPeriod.SyncCommitteePeriod, count.uint64)
if e.isErr:
return 1
var didProgress = false
for i in 0 ..< updates.len:
doAssert updates[i].kind > LightClientDataFork.None
updates[i].migrateToDataFork(lcDataFork)
let res = process_light_client_update(
store[], updates[i].forky(lcDataFork),
currentSlot, cfg[], genesisValRoot[])
if res.isOk:
didProgress = true
else:
case res.error
of VerifierError.MissingParent:
break
of VerifierError.Duplicate:
discard
of VerifierError.UnviableFork:
break
of VerifierError.Invalid:
return 1
if not didProgress:
return 2
0
proc ETHLightClientStoreProcessFinalityUpdate(
store: ptr lcDataFork.LightClientStore,
cfg: ptr RuntimeConfig,
forkDigests: ptr ForkDigests,
genesisValRoot: ptr Eth2Digest,
beaconClock: ptr BeaconClock,
mediaType: cstring,
consensusVersion #[optional]#: cstring,
finUpdateBytes: ptr UncheckedArray[byte],
numFinUpdateBytes: cint): cint {.exported.} =
## Processes light client finality update data.
##
## * This processes the response data for a sync task of kind
## `kETHLcSyncKind_FinalityUpdate`, as indicated by
## `ETHLightClientStoreGetNextSyncTask`. After processing, call
## `ETHLightClientStoreGetMillisecondsToNextSyncTask` to obtain a delay
## until a new sync task becomes available.
##
## * This also processes event data from the REST
## `/eth/v1/events?topics=light_client_finality_update` beacon API.
## Set `mediaType` to `application/json`, and `consensusVersion` to `NULL`.
## Events may be processed at any time, it is not necessary to call
## `ETHLightClientStoreGetMillisecondsToNextSyncTask`.
##
## Parameters:
## * `store` - Light client store.
## * `cfg` - Ethereum Consensus Layer network configuration.
## * `forkDigests` - Fork digests cache.
## * `genesisValRoot` - Genesis validators root.
## * `beaconClock` - Beacon clock.
## * `mediaType` - HTTP `Content-Type` associated with `finUpdateBytes`;
## `application/json` for JSON, `application/octet-stream` for SSZ.
## * `consensusVersion` - HTTP `Eth-Consensus-Version` response header
## associated with `finUpdateBytes`. `NULL` when processing event.
## * `finUpdateBytes` - Buffer with encoded finality update data.
## * `numFinUpdateBytes` - Length of buffer.
##
## Returns:
## * `0` - If the given `finUpdateBytes` is valid and sync did progress.
## * `1` - If the given `finUpdateBytes` is malformed or incompatible.
## * `2` - If the given `finUpdateBytes` did not advance sync progress.
##
## See:
## * https://ethereum.github.io/beacon-APIs/?urls.primaryName=v2.4.1#/Beacon/getLightClientFinalityUpdate
## * https://ethereum.github.io/beacon-APIs/?urls.primaryName=v2.4.1#/Events/eventstream
let
wallTime = beaconClock[].now()
currentSlot = wallTime.slotOrZero()
mediaType = MediaType.init($mediaType)
var finalityUpdate =
try:
if consensusVersion == nil:
if mediaType != ApplicationJsonMediaType:
return 1
ForkedLightClientFinalityUpdate.decodeJsonLightClientObject(
finUpdateBytes.toOpenArray(0, numFinUpdateBytes - 1),
Opt.none(ConsensusFork), cfg[])
else:
let consensusFork = decodeEthConsensusVersion(
$consensusVersion).valueOr:
return 1
ForkedLightClientFinalityUpdate.decodeHttpLightClientObject(
finUpdateBytes.toOpenArray(0, numFinUpdateBytes - 1),
mediaType, consensusFork, cfg[])
except RestError:
return 1
doAssert finalityUpdate.kind > LightClientDataFork.None
finalityUpdate.migrateToDataFork(lcDataFork)
let res = process_light_client_update(
store[], finalityUpdate.forky(lcDataFork),
currentSlot, cfg[], genesisValRoot[])
return
if res.isOk:
0
else:
case res.error
of VerifierError.MissingParent:
2
of VerifierError.Duplicate:
2
of VerifierError.UnviableFork:
2
of VerifierError.Invalid:
1
proc ETHLightClientStoreProcessOptimisticUpdate(
store: ptr lcDataFork.LightClientStore,
cfg: ptr RuntimeConfig,
forkDigests: ptr ForkDigests,
genesisValRoot: ptr Eth2Digest,
beaconClock: ptr BeaconClock,
mediaType: cstring,
consensusVersion #[optional]#: cstring,
optUpdateBytes: ptr UncheckedArray[byte],
numOptUpdateBytes: cint): cint {.exported.} =
## Processes light client optimistic update data.
##
## * This processes the response data for a sync task of kind
## `kETHLcSyncKind_OptimisticUpdate`, as indicated by
## `ETHLightClientStoreGetNextSyncTask`. After processing, call
## `ETHLightClientStoreGetMillisecondsToNextSyncTask` to obtain a delay
## until a new sync task becomes available.
##
## * This also processes event data from the REST
## `/eth/v1/events?topics=light_client_optimistic_update` beacon API.
## Set `mediaType` to `application/json`, and `consensusVersion` to `NULL`.
## Events may be processed at any time, it is not necessary to call
## `ETHLightClientStoreGetMillisecondsToNextSyncTask`.
##
## Parameters:
## * `store` - Light client store.
## * `cfg` - Ethereum Consensus Layer network configuration.
## * `forkDigests` - Fork digests cache.
## * `genesisValRoot` - Genesis validators root.
## * `beaconClock` - Beacon clock.
## * `mediaType` - HTTP `Content-Type` associated with `optUpdateBytes`;
## `application/json` for JSON, `application/octet-stream` for SSZ.
## * `consensusVersion` - HTTP `Eth-Consensus-Version` response header
## associated with `optUpdateBytes`. `NULL` when processing event.
## * `optUpdateBytes` - Buffer with encoded optimistic update data.
## * `numOptUpdateBytes` - Length of buffer.
##
## Returns:
## * `0` - If the given `optUpdateBytes` is valid and sync did progress.
## * `1` - If the given `optUpdateBytes` is malformed or incompatible.
## * `2` - If the given `optUpdateBytes` did not advance sync progress.
##
## See:
## * https://ethereum.github.io/beacon-APIs/?urls.primaryName=v2.4.1#/Beacon/getLightClientOptimisticUpdate
## * https://ethereum.github.io/beacon-APIs/?urls.primaryName=v2.4.1#/Events/eventstream
let
wallTime = beaconClock[].now()
currentSlot = wallTime.slotOrZero()
mediaType = MediaType.init($mediaType)
var optimisticUpdate =
try:
if consensusVersion == nil:
if mediaType != ApplicationJsonMediaType:
return 1
ForkedLightClientOptimisticUpdate.decodeJsonLightClientObject(
optUpdateBytes.toOpenArray(0, numOptUpdateBytes - 1),
Opt.none(ConsensusFork), cfg[])
else:
let consensusFork = decodeEthConsensusVersion(
$consensusVersion).valueOr:
return 1
ForkedLightClientOptimisticUpdate.decodeHttpLightClientObject(
optUpdateBytes.toOpenArray(0, numOptUpdateBytes - 1),
mediaType, consensusFork, cfg[])
except RestError:
return 1
doAssert optimisticUpdate.kind > LightClientDataFork.None
optimisticUpdate.migrateToDataFork(lcDataFork)
let res = process_light_client_update(
store[], optimisticUpdate.forky(lcDataFork),
currentSlot, cfg[], genesisValRoot[])
return
if res.isOk:
0
else:
case res.error
of VerifierError.MissingParent:
2
of VerifierError.Duplicate:
2
of VerifierError.UnviableFork:
2
of VerifierError.Invalid:
1
func ETHLightClientStoreGetFinalizedHeader(
store: ptr lcDataFork.LightClientStore
): ptr lcDataFork.LightClientHeader {.exported.} =
## Obtains the latest finalized header of a given light client store.
##
## * The returned value is allocated in the given light client store.
## It must neither be released nor written to, and the light client store
## must not be released while the returned value is in use.
##
## Parameters:
## * `store` - Light client store.
##
## Returns:
## * Latest finalized header.
##
## See:
## * https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.1/specs/capella/light-client/sync-protocol.md#modified-lightclientheader
addr store[].finalized_header
func ETHLightClientStoreIsNextSyncCommitteeKnown(
store: ptr lcDataFork.LightClientStore): bool {.exported.} =
## Indicates whether or not the next sync committee is currently known.
##
## * The light client sync process ensures that the next sync committee
## is obtained in time, before it starts signing light client data.
## To stay in sync, use `ETHLightClientStoreGetNextSyncTask` and
## `ETHLightClientStoreGetMillisecondsToNextSyncTask`.
##
## Parameters:
## * `store` - Light client store.
##
## Returns:
## * Whether or not the next sync committee is currently known.
##
## See:
## * https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.1/specs/altair/light-client/sync-protocol.md#is_next_sync_committee_known
## * https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.1/specs/altair/light-client/light-client.md
store[].is_next_sync_committee_known
func ETHLightClientStoreGetOptimisticHeader(
store: ptr lcDataFork.LightClientStore
): ptr lcDataFork.LightClientHeader {.exported.} =
## Obtains the latest optimistic header of a given light client store.
##
## * The returned value is allocated in the given light client store.
## It must neither be released nor written to, and the light client store
## must not be released while the returned value is in use.
##
## Parameters:
## * `store` - Light client store.
##
## Returns:
## * Latest optimistic header.
##
## See:
## * https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.1/specs/capella/light-client/sync-protocol.md#modified-lightclientheader
addr store[].optimistic_header
func ETHLightClientStoreGetSafetyThreshold(
store: ptr lcDataFork.LightClientStore): cint {.exported.} =
## Calculates the safety threshold for a given light client store.
##
## * Light client data can only update the optimistic header if it is signed
## by more sync committee participants than the safety threshold indicates.
##
## * The finalized header is not affected by the safety threshold;
## light client data can only update the finalized header if it is signed
## by a supermajority of the sync committee, regardless of safety threshold.
##
## Parameters:
## * `store` - Light client store.
##
## Returns:
## * Light client store safety threshold.
##
## See:
## * https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.1/specs/altair/light-client/sync-protocol.md#get_safety_threshold
store[].get_safety_threshold.cint
proc ETHLightClientHeaderCreateCopy(
header: ptr lcDataFork.LightClientHeader
): ptr lcDataFork.LightClientHeader {.exported.} =
## Creates a shallow copy of a given light client header.
##
## * The copy must be destroyed with `ETHLightClientHeaderDestroy`
## once no longer needed, to release memory.
##
## Parameters:
## * `header` - Light client header.
##
## Returns:
## * Pointer to a shallow copy of the given header.
let copy = lcDataFork.LightClientHeader.new()
copy[] = header[]
copy.toUnmanagedPtr()
proc ETHLightClientHeaderDestroy(
header: ptr lcDataFork.LightClientHeader) {.exported.} =
## Destroys a light client header.
##
## * The light client header must no longer be used after destruction.
##
## Parameters:
## * `header` - Light client header.
header.destroy()
proc ETHLightClientHeaderCopyBeaconRoot(
header: ptr lcDataFork.LightClientHeader,
cfg: ptr RuntimeConfig): ptr Eth2Digest {.exported.} =
## Computes the beacon block Merkle root for a given light client header.
##
## * The Merkle root must be destroyed with `ETHRootDestroy`
## once no longer needed, to release memory.
##
## Parameters:
## * `header` - Light client header.
## * `cfg` - Ethereum Consensus Layer network configuration.
##
## Returns:
## * Pointer to a copy of the given header's beacon block root.
##
## See:
## * https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.1/specs/phase0/beacon-chain.md#hash_tree_root
discard cfg # Future-proof against new fields, see `get_lc_execution_root`.
let root = Eth2Digest.new()
root[] = header[].beacon.hash_tree_root()
root.toUnmanagedPtr()
func ETHLightClientHeaderGetBeacon(
header: ptr lcDataFork.LightClientHeader
): ptr BeaconBlockHeader {.exported.} =
## Obtains the beacon block header of a given light client header.
##
## * The returned value is allocated in the given light client header.
## It must neither be released nor written to, and the light client header
## must not be released while the returned value is in use.
##
## Parameters:
## * `header` - Light client header.
##
## Returns:
## * Beacon block header.
##
## See:
## * https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.1/specs/phase0/beacon-chain.md#beaconblockheader
addr header[].beacon
func ETHBeaconBlockHeaderGetSlot(
beacon: ptr BeaconBlockHeader): cint {.exported.} =
## Obtains the slot number of a given beacon block header.
##
## Parameters:
## * `beacon` - Beacon block header.
##
## Returns:
## * Slot number.
beacon[].slot.cint
func ETHBeaconBlockHeaderGetProposerIndex(
beacon: ptr BeaconBlockHeader): cint {.exported.} =
## Obtains the proposer validator registry index
## of a given beacon block header.
##
## Parameters:
## * `beacon` - Beacon block header.
##
## Returns:
## * Proposer validator registry index.
beacon[].proposer_index.cint
func ETHBeaconBlockHeaderGetParentRoot(
beacon: ptr BeaconBlockHeader): ptr Eth2Digest {.exported.} =
## Obtains the parent beacon block Merkle root of a given beacon block header.
##
## * The returned value is allocated in the given beacon block header.
## It must neither be released nor written to, and the beacon block header
## must not be released while the returned value is in use.
##
## Parameters:
## * `beacon` - Beacon block header.
##
## Returns:
## * Parent beacon block root.
addr beacon[].parent_root
func ETHBeaconBlockHeaderGetStateRoot(
beacon: ptr BeaconBlockHeader): ptr Eth2Digest {.exported.} =
## Obtains the beacon state Merkle root of a given beacon block header.
##
## * The returned value is allocated in the given beacon block header.
## It must neither be released nor written to, and the beacon block header
## must not be released while the returned value is in use.
##
## Parameters:
## * `beacon` - Beacon block header.
##
## Returns:
## * Beacon state root.
addr beacon[].state_root
func ETHBeaconBlockHeaderGetBodyRoot(
beacon: ptr BeaconBlockHeader): ptr Eth2Digest {.exported.} =
## Obtains the beacon block body Merkle root of a given beacon block header.
##
## * The returned value is allocated in the given beacon block header.
## It must neither be released nor written to, and the beacon block header
## must not be released while the returned value is in use.
##
## Parameters:
## * `beacon` - Beacon block header.
##
## Returns:
## * Beacon block body root.
addr beacon[].body_root
proc ETHLightClientHeaderCopyExecutionHash(
header: ptr lcDataFork.LightClientHeader,
cfg: ptr RuntimeConfig
): ptr Eth2Digest {.exported.} =
## Computes the execution block hash for a given light client header.
##
## * The hash must be destroyed with `ETHRootDestroy`
## once no longer needed, to release memory.
##
## Parameters:
## * `header` - Light client header.
## * `cfg` - Ethereum Consensus Layer network configuration.
##
## Returns:
## * Pointer to a copy of the given header's execution block hash.
##
## See:
## * https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.0/specs/deneb/beacon-chain.md#executionpayloadheader
discard cfg # Future-proof against SSZ execution block header, EIP-6404ff.
let root = Eth2Digest.new()
root[] = header[].execution.block_hash
root.toUnmanagedPtr()
type ExecutionPayloadHeader =
typeof(default(lcDataFork.LightClientHeader).execution)
func ETHLightClientHeaderGetExecution(
header: ptr lcDataFork.LightClientHeader
): ptr ExecutionPayloadHeader {.exported.} =
## Obtains the execution payload header of a given light client header.
##
## * The returned value is allocated in the given light client header.
## It must neither be released nor written to, and the light client header
## must not be released while the returned value is in use.
##
## Parameters:
## * `header` - Light client header.
##
## Returns:
## * Execution payload header.
##
## See:
## * https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.0/specs/deneb/beacon-chain.md#executionpayloadheader
addr header[].execution
func ETHExecutionPayloadHeaderGetParentHash(
execution: ptr ExecutionPayloadHeader): ptr Eth2Digest {.exported.} =
## Obtains the parent execution block hash of a given
## execution payload header.
##
## * The returned value is allocated in the given execution payload header.
## It must neither be released nor written to, and the execution payload
## header must not be released while the returned value is in use.
##
## Parameters:
## * `execution` - Execution payload header.
##
## Returns:
## * Parent execution block hash.
addr execution[].parent_hash
func ETHExecutionPayloadHeaderGetFeeRecipient(
execution: ptr ExecutionPayloadHeader): ptr ExecutionAddress {.exported.} =
## Obtains the fee recipient address of a given execution payload header.
##
## * The returned value is allocated in the given execution payload header.
## It must neither be released nor written to, and the execution payload
## header must not be released while the returned value is in use.
##
## Parameters:
## * `execution` - Execution payload header.
##
## Returns:
## * Fee recipient execution address.
addr execution[].fee_recipient
func ETHExecutionPayloadHeaderGetStateRoot(
execution: ptr ExecutionPayloadHeader): ptr Eth2Digest {.exported.} =
## Obtains the state MPT root of a given execution payload header.
##
## * The returned value is allocated in the given execution payload header.
## It must neither be released nor written to, and the execution payload
## header must not be released while the returned value is in use.
##
## Parameters:
## * `execution` - Execution payload header.
##
## Returns:
## * Execution state root.
addr execution[].state_root
func ETHExecutionPayloadHeaderGetReceiptsRoot(
execution: ptr ExecutionPayloadHeader): ptr Eth2Digest {.exported.} =
## Obtains the receipts MPT root of a given execution payload header.
##
## * The returned value is allocated in the given execution payload header.
## It must neither be released nor written to, and the execution payload
## header must not be released while the returned value is in use.
##
## Parameters:
## * `execution` - Execution payload header.
##
## Returns:
## * Execution receipts root.
addr execution[].receipts_root
func ETHExecutionPayloadHeaderGetLogsBloom(
execution: ptr ExecutionPayloadHeader): ptr BloomLogs {.exported.} =
## Obtains the logs bloom of a given execution payload header.
##
## * The returned value is allocated in the given execution payload header.
## It must neither be released nor written to, and the execution payload
## header must not be released while the returned value is in use.
##
## Parameters:
## * `execution` - Execution payload header.
##
## Returns:
## * Execution logs bloom.
addr execution[].logs_bloom
func ETHExecutionPayloadHeaderGetPrevRandao(
execution: ptr ExecutionPayloadHeader): ptr Eth2Digest {.exported.} =
## Obtains the previous randao mix of a given execution payload header.
##
## * The returned value is allocated in the given execution payload header.
## It must neither be released nor written to, and the execution payload
## header must not be released while the returned value is in use.
##
## Parameters:
## * `execution` - Execution payload header.
##
## Returns:
## * Previous randao mix.
addr execution[].prev_randao
func ETHExecutionPayloadHeaderGetBlockNumber(
execution: ptr ExecutionPayloadHeader): cint {.exported.} =
## Obtains the execution block number of a given execution payload header.
##
## Parameters:
## * `execution` - Execution payload header.
##
## Returns:
## * Execution block number.
execution[].block_number.cint
func ETHExecutionPayloadHeaderGetGasLimit(
execution: ptr ExecutionPayloadHeader): cint {.exported.} =
## Obtains the gas limit of a given execution payload header.
##
## Parameters:
## * `execution` - Execution payload header.
##
## Returns:
## * Gas limit.
execution[].gas_limit.cint
func ETHExecutionPayloadHeaderGetGasUsed(
execution: ptr ExecutionPayloadHeader): cint {.exported.} =
## Obtains the gas used of a given execution payload header.
##
## Parameters:
## * `execution` - Execution payload header.
##
## Returns:
## * Gas used.
execution[].gas_used.cint
func ETHExecutionPayloadHeaderGetTimestamp(
execution: ptr ExecutionPayloadHeader): cint {.exported.} =
## Obtains the timestamp of a given execution payload header.
##
## Parameters:
## * `execution` - Execution payload header.
##
## Returns:
## * Execution block timestamp.
execution[].timestamp.cint
func ETHExecutionPayloadHeaderGetExtraDataBytes(
execution: ptr ExecutionPayloadHeader
): ptr UncheckedArray[byte] {.exported.} =
## Obtains the extra data buffer of a given execution payload header.
##
## * The returned value is allocated in the given execution payload header.
## It must neither be released nor written to, and the execution payload
## header must not be released while the returned value is in use.
##
## * Use `ETHExecutionPayloadHeaderGetNumExtraDataBytes`
## to obtain the length of the buffer.
##
## Parameters:
## * `execution` - Execution payload header.
##
## Returns:
## * Buffer with execution block extra data.
cast[ptr UncheckedArray[byte]](addr execution[].extra_data[0])
func ETHExecutionPayloadHeaderGetNumExtraDataBytes(
execution: ptr ExecutionPayloadHeader): cint {.exported.} =
## Obtains the extra data buffer's length of a given execution payload header.
##
## Parameters:
## * `execution` - Execution payload header.
##
## Returns:
## * Length of execution block extra data.
execution[].extra_data.len.cint
func ETHExecutionPayloadHeaderGetBaseFeePerGas(
execution: ptr ExecutionPayloadHeader): ptr UInt256 {.exported.} =
## Obtains the base fee per gas of a given execution payload header.
##
## * The returned value is allocated in the given execution payload header.
## It must neither be released nor written to, and the execution payload
## header must not be released while the returned value is in use.
##
## Parameters:
## * `execution` - Execution payload header.
##
## Returns:
## * Base fee per gas.
addr execution[].base_fee_per_gas
func ETHExecutionPayloadHeaderGetBlobGasUsed(
execution: ptr ExecutionPayloadHeader): cint {.exported.} =
## Obtains the blob gas used of a given execution payload header.
##
## Parameters:
## * `execution` - Execution payload header.
##
## Returns:
## * Blob gas used.
execution[].blob_gas_used.cint
func ETHExecutionPayloadHeaderGetExcessBlobGas(
execution: ptr ExecutionPayloadHeader): cint {.exported.} =
## Obtains the excess blob gas of a given execution payload header.
##
## Parameters:
## * `execution` - Execution payload header.
##
## Returns:
## * Excess blob gas.
execution[].excess_blob_gas.cint
type ETHExecutionBlockHeader = object
transactionsRoot: Eth2Digest
withdrawalsRoot: Eth2Digest
proc ETHExecutionBlockHeaderCreateFromJson(
executionHash: ptr Eth2Digest,
blockHeaderJson: cstring): ptr ETHExecutionBlockHeader {.exported.} =
## Verifies that a JSON execution block header is valid and that it matches
## the given `executionHash`.
##
## * The JSON-RPC `eth_getBlockByHash` with params `[executionHash, false]`
## may be used to obtain execution block header data for a given execution
## block hash. Pass the response's `result` property as `blockHeaderJson`.
##
## * The execution block header must be destroyed with
## `ETHExecutionBlockHeaderDestroy` once no longer needed,
## to release memory.
##
## Parameters:
## * `executionHash` - Execution block hash.
## * `blockHeaderJson` - Buffer with JSON encoded header. NULL-terminated.
##
## Returns:
## * Pointer to an initialized execution block header - If successful.
## * `NULL` - If the given `blockHeaderJson` is malformed or incompatible.
##
## See:
## * https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_getblockbyhash
let node =
try:
parseJson($blockHeaderJson)
except Exception:
return nil
var data: BlockObject
try:
fromJson(node, argName = "", data)
except KeyError, ValueError:
return nil
if data == nil:
return nil
# Sanity check
if data.hash.asEth2Digest != executionHash[]:
return nil
# Check fork consistency
static: doAssert totalSerializedFields(BlockObject) == 26,
"Only update this number once code is adjusted to check new fields!"
if data.baseFeePerGas.isNone and (
data.withdrawals.isSome or data.withdrawalsRoot.isSome or
data.blobGasUsed.isSome or data.excessBlobGas.isSome):
return nil
if data.withdrawalsRoot.isNone and (
data.blobGasUsed.isSome or data.excessBlobGas.isSome):
return nil
if data.withdrawals.isSome != data.withdrawalsRoot.isSome:
return nil
if data.blobGasUsed.isSome != data.excessBlobGas.isSome:
return nil
# Construct block header
static: # `GasInt` is signed. We only use it for hashing.
doAssert sizeof(int64) == sizeof(data.gasLimit)
doAssert sizeof(int64) == sizeof(data.gasUsed)
if distinctBase(data.timestamp) > int64.high.uint64:
return nil
if data.nonce.isNone:
return nil
let blockHeader = ExecutionBlockHeader(
parentHash: data.parentHash.asEth2Digest,
ommersHash: data.sha3Uncles.asEth2Digest,
coinbase: distinctBase(data.miner),
stateRoot: data.stateRoot.asEth2Digest,
txRoot: data.transactionsRoot.asEth2Digest,
receiptRoot: data.receiptsRoot.asEth2Digest,
bloom: distinctBase(data.logsBloom),
difficulty: data.difficulty,
blockNumber: distinctBase(data.number).u256,
gasLimit: cast[int64](data.gasLimit),
gasUsed: cast[int64](data.gasUsed),
timestamp: fromUnix(int64.saturate distinctBase(data.timestamp)),
extraData: distinctBase(data.extraData),
mixDigest: data.mixHash.asEth2Digest,
nonce: distinctBase(data.nonce.get),
fee: data.baseFeePerGas,
withdrawalsRoot:
if data.withdrawalsRoot.isSome:
some(data.withdrawalsRoot.get.asEth2Digest)
else:
none(ExecutionHash256),
blobGasUsed:
if data.blobGasUsed.isSome:
some distinctBase(data.blobGasUsed.get)
else:
none(uint64),
excessBlobGas:
if data.excessBlobGas.isSome:
some distinctBase(data.excessBlobGas.get)
else:
none(uint64),
parentBeaconBlockRoot:
if data.parentBeaconBlockRoot.isSome:
some distinctBase(data.parentBeaconBlockRoot.get.asEth2Digest)
else:
none(ExecutionHash256))
if rlpHash(blockHeader) != executionHash[]:
return nil
let executionBlockHeader = ETHExecutionBlockHeader.new()
executionBlockHeader[] = ETHExecutionBlockHeader(
transactionsRoot: blockHeader.txRoot,
withdrawalsRoot: blockHeader.withdrawalsRoot.get(ZERO_HASH))
executionBlockHeader.toUnmanagedPtr()
proc ETHExecutionBlockHeaderDestroy(
executionBlockHeader: ptr ETHExecutionBlockHeader) {.exported.} =
## Destroys an execution block header.
##
## * The execution block header must no longer be used after destruction.
##
## Parameters:
## * `executionBlockHeader` - Execution block header.
executionBlockHeader.destroy()
func ETHExecutionBlockHeaderGetTransactionsRoot(
executionBlockHeader: ptr ETHExecutionBlockHeader
): ptr Eth2Digest {.exported.} =
## Obtains the transactions MPT root of a given execution block header.
##
## * The returned value is allocated in the given execution block header.
## It must neither be released nor written to, and the execution block
## header must not be released while the returned value is in use.
##
## Parameters:
## * `executionBlockHeader` - Execution block header.
##
## Returns:
## * Execution transactions root.
addr executionBlockHeader[].transactionsRoot
func ETHExecutionBlockHeaderGetWithdrawalsRoot(
executionBlockHeader: ptr ETHExecutionBlockHeader
): ptr Eth2Digest {.exported.} =
## Obtains the withdrawals MPT root of a given execution block header.
##
## * The returned value is allocated in the given execution block header.
## It must neither be released nor written to, and the execution block
## header must not be released while the returned value is in use.
##
## Parameters:
## * `executionBlockHeader` - Execution block header.
##
## Returns:
## * Execution withdrawals root.
addr executionBlockHeader[].withdrawalsRoot
type
ETHAccessTuple = object
address: ExecutionAddress
storageKeys: seq[Eth2Digest]
DestinationType {.pure.} = enum
Regular,
Create
ETHTransaction = object
hash: Eth2Digest
chainId: UInt256
`from`: ExecutionAddress
nonce: uint64
maxPriorityFeePerGas: uint64
maxFeePerGas: uint64
gas: uint64
destinationType: DestinationType
to: ExecutionAddress
value: UInt256
input: seq[byte]
accessList: seq[ETHAccessTuple]
maxFeePerBlobGas: uint64
blobVersionedHashes: seq[Eth2Digest]
signature: seq[byte]
bytes: TypedTransaction
proc ETHTransactionsCreateFromJson(
transactionsRoot: ptr Eth2Digest,
transactionsJson: cstring): ptr seq[ETHTransaction] {.exported.} =
## Verifies that JSON transactions data is valid and that it matches
## the given `transactionsRoot`.
##
## * The JSON-RPC `eth_getBlockByHash` with params `[executionHash, true]`
## may be used to obtain transactions data for a given execution
## block hash. Pass `result.transactions` as `transactionsJson`.
##
## * The transaction sequence must be destroyed with
## `ETHTransactionsDestroy` once no longer needed,
## to release memory.
##
## Parameters:
## * `transactionsRoot` - Execution transactions root.
## * `transactionsJson` - Buffer with JSON transactions list. NULL-terminated.
##
## Returns:
## * Pointer to an initialized transaction sequence - If successful.
## * `NULL` - If the given `transactionsJson` is malformed or incompatible.
##
## See:
## * https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_getblockbyhash
let node =
try:
parseJson($transactionsJson)
except Exception:
return nil
var datas: seq[TransactionObject]
try:
fromJson(node, argName = "", datas)
except KeyError, ValueError:
return nil
var txs = newSeqOfCap[ETHTransaction](datas.len)
for i, data in datas:
# Sanity check
if data.transactionIndex.isNone:
return nil
if distinctBase(data.transactionIndex.get) != i.uint64:
return nil
# Check fork consistency
static: doAssert totalSerializedFields(TransactionObject) == 21,
"Only update this number once code is adjusted to check new fields!"
let txType =
case data.`type`.get(0.Quantity):
of 0.Quantity:
if data.accessList.isSome or
data.maxFeePerGas.isSome or data.maxPriorityFeePerGas.isSome or
data.maxFeePerBlobGas.isSome or data.blobVersionedHashes.isSome:
return nil
TxLegacy
of 1.Quantity:
if data.chainId.isNone or data.accessList.isNone:
return nil
if data.maxFeePerGas.isSome or data.maxPriorityFeePerGas.isSome or
data.maxFeePerBlobGas.isSome or data.blobVersionedHashes.isSome:
return nil
TxEip2930
of 2.Quantity:
if data.chainId.isNone or data.accessList.isNone or
data.maxFeePerGas.isNone or data.maxPriorityFeePerGas.isNone:
return nil
if data.maxFeePerBlobGas.isSome or data.blobVersionedHashes.isSome:
return nil
TxEip1559
of 3.Quantity:
if data.to.isNone or data.chainId.isNone or data.accessList.isNone or
data.maxFeePerGas.isNone or data.maxPriorityFeePerGas.isNone or
data.maxFeePerBlobGas.isNone or data.blobVersionedHashes.isNone:
return nil
TxEip4844
else:
return nil
# Construct transaction
static:
doAssert sizeof(uint64) == sizeof(ChainId)
doAssert sizeof(int64) == sizeof(data.gasPrice)
doAssert sizeof(int64) == sizeof(data.maxPriorityFeePerGas.get)
doAssert sizeof(int64) == sizeof(data.maxFeePerBlobGas.get)
if data.chainId.get(default(UInt256)) > distinctBase(ChainId.high).u256:
return nil
if distinctBase(data.gasPrice) > int64.high.uint64:
return nil
if distinctBase(data.maxFeePerGas.get(0.Quantity)) > int64.high.uint64:
return nil
if distinctBase(data.maxPriorityFeePerGas.get(0.Quantity)) >
int64.high.uint64:
return nil
if distinctBase(data.maxFeePerBlobGas.get(0.Quantity)) >
int64.high.uint64:
return nil
if distinctBase(data.gas) > int64.high.uint64:
return nil
if data.v > int64.high.u256:
return nil
let
tx = ExecutionTransaction(
txType: txType,
chainId: data.chainId.get(default(UInt256)).truncate(uint64).ChainId,
nonce: distinctBase(data.nonce),
gasPrice: data.gasPrice.GasInt,
maxPriorityFee:
distinctBase(data.maxPriorityFeePerGas.get(data.gasPrice)).GasInt,
maxFee: distinctBase(data.maxFeePerGas.get(data.gasPrice)).GasInt,
gasLimit: distinctBase(data.gas).GasInt,
to:
if data.to.isSome:
some(distinctBase(data.to.get))
else:
none(EthAddress),
value: data.value,
payload: data.input,
accessList:
if data.accessList.isSome:
data.accessList.get.mapIt(AccessPair(
address: distinctBase(it.address),
storageKeys: it.storageKeys.mapIt(distinctBase(it))))
else:
@[],
maxFeePerBlobGas:
distinctBase(data.maxFeePerBlobGas.get(0.Quantity)).GasInt,
versionedHashes:
if data.blobVersionedHashes.isSome:
data.blobVersionedHashes.get.mapIt(
ExecutionHash256(data: distinctBase(it)))
else:
@[],
V: data.v.truncate(uint64).int64,
R: data.r,
S: data.s)
rlpBytes =
try:
rlp.encode(tx)
except RlpError:
raiseAssert "Unreachable"
hash = keccakHash(rlpBytes)
if data.hash.asEth2Digest != hash:
return nil
# Compute from execution address
var rawSig {.noinit.}: array[65, byte]
rawSig[0 ..< 32] = tx.R.toBytesBE()
rawSig[32 ..< 64] = tx.S.toBytesBE()
rawSig[64] =
if txType != TxLegacy:
tx.V.uint8
elif tx.V.isEven:
1
else:
0
let
sig = SkRecoverableSignature.fromRaw(rawSig).valueOr:
return nil
sigHash = SkMessage.fromBytes(tx.txHashNoSignature().data).valueOr:
return nil
fromPubkey = sig.recover(sigHash).valueOr:
return nil
fromAddress = keys.PublicKey(fromPubkey).toCanonicalAddress()
if distinctBase(data.`from`) != fromAddress:
return nil
# Compute to execution address
let
destinationType =
if tx.to.isSome:
DestinationType.Regular
else:
DestinationType.Create
toAddress =
case destinationType
of DestinationType.Regular:
tx.to.get
of DestinationType.Create:
var res {.noinit.}: array[20, byte]
res[0 ..< 20] = keccakHash(rlp.encodeList(fromAddress, tx.nonce))
.data.toOpenArray(12, 31)
res
txs.add ETHTransaction(
hash: keccakHash(rlpBytes),
chainId: distinctBase(tx.chainId).u256,
`from`: ExecutionAddress(data: fromAddress),
nonce: tx.nonce,
maxPriorityFeePerGas: tx.maxPriorityFee.uint64,
maxFeePerGas: tx.maxFee.uint64,
gas: tx.gasLimit.uint64,
destinationType: destinationType,
to: ExecutionAddress(data: toAddress),
value: tx.value,
input: tx.payload,
accessList: tx.accessList.mapIt(ETHAccessTuple(
address: ExecutionAddress(data: it.address),
storageKeys: it.storageKeys.mapIt(Eth2Digest(data: it)))),
maxFeePerBlobGas: tx.maxFeePerBlobGas.uint64,
blobVersionedHashes: tx.versionedHashes,
signature: @rawSig,
bytes: rlpBytes.TypedTransaction)
var tr = initHexaryTrie(newMemoryDB())
for i, transaction in txs:
try:
tr.put(rlp.encode(i), distinctBase(transaction.bytes))
except RlpError:
raiseAssert "Unreachable"
if tr.rootHash() != transactionsRoot[]:
return nil
let transactions = seq[ETHTransaction].new()
transactions[] = txs
transactions.toUnmanagedPtr()
proc ETHTransactionsDestroy(
transactions: ptr seq[ETHTransaction]) {.exported.} =
## Destroys a transaction sequence.
##
## * The transaction sequence must no longer be used after destruction.
##
## Parameters:
## * `transactions` - Transaction sequence.
transactions.destroy()
func ETHTransactionsGetCount(
transactions: ptr seq[ETHTransaction]): cint {.exported.} =
## Indicates the total number of transactions in a transaction sequence.
##
## * Individual transactions may be investigated using `ETHTransactionsGet`.
##
## Parameters:
## * `transactions` - Transaction sequence.
##
## Returns:
## * Number of available transactions.
transactions[].len.cint
func ETHTransactionsGet(
transactions: ptr seq[ETHTransaction],
transactionIndex: cint): ptr ETHTransaction {.exported.} =
## Obtains an individual transaction by sequential index
## in a transaction sequence.
##
## * The returned value is allocated in the given transaction sequence.
## It must neither be released nor written to, and the transaction
## sequence must not be released while the returned value is in use.
##
## Parameters:
## * `transactions` - Transaction sequence.
## * `transactionIndex` - Sequential transaction index.
##
## Returns:
## * Transaction.
addr transactions[][transactionIndex.int]
func ETHTransactionGetHash(
transaction: ptr ETHTransaction): ptr Eth2Digest {.exported.} =
## Obtains the transaction hash of a transaction.
##
## * The returned value is allocated in the given transaction.
## It must neither be released nor written to, and the transaction
## must not be released while the returned value is in use.
##
## Parameters:
## * `transaction` - Transaction.
##
## Returns:
## * Transaction hash.
addr transaction[].hash
func ETHTransactionGetChainId(
transaction: ptr ETHTransaction): ptr UInt256 {.exported.} =
## Obtains the chain ID of a transaction.
##
## * The returned value is allocated in the given transaction.
## It must neither be released nor written to, and the transaction
## must not be released while the returned value is in use.
##
## Parameters:
## * `transaction` - Transaction.
##
## Returns:
## * Chain ID.
addr transaction[].chainId
func ETHTransactionGetFrom(
transaction: ptr ETHTransaction): ptr ExecutionAddress {.exported.} =
## Obtains the from address of a transaction.
##
## * The returned value is allocated in the given transaction.
## It must neither be released nor written to, and the transaction
## must not be released while the returned value is in use.
##
## Parameters:
## * `transaction` - Transaction.
##
## Returns:
## * From execution address.
addr transaction[].`from`
func ETHTransactionGetNonce(
transaction: ptr ETHTransaction): ptr uint64 {.exported.} =
## Obtains the nonce of a transaction.
##
## * The returned value is allocated in the given transaction.
## It must neither be released nor written to, and the transaction
## must not be released while the returned value is in use.
##
## Parameters:
## * `transaction` - Transaction.
##
## Returns:
## * Nonce.
addr transaction[].nonce
func ETHTransactionGetMaxPriorityFeePerGas(
transaction: ptr ETHTransaction): ptr uint64 {.exported.} =
## Obtains the max priority fee per gas of a transaction.
##
## * The returned value is allocated in the given transaction.
## It must neither be released nor written to, and the transaction
## must not be released while the returned value is in use.
##
## Parameters:
## * `transaction` - Transaction.
##
## Returns:
## * Max priority fee per gas.
addr transaction[].maxPriorityFeePerGas
func ETHTransactionGetMaxFeePerGas(
transaction: ptr ETHTransaction): ptr uint64 {.exported.} =
## Obtains the max fee per gas of a transaction.
##
## * The returned value is allocated in the given transaction.
## It must neither be released nor written to, and the transaction
## must not be released while the returned value is in use.
##
## Parameters:
## * `transaction` - Transaction.
##
## Returns:
## * Max fee per gas.
addr transaction[].maxFeePerGas
func ETHTransactionGetGas(
transaction: ptr ETHTransaction): ptr uint64 {.exported.} =
## Obtains the gas of a transaction.
##
## * The returned value is allocated in the given transaction.
## It must neither be released nor written to, and the transaction
## must not be released while the returned value is in use.
##
## Parameters:
## * `transaction` - Transaction.
##
## Returns:
## * Gas.
addr transaction[].gas
func ETHTransactionIsCreatingContract(
transaction: ptr ETHTransaction): bool {.exported.} =
## Indicates whether or not a transaction is creating a contract.
##
## Parameters:
## * `transaction` - Transaction.
##
## Returns:
## * Whether or not the transaction is creating a contract.
case transaction[].destinationType
of DestinationType.Regular:
false
of DestinationType.Create:
true
func ETHTransactionGetTo(
transaction: ptr ETHTransaction): ptr ExecutionAddress {.exported.} =
## Obtains the to address of a transaction.
##
## * If the transaction is creating a contract, this function returns
## the address of the new contract.
##
## * The returned value is allocated in the given transaction.
## It must neither be released nor written to, and the transaction
## must not be released while the returned value is in use.
##
## Parameters:
## * `transaction` - Transaction.
##
## Returns:
## * To execution address.
addr transaction[].to
func ETHTransactionGetValue(
transaction: ptr ETHTransaction): ptr UInt256 {.exported.} =
## Obtains the value of a transaction.
##
## * The returned value is allocated in the given transaction.
## It must neither be released nor written to, and the transaction
## must not be released while the returned value is in use.
##
## Parameters:
## * `transaction` - Transaction.
##
## Returns:
## * Value.
addr transaction[].value
func ETHTransactionGetInputBytes(
transaction: ptr ETHTransaction,
numBytes #[out]#: ptr cint): ptr UncheckedArray[byte] {.exported.} =
## Obtains the input of a transaction.
##
## * The returned value is allocated in the given transaction.
## It must neither be released nor written to, and the transaction
## must not be released while the returned value is in use.
##
## Parameters:
## * `transaction` - Transaction.
## * `numBytes` [out] - Length of buffer.
##
## Returns:
## * Buffer with input.
numBytes[] = transaction[].input.len.cint
if transaction[].input.len == 0:
# https://github.com/nim-lang/Nim/issues/22389
const defaultInput: cstring = ""
return cast[ptr UncheckedArray[byte]](defaultInput)
cast[ptr UncheckedArray[byte]](addr transaction[].input[0])
func ETHTransactionGetAccessList(
transaction: ptr ETHTransaction): ptr seq[ETHAccessTuple] {.exported.} =
## Obtains the access list of a transaction.
##
## * The returned value is allocated in the given transaction.
## It must neither be released nor written to, and the transaction
## must not be released while the returned value is in use.
##
## Parameters:
## * `transaction` - Transaction.
##
## Returns:
## * Transaction access list.
addr transaction[].accessList
func ETHAccessListGetCount(
accessList: ptr seq[ETHAccessTuple]): cint {.exported.} =
## Indicates the total number of access tuples in a transaction access list.
##
## * Individual access tuples may be investigated using `ETHAccessListGet`.
##
## Parameters:
## * `accessList` - Transaction access list.
##
## Returns:
## * Number of available access tuples.
accessList[].len.cint
func ETHAccessListGet(
accessList: ptr seq[ETHAccessTuple],
accessTupleIndex: cint): ptr ETHAccessTuple {.exported.} =
## Obtains an individual access tuple by sequential index
## in a transaction access list.
##
## * The returned value is allocated in the given transaction access list.
## It must neither be released nor written to, and the transaction
## access list must not be released while the returned value is in use.
##
## Parameters:
## * `accessList` - Transaction access list.
## * `accessTupleIndex` - Sequential access tuple index.
##
## Returns:
## * Access tuple.
addr accessList[][accessTupleIndex.int]
func ETHAccessTupleGetAddress(
accessTuple: ptr ETHAccessTuple): ptr ExecutionAddress {.exported.} =
## Obtains the address of an access tuple.
##
## * The returned value is allocated in the given access tuple.
## It must neither be released nor written to, and the access tuple
## must not be released while the returned value is in use.
##
## Parameters:
## * `accessTuple` - Access tuple.
##
## Returns:
## * Address.
addr accessTuple[].address
func ETHAccessTupleGetNumStorageKeys(
accessTuple: ptr ETHAccessTuple): cint {.exported.} =
## Indicates the total number of storage keys in an access tuple.
##
## * Individual storage keys may be investigated using
## `ETHAccessTupleGetStorageKey`.
##
## Parameters:
## * `accessTuple` - Access tuple.
##
## Returns:
## * Number of available storage keys.
accessTuple[].storageKeys.len.cint
func ETHAccessTupleGetStorageKey(
accessTuple: ptr ETHAccessTuple,
storageKeyIndex: cint): ptr Eth2Digest {.exported.} =
## Obtains an individual storage key by sequential index
## in an access tuple.
##
## * The returned value is allocated in the given transaction access tuple.
## It must neither be released nor written to, and the transaction
## access tuple must not be released while the returned value is in use.
##
## Parameters:
## * `accessTuple` - Access tuple.
## * `storageKeyIndex` - Sequential storage key index.
##
## Returns:
## * Storage key.
addr accessTuple[].storageKeys[storageKeyIndex.int]
func ETHTransactionGetMaxFeePerBlobGas(
transaction: ptr ETHTransaction): ptr uint64 {.exported.} =
## Obtains the max fee per blob gas of a transaction.
##
## * The returned value is allocated in the given transaction.
## It must neither be released nor written to, and the transaction
## must not be released while the returned value is in use.
##
## Parameters:
## * `transaction` - Transaction.
##
## Returns:
## * Max fee per blob gas.
addr transaction[].maxFeePerBlobGas
func ETHTransactionGetNumBlobVersionedHashes(
transaction: ptr ETHTransaction): cint {.exported.} =
## Indicates the total number of blob versioned hashes of a transaction.
##
## * Individual blob versioned hashes may be investigated using
## `ETHTransactionGetBlobVersionedHash`.
##
## Parameters:
## * `transaction` - Transaction.
##
## Returns:
## * Number of available blob versioned hashes.
transaction[].blobVersionedHashes.len.cint
func ETHTransactionGetBlobVersionedHash(
transaction: ptr ETHTransaction,
versionedHashIndex: cint): ptr Eth2Digest {.exported.} =
## Obtains an individual blob versioned hash by sequential index
## in a transaction.
##
## * The returned value is allocated in the given transaction.
## It must neither be released nor written to, and the transaction
## must not be released while the returned value is in use.
##
## Parameters:
## * `transaction` - Transaction.
## * `versionedHashIndex` - Sequential blob versioned hash index.
##
## Returns:
## * Blob versioned hash.
addr transaction[].blobVersionedHashes[versionedHashIndex.int]
func ETHTransactionGetSignatureBytes(
transaction: ptr ETHTransaction,
numBytes #[out]#: ptr cint): ptr UncheckedArray[byte] {.exported.} =
## Obtains the signature of a transaction.
##
## * The returned value is allocated in the given transaction.
## It must neither be released nor written to, and the transaction
## must not be released while the returned value is in use.
##
## Parameters:
## * `transaction` - Transaction.
## * `numBytes` [out] - Length of buffer.
##
## Returns:
## * Buffer with signature.
numBytes[] = distinctBase(transaction[].signature).len.cint
if distinctBase(transaction[].signature).len == 0:
# https://github.com/nim-lang/Nim/issues/22389
const defaultBytes: cstring = ""
return cast[ptr UncheckedArray[byte]](defaultBytes)
cast[ptr UncheckedArray[byte]](addr distinctBase(transaction[].signature)[0])
func ETHTransactionGetBytes(
transaction: ptr ETHTransaction,
numBytes #[out]#: ptr cint): ptr UncheckedArray[byte] {.exported.} =
## Obtains the raw byte representation of a transaction.
##
## * The returned value is allocated in the given transaction.
## It must neither be released nor written to, and the transaction
## must not be released while the returned value is in use.
##
## Parameters:
## * `transaction` - Transaction.
## * `numBytes` [out] - Length of buffer.
##
## Returns:
## * Buffer with raw transaction data.
numBytes[] = distinctBase(transaction[].bytes).len.cint
if distinctBase(transaction[].bytes).len == 0:
# https://github.com/nim-lang/Nim/issues/22389
const defaultBytes: cstring = ""
return cast[ptr UncheckedArray[byte]](defaultBytes)
cast[ptr UncheckedArray[byte]](addr distinctBase(transaction[].bytes)[0])