mirror of https://github.com/nucypher/nucypher.git
commit
ede371eede
|
@ -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
|
||||
-----
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
|
@ -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'
|
||||
|
|
|
@ -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,8 +76,12 @@ ENTRY_POINTS = (
|
|||
ursula.ursula, # Untrusted Re-Encryption Proxy
|
||||
stake.stake, # Stake Management
|
||||
|
||||
# PRE Application
|
||||
bond.bond,
|
||||
bond.unbond,
|
||||
|
||||
# Utility Commands
|
||||
status.status, # Network Status
|
||||
status.status, # Network Status
|
||||
cloudworkers.cloudworkers, # Remote Operator node management
|
||||
contacts.contacts, # Character "card" management
|
||||
porter.porter
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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))
|
||||
|
|
Loading…
Reference in New Issue