From 4c1af5b5ab2e0c8f8953f054921918ae00020b01 Mon Sep 17 00:00:00 2001 From: Kieran Prasch Date: Fri, 19 Apr 2019 10:17:25 +0300 Subject: [PATCH] Baseline rollback CLI scenerio tests --- nucypher/blockchain/eth/actors.py | 8 +- nucypher/blockchain/eth/deployers.py | 10 +- nucypher/cli/deploy.py | 12 +- tests/cli/conftest.py | 43 ++++- tests/cli/test_deploy.py | 216 +++++++++++++++++-------- tests/cli/test_felix.py | 2 +- tests/cli/test_mixed_configurations.py | 2 +- 7 files changed, 209 insertions(+), 84 deletions(-) diff --git a/nucypher/blockchain/eth/actors.py b/nucypher/blockchain/eth/actors.py index a80aeb249..278871535 100644 --- a/nucypher/blockchain/eth/actors.py +++ b/nucypher/blockchain/eth/actors.py @@ -200,13 +200,13 @@ class Deployer(NucypherTokenActor): txhashes = deployer.deploy() return txhashes, deployer - def upgrade_contract(self, contract_name: str, existing_secret: str, new_plaintext_secret: str) -> dict: + def upgrade_contract(self, contract_name: str, existing_plaintext_secret: str, new_plaintext_secret: str) -> dict: Deployer = self.__get_deployer(contract_name=contract_name) deployer = Deployer(blockchain=self.blockchain, deployer_address=self.deployer_address) new_secret_hash = self.blockchain.interface.w3.keccak(bytes(new_plaintext_secret, encoding='utf-8')) - txhashes = deployer.upgrade(existing_secret_plaintext=bytes(existing_secret, encoding='utf-8'), + txhashes = deployer.upgrade(existing_secret_plaintext=bytes(existing_plaintext_secret, encoding='utf-8'), new_secret_hash=new_secret_hash) - return deployer + return txhashes def rollback_contract(self, contract_name: str, existing_plaintext_secret: str, new_plaintext_secret: str): Deployer = self.__get_deployer(contract_name=contract_name) @@ -214,7 +214,7 @@ class Deployer(NucypherTokenActor): new_secret_hash = self.blockchain.interface.w3.keccak(bytes(new_plaintext_secret, encoding='utf-8')) txhash = deployer.rollback(existing_secret_plaintext=bytes(existing_plaintext_secret, encoding='utf-8'), new_secret_hash=new_secret_hash) - return deployer + return txhash def deploy_user_escrow(self, allocation_registry: AllocationRegistry): user_escrow_deployer = UserEscrowDeployer(blockchain=self.blockchain, diff --git a/nucypher/blockchain/eth/deployers.py b/nucypher/blockchain/eth/deployers.py index 5ff242014..3f81a4d15 100644 --- a/nucypher/blockchain/eth/deployers.py +++ b/nucypher/blockchain/eth/deployers.py @@ -206,13 +206,13 @@ class DispatcherDeployer(ContractDeployer): if new_target == self._contract.address: raise self.ContractDeploymentError(f"{self.contract_name} {self._contract.address} cannot target itself.") - origin_args = {'from': self.deployer_address} # FIXME + origin_args = {'from': self.deployer_address, 'gasPrice': self.blockchain.interface.w3.eth.gasPrice} # TODO: Gas management txhash = self._contract.functions.upgrade(new_target, existing_secret_plaintext, new_secret_hash).transact(origin_args) _receipt = self.blockchain.wait_for_receipt(txhash=txhash) return txhash def rollback(self, existing_secret_plaintext: bytes, new_secret_hash: bytes) -> bytes: - origin_args = {'from': self.deployer_address} # FIXME + origin_args = {'from': self.deployer_address, 'gasPrice': self.blockchain.interface.w3.eth.gasPrice} # TODO: Gas management txhash = self._contract.functions.rollback(existing_secret_plaintext, new_secret_hash).transact(origin_args) _receipt = self.blockchain.wait_for_receipt(txhash=txhash) return txhash @@ -410,10 +410,10 @@ class PolicyManagerDeployer(ContractDeployer): self.__proxy_contract = proxy_contract # Wrap the escrow contract - wrapped_policy_manager_contract = self.blockchain.interface._wrap_contract(proxy_contract, target_contract=policy_manager_contract) + wrapped = self.blockchain.interface._wrap_contract(proxy_contract, target_contract=policy_manager_contract) # Switch the contract for the wrapped one - policy_manager_contract = wrapped_policy_manager_contract + policy_manager_contract = wrapped # Configure the MinerEscrow by setting the PolicyManager policy_setter_txhash = self.miner_agent.contract.functions.setPolicyManager(policy_manager_contract.address) \ @@ -436,6 +436,7 @@ class PolicyManagerDeployer(ContractDeployer): self.check_deployment_readiness() existing_bare_contract = self.blockchain.interface.get_contract_by_name(name=self.contract_name, + proxy_name=self.__proxy_deployer.contract_name, use_proxy_address=False) proxy_deployer = self.__proxy_deployer(blockchain=self.blockchain, @@ -750,6 +751,7 @@ class MiningAdjudicatorDeployer(ContractDeployer): self.check_deployment_readiness() existing_bare_contract = self.blockchain.interface.get_contract_by_name(name=self.contract_name, + proxy_name=self.__proxy_deployer.contract_name, use_proxy_address=False) proxy_deployer = self.__proxy_deployer(blockchain=self.blockchain, diff --git a/nucypher/cli/deploy.py b/nucypher/cli/deploy.py index de35ccabf..f70144a3e 100644 --- a/nucypher/cli/deploy.py +++ b/nucypher/cli/deploy.py @@ -96,15 +96,19 @@ def deploy(click_config, if action == 'upgrade': if not contract_name: raise click.BadArgumentUsage(message="--contract-name is required when using --upgrade") - existing_secret = click.prompt('Enter existing contract upgrade secret', hide_input=True, confirmation_prompt=True) + existing_secret = click.prompt('Enter existing contract upgrade secret', hide_input=True) new_secret = click.prompt('Enter new contract upgrade secret', hide_input=True, confirmation_prompt=True) - deployer.upgrade_contract(contract_name=contract_name, existing_secret=existing_secret, new_plaintext_secret=new_secret) + deployer.upgrade_contract(contract_name=contract_name, + existing_plaintext_secret=existing_secret, + new_plaintext_secret=new_secret) return elif action == 'rollback': - existing_secret = click.prompt('Enter existing contract upgrade secret', hide_input=True, confirmation_prompt=True) + existing_secret = click.prompt('Enter existing contract upgrade secret', hide_input=True) new_secret = click.prompt('Enter new contract upgrade secret', hide_input=True, confirmation_prompt=True) - deployer.rollback_contract(contract_name=contract_name, existing_plaintext_secret=existing_secret, new_plaintext_secret=new_secret) + deployer.rollback_contract(contract_name=contract_name, + existing_plaintext_secret=existing_secret, + new_plaintext_secret=new_secret) return elif action == "deploy": diff --git a/tests/cli/conftest.py b/tests/cli/conftest.py index 96962eddc..73f0306c3 100644 --- a/tests/cli/conftest.py +++ b/tests/cli/conftest.py @@ -30,8 +30,12 @@ from nucypher.utilities.sandbox.constants import ( MOCK_CUSTOM_INSTALLATION_PATH, MOCK_ALLOCATION_INFILE, MOCK_REGISTRY_FILEPATH, - ONE_YEAR_IN_SECONDS -) + DEVELOPMENT_ETH_AIRDROP_AMOUNT, + ONE_YEAR_IN_SECONDS, + MINERS_ESCROW_DEPLOYMENT_SECRET, + POLICY_MANAGER_DEPLOYMENT_SECRET, + MINING_ADJUDICATOR_DEPLOYMENT_SECRET, + USER_ESCROW_PROXY_DEPLOYMENT_SECRET) from nucypher.utilities.sandbox.constants import MOCK_CUSTOM_INSTALLATION_PATH_2 @@ -93,6 +97,41 @@ def custom_filepath_2(): shutil.rmtree(_custom_filepath, ignore_errors=True) +@pytest.fixture(scope='session') +def deployed_blockchain(token_economics): + + # Interface + compiler = SolidityCompiler() + registry = InMemoryEthereumContractRegistry() + allocation_registry = InMemoryAllocationRegistry() + interface = BlockchainDeployerInterface(compiler=compiler, + registry=registry, + provider_uri=TEST_PROVIDER_URI) + + # Blockchain + blockchain = TesterBlockchain(interface=interface, airdrop=True, test_accounts=5, poa=True) + deployer_address = blockchain.etherbase_account + + # Deployer + deployer = Deployer(blockchain=blockchain, deployer_address=deployer_address) + + # The Big Three (+ Dispatchers) + deployer.deploy_network_contracts(miner_secret=MINERS_ESCROW_DEPLOYMENT_SECRET, + policy_secret=POLICY_MANAGER_DEPLOYMENT_SECRET, + adjudicator_secret=MINING_ADJUDICATOR_DEPLOYMENT_SECRET, + user_escrow_proxy_secret=USER_ESCROW_PROXY_DEPLOYMENT_SECRET) + + # Start with some hard-coded cases... + all_yall = blockchain.unassigned_accounts + allocation_data = [{'address': all_yall[1], + 'amount': token_economics.maximum_allowed_locked, + 'duration': ONE_YEAR_IN_SECONDS}] + + deployer.deploy_beneficiary_contracts(allocations=allocation_data, allocation_registry=allocation_registry) + + yield blockchain, deployer_address, registry + + @pytest.fixture(scope='module') def custom_filepath_2(): _custom_filepath = MOCK_CUSTOM_INSTALLATION_PATH_2 diff --git a/tests/cli/test_deploy.py b/tests/cli/test_deploy.py index c293fcd6e..d49cee71e 100644 --- a/tests/cli/test_deploy.py +++ b/tests/cli/test_deploy.py @@ -1,5 +1,9 @@ import json import os +from random import SystemRandom +from string import ascii_uppercase, digits + +import pytest from nucypher.blockchain.eth.actors import Deployer from nucypher.blockchain.eth.agents import ( @@ -10,17 +14,28 @@ from nucypher.blockchain.eth.agents import ( MiningAdjudicatorAgent ) from nucypher.blockchain.eth.chains import Blockchain +from nucypher.blockchain.eth.interfaces import BlockchainInterface from nucypher.blockchain.eth.registry import AllocationRegistry, EthereumContractRegistry from nucypher.cli.deploy import deploy from nucypher.config.constants import DEFAULT_CONFIG_ROOT from nucypher.utilities.sandbox.constants import ( - INSECURE_DEVELOPMENT_PASSWORD, TEST_PROVIDER_URI, MOCK_ALLOCATION_INFILE, - MOCK_REGISTRY_FILEPATH, MOCK_ALLOCATION_REGISTRY_FILEPATH) + MOCK_REGISTRY_FILEPATH, MOCK_ALLOCATION_REGISTRY_FILEPATH +) -def test_nucypher_deploy_contracts(testerchain, click_runner, mock_primary_registry_filepath): +def generate_insecure_secret() -> str: + insecure_secret = ''.join(SystemRandom().choice(ascii_uppercase + digits) for _ in range(16)) + formatted_secret = insecure_secret + '\n' + return formatted_secret + + +PLANNED_UPGRADES = 4 +INSECURE_SECRETS = {v: generate_insecure_secret() for v in range(1, PLANNED_UPGRADES+1)} + + +def test_nucypher_deploy_all_contracts(testerchain, click_runner, mock_primary_registry_filepath): # We start with a blockchain node, and nothing else... assert not os.path.isfile(mock_primary_registry_filepath) @@ -30,7 +45,7 @@ def test_nucypher_deploy_contracts(testerchain, click_runner, mock_primary_regis '--provider-uri', TEST_PROVIDER_URI, '--poa') - user_input = 'Y\n'+f'{INSECURE_DEVELOPMENT_PASSWORD}\n'*8 + user_input = 'Y\n' + (f'{INSECURE_SECRETS[1]}\n' * 8) result = click_runner.invoke(deploy, command, input=user_input, catch_exceptions=False) assert result.exit_code == 0 @@ -74,80 +89,126 @@ def test_nucypher_deploy_contracts(testerchain, click_runner, mock_primary_regis def test_upgrade_contracts(click_runner): - contracts_to_upgrade = ('MinersEscrow', # Initial upgrades (version 2) - 'PolicyManager', - 'MiningAdjudicator', - 'UserEscrowProxy', - # Additional Upgrades - 'MinersEscrow', # v3 - 'MinersEscrow', # v4 - 'MiningAdjudicator', # v3 - 'PolicyAgent,', # v3 - 'UserEscrowProxy' # v3 - ) + # + # Setup + # - executed_upgrades = {name: 0 for name in set(contracts_to_upgrade)} - - blockchain = Blockchain.connect(registry=EthereumContractRegistry(registry_filepath=MOCK_REGISTRY_FILEPATH)) - - yes = 'Y\n' - version_1_secret = f'{INSECURE_DEVELOPMENT_PASSWORD}\n' * 2 - version_2_secret = f'{INSECURE_DEVELOPMENT_PASSWORD[::-1]}\n' * 2 - version_3_secret = f'{INSECURE_DEVELOPMENT_PASSWORD[2:-2:-1]}\n' * 2 - version_4_secret = f'{INSECURE_DEVELOPMENT_PASSWORD[1:-3:-1]}\n' * 2 - - user_input_1_to_2 = yes + version_1_secret + version_2_secret - user_input_2_to_3 = yes + version_2_secret + version_3_secret - user_input_3_to_4 = yes + version_3_secret + version_4_secret - - user_inputs = {0: user_input_1_to_2, - 1: user_input_2_to_3, - 2: user_input_3_to_4} + # Connect to the blockchain with a blank temporary file-based registry + mock_temporary_registry = EthereumContractRegistry(registry_filepath=MOCK_REGISTRY_FILEPATH) + blockchain = Blockchain.connect(registry=mock_temporary_registry) + # Check the existing state of the registry before the meat and potatoes expected_registrations = 9 with open(MOCK_REGISTRY_FILEPATH, 'r') as file: raw_registry_data = file.read() registry_data = json.loads(raw_registry_data) assert len(registry_data) == expected_registrations + # + # Input Components + # + + cli_action = 'upgrade' + base_command = ('--registry-infile', MOCK_REGISTRY_FILEPATH, '--provider-uri', TEST_PROVIDER_URI, '--poa') + + # Generate user inputs + yes = 'Y\n' # :-) + upgrade_inputs = dict() + for version, insecure_secret in INSECURE_SECRETS.items(): + + next_version = version + 1 + old_secret = INSECURE_SECRETS[version] + try: + new_secret = INSECURE_SECRETS[next_version] + except KeyError: + continue + + user_input = yes + old_secret + (new_secret * 2) # twice for confirmation prompt + upgrade_inputs[next_version] = user_input + + # + # Stage Upgrades + # + + contracts_to_upgrade = ('MinersEscrow', # v1 -> v2 + 'PolicyManager', # v1 -> v2 + 'MiningAdjudicator', # v1 -> v2 + 'UserEscrowProxy', # v1 -> v2 + + 'MinersEscrow', # v2 -> v3 + 'MinersEscrow', # v3 -> v4 + + 'MiningAdjudicator', # v2 -> v3 + 'PolicyManager', # v2 -> v3 + 'UserEscrowProxy', # v2 -> v3 + + 'UserEscrowProxy', # v3 -> v4 + 'PolicyManager', # v3 -> v4 + 'MiningAdjudicator', # v3 -> v4 + + ) # NOTE: Keep all versions the same in this test (all version 4, for example) + + # Each contract starts at version 1 + version_tracker = {name: 1 for name in contracts_to_upgrade} + + # + # Upgrade Contracts + # + for contract_name in contracts_to_upgrade: - command = ('upgrade', - '--contract-name', contract_name, - '--registry-infile', MOCK_REGISTRY_FILEPATH, - '--provider-uri', TEST_PROVIDER_URI, - '--poa') + # Assemble CLI command + command = (cli_action, '--contract-name', contract_name, *base_command) - user_input = user_inputs[executed_upgrades[contract_name]] + # Select upgrade interactive input scenario + current_version = version_tracker[contract_name] + new_version = current_version + 1 + user_input = upgrade_inputs[new_version] + + # Execute upgrade (Meat) result = click_runner.invoke(deploy, command, input=user_input, catch_exceptions=False) - assert result.exit_code == 0 + assert result.exit_code == 0 # TODO: Console painting - executed_upgrades[contract_name] += 1 + # Mutate the version tracking + version_tracker[contract_name] += 1 expected_registrations += 1 + # Verify the registry is updated (Potatoes) with open(MOCK_REGISTRY_FILEPATH, 'r') as file: + + # Read the registry file directly, bypassing its interfaces raw_registry_data = file.read() registry_data = json.loads(raw_registry_data) assert len(registry_data) == expected_registrations # Check that there is more than one entry, since we've deployed a "version 2" + expected_enrollments = current_version + 1 + registered_names = [r[0] for r in registry_data] - assert registered_names.count(contract_name) == 2 + enrollments = registered_names.count(contract_name) + + assert enrollments > 1, f"New contract is not enrolled in {MOCK_REGISTRY_FILEPATH}" + assert enrollments == expected_enrollments, f"Incorrect number of records enrolled for {contract_name}. " \ + f"Expected {expected_enrollments} got {enrollments}." # Ensure deployments are different addresses records = blockchain.interface.registry.search(contract_name=contract_name) - assert len(records) == executed_upgrades[contract_name] + 1 + assert len(records) == expected_enrollments - # Get the last two entries - old, new = records[-2:] - old_name, old_address, *abi = old - new_name, new_address, *abi = new - assert old_name == new_name + old, new = records[-2:] # Get the last two entries + old_name, old_address, *abi = old # Previous version + new_name, new_address, *abi = new # New version + assert old_name == new_name # TODO: Inspect ABI? assert old_address != new_address + # Select proxy (Dispatcher vs Linker) + if contract_name == "UserEscrowProxy": + proxy_name = "UserEscrowLibraryLinker" + else: + proxy_name = 'Dispatcher' + # Ensure the proxy targets the new deployment - proxy_name = 'Dispatcher' if contract_name != "UserEscrowProxy" else "UserEscrowLibraryLinker" proxy = blockchain.interface.get_proxy(target_address=new_address, proxy_name=proxy_name) targeted_address = proxy.functions.target().call() assert targeted_address != old_address @@ -157,14 +218,23 @@ def test_upgrade_contracts(click_runner): def test_rollback(click_runner): """Roll 'em all back!""" - contracts_to_rollback = ('MinersEscrow', 'PolicyManager', 'MiningAdjudicator') + mock_temporary_registry = EthereumContractRegistry(registry_filepath=MOCK_REGISTRY_FILEPATH) + blockchain = Blockchain.connect(registry=mock_temporary_registry) - user_input = 'Y\n' \ - + f'{INSECURE_DEVELOPMENT_PASSWORD[::-1]}\n' * 2 \ - + f'{INSECURE_DEVELOPMENT_PASSWORD}\n' * 2 + # Input Components + yes = 'Y\n' - blockchain = Blockchain.connect(registry=EthereumContractRegistry(registry_filepath=MOCK_REGISTRY_FILEPATH)) + # Stage Rollbacks + old_secret = INSECURE_SECRETS[PLANNED_UPGRADES] + rollback_secret = generate_insecure_secret() + user_input = yes + old_secret + rollback_secret + rollback_secret + contracts_to_rollback = ('MinersEscrow', # v4 -> v3 + 'PolicyManager', # v4 -> v3 + 'MiningAdjudicator', # v4 -> v3 + # 'UserEscrowProxy' # v4 -> v3 # TODO + ) + # Execute Rollbacks for contract_name in contracts_to_rollback: command = ('rollback', @@ -176,20 +246,32 @@ def test_rollback(click_runner): result = click_runner.invoke(deploy, command, input=user_input, catch_exceptions=False) assert result.exit_code == 0 - records = blockchain.interface.registry.search(contract_name='UserEscrowProxy') - assert len(records) == 2 + records = blockchain.interface.registry.search(contract_name=contract_name) # TODO + assert len(records) == 4 - old, new = records - _name, old_address, *abi = old - _name, new_address, *abi = new - assert old_address != new_address + *old_records, v3, v4 = records + current_target, rollback_target = v4, v3 - # Ensure the proxy targets the old deployment - proxy_name = 'Dispatcher' - proxy = blockchain.interface.get_proxy(target_address=new_address, proxy_name=proxy_name) + _name, current_target_address, *abi = current_target + _name, rollback_target_address, *abi = rollback_target + assert current_target_address != rollback_target_address + + # Select proxy (Dispatcher vs Linker) + if contract_name == "UserEscrowProxy": + proxy_name = "UserEscrowLibraryLinker" + else: + proxy_name = 'Dispatcher' + + # Ensure the proxy targets the rollback target (previous version) + with pytest.raises(BlockchainInterface.UnknownContract): + blockchain.interface.get_proxy(target_address=current_target_address, proxy_name=proxy_name) + + proxy = blockchain.interface.get_proxy(target_address=rollback_target_address, proxy_name=proxy_name) + + # Deeper - Ensure the proxy targets the old deployment on-chain targeted_address = proxy.functions.target().call() - assert targeted_address != new_address - assert targeted_address == old_address + assert targeted_address != current_target + assert targeted_address == rollback_target_address def test_nucypher_deploy_allocations(testerchain, click_runner, mock_allocation_infile, token_economics): @@ -199,8 +281,7 @@ def test_nucypher_deploy_allocations(testerchain, click_runner, mock_allocation_ '--allocation-infile', MOCK_ALLOCATION_INFILE, '--allocation-outfile', MOCK_ALLOCATION_REGISTRY_FILEPATH, '--provider-uri', TEST_PROVIDER_URI, - '--poa', - ) + '--poa') user_input = 'Y\n'*2 result = click_runner.invoke(deploy, deploy_command, @@ -221,8 +302,7 @@ def test_destroy_registry(click_runner, mock_primary_registry_filepath): destroy_command = ('destroy-registry', '--registry-infile', mock_primary_registry_filepath, '--provider-uri', TEST_PROVIDER_URI, - '--poa', - ) + '--poa') user_input = 'Y\n'*2 result = click_runner.invoke(deploy, destroy_command, input=user_input, catch_exceptions=False) diff --git a/tests/cli/test_felix.py b/tests/cli/test_felix.py index 0eb5ec19c..1d74d2bf1 100644 --- a/tests/cli/test_felix.py +++ b/tests/cli/test_felix.py @@ -36,7 +36,7 @@ def test_run_felix(click_runner, testerchain, federated_ursulas, mock_primary_re 'FLASK_DEBUG': '1'} # Deploy contracts - deploy_args = ('contracts', + deploy_args = ('deploy', '--registry-outfile', mock_primary_registry_filepath, '--provider-uri', TEST_PROVIDER_URI, '--poa') diff --git a/tests/cli/test_mixed_configurations.py b/tests/cli/test_mixed_configurations.py index 46ada6390..e9ba676a3 100644 --- a/tests/cli/test_mixed_configurations.py +++ b/tests/cli/test_mixed_configurations.py @@ -42,7 +42,7 @@ def test_coexisting_configurations(click_runner, assert not os.path.isfile(known_nodes_dir) # Deploy contracts - deploy_args = ('contracts', + deploy_args = ('deploy', '--registry-outfile', mock_primary_registry_filepath, '--provider-uri', TEST_PROVIDER_URI, '--deployer-address', deployer,