Repository: ethereum/consensus-specs

Issue:
add config validity conditions
# Issue
There are many conditions that are implicit for a configuration of constants to be valid.

To name a few:
* `MIN_ATTESTATION_INCLUSION_DELAY > 0`
* `MAX_DEPOSIT >= MIN_DEPOSIT`
* `ENTRY_EXIT_DELAY >= SEED_LOOKAHEAD`

# Proposed implementation
I propose we add an accompanying document on configuration to make these conditions explicit. This will aid in testing/testnets and will help if some party is looking to deploy a different instantiation of the protocol.


Discussion:
> `ENTRY_EXIT_DELAY > SEED_LOOKAHEAD`

`>=`, no?

Also, is that even technically a requirement?

There's probably a pile of constants that need to be nonzero because we're dividing or moduloing by them; `SHARD_COUNT` and `EPOCH_LENGTH` come up as trivial examples.

There's also a bunch of divisibility requirements (eg.`SHARD_COUNT % EPOCH_LENGTH = 0` is I think a requirement at least if we want things to work out nicely without extra special case code) that we don't need to think about because everything is a power of two, but people making alternate networks might decide to turn into not being a power of two.

A bunch of stuff that gets Merkle-tree'd must be a power of two.
> `>=`, no?

yes (edited)

> Also, is that even technically a requirement?

You're right it is not technically a requirement, but might be a source of error for validators depending on implementation and/or the helpers provided in the spec. If `SEED_LOOKAHEAD > ENTRY_EXIT_DELAY`, then although the seed was know `SEED_LOOKAHEAD` slots ago, the shuffling cannot be predicted until `ENTRY_EXIT_DELAY` slots ago.
Another approach could be to have "reduced constant sets" for testing, maybe just one to start with. That way all implementers can converge on the same constant set for testing.
plus: `LATEST_RANDAO_MIXES_LENGTH > MIN_SEED_LOOKAHEAD` per #1027
Was chatting w/ @djrtwo  and this one came up: 

`ETH1_FOLLOW_DISTANCE` << 24hrs

there is effectively a "dynamic constant" between the time of the `Eth2Genesis` event and the `genesis_time`. the range of this duration is between 24 and 48 hours, based on the computation performed in the deposit contract.

the `ETH1_FOLLOW_DISTANCE` should be no smaller than the minimum of this range to ensure that proposers will be able to submit useful `Eth1Data` in the early beacon chain blocks. if this invariant does not hold, early proposers will essentially have to coordinate to prevent consensus on the initial `latest_eth1_data`
I'm working on this (finally) :)
`MIN_SLASHING_PENALTY_QUOTIENT >= WHISTLEBLOWING_REWARD_QUOTIENT` to ensure no eth is minted in `slash_validator`
ran into this one while working on some tests...

`SHARD_COUNT >= SLOTS_PER_EPOCH` is required to maintain the invariant that every validator is sampled *exactly once* every epoch to perform an attestation. the math under `get_epoch_committee_count` seems to break down if there are not enough shards

credits to @protolambda for recognizing this one

`MIN_SLASHING_PENALTY_QUOTIENT <= WHISTLEBLOWER_REWARD_QUOTIENT` to avoid printing money when slashing
"`MAX_EPOCHS_PER_CROSSLINK` should be a small constant times `SHARD_COUNT // SLOTS_PER_EPOCH`"
`SLOTS_PER_HISTORICAL_ROOT >= SLOTS_PER_EPOCH`
I created a spreadsheet of phase 0 configuration: https://docs.google.com/spreadsheets/d/1xKwwjA3QO7k7VuqomsYc_uPeOSAV4VKRGo7m6Pb5TYA/edit?usp=sharing

Comments here or on the spreadsheet are most welcome and appreciated!

