diff --git a/nucypher/blockchain/eth/actors.py b/nucypher/blockchain/eth/actors.py index b0a318c66..a0d9970f2 100644 --- a/nucypher/blockchain/eth/actors.py +++ b/nucypher/blockchain/eth/actors.py @@ -170,12 +170,6 @@ class Deployer(NucypherTokenActor): deployer_address=self.deployer_address) return r - @classmethod - def from_blockchain(cls, provider_uri: str, registry=None, *args, **kwargs): - blockchain = BlockchainInterface.connect(provider_uri=provider_uri, registry=registry) - instance = cls(blockchain=blockchain, *args, **kwargs) - return instance - @property def deployer_address(self): return self.blockchain.deployer_address diff --git a/nucypher/blockchain/eth/interfaces.py b/nucypher/blockchain/eth/interfaces.py index 46dd2e460..14d67214d 100644 --- a/nucypher/blockchain/eth/interfaces.py +++ b/nucypher/blockchain/eth/interfaces.py @@ -49,7 +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 +from nucypher.crypto.powers import TransactingPower Web3Providers = Union[IPCProvider, WebsocketProvider, HTTPProvider, EthereumTester] @@ -86,7 +86,7 @@ class BlockchainInterface: sync_now: bool = True, provider_process: NuCypherGethProcess = NO_PROVIDER_PROCESS, provider_uri: str = NO_BLOCKCHAIN_CONNECTION, - transacting_power: BlockchainPower = READ_ONLY_INTERFACE, + transacting_power: TransactingPower = READ_ONLY_INTERFACE, provider: Web3Providers = NO_BLOCKCHAIN_CONNECTION, registry: EthereumContractRegistry = None, fetch_registry: bool = True): @@ -164,10 +164,10 @@ 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 @@ -208,21 +208,18 @@ 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, fetch_registry: bool = True, sync_now: bool = True): # Spawn child process if self._provider_process: self._provider_process.start() provider_uri = self._provider_process.provider_uri(scheme='file') else: - self.log.info(f"Using external Web3 Provider '{provider_uri}'") + provider_uri = self.provider_uri + self.log.info(f"Using external Web3 Provider '{self.provider_uri}'") # Attach Provider - self._attach_provider(provider=provider, provider_uri=provider_uri) + self._attach_provider(provider=self._provider, provider_uri=provider_uri) self.log.info("Connecting to {}".format(self.provider_uri)) if self._provider is NO_BLOCKCHAIN_CONNECTION: raise self.NoProvider("There are no configured blockchain providers") diff --git a/nucypher/characters/lawful.py b/nucypher/characters/lawful.py index cb97a81e2..27e435381 100644 --- a/nucypher/characters/lawful.py +++ b/nucypher/characters/lawful.py @@ -91,6 +91,7 @@ class Alice(Character, PolicyAuthor): controller=True, policy_agent=None, device = NO_STAKING_DEVICE, + client_password: str = None, *args, **kwargs) -> None: # @@ -120,14 +121,16 @@ class Alice(Character, PolicyAuthor): *args, **kwargs) if is_me and not federated_only: # TODO: #289 + transacting_power = TransactingPower(account=self.checksum_address, + device=device, + blockchain=self.blockchain) + self._crypto_power.consume_power_up(transacting_power, password=client_password) + PolicyAuthor.__init__(self, blockchain=self.blockchain, policy_agent=policy_agent, checksum_address=checksum_address) - transacting_power = TransactingPower(blockchain=self.blockchain, account=self.checksum_address) - self._crypto_power.consume_power_up(transacting_power) - if is_me and controller: self.controller = self._controller_class(alice=self) @@ -843,6 +846,7 @@ class Ursula(Teacher, Character, Worker): stake_tracker: StakeTracker = None, staking_agent: StakingEscrowAgent = None, device = NO_STAKING_DEVICE, + client_password: str = None, # Character password: str = None, @@ -889,6 +893,17 @@ class Ursula(Teacher, Character, Worker): # Ursula is a Decentralized Worker # if not federated_only: + + # Access staking node via node's transacting keys + transacting_power = TransactingPower(account=self.checksum_address, + device=device, + password=client_password, # FIXME: password from somewhere + blockchain=self.blockchain) + self._crypto_power.consume_power_up(transacting_power) + + # Use blockchain power to substantiate stamp + self.substantiate_stamp(client_password=password) + Worker.__init__(self, is_me=is_me, blockchain=self.blockchain, @@ -896,11 +911,6 @@ class Ursula(Teacher, Character, Worker): worker_address=worker_address, stake_tracker=stake_tracker) - # Access to worker's ETH client via node's transacting keys - transacting_power = TransactingPower(blockchain=self.blockchain, account=worker_address) - self._crypto_power.consume_power_up(transacting_power) - self.substantiate_stamp(client_password=password) # TODO: Use PowerUp / Derive from keyring - # # ProxyRESTServer and TLSHostingPower # # diff --git a/nucypher/config/node.py b/nucypher/config/node.py index 58a92a73f..c7c39e53e 100644 --- a/nucypher/config/node.py +++ b/nucypher/config/node.py @@ -193,6 +193,7 @@ class CharacterConfiguration(BaseConfiguration): sync_now=sync_now) # Read Ethereum Node Keyring + self.blockchain.connect(sync_now=sync_now) self.accounts = self.blockchain.client.accounts def connect_to_contracts(self) -> None: diff --git a/nucypher/crypto/powers.py b/nucypher/crypto/powers.py index cf58a880f..62e340014 100644 --- a/nucypher/crypto/powers.py +++ b/nucypher/crypto/powers.py @@ -62,9 +62,10 @@ class CryptoPower(object): else: return True - def consume_power_up(self, power_up): + def consume_power_up(self, power_up, *args, **kwargs): if isinstance(power_up, CryptoPowerUp): power_up_class = power_up.__class__ + power_up.activate(*args, **kwargs) power_up_instance = power_up elif CryptoPowerUp in inspect.getmro(power_up): power_up_class = power_up @@ -85,38 +86,66 @@ class CryptoPower(object): raise power_up_class.not_found_error -class CryptoPowerUp(object): +class CryptoPowerUp: """ Gives you MORE CryptoPower! """ confers_public_key = False + def activate(self, *args, **kwargs): + return + class TransactingPower(CryptoPowerUp): """ Allows for transacting on a Blockchain via web3 backend. """ not_found_error = NoTransactingPower - __accounts = {} - def __init__(self, blockchain: 'Blockchain', account: str, device = NO_STAKING_DEVICE): + def __init__(self, + blockchain, + account: str, + password: str = None, + device=NO_STAKING_DEVICE): """ - # TODO: TrustedDevice Integration Instantiates a TransactingPower for the given checksum_address. """ - self.blockchain = blockchain - self.client = self.blockchain.client - self.account = account - self.device = device - self.is_unlocked = False + if password and (device is not NO_STAKING_DEVICE): + raise ValueError(f"Cannot create a {self.__class__.__name__} with both a client and an device signer.") - def unlock_account(self, password: str): + self.blockchain = blockchain + + self.account = account + self.client = self.blockchain.client + self.device = device + + self.__password = password + self.__unlocked = False + self.unlock_account() + + @property + def is_unlocked(self): + return self.__unlocked + + def activate(self, password: str): + self.blockchain.connect() + self.unlock_account(password=password) + self.blockchain.transacting_power = self + self.__password = None + + def lock_account(self): + if self.client: + self.client.lock_account(address=self.account) + elif self.device: + # TODO: Implement TrustedDevice + raise NotImplementedError + self.__unlocked = False + + def unlock_account(self, password: str = None): """ Unlocks the account for the specified duration. If no duration is provided, it will remain unlocked indefinitely. """ - if not self.is_unlocked: - raise PowerUpError("Failed to unlock account {}".format(self.account)) if self.device is not NO_STAKING_DEVICE: _hd_path = self.device.get_address_path(checksum_address=self.account) @@ -125,17 +154,15 @@ class TransactingPower(CryptoPowerUp): if not ping == pong: raise self.device.NoDeviceDetected unlocked = True - else: unlocked = self.client.unlock_account(address=self.account, password=password) - self.is_unlocked = unlocked + self.__unlocked = unlocked def sign_message(self, message: bytes) -> bytes: """ Signs the message with the private key of the TransactingPower. """ - if not self.is_unlocked: raise PowerUpError("Failed to unlock account {}".format(self.account)) @@ -150,17 +177,17 @@ class TransactingPower(CryptoPowerUp): return signature - def sign_transaction(self, checksum_address: str, unsigned_transaction: dict) -> HexBytes: - if not self.__accounts.get(checksum_address, False): - raise PowerUpError("Account is locked.") + def sign_transaction(self, unsigned_transaction: dict) -> HexBytes: + if not self.is_unlocked: + raise PowerUpError("Failed to unlock account {}".format(self.account)) # HW Signer if self.device is not NO_STAKING_DEVICE: signed_raw_transaction = self.device.sign_eth_transaction(unsigned_transaction=unsigned_transaction, - checksum_address=checksum_address) + checksum_address=self.account) # Web3 Signer else: - # This check is also performed client-side. + # Note: This check is also performed client-side. sender_address = unsigned_transaction['from'] if sender_address != self.account: raise PowerUpError(f"'from' field must match key's {self.account}, but it was {sender_address}") diff --git a/nucypher/utilities/sandbox/blockchain.py b/nucypher/utilities/sandbox/blockchain.py index 647655620..cd9d41870 100644 --- a/nucypher/utilities/sandbox/blockchain.py +++ b/nucypher/utilities/sandbox/blockchain.py @@ -32,7 +32,7 @@ from nucypher.blockchain.eth.sol.compile import SolidityCompiler from nucypher.blockchain.eth.token import NU from nucypher.blockchain.eth.utils import epoch_to_period from nucypher.config.constants import BASE_DIR -from nucypher.crypto.powers import BlockchainPower +from nucypher.crypto.powers import TransactingPower from nucypher.utilities.sandbox.constants import ( NUMBER_OF_ETH_TEST_ACCOUNTS, NUMBER_OF_STAKERS_IN_BLOCKCHAIN_TESTS, @@ -110,12 +110,13 @@ class TesterBlockchain(BlockchainDeployerInterface): *args, **kwargs) self.log = Logger("test-blockchain") + self.connect() # Generate additional ethereum accounts for testing population = test_accounts - enough_accounts = len(self.w3.eth.accounts) >= population + enough_accounts = len(self.client.accounts) >= population if not enough_accounts: - accounts_to_make = population - len(self.w3.eth.accounts) + accounts_to_make = population - len(self.client.accounts) self.__generate_insecure_unlocked_accounts(quantity=accounts_to_make) assert test_accounts == len(self.w3.eth.accounts) @@ -212,7 +213,7 @@ class TesterBlockchain(BlockchainDeployerInterface): """For use with metric testing scripts""" testerchain = cls(compiler=SolidityCompiler()) - power = BlockchainPower(blockchain=testerchain, account=testerchain.client.etherbase) + power = TransactingPower(blockchain=testerchain, account=testerchain.etherbase_account) power.unlock_account(password=INSECURE_DEVELOPMENT_PASSWORD) testerchain.transacting_power = power diff --git a/tests/characters/test_crypto_characters_and_their_powers.py b/tests/characters/test_crypto_characters_and_their_powers.py index 017cccd66..fdae0de43 100644 --- a/tests/characters/test_crypto_characters_and_their_powers.py +++ b/tests/characters/test_crypto_characters_and_their_powers.py @@ -118,8 +118,8 @@ def test_anybody_can_verify(): def test_character_client_transacting_power(testerchain, agency): # TODO: Handle multiple providers - eth_address = testerchain.interface.w3.eth.accounts[0] - sig_privkey = testerchain.interface.provider.ethereum_tester.backend._key_lookup[eth_utils.to_canonical_address(eth_address)] + eth_address = testerchain.etherbase_account + sig_privkey = testerchain.provider.ethereum_tester.backend._key_lookup[eth_utils.to_canonical_address(eth_address)] sig_pubkey = sig_privkey.public_key signer = Character(is_me=True, blockchain=testerchain, checksum_address=eth_address) diff --git a/tests/fixtures.py b/tests/fixtures.py index df0b6fc40..bbd2e13d8 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -39,15 +39,12 @@ from nucypher.blockchain.eth.deployers import (NucypherTokenDeployer, PolicyManagerDeployer, DispatcherDeployer, AdjudicatorDeployer) -from nucypher.blockchain.eth.interfaces import BlockchainDeployerInterface -from nucypher.blockchain.eth.registry import InMemoryEthereumContractRegistry from nucypher.blockchain.eth.sol.compile import SolidityCompiler from nucypher.blockchain.eth.token import NU from nucypher.characters.lawful import Enrico, Bob from nucypher.config.characters import UrsulaConfiguration, AliceConfiguration, BobConfiguration -from nucypher.config.constants import BASE_DIR from nucypher.config.node import CharacterConfiguration -from nucypher.crypto.powers import BlockchainPower +from nucypher.crypto.powers import TransactingPower from nucypher.crypto.utils import canonical_address_from_umbral_key from nucypher.keystore import keystore from nucypher.keystore.db import Base @@ -59,8 +56,8 @@ from nucypher.utilities.sandbox.constants import (DEVELOPMENT_ETH_AIRDROP_AMOUNT MOCK_URSULA_STARTING_PORT, NUMBER_OF_URSULAS_IN_DEVELOPMENT_NETWORK, TEMPORARY_DOMAIN, - TEST_PROVIDER_URI - ) + TEST_PROVIDER_URI, + INSECURE_DEVELOPMENT_PASSWORD) from nucypher.utilities.sandbox.middleware import MockRestMiddleware from nucypher.utilities.sandbox.policy import generate_random_label from nucypher.utilities.sandbox.ursula import (make_decentralized_ursulas, @@ -371,10 +368,12 @@ def testerchain(): # Create the blockchain testerchain = TesterBlockchain(eth_airdrop=True, free_transactions=True) - # TODO: TransactingPower # Mock TransactingPower Consumption - testerchain.transacting_power = BlockchainPower(blockchain=testerchain, account=testerchain.etherbase_account) + testerchain.transacting_power = TransactingPower(blockchain=testerchain, + password=INSECURE_DEVELOPMENT_PASSWORD, + account=testerchain.etherbase_account) testerchain.deployer_address = testerchain.etherbase_account + testerchain.transacting_power.unlock_account() yield testerchain testerchain.disconnect() @@ -430,8 +429,8 @@ def stakers(agency, token_economics): blockchain = token_agent.blockchain # Mock Powerup consumption (Deployer) - blockchain.transacting_power = BlockchainPower(blockchain=blockchain, - account=blockchain.etherbase_account) + blockchain.transacting_power = TransactingPower(blockchain=blockchain, + account=blockchain.etherbase_account) token_airdrop(origin=blockchain.etherbase_account, addresses=blockchain.stakers_accounts, @@ -442,9 +441,10 @@ def stakers(agency, token_economics): for index, account in enumerate(blockchain.stakers_accounts): staker = Staker(is_me=True, checksum_address=account, blockchain=blockchain) - # Mock TransactingPower consumption (Ursula-Staker) - staker.blockchain.transacting_power = BlockchainPower(blockchain=staker.blockchain, - account=staker.checksum_address) + # Mock TransactingPower consumption + staker.blockchain.transacting_power = TransactingPower(blockchain=blockchain, + account=account, + password=INSECURE_DEVELOPMENT_PASSWORD) min_stake, balance = token_economics.minimum_allowed_locked, staker.token_balance amount = random.randint(min_stake, balance)