From 5784b2d7f7d65c3cc447dad5f1d0bc1c139c9841 Mon Sep 17 00:00:00 2001 From: Mamy Ratsimbazafy Date: Mon, 29 Apr 2019 18:10:01 +0200 Subject: [PATCH] Full EF state tests parsing (#242) * Load full state test + Add json-serialization for Bitfield * Implement empty_block_transition test (but test is missing the end state) * Use the provided empty block instead of mocking one * Add failing block signing test * Tests that can't be passed now are now "for information" + indent the "information"/hash given --- beacon_chain.nimble | 2 +- beacon_chain/spec/bitfield.nim | 7 ++- beacon_chain/state_transition.nim | 7 +++ tests/official/fixtures | 2 +- tests/official/state_test_utils.nim | 40 +++++++++++--- tests/official/test_fixture_state.nim | 79 ++++++++++++++++++++++++--- 6 files changed, 116 insertions(+), 21 deletions(-) diff --git a/beacon_chain.nimble b/beacon_chain.nimble index 0da73f015..aad3f9252 100644 --- a/beacon_chain.nimble +++ b/beacon_chain.nimble @@ -30,7 +30,7 @@ requires "nim >= 0.19.0", "chronos", "yaml", "libp2p", - "byteutils" # test only + "byteutils" # test only (BitField and bytes datatypes deserialization) ### Helper functions proc buildBinary(name: string, srcDir = "./", params = "", lang = "c") = diff --git a/beacon_chain/spec/bitfield.nim b/beacon_chain/spec/bitfield.nim index f2ba9f7e0..9d66de824 100644 --- a/beacon_chain/spec/bitfield.nim +++ b/beacon_chain/spec/bitfield.nim @@ -1,11 +1,11 @@ +import byteutils, json_serialization + type BitField* = object ## A simple bit field type that follows the semantics of the spec, with ## regards to bit endian operations # TODO nim-ranges contains utilities for with bitsets - could try to # recycle that, but there are open questions about bit endianess there. - # TODO define a json serialization.. together with spec tests? - # https://github.com/ethereum/eth2.0-tests/tree/master/state bits*: seq[byte] func ceil_div8(v: int): int = (v + 7) div 8 @@ -13,6 +13,9 @@ func ceil_div8(v: int): int = (v + 7) div 8 func init*(T: type BitField, bits: int): BitField = BitField(bits: newSeq[byte](ceil_div8(bits))) +proc readValue*(r: var JsonReader, a: var BitField) {.inline.} = + a.bits = r.readValue(string).hexToSeqByte() + # https://github.com/ethereum/eth2.0-specs/blob/v0.5.0/specs/core/0_beacon-chain.md#get_bitfield_bit func get_bitfield_bit*(bitfield: BitField, i: int): bool = # Extract the bit in ``bitfield`` at position ``i``. diff --git a/beacon_chain/state_transition.nim b/beacon_chain/state_transition.nim index 8f4c236a2..c07aa6545 100644 --- a/beacon_chain/state_transition.nim +++ b/beacon_chain/state_transition.nim @@ -484,26 +484,33 @@ proc processBlock( return false if not processRandao(state, blck, flags): + debug "[Block processing] Randao failure", slot = humaneSlotNum(state.slot) return false processEth1Data(state, blck) if not processProposerSlashings(state, blck, flags): + debug "[Block processing] Proposer slashing failure", slot = humaneSlotNum(state.slot) return false if not processAttesterSlashings(state, blck): + debug "[Block processing] Attester slashing failure", slot = humaneSlotNum(state.slot) return false if not processAttestations(state, blck, flags): + debug "[Block processing] Attestation processing failure", slot = humaneSlotNum(state.slot) return false if not processDeposits(state, blck): + debug "[Block processing] Deposit processing failure", slot = humaneSlotNum(state.slot) return false if not processExits(state, blck, flags): + debug "[Block processing] Exit processing failure", slot = humaneSlotNum(state.slot) return false if not processTransfers(state, blck, flags): + debug "[Block processing] Transfer processing failure", slot = humaneSlotNum(state.slot) return false true diff --git a/tests/official/fixtures b/tests/official/fixtures index 6517fe82f..da49e682b 160000 --- a/tests/official/fixtures +++ b/tests/official/fixtures @@ -1 +1 @@ -Subproject commit 6517fe82f9b1a8a8d7b6f4eefdf1a3fb00d1ffab +Subproject commit da49e682bf93c43ffc207e4b1b948962c5068196 diff --git a/tests/official/state_test_utils.nim b/tests/official/state_test_utils.nim index 47eaf6234..ff8683611 100644 --- a/tests/official/state_test_utils.nim +++ b/tests/official/state_test_utils.nim @@ -4,7 +4,8 @@ import eth/common, serialization, json_serialization, # Beacon chain internals # submodule in nim-beacon-chain/tests/official/fixtures/ - ../../beacon_chain/spec/[datatypes, crypto, digest] + ../../beacon_chain/spec/[datatypes, crypto, digest], + ../../beacon_chain/ssz export nimcrypto.toHex @@ -71,12 +72,8 @@ type verify_signatures*: bool initial_state*: BeaconState blocks*: seq[BeaconBlock] - expected_state*: ExpectedState + expected_state*: BeaconState - ExpectedState* = object - ## TODO what is this? - slot*: Slot - # ####################### # Default init proc default*(T: typedesc): T = discard @@ -87,7 +84,7 @@ proc default*(T: typedesc): T = discard proc readValue*[N: static int](r: var JsonReader, a: var array[N, byte]) {.inline.} = # Needed for; # - BLS_WITHDRAWAL_PREFIX_BYTE - # - FOrk datatypes + # - Fork datatypes # TODO: are all bytes and bytearray serialized as hex? # if so export that to nim-eth hexToByteArray(r.readValue(string), a) @@ -99,4 +96,31 @@ proc parseStateTests*(jsonPath: string): StateTest = writeStackTrace() stderr.write "Json load issue for file \"", jsonPath, "\"\n" stderr.write err.formatMsg(jsonPath), "\n" - quit 1 \ No newline at end of file + quit 1 + +# ####################### +# Mocking helpers +# https://github.com/ethereum/eth2.0-specs/blob/75f0af45bb0613bb406fc72d10266cee4cfb402a/tests/phase0/helpers.py#L107 + +proc build_empty_block_for_next_slot*(state: BeaconState): BeaconBlock = + ## TODO: why can the official spec get away with a simple proc + + # result.slot = state.slot + 1 + # var previous_block_header = state.latest_block_header + # if previous_block_header.state_root == ZERO_HASH: + # previous_block_header.state_root = state.hash_tree_root() + # result.previous_block_root = signed_root(previous_block_header) + + ## TODO: `makeBlock` from testutil.nim + ## doesn't work either due to use of fake private keys + + # let prev_root = block: + # if state.latest_block_header.state_root == ZERO_HASH: + # state.hash_tree_root() + # else: state.latest_block_header.state_root + # result = makeBlock( + # state, + # prev_root, + # BeaconBlockBody() + # ) + {.error: "Not implemented".} \ No newline at end of file diff --git a/tests/official/test_fixture_state.nim b/tests/official/test_fixture_state.nim index 1a51ae58b..bc4aeeb40 100644 --- a/tests/official/test_fixture_state.nim +++ b/tests/official/test_fixture_state.nim @@ -6,24 +6,85 @@ # at your option. This file may not be copied, modified, or distributed except according to those terms. import - # Standard lib - ospaths, strutils, json, unittest, + # Standard libs + ospaths, strutils, json, unittest, strformat, + # Third parties + byteutils, # Beacon chain internals ../../beacon_chain/spec/[datatypes, crypto, digest, beaconstate], - ../../beacon_chain/ssz, + ../../beacon_chain/[ssz, state_transition], # Test utilities ./state_test_utils const TestFolder = currentSourcePath.rsplit(DirSep, 1)[0] -const TestsPath = "fixtures" / "json_tests" / "state" / "sanity-check_default-config_100-vals-first_test.json" +const TestsPath = "fixtures" / "json_tests" / "state" / "sanity-check_default-config_100-vals.json" + +var stateTests: StateTest suite "Official - State tests": # Initializing a beacon state from the deposits - var stateTests: StateTest + # Source: https://github.com/ethereum/eth2.0-specs/blob/2baa242ac004b0475604c4c4ef4315e14f56c5c7/tests/phase0/test_sanity.py#L55-L460 test "Parsing the official state tests into Nimbus beacon types": stateTests = parseStateTests(TestFolder / TestsPath) doAssert $stateTests.test_cases[0].name == "test_empty_block_transition" + + test "[For information - Non-blocking] Block root signing": + # TODO: Currently we are unable to use the official EF tests: + # - The provided zero signature "0x0000..." is an invalid compressed BLS signature + # - Block headers are using that signature + # - Block processing checks that block.previous_block_root == signed_root(state.latest_block_header) + # -> Changing EF provided previous_block_root would render the block transition tests meaningless + # -> Changing the signature to a valid "0xc000..." makes all hashes/signed_root wrong ... + # + # So we only test that block header signing in Nimbus matches block header signing from the EF + # And we can't deserialize from the raw YAML/JSON to avoid sanity checks on the signature + + # TODO: Move that in an actual SSZ test suite + + block: # sanity-check_default-config_100-vals.yaml - test "test_empty_block_transition" + let header = BeaconBlockHeader( + slot: Slot(4294967296), + previous_block_root: ZERO_HASH, + state_root: ZERO_HASH, + block_body_root: Eth2Digest(data: + hexToByteArray[32]("0x13f2001ff0ee4a528b3c43f63d70a997aefca990ed8eada2223ee6ec3807f7cc") + ), + signature: ValidatorSig() + ) + let previous_block_root = Eth2Digest(data: + hexToByteArray[32]("0x1179346f489d8be1731377cb199af5cc61faa38353e2d67e096bed182677062a") + ) + echo " Expected previous block root: 0x", previous_block_root + echo " Computed header signed root: 0x", signed_root(header) + + test "[For information] Print list of official tests to implement": + for i, test in stateTests.test_cases: + echo &" Test #{i:03}: {test.name}" + + # test "Empty block transition": + # # TODO - assert that the constants match + # var state: BeaconState + # doAssert stateTests.test_cases[0].name == "test_empty_block_transition" + + # template tcase(): untyped {.dirty.} = + # # Alias + # stateTests.test_cases[0] + + # deepCopy(state, tcase.initial_state) + + # # Use the provided empty block + # # Alternatively, generate one with `build_empty_block_for_next_slot` + # let blck = tcase.blocks[0] + # debugEcho blck.previous_block_root + + # let ok = updateState(state, blck, flags = {}) + # check: + # ok + # tcase.expected_state.eth1_data_votes.len == state.eth1_data_votes.len + 1 + # get_block_root(tcase.expected_state, state.slot) == blck.previous_block_root + +suite "[For information - non-blocking] Extra state tests": var initialState: BeaconState - test "Initializing from scratch a new beacon chain with the same constants and deposit configuration as official state": + test "Initializing from scratch a new beacon chain with the same constants and deposit configuration as official state test 0": var deposits: seq[Deposit] var index = 0'u64 for v in stateTests.test_cases[0].initial_state.validator_registry: @@ -46,8 +107,8 @@ suite "Official - State tests": # Initializing a beacon state from the deposits genesis_time = 0, genesis_eth1_data = Eth1Data() ) - test "[For information] Comparing state hashes": + test "Comparing state hashes": # TODO - Add official hashes when available # TODO - Make that a blocking test requirement - echo "Deserialized state hash: 0x" & $stateTests.test_cases[0].initial_state.hash_tree_root() - echo "From-scratch state hash: 0x" & $initialState.hash_tree_root() \ No newline at end of file + echo " Deserialized state hash: 0x" & $stateTests.test_cases[0].initial_state.hash_tree_root() + echo " From-scratch state hash: 0x" & $initialState.hash_tree_root() \ No newline at end of file