Merge pull request #2862 from KPrasch/bonding

PREApplication Bonding CLI
pull/2871/head
KPrasch 2022-02-16 13:54:56 -08:00 committed by GitHub
commit ede371eede
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 607 additions and 16 deletions

View File

@ -25,7 +25,8 @@ General
* `NUCYPHER_STAKING_PROVIDERS_PAGINATION_SIZE_LIGHT_NODE`
Default pagination size for the maximum number of active staking providers to retrieve from PREApplication in
one contract call when a light node provider is being used.
* `NUCYPHER_STAKING_PROVIDER_ETH_PASSWORD`
Password for a staking provider's Keystore.
Alice
-----

View File

@ -19,7 +19,7 @@ import random
import sys
from bisect import bisect_right
from itertools import accumulate
from typing import Dict, Iterable, List, Tuple, Type, Union, Any, Optional, cast, Iterator, NamedTuple
from typing import Dict, Iterable, List, Tuple, Type, Union, Any, Optional, cast, NamedTuple
from constant_sorrow.constants import ( # type: ignore
CONTRACT_CALL,
@ -64,8 +64,7 @@ from nucypher.types import (
StakingProviderInfo,
PeriodDelta,
StakingEscrowParameters,
PolicyInfo,
ArrangementInfo, TuNits
TuNits
)
from nucypher.utilities.logging import Logger # type: ignore
@ -1124,9 +1123,9 @@ class PREApplicationAgent(EthereumContractAgent):
return receipt
@contract_api(TRANSACTION)
def bond_operator(self, provider: ChecksumAddress, operator: ChecksumAddress, transacting_power: TransactingPower) -> TxReceipt:
def bond_operator(self, staking_provider: ChecksumAddress, operator: ChecksumAddress, transacting_power: TransactingPower) -> TxReceipt:
"""For use by threshold operator accounts only."""
contract_function: ContractFunction = self.contract.functions.bondOperator(provider, operator)
contract_function: ContractFunction = self.contract.functions.bondOperator(staking_provider, operator)
receipt = self.blockchain.send_transaction(contract_function=contract_function,
transacting_power=transacting_power)
return receipt

View File

@ -0,0 +1,197 @@
"""
This file is part of nucypher.
nucypher is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
nucypher is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
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 <https://www.gnu.org/licenses/>.
"""
from typing import Tuple, Union
import click
import maya
from eth_typing import ChecksumAddress
from nucypher.blockchain.eth.agents import ContractAgency, PREApplicationAgent
from nucypher.blockchain.eth.constants import NULL_ADDRESS
from nucypher.blockchain.eth.signers import Signer
from nucypher.cli.actions.auth import get_client_password
from nucypher.cli.actions.select import select_network
from nucypher.cli.literature import (
STAKING_PROVIDER_UNAUTHORIZED,
BONDING_TIME,
ALREADY_BONDED,
UNEXPECTED_HUMAN_OPERATOR,
BONDING,
CONFIRM_BONDING,
NOT_BONDED,
CONFIRM_UNBONDING,
UNBONDING
)
from nucypher.cli.options import (
option_registry_filepath,
option_signer_uri,
option_provider_uri,
option_network,
option_staking_provider,
option_operator_address,
option_force
)
from nucypher.cli.painting.transactions import paint_receipt_summary
from nucypher.cli.utils import connect_to_blockchain, get_registry
from nucypher.config.constants import NUCYPHER_ENVVAR_STAKING_PROVIDER_ETH_PASSWORD
from nucypher.control.emitters import StdoutEmitter
from nucypher.crypto.powers import TransactingPower
def is_authorized(emitter, staking_provider: ChecksumAddress, agent: PREApplicationAgent) -> None:
_authorized = agent.is_authorized(staking_provider=staking_provider)
if not _authorized:
emitter.message(STAKING_PROVIDER_UNAUTHORIZED.format(provider=staking_provider), color='red')
raise click.Abort()
def is_bonded(agent, staking_provider: ChecksumAddress, return_address: bool = False) -> Union[bool, Tuple[bool, ChecksumAddress]]:
onchain_operator = agent.get_operator_from_staking_provider(staking_provider=staking_provider)
result = onchain_operator != NULL_ADDRESS
if not return_address:
return result
return result, onchain_operator
def check_bonding_requirements(emitter, agent: PREApplicationAgent, staking_provider: ChecksumAddress) -> None:
blockchain = agent.blockchain
now = blockchain.get_blocktime()
commencement = agent.get_staking_provider_info(staking_provider=staking_provider).operator_start_timestamp
min_seconds = agent.get_min_operator_seconds()
termination = (commencement + min_seconds)
if now < termination:
emitter.error(BONDING_TIME.format(date=maya.MayaDT(termination)))
raise click.Abort()
@click.command('bond')
@option_registry_filepath
@option_provider_uri(required=True)
@option_signer_uri
@option_operator_address
@option_staking_provider
@option_network(required=True)
@option_force
def bond(registry_filepath, provider_uri, signer_uri, operator_address, staking_provider, network, force):
"""
Bond an operator to a staking provider.
The staking provider must be authorized to use the PREApplication.
"""
#
# Setup
#
emitter = StdoutEmitter()
connect_to_blockchain(provider_uri=provider_uri, emitter=emitter)
if not signer_uri:
emitter.message('--signer is required', color='red')
raise click.Abort()
if not network:
network = select_network(emitter=emitter)
signer = Signer.from_signer_uri(signer_uri)
transacting_power = TransactingPower(account=staking_provider, signer=signer)
registry = get_registry(network=network, registry_filepath=registry_filepath)
agent = ContractAgency.get_agent(PREApplicationAgent, registry=registry)
#
# Checks
#
# Check for authorization
is_authorized(emitter=emitter, agent=agent, staking_provider=staking_provider)
# Check bonding
if is_bonded(agent=agent, staking_provider=staking_provider, return_address=False):
# operator is already set - check timing
check_bonding_requirements(emitter=emitter, agent=agent, staking_provider=staking_provider)
# Check for pre-existing staking providers for this operator
onchain_staking_provider = agent.get_staking_provider_from_operator(operator_address=operator_address)
if onchain_staking_provider != NULL_ADDRESS:
emitter.message(ALREADY_BONDED.format(provider=onchain_staking_provider, operator=operator_address), color='red')
raise click.Abort() # dont steal bananas
# Check that operator is not human
if staking_provider != operator_address:
# if the operator has a beneficiary it is the staking provider.
beneficiary = agent.get_beneficiary(staking_provider=operator_address)
if beneficiary != NULL_ADDRESS:
emitter.message(UNEXPECTED_HUMAN_OPERATOR, color='red')
raise click.Abort()
#
# Bond
#
if not force:
click.confirm(CONFIRM_BONDING.format(provider=staking_provider, operator=operator_address), abort=True)
transacting_power.unlock(password=get_client_password(checksum_address=staking_provider, envvar=NUCYPHER_ENVVAR_STAKING_PROVIDER_ETH_PASSWORD))
emitter.echo(BONDING.format(operator=operator_address))
receipt = agent.bond_operator(operator=operator_address, transacting_power=transacting_power, staking_provider=staking_provider)
paint_receipt_summary(receipt=receipt, emitter=emitter)
@click.command('unbond')
@option_registry_filepath
@option_provider_uri(required=True)
@option_signer_uri
@option_staking_provider
@option_network()
@option_force
def unbond(registry_filepath, provider_uri, signer_uri, staking_provider, network, force):
"""Unbonds an operator from an authorized staking provider."""
#
# Setup
#
emitter = StdoutEmitter()
if not signer_uri:
emitter.message('--signer is required', color='red')
raise click.Abort()
if not network:
network = select_network(emitter=emitter)
connect_to_blockchain(provider_uri=provider_uri, emitter=emitter)
registry = get_registry(network=network, registry_filepath=registry_filepath)
agent = ContractAgency.get_agent(PREApplicationAgent, registry=registry)
signer = Signer.from_signer_uri(signer_uri)
transacting_power = TransactingPower(account=staking_provider, signer=signer)
#
# Check
#
bonded, onchain_operator_address = is_bonded(agent=agent, staking_provider=staking_provider, return_address=True)
if not bonded:
emitter.message(NOT_BONDED.format(provider=staking_provider), color='red')
raise click.Abort()
check_bonding_requirements(emitter=emitter, agent=agent, staking_provider=staking_provider)
#
# Unbond
#
if not force:
click.confirm(CONFIRM_UNBONDING.format(provider=staking_provider, operator=onchain_operator_address), abort=True)
transacting_power.unlock(password=get_client_password(checksum_address=staking_provider, envvar=NUCYPHER_ENVVAR_STAKING_PROVIDER_ETH_PASSWORD))
emitter.echo(UNBONDING.format(operator=onchain_operator_address))
receipt = agent.bond_operator(operator=NULL_ADDRESS, transacting_power=transacting_power, staking_provider=staking_provider)
paint_receipt_summary(receipt=receipt, emitter=emitter)

View File

@ -594,3 +594,26 @@ PORTER_CORS_ALLOWED_ORIGINS = "CORS Allow Origins: {allow_origins}"
PORTER_BOTH_TLS_KEY_AND_CERTIFICATION_MUST_BE_PROVIDED = "Both --tls-key-filepath and --tls-certificate-filepath must be provided to launch porter with TLS; only one specified"
PORTER_BASIC_AUTH_REQUIRES_HTTPS = "Basic authentication can only be used with HTTPS. --tls-key-filepath and --tls-certificate-filepath must also be provided"
#
# PREApplication
#
STAKING_PROVIDER_UNAUTHORIZED = '{provider} is not authorized.'
CONFIRM_BONDING = 'Are you sure you want to bond staking provider {provider} to operator {operator}?'
BONDING_TIME = 'Bonding/Unbonding not permitted until {date}.'
ALREADY_BONDED = '{operator} is already bonded to {provider}'
BONDING = 'Bonding operator {operator}'
UNEXPECTED_HUMAN_OPERATOR = 'Operation not permitted'
UNBONDING = 'Unbonding operator {operator}'
CONFIRM_UNBONDING = 'Are you sure you want to unbond {operator} from {provider}?'
NOT_BONDED = '{provider} is not bonded to any operator'

View File

@ -26,7 +26,8 @@ from nucypher.cli.commands import (
ursula,
cloudworkers,
contacts,
porter
porter,
bond,
)
from nucypher.cli.painting.help import echo_version, echo_config_root_path, echo_logging_root_path
@ -75,6 +76,10 @@ ENTRY_POINTS = (
ursula.ursula, # Untrusted Re-Encryption Proxy
stake.stake, # Stake Management
# PRE Application
bond.bond,
bond.unbond,
# Utility Commands
status.status, # Network Status
cloudworkers.cloudworkers, # Remote Operator node management

View File

@ -55,6 +55,7 @@ option_hw_wallet = click.option('--hw-wallet/--no-hw-wallet')
option_light = click.option('--light', help="Indicate that node is light", is_flag=True, default=None)
option_lonely = click.option('--lonely', help="Do not connect to seednodes", is_flag=True)
option_min_stake = click.option('--min-stake', help="The minimum stake the teacher must have to be locally accepted.", type=STAKED_TOKENS_RANGE, default=MIN_AUTHORIZATION)
option_operator_address = click.option('--operator-address', help="Address to bond as an operator", type=EIP55_CHECKSUM_ADDRESS, required=True)
option_parameters = click.option('--parameters', help="Filepath to a JSON file containing additional parameters", type=EXISTING_READABLE_FILE)
option_participant_address = click.option('--participant-address', help="Participant's checksum address.", type=EIP55_CHECKSUM_ADDRESS)
option_payment_provider = click.option('--payment-provider', help="Connection URL for payment method", type=click.STRING, required=False)
@ -65,6 +66,7 @@ option_registry_filepath = click.option('--registry-filepath', help="Custom cont
option_shares = click.option('--shares', '-n', help="N-Total shares", type=click.INT)
option_signer_uri = click.option('--signer', 'signer_uri', '-S', default=None, type=str)
option_staking_address = click.option('--staking-address', help="Address of a NuCypher staker", type=EIP55_CHECKSUM_ADDRESS)
option_staking_provider = click.option('--staking-provider', help="Staking provider ethereum address", type=EIP55_CHECKSUM_ADDRESS, required=True)
option_teacher_uri = click.option('--teacher', 'teacher_uri', help="An Ursula URI to start learning from (seednode)", type=click.STRING)
option_threshold = click.option('--threshold', '-m', help="M-Threshold KFrags", type=click.INT)
option_treasure_map = click.option('--treasure-map', 'treasure_map', help="Encrypted treasure map as base64 for retrieval", type=click.STRING, required=True)

View File

@ -30,6 +30,7 @@ import nucypher
NUCYPHER_ENVVAR_KEYSTORE_PASSWORD = "NUCYPHER_KEYSTORE_PASSWORD"
NUCYPHER_ENVVAR_OPERATOR_ADDRESS = "NUCYPHER_OPERATOR_ADDRESS"
NUCYPHER_ENVVAR_OPERATOR_ETH_PASSWORD = "NUCYPHER_WORKER_ETH_PASSWORD"
NUCYPHER_ENVVAR_STAKING_PROVIDER_ETH_PASSWORD = "NUCYPHER_STAKING_PROVIDER_ETH_PASSWORD"
NUCYPHER_ENVVAR_ALICE_ETH_PASSWORD = "NUCYPHER_ALICE_ETH_PASSWORD"
NUCYPHER_ENVVAR_BOB_ETH_PASSWORD = "NUCYPHER_BOB_ETH_PASSWORD"
NUCYPHER_ENVVAR_PROVIDER_URI = "NUCYPHER_PROVIDER_URI"

View File

@ -212,7 +212,7 @@ def test_ursula_operator_confirmation(ursula_decentralized_test_config,
# now lets visit stake.nucypher.network and bond this operator
tpower = TransactingPower(account=staking_provider, signer=Web3Signer(testerchain.client))
application_agent.bond_operator(provider=staking_provider,
application_agent.bond_operator(staking_provider=staking_provider,
operator=operator_address,
transacting_power=tpower)
@ -251,7 +251,7 @@ def test_ursula_operator_confirmation_autopilot(mocker,
# now lets bond this worker
tpower = TransactingPower(account=staking_provider2, signer=Web3Signer(testerchain.client))
application_agent.bond_operator(provider=staking_provider2,
application_agent.bond_operator(staking_provider=staking_provider2,
operator=operator2,
transacting_power=tpower)

View File

@ -69,7 +69,7 @@ def test_staking_providers_and_operators_relationships(testerchain,
tpower = TransactingPower(account=staking_provider_account, signer=Web3Signer(testerchain.client))
_txhash = application_agent.bond_operator(transacting_power=tpower,
provider=staking_provider_account,
staking_provider=staking_provider_account,
operator=operator_account)
# We can check the staker-worker relation from both sides

View File

@ -52,7 +52,7 @@ def test_sampling_distribution(testerchain, test_registry, threshold_staking, ap
power = TransactingPower(account=provider_address, signer=Web3Signer(testerchain.client))
# We assume that the staking provider knows in advance the account of her operator
application_agent.bond_operator(provider=provider_address,
application_agent.bond_operator(staking_provider=provider_address,
operator=operator_address,
transacting_power=power)

View File

@ -0,0 +1,136 @@
"""
This file is part of nucypher.
nucypher is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
nucypher is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
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 <https://www.gnu.org/licenses/>.
"""
import pytest
from eth_typing import ChecksumAddress
from nucypher.cli.commands.bond import bond, unbond
from nucypher.config.constants import TEMPORARY_DOMAIN
from tests.constants import TEST_PROVIDER_URI, INSECURE_DEVELOPMENT_PASSWORD
@pytest.fixture(scope='module')
def operator_address(testerchain):
return testerchain.unassigned_accounts.pop(1)
@pytest.fixture(scope='module')
@pytest.mark.usefixtures('test_registry_source_manager', 'agency')
def staking_provider_address(testerchain):
return testerchain.unassigned_accounts.pop(1)
def test_nucypher_bond_help(click_runner, testerchain):
command = '--help'
result = click_runner.invoke(bond, command, catch_exceptions=False)
assert result.exit_code == 0
@pytest.fixture(scope='module')
def authorized_staking_provider(testerchain, threshold_staking, staking_provider_address, application_economics):
# initialize threshold stake
tx = threshold_staking.functions.setRoles(staking_provider_address).transact()
testerchain.wait_for_receipt(tx)
tx = threshold_staking.functions.setStakes(staking_provider_address, application_economics.min_authorization, 0, 0).transact()
testerchain.wait_for_receipt(tx)
return staking_provider_address
def exec_bond(click_runner, operator_address: ChecksumAddress, staking_provider_address: ChecksumAddress):
command = ('--operator-address', operator_address,
'--staking-provider', staking_provider_address,
'--provider', TEST_PROVIDER_URI,
'--network', TEMPORARY_DOMAIN,
'--signer', TEST_PROVIDER_URI,
'--force')
result = click_runner.invoke(bond,
command,
catch_exceptions=False,
env=dict(NUCYPHER_STAKING_PROVIDER_ETH_PASSWORD=INSECURE_DEVELOPMENT_PASSWORD))
return result
def exec_unbond(click_runner, staking_provider_address: ChecksumAddress):
command = ('--staking-provider', staking_provider_address,
'--provider', TEST_PROVIDER_URI,
'--network', TEMPORARY_DOMAIN,
'--signer', TEST_PROVIDER_URI,
'--force')
result = click_runner.invoke(unbond,
command,
catch_exceptions=False,
env=dict(NUCYPHER_STAKING_PROVIDER_ETH_PASSWORD=INSECURE_DEVELOPMENT_PASSWORD))
return result
@pytest.mark.usefixtures('test_registry_source_manager', 'agency')
def test_nucypher_bond_unauthorized(click_runner, testerchain, operator_address, staking_provider_address):
result = exec_bond(
click_runner=click_runner,
operator_address=operator_address,
staking_provider_address=staking_provider_address
)
assert result.exit_code == 1
error_message = f'{staking_provider_address} is not authorized'
assert error_message in result.output
@pytest.mark.usefixtures('test_registry_source_manager', 'agency', 'test_registry')
def test_nucypher_bond(click_runner, testerchain, operator_address, authorized_staking_provider):
result = exec_bond(
click_runner=click_runner,
operator_address=operator_address,
staking_provider_address=authorized_staking_provider
)
assert result.exit_code == 0
@pytest.mark.usefixtures('test_registry_source_manager', 'agency')
def test_nucypher_rebond_too_soon(click_runner, testerchain, operator_address, staking_provider_address):
result = exec_bond(
click_runner=click_runner,
operator_address=operator_address,
staking_provider_address=staking_provider_address
)
assert result.exit_code == 1
error_message = 'Bonding/Unbonding not permitted until '
assert error_message in result.output
@pytest.mark.usefixtures('test_registry_source_manager', 'agency')
def test_nucypher_rebond_operator(click_runner,
testerchain,
operator_address,
staking_provider_address,
application_economics):
testerchain.time_travel(seconds=application_economics.min_operator_seconds)
result = exec_bond(
click_runner=click_runner,
operator_address=testerchain.unassigned_accounts[-1],
staking_provider_address=staking_provider_address
)
assert result.exit_code == 0
@pytest.mark.usefixtures('test_registry_source_manager', 'agency')
def test_nucypher_unbond_operator(click_runner,
testerchain,
staking_provider_address,
application_economics):
testerchain.time_travel(seconds=application_economics.min_operator_seconds)
result = exec_unbond(click_runner=click_runner, staking_provider_address=staking_provider_address)
assert result.exit_code == 0

View File

@ -72,7 +72,7 @@ def mock_funded_account_password_keystore(tmp_path_factory, testerchain, thresho
provider_power.unlock(password=INSECURE_DEVELOPMENT_PASSWORD)
pre_application_agent = ContractAgency.get_agent(PREApplicationAgent, registry=test_registry)
pre_application_agent.bond_operator(provider=provider_address,
pre_application_agent.bond_operator(staking_provider=provider_address,
operator=account.address,
transacting_power=provider_power)

View File

@ -632,7 +632,7 @@ def staking_providers(testerchain, agency, test_registry, threshold_staking):
testerchain.wait_for_receipt(tx)
# We assume that the staking provider knows in advance the account of her operator
pre_application_agent.bond_operator(provider=provider_address,
pre_application_agent.bond_operator(staking_provider=provider_address,
operator=operator_address,
transacting_power=provider_power)

View File

@ -0,0 +1,216 @@
"""
This file is part of nucypher.
nucypher is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
nucypher is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
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 <https://www.gnu.org/licenses/>.
"""
import maya
import pytest
from eth_typing import ChecksumAddress
from nucypher.blockchain.eth.constants import NULL_ADDRESS
from nucypher.cli.commands.bond import unbond, bond
from nucypher.cli.literature import UNEXPECTED_HUMAN_OPERATOR, BONDING_TIME, ALREADY_BONDED
from nucypher.config.constants import (
TEMPORARY_DOMAIN,
NUCYPHER_ENVVAR_STAKING_PROVIDER_ETH_PASSWORD
)
from nucypher.crypto.powers import TransactingPower
from nucypher.types import StakingProviderInfo
from tests.constants import TEST_PROVIDER_URI, INSECURE_DEVELOPMENT_PASSWORD
cli_env = {NUCYPHER_ENVVAR_STAKING_PROVIDER_ETH_PASSWORD: INSECURE_DEVELOPMENT_PASSWORD}
@pytest.fixture(scope='module', autouse=True)
def mock_transacting_power(module_mocker):
module_mocker.patch.object(TransactingPower, 'unlock')
@pytest.fixture(scope='module')
def operator_address(mock_testerchain):
return mock_testerchain.unassigned_accounts[1]
@pytest.fixture(scope='module')
@pytest.mark.usefixtures('test_registry_source_manager', 'mock_contract_agency')
def staking_provider_address(mock_testerchain):
return mock_testerchain.unassigned_accounts[2]
def test_nucypher_bond_help(click_runner, mock_testerchain):
command = '--help'
result = click_runner.invoke(bond, command, catch_exceptions=False)
assert result.exit_code == 0
def exec_bond(click_runner, operator_address: ChecksumAddress, staking_provider_address: ChecksumAddress):
command = ('--operator-address', operator_address,
'--staking-provider', staking_provider_address,
'--provider', TEST_PROVIDER_URI,
'--network', TEMPORARY_DOMAIN,
'--signer', TEST_PROVIDER_URI,
'--force' # non-interactive only
)
result = click_runner.invoke(bond, command, catch_exceptions=False, env=cli_env)
return result
def exec_unbond(click_runner, staking_provider_address: ChecksumAddress):
command = ('--staking-provider', staking_provider_address,
'--provider', TEST_PROVIDER_URI,
'--network', TEMPORARY_DOMAIN,
'--signer', TEST_PROVIDER_URI,
'--force' # non-interactive only
)
result = click_runner.invoke(unbond, command, catch_exceptions=False, env=cli_env)
return result
@pytest.mark.usefixtures('test_registry_source_manager', 'mock_contract_agency', 'patch_keystore')
def test_nucypher_bond_unauthorized(click_runner, mock_testerchain, operator_address, staking_provider_address, mock_application_agent):
mock_application_agent.is_authorized.return_value = False
mock_application_agent.get_staking_provider_info.return_value = StakingProviderInfo(
operator=NULL_ADDRESS,
operator_confirmed=False,
operator_start_timestamp=1
)
result = exec_bond(
click_runner=click_runner,
operator_address=operator_address,
staking_provider_address=staking_provider_address
)
assert result.exit_code == 1
error_message = f'{staking_provider_address} is not authorized'
assert error_message in result.output
@pytest.mark.usefixtures('test_registry_source_manager', 'mock_contract_agency', 'test_registry')
def test_nucypher_unexpected_beneficiary(click_runner, mock_testerchain, operator_address, staking_provider_address, mock_application_agent):
mock_application_agent.get_staking_provider_info.return_value = StakingProviderInfo(
operator=NULL_ADDRESS,
operator_confirmed=False,
operator_start_timestamp=1
)
mock_application_agent.get_beneficiary.return_value = mock_testerchain.unassigned_accounts[-1]
mock_application_agent.get_staking_provider_from_operator.return_value = NULL_ADDRESS
result = exec_bond(
click_runner=click_runner,
operator_address=operator_address,
staking_provider_address=staking_provider_address
)
assert result.exit_code == 1
assert UNEXPECTED_HUMAN_OPERATOR in result.output
@pytest.mark.usefixtures('test_registry_source_manager', 'mock_contract_agency', 'test_registry')
def test_nucypher_bond(click_runner, mock_testerchain, operator_address, staking_provider_address, mock_application_agent):
mock_application_agent.get_staking_provider_info.return_value = StakingProviderInfo(
operator=NULL_ADDRESS,
operator_confirmed=False,
operator_start_timestamp=1
)
mock_application_agent.get_beneficiary.return_value = NULL_ADDRESS
mock_application_agent.get_staking_provider_from_operator.return_value = NULL_ADDRESS
result = exec_bond(
click_runner=click_runner,
operator_address=operator_address,
staking_provider_address=staking_provider_address
)
assert result.exit_code == 0
@pytest.mark.usefixtures('test_registry_source_manager', 'mock_contract_agency')
def test_nucypher_unbond_operator(click_runner, mock_testerchain, staking_provider_address, mock_application_agent, operator_address):
mock_application_agent.get_staking_provider_info.return_value = StakingProviderInfo(
operator=operator_address,
operator_confirmed=False,
operator_start_timestamp=1
)
mock_application_agent.get_staking_provider_from_operator.return_value = staking_provider_address
result = exec_unbond(click_runner=click_runner, staking_provider_address=staking_provider_address)
assert result.exit_code == 0
@pytest.mark.usefixtures('test_registry_source_manager', 'mock_contract_agency')
def test_nucypher_rebond_too_soon(click_runner, mock_testerchain, operator_address, staking_provider_address, mock_application_agent):
min_authorized_seconds = 5
now = mock_testerchain.get_blocktime()
operator_start_timestamp = now
termination = operator_start_timestamp + min_authorized_seconds
mock_application_agent.get_staking_provider_info.return_value = StakingProviderInfo(
operator=operator_address,
operator_confirmed=False,
operator_start_timestamp=operator_start_timestamp
)
mock_application_agent.get_min_operator_seconds.return_value = min_authorized_seconds
result = exec_bond(
click_runner=click_runner,
operator_address=operator_address,
staking_provider_address=staking_provider_address
)
assert result.exit_code == 1
error_message = BONDING_TIME.format(date=maya.MayaDT(termination))
assert error_message in result.output
@pytest.mark.usefixtures('test_registry_source_manager', 'mock_contract_agency')
def test_nucypher_bond_already_claimed_operator(click_runner, mock_testerchain, operator_address, staking_provider_address, mock_application_agent):
mock_application_agent.get_staking_provider_info.return_value = StakingProviderInfo(
operator=NULL_ADDRESS,
operator_confirmed=False,
operator_start_timestamp=1
)
mock_application_agent.get_beneficiary.return_value = NULL_ADDRESS
mock_application_agent.get_operator_from_staking_provider.return_value = NULL_ADDRESS
mock_application_agent.get_staking_provider_from_operator.return_value = mock_testerchain.unassigned_accounts[4]
result = exec_bond(
click_runner=click_runner,
operator_address=operator_address,
staking_provider_address=staking_provider_address
)
assert result.exit_code == 1
@pytest.mark.usefixtures('test_registry_source_manager', 'mock_contract_agency')
def test_nucypher_rebond_operator(click_runner, mock_testerchain, operator_address, staking_provider_address, mock_application_agent):
mock_application_agent.get_staking_provider_info.return_value = StakingProviderInfo(
operator=mock_testerchain.unassigned_accounts[-1],
operator_confirmed=False,
operator_start_timestamp=1
)
mock_application_agent.get_beneficiary.return_value = NULL_ADDRESS
mock_application_agent.get_staking_provider_from_operator.return_value = NULL_ADDRESS
result = exec_bond(
click_runner=click_runner,
operator_address=operator_address,
staking_provider_address=staking_provider_address
)
assert result.exit_code == 0

View File

@ -24,7 +24,7 @@ from nucypher.blockchain.eth.agents import (
AdjudicatorAgent,
ContractAgency,
NucypherTokenAgent,
StakingEscrowAgent
StakingEscrowAgent, PREApplicationAgent
)
from nucypher.blockchain.eth.interfaces import BlockchainInterface
from nucypher.blockchain.eth.registry import InMemoryContractRegistry
@ -74,6 +74,17 @@ def mock_staking_agent(mock_testerchain, application_economics, mock_contract_ag
mock_agent.reset()
@pytest.fixture(scope='function', autouse=True)
def mock_application_agent(mock_testerchain, application_economics, mock_contract_agency, mocker):
mock_agent = mock_contract_agency.get_agent(PREApplicationAgent)
# Handle the special case of commit_to_next_period, which returns a txhash due to the fire_and_forget option
mock_agent.confirm_operator_address = mocker.Mock(return_value=MockContractAgent.FAKE_TX_HASH)
yield mock_agent
mock_agent.reset()
@pytest.fixture(scope='function', autouse=True)
def mock_adjudicator_agent(mock_testerchain, application_economics, mock_contract_agency):
mock_agent = mock_contract_agency.get_agent(AdjudicatorAgent)
@ -235,7 +246,7 @@ def bob_blockchain_test_config(mock_testerchain, test_registry):
def ursula_decentralized_test_config(mock_testerchain, test_registry):
config = make_ursula_test_configuration(federated=False,
provider_uri=MOCK_PROVIDER_URI, # L1
payment_provider=MOCK_PROVIDER_URI, # L2
payment_provider=MOCK_PROVIDER_URI, # L1/L2
test_registry=test_registry,
rest_port=MOCK_URSULA_STARTING_PORT,
checksum_address=mock_testerchain.ursula_account(index=0))