Resolving patch:
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -303,14 +303,6 @@ def hardcoded_custom_type_dep_constants(cls) -> Dict[str, str]:  # TODO
         """
         raise NotImplementedError()
 
-    @classmethod
-    @abstractmethod
-    def invariant_checks(cls) -> str:
-        """
-        The invariant checks
-        """
-        raise NotImplementedError()
-
     @classmethod
     @abstractmethod
     def implement_optimizations(cls, functions: Dict[str, str]) -> Dict[str, str]:
@@ -436,10 +428,6 @@ def hardcoded_ssz_dep_constants(cls) -> Dict[str, str]:
     def hardcoded_custom_type_dep_constants(cls) -> Dict[str, str]:
         return {}
 
-    @classmethod
-    def invariant_checks(cls) -> str:
-        return ''
-
     @classmethod
     def implement_optimizations(cls, functions: Dict[str, str]) -> Dict[str, str]:
         return functions
@@ -490,13 +478,6 @@ def hardcoded_ssz_dep_constants(cls) -> Dict[str, str]:
         }
         return {**super().hardcoded_ssz_dep_constants(), **constants}
 
-    @classmethod
-    def invariant_checks(cls) -> str:
-        return '''
-assert (
-    TIMELY_HEAD_WEIGHT + TIMELY_SOURCE_WEIGHT + TIMELY_TARGET_WEIGHT + SYNC_REWARD_WEIGHT + PROPOSER_WEIGHT
-) == WEIGHT_DENOMINATOR'''
-
     @classmethod
     def implement_optimizations(cls, functions: Dict[str, str]) -> Dict[str, str]:
         if "eth2_aggregate_pubkeys" in functions:
@@ -668,7 +649,6 @@ def format_constant(name: str, vardef: VariableDefinition) -> str:
             # Since some constants are hardcoded in setup.py, the following assertions verify that the hardcoded constants are
             # as same as the spec definition.
             + ('\n\n\n' + ssz_dep_constants_verification if ssz_dep_constants_verification != '' else '')
-            + ('\n' + builder.invariant_checks() if builder.invariant_checks() != '' else '')
             + '\n'
     )
     return spec


Test patch:
diff --git a/tests/core/pyspec/eth2spec/test/altair/unittests/__init__.py b/tests/core/pyspec/eth2spec/test/altair/unittests/__init__.py
new file mode 100644
diff --git a/tests/core/pyspec/eth2spec/test/altair/unittests/test_config_invariants.py b/tests/core/pyspec/eth2spec/test/altair/unittests/test_config_invariants.py
new file mode 100644
--- /dev/null
+++ b/tests/core/pyspec/eth2spec/test/altair/unittests/test_config_invariants.py
@@ -0,0 +1,23 @@
+from eth2spec.test.context import (
+    spec_state_test,
+    with_phases,
+)
+from eth2spec.test.helpers.constants import ALTAIR
+
+
+@with_phases([ALTAIR])
+@spec_state_test
+def test_weight_denominator(spec, state):
+    assert (
+        spec.TIMELY_HEAD_WEIGHT
+        + spec.TIMELY_SOURCE_WEIGHT
+        + spec.TIMELY_TARGET_WEIGHT
+        + spec.SYNC_REWARD_WEIGHT
+        + spec.PROPOSER_WEIGHT
+    ) == spec.WEIGHT_DENOMINATOR
+
+
+@with_phases([ALTAIR])
+@spec_state_test
+def test_inactivity_score(spec, state):
+    assert spec.config.INACTIVITY_SCORE_BIAS <= spec.config.INACTIVITY_SCORE_RECOVERY_RATE
diff --git a/tests/core/pyspec/eth2spec/test/helpers/constants.py b/tests/core/pyspec/eth2spec/test/helpers/constants.py
--- a/tests/core/pyspec/eth2spec/test/helpers/constants.py
+++ b/tests/core/pyspec/eth2spec/test/helpers/constants.py
@@ -32,3 +32,9 @@
 MINIMAL = PresetBaseName('minimal')
 
 ALL_PRESETS = (MINIMAL, MAINNET)
+
+
+#
+# Number
+#
+MAX_UINT_64 = 2**64 - 1
diff --git a/tests/core/pyspec/eth2spec/test/phase0/unittests/__init__.py b/tests/core/pyspec/eth2spec/test/phase0/unittests/__init__.py
new file mode 100644
diff --git a/tests/core/pyspec/eth2spec/test/phase0/unittests/test_config_invariants.py b/tests/core/pyspec/eth2spec/test/phase0/unittests/test_config_invariants.py
new file mode 100644
--- /dev/null
+++ b/tests/core/pyspec/eth2spec/test/phase0/unittests/test_config_invariants.py
@@ -0,0 +1,74 @@
+from eth2spec.test.context import (
+    spec_state_test,
+    with_all_phases,
+    is_post_altair,
+)
+from eth2spec.test.helpers.constants import MAX_UINT_64
+
+
+def check_bound(value, lower_bound, upper_bound):
+    assert value >= lower_bound
+    assert value <= upper_bound
+
+
+@with_all_phases
+@spec_state_test
+def test_validators(spec, state):
+    check_bound(spec.VALIDATOR_REGISTRY_LIMIT, 1, MAX_UINT_64)
+    check_bound(spec.MAX_COMMITTEES_PER_SLOT, 1, MAX_UINT_64)
+    check_bound(spec.TARGET_COMMITTEE_SIZE, 1, MAX_UINT_64)
+
+    # Note: can be less if you assume stricters bounds on validator set based on total ETH supply
+    maximum_validators_per_committee = (
+        spec.VALIDATOR_REGISTRY_LIMIT
+        // spec.SLOTS_PER_EPOCH
+        // spec.MAX_COMMITTEES_PER_SLOT
+    )
+    check_bound(spec.MAX_VALIDATORS_PER_COMMITTEE, 1, maximum_validators_per_committee)
+    check_bound(spec.config.MIN_PER_EPOCH_CHURN_LIMIT, 1, spec.VALIDATOR_REGISTRY_LIMIT)
+    check_bound(spec.config.CHURN_LIMIT_QUOTIENT, 1, spec.VALIDATOR_REGISTRY_LIMIT)
+
+    check_bound(spec.config.MIN_GENESIS_ACTIVE_VALIDATOR_COUNT, spec.TARGET_COMMITTEE_SIZE, MAX_UINT_64)
+
+
+@with_all_phases
+@spec_state_test
+def test_balances(spec, state):
+    assert spec.MAX_EFFECTIVE_BALANCE % spec.EFFECTIVE_BALANCE_INCREMENT == 0
+    check_bound(spec.MIN_DEPOSIT_AMOUNT, 1, MAX_UINT_64)
+    check_bound(spec.MAX_EFFECTIVE_BALANCE, spec.MIN_DEPOSIT_AMOUNT, MAX_UINT_64)
+    check_bound(spec.MAX_EFFECTIVE_BALANCE, spec.EFFECTIVE_BALANCE_INCREMENT, MAX_UINT_64)
+
+
+@with_all_phases
+@spec_state_test
+def test_hysteresis_quotient(spec, state):
+    check_bound(spec.HYSTERESIS_QUOTIENT, 1, MAX_UINT_64)
+    check_bound(spec.HYSTERESIS_DOWNWARD_MULTIPLIER, 1, spec.HYSTERESIS_QUOTIENT)
+    check_bound(spec.HYSTERESIS_UPWARD_MULTIPLIER, spec.HYSTERESIS_QUOTIENT, MAX_UINT_64)
+
+
+@with_all_phases
+@spec_state_test
+def test_incentives(spec, state):
+    # Ensure no ETH is minted in slash_validator
+    if is_post_altair(spec):
+        assert spec.MIN_SLASHING_PENALTY_QUOTIENT_ALTAIR <= spec.WHISTLEBLOWER_REWARD_QUOTIENT
+    else:
+        assert spec.MIN_SLASHING_PENALTY_QUOTIENT <= spec.WHISTLEBLOWER_REWARD_QUOTIENT
+
+
+@with_all_phases
+@spec_state_test
+def test_time(spec, state):
+    assert spec.SLOTS_PER_EPOCH <= spec.SLOTS_PER_HISTORICAL_ROOT
+    assert spec.MIN_SEED_LOOKAHEAD < spec.MAX_SEED_LOOKAHEAD
+    assert spec.SLOTS_PER_HISTORICAL_ROOT % spec.SLOTS_PER_EPOCH == 0
+    check_bound(spec.SLOTS_PER_HISTORICAL_ROOT, spec.SLOTS_PER_EPOCH, MAX_UINT_64)
+    check_bound(spec.MIN_ATTESTATION_INCLUSION_DELAY, 1, spec.SLOTS_PER_EPOCH)
+
+
+@with_all_phases
+@spec_state_test
+def test_networking(spec, state):
+    assert spec.RANDOM_SUBNETS_PER_VALIDATOR <= spec.ATTESTATION_SUBNET_COUNT
diff --git a/tests/core/pyspec/eth2spec/test/phase0/unittests/validator/__init__.py b/tests/core/pyspec/eth2spec/test/phase0/unittests/validator/__init__.py
new file mode 100644