confirmation rule implementation WIP DEBUG DO NOT MERGE

This commit is contained in:
Etan Kissling 2023-06-14 16:43:58 +02:00
parent c27a65129b
commit 53d8c63d07
No known key found for this signature in database
GPG Key ID: B21DA824C5A3D03D
7 changed files with 218 additions and 50 deletions

View File

@ -133,7 +133,8 @@ proc init*(T: type AttestationPool, dag: ChainDAGRef,
# and then to make sure the fork choice data structure doesn't grow
# too big - getting an EpochRef can be expensive.
forkChoice.backend.process_block(
blckRef.bid, blckRef.parent.root, epochRef.checkpoints)
blckRef.bid, blckRef.parent.root,
epochRef.checkpoints, epochRef.total_active_balance)
else:
epochRef = dag.getEpochRef(blckRef, blckRef.slot.epoch, false).expect(
"Getting an EpochRef should always work for non-finalized blocks")
@ -777,12 +778,10 @@ proc getBeaconHead*(
safeBlock = pool.dag.getBlockRef(safeBlockRoot)
safeExecutionPayloadHash =
if safeBlock.isErr:
# Safe block is currently the justified block determined by fork choice.
# If finality already advanced beyond the current justified checkpoint,
# e.g., because we have selected a head that did not yet realize the cp,
# the justified block may end up not having a `BlockRef` anymore.
# Because we know that a different fork already finalized a later point,
# let's just report the finalized execution payload hash instead.
# If finality already advanced beyond the current safe block,
# the safe block may end up not having a `BlockRef` anymore.
# Because a different fork already finalized a later point,
# report the finalized execution payload hash instead.
finalizedExecutionPayloadHash
else:
pool.dag.loadExecutionBlockHash(safeBlock.get)

View File

@ -49,8 +49,10 @@ func compute_deltas(
logScope: topics = "fork_choice"
func init*(
T: type ForkChoiceBackend, checkpoints: FinalityCheckpoints): T =
T(proto_array: ProtoArray.init(checkpoints))
T: type ForkChoiceBackend,
checkpoints: FinalityCheckpoints,
total_active_balance: Gwei): T =
T(proto_array: ProtoArray.init(checkpoints, total_active_balance))
proc init*(
T: type ForkChoice, epochRef: EpochRef, blck: BlockRef): T =
@ -65,10 +67,12 @@ proc init*(
backend: ForkChoiceBackend.init(
FinalityCheckpoints(
justified: checkpoint,
finalized: checkpoint)),
finalized: checkpoint),
epochRef.total_active_balance),
checkpoints: Checkpoints(
justified: BalanceCheckpoint(
checkpoint: checkpoint,
total_active_balance: epochRef.total_active_balance,
balances: epochRef.effective_balances),
finalized: checkpoint,
best_justified: checkpoint))
@ -94,6 +98,7 @@ proc update_justified(
store = self.justified.checkpoint, state = justified
self.justified = BalanceCheckpoint(
checkpoint: Checkpoint(root: blck.root, epoch: epochRef.epoch),
total_active_balance: epochRef.total_active_balance,
balances: epochRef.effective_balances)
proc update_justified(
@ -256,8 +261,10 @@ func process_block*(self: var ForkChoiceBackend,
bid: BlockId,
parent_root: Eth2Digest,
checkpoints: FinalityCheckpoints,
total_active_balance: Gwei,
unrealized = none(FinalityCheckpoints)): FcResult[void] =
self.proto_array.onBlock(bid, parent_root, checkpoints, unrealized)
self.proto_array.onBlock(
bid, parent_root, checkpoints, total_active_balance, unrealized)
proc process_block*(self: var ForkChoice,
dag: ChainDAGRef,
@ -304,24 +311,28 @@ proc process_block*(self: var ForkChoice,
blck = shortLog(blckRef), checkpoints = epochRef.checkpoints, unrealized
? update_checkpoints(self.checkpoints, dag, unrealized)
? process_block(
self.backend, blckRef.bid, blck.parent_root, unrealized)
self.backend, blckRef.bid, blck.parent_root,
unrealized, epochRef.total_active_balance)
else:
? process_block(
self.backend, blckRef.bid, blck.parent_root,
epochRef.checkpoints, some unrealized) # Realized in `on_tick`
epochRef.checkpoints, epochRef.total_active_balance,
some unrealized) # Realized in `on_tick`
else:
? process_block(
self.backend, blckRef.bid, blck.parent_root, epochRef.checkpoints)
self.backend, blckRef.bid, blck.parent_root,
epochRef.checkpoints, epochRef.total_active_balance)
ok()
func find_head*(
func find_head(
self: var ForkChoiceBackend,
current_epoch: Epoch,
current_slot: Slot,
checkpoints: FinalityCheckpoints,
justified_total_active_balance: Gwei,
justified_state_balances: seq[Gwei],
proposer_boost_root: Eth2Digest
): FcResult[Eth2Digest] =
): FcResult[tuple[headBlockRoot, safeBlockRoot: Eth2Digest]] =
## Returns the new blockchain head
# Compute deltas with previous call
@ -336,19 +347,21 @@ func find_head*(
# Apply score changes
? self.proto_array.applyScoreChanges(
deltas, current_epoch, checkpoints,
justified_state_balances, proposer_boost_root)
deltas, current_slot, checkpoints,
justified_total_active_balance, proposer_boost_root)
self.balances = justified_state_balances
# Find the best block
var new_head{.noinit.}: Eth2Digest
? self.proto_array.findHead(new_head, checkpoints.justified.root)
var
new_head {.noinit.}: Eth2Digest
confirmed {.noinit.}: Eth2Digest
? self.proto_array.findHead(new_head, confirmed, checkpoints.justified.root)
trace "Fork choice requested",
checkpoints, fork_choice_head = shortLog(new_head)
return ok(new_head)
ok (headBlockRoot: new_head, safeBlockRoot: confirmed)
# https://github.com/ethereum/consensus-specs/blob/v1.4.0-alpha.3/specs/phase0/fork-choice.md#get_head
proc get_head*(self: var ForkChoice,
@ -356,13 +369,16 @@ proc get_head*(self: var ForkChoice,
wallTime: BeaconTime): FcResult[Eth2Digest] =
? self.update_time(dag, wallTime)
self.backend.find_head(
self.checkpoints.time.slotOrZero.epoch,
let res = ? self.backend.find_head(
self.checkpoints.time.slotOrZero,
FinalityCheckpoints(
justified: self.checkpoints.justified.checkpoint,
finalized: self.checkpoints.finalized),
self.checkpoints.justified.total_active_balance,
self.checkpoints.justified.balances,
self.checkpoints.proposer_boost_root)
self.safeBlockRoot = res.safeBlockRoot
ok res.headBlockRoot
# https://github.com/ethereum/consensus-specs/blob/v1.4.0-alpha.3/fork_choice/safe-block.md#get_safe_beacon_block_root
func get_safe_beacon_block_root*(self: ForkChoice): Eth2Digest =

View File

@ -88,8 +88,9 @@ type
## Subtracted from logical index to get the physical index
ProtoArray* = object
currentEpoch*: Epoch
currentSlot*: Slot
checkpoints*: FinalityCheckpoints
justifiedTotalActiveBalance*: Gwei
nodes*: ProtoNodes
indices*: Table[Eth2Digest, Index]
currentEpochTips*: Table[Index, FinalityCheckpoints]
@ -100,6 +101,7 @@ type
bid*: BlockId
parent*: Option[Index]
checkpoints*: FinalityCheckpoints
totalActiveBalance*: Gwei
weight*: int64
invalid*: bool
bestChild*: Option[Index]
@ -107,6 +109,7 @@ type
BalanceCheckpoint* = object
checkpoint*: Checkpoint
total_active_balance*: Gwei
balances*: seq[Gwei]
Checkpoints* = object
@ -139,6 +142,7 @@ type
ForkChoice* = object
backend*: ForkChoiceBackend
checkpoints*: Checkpoints
safeBlockRoot*: Eth2Digest
queuedAttestations*: seq[QueuedAttestation]
func shortLog*(vote: VoteTracker): auto =

View File

@ -72,7 +72,150 @@ func add(nodes: var ProtoNodes, node: ProtoNode) =
nodes.buf.add node
func isPreviousEpochJustified(self: ProtoArray): bool =
self.checkpoints.justified.epoch + 1 == self.currentEpoch
self.checkpoints.justified.epoch + 1 == self.currentSlot.epoch
func parentNode(self: ProtoArray, node: ProtoNode): Option[ProtoNode] =
if node.parent.isSome:
self.nodes[node.parent.unsafeGet]
else:
none(ProtoNode)
# https://github.com/ethereum/consensus-specs/blob/v1.4.0-alpha.3/specs/phase0/fork-choice.md#get_checkpoint_block
func get_checkpoint_block(
self: ProtoArray, node: ProtoNode, epoch: Epoch): Option[ProtoNode] =
## Compute the checkpoint block for epoch ``epoch`` in the chain of ``node``
let slot = epoch.start_slot
var ancestor = some node
while ancestor.isSome and ancestor.unsafeGet.bid.slot > slot:
if ancestor.unsafeGet.parent.isSome:
ancestor = self.nodes[ancestor.unsafeGet.parent.unsafeGet]
else:
ancestor.reset()
ancestor
# https://github.com/ethereum/consensus-specs/blob/17d528a8ab40d765a7f3f765f370181dc5c795c1/specs/phase0/fork-choice.md#get_proposer_score
func get_proposer_score(self: ProtoArray): Gwei =
let committeeWeight = self.justifiedTotalActiveBalance div SLOTS_PER_EPOCH
(committeeWeight * PROPOSER_SCORE_BOOST) div 100
# Confirmation rule
# ----------------------------------------------------------------------
# https://github.com/ethereum/consensus-specs/blob/17d528a8ab40d765a7f3f765f370181dc5c795c1/fork_choice/confirmation-rule.md#get_committee_weight_between_slots
func get_committee_weight_between_slots(
self: ProtoArray, slots: Slice[Slot]): Gwei =
## Returns the total weight of committees between ``slots``.
## Uses the justified state to compute committee weights.
# If an entire epoch is covered by the range, return the total active balance
let
startEpoch = slots.a.epoch
endEpoch = slots.b.epoch
if endEpoch > startEpoch + 1:
return self.justifiedTotalActiveBalance
let
committeeWeight = self.justifiedTotalActiveBalance div SLOTS_PER_EPOCH
numCommittees =
if startEpoch == endEpoch:
slots.len.float
else:
# A range that spans an epoch boundary, but does not span any full epoch
# needs pro-rata calculation
let
# First, calculate the number of committees in the current epoch
numSlotsInCurrentEpoch =
slots.b.since_epoch_start + 1
# Next, calculate the number of slots remaining in the current epoch
remainingSlotsInCurrentEpoch =
SLOTS_PER_EPOCH - numSlotsInCurrentEpoch
# Then, calculate the number of slots in the previous epoch
numSlotsInPreviousEpoch =
SLOTS_PER_EPOCH - slots.a.since_epoch_start
# Each committee from the previous epoch only contributes a
# pro-rated weight
multiplier =
remainingSlotsInCurrentEpoch.float / SLOTS_PER_EPOCH.float
numSlotsInCurrentEpoch.float +
numSlotsInPreviousEpoch.float * multiplier
(numCommittees * committeeWeight.float).Gwei
# https://github.com/ethereum/consensus-specs/blob/17d528a8ab40d765a7f3f765f370181dc5c795c1/fork_choice/confirmation-rule.md#is_one_confirmed
func is_one_confirmed(self: ProtoArray, node, parentNode: ProtoNode): bool =
let
support = node.weight.float
maximumSupport = self.get_committee_weight_between_slots(
parentNode.bid.slot + 1 .. self.currentSlot).float
proposerScore = self.get_proposer_score().float
(support / maximumSupport) >
0.5 * proposerScore / maximumSupport +
static((50 + confirmation_byzantine_threshold).float / 100.0)
# https://github.com/ethereum/consensus-specs/blob/17d528a8ab40d765a7f3f765f370181dc5c795c1/fork_choice/confirmation-rule.md#is_lmd_confirmed
func is_lmd_confirmed(self: ProtoArray, node: ProtoNode): bool =
return
if node.bid.root == self.checkpoints.finalized.root:
true
elif node.bid.slot <= self.checkpoints.finalized.epoch.start_slot:
# This block is not in the finalized chain.
false
else:
# Check is_one_confirmed for this block and is_lmd_confirmed for
# the preceding chain.
let parentNode = self.parentNode(node)
if parentNode.isSome:
self.is_one_confirmed(node, parentNode.unsafeGet) and
self.is_lmd_confirmed(parentNode.unsafeGet)
else:
false
# https://github.com/ethereum/consensus-specs/blob/17d528a8ab40d765a7f3f765f370181dc5c795c1/fork_choice/confirmation-rule.md#get_remaining_weight_in_epoch
func get_remaining_weight_in_epoch(self: ProtoArray): Gwei =
## Returns the total weight of votes for this epoch from future committees
## after the current slot
let firstSlotNextEpoch = (self.currentSlot.epoch + 1).start_slot
self.get_committee_weight_between_slots(
self.currentSlot + 1 ..< firstSlotNextEpoch)
# https://github.com/ethereum/consensus-specs/blob/17d528a8ab40d765a7f3f765f370181dc5c795c1/fork_choice/confirmation-rule.md#is_ffg_confirmed
func is_ffg_confirmed(self: ProtoArray, node: ProtoNode): bool =
let currentEpoch = self.currentSlot.epoch
doAssert node.bid.slot.epoch == currentEpoch
let checkpointNode = self.get_checkpoint_block(node, currentEpoch)
if checkpointNode.isNone:
return false
let
remainingFfgWeight = self.get_remaining_weight_in_epoch()
totalActiveBalance = checkpointNode.unsafeGet.totalActiveBalance
currentWeightInEpoch =
# https://github.com/ethereum/consensus-specs/blob/17d528a8ab40d765a7f3f765f370181dc5c795c1/fork_choice/confirmation-rule.md#is_confirmed
func is_confirmed(self: ProtoArray, node: ProtoNode): bool =
# This function is only applicable to current epoch blocks
let currentEpoch = self.currentSlot.epoch
doAssert node.bid.slot.epoch == currentEpoch
node.checkpoints.justified.epoch + 1 == currentEpoch and
self.is_lmd_confirmed(node) and self.is_ffg_confirmed(node)
# https://github.com/ethereum/consensus-specs/blob/17d528a8ab40d765a7f3f765f370181dc5c795c1/fork_choice/confirmation-rule.md#find_confirmed_block
func find_confirmed_block(self: ProtoArray, node: ProtoNode): Eth2Digest =
let currentEpoch = self.currentSlot.epoch
return
if node.bid.slot.epoch != currentEpoch:
self.checkpoints.finalized.root
elif self.is_confirmed(node):
node.bid.root
else:
let parentNode = self.parentNode(node)
if parentNode.isSome:
self.find_confirmed_block(parentNode.unsafeGet)
else:
self.checkpoints.finalized.root
# Forward declarations
# ----------------------------------------------------------------------
@ -90,13 +233,16 @@ func nodeLeadsToViableHead(
# ----------------------------------------------------------------------
func init*(
T: type ProtoArray, checkpoints: FinalityCheckpoints): T =
T: type ProtoArray,
checkpoints: FinalityCheckpoints,
totalActiveBalance: Gwei): T =
let node = ProtoNode(
bid: BlockId(
slot: checkpoints.finalized.epoch.start_slot,
root: checkpoints.finalized.root),
parent: none(int),
checkpoints: checkpoints,
totalActiveBalance: totalActiveBalance,
weight: 0,
invalid: false,
bestChild: none(int),
@ -123,19 +269,11 @@ iterator realizePendingCheckpoints*(
# Reset tip tracking for new epoch
self.currentEpochTips.clear()
# https://github.com/ethereum/consensus-specs/blob/v1.3.0/specs/phase0/fork-choice.md#get_weight
func calculateProposerBoost(validatorBalances: openArray[Gwei]): uint64 =
var total_balance: uint64
for balance in validatorBalances:
total_balance += balance
let committee_weight = total_balance div SLOTS_PER_EPOCH
(committee_weight * PROPOSER_SCORE_BOOST) div 100
func applyScoreChanges*(self: var ProtoArray,
deltas: var openArray[Delta],
currentEpoch: Epoch,
currentSlot: Slot,
checkpoints: FinalityCheckpoints,
newBalances: openArray[Gwei],
justifiedTotalActiveBalance: Gwei,
proposerBoostRoot: Eth2Digest): FcResult[void] =
## Iterate backwards through the array, touching all nodes and their parents
## and potentially the best-child of each parent.
@ -158,8 +296,9 @@ func applyScoreChanges*(self: var ProtoArray,
deltasLen: deltas.len,
indicesLen: self.indices.len)
self.currentEpoch = currentEpoch
self.currentSlot = currentSlot
self.checkpoints = checkpoints
self.justifiedTotalActiveBalance = justifiedTotalActiveBalance
## Alias
# This cannot raise the IndexError exception, how to tell compiler?
@ -192,7 +331,7 @@ func applyScoreChanges*(self: var ProtoArray,
#
# https://github.com/ethereum/consensus-specs/blob/v1.4.0-alpha.3/specs/phase0/fork-choice.md#get_weight
if (not proposerBoostRoot.isZero) and proposerBoostRoot == node.bid.root:
proposerBoostScore = calculateProposerBoost(newBalances)
proposerBoostScore = self.get_proposer_score()
if nodeDelta >= 0 and
high(Delta) - nodeDelta < proposerBoostScore.int64:
return err ForkChoiceError(
@ -268,6 +407,7 @@ func onBlock*(self: var ProtoArray,
bid: BlockId,
parent: Eth2Digest,
checkpoints: FinalityCheckpoints,
totalActiveBalance: Gwei,
unrealized = none(FinalityCheckpoints)): FcResult[void] =
## Register a block with the fork choice
## A block `hasParentInForkChoice` may be false
@ -296,6 +436,7 @@ func onBlock*(self: var ProtoArray,
bid: bid,
parent: some(parentIdx),
checkpoints: checkpoints,
totalActiveBalance: totalActiveBalance,
weight: 0,
invalid: false,
bestChild: none(int),
@ -314,6 +455,7 @@ func onBlock*(self: var ProtoArray,
func findHead*(self: var ProtoArray,
head: var Eth2Digest,
confirmed: var Eth2Digest,
justifiedRoot: Eth2Digest): FcResult[void] =
## Follows the best-descendant links to find the best-block (i.e. head-block)
##
@ -352,6 +494,7 @@ func findHead*(self: var ProtoArray,
headCheckpoints: justifiedNode.get().checkpoints)
head = bestNode.get().bid.root
confirmed = self.find_confirmed_block(bestNode.get())
ok()
func prune*(
@ -534,27 +677,23 @@ func nodeIsViableForHead(
# If the previous epoch is justified, the block should be pulled-up.
# In this case, check that unrealized justification is higher than the store
# and that the voting source is not more than two epochs ago
let currentEpoch = self.currentSlot.epoch
if not correctJustified and self.isPreviousEpochJustified and
node.bid.slot.epoch == self.currentEpoch:
node.bid.slot.epoch == currentEpoch:
let unrealized =
self.currentEpochTips.getOrDefault(nodeIdx, node.checkpoints)
correctJustified =
unrealized.justified.epoch >= self.checkpoints.justified.epoch and
node.checkpoints.justified.epoch + 2 >= self.currentEpoch
node.checkpoints.justified.epoch + 2 >= currentEpoch
return
if not correctJustified:
false
elif self.checkpoints.finalized.epoch == GENESIS_EPOCH:
true
else:
let finalizedSlot = self.checkpoints.finalized.epoch.start_slot
var ancestor = some node
while ancestor.isSome and ancestor.unsafeGet.bid.slot > finalizedSlot:
if ancestor.unsafeGet.parent.isSome:
ancestor = self.nodes[ancestor.unsafeGet.parent.unsafeGet]
else:
ancestor.reset()
let ancestor = self.get_checkpoint_block(
node, self.checkpoints.finalized.epoch)
if ancestor.isSome:
ancestor.unsafeGet.bid.root == self.checkpoints.finalized.root
else:

View File

@ -185,6 +185,10 @@ proc installConfigApiHandlers*(router: var RestRouter, node: BeaconNode) =
Base10.toString(cfg.CHURN_LIMIT_QUOTIENT),
PROPOSER_SCORE_BOOST:
Base10.toString(PROPOSER_SCORE_BOOST),
confirmation_byzantine_threshold:
Base10.toString(confirmation_byzantine_threshold),
confirmation_slashing_threshold:
Base10.toString(confirmation_slashing_threshold),
DEPOSIT_CHAIN_ID:
Base10.toString(cfg.DEPOSIT_CHAIN_ID),
DEPOSIT_NETWORK_ID:

View File

@ -48,5 +48,11 @@ const
# https://github.com/ethereum/consensus-specs/blob/v1.3.0/specs/phase0/fork-choice.md#configuration
PROPOSER_SCORE_BOOST*: uint64 = 40
# https://github.com/ethereum/consensus-specs/blob/17d528a8ab40d765a7f3f765f370181dc5c795c1/fork_choice/confirmation-rule.md#confirmation-rule
confirmation_byzantine_threshold*: uint64 = 33
## the maximum percentage of Byzantine validators among the validator set
confirmation_slashing_threshold*: uint64 = confirmation_byzantine_threshold
## the maximum percentage of slashings among the validator set
# https://github.com/ethereum/consensus-specs/blob/v1.3.0/specs/phase0/validator.md#misc
ATTESTATION_SUBNET_COUNT*: uint64 = 64

View File

@ -2802,7 +2802,7 @@
"response": {
"status": {"operator": "equals", "value": "200"},
"headers": [{"key": "Content-Type", "value": "application/json", "operator": "equals"}],
"body": [{"operator": "jstructcmps", "start": ["data"], "value": {"MAX_COMMITTEES_PER_SLOT":"","TARGET_COMMITTEE_SIZE":"","MAX_VALIDATORS_PER_COMMITTEE":"","SHUFFLE_ROUND_COUNT":"","HYSTERESIS_QUOTIENT":"","HYSTERESIS_DOWNWARD_MULTIPLIER":"","HYSTERESIS_UPWARD_MULTIPLIER":"","MIN_DEPOSIT_AMOUNT":"","MAX_EFFECTIVE_BALANCE":"","EFFECTIVE_BALANCE_INCREMENT":"","MIN_ATTESTATION_INCLUSION_DELAY":"","SLOTS_PER_EPOCH":"","MIN_SEED_LOOKAHEAD":"","MAX_SEED_LOOKAHEAD":"","EPOCHS_PER_ETH1_VOTING_PERIOD":"","SLOTS_PER_HISTORICAL_ROOT":"","MIN_EPOCHS_TO_INACTIVITY_PENALTY":"","EPOCHS_PER_HISTORICAL_VECTOR":"","EPOCHS_PER_SLASHINGS_VECTOR":"","HISTORICAL_ROOTS_LIMIT":"","VALIDATOR_REGISTRY_LIMIT":"","BASE_REWARD_FACTOR":"","WHISTLEBLOWER_REWARD_QUOTIENT":"","PROPOSER_REWARD_QUOTIENT":"","INACTIVITY_PENALTY_QUOTIENT":"","MIN_SLASHING_PENALTY_QUOTIENT":"","PROPORTIONAL_SLASHING_MULTIPLIER":"","MAX_PROPOSER_SLASHINGS":"","MAX_ATTESTER_SLASHINGS":"","MAX_ATTESTATIONS":"","MAX_DEPOSITS":"","MAX_VOLUNTARY_EXITS":"","INACTIVITY_PENALTY_QUOTIENT_ALTAIR":"","MIN_SLASHING_PENALTY_QUOTIENT_ALTAIR":"","PROPORTIONAL_SLASHING_MULTIPLIER_ALTAIR":"","SYNC_COMMITTEE_SIZE":"","EPOCHS_PER_SYNC_COMMITTEE_PERIOD":"","MIN_SYNC_COMMITTEE_PARTICIPANTS":"","UPDATE_TIMEOUT":"","INACTIVITY_PENALTY_QUOTIENT_BELLATRIX":"","MIN_SLASHING_PENALTY_QUOTIENT_BELLATRIX":"","PROPORTIONAL_SLASHING_MULTIPLIER_BELLATRIX":"","MAX_BYTES_PER_TRANSACTION":"","MAX_TRANSACTIONS_PER_PAYLOAD":"","BYTES_PER_LOGS_BLOOM":"","MAX_EXTRA_DATA_BYTES":"","MAX_BLS_TO_EXECUTION_CHANGES":"","MAX_WITHDRAWALS_PER_PAYLOAD":"","MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP":"","PRESET_BASE":"","CONFIG_NAME":"","TERMINAL_TOTAL_DIFFICULTY":"","TERMINAL_BLOCK_HASH":"","TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH":"","MIN_GENESIS_ACTIVE_VALIDATOR_COUNT":"","MIN_GENESIS_TIME":"","GENESIS_FORK_VERSION":"","GENESIS_DELAY":"","ALTAIR_FORK_VERSION":"","ALTAIR_FORK_EPOCH":"","BELLATRIX_FORK_VERSION":"","BELLATRIX_FORK_EPOCH":"","CAPELLA_FORK_VERSION":"","CAPELLA_FORK_EPOCH":"","DENEB_FORK_VERSION":"","DENEB_FORK_EPOCH":"","SECONDS_PER_SLOT":"","SECONDS_PER_ETH1_BLOCK":"","MIN_VALIDATOR_WITHDRAWABILITY_DELAY":"","SHARD_COMMITTEE_PERIOD":"","ETH1_FOLLOW_DISTANCE":"","INACTIVITY_SCORE_BIAS":"","INACTIVITY_SCORE_RECOVERY_RATE":"","EJECTION_BALANCE":"","MIN_PER_EPOCH_CHURN_LIMIT":"","CHURN_LIMIT_QUOTIENT":"","PROPOSER_SCORE_BOOST":"","DEPOSIT_CHAIN_ID":"","DEPOSIT_NETWORK_ID":"","DEPOSIT_CONTRACT_ADDRESS":"","BLS_WITHDRAWAL_PREFIX":"","ETH1_ADDRESS_WITHDRAWAL_PREFIX":"","DOMAIN_BEACON_PROPOSER":"","DOMAIN_BEACON_ATTESTER":"","DOMAIN_RANDAO":"","DOMAIN_DEPOSIT":"","DOMAIN_VOLUNTARY_EXIT":"","DOMAIN_SELECTION_PROOF":"","DOMAIN_AGGREGATE_AND_PROOF":"","TIMELY_SOURCE_FLAG_INDEX":"","TIMELY_TARGET_FLAG_INDEX":"","TIMELY_HEAD_FLAG_INDEX":"","TIMELY_SOURCE_WEIGHT":"","TIMELY_TARGET_WEIGHT":"","TIMELY_HEAD_WEIGHT":"","SYNC_REWARD_WEIGHT":"","PROPOSER_WEIGHT":"","WEIGHT_DENOMINATOR":"","DOMAIN_SYNC_COMMITTEE":"","DOMAIN_SYNC_COMMITTEE_SELECTION_PROOF":"","DOMAIN_CONTRIBUTION_AND_PROOF":"","DOMAIN_BLS_TO_EXECUTION_CHANGE":"","TARGET_AGGREGATORS_PER_COMMITTEE":"","RANDOM_SUBNETS_PER_VALIDATOR":"","EPOCHS_PER_RANDOM_SUBNET_SUBSCRIPTION":"","ATTESTATION_SUBNET_COUNT":"","TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE":"","SYNC_COMMITTEE_SUBNET_COUNT":""}}]
"body": [{"operator": "jstructcmps", "start": ["data"], "value": {"MAX_COMMITTEES_PER_SLOT":"","TARGET_COMMITTEE_SIZE":"","MAX_VALIDATORS_PER_COMMITTEE":"","SHUFFLE_ROUND_COUNT":"","HYSTERESIS_QUOTIENT":"","HYSTERESIS_DOWNWARD_MULTIPLIER":"","HYSTERESIS_UPWARD_MULTIPLIER":"","MIN_DEPOSIT_AMOUNT":"","MAX_EFFECTIVE_BALANCE":"","EFFECTIVE_BALANCE_INCREMENT":"","MIN_ATTESTATION_INCLUSION_DELAY":"","SLOTS_PER_EPOCH":"","MIN_SEED_LOOKAHEAD":"","MAX_SEED_LOOKAHEAD":"","EPOCHS_PER_ETH1_VOTING_PERIOD":"","SLOTS_PER_HISTORICAL_ROOT":"","MIN_EPOCHS_TO_INACTIVITY_PENALTY":"","EPOCHS_PER_HISTORICAL_VECTOR":"","EPOCHS_PER_SLASHINGS_VECTOR":"","HISTORICAL_ROOTS_LIMIT":"","VALIDATOR_REGISTRY_LIMIT":"","BASE_REWARD_FACTOR":"","WHISTLEBLOWER_REWARD_QUOTIENT":"","PROPOSER_REWARD_QUOTIENT":"","INACTIVITY_PENALTY_QUOTIENT":"","MIN_SLASHING_PENALTY_QUOTIENT":"","PROPORTIONAL_SLASHING_MULTIPLIER":"","MAX_PROPOSER_SLASHINGS":"","MAX_ATTESTER_SLASHINGS":"","MAX_ATTESTATIONS":"","MAX_DEPOSITS":"","MAX_VOLUNTARY_EXITS":"","INACTIVITY_PENALTY_QUOTIENT_ALTAIR":"","MIN_SLASHING_PENALTY_QUOTIENT_ALTAIR":"","PROPORTIONAL_SLASHING_MULTIPLIER_ALTAIR":"","SYNC_COMMITTEE_SIZE":"","EPOCHS_PER_SYNC_COMMITTEE_PERIOD":"","MIN_SYNC_COMMITTEE_PARTICIPANTS":"","UPDATE_TIMEOUT":"","INACTIVITY_PENALTY_QUOTIENT_BELLATRIX":"","MIN_SLASHING_PENALTY_QUOTIENT_BELLATRIX":"","PROPORTIONAL_SLASHING_MULTIPLIER_BELLATRIX":"","MAX_BYTES_PER_TRANSACTION":"","MAX_TRANSACTIONS_PER_PAYLOAD":"","BYTES_PER_LOGS_BLOOM":"","MAX_EXTRA_DATA_BYTES":"","MAX_BLS_TO_EXECUTION_CHANGES":"","MAX_WITHDRAWALS_PER_PAYLOAD":"","MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP":"","PRESET_BASE":"","CONFIG_NAME":"","TERMINAL_TOTAL_DIFFICULTY":"","TERMINAL_BLOCK_HASH":"","TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH":"","MIN_GENESIS_ACTIVE_VALIDATOR_COUNT":"","MIN_GENESIS_TIME":"","GENESIS_FORK_VERSION":"","GENESIS_DELAY":"","ALTAIR_FORK_VERSION":"","ALTAIR_FORK_EPOCH":"","BELLATRIX_FORK_VERSION":"","BELLATRIX_FORK_EPOCH":"","CAPELLA_FORK_VERSION":"","CAPELLA_FORK_EPOCH":"","DENEB_FORK_VERSION":"","DENEB_FORK_EPOCH":"","SECONDS_PER_SLOT":"","SECONDS_PER_ETH1_BLOCK":"","MIN_VALIDATOR_WITHDRAWABILITY_DELAY":"","SHARD_COMMITTEE_PERIOD":"","ETH1_FOLLOW_DISTANCE":"","INACTIVITY_SCORE_BIAS":"","INACTIVITY_SCORE_RECOVERY_RATE":"","EJECTION_BALANCE":"","MIN_PER_EPOCH_CHURN_LIMIT":"","CHURN_LIMIT_QUOTIENT":"","PROPOSER_SCORE_BOOST":"","confirmation_byzantine_threshold":"","confirmation_slashing_threshold":"","DEPOSIT_CHAIN_ID":"","DEPOSIT_NETWORK_ID":"","DEPOSIT_CONTRACT_ADDRESS":"","BLS_WITHDRAWAL_PREFIX":"","ETH1_ADDRESS_WITHDRAWAL_PREFIX":"","DOMAIN_BEACON_PROPOSER":"","DOMAIN_BEACON_ATTESTER":"","DOMAIN_RANDAO":"","DOMAIN_DEPOSIT":"","DOMAIN_VOLUNTARY_EXIT":"","DOMAIN_SELECTION_PROOF":"","DOMAIN_AGGREGATE_AND_PROOF":"","TIMELY_SOURCE_FLAG_INDEX":"","TIMELY_TARGET_FLAG_INDEX":"","TIMELY_HEAD_FLAG_INDEX":"","TIMELY_SOURCE_WEIGHT":"","TIMELY_TARGET_WEIGHT":"","TIMELY_HEAD_WEIGHT":"","SYNC_REWARD_WEIGHT":"","PROPOSER_WEIGHT":"","WEIGHT_DENOMINATOR":"","DOMAIN_SYNC_COMMITTEE":"","DOMAIN_SYNC_COMMITTEE_SELECTION_PROOF":"","DOMAIN_CONTRIBUTION_AND_PROOF":"","DOMAIN_BLS_TO_EXECUTION_CHANGE":"","TARGET_AGGREGATORS_PER_COMMITTEE":"","RANDOM_SUBNETS_PER_VALIDATOR":"","EPOCHS_PER_RANDOM_SUBNET_SUBSCRIPTION":"","ATTESTATION_SUBNET_COUNT":"","TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE":"","SYNC_COMMITTEE_SUBNET_COUNT":""}}]
}
},
{