mirror of
https://github.com/status-im/nimbus-eth2.git
synced 2025-01-23 21:12:52 +00:00
Merge branch 'stable' into dev/etan/lc-wasm4
This commit is contained in:
commit
43b3b5b8b1
22
CHANGELOG.md
22
CHANGELOG.md
@ -1,3 +1,25 @@
|
||||
2023-02-27 v24.2.2
|
||||
==================
|
||||
|
||||
Nimbus `v24.2.2` is a hotfix release addressing a consensus violation issue affecting Deneb-transitioned network such as Holešky. Please upgrade as soon as possible if your node is affected.
|
||||
|
||||
### Improvements
|
||||
|
||||
* Added metrics `validator_monitor_block_hit` and `validator_monitor_block_miss` tracking the number of successful and missed block proposals:
|
||||
https://github.com/status-im/nimbus-eth2/pull/5913
|
||||
|
||||
### Fixes
|
||||
|
||||
* Nimbus had an incomplete implementation of EIP-7044 (Perpetually Valid Signed Voluntary Exits):
|
||||
https://github.com/status-im/nimbus-eth2/pull/5953
|
||||
https://github.com/status-im/nimbus-eth2/pull/5954
|
||||
https://github.com/status-im/nimbus-eth2/pull/5959
|
||||
https://github.com/status-im/nimbus-eth2/pull/5966
|
||||
|
||||
* The Nimbus `v24.2.1` validator client was crashing with a `RangeDefect` error message during block proposal when paired with a `v24.1.x` beacon node:
|
||||
https://github.com/status-im/nim-stint/pull/148
|
||||
|
||||
|
||||
2023-02-20 v24.2.1
|
||||
==================
|
||||
|
||||
|
@ -285,7 +285,7 @@ proc addHeadBlockWithParent*(
|
||||
var sigs: seq[SignatureSet]
|
||||
if (let e = sigs.collectSignatureSets(
|
||||
signedBlock, dag.db.immutableValidators,
|
||||
dag.clearanceState, dag.cfg.genesisFork(), dag.cfg.capellaFork(),
|
||||
dag.clearanceState, dag.cfg.genesisFork(), dag.cfg.CAPELLA_FORK_VERSION,
|
||||
cache); e.isErr()):
|
||||
# A PublicKey or Signature isn't on the BLS12-381 curve
|
||||
info "Unable to load signature sets",
|
||||
|
@ -956,9 +956,14 @@ proc advanceSlots*(
|
||||
# which is an acceptable tradeoff for monitoring.
|
||||
withState(state):
|
||||
let postEpoch = forkyState.data.slot.epoch
|
||||
if preEpoch != postEpoch:
|
||||
if preEpoch != postEpoch and postEpoch >= 2:
|
||||
var proposers: array[SLOTS_PER_EPOCH, Opt[ValidatorIndex]]
|
||||
let epochRef = dag.findEpochRef(stateBid, postEpoch - 2)
|
||||
if epochRef.isSome():
|
||||
proposers = epochRef[][].beacon_proposers
|
||||
|
||||
dag.validatorMonitor[].registerEpochInfo(
|
||||
postEpoch, info, forkyState.data)
|
||||
forkyState.data, proposers, info)
|
||||
|
||||
proc applyBlock(
|
||||
dag: ChainDAGRef, state: var ForkedHashedBeaconState, bid: BlockId,
|
||||
|
@ -220,33 +220,19 @@ proc restValidatorExit(config: BeaconNodeConf) {.async.} =
|
||||
reason = exc.msg
|
||||
quit 1
|
||||
|
||||
# https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.6/specs/phase0/beacon-chain.md#voluntary-exits
|
||||
# https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.0/specs/deneb/beacon-chain.md#modified-process_voluntary_exit
|
||||
let signingFork = try:
|
||||
let response = await client.getSpecVC()
|
||||
if response.status == 200:
|
||||
let
|
||||
spec = response.data.data
|
||||
denebForkEpoch =
|
||||
block:
|
||||
let s = spec.getOrDefault("DENEB_FORK_EPOCH", $FAR_FUTURE_EPOCH)
|
||||
Epoch(Base10.decode(uint64, s).get(uint64(FAR_FUTURE_EPOCH)))
|
||||
# https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.6/specs/phase0/beacon-chain.md#voluntary-exits
|
||||
# https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.0/specs/deneb/beacon-chain.md#modified-process_voluntary_exit
|
||||
if currentEpoch >= denebForkEpoch:
|
||||
let capellaForkVersion =
|
||||
block:
|
||||
var res: Version
|
||||
# CAPELLA_FOR_VERSION has specific format - "0x01000000", so
|
||||
# default empty string is invalid, so `hexToByteArrayStrict`
|
||||
# will raise exception on empty string.
|
||||
let s = spec.getOrDefault("CAPELLA_FORK_VERSION", "")
|
||||
hexToByteArrayStrict(s, distinctBase(res))
|
||||
res
|
||||
Fork(
|
||||
current_version: capellaForkVersion,
|
||||
previous_version: capellaForkVersion,
|
||||
epoch: GENESIS_EPOCH) # irrelevant when current/previous identical
|
||||
else:
|
||||
fork
|
||||
let forkConfig = response.data.data.getConsensusForkConfig()
|
||||
if forkConfig.isErr:
|
||||
raise newException(RestError, "Invalid config: " & forkConfig.error)
|
||||
let capellaForkVersion = forkConfig.get.capellaVersion.valueOr:
|
||||
raise newException(RestError,
|
||||
ConsensusFork.Capella.forkVersionConfigKey() & " missing")
|
||||
voluntary_exit_signature_fork(
|
||||
fork, capellaForkVersion, currentEpoch, forkConfig.get.denebEpoch)
|
||||
else:
|
||||
raise newException(RestError, "Error response (" & $response.status & ")")
|
||||
except CatchableError as exc:
|
||||
@ -254,7 +240,7 @@ proc restValidatorExit(config: BeaconNodeConf) {.async.} =
|
||||
reason = exc.msg
|
||||
quit 1
|
||||
|
||||
debug "Signing fork obtained", fork = fork
|
||||
debug "Signing fork obtained", fork, signingFork
|
||||
|
||||
if not config.printData:
|
||||
case askForExitConfirmation()
|
||||
@ -292,7 +278,7 @@ proc restValidatorExit(config: BeaconNodeConf) {.async.} =
|
||||
validatorKeyAsStr,
|
||||
exitAtEpoch,
|
||||
validatorIdx,
|
||||
fork,
|
||||
signingFork,
|
||||
genesis_validators_root)
|
||||
|
||||
if config.printData:
|
||||
|
@ -755,6 +755,12 @@ proc init*(T: type BeaconNode,
|
||||
withState(dag.headState):
|
||||
getValidator(forkyState().data.validators.asSeq(), pubkey)
|
||||
|
||||
func getCapellaForkVersion(): Opt[Version] =
|
||||
Opt.some(cfg.CAPELLA_FORK_VERSION)
|
||||
|
||||
func getDenebForkEpoch(): Opt[Epoch] =
|
||||
Opt.some(cfg.DENEB_FORK_EPOCH)
|
||||
|
||||
proc getForkForEpoch(epoch: Epoch): Opt[Fork] =
|
||||
Opt.some(dag.forkAtEpoch(epoch))
|
||||
|
||||
@ -784,6 +790,8 @@ proc init*(T: type BeaconNode,
|
||||
config.getPayloadBuilderAddress,
|
||||
getValidatorAndIdx,
|
||||
getBeaconTime,
|
||||
getCapellaForkVersion,
|
||||
getDenebForkEpoch,
|
||||
getForkForEpoch,
|
||||
getGenesisRoot)
|
||||
else: nil
|
||||
|
@ -4,6 +4,9 @@
|
||||
# * 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
|
||||
stew/io2, presto, metrics, metrics/chronos_httpserver,
|
||||
./rpc/rest_key_management_api,
|
||||
@ -348,6 +351,18 @@ proc asyncInit(vc: ValidatorClientRef): Future[ValidatorClientRef] {.async.} =
|
||||
let
|
||||
keymanagerInitResult = initKeymanagerServer(vc.config, nil)
|
||||
|
||||
func getCapellaForkVersion(): Opt[Version] =
|
||||
if vc.runtimeConfig.forkConfig.isSome():
|
||||
vc.runtimeConfig.forkConfig.get().capellaVersion
|
||||
else:
|
||||
Opt.none(Version)
|
||||
|
||||
func getDenebForkEpoch(): Opt[Epoch] =
|
||||
if vc.runtimeConfig.forkConfig.isSome():
|
||||
Opt.some(vc.runtimeConfig.forkConfig.get().denebEpoch)
|
||||
else:
|
||||
Opt.none(Epoch)
|
||||
|
||||
proc getForkForEpoch(epoch: Epoch): Opt[Fork] =
|
||||
if len(vc.forks) > 0:
|
||||
Opt.some(vc.forkAtEpoch(epoch))
|
||||
@ -379,6 +394,8 @@ proc asyncInit(vc: ValidatorClientRef): Future[ValidatorClientRef] {.async.} =
|
||||
Opt.none(string),
|
||||
nil,
|
||||
vc.beaconClock.getBeaconTimeFn,
|
||||
getCapellaForkVersion,
|
||||
getDenebForkEpoch,
|
||||
getForkForEpoch,
|
||||
getGenesisRoot
|
||||
)
|
||||
|
@ -241,6 +241,10 @@ const
|
||||
"The given Merkle proof is invalid"
|
||||
InvalidMerkleProofIndexError* =
|
||||
"The given Merkle proof index is invalid"
|
||||
FailedToObtainForkVersionError* =
|
||||
"Failed to obtain fork version"
|
||||
FailedToObtainConsensusForkError* =
|
||||
"Failed to obtain consensus fork information"
|
||||
FailedToObtainForkError* =
|
||||
"Failed to obtain fork information"
|
||||
InvalidTimestampValue* =
|
||||
|
@ -1,9 +1,12 @@
|
||||
# beacon_chain
|
||||
# Copyright (c) 2021-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: [].}
|
||||
|
||||
# NOTE: This module has been used in both `beacon_node` and `validator_client`,
|
||||
# please keep imports clear of `rest_utils` or any other module which imports
|
||||
# beacon node's specific networking code.
|
||||
@ -561,6 +564,7 @@ proc installKeymanagerHandlers*(router: var RestRouter, host: KeymanagerHost) =
|
||||
let
|
||||
qpubkey = pubkey.valueOr:
|
||||
return keymanagerApiError(Http400, InvalidValidatorPublicKey)
|
||||
currentEpoch = host.getBeaconTimeFn().slotOrZero().epoch()
|
||||
qepoch =
|
||||
if epoch.isSome():
|
||||
let res = epoch.get()
|
||||
@ -568,7 +572,7 @@ proc installKeymanagerHandlers*(router: var RestRouter, host: KeymanagerHost) =
|
||||
return keymanagerApiError(Http400, InvalidEpochValueError)
|
||||
res.get()
|
||||
else:
|
||||
host.getBeaconTimeFn().slotOrZero().epoch()
|
||||
currentEpoch
|
||||
validator =
|
||||
block:
|
||||
let res = host.validatorPool[].getValidator(qpubkey).valueOr:
|
||||
@ -581,10 +585,16 @@ proc installKeymanagerHandlers*(router: var RestRouter, host: KeymanagerHost) =
|
||||
validator_index: uint64(validator.index.get()))
|
||||
fork = host.getForkFn(qepoch).valueOr:
|
||||
return keymanagerApiError(Http500, FailedToObtainForkError)
|
||||
capellaForkVersion = host.getCapellaForkVersionFn().valueOr:
|
||||
return keymanagerApiError(Http500, FailedToObtainForkVersionError)
|
||||
denebForkEpoch = host.getDenebForkEpochFn().valueOr:
|
||||
return keymanagerApiError(Http500, FailedToObtainConsensusForkError)
|
||||
signingFork = voluntary_exit_signature_fork(
|
||||
fork, capellaForkVersion, currentEpoch, denebForkEpoch)
|
||||
signature =
|
||||
try:
|
||||
let res = await validator.getValidatorExitSignature(
|
||||
fork, host.getGenesisFn(), voluntaryExit)
|
||||
signingFork, host.getGenesisFn(), voluntaryExit)
|
||||
if res.isErr():
|
||||
return keymanagerApiError(Http500, res.error())
|
||||
res.get()
|
||||
|
@ -13,7 +13,8 @@ import
|
||||
rest_beacon_calls, rest_builder_calls, rest_config_calls, rest_debug_calls,
|
||||
rest_keymanager_calls, rest_light_client_calls,
|
||||
rest_node_calls, rest_validator_calls,
|
||||
rest_nimbus_calls, rest_event_calls, rest_common
|
||||
rest_nimbus_calls, rest_event_calls, rest_common,
|
||||
rest_fork_config
|
||||
]
|
||||
|
||||
export
|
||||
@ -21,4 +22,5 @@ export
|
||||
rest_beacon_calls, rest_builder_calls, rest_config_calls, rest_debug_calls,
|
||||
rest_keymanager_calls, rest_light_client_calls,
|
||||
rest_node_calls, rest_validator_calls,
|
||||
rest_nimbus_calls, rest_event_calls, rest_common
|
||||
rest_nimbus_calls, rest_event_calls, rest_common,
|
||||
rest_fork_config
|
||||
|
97
beacon_chain/spec/eth2_apis/rest_fork_config.nim
Normal file
97
beacon_chain/spec/eth2_apis/rest_fork_config.nim
Normal file
@ -0,0 +1,97 @@
|
||||
# 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/strutils,
|
||||
stew/[base10, byteutils],
|
||||
../forks
|
||||
|
||||
from ./rest_types import VCRuntimeConfig
|
||||
|
||||
export forks, rest_types
|
||||
|
||||
type VCForkConfig* = object
|
||||
altairEpoch*: Epoch
|
||||
capellaVersion*: Opt[Version]
|
||||
capellaEpoch*: Epoch
|
||||
denebEpoch*: Epoch
|
||||
|
||||
func forkVersionConfigKey*(consensusFork: ConsensusFork): string =
|
||||
if consensusFork > ConsensusFork.Phase0:
|
||||
($consensusFork).toUpperAscii() & "_FORK_VERSION"
|
||||
else:
|
||||
"GENESIS_FORK_VERSION"
|
||||
|
||||
func forkEpochConfigKey*(consensusFork: ConsensusFork): string =
|
||||
doAssert consensusFork > ConsensusFork.Phase0
|
||||
($consensusFork).toUpperAscii() & "_FORK_EPOCH"
|
||||
|
||||
proc getOrDefault*(info: VCRuntimeConfig, name: string,
|
||||
default: uint64): uint64 =
|
||||
let numstr = info.getOrDefault(name, "missing")
|
||||
if numstr == "missing": return default
|
||||
Base10.decode(uint64, numstr).valueOr:
|
||||
return default
|
||||
|
||||
proc getOrDefault*(info: VCRuntimeConfig, name: string, default: Epoch): Epoch =
|
||||
Epoch(info.getOrDefault(name, uint64(default)))
|
||||
|
||||
func getForkVersion(
|
||||
info: VCRuntimeConfig,
|
||||
consensusFork: Consensusfork): Result[Opt[Version], string] =
|
||||
let key = consensusFork.forkVersionConfigKey()
|
||||
let stringValue = info.getOrDefault(key, "missing")
|
||||
if stringValue == "missing": return ok Opt.none(Version)
|
||||
var value: Version
|
||||
try:
|
||||
hexToByteArrayStrict(stringValue, distinctBase(value))
|
||||
except ValueError as exc:
|
||||
return err(key & " is invalid: " & exc.msg)
|
||||
ok Opt.some value
|
||||
|
||||
func getForkEpoch(info: VCRuntimeConfig, consensusFork: ConsensusFork): Epoch =
|
||||
if consensusFork > ConsensusFork.Phase0:
|
||||
let key = consensusFork.forkEpochConfigKey()
|
||||
info.getOrDefault(key, FAR_FUTURE_EPOCH)
|
||||
else:
|
||||
GENESIS_EPOCH
|
||||
|
||||
func getConsensusForkConfig*(
|
||||
info: VCRuntimeConfig): Result[VCForkConfig, string] =
|
||||
## This extracts all `_FORK_VERSION` and `_FORK_EPOCH` constants
|
||||
## that are relevant for Validator Client operation.
|
||||
##
|
||||
## Note that the fork schedule (`/eth/v1/config/fork_schedule`) cannot be used
|
||||
## because it does not indicate whether the forks refer to `ConsensusFork` or
|
||||
## to a different fork sequence from an incompatible network (e.g., devnet)
|
||||
let
|
||||
res = VCForkConfig(
|
||||
altairEpoch: info.getForkEpoch(ConsensusFork.Altair),
|
||||
capellaVersion: ? info.getForkVersion(ConsensusFork.Capella),
|
||||
capellaEpoch: info.getForkEpoch(ConsensusFork.Capella),
|
||||
denebEpoch: info.getForkEpoch(ConsensusFork.Deneb))
|
||||
|
||||
if res.capellaEpoch < res.altairEpoch:
|
||||
return err(
|
||||
"Fork epochs are inconsistent, " & $ConsensusFork.Capella &
|
||||
" is scheduled at epoch " & $res.capellaEpoch &
|
||||
" which is before prior fork epoch " & $res.altairEpoch)
|
||||
if res.denebEpoch < res.capellaEpoch:
|
||||
return err(
|
||||
"Fork epochs are inconsistent, " & $ConsensusFork.Deneb &
|
||||
" is scheduled at epoch " & $res.denebEpoch &
|
||||
" which is before prior fork epoch " & $res.capellaEpoch)
|
||||
|
||||
if res.capellaEpoch != FAR_FUTURE_EPOCH and res.capellaVersion.isNone:
|
||||
return err(
|
||||
"Beacon node has scheduled " &
|
||||
ConsensusFork.Capella.forkEpochConfigKey() &
|
||||
" but does not report " &
|
||||
ConsensusFork.Capella.forkVersionConfigKey())
|
||||
ok res
|
@ -216,6 +216,41 @@ func compute_voluntary_exit_signing_root*(
|
||||
fork, DOMAIN_VOLUNTARY_EXIT, epoch, genesis_validators_root)
|
||||
compute_signing_root(voluntary_exit, domain)
|
||||
|
||||
func voluntary_exit_signature_fork(
|
||||
is_post_deneb: static bool,
|
||||
state_fork: Fork,
|
||||
capella_fork_version: Version): Fork =
|
||||
when is_post_deneb:
|
||||
# Always use Capella fork version, disregarding `VoluntaryExit` epoch
|
||||
# [Modified in Deneb:EIP7044]
|
||||
Fork(
|
||||
previous_version: capella_fork_version,
|
||||
current_version: capella_fork_version,
|
||||
epoch: GENESIS_EPOCH) # irrelevant when current/previous identical
|
||||
else:
|
||||
state_fork
|
||||
|
||||
func voluntary_exit_signature_fork*(
|
||||
consensusFork: static ConsensusFork,
|
||||
state_fork: Fork,
|
||||
capella_fork_version: Version): Fork =
|
||||
const is_post_deneb = (consensusFork >= ConsensusFork.Deneb)
|
||||
voluntary_exit_signature_fork(is_post_deneb, state_fork, capella_fork_version)
|
||||
|
||||
func voluntary_exit_signature_fork*(
|
||||
state_fork: Fork,
|
||||
capella_fork_version: Version,
|
||||
current_epoch: Epoch,
|
||||
deneb_fork_epoch: Epoch): Fork =
|
||||
if current_epoch >= deneb_fork_epoch:
|
||||
const is_post_deneb = true
|
||||
voluntary_exit_signature_fork(
|
||||
is_post_deneb, state_fork, capella_fork_version)
|
||||
else:
|
||||
const is_post_deneb = false
|
||||
voluntary_exit_signature_fork(
|
||||
is_post_deneb, state_fork, capella_fork_version)
|
||||
|
||||
func get_voluntary_exit_signature*(
|
||||
fork: Fork, genesis_validators_root: Eth2Digest,
|
||||
voluntary_exit: VoluntaryExit,
|
||||
|
@ -235,7 +235,7 @@ proc collectSignatureSets*(
|
||||
validatorKeys: openArray[ImmutableValidatorData2],
|
||||
state: ForkedHashedBeaconState,
|
||||
genesis_fork: Fork,
|
||||
capella_fork: Fork,
|
||||
capella_fork_version: Version,
|
||||
cache: var StateCache): Result[void, cstring] =
|
||||
## Collect all signature verifications that process_block would normally do
|
||||
## except deposits, in one go.
|
||||
@ -385,25 +385,24 @@ proc collectSignatureSets*(
|
||||
# SSZ deserialization guarantees that blocks received from random sources
|
||||
# including peer or RPC
|
||||
# have at most MAX_VOLUNTARY_EXITS voluntary exits.
|
||||
for i in 0 ..< signed_block.message.body.voluntary_exits.len:
|
||||
# don't use "items" for iterating over large type
|
||||
# due to https://github.com/nim-lang/Nim/issues/14421
|
||||
# fixed in 1.4.2
|
||||
template volex: untyped = signed_block.message.body.voluntary_exits[i]
|
||||
let key = validatorKeys.load(volex.message.validator_index).valueOr:
|
||||
return err("collectSignatureSets: invalid voluntary exit")
|
||||
if signed_block.message.body.voluntary_exits.len > 0:
|
||||
let voluntary_exit_fork = withConsensusFork(state.kind):
|
||||
consensusFork.voluntary_exit_signature_fork(fork, capella_fork_version)
|
||||
for i in 0 ..< signed_block.message.body.voluntary_exits.len:
|
||||
# don't use "items" for iterating over large type
|
||||
# due to https://github.com/nim-lang/Nim/issues/14421
|
||||
# fixed in 1.4.2
|
||||
template volex: untyped = signed_block.message.body.voluntary_exits[i]
|
||||
let key = validatorKeys.load(volex.message.validator_index).valueOr:
|
||||
return err("collectSignatureSets: invalid voluntary exit")
|
||||
|
||||
sigs.add voluntary_exit_signature_set(
|
||||
# https://eips.ethereum.org/EIPS/eip-7044
|
||||
# https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.4/specs/deneb/beacon-chain.md#modified-process_voluntary_exit
|
||||
(if state.kind >= ConsensusFork.Capella:
|
||||
capella_fork
|
||||
else:
|
||||
fork),
|
||||
genesis_validators_root, volex.message, key,
|
||||
volex.signature.load.valueOr do:
|
||||
return err(
|
||||
"collectSignatureSets: cannot load voluntary exit signature"))
|
||||
sigs.add voluntary_exit_signature_set(
|
||||
# https://eips.ethereum.org/EIPS/eip-7044
|
||||
# https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.7/specs/deneb/beacon-chain.md#modified-process_voluntary_exit
|
||||
voluntary_exit_fork, genesis_validators_root, volex.message, key,
|
||||
volex.signature.load.valueOr do:
|
||||
return err(
|
||||
"collectSignatureSets: cannot load voluntary exit signature"))
|
||||
|
||||
block:
|
||||
when signed_block is phase0.SignedBeaconBlock:
|
||||
|
@ -269,9 +269,23 @@ func findValidatorIndex*(state: ForkyBeaconState, pubkey: ValidatorPubKey):
|
||||
# given that each block can hold no more than 16 deposits, it's slower to
|
||||
# build the table and use it for lookups than to scan it like this.
|
||||
# Once we have a reusable, long-lived cache, this should be revisited
|
||||
for vidx in state.validators.vindices:
|
||||
if state.validators.asSeq[vidx].pubkey == pubkey:
|
||||
return Opt[ValidatorIndex].ok(vidx)
|
||||
#
|
||||
# For deposit processing purposes, two broad cases exist, either
|
||||
#
|
||||
# (a) someone has deposited all 32 required ETH as a single transaction,
|
||||
# in which case the index doesn't yet exist so the search order does
|
||||
# not matter so long as it's generally in an order memory controller
|
||||
# prefetching can predict; or
|
||||
#
|
||||
# (b) the deposit has been split into multiple parts, typically not far
|
||||
# apart from each other, such that on average one would expect this
|
||||
# validator index to be nearer the maximal than minimal index.
|
||||
#
|
||||
# countdown() infinite-loops if the lower bound with uint32 is 0, so
|
||||
# shift indices by 1, which avoids triggering unsigned wraparound.
|
||||
for vidx in countdown(state.validators.len.uint32, 1):
|
||||
if state.validators.asSeq[vidx - 1].pubkey == pubkey:
|
||||
return Opt[ValidatorIndex].ok((vidx - 1).ValidatorIndex)
|
||||
|
||||
proc process_deposit*(cfg: RuntimeConfig,
|
||||
state: var ForkyBeaconState,
|
||||
@ -365,16 +379,11 @@ proc check_voluntary_exit*(
|
||||
|
||||
# Verify signature
|
||||
if skipBlsValidation notin flags:
|
||||
let exitSignatureFork =
|
||||
when typeof(state).kind >= ConsensusFork.Deneb:
|
||||
Fork(
|
||||
previous_version: cfg.CAPELLA_FORK_VERSION,
|
||||
current_version: cfg.CAPELLA_FORK_VERSION,
|
||||
epoch: cfg.CAPELLA_FORK_EPOCH)
|
||||
else:
|
||||
state.fork
|
||||
const consensusFork = typeof(state).kind
|
||||
let voluntary_exit_fork = consensusFork.voluntary_exit_signature_fork(
|
||||
state.fork, cfg.CAPELLA_FORK_VERSION)
|
||||
if not verify_voluntary_exit_signature(
|
||||
exitSignatureFork, state.genesis_validators_root, voluntary_exit,
|
||||
voluntary_exit_fork, state.genesis_validators_root, voluntary_exit,
|
||||
validator[].pubkey, signed_voluntary_exit.signature):
|
||||
return err("Exit: invalid signature")
|
||||
|
||||
|
@ -192,7 +192,7 @@ type
|
||||
waiters*: seq[BlockWaiter]
|
||||
|
||||
ValidatorRuntimeConfig* = object
|
||||
altairEpoch*: Opt[Epoch]
|
||||
forkConfig*: Opt[VCForkConfig]
|
||||
|
||||
ValidatorClient* = object
|
||||
config*: ValidatorClientConf
|
||||
@ -518,16 +518,6 @@ proc equals*(info: VCRuntimeConfig, name: string, check: DomainType): bool =
|
||||
proc equals*(info: VCRuntimeConfig, name: string, check: Epoch): bool =
|
||||
info.equals(name, uint64(check))
|
||||
|
||||
proc getOrDefault*(info: VCRuntimeConfig, name: string,
|
||||
default: uint64): uint64 =
|
||||
let numstr = info.getOrDefault(name, "missing")
|
||||
if numstr == "missing": return default
|
||||
Base10.decode(uint64, numstr).valueOr:
|
||||
return default
|
||||
|
||||
proc getOrDefault*(info: VCRuntimeConfig, name: string, default: Epoch): Epoch =
|
||||
Epoch(info.getOrDefault(name, uint64(default)))
|
||||
|
||||
proc checkConfig*(c: VCRuntimeConfig): bool =
|
||||
c.equals("MAX_VALIDATORS_PER_COMMITTEE", MAX_VALIDATORS_PER_COMMITTEE) and
|
||||
c.equals("SLOTS_PER_EPOCH", SLOTS_PER_EPOCH) and
|
||||
@ -1436,33 +1426,86 @@ func `==`*(a, b: SyncCommitteeDuty): bool =
|
||||
proc updateRuntimeConfig*(vc: ValidatorClientRef,
|
||||
node: BeaconNodeServerRef,
|
||||
info: VCRuntimeConfig): Result[void, string] =
|
||||
if not(info.hasKey("ALTAIR_FORK_EPOCH")):
|
||||
debug "Beacon node's configuration missing ALTAIR_FORK_EPOCH value",
|
||||
node = node
|
||||
var forkConfig = ? info.getConsensusForkConfig()
|
||||
|
||||
let
|
||||
res = info.getOrDefault("ALTAIR_FORK_EPOCH", FAR_FUTURE_EPOCH)
|
||||
wallEpoch = vc.beaconClock.now().slotOrZero().epoch()
|
||||
if vc.runtimeConfig.forkConfig.isNone():
|
||||
vc.runtimeConfig.forkConfig = Opt.some(forkConfig)
|
||||
else:
|
||||
template localForkConfig: untyped = vc.runtimeConfig.forkConfig.get()
|
||||
let wallEpoch = vc.beaconClock.now().slotOrZero().epoch()
|
||||
|
||||
return
|
||||
if vc.runtimeConfig.altairEpoch.get(FAR_FUTURE_EPOCH) == FAR_FUTURE_EPOCH:
|
||||
vc.runtimeConfig.altairEpoch = Opt.some(res)
|
||||
ok()
|
||||
else:
|
||||
if res == vc.runtimeConfig.altairEpoch.get():
|
||||
ok()
|
||||
proc validateForkVersionCompatibility(
|
||||
consensusFork: ConsensusFork,
|
||||
localForkVersion: Opt[Version],
|
||||
localForkEpoch: Epoch,
|
||||
forkVersion: Opt[Version]): Result[void, string] =
|
||||
if localForkVersion.isNone():
|
||||
ok() # Potentially discovered new fork, save it at end of function
|
||||
else:
|
||||
if res == FAR_FUTURE_EPOCH:
|
||||
if wallEpoch < vc.runtimeConfig.altairEpoch.get():
|
||||
debug "Beacon node must be updated before Altair activates",
|
||||
if forkVersion.isSome():
|
||||
if forkVersion.get() == localForkVersion.get():
|
||||
ok() # Already known
|
||||
else:
|
||||
err("Beacon node has conflicting " &
|
||||
consensusFork.forkVersionConfigKey() & " value")
|
||||
else:
|
||||
if wallEpoch < localForkEpoch:
|
||||
debug "Beacon node must be updated before fork activates",
|
||||
node = node,
|
||||
altairForkEpoch = vc.runtimeConfig.altairEpoch.get()
|
||||
consensusFork,
|
||||
forkEpoch = localForkEpoch
|
||||
ok()
|
||||
else:
|
||||
err("Beacon node must be updated and report correct " &
|
||||
"ALTAIR_FORK_EPOCH value")
|
||||
$consensusFork & " config value")
|
||||
|
||||
? ConsensusFork.Capella.validateForkVersionCompatibility(
|
||||
localForkConfig.capellaVersion,
|
||||
localForkConfig.capellaEpoch,
|
||||
forkConfig.capellaVersion)
|
||||
|
||||
proc validateForkEpochCompatibility(
|
||||
consensusFork: ConsensusFork,
|
||||
localForkEpoch: Epoch,
|
||||
forkEpoch: Epoch): Result[void, string] =
|
||||
if localForkEpoch == FAR_FUTURE_EPOCH:
|
||||
ok() # Potentially discovered new fork, save it at end of function
|
||||
else:
|
||||
if forkEpoch != FAR_FUTURE_EPOCH:
|
||||
if forkEpoch == localForkEpoch:
|
||||
ok() # Already known
|
||||
else:
|
||||
err("Beacon node has conflicting " &
|
||||
consensusFork.forkEpochConfigKey() & " value")
|
||||
else:
|
||||
err("Beacon node has conflicting ALTAIR_FORK_EPOCH value")
|
||||
if wallEpoch < localForkEpoch:
|
||||
debug "Beacon node must be updated before fork activates",
|
||||
node = node,
|
||||
consensusFork,
|
||||
forkEpoch = localForkEpoch
|
||||
ok()
|
||||
else:
|
||||
err("Beacon node must be updated and report correct " &
|
||||
$consensusFork & " config value")
|
||||
|
||||
? ConsensusFork.Altair.validateForkEpochCompatibility(
|
||||
localForkConfig.altairEpoch, forkConfig.altairEpoch)
|
||||
? ConsensusFork.Capella.validateForkEpochCompatibility(
|
||||
localForkConfig.capellaEpoch, forkConfig.capellaEpoch)
|
||||
? ConsensusFork.Deneb.validateForkEpochCompatibility(
|
||||
localForkConfig.denebEpoch, forkConfig.denebEpoch)
|
||||
|
||||
# Save newly discovered forks.
|
||||
if localForkConfig.altairEpoch == FAR_FUTURE_EPOCH:
|
||||
localForkConfig.altairEpoch = forkConfig.altairEpoch
|
||||
if localForkConfig.capellaVersion.isNone():
|
||||
localForkConfig.capellaVersion = forkConfig.capellaVersion
|
||||
if localForkConfig.capellaEpoch == FAR_FUTURE_EPOCH:
|
||||
localForkConfig.capellaEpoch = forkConfig.capellaEpoch
|
||||
if localForkConfig.denebEpoch == FAR_FUTURE_EPOCH:
|
||||
localForkConfig.denebEpoch = forkConfig.denebEpoch
|
||||
|
||||
ok()
|
||||
|
||||
proc `+`*(slot: Slot, epochs: Epoch): Slot =
|
||||
slot + uint64(epochs) * SLOTS_PER_EPOCH
|
||||
|
@ -5,6 +5,8 @@
|
||||
# * 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/[sets, sequtils]
|
||||
import chronicles, metrics
|
||||
import "."/[common, api, block_service, selection_proofs]
|
||||
@ -210,7 +212,8 @@ proc pollForSyncCommitteeDuties*(
|
||||
let
|
||||
vc = service.client
|
||||
indices = toSeq(vc.attachedValidators[].indices())
|
||||
epoch = max(period.start_epoch(), vc.runtimeConfig.altairEpoch.get())
|
||||
altairEpoch = vc.runtimeConfig.forkConfig.get().altairEpoch
|
||||
epoch = max(period.start_epoch(), altairEpoch)
|
||||
relevantDuties =
|
||||
block:
|
||||
var duties: seq[RestSyncCommitteeDuty]
|
||||
@ -369,8 +372,11 @@ proc pollForSyncCommitteeDuties*(service: DutiesServiceRef) {.async.} =
|
||||
let
|
||||
currentSlot = vc.getCurrentSlot().get(Slot(0))
|
||||
currentEpoch = currentSlot.epoch()
|
||||
altairEpoch = vc.runtimeConfig.altairEpoch.valueOr:
|
||||
return
|
||||
altairEpoch =
|
||||
if vc.runtimeConfig.forkConfig.isSome():
|
||||
vc.runtimeConfig.forkConfig.get().altairEpoch
|
||||
else:
|
||||
return
|
||||
|
||||
if currentEpoch < altairEpoch:
|
||||
# We are not going to poll for sync committee duties until `altairEpoch`.
|
||||
|
@ -73,6 +73,10 @@ type
|
||||
proc (pubkey: ValidatorPubKey): Opt[ValidatorAndIndex]
|
||||
{.raises: [], gcsafe.}
|
||||
|
||||
GetCapellaForkVersionFn* =
|
||||
proc (): Opt[Version] {.raises: [], gcsafe.}
|
||||
GetDenebForkEpochFn* =
|
||||
proc (): Opt[Epoch] {.raises: [], gcsafe.}
|
||||
GetForkFn* =
|
||||
proc (epoch: Epoch): Opt[Fork] {.raises: [], gcsafe.}
|
||||
GetGenesisFn* =
|
||||
@ -90,6 +94,8 @@ type
|
||||
defaultBuilderAddress*: Opt[string]
|
||||
getValidatorAndIdxFn*: ValidatorPubKeyToDataFn
|
||||
getBeaconTimeFn*: GetBeaconTimeFn
|
||||
getCapellaForkVersionFn*: GetCapellaForkVersionFn
|
||||
getDenebForkEpochFn*: GetDenebForkEpochFn
|
||||
getForkFn*: GetForkFn
|
||||
getGenesisFn*: GetGenesisFn
|
||||
|
||||
@ -122,6 +128,8 @@ func init*(T: type KeymanagerHost,
|
||||
defaultBuilderAddress: Opt[string],
|
||||
getValidatorAndIdxFn: ValidatorPubKeyToDataFn,
|
||||
getBeaconTimeFn: GetBeaconTimeFn,
|
||||
getCapellaForkVersionFn: GetCapellaForkVersionFn,
|
||||
getDenebForkEpochFn: GetDenebForkEpochFn,
|
||||
getForkFn: GetForkFn,
|
||||
getGenesisFn: GetGenesisFn): T =
|
||||
T(validatorPool: validatorPool,
|
||||
@ -135,6 +143,8 @@ func init*(T: type KeymanagerHost,
|
||||
defaultBuilderAddress: defaultBuilderAddress,
|
||||
getValidatorAndIdxFn: getValidatorAndIdxFn,
|
||||
getBeaconTimeFn: getBeaconTimeFn,
|
||||
getCapellaForkVersionFn: getCapellaForkVersionFn,
|
||||
getDenebForkEpochFn: getDenebForkEpochFn,
|
||||
getForkFn: getForkFn,
|
||||
getGenesisFn: getGenesisFn)
|
||||
|
||||
|
@ -149,6 +149,11 @@ declareCounter validator_monitor_proposer_slashing,
|
||||
declareCounter validator_monitor_attester_slashing,
|
||||
"Number of attester slashings seen", labels = ["src", "validator"]
|
||||
|
||||
declareCounter validator_monitor_block_hit,
|
||||
"Number of times a block proposed by the validator was included an epoch later", labels = ["validator"]
|
||||
declareCounter validator_monitor_block_miss,
|
||||
"Number of times the validator was expected to propose a block but no block was included", labels = ["validator"]
|
||||
|
||||
const
|
||||
total = "total" # what we use for label when using "totals" mode
|
||||
|
||||
@ -405,12 +410,15 @@ func is_active_unslashed_in_previous_epoch(status: ParticipationInfo): bool =
|
||||
ParticipationFlag.eligible in status.flags
|
||||
|
||||
proc registerEpochInfo*(
|
||||
self: var ValidatorMonitor, epoch: Epoch, info: ForkedEpochInfo,
|
||||
state: ForkyBeaconState) =
|
||||
self: var ValidatorMonitor, state: ForkyBeaconState,
|
||||
proposers: array[SLOTS_PER_EPOCH, Opt[ValidatorIndex]],
|
||||
info: ForkedEpochInfo) =
|
||||
# Register rewards, as computed during the epoch transition that lands in
|
||||
# `epoch` - the rewards will be from attestations that were created at
|
||||
# `epoch - 2`.
|
||||
|
||||
let epoch = state.slot.epoch
|
||||
|
||||
if epoch < 2 or self.monitors.len == 0:
|
||||
return
|
||||
|
||||
@ -442,6 +450,24 @@ proc registerEpochInfo*(
|
||||
# attestations.
|
||||
continue
|
||||
|
||||
# Check that block proposals are sticky an epoch later
|
||||
for i in 0..<SLOTS_PER_EPOCH:
|
||||
let slot = prev_epoch.start_slot + i
|
||||
if slot == 0:
|
||||
continue
|
||||
|
||||
if proposers[i] == Opt.some(idx):
|
||||
let hasBlock =
|
||||
# When a block is missing in a slot, the beacon root repeats
|
||||
get_block_root_at_slot(state, slot - 1) !=
|
||||
get_block_root_at_slot(state, slot)
|
||||
if hasBlock:
|
||||
validator_monitor_block_hit.inc(1, [metricId])
|
||||
info "Block proposal included", slot, validator = id
|
||||
else:
|
||||
validator_monitor_block_miss.inc(1, [metricId])
|
||||
notice "Block proposal missing", slot, validator = id
|
||||
|
||||
let
|
||||
previous_epoch_matched_source = status.is_previous_epoch_source_attester()
|
||||
previous_epoch_matched_target = status.is_previous_epoch_target_attester()
|
||||
|
@ -22,6 +22,7 @@ import
|
||||
|
||||
export
|
||||
streams, keystore, phase0, altair, tables, uri, crypto,
|
||||
signatures.voluntary_exit_signature_fork,
|
||||
rest_types, eth2_rest_serialization, rest_remote_signer_calls,
|
||||
slashing_protection
|
||||
|
||||
|
@ -19,7 +19,7 @@ const
|
||||
|
||||
versionMajor* = 24
|
||||
versionMinor* = 2
|
||||
versionBuild* = 1
|
||||
versionBuild* = 2
|
||||
|
||||
versionBlob* = "stateofus" # Single word - ends up in the default graffiti
|
||||
|
||||
|
@ -5997,7 +5997,7 @@
|
||||
"x": 0,
|
||||
"y": 111
|
||||
},
|
||||
"id": 89,
|
||||
"id": 125,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
@ -6017,37 +6017,17 @@
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS-PROXY}"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"exemplar": true,
|
||||
"expr": "sum(rate(validator_monitor_prev_epoch_on_chain_head_attester_miss_total{instance=\"${instance}\",container=\"${container}\"}[$__rate_interval]))*384",
|
||||
"interval": "384s",
|
||||
"legendFormat": "head",
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS-PROXY}"
|
||||
},
|
||||
"exemplar": true,
|
||||
"expr": "sum(rate(validator_monitor_prev_epoch_on_chain_target_attester_miss_total{instance=\"${instance}\",container=\"${container}\"}[$__rate_interval]))*384",
|
||||
"interval": "384s",
|
||||
"legendFormat": "target",
|
||||
"refId": "B"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS-PROXY}"
|
||||
},
|
||||
"exemplar": true,
|
||||
"expr": "sum(rate(validator_monitor_prev_epoch_on_chain_source_attester_miss_total{instance=\"${instance}\",container=\"${container}\"}[$__rate_interval]))*384",
|
||||
"expr": "sum(rate(validator_monitor_block_miss_total{instance=\"${instance}\",container=\"${container}\"}[$__rate_interval]))*384",
|
||||
"hide": false,
|
||||
"interval": "384s",
|
||||
"legendFormat": "source",
|
||||
"legendFormat": "miss ({{validator}})",
|
||||
"range": true,
|
||||
"refId": "C"
|
||||
}
|
||||
],
|
||||
"title": "Attestation misses",
|
||||
"title": "Block misses",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
@ -6145,6 +6125,141 @@
|
||||
"title": "Balance",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS-PROXY}"
|
||||
},
|
||||
"description": "",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"drawStyle": "bars",
|
||||
"fillOpacity": 10,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"lineInterpolation": "linear",
|
||||
"lineStyle": {
|
||||
"fill": "solid"
|
||||
},
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "never",
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"links": [],
|
||||
"mappings": [],
|
||||
"min": 0,
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "short"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "source"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "color",
|
||||
"value": {
|
||||
"fixedColor": "red",
|
||||
"mode": "fixed"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 119
|
||||
},
|
||||
"id": 89,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "8.0.4",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS-PROXY}"
|
||||
},
|
||||
"exemplar": true,
|
||||
"expr": "sum(rate(validator_monitor_prev_epoch_on_chain_head_attester_miss_total{instance=\"${instance}\",container=\"${container}\"}[$__rate_interval]))*384",
|
||||
"interval": "384s",
|
||||
"legendFormat": "head",
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS-PROXY}"
|
||||
},
|
||||
"exemplar": true,
|
||||
"expr": "sum(rate(validator_monitor_prev_epoch_on_chain_target_attester_miss_total{instance=\"${instance}\",container=\"${container}\"}[$__rate_interval]))*384",
|
||||
"interval": "384s",
|
||||
"legendFormat": "target",
|
||||
"refId": "B"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS-PROXY}"
|
||||
},
|
||||
"exemplar": true,
|
||||
"expr": "sum(rate(validator_monitor_prev_epoch_on_chain_source_attester_miss_total{instance=\"${instance}\",container=\"${container}\"}[$__rate_interval]))*384",
|
||||
"hide": false,
|
||||
"interval": "384s",
|
||||
"legendFormat": "source",
|
||||
"refId": "C"
|
||||
}
|
||||
],
|
||||
"title": "Attestation misses",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"collapsed": true,
|
||||
"datasource": {
|
||||
@ -6155,7 +6270,7 @@
|
||||
"h": 1,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 119
|
||||
"y": 127
|
||||
},
|
||||
"id": 75,
|
||||
"panels": [
|
||||
@ -6495,6 +6610,6 @@
|
||||
"timezone": "",
|
||||
"title": "NBC local testnet/sim (all nodes)",
|
||||
"uid": "pgeNfj2Wz23",
|
||||
"version": 15,
|
||||
"version": 16,
|
||||
"weekStart": ""
|
||||
}
|
||||
|
2
vendor/nim-stint
vendored
2
vendor/nim-stint
vendored
@ -1 +1 @@
|
||||
Subproject commit 711cda4456c32d3ba3c6c4524135b3453dffeb9c
|
||||
Subproject commit e639ba700cb83a6b22e5b5a1053bea2820c8b4f6
|
Loading…
x
Reference in New Issue
Block a user