diff --git a/nucypher/blockchain/eth/deployers.py b/nucypher/blockchain/eth/deployers.py index df743f016..d4bbf0db8 100644 --- a/nucypher/blockchain/eth/deployers.py +++ b/nucypher/blockchain/eth/deployers.py @@ -165,6 +165,13 @@ class BaseContractDeployer: agent = self.agency(registry=self.registry, contract=self._contract) return agent + def get_latest_enrollment(self, registry: BaseContractRegistry) -> Contract: + """Get the latest enrolled version of the contract from the registry.""" + contract = self.blockchain.get_contract_by_name(name=self.contract_name, + registry=registry, + use_proxy_address=False, + version='latest') + return contract class OwnableContractMixin: @@ -221,15 +228,17 @@ class UpgradeableContractMixin: raise self.ContractNotUpgradeable(f"{self.contract_name} is not upgradeable.") raise NotImplementedError - @classmethod - def get_latest_version(cls, registry: BaseContractRegistry, provider_uri: str = None) -> Contract: - """Get the latest version of the contract without assembling it with it's proxy.""" - if not cls._upgradeable: - raise cls.ContractNotUpgradeable(f"{cls.contract_name} is not upgradeable.") + def get_principal_contract(self, registry: BaseContractRegistry, provider_uri: str = None) -> Contract: + """ + Get the on-chain targeted version of the principal contract directly + without assembling it with it's proxy. + """ + if not self._upgradeable: + raise cls.ContractNotUpgradeable(f"{self.contract_name} is not upgradeable.") blockchain = BlockchainInterfaceFactory.get_interface(provider_uri=provider_uri) - contract = blockchain.get_contract_by_name(name=cls.contract_name, + contract = blockchain.get_contract_by_name(name=self.contract_name, registry=registry, - proxy_name=cls._proxy_deployer.contract_name, + proxy_name=self._proxy_deployer.contract_name, use_proxy_address=False) return contract @@ -247,8 +256,8 @@ class UpgradeableContractMixin: raise self.ContractNotUpgradeable(f"{self.contract_name} is not upgradeable.") # 1 - Get Bare Contracts - existing_bare_contract = self.get_latest_version(registry=self.registry, - provider_uri=self.blockchain.provider_uri) + existing_bare_contract = self.get_principal_contract(registry=self.registry, + provider_uri=self.blockchain.provider_uri) proxy_deployer = self._proxy_deployer(registry=self.registry, target_contract=existing_bare_contract, @@ -275,8 +284,8 @@ class UpgradeableContractMixin: self.check_deployment_readiness() # 2 - Get Bare Contracts - existing_bare_contract = self.get_latest_version(registry=self.registry, - provider_uri=self.blockchain.provider_uri) + existing_bare_contract = self.get_principal_contract(registry=self.registry, + provider_uri=self.blockchain.provider_uri) proxy_deployer = self._proxy_deployer(registry=self.registry, target_contract=existing_bare_contract, diff --git a/nucypher/blockchain/eth/interfaces.py b/nucypher/blockchain/eth/interfaces.py index 457fe45c8..df0984656 100644 --- a/nucypher/blockchain/eth/interfaces.py +++ b/nucypher/blockchain/eth/interfaces.py @@ -425,6 +425,7 @@ class BlockchainInterface: def get_contract_by_name(self, registry: BaseContractRegistry, name: str, + version: int = None, proxy_name: str = None, use_proxy_address: bool = True ) -> Union[Contract, List[tuple]]: @@ -438,9 +439,9 @@ class BlockchainInterface: if not target_contract_records: raise self.UnknownContract(f"No such contract records with name {name}.") - if proxy_name: # It's upgradeable - # Lookup proxies; Search for a published proxy that targets this contract record + if proxy_name: + # Lookup proxies; Search for a published proxy that targets this contract record proxy_records = registry.search(contract_name=proxy_name) results = list() @@ -474,11 +475,21 @@ class BlockchainInterface: except IndexError: raise self.UnknownContract(f"There are no Dispatcher records targeting '{name}'") - else: # It's not upgradeable + else: + # NOTE: 0 must be allowed as a valid version number if len(target_contract_records) != 1: - m = "Multiple records registered for non-upgradeable contract {}" - raise self.InterfaceError(m.format(name)) - _target_contract_name, selected_address, selected_abi = target_contract_records[0] + if version is None: + m = f"{len(target_contract_records)} records enrolled for contract {name} " \ + f"and no version index was supplied." + raise self.InterfaceError(m.format(name)) + version = self.__get_version_index(name=name, + version_index=version, + enrollments=len(target_contract_records)) + + else: + version = -1 # default + + _target_contract_name, selected_address, selected_abi = target_contract_records[version] # Create the contract from selected sources unified_contract = self.client.w3.eth.contract(abi=selected_abi, @@ -487,6 +498,23 @@ class BlockchainInterface: return unified_contract + @staticmethod + def __get_version_index(version_index: Union[int, str], enrollments: int, name: str): + version_names = {'latest': -1, 'earliest': 0} + try: + version = version_names[version_index] + except KeyError: + try: + version = int(version_index) + except ValueError: + what_is_this = version_index + raise ValueError(f"'{what_is_this}' is a valid version number") + else: + if version > enrollments - 1: + message = f"Version index '{version}' is larger than the number of enrollments for {name}." + raise ValueError(message) + return version + class BlockchainDeployerInterface(BlockchainInterface): diff --git a/nucypher/blockchain/eth/registry.py b/nucypher/blockchain/eth/registry.py index 390d78663..6872de5c7 100644 --- a/nucypher/blockchain/eth/registry.py +++ b/nucypher/blockchain/eth/registry.py @@ -22,11 +22,12 @@ import tempfile from abc import ABC, abstractmethod from json import JSONDecodeError from os.path import dirname, abspath -from typing import Union +from typing import Union, Iterator import requests from constant_sorrow.constants import REGISTRY_COMMITTED from twisted.logger import Logger +from web3.contract import Contract from nucypher.config.constants import DEFAULT_CONFIG_ROOT @@ -129,16 +130,16 @@ class BaseContractRegistry(ABC): return instance @property - def enrolled_names(self): + def enrolled_names(self) -> Iterator: entries = iter(record[0] for record in self.read()) return entries @property - def enrolled_addresses(self): + def enrolled_addresses(self) -> Iterator: entries = iter(record[1] for record in self.read()) return entries - def enroll(self, contract_name, contract_address, contract_abi): + def enroll(self, contract_name, contract_address, contract_abi) -> None: """ Enrolls a contract to the chain registry by writing the name, address, and abi information to the filesystem as JSON. @@ -157,7 +158,7 @@ class BaseContractRegistry(ABC): self.write(registry_data) self.log.info("Enrolled {}:{} into registry.".format(contract_name, contract_address)) - def search(self, contract_name: str = None, contract_address: str = None): + def search(self, contract_name: str = None, contract_address: str = None) -> tuple: """ Searches the registry for a contract with the provided name or address and returns the contracts component data. @@ -185,7 +186,8 @@ class BaseContractRegistry(ABC): self.log.critical(m) raise self.IllegalRegistry(m.format(contract_address)) - return contracts if contract_name else contracts[0] + result = tuple(contracts) if contract_name else contracts[0] + return result class LocalContractRegistry(BaseContractRegistry): diff --git a/tests/blockchain/eth/entities/deployers/test_staking_escrow_deployer.py b/tests/blockchain/eth/entities/deployers/test_staking_escrow_deployer.py index 1aba2b8b6..f48e42410 100644 --- a/tests/blockchain/eth/entities/deployers/test_staking_escrow_deployer.py +++ b/tests/blockchain/eth/entities/deployers/test_staking_escrow_deployer.py @@ -155,7 +155,7 @@ def test_deploy_bare_upgradeable_contract_deployment(testerchain, test_registry, new_number_of_enrollments = enrolled_names.count(StakingEscrowDeployer.contract_name) new_number_of_proxy_enrollments = enrolled_names.count(StakingEscrowDeployer._proxy_deployer.contract_name) - # The prinicipal contract was deployed. + # The principal contract was deployed. assert new_number_of_enrollments == (old_number_of_enrollments + 1) # The Dispatcher was not deployed. @@ -169,30 +169,31 @@ def test_manual_proxy_retargeting(testerchain, test_registry, token_economics): economics=token_economics) # Get Proxy-Direct - existing_bare_contract = deployer.get_latest_version(registry=test_registry, provider_uri=TEST_PROVIDER_URI) + existing_bare_contract = deployer.get_principal_contract(registry=test_registry, provider_uri=TEST_PROVIDER_URI) proxy_deployer = StakingEscrowDeployer._proxy_deployer(registry=test_registry, target_contract=existing_bare_contract, deployer_address=testerchain.etherbase_account, bare=True) # acquire agency for the proxy itself. # Re-Deploy Staking Escrow - staking_agent = ContractAgency.get_agent(StakingEscrowAgent, registry=test_registry) old_target = proxy_deployer.contract.functions.target().call() old_secret = bytes("...maybe not.", encoding='utf-8') new_secret = keccak_digest(bytes('thistimeforsure', encoding='utf-8')) - receipt = deployer.retarget(target_address=staking_agent.contract_address, + + # Get the latest un-targeted contract from the registry + latest_deployment = deployer.get_latest_enrollment(registry=test_registry) + receipt = deployer.retarget(target_address=latest_deployment.address, existing_secret_plaintext=old_secret, new_secret_hash=new_secret) assert receipt['status'] == 1 - # - # Post-Retargeting - # - - staking_agent = ContractAgency.get_agent(StakingEscrowAgent, registry=test_registry) + # Check proxy targets new_target = proxy_deployer.contract.functions.target().call() - assert old_target != new_target - assert new_target == staking_agent.contract_address + assert new_target == latest_deployment.address + + # Check address consistency + new_bare_contract = deployer.get_principal_contract(registry=test_registry, provider_uri=TEST_PROVIDER_URI) + assert new_bare_contract.address == latest_deployment.address == new_target