diff --git a/tests/blockchain/eth/entities/actors/conftest.py b/tests/acceptance/blockchain/actors/conftest.py similarity index 100% rename from tests/blockchain/eth/entities/actors/conftest.py rename to tests/acceptance/blockchain/actors/conftest.py diff --git a/tests/blockchain/eth/entities/actors/test_bidder.py b/tests/acceptance/blockchain/actors/test_bidder.py similarity index 100% rename from tests/blockchain/eth/entities/actors/test_bidder.py rename to tests/acceptance/blockchain/actors/test_bidder.py diff --git a/tests/blockchain/eth/entities/actors/test_deployer.py b/tests/acceptance/blockchain/actors/test_deployer.py similarity index 100% rename from tests/blockchain/eth/entities/actors/test_deployer.py rename to tests/acceptance/blockchain/actors/test_deployer.py diff --git a/tests/blockchain/eth/entities/actors/test_investigator.py b/tests/acceptance/blockchain/actors/test_investigator.py similarity index 100% rename from tests/blockchain/eth/entities/actors/test_investigator.py rename to tests/acceptance/blockchain/actors/test_investigator.py diff --git a/tests/blockchain/eth/entities/actors/test_multisig_actors.py b/tests/acceptance/blockchain/actors/test_multisig_actors.py similarity index 100% rename from tests/blockchain/eth/entities/actors/test_multisig_actors.py rename to tests/acceptance/blockchain/actors/test_multisig_actors.py diff --git a/tests/blockchain/eth/entities/actors/test_policy_author.py b/tests/acceptance/blockchain/actors/test_policy_author.py similarity index 100% rename from tests/blockchain/eth/entities/actors/test_policy_author.py rename to tests/acceptance/blockchain/actors/test_policy_author.py diff --git a/tests/blockchain/eth/entities/actors/test_staker.py b/tests/acceptance/blockchain/actors/test_staker.py similarity index 100% rename from tests/blockchain/eth/entities/actors/test_staker.py rename to tests/acceptance/blockchain/actors/test_staker.py diff --git a/tests/blockchain/eth/entities/actors/test_worker.py b/tests/acceptance/blockchain/actors/test_worker.py similarity index 100% rename from tests/blockchain/eth/entities/actors/test_worker.py rename to tests/acceptance/blockchain/actors/test_worker.py diff --git a/tests/blockchain/eth/entities/agents/test_adjudicator_agent.py b/tests/acceptance/blockchain/agents/test_adjudicator_agent.py similarity index 100% rename from tests/blockchain/eth/entities/agents/test_adjudicator_agent.py rename to tests/acceptance/blockchain/agents/test_adjudicator_agent.py diff --git a/tests/blockchain/eth/entities/agents/test_policy_manager_agent.py b/tests/acceptance/blockchain/agents/test_policy_manager_agent.py similarity index 100% rename from tests/blockchain/eth/entities/agents/test_policy_manager_agent.py rename to tests/acceptance/blockchain/agents/test_policy_manager_agent.py diff --git a/tests/blockchain/eth/entities/agents/test_preallocation_escrow_agent.py b/tests/acceptance/blockchain/agents/test_preallocation_escrow_agent.py similarity index 100% rename from tests/blockchain/eth/entities/agents/test_preallocation_escrow_agent.py rename to tests/acceptance/blockchain/agents/test_preallocation_escrow_agent.py diff --git a/tests/blockchain/eth/entities/agents/test_sampling_distribution.py b/tests/acceptance/blockchain/agents/test_sampling_distribution.py similarity index 100% rename from tests/blockchain/eth/entities/agents/test_sampling_distribution.py rename to tests/acceptance/blockchain/agents/test_sampling_distribution.py diff --git a/tests/blockchain/eth/entities/agents/test_staking_escrow_agent.py b/tests/acceptance/blockchain/agents/test_staking_escrow_agent.py similarity index 100% rename from tests/blockchain/eth/entities/agents/test_staking_escrow_agent.py rename to tests/acceptance/blockchain/agents/test_staking_escrow_agent.py diff --git a/tests/blockchain/eth/entities/agents/test_token_agent.py b/tests/acceptance/blockchain/agents/test_token_agent.py similarity index 100% rename from tests/blockchain/eth/entities/agents/test_token_agent.py rename to tests/acceptance/blockchain/agents/test_token_agent.py diff --git a/tests/blockchain/eth/entities/agents/test_worklock_agent.py b/tests/acceptance/blockchain/agents/test_worklock_agent.py similarity index 100% rename from tests/blockchain/eth/entities/agents/test_worklock_agent.py rename to tests/acceptance/blockchain/agents/test_worklock_agent.py diff --git a/tests/blockchain/eth/clients/test_geth_integration.py b/tests/acceptance/blockchain/clients/test_geth_integration.py similarity index 100% rename from tests/blockchain/eth/clients/test_geth_integration.py rename to tests/acceptance/blockchain/clients/test_geth_integration.py diff --git a/tests/acceptance/blockchain/clients/test_syncing.py b/tests/acceptance/blockchain/clients/test_syncing.py new file mode 100644 index 000000000..030169bb8 --- /dev/null +++ b/tests/acceptance/blockchain/clients/test_syncing.py @@ -0,0 +1,68 @@ +""" + This file is part of nucypher. + + nucypher is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + nucypher is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with nucypher. If not, see . +""" + +import pytest + +from nucypher.blockchain.eth.clients import (EthereumClient, GethClient) +from tests.unit.test_mocked_clients import GethClientTestBlockchain, SyncedMockWeb3, SyncingMockWeb3, \ + SyncingMockWeb3NoPeers + + +def test_synced_geth_client(): + + class SyncedBlockchainInterface(GethClientTestBlockchain): + + Web3 = SyncedMockWeb3 + + interface = SyncedBlockchainInterface(provider_uri='file:///ipc.geth') + interface.connect() + + assert interface.client._has_latest_block() + assert interface.client.sync() + + +def test_unsynced_geth_client(): + + GethClient.SYNC_SLEEP_DURATION = .1 + + class NonSyncedBlockchainInterface(GethClientTestBlockchain): + + Web3 = SyncingMockWeb3 + + interface = NonSyncedBlockchainInterface(provider_uri='file:///ipc.geth') + interface.connect() + + assert interface.client._has_latest_block() is False + assert interface.client.syncing + + assert len(list(interface.client.sync())) == 8 + + +def test_no_peers_unsynced_geth_client(): + + GethClient.PEERING_TIMEOUT = .1 + + class NonSyncedNoPeersBlockchainInterface(GethClientTestBlockchain): + + Web3 = SyncingMockWeb3NoPeers + + interface = NonSyncedNoPeersBlockchainInterface(provider_uri='file:///ipc.geth') + interface.connect() + + assert interface.client._has_latest_block() is False + with pytest.raises(EthereumClient.SyncTimeout): + list(interface.client.sync()) diff --git a/tests/blockchain/eth/entities/deployers/conftest.py b/tests/acceptance/blockchain/deployers/conftest.py similarity index 100% rename from tests/blockchain/eth/entities/deployers/conftest.py rename to tests/acceptance/blockchain/deployers/conftest.py diff --git a/tests/blockchain/eth/entities/deployers/test_adjudicator_deployer.py b/tests/acceptance/blockchain/deployers/test_adjudicator_deployer.py similarity index 100% rename from tests/blockchain/eth/entities/deployers/test_adjudicator_deployer.py rename to tests/acceptance/blockchain/deployers/test_adjudicator_deployer.py diff --git a/tests/blockchain/eth/entities/deployers/test_deploy_idle_network.py b/tests/acceptance/blockchain/deployers/test_deploy_idle_network.py similarity index 100% rename from tests/blockchain/eth/entities/deployers/test_deploy_idle_network.py rename to tests/acceptance/blockchain/deployers/test_deploy_idle_network.py diff --git a/tests/blockchain/eth/entities/deployers/test_deploy_preallocations.py b/tests/acceptance/blockchain/deployers/test_deploy_preallocations.py similarity index 100% rename from tests/blockchain/eth/entities/deployers/test_deploy_preallocations.py rename to tests/acceptance/blockchain/deployers/test_deploy_preallocations.py diff --git a/tests/blockchain/eth/entities/deployers/test_interdeployer_integration.py b/tests/acceptance/blockchain/deployers/test_interdeployer_integration.py similarity index 100% rename from tests/blockchain/eth/entities/deployers/test_interdeployer_integration.py rename to tests/acceptance/blockchain/deployers/test_interdeployer_integration.py diff --git a/tests/blockchain/eth/entities/deployers/test_multisig_deployer.py b/tests/acceptance/blockchain/deployers/test_multisig_deployer.py similarity index 100% rename from tests/blockchain/eth/entities/deployers/test_multisig_deployer.py rename to tests/acceptance/blockchain/deployers/test_multisig_deployer.py diff --git a/tests/blockchain/eth/entities/deployers/test_policy_manager_deployer.py b/tests/acceptance/blockchain/deployers/test_policy_manager_deployer.py similarity index 100% rename from tests/blockchain/eth/entities/deployers/test_policy_manager_deployer.py rename to tests/acceptance/blockchain/deployers/test_policy_manager_deployer.py diff --git a/tests/blockchain/eth/entities/deployers/test_preallocation_escrow_deployer.py b/tests/acceptance/blockchain/deployers/test_preallocation_escrow_deployer.py similarity index 100% rename from tests/blockchain/eth/entities/deployers/test_preallocation_escrow_deployer.py rename to tests/acceptance/blockchain/deployers/test_preallocation_escrow_deployer.py diff --git a/tests/blockchain/eth/entities/deployers/test_staking_escrow_deployer.py b/tests/acceptance/blockchain/deployers/test_staking_escrow_deployer.py similarity index 100% rename from tests/blockchain/eth/entities/deployers/test_staking_escrow_deployer.py rename to tests/acceptance/blockchain/deployers/test_staking_escrow_deployer.py diff --git a/tests/blockchain/eth/entities/deployers/test_token_deployer.py b/tests/acceptance/blockchain/deployers/test_token_deployer.py similarity index 100% rename from tests/blockchain/eth/entities/deployers/test_token_deployer.py rename to tests/acceptance/blockchain/deployers/test_token_deployer.py diff --git a/tests/blockchain/eth/entities/deployers/test_worklock_deployer.py b/tests/acceptance/blockchain/deployers/test_worklock_deployer.py similarity index 100% rename from tests/blockchain/eth/entities/deployers/test_worklock_deployer.py rename to tests/acceptance/blockchain/deployers/test_worklock_deployer.py diff --git a/tests/blockchain/eth/interfaces/contracts/multiversion/v1/VersionTest.sol b/tests/acceptance/blockchain/interfaces/contracts/multiversion/v1/VersionTest.sol similarity index 100% rename from tests/blockchain/eth/interfaces/contracts/multiversion/v1/VersionTest.sol rename to tests/acceptance/blockchain/interfaces/contracts/multiversion/v1/VersionTest.sol diff --git a/tests/blockchain/eth/interfaces/contracts/multiversion/v2/VersionTest.sol b/tests/acceptance/blockchain/interfaces/contracts/multiversion/v2/VersionTest.sol similarity index 100% rename from tests/blockchain/eth/interfaces/contracts/multiversion/v2/VersionTest.sol rename to tests/acceptance/blockchain/interfaces/contracts/multiversion/v2/VersionTest.sol diff --git a/tests/blockchain/eth/interfaces/test_chains.py b/tests/acceptance/blockchain/interfaces/test_chains.py similarity index 100% rename from tests/blockchain/eth/interfaces/test_chains.py rename to tests/acceptance/blockchain/interfaces/test_chains.py diff --git a/tests/blockchain/eth/interfaces/test_registry.py b/tests/acceptance/blockchain/interfaces/test_preallocation_registry.py similarity index 56% rename from tests/blockchain/eth/interfaces/test_registry.py rename to tests/acceptance/blockchain/interfaces/test_preallocation_registry.py index 8993ecbff..1016ccd4f 100644 --- a/tests/blockchain/eth/interfaces/test_registry.py +++ b/tests/acceptance/blockchain/interfaces/test_preallocation_registry.py @@ -20,71 +20,10 @@ import json import pytest from nucypher.blockchain.eth.constants import PREALLOCATION_ESCROW_CONTRACT_NAME -from nucypher.blockchain.eth.interfaces import BaseContractRegistry -from nucypher.blockchain.eth.registry import IndividualAllocationRegistry, LocalContractRegistry +from nucypher.blockchain.eth.registry import IndividualAllocationRegistry from nucypher.config.constants import TEMPORARY_DOMAIN -def test_contract_registry(tempfile_path): - - # ABC - with pytest.raises(TypeError): - BaseContractRegistry(filepath='test') - - with pytest.raises(BaseContractRegistry.RegistryError): - bad_registry = LocalContractRegistry(filepath='/fake/file/path/registry.json') - bad_registry.search(contract_address='0xdeadbeef') - - # Tests everything is as it should be when initially created - test_registry = LocalContractRegistry(filepath=tempfile_path) - - assert test_registry.read() == list() - - # Test contract enrollment and dump_chain - test_name = 'TestContract' - test_addr = '0xDEADBEEF' - test_abi = ['fake', 'data'] - test_version = "some_version" - - test_registry.enroll(contract_name=test_name, - contract_address=test_addr, - contract_abi=test_abi, - contract_version=test_version) - - # Search by name... - contract_records = test_registry.search(contract_name=test_name) - assert len(contract_records) == 1, 'More than one record for {}'.format(test_name) - assert len(contract_records[0]) == 4, 'Registry record is the wrong length' - name, version, address, abi = contract_records[0] - - assert name == test_name - assert address == test_addr - assert abi == test_abi - assert version == test_version - - # ...or by address - contract_record = test_registry.search(contract_address=test_addr) - name, version, address, abi = contract_record - - assert name == test_name - assert address == test_addr - assert abi == test_abi - assert version == test_version - - # Check that searching for an unknown contract raises - with pytest.raises(BaseContractRegistry.UnknownContract): - test_registry.search(contract_name='this does not exist') - - current_dataset = test_registry.read() - # Corrupt the registry with a duplicate address - current_dataset.append([test_name, test_addr, test_abi]) - test_registry.write(current_dataset) - - # Check that searching for an unknown contract raises - with pytest.raises(BaseContractRegistry.InvalidRegistry): - test_registry.search(contract_address=test_addr) - - def test_individual_allocation_registry(get_random_checksum_address, test_registry, tempfile_path, diff --git a/tests/blockchain/eth/interfaces/test_solidity_compiler.py b/tests/acceptance/blockchain/interfaces/test_solidity_compiler.py similarity index 100% rename from tests/blockchain/eth/interfaces/test_solidity_compiler.py rename to tests/acceptance/blockchain/interfaces/test_solidity_compiler.py diff --git a/tests/blockchain/eth/interfaces/test_token_and_stake.py b/tests/acceptance/blockchain/interfaces/test_token_and_stake.py similarity index 56% rename from tests/blockchain/eth/interfaces/test_token_and_stake.py rename to tests/acceptance/blockchain/interfaces/test_token_and_stake.py index 26fd955c1..de841148c 100644 --- a/tests/blockchain/eth/interfaces/test_token_and_stake.py +++ b/tests/acceptance/blockchain/interfaces/test_token_and_stake.py @@ -15,110 +15,12 @@ along with nucypher. If not, see . """ -import pytest -from decimal import Decimal, InvalidOperation from web3 import Web3 from nucypher.blockchain.eth.token import NU, Stake from tests.constants import INSECURE_DEVELOPMENT_PASSWORD -def test_NU(token_economics): - - # Starting Small - min_allowed_locked = NU(token_economics.minimum_allowed_locked, 'NuNit') - assert token_economics.minimum_allowed_locked == int(min_allowed_locked.to_nunits()) - - min_NU_locked = int(str(token_economics.minimum_allowed_locked)[0:-18]) - expected = NU(min_NU_locked, 'NU') - assert min_allowed_locked == expected - - # Starting Big - min_allowed_locked = NU(min_NU_locked, 'NU') - assert token_economics.minimum_allowed_locked == int(min_allowed_locked) - assert token_economics.minimum_allowed_locked == int(min_allowed_locked.to_nunits()) - assert str(min_allowed_locked) == '15000 NU' - - # Alternate construction - assert NU(1, 'NU') == NU('1.0', 'NU') == NU(1.0, 'NU') - - # Arithmetic - - # NUs - one_nu = NU(1, 'NU') - zero_nu = NU(0, 'NU') - one_hundred_nu = NU(100, 'NU') - two_hundred_nu = NU(200, 'NU') - three_hundred_nu = NU(300, 'NU') - - # Nits - one_nu_wei = NU(1, 'NuNit') - three_nu_wei = NU(3, 'NuNit') - assert three_nu_wei.to_tokens() == Decimal('3E-18') - assert one_nu_wei.to_tokens() == Decimal('1E-18') - - # Base Operations - assert one_hundred_nu < two_hundred_nu < three_hundred_nu - assert one_hundred_nu <= two_hundred_nu <= three_hundred_nu - - assert three_hundred_nu > two_hundred_nu > one_hundred_nu - assert three_hundred_nu >= two_hundred_nu >= one_hundred_nu - - assert (one_hundred_nu + two_hundred_nu) == three_hundred_nu - assert (three_hundred_nu - two_hundred_nu) == one_hundred_nu - - difference = one_nu - one_nu_wei - assert not difference == zero_nu - - actual = float(difference.to_tokens()) - expected = 0.999999999999999999 - assert actual == expected - - # 3.14 NU is 3_140_000_000_000_000_000 NuNit - pi_nuweis = NU(3.14, 'NU') - assert NU('3.14', 'NU') == pi_nuweis.to_nunits() == NU(3_140_000_000_000_000_000, 'NuNit') - - # Mixed type operations - difference = NU('3.14159265', 'NU') - NU(1.1, 'NU') - assert difference == NU('2.04159265', 'NU') - - result = difference + one_nu_wei - assert result == NU(2041592650000000001, 'NuNit') - - # Similar to stake read + metadata operations in Staker - collection = [one_hundred_nu, two_hundred_nu, three_hundred_nu] - assert sum(collection) == NU('600', 'NU') == NU(600, 'NU') == NU(600.0, 'NU') == NU(600e+18, 'NuNit') - - # - # Fractional Inputs - # - - # A decimal amount of NuNit (i.e., a fraction of a NuNit) - pi_nuweis = NU('3.14', 'NuNit') - assert pi_nuweis == three_nu_wei # Floor - - # A decimal amount of NU, which amounts to NuNit with decimals - pi_nus = NU('3.14159265358979323846', 'NU') - assert pi_nus == NU(3141592653589793238, 'NuNit') # Floor - - # Positive Infinity - with pytest.raises(NU.InvalidAmount): - _inf = NU(float('infinity'), 'NU') - - # Negative Infinity - with pytest.raises(NU.InvalidAmount): - _neg_inf = NU(float('-infinity'), 'NU') - - # Not a Number - with pytest.raises(InvalidOperation): - _nan = NU(float('NaN'), 'NU') - - # Rounding NUs - assert round(pi_nus, 2) == NU("3.14", "NU") - assert round(pi_nus, 1) == NU("3.1", "NU") - assert round(pi_nus, 0) == round(pi_nus) == NU("3", "NU") - - def test_stake(testerchain, token_economics, agency): token_agent, staking_agent, _policy_agent = agency diff --git a/tests/acceptance/blockchain/test_economics_acceptance.py b/tests/acceptance/blockchain/test_economics_acceptance.py new file mode 100644 index 000000000..f12376c6b --- /dev/null +++ b/tests/acceptance/blockchain/test_economics_acceptance.py @@ -0,0 +1,30 @@ +""" + This file is part of nucypher. + + nucypher is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + nucypher is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with nucypher. If not, see . +""" + +import pytest + +from nucypher.blockchain.economics import EconomicsFactory + + +@pytest.mark.usefixtures('agency') +def test_retrieving_from_blockchain(token_economics, test_registry): + + economics = EconomicsFactory.get_economics(registry=test_registry) + + assert economics.staking_deployment_parameters == token_economics.staking_deployment_parameters + assert economics.slashing_deployment_parameters == token_economics.slashing_deployment_parameters + assert economics.worklock_deployment_parameters == token_economics.worklock_deployment_parameters diff --git a/tests/characters/conftest.py b/tests/acceptance/characters/conftest.py similarity index 100% rename from tests/characters/conftest.py rename to tests/acceptance/characters/conftest.py diff --git a/tests/characters/control/blockchain/conftest.py b/tests/acceptance/characters/control/conftest.py similarity index 100% rename from tests/characters/control/blockchain/conftest.py rename to tests/acceptance/characters/control/conftest.py diff --git a/tests/characters/control/blockchain/test_rpc_control_blockchain.py b/tests/acceptance/characters/control/test_rpc_control_blockchain.py similarity index 100% rename from tests/characters/control/blockchain/test_rpc_control_blockchain.py rename to tests/acceptance/characters/control/test_rpc_control_blockchain.py diff --git a/tests/characters/control/blockchain/test_web_control_blockchain.py b/tests/acceptance/characters/control/test_web_control_blockchain.py similarity index 100% rename from tests/characters/control/blockchain/test_web_control_blockchain.py rename to tests/acceptance/characters/control/test_web_control_blockchain.py diff --git a/tests/acceptance/characters/test_decentralized_grant.py b/tests/acceptance/characters/test_decentralized_grant.py new file mode 100644 index 000000000..77372348b --- /dev/null +++ b/tests/acceptance/characters/test_decentralized_grant.py @@ -0,0 +1,84 @@ +""" +This file is part of nucypher. + +nucypher is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +nucypher is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with nucypher. If not, see . +""" + +import datetime +import maya +import pytest +from umbral.kfrags import KFrag + +from nucypher.crypto.api import keccak_digest +from nucypher.policy.collections import PolicyCredential + + +@pytest.mark.usefixtures('blockchain_ursulas') +def test_decentralized_grant(blockchain_alice, blockchain_bob, agency): + # Setup the policy details + n = 3 + policy_end_datetime = maya.now() + datetime.timedelta(days=5) + label = b"this_is_the_path_to_which_access_is_being_granted" + + # Create the Policy, Granting access to Bob + policy = blockchain_alice.grant(bob=blockchain_bob, + label=label, + m=2, + n=n, + rate=int(1e18), # one ether + expiration=policy_end_datetime) + + # Check the policy ID + policy_id = keccak_digest(policy.label + bytes(policy.bob.stamp)) + assert policy_id == policy.id + + # The number of accepted arrangements at least the number of Ursulas we're using (n) + assert len(policy._accepted_arrangements) >= n + + # The number of actually enacted arrangements is exactly equal to n. + assert len(policy._enacted_arrangements) == n + + # Let's look at the enacted arrangements. + for kfrag in policy.kfrags: + arrangement = policy._enacted_arrangements[kfrag] + + # Get the Arrangement from Ursula's datastore, looking up by the Arrangement ID. + retrieved_policy = arrangement.ursula.datastore.get_policy_arrangement(arrangement.id.hex().encode()) + retrieved_kfrag = KFrag.from_bytes(retrieved_policy.kfrag) + + assert kfrag == retrieved_kfrag + + # Test PolicyCredential w/o TreasureMap + credential = policy.credential(with_treasure_map=False) + assert credential.alice_verifying_key == policy.alice.stamp + assert credential.label == policy.label + assert credential.expiration == policy.expiration + assert credential.policy_pubkey == policy.public_key + assert credential.treasure_map is None + + cred_json = credential.to_json() + deserialized_cred = PolicyCredential.from_json(cred_json) + assert credential == deserialized_cred + + # Test PolicyCredential w/ TreasureMap + credential = policy.credential() + assert credential.alice_verifying_key == policy.alice.stamp + assert credential.label == policy.label + assert credential.expiration == policy.expiration + assert credential.policy_pubkey == policy.public_key + assert credential.treasure_map == policy.treasure_map + + cred_json = credential.to_json() + deserialized_cred = PolicyCredential.from_json(cred_json) + assert credential == deserialized_cred diff --git a/tests/characters/test_freerider_attacks.py b/tests/acceptance/characters/test_freerider_attacks.py similarity index 100% rename from tests/characters/test_freerider_attacks.py rename to tests/acceptance/characters/test_freerider_attacks.py diff --git a/tests/characters/test_stakeholder.py b/tests/acceptance/characters/test_stakeholder.py similarity index 100% rename from tests/characters/test_stakeholder.py rename to tests/acceptance/characters/test_stakeholder.py diff --git a/tests/acceptance/characters/test_transacting_power_acceptance.py b/tests/acceptance/characters/test_transacting_power_acceptance.py new file mode 100644 index 000000000..608fd3d11 --- /dev/null +++ b/tests/acceptance/characters/test_transacting_power_acceptance.py @@ -0,0 +1,71 @@ +""" +This file is part of nucypher. + +nucypher is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +nucypher is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with nucypher. If not, see . +""" + +from eth_account._utils.transactions import Transaction +from eth_utils import to_checksum_address + +from nucypher.blockchain.eth.signers import Web3Signer +from nucypher.characters.lawful import Character +from nucypher.crypto.api import verify_eip_191 +from nucypher.crypto.powers import (TransactingPower) +from tests.constants import INSECURE_DEVELOPMENT_PASSWORD + + +def test_character_transacting_power_signing(testerchain, agency, test_registry): + + # Pretend to be a character. + eth_address = testerchain.etherbase_account + signer = Character(is_me=True, registry=test_registry, checksum_address=eth_address) + + # Manually consume the power up + transacting_power = TransactingPower(password=INSECURE_DEVELOPMENT_PASSWORD, + signer=Web3Signer(testerchain.client), + account=eth_address) + + signer._crypto_power.consume_power_up(transacting_power) + + # Retrieve the power up + power = signer._crypto_power.power_ups(TransactingPower) + + assert power == transacting_power + assert testerchain.transacting_power == power + + assert power.is_active is True + assert power.is_unlocked is True + assert testerchain.transacting_power.is_unlocked is True + + # Sign Message + data_to_sign = b'Premium Select Luxury Pencil Holder' + signature = power.sign_message(message=data_to_sign) + is_verified = verify_eip_191(address=eth_address, message=data_to_sign, signature=signature) + assert is_verified is True + + # Sign Transaction + transaction_dict = {'nonce': testerchain.client.w3.eth.getTransactionCount(eth_address), + 'gasPrice': testerchain.client.w3.eth.gasPrice, + 'gas': 100000, + 'from': eth_address, + 'to': testerchain.unassigned_accounts[1], + 'value': 1, + 'data': b''} + + signed_transaction = power.sign_transaction(transaction_dict=transaction_dict) + + # Demonstrate that the transaction is valid RLP encoded. + restored_transaction = Transaction.from_bytes(serialized_bytes=signed_transaction) + restored_dict = restored_transaction.as_dict() + assert to_checksum_address(restored_dict['to']) == transaction_dict['to'] diff --git a/tests/characters/test_ursula_prepares_to_act_as_mining_node.py b/tests/acceptance/characters/test_ursula_prepares_to_act_as_worker.py similarity index 85% rename from tests/characters/test_ursula_prepares_to_act_as_mining_node.py rename to tests/acceptance/characters/test_ursula_prepares_to_act_as_worker.py index 4ac3e693e..1149a5d44 100644 --- a/tests/characters/test_ursula_prepares_to_act_as_mining_node.py +++ b/tests/acceptance/characters/test_ursula_prepares_to_act_as_worker.py @@ -25,30 +25,8 @@ from nucypher.crypto.api import verify_eip_191 from nucypher.crypto.powers import SigningPower from nucypher.policy.policies import Policy from tests.constants import INSECURE_DEVELOPMENT_PASSWORD -from tests.utils.middleware import MockRestMiddleware, NodeIsDownMiddleware -from tests.utils.ursula import make_decentralized_ursulas, make_federated_ursulas - - -def test_new_federated_ursula_announces_herself(ursula_federated_test_config): - ursula_in_a_house, ursula_with_a_mouse = make_federated_ursulas(ursula_config=ursula_federated_test_config, - quantity=2, - know_each_other=False, - network_middleware=MockRestMiddleware()) - - # Neither Ursula knows about the other. - assert ursula_in_a_house.known_nodes == ursula_with_a_mouse.known_nodes - - ursula_in_a_house.remember_node(ursula_with_a_mouse) - - # OK, now, ursula_in_a_house knows about ursula_with_a_mouse, but not vice-versa. - assert ursula_with_a_mouse in ursula_in_a_house.known_nodes - assert ursula_in_a_house not in ursula_with_a_mouse.known_nodes - - # But as ursula_in_a_house learns, she'll announce herself to ursula_with_a_mouse. - ursula_in_a_house.learn_from_teacher_node() - - assert ursula_with_a_mouse in ursula_in_a_house.known_nodes - assert ursula_in_a_house in ursula_with_a_mouse.known_nodes +from tests.utils.middleware import NodeIsDownMiddleware +from tests.utils.ursula import make_decentralized_ursulas def test_stakers_bond_to_ursulas(testerchain, test_registry, stakers, ursula_decentralized_test_config): diff --git a/tests/characters/test_ursula_web_status.py b/tests/acceptance/characters/test_ursula_web_status.py similarity index 100% rename from tests/characters/test_ursula_web_status.py rename to tests/acceptance/characters/test_ursula_web_status.py diff --git a/tests/cli/test_alice.py b/tests/acceptance/cli/test_alice.py similarity index 100% rename from tests/cli/test_alice.py rename to tests/acceptance/cli/test_alice.py diff --git a/tests/cli/test_bob.py b/tests/acceptance/cli/test_bob.py similarity index 100% rename from tests/cli/test_bob.py rename to tests/acceptance/cli/test_bob.py diff --git a/tests/cli/test_cli_config.py b/tests/acceptance/cli/test_cli_config.py similarity index 100% rename from tests/cli/test_cli_config.py rename to tests/acceptance/cli/test_cli_config.py diff --git a/tests/cli/test_cli_lifecycle.py b/tests/acceptance/cli/test_cli_lifecycle.py similarity index 100% rename from tests/cli/test_cli_lifecycle.py rename to tests/acceptance/cli/test_cli_lifecycle.py diff --git a/tests/cli/test_deploy.py b/tests/acceptance/cli/test_deploy.py similarity index 100% rename from tests/cli/test_deploy.py rename to tests/acceptance/cli/test_deploy.py diff --git a/tests/cli/test_deploy_commands.py b/tests/acceptance/cli/test_deploy_commands.py similarity index 100% rename from tests/cli/test_deploy_commands.py rename to tests/acceptance/cli/test_deploy_commands.py diff --git a/tests/cli/test_enrico.py b/tests/acceptance/cli/test_enrico.py similarity index 100% rename from tests/cli/test_enrico.py rename to tests/acceptance/cli/test_enrico.py diff --git a/tests/cli/test_felix.py b/tests/acceptance/cli/test_felix.py similarity index 100% rename from tests/cli/test_felix.py rename to tests/acceptance/cli/test_felix.py diff --git a/tests/cli/test_help.py b/tests/acceptance/cli/test_help.py similarity index 100% rename from tests/cli/test_help.py rename to tests/acceptance/cli/test_help.py diff --git a/tests/cli/test_mixed_configurations.py b/tests/acceptance/cli/test_mixed_configurations.py similarity index 100% rename from tests/cli/test_mixed_configurations.py rename to tests/acceptance/cli/test_mixed_configurations.py diff --git a/tests/cli/test_multisig_cli.py b/tests/acceptance/cli/test_multisig_cli.py similarity index 100% rename from tests/cli/test_multisig_cli.py rename to tests/acceptance/cli/test_multisig_cli.py diff --git a/tests/cli/test_rpc_ipc_transport.py b/tests/acceptance/cli/test_rpc_ipc_transport.py similarity index 100% rename from tests/cli/test_rpc_ipc_transport.py rename to tests/acceptance/cli/test_rpc_ipc_transport.py diff --git a/tests/cli/test_status.py b/tests/acceptance/cli/test_status.py similarity index 100% rename from tests/cli/test_status.py rename to tests/acceptance/cli/test_status.py diff --git a/tests/cli/test_worklock_cli.py b/tests/acceptance/cli/test_worklock_cli.py similarity index 100% rename from tests/cli/test_worklock_cli.py rename to tests/acceptance/cli/test_worklock_cli.py diff --git a/tests/cli/ursula/test_federated_ursula.py b/tests/acceptance/cli/ursula/test_federated_ursula.py similarity index 100% rename from tests/cli/ursula/test_federated_ursula.py rename to tests/acceptance/cli/ursula/test_federated_ursula.py diff --git a/tests/cli/ursula/test_local_keystore_integration.py b/tests/acceptance/cli/ursula/test_local_keystore_integration.py similarity index 100% rename from tests/cli/ursula/test_local_keystore_integration.py rename to tests/acceptance/cli/ursula/test_local_keystore_integration.py diff --git a/tests/cli/ursula/test_run_ursula.py b/tests/acceptance/cli/ursula/test_run_ursula.py similarity index 100% rename from tests/cli/ursula/test_run_ursula.py rename to tests/acceptance/cli/ursula/test_run_ursula.py diff --git a/tests/cli/ursula/test_stake_via_allocation_contract.py b/tests/acceptance/cli/ursula/test_stake_via_allocation_contract.py similarity index 100% rename from tests/cli/ursula/test_stake_via_allocation_contract.py rename to tests/acceptance/cli/ursula/test_stake_via_allocation_contract.py diff --git a/tests/cli/ursula/test_stakeholder_and_ursula.py b/tests/acceptance/cli/ursula/test_stakeholder_and_ursula.py similarity index 100% rename from tests/cli/ursula/test_stakeholder_and_ursula.py rename to tests/acceptance/cli/ursula/test_stakeholder_and_ursula.py diff --git a/tests/cli/ursula/test_ursula_command.py b/tests/acceptance/cli/ursula/test_ursula_command.py similarity index 100% rename from tests/cli/ursula/test_ursula_command.py rename to tests/acceptance/cli/ursula/test_ursula_command.py diff --git a/tests/learning/test_fault_tolerance.py b/tests/acceptance/learning/test_fault_tolerance.py similarity index 61% rename from tests/learning/test_fault_tolerance.py rename to tests/acceptance/learning/test_fault_tolerance.py index 8acbae73d..1e452e5f5 100644 --- a/tests/learning/test_fault_tolerance.py +++ b/tests/acceptance/learning/test_fault_tolerance.py @@ -15,21 +15,14 @@ along with nucypher. If not, see . """ -from collections import namedtuple - -import os import pytest -from bytestring_splitter import VariableLengthBytestring from constant_sorrow.constants import NOT_SIGNED -from eth_utils.address import to_checksum_address from twisted.logger import LogLevel, globalLogPublisher -from nucypher.characters.base import Character from nucypher.crypto.powers import TransactingPower -from nucypher.network.nicknames import nickname_from_seed from nucypher.network.nodes import FleetStateTracker, Learner from tests.utils.middleware import MockRestMiddleware -from tests.utils.ursula import make_federated_ursulas, make_ursula_for_staker +from tests.utils.ursula import make_ursula_for_staker def test_blockchain_ursula_stamp_verification_tolerance(blockchain_ursulas, mocker): @@ -176,98 +169,3 @@ def test_invalid_workers_tolerance(testerchain, assert worker not in lonely_blockchain_learner.known_nodes # TODO: Write a similar test but for detached worker (#1075) - - -def test_emit_warning_upon_new_version(ursula_federated_test_config, caplog): - nodes = make_federated_ursulas(ursula_config=ursula_federated_test_config, - quantity=3, - know_each_other=False) - teacher, learner, new_node = nodes - - learner.remember_node(teacher) - teacher.remember_node(learner) - teacher.remember_node(new_node) - - new_node.TEACHER_VERSION = learner.LEARNER_VERSION + 1 - - warnings = [] - - def warning_trapper(event): - if event['log_level'] == LogLevel.warn: - warnings.append(event) - - globalLogPublisher.addObserver(warning_trapper) - learner.learn_from_teacher_node() - - assert len(warnings) == 1 - assert warnings[0]['log_format'] == learner.unknown_version_message.format(new_node, - new_node.TEACHER_VERSION, - learner.LEARNER_VERSION) - - # Now let's go a little further: make the version totally unrecognizable. - - # First, there's enough garbage to at least scrape a potential checksum address - fleet_snapshot = os.urandom(32 + 4) - random_bytes = os.urandom(50) # lots of garbage in here - future_version = learner.LEARNER_VERSION + 42 - version_bytes = future_version.to_bytes(2, byteorder="big") - crazy_bytes = fleet_snapshot + VariableLengthBytestring(version_bytes + random_bytes) - signed_crazy_bytes = bytes(teacher.stamp(crazy_bytes)) - - Response = namedtuple("MockResponse", ("content", "status_code")) - response = Response(content=signed_crazy_bytes + crazy_bytes, status_code=200) - - learner._current_teacher_node = teacher - learner.network_middleware.get_nodes_via_rest = lambda *args, **kwargs: response - learner.learn_from_teacher_node() - - # If you really try, you can read a node representation from the garbage - accidental_checksum = to_checksum_address(random_bytes[:20]) - accidental_nickname = nickname_from_seed(accidental_checksum)[0] - accidental_node_repr = Character._display_name_template.format("Ursula", accidental_nickname, accidental_checksum) - - assert len(warnings) == 2 - assert warnings[1]['log_format'] == learner.unknown_version_message.format(accidental_node_repr, - future_version, - learner.LEARNER_VERSION) - - # This time, however, there's not enough garbage to assume there's a checksum address... - random_bytes = os.urandom(2) - crazy_bytes = fleet_snapshot + VariableLengthBytestring(version_bytes + random_bytes) - signed_crazy_bytes = bytes(teacher.stamp(crazy_bytes)) - - response = Response(content=signed_crazy_bytes + crazy_bytes, status_code=200) - - learner._current_teacher_node = teacher - learner.learn_from_teacher_node() - - assert len(warnings) == 3 - # ...so this time we get a "really unknown version message" - assert warnings[2]['log_format'] == learner.really_unknown_version_message.format(future_version, - learner.LEARNER_VERSION) - - globalLogPublisher.removeObserver(warning_trapper) - - -def test_node_posts_future_version(federated_ursulas): - ursula = list(federated_ursulas)[0] - middleware = MockRestMiddleware() - - warnings = [] - - def warning_trapper(event): - if event['log_level'] == LogLevel.warn: - warnings.append(event) - - globalLogPublisher.addObserver(warning_trapper) - - crazy_node = b"invalid-node" - middleware.get_nodes_via_rest(node=ursula, - announce_nodes=(crazy_node,)) - assert len(warnings) == 1 - future_node = list(federated_ursulas)[1] - future_node.TEACHER_VERSION = future_node.TEACHER_VERSION + 10 - future_node_bytes = bytes(future_node) - middleware.get_nodes_via_rest(node=ursula, - announce_nodes=(future_node_bytes,)) - assert len(warnings) == 2 diff --git a/tests/network/test_availability.py b/tests/acceptance/network/test_availability.py similarity index 100% rename from tests/network/test_availability.py rename to tests/acceptance/network/test_availability.py diff --git a/tests/network/test_network_actors.py b/tests/acceptance/network/test_network_actors.py similarity index 56% rename from tests/network/test_network_actors.py rename to tests/acceptance/network/test_network_actors.py index fea6b8b7a..8dca5db8f 100644 --- a/tests/network/test_network_actors.py +++ b/tests/acceptance/network/test_network_actors.py @@ -57,80 +57,6 @@ def test_blockchain_alice_finds_ursula_via_rest(blockchain_alice, blockchain_urs assert ursula in blockchain_alice.known_nodes -def test_alice_creates_policy_with_correct_hrac(idle_federated_policy): - """ - Alice creates a Policy. It has the proper HRAC, unique per her, Bob, and the label - """ - alice = idle_federated_policy.alice - bob = idle_federated_policy.bob - - assert idle_federated_policy.hrac() == keccak_digest(bytes(alice.stamp) - + bytes(bob.stamp) - + idle_federated_policy.label) - - -def test_alice_sets_treasure_map(enacted_federated_policy, federated_ursulas): - """ - Having enacted all the policies of a PolicyGroup, Alice creates a TreasureMap and ...... TODO - """ - enacted_federated_policy.publish_treasure_map(network_middleware=MockRestMiddleware()) - treasure_map_index = bytes.fromhex(enacted_federated_policy.treasure_map.public_id()) - treasure_map_as_set_on_network = list(federated_ursulas)[0].treasure_maps[treasure_map_index] - assert treasure_map_as_set_on_network == enacted_federated_policy.treasure_map - - -def test_treasure_map_stored_by_ursula_is_the_correct_one_for_bob(federated_alice, federated_bob, federated_ursulas, - enacted_federated_policy): - """ - The TreasureMap given by Alice to Ursula is the correct one for Bob; he can decrypt and read it. - """ - - treasure_map_index = bytes.fromhex(enacted_federated_policy.treasure_map.public_id()) - treasure_map_as_set_on_network = list(federated_ursulas)[0].treasure_maps[treasure_map_index] - - hrac_by_bob = federated_bob.construct_policy_hrac(federated_alice.stamp, enacted_federated_policy.label) - assert enacted_federated_policy.hrac() == hrac_by_bob - - hrac, map_id_by_bob = federated_bob.construct_hrac_and_map_id(federated_alice.stamp, enacted_federated_policy.label) - assert map_id_by_bob == treasure_map_as_set_on_network.public_id() - - -def test_bob_can_retreive_the_treasure_map_and_decrypt_it(enacted_federated_policy, federated_ursulas): - """ - Above, we showed that the TreasureMap saved on the network is the correct one for Bob. Here, we show - that Bob can retrieve it with only the information about which he is privy pursuant to the PolicyGroup. - """ - bob = enacted_federated_policy.bob - - # Of course, in the real world, Bob has sufficient information to reconstitute a PolicyGroup, gleaned, we presume, - # through a side-channel with Alice. - - # If Bob doesn't know about any Ursulas, he can't find the TreasureMap via the REST swarm: - with pytest.raises(bob.NotEnoughTeachers): - treasure_map_from_wire = bob.get_treasure_map(enacted_federated_policy.alice.stamp, - enacted_federated_policy.label) - - # Bob finds out about one Ursula (in the real world, a seed node) - bob.remember_node(list(federated_ursulas)[0]) - - # ...and then learns about the rest of the network. - bob.learn_from_teacher_node(eager=True) - - # Now he'll have better success finding that map. - treasure_map_from_wire = bob.get_treasure_map(enacted_federated_policy.alice.stamp, - enacted_federated_policy.label) - - assert enacted_federated_policy.treasure_map == treasure_map_from_wire - - -def test_treasure_map_is_legit(enacted_federated_policy): - """ - Sure, the TreasureMap can get to Bob, but we also need to know that each Ursula in the TreasureMap is on the network. - """ - for ursula_address, _node_id in enacted_federated_policy.treasure_map: - assert ursula_address in enacted_federated_policy.bob.known_nodes.addresses() - - @pytest.mark.skip("See Issue #1075") # TODO: Issue #1075 def test_vladimir_illegal_interface_key_does_not_propagate(blockchain_ursulas): """ @@ -203,28 +129,3 @@ def test_alice_refuses_to_make_arrangement_unless_ursula_is_valid(blockchain_ali idle_blockchain_policy.consider_arrangement(network_middleware=blockchain_alice.network_middleware, arrangement=FakeArrangement(), ursula=vladimir) - - -def test_alice_does_not_update_with_old_ursula_info(federated_alice, federated_ursulas): - ursula = list(federated_ursulas)[0] - old_metadata = bytes(ursula) - - # Alice has remembered Ursula. - assert federated_alice.known_nodes[ursula.checksum_address] == ursula - - # But now, Ursula wants to sign and date her interface info again. This causes a new timestamp. - ursula._sign_and_date_interface_info() - - # Indeed, her metadata is not the same now. - assert bytes(ursula) != old_metadata - - old_ursula = Ursula.from_bytes(old_metadata) - - # Once Alice learns about Ursula's updated info... - federated_alice.remember_node(ursula) - - # ...she can't learn about old ursula anymore. - federated_alice.remember_node(old_ursula) - - new_metadata = bytes(federated_alice.known_nodes[ursula.checksum_address]) - assert new_metadata != old_metadata diff --git a/tests/crypto/test_transacting_power.py b/tests/acceptance/test_transacting_power.py similarity index 100% rename from tests/crypto/test_transacting_power.py rename to tests/acceptance/test_transacting_power.py diff --git a/tests/characters/test_alice_can_grant_and_revoke.py b/tests/characters/test_alice_can_grant_and_revoke.py deleted file mode 100644 index 13439d1ba..000000000 --- a/tests/characters/test_alice_can_grant_and_revoke.py +++ /dev/null @@ -1,289 +0,0 @@ -""" -This file is part of nucypher. - -nucypher is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -nucypher is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with nucypher. If not, see . -""" - -import datetime -import maya -import os -import pytest -from umbral.kfrags import KFrag - -from nucypher.characters.lawful import Bob, Enrico -from nucypher.config.characters import AliceConfiguration -from nucypher.crypto.api import keccak_digest -from nucypher.crypto.powers import DecryptingPower, SigningPower -from nucypher.policy.collections import PolicyCredential, Revocation -from tests.constants import INSECURE_DEVELOPMENT_PASSWORD -from tests.utils.middleware import MockRestMiddleware - - -@pytest.mark.usefixtures('blockchain_ursulas') -def test_decentralized_grant(blockchain_alice, blockchain_bob, agency): - # Setup the policy details - n = 3 - policy_end_datetime = maya.now() + datetime.timedelta(days=5) - label = b"this_is_the_path_to_which_access_is_being_granted" - - # Create the Policy, Granting access to Bob - policy = blockchain_alice.grant(bob=blockchain_bob, - label=label, - m=2, - n=n, - rate=int(1e18), # one ether - expiration=policy_end_datetime) - - # Check the policy ID - policy_id = keccak_digest(policy.label + bytes(policy.bob.stamp)) - assert policy_id == policy.id - - # The number of accepted arrangements at least the number of Ursulas we're using (n) - assert len(policy._accepted_arrangements) >= n - - # The number of actually enacted arrangements is exactly equal to n. - assert len(policy._enacted_arrangements) == n - - # Let's look at the enacted arrangements. - for kfrag in policy.kfrags: - arrangement = policy._enacted_arrangements[kfrag] - - # Get the Arrangement from Ursula's datastore, looking up by the Arrangement ID. - retrieved_policy = arrangement.ursula.datastore.get_policy_arrangement(arrangement.id.hex().encode()) - retrieved_kfrag = KFrag.from_bytes(retrieved_policy.kfrag) - - assert kfrag == retrieved_kfrag - - # Test PolicyCredential w/o TreasureMap - credential = policy.credential(with_treasure_map=False) - assert credential.alice_verifying_key == policy.alice.stamp - assert credential.label == policy.label - assert credential.expiration == policy.expiration - assert credential.policy_pubkey == policy.public_key - assert credential.treasure_map is None - - cred_json = credential.to_json() - deserialized_cred = PolicyCredential.from_json(cred_json) - assert credential == deserialized_cred - - # Test PolicyCredential w/ TreasureMap - credential = policy.credential() - assert credential.alice_verifying_key == policy.alice.stamp - assert credential.label == policy.label - assert credential.expiration == policy.expiration - assert credential.policy_pubkey == policy.public_key - assert credential.treasure_map == policy.treasure_map - - cred_json = credential.to_json() - deserialized_cred = PolicyCredential.from_json(cred_json) - assert credential == deserialized_cred - - -@pytest.mark.usefixtures('federated_ursulas') -def test_federated_grant(federated_alice, federated_bob): - # Setup the policy details - m, n = 2, 3 - policy_end_datetime = maya.now() + datetime.timedelta(days=5) - label = b"this_is_the_path_to_which_access_is_being_granted" - - # Create the Policy, granting access to Bob - policy = federated_alice.grant(federated_bob, label, m=m, n=n, expiration=policy_end_datetime) - - # Check the policy ID - policy_id = keccak_digest(policy.label + bytes(policy.bob.stamp)) - assert policy_id == policy.id - - # Check Alice's active policies - assert policy_id in federated_alice.active_policies - assert federated_alice.active_policies[policy_id] == policy - - # The number of accepted arrangements at least the number of Ursulas we're using (n) - assert len(policy._accepted_arrangements) >= n - - # The number of actually enacted arrangements is exactly equal to n. - assert len(policy._enacted_arrangements) == n - - # Let's look at the enacted arrangements. - for kfrag in policy.kfrags: - arrangement = policy._enacted_arrangements[kfrag] - - # Get the Arrangement from Ursula's datastore, looking up by the Arrangement ID. - retrieved_policy = arrangement.ursula.datastore.get_policy_arrangement(arrangement.id.hex().encode()) - retrieved_kfrag = KFrag.from_bytes(retrieved_policy.kfrag) - - assert kfrag == retrieved_kfrag - - -def test_federated_alice_can_decrypt(federated_alice, federated_bob): - """ - Test that alice can decrypt data encrypted by an enrico - for her own derived policy pubkey. - """ - - # Setup the policy details - m, n = 2, 3 - policy_end_datetime = maya.now() + datetime.timedelta(days=5) - label = b"this_is_the_path_to_which_access_is_being_granted" - - policy = federated_alice.create_policy( - bob=federated_bob, - label=label, - m=m, - n=n, - expiration=policy_end_datetime, - ) - - enrico = Enrico.from_alice( - federated_alice, - policy.label, - ) - plaintext = b"this is the first thing i'm encrypting ever." - - # use the enrico to encrypt the message - message_kit, signature = enrico.encrypt_message(plaintext) - - # decrypt the data - decrypted_data = federated_alice.verify_from( - enrico, - message_kit, - signature=signature, - decrypt=True, - label=policy.label - ) - - assert plaintext == decrypted_data - - -@pytest.mark.usefixtures('federated_ursulas') -def test_revocation(federated_alice, federated_bob): - m, n = 2, 3 - policy_end_datetime = maya.now() + datetime.timedelta(days=5) - label = b"revocation test" - - policy = federated_alice.grant(federated_bob, label, m=m, n=n, expiration=policy_end_datetime) - - # Test that all arrangements are included in the RevocationKit - for node_id, arrangement_id in policy.treasure_map: - assert policy.revocation_kit[node_id].arrangement_id == arrangement_id - - # Test revocation kit's signatures - for revocation in policy.revocation_kit: - assert revocation.verify_signature(federated_alice.stamp.as_umbral_pubkey()) - - # Test Revocation deserialization - revocation = policy.revocation_kit[node_id] - revocation_bytes = bytes(revocation) - deserialized_revocation = Revocation.from_bytes(revocation_bytes) - assert deserialized_revocation == revocation - - # Attempt to revoke the new policy - failed_revocations = federated_alice.revoke(policy) - assert len(failed_revocations) == 0 - - # Try to revoke the already revoked policy - already_revoked = federated_alice.revoke(policy) - assert len(already_revoked) == 3 - - -def test_alices_powers_are_persistent(federated_ursulas, tmpdir): - # Create a non-learning AliceConfiguration - alice_config = AliceConfiguration( - config_root=os.path.join(tmpdir, 'nucypher-custom-alice-config'), - network_middleware=MockRestMiddleware(), - known_nodes=federated_ursulas, - start_learning_now=False, - federated_only=True, - save_metadata=False, - reload_metadata=False - ) - - # Generate keys and write them the disk - alice_config.initialize(password=INSECURE_DEVELOPMENT_PASSWORD) - - # Unlock Alice's keyring - alice_config.keyring.unlock(password=INSECURE_DEVELOPMENT_PASSWORD) - - # Produce an Alice - alice = alice_config() # or alice_config.produce() - - # Save Alice's node configuration file to disk for later use - alice_config_file = alice_config.to_configuration_file() - - # Let's save Alice's public keys too to check they are correctly restored later - alices_verifying_key = alice.public_keys(SigningPower) - alices_receiving_key = alice.public_keys(DecryptingPower) - - # Next, let's fix a label for all the policies we will create later. - label = b"this_is_the_path_to_which_access_is_being_granted" - - # Even before creating the policies, we can know what will be its public key. - # This can be used by Enrico (i.e., a Data Source) to encrypt messages - # before Alice grants access to Bobs. - policy_pubkey = alice.get_policy_encrypting_key_from_label(label) - - # Now, let's create a policy for some Bob. - m, n = 3, 4 - policy_end_datetime = maya.now() + datetime.timedelta(days=5) - - bob = Bob(federated_only=True, - start_learning_now=False, - network_middleware=MockRestMiddleware()) - - bob_policy = alice.grant(bob, label, m=m, n=n, expiration=policy_end_datetime) - - assert policy_pubkey == bob_policy.public_key - - # ... and Alice and her configuration disappear. - del alice - del alice_config - - ################################### - # Some time passes. # - # ... # - # (jmyles plays the Song of Time) # - # ... # - # Alice appears again. # - ################################### - - # A new Alice is restored from the configuration file - new_alice_config = AliceConfiguration.from_configuration_file( - filepath=alice_config_file, - network_middleware=MockRestMiddleware(), - known_nodes=federated_ursulas, - start_learning_now=False, - ) - - # Alice unlocks her restored keyring from disk - new_alice_config.attach_keyring() - new_alice_config.keyring.unlock(password=INSECURE_DEVELOPMENT_PASSWORD) - new_alice = new_alice_config() - - # First, we check that her public keys are correctly restored - assert alices_verifying_key == new_alice.public_keys(SigningPower) - assert alices_receiving_key == new_alice.public_keys(DecryptingPower) - - # Bob's eldest brother, Roberto, appears too - roberto = Bob(federated_only=True, - start_learning_now=False, - network_middleware=MockRestMiddleware()) - - # Alice creates a new policy for Roberto. Note how all the parameters - # except for the label (i.e., recipient, m, n, policy_end) are different - # from previous policy - m, n = 2, 5 - policy_end_datetime = maya.now() + datetime.timedelta(days=3) - roberto_policy = new_alice.grant(roberto, label, m=m, n=n, expiration=policy_end_datetime) - - # Both policies must share the same public key (i.e., the policy public key) - assert policy_pubkey == roberto_policy.public_key diff --git a/tests/characters/test_crypto_characters_and_their_powers.py b/tests/characters/test_crypto_characters_and_their_powers.py deleted file mode 100644 index 4ff064753..000000000 --- a/tests/characters/test_crypto_characters_and_their_powers.py +++ /dev/null @@ -1,284 +0,0 @@ -""" -This file is part of nucypher. - -nucypher is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -nucypher is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with nucypher. If not, see . -""" - - -import pytest -from constant_sorrow import constants -from cryptography.exceptions import InvalidSignature -from eth_account._utils.transactions import Transaction -from eth_utils import to_checksum_address - -from nucypher.blockchain.eth.signers import Web3Signer -from nucypher.characters.lawful import Alice, Bob, Character, Enrico -from nucypher.crypto import api -from nucypher.crypto.api import verify_eip_191 -from nucypher.crypto.powers import (CryptoPower, NoSigningPower, SigningPower, TransactingPower) -from tests.constants import INSECURE_DEVELOPMENT_PASSWORD - -""" -Chapter 1: SIGNING -""" - - -def test_actor_without_signing_power_cannot_sign(): - """ - We can create a Character with no real CryptoPower to speak of. - This Character can't even sign a message. - """ - cannot_sign = CryptoPower(power_ups=[]) - non_signer = Character(crypto_power=cannot_sign, - start_learning_now=False, - federated_only=True) - - # The non-signer's stamp doesn't work for signing... - with pytest.raises(NoSigningPower): - non_signer.stamp("something") - - # ...or as a way to cast the (non-existent) public key to bytes. - with pytest.raises(NoSigningPower): - bytes(non_signer.stamp) - - -def test_actor_with_signing_power_can_sign(): - """ - However, simply giving that character a PowerUp bestows the power to sign. - - Instead of having a Character verify the signature, we'll use the lower level API. - """ - message = b"Llamas." - - signer = Character(crypto_power_ups=[SigningPower], is_me=True, - start_learning_now=False, federated_only=True) - stamp_of_the_signer = signer.stamp - - # We can use the signer's stamp to sign a message (since the signer is_me)... - signature = stamp_of_the_signer(message) - - # ...or to get the signer's public key for verification purposes. - # (note: we use the private _der_encoded_bytes here to test directly against the API, instead of Character) - verification = api.verify_ecdsa(message, signature._der_encoded_bytes(), - stamp_of_the_signer.as_umbral_pubkey()) - - assert verification is True - - -def test_anybody_can_verify(): - """ - In the last example, we used the lower-level Crypto API to verify the signature. - - Here, we show that anybody can do it without needing to directly access Crypto. - """ - # Alice can sign by default, by dint of her _default_crypto_powerups. - alice = Alice(federated_only=True, start_learning_now=False) - - # So, our story is fairly simple: an everyman meets Alice. - somebody = Character(start_learning_now=False, federated_only=True) - - # Alice signs a message. - message = b"A message for all my friends who can only verify and not sign." - signature = alice.stamp(message) - - # Our everyman can verify it. - cleartext = somebody.verify_from(alice, message, signature, decrypt=False) - assert cleartext is constants.NO_DECRYPTION_PERFORMED - - # Of course, verification fails with any fake message - with pytest.raises(InvalidSignature): - fake = b"McLovin 892 Momona St. Honolulu, HI 96820" - _ = somebody.verify_from(alice, fake, signature, decrypt=False) - - # Signature verification also works when Alice is not living with our - # everyman in the same process, and he only knows her by her public key - alice_pubkey_bytes = bytes(alice.stamp) - hearsay_alice = Character.from_public_keys({SigningPower: alice_pubkey_bytes}) - - cleartext = somebody.verify_from(hearsay_alice, message, signature, decrypt=False) - assert cleartext is constants.NO_DECRYPTION_PERFORMED - - hearsay_alice = Character.from_public_keys(verifying_key=alice_pubkey_bytes) - - cleartext = somebody.verify_from(hearsay_alice, message, signature, decrypt=False) - assert cleartext is constants.NO_DECRYPTION_PERFORMED - - -def test_character_transacting_power_signing(testerchain, agency, test_registry): - - # Pretend to be a character. - eth_address = testerchain.etherbase_account - signer = Character(is_me=True, registry=test_registry, checksum_address=eth_address) - - # Manually consume the power up - transacting_power = TransactingPower(password=INSECURE_DEVELOPMENT_PASSWORD, - signer=Web3Signer(testerchain.client), - account=eth_address) - - signer._crypto_power.consume_power_up(transacting_power) - - # Retrieve the power up - power = signer._crypto_power.power_ups(TransactingPower) - - assert power == transacting_power - assert testerchain.transacting_power == power - - assert power.is_active is True - assert power.is_unlocked is True - assert testerchain.transacting_power.is_unlocked is True - - # Sign Message - data_to_sign = b'Premium Select Luxury Pencil Holder' - signature = power.sign_message(message=data_to_sign) - is_verified = verify_eip_191(address=eth_address, message=data_to_sign, signature=signature) - assert is_verified is True - - # Sign Transaction - transaction_dict = {'nonce': testerchain.client.w3.eth.getTransactionCount(eth_address), - 'gasPrice': testerchain.client.w3.eth.gasPrice, - 'gas': 100000, - 'from': eth_address, - 'to': testerchain.unassigned_accounts[1], - 'value': 1, - 'data': b''} - - signed_transaction = power.sign_transaction(transaction_dict=transaction_dict) - - # Demonstrate that the transaction is valid RLP encoded. - restored_transaction = Transaction.from_bytes(serialized_bytes=signed_transaction) - restored_dict = restored_transaction.as_dict() - assert to_checksum_address(restored_dict['to']) == transaction_dict['to'] - - -""" -Chapter 2: ENCRYPTION -""" - - -def test_anybody_can_encrypt(): - """ - Similar to anybody_can_verify() above; we show that anybody can encrypt. - """ - someone = Character(start_learning_now=False, federated_only=True) - bob = Bob(is_me=False, federated_only=True) - - cleartext = b"This is Officer Rod Farva. Come in, Ursula! Come in Ursula!" - - ciphertext, signature = someone.encrypt_for(bob, cleartext, sign=False) - - assert signature == constants.NOT_SIGNED - assert ciphertext is not None - - -def test_node_deployer(federated_ursulas): - for ursula in federated_ursulas: - deployer = ursula.get_deployer() - assert deployer.options['https_port'] == ursula.rest_information()[0].port - assert deployer.application == ursula.rest_app - - -""" -What follows are various combinations of signing and encrypting, to match -real-world scenarios. -""" - - -def test_sign_cleartext_and_encrypt(federated_alice, federated_bob): - """ - Exhibit One: federated_alice signs the cleartext and encrypts her signature inside - the ciphertext. - """ - message = b"Have you accepted my answer on StackOverflow yet?" - - message_kit, _signature = federated_alice.encrypt_for(federated_bob, message, - sign_plaintext=True) - - # Notice that our function still returns the signature here, in case federated_alice - # wants to do something else with it, such as post it publicly for later - # public verifiability. - - # However, we can expressly refrain from passing the Signature, and the - # verification still works: - cleartext = federated_bob.verify_from(federated_alice, message_kit, signature=None, - decrypt=True) - assert cleartext == message - - -def test_encrypt_and_sign_the_ciphertext(federated_alice, federated_bob): - """ - Now, federated_alice encrypts first and then signs the ciphertext, providing a - Signature that is completely separate from the message. - This is useful in a scenario in which federated_bob needs to prove authenticity - publicly without disclosing contents. - """ - message = b"We have a reaaall problem." - message_kit, signature = federated_alice.encrypt_for(federated_bob, message, - sign_plaintext=False) - cleartext = federated_bob.verify_from(federated_alice, message_kit, signature, decrypt=True) - assert cleartext == message - - -def test_encrypt_and_sign_including_signature_in_both_places(federated_alice, federated_bob): - """ - Same as above, but showing that we can include the signature in both - the plaintext (to be found upon decryption) and also passed into - verify_from() (eg, gleaned over a side-channel). - """ - message = b"We have a reaaall problem." - message_kit, signature = federated_alice.encrypt_for(federated_bob, message, - sign_plaintext=True) - cleartext = federated_bob.verify_from(federated_alice, message_kit, signature, - decrypt=True) - assert cleartext == message - - -def test_encrypt_but_do_not_sign(federated_alice, federated_bob): - """ - Finally, federated_alice encrypts but declines to sign. - This is useful in a scenario in which federated_alice wishes to plausibly disavow - having created this content. - """ - # TODO: How do we accurately demonstrate this test safely, if at all? - message = b"If Bonnie comes home and finds an unencrypted private key in her keystore, I'm gonna get divorced." - - # Alice might also want to encrypt a message but *not* sign it, in order - # to refrain from creating evidence that can prove she was the - # original sender. - message_kit, not_signature = federated_alice.encrypt_for(federated_bob, message, sign=False) - - # The message is not signed... - assert not_signature == constants.NOT_SIGNED - - # ...and thus, the message is not verified. - with pytest.raises(InvalidSignature): - federated_bob.verify_from(federated_alice, message_kit, decrypt=True) - - -def test_alice_can_decrypt(federated_alice): - label = b"boring test label" - - policy_pubkey = federated_alice.get_policy_encrypting_key_from_label(label) - - enrico = Enrico(policy_encrypting_key=policy_pubkey) - - message = b"boring test message" - message_kit, signature = enrico.encrypt_message(message=message) - - # Interesting thing: if Alice wants to decrypt, she needs to provide the label directly. - cleartext = federated_alice.verify_from(stranger=enrico, - message_kit=message_kit, - signature=signature, - decrypt=True, - label=label) - assert cleartext == message diff --git a/tests/cli/conftest.py b/tests/cli/conftest.py deleted file mode 100644 index c8a1d8882..000000000 --- a/tests/cli/conftest.py +++ /dev/null @@ -1,132 +0,0 @@ -""" -This file is part of nucypher. - -nucypher is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -nucypher is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with nucypher. If not, see . -""" - - -import contextlib -import json -from io import StringIO - -import os -import pytest -import shutil -from click.testing import CliRunner -from datetime import datetime - -from nucypher.blockchain.eth.registry import InMemoryContractRegistry, LocalContractRegistry -from nucypher.characters.control.emitters import StdoutEmitter -from nucypher.config.characters import StakeHolderConfiguration, UrsulaConfiguration -from tests.constants import ( - BASE_TEMP_DIR, - BASE_TEMP_PREFIX, - DATETIME_FORMAT, - MOCK_ALLOCATION_INFILE, - MOCK_CUSTOM_INSTALLATION_PATH, - MOCK_CUSTOM_INSTALLATION_PATH_2 -) - - -@pytest.fixture(scope='function', autouse=True) -def stdout_trap(mocker): - trap = StringIO() - mocker.patch('sys.stdout', new=trap) - return trap - - -@pytest.fixture(scope='function') -def test_emitter(mocker, stdout_trap): - mocker.patch('sys.stdout', new=stdout_trap) - return StdoutEmitter() - - -@pytest.fixture(scope='module') -def click_runner(): - runner = CliRunner() - yield runner - - -@pytest.fixture(scope='session') -def nominal_federated_configuration_fields(): - config = UrsulaConfiguration(dev_mode=True, federated_only=True) - config_fields = config.static_payload() - yield tuple(config_fields.keys()) - del config - - -@pytest.fixture(scope='module') -def mock_allocation_infile(testerchain, token_economics, get_random_checksum_address): - accounts = [get_random_checksum_address() for _ in range(10)] - # accounts = testerchain.unassigned_accounts - allocation_data = list() - amount = 2 * token_economics.minimum_allowed_locked - min_periods = token_economics.minimum_locked_periods - for account in accounts: - substake = [{'checksum_address': account, 'amount': amount, 'lock_periods': min_periods + i} for i in range(24)] - allocation_data.extend(substake) - - with open(MOCK_ALLOCATION_INFILE, 'w') as file: - file.write(json.dumps(allocation_data)) - - yield MOCK_ALLOCATION_INFILE - if os.path.isfile(MOCK_ALLOCATION_INFILE): - os.remove(MOCK_ALLOCATION_INFILE) - - -@pytest.fixture(scope='function') -def new_local_registry(): - filename = f'{BASE_TEMP_PREFIX}mock-empty-registry-{datetime.now().strftime(DATETIME_FORMAT)}.json' - registry_filepath = os.path.join(BASE_TEMP_DIR, filename) - registry = LocalContractRegistry(filepath=registry_filepath) - registry.write(InMemoryContractRegistry().read()) - yield registry - if os.path.exists(registry_filepath): - os.remove(registry_filepath) - - -@pytest.fixture(scope='module') -def custom_filepath(): - _custom_filepath = MOCK_CUSTOM_INSTALLATION_PATH - with contextlib.suppress(FileNotFoundError): - shutil.rmtree(_custom_filepath, ignore_errors=True) - yield _custom_filepath - with contextlib.suppress(FileNotFoundError): - shutil.rmtree(_custom_filepath, ignore_errors=True) - - -@pytest.fixture(scope='module') -def custom_filepath_2(): - _custom_filepath = MOCK_CUSTOM_INSTALLATION_PATH_2 - with contextlib.suppress(FileNotFoundError): - shutil.rmtree(_custom_filepath, ignore_errors=True) - try: - yield _custom_filepath - finally: - with contextlib.suppress(FileNotFoundError): - shutil.rmtree(_custom_filepath, ignore_errors=True) - - -@pytest.fixture(scope='module') -def worker_configuration_file_location(custom_filepath): - _configuration_file_location = os.path.join(MOCK_CUSTOM_INSTALLATION_PATH, - UrsulaConfiguration.generate_filename()) - return _configuration_file_location - - -@pytest.fixture(scope='module') -def stakeholder_configuration_file_location(custom_filepath): - _configuration_file_location = os.path.join(MOCK_CUSTOM_INSTALLATION_PATH, - StakeHolderConfiguration.generate_filename()) - return _configuration_file_location diff --git a/tests/blockchain/eth/contracts/base/test_dispatcher.py b/tests/contracts/base/test_dispatcher.py similarity index 100% rename from tests/blockchain/eth/contracts/base/test_dispatcher.py rename to tests/contracts/base/test_dispatcher.py diff --git a/tests/blockchain/eth/contracts/base/test_issuer.py b/tests/contracts/base/test_issuer.py similarity index 100% rename from tests/blockchain/eth/contracts/base/test_issuer.py rename to tests/contracts/base/test_issuer.py diff --git a/tests/blockchain/eth/contracts/base/test_multisig.py b/tests/contracts/base/test_multisig.py similarity index 100% rename from tests/blockchain/eth/contracts/base/test_multisig.py rename to tests/contracts/base/test_multisig.py diff --git a/tests/blockchain/eth/contracts/contracts/AdjudicatorTestSet.sol b/tests/contracts/contracts/AdjudicatorTestSet.sol similarity index 100% rename from tests/blockchain/eth/contracts/contracts/AdjudicatorTestSet.sol rename to tests/contracts/contracts/AdjudicatorTestSet.sol diff --git a/tests/blockchain/eth/contracts/contracts/IssuerTestSet.sol b/tests/contracts/contracts/IssuerTestSet.sol similarity index 100% rename from tests/blockchain/eth/contracts/contracts/IssuerTestSet.sol rename to tests/contracts/contracts/IssuerTestSet.sol diff --git a/tests/blockchain/eth/contracts/contracts/LibTestSet.sol b/tests/contracts/contracts/LibTestSet.sol similarity index 100% rename from tests/blockchain/eth/contracts/contracts/LibTestSet.sol rename to tests/contracts/contracts/LibTestSet.sol diff --git a/tests/blockchain/eth/contracts/contracts/PolicyManagerTestSet.sol b/tests/contracts/contracts/PolicyManagerTestSet.sol similarity index 100% rename from tests/blockchain/eth/contracts/contracts/PolicyManagerTestSet.sol rename to tests/contracts/contracts/PolicyManagerTestSet.sol diff --git a/tests/blockchain/eth/contracts/contracts/ReceiveApprovalMethodMock.sol b/tests/contracts/contracts/ReceiveApprovalMethodMock.sol similarity index 100% rename from tests/blockchain/eth/contracts/contracts/ReceiveApprovalMethodMock.sol rename to tests/contracts/contracts/ReceiveApprovalMethodMock.sol diff --git a/tests/blockchain/eth/contracts/contracts/ReentrancyTest.sol b/tests/contracts/contracts/ReentrancyTest.sol similarity index 100% rename from tests/blockchain/eth/contracts/contracts/ReentrancyTest.sol rename to tests/contracts/contracts/ReentrancyTest.sol diff --git a/tests/blockchain/eth/contracts/contracts/StakingContractsTestSet.sol b/tests/contracts/contracts/StakingContractsTestSet.sol similarity index 100% rename from tests/blockchain/eth/contracts/contracts/StakingContractsTestSet.sol rename to tests/contracts/contracts/StakingContractsTestSet.sol diff --git a/tests/blockchain/eth/contracts/contracts/StakingEscrowTestSet.sol b/tests/contracts/contracts/StakingEscrowTestSet.sol similarity index 100% rename from tests/blockchain/eth/contracts/contracts/StakingEscrowTestSet.sol rename to tests/contracts/contracts/StakingEscrowTestSet.sol diff --git a/tests/blockchain/eth/contracts/contracts/WorkLockTestSet.sol b/tests/contracts/contracts/WorkLockTestSet.sol similarity index 100% rename from tests/blockchain/eth/contracts/contracts/WorkLockTestSet.sol rename to tests/contracts/contracts/WorkLockTestSet.sol diff --git a/tests/blockchain/eth/contracts/contracts/proxy/BadContracts.sol b/tests/contracts/contracts/proxy/BadContracts.sol similarity index 100% rename from tests/blockchain/eth/contracts/contracts/proxy/BadContracts.sol rename to tests/contracts/contracts/proxy/BadContracts.sol diff --git a/tests/blockchain/eth/contracts/contracts/proxy/ContractV1.sol b/tests/contracts/contracts/proxy/ContractV1.sol similarity index 100% rename from tests/blockchain/eth/contracts/contracts/proxy/ContractV1.sol rename to tests/contracts/contracts/proxy/ContractV1.sol diff --git a/tests/blockchain/eth/contracts/contracts/proxy/ContractV2.sol b/tests/contracts/contracts/proxy/ContractV2.sol similarity index 100% rename from tests/blockchain/eth/contracts/contracts/proxy/ContractV2.sol rename to tests/contracts/contracts/proxy/ContractV2.sol diff --git a/tests/blockchain/eth/contracts/contracts/proxy/ContractV3.sol b/tests/contracts/contracts/proxy/ContractV3.sol similarity index 100% rename from tests/blockchain/eth/contracts/contracts/proxy/ContractV3.sol rename to tests/contracts/contracts/proxy/ContractV3.sol diff --git a/tests/blockchain/eth/contracts/contracts/proxy/ContractV4.sol b/tests/contracts/contracts/proxy/ContractV4.sol similarity index 100% rename from tests/blockchain/eth/contracts/contracts/proxy/ContractV4.sol rename to tests/contracts/contracts/proxy/ContractV4.sol diff --git a/tests/blockchain/eth/contracts/contracts/proxy/Destroyable.sol b/tests/contracts/contracts/proxy/Destroyable.sol similarity index 100% rename from tests/blockchain/eth/contracts/contracts/proxy/Destroyable.sol rename to tests/contracts/contracts/proxy/Destroyable.sol diff --git a/tests/blockchain/eth/contracts/integration/test_contract_economics.py b/tests/contracts/integration/test_contract_economics.py similarity index 100% rename from tests/blockchain/eth/contracts/integration/test_contract_economics.py rename to tests/contracts/integration/test_contract_economics.py diff --git a/tests/blockchain/eth/contracts/integration/test_intercontract_integration.py b/tests/contracts/integration/test_intercontract_integration.py similarity index 100% rename from tests/blockchain/eth/contracts/integration/test_intercontract_integration.py rename to tests/contracts/integration/test_intercontract_integration.py diff --git a/tests/blockchain/eth/contracts/lib/test_reencryption_validator.py b/tests/contracts/lib/test_reencryption_validator.py similarity index 100% rename from tests/blockchain/eth/contracts/lib/test_reencryption_validator.py rename to tests/contracts/lib/test_reencryption_validator.py diff --git a/tests/blockchain/eth/contracts/lib/test_signature_verifier.py b/tests/contracts/lib/test_signature_verifier.py similarity index 100% rename from tests/blockchain/eth/contracts/lib/test_signature_verifier.py rename to tests/contracts/lib/test_signature_verifier.py diff --git a/tests/blockchain/eth/contracts/lib/test_snapshot.py b/tests/contracts/lib/test_snapshot.py similarity index 100% rename from tests/blockchain/eth/contracts/lib/test_snapshot.py rename to tests/contracts/lib/test_snapshot.py diff --git a/tests/blockchain/eth/contracts/lib/test_umbral_deserializer.py b/tests/contracts/lib/test_umbral_deserializer.py similarity index 100% rename from tests/blockchain/eth/contracts/lib/test_umbral_deserializer.py rename to tests/contracts/lib/test_umbral_deserializer.py diff --git a/tests/blockchain/eth/contracts/main/adjudicator/conftest.py b/tests/contracts/main/adjudicator/conftest.py similarity index 100% rename from tests/blockchain/eth/contracts/main/adjudicator/conftest.py rename to tests/contracts/main/adjudicator/conftest.py diff --git a/tests/blockchain/eth/contracts/main/adjudicator/test_adjudicator.py b/tests/contracts/main/adjudicator/test_adjudicator.py similarity index 100% rename from tests/blockchain/eth/contracts/main/adjudicator/test_adjudicator.py rename to tests/contracts/main/adjudicator/test_adjudicator.py diff --git a/tests/blockchain/eth/contracts/main/policy_manager/conftest.py b/tests/contracts/main/policy_manager/conftest.py similarity index 100% rename from tests/blockchain/eth/contracts/main/policy_manager/conftest.py rename to tests/contracts/main/policy_manager/conftest.py diff --git a/tests/blockchain/eth/contracts/main/policy_manager/test_policy_manager.py b/tests/contracts/main/policy_manager/test_policy_manager.py similarity index 100% rename from tests/blockchain/eth/contracts/main/policy_manager/test_policy_manager.py rename to tests/contracts/main/policy_manager/test_policy_manager.py diff --git a/tests/blockchain/eth/contracts/main/policy_manager/test_policy_manager_operations.py b/tests/contracts/main/policy_manager/test_policy_manager_operations.py similarity index 100% rename from tests/blockchain/eth/contracts/main/policy_manager/test_policy_manager_operations.py rename to tests/contracts/main/policy_manager/test_policy_manager_operations.py diff --git a/tests/blockchain/eth/contracts/main/staking_contracts/conftest.py b/tests/contracts/main/staking_contracts/conftest.py similarity index 100% rename from tests/blockchain/eth/contracts/main/staking_contracts/conftest.py rename to tests/contracts/main/staking_contracts/conftest.py diff --git a/tests/blockchain/eth/contracts/main/staking_contracts/test_preallocation_escrow.py b/tests/contracts/main/staking_contracts/test_preallocation_escrow.py similarity index 100% rename from tests/blockchain/eth/contracts/main/staking_contracts/test_preallocation_escrow.py rename to tests/contracts/main/staking_contracts/test_preallocation_escrow.py diff --git a/tests/blockchain/eth/contracts/main/staking_contracts/test_staking_interface.py b/tests/contracts/main/staking_contracts/test_staking_interface.py similarity index 100% rename from tests/blockchain/eth/contracts/main/staking_contracts/test_staking_interface.py rename to tests/contracts/main/staking_contracts/test_staking_interface.py diff --git a/tests/blockchain/eth/contracts/main/staking_contracts/test_staking_interface_router.py b/tests/contracts/main/staking_contracts/test_staking_interface_router.py similarity index 100% rename from tests/blockchain/eth/contracts/main/staking_contracts/test_staking_interface_router.py rename to tests/contracts/main/staking_contracts/test_staking_interface_router.py diff --git a/tests/blockchain/eth/contracts/main/staking_escrow/conftest.py b/tests/contracts/main/staking_escrow/conftest.py similarity index 100% rename from tests/blockchain/eth/contracts/main/staking_escrow/conftest.py rename to tests/contracts/main/staking_escrow/conftest.py diff --git a/tests/blockchain/eth/contracts/main/staking_escrow/test_staking.py b/tests/contracts/main/staking_escrow/test_staking.py similarity index 100% rename from tests/blockchain/eth/contracts/main/staking_escrow/test_staking.py rename to tests/contracts/main/staking_escrow/test_staking.py diff --git a/tests/blockchain/eth/contracts/main/staking_escrow/test_staking_escrow.py b/tests/contracts/main/staking_escrow/test_staking_escrow.py similarity index 100% rename from tests/blockchain/eth/contracts/main/staking_escrow/test_staking_escrow.py rename to tests/contracts/main/staking_escrow/test_staking_escrow.py diff --git a/tests/blockchain/eth/contracts/main/staking_escrow/test_staking_escrow_additional.py b/tests/contracts/main/staking_escrow/test_staking_escrow_additional.py similarity index 100% rename from tests/blockchain/eth/contracts/main/staking_escrow/test_staking_escrow_additional.py rename to tests/contracts/main/staking_escrow/test_staking_escrow_additional.py diff --git a/tests/blockchain/eth/contracts/main/token/test_token.py b/tests/contracts/main/token/test_token.py similarity index 100% rename from tests/blockchain/eth/contracts/main/token/test_token.py rename to tests/contracts/main/token/test_token.py diff --git a/tests/blockchain/eth/contracts/main/worklock/test_worklock.py b/tests/contracts/main/worklock/test_worklock.py similarity index 100% rename from tests/blockchain/eth/contracts/main/worklock/test_worklock.py rename to tests/contracts/main/worklock/test_worklock.py diff --git a/tests/blockchain/eth/contracts/test_contracts_upgradeability.py b/tests/contracts/test_contracts_upgradeability.py similarity index 100% rename from tests/blockchain/eth/contracts/test_contracts_upgradeability.py rename to tests/contracts/test_contracts_upgradeability.py diff --git a/tests/fixtures.py b/tests/fixtures.py index 756141fdb..de2e26c84 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -16,14 +16,19 @@ along with nucypher. If not, see . """ +import contextlib +import json import random -import datetime import maya import os import pytest +import shutil import tempfile +from click.testing import CliRunner +from datetime import datetime, timedelta from eth_utils import to_checksum_address +from io import StringIO from sqlalchemy.engine import create_engine from twisted.logger import Logger from typing import Tuple @@ -46,14 +51,11 @@ from nucypher.blockchain.eth.deployers import ( WorklockDeployer ) from nucypher.blockchain.eth.interfaces import BlockchainInterfaceFactory -from nucypher.blockchain.eth.networks import NetworksInventory -from nucypher.blockchain.eth.registry import ( - InMemoryContractRegistry, - LocalContractRegistry -) +from nucypher.blockchain.eth.registry import InMemoryContractRegistry, LocalContractRegistry from nucypher.blockchain.eth.signers import Web3Signer from nucypher.blockchain.eth.sol.compile import SolidityCompiler from nucypher.blockchain.eth.token import NU +from nucypher.characters.control.emitters import StdoutEmitter from nucypher.characters.lawful import Bob, Enrico from nucypher.config.characters import ( AliceConfiguration, @@ -69,12 +71,18 @@ from nucypher.datastore.db import Base from nucypher.policy.collections import IndisputableEvidence, WorkOrder from nucypher.utilities.logging import GlobalLoggerSettings from tests.constants import ( + BASE_TEMP_DIR, + BASE_TEMP_PREFIX, BONUS_TOKENS_FOR_TESTS, + DATETIME_FORMAT, DEVELOPMENT_ETH_AIRDROP_AMOUNT, DEVELOPMENT_TOKEN_AIRDROP_AMOUNT, FEE_RATE_RANGE, INSECURE_DEVELOPMENT_PASSWORD, MIN_STAKE_FOR_TESTS, + MOCK_ALLOCATION_INFILE, + MOCK_CUSTOM_INSTALLATION_PATH, + MOCK_CUSTOM_INSTALLATION_PATH_2, MOCK_POLICY_DEFAULT_M, MOCK_REGISTRY_FILEPATH, NUMBER_OF_URSULAS_IN_DEVELOPMENT_NETWORK, @@ -82,7 +90,7 @@ from tests.constants import ( TEST_PROVIDER_URI ) from tests.mock.interfaces import MockBlockchain, mock_registry_source_manager -from tests.performance_mocks import ( +from tests.mock.performance_mocks import ( mock_cert_generation, mock_cert_loading, mock_cert_storage, @@ -223,7 +231,7 @@ def idle_federated_policy(federated_alice, federated_bob): label=random_label, m=m, n=n, - expiration=maya.now() + datetime.timedelta(days=5)) + expiration=maya.now() + timedelta(days=5)) return policy @@ -958,3 +966,100 @@ def highperf_mocked_bob(fleet_of_highperf_mocked_ursulas): with mock_cert_storage, mock_verify_node, mock_record_fleet_state: bob = config.produce(known_nodes=list(fleet_of_highperf_mocked_ursulas)[:1]) return bob + +# +# CLI +# + + +@pytest.fixture(scope='function', autouse=True) +def stdout_trap(mocker): + trap = StringIO() + mocker.patch('sys.stdout', new=trap) + return trap + + +@pytest.fixture(scope='function') +def test_emitter(mocker, stdout_trap): + mocker.patch('sys.stdout', new=stdout_trap) + return StdoutEmitter() + + +@pytest.fixture(scope='module') +def click_runner(): + runner = CliRunner() + yield runner + + +@pytest.fixture(scope='session') +def nominal_federated_configuration_fields(): + config = UrsulaConfiguration(dev_mode=True, federated_only=True) + config_fields = config.static_payload() + yield tuple(config_fields.keys()) + del config + + +@pytest.fixture(scope='module') +def mock_allocation_infile(testerchain, token_economics, get_random_checksum_address): + accounts = [get_random_checksum_address() for _ in range(10)] + # accounts = testerchain.unassigned_accounts + allocation_data = list() + amount = 2 * token_economics.minimum_allowed_locked + min_periods = token_economics.minimum_locked_periods + for account in accounts: + substake = [{'checksum_address': account, 'amount': amount, 'lock_periods': min_periods + i} for i in range(24)] + allocation_data.extend(substake) + + with open(MOCK_ALLOCATION_INFILE, 'w') as file: + file.write(json.dumps(allocation_data)) + + yield MOCK_ALLOCATION_INFILE + if os.path.isfile(MOCK_ALLOCATION_INFILE): + os.remove(MOCK_ALLOCATION_INFILE) + + +@pytest.fixture(scope='function') +def new_local_registry(): + filename = f'{BASE_TEMP_PREFIX}mock-empty-registry-{datetime.now().strftime(DATETIME_FORMAT)}.json' + registry_filepath = os.path.join(BASE_TEMP_DIR, filename) + registry = LocalContractRegistry(filepath=registry_filepath) + registry.write(InMemoryContractRegistry().read()) + yield registry + if os.path.exists(registry_filepath): + os.remove(registry_filepath) + + +@pytest.fixture(scope='module') +def custom_filepath(): + _custom_filepath = MOCK_CUSTOM_INSTALLATION_PATH + with contextlib.suppress(FileNotFoundError): + shutil.rmtree(_custom_filepath, ignore_errors=True) + yield _custom_filepath + with contextlib.suppress(FileNotFoundError): + shutil.rmtree(_custom_filepath, ignore_errors=True) + + +@pytest.fixture(scope='module') +def custom_filepath_2(): + _custom_filepath = MOCK_CUSTOM_INSTALLATION_PATH_2 + with contextlib.suppress(FileNotFoundError): + shutil.rmtree(_custom_filepath, ignore_errors=True) + try: + yield _custom_filepath + finally: + with contextlib.suppress(FileNotFoundError): + shutil.rmtree(_custom_filepath, ignore_errors=True) + + +@pytest.fixture(scope='module') +def worker_configuration_file_location(custom_filepath): + _configuration_file_location = os.path.join(MOCK_CUSTOM_INSTALLATION_PATH, + UrsulaConfiguration.generate_filename()) + return _configuration_file_location + + +@pytest.fixture(scope='module') +def stakeholder_configuration_file_location(custom_filepath): + _configuration_file_location = os.path.join(MOCK_CUSTOM_INSTALLATION_PATH, + StakeHolderConfiguration.generate_filename()) + return _configuration_file_location diff --git a/tests/integration/blockchain/test_currency.py b/tests/integration/blockchain/test_currency.py new file mode 100644 index 000000000..1f598d36e --- /dev/null +++ b/tests/integration/blockchain/test_currency.py @@ -0,0 +1,117 @@ +""" + This file is part of nucypher. + + nucypher is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + nucypher is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with nucypher. If not, see . +""" + +import pytest +from decimal import Decimal, InvalidOperation + +from nucypher.blockchain.eth.token import NU + + +def test_NU(token_economics): + + # Starting Small + min_allowed_locked = NU(token_economics.minimum_allowed_locked, 'NuNit') + assert token_economics.minimum_allowed_locked == int(min_allowed_locked.to_nunits()) + + min_NU_locked = int(str(token_economics.minimum_allowed_locked)[0:-18]) + expected = NU(min_NU_locked, 'NU') + assert min_allowed_locked == expected + + # Starting Big + min_allowed_locked = NU(min_NU_locked, 'NU') + assert token_economics.minimum_allowed_locked == int(min_allowed_locked) + assert token_economics.minimum_allowed_locked == int(min_allowed_locked.to_nunits()) + assert str(min_allowed_locked) == '15000 NU' + + # Alternate construction + assert NU(1, 'NU') == NU('1.0', 'NU') == NU(1.0, 'NU') + + # Arithmetic + + # NUs + one_nu = NU(1, 'NU') + zero_nu = NU(0, 'NU') + one_hundred_nu = NU(100, 'NU') + two_hundred_nu = NU(200, 'NU') + three_hundred_nu = NU(300, 'NU') + + # Nits + one_nu_wei = NU(1, 'NuNit') + three_nu_wei = NU(3, 'NuNit') + assert three_nu_wei.to_tokens() == Decimal('3E-18') + assert one_nu_wei.to_tokens() == Decimal('1E-18') + + # Base Operations + assert one_hundred_nu < two_hundred_nu < three_hundred_nu + assert one_hundred_nu <= two_hundred_nu <= three_hundred_nu + + assert three_hundred_nu > two_hundred_nu > one_hundred_nu + assert three_hundred_nu >= two_hundred_nu >= one_hundred_nu + + assert (one_hundred_nu + two_hundred_nu) == three_hundred_nu + assert (three_hundred_nu - two_hundred_nu) == one_hundred_nu + + difference = one_nu - one_nu_wei + assert not difference == zero_nu + + actual = float(difference.to_tokens()) + expected = 0.999999999999999999 + assert actual == expected + + # 3.14 NU is 3_140_000_000_000_000_000 NuNit + pi_nuweis = NU(3.14, 'NU') + assert NU('3.14', 'NU') == pi_nuweis.to_nunits() == NU(3_140_000_000_000_000_000, 'NuNit') + + # Mixed type operations + difference = NU('3.14159265', 'NU') - NU(1.1, 'NU') + assert difference == NU('2.04159265', 'NU') + + result = difference + one_nu_wei + assert result == NU(2041592650000000001, 'NuNit') + + # Similar to stake read + metadata operations in Staker + collection = [one_hundred_nu, two_hundred_nu, three_hundred_nu] + assert sum(collection) == NU('600', 'NU') == NU(600, 'NU') == NU(600.0, 'NU') == NU(600e+18, 'NuNit') + + # + # Fractional Inputs + # + + # A decimal amount of NuNit (i.e., a fraction of a NuNit) + pi_nuweis = NU('3.14', 'NuNit') + assert pi_nuweis == three_nu_wei # Floor + + # A decimal amount of NU, which amounts to NuNit with decimals + pi_nus = NU('3.14159265358979323846', 'NU') + assert pi_nus == NU(3141592653589793238, 'NuNit') # Floor + + # Positive Infinity + with pytest.raises(NU.InvalidAmount): + _inf = NU(float('infinity'), 'NU') + + # Negative Infinity + with pytest.raises(NU.InvalidAmount): + _neg_inf = NU(float('-infinity'), 'NU') + + # Not a Number + with pytest.raises(InvalidOperation): + _nan = NU(float('NaN'), 'NU') + + # Rounding NUs + assert round(pi_nus, 2) == NU("3.14", "NU") + assert round(pi_nus, 1) == NU("3.1", "NU") + assert round(pi_nus, 0) == round(pi_nus) == NU("3", "NU") diff --git a/tests/blockchain/eth/entities/deployers/test_economics.py b/tests/integration/blockchain/test_exact_economics_model.py similarity index 70% rename from tests/blockchain/eth/entities/deployers/test_economics.py rename to tests/integration/blockchain/test_exact_economics_model.py index 00869b299..c6f2ff995 100644 --- a/tests/blockchain/eth/entities/deployers/test_economics.py +++ b/tests/integration/blockchain/test_exact_economics_model.py @@ -19,52 +19,7 @@ along with nucypher. If not, see . from decimal import Decimal, localcontext from math import log -import pytest - -from nucypher.blockchain.economics import LOG2, StandardTokenEconomics, EconomicsFactory - - -def test_rough_economics(): - """ - Formula for staking in one period: - (totalSupply - currentSupply) * (lockedValue / totalLockedValue) * (k1 + allLockedPeriods) / d / k2 - - d - Coefficient which modifies the rate at which the maximum issuance decays - k1 - Numerator of the locking duration coefficient - k2 - Denominator of the locking duration coefficient - - if allLockedPeriods > awarded_periods then allLockedPeriods = awarded_periods - kappa * log(2) / halving_delay === (k1 + allLockedPeriods) / d / k2 - - kappa = small_stake_multiplier + (1 - small_stake_multiplier) * min(T, T1) / T1 - where allLockedPeriods == min(T, T1) - """ - - e = StandardTokenEconomics(initial_supply=int(1e9), - first_phase_supply=1829579800, - first_phase_duration=5, - decay_half_life=2, - reward_saturation=1, - small_stake_multiplier=Decimal(0.5)) - - assert float(round(e.erc20_total_supply / Decimal(1e9), 2)) == 3.89 # As per economics paper - - # Check that we have correct numbers in day 1 of the second phase - initial_rate = (e.erc20_total_supply - int(e.first_phase_total_supply)) * (e.lock_duration_coefficient_1 + 365) / \ - (e.issuance_decay_coefficient * e.lock_duration_coefficient_2) - assert int(initial_rate) == int(e.first_phase_max_issuance) - - initial_rate_small = (e.erc20_total_supply - int(e.first_phase_total_supply)) * e.lock_duration_coefficient_1 / \ - (e.issuance_decay_coefficient * e.lock_duration_coefficient_2) - assert int(initial_rate_small) == int(initial_rate / 2) - - # Sanity check that total and reward supply calculated correctly - assert int(LOG2 / (e.token_halving * 365) * (e.erc20_total_supply - int(e.first_phase_total_supply))) == int(initial_rate) - assert int(e.reward_supply) == int(e.erc20_total_supply - Decimal(int(1e9))) - - # Sanity check for lock_duration_coefficient_1 (k1), issuance_decay_coefficient (d) and lock_duration_coefficient_2 (k2) - assert e.lock_duration_coefficient_1 * e.token_halving == \ - e.issuance_decay_coefficient * e.lock_duration_coefficient_2 * LOG2 * e.small_stake_multiplier / 365 +from nucypher.blockchain.economics import LOG2, StandardTokenEconomics def test_exact_economics(): @@ -206,28 +161,3 @@ def test_exact_economics(): tomorrows_supply = e.token_supply_at_period(period=t + 1) assert tomorrows_supply >= todays_supply todays_supply = tomorrows_supply - - -def test_economic_parameter_aliases(): - - e = StandardTokenEconomics() - - assert e.lock_duration_coefficient_1 == 365 - assert e.lock_duration_coefficient_2 == 2 * 365 - assert int(e.issuance_decay_coefficient) == 1053 - assert e.maximum_rewarded_periods == 365 - - deployment_params = e.staking_deployment_parameters - assert isinstance(deployment_params, tuple) - for parameter in deployment_params: - assert isinstance(parameter, int) - - -@pytest.mark.usefixtures('agency') -def test_retrieving_from_blockchain(token_economics, test_registry): - - economics = EconomicsFactory.get_economics(registry=test_registry) - - assert economics.staking_deployment_parameters == token_economics.staking_deployment_parameters - assert economics.slashing_deployment_parameters == token_economics.slashing_deployment_parameters - assert economics.worklock_deployment_parameters == token_economics.worklock_deployment_parameters diff --git a/tests/characters/control/federated/conftest.py b/tests/integration/characters/control/conftest.py similarity index 100% rename from tests/characters/control/federated/conftest.py rename to tests/integration/characters/control/conftest.py diff --git a/tests/characters/control/federated/test_rpc_control_federated.py b/tests/integration/characters/control/test_rpc_control_federated.py similarity index 100% rename from tests/characters/control/federated/test_rpc_control_federated.py rename to tests/integration/characters/control/test_rpc_control_federated.py diff --git a/tests/characters/control/federated/test_web_control_federated.py b/tests/integration/characters/control/test_web_control_federated.py similarity index 100% rename from tests/characters/control/federated/test_web_control_federated.py rename to tests/integration/characters/control/test_web_control_federated.py diff --git a/tests/integration/characters/federated_encrypt_and_decrypt.py b/tests/integration/characters/federated_encrypt_and_decrypt.py new file mode 100644 index 000000000..51557d550 --- /dev/null +++ b/tests/integration/characters/federated_encrypt_and_decrypt.py @@ -0,0 +1,118 @@ +""" +This file is part of nucypher. + +nucypher is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +nucypher is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with nucypher. If not, see . +""" + + +import pytest +from constant_sorrow import constants +from cryptography.exceptions import InvalidSignature + +from nucypher.characters.lawful import Enrico + +""" +What follows are various combinations of signing and encrypting, to match +real-world scenarios. +""" + + +def test_sign_cleartext_and_encrypt(federated_alice, federated_bob): + """ + Exhibit One: federated_alice signs the cleartext and encrypts her signature inside + the ciphertext. + """ + message = b"Have you accepted my answer on StackOverflow yet?" + + message_kit, _signature = federated_alice.encrypt_for(federated_bob, message, + sign_plaintext=True) + + # Notice that our function still returns the signature here, in case federated_alice + # wants to do something else with it, such as post it publicly for later + # public verifiability. + + # However, we can expressly refrain from passing the Signature, and the + # verification still works: + cleartext = federated_bob.verify_from(federated_alice, message_kit, signature=None, + decrypt=True) + assert cleartext == message + + +def test_encrypt_and_sign_the_ciphertext(federated_alice, federated_bob): + """ + Now, federated_alice encrypts first and then signs the ciphertext, providing a + Signature that is completely separate from the message. + This is useful in a scenario in which federated_bob needs to prove authenticity + publicly without disclosing contents. + """ + message = b"We have a reaaall problem." + message_kit, signature = federated_alice.encrypt_for(federated_bob, message, + sign_plaintext=False) + cleartext = federated_bob.verify_from(federated_alice, message_kit, signature, decrypt=True) + assert cleartext == message + + +def test_encrypt_and_sign_including_signature_in_both_places(federated_alice, federated_bob): + """ + Same as above, but showing that we can include the signature in both + the plaintext (to be found upon decryption) and also passed into + verify_from() (eg, gleaned over a side-channel). + """ + message = b"We have a reaaall problem." + message_kit, signature = federated_alice.encrypt_for(federated_bob, message, + sign_plaintext=True) + cleartext = federated_bob.verify_from(federated_alice, message_kit, signature, + decrypt=True) + assert cleartext == message + + +def test_encrypt_but_do_not_sign(federated_alice, federated_bob): + """ + Finally, federated_alice encrypts but declines to sign. + This is useful in a scenario in which federated_alice wishes to plausibly disavow + having created this content. + """ + # TODO: How do we accurately demonstrate this test safely, if at all? + message = b"If Bonnie comes home and finds an unencrypted private key in her keystore, I'm gonna get divorced." + + # Alice might also want to encrypt a message but *not* sign it, in order + # to refrain from creating evidence that can prove she was the + # original sender. + message_kit, not_signature = federated_alice.encrypt_for(federated_bob, message, sign=False) + + # The message is not signed... + assert not_signature == constants.NOT_SIGNED + + # ...and thus, the message is not verified. + with pytest.raises(InvalidSignature): + federated_bob.verify_from(federated_alice, message_kit, decrypt=True) + + +def test_alice_can_decrypt(federated_alice): + label = b"boring test label" + + policy_pubkey = federated_alice.get_policy_encrypting_key_from_label(label) + + enrico = Enrico(policy_encrypting_key=policy_pubkey) + + message = b"boring test message" + message_kit, signature = enrico.encrypt_message(message=message) + + # Interesting thing: if Alice wants to decrypt, she needs to provide the label directly. + cleartext = federated_alice.verify_from(stranger=enrico, + message_kit=message_kit, + signature=signature, + decrypt=True, + label=label) + assert cleartext == message diff --git a/tests/characters/test_bob_handles_frags.py b/tests/integration/characters/test_bob_handles_frags.py similarity index 100% rename from tests/characters/test_bob_handles_frags.py rename to tests/integration/characters/test_bob_handles_frags.py diff --git a/tests/characters/test_bob_joins_policy_and_retrieves.py b/tests/integration/characters/test_bob_joins_policy_and_retrieves.py similarity index 100% rename from tests/characters/test_bob_joins_policy_and_retrieves.py rename to tests/integration/characters/test_bob_joins_policy_and_retrieves.py diff --git a/tests/characters/test_character_serialization.py b/tests/integration/characters/test_character_serialization.py similarity index 100% rename from tests/characters/test_character_serialization.py rename to tests/integration/characters/test_character_serialization.py diff --git a/tests/integration/characters/test_federated_grant_and_revoke.py b/tests/integration/characters/test_federated_grant_and_revoke.py new file mode 100644 index 000000000..2cdf49384 --- /dev/null +++ b/tests/integration/characters/test_federated_grant_and_revoke.py @@ -0,0 +1,131 @@ +""" +This file is part of nucypher. + +nucypher is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +nucypher is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with nucypher. If not, see . +""" + +import datetime +import maya +import pytest +from umbral.kfrags import KFrag + +from nucypher.characters.lawful import Enrico +from nucypher.crypto.api import keccak_digest +from nucypher.policy.collections import Revocation + + +@pytest.mark.usefixtures('federated_ursulas') +def test_federated_grant(federated_alice, federated_bob): + # Setup the policy details + m, n = 2, 3 + policy_end_datetime = maya.now() + datetime.timedelta(days=5) + label = b"this_is_the_path_to_which_access_is_being_granted" + + # Create the Policy, granting access to Bob + policy = federated_alice.grant(federated_bob, label, m=m, n=n, expiration=policy_end_datetime) + + # Check the policy ID + policy_id = keccak_digest(policy.label + bytes(policy.bob.stamp)) + assert policy_id == policy.id + + # Check Alice's active policies + assert policy_id in federated_alice.active_policies + assert federated_alice.active_policies[policy_id] == policy + + # The number of accepted arrangements at least the number of Ursulas we're using (n) + assert len(policy._accepted_arrangements) >= n + + # The number of actually enacted arrangements is exactly equal to n. + assert len(policy._enacted_arrangements) == n + + # Let's look at the enacted arrangements. + for kfrag in policy.kfrags: + arrangement = policy._enacted_arrangements[kfrag] + + # Get the Arrangement from Ursula's datastore, looking up by the Arrangement ID. + retrieved_policy = arrangement.ursula.datastore.get_policy_arrangement(arrangement.id.hex().encode()) + retrieved_kfrag = KFrag.from_bytes(retrieved_policy.kfrag) + + assert kfrag == retrieved_kfrag + + +def test_federated_alice_can_decrypt(federated_alice, federated_bob): + """ + Test that alice can decrypt data encrypted by an enrico + for her own derived policy pubkey. + """ + + # Setup the policy details + m, n = 2, 3 + policy_end_datetime = maya.now() + datetime.timedelta(days=5) + label = b"this_is_the_path_to_which_access_is_being_granted" + + policy = federated_alice.create_policy( + bob=federated_bob, + label=label, + m=m, + n=n, + expiration=policy_end_datetime, + ) + + enrico = Enrico.from_alice( + federated_alice, + policy.label, + ) + plaintext = b"this is the first thing i'm encrypting ever." + + # use the enrico to encrypt the message + message_kit, signature = enrico.encrypt_message(plaintext) + + # decrypt the data + decrypted_data = federated_alice.verify_from( + enrico, + message_kit, + signature=signature, + decrypt=True, + label=policy.label + ) + + assert plaintext == decrypted_data + + +@pytest.mark.usefixtures('federated_ursulas') +def test_revocation(federated_alice, federated_bob): + m, n = 2, 3 + policy_end_datetime = maya.now() + datetime.timedelta(days=5) + label = b"revocation test" + + policy = federated_alice.grant(federated_bob, label, m=m, n=n, expiration=policy_end_datetime) + + # Test that all arrangements are included in the RevocationKit + for node_id, arrangement_id in policy.treasure_map: + assert policy.revocation_kit[node_id].arrangement_id == arrangement_id + + # Test revocation kit's signatures + for revocation in policy.revocation_kit: + assert revocation.verify_signature(federated_alice.stamp.as_umbral_pubkey()) + + # Test Revocation deserialization + revocation = policy.revocation_kit[node_id] + revocation_bytes = bytes(revocation) + deserialized_revocation = Revocation.from_bytes(revocation_bytes) + assert deserialized_revocation == revocation + + # Attempt to revoke the new policy + failed_revocations = federated_alice.revoke(policy) + assert len(failed_revocations) == 0 + + # Try to revoke the already revoked policy + already_revoked = federated_alice.revoke(policy) + assert len(already_revoked) == 3 diff --git a/tests/characters/test_specifications.py b/tests/integration/characters/test_specifications.py similarity index 100% rename from tests/characters/test_specifications.py rename to tests/integration/characters/test_specifications.py diff --git a/tests/integration/characters/test_ursula_startup.py b/tests/integration/characters/test_ursula_startup.py new file mode 100644 index 000000000..3b20229c7 --- /dev/null +++ b/tests/integration/characters/test_ursula_startup.py @@ -0,0 +1,48 @@ +""" +This file is part of nucypher. + +nucypher is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +nucypher is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with nucypher. If not, see . +""" + +from tests.utils.middleware import MockRestMiddleware +from tests.utils.ursula import make_federated_ursulas + + +def test_new_federated_ursula_announces_herself(ursula_federated_test_config): + ursula_in_a_house, ursula_with_a_mouse = make_federated_ursulas(ursula_config=ursula_federated_test_config, + quantity=2, + know_each_other=False, + network_middleware=MockRestMiddleware()) + + # Neither Ursula knows about the other. + assert ursula_in_a_house.known_nodes == ursula_with_a_mouse.known_nodes + + ursula_in_a_house.remember_node(ursula_with_a_mouse) + + # OK, now, ursula_in_a_house knows about ursula_with_a_mouse, but not vice-versa. + assert ursula_with_a_mouse in ursula_in_a_house.known_nodes + assert ursula_in_a_house not in ursula_with_a_mouse.known_nodes + + # But as ursula_in_a_house learns, she'll announce herself to ursula_with_a_mouse. + ursula_in_a_house.learn_from_teacher_node() + + assert ursula_with_a_mouse in ursula_in_a_house.known_nodes + assert ursula_in_a_house in ursula_with_a_mouse.known_nodes + + +def test_node_deployer(federated_ursulas): + for ursula in federated_ursulas: + deployer = ursula.get_deployer() + assert deployer.options['https_port'] == ursula.rest_information()[0].port + assert deployer.application == ursula.rest_app diff --git a/tests/cli/functional/actions/test_auth_actions.py b/tests/integration/cli/actions/test_auth_actions.py similarity index 100% rename from tests/cli/functional/actions/test_auth_actions.py rename to tests/integration/cli/actions/test_auth_actions.py diff --git a/tests/cli/functional/actions/test_config_actions.py b/tests/integration/cli/actions/test_config_actions.py similarity index 100% rename from tests/cli/functional/actions/test_config_actions.py rename to tests/integration/cli/actions/test_config_actions.py diff --git a/tests/cli/functional/actions/test_confirm_actions.py b/tests/integration/cli/actions/test_confirm_actions.py similarity index 100% rename from tests/cli/functional/actions/test_confirm_actions.py rename to tests/integration/cli/actions/test_confirm_actions.py diff --git a/tests/cli/functional/actions/test_select_client_account.py b/tests/integration/cli/actions/test_select_client_account.py similarity index 100% rename from tests/cli/functional/actions/test_select_client_account.py rename to tests/integration/cli/actions/test_select_client_account.py diff --git a/tests/cli/functional/actions/test_select_client_account_for_staking.py b/tests/integration/cli/actions/test_select_client_account_for_staking.py similarity index 100% rename from tests/cli/functional/actions/test_select_client_account_for_staking.py rename to tests/integration/cli/actions/test_select_client_account_for_staking.py diff --git a/tests/cli/functional/actions/test_select_config_file.py b/tests/integration/cli/actions/test_select_config_file.py similarity index 100% rename from tests/cli/functional/actions/test_select_config_file.py rename to tests/integration/cli/actions/test_select_config_file.py diff --git a/tests/cli/functional/actions/test_select_network.py b/tests/integration/cli/actions/test_select_network.py similarity index 100% rename from tests/cli/functional/actions/test_select_network.py rename to tests/integration/cli/actions/test_select_network.py diff --git a/tests/cli/functional/actions/test_select_stake.py b/tests/integration/cli/actions/test_select_stake.py similarity index 100% rename from tests/cli/functional/actions/test_select_stake.py rename to tests/integration/cli/actions/test_select_stake.py diff --git a/tests/cli/functional/test_ursula_local_keystore_cli_functionality.py b/tests/integration/cli/test_ursula_local_keystore_cli_functionality.py similarity index 100% rename from tests/cli/functional/test_ursula_local_keystore_cli_functionality.py rename to tests/integration/cli/test_ursula_local_keystore_cli_functionality.py diff --git a/tests/cli/functional/test_worklock_cli_functionality.py b/tests/integration/cli/test_worklock_cli_functionality.py similarity index 100% rename from tests/cli/functional/test_worklock_cli_functionality.py rename to tests/integration/cli/test_worklock_cli_functionality.py diff --git a/tests/config/test_base_configuration.py b/tests/integration/config/test_base_configuration.py similarity index 100% rename from tests/config/test_base_configuration.py rename to tests/integration/config/test_base_configuration.py diff --git a/tests/config/test_character_configuration.py b/tests/integration/config/test_character_configuration.py similarity index 100% rename from tests/config/test_character_configuration.py rename to tests/integration/config/test_character_configuration.py diff --git a/tests/integration/config/test_configuration_persistence.py b/tests/integration/config/test_configuration_persistence.py new file mode 100644 index 000000000..c567d838a --- /dev/null +++ b/tests/integration/config/test_configuration_persistence.py @@ -0,0 +1,119 @@ +""" +This file is part of nucypher. + +nucypher is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +nucypher is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with nucypher. If not, see . +""" + +import datetime +import maya +import os + +from nucypher.characters.lawful import Bob +from nucypher.config.characters import AliceConfiguration +from nucypher.crypto.powers import DecryptingPower, SigningPower +from tests.constants import INSECURE_DEVELOPMENT_PASSWORD +from tests.utils.middleware import MockRestMiddleware + + +def test_alices_powers_are_persistent(federated_ursulas, tmpdir): + # Create a non-learning AliceConfiguration + alice_config = AliceConfiguration( + config_root=os.path.join(tmpdir, 'nucypher-custom-alice-config'), + network_middleware=MockRestMiddleware(), + known_nodes=federated_ursulas, + start_learning_now=False, + federated_only=True, + save_metadata=False, + reload_metadata=False + ) + + # Generate keys and write them the disk + alice_config.initialize(password=INSECURE_DEVELOPMENT_PASSWORD) + + # Unlock Alice's keyring + alice_config.keyring.unlock(password=INSECURE_DEVELOPMENT_PASSWORD) + + # Produce an Alice + alice = alice_config() # or alice_config.produce() + + # Save Alice's node configuration file to disk for later use + alice_config_file = alice_config.to_configuration_file() + + # Let's save Alice's public keys too to check they are correctly restored later + alices_verifying_key = alice.public_keys(SigningPower) + alices_receiving_key = alice.public_keys(DecryptingPower) + + # Next, let's fix a label for all the policies we will create later. + label = b"this_is_the_path_to_which_access_is_being_granted" + + # Even before creating the policies, we can know what will be its public key. + # This can be used by Enrico (i.e., a Data Source) to encrypt messages + # before Alice grants access to Bobs. + policy_pubkey = alice.get_policy_encrypting_key_from_label(label) + + # Now, let's create a policy for some Bob. + m, n = 3, 4 + policy_end_datetime = maya.now() + datetime.timedelta(days=5) + + bob = Bob(federated_only=True, + start_learning_now=False, + network_middleware=MockRestMiddleware()) + + bob_policy = alice.grant(bob, label, m=m, n=n, expiration=policy_end_datetime) + + assert policy_pubkey == bob_policy.public_key + + # ... and Alice and her configuration disappear. + del alice + del alice_config + + ################################### + # Some time passes. # + # ... # + # (jmyles plays the Song of Time) # + # ... # + # Alice appears again. # + ################################### + + # A new Alice is restored from the configuration file + new_alice_config = AliceConfiguration.from_configuration_file( + filepath=alice_config_file, + network_middleware=MockRestMiddleware(), + known_nodes=federated_ursulas, + start_learning_now=False, + ) + + # Alice unlocks her restored keyring from disk + new_alice_config.attach_keyring() + new_alice_config.keyring.unlock(password=INSECURE_DEVELOPMENT_PASSWORD) + new_alice = new_alice_config() + + # First, we check that her public keys are correctly restored + assert alices_verifying_key == new_alice.public_keys(SigningPower) + assert alices_receiving_key == new_alice.public_keys(DecryptingPower) + + # Bob's eldest brother, Roberto, appears too + roberto = Bob(federated_only=True, + start_learning_now=False, + network_middleware=MockRestMiddleware()) + + # Alice creates a new policy for Roberto. Note how all the parameters + # except for the label (i.e., recipient, m, n, policy_end) are different + # from previous policy + m, n = 2, 5 + policy_end_datetime = maya.now() + datetime.timedelta(days=3) + roberto_policy = new_alice.grant(roberto, label, m=m, n=n, expiration=policy_end_datetime) + + # Both policies must share the same public key (i.e., the policy public key) + assert policy_pubkey == roberto_policy.public_key diff --git a/tests/config/test_keyring.py b/tests/integration/config/test_keyring_integration.py similarity index 100% rename from tests/config/test_keyring.py rename to tests/integration/config/test_keyring_integration.py diff --git a/tests/config/test_storages.py b/tests/integration/config/test_storages.py similarity index 100% rename from tests/config/test_storages.py rename to tests/integration/config/test_storages.py diff --git a/tests/cli/functional/conftest.py b/tests/integration/conftest.py similarity index 100% rename from tests/cli/functional/conftest.py rename to tests/integration/conftest.py diff --git a/tests/datastore/test_datastore.py b/tests/integration/datastore/test_datastore.py similarity index 100% rename from tests/datastore/test_datastore.py rename to tests/integration/datastore/test_datastore.py diff --git a/tests/learning/test_discovery_phases.py b/tests/integration/learning/test_discovery_phases.py similarity index 94% rename from tests/learning/test_discovery_phases.py rename to tests/integration/learning/test_discovery_phases.py index 45cc006b4..619dc690e 100644 --- a/tests/learning/test_discovery_phases.py +++ b/tests/integration/learning/test_discovery_phases.py @@ -14,6 +14,8 @@ GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with nucypher. If not, see . """ + + import maya import pytest import time @@ -22,9 +24,20 @@ from umbral.keys import UmbralPublicKey from unittest.mock import patch from nucypher.characters.lawful import Ursula -from tests.performance_mocks import NotAPublicKey, NotARestApp, VerificationTracker, mock_cert_loading, \ - mock_cert_storage, mock_message_verification, mock_metadata_validation, mock_pubkey_from_bytes, mock_secret_source, \ - mock_signature_bytes, mock_stamp_call, mock_verify_node +from tests.mock.performance_mocks import ( + NotAPublicKey, + NotARestApp, + VerificationTracker, + mock_cert_loading, + mock_cert_storage, + mock_message_verification, + mock_metadata_validation, + mock_pubkey_from_bytes, + mock_secret_source, + mock_signature_bytes, + mock_stamp_call, + mock_verify_node +) """ Node Discovery happens in phases. The first step is for a network actor to learn about the mere existence of a Node. diff --git a/tests/learning/test_domains.py b/tests/integration/learning/test_domains.py similarity index 100% rename from tests/learning/test_domains.py rename to tests/integration/learning/test_domains.py diff --git a/tests/learning/test_firstula_circumstances.py b/tests/integration/learning/test_firstula_circumstances.py similarity index 100% rename from tests/learning/test_firstula_circumstances.py rename to tests/integration/learning/test_firstula_circumstances.py diff --git a/tests/learning/test_fleet_state.py b/tests/integration/learning/test_fleet_state.py similarity index 100% rename from tests/learning/test_fleet_state.py rename to tests/integration/learning/test_fleet_state.py diff --git a/tests/integration/learning/test_learning_upgrade.py b/tests/integration/learning/test_learning_upgrade.py new file mode 100644 index 000000000..5d08bc314 --- /dev/null +++ b/tests/integration/learning/test_learning_upgrade.py @@ -0,0 +1,123 @@ +""" + This file is part of nucypher. + + nucypher is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + nucypher is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with nucypher. If not, see . +""" + +from collections import namedtuple + +import os +from bytestring_splitter import VariableLengthBytestring +from eth_utils.address import to_checksum_address +from twisted.logger import LogLevel, globalLogPublisher + +from nucypher.characters.base import Character +from nucypher.network.nicknames import nickname_from_seed +from tests.utils.middleware import MockRestMiddleware +from tests.utils.ursula import make_federated_ursulas + + +def test_emit_warning_upon_new_version(ursula_federated_test_config, caplog): + nodes = make_federated_ursulas(ursula_config=ursula_federated_test_config, + quantity=3, + know_each_other=False) + teacher, learner, new_node = nodes + + learner.remember_node(teacher) + teacher.remember_node(learner) + teacher.remember_node(new_node) + + new_node.TEACHER_VERSION = learner.LEARNER_VERSION + 1 + + warnings = [] + + def warning_trapper(event): + if event['log_level'] == LogLevel.warn: + warnings.append(event) + + globalLogPublisher.addObserver(warning_trapper) + learner.learn_from_teacher_node() + + assert len(warnings) == 1 + assert warnings[0]['log_format'] == learner.unknown_version_message.format(new_node, + new_node.TEACHER_VERSION, + learner.LEARNER_VERSION) + + # Now let's go a little further: make the version totally unrecognizable. + + # First, there's enough garbage to at least scrape a potential checksum address + fleet_snapshot = os.urandom(32 + 4) + random_bytes = os.urandom(50) # lots of garbage in here + future_version = learner.LEARNER_VERSION + 42 + version_bytes = future_version.to_bytes(2, byteorder="big") + crazy_bytes = fleet_snapshot + VariableLengthBytestring(version_bytes + random_bytes) + signed_crazy_bytes = bytes(teacher.stamp(crazy_bytes)) + + Response = namedtuple("MockResponse", ("content", "status_code")) + response = Response(content=signed_crazy_bytes + crazy_bytes, status_code=200) + + learner._current_teacher_node = teacher + learner.network_middleware.get_nodes_via_rest = lambda *args, **kwargs: response + learner.learn_from_teacher_node() + + # If you really try, you can read a node representation from the garbage + accidental_checksum = to_checksum_address(random_bytes[:20]) + accidental_nickname = nickname_from_seed(accidental_checksum)[0] + accidental_node_repr = Character._display_name_template.format("Ursula", accidental_nickname, accidental_checksum) + + assert len(warnings) == 2 + assert warnings[1]['log_format'] == learner.unknown_version_message.format(accidental_node_repr, + future_version, + learner.LEARNER_VERSION) + + # This time, however, there's not enough garbage to assume there's a checksum address... + random_bytes = os.urandom(2) + crazy_bytes = fleet_snapshot + VariableLengthBytestring(version_bytes + random_bytes) + signed_crazy_bytes = bytes(teacher.stamp(crazy_bytes)) + + response = Response(content=signed_crazy_bytes + crazy_bytes, status_code=200) + + learner._current_teacher_node = teacher + learner.learn_from_teacher_node() + + assert len(warnings) == 3 + # ...so this time we get a "really unknown version message" + assert warnings[2]['log_format'] == learner.really_unknown_version_message.format(future_version, + learner.LEARNER_VERSION) + + globalLogPublisher.removeObserver(warning_trapper) + + +def test_node_posts_future_version(federated_ursulas): + ursula = list(federated_ursulas)[0] + middleware = MockRestMiddleware() + + warnings = [] + + def warning_trapper(event): + if event['log_level'] == LogLevel.warn: + warnings.append(event) + + globalLogPublisher.addObserver(warning_trapper) + + crazy_node = b"invalid-node" + middleware.get_nodes_via_rest(node=ursula, + announce_nodes=(crazy_node,)) + assert len(warnings) == 1 + future_node = list(federated_ursulas)[1] + future_node.TEACHER_VERSION = future_node.TEACHER_VERSION + 10 + future_node_bytes = bytes(future_node) + middleware.get_nodes_via_rest(node=ursula, + announce_nodes=(future_node_bytes,)) + assert len(warnings) == 2 diff --git a/tests/network/test_failure_modes.py b/tests/integration/network/test_failure_modes.py similarity index 99% rename from tests/network/test_failure_modes.py rename to tests/integration/network/test_failure_modes.py index 05c68ac1a..1788b6d5f 100644 --- a/tests/network/test_failure_modes.py +++ b/tests/integration/network/test_failure_modes.py @@ -172,6 +172,8 @@ def test_huge_treasure_maps_are_rejected(federated_alice, federated_ursulas): ) """ + +@pytest.mark.skip("Hangs forever") @pytest_twisted.inlineCallbacks def test_hendrix_handles_content_length_validation(ursula_federated_test_config): node = make_federated_ursulas(ursula_config=ursula_federated_test_config, quantity=1).pop() diff --git a/tests/network/test_network_upgrade.py b/tests/integration/network/test_network_upgrade.py similarity index 99% rename from tests/network/test_network_upgrade.py rename to tests/integration/network/test_network_upgrade.py index e9a153ef9..e3757b341 100644 --- a/tests/network/test_network_upgrade.py +++ b/tests/integration/network/test_network_upgrade.py @@ -14,6 +14,8 @@ GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with nucypher. If not, see . """ + + import os import pytest_twisted import requests diff --git a/tests/network/test_node_storage.py b/tests/integration/network/test_node_storage.py similarity index 100% rename from tests/network/test_node_storage.py rename to tests/integration/network/test_node_storage.py diff --git a/tests/integration/network/test_treasure_map_integration.py b/tests/integration/network/test_treasure_map_integration.py new file mode 100644 index 000000000..608cdf151 --- /dev/null +++ b/tests/integration/network/test_treasure_map_integration.py @@ -0,0 +1,121 @@ +""" + This file is part of nucypher. + + nucypher is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + nucypher is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with nucypher. If not, see . +""" + +import pytest + +from nucypher.characters.lawful import Ursula +from nucypher.crypto.api import keccak_digest +from tests.utils.middleware import MockRestMiddleware + + +def test_alice_creates_policy_with_correct_hrac(idle_federated_policy): + """ + Alice creates a Policy. It has the proper HRAC, unique per her, Bob, and the label + """ + alice = idle_federated_policy.alice + bob = idle_federated_policy.bob + + assert idle_federated_policy.hrac() == keccak_digest(bytes(alice.stamp) + + bytes(bob.stamp) + + idle_federated_policy.label) + + +def test_alice_sets_treasure_map(enacted_federated_policy, federated_ursulas): + """ + Having enacted all the policies of a PolicyGroup, Alice creates a TreasureMap and ...... TODO + """ + enacted_federated_policy.publish_treasure_map(network_middleware=MockRestMiddleware()) + treasure_map_index = bytes.fromhex(enacted_federated_policy.treasure_map.public_id()) + treasure_map_as_set_on_network = list(federated_ursulas)[0].treasure_maps[treasure_map_index] + assert treasure_map_as_set_on_network == enacted_federated_policy.treasure_map + + +def test_treasure_map_stored_by_ursula_is_the_correct_one_for_bob(federated_alice, federated_bob, federated_ursulas, + enacted_federated_policy): + """ + The TreasureMap given by Alice to Ursula is the correct one for Bob; he can decrypt and read it. + """ + + treasure_map_index = bytes.fromhex(enacted_federated_policy.treasure_map.public_id()) + treasure_map_as_set_on_network = list(federated_ursulas)[0].treasure_maps[treasure_map_index] + + hrac_by_bob = federated_bob.construct_policy_hrac(federated_alice.stamp, enacted_federated_policy.label) + assert enacted_federated_policy.hrac() == hrac_by_bob + + hrac, map_id_by_bob = federated_bob.construct_hrac_and_map_id(federated_alice.stamp, enacted_federated_policy.label) + assert map_id_by_bob == treasure_map_as_set_on_network.public_id() + + +def test_bob_can_retreive_the_treasure_map_and_decrypt_it(enacted_federated_policy, federated_ursulas): + """ + Above, we showed that the TreasureMap saved on the network is the correct one for Bob. Here, we show + that Bob can retrieve it with only the information about which he is privy pursuant to the PolicyGroup. + """ + bob = enacted_federated_policy.bob + + # Of course, in the real world, Bob has sufficient information to reconstitute a PolicyGroup, gleaned, we presume, + # through a side-channel with Alice. + + # If Bob doesn't know about any Ursulas, he can't find the TreasureMap via the REST swarm: + with pytest.raises(bob.NotEnoughTeachers): + treasure_map_from_wire = bob.get_treasure_map(enacted_federated_policy.alice.stamp, + enacted_federated_policy.label) + + # Bob finds out about one Ursula (in the real world, a seed node) + bob.remember_node(list(federated_ursulas)[0]) + + # ...and then learns about the rest of the network. + bob.learn_from_teacher_node(eager=True) + + # Now he'll have better success finding that map. + treasure_map_from_wire = bob.get_treasure_map(enacted_federated_policy.alice.stamp, + enacted_federated_policy.label) + + assert enacted_federated_policy.treasure_map == treasure_map_from_wire + + +def test_treasure_map_is_legit(enacted_federated_policy): + """ + Sure, the TreasureMap can get to Bob, but we also need to know that each Ursula in the TreasureMap is on the network. + """ + for ursula_address, _node_id in enacted_federated_policy.treasure_map: + assert ursula_address in enacted_federated_policy.bob.known_nodes.addresses() + + +def test_alice_does_not_update_with_old_ursula_info(federated_alice, federated_ursulas): + ursula = list(federated_ursulas)[0] + old_metadata = bytes(ursula) + + # Alice has remembered Ursula. + assert federated_alice.known_nodes[ursula.checksum_address] == ursula + + # But now, Ursula wants to sign and date her interface info again. This causes a new timestamp. + ursula._sign_and_date_interface_info() + + # Indeed, her metadata is not the same now. + assert bytes(ursula) != old_metadata + + old_ursula = Ursula.from_bytes(old_metadata) + + # Once Alice learns about Ursula's updated info... + federated_alice.remember_node(ursula) + + # ...she can't learn about old ursula anymore. + federated_alice.remember_node(old_ursula) + + new_metadata = bytes(federated_alice.known_nodes[ursula.checksum_address]) + assert new_metadata != old_metadata diff --git a/tests/performance_mocks.py b/tests/mock/performance_mocks.py similarity index 100% rename from tests/performance_mocks.py rename to tests/mock/performance_mocks.py diff --git a/tests/unit/test_blockchain_economics_model.py b/tests/unit/test_blockchain_economics_model.py new file mode 100644 index 000000000..21de115d9 --- /dev/null +++ b/tests/unit/test_blockchain_economics_model.py @@ -0,0 +1,80 @@ +""" +This file is part of nucypher. + +nucypher is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +nucypher is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with nucypher. If not, see . +""" + + +from decimal import Decimal + +from nucypher.blockchain.economics import LOG2, StandardTokenEconomics + + +def test_rough_economics(): + """ + Formula for staking in one period: + (totalSupply - currentSupply) * (lockedValue / totalLockedValue) * (k1 + allLockedPeriods) / d / k2 + + d - Coefficient which modifies the rate at which the maximum issuance decays + k1 - Numerator of the locking duration coefficient + k2 - Denominator of the locking duration coefficient + + if allLockedPeriods > awarded_periods then allLockedPeriods = awarded_periods + kappa * log(2) / halving_delay === (k1 + allLockedPeriods) / d / k2 + + kappa = small_stake_multiplier + (1 - small_stake_multiplier) * min(T, T1) / T1 + where allLockedPeriods == min(T, T1) + """ + + e = StandardTokenEconomics(initial_supply=int(1e9), + first_phase_supply=1829579800, + first_phase_duration=5, + decay_half_life=2, + reward_saturation=1, + small_stake_multiplier=Decimal(0.5)) + + assert float(round(e.erc20_total_supply / Decimal(1e9), 2)) == 3.89 # As per economics paper + + # Check that we have correct numbers in day 1 of the second phase + initial_rate = (e.erc20_total_supply - int(e.first_phase_total_supply)) * (e.lock_duration_coefficient_1 + 365) / \ + (e.issuance_decay_coefficient * e.lock_duration_coefficient_2) + assert int(initial_rate) == int(e.first_phase_max_issuance) + + initial_rate_small = (e.erc20_total_supply - int(e.first_phase_total_supply)) * e.lock_duration_coefficient_1 / \ + (e.issuance_decay_coefficient * e.lock_duration_coefficient_2) + assert int(initial_rate_small) == int(initial_rate / 2) + + # Sanity check that total and reward supply calculated correctly + assert int(LOG2 / (e.token_halving * 365) * (e.erc20_total_supply - int(e.first_phase_total_supply))) == int(initial_rate) + assert int(e.reward_supply) == int(e.erc20_total_supply - Decimal(int(1e9))) + + # Sanity check for lock_duration_coefficient_1 (k1), issuance_decay_coefficient (d) and lock_duration_coefficient_2 (k2) + assert e.lock_duration_coefficient_1 * e.token_halving == \ + e.issuance_decay_coefficient * e.lock_duration_coefficient_2 * LOG2 * e.small_stake_multiplier / 365 + + + +def test_economic_parameter_aliases(): + + e = StandardTokenEconomics() + + assert e.lock_duration_coefficient_1 == 365 + assert e.lock_duration_coefficient_2 == 2 * 365 + assert int(e.issuance_decay_coefficient) == 1053 + assert e.maximum_rewarded_periods == 365 + + deployment_params = e.staking_deployment_parameters + assert isinstance(deployment_params, tuple) + for parameter in deployment_params: + assert isinstance(parameter, int) diff --git a/tests/crypto/test_bytestring_types.py b/tests/unit/test_bytestring_types.py similarity index 100% rename from tests/crypto/test_bytestring_types.py rename to tests/unit/test_bytestring_types.py diff --git a/tests/unit/test_character_sign_and_verify.py b/tests/unit/test_character_sign_and_verify.py new file mode 100644 index 000000000..b9a072005 --- /dev/null +++ b/tests/unit/test_character_sign_and_verify.py @@ -0,0 +1,130 @@ +""" +This file is part of nucypher. + +nucypher is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +nucypher is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with nucypher. If not, see . +""" + + +import pytest +from constant_sorrow import constants +from cryptography.exceptions import InvalidSignature + +from nucypher.characters.lawful import Alice, Bob, Character +from nucypher.crypto import api +from nucypher.crypto.powers import (CryptoPower, NoSigningPower, SigningPower) + +""" +Chapter 1: SIGNING +""" + + +def test_actor_without_signing_power_cannot_sign(): + """ + We can create a Character with no real CryptoPower to speak of. + This Character can't even sign a message. + """ + cannot_sign = CryptoPower(power_ups=[]) + non_signer = Character(crypto_power=cannot_sign, + start_learning_now=False, + federated_only=True) + + # The non-signer's stamp doesn't work for signing... + with pytest.raises(NoSigningPower): + non_signer.stamp("something") + + # ...or as a way to cast the (non-existent) public key to bytes. + with pytest.raises(NoSigningPower): + bytes(non_signer.stamp) + + +def test_actor_with_signing_power_can_sign(): + """ + However, simply giving that character a PowerUp bestows the power to sign. + + Instead of having a Character verify the signature, we'll use the lower level API. + """ + message = b"Llamas." + + signer = Character(crypto_power_ups=[SigningPower], is_me=True, + start_learning_now=False, federated_only=True) + stamp_of_the_signer = signer.stamp + + # We can use the signer's stamp to sign a message (since the signer is_me)... + signature = stamp_of_the_signer(message) + + # ...or to get the signer's public key for verification purposes. + # (note: we use the private _der_encoded_bytes here to test directly against the API, instead of Character) + verification = api.verify_ecdsa(message, signature._der_encoded_bytes(), + stamp_of_the_signer.as_umbral_pubkey()) + + assert verification is True + + +def test_anybody_can_verify(): + """ + In the last example, we used the lower-level Crypto API to verify the signature. + + Here, we show that anybody can do it without needing to directly access Crypto. + """ + # Alice can sign by default, by dint of her _default_crypto_powerups. + alice = Alice(federated_only=True, start_learning_now=False) + + # So, our story is fairly simple: an everyman meets Alice. + somebody = Character(start_learning_now=False, federated_only=True) + + # Alice signs a message. + message = b"A message for all my friends who can only verify and not sign." + signature = alice.stamp(message) + + # Our everyman can verify it. + cleartext = somebody.verify_from(alice, message, signature, decrypt=False) + assert cleartext is constants.NO_DECRYPTION_PERFORMED + + # Of course, verification fails with any fake message + with pytest.raises(InvalidSignature): + fake = b"McLovin 892 Momona St. Honolulu, HI 96820" + _ = somebody.verify_from(alice, fake, signature, decrypt=False) + + # Signature verification also works when Alice is not living with our + # everyman in the same process, and he only knows her by her public key + alice_pubkey_bytes = bytes(alice.stamp) + hearsay_alice = Character.from_public_keys({SigningPower: alice_pubkey_bytes}) + + cleartext = somebody.verify_from(hearsay_alice, message, signature, decrypt=False) + assert cleartext is constants.NO_DECRYPTION_PERFORMED + + hearsay_alice = Character.from_public_keys(verifying_key=alice_pubkey_bytes) + + cleartext = somebody.verify_from(hearsay_alice, message, signature, decrypt=False) + assert cleartext is constants.NO_DECRYPTION_PERFORMED + + +""" +Chapter 2: ENCRYPTION +""" + + +def test_anybody_can_encrypt(): + """ + Similar to anybody_can_verify() above; we show that anybody can encrypt. + """ + someone = Character(start_learning_now=False, federated_only=True) + bob = Bob(is_me=False, federated_only=True) + + cleartext = b"This is Officer Rod Farva. Come in, Ursula! Come in Ursula!" + + ciphertext, signature = someone.encrypt_for(bob, cleartext, sign=False) + + assert signature == constants.NOT_SIGNED + assert ciphertext is not None diff --git a/tests/crypto/test_utils.py b/tests/unit/test_coordinates_serialization.py similarity index 100% rename from tests/crypto/test_utils.py rename to tests/unit/test_coordinates_serialization.py diff --git a/tests/blockchain/eth/interfaces/test_decorators.py b/tests/unit/test_decorators.py similarity index 100% rename from tests/blockchain/eth/interfaces/test_decorators.py rename to tests/unit/test_decorators.py diff --git a/tests/crypto/test_api.py b/tests/unit/test_keccak_sanity.py similarity index 100% rename from tests/crypto/test_api.py rename to tests/unit/test_keccak_sanity.py diff --git a/tests/datastore/test_keypairs.py b/tests/unit/test_keypairs.py similarity index 100% rename from tests/datastore/test_keypairs.py rename to tests/unit/test_keypairs.py diff --git a/tests/blockchain/eth/signers/test_keystore_signer.py b/tests/unit/test_keystore_signer.py similarity index 99% rename from tests/blockchain/eth/signers/test_keystore_signer.py rename to tests/unit/test_keystore_signer.py index 0e5200f82..9dedcf355 100644 --- a/tests/blockchain/eth/signers/test_keystore_signer.py +++ b/tests/unit/test_keystore_signer.py @@ -117,6 +117,7 @@ def test_invalid_keystore(mocker, tmp_path): 'does not contain a valid ethereum address') as e: Signer.from_signer_uri(uri=f'keystore:{bad_address}') + def test_signer_reads_keystore_from_disk(mock_account, mock_key, tmpdir): # Test reading a keyfile from the disk via KeystoreSigner since diff --git a/tests/unit/test_registry_basics.py b/tests/unit/test_registry_basics.py new file mode 100644 index 000000000..f6423a49a --- /dev/null +++ b/tests/unit/test_registry_basics.py @@ -0,0 +1,81 @@ +""" +This file is part of nucypher. + +nucypher is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +nucypher is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with nucypher. If not, see . +""" + +import pytest + +from nucypher.blockchain.eth.interfaces import BaseContractRegistry +from nucypher.blockchain.eth.registry import LocalContractRegistry + + +def test_contract_registry(tempfile_path): + + # ABC + with pytest.raises(TypeError): + BaseContractRegistry(filepath='test') + + with pytest.raises(BaseContractRegistry.RegistryError): + bad_registry = LocalContractRegistry(filepath='/fake/file/path/registry.json') + bad_registry.search(contract_address='0xdeadbeef') + + # Tests everything is as it should be when initially created + test_registry = LocalContractRegistry(filepath=tempfile_path) + + assert test_registry.read() == list() + + # Test contract enrollment and dump_chain + test_name = 'TestContract' + test_addr = '0xDEADBEEF' + test_abi = ['fake', 'data'] + test_version = "some_version" + + test_registry.enroll(contract_name=test_name, + contract_address=test_addr, + contract_abi=test_abi, + contract_version=test_version) + + # Search by name... + contract_records = test_registry.search(contract_name=test_name) + assert len(contract_records) == 1, 'More than one record for {}'.format(test_name) + assert len(contract_records[0]) == 4, 'Registry record is the wrong length' + name, version, address, abi = contract_records[0] + + assert name == test_name + assert address == test_addr + assert abi == test_abi + assert version == test_version + + # ...or by address + contract_record = test_registry.search(contract_address=test_addr) + name, version, address, abi = contract_record + + assert name == test_name + assert address == test_addr + assert abi == test_abi + assert version == test_version + + # Check that searching for an unknown contract raises + with pytest.raises(BaseContractRegistry.UnknownContract): + test_registry.search(contract_name='this does not exist') + + current_dataset = test_registry.read() + # Corrupt the registry with a duplicate address + current_dataset.append([test_name, test_addr, test_abi]) + test_registry.write(current_dataset) + + # Check that searching for an unknown contract raises + with pytest.raises(BaseContractRegistry.InvalidRegistry): + test_registry.search(contract_address=test_addr) diff --git a/tests/crypto/test_signature.py b/tests/unit/test_umbral_signatures.py similarity index 100% rename from tests/crypto/test_signature.py rename to tests/unit/test_umbral_signatures.py diff --git a/tests/blockchain/eth/clients/test_mocked_clients.py b/tests/unit/test_web3_clients.py similarity index 89% rename from tests/blockchain/eth/clients/test_mocked_clients.py rename to tests/unit/test_web3_clients.py index d04c197ba..b2a190eb2 100644 --- a/tests/blockchain/eth/clients/test_mocked_clients.py +++ b/tests/unit/test_web3_clients.py @@ -330,49 +330,3 @@ def test_ganache_web3_client(): assert interface.client.platform is None assert interface.client.backend == 'ethereum-js' assert interface.client.is_local - - -def test_synced_geth_client(): - - class SyncedBlockchainInterface(GethClientTestBlockchain): - - Web3 = SyncedMockWeb3 - - interface = SyncedBlockchainInterface(provider_uri='file:///ipc.geth') - interface.connect() - - assert interface.client._has_latest_block() - assert interface.client.sync() - - -def test_unsynced_geth_client(): - - GethClient.SYNC_SLEEP_DURATION = .1 - - class NonSyncedBlockchainInterface(GethClientTestBlockchain): - - Web3 = SyncingMockWeb3 - - interface = NonSyncedBlockchainInterface(provider_uri='file:///ipc.geth') - interface.connect() - - assert interface.client._has_latest_block() is False - assert interface.client.syncing - - assert len(list(interface.client.sync())) == 8 - - -def test_no_peers_unsynced_geth_client(): - - GethClient.PEERING_TIMEOUT = 1 - - class NonSyncedNoPeersBlockchainInterface(GethClientTestBlockchain): - - Web3 = SyncingMockWeb3NoPeers - - interface = NonSyncedNoPeersBlockchainInterface(provider_uri='file:///ipc.geth') - interface.connect() - - assert interface.client._has_latest_block() is False - with pytest.raises(EthereumClient.SyncTimeout): - list(interface.client.sync())