From b59d12d89cdc0e1b35b343589fdb7c28d0ab8259 Mon Sep 17 00:00:00 2001 From: Kieran Prasch Date: Wed, 19 Jun 2019 11:40:18 -0700 Subject: [PATCH] Iterating on BlockchainInterface Composition for accuracy; Use interface cache. --- nucypher/blockchain/eth/agents.py | 4 +- nucypher/blockchain/eth/deployers.py | 63 ++++----- nucypher/blockchain/eth/interfaces.py | 172 ++++++++++++----------- nucypher/cli/deploy.py | 14 +- nucypher/utilities/sandbox/blockchain.py | 8 +- 5 files changed, 128 insertions(+), 133 deletions(-) diff --git a/nucypher/blockchain/eth/agents.py b/nucypher/blockchain/eth/agents.py index 9da08d8ed..a9b82eb18 100644 --- a/nucypher/blockchain/eth/agents.py +++ b/nucypher/blockchain/eth/agents.py @@ -383,11 +383,11 @@ class UserEscrowAgent(EthereumContractAgent): def __init__(self, beneficiary: str, - blockchain: BlockchainInterface = None, + blockchain: BlockchainInterface, allocation_registry: AllocationRegistry = None, *args, **kwargs) -> None: - self.blockchain = blockchain or BlockchainInterface.connect() + self.blockchain = blockchain self.__allocation_registry = allocation_registry or self.__allocation_registry() self.__beneficiary = beneficiary diff --git a/nucypher/blockchain/eth/deployers.py b/nucypher/blockchain/eth/deployers.py index da4a47f4a..416d34e00 100644 --- a/nucypher/blockchain/eth/deployers.py +++ b/nucypher/blockchain/eth/deployers.py @@ -14,12 +14,11 @@ 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_tester.exceptions import TransactionFailed -from eth_utils import is_checksum_address from typing import Tuple, Dict -from web3.contract import Contract from constant_sorrow.constants import CONTRACT_NOT_DEPLOYED, NO_DEPLOYER_CONFIGURED, NO_BENEFICIARY +from eth_utils import is_checksum_address +from web3.contract import Contract from nucypher.blockchain.economics import TokenEconomics, SlashingEconomics from nucypher.blockchain.eth.agents import ( @@ -29,10 +28,8 @@ from nucypher.blockchain.eth.agents import ( PolicyAgent, UserEscrowAgent, AdjudicatorAgent) - from nucypher.blockchain.eth.interfaces import BlockchainDeployerInterface from nucypher.blockchain.eth.registry import AllocationRegistry -from .interfaces import BlockchainInterface class ContractDeployer: @@ -49,7 +46,7 @@ class ContractDeployer: class ContractNotDeployed(ContractDeploymentError): pass - def __init__(self, deployer_address: str, blockchain: BlockchainInterface) -> None: + def __init__(self, deployer_address: str, blockchain: BlockchainDeployerInterface) -> None: self.blockchain = blockchain self.deployment_transactions = CONTRACT_NOT_DEPLOYED @@ -288,7 +285,7 @@ class StakingEscrowDeployer(ContractDeployer): # Wrap the escrow contract wrapped_escrow_contract = self.blockchain._wrap_contract(dispatcher_contract, - target_contract=the_escrow_contract) + target_contract=the_escrow_contract) # Switch the contract for the wrapped one the_escrow_contract = wrapped_escrow_contract @@ -326,7 +323,6 @@ class StakingEscrowDeployer(ContractDeployer): # Raise if not all-systems-go self.check_deployment_readiness() - origin_args = {'from': self.deployer_address, 'gas': 5000000} # TODO: Gas management existing_bare_contract = self.blockchain.get_contract_by_name(name=self.contract_name, proxy_name=self.__proxy_deployer.contract_name, @@ -347,13 +343,12 @@ class StakingEscrowDeployer(ContractDeployer): self._contract = wrapped_escrow_contract # 4 - Set the new Dispatcher target - upgrade_tx_hash = dispatcher_deployer.retarget(new_target=the_escrow_contract.address, + upgrade_receipt = dispatcher_deployer.retarget(new_target=the_escrow_contract.address, existing_secret_plaintext=existing_secret_plaintext, new_secret_hash=new_secret_hash) - _upgrade_receipt = self.blockchain.wait_for_receipt(upgrade_tx_hash) # Respond - upgrade_transaction = {'deploy': deploy_txhash, 'retarget': upgrade_tx_hash} + upgrade_transaction = {'deploy': deploy_txhash, 'retarget': upgrade_receipt['transactionHash']} return upgrade_transaction def rollback(self, existing_secret_plaintext: bytes, new_secret_hash: bytes): @@ -365,11 +360,11 @@ class StakingEscrowDeployer(ContractDeployer): deployer_address=self.deployer_address, bare=True) # acquire agency for the dispatcher itself. - rollback_txhash = dispatcher_deployer.rollback(existing_secret_plaintext=existing_secret_plaintext, - new_secret_hash=new_secret_hash) + rollback_receipt = dispatcher_deployer.rollback(existing_secret_plaintext=existing_secret_plaintext, + new_secret_hash=new_secret_hash) - _rollback_receipt = self.blockchain.wait_for_receipt(txhash=rollback_txhash) - return rollback_txhash + txhash = rollback_receipt['transactionHash'] + return txhash def make_agent(self) -> EthereumContractAgent: self.__check_policy_manager() # Ensure the PolicyManager contract has already been initialized @@ -454,38 +449,34 @@ class PolicyManagerDeployer(ContractDeployer): policy_manager_contract, deploy_txhash = self.blockchain.deploy_contract(self.contract_name, self.staking_agent.contract_address) - upgrade_tx_hash = proxy_deployer.retarget(new_target=policy_manager_contract.address, + upgrade_receipt = proxy_deployer.retarget(new_target=policy_manager_contract.address, existing_secret_plaintext=existing_secret_plaintext, new_secret_hash=new_secret_hash) - _upgrade_receipt = self.blockchain.wait_for_receipt(upgrade_tx_hash) # Wrap the escrow contract wrapped_policy_manager_contract = self.blockchain._wrap_contract(proxy_deployer.contract, - target_contract=policy_manager_contract) + target_contract=policy_manager_contract) # Switch the contract for the wrapped one policy_manager_contract = wrapped_policy_manager_contract - self._contract = policy_manager_contract - upgrade_transaction = {'deploy': deploy_txhash, - 'retarget': upgrade_tx_hash} - + upgrade_transaction = {'deploy': deploy_txhash, 'retarget': upgrade_receipt['transactionHash']} return upgrade_transaction def rollback(self, existing_secret_plaintext: bytes, new_secret_hash: bytes): existing_bare_contract = self.blockchain.get_contract_by_name(name=self.contract_name, - proxy_name=self.__proxy_deployer.contract_name, - use_proxy_address=False) + proxy_name=self.__proxy_deployer.contract_name, + use_proxy_address=False) dispatcher_deployer = DispatcherDeployer(blockchain=self.blockchain, target_contract=existing_bare_contract, deployer_address=self.deployer_address, bare=True) # acquire agency for the dispatcher itself. - rollback_txhash = dispatcher_deployer.rollback(existing_secret_plaintext=existing_secret_plaintext, - new_secret_hash=new_secret_hash) + rollback_receipt = dispatcher_deployer.rollback(existing_secret_plaintext=existing_secret_plaintext, + new_secret_hash=new_secret_hash) - _rollback_receipt = self.blockchain.wait_for_receipt(txhash=rollback_txhash) + rollback_txhash = rollback_receipt['transactionHash'] return rollback_txhash @@ -498,7 +489,7 @@ class LibraryLinkerDeployer(ContractDeployer): super().__init__(*args, **kwargs) if bare: self._contract = self.blockchain.get_proxy(target_address=self.target_contract.address, - proxy_name=self.contract_name) + proxy_name=self.contract_name) def deploy(self, secret_hash: bytes, gas_limit: int = None) -> dict: linker_args = (self.contract_name, self.target_contract.address, secret_hash) @@ -552,7 +543,7 @@ class UserEscrowProxyDeployer(ContractDeployer): deployer_address=self.deployer_address, target_contract=user_escrow_proxy_contract) - proxy_deployment_txhashes = proxy_deployer.deploy(secret_hash=secret_hash, gas_limit=gas_limit) + _proxy_deployment_txhashes = proxy_deployer.deploy(secret_hash=secret_hash, gas_limit=gas_limit) deployment_transactions['proxy_deployment'] = proxy_deployment_txhash return deployment_transactions @@ -603,10 +594,10 @@ class UserEscrowProxyDeployer(ContractDeployer): deployer_address=self.deployer_address, bare=True) # acquire agency for the dispatcher itself. - rollback_txhash = dispatcher_deployer.rollback(existing_secret_plaintext=existing_secret_plaintext, - new_secret_hash=new_secret_hash) + _rollback_receipt = dispatcher_deployer.rollback(existing_secret_plaintext=existing_secret_plaintext, + new_secret_hash=new_secret_hash) - _rollback_receipt = self.blockchain.wait_for_receipt(txhash=rollback_txhash) + rollback_txhash = _rollback_receipt['transactionHash'] return rollback_txhash @@ -642,6 +633,7 @@ class UserEscrowDeployer(ContractDeployer): """Relinquish ownership of a UserEscrow deployment to the beneficiary""" if not is_checksum_address(beneficiary_address): raise self.ContractDeploymentError("{} is not a valid checksum address.".format(beneficiary_address)) + # TODO: #413, #842 - Gas Management payload = {'from': self.deployer_address, 'gas': 500_000, 'gasPrice': self.blockchain.client.gasPrice} transfer_owner_function = self.contract.functions.transferOwnership(beneficiary_address) transfer_owner_receipt = self.blockchain.send_transaction(transaction_function=transfer_owner_function, @@ -660,7 +652,7 @@ class UserEscrowDeployer(ContractDeployer): allocation_receipts['approve'] = approve_receipt # Deposit - # TODO: Gas management + # TODO: #413, #842 - Gas Management args = {'from': self.deployer_address, 'gasPrice': self.blockchain.client.gasPrice, 'gas': 200_000} @@ -777,10 +769,9 @@ class AdjudicatorDeployer(ContractDeployer): self.staking_agent.contract_address, *self.__economics.deployment_parameters) - upgrade_tx_hash = proxy_deployer.retarget(new_target=adjudicator_contract.address, + upgrade_receipt = proxy_deployer.retarget(new_target=adjudicator_contract.address, existing_secret_plaintext=existing_secret_plaintext, new_secret_hash=new_secret_hash) - _upgrade_receipt = self.blockchain.wait_for_receipt(upgrade_tx_hash) # Wrap the escrow contract wrapped_adjudicator_contract = self.blockchain._wrap_contract(proxy_deployer.contract, target_contract=adjudicator_contract) @@ -790,7 +781,7 @@ class AdjudicatorDeployer(ContractDeployer): self._contract = policy_manager_contract - upgrade_transaction = {'deploy': deploy_txhash, 'retarget': upgrade_tx_hash} + upgrade_transaction = {'deploy': deploy_txhash, 'retarget': upgrade_receipt['transactionHash']} return upgrade_transaction def rollback(self, existing_secret_plaintext: bytes, new_secret_hash: bytes): diff --git a/nucypher/blockchain/eth/interfaces.py b/nucypher/blockchain/eth/interfaces.py index 97cb638bf..b0474cb4d 100644 --- a/nucypher/blockchain/eth/interfaces.py +++ b/nucypher/blockchain/eth/interfaces.py @@ -49,6 +49,7 @@ from nucypher.blockchain.eth.providers import ( ) from nucypher.blockchain.eth.registry import EthereumContractRegistry from nucypher.blockchain.eth.sol.compile import SolidityCompiler +from nucypher.crypto.powers import BlockchainPower Web3Providers = Union[IPCProvider, WebsocketProvider, HTTPProvider, EthereumTester] @@ -62,10 +63,11 @@ class BlockchainInterface: TIMEOUT = 180 # seconds NULL_ADDRESS = '0x' + '0' * 40 + _instance = NO_BLOCKCHAIN_CONNECTION process = NO_PROVIDER_PROCESS.bool_value(False) Web3 = Web3 - _contract_factory = ConciseContract + _contract_factory = Contract class InterfaceError(Exception): pass @@ -84,7 +86,7 @@ class BlockchainInterface: sync_now: bool = True, provider_process: NuCypherGethProcess = NO_PROVIDER_PROCESS, provider_uri: str = NO_BLOCKCHAIN_CONNECTION, - transacting_power = READ_ONLY_INTERFACE, + transacting_power: BlockchainPower = READ_ONLY_INTERFACE, provider: Web3Providers = NO_BLOCKCHAIN_CONNECTION, registry: EthereumContractRegistry = None, fetch_registry: bool = True): @@ -162,10 +164,12 @@ class BlockchainInterface: self.transacting_power = transacting_power self.registry = registry - self.__connect(provider=provider, - provider_uri=provider_uri, - fetch_registry=fetch_registry, - sync_now=sync_now) + self.connect(provider=provider, + provider_uri=provider_uri, + fetch_registry=fetch_registry, + sync_now=sync_now) + + BlockchainInterface._instance = self def __repr__(self): r = '{name}({uri})'.format(name=self.__class__.__name__, uri=self.provider_uri) @@ -189,8 +193,13 @@ class BlockchainInterface: def disconnect(self): if self._provider_process: self._provider_process.stop() - self._provider_process = NO_BLOCKCHAIN_CONNECTION + self._provider_process = NO_PROVIDER_PROCESS self._provider = NO_BLOCKCHAIN_CONNECTION + BlockchainInterface._instance = NO_BLOCKCHAIN_CONNECTION + + @classmethod + def reconnect(cls, *args, **kwargs) -> 'BlockchainInterface': + return cls._instance def attach_middleware(self): @@ -199,11 +208,11 @@ class BlockchainInterface: self.log.debug('Injecting POA middleware at layer 0') self.client.inject_middleware(geth_poa_middleware, layer=0) - def __connect(self, - provider: Web3Providers = None, - provider_uri: str = None, - fetch_registry: bool = True, - sync_now: bool = True): + def connect(self, + provider: Web3Providers = None, + provider_uri: str = None, + fetch_registry: bool = True, + sync_now: bool = True): # Spawn child process if self._provider_process: @@ -239,6 +248,10 @@ class BlockchainInterface: return self.is_connected + @property + def provider(self) -> Union[IPCProvider, WebsocketProvider, HTTPProvider]: + return self._provider + def _attach_provider(self, provider: Web3Providers = None, provider_uri: str = None) -> None: """ https://web3py.readthedocs.io/en/latest/providers.html#providers @@ -322,7 +335,7 @@ class BlockchainInterface: if deployment_status is 0: failure = f"Transaction transmitted, but receipt returned status code 0. " \ f"Full receipt: \n {pprint.pformat(receipt, indent=2)}" - raise self.DeploymentFailed(failure) + raise self.InterfaceError(failure) if deployment_status is UNKNOWN_TX_STATUS: self.log.info(f"Unknown transaction status for {txhash} (receipt did not contain a status field)") @@ -330,13 +343,71 @@ class BlockchainInterface: # Secondary check TODO: Is this a sensible check? tx = self.client.w3.eth.getTransaction(txhash) if tx["gas"] == receipt["gasUsed"]: - raise self.DeploymentFailed(f"Deployment transaction consumed 100% of transaction gas." - f"Full receipt: \n {pprint.pformat(receipt, indent=2)}") + raise self.InterfaceError(f"Transaction consumed 100% of transaction gas." + f"Full receipt: \n {pprint.pformat(receipt, indent=2)}") return receipt - def read(self, query_function: ContractFunction): - raise NotImplementedError # TODO + def get_contract_by_name(self, + name: str, + proxy_name: str = None, + use_proxy_address: bool = True + ) -> Union[Contract, List[tuple]]: + """ + Instantiate a deployed contract from registry data, + and assimilate it with it's proxy if it is upgradeable, + or return all registered records if use_proxy_address is False. + """ + target_contract_records = self.registry.search(contract_name=name) + + 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 fot a published proxy that targets this contract record + + proxy_records = self.registry.search(contract_name=proxy_name) + + results = list() + for proxy_name, proxy_addr, proxy_abi in proxy_records: + proxy_contract = self.client.w3.eth.contract(abi=proxy_abi, + address=proxy_addr, + ContractFactoryClass=self._contract_factory) + + # Read this dispatchers target address from the blockchain + proxy_live_target_address = proxy_contract.functions.target().call() + for target_name, target_addr, target_abi in target_contract_records: + + if target_addr == proxy_live_target_address: + if use_proxy_address: + pair = (proxy_addr, target_abi) + else: + pair = (proxy_live_target_address, target_abi) + else: + continue + + results.append(pair) + + if len(results) > 1: + address, abi = results[0] + message = "Multiple {} deployments are targeting {}".format(proxy_name, address) + raise self.InterfaceError(message.format(name)) + + else: + selected_address, selected_abi = results[0] + + else: # It's not upgradeable + 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] + + # Create the contract from selected sources + unified_contract = self.client.w3.eth.contract(abi=selected_abi, + address=selected_address, + ContractFactoryClass=self._contract_factory) + + return unified_contract class BlockchainDeployerInterface(BlockchainInterface): @@ -460,9 +531,7 @@ class BlockchainDeployerInterface(BlockchainInterface): ContractFactoryClass=Contract) return contract - def _wrap_contract(self, - wrapper_contract: Contract, - target_contract: Contract) -> Contract: + def _wrap_contract(self, wrapper_contract: Contract, target_contract: Contract) -> Contract: """ Used for upgradeable contracts; Returns a new contract object assembled with its own address but the abi of the other. @@ -474,7 +543,7 @@ class BlockchainDeployerInterface(BlockchainInterface): ContractFactoryClass=self._contract_factory) return wrapped_contract - def get_proxy(self, target_address: str, proxy_name: str): + def get_proxy(self, target_address: str, proxy_name: str) -> Contract: # Lookup proxies; Search for a registered proxy that targets this contract record records = self.registry.search(contract_name=proxy_name) @@ -499,64 +568,3 @@ class BlockchainDeployerInterface(BlockchainInterface): return dispatchers[0] except IndexError: raise self.UnknownContract(f"No registered Dispatcher deployments target {target_address}") - - def get_contract_by_name(self, - name: str, - proxy_name: str = None, - use_proxy_address: bool = True - ) -> Union[Contract, List[tuple]]: - """ - Instantiate a deployed contract from registry data, - and assimilate it with it's proxy if it is upgradeable, - or return all registered records if use_proxy_address is False. - """ - target_contract_records = self.registry.search(contract_name=name) - - 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 fot a published proxy that targets this contract record - - proxy_records = self.registry.search(contract_name=proxy_name) - - results = list() - for proxy_name, proxy_addr, proxy_abi in proxy_records: - proxy_contract = self.client.w3.eth.contract(abi=proxy_abi, - address=proxy_addr, - ContractFactoryClass=self._contract_factory) - - # Read this dispatchers target address from the blockchain - proxy_live_target_address = proxy_contract.functions.target().call() - for target_name, target_addr, target_abi in target_contract_records: - - if target_addr == proxy_live_target_address: - if use_proxy_address: - pair = (proxy_addr, target_abi) - else: - pair = (proxy_live_target_address, target_abi) - else: - continue - - results.append(pair) - - if len(results) > 1: - address, abi = results[0] - message = "Multiple {} deployments are targeting {}".format(proxy_name, address) - raise self.InterfaceError(message.format(name)) - - else: - selected_address, selected_abi = results[0] - - else: # It's not upgradeable - 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] - - # Create the contract from selected sources - unified_contract = self.client.w3.eth.contract(abi=selected_abi, - address=selected_address, - ContractFactoryClass=self._contract_factory) - - return unified_contract diff --git a/nucypher/cli/deploy.py b/nucypher/cli/deploy.py index 8c21c0fc4..eb3f439e7 100644 --- a/nucypher/cli/deploy.py +++ b/nucypher/cli/deploy.py @@ -88,20 +88,18 @@ def deploy(click_config, # Connect to Blockchain # - # Establish a contract registry from disk if specified - registry, registry_filepath = None, (registry_outfile or registry_infile) - if registry_filepath is not None: - registry = EthereumContractRegistry(registry_filepath=registry_filepath) - if geth: # Spawn geth child process ETH_NODE = NuCypherGethDevnetProcess(config_root=config_root) ETH_NODE.ensure_account_exists(password=click_config.get_password(confirm=True)) - if not ETH_NODE.initialized: - ETH_NODE.initialize_blockchain() ETH_NODE.start() # TODO: Graceful shutdown provider_uri = ETH_NODE.provider_uri + # Establish a contract registry from disk if specified + registry, registry_filepath = None, (registry_outfile or registry_infile) + if registry_filepath is not None: + registry = EthereumContractRegistry(registry_filepath=registry_filepath) + # Deployment-tuned blockchain connection blockchain = BlockchainDeployerInterface(provider_uri=provider_uri, poa=poa, @@ -110,9 +108,9 @@ def deploy(click_config, fetch_registry=False, sync_now=sync) + # TODO: Integrate with Deployer Actor (Character) blockchain.transacting_power = BlockchainPower(client=blockchain.client) - # # Deployment Actor # diff --git a/nucypher/utilities/sandbox/blockchain.py b/nucypher/utilities/sandbox/blockchain.py index 1b3b99eaf..73f0c1fcc 100644 --- a/nucypher/utilities/sandbox/blockchain.py +++ b/nucypher/utilities/sandbox/blockchain.py @@ -65,6 +65,8 @@ class TesterBlockchain(BlockchainDeployerInterface): Blockchain subclass with additional test utility methods and options. """ + _instance = None + _PROVIDER_URI = 'tester://pyevm' TEST_CONTRACTS_DIR = os.path.join(BASE_DIR, 'tests', 'blockchain', 'eth', 'contracts', 'contracts') _compiler = SolidityCompiler(test_contract_dir=TEST_CONTRACTS_DIR) @@ -202,17 +204,13 @@ class TesterBlockchain(BlockchainDeployerInterface): f"| period {epoch_to_period(epoch=end_timestamp)} " f"| epoch {end_timestamp}") - @property - def provider(self) -> Union[IPCProvider, WebsocketProvider, HTTPProvider]: - return self._provider @classmethod def bootstrap_network(cls) -> Tuple['TesterBlockchain', Dict[str, EthereumContractAgent]]: - testerchain = cls.connect() + testerchain = cls.connect() # FIXME origin = testerchain.client.accounts[0] deployer = Deployer(blockchain=testerchain, deployer_address=origin, bare=True) - _txhashes, agents = deployer.deploy_network_contracts(staker_secret=STAKING_ESCROW_DEPLOYMENT_SECRET, policy_secret=POLICY_MANAGER_DEPLOYMENT_SECRET, adjudicator_secret=ADJUDICATOR_DEPLOYMENT_SECRET,