diff --git a/nucypher/cli/deploy.py b/nucypher/cli/deploy.py index feecd0932..de35ccabf 100644 --- a/nucypher/cli/deploy.py +++ b/nucypher/cli/deploy.py @@ -34,7 +34,6 @@ from nucypher.config.constants import DEFAULT_CONFIG_ROOT @click.argument('action') @click.option('--force', is_flag=True) @click.option('--poa', help="Inject POA middleware", is_flag=True) -@click.option('--upgrade', help="Upgrade an already deployed contract", is_flag=True) @click.option('--no-compile', help="Disables solidity contract compilation", is_flag=True) @click.option('--provider-uri', help="Blockchain provider's URI", type=click.STRING) @click.option('--config-root', help="Custom configuration directory", type=click.Path()) @@ -50,7 +49,6 @@ from nucypher.config.constants import DEFAULT_CONFIG_ROOT def deploy(click_config, action, poa, - upgrade, provider_uri, deployer_address, contract_name, @@ -95,7 +93,7 @@ def deploy(click_config, # Upgrade # - if upgrade: + 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) @@ -103,26 +101,31 @@ def deploy(click_config, deployer.upgrade_contract(contract_name=contract_name, existing_secret=existing_secret, new_plaintext_secret=new_secret) return - - # - # Deploy Single Contract - # - - if contract_name: - - try: - deployer_func = deployer.deployers[contract_name] - except KeyError: - message = "No such contract {}. Available contracts are {}".format(contract_name, deployer.deployers.keys()) - click.secho(message, fg='red', bold=True) - raise click.Abort() - else: - _txs, _agent = deployer_func() - + elif action == 'rollback': + existing_secret = click.prompt('Enter existing contract upgrade secret', hide_input=True, confirmation_prompt=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) return - # The Big Three - if action == "contracts": + elif action == "deploy": + + # + # Deploy Single Contract + # + + if contract_name: + + try: + deployer_func = deployer.deployers[contract_name] + except KeyError: + message = "No such contract {}. Available contracts are {}".format(contract_name, + deployer.deployers.keys()) + click.secho(message, fg='red', bold=True) + raise click.Abort() + else: + _txs, _agent = deployer_func() + + return secrets = click_config.collect_deployment_secrets() @@ -169,6 +172,7 @@ def deploy(click_config, click.secho("Block #{} | {}\n".format(receipt['blockNumber'], receipt['blockHash'].hex())) click.secho("Cumulative Gas Consumption: {} gas\n".format(total_gas_used), bold=True, fg='blue') + return elif action == "allocations": if not allocation_infile: @@ -176,6 +180,7 @@ def deploy(click_config, click.confirm("Continue deploying and allocating?", abort=True) deployer.deploy_beneficiaries_from_file(allocation_data_filepath=allocation_infile, allocation_outfile=allocation_outfile) + return elif action == "transfer": token_agent = NucypherTokenAgent(blockchain=blockchain) @@ -189,6 +194,7 @@ def deploy(click_config, click.confirm(f"Are you absolutely sure you want to destroy the contract registry at {registry_filepath}?", abort=True) os.remove(registry_filepath) click.secho(f"Successfully destroyed {registry_filepath}", fg='red') + return else: raise click.BadArgumentUsage(message=f"Unknown action '{action}'") diff --git a/tests/cli/test_deploy.py b/tests/cli/test_deploy.py index 2f5803a7d..c293fcd6e 100644 --- a/tests/cli/test_deploy.py +++ b/tests/cli/test_deploy.py @@ -25,7 +25,7 @@ def test_nucypher_deploy_contracts(testerchain, click_runner, mock_primary_regis # We start with a blockchain node, and nothing else... assert not os.path.isfile(mock_primary_registry_filepath) - command = ('contracts', + command = ('deploy', '--registry-outfile', mock_primary_registry_filepath, '--provider-uri', TEST_PROVIDER_URI, '--poa') @@ -74,86 +74,122 @@ 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', - miner_agent = MinerAgent() - contract_name = miner_agent.contract_name - bound_miner_escrow_address = miner_agent.contract_address + # Additional Upgrades + 'MinersEscrow', # v3 + 'MinersEscrow', # v4 + 'MiningAdjudicator', # v3 + 'PolicyAgent,', # v3 + 'UserEscrowProxy' # v3 + ) - command = ('contracts', - '--upgrade', - '--contract-name', contract_name, - '--registry-infile', MOCK_REGISTRY_FILEPATH, - '--provider-uri', TEST_PROVIDER_URI, - '--poa') - - user_input = 'Y\n' \ - + f'{INSECURE_DEVELOPMENT_PASSWORD}\n' * 2 \ - + f'{INSECURE_DEVELOPMENT_PASSWORD[::-1]}\n' * 2 - - result = click_runner.invoke(deploy, command, input=user_input, catch_exceptions=False) - assert result.exit_code == 0 - - # original bound address is the same - assert bound_miner_escrow_address == miner_agent.contract_address - - with open(MOCK_REGISTRY_FILEPATH, 'r') as file: - - # Ensure every contract's name was written to the file, somehow - raw_registry_data = file.read() - for registry_name in Deployer.contract_names: - assert registry_name in raw_registry_data - assert raw_registry_data.count(contract_name) == 2 - - registry_data = json.loads(raw_registry_data) - assert len(registry_data) == 10 + executed_upgrades = {name: 0 for name in set(contracts_to_upgrade)} blockchain = Blockchain.connect(registry=EthereumContractRegistry(registry_filepath=MOCK_REGISTRY_FILEPATH)) - records = blockchain.interface.registry.search(contract_name=contract_name) - assert len(records) == 2 - old, new = records - assert old[1] != new[1] # deployments are different addresses - # Ensure the dispatcher targets the new deployment - dispatcher = blockchain.interface.get_proxy(target_address=new[1], proxy_name='Dispatcher') - targeted_address = dispatcher.functions.target().call() - assert targeted_address != old[1] - assert targeted_address == new[1] + 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 - command = ('contracts', - '--upgrade', - '--contract-name', 'PolicyManager', - '--registry-infile', MOCK_REGISTRY_FILEPATH, - '--provider-uri', TEST_PROVIDER_URI, - '--poa') + 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 - result = click_runner.invoke(deploy, command, input=user_input, catch_exceptions=False) - assert result.exit_code == 0 - records = blockchain.interface.registry.search(contract_name='PolicyManager') - assert len(records) == 2 + user_inputs = {0: user_input_1_to_2, + 1: user_input_2_to_3, + 2: user_input_3_to_4} - command = ('contracts', - '--upgrade', - '--contract-name', 'MiningAdjudicator', - '--registry-infile', MOCK_REGISTRY_FILEPATH, - '--provider-uri', TEST_PROVIDER_URI, - '--poa') + 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 - result = click_runner.invoke(deploy, command, input=user_input, catch_exceptions=False) - assert result.exit_code == 0 - records = blockchain.interface.registry.search(contract_name='MiningAdjudicator') - assert len(records) == 2 + for contract_name in contracts_to_upgrade: - command = ('contracts', - '--upgrade', - '--contract-name', 'UserEscrowProxy', - '--registry-infile', MOCK_REGISTRY_FILEPATH, - '--provider-uri', TEST_PROVIDER_URI, - '--poa') + command = ('upgrade', + '--contract-name', contract_name, + '--registry-infile', MOCK_REGISTRY_FILEPATH, + '--provider-uri', TEST_PROVIDER_URI, + '--poa') - 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 + user_input = user_inputs[executed_upgrades[contract_name]] + result = click_runner.invoke(deploy, command, input=user_input, catch_exceptions=False) + assert result.exit_code == 0 + + executed_upgrades[contract_name] += 1 + expected_registrations += 1 + + 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 + + # Check that there is more than one entry, since we've deployed a "version 2" + registered_names = [r[0] for r in registry_data] + assert registered_names.count(contract_name) == 2 + + # Ensure deployments are different addresses + records = blockchain.interface.registry.search(contract_name=contract_name) + assert len(records) == executed_upgrades[contract_name] + 1 + + # 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 + assert old_address != new_address + + # 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 + assert targeted_address == new_address + + +def test_rollback(click_runner): + """Roll 'em all back!""" + + contracts_to_rollback = ('MinersEscrow', 'PolicyManager', 'MiningAdjudicator') + + user_input = 'Y\n' \ + + f'{INSECURE_DEVELOPMENT_PASSWORD[::-1]}\n' * 2 \ + + f'{INSECURE_DEVELOPMENT_PASSWORD}\n' * 2 + + blockchain = Blockchain.connect(registry=EthereumContractRegistry(registry_filepath=MOCK_REGISTRY_FILEPATH)) + + for contract_name in contracts_to_rollback: + + command = ('rollback', + '--contract-name', contract_name, + '--registry-infile', MOCK_REGISTRY_FILEPATH, + '--provider-uri', TEST_PROVIDER_URI, + '--poa') + + 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 + + old, new = records + _name, old_address, *abi = old + _name, new_address, *abi = new + assert old_address != new_address + + # Ensure the proxy targets the old deployment + proxy_name = 'Dispatcher' + proxy = blockchain.interface.get_proxy(target_address=new_address, proxy_name=proxy_name) + targeted_address = proxy.functions.target().call() + assert targeted_address != new_address + assert targeted_address == old_address def test_nucypher_deploy_allocations(testerchain, click_runner, mock_allocation_infile, token_economics):