diff --git a/.circleci/config.yml b/.circleci/config.yml index 157c56ca5..8e82eec1b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -168,6 +168,19 @@ jobs: command: make citest fork=eip6110 - store_test_results: path: tests/core/pyspec/test-reports + test-whisk: + docker: + - image: circleci/python:3.9 + working_directory: ~/specs-repo + steps: + - restore_cache: + key: v3-specs-repo-{{ .Branch }}-{{ .Revision }} + - restore_pyspec_cached_venv + - run: + name: Run py-tests + command: make citest fork=whisk + - store_test_results: + path: tests/core/pyspec/test-reports table_of_contents: docker: - image: circleci/node:10.16.3 @@ -291,6 +304,9 @@ workflows: - test-eip6110: requires: - install_pyspec_test + - test-whisk: + requires: + - install_pyspec_test - table_of_contents - codespell - lint: diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 6b24ef5eb..6d736c316 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -71,7 +71,7 @@ jobs: needs: [preclear,lint,codespell,table_of_contents] strategy: matrix: - version: ["phase0", "altair", "bellatrix", "capella", "deneb", "eip6110"] + version: ["phase0", "altair", "bellatrix", "capella", "deneb", "eip6110", "whisk"] steps: - name: Checkout this repo uses: actions/checkout@v3.2.0 diff --git a/setup.py b/setup.py index efa57ac2b..ecc1f7ad0 100644 --- a/setup.py +++ b/setup.py @@ -257,6 +257,11 @@ def get_spec(file_name: Path, preset: Dict[str, str], config: Dict[str, str], pr if any('KZG_SETUP' in name for name in constant_vars): _update_constant_vars_with_kzg_setups(constant_vars, preset_name) + if any('CURDLEPROOFS_CRS' in name for name in constant_vars): + # TODO: Use actual CRS derived from a fixed string like 'nankokita_no_kakurenbo' + crs_len = int(preset_vars['WHISK_VALIDATORS_PER_SHUFFLE'].value) + int(preset_vars['CURDLEPROOFS_N_BLINDERS'].value) + 3 + constant_vars['CURDLEPROOFS_CRS_G1'] = VariableDefinition(constant_vars['CURDLEPROOFS_CRS_G1'].value, str(ALL_KZG_SETUPS['mainnet'][0][0:crs_len]), "noqa: E501", None) + return SpecObject( functions=functions, protocols=protocols, @@ -519,6 +524,6 @@ setup( "lru-dict==1.2.0", MARKO_VERSION, "py_arkworks_bls12381==0.3.4", - "curdleproofs @ git+https://github.com/nalinbhardwaj/curdleproofs.pie@805d06785b6ff35fde7148762277dd1ae678beeb#egg=curdleproofs&subdirectory=curdleproofs", + "curdleproofs @ git+https://github.com/nalinbhardwaj/curdleproofs.pie@5fe661b7183454655ff1e47690bb28e01e66ea66#egg=curdleproofs&subdirectory=curdleproofs", ] ) diff --git a/specs/_features/whisk/beacon-chain.md b/specs/_features/whisk/beacon-chain.md index f2a51e622..72207a50a 100644 --- a/specs/_features/whisk/beacon-chain.md +++ b/specs/_features/whisk/beacon-chain.md @@ -33,7 +33,7 @@ This document details the beacon chain additions and changes of to support the Whisk SSLE, -*Note:* This specification is built upon [Capella](../../capella/beacon-chain.md) and is under active development. +*Note:* This specification is built upon [capella](../../capella/beacon-chain.md) and is under active development. ## Constants @@ -83,14 +83,15 @@ def bytes_to_bls_field(b: Bytes32) -> BLSFieldElement: TODO: Deneb will introduces this helper too. Should delete it once it's rebased to post-Deneb. """ field_element = int.from_bytes(b, ENDIANNESS) - assert field_element < BLS_MODULUS - return BLSFieldElement(field_element) + return BLSFieldElement(field_element % BLS_MODULUS) ``` | Name | Value | | ------------------ | ------------------------------------------------------------------------------- | -| `BLS_G1_GENERATOR` | `bls.G1_to_bytes48(bls.G1)` | +| `BLS_G1_GENERATOR` | `Bytes48('0x97f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb')` | | `BLS_MODULUS` | `52435875175126190479447740508185965837690552500527637822603658699938581184513` | +| `CURDLEPROOFS_CRS_G1` | `Vector[G1Point, WHISK_VALIDATORS_PER_SHUFFLE + CURDLEPROOFS_N_BLINDERS + 3]`, contents TBD +| `CURDLEPROOFS_CRS` | `curdleproofs.CurdleproofsCrs.from_points_compressed(WHISK_VALIDATORS_PER_SHUFFLE, CURDLEPROOFS_N_BLINDERS, CURDLEPROOFS_CRS_G1)` | ### Curdleproofs and opening proofs @@ -105,8 +106,16 @@ def IsValidWhiskShuffleProof(pre_shuffle_trackers: Sequence[WhiskTracker], Verify `post_shuffle_trackers` is a permutation of `pre_shuffle_trackers`. Defined in https://github.com/nalinbhardwaj/curdleproofs.pie/tree/verifier-only. """ - # pylint: disable=unused-argument - return True + try: + return curdleproofs.IsValidWhiskShuffleProof( + CURDLEPROOFS_CRS, + pre_shuffle_trackers, + post_shuffle_trackers, + M, + shuffle_proof, + ) + except: + return False ``` ```python @@ -117,8 +126,11 @@ def IsValidWhiskOpeningProof(tracker: WhiskTracker, Verify knowledge of `k` such that `tracker.k_r_G == k * tracker.r_G` and `k_commitment == k * BLS_G1_GENERATOR`. Defined in https://github.com/nalinbhardwaj/curdleproofs.pie/tree/verifier-only. """ - # pylint: disable=unused-argument - return True + try: + return curdleproofs.IsValidWhiskOpeningProof(tracker, k_commitment, tracker_proof) + except: + return False + ``` ## Epoch processing @@ -276,7 +288,7 @@ def process_block_header(state: BeaconState, block: BeaconBlock) -> None: #### `BeaconBlockBody` ```python -class BeaconBlockBody(capella.BeaconBlockBody): +class BeaconBlockBody(Container): randao_reveal: BLSSignature eth1_data: Eth1Data # Eth1 data vote graffiti: Bytes32 # Arbitrary data @@ -289,7 +301,6 @@ class BeaconBlockBody(capella.BeaconBlockBody): sync_aggregate: SyncAggregate # Execution execution_payload: ExecutionPayload - # Capella operations bls_to_execution_changes: List[SignedBLSToExecutionChange, MAX_BLS_TO_EXECUTION_CHANGES] # Whisk whisk_opening_proof: WhiskTrackerProof # [New in Whisk] @@ -350,10 +361,7 @@ def is_k_commitment_unique(state: BeaconState, k_commitment: BLSG1Point) -> bool ``` ```python -def process_whisk(state: BeaconState, body: BeaconBlockBody) -> None: - process_shuffled_trackers(state, body) - - # Overwrite all validator Whisk fields (first Whisk proposal) or just the permutation commitment (next proposals) +def process_whisk_registration(state: BeaconState, body: BeaconBlockBody) -> None: proposer = state.validators[get_beacon_proposer_index(state)] if proposer.whisk_tracker.r_G == BLS_G1_GENERATOR: # first Whisk proposal assert body.whisk_tracker.r_G != BLS_G1_GENERATOR @@ -369,20 +377,19 @@ def process_whisk(state: BeaconState, body: BeaconBlockBody) -> None: assert body.whisk_registration_proof == WhiskTrackerProof() assert body.whisk_tracker == WhiskTracker() assert body.whisk_k_commitment == BLSG1Point() - assert body.whisk_shuffle_proof_M_commitment == BLSG1Point() ``` ```python def process_block(state: BeaconState, block: BeaconBlock) -> None: process_block_header(state, block) - if is_execution_enabled(state, block.body): - process_withdrawals(state, block.body.execution_payload) - process_execution_payload(state, block.body.execution_payload, EXECUTION_ENGINE) + process_withdrawals(state, block.body.execution_payload) + process_execution_payload(state, block.body, EXECUTION_ENGINE) process_randao(state, block.body) process_eth1_data(state, block.body) process_operations(state, block.body) process_sync_aggregate(state, block.body.sync_aggregate) - process_whisk(state, block.body) # [New in Whisk] + process_shuffled_trackers(state, block.body) # [New in Whisk] + process_whisk_registration(state, block.body) # [New in Whisk] ``` ### Deposits diff --git a/tests/core/pyspec/eth2spec/test/context.py b/tests/core/pyspec/eth2spec/test/context.py index 48f6857f6..3b1ab8eca 100644 --- a/tests/core/pyspec/eth2spec/test/context.py +++ b/tests/core/pyspec/eth2spec/test/context.py @@ -9,12 +9,14 @@ from eth2spec.bellatrix import mainnet as spec_bellatrix_mainnet, minimal as spe from eth2spec.capella import mainnet as spec_capella_mainnet, minimal as spec_capella_minimal from eth2spec.deneb import mainnet as spec_deneb_mainnet, minimal as spec_deneb_minimal from eth2spec.eip6110 import mainnet as spec_eip6110_mainnet, minimal as spec_eip6110_minimal +from eth2spec.whisk import mainnet as spec_whisk_mainnet, minimal as spec_whisk_minimal from eth2spec.utils import bls from .exceptions import SkippedTest from .helpers.constants import ( PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, EIP6110, + WHISK, MINIMAL, MAINNET, ALL_PHASES, ALL_FORK_UPGRADES, @@ -83,6 +85,7 @@ spec_targets: Dict[PresetBaseName, Dict[SpecForkName, Spec]] = { CAPELLA: spec_capella_minimal, DENEB: spec_deneb_minimal, EIP6110: spec_eip6110_minimal, + WHISK: spec_whisk_minimal, }, MAINNET: { PHASE0: spec_phase0_mainnet, @@ -91,6 +94,7 @@ spec_targets: Dict[PresetBaseName, Dict[SpecForkName, Spec]] = { CAPELLA: spec_capella_mainnet, DENEB: spec_deneb_mainnet, EIP6110: spec_eip6110_mainnet, + WHISK: spec_whisk_mainnet, }, } @@ -541,6 +545,7 @@ with_bellatrix_and_later = with_all_phases_from(BELLATRIX) with_capella_and_later = with_all_phases_from(CAPELLA) with_deneb_and_later = with_all_phases_from(DENEB) with_eip6110_and_later = with_all_phases_from(EIP6110) +with_whisk_and_later = with_all_phases_from(WHISK) class quoted_str(str): diff --git a/tests/core/pyspec/eth2spec/test/helpers/block.py b/tests/core/pyspec/eth2spec/test/helpers/block.py index 270bc4be1..f4338dab8 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/block.py +++ b/tests/core/pyspec/eth2spec/test/helpers/block.py @@ -1,10 +1,25 @@ from eth2spec.test.helpers.execution_payload import build_empty_execution_payload -from eth2spec.test.helpers.forks import is_post_altair, is_post_bellatrix -from eth2spec.test.helpers.keys import privkeys +from eth2spec.test.helpers.forks import is_post_whisk, is_post_altair, is_post_bellatrix +from eth2spec.test.helpers.keys import privkeys, whisk_ks_initial, whisk_ks_final from eth2spec.utils import bls from eth2spec.utils.bls import only_with_bls from eth2spec.utils.ssz.ssz_impl import hash_tree_root +from curdleproofs import ( + GenerateWhiskTrackerProof, + WhiskTracker, + IsValidWhiskOpeningProof, + GenerateWhiskShuffleProof, + IsValidWhiskShuffleProof +) +from py_ecc.optimized_bls12_381.optimized_curve import G1, multiply +from py_ecc.typing import Optimized_Field, Optimized_Point3D +from py_ecc.bls.g2_primitives import ( + G1_to_pubkey as py_ecc_G1_to_bytes48, + pubkey_to_G1 as py_ecc_bytes48_to_G1, +) +from eth2spec.test.helpers.whisk import get_whisk_tracker_and_commitment +PointProjective = Optimized_Point3D[Optimized_Field] def get_proposer_index_maybe(spec, state, slot, proposer_index=None): if proposer_index is None: @@ -24,10 +39,9 @@ def get_proposer_index_maybe(spec, state, slot, proposer_index=None): @only_with_bls() -def apply_randao_reveal(spec, state, block, proposer_index=None): +def apply_randao_reveal(spec, state, block, proposer_index): assert state.slot <= block.slot - proposer_index = get_proposer_index_maybe(spec, state, block.slot, proposer_index) privkey = privkeys[proposer_index] domain = spec.get_domain(state, spec.DOMAIN_RANDAO, spec.compute_epoch_at_slot(block.slot)) @@ -72,7 +86,7 @@ def apply_empty_block(spec, state, slot=None): return transition_unsigned_block(spec, state, block) -def build_empty_block(spec, state, slot=None): +def build_empty_block(spec, state, slot=None, proposer_index=None): """ Build empty block for ``slot``, built upon the latest block header seen by ``state``. Slot must be greater than or equal to the current slot in ``state``. @@ -87,13 +101,14 @@ def build_empty_block(spec, state, slot=None): spec.process_slots(state, slot) state, parent_block_root = get_state_and_beacon_parent_root_at_slot(spec, state, slot) + proposer_index = get_beacon_proposer_to_build(spec, state, proposer_index) empty_block = spec.BeaconBlock() empty_block.slot = slot - empty_block.proposer_index = spec.get_beacon_proposer_index(state) + empty_block.proposer_index = proposer_index empty_block.body.eth1_data.deposit_count = state.eth1_deposit_index empty_block.parent_root = parent_block_root - apply_randao_reveal(spec, state, empty_block) + apply_randao_reveal(spec, state, empty_block, proposer_index) if is_post_altair(spec): empty_block.body.sync_aggregate.sync_committee_signature = spec.G2_POINT_AT_INFINITY @@ -101,11 +116,112 @@ def build_empty_block(spec, state, slot=None): if is_post_bellatrix(spec): empty_block.body.execution_payload = build_empty_execution_payload(spec, state) + if is_post_whisk(spec): + # Whisk opening proof + ####### + + # Create valid whisk opening proof + # TODO: Use k_initial or k_final to handle first and subsequent proposals + k_initial = whisk_ks_initial[proposer_index] + + # Sanity check proposer is correct + proposer_k_commitment = state.validators[proposer_index].whisk_k_commitment + k_commitment = py_ecc_G1_to_bytes48(multiply(G1, int(k_initial))) + if proposer_k_commitment != k_commitment: + raise Exception("k proposer_index does not match proposer_k_commitment", proposer_k_commitment, k_commitment) + + proposer_tracker = state.whisk_proposer_trackers[state.slot % spec.WHISK_PROPOSER_TRACKERS_COUNT] + if not is_whisk_proposer(proposer_tracker, k_initial): + raise Exception("k proposer_index does not match proposer_tracker") + + empty_block.body.whisk_opening_proof = GenerateWhiskTrackerProof(proposer_tracker, k_initial) + if not IsValidWhiskOpeningProof(proposer_tracker, proposer_k_commitment, empty_block.body.whisk_opening_proof): + raise Exception( + "produced opening proof is not valid", + proposer_tracker, + proposer_k_commitment, + empty_block.body.whisk_opening_proof + ) + + # Whisk shuffle proof + ####### + + shuffle_indices = spec.get_shuffle_indices(empty_block.body.randao_reveal) + pre_shuffle_trackers = [state.whisk_candidate_trackers[i] for i in shuffle_indices] + + post_trackers, m, shuffle_proof = GenerateWhiskShuffleProof(spec.CURDLEPROOFS_CRS, pre_shuffle_trackers) + empty_block.body.whisk_post_shuffle_trackers = post_trackers + empty_block.body.whisk_shuffle_proof = shuffle_proof + empty_block.body.whisk_shuffle_proof_M_commitment = m + + if not IsValidWhiskShuffleProof( + spec.CURDLEPROOFS_CRS, + pre_shuffle_trackers, + empty_block.body.whisk_post_shuffle_trackers, + empty_block.body.whisk_shuffle_proof_M_commitment, + empty_block.body.whisk_shuffle_proof, + ): + raise Exception( + "produced shuffle proof is not valid", + pre_shuffle_trackers, + empty_block.body.whisk_post_shuffle_trackers, + empty_block.body.whisk_shuffle_proof_M_commitment, + empty_block.body.whisk_shuffle_proof, + ) + + # Whisk registration proof + ####### + + # Branching logic depending if first proposal or not + is_first_proposal = state.validators[proposer_index].whisk_tracker.r_G == spec.BLS_G1_GENERATOR + if is_first_proposal: + # Register new tracker + k_final = whisk_ks_final[proposer_index] + # TODO: Actual logic should pick a random r, but we may want to do something fancy to locate trackers quickly + r = 2 + tracker, k_commitment = get_whisk_tracker_and_commitment(k_final, r) + empty_block.body.whisk_registration_proof = GenerateWhiskTrackerProof(tracker, k_final) + empty_block.body.whisk_tracker = tracker + empty_block.body.whisk_k_commitment = k_commitment + + else: + # Subsequent proposals, just leave empty + empty_block.body.whisk_registration_proof = spec.WhiskTrackerProof() + empty_block.body.whisk_tracker = spec.WhiskTracker() + empty_block.body.whisk_k_commitment = spec.BLSG1Point() + return empty_block -def build_empty_block_for_next_slot(spec, state): - return build_empty_block(spec, state, state.slot + 1) +def is_whisk_proposer(tracker: WhiskTracker, k: int) -> bool: + return py_ecc_G1_to_bytes48(multiply(py_ecc_bytes48_to_G1(tracker.r_G), k)) == tracker.k_r_G + + +def get_beacon_proposer_to_build(spec, state, proposer_index=None): + if is_post_whisk(spec): + if proposer_index is None: + return find_whisk_proposer(spec, state) + else: + return proposer_index + else: + return spec.get_beacon_proposer_index(state) + + +def find_whisk_proposer(spec, state): + raise Exception("proposer not known without heavy math") + proposer_tracker = state.whisk_proposer_trackers[state.slot % spec.WHISK_PROPOSER_TRACKERS_COUNT] + # First attempt direct equality with trackers + for i, validator in enumerate(state.validators): + # # This is insanely slow + # if validator.whisk_tracker == proposer_tracker: + if True: + return i + # In Whisk where to get proposer from? + raise Exception("proposer_tracker not matched") + + +def build_empty_block_for_next_slot(spec, state, proposer_index=None): + return build_empty_block(spec, state, state.slot + 1, proposer_index) def get_state_and_beacon_parent_root_at_slot(spec, state, slot): diff --git a/tests/core/pyspec/eth2spec/test/helpers/constants.py b/tests/core/pyspec/eth2spec/test/helpers/constants.py index 049c354ca..757f1c007 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/constants.py +++ b/tests/core/pyspec/eth2spec/test/helpers/constants.py @@ -17,6 +17,7 @@ SHARDING = SpecForkName('sharding') CUSTODY_GAME = SpecForkName('custody_game') DAS = SpecForkName('das') EIP6110 = SpecForkName('eip6110') +WHISK = SpecForkName('whisk') # # SpecFork settings @@ -32,11 +33,12 @@ ALL_PHASES = ( DENEB, # Experimental patches EIP6110, + WHISK, ) # The forks that have light client specs LIGHT_CLIENT_TESTING_FORKS = (*[item for item in MAINNET_FORKS if item != PHASE0], DENEB) # The forks that output to the test vectors. -TESTGEN_FORKS = (*MAINNET_FORKS, DENEB, EIP6110) +TESTGEN_FORKS = (*MAINNET_FORKS, DENEB, EIP6110, WHISK) ALL_FORK_UPGRADES = { # pre_fork_name: post_fork_name diff --git a/tests/core/pyspec/eth2spec/test/helpers/forks.py b/tests/core/pyspec/eth2spec/test/helpers/forks.py index 5e97522db..b2072171b 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/forks.py +++ b/tests/core/pyspec/eth2spec/test/helpers/forks.py @@ -1,10 +1,13 @@ from .constants import ( PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, EIP6110, + WHISK, ) def is_post_fork(a, b): + if a == WHISK: + return b in [PHASE0, ALTAIR, BELLATRIX, CAPELLA, WHISK] if a == EIP6110: return b in [PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, EIP6110] if a == DENEB: @@ -38,3 +41,7 @@ def is_post_deneb(spec): def is_post_eip6110(spec): return is_post_fork(spec.fork, EIP6110) + + +def is_post_whisk(spec): + return is_post_fork(spec.fork, WHISK) \ No newline at end of file diff --git a/tests/core/pyspec/eth2spec/test/helpers/genesis.py b/tests/core/pyspec/eth2spec/test/helpers/genesis.py index fea259013..2ddf23965 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/genesis.py +++ b/tests/core/pyspec/eth2spec/test/helpers/genesis.py @@ -1,13 +1,13 @@ from eth2spec.test.helpers.constants import ( - ALTAIR, BELLATRIX, CAPELLA, DENEB, EIP6110, + ALTAIR, BELLATRIX, CAPELLA, DENEB, EIP6110, WHISK, ) from eth2spec.test.helpers.execution_payload import ( compute_el_header_block_hash, ) from eth2spec.test.helpers.forks import ( - is_post_altair, is_post_bellatrix, is_post_capella, is_post_eip6110, + is_post_altair, is_post_bellatrix, is_post_capella, is_post_eip6110, is_post_whisk, ) -from eth2spec.test.helpers.keys import pubkeys +from eth2spec.test.helpers.keys import pubkeys, whisk_ks_initial def build_mock_validator(spec, i: int, balance: int): @@ -86,6 +86,9 @@ def create_genesis_state(spec, validator_balances, activation_threshold): elif spec.fork == EIP6110: previous_version = spec.config.DENEB_FORK_VERSION current_version = spec.config.EIP6110_FORK_VERSION + elif spec.fork == WHISK: + previous_version = spec.config.CAPELLA_FORK_VERSION + current_version = spec.config.WHISK_FORK_VERSION state = spec.BeaconState( genesis_time=0, diff --git a/tests/core/pyspec/eth2spec/test/helpers/keys.py b/tests/core/pyspec/eth2spec/test/helpers/keys.py index 5e36e90df..01cf20636 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/keys.py +++ b/tests/core/pyspec/eth2spec/test/helpers/keys.py @@ -4,3 +4,13 @@ from py_ecc.bls import G2ProofOfPossession as bls privkeys = [i + 1 for i in range(32 * 256)] pubkeys = [bls.SkToPk(privkey) for privkey in privkeys] pubkey_to_privkey = {pubkey: privkey for privkey, pubkey in zip(privkeys, pubkeys)} + +MAX_KEYS = 32 * 256 +whisk_ks_initial = [i for i in range(MAX_KEYS)] +# Must be unique among the set `whisk_ks_initial + whisk_ks_final` +whisk_ks_final = [MAX_KEYS + i for i in range(MAX_KEYS)] + +known_whisk_trackers = {} + +def register_known_whisk_tracker(k_r_G: bytes, index: int): + known_whisk_trackers[k_r_G] = index diff --git a/tests/core/pyspec/eth2spec/test/helpers/whisk.py b/tests/core/pyspec/eth2spec/test/helpers/whisk.py new file mode 100644 index 000000000..28ef7e40d --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/helpers/whisk.py @@ -0,0 +1,53 @@ +from typing import Tuple +from eth_typing import BLSPubkey +from py_ecc.optimized_bls12_381.optimized_curve import G1, multiply +from py_ecc.bls.g2_primitives import ( + G1_to_pubkey as py_ecc_G1_to_bytes48, + pubkey_to_G1 as py_ecc_bytes48_to_G1, +) +from curdleproofs import ( + GenerateWhiskTrackerProof, + WhiskTracker, + IsValidWhiskOpeningProof, + GenerateWhiskShuffleProof, + IsValidWhiskShuffleProof +) + +def get_whisk_k_commitment(k: int) -> BLSPubkey: + return py_ecc_G1_to_bytes48(multiply(G1, int(k))) + +def get_whisk_tracker(k: int, r: int) -> WhiskTracker: + r_G = multiply(G1, int(r)) + k_r_G = multiply(r_G, int(k)) + return WhiskTracker(py_ecc_G1_to_bytes48(r_G), py_ecc_G1_to_bytes48(k_r_G)) + +def get_whisk_tracker_and_commitment(k: int, r: int) -> Tuple[WhiskTracker, BLSPubkey]: + k_G = multiply(G1, int(k)) + r_G = multiply(G1, int(r)) + k_r_G = multiply(r_G, int(k)) + tracker = WhiskTracker(py_ecc_G1_to_bytes48(r_G), py_ecc_G1_to_bytes48(k_r_G)) + commitment = py_ecc_G1_to_bytes48(k_G) + return tracker, commitment + +# Trigger condition for first proposal +def set_as_first_proposal(spec, state, proposer_index: int): + # Ensure tracker is empty to prevent breaking it + assert state.validators[proposer_index].whisk_tracker.r_G == spec.BLSG1Point() + state.validators[proposer_index].whisk_tracker.r_G = spec.BLS_G1_GENERATOR + + +def is_first_proposal(spec, state, proposer_index: int) -> bool: + return state.validators[proposer_index].whisk_tracker.r_G == spec.BLS_G1_GENERATOR + +def set_registration(body, k: int, r: int): + tracker, k_commitment = get_whisk_tracker_and_commitment(k, r) + body.whisk_registration_proof = GenerateWhiskTrackerProof(tracker, k) + body.whisk_tracker = tracker + body.whisk_k_commitment = k_commitment + +def set_opening_proof(spec, state, block, proposer_index: int, k: int, r: int): + tracker, k_commitment = get_whisk_tracker_and_commitment(k, r) + state.whisk_proposer_trackers[state.slot % spec.WHISK_PROPOSER_TRACKERS_COUNT] = tracker + state.validators[proposer_index].whisk_k_commitment = k_commitment + block.proposer_index = proposer_index + block.body.whisk_opening_proof = GenerateWhiskTrackerProof(tracker, k) diff --git a/tests/core/pyspec/eth2spec/test/whisk/__init__.py b/tests/core/pyspec/eth2spec/test/whisk/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/core/pyspec/eth2spec/test/whisk/block_processing/__init__.py b/tests/core/pyspec/eth2spec/test/whisk/block_processing/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/core/pyspec/eth2spec/test/whisk/block_processing/test_process_shuffled_trackers.py b/tests/core/pyspec/eth2spec/test/whisk/block_processing/test_process_shuffled_trackers.py new file mode 100644 index 000000000..93490d36b --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/whisk/block_processing/test_process_shuffled_trackers.py @@ -0,0 +1,148 @@ +from eth2spec.test.context import spec_state_test, with_whisk_and_later, expect_assertion_error +from eth2spec.test.helpers.keys import whisk_ks_initial +from eth2spec.test.helpers.whisk import get_whisk_tracker +from curdleproofs import GenerateWhiskShuffleProof + +def set_correct_shuffle_proofs(spec, state, body): + pre_shuffle_trackers = get_and_populate_pre_shuffle_trackers(spec, state, body) + + post_trackers, m, shuffle_proof = GenerateWhiskShuffleProof(spec.CURDLEPROOFS_CRS, pre_shuffle_trackers) + body.whisk_post_shuffle_trackers = post_trackers + body.whisk_shuffle_proof = shuffle_proof + body.whisk_shuffle_proof_M_commitment = m + +def get_and_populate_pre_shuffle_trackers(spec, state, body): + shuffle_indices = spec.get_shuffle_indices(body.randao_reveal) + pre_shuffle_trackers = [] + for i in shuffle_indices: + # Set r to some value > 1 ( = 2+i) + tracker = get_whisk_tracker(whisk_ks_initial[i], 2 + i) + state.whisk_candidate_trackers[i] = tracker + pre_shuffle_trackers.append(tracker) + return pre_shuffle_trackers + +def get_pre_shuffle_trackers(spec, state, body): + return [state.whisk_candidate_trackers[i] for i in spec.get_shuffle_indices(body.randao_reveal)] + +def set_state_epoch(spec, state, epoch): + state.slot = epoch * spec.SLOTS_PER_EPOCH + +def set_state_epoch_selection_gap(spec, state): + set_state_epoch(spec, state, spec.WHISK_EPOCHS_PER_SHUFFLING_PHASE - 1) + +def empty_block_body(spec): + return spec.BeaconBlockBody() + +def run_process_shuffled_trackers(spec, state, body, valid=True): + yield 'pre', state + yield 'body', body + + if not valid: + expect_assertion_error(lambda: spec.process_shuffled_trackers(state, body)) + yield 'post', None + return + + spec.process_shuffled_trackers(state, body) + + yield 'post', state + + +@with_whisk_and_later +@spec_state_test +def test_shuffle_trackers(spec, state): + body = empty_block_body(spec) + set_correct_shuffle_proofs(spec, state, body) + yield from run_process_shuffled_trackers(spec, state, body) + +@with_whisk_and_later +@spec_state_test +def test_no_shuffle_minus_selection_gap(spec, state): + body = empty_block_body(spec) + set_state_epoch_selection_gap(spec, state) + yield from run_process_shuffled_trackers(spec, state, body) + +@with_whisk_and_later +@spec_state_test +def test_no_shuffle_minus_one_and_selection_gap(spec, state): + body = empty_block_body(spec) + set_state_epoch(spec, state, spec.WHISK_EPOCHS_PER_SHUFFLING_PHASE - spec.WHISK_PROPOSER_SELECTION_GAP - 1) + yield from run_process_shuffled_trackers(spec, state, body) + +@with_whisk_and_later +@spec_state_test +def test_shuffle_during_selection_gap(spec, state): + body = empty_block_body(spec) + set_correct_shuffle_proofs(spec, state, body) + set_state_epoch_selection_gap(spec, state) + yield from run_process_shuffled_trackers(spec, state, body, valid=False) + +# Invalid cases on shuffle +# - wrong m +# - wrong proof +# - wrong post shuffle + +@with_whisk_and_later +@spec_state_test +def test_invalid_shuffle_bad_m(spec, state): + body = empty_block_body(spec) + set_correct_shuffle_proofs(spec, state, body) + body.whisk_shuffle_proof_M_commitment = spec.BLSG1Point() + yield from run_process_shuffled_trackers(spec, state, body, valid=False) + +@with_whisk_and_later +@spec_state_test +def test_invalid_shuffle_bad_proof(spec, state): + body = empty_block_body(spec) + set_correct_shuffle_proofs(spec, state, body) + body.whisk_shuffle_proof = spec.WhiskShuffleProof() + yield from run_process_shuffled_trackers(spec, state, body, valid=False) + +@with_whisk_and_later +@spec_state_test +def test_invalid_shuffle_bad_trackers_zero(spec, state): + body = empty_block_body(spec) + set_correct_shuffle_proofs(spec, state, body) + body.whisk_post_shuffle_trackers[0] = spec.WhiskTracker() + yield from run_process_shuffled_trackers(spec, state, body, valid=False) + +# TODO: This test does not fail +# @with_whisk_and_later +# @spec_state_test +# def test_invalid_shuffle_bad_trackers_swap(spec, state): +# body = empty_block_body(spec) +# set_correct_shuffle_proofs(spec, state, body) +# assert body.whisk_post_shuffle_trackers[0] != body.whisk_post_shuffle_trackers[1] +# tracker_0 = body.whisk_post_shuffle_trackers[0] +# body.whisk_post_shuffle_trackers[0] = body.whisk_post_shuffle_trackers[1] +# body.whisk_post_shuffle_trackers[1] = tracker_0 +# yield from run_process_shuffled_trackers(spec, state, body, valid=False) + +# Invalid things on gap +# - not empty shuffle trackers +# - not empty m +# - not empty proof + +@with_whisk_and_later +@spec_state_test +def test_invalid_gap_non_zero_m(spec, state): + body = empty_block_body(spec) + body.whisk_shuffle_proof_M_commitment = spec.BLSG1Point('0xc00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000') + set_state_epoch_selection_gap(spec, state) + yield from run_process_shuffled_trackers(spec, state, body, valid=False) + +@with_whisk_and_later +@spec_state_test +def test_invalid_gap_non_zero_proof(spec, state): + body = empty_block_body(spec) + body.whisk_shuffle_proof = spec.WhiskShuffleProof('0xff') + set_state_epoch_selection_gap(spec, state) + yield from run_process_shuffled_trackers(spec, state, body, valid=False) + +@with_whisk_and_later +@spec_state_test +def test_invalid_gap_non_zero_trackers(spec, state): + body = empty_block_body(spec) + body.whisk_post_shuffle_trackers = get_and_populate_pre_shuffle_trackers(spec, state, body) + set_state_epoch_selection_gap(spec, state) + yield from run_process_shuffled_trackers(spec, state, body, valid=False) + diff --git a/tests/core/pyspec/eth2spec/test/whisk/block_processing/test_process_whisk_opening_proof.py b/tests/core/pyspec/eth2spec/test/whisk/block_processing/test_process_whisk_opening_proof.py new file mode 100644 index 000000000..9fc204b04 --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/whisk/block_processing/test_process_whisk_opening_proof.py @@ -0,0 +1,59 @@ +from eth2spec.test.context import spec_state_test, with_whisk_and_later, expect_assertion_error +from eth2spec.test.helpers.whisk import ( + get_whisk_k_commitment, + get_whisk_tracker, + set_opening_proof +) + +def empty_block(spec): + return spec.BeaconBlock() + +def run_process_whisk_opening_proof(spec, state, block, valid=True): + yield 'pre', state + yield 'block', block + + if not valid: + expect_assertion_error(lambda: spec.process_whisk_opening_proof(state, block)) + yield 'post', None + return + + spec.process_whisk_opening_proof(state, block) + + yield 'post', state + +PROPOSER_INDEX = 0 +K_OK = 2 +K_WRONG = 3 +R_OK = 2 +R_WRONG = 3 + +@with_whisk_and_later +@spec_state_test +def test_valid_proof(spec, state): + block = empty_block(spec) + set_opening_proof(spec, state, block, PROPOSER_INDEX, K_OK, R_OK) + run_process_whisk_opening_proof(spec, state, block) + +@with_whisk_and_later +@spec_state_test +def test_wrong_commitment(spec, state): + block = empty_block(spec) + set_opening_proof(spec, state, block, PROPOSER_INDEX, K_OK, R_OK) + state.validators[PROPOSER_INDEX].whisk_k_commitment = get_whisk_k_commitment(K_WRONG) + run_process_whisk_opening_proof(spec, state, block, valid=False) + +@with_whisk_and_later +@spec_state_test +def test_wrong_tracker_r(spec, state): + block = empty_block(spec) + set_opening_proof(spec, state, block, PROPOSER_INDEX, K_OK, R_OK) + state.whisk_proposer_trackers[state.slot % spec.WHISK_PROPOSER_TRACKERS_COUNT] = get_whisk_tracker(K_OK, R_WRONG) + run_process_whisk_opening_proof(spec, state, block, valid=False) + +@with_whisk_and_later +@spec_state_test +def test_wrong_proof(spec, state): + block = empty_block(spec) + set_opening_proof(spec, state, block, PROPOSER_INDEX, K_OK, R_OK) + block.body.whisk_opening_proof = spec.WhiskTrackerProof() + run_process_whisk_opening_proof(spec, state, block, valid=False) \ No newline at end of file diff --git a/tests/core/pyspec/eth2spec/test/whisk/block_processing/test_process_whisk_registration.py b/tests/core/pyspec/eth2spec/test/whisk/block_processing/test_process_whisk_registration.py new file mode 100644 index 000000000..25bb714b1 --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/whisk/block_processing/test_process_whisk_registration.py @@ -0,0 +1,90 @@ +from eth2spec.test.context import spec_state_test, with_whisk_and_later, expect_assertion_error +from eth2spec.test.helpers.keys import whisk_ks_initial, whisk_ks_final +from eth2spec.test.helpers.whisk import get_whisk_tracker, set_as_first_proposal, get_whisk_k_commitment, set_registration +from curdleproofs import GenerateWhiskShuffleProof + +def empty_block_body(spec): + return spec.BeaconBlockBody() + +def set_as_first_proposal_and_proposer(spec, state, proposer_index): + state.latest_block_header.proposer_index = proposer_index + set_as_first_proposal(spec, state, proposer_index) + +def run_process_whisk_registration(spec, state, body, valid=True): + yield 'pre', state + yield 'body', body + + if not valid: + expect_assertion_error(lambda: spec.process_whisk_registration(state, body)) + yield 'post', None + return + + spec.process_whisk_registration(state, body) + + yield 'post', state + +IDENTITY_R = 1 +OTHER_R = 2 +PROPOSER_INDEX = 0 +OTHER_INDEX = 1 +OTHER_K = 2 + +# First proposal + +@with_whisk_and_later +@spec_state_test +def test_first_proposal_ok(spec, state): + body = empty_block_body(spec) + set_as_first_proposal_and_proposer(spec, state, PROPOSER_INDEX) + set_registration(body, OTHER_K, OTHER_R) + yield from run_process_whisk_registration(spec, state, body) + +@with_whisk_and_later +@spec_state_test +def test_first_proposal_indentity_tracker(spec, state): + body = empty_block_body(spec) + set_as_first_proposal_and_proposer(spec, state, PROPOSER_INDEX) + set_registration(body, OTHER_K, IDENTITY_R) + yield from run_process_whisk_registration(spec, state, body, valid=False) + +@with_whisk_and_later +@spec_state_test +def test_first_proposal_non_unique_k_other(spec, state): + body = empty_block_body(spec) + set_as_first_proposal_and_proposer(spec, state, PROPOSER_INDEX) + state.validators[OTHER_INDEX].whisk_k_commitment = get_whisk_k_commitment(OTHER_K) + set_registration(body, OTHER_K, OTHER_R) + yield from run_process_whisk_registration(spec, state, body, valid=False) + +@with_whisk_and_later +@spec_state_test +def test_first_proposal_non_unique_k_self(spec, state): + body = empty_block_body(spec) + set_as_first_proposal_and_proposer(spec, state, PROPOSER_INDEX) + state.validators[PROPOSER_INDEX].whisk_k_commitment = get_whisk_k_commitment(OTHER_K) + set_registration(body, OTHER_K, OTHER_R) + yield from run_process_whisk_registration(spec, state, body, valid=False) + +@with_whisk_and_later +@spec_state_test +def test_first_proposal_invalid_proof(spec, state): + body = empty_block_body(spec) + set_as_first_proposal_and_proposer(spec, state, PROPOSER_INDEX) + set_registration(body, OTHER_K, OTHER_R) + body.whisk_tracker.k_r_G = spec.BLSG1Point() + yield from run_process_whisk_registration(spec, state, body, valid=False) + +# Second proposal + +@with_whisk_and_later +@spec_state_test +def test_second_proposal_ok(spec, state): + body = empty_block_body(spec) + yield from run_process_whisk_registration(spec, state, body) + +@with_whisk_and_later +@spec_state_test +def test_second_proposal_not_zero(spec, state): + body = empty_block_body(spec) + set_registration(body, OTHER_K, OTHER_R) + yield from run_process_whisk_registration(spec, state, body, valid=False) diff --git a/tests/core/pyspec/eth2spec/test/whisk/sanity/__init__.py b/tests/core/pyspec/eth2spec/test/whisk/sanity/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/core/pyspec/eth2spec/test/whisk/sanity/blocks/__init__.py b/tests/core/pyspec/eth2spec/test/whisk/sanity/blocks/__init__.py new file mode 100644 index 000000000..818aa2cf9 --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/whisk/sanity/blocks/__init__.py @@ -0,0 +1 @@ +from .test_whisk import * # noqa: F401 F403 diff --git a/tests/core/pyspec/eth2spec/test/whisk/sanity/blocks/test_whisk.py b/tests/core/pyspec/eth2spec/test/whisk/sanity/blocks/test_whisk.py new file mode 100644 index 000000000..1c5c0a17e --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/whisk/sanity/blocks/test_whisk.py @@ -0,0 +1,58 @@ +from eth2spec.test.helpers.block import ( + build_empty_block_for_next_slot, build_empty_block +) +from eth2spec.test.context import ( + spec_state_test, + with_whisk_and_later, + WHISK, +) +from eth2spec.test.helpers.keys import privkeys, pubkeys, whisk_ks_initial +from eth2spec.test.helpers.state import ( + state_transition_and_sign_block +) +from curdleproofs import IsValidWhiskShuffleProof +from eth2spec.test.helpers.whisk import is_first_proposal, get_whisk_tracker_and_commitment, set_as_first_proposal +from curdleproofs import WhiskTracker + +known_whisk_trackers = {} + + +def assign_proposer_at_slot(state, slot: int): + state + + +def initialize_whisk_full(spec, state): + # TODO: De-duplicate code from whisk/fork.md + for index, validator in enumerate(state.validators): + whisk_k_commitment, whisk_tracker = spec.get_initial_commitments(whisk_ks_initial[index]) + validator.whisk_k_commitment = whisk_k_commitment + validator.whisk_tracker = whisk_tracker + + # Do a candidate selection followed by a proposer selection so that we have proposers for the upcoming day + # Use an old epoch when selecting candidates so that we don't get the same seed as in the next candidate selection + spec.select_whisk_candidate_trackers(state, spec.Epoch(0)) + spec.select_whisk_proposer_trackers(state, spec.Epoch(0)) + +# Fill candidate trackers with the same tracker so shuffling does not break +def fill_candidate_trackers(spec, state, tracker: WhiskTracker): + for i in range(spec.WHISK_CANDIDATE_TRACKERS_COUNT): + state.whisk_candidate_trackers[i] = tracker + +@with_whisk_and_later +@spec_state_test +def test_whisk__process_block_single_initial(spec, state): + assert state.slot == 0 + proposer_slot_1 = 0 + tracker_slot_1, k_commitment = get_whisk_tracker_and_commitment(whisk_ks_initial[proposer_slot_1], 1) + state.validators[proposer_slot_1].whisk_k_commitment = k_commitment + state.whisk_proposer_trackers[1] = tracker_slot_1 + fill_candidate_trackers(spec, state, tracker_slot_1) + + # Produce and process a whisk block + yield 'pre', state + + block = build_empty_block(spec, state, 1, proposer_slot_1) + signed_block = state_transition_and_sign_block(spec, state, block) + + yield 'blocks', [signed_block] + yield 'post', state