From d968cdd84852cc688cfb2dfe6a3ac1c298a0b09e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=BA=C3=B1ez?= Date: Fri, 4 Oct 2019 22:03:20 +0200 Subject: [PATCH] Add account selection to collect-reward. Complete tests for stake via contract. --- nucypher/blockchain/eth/actors.py | 7 +- nucypher/blockchain/eth/agents.py | 3 +- nucypher/characters/lawful.py | 4 +- nucypher/cli/characters/stake.py | 13 +- .../test_stake_via_allocation_contract.py | 323 +++++++++++++++++- .../cli/ursula/test_stakeholder_and_ursula.py | 20 +- 6 files changed, 352 insertions(+), 18 deletions(-) diff --git a/nucypher/blockchain/eth/actors.py b/nucypher/blockchain/eth/actors.py index 2413ec166..729a9ddee 100644 --- a/nucypher/blockchain/eth/actors.py +++ b/nucypher/blockchain/eth/actors.py @@ -827,8 +827,8 @@ class Staker(NucypherTokenActor): """Withdraw tokens rewarded for staking.""" if self.is_contract: reward_amount = self.staking_agent.calculate_staking_reward(staker_address=self.checksum_address) - self.log.debug(f"Withdrawing staking reward, {reward_amount}, to {self.checksum_address}") - receipt = self.preallocation_escrow_agent.withdraw_as_staker(amount=reward_amount) + self.log.debug(f"Withdrawing staking reward ({NU.from_nunits(reward_amount)}) to {self.checksum_address}") + receipt = self.preallocation_escrow_agent.withdraw_as_staker(value=reward_amount) else: receipt = self.staking_agent.collect_staking_reward(staker_address=self.checksum_address) return receipt @@ -838,7 +838,7 @@ class Staker(NucypherTokenActor): def withdraw(self, amount: NU) -> str: """Withdraw tokens (assuming they're unlocked)""" if self.is_contract: - receipt = self.preallocation_escrow_agent.withdraw_as_staker(amount=int(amount)) + receipt = self.preallocation_escrow_agent.withdraw_as_staker(value=int(amount)) else: receipt = self.staking_agent.withdraw(staker_address=self.checksum_address, amount=int(amount)) @@ -861,7 +861,6 @@ class Worker(NucypherTokenActor): work_tracker: WorkTracker = None, worker_address: str = None, start_working_now: bool = True, - confirm_now: bool = True, check_active_worker: bool = True, *args, **kwargs): diff --git a/nucypher/blockchain/eth/agents.py b/nucypher/blockchain/eth/agents.py index 5355b813d..b543fb344 100644 --- a/nucypher/blockchain/eth/agents.py +++ b/nucypher/blockchain/eth/agents.py @@ -374,7 +374,8 @@ class StakingEscrowAgent(EthereumContractAgent): def collect_staking_reward(self, staker_address: str): """Withdraw tokens rewarded for staking.""" reward_amount = self.calculate_staking_reward(staker_address=staker_address) - self.log.debug(f"Withdrawing staking reward, {reward_amount}, to {staker_address}") + from nucypher.blockchain.eth.token import NU + self.log.debug(f"Withdrawing staking reward ({NU.from_nunits(reward_amount)}) to {staker_address}") return self.withdraw(staker_address=staker_address, amount=reward_amount) @validate_checksum_address diff --git a/nucypher/characters/lawful.py b/nucypher/characters/lawful.py index 7f53ecb65..716715e8f 100644 --- a/nucypher/characters/lawful.py +++ b/nucypher/characters/lawful.py @@ -796,6 +796,7 @@ class Ursula(Teacher, Character, Worker): checksum_address: str = None, # Staker address worker_address: str = None, work_tracker: WorkTracker = None, + start_working_now: bool = True, client_password: str = None, # Character @@ -856,7 +857,8 @@ class Ursula(Teacher, Character, Worker): registry=self.registry, checksum_address=checksum_address, worker_address=worker_address, - work_tracker=work_tracker) + work_tracker=work_tracker, + start_working_now=start_working_now) # # ProxyRESTServer and TLSHostingPower # diff --git a/nucypher/cli/characters/stake.py b/nucypher/cli/characters/stake.py index d0a917cc7..d99e8054a 100644 --- a/nucypher/cli/characters/stake.py +++ b/nucypher/cli/characters/stake.py @@ -191,6 +191,7 @@ def stake(click_config, return # Exit elif action == 'accounts': + # TODO: Order accounts like shown by blockchain.client.accounts for address, balances in STAKEHOLDER.wallet.balances.items(): emitter.echo(f"{address} | {Web3.fromWei(balances['ETH'], 'ether')} ETH | {NU.from_nunits(balances['NU'])}") return # Exit @@ -420,16 +421,22 @@ def stake(click_config, elif action == 'collect-reward': """Withdraw staking reward to the specified wallet address""" - # TODO: Missing account selection + # Authenticate + client_account, staking_address = handle_client_account_for_staking(emitter=emitter, + stakeholder=STAKEHOLDER, + staking_address=staking_address, + is_preallocation_staker=is_preallocation_staker, + beneficiary_address=beneficiary_address, + force=force) password = None if not hw_wallet and not blockchain.client.is_local: - password = get_client_password(checksum_address=staking_address) + password = get_client_password(checksum_address=client_account) if not staking_reward and not policy_reward: raise click.BadArgumentUsage(f"Either --staking-reward or --policy-reward must be True to collect rewards.") - STAKEHOLDER.assimilate(checksum_address=staking_address, password=password) + STAKEHOLDER.assimilate(checksum_address=client_account, password=password) if staking_reward: # Note: Sending staking / inflation rewards to another account is not allowed. staking_receipt = STAKEHOLDER.collect_staking_reward() diff --git a/tests/cli/ursula/test_stake_via_allocation_contract.py b/tests/cli/ursula/test_stake_via_allocation_contract.py index cb08bb57f..dce7cbf8f 100644 --- a/tests/cli/ursula/test_stake_via_allocation_contract.py +++ b/tests/cli/ursula/test_stake_via_allocation_contract.py @@ -15,19 +15,33 @@ You should have received a copy of the GNU Affero General Public License along with nucypher. If not, see . """ +import datetime import json +import os +import random +import maya import pytest +from twisted.logger import Logger from web3 import Web3 from nucypher.blockchain.eth.actors import Staker from nucypher.blockchain.eth.agents import StakingEscrowAgent, ContractAgency, PreallocationEscrowAgent, NucypherTokenAgent -from nucypher.blockchain.eth.token import NU, Stake +from nucypher.blockchain.eth.token import NU, Stake, StakeList +from nucypher.characters.lawful import Enrico, Ursula from nucypher.cli.main import nucypher_cli +from nucypher.config.characters import UrsulaConfiguration +from nucypher.crypto.powers import TransactingPower from nucypher.utilities.sandbox.constants import ( TEST_PROVIDER_URI, INSECURE_DEVELOPMENT_PASSWORD, + MOCK_IP_ADDRESS, + MOCK_URSULA_STARTING_PORT, + TEMPORARY_DOMAIN, + MOCK_KNOWN_URSULAS_CACHE, + select_test_port, ) +from nucypher.utilities.sandbox.middleware import MockRestMiddleware # # This test module is intended to mirror tests/cli/ursula/test_stakeholder_and_ursula.py, @@ -169,6 +183,68 @@ def test_stake_set_worker(click_runner, assert staker.worker_address == manual_worker +def test_stake_detach_worker(click_runner, + testerchain, + token_economics, + beneficiary, + preallocation_escrow_agent, + mock_allocation_registry, + manual_worker, + test_registry, + stakeholder_configuration_file_location): + + staker_address = preallocation_escrow_agent.principal_contract.address + + staking_agent = ContractAgency.get_agent(StakingEscrowAgent, registry=test_registry) + assert manual_worker == staking_agent.get_worker_from_staker(staker_address=staker_address) + + testerchain.time_travel(periods=token_economics.minimum_worker_periods) + + init_args = ('stake', 'detach-worker', + '--config-file', stakeholder_configuration_file_location, + '--escrow', + '--beneficiary-address', beneficiary, + '--allocation-filepath', mock_allocation_registry.filepath, + '--force') + + result = click_runner.invoke(nucypher_cli, + init_args, + input=INSECURE_DEVELOPMENT_PASSWORD, + catch_exceptions=False) + assert result.exit_code == 0 + + staker = Staker(is_me=True, + checksum_address=beneficiary, + allocation_registry=mock_allocation_registry, + registry=test_registry) + + assert not staker.worker_address + + # Ok ok, let's set the worker again. + + init_args = ('stake', 'set-worker', + '--config-file', stakeholder_configuration_file_location, + '--escrow', + '--beneficiary-address', beneficiary, + '--allocation-filepath', mock_allocation_registry.filepath, + '--worker-address', manual_worker, + '--force') + + user_input = f'{INSECURE_DEVELOPMENT_PASSWORD}' + result = click_runner.invoke(nucypher_cli, + init_args, + input=user_input, + catch_exceptions=False) + assert result.exit_code == 0 + + staker = Staker(is_me=True, + checksum_address=beneficiary, + allocation_registry=mock_allocation_registry, + registry=test_registry) + + assert staker.worker_address == manual_worker + + def test_stake_restake(click_runner, beneficiary, preallocation_escrow_agent, @@ -253,3 +329,248 @@ def test_stake_restake(click_runner, allocation_registry=mock_allocation_registry) assert not staker.is_restaking assert "Successfully disabled" in result.output + + +def test_ursula_init(click_runner, + custom_filepath, + mock_registry_filepath, + preallocation_escrow_agent, + manual_worker, + testerchain): + + init_args = ('ursula', 'init', + '--poa', + '--network', TEMPORARY_DOMAIN, + '--staker-address', preallocation_escrow_agent.principal_contract.address, + '--worker-address', manual_worker, + '--config-root', custom_filepath, + '--provider', TEST_PROVIDER_URI, + '--registry-filepath', mock_registry_filepath, + '--rest-host', MOCK_IP_ADDRESS, + '--rest-port', MOCK_URSULA_STARTING_PORT) + + user_input = '{password}\n{password}'.format(password=INSECURE_DEVELOPMENT_PASSWORD) + result = click_runner.invoke(nucypher_cli, + init_args, + input=user_input, + catch_exceptions=False) + assert result.exit_code == 0 + + # Files and Directories + assert os.path.isdir(custom_filepath), 'Configuration file does not exist' + assert os.path.isdir(os.path.join(custom_filepath, 'keyring')), 'Keyring does not exist' + assert os.path.isdir(os.path.join(custom_filepath, 'known_nodes')), 'known_nodes directory does not exist' + + custom_config_filepath = os.path.join(custom_filepath, UrsulaConfiguration.generate_filename()) + assert os.path.isfile(custom_config_filepath), 'Configuration file does not exist' + + with open(custom_config_filepath, 'r') as config_file: + raw_config_data = config_file.read() + config_data = json.loads(raw_config_data) + assert config_data['provider_uri'] == TEST_PROVIDER_URI + assert config_data['worker_address'] == manual_worker + assert config_data['checksum_address'] == preallocation_escrow_agent.principal_contract.address + assert TEMPORARY_DOMAIN in config_data['domains'] + + +def test_ursula_run(click_runner, + manual_worker, + custom_filepath, + testerchain): + + custom_config_filepath = os.path.join(custom_filepath, UrsulaConfiguration.generate_filename()) + + # Now start running your Ursula! + init_args = ('ursula', 'run', + '--dry-run', + '--config-file', custom_config_filepath) + + user_input = f'{INSECURE_DEVELOPMENT_PASSWORD}\n' * 2 + result = click_runner.invoke(nucypher_cli, + init_args, + input=user_input, + catch_exceptions=False) + assert result.exit_code == 0 + + +def test_collect_rewards_integration(click_runner, + testerchain, + test_registry, + stakeholder_configuration_file_location, + blockchain_alice, + blockchain_bob, + random_policy_label, + beneficiary, + preallocation_escrow_agent, + mock_allocation_registry, + manual_worker, + token_economics, + stake_value, + policy_value, + policy_rate): + + half_stake_time = token_economics.minimum_locked_periods // 2 # Test setup + logger = Logger("Test-CLI") # Enter the Teacher's Logger, and + current_period = 0 # State the initial period for incrementing + + staker_address = preallocation_escrow_agent.principal_contract.address + worker_address = manual_worker + + # The staker is staking. + stakes = StakeList(registry=test_registry, checksum_address=staker_address) + stakes.refresh() + assert stakes + + staking_agent = ContractAgency.get_agent(StakingEscrowAgent, registry=test_registry) + assert worker_address == staking_agent.get_worker_from_staker(staker_address=staker_address) + + ursula_port = select_test_port() + ursula = Ursula(is_me=True, + checksum_address=staker_address, + worker_address=worker_address, + registry=test_registry, + rest_host='127.0.0.1', + rest_port=ursula_port, + start_working_now=False, + network_middleware=MockRestMiddleware()) + + MOCK_KNOWN_URSULAS_CACHE[ursula_port] = ursula + assert ursula.worker_address == worker_address + assert ursula.checksum_address == staker_address + + # Mock TransactingPower consumption (Worker-Ursula) + testerchain.transacting_power = TransactingPower(account=worker_address, password=INSECURE_DEVELOPMENT_PASSWORD) + testerchain.transacting_power.activate() + + # Confirm for half the first stake duration + for _ in range(half_stake_time): + logger.debug(f">>>>>>>>>>> TEST PERIOD {current_period} <<<<<<<<<<<<<<<<") + ursula.confirm_activity() + testerchain.time_travel(periods=1) + current_period += 1 + + # Alice creates a policy and grants Bob access + blockchain_alice.selection_buffer = 1 + + M, N = 1, 1 + expiration = maya.now() + datetime.timedelta(days=3) + blockchain_policy = blockchain_alice.grant(bob=blockchain_bob, + label=random_policy_label, + m=M, n=N, + value=policy_value, + expiration=expiration, + handpicked_ursulas={ursula}) + + # Ensure that the handpicked Ursula was selected for the policy + arrangement = list(blockchain_policy._accepted_arrangements)[0] + assert arrangement.ursula == ursula + + # Bob learns about the new staker and joins the policy + blockchain_bob.start_learning_loop() + blockchain_bob.remember_node(node=ursula) + blockchain_bob.join_policy(random_policy_label, bytes(blockchain_alice.stamp)) + + # Enrico Encrypts (of course) + enrico = Enrico(policy_encrypting_key=blockchain_policy.public_key, + network_middleware=MockRestMiddleware()) + + verifying_key = blockchain_alice.stamp.as_umbral_pubkey() + + for index in range(half_stake_time - 5): + logger.debug(f">>>>>>>>>>> TEST PERIOD {current_period} <<<<<<<<<<<<<<<<") + ursula.confirm_activity() + + # Encrypt + random_data = os.urandom(random.randrange(20, 100)) + ciphertext, signature = enrico.encrypt_message(message=random_data) + + # Decrypt + cleartexts = blockchain_bob.retrieve(message_kit=ciphertext, + data_source=enrico, + alice_verifying_key=verifying_key, + label=random_policy_label) + assert random_data == cleartexts[0] + + # Ursula Staying online and the clock advancing + testerchain.time_travel(periods=1) + current_period += 1 + + # Finish the passage of time + for _ in range(5 - 1): # minus 1 because the first period was already confirmed in test_ursula_run + logger.debug(f">>>>>>>>>>> TEST PERIOD {current_period} <<<<<<<<<<<<<<<<") + ursula.confirm_activity() + current_period += 1 + testerchain.time_travel(periods=1) + + # + # WHERES THE MONEY URSULA?? - Collecting Rewards + # + + # The address the client wants Ursula to send policy rewards to + burner_wallet = testerchain.w3.eth.account.create(INSECURE_DEVELOPMENT_PASSWORD) + + # The policy rewards wallet is initially empty, because it is freshly created + assert testerchain.client.get_balance(burner_wallet.address) == 0 + + # Rewards will be unlocked after the + # final confirmed period has passed (+1). + logger.debug(f">>>>>>>>>>> TEST PERIOD {current_period} <<<<<<<<<<<<<<<<") + testerchain.time_travel(periods=1) + current_period += 1 + logger.debug(f">>>>>>>>>>> TEST PERIOD {current_period} <<<<<<<<<<<<<<<<") + + # Since we are mocking the blockchain connection, manually consume the transacting power of the Beneficiary. + testerchain.transacting_power = TransactingPower(account=beneficiary, + password=INSECURE_DEVELOPMENT_PASSWORD) + testerchain.transacting_power.activate() + + # Collect Policy Reward + collection_args = ('stake', 'collect-reward', + '--mock-networking', + '--config-file', stakeholder_configuration_file_location, + '--policy-reward', + '--no-staking-reward', + '--withdraw-address', burner_wallet.address, + '--escrow', + '--beneficiary-address', beneficiary, + '--allocation-filepath', mock_allocation_registry.filepath, + '--force') + + result = click_runner.invoke(nucypher_cli, + collection_args, + input=INSECURE_DEVELOPMENT_PASSWORD, + catch_exceptions=False) + assert result.exit_code == 0 + + # Policy Reward + collected_policy_reward = testerchain.client.get_balance(burner_wallet.address) + expected_collection = policy_rate * 30 + assert collected_policy_reward == expected_collection + + # + # Collect Staking Reward + # + token_agent = ContractAgency.get_agent(agent_class=NucypherTokenAgent, registry=test_registry) + balance_before_collecting = token_agent.get_balance(address=staker_address) + + collection_args = ('stake', 'collect-reward', + '--mock-networking', + '--config-file', stakeholder_configuration_file_location, + '--no-policy-reward', + '--staking-reward', + '--escrow', + '--beneficiary-address', beneficiary, + '--allocation-filepath', mock_allocation_registry.filepath, + '--force') + + result = click_runner.invoke(nucypher_cli, + collection_args, + input=INSECURE_DEVELOPMENT_PASSWORD, + catch_exceptions=False) + assert result.exit_code == 0 + + # The beneficiary has withdrawn her staking rewards, which are now in the staking contract + assert token_agent.get_balance(address=staker_address) >= balance_before_collecting + + + diff --git a/tests/cli/ursula/test_stakeholder_and_ursula.py b/tests/cli/ursula/test_stakeholder_and_ursula.py index 5298aa68d..66caf72b8 100644 --- a/tests/cli/ursula/test_stakeholder_and_ursula.py +++ b/tests/cli/ursula/test_stakeholder_and_ursula.py @@ -400,9 +400,8 @@ def test_collect_rewards_integration(click_runner, verifying_key = blockchain_alice.stamp.as_umbral_pubkey() for index in range(half_stake_time - 5): - ursula.confirm_activity() - logger.debug(f">>>>>>>>>>> TEST PERIOD {current_period} <<<<<<<<<<<<<<<<") + ursula.confirm_activity() # Encrypt random_data = os.urandom(random.randrange(20, 100)) @@ -421,10 +420,10 @@ def test_collect_rewards_integration(click_runner, # Finish the passage of time for the first Stake for _ in range(5): # plus the extended periods from stake division - ursula.confirm_activity() - current_period += 1 logger.debug(f">>>>>>>>>>> TEST PERIOD {current_period} <<<<<<<<<<<<<<<<") + ursula.confirm_activity() testerchain.time_travel(periods=1) + current_period += 1 # # WHERES THE MONEY URSULA?? - Collecting Rewards @@ -438,6 +437,7 @@ def test_collect_rewards_integration(click_runner, # Rewards will be unlocked after the # final confirmed period has passed (+1). + logger.debug(f">>>>>>>>>>> TEST PERIOD {current_period} <<<<<<<<<<<<<<<<") testerchain.time_travel(periods=1) current_period += 1 logger.debug(f">>>>>>>>>>> TEST PERIOD {current_period} <<<<<<<<<<<<<<<<") @@ -479,14 +479,18 @@ def test_collect_rewards_integration(click_runner, logger.debug(f">>>>>>>>>>> TEST PERIOD {current_period} <<<<<<<<<<<<<<<<") testerchain.time_travel(periods=1) - # Collect Inflation Reward + # + # Collect Staking Reward + # + + balance_before_collecting = staker.token_agent.get_balance(address=staker_address) + collection_args = ('stake', 'collect-reward', '--mock-networking', '--config-file', stakeholder_configuration_file_location, '--no-policy-reward', '--staking-reward', '--staking-address', staker_address, - '--withdraw-address', burner_wallet.address, '--force') result = click_runner.invoke(nucypher_cli, @@ -495,8 +499,8 @@ def test_collect_rewards_integration(click_runner, catch_exceptions=False) assert result.exit_code == 0 - # The burner wallet has the reward ethers - assert staker.token_agent.get_balance(address=staker_address) + # The staker has withdrawn her staking rewards + assert staker.token_agent.get_balance(address=staker_address) >= balance_before_collecting def test_stake_detach_worker(click_runner,