From 1938379bcdd42edddbfec2b7d1ecd0398b25ff01 Mon Sep 17 00:00:00 2001 From: Mamy Ratsimbazafy Date: Fri, 22 Nov 2019 20:56:39 +0100 Subject: [PATCH] Automated beacon constant checks (#583) * Fix SSZ bitlist deserialization super silly bug * Add an automated sanity checks of the beacon chain constants * Remove SSZ consensus skipping procs [skip ci] * Add phase 1 domains * Fix mainnet constants * Add missing phase 1 constants on minimal (they are not needed somehow on mainnet) * Rebase artifact: constants were defined twice --- beacon_chain/spec/datatypes.nim | 17 +++ beacon_chain/spec/presets/mainnet.nim | 39 ++++-- beacon_chain/spec/presets/minimal.nim | 48 +++++-- tests/all_tests.nim | 3 + .../test_fixture_const_sanity_check.nim | 121 ++++++++++++++++++ .../test_fixture_ssz_consensus_objects.nim | 52 +------- 6 files changed, 214 insertions(+), 66 deletions(-) create mode 100644 tests/official/test_fixture_const_sanity_check.nim diff --git a/beacon_chain/spec/datatypes.nim b/beacon_chain/spec/datatypes.nim index d5c6989be..5d92ee538 100644 --- a/beacon_chain/spec/datatypes.nim +++ b/beacon_chain/spec/datatypes.nim @@ -72,6 +72,23 @@ template maxSize*(n: int) {.pragma.} type Bytes = seq[byte] + # Domains + # --------------------------------------------------------------- + # https://github.com/ethereum/eth2.0-specs/blob/v0.9.2/specs/core/0_beacon-chain.md#domain-types + DomainType* {.pure.} = enum + DOMAIN_BEACON_PROPOSER = 0 + DOMAIN_BEACON_ATTESTER = 1 + DOMAIN_RANDAO = 2 + DOMAIN_DEPOSIT = 3 + DOMAIN_VOLUNTARY_EXIT = 4 + # Phase 1 - Custody game + # https://github.com/ethereum/eth2.0-specs/blob/v0.9.2/specs/core/1_custody-game.md#signature-domain-types + DOMAIN_CUSTODY_BIT_CHALLENGE = 6 + # Phase 1 - Sharding + # https://github.com/ethereum/eth2.0-specs/blob/v0.9.2/specs/core/1_shard-data-chains.md#signature-domain-types + DOMAIN_SHARD_PROPOSER = 128 + DOMAIN_SHARD_ATTESTER = 129 + # https://github.com/nim-lang/Nim/issues/574 and be consistent across # 32-bit and 64-bit word platforms. # TODO VALIDATOR_REGISTRY_LIMIT is 1 shl 40 in 0.8.3, and diff --git a/beacon_chain/spec/presets/mainnet.nim b/beacon_chain/spec/presets/mainnet.nim index 472406100..42bd47c22 100644 --- a/beacon_chain/spec/presets/mainnet.nim +++ b/beacon_chain/spec/presets/mainnet.nim @@ -151,13 +151,34 @@ const MAX_DEPOSITS* = 2^4 MAX_VOLUNTARY_EXITS* = 2^4 -type - # Domains + # Fork choice # --------------------------------------------------------------- - # https://github.com/ethereum/eth2.0-specs/blob/v0.9.2/configs/mainnet.yaml#L138 - DomainType* {.pure.} = enum - DOMAIN_BEACON_PROPOSER = 0 - DOMAIN_BEACON_ATTESTER = 1 - DOMAIN_RANDAO = 2 - DOMAIN_DEPOSIT = 3 - DOMAIN_VOLUNTARY_EXIT = 4 + # https://github.com/ethereum/eth2.0-specs/blob/v0.9.2/specs/core/0_fork-choice.md#configuration + SAFE_SLOTS_TO_UPDATE_JUSTIFIED* = 8 # 96 seconds + + # Validators + # --------------------------------------------------------------- + # https://github.com/ethereum/eth2.0-specs/blob/v0.9.2/specs/validator/0_beacon-chain-validator.md#misc + ETH1_FOLLOW_DISTANCE* = 1024 # blocks ~ 4 hours + TARGET_AGGREGATORS_PER_COMMITTEE* = 16 # validators + RANDOM_SUBNETS_PER_VALIDATOR* = 1 # subnet + EPOCHS_PER_RANDOM_SUBNET_SUBSCRIPTION* = 256 # epochs ~ 27 hours + + # Phase 1 - Sharding + # --------------------------------------------------------------- + # https://github.com/ethereum/eth2.0-specs/blob/v0.9.2/specs/core/1_shard-data-chains.md#time-parameters + # TODO those are included in minimal.yaml but not mainnet.yaml + # Why? + # SHARD_SLOTS_PER_BEACON_SLOT* = 2 # spec: SHARD_SLOTS_PER_EPOCH + # EPOCHS_PER_SHARD_PERIOD* = 4 + # PHASE_1_FORK_EPOCH* = 8 + # PHASE_1_FORK_SLOT* = 64 + + # Phase 1 - Custody game + # --------------------------------------------------------------- + # https://github.com/ethereum/eth2.0-specs/blob/v0.9.2/specs/core/1_custody-game.md#constants + # TODO those are included in minimal.yaml but not mainnet.yaml + # Why? + # EARLY_DERIVED_SECRET_PENALTY_MAX_FUTURE_EPOCHS* = 4096 # epochs + # EPOCHS_PER_CUSTODY_PERIOD* = 4 + # CUSTODY_PERIOD_TO_RANDAO_PADDING* = 4 diff --git a/beacon_chain/spec/presets/minimal.nim b/beacon_chain/spec/presets/minimal.nim index af9554adf..ade5354f3 100644 --- a/beacon_chain/spec/presets/minimal.nim +++ b/beacon_chain/spec/presets/minimal.nim @@ -34,7 +34,7 @@ const # Changed SHUFFLE_ROUND_COUNT* = 10 MIN_GENESIS_ACTIVE_VALIDATOR_COUNT* {.intdefine.} = 64 - MIN_GENESIS_TIME* {.intdefine.} = 0 + MIN_GENESIS_TIME* {.intdefine.} = 1578009600 # 3 Jan, 2020 # Constants # --------------------------------------------------------------- @@ -121,14 +121,40 @@ const MAX_DEPOSITS* = 2^4 MAX_VOLUNTARY_EXITS* = 2^4 - -type - # Domains + # Fork choice # --------------------------------------------------------------- - # https://github.com/ethereum/eth2.0-specs/blob/v0.9.2/specs/core/0_beacon-chain.md#domain-types - DomainType* {.pure.} = enum - DOMAIN_BEACON_PROPOSER = 0 - DOMAIN_BEACON_ATTESTER = 1 - DOMAIN_RANDAO = 2 - DOMAIN_DEPOSIT = 3 - DOMAIN_VOLUNTARY_EXIT = 4 + # https://github.com/ethereum/eth2.0-specs/blob/v0.9.2/specs/core/0_fork-choice.md#configuration + + # Changed + SAFE_SLOTS_TO_UPDATE_JUSTIFIED* = 2 + + # Validators + # --------------------------------------------------------------- + # https://github.com/ethereum/eth2.0-specs/blob/v0.9.2/specs/validator/0_beacon-chain-validator.md#misc + + # Changed + ETH1_FOLLOW_DISTANCE* = 16 # blocks + + # Unchanged + TARGET_AGGREGATORS_PER_COMMITTEE* = 16 # validators + RANDOM_SUBNETS_PER_VALIDATOR* = 1 # subnet + EPOCHS_PER_RANDOM_SUBNET_SUBSCRIPTION* = 256 # epochs ~ 27 hours + + # Phase 1 - Sharding + # --------------------------------------------------------------- + # https://github.com/ethereum/eth2.0-specs/blob/v0.9.2/specs/core/1_shard-data-chains.md#time-parameters + # TODO those are included in minimal.yaml but not mainnet.yaml + # Why? + SHARD_SLOTS_PER_BEACON_SLOT* = 2 # spec: SHARD_SLOTS_PER_EPOCH + EPOCHS_PER_SHARD_PERIOD* = 4 + PHASE_1_FORK_EPOCH* = 8 + PHASE_1_FORK_SLOT* = 64 + + # Phase 1 - Custody game + # --------------------------------------------------------------- + # https://github.com/ethereum/eth2.0-specs/blob/v0.9.2/specs/core/1_custody-game.md#constants + # TODO those are included in minimal.yaml but not mainnet.yaml + # Why? + EARLY_DERIVED_SECRET_PENALTY_MAX_FUTURE_EPOCHS* = 4096 # epochs + EPOCHS_PER_CUSTODY_PERIOD* = 4 + CUSTODY_PERIOD_TO_RANDAO_PADDING* = 4 diff --git a/tests/all_tests.nim b/tests/all_tests.nim index 6bea80d1d..65b76b9c0 100644 --- a/tests/all_tests.nim +++ b/tests/all_tests.nim @@ -5,6 +5,9 @@ # * Apache v2 license (license terms in the root directory or at http://www.apache.org/licenses/LICENSE-2.0). # at your option. This file may not be copied, modified, or distributed except according to those terms. +import # Official constants + ./official/test_fixture_const_sanity_check + import # Unit test ./test_attestation_pool, ./test_beacon_chain_db, diff --git a/tests/official/test_fixture_const_sanity_check.nim b/tests/official/test_fixture_const_sanity_check.nim new file mode 100644 index 000000000..48cc904e4 --- /dev/null +++ b/tests/official/test_fixture_const_sanity_check.nim @@ -0,0 +1,121 @@ +# Sanity check on constants +# ---------------------------------------------------------------- + +import + # Standard library + macros, os, strutils, tables, math, json, streams, + strformat, unittest, + # Third party + yaml, + # Status libraries + stew/[byteutils, endians2], + # Internals + ../../beacon_chain/spec/[datatypes, digest], + # Test utilities + ../testutil + +const + SpecDir = currentSourcePath.rsplit(DirSep, 1)[0] / + ".."/".."/"beacon_chain"/"spec" + FixturesDir = currentSourcePath.rsplit(DirSep, 1)[0] / "fixtures" + Config = FixturesDir/"tests-v0.9.2"/const_preset/"config.yaml" + +type + CheckedType = SomeInteger or Slot or Epoch + # Only test numerical types, constants + # defined for other type will get a placeholder low(int64) value + +macro parseNumConsts(file: static string): untyped = + ## Important: All const are assumed to be top-level + ## i.e. not hidden behind a "when" statement + + var constsToCheck: seq[(string, NimNode)] + # We can't create a table directly and quote do it + # Nim complains about the "data" field not being accessible + + let fileAST = parseStmt(slurp(file)) + for statement in fileAST: + if statement.kind == nnkConstSection: + for constDef in statement: + if constDef.len == 0 or + constDef[0].kind notin {nnkPragmaExpr, nnkPostfix}: + # Comments in a const section need to be skipped. + # And we only want exported constants. + continue + # Note: we assume that all const with a pragma are exported + # 1. Simple statement + # + # ConstDef + # Postfix + # Ident "*" + # Ident "HISTORICAL_ROOTS_LIMIT" + # Empty + # IntLit 16777216 + # + # 2. with intdefine pragma + # + # ConstDef + # PragmaExpr + # Postfix + # Ident "*" + # Ident "MIN_GENESIS_ACTIVE_VALIDATOR_COUNT" + # Pragma + # Ident "intdefine" + # Empty + # IntLit 99 + let name = if constDef[0].kind == nnkPostfix: $constDef[0][1] + else: $constDef[0][0][1] + + # ConstsToCheck["HISTORICAL_ROOTS_LIMIT"} = uint64(16777216) + # Put a placeholder values for strings + let value = block: + let node = constDef[2] + quote do: + when `node` is CheckedType: + uint64(`node`) + else: + high(uint64) + constsToCheck.add (name, value) + + result = quote do: `constsToCheck` + +const + datatypesConsts = @(parseNumConsts(SpecDir/"datatypes.nim")) + mainnetConsts = @(parseNumConsts(SpecDir/"presets"/"mainnet.nim")) + minimalConsts = @(parseNumConsts(SpecDir/"presets"/"minimal.nim")) + +const IgnoreKeys = [ + # Ignore all non-numeric types + "DEPOSIT_CONTRACT_ADDRESS" +] + +func parseU32LEHex(hexValue: string): uint32 = + ## Parse little-endian uint32 hex string + result = uint32.fromBytesLE hexToByteArray[4](hexValue) + +proc checkConfig() = + let ConstsToCheck = toTable( + when const_preset == "minimal": + minimalConsts & datatypesConsts + else: + mainnetConsts & datatypesConsts + ) + + var yamlStream = openFileStream(Config) + defer: yamlStream.close() + var config = yamlStream.loadToJson() + doAssert config.len == 1 + for constant, value in config[0]: + test &"{constant:<50}{value:<20}{preset()}": + if constant in IgnoreKeys: + echo &" ↶↶ Skipping {constant}" + continue + if constant.startsWith("DOMAIN"): + let domain = parseEnum[DomainType](constant) + let value = parseU32LEHex(value.getStr()) + check: uint32(domain) == value + else: + check: ConstsToCheck[constant] == value.getBiggestInt().uint64() + +suite "Official - 0.9.2 - constants & config " & preset(): + checkConfig() diff --git a/tests/official/test_fixture_ssz_consensus_objects.nim b/tests/official/test_fixture_ssz_consensus_objects.nim index 9224cf5de..f4d915838 100644 --- a/tests/official/test_fixture_ssz_consensus_objects.nim +++ b/tests/official/test_fixture_ssz_consensus_objects.nim @@ -43,50 +43,16 @@ setDefaultValue(SSZHashTreeRoot, signing_root, "") # Checking the values against the yaml file is TODO (require more flexible Yaml parser) const Unsupported = toHashSet([ "AggregateAndProof", # Type for signature aggregation - not implemented - # "Attestation", # - # "AttestationData", # - # "AttesterSlashing", # - # "BeaconBlock", # - # "BeaconBlockBody", # - # "BeaconBlockHeader", # - # "BeaconState", # - # "Checkpoint", # - # "Deposit", # - # "DepositData", # - # "Eth1Data", # - # "Fork", # - # "HistoricalBatch", # - # "IndexedAttestation", # - # "PendingAttestation", # - # "ProposerSlashing", # - # "Validator", # HashTreeRoot KO - # "VoluntaryExit" # ]) -const UnsupportedMainnet = toHashSet([ - "PendingAttestation", # HashTreeRoot KO - "BeaconState", - "AttesterSlashing", - "BeaconBlockBody", - "IndexedAttestation", - "Attestation", - "BeaconBlock" - ]) - -type Skip = enum - SkipNone - SkipHashTreeRoot - SkipSigningRoot - -proc checkSSZ(T: typedesc, dir: string, expectedHash: SSZHashTreeRoot, skip = SkipNone) = +proc checkSSZ(T: typedesc, dir: string, expectedHash: SSZHashTreeRoot) = # Deserialize into a ref object to not fill Nim stack var deserialized: ref T new deserialized deserialized[] = SSZ.loadFile(dir/"serialized.ssz", T) - if not(skip == SkipHashTreeRoot): - check: expectedHash.root == "0x" & toLowerASCII($deserialized.hashTreeRoot()) - if expectedHash.signing_root != "" and not(skip == SkipSigningRoot): + check: expectedHash.root == "0x" & toLowerASCII($deserialized.hashTreeRoot()) + if expectedHash.signing_root != "": check: expectedHash.signing_root == "0x" & toLowerASCII($deserialized[].signingRoot()) # TODO check the value @@ -104,17 +70,11 @@ proc runSSZtests() = for pathKind, sszType in walkDir(SSZDir, relative = true): doAssert pathKind == pcDir if sszType in Unsupported: - test &" Skipping {sszType:20} consensus object ✗✗✗": + test &" Skipping {sszType:20} ✗✗✗": discard continue - when const_preset == "mainnet": - if sszType in UnsupportedMainnet: - test &" Skipping {sszType:20} consensus object ✗✗✗ (skipped on mainnet-only)": - discard - continue - - test &" Testing {sszType:20} consensus object ✓✓✓": + test &" Testing {sszType}": let path = SSZDir/sszType for pathKind, sszTestKind in walkDir(path, relative = true): doAssert pathKind == pcDir @@ -146,5 +106,5 @@ proc runSSZtests() = else: raise newException(ValueError, "Unsupported test: " & sszType) -suite "Official - 0.9.1 - SSZ consensus objects " & preset(): +suite "Official - 0.9.2 - SSZ consensus objects " & preset(): runSSZtests()