diff --git a/newsfragments/2431.misc.rst b/newsfragments/2431.misc.rst
new file mode 100644
index 000000000..8d1d8c6bb
--- /dev/null
+++ b/newsfragments/2431.misc.rst
@@ -0,0 +1 @@
+Introduces a probationary period for policy creation in the network, until 2021-02-28 23:59:59 UTC.
diff --git a/newsfragments/2460.feature.rst b/newsfragments/2460.feature.rst
new file mode 100644
index 000000000..9c1233c38
--- /dev/null
+++ b/newsfragments/2460.feature.rst
@@ -0,0 +1 @@
+Complete interactive collection of policy parameters via alice grant CLI.
diff --git a/nucypher/characters/control/specifications/alice.py b/nucypher/characters/control/specifications/alice.py
index 221c04044..6c61548c6 100644
--- a/nucypher/characters/control/specifications/alice.py
+++ b/nucypher/characters/control/specifications/alice.py
@@ -15,14 +15,14 @@
along with nucypher. If not, see .
"""
+
import click
from marshmallow import validates_schema
+from nucypher.cli import options, types
from nucypher.characters.control.specifications import fields
from nucypher.characters.control.specifications.base import BaseSchema
-from nucypher.characters.control.specifications.exceptions import (
- InvalidArgumentCombo)
-from nucypher.cli import options, types
+from nucypher.characters.control.specifications.exceptions import InvalidArgumentCombo
class PolicyBaseSchema(BaseSchema):
@@ -52,7 +52,8 @@ class PolicyBaseSchema(BaseSchema):
click=click.option(
'--expiration',
help="Expiration Datetime of a policy",
- type=click.STRING))
+ type=click.DateTime())
+ )
# optional input
value = fields.Wei(
@@ -96,7 +97,7 @@ class GrantPolicy(PolicyBaseSchema):
label = fields.Label(
load_only=True, required=True,
- click=options.option_label(required=True))
+ click=options.option_label(required=False))
# output fields
treasure_map = fields.TreasureMap(dump_only=True)
diff --git a/nucypher/characters/lawful.py b/nucypher/characters/lawful.py
index a9b2dd6d1..b019b1c1c 100644
--- a/nucypher/characters/lawful.py
+++ b/nucypher/characters/lawful.py
@@ -14,56 +14,68 @@ 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 .
"""
+
+
import contextlib
import json
+from collections import OrderedDict
+
import maya
import random
import time
from base64 import b64decode, b64encode
-from collections import OrderedDict
-from datetime import datetime
-from functools import partial
-from json.decoder import JSONDecodeError
-from queue import Queue
-from random import shuffle
-from typing import Dict, Iterable, List, Optional, Set, Tuple, Union
-
+from bytestring_splitter import (
+ BytestringKwargifier,
+ BytestringSplitter,
+ BytestringSplittingError,
+ VariableLengthBytestring
+)
+from constant_sorrow import constants
+from constant_sorrow.constants import (
+ INCLUDED_IN_BYTESTRING,
+ PUBLIC_ONLY,
+ STRANGER_ALICE,
+ UNKNOWN_VERSION,
+ READY,
+ INVALIDATED
+)
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurve
from cryptography.hazmat.primitives.serialization import Encoding
from cryptography.x509 import Certificate, NameOID, load_pem_x509_certificate
+from datetime import datetime
from eth_utils import to_checksum_address
from flask import Response, request
+from functools import partial
+from json.decoder import JSONDecodeError
+from queue import Queue
+from random import shuffle
from twisted.internet import reactor, stdio, threads
from twisted.internet.task import LoopingCall
+from typing import Dict, Iterable, List, Optional, Tuple, Union
+from umbral import pre
+from umbral.keys import UmbralPublicKey
+from umbral.kfrags import KFrag
+from umbral.pre import UmbralCorrectnessError
+from umbral.signing import Signature
import nucypher
-from bytestring_splitter import BytestringKwargifier, BytestringSplitter, BytestringSplittingError, \
- VariableLengthBytestring
-from constant_sorrow import constants
-from constant_sorrow.constants import (INCLUDED_IN_BYTESTRING,
- PUBLIC_ONLY,
- STRANGER_ALICE,
- UNKNOWN_VERSION,
- READY,
- INVALIDATED)
from nucypher.acumen.nicknames import Nickname
from nucypher.acumen.perception import FleetSensor
from nucypher.blockchain.eth.actors import BlockchainPolicyAuthor, Worker
from nucypher.blockchain.eth.agents import ContractAgency, StakingEscrowAgent
-from nucypher.blockchain.eth.constants import LENGTH_ECDSA_SIGNATURE_WITH_RECOVERY, ETH_ADDRESS_BYTE_LENGTH
+from nucypher.blockchain.eth.constants import ETH_ADDRESS_BYTE_LENGTH
from nucypher.blockchain.eth.interfaces import BlockchainInterfaceFactory
from nucypher.blockchain.eth.registry import BaseContractRegistry
from nucypher.blockchain.eth.signers.software import Web3Signer
from nucypher.blockchain.eth.token import WorkTracker
from nucypher.characters.banners import ALICE_BANNER, BOB_BANNER, ENRICO_BANNER, URSULA_BANNER
from nucypher.characters.base import Character, Learner
-from nucypher.characters.control.controllers import (
- WebController
-)
+from nucypher.characters.control.controllers import WebController
from nucypher.characters.control.emitters import StdoutEmitter
from nucypher.characters.control.interfaces import AliceInterface, BobInterface, EnricoInterface
from nucypher.cli.processes import UrsulaCommandProtocol
+from nucypher.config.constants import END_OF_POLICIES_PROBATIONARY_PERIOD
from nucypher.config.storages import ForgetfulNodeStorage, NodeStorage
from nucypher.crypto.api import encrypt_and_sign, keccak_digest
from nucypher.crypto.constants import HRAC_LENGTH, PUBLIC_KEY_LENGTH
@@ -80,11 +92,6 @@ from nucypher.network.protocols import InterfaceInfo, parse_node_uri
from nucypher.network.server import ProxyRESTServer, TLSHostingPower, make_rest_app
from nucypher.network.trackers import AvailabilityTracker
from nucypher.utilities.logging import Logger
-from umbral import pre
-from umbral.keys import UmbralPublicKey
-from umbral.kfrags import KFrag
-from umbral.pre import UmbralCorrectnessError
-from umbral.signing import Signature
class Alice(Character, BlockchainPolicyAuthor):
@@ -288,7 +295,13 @@ class Alice(Character, BlockchainPolicyAuthor):
self.remember_node(node=handpicked_ursula)
policy = self.create_policy(bob=bob, label=label, **policy_params)
- self.log.debug(f"Successfully created {policy} ... ")
+
+ # TODO: Remove when the time is right.
+ if policy.expiration > END_OF_POLICIES_PROBATIONARY_PERIOD:
+ raise self.ActorError(f"The requested duration for this policy (until {policy.expiration}) exceeds the "
+ f"probationary period ({END_OF_POLICIES_PROBATIONARY_PERIOD}).")
+
+ self.log.debug(f"Generated new policy proposal {policy} ... ")
#
# We'll find n Ursulas by default. It's possible to "play the field" by trying different
diff --git a/nucypher/cli/actions/auth.py b/nucypher/cli/actions/auth.py
index 44d656ae1..894acc110 100644
--- a/nucypher/cli/actions/auth.py
+++ b/nucypher/cli/actions/auth.py
@@ -65,7 +65,7 @@ def get_nucypher_password(confirm: bool = False, envvar=NUCYPHER_ENVVAR_KEYRING_
def unlock_nucypher_keyring(emitter: StdoutEmitter, password: str, character_configuration: CharacterConfiguration) -> bool:
"""Unlocks a nucypher keyring and attaches it to the supplied configuration if successful."""
- emitter.message(DECRYPTING_CHARACTER_KEYRING.format(name=character_configuration.NAME), color='yellow')
+ emitter.message(DECRYPTING_CHARACTER_KEYRING.format(name=character_configuration.NAME.capitalize()), color='yellow')
# precondition
if character_configuration.dev_mode:
diff --git a/nucypher/cli/actions/confirm.py b/nucypher/cli/actions/confirm.py
index ed8d65273..9047988f2 100644
--- a/nucypher/cli/actions/confirm.py
+++ b/nucypher/cli/actions/confirm.py
@@ -14,7 +14,8 @@
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see .
"""
-from typing import Type, Union
+from tabulate import tabulate
+from typing import Type, Union, Dict
import click
from constant_sorrow.constants import UNKNOWN_DEVELOPMENT_CHAIN_ID
@@ -144,3 +145,11 @@ def verify_upgrade_details(blockchain: Union[BlockchainDeployerInterface, Blockc
click.confirm(CONFIRM_VERSIONED_UPGRADE.format(contract_name=deployer.contract_name,
old_version=old_contract.version,
new_version=new_version), abort=True)
+
+
+def confirm_staged_grant(emitter, grant_request: Dict) -> None:
+ # TODO: Expand and detail
+ emitter.echo("Successfully staged grant. Please review the details:\n", color='green')
+ table = ([field, value] for field, value in grant_request.items())
+ emitter.echo(tabulate(table, tablefmt="simple"))
+ click.confirm('\nGrant access and sign transaction?', abort=True)
diff --git a/nucypher/cli/commands/alice.py b/nucypher/cli/commands/alice.py
index b239c104c..7814a4061 100644
--- a/nucypher/cli/commands/alice.py
+++ b/nucypher/cli/commands/alice.py
@@ -15,9 +15,13 @@ You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see .
"""
+
import click
+import maya
import os
from constant_sorrow.constants import NO_BLOCKCHAIN_CONNECTION, NO_PASSWORD
+from datetime import timedelta
+from web3.main import Web3
from nucypher.blockchain.eth.signers.software import ClefSigner
from nucypher.characters.control.emitters import StdoutEmitter
@@ -25,8 +29,10 @@ from nucypher.characters.control.interfaces import AliceInterface
from nucypher.cli.actions.auth import get_client_password, get_nucypher_password
from nucypher.cli.actions.configure import (
destroy_configuration,
- handle_missing_configuration_file, get_or_update_configuration
+ handle_missing_configuration_file,
+ get_or_update_configuration
)
+from nucypher.cli.actions.confirm import confirm_staged_grant
from nucypher.cli.actions.select import select_client_account, select_config_file
from nucypher.cli.commands.deploy import option_gas_strategy
from nucypher.cli.config import group_general_config
@@ -55,12 +61,19 @@ from nucypher.cli.options import (
option_teacher_uri,
option_lonely
)
-from nucypher.cli.painting.help import paint_new_installation_help
+from nucypher.cli.painting.help import (
+ paint_new_installation_help,
+ paint_probationary_period_disclaimer,
+ enforce_probationary_period
+)
from nucypher.cli.processes import get_geth_provider_process
-from nucypher.cli.types import EIP55_CHECKSUM_ADDRESS
+from nucypher.cli.types import EIP55_CHECKSUM_ADDRESS, GWEI
from nucypher.cli.utils import make_cli_character, setup_emitter
from nucypher.config.characters import AliceConfiguration
-from nucypher.config.constants import NUCYPHER_ENVVAR_ALICE_ETH_PASSWORD, TEMPORARY_DOMAIN
+from nucypher.config.constants import (
+ NUCYPHER_ENVVAR_ALICE_ETH_PASSWORD,
+ TEMPORARY_DOMAIN,
+)
from nucypher.config.keyring import NucypherKeyring
from nucypher.network.middleware import RestMiddleware
@@ -428,6 +441,7 @@ def derive_policy_pubkey(general_config, label, character_options, config_file):
@option_config_file
@group_general_config
@group_character_options
+@option_force
def grant(general_config,
bob_encrypting_key,
bob_verifying_key,
@@ -437,7 +451,8 @@ def grant(general_config,
expiration,
m, n,
character_options,
- config_file):
+ config_file,
+ force):
"""Create and enact an access policy for some Bob. """
# Setup
@@ -447,13 +462,62 @@ def grant(general_config,
# Input validation
if ALICE.federated_only:
if any((value, rate)):
- raise click.BadOptionUsage(option_name="--value, --rate",
- message="Can't use --value or --rate with a federated Alice.")
+ message = "Can't use --value or --rate with a federated Alice."
+ raise click.BadOptionUsage(option_name="--value, --rate", message=message)
elif bool(value) and bool(rate):
raise click.BadOptionUsage(option_name="--rate", message="Can't use --value if using --rate")
- elif not (bool(value) or bool(rate)):
- rate = ALICE.default_rate # TODO #1709
- click.confirm(f"Confirm default rate {rate}?", abort=True)
+
+ # Interactive collection follows:
+ # TODO: Extricate to support modules
+ # - Disclaimer
+ # - Label
+ # - Expiration Date & Time
+ # - M of N
+ # - Policy Value (ETH)
+
+ # Policy Expiration
+ # TODO: Remove this line when the time is right.
+ paint_probationary_period_disclaimer(emitter)
+
+ # Label
+ if not label:
+ label = click.prompt(f'Enter label to grant Bob {bob_verifying_key[:8]}', type=click.STRING)
+
+ if not force and not expiration:
+ if ALICE.duration_periods:
+ # TODO: use a default in days or periods?
+ expiration = maya.now() + timedelta(days=ALICE.duration_periods) # default
+ if not click.confirm(f'Use default policy duration (expires {expiration})?'):
+ expiration = click.prompt('Enter policy expiration datetime', type=click.DateTime())
+ else:
+ # No policy duration default default available; Go interactive
+ expiration = click.prompt('Enter policy expiration datetime', type=click.DateTime())
+
+ # TODO: Remove this line when the time is right.
+ enforce_probationary_period(emitter=emitter, expiration=expiration)
+
+ # Policy Threshold and Shares
+ if not n:
+ n = ALICE.n
+ if not force and not click.confirm(f'Use default value for N ({n})?', default=True):
+ n = click.prompt('Enter total number of shares (N)', type=click.INT)
+ if not m:
+ m = ALICE.m
+ if not force and not click.confirm(f'Use default value for M ({m})?', default=True):
+ m = click.prompt('Enter threshold (M)', type=click.IntRange(1, n))
+
+ # Policy Value
+ policy_value_provided = bool(value) or bool(rate)
+ if not ALICE.federated_only and not policy_value_provided:
+ rate = ALICE.default_rate # TODO #1709 - Fine tuning and selection of default rates
+ if not force:
+ default_gwei = Web3.fromWei(rate, 'gwei')
+ prompt = "Confirm rate of {node_rate} gwei ({total_rate} gwei per period)?"
+ if not click.confirm(prompt.format(node_rate=default_gwei, total_rate=default_gwei*n), default=True):
+ interactive_rate = click.prompt('Enter rate per period in gwei', type=GWEI)
+ # TODO: Validate interactively collected rate (#1709)
+ click.confirm(prompt.format(node_rate=rate, total_rate=rate*n), default=True, abort=True)
+ rate = Web3.toWei(interactive_rate, 'gwei')
# Request
grant_request = {
@@ -468,7 +532,11 @@ def grant(general_config,
if value:
grant_request['value'] = value
elif rate:
- grant_request['rate'] = rate
+ grant_request['rate'] = rate # in wei
+
+ if not force and not general_config.json_ipc:
+ confirm_staged_grant(emitter=emitter, grant_request=grant_request)
+ emitter.echo(f'Granting Access to {bob_verifying_key[:8]}', color='yellow')
return ALICE.controller.grant(request=grant_request)
diff --git a/nucypher/cli/commands/stake.py b/nucypher/cli/commands/stake.py
index d2a3a7344..9fb6211cf 100644
--- a/nucypher/cli/commands/stake.py
+++ b/nucypher/cli/commands/stake.py
@@ -454,6 +454,10 @@ def unbond_worker(general_config: GroupGeneralConfig,
blockchain=blockchain,
client_account=client_account,
hw_wallet=transacting_staker_options.hw_wallet)
+
+ if not force:
+ click.confirm("Are you sure you want to unbond your worker?", abort=True)
+
STAKEHOLDER.assimilate(password=password)
receipt = STAKEHOLDER.unbond_worker()
diff --git a/nucypher/cli/literature.py b/nucypher/cli/literature.py
index 42bb5bd8a..e45c80f91 100644
--- a/nucypher/cli/literature.py
+++ b/nucypher/cli/literature.py
@@ -355,7 +355,7 @@ COLLECT_NUCYPHER_PASSWORD = "Enter NuCypher keyring password"
GENERIC_PASSWORD_PROMPT = "Enter password"
-DECRYPTING_CHARACTER_KEYRING = 'Decrypting {name} keyring...'
+DECRYPTING_CHARACTER_KEYRING = 'Authenticating {name}'
#
diff --git a/nucypher/cli/painting/help.py b/nucypher/cli/painting/help.py
index cf11f9c8a..dad3dd1dc 100644
--- a/nucypher/cli/painting/help.py
+++ b/nucypher/cli/painting/help.py
@@ -16,10 +16,11 @@ along with nucypher. If not, see .
"""
import click
+import maya
from nucypher.blockchain.eth.sol.__conf__ import SOLIDITY_COMPILER_VERSION
from nucypher.characters.banners import NUCYPHER_BANNER
-from nucypher.config.constants import DEFAULT_CONFIG_ROOT, USER_LOG_DIR
+from nucypher.config.constants import DEFAULT_CONFIG_ROOT, USER_LOG_DIR, END_OF_POLICIES_PROBATIONARY_PERIOD
def echo_version(ctx, param, value):
@@ -74,6 +75,38 @@ def paint_new_installation_help(emitter, new_configuration):
character_name_starts_with_vowel = character_name[0].lower() in vowels
adjective = 'an' if character_name_starts_with_vowel else 'a'
suggested_command = f'nucypher {character_name} run'
- how_to_run_message = f"\nTo run {adjective} {character_name.capitalize()} node from the default configuration filepath run: \n\n'{suggested_command}'\n"
+ how_to_run_message = f"\nTo run {adjective} {character_name.capitalize()} node from the default configuration " \
+ f"filepath run: \n\n'{suggested_command}'\n"
emitter.echo(how_to_run_message.format(suggested_command), color='green')
+
+
+def paint_probationary_period_disclaimer(emitter):
+ width = 60
+ import textwrap
+ disclaimer_title = " DISCLAIMER ".center(width, "=")
+ paragraph = f"""
+Some areas of the NuCypher network are still under active development;
+as a consequence, we have established a probationary period for policies in the network.
+Currently the creation of sharing policies with durations beyond {END_OF_POLICIES_PROBATIONARY_PERIOD} are prevented.
+After this date the probationary period will be over, and you will be able to create policies with any duration
+as supported by nodes on the network.
+"""
+
+ text = (
+ "\n",
+ disclaimer_title,
+ *[line.center(width) for line in textwrap.wrap(paragraph, width - 2)],
+ "=" * len(disclaimer_title),
+ "\n"
+ )
+ for sentence in text:
+ emitter.echo(sentence, color='yellow')
+
+
+def enforce_probationary_period(emitter, expiration):
+ """Used during CLI grant to prevent publication of a policy outside the probationary period."""
+ if maya.MayaDT.from_datetime(expiration) > END_OF_POLICIES_PROBATIONARY_PERIOD:
+ emitter.echo(f"The requested duration for this policy (until {expiration}) exceeds the probationary period"
+ f" ({END_OF_POLICIES_PROBATIONARY_PERIOD}).", color="red")
+ raise click.Abort()
diff --git a/nucypher/config/constants.py b/nucypher/config/constants.py
index e87410e10..d6a5762fa 100644
--- a/nucypher/config/constants.py
+++ b/nucypher/config/constants.py
@@ -22,6 +22,8 @@ from pathlib import Path
import os
from appdirs import AppDirs
+from maya import MayaDT
+
import nucypher
# Environment variables
@@ -69,3 +71,6 @@ TEMPORARY_DOMAIN = ":temporary-domain:" # for use with `--dev` node runtimes
# Event Blocks Throttling
NUCYPHER_EVENTS_THROTTLE_MAX_BLOCKS = 'NUCYPHER_EVENTS_THROTTLE_MAX_BLOCKS'
+
+# Probationary period (see #2353)
+END_OF_POLICIES_PROBATIONARY_PERIOD = MayaDT.from_iso8601('2021-02-28T23:59:59.0Z')
diff --git a/tests/acceptance/cli/lifecycle.py b/tests/acceptance/cli/lifecycle.py
index 115cda448..1bb50d33d 100644
--- a/tests/acceptance/cli/lifecycle.py
+++ b/tests/acceptance/cli/lifecycle.py
@@ -294,6 +294,7 @@ def run_entire_cli_lifecycle(click_runner,
bob_keys = side_channel.fetch_bob_public_keys()
bob_encrypting_key = bob_keys.bob_encrypting_key
bob_verifying_key = bob_keys.bob_verifying_key
+ expiration = (maya.now() + datetime.timedelta(days=3)).datetime().strftime("%Y-%m-%d %H:%M:%S")
grant_args = ('alice', 'grant',
'--mock-networking',
@@ -303,7 +304,7 @@ def run_entire_cli_lifecycle(click_runner,
'--config-file', alice_configuration_file_location,
'--m', 2,
'--n', 3,
- '--expiration', (maya.now() + datetime.timedelta(days=3)).iso8601(),
+ '--expiration', expiration,
'--label', random_label,
'--bob-encrypting-key', bob_encrypting_key,
'--bob-verifying-key', bob_verifying_key)
@@ -314,9 +315,8 @@ def run_entire_cli_lifecycle(click_runner,
grant_args += ('--provider', TEST_PROVIDER_URI,
'--rate', Web3.toWei(9, 'gwei'))
- # TODO: Stop.
grant_result = click_runner.invoke(nucypher_cli, grant_args, catch_exceptions=False, env=envvars)
- assert grant_result.exit_code == 0
+ assert grant_result.exit_code == 0, grant_result.output
grant_result = json.loads(grant_result.output)
diff --git a/tests/acceptance/cli/ursula/test_stakeholder_and_ursula.py b/tests/acceptance/cli/ursula/test_stakeholder_and_ursula.py
index 159861597..500ab31b2 100644
--- a/tests/acceptance/cli/ursula/test_stakeholder_and_ursula.py
+++ b/tests/acceptance/cli/ursula/test_stakeholder_and_ursula.py
@@ -755,6 +755,7 @@ def test_stake_unbond_worker(click_runner,
init_args = ('stake', 'unbond-worker',
'--config-file', stakeholder_configuration_file_location,
'--staking-address', manual_staker,
+ '--force'
)
user_input = f'{INSECURE_DEVELOPMENT_PASSWORD}'
diff --git a/tests/integration/cli/actions/test_auth_actions.py b/tests/integration/cli/actions/test_auth_actions.py
index 99eedbbac..c0dd54701 100644
--- a/tests/integration/cli/actions/test_auth_actions.py
+++ b/tests/integration/cli/actions/test_auth_actions.py
@@ -118,7 +118,7 @@ def test_unlock_nucypher_keyring_invalid_password(mocker, test_emitter, alice_bl
keyring_attach_spy.assert_called_once()
captured = capsys.readouterr()
- assert DECRYPTING_CHARACTER_KEYRING.format(name='alice') in captured.out
+ assert DECRYPTING_CHARACTER_KEYRING.format(name=alice_blockchain_test_config.NAME.capitalize()) in captured.out
def test_unlock_nucypher_keyring_dev_mode(mocker, test_emitter, capsys, alice_blockchain_test_config):
@@ -137,7 +137,7 @@ def test_unlock_nucypher_keyring_dev_mode(mocker, test_emitter, capsys, alice_bl
assert result
output = capsys.readouterr().out
- message = DECRYPTING_CHARACTER_KEYRING.format(name=alice_blockchain_test_config.NAME)
+ message = DECRYPTING_CHARACTER_KEYRING.format(name=alice_blockchain_test_config.NAME.capitalize())
assert message in output
unlock_spy.assert_not_called()
@@ -166,7 +166,7 @@ def test_unlock_nucypher_keyring(mocker,
assert result
captured = capsys.readouterr()
- message = DECRYPTING_CHARACTER_KEYRING.format(name=alice_blockchain_test_config.NAME)
+ message = DECRYPTING_CHARACTER_KEYRING.format(name=alice_blockchain_test_config.NAME.capitalize())
assert message in captured.out
unlock_spy.assert_called_once_with(password=INSECURE_DEVELOPMENT_PASSWORD)