From e36d4da5d4a17c11fd1232b2372c5b1eed661667 Mon Sep 17 00:00:00 2001 From: Kieran Prasch Date: Tue, 29 Sep 2020 20:22:10 -0700 Subject: [PATCH] Deployment CLI plumbing adjustments: allow for domain-specefic pre-flight checks on upgrade. --- nucypher/blockchain/eth/registry.py | 2 +- nucypher/cli/commands/deploy.py | 49 +++--- nucypher/cli/literature.py | 1 + nucypher/cli/utils.py | 3 +- .../{test_deploy.py => test_deploy_cli.py} | 142 +++--------------- tests/acceptance/cli/test_deploy_commands.py | 1 + 6 files changed, 52 insertions(+), 146 deletions(-) rename tests/acceptance/cli/{test_deploy.py => test_deploy_cli.py} (65%) diff --git a/nucypher/blockchain/eth/registry.py b/nucypher/blockchain/eth/registry.py index fc14aa0f6..e5da62f3a 100644 --- a/nucypher/blockchain/eth/registry.py +++ b/nucypher/blockchain/eth/registry.py @@ -152,7 +152,7 @@ class RegistrySourceManager: def get_primary_sources(cls): return [source for source in cls._FALLBACK_CHAIN if source.is_primary] - def fetch_latest_publication(self, registry_class, network: str = NetworksInventory.DEFAULT): # TODO: see #1496 + def fetch_latest_publication(self, registry_class, network: str): # TODO: see #1496 """ Get the latest contract registry data available from a registry source chain. """ diff --git a/nucypher/cli/commands/deploy.py b/nucypher/cli/commands/deploy.py index efc70ff20..7cfc0e6ce 100644 --- a/nucypher/cli/commands/deploy.py +++ b/nucypher/cli/commands/deploy.py @@ -21,11 +21,10 @@ import click import os from constant_sorrow import constants from constant_sorrow.constants import FULL -from eth_utils.address import to_checksum_address from typing import Tuple from nucypher.blockchain.eth.actors import ContractAdministrator, Trustee -from nucypher.blockchain.eth.agents import ContractAgency, MultiSigAgent, NucypherTokenAgent +from nucypher.blockchain.eth.agents import ContractAgency, MultiSigAgent from nucypher.blockchain.eth.constants import STAKING_ESCROW_CONTRACT_NAME from nucypher.blockchain.eth.interfaces import BlockchainInterface from nucypher.blockchain.eth.networks import NetworksInventory @@ -37,7 +36,6 @@ from nucypher.blockchain.eth.registry import ( ) from nucypher.blockchain.eth.signers.base import Signer from nucypher.blockchain.eth.signers.software import ClefSigner -from nucypher.blockchain.eth.token import NU from nucypher.characters.control.emitters import StdoutEmitter from nucypher.cli.actions.auth import get_client_password from nucypher.cli.actions.confirm import confirm_deployment @@ -52,19 +50,14 @@ from nucypher.cli.literature import ( CONFIRM_NETWORK_ACTIVATION, CONFIRM_RETARGET, CONFIRM_SELECTED_ACCOUNT, - CONFIRM_TOKEN_ALLOWANCE, - CONFIRM_TOKEN_TRANSFER, CONTRACT_DEPLOYMENT_SERIES_BEGIN_ADVISORY, CONTRACT_IS_NOT_OWNABLE, DEPLOYER_ADDRESS_ZERO_ETH, DEPLOYER_BALANCE, - DISPLAY_SENDER_TOKEN_BALANCE_BEFORE_TRANSFER, EXISTING_REGISTRY_FOR_DOMAIN, MINIMUM_POLICY_RATE_EXCEEDED_WARNING, PROMPT_FOR_ALLOCATION_DATA_FILEPATH, PROMPT_NEW_OWNER_ADDRESS, - PROMPT_RECIPIENT_CHECKSUM_ADDRESS, - PROMPT_TOKEN_VALUE, REGISTRY_NOT_AVAILABLE, SELECT_DEPLOYER_ACCOUNT, SUCCESSFUL_REGISTRY_CREATION, @@ -76,7 +69,7 @@ from nucypher.cli.literature import ( SUCCESSFUL_SAVE_MULTISIG_TX_PROPOSAL, SUCCESSFUL_UPGRADE, UNKNOWN_CONTRACT_NAME, - IDENTICAL_REGISTRY_WARNING + IDENTICAL_REGISTRY_WARNING, DEPLOYER_IS_NOT_OWNER ) from nucypher.cli.options import ( group_options, @@ -106,7 +99,6 @@ from nucypher.cli.utils import ( establish_deployer_registry, initialize_deployer_interface ) -from nucypher.types import NuNits option_deployer_address = click.option('--deployer-address', help="Deployer's checksum address", type=EIP55_CHECKSUM_ADDRESS) option_registry_infile = click.option('--registry-infile', help="Input path for contract registry file", type=EXISTING_READABLE_FILE) @@ -138,7 +130,8 @@ class ActorOptions: se_test_mode, ignore_solidity_check, gas_strategy: str, - signer_uri: str + signer_uri: str, + network: str ): self.provider_uri = provider_uri @@ -156,6 +149,7 @@ class ActorOptions: self.poa = poa self.se_test_mode = se_test_mode self.ignore_solidity_check = ignore_solidity_check + self.network = network def create_actor(self, emitter: StdoutEmitter, @@ -175,11 +169,13 @@ class ActorOptions: # # Establish Registry # + local_registry = establish_deployer_registry(emitter=emitter, - use_existing_registry=bool(self.contract_name), # TODO: Eh? + use_existing_registry=bool(self.contract_name), # TODO: Issue #2314 registry_infile=self.registry_infile, registry_outfile=self.registry_outfile, - dev=self.dev) + dev=self.dev, + network=self.network) # # Make Authenticated Deployment Actor # @@ -240,7 +236,8 @@ group_actor_options = group_options( se_test_mode=click.option('--se-test-mode', help="Enable test mode for StakingEscrow in deployment.", is_flag=True), config_root=option_config_root, etherscan=option_etherscan, - ignore_solidity_check=option_ignore_solidity_version + ignore_solidity_check=option_ignore_solidity_version, + network=option_network(required=True, default=NetworksInventory.DEFAULT) ) @@ -333,24 +330,25 @@ def upgrade(general_config, actor_options, retarget, target_address, ignore_depl contract_name = actor_options.contract_name # Check deployer address is owner - agent = ContractAgency.get_agent_by_contract_name(contract_name=contract_name, registry=local_registry) - dispatcher = agent.get_proxy_contract() # Implicit verification of updatability - dispatcher_owner = dispatcher.owner # contract read - if deployer_address != dispatcher_owner: - DEPLOYER_IS_NOT_OWNER = f"Address {deployer_address} is not the owner of {contract_name}'s" \ - f" Dispatcher ({dispatcher.address}). Aborting." # TODO: Move to literature.py - emitter.echo(DEPLOYER_IS_NOT_OWNER) + Deployer = ADMINISTRATOR.deployers[contract_name] + deployer = Deployer(registry=local_registry) + if Deployer._ownable and deployer_address != deployer.owner: # blockchain read + emitter.echo(DEPLOYER_IS_NOT_OWNER.format(deployer_address=deployer_address, + contract_name=contract_name, + agent=deployer.make_agent())) raise click.Abort() + else: + emitter.echo('✓ Verified deployer address as contract owner', color='green') # Check registry ID has changed locally compared to remote source - github_registry = establish_deployer_registry(emitter=emitter, download_registry=True) - if github_registry.id == local_registry.id: + github_registry = establish_deployer_registry(emitter=emitter, download_registry=True, network=actor_options.network) + if (github_registry.id == local_registry.id) and not actor_options.force: emitter.echo(IDENTICAL_REGISTRY_WARNING.format(github_registry=github_registry, local_registry=local_registry), color='red') raise click.Abort() + else: + emitter.echo('✓ Verified local registry contains updates', color='green') - if not contract_name: - raise click.BadArgumentUsage(message="--contract-name is required when using --upgrade") # # Business @@ -397,6 +395,7 @@ def upgrade(general_config, actor_options, retarget, target_address, ignore_depl message = SUCCESSFUL_RETARGET.format(contract_name=contract_name, target_address=target_address) emitter.message(message, color='green') paint_receipt_summary(emitter=emitter, receipt=receipt) + else: if not actor_options.force: click.confirm(CONFIRM_BEGIN_UPGRADE.format(contract_name=contract_name), abort=True) diff --git a/nucypher/cli/literature.py b/nucypher/cli/literature.py index 3b31425b1..55cea6172 100644 --- a/nucypher/cli/literature.py +++ b/nucypher/cli/literature.py @@ -475,6 +475,7 @@ WARNING: --etherscan is disabled. If you want to see deployed contracts and TXs IDENTICAL_REGISTRY_WARNING = "Local registry ({local_registry.id}) is identical to the one on GitHub ({github_registry.id})." +DEPLOYER_IS_NOT_OWNER = "Address {deployer_address} is not the owner of {contract_name}'s Dispatcher ({agent.contract_address}). Aborting." # # Multisig diff --git a/nucypher/cli/utils.py b/nucypher/cli/utils.py index 3b76fa068..2512d1b24 100644 --- a/nucypher/cli/utils.py +++ b/nucypher/cli/utils.py @@ -105,6 +105,7 @@ def make_cli_character(character_config, def establish_deployer_registry(emitter, + network: str = None, registry_infile: str = None, registry_outfile: str = None, use_existing_registry: bool = False, @@ -113,7 +114,7 @@ def establish_deployer_registry(emitter, ) -> BaseContractRegistry: if download_registry: - registry = InMemoryContractRegistry.from_latest_publication() + registry = InMemoryContractRegistry.from_latest_publication(network=network) emitter.message(PRODUCTION_REGISTRY_ADVISORY.format(source=registry.source)) return registry diff --git a/tests/acceptance/cli/test_deploy.py b/tests/acceptance/cli/test_deploy_cli.py similarity index 65% rename from tests/acceptance/cli/test_deploy.py rename to tests/acceptance/cli/test_deploy_cli.py index 12f5f7e61..130eebcd5 100644 --- a/tests/acceptance/cli/test_deploy.py +++ b/tests/acceptance/cli/test_deploy_cli.py @@ -20,9 +20,10 @@ import json import os import pytest -from nucypher.blockchain.eth.actors import ContractAdministrator -from nucypher.blockchain.eth.agents import (AdjudicatorAgent, ContractAgency, NucypherTokenAgent, PolicyManagerAgent, - StakingEscrowAgent) +from nucypher.blockchain.eth.networks import NetworksInventory +from nucypher.config.constants import TEMPORARY_DOMAIN +from nucypher.blockchain.eth.agents import NucypherTokenAgent +from nucypher.blockchain.eth.clients import EthereumClient from nucypher.blockchain.eth.interfaces import BlockchainInterface from nucypher.blockchain.eth.registry import LocalContractRegistry from nucypher.blockchain.eth.sol.compile import SOLIDITY_COMPILER_VERSION @@ -33,6 +34,14 @@ PLANNED_UPGRADES = 4 CONTRACTS_TO_UPGRADE = ('StakingEscrow', 'PolicyManager', 'Adjudicator', 'StakingInterface') +@pytest.fixture(autouse=True) +def monkeypatch_confirmations(testerchain, monkeypatch): + def monkey_block_until_enough_confirmations(client, transaction_hash, timeout, confirmations): + receipt = testerchain.wait_for_receipt(txhash=transaction_hash) + return receipt + monkeypatch.setattr(EthereumClient, 'block_until_enough_confirmations', monkey_block_until_enough_confirmations) + + @pytest.fixture(scope="module") def registry_filepath(temp_dir_path): return os.path.join(temp_dir_path, 'nucypher-test-autodeploy.json') @@ -45,75 +54,6 @@ def test_echo_solidity_version(click_runner): assert str(SOLIDITY_COMPILER_VERSION) in result.output, 'Solidity version text was not produced.' -@pytest.mark.skip("Retired test") -def test_nucypher_deploy_contracts(click_runner, - token_economics, - registry_filepath, - testerchain): - - # - # Main - # - - assert not os.path.exists(registry_filepath), f"Registry File '{registry_filepath}' Exists." - assert not os.path.lexists(registry_filepath), f"Registry File '{registry_filepath}' Exists." - - command = ['contracts', - '--registry-outfile', registry_filepath, - '--provider', TEST_PROVIDER_URI, - '--se-test-mode'] - - user_input = '0\n' + YES_ENTER + 'DEPLOY' - result = click_runner.invoke(deploy, command, input=user_input, catch_exceptions=False) - assert result.exit_code == 0 - - # Ensure there is a report on each primary contract - contract_names = tuple(a.contract_name for a in ContractAdministrator.primary_deployer_classes) - for registry_name in contract_names: - assert registry_name in result.output - - # Check that the primary contract registry was written - # and peek at some of the registered entries - assert os.path.isfile(registry_filepath) - with open(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 contract_names: - assert registry_name in raw_registry_data - - # Ensure the Registry is JSON deserializable - registry_data = json.loads(raw_registry_data) - - # and that is has the correct number of entries - assert len(registry_data) == 9 - - # Read several records - token_record, escrow_record, dispatcher_record, *other_records = registry_data - registered_name, registered_version, registered_address, registered_abi = token_record - - # - # Agency - # - registry = LocalContractRegistry(filepath=registry_filepath) - - token_agent = NucypherTokenAgent(registry=registry) - assert token_agent.contract_name == registered_name - assert token_agent.contract_name == registered_name - assert token_agent.contract_address == registered_address - assert token_agent.contract.version == registered_version - - # Now show that we can use contract Agency and read from the blockchain - assert token_agent.get_balance() == 0 - staking_agent = ContractAgency.get_agent(StakingEscrowAgent, registry=registry) - assert staking_agent.get_current_period() - assert staking_agent.contract.functions.isTestContract().call() - - # and at least the others can be instantiated - assert PolicyManagerAgent(registry=registry) - assert AdjudicatorAgent(registry=registry) - - def test_deploy_single_contract(click_runner, tempfile_path): # Perform the Test @@ -125,59 +65,21 @@ def test_deploy_single_contract(click_runner, tempfile_path): user_input = '0\n' + YES_ENTER result = click_runner.invoke(deploy, command, input=user_input, catch_exceptions=False) - assert result.exit_code == 0 + assert result.exit_code == 0, result.output -def test_transfer_tokens(click_runner, registry_filepath, get_random_checksum_address, testerchain): +def test_upgrade_contracts(click_runner, test_registry_source_manager, test_registry, + testerchain, registry_filepath, agency): - # Let's transfer some NU to a random stranger - recipient_address = get_random_checksum_address() - - registry = LocalContractRegistry(filepath=registry_filepath) - token_agent = NucypherTokenAgent(registry=registry) - assert token_agent.get_balance(address=recipient_address) == 0 - - command = ['transfer-tokens', - '--target-address', recipient_address, - '--value', 42, - '--registry-infile', registry_filepath, - '--provider', TEST_PROVIDER_URI] - - user_input = '0\n' + YES_ENTER + YES_ENTER - result = click_runner.invoke(deploy, command, input=user_input, catch_exceptions=False) - assert result.exit_code == 0 - - # Check that the NU has arrived to the recipient - assert token_agent.get_balance(address=recipient_address) == 42 - - # Let's approve an allowance to a random spender - spender_address = get_random_checksum_address() - owner_address = testerchain.client.accounts[0] - assert token_agent.get_allowance(spender=spender_address, owner=owner_address) == 0 - - command = ['transfer-tokens', - '--target-address', spender_address, - '--value', 42, - '--allowance', - '--registry-infile', registry_filepath, - '--provider', TEST_PROVIDER_URI] - - user_input = '0\n' + YES_ENTER + YES_ENTER - result = click_runner.invoke(deploy, command, input=user_input, catch_exceptions=False) - assert result.exit_code == 0 - - # Check that the NU was approved for the spender - assert token_agent.get_allowance(spender=spender_address, owner=owner_address) == 42 - - -def test_upgrade_contracts(click_runner, registry_filepath, testerchain): + NetworksInventory.DEFAULT = TEMPORARY_DOMAIN + registry_filepath = test_registry.commit(filepath=registry_filepath) # # Setup # # Check the existing state of the registry before the meat and potatoes - expected_enrollments = 9 + expected_enrollments = 10 with open(registry_filepath, 'r') as file: raw_registry_data = file.read() registry_data = json.loads(raw_registry_data) @@ -190,7 +92,9 @@ def test_upgrade_contracts(click_runner, registry_filepath, testerchain): cli_action = 'upgrade' base_command = ('--registry-infile', registry_filepath, '--provider', TEST_PROVIDER_URI, - '--confirmations', 1) + '--confirmations', 30, + '--network', TEMPORARY_DOMAIN, + '--force') # skip some preflights # # Stage Upgrades @@ -251,7 +155,7 @@ def test_upgrade_contracts(click_runner, registry_filepath, testerchain): # 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, result.output assert "Successfully deployed" in result.output # Mutate the version tracking @@ -299,7 +203,7 @@ def test_upgrade_contracts(click_runner, registry_filepath, testerchain): assert targeted_address == new_address -def test_rollback(click_runner, testerchain, registry_filepath): +def test_rollback(click_runner, testerchain, registry_filepath, agency): """Roll 'em back!""" # Stage Rollbacks diff --git a/tests/acceptance/cli/test_deploy_commands.py b/tests/acceptance/cli/test_deploy_commands.py index 5072e2807..3a02fe03f 100644 --- a/tests/acceptance/cli/test_deploy_commands.py +++ b/tests/acceptance/cli/test_deploy_commands.py @@ -15,6 +15,7 @@ along with nucypher. If not, see . """ + import os import pytest