mirror of https://github.com/nucypher/nucypher.git
Pre-arranged merge commit between @kprasch and myself to reconcile our branches.
Quite a few conflicts resolved.pull/574/head
commit
a3fb853ffa
|
@ -12,7 +12,11 @@ workflows:
|
|||
filters:
|
||||
tags:
|
||||
only: /.*/
|
||||
- eth_contract_unit:
|
||||
- mypy:
|
||||
filters:
|
||||
tags:
|
||||
only: /.*/
|
||||
- contracts:
|
||||
context: "NuCypher Tests"
|
||||
filters:
|
||||
tags:
|
||||
|
@ -20,7 +24,14 @@ workflows:
|
|||
requires:
|
||||
- pip_install
|
||||
- pipenv_install
|
||||
- config_unit:
|
||||
- config:
|
||||
context: "NuCypher Tests"
|
||||
filters:
|
||||
tags:
|
||||
only: /.*/
|
||||
requires:
|
||||
- contracts
|
||||
- crypto:
|
||||
context: "NuCypher Tests"
|
||||
filters:
|
||||
tags:
|
||||
|
@ -28,7 +39,7 @@ workflows:
|
|||
requires:
|
||||
- pip_install
|
||||
- pipenv_install
|
||||
- crypto_unit:
|
||||
- network:
|
||||
context: "NuCypher Tests"
|
||||
filters:
|
||||
tags:
|
||||
|
@ -36,7 +47,7 @@ workflows:
|
|||
requires:
|
||||
- pip_install
|
||||
- pipenv_install
|
||||
- network_unit:
|
||||
- keystore:
|
||||
context: "NuCypher Tests"
|
||||
filters:
|
||||
tags:
|
||||
|
@ -44,23 +55,7 @@ workflows:
|
|||
requires:
|
||||
- pip_install
|
||||
- pipenv_install
|
||||
- keystore_unit:
|
||||
context: "NuCypher Tests"
|
||||
filters:
|
||||
tags:
|
||||
only: /.*/
|
||||
requires:
|
||||
- pip_install
|
||||
- pipenv_install
|
||||
- blockchain_interface_unit:
|
||||
context: "NuCypher Tests"
|
||||
filters:
|
||||
tags:
|
||||
only: /.*/
|
||||
requires:
|
||||
- pip_install
|
||||
- pipenv_install
|
||||
- blockchain_entities:
|
||||
- blockchain:
|
||||
context: "NuCypher Tests"
|
||||
filters:
|
||||
tags:
|
||||
|
@ -74,66 +69,102 @@ workflows:
|
|||
tags:
|
||||
only: /.*/
|
||||
requires:
|
||||
- pip_install
|
||||
- pipenv_install
|
||||
- intercontract_integration:
|
||||
- crypto
|
||||
- network
|
||||
- keystore
|
||||
- agents:
|
||||
context: "NuCypher Tests"
|
||||
filters:
|
||||
tags:
|
||||
only: /.*/
|
||||
requires:
|
||||
- eth_contract_unit
|
||||
- mypy_type_check:
|
||||
filters:
|
||||
tags:
|
||||
only: /.*/
|
||||
requires:
|
||||
- config_unit
|
||||
- crypto_unit
|
||||
- network_unit
|
||||
- keystore_unit
|
||||
- character
|
||||
- cli_tests:
|
||||
- blockchain
|
||||
- contracts
|
||||
- actors:
|
||||
context: "NuCypher Tests"
|
||||
filters:
|
||||
tags:
|
||||
only: /.*/
|
||||
requires:
|
||||
- blockchain_entities
|
||||
- blockchain_interface_unit
|
||||
- config_unit
|
||||
- crypto_unit
|
||||
- network_unit
|
||||
- keystore_unit
|
||||
- blockchain
|
||||
- contracts
|
||||
- deployers:
|
||||
context: "NuCypher Tests"
|
||||
filters:
|
||||
tags:
|
||||
only: /.*/
|
||||
requires:
|
||||
- blockchain
|
||||
- contracts
|
||||
- config:
|
||||
context: "NuCypher Tests"
|
||||
filters:
|
||||
tags:
|
||||
only: /.*/
|
||||
requires:
|
||||
- blockchain
|
||||
- crypto
|
||||
- network
|
||||
- keystore
|
||||
- cli:
|
||||
context: "NuCypher Tests"
|
||||
filters:
|
||||
tags:
|
||||
only: /.*/
|
||||
requires:
|
||||
- actors
|
||||
- deployers
|
||||
- config
|
||||
- character
|
||||
- test_deploy:
|
||||
context: "NuCypher PyPI"
|
||||
requires:
|
||||
- mypy_type_check
|
||||
- cli_tests
|
||||
- ursula_command:
|
||||
context: "NuCypher Tests"
|
||||
filters:
|
||||
tags:
|
||||
only: /v[0-9]+.*/
|
||||
branches:
|
||||
ignore: /.*/
|
||||
- request_publication_approval:
|
||||
type: approval
|
||||
only: /.*/
|
||||
requires:
|
||||
- test_deploy
|
||||
filters:
|
||||
tags:
|
||||
only: /v[0-9]+.*/
|
||||
branches:
|
||||
ignore: /.*/
|
||||
- deploy:
|
||||
context: "NuCypher PyPI"
|
||||
requires:
|
||||
- request_publication_approval
|
||||
filters:
|
||||
tags:
|
||||
only: /v[0-9]+.*/
|
||||
branches:
|
||||
ignore: /.*/
|
||||
- actors
|
||||
- deployers
|
||||
- config
|
||||
- character
|
||||
|
||||
#
|
||||
# TODO: Initial Publication Automation
|
||||
#
|
||||
# - test_build:
|
||||
# filters:
|
||||
# tags:
|
||||
# only: /.*/
|
||||
# requires:
|
||||
# - cli
|
||||
# - ursula_command
|
||||
# - test_deploy:
|
||||
# context: "NuCypher PyPI"
|
||||
# requires:
|
||||
# - test_build
|
||||
# filters:
|
||||
# tags:
|
||||
# only: /v[0-9]+.*/
|
||||
# branches:
|
||||
# ignore: /.*/
|
||||
# - request_publication_approval:
|
||||
# type: approval
|
||||
# requires:
|
||||
# - test_deploy
|
||||
# filters:
|
||||
# tags:
|
||||
# only: /v[0-9]+.*/
|
||||
# branches:
|
||||
# ignore: /.*/
|
||||
# - deploy:
|
||||
# context: "NuCypher PyPI"
|
||||
# requires:
|
||||
# - request_publication_approval
|
||||
# filters:
|
||||
# tags:
|
||||
# only: /v[0-9]+.*/
|
||||
# branches:
|
||||
# ignore: /.*/
|
||||
#
|
||||
|
||||
|
||||
python_36_base: &python_36_base
|
||||
|
@ -187,7 +218,7 @@ jobs:
|
|||
name: Check Python Entrypoint
|
||||
command: python3 -c "import nucypher; print(nucypher.__version__)"
|
||||
|
||||
blockchain_interface_unit:
|
||||
blockchain:
|
||||
<<: *python_36_base
|
||||
parallelism: 2
|
||||
steps:
|
||||
|
@ -196,30 +227,56 @@ jobs:
|
|||
at: ~/.local/share/virtualenvs/
|
||||
- run:
|
||||
name: Blockchain Interface Tests
|
||||
command: |
|
||||
pipenv run pytest --cov=nucypher/blockchain/eth -v --runslow $(circleci tests glob tests/blockchain/eth/interfaces/**/test_*.py | circleci tests split --split-by=timings) --junitxml=./reports/pytest/blockchain_interface_results.xml
|
||||
command: pipenv run pytest --cov=nucypher/blockchain/eth -v --runslow tests/blockchain/eth/interfaces --junitxml=./reports/pytest/results.xml
|
||||
- run: *coveralls
|
||||
- store_test_results:
|
||||
path: ./reports/pytest/
|
||||
|
||||
blockchain_entities:
|
||||
agents:
|
||||
<<: *python_36_base
|
||||
parallelism: 6
|
||||
parallelism: 2
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: ~/.local/share/virtualenvs/
|
||||
- run:
|
||||
name: Blockchain Interface Tests
|
||||
command: |
|
||||
pipenv run pytest --cov=nucypher/blockchain/eth -v --runslow $(circleci tests glob tests/blockchain/eth/entities/**/test_*.py | circleci tests split --split-by=timings) --junitxml=./reports/pytest/blockchain_interface_results.xml
|
||||
name: Blockchain Agent Tests
|
||||
command: pipenv run pytest --cov=nucypher/blockchain/eth -v --runslow $(circleci tests glob tests/blockchain/eth/entities/agents/**/*.py | circleci tests split --split-by=timings) --junitxml=./reports/pytest/results.xml
|
||||
- run: *coveralls
|
||||
- store_test_results:
|
||||
path: ./reports/pytest/
|
||||
|
||||
eth_contract_unit:
|
||||
actors:
|
||||
<<: *python_36_base
|
||||
parallelism: 2
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: ~/.local/share/virtualenvs/
|
||||
- run:
|
||||
name: Blockchain Actor Tests
|
||||
command: pipenv run pytest --cov=nucypher/blockchain/eth -v --runslow $(circleci tests glob tests/blockchain/eth/entities/actors/**/*.py | circleci tests split --split-by=timings) --junitxml=./reports/pytest/results.xml
|
||||
- run: *coveralls
|
||||
- store_test_results:
|
||||
path: ./reports/pytest/
|
||||
|
||||
deployers:
|
||||
<<: *python_36_base
|
||||
parallelism: 4
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: ~/.local/share/virtualenvs/
|
||||
- run:
|
||||
name: Contract Deployer Tests
|
||||
command: pipenv run pytest --cov=nucypher/blockchain/eth -v --runslow --junitxml=./reports/pytest/results.xml $(circleci tests glob tests/blockchain/eth/entities/deployers/test_*.py | circleci tests split --split-by=timings)
|
||||
- run: *coveralls
|
||||
- store_test_results:
|
||||
path: ./reports/pytest/
|
||||
|
||||
contracts:
|
||||
<<: *python_36_base
|
||||
parallelism: 5
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
|
@ -228,11 +285,10 @@ jobs:
|
|||
name: Ethereum Contract Unit Tests
|
||||
command: |
|
||||
pipenv run pytest --junitxml=./reports/pytest/eth-contract-unit-report.xml -v --runslow $(circleci tests glob tests/blockchain/eth/contracts/**/**/test_*.py | circleci tests split --split-by=timings)
|
||||
- run: *coveralls
|
||||
- store_test_results:
|
||||
path: ./reports/pytest/
|
||||
|
||||
config_unit:
|
||||
config:
|
||||
<<: *python_36_base
|
||||
parallelism: 2
|
||||
steps:
|
||||
|
@ -247,7 +303,7 @@ jobs:
|
|||
- store_test_results:
|
||||
path: ./reports/pytest/
|
||||
|
||||
crypto_unit:
|
||||
crypto:
|
||||
<<: *python_36_base
|
||||
steps:
|
||||
- checkout
|
||||
|
@ -261,7 +317,7 @@ jobs:
|
|||
- store_test_results:
|
||||
path: ./reports/pytest/
|
||||
|
||||
network_unit:
|
||||
network:
|
||||
<<: *python_36_base
|
||||
steps:
|
||||
- checkout
|
||||
|
@ -275,7 +331,7 @@ jobs:
|
|||
- store_test_results:
|
||||
path: ./reports/pytest/
|
||||
|
||||
keystore_unit:
|
||||
keystore:
|
||||
<<: *python_36_base
|
||||
parallelism: 2
|
||||
steps:
|
||||
|
@ -305,20 +361,20 @@ jobs:
|
|||
- store_test_results:
|
||||
path: ./reports/pytest/
|
||||
|
||||
intercontract_integration:
|
||||
learning:
|
||||
<<: *python_36_base
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: ~/.local/share/virtualenvs/
|
||||
- run:
|
||||
name: Ethereum Inter-Contract Integration Test
|
||||
command: |
|
||||
pipenv run pytest -v --runslow tests/blockchain/eth/contracts/main/test_intercontract_integration.py --junitxml=./reports/pytest/intercontract_integration_results.xml
|
||||
name: Learner Tests
|
||||
command: pipenv run pytest --cov=nucypher -v --runslow tests/learning --junitxml=./reports/pytest/results.xml
|
||||
- run: *coveralls
|
||||
- store_test_results:
|
||||
path: ./reports/pytest/
|
||||
|
||||
cli_tests:
|
||||
cli:
|
||||
<<: *python_36_base
|
||||
parallelism: 4
|
||||
steps:
|
||||
|
@ -328,21 +384,34 @@ jobs:
|
|||
- run:
|
||||
name: Nucypher CLI Tests
|
||||
command: |
|
||||
pipenv run pytest --cov=nucypher/cli.py -v --runslow $(circleci tests glob tests/cli/**/test_*.py | circleci tests split --split-by=timings) --junitxml=./reports/pytest/cli_results.xml
|
||||
- run: *coveralls
|
||||
pipenv run pytest -v --runslow $(circleci tests glob tests/cli/**/test_*.py | circleci tests split --split-by=timings) --junitxml=./reports/pytest/cli_results.xml
|
||||
- store_test_results:
|
||||
path: ./reports/pytest/
|
||||
|
||||
mypy_type_check:
|
||||
ursula_command:
|
||||
<<: *python_36_base
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: ~/.local/share/virtualenvs/
|
||||
- run:
|
||||
name: Ursula Command Tests
|
||||
command: |
|
||||
pipenv run pytest -v --runslow tests/cli/protocol --junitxml=./reports/pytest/results.xml
|
||||
- store_test_results:
|
||||
path: ./reports/pytest/
|
||||
|
||||
mypy:
|
||||
<<: *python_36_base
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: ~/.local/share/virtualenvs/
|
||||
- run:
|
||||
name: Install lxml
|
||||
name: Install mypy
|
||||
command: |
|
||||
pipenv run pip install lxml
|
||||
pipenv run pip install mypy
|
||||
- run:
|
||||
name: Run Mypy Static Type Checks (Always Succeed)
|
||||
command: |
|
||||
|
@ -352,6 +421,30 @@ jobs:
|
|||
- store_artifacts:
|
||||
path: ./mypy_reports
|
||||
|
||||
test_build:
|
||||
<<: *python_36_base
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Install Dependencies (Test Build)
|
||||
command: |
|
||||
pipenv install --three --dev --skip-lock --pre
|
||||
pipenv install --dev --skip-lock twine
|
||||
- run:
|
||||
name: Build Python Wheel
|
||||
command: |
|
||||
pipenv run python setup.py sdist
|
||||
pipenv run python setup.py bdist_wheel -v
|
||||
- run:
|
||||
name: Install Nucypher via Wheel
|
||||
command: |
|
||||
pip3 install --user ./dist/nucypher-0.1.0a0-py3-none-any.whl[test]
|
||||
- run:
|
||||
name: Run Entrypoint Version Commands
|
||||
command: |
|
||||
python -c "import nucypher; print(nucypher.__version__)"
|
||||
nucypher --version
|
||||
|
||||
test_deploy:
|
||||
<<: *python_36_base
|
||||
steps:
|
||||
|
|
1
Pipfile
1
Pipfile
|
@ -59,6 +59,7 @@ python-coveralls = "*"
|
|||
ansible = "*"
|
||||
moto = "*"
|
||||
nucypher = {editable = true, path = "."}
|
||||
pytest-mock = "*"
|
||||
|
||||
[scripts]
|
||||
install-solc = "./scripts/install_solc.sh"
|
||||
|
|
|
@ -32,16 +32,16 @@ MY_REST_PORT = sys.argv[1]
|
|||
# TODO: Use real path tooling here.
|
||||
SHARED_CRUFTSPACE = "{}/examples-runtime-cruft".format(os.path.dirname(os.path.abspath(__file__)))
|
||||
CRUFTSPACE = "{}/{}".format(SHARED_CRUFTSPACE, MY_REST_PORT)
|
||||
DB_NAME = "{}/database".format(CRUFTSPACE)
|
||||
db_filepath = "{}/database".format(CRUFTSPACE)
|
||||
CERTIFICATE_DIR = "{}/certs".format(CRUFTSPACE)
|
||||
|
||||
|
||||
def spin_up_ursula(rest_port, db_name, teachers=(), certificate_dir=None):
|
||||
def spin_up_ursula(rest_port, db_filepath, teachers=(), certificate_dir=None):
|
||||
metadata_file = "examples-runtime-cruft/node-metadata-{}".format(rest_port)
|
||||
|
||||
_URSULA = Ursula(rest_port=rest_port,
|
||||
rest_host="localhost",
|
||||
db_name=db_name,
|
||||
db_filepath=db_filepath,
|
||||
federated_only=True,
|
||||
known_nodes=teachers,
|
||||
known_certificates_dir=certificate_dir
|
||||
|
@ -52,7 +52,7 @@ def spin_up_ursula(rest_port, db_name, teachers=(), certificate_dir=None):
|
|||
_URSULA.start_learning_loop()
|
||||
_URSULA.get_deployer().run()
|
||||
finally:
|
||||
os.remove(db_name)
|
||||
os.remove(db_filepath)
|
||||
os.remove(metadata_file)
|
||||
|
||||
|
||||
|
@ -78,7 +78,7 @@ if __name__ == "__main__":
|
|||
except FileNotFoundError as e:
|
||||
raise ValueError("Can't find a metadata file for node {}".format(teacher_rest_port))
|
||||
|
||||
spin_up_ursula(MY_REST_PORT, DB_NAME,
|
||||
spin_up_ursula(MY_REST_PORT, db_filepath,
|
||||
teachers=teachers,
|
||||
certificate_dir=CERTIFICATE_DIR)
|
||||
finally:
|
||||
|
|
|
@ -14,6 +14,8 @@ GNU General Public License for more details.
|
|||
You should have received a copy of the GNU General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__all__ = [
|
||||
|
|
|
@ -14,6 +14,8 @@ GNU General Public License for more details.
|
|||
You should have received a copy of the GNU General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
|
@ -189,7 +191,7 @@ class TemporaryEthereumContractRegistry(EthereumContractRegistry):
|
|||
class InMemoryEthereumContractRegistry(EthereumContractRegistry):
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(registry_filepath=":memory:")
|
||||
super().__init__(registry_filepath="::memory-registry::")
|
||||
self.__registry_data = None # type: str
|
||||
|
||||
def clear(self):
|
||||
|
@ -277,7 +279,7 @@ class AllocationRegistry(EthereumContractRegistry):
|
|||
class InMemoryAllocationRegistry(AllocationRegistry):
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(registry_filepath=":memory:", *args, **kwargs)
|
||||
super().__init__(registry_filepath="::memory-registry::", *args, **kwargs)
|
||||
self.__registry_data = None # type: str
|
||||
|
||||
def clear(self):
|
||||
|
|
|
@ -16,10 +16,6 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
|||
"""
|
||||
|
||||
from contextlib import suppress
|
||||
from typing import Dict, ClassVar, Set
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
from typing import Union, List
|
||||
|
||||
from eth_keys import KeyAPI as EthKeyAPI
|
||||
from eth_utils import to_checksum_address, to_canonical_address
|
||||
|
@ -27,6 +23,24 @@ from umbral.keys import UmbralPublicKey
|
|||
from umbral.signing import Signature
|
||||
|
||||
from constant_sorrow import constants, default_constant_splitter
|
||||
from eth_keys import KeyAPI as EthKeyAPI
|
||||
from eth_utils import to_checksum_address, to_canonical_address
|
||||
from typing import Dict, ClassVar
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
from typing import Union, List
|
||||
|
||||
from constant_sorrow import default_constant_splitter
|
||||
from constant_sorrow.constants import (
|
||||
NO_NICKNAME,
|
||||
NO_BLOCKCHAIN_CONNECTION,
|
||||
STRANGER,
|
||||
NO_SIGNING_POWER,
|
||||
DO_NOT_SIGN,
|
||||
NO_DECRYPTION_PERFORMED,
|
||||
SIGNATURE_TO_FOLLOW,
|
||||
SIGNATURE_IS_ON_CIPHERTEXT
|
||||
)
|
||||
from nucypher.blockchain.eth.chains import Blockchain
|
||||
from nucypher.crypto.api import encrypt_and_sign
|
||||
from nucypher.crypto.kits import UmbralMessageKit
|
||||
|
@ -42,6 +56,8 @@ from nucypher.crypto.signing import signature_splitter, StrangerStamp, Signature
|
|||
from nucypher.network.middleware import RestMiddleware
|
||||
from nucypher.network.nicknames import nickname_from_seed
|
||||
from nucypher.network.nodes import Learner
|
||||
from umbral.keys import UmbralPublicKey
|
||||
from umbral.signing import Signature
|
||||
|
||||
|
||||
class Character(Learner):
|
||||
|
@ -61,7 +77,7 @@ class Character(Learner):
|
|||
is_me: bool = True,
|
||||
federated_only: bool = False,
|
||||
blockchain: Blockchain = None,
|
||||
checksum_address: bytes = constants.NO_BLOCKCHAIN_CONNECTION.bool_value(False),
|
||||
checksum_public_address: bytes = NO_BLOCKCHAIN_CONNECTION.bool_value(False),
|
||||
network_middleware: RestMiddleware = None,
|
||||
keyring_dir: str = None,
|
||||
crypto_power: CryptoPower = None,
|
||||
|
@ -109,7 +125,7 @@ class Character(Learner):
|
|||
else:
|
||||
self._crypto_power = CryptoPower(power_ups=self._default_crypto_powerups)
|
||||
|
||||
self._checksum_address = checksum_address
|
||||
self._checksum_address = checksum_public_address
|
||||
#
|
||||
# Self-Character
|
||||
#
|
||||
|
@ -128,7 +144,7 @@ class Character(Learner):
|
|||
signing_power = self._crypto_power.power_ups(SigningPower) # type: SigningPower
|
||||
self._stamp = signing_power.get_signature_stamp() # type: SignatureStamp
|
||||
except NoSigningPower:
|
||||
self._stamp = constants.NO_SIGNING_POWER
|
||||
self._stamp = NO_SIGNING_POWER
|
||||
|
||||
#
|
||||
# Learner
|
||||
|
@ -145,17 +161,18 @@ class Character(Learner):
|
|||
if network_middleware is not None:
|
||||
raise TypeError("Network middleware cannot be attached to a Stanger-Character.")
|
||||
self._stamp = StrangerStamp(self.public_keys(SigningPower))
|
||||
self.keyring_dir = constants.STRANGER
|
||||
self.network_middleware = constants.STRANGER
|
||||
self.keyring_dir = STRANGER
|
||||
self.network_middleware = STRANGER
|
||||
|
||||
|
||||
#
|
||||
# Decentralized
|
||||
#
|
||||
if not federated_only:
|
||||
if not checksum_address:
|
||||
raise ValueError("No checksum_address provided while running in a non-federated mode.")
|
||||
if not checksum_public_address:
|
||||
raise ValueError("No checksum_public_address provided while running in a non-federated mode.")
|
||||
else:
|
||||
self._checksum_address = checksum_address # TODO: Check that this matches BlockchainPower
|
||||
self._checksum_address = checksum_public_address # TODO: Check that this matches BlockchainPower
|
||||
#
|
||||
# Federated
|
||||
#
|
||||
|
@ -163,21 +180,27 @@ class Character(Learner):
|
|||
try:
|
||||
self._set_checksum_address() # type: str
|
||||
except NoSigningPower:
|
||||
self._checksum_address = constants.NO_BLOCKCHAIN_CONNECTION
|
||||
if checksum_address:
|
||||
self._checksum_address = NO_BLOCKCHAIN_CONNECTION
|
||||
if checksum_public_address:
|
||||
# We'll take a checksum address, as long as it matches their singing key
|
||||
if not checksum_address == self.checksum_public_address:
|
||||
if not checksum_public_address == self.checksum_public_address:
|
||||
error = "Federated-only Characters derive their address from their Signing key; got {} instead."
|
||||
raise self.SuspiciousActivity(error.format(checksum_address))
|
||||
raise self.SuspiciousActivity(error.format(checksum_public_address))
|
||||
|
||||
#
|
||||
# Nicknames
|
||||
#
|
||||
try:
|
||||
self.nickname, self.nickname_metadata = nickname_from_seed(self.checksum_public_address)
|
||||
except SigningPower.not_found_error:
|
||||
if self.federated_only:
|
||||
self.nickname = self.nickname_metadata = constants.NO_NICKNAME
|
||||
self.nickname = self.nickname_metadata = NO_NICKNAME
|
||||
else:
|
||||
raise
|
||||
|
||||
#
|
||||
# Fleet state
|
||||
#
|
||||
if is_me is True:
|
||||
self.known_nodes.record_fleet_state()
|
||||
|
||||
|
@ -202,7 +225,7 @@ class Character(Learner):
|
|||
|
||||
@property
|
||||
def stamp(self):
|
||||
if self._stamp is constants.NO_SIGNING_POWER:
|
||||
if self._stamp is NO_SIGNING_POWER:
|
||||
raise NoSigningPower
|
||||
elif not self._stamp:
|
||||
raise AttributeError("SignatureStamp has not been set up yet.")
|
||||
|
@ -219,7 +242,7 @@ class Character(Learner):
|
|||
|
||||
@property
|
||||
def checksum_public_address(self):
|
||||
if self._checksum_address is constants.NO_BLOCKCHAIN_CONNECTION:
|
||||
if self._checksum_address is NO_BLOCKCHAIN_CONNECTION:
|
||||
self._set_checksum_address()
|
||||
return self._checksum_address
|
||||
|
||||
|
@ -239,7 +262,7 @@ class Character(Learner):
|
|||
with the public_material_bytes, and the resulting CryptoPowerUp instance
|
||||
consumed by the Character.
|
||||
|
||||
# TODO: Need to be federated only until we figure out the best way to get the checksum_address in here.
|
||||
# TODO: Need to be federated only until we figure out the best way to get the checksum_public_address in here.
|
||||
|
||||
"""
|
||||
|
||||
|
@ -274,7 +297,7 @@ class Character(Learner):
|
|||
:return: A tuple, (ciphertext, signature). If sign==False,
|
||||
then signature will be NOT_SIGNED.
|
||||
"""
|
||||
signer = self.stamp if sign else constants.DO_NOT_SIGN
|
||||
signer = self.stamp if sign else DO_NOT_SIGN
|
||||
|
||||
message_kit, signature = encrypt_and_sign(recipient_pubkey_enc=recipient.public_keys(EncryptingPower),
|
||||
plaintext=plaintext,
|
||||
|
@ -323,12 +346,12 @@ class Character(Learner):
|
|||
cleartext_with_sig_header = self.decrypt(message_kit=message_kit,
|
||||
label=label)
|
||||
sig_header, cleartext = default_constant_splitter(cleartext_with_sig_header, return_remainder=True)
|
||||
if sig_header == constants.SIGNATURE_IS_ON_CIPHERTEXT:
|
||||
# The ciphertext is what is signed - note that for later.
|
||||
if sig_header == SIGNATURE_IS_ON_CIPHERTEXT:
|
||||
# THe ciphertext is what is signed - note that for later.
|
||||
message = message_kit.ciphertext
|
||||
if not signature:
|
||||
raise ValueError("Can't check a signature on the ciphertext if don't provide one.")
|
||||
elif sig_header == constants.SIGNATURE_TO_FOLLOW:
|
||||
elif sig_header == SIGNATURE_TO_FOLLOW:
|
||||
# The signature follows in this cleartext - split it off.
|
||||
signature_from_kit, cleartext = signature_splitter(cleartext,
|
||||
return_remainder=True)
|
||||
|
@ -336,7 +359,7 @@ class Character(Learner):
|
|||
else:
|
||||
# Not decrypting - the message is the object passed in as a message kit. Cast it.
|
||||
message = bytes(message_kit)
|
||||
cleartext = constants.NO_DECRYPTION_PERFORMED
|
||||
cleartext = NO_DECRYPTION_PERFORMED
|
||||
|
||||
if signature and signature_from_kit:
|
||||
if signature != signature_from_kit:
|
||||
|
|
|
@ -24,23 +24,30 @@ from typing import Set
|
|||
|
||||
import maya
|
||||
import requests
|
||||
import socket
|
||||
import time
|
||||
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 load_pem_x509_certificate, Certificate
|
||||
from cryptography.x509 import load_pem_x509_certificate, Certificate, NameOID
|
||||
from eth_utils import to_checksum_address
|
||||
from functools import partial
|
||||
from twisted.internet import threads
|
||||
from umbral.keys import UmbralPublicKey
|
||||
from umbral.signing import Signature
|
||||
from typing import Dict
|
||||
from typing import Iterable
|
||||
from typing import List
|
||||
|
||||
from bytestring_splitter import VariableLengthBytestring, BytestringKwargifier, BytestringSplitter, \
|
||||
BytestringSplittingError
|
||||
from constant_sorrow import constants
|
||||
from constant_sorrow.constants import INCLUDED_IN_BYTESTRING, constant_or_bytes
|
||||
from bytestring_splitter import BytestringSplitter, VariableLengthBytestring
|
||||
from constant_sorrow import constants
|
||||
from constant_sorrow.constants import PUBLIC_ONLY
|
||||
from nucypher.blockchain.eth.actors import PolicyAuthor, Miner
|
||||
from nucypher.blockchain.eth.agents import MinerAgent
|
||||
from nucypher.characters.base import Character, Learner
|
||||
from nucypher.config.storages import NodeStorage
|
||||
from nucypher.config.storages import NodeStorage, ForgetfulNodeStorage
|
||||
from nucypher.crypto.api import keccak_digest
|
||||
from nucypher.crypto.constants import PUBLIC_KEY_LENGTH, PUBLIC_ADDRESS_LENGTH
|
||||
from nucypher.crypto.powers import SigningPower, EncryptingPower, DelegatingPower, BlockchainPower
|
||||
|
@ -48,7 +55,13 @@ from nucypher.keystore.keypairs import HostingKeypair
|
|||
from nucypher.network.nicknames import nickname_from_seed
|
||||
from nucypher.network.nodes import Teacher
|
||||
from nucypher.network.protocols import InterfaceInfo
|
||||
from nucypher.network.middleware import RestMiddleware
|
||||
from nucypher.network.nodes import Teacher
|
||||
from nucypher.network.protocols import InterfaceInfo, parse_node_uri
|
||||
from nucypher.network.server import ProxyRESTServer, TLSHostingPower, ProxyRESTRoutes
|
||||
from nucypher.utilities.decorators import validate_checksum_address
|
||||
from umbral.keys import UmbralPublicKey
|
||||
from umbral.signing import Signature
|
||||
|
||||
|
||||
class Alice(Character, PolicyAuthor):
|
||||
|
@ -57,11 +70,11 @@ class Alice(Character, PolicyAuthor):
|
|||
def __init__(self, is_me=True, federated_only=False, network_middleware=None, *args, **kwargs) -> None:
|
||||
|
||||
policy_agent = kwargs.pop("policy_agent", None)
|
||||
checksum_address = kwargs.pop("checksum_address", None)
|
||||
checksum_address = kwargs.pop("checksum_public_address", None)
|
||||
Character.__init__(self,
|
||||
is_me=is_me,
|
||||
federated_only=federated_only,
|
||||
checksum_address=checksum_address,
|
||||
checksum_public_address=checksum_address,
|
||||
network_middleware=network_middleware,
|
||||
*args, **kwargs)
|
||||
|
||||
|
@ -177,8 +190,8 @@ class Alice(Character, PolicyAuthor):
|
|||
# Wait for a revocation threshold of nodes to be known ((n - m) + 1)
|
||||
revocation_threshold = ((policy.n - policy.treasure_map.m) + 1)
|
||||
self.block_until_specific_nodes_are_known(
|
||||
policy.revocation_kit.revokable_addresses,
|
||||
allow_missing=(policy.n - revocation_threshold))
|
||||
policy.revocation_kit.revokable_addresses,
|
||||
allow_missing=(policy.n - revocation_threshold))
|
||||
except self.NotEnoughTeachers as e:
|
||||
raise e
|
||||
else:
|
||||
|
@ -445,19 +458,17 @@ class Ursula(Teacher, Character, Miner):
|
|||
domains: Set = (constants.GLOBAL_DOMAIN,), # For now, serving and learning domains will be the same.
|
||||
certificate: Certificate = None,
|
||||
certificate_filepath: str = None,
|
||||
|
||||
db_name: str = None,
|
||||
db_filepath: str = None,
|
||||
is_me: bool = True,
|
||||
interface_signature=None,
|
||||
timestamp=None,
|
||||
|
||||
# Blockchain
|
||||
checksum_address: str = None,
|
||||
identity_evidence: bytes = constants.NOT_SIGNED,
|
||||
checksum_public_address: str = None,
|
||||
|
||||
# Character
|
||||
passphrase: str = None,
|
||||
password: str = None,
|
||||
abort_on_learning_error: bool = False,
|
||||
federated_only: bool = False,
|
||||
start_learning_now: bool = None,
|
||||
|
@ -474,7 +485,7 @@ class Ursula(Teacher, Character, Miner):
|
|||
self._work_orders = list()
|
||||
Character.__init__(self,
|
||||
is_me=is_me,
|
||||
checksum_address=checksum_address,
|
||||
checksum_public_address=checksum_public_address,
|
||||
start_learning_now=start_learning_now,
|
||||
federated_only=federated_only,
|
||||
crypto_power=crypto_power,
|
||||
|
@ -493,12 +504,15 @@ class Ursula(Teacher, Character, Miner):
|
|||
# Staking Ursula
|
||||
#
|
||||
if not federated_only:
|
||||
Miner.__init__(self, is_me=is_me, checksum_address=checksum_address)
|
||||
Miner.__init__(self, is_me=is_me, checksum_address=checksum_public_address)
|
||||
|
||||
# Access staking node via node's transacting keys TODO: Better handle ephemeral staking self ursula
|
||||
blockchain_power = BlockchainPower(blockchain=self.blockchain, account=self.checksum_public_address)
|
||||
self._crypto_power.consume_power_up(blockchain_power)
|
||||
|
||||
# Use blockchain power to substantiate stamp, instead of signing key
|
||||
self.substantiate_stamp(password=password) # TODO: Derive from keyring
|
||||
|
||||
#
|
||||
# ProxyRESTServer and TLSHostingPower # TODO: Maybe we want _power_ups to be public after all?
|
||||
#
|
||||
|
@ -514,7 +528,6 @@ class Ursula(Teacher, Character, Miner):
|
|||
# REST Server (Ephemeral Self-Ursula)
|
||||
#
|
||||
rest_routes = ProxyRESTRoutes(
|
||||
db_name=db_name,
|
||||
db_filepath=db_filepath,
|
||||
network_middleware=self.network_middleware,
|
||||
federated_only=self.federated_only, # TODO: 466
|
||||
|
@ -578,7 +591,7 @@ class Ursula(Teacher, Character, Miner):
|
|||
timestamp=timestamp,
|
||||
identity_evidence=identity_evidence,
|
||||
substantiate_immediately=is_me and not federated_only,
|
||||
passphrase=passphrase)
|
||||
)
|
||||
|
||||
#
|
||||
# Logging / Updating
|
||||
|
|
|
@ -18,7 +18,7 @@ from eth_tester.exceptions import ValidationError
|
|||
|
||||
from nucypher.characters.lawful import Ursula
|
||||
from nucypher.crypto.powers import CryptoPower, SigningPower
|
||||
from nucypher.utilities.sandbox.constants import TEST_URSULA_INSECURE_DEVELOPMENT_PASSWORD
|
||||
from nucypher.utilities.sandbox.constants import INSECURE_DEVELOPMENT_PASSWORD, MOCK_URSULA_DB_FILEPATH
|
||||
from nucypher.utilities.sandbox.middleware import EvilMiddleWare
|
||||
|
||||
|
||||
|
@ -30,7 +30,7 @@ class Vladimir(Ursula):
|
|||
network_middleware = EvilMiddleWare()
|
||||
fraud_address = '0xbad022A87Df21E4c787C7B1effD5077014b8CC45'
|
||||
fraud_key = 'a75d701cc4199f7646909d15f22e2e0ef6094b3e2aa47a188f35f47e8932a7b9'
|
||||
db_name = 'vladimir.db'
|
||||
db_filepath = MOCK_URSULA_DB_FILEPATH
|
||||
|
||||
@classmethod
|
||||
def from_target_ursula(cls,
|
||||
|
@ -53,13 +53,12 @@ class Vladimir(Ursula):
|
|||
|
||||
vladimir = cls(is_me=True,
|
||||
crypto_power=crypto_power,
|
||||
db_name=cls.db_name,
|
||||
db_filepath=cls.db_name,
|
||||
db_filepath=cls.db_filepath,
|
||||
rest_host=target_ursula.rest_information()[0].host,
|
||||
rest_port=target_ursula.rest_information()[0].port,
|
||||
certificate=target_ursula.rest_server_certificate(),
|
||||
network_middleware=cls.network_middleware,
|
||||
checksum_address = cls.fraud_address,
|
||||
checksum_public_address = cls.fraud_address,
|
||||
######### Asshole.
|
||||
timestamp=target_ursula._timestamp,
|
||||
interface_signature=target_ursula._interface_signature_object,
|
||||
|
@ -76,8 +75,8 @@ class Vladimir(Ursula):
|
|||
Upload Vladimir's ETH keys to the keychain via web3 / RPC.
|
||||
"""
|
||||
try:
|
||||
passphrase = TEST_URSULA_INSECURE_DEVELOPMENT_PASSWORD
|
||||
blockchain.interface.w3.personal.importRawKey(private_key=cls.fraud_key, passphrase=passphrase)
|
||||
password = INSECURE_DEVELOPMENT_PASSWORD
|
||||
blockchain.interface.w3.personal.importRawKey(private_key=cls.fraud_key, passphrase=password)
|
||||
except (ValidationError, ):
|
||||
# check if Vlad's key is already on the keyring...
|
||||
if cls.fraud_address in blockchain.interface.w3.personal.listAccounts:
|
||||
|
|
1152
nucypher/cli.py
1152
nucypher/cli.py
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,223 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU 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 General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
|
||||
import collections
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
import click
|
||||
from twisted.logger import Logger
|
||||
from twisted.logger import globalLogPublisher
|
||||
from typing import ClassVar, Tuple
|
||||
|
||||
from nucypher.blockchain.eth.agents import EthereumContractAgent
|
||||
from nucypher.blockchain.eth.deployers import (
|
||||
NucypherTokenDeployer,
|
||||
MinerEscrowDeployer,
|
||||
PolicyManagerDeployer,
|
||||
ContractDeployer
|
||||
)
|
||||
from nucypher.cli.painting import BANNER
|
||||
from nucypher.cli.types import EIP55_CHECKSUM_ADDRESS
|
||||
from nucypher.config.node import NodeConfiguration
|
||||
from nucypher.utilities.logging import getTextFileObserver
|
||||
|
||||
|
||||
#
|
||||
# Click Eager Functions
|
||||
#
|
||||
|
||||
def echo_version(ctx, param, value):
|
||||
if not value or ctx.resilient_parsing:
|
||||
return
|
||||
click.secho(BANNER, bold=True)
|
||||
ctx.exit()
|
||||
|
||||
|
||||
#
|
||||
# Deployers
|
||||
#
|
||||
DeployerInfo = collections.namedtuple('DeployerInfo', ('deployer_class', # type: ContractDeployer
|
||||
'upgradeable', # type: bool
|
||||
'agent_name', # type: EthereumContractAgent
|
||||
'dependant')) # type: EthereumContractAgent
|
||||
|
||||
|
||||
DEPLOYERS = collections.OrderedDict({
|
||||
|
||||
NucypherTokenDeployer._contract_name: DeployerInfo(deployer_class=NucypherTokenDeployer,
|
||||
upgradeable=False,
|
||||
agent_name='token_agent',
|
||||
dependant=None),
|
||||
|
||||
MinerEscrowDeployer._contract_name: DeployerInfo(deployer_class=MinerEscrowDeployer,
|
||||
upgradeable=True,
|
||||
agent_name='miner_agent',
|
||||
dependant='token_agent'),
|
||||
|
||||
PolicyManagerDeployer._contract_name: DeployerInfo(deployer_class=PolicyManagerDeployer,
|
||||
upgradeable=True,
|
||||
agent_name='policy_agent',
|
||||
dependant='miner_agent')
|
||||
})
|
||||
|
||||
|
||||
class NucypherDeployerClickConfig:
|
||||
|
||||
log_to_file = True # TODO: Use envvar
|
||||
|
||||
def __init__(self):
|
||||
if self.log_to_file is True:
|
||||
globalLogPublisher.addObserver(getTextFileObserver())
|
||||
self.log = Logger(self.__class__.__name__)
|
||||
|
||||
|
||||
# Register the above class as a decorator
|
||||
nucypher_deployer_config = click.make_pass_decorator(NucypherDeployerClickConfig, ensure=True)
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.argument('action')
|
||||
@click.option('--contract-name', help="Deploy a single contract by name", type=click.STRING)
|
||||
@click.option('--force', is_flag=True)
|
||||
@click.option('--deployer-address', help="Deployer's checksum address", type=EIP55_CHECKSUM_ADDRESS)
|
||||
@click.option('--registry-outfile', help="Output path for new registry", type=click.Path(), default=NodeConfiguration.REGISTRY_SOURCE)
|
||||
@nucypher_deployer_config
|
||||
def deploy(config,
|
||||
action,
|
||||
deployer_address,
|
||||
contract_name,
|
||||
registry_outfile,
|
||||
force):
|
||||
"""Manage contract and registry deployment"""
|
||||
|
||||
if not config.deployer:
|
||||
click.secho("The --deployer flag must be used to issue the deploy command.", fg='red', bold=True)
|
||||
raise click.Abort()
|
||||
|
||||
def __get_deployers():
|
||||
|
||||
config.registry_filepath = registry_outfile
|
||||
config.connect_to_blockchain()
|
||||
config.blockchain.interface.deployer_address = deployer_address or config.accounts[0]
|
||||
click.confirm("Continue?", abort=True)
|
||||
return deployers
|
||||
|
||||
if action == "contracts":
|
||||
deployers = __get_deployers()
|
||||
__deployment_transactions = dict()
|
||||
__deployment_agents = dict()
|
||||
|
||||
available_deployers = ", ".join(deployers)
|
||||
click.echo("\n-----------------------------------------------")
|
||||
click.echo("Available Deployers: {}".format(available_deployers))
|
||||
click.echo("Blockchain Provider URI ... {}".format(config.blockchain.interface.provider_uri))
|
||||
click.echo("Registry Output Filepath .. {}".format(config.blockchain.interface.registry.filepath))
|
||||
click.echo("Deployer's Address ........ {}".format(config.blockchain.interface.deployer_address))
|
||||
click.echo("-----------------------------------------------\n")
|
||||
|
||||
def __deploy_contract(deployer_class: ClassVar,
|
||||
upgradeable: bool,
|
||||
agent_name: str,
|
||||
dependant: str = None
|
||||
) -> Tuple[dict, EthereumContractAgent]:
|
||||
|
||||
__contract_name = deployer_class._contract_name
|
||||
|
||||
__deployer_init_args = dict(blockchain=config.blockchain,
|
||||
deployer_address=config.blockchain.interface.deployer_address)
|
||||
|
||||
if dependant is not None:
|
||||
__deployer_init_args.update({dependant: __deployment_agents[dependant]})
|
||||
|
||||
if upgradeable:
|
||||
secret = click.prompt("Enter deployment secret for {}".format(__contract_name),
|
||||
hide_input=True, confirmation_prompt=True)
|
||||
secret_hash = hashlib.sha256(secret)
|
||||
__deployer_init_args.update({'secret_hash': secret_hash})
|
||||
|
||||
__deployer = deployer_class(**__deployer_init_args)
|
||||
|
||||
#
|
||||
# Arm
|
||||
#
|
||||
if not force:
|
||||
click.confirm("Arm {}?".format(deployer_class.__name__), abort=True)
|
||||
|
||||
is_armed, disqualifications = __deployer.arm(abort=False)
|
||||
if not is_armed:
|
||||
disqualifications = ', '.join(disqualifications)
|
||||
click.secho("Failed to arm {}. Disqualifications: {}".format(__contract_name, disqualifications),
|
||||
fg='red', bold=True)
|
||||
raise click.Abort()
|
||||
|
||||
#
|
||||
# Deploy
|
||||
#
|
||||
if not force:
|
||||
click.confirm("Deploy {}?".format(__contract_name), abort=True)
|
||||
__transactions = __deployer.deploy()
|
||||
__deployment_transactions[__contract_name] = __transactions
|
||||
|
||||
__agent = __deployer.make_agent()
|
||||
__deployment_agents[agent_name] = __agent
|
||||
|
||||
click.secho("Deployed {} - Contract Address: {}".format(contract_name, __agent.contract_address),
|
||||
fg='green', bold=True)
|
||||
|
||||
return __transactions, __agent
|
||||
|
||||
if contract_name:
|
||||
#
|
||||
# Deploy Single Contract
|
||||
#
|
||||
try:
|
||||
deployer_info = deployers[contract_name]
|
||||
except KeyError:
|
||||
click.secho(
|
||||
"No such contract {}. Available contracts are {}".format(contract_name, available_deployers),
|
||||
fg='red', bold=True)
|
||||
raise click.Abort()
|
||||
else:
|
||||
_txs, _agent = __deploy_contract(deployer_info.deployer_class,
|
||||
upgradeable=deployer_info.upgradeable,
|
||||
agent_name=deployer_info.agent_name,
|
||||
dependant=deployer_info.dependant)
|
||||
else:
|
||||
#
|
||||
# Deploy All Contracts
|
||||
#
|
||||
for deployer_name, deployer_info in deployers.items():
|
||||
_txs, _agent = __deploy_contract(deployer_info.deployer_class,
|
||||
upgradeable=deployer_info.upgradeable,
|
||||
agent_name=deployer_info.agent_name,
|
||||
dependant=deployer_info.dependant)
|
||||
|
||||
if not force and click.prompt("View deployment transaction hashes?"):
|
||||
for contract_name, transactions in __deployment_transactions.items():
|
||||
click.echo(contract_name)
|
||||
for tx_name, txhash in transactions.items():
|
||||
click.echo("{}:{}".format(tx_name, txhash))
|
||||
|
||||
if not force and click.confirm("Save transaction hashes to JSON file?"):
|
||||
file = click.prompt("Enter output filepath", type=click.File(mode='w')) # TODO: Save Txhashes
|
||||
file.__write(json.dumps(__deployment_transactions))
|
||||
click.secho("Successfully wrote transaction hashes file to {}".format(file.path), fg='green')
|
||||
|
||||
else:
|
||||
raise click.BadArgumentUsage
|
|
@ -0,0 +1,525 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU 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 General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
import click
|
||||
from nacl.exceptions import CryptoError
|
||||
from twisted.internet import stdio
|
||||
from twisted.logger import Logger
|
||||
from twisted.logger import globalLogPublisher
|
||||
|
||||
from constant_sorrow.constants import NO_BLOCKCHAIN_CONNECTION, NO_PASSWORD
|
||||
from nucypher.blockchain.eth.constants import MIN_LOCKED_PERIODS, MAX_MINTING_PERIODS
|
||||
from nucypher.characters.lawful import Ursula
|
||||
from nucypher.cli.painting import BANNER, paint_configuration, paint_known_nodes, paint_contract_status
|
||||
from nucypher.cli.protocol import UrsulaCommandProtocol
|
||||
from nucypher.cli.types import (
|
||||
EIP55_CHECKSUM_ADDRESS,
|
||||
UNREGISTERED_PORT,
|
||||
EXISTING_READABLE_FILE,
|
||||
EXISTING_WRITABLE_DIRECTORY,
|
||||
STAKE_VALUE,
|
||||
STAKE_DURATION
|
||||
)
|
||||
from nucypher.config.characters import UrsulaConfiguration
|
||||
from nucypher.utilities.logging import (
|
||||
logToSentry,
|
||||
getTextFileObserver,
|
||||
initialize_sentry,
|
||||
getJsonFileObserver,
|
||||
SimpleObserver)
|
||||
|
||||
|
||||
#
|
||||
# Click CLI Config
|
||||
#
|
||||
|
||||
class NucypherClickConfig:
|
||||
|
||||
__sentry_endpoint = "https://d8af7c4d692e4692a455328a280d845e@sentry.io/1310685" # TODO: Use nucypher domain
|
||||
|
||||
# Environment Variables
|
||||
config_file = os.environ.get('NUCYPHER_CONFIG_FILE', None)
|
||||
sentry_endpoint = os.environ.get("NUCYPHER_SENTRY_DSN", __sentry_endpoint)
|
||||
log_to_sentry = os.environ.get("NUCYPHER_SENTRY_LOGS", True)
|
||||
log_to_file = os.environ.get("NUCYPHER_FILE_LOGS", True)
|
||||
|
||||
# Sentry Logging
|
||||
if log_to_sentry is True:
|
||||
initialize_sentry(dsn=__sentry_endpoint)
|
||||
globalLogPublisher.addObserver(logToSentry)
|
||||
|
||||
# File Logging
|
||||
if log_to_file is True:
|
||||
globalLogPublisher.addObserver(getTextFileObserver())
|
||||
globalLogPublisher.addObserver(getJsonFileObserver())
|
||||
|
||||
def __init__(self):
|
||||
self.log = Logger(self.__class__.__name__)
|
||||
self.__keyring_password = NO_PASSWORD
|
||||
|
||||
def get_password(self, confirm: bool =False) -> str:
|
||||
keyring_password = os.environ.get("NUCYPHER_KEYRING_PASSWORD", NO_PASSWORD)
|
||||
|
||||
if keyring_password is NO_PASSWORD: # Collect password, prefer env var
|
||||
prompt = "Enter keyring password"
|
||||
keyring_password = click.prompt(prompt, confirmation_prompt=confirm, hide_input=True)
|
||||
|
||||
self.__keyring_password = keyring_password
|
||||
return self.__keyring_password
|
||||
|
||||
|
||||
# Register the above click configuration class as a decorator
|
||||
nucypher_click_config = click.make_pass_decorator(NucypherClickConfig, ensure=True)
|
||||
|
||||
|
||||
def echo_version(ctx, param, value):
|
||||
if not value or ctx.resilient_parsing:
|
||||
return
|
||||
click.secho(BANNER, bold=True)
|
||||
ctx.exit()
|
||||
|
||||
|
||||
#
|
||||
# Common CLI
|
||||
#
|
||||
|
||||
@click.group()
|
||||
@click.option('--version', help="Echo the CLI version", is_flag=True, callback=echo_version, expose_value=False, is_eager=True)
|
||||
@click.option('-v', '--verbose', help="Specify verbosity level", count=True)
|
||||
@nucypher_click_config
|
||||
def nucypher_cli(click_config, verbose):
|
||||
click.echo(BANNER)
|
||||
click_config.verbose = verbose
|
||||
if click_config.verbose:
|
||||
click.secho("Verbose mode is enabled", fg='blue')
|
||||
|
||||
|
||||
@nucypher_cli.command()
|
||||
@click.option('--config-file', help="Path to configuration file", type=EXISTING_READABLE_FILE)
|
||||
@nucypher_click_config
|
||||
def status(click_config, config_file):
|
||||
"""
|
||||
Echo a snapshot of live network metadata.
|
||||
"""
|
||||
#
|
||||
# Initialize
|
||||
#
|
||||
ursula_config = UrsulaConfiguration.from_configuration_file(filepath=config_file)
|
||||
if not ursula_config.federated_only:
|
||||
ursula_config.connect_to_blockchain(provider_uri=ursula_config.provider_uri)
|
||||
ursula_config.connect_to_contracts()
|
||||
|
||||
# Contracts
|
||||
paint_contract_status(ursula_config=ursula_config, click_config=click_config)
|
||||
|
||||
# Known Nodes
|
||||
paint_known_nodes(ursula=ursula_config)
|
||||
|
||||
|
||||
@nucypher_cli.command()
|
||||
@click.argument('action')
|
||||
@click.option('--debug', '-D', help="Enable debugging mode", is_flag=True)
|
||||
@click.option('--dev', '-d', help="Enable development mode", is_flag=True)
|
||||
@click.option('--force', '-f', help="Don't ask for confirmation", is_flag=True)
|
||||
@click.option('--teacher-uri', help="An Ursula URI to start learning from (seednode)", type=click.STRING)
|
||||
@click.option('--min-stake', help="The minimum stake the teacher must have to be a teacher", type=click.INT, default=0)
|
||||
@click.option('--rest-host', help="The host IP address to run Ursula network services on", type=click.STRING)
|
||||
@click.option('--rest-port', help="The host port to run Ursula network services on", type=UNREGISTERED_PORT)
|
||||
@click.option('--db-filepath', help="The database filepath to connect to", type=click.STRING)
|
||||
@click.option('--checksum-address', help="Run with a specified account", type=EIP55_CHECKSUM_ADDRESS)
|
||||
@click.option('--federated-only', help="Connect only to federated nodes", is_flag=True, default=True)
|
||||
@click.option('--poa', help="Inject POA middleware", is_flag=True)
|
||||
@click.option('--config-root', help="Custom configuration directory", type=click.Path())
|
||||
@click.option('--config-file', help="Path to configuration file", type=EXISTING_READABLE_FILE)
|
||||
@click.option('--metadata-dir', help="Custom known metadata directory", type=EXISTING_WRITABLE_DIRECTORY)
|
||||
@click.option('--provider-uri', help="Blockchain provider's URI", type=click.STRING)
|
||||
@click.option('--no-registry', help="Skip importing the default contract registry", is_flag=True)
|
||||
@click.option('--registry-filepath', help="Custom contract registry filepath", type=EXISTING_READABLE_FILE)
|
||||
@nucypher_click_config
|
||||
def ursula(click_config,
|
||||
action,
|
||||
debug,
|
||||
dev,
|
||||
force,
|
||||
teacher_uri,
|
||||
min_stake,
|
||||
rest_host,
|
||||
rest_port,
|
||||
db_filepath,
|
||||
checksum_address,
|
||||
federated_only,
|
||||
poa,
|
||||
config_root,
|
||||
config_file,
|
||||
metadata_dir, # TODO: Start nodes from an additional existing metadata dir
|
||||
provider_uri,
|
||||
no_registry,
|
||||
registry_filepath
|
||||
) -> None:
|
||||
"""
|
||||
Manage and run an Ursula node.
|
||||
|
||||
\b
|
||||
Actions
|
||||
-------------------------------------------------
|
||||
\b
|
||||
run Run an "Ursula" node.
|
||||
init Create a new Ursula node configuration.
|
||||
view View the Ursula node's configuration.
|
||||
forget Forget all known nodes.
|
||||
save-metadata Manually write node metadata to disk without running
|
||||
destroy Delete Ursula node configuration.
|
||||
|
||||
"""
|
||||
|
||||
#
|
||||
# Boring Setup Stuff
|
||||
#
|
||||
log = Logger('ursula.cli')
|
||||
|
||||
if debug:
|
||||
click_config.log_to_sentry = False
|
||||
click_config.log_to_file = True
|
||||
globalLogPublisher.removeObserver(logToSentry) # Sentry
|
||||
globalLogPublisher.addObserver(SimpleObserver(log_level_name='debug')) # Print
|
||||
|
||||
#
|
||||
# Launch Warnings
|
||||
#
|
||||
if dev:
|
||||
click.secho("WARNING: Running in development mode", fg='yellow')
|
||||
if federated_only:
|
||||
click.secho("WARNING: Running in Federated mode", fg='yellow')
|
||||
if force:
|
||||
click.secho("WARNING: Force is enabled", fg='yellow')
|
||||
|
||||
#
|
||||
# Unauthenticated Configurations
|
||||
#
|
||||
if action == "init":
|
||||
"""Create a brand-new persistent Ursula"""
|
||||
|
||||
if dev:
|
||||
click.secho("WARNING: Using temporary storage area", fg='yellow')
|
||||
|
||||
if not config_root: # Flag
|
||||
config_root = click_config.config_file # Envvar
|
||||
|
||||
if not rest_host:
|
||||
rest_host = click.prompt("Enter Ursula's public-facing IPv4 address")
|
||||
|
||||
ursula_config = UrsulaConfiguration.generate(password=click_config.get_password(confirm=True),
|
||||
config_root=config_root,
|
||||
rest_host=rest_host,
|
||||
rest_port=rest_port,
|
||||
db_filepath=db_filepath,
|
||||
federated_only=federated_only,
|
||||
checksum_public_address=checksum_address,
|
||||
no_registry=federated_only or no_registry,
|
||||
registry_filepath=registry_filepath,
|
||||
provider_uri=provider_uri)
|
||||
|
||||
click.secho("Generated keyring {}".format(ursula_config.keyring_dir), fg='green')
|
||||
click.secho("Saved configuration file {}".format(ursula_config.config_file_location), fg='green')
|
||||
|
||||
# Give the use a suggestion as to what to do next...
|
||||
how_to_run_message = "\nTo run an Ursula node from the default configuration filepath run: \n\n'{}'\n"
|
||||
suggested_command = 'nucypher ursula run'
|
||||
if config_root is not None:
|
||||
config_file_location = os.path.join(config_root, config_file or UrsulaConfiguration.CONFIG_FILENAME)
|
||||
suggested_command += ' --config-file {}'.format(config_file_location)
|
||||
click.secho(how_to_run_message.format(suggested_command), fg='green')
|
||||
return # FIN
|
||||
|
||||
# Development Configuration
|
||||
if dev:
|
||||
ursula_config = UrsulaConfiguration(dev_mode=True,
|
||||
poa=poa,
|
||||
registry_filepath=registry_filepath,
|
||||
provider_uri=provider_uri,
|
||||
checksum_public_address=checksum_address,
|
||||
federated_only=federated_only,
|
||||
rest_host=rest_host,
|
||||
rest_port=rest_port,
|
||||
db_filepath=db_filepath)
|
||||
# Authenticated Configurations
|
||||
else:
|
||||
|
||||
# Restore configuration from file
|
||||
ursula_config = UrsulaConfiguration.from_configuration_file(filepath=config_file
|
||||
# TODO: CLI Overrides for file-based configurations
|
||||
# poa = poa,
|
||||
# registry_filepath = registry_filepath,
|
||||
# provider_uri = provider_uri,
|
||||
# checksum_public_address = checksum_public_address,
|
||||
# federated_only = federated_only,
|
||||
# rest_host = rest_host,
|
||||
# rest_port = rest_port,
|
||||
# db_filepath = db_filepath
|
||||
)
|
||||
|
||||
try: # Unlock Keyring
|
||||
# ursula_config.attach_keyring()
|
||||
click.secho('Decrypting keyring...', fg='blue')
|
||||
ursula_config.keyring.unlock(password=click_config.get_password()) # Takes ~3 seconds, ~1GB Ram
|
||||
except CryptoError:
|
||||
raise ursula_config.keyring.AuthenticationFailed
|
||||
|
||||
click_config.ursula_config = ursula_config # Pass Ursula's config onto staking sub-command
|
||||
|
||||
#
|
||||
# Action Switch
|
||||
#
|
||||
if action == 'run':
|
||||
"""Seed, Produce, Run!"""
|
||||
|
||||
#
|
||||
# Seed - Step 1
|
||||
#
|
||||
teacher_nodes = list()
|
||||
if teacher_uri:
|
||||
node = Ursula.from_teacher_uri(teacher_uri=teacher_uri, min_stake=min_stake, federated_only=federated_only)
|
||||
teacher_nodes.append(node)
|
||||
|
||||
#
|
||||
# Produce - Step 2
|
||||
#
|
||||
ursula = ursula_config.produce(known_nodes=teacher_nodes)
|
||||
ursula_config.log.debug("Initialized Ursula {}".format(ursula), fg='green')
|
||||
|
||||
# GO!
|
||||
try:
|
||||
|
||||
#
|
||||
# Run - Step 3
|
||||
#
|
||||
click.secho("Running Ursula on {}".format(ursula.rest_interface), fg='green', bold=True)
|
||||
if not debug:
|
||||
stdio.StandardIO(UrsulaCommandProtocol(ursula=ursula))
|
||||
ursula.get_deployer().run()
|
||||
|
||||
except Exception as e:
|
||||
ursula_config.log.critical(str(e))
|
||||
click.secho("{} {}".format(e.__class__.__name__, str(e)), fg='red')
|
||||
raise # Crash :-(
|
||||
|
||||
finally:
|
||||
click.secho("Stopping Ursula")
|
||||
ursula_config.cleanup()
|
||||
click.secho("Ursula Stopped", fg='red')
|
||||
return
|
||||
|
||||
elif action == "save-metadata":
|
||||
"""Manually save a node self-metadata file"""
|
||||
|
||||
ursula = ursula_config.produce(ursula_config=ursula_config)
|
||||
metadata_path = ursula.write_node_metadata(node=ursula)
|
||||
click.secho("Successfully saved node metadata to {}.".format(metadata_path), fg='green')
|
||||
return
|
||||
|
||||
elif action == "view":
|
||||
"""Paint an existing configuration to the console"""
|
||||
|
||||
paint_configuration(config_filepath=config_file or ursula_config.config_file_location)
|
||||
return
|
||||
|
||||
elif action == "forget":
|
||||
"""Forget all known nodes via storages"""
|
||||
|
||||
click.confirm("Permanently delete all known node data?", abort=True)
|
||||
ursula_config.forget_nodes()
|
||||
message = "Removed all stored node node metadata and certificates"
|
||||
click.secho(message=message, fg='red')
|
||||
return
|
||||
|
||||
elif action == "destroy":
|
||||
"""Delete all configuration files from the disk"""
|
||||
|
||||
if not force:
|
||||
click.confirm('''
|
||||
*Permanently and irreversibly delete all* nucypher files including:
|
||||
- Private and Public Keys
|
||||
- Known Nodes
|
||||
- TLS certificates
|
||||
- Node Configurations
|
||||
- Log Files
|
||||
|
||||
Delete {}?'''.format(ursula_config.config_root), abort=True)
|
||||
|
||||
try:
|
||||
ursula_config.destroy(force=force)
|
||||
except FileNotFoundError:
|
||||
message = 'Failed: No nucypher files found at {}'.format(ursula_config.config_root)
|
||||
click.secho(message, fg='red')
|
||||
log.debug(message)
|
||||
raise click.Abort()
|
||||
else:
|
||||
message = "Deleted configuration files at {}".format(ursula_config.config_root)
|
||||
click.secho(message, fg='green')
|
||||
log.debug(message)
|
||||
return
|
||||
|
||||
else:
|
||||
raise click.BadArgumentUsage("No such argument {}".format(action))
|
||||
|
||||
|
||||
@click.argument('action', default='list', required=False)
|
||||
@click.option('--checksum-address', type=EIP55_CHECKSUM_ADDRESS)
|
||||
@click.option('--value', help="Token value of stake", type=STAKE_VALUE)
|
||||
@click.option('--duration', help="Period duration of stake", type=STAKE_DURATION)
|
||||
@click.option('--index', help="A specific stake index to resume", type=click.INT)
|
||||
@nucypher_click_config
|
||||
def stake(click_config,
|
||||
action,
|
||||
checksum_address,
|
||||
index,
|
||||
value,
|
||||
duration):
|
||||
"""
|
||||
Manage token staking. TODO
|
||||
|
||||
\b
|
||||
Actions
|
||||
-------------------------------------------------
|
||||
\b
|
||||
list List all stakes for this node.
|
||||
init Stage a new stake.
|
||||
confirm-activity Manually confirm-activity for the current period.
|
||||
divide Divide an existing stake.
|
||||
collect-reward Withdraw staking reward.
|
||||
|
||||
"""
|
||||
ursula_config = click_config.ursula_config
|
||||
|
||||
#
|
||||
# Initialize
|
||||
#
|
||||
if not ursula_config.federated_only:
|
||||
ursula_config.connect_to_blockchain(click_config)
|
||||
ursula_config.connect_to_contracts(click_config)
|
||||
|
||||
if not checksum_address:
|
||||
|
||||
if click_config.accounts == NO_BLOCKCHAIN_CONNECTION:
|
||||
click.echo('No account found.')
|
||||
raise click.Abort()
|
||||
|
||||
for index, address in enumerate(click_config.accounts):
|
||||
if index == 0:
|
||||
row = 'etherbase (0) | {}'.format(address)
|
||||
else:
|
||||
row = '{} .......... | {}'.format(index, address)
|
||||
click.echo(row)
|
||||
|
||||
click.echo("Select ethereum address")
|
||||
account_selection = click.prompt("Enter 0-{}".format(len(ur.accounts)), type=click.INT)
|
||||
address = click_config.accounts[account_selection]
|
||||
|
||||
if action == 'list':
|
||||
live_stakes = ursula_config.miner_agent.get_all_stakes(miner_address=checksum_address)
|
||||
for index, stake_info in enumerate(live_stakes):
|
||||
row = '{} | {}'.format(index, stake_info)
|
||||
click.echo(row)
|
||||
|
||||
elif action == 'init':
|
||||
click.confirm("Stage a new stake?", abort=True)
|
||||
|
||||
live_stakes = ursula_config.miner_agent.get_all_stakes(miner_address=checksum_address)
|
||||
if len(live_stakes) > 0:
|
||||
raise RuntimeError("There is an existing stake for {}".format(checksum_address))
|
||||
|
||||
# Value
|
||||
balance = ursula_config.miner_agent.token_agent.get_balance(address=checksum_address)
|
||||
click.echo("Current balance: {}".format(balance))
|
||||
value = click.prompt("Enter stake value", type=click.INT)
|
||||
|
||||
# Duration
|
||||
message = "Minimum duration: {} | Maximum Duration: {}".format(MIN_LOCKED_PERIODS, MAX_MINTING_PERIODS)
|
||||
click.echo(message)
|
||||
duration = click.prompt("Enter stake duration in periods (1 Period = 24 Hours)", type=click.INT)
|
||||
|
||||
start_period = ursula_config.miner_agent.get_current_period()
|
||||
end_period = start_period + duration
|
||||
|
||||
# Review
|
||||
click.echo("""
|
||||
|
||||
| Staged Stake |
|
||||
|
||||
Node: {address}
|
||||
Value: {value}
|
||||
Duration: {duration}
|
||||
Start Period: {start_period}
|
||||
End Period: {end_period}
|
||||
|
||||
""".format(address=checksum_address,
|
||||
value=value,
|
||||
duration=duration,
|
||||
start_period=start_period,
|
||||
end_period=end_period))
|
||||
|
||||
raise NotImplementedError
|
||||
|
||||
elif action == 'confirm-activity':
|
||||
"""Manually confirm activity for the active period"""
|
||||
stakes = ursula_config.miner_agent.get_all_stakes(miner_address=checksum_address)
|
||||
if len(stakes) == 0:
|
||||
raise RuntimeError("There are no active stakes for {}".format(checksum_address))
|
||||
ursula_config.miner_agent.confirm_activity(node_address=checksum_address)
|
||||
|
||||
elif action == 'divide':
|
||||
"""Divide an existing stake by specifying the new target value and end period"""
|
||||
|
||||
stakes = ursula_config.miner_agent.get_all_stakes(miner_address=checksum_address)
|
||||
if len(stakes) == 0:
|
||||
raise RuntimeError("There are no active stakes for {}".format(checksum_address))
|
||||
|
||||
if not index:
|
||||
for selection_index, stake_info in enumerate(stakes):
|
||||
click.echo("{} ....... {}".format(selection_index, stake_info))
|
||||
index = click.prompt("Select a stake to divide", type=click.INT)
|
||||
|
||||
target_value = click.prompt("Enter new target value", type=click.INT)
|
||||
extension = click.prompt("Enter number of periods to extend", type=click.INT)
|
||||
|
||||
click.echo("""
|
||||
Current Stake: {}
|
||||
|
||||
New target value {}
|
||||
New end period: {}
|
||||
|
||||
""".format(stakes[index],
|
||||
target_value,
|
||||
target_value + extension))
|
||||
|
||||
click.confirm("Is this correct?", abort=True)
|
||||
ursula_config.miner_agent.divide_stake(miner_address=checksum_address,
|
||||
stake_index=index,
|
||||
value=value,
|
||||
periods=extension)
|
||||
|
||||
elif action == 'collect-reward': # TODO: Implement
|
||||
"""Withdraw staking reward to the specified wallet address"""
|
||||
# click.confirm("Send {} to {}?".format)
|
||||
# ursula_config.miner_agent.collect_staking_reward(collector_address=address)
|
||||
raise NotImplementedError
|
||||
|
||||
else:
|
||||
raise click.BadArgumentUsage("No such argument {}".format(action))
|
|
@ -0,0 +1,184 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU 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 General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
|
||||
import click
|
||||
import maya
|
||||
|
||||
import nucypher
|
||||
from constant_sorrow.constants import NO_KNOWN_NODES
|
||||
from nucypher.config.characters import UrsulaConfiguration
|
||||
from nucypher.config.constants import SEEDNODES
|
||||
|
||||
#
|
||||
# Art
|
||||
#
|
||||
|
||||
BANNER = """
|
||||
_
|
||||
| |
|
||||
_ __ _ _ ___ _ _ _ __ | |__ ___ _ __
|
||||
| '_ \| | | |/ __| | | | '_ \| '_ \ / _ \ '__|
|
||||
| | | | |_| | (__| |_| | |_) | | | | __/ |
|
||||
|_| |_|\__,_|\___|\__, | .__/|_| |_|\___|_|
|
||||
__/ | |
|
||||
|___/|_|
|
||||
|
||||
version {}
|
||||
|
||||
""".format(nucypher.__version__)
|
||||
|
||||
|
||||
#
|
||||
# Paint
|
||||
#
|
||||
|
||||
def build_fleet_state_status(ursula) -> str:
|
||||
# Build FleetState status line
|
||||
if ursula.known_nodes.checksum is not NO_KNOWN_NODES:
|
||||
fleet_state_checksum = ursula.known_nodes.checksum[:7]
|
||||
fleet_state_nickname = ursula.known_nodes.nickname
|
||||
fleet_state_icon = ursula.known_nodes.icon
|
||||
fleet_state = '{checksum} ⇀{nickname}↽ {icon}'.format(icon=fleet_state_icon,
|
||||
nickname=fleet_state_nickname,
|
||||
checksum=fleet_state_checksum)
|
||||
elif ursula.known_nodes.checksum is not NO_KNOWN_NODES:
|
||||
fleet_state = 'No Known Nodes'
|
||||
else:
|
||||
fleet_state = 'Unknown'
|
||||
|
||||
return fleet_state
|
||||
|
||||
|
||||
def paint_configuration(config_filepath: str) -> None:
|
||||
json_config = UrsulaConfiguration._read_configuration_file(filepath=config_filepath)
|
||||
click.secho("\n======== Ursula Configuration ======== \n", bold=True)
|
||||
for key, value in json_config.items():
|
||||
click.secho("{} = {}".format(key, value))
|
||||
|
||||
|
||||
def paint_node_status(ursula, start_time):
|
||||
|
||||
# Build Learning status line
|
||||
learning_status = "Unknown"
|
||||
if ursula._learning_task.running:
|
||||
learning_status = "Learning at {}s Intervals".format(ursula._learning_task.interval)
|
||||
elif not ursula._learning_task.running:
|
||||
learning_status = "Not Learning"
|
||||
|
||||
teacher = 'Current Teacher ..... No Teacher Connection'
|
||||
if ursula._current_teacher_node:
|
||||
teacher = 'Current Teacher ..... {}'.format(ursula._current_teacher_node)
|
||||
|
||||
# Build FleetState status line
|
||||
fleet_state = build_fleet_state_status(ursula=ursula)
|
||||
|
||||
stats = ['⇀URSULA {}↽'.format(ursula.nickname_icon),
|
||||
'{}'.format(ursula),
|
||||
'Uptime .............. {}'.format(maya.now() - start_time),
|
||||
'Start Time .......... {}'.format(start_time.slang_time()),
|
||||
'Fleet State.......... {}'.format(fleet_state),
|
||||
'Learning Status ..... {}'.format(learning_status),
|
||||
'Learning Round ...... Round #{}'.format(ursula._learning_round),
|
||||
'Operating Mode ...... {}'.format('Federated' if ursula.federated_only else 'Decentralized'),
|
||||
'Rest Interface ...... {}'.format(ursula.rest_url()),
|
||||
'Node Storage Type ... {}'.format(ursula.node_storage._name.capitalize()),
|
||||
'Known Nodes ......... {}'.format(len(ursula.known_nodes)),
|
||||
'Work Orders ......... {}'.format(len(ursula._work_orders)),
|
||||
teacher]
|
||||
|
||||
click.echo('\n' + '\n'.join(stats) + '\n')
|
||||
|
||||
|
||||
def paint_known_nodes(ursula) -> None:
|
||||
# Gather Data
|
||||
known_nodes = ursula.known_nodes
|
||||
number_of_known_nodes = len(ursula.node_storage.all(federated_only=ursula.federated_only))
|
||||
seen_nodes = len(ursula.node_storage.all(federated_only=ursula.federated_only, certificates_only=True))
|
||||
|
||||
# Operating Mode
|
||||
federated_only = ursula.federated_only
|
||||
if federated_only:
|
||||
click.secho("Configured in Federated Only mode", fg='green')
|
||||
|
||||
# Heading
|
||||
label = "Known Nodes (connected {} / seen {})".format(number_of_known_nodes, seen_nodes)
|
||||
heading = '\n' + label + " " * (45 - len(label))
|
||||
click.secho(heading, bold=True, nl=True)
|
||||
|
||||
# Build FleetState status line
|
||||
fleet_state = build_fleet_state_status(ursula=ursula)
|
||||
fleet_status_line = 'Fleet State {}'.format(fleet_state)
|
||||
click.secho(fleet_status_line, fg='blue', bold=True, nl=True)
|
||||
|
||||
# Legend
|
||||
color_index = {
|
||||
'self': 'yellow',
|
||||
'known': 'white',
|
||||
'seednode': 'blue'
|
||||
}
|
||||
|
||||
# Ledgend
|
||||
# for node_type, color in color_index.items():
|
||||
# click.secho('{0:<6} | '.format(node_type), fg=color, nl=False)
|
||||
# click.echo('\n')
|
||||
|
||||
seednode_addresses = list(bn.checksum_address for bn in SEEDNODES)
|
||||
|
||||
for node in known_nodes:
|
||||
row_template = "{} | {}"
|
||||
node_type = 'known'
|
||||
if node.checksum_public_address == ursula.checksum_public_address:
|
||||
node_type = 'self'
|
||||
row_template += ' ({})'.format(node_type)
|
||||
elif node.checksum_public_address in seednode_addresses:
|
||||
node_type = 'seednode'
|
||||
row_template += ' ({})'.format(node_type)
|
||||
click.secho(row_template.format(node.rest_url().ljust(20), node), fg=color_index[node_type])
|
||||
|
||||
|
||||
def paint_contract_status(ursula_config, click_config):
|
||||
contract_payload = """
|
||||
|
||||
| NuCypher ETH Contracts |
|
||||
|
||||
Provider URI ............. {provider_uri}
|
||||
Registry Path ............ {registry_filepath}
|
||||
|
||||
NucypherToken ............ {token}
|
||||
MinerEscrow .............. {escrow}
|
||||
PolicyManager ............ {manager}
|
||||
|
||||
""".format(provider_uri=ursula_config.blockchain.interface.provider_uri,
|
||||
registry_filepath=ursula_config.blockchain.interface.registry.filepath,
|
||||
token=ursula_config.token_agent.contract_address,
|
||||
escrow=ursula_config.miner_agent.contract_address,
|
||||
manager=ursula_config.policy_agent.contract_address,
|
||||
period=ursula_config.miner_agent.get_current_period())
|
||||
click.secho(contract_payload)
|
||||
|
||||
network_payload = """
|
||||
| Blockchain Network |
|
||||
|
||||
Current Period ........... {period}
|
||||
Gas Price ................ {gas_price}
|
||||
Active Staking Ursulas ... {ursulas}
|
||||
|
||||
""".format(period=click_config.miner_agent.get_current_period(),
|
||||
gas_price=click_config.blockchain.interface.w3.eth.gasPrice,
|
||||
ursulas=click_config.miner_agent.get_miner_population())
|
||||
click.secho(network_payload)
|
|
@ -0,0 +1,110 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU 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 General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
|
||||
import os
|
||||
from collections import deque
|
||||
|
||||
import click
|
||||
import maya
|
||||
from twisted.internet import reactor
|
||||
from twisted.protocols.basic import LineReceiver
|
||||
|
||||
from nucypher.cli.painting import build_fleet_state_status
|
||||
|
||||
|
||||
class UrsulaCommandProtocol(LineReceiver):
|
||||
|
||||
encoding = 'utf-8'
|
||||
delimiter = os.linesep.encode(encoding=encoding)
|
||||
|
||||
def __init__(self, ursula):
|
||||
self.ursula = ursula
|
||||
self.start_time = maya.now()
|
||||
|
||||
self.__history = deque(maxlen=10)
|
||||
self.prompt = bytes('Ursula({}) >>> '.format(self.ursula.checksum_public_address[:9]), encoding='utf-8')
|
||||
|
||||
# Expose Ursula functional entry points
|
||||
self.__commands = {
|
||||
|
||||
# Status
|
||||
'status': self.paintStatus,
|
||||
'known_nodes': self.paintKnownNodes,
|
||||
'fleet_state': self.paintFleetState,
|
||||
|
||||
# Learning Control
|
||||
'cycle_teacher': self.ursula.cycle_teacher_node,
|
||||
'start_learning': self.ursula.start_learning_loop,
|
||||
'stop_learning': self.ursula.stop_learning_loop,
|
||||
|
||||
# Process Control
|
||||
'stop': reactor.stop,
|
||||
|
||||
}
|
||||
|
||||
super().__init__()
|
||||
|
||||
@property
|
||||
def commands(self):
|
||||
return self.__commands.keys()
|
||||
|
||||
def paintKnownNodes(self):
|
||||
from nucypher.cli.painting import paint_known_nodes
|
||||
paint_known_nodes(ursula=self.ursula)
|
||||
|
||||
def paintStatus(self):
|
||||
from nucypher.cli.painting import paint_node_status
|
||||
paint_node_status(ursula=self.ursula, start_time=self.start_time)
|
||||
|
||||
def paintFleetState(self):
|
||||
line = '{}'.format(build_fleet_state_status(ursula=self.ursula))
|
||||
click.secho(line)
|
||||
|
||||
def connectionMade(self):
|
||||
|
||||
message = 'Attached {}@{}'.format(
|
||||
self.ursula.checksum_public_address,
|
||||
self.ursula.rest_url())
|
||||
|
||||
click.secho(message, fg='green')
|
||||
click.secho('{} | {}'.format(self.ursula.nickname_icon, self.ursula.nickname), fg='blue', bold=True)
|
||||
|
||||
click.secho("\nType 'help' or '?' for help")
|
||||
self.transport.write(self.prompt)
|
||||
|
||||
def lineReceived(self, line):
|
||||
"""Ursula Console REPL"""
|
||||
|
||||
# Read
|
||||
raw_line = line.decode(encoding=self.encoding)
|
||||
line = raw_line.strip().lower()
|
||||
|
||||
# Evaluate
|
||||
try:
|
||||
self.__commands[line]()
|
||||
|
||||
# Print
|
||||
except KeyError:
|
||||
if line: # allow for empty string
|
||||
click.secho("Invalid input. Options are {}".format(', '.join(self.__commands.keys())))
|
||||
|
||||
else:
|
||||
self.__history.append(raw_line)
|
||||
|
||||
# Loop
|
||||
self.transport.write(self.prompt)
|
|
@ -0,0 +1,54 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU 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 General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
from ipaddress import ip_address
|
||||
|
||||
import click
|
||||
from eth_utils import is_checksum_address
|
||||
|
||||
from nucypher.blockchain.eth.constants import MIN_ALLOWED_LOCKED, MAX_MINTING_PERIODS, MIN_LOCKED_PERIODS, \
|
||||
MAX_ALLOWED_LOCKED
|
||||
|
||||
|
||||
class ChecksumAddress(click.ParamType):
|
||||
name = 'checksum_public_address'
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
if is_checksum_address(value):
|
||||
return value
|
||||
self.fail('{} is not a valid EIP-55 checksum address'.format(value, param, ctx))
|
||||
|
||||
|
||||
class IPv4Address(click.ParamType):
|
||||
name = 'ipv4_address'
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
try:
|
||||
_address = ip_address(value)
|
||||
except ValueError as e:
|
||||
self.fail(str(e))
|
||||
else:
|
||||
return value
|
||||
|
||||
|
||||
STAKE_DURATION = click.IntRange(min=MIN_LOCKED_PERIODS, max=MAX_MINTING_PERIODS, clamp=False)
|
||||
STAKE_VALUE = click.IntRange(min=MIN_ALLOWED_LOCKED, max=MAX_ALLOWED_LOCKED, clamp=False)
|
||||
EXISTING_WRITABLE_DIRECTORY = click.Path(exists=True, dir_okay=True, file_okay=False, writable=True)
|
||||
EXISTING_READABLE_FILE = click.Path(exists=True, dir_okay=False, file_okay=True, readable=True)
|
||||
UNREGISTERED_PORT = click.IntRange(min=49151, max=65535, clamp=False)
|
||||
IPV4_ADDRESS = IPv4Address()
|
||||
EIP55_CHECKSUM_ADDRESS = ChecksumAddress()
|
|
@ -14,134 +14,88 @@ GNU General Public License for more details.
|
|||
You should have received a copy of the GNU General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
|
||||
import os
|
||||
|
||||
from constant_sorrow import constants
|
||||
from cryptography.hazmat.primitives.asymmetric import ec
|
||||
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurve
|
||||
from cryptography.x509 import Certificate
|
||||
from web3.middleware import geth_poa_middleware
|
||||
|
||||
from constant_sorrow.constants import (
|
||||
UNINITIALIZED_CONFIGURATION,
|
||||
NO_KEYRING_ATTACHED
|
||||
)
|
||||
from nucypher.blockchain.eth.agents import NucypherTokenAgent, MinerAgent
|
||||
from nucypher.blockchain.eth.chains import Blockchain
|
||||
from nucypher.config.constants import DEFAULT_CONFIG_ROOT
|
||||
from nucypher.config.node import NodeConfiguration
|
||||
from nucypher.crypto.powers import CryptoPower
|
||||
|
||||
|
||||
class UrsulaConfiguration(NodeConfiguration):
|
||||
from nucypher.characters.lawful import Ursula
|
||||
|
||||
_character_class = Ursula
|
||||
_name = 'ursula'
|
||||
DEFAULT_CONFIG_FILE_LOCATION = os.path.join(DEFAULT_CONFIG_ROOT, '{}.config'.format(_name))
|
||||
DEFAULT_REST_HOST = '127.0.0.1'
|
||||
DEFAULT_REST_PORT = 9151
|
||||
_CHARACTER_CLASS = Ursula
|
||||
_NAME = 'ursula'
|
||||
|
||||
__DB_TEMPLATE = "ursula.{port}.db"
|
||||
DEFAULT_DB_NAME = __DB_TEMPLATE.format(port=DEFAULT_REST_PORT)
|
||||
|
||||
__DEFAULT_TLS_CURVE = ec.SECP384R1
|
||||
CONFIG_FILENAME = '{}.config'.format(_NAME)
|
||||
DEFAULT_CONFIG_FILE_LOCATION = os.path.join(DEFAULT_CONFIG_ROOT, CONFIG_FILENAME)
|
||||
DEFAULT_DB_NAME = '{}.db'.format(_NAME)
|
||||
|
||||
def __init__(self,
|
||||
rest_host: str = None,
|
||||
rest_port: int = None,
|
||||
|
||||
# TLS
|
||||
tls_curve: EllipticCurve = None,
|
||||
certificate: Certificate = None,
|
||||
certificate_filepath: str = None,
|
||||
|
||||
# Ursula
|
||||
db_name: str = None,
|
||||
dev_mode: bool = False,
|
||||
db_filepath: str = None,
|
||||
interface_signature=None,
|
||||
crypto_power: CryptoPower = None,
|
||||
|
||||
# Blockchain
|
||||
poa: bool = False,
|
||||
provider_uri: str = None,
|
||||
|
||||
*args, **kwargs
|
||||
) -> None:
|
||||
|
||||
# REST
|
||||
self.rest_host = rest_host or self.DEFAULT_REST_HOST
|
||||
self.rest_port = rest_port or self.DEFAULT_REST_PORT
|
||||
|
||||
self.db_name = db_name or self.__DB_TEMPLATE.format(port=self.rest_port)
|
||||
self.db_filepath = db_filepath or constants.UNINITIALIZED_CONFIGURATION
|
||||
|
||||
#
|
||||
# TLS
|
||||
#
|
||||
self.tls_curve = tls_curve or self.__DEFAULT_TLS_CURVE
|
||||
self.certificate = certificate
|
||||
self.certificate_filepath = certificate_filepath
|
||||
|
||||
# Ursula
|
||||
self.interface_signature = interface_signature
|
||||
self.crypto_power = crypto_power
|
||||
|
||||
#
|
||||
# Blockchain
|
||||
#
|
||||
self.poa = poa
|
||||
self.provider_uri = provider_uri
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
*args, **kwargs) -> None:
|
||||
if dev_mode is True:
|
||||
db_filepath = ':memory:' # sqlite in-memory db
|
||||
self.db_filepath = db_filepath or UNINITIALIZED_CONFIGURATION
|
||||
super().__init__(dev_mode=dev_mode, *args, **kwargs)
|
||||
|
||||
def generate_runtime_filepaths(self, config_root: str) -> dict:
|
||||
base_filepaths = NodeConfiguration.generate_runtime_filepaths(config_root=config_root)
|
||||
filepaths = dict(db_filepath=os.path.join(config_root, self.db_name))
|
||||
base_filepaths = super().generate_runtime_filepaths(config_root=config_root)
|
||||
filepaths = dict(db_filepath=os.path.join(config_root, self.DEFAULT_DB_NAME))
|
||||
base_filepaths.update(filepaths)
|
||||
return base_filepaths
|
||||
|
||||
def initialize(self, tls: bool = True, host=None, *args, **kwargs):
|
||||
super().initialize(tls=tls, host=host or self.rest_host, curve=self.tls_curve, *args, **kwargs)
|
||||
if self.db_name is constants.UNINITIALIZED_CONFIGURATION:
|
||||
self.db_name = self.__DB_TEMPLATE.format(self.rest_port)
|
||||
if self.db_filepath is constants.UNINITIALIZED_CONFIGURATION:
|
||||
self.db_filepath = os.path.join(self.config_root, self.db_name)
|
||||
|
||||
@property
|
||||
def static_payload(self) -> dict:
|
||||
payload = dict(
|
||||
rest_host=self.rest_host,
|
||||
rest_port=self.rest_port,
|
||||
db_name=self.db_name,
|
||||
db_filepath=self.db_filepath,
|
||||
)
|
||||
if not self.temp:
|
||||
certificate_filepath = self.certificate_filepath or self.keyring.certificate_filepath
|
||||
payload.update(dict(certificate_filepath=certificate_filepath))
|
||||
return {**super().static_payload, **payload}
|
||||
|
||||
@property
|
||||
def dynamic_payload(self) -> dict:
|
||||
payload = dict(
|
||||
network_middleware=self.network_middleware,
|
||||
tls_curve=self.tls_curve, # TODO: Needs to be in static payload with mapping
|
||||
tls_curve=self.tls_curve, # TODO: Needs to be in static payload with [str -> curve] mapping
|
||||
certificate=self.certificate,
|
||||
interface_signature=self.interface_signature,
|
||||
timestamp=None,
|
||||
)
|
||||
return {**super().dynamic_payload, **payload}
|
||||
|
||||
def produce(self, passphrase: str = None, **overrides):
|
||||
def produce(self, **overrides):
|
||||
"""Produce a new Ursula from configuration"""
|
||||
|
||||
if not self.temp:
|
||||
self.read_keyring()
|
||||
self.keyring.unlock(passphrase=passphrase)
|
||||
|
||||
# Build a merged dict of Ursula parameters
|
||||
merged_parameters = {**self.static_payload, **self.dynamic_payload, **overrides}
|
||||
|
||||
#
|
||||
# Pre-Init
|
||||
#
|
||||
|
||||
# Verify the configuration file refers to the same configuration root as this instance
|
||||
config_root_from_config_file = merged_parameters.pop('config_root')
|
||||
if config_root_from_config_file != self.config_root:
|
||||
message = "Configuration root mismatch {} and {}.".format(config_root_from_config_file, self.config_root)
|
||||
raise self.ConfigurationError(message)
|
||||
|
||||
if self.federated_only is False:
|
||||
|
||||
self.blockchain = Blockchain.connect(provider_uri=self.provider_uri)
|
||||
|
||||
if self.poa: # TODO: move this..?
|
||||
if self.poa:
|
||||
w3 = self.miner_agent.blockchain.interface.w3
|
||||
w3.middleware_stack.inject(geth_poa_middleware, layer=0)
|
||||
|
||||
|
@ -149,9 +103,15 @@ class UrsulaConfiguration(NodeConfiguration):
|
|||
self.miner_agent = MinerAgent(blockchain=self.blockchain)
|
||||
merged_parameters.update(blockchain=self.blockchain)
|
||||
|
||||
ursula = self._character_class(**merged_parameters)
|
||||
#
|
||||
# Init
|
||||
#
|
||||
ursula = self._CHARACTER_CLASS(**merged_parameters)
|
||||
|
||||
if self.temp: # TODO: Move this..?
|
||||
#
|
||||
# Post-Init
|
||||
#
|
||||
if self.dev_mode:
|
||||
class MockDatastoreThreadPool(object):
|
||||
def callInThread(self, f, *args, **kwargs):
|
||||
return f(*args, **kwargs)
|
||||
|
@ -159,15 +119,33 @@ class UrsulaConfiguration(NodeConfiguration):
|
|||
|
||||
return ursula
|
||||
|
||||
def __write(self, password: str, no_registry: bool):
|
||||
_new_installation_path = self.initialize(password=password, import_registry=no_registry)
|
||||
_configuration_filepath = self.to_configuration_file(filepath=self.config_file_location)
|
||||
|
||||
@classmethod
|
||||
def generate(cls, password: str, no_registry: bool, *args, **kwargs) -> 'UrsulaConfiguration':
|
||||
"""Hook-up a new initial installation and write configuration file to the disk"""
|
||||
ursula_config = cls(dev_mode=False, is_me=True, *args, **kwargs)
|
||||
ursula_config.__write(password=password, no_registry=no_registry)
|
||||
return ursula_config
|
||||
|
||||
|
||||
class AliceConfiguration(NodeConfiguration):
|
||||
from nucypher.characters.lawful import Alice
|
||||
|
||||
_character_class = Alice
|
||||
_name = 'alice'
|
||||
_CHARACTER_CLASS = Alice
|
||||
_NAME = 'alice'
|
||||
|
||||
CONFIG_FILENAME = '{}.config'.format(_NAME)
|
||||
DEFAULT_CONFIG_FILE_LOCATION = os.path.join(DEFAULT_CONFIG_ROOT, CONFIG_FILENAME)
|
||||
|
||||
|
||||
class BobConfiguration(NodeConfiguration):
|
||||
from nucypher.characters.lawful import Bob
|
||||
_character_class = Bob
|
||||
_name = 'bob'
|
||||
|
||||
_CHARACTER_CLASS = Bob
|
||||
_NAME = 'bob'
|
||||
|
||||
CONFIG_FILENAME = '{}.config'.format(_NAME)
|
||||
DEFAULT_CONFIG_FILE_LOCATION = os.path.join(DEFAULT_CONFIG_ROOT, CONFIG_FILENAME)
|
||||
|
|
|
@ -34,5 +34,5 @@ DEFAULT_CONFIG_ROOT = APP_DIR.user_data_dir
|
|||
USER_LOG_DIR = APP_DIR.user_log_dir
|
||||
|
||||
# Static Seednodes
|
||||
SeednodeMetadata = namedtuple('seednode', ['checksum_address', 'rest_host', 'rest_port'])
|
||||
SeednodeMetadata = namedtuple('seednode', ['checksum_public_address', 'rest_host', 'rest_port'])
|
||||
SEEDNODES = tuple()
|
||||
|
|
|
@ -19,6 +19,8 @@ import json
|
|||
import os
|
||||
import stat
|
||||
from json import JSONDecodeError
|
||||
|
||||
from cryptography.hazmat.primitives.asymmetric import ec
|
||||
from typing import ClassVar, Tuple, Callable, Union, Dict
|
||||
|
||||
from constant_sorrow import constants
|
||||
|
@ -188,9 +190,7 @@ def _read_tls_public_certificate(filepath: str) -> Certificate:
|
|||
# Encrypt and Decrypt
|
||||
#
|
||||
|
||||
def _derive_key_material_from_passphrase(salt: bytes,
|
||||
passphrase: str
|
||||
) -> bytes:
|
||||
def _derive_key_material_from_password(salt: bytes, password: str) -> bytes:
|
||||
"""
|
||||
Uses Scrypt derivation to derive a key for encrypting key material.
|
||||
See RFC 7914 for n, r, and p value selections.
|
||||
|
@ -204,7 +204,7 @@ def _derive_key_material_from_passphrase(salt: bytes,
|
|||
r=8,
|
||||
p=1,
|
||||
backend=default_backend()
|
||||
).derive(passphrase.encode())
|
||||
).derive(password.encode())
|
||||
except InternalError as e:
|
||||
# OpenSSL Attempts to malloc 1 GB of mem for scrypt key derivation
|
||||
if e.err_code[0].reason == 65:
|
||||
|
@ -278,10 +278,10 @@ def _generate_signing_keys() -> Tuple[UmbralPrivateKey, UmbralPublicKey]:
|
|||
return privkey, pubkey
|
||||
|
||||
|
||||
def _generate_wallet(passphrase: str) -> Tuple[str, dict]:
|
||||
"""Create a new wallet address and private "transacting" key encrypted with the passphrase"""
|
||||
def _generate_wallet(password: str) -> Tuple[str, dict]:
|
||||
"""Create a new wallet address and private "transacting" key encrypted with the password"""
|
||||
account = Account.create(extra_entropy=os.urandom(32)) # max out entropy for keccak256
|
||||
encrypted_wallet_data = Account.encrypt(private_key=account.privateKey, password=passphrase)
|
||||
encrypted_wallet_data = Account.encrypt(private_key=account.privateKey, password=password)
|
||||
return account.address, encrypted_wallet_data
|
||||
|
||||
|
||||
|
@ -358,6 +358,7 @@ class NucypherKeyring:
|
|||
|
||||
__default_keyring_root = os.path.join(DEFAULT_CONFIG_ROOT, 'keyring')
|
||||
_private_key_serializer = _PrivateKeySerializer()
|
||||
__DEFAULT_TLS_CURVE = ec.SECP384R1
|
||||
|
||||
class KeyringError(Exception):
|
||||
pass
|
||||
|
@ -365,7 +366,7 @@ class NucypherKeyring:
|
|||
class KeyringLocked(KeyringError):
|
||||
pass
|
||||
|
||||
class InvalidPassphrase(KeyringError):
|
||||
class AuthenticationFailed(KeyringError):
|
||||
pass
|
||||
|
||||
def __init__(self,
|
||||
|
@ -422,7 +423,7 @@ class NucypherKeyring:
|
|||
@property
|
||||
def checksum_address(self) -> str:
|
||||
key_data = _read_keyfile(keypath=self.__wallet_path, deserializer=None)
|
||||
# TODO Json joads
|
||||
# TODO Json joads # TODO: what is this TODO?
|
||||
address = key_data['address']
|
||||
return to_checksum_address(address)
|
||||
|
||||
|
@ -477,12 +478,12 @@ class NucypherKeyring:
|
|||
|
||||
return __key_filepaths
|
||||
|
||||
def _export_wallet_to_node(self, blockchain, passphrase): # TODO: Deprecate with geth.parity signing EIPs
|
||||
"""Decrypt the wallet with a passphrase, then import the key to the nodes's keyring over RPC"""
|
||||
def _export_wallet_to_node(self, blockchain, password): # TODO: Deprecate with geth.parity signing EIPs
|
||||
"""Decrypt the wallet with a password, then import the key to the nodes's keyring over RPC"""
|
||||
with open(self.__wallet_path, 'rb') as wallet:
|
||||
data = wallet.read().decode(FILE_ENCODING)
|
||||
account = Account.decrypt(keyfile_json=data, password=passphrase)
|
||||
blockchain.interface.w3.personal.importRawKey(private_key=account, passphrase=passphrase)
|
||||
account = Account.decrypt(keyfile_json=data, password=password)
|
||||
blockchain.interface.w3.personal.importRawKey(private_key=account, password=password)
|
||||
|
||||
@unlock_required
|
||||
def __decrypt_keyfile(self, key_path: str) -> UmbralPrivateKey:
|
||||
|
@ -510,12 +511,12 @@ class NucypherKeyring:
|
|||
self.__derived_key_material = constants.KEYRING_LOCKED
|
||||
return self.is_unlocked
|
||||
|
||||
def unlock(self, passphrase: str) -> bool:
|
||||
def unlock(self, password: str) -> bool:
|
||||
if self.is_unlocked:
|
||||
return self.is_unlocked
|
||||
key_data = _read_keyfile(keypath=self.__root_keypath, deserializer=self._private_key_serializer)
|
||||
try:
|
||||
derived_key = _derive_key_material_from_passphrase(passphrase=passphrase, salt=key_data['master_salt'])
|
||||
derived_key = _derive_key_material_from_password(password=password, salt=key_data['master_salt'])
|
||||
except CryptoError:
|
||||
raise
|
||||
else:
|
||||
|
@ -568,7 +569,7 @@ class NucypherKeyring:
|
|||
#
|
||||
@classmethod
|
||||
def generate(cls,
|
||||
passphrase: str,
|
||||
password: str,
|
||||
encrypting: bool = True,
|
||||
wallet: bool = True,
|
||||
tls: bool = True,
|
||||
|
@ -577,28 +578,28 @@ class NucypherKeyring:
|
|||
keyring_root: str = None,
|
||||
) -> 'NucypherKeyring':
|
||||
"""
|
||||
Generates new encrypting, signing, and wallet keys encrypted with the passphrase,
|
||||
Generates new encrypting, signing, and wallet keys encrypted with the password,
|
||||
respectively saving keyfiles on the local filesystem from *default* paths,
|
||||
returning the corresponding Keyring instance.
|
||||
"""
|
||||
|
||||
failures = cls.validate_passphrase(passphrase)
|
||||
failures = cls.validate_password(password)
|
||||
if failures:
|
||||
raise cls.InvalidPassphrase(", ".join(failures)) # TODO: Ensure this scope is seperable from the scope containing the passphrase
|
||||
raise cls.AuthenticationFailed(", ".join(failures)) # TODO: Ensure this scope is seperable from the scope containing the password
|
||||
|
||||
if not any((wallet, encrypting, tls)):
|
||||
raise ValueError('Either "encrypting", "wallet", or "tls" must be True '
|
||||
'to generate new keys, or set "no_keys" to True to skip generation.')
|
||||
|
||||
if curve is None:
|
||||
curve = cls.__DEFAULT_TLS_CURVE
|
||||
|
||||
_base_filepaths = cls._generate_base_filepaths(keyring_root=keyring_root)
|
||||
_public_key_dir = _base_filepaths['public_key_dir']
|
||||
_private_key_dir = _base_filepaths['private_key_dir']
|
||||
|
||||
# Create the key directories with default paths. Raises OSError if dirs exist
|
||||
# if exists_ok and not os.path.isdir(_public_key_dir):
|
||||
# Write to disk
|
||||
os.mkdir(_public_key_dir, mode=0o744) # public dir
|
||||
|
||||
# if exists_ok and not os.path.isdir(_private_key_dir):
|
||||
os.mkdir(_private_key_dir, mode=0o700) # private dir
|
||||
|
||||
#
|
||||
|
@ -608,10 +609,11 @@ class NucypherKeyring:
|
|||
keyring_args = dict()
|
||||
|
||||
if wallet is True:
|
||||
new_address, new_wallet = _generate_wallet(passphrase)
|
||||
new_address, new_wallet = _generate_wallet(password)
|
||||
new_wallet_path = os.path.join(_private_key_dir, 'wallet-{}.json'.format(new_address))
|
||||
saved_wallet_path = _write_private_keyfile(new_wallet_path, json.dumps(new_wallet), serializer=None)
|
||||
keyring_args.update(wallet_path=saved_wallet_path)
|
||||
with open(new_wallet_path, 'w') as wallet: # TODO: is this pub or private?
|
||||
wallet.write(json.dumps(new_wallet))
|
||||
keyring_args.update(wallet_path=new_wallet_path)
|
||||
account = new_address
|
||||
|
||||
if encrypting is True:
|
||||
|
@ -630,33 +632,27 @@ class NucypherKeyring:
|
|||
delegating_keying_material = UmbralKeyingMaterial().to_bytes()
|
||||
|
||||
# Derive Wrapping Keys
|
||||
passphrase_salt, encrypting_salt, signing_salt, delegating_salt = (os.urandom(32) for _ in range(4))
|
||||
derived_key_material = _derive_key_material_from_passphrase(salt=passphrase_salt,
|
||||
passphrase=passphrase)
|
||||
encrypting_wrap_key = _derive_wrapping_key_from_key_material(salt=encrypting_salt,
|
||||
key_material=derived_key_material)
|
||||
signature_wrap_key = _derive_wrapping_key_from_key_material(salt=signing_salt,
|
||||
key_material=derived_key_material)
|
||||
delegating_wrap_key = _derive_wrapping_key_from_key_material(salt=delegating_salt,
|
||||
key_material=derived_key_material)
|
||||
password_salt, encrypting_salt, signing_salt, delegating_salt = (os.urandom(32) for _ in range(4))
|
||||
derived_key_material = _derive_key_material_from_password(salt=password_salt, password=password)
|
||||
encrypting_wrap_key = _derive_wrapping_key_from_key_material(salt=encrypting_salt, key_material=derived_key_material)
|
||||
signature_wrap_key = _derive_wrapping_key_from_key_material(salt=signing_salt, key_material=derived_key_material)
|
||||
delegating_wrap_key = _derive_wrapping_key_from_key_material(salt=delegating_salt, key_material=derived_key_material)
|
||||
|
||||
# TODO: Deprecate _encrypt_umbral_key with new pyumbral release
|
||||
# Encapsulate Private Keys
|
||||
encrypting_key_data = _encrypt_umbral_key(umbral_key=encrypting_private_key,
|
||||
wrapping_key=encrypting_wrap_key)
|
||||
signing_key_data = _encrypt_umbral_key(umbral_key=signing_private_key,
|
||||
wrapping_key=signature_wrap_key)
|
||||
encrypting_key_data = _encrypt_umbral_key(umbral_key=encrypting_private_key, wrapping_key=encrypting_wrap_key)
|
||||
signing_key_data = _encrypt_umbral_key(umbral_key=signing_private_key, wrapping_key=signature_wrap_key)
|
||||
delegating_key_data = bytes(SecretBox(delegating_wrap_key).encrypt(delegating_keying_material))
|
||||
|
||||
# Assemble Private Keys
|
||||
encrypting_key_metadata = _assemble_key_data(key_data=encrypting_key_data,
|
||||
master_salt=passphrase_salt,
|
||||
master_salt=password_salt,
|
||||
wrap_salt=encrypting_salt)
|
||||
signing_key_metadata = _assemble_key_data(key_data=signing_key_data,
|
||||
master_salt=passphrase_salt,
|
||||
master_salt=password_salt,
|
||||
wrap_salt=signing_salt)
|
||||
delegating_key_metadata = _assemble_key_data(key_data=delegating_key_data,
|
||||
master_salt=passphrase_salt,
|
||||
master_salt=password_salt,
|
||||
wrap_salt=delegating_salt)
|
||||
|
||||
# Write Private Keys
|
||||
|
@ -686,7 +682,7 @@ class NucypherKeyring:
|
|||
|
||||
if tls is True:
|
||||
if not all((host, curve)):
|
||||
raise ValueError("Host and curve are required to make a new keyring TLS certificate")
|
||||
raise ValueError("Host and curve are required to make a new keyring TLS certificate. Got {}, {}".format(host, curve))
|
||||
private_key, cert = _generate_tls_keys(host, curve)
|
||||
|
||||
def __serialize_pem(pk):
|
||||
|
@ -704,15 +700,15 @@ class NucypherKeyring:
|
|||
return keyring_instance
|
||||
|
||||
@staticmethod
|
||||
def validate_passphrase(passphrase: str) -> bool:
|
||||
def validate_password(password: str) -> bool:
|
||||
"""
|
||||
Validate a passphrase and return True or raise an error with a failure reason.
|
||||
Validate a password and return True or raise an error with a failure reason.
|
||||
|
||||
NOTICE: Do not raise inside this function.
|
||||
"""
|
||||
rules = (
|
||||
(bool(passphrase), 'Passphrase must not be blank.'),
|
||||
(len(passphrase) >= 16, 'Passphrase is too short, must be >= 16 chars.'),
|
||||
(bool(password), 'Password must not be blank.'),
|
||||
(len(password) >= 16, 'Password is too short, must be >= 16 chars.'),
|
||||
)
|
||||
|
||||
failures = list()
|
||||
|
|
|
@ -19,41 +19,69 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
|||
import binascii
|
||||
import json
|
||||
import os
|
||||
import secrets
|
||||
import string
|
||||
from abc import ABC, abstractmethod
|
||||
from json import JSONDecodeError
|
||||
from tempfile import TemporaryDirectory
|
||||
from typing import List
|
||||
|
||||
from constant_sorrow.constants import UNINITIALIZED_CONFIGURATION, STRANGER_CONFIGURATION, LIVE_CONFIGURATION
|
||||
import shutil
|
||||
from cryptography.hazmat.primitives.asymmetric import ec
|
||||
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurve
|
||||
from cryptography.x509 import Certificate
|
||||
from twisted.logger import Logger
|
||||
from typing import List
|
||||
from web3.middleware import geth_poa_middleware
|
||||
|
||||
from nucypher.characters.lawful import Ursula
|
||||
from nucypher.config.constants import DEFAULT_CONFIG_ROOT, BASE_DIR
|
||||
from constant_sorrow.constants import (
|
||||
UNINITIALIZED_CONFIGURATION,
|
||||
STRANGER_CONFIGURATION,
|
||||
NO_BLOCKCHAIN_CONNECTION,
|
||||
LIVE_CONFIGURATION,
|
||||
NO_KEYRING_ATTACHED
|
||||
)
|
||||
from nucypher.blockchain.eth.agents import PolicyAgent, MinerAgent, NucypherTokenAgent
|
||||
from nucypher.blockchain.eth.chains import Blockchain
|
||||
from nucypher.config.constants import DEFAULT_CONFIG_ROOT, BASE_DIR, USER_LOG_DIR
|
||||
from nucypher.config.keyring import NucypherKeyring
|
||||
from nucypher.config.storages import NodeStorage, InMemoryNodeStorage, LocalFileBasedNodeStorage
|
||||
from nucypher.crypto.powers import CryptoPowerUp
|
||||
from nucypher.config.storages import NodeStorage, ForgetfulNodeStorage, LocalFileBasedNodeStorage
|
||||
from nucypher.crypto.powers import CryptoPowerUp, CryptoPower
|
||||
from nucypher.network.middleware import RestMiddleware
|
||||
from nucypher.network.nodes import FleetStateTracker
|
||||
from umbral.signing import Signature
|
||||
|
||||
|
||||
class NodeConfiguration:
|
||||
class NodeConfiguration(ABC):
|
||||
"""
|
||||
'Sideways Engagement' of Character classes; a reflection of input parameters.
|
||||
"""
|
||||
|
||||
_name = 'ursula'
|
||||
_character_class = Ursula
|
||||
# Abstract
|
||||
_NAME = NotImplemented
|
||||
_CHARACTER_CLASS = NotImplemented
|
||||
CONFIG_FILENAME = NotImplemented
|
||||
DEFAULT_CONFIG_FILE_LOCATION = NotImplemented
|
||||
|
||||
DEFAULT_CONFIG_FILE_LOCATION = os.path.join(DEFAULT_CONFIG_ROOT, '{}.config'.format(_name))
|
||||
# Mode
|
||||
DEFAULT_OPERATING_MODE = 'decentralized'
|
||||
NODE_SERIALIZER = binascii.hexlify
|
||||
NODE_DESERIALIZER = binascii.unhexlify
|
||||
|
||||
# Configuration
|
||||
__CONFIG_FILE_EXT = '.config'
|
||||
__CONFIG_FILE_DESERIALIZER = json.loads
|
||||
__TEMP_CONFIGURATION_DIR_PREFIX = "nucypher-tmp-"
|
||||
__DEFAULT_NETWORK_MIDDLEWARE_CLASS = RestMiddleware
|
||||
__DEFAULT_NODE_STORAGE = LocalFileBasedNodeStorage
|
||||
|
||||
# Registry
|
||||
__REGISTRY_NAME = 'contract_registry.json'
|
||||
REGISTRY_SOURCE = os.path.join(BASE_DIR, __REGISTRY_NAME) # TODO: #461 Where will this be hosted?
|
||||
|
||||
# Rest + TLS
|
||||
DEFAULT_REST_HOST = '127.0.0.1'
|
||||
DEFAULT_REST_PORT = 9151
|
||||
__DEFAULT_TLS_CURVE = ec.SECP384R1
|
||||
__DEFAULT_NETWORK_MIDDLEWARE_CLASS = RestMiddleware
|
||||
|
||||
class ConfigurationError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
@ -62,156 +90,273 @@ class NodeConfiguration:
|
|||
|
||||
def __init__(self,
|
||||
|
||||
temp: bool = False,
|
||||
config_root: str = DEFAULT_CONFIG_ROOT,
|
||||
# Base
|
||||
config_root: str = None,
|
||||
config_file_location: str = None,
|
||||
|
||||
passphrase: str = None,
|
||||
auto_initialize: bool = False,
|
||||
auto_generate_keys: bool = False,
|
||||
|
||||
config_file_location: str = DEFAULT_CONFIG_FILE_LOCATION,
|
||||
keyring_dir: str = None,
|
||||
|
||||
checksum_address: str = None,
|
||||
is_me: bool = True,
|
||||
# Mode
|
||||
dev_mode: bool = False,
|
||||
federated_only: bool = False,
|
||||
network_middleware: RestMiddleware = None,
|
||||
|
||||
registry_source: str = REGISTRY_SOURCE,
|
||||
registry_filepath: str = None,
|
||||
import_seed_registry: bool = False,
|
||||
# Identity
|
||||
is_me: bool = True,
|
||||
checksum_public_address: str = None,
|
||||
crypto_power: CryptoPower = None,
|
||||
|
||||
# Keyring
|
||||
keyring: NucypherKeyring = None,
|
||||
keyring_dir: str = None,
|
||||
|
||||
# Learner
|
||||
learn_on_same_thread: bool = False,
|
||||
abort_on_learning_error: bool = False,
|
||||
start_learning_now: bool = True,
|
||||
|
||||
# TLS
|
||||
known_certificates_dir: str = None,
|
||||
# REST
|
||||
rest_host: str = None,
|
||||
rest_port: int = None,
|
||||
|
||||
# Metadata
|
||||
# TLS
|
||||
tls_curve: EllipticCurve = None,
|
||||
certificate: Certificate = None,
|
||||
|
||||
# Network
|
||||
interface_signature: Signature = None,
|
||||
network_middleware: RestMiddleware = None,
|
||||
|
||||
# Node Storage
|
||||
known_nodes: set = None,
|
||||
node_storage: NodeStorage = None,
|
||||
load_metadata: bool = True,
|
||||
save_metadata: bool = True
|
||||
reload_metadata: bool = True,
|
||||
save_metadata: bool = True,
|
||||
|
||||
# Blockchain
|
||||
poa: bool = False,
|
||||
provider_uri: str = None,
|
||||
|
||||
# Registry
|
||||
registry_source: str = None,
|
||||
registry_filepath: str = None,
|
||||
import_seed_registry: bool = False # TODO: needs cleanup
|
||||
|
||||
) -> None:
|
||||
|
||||
# Logs
|
||||
self.log = Logger(self.__class__.__name__)
|
||||
|
||||
# Known Nodes
|
||||
self.known_nodes_dir = UNINITIALIZED_CONFIGURATION
|
||||
self.known_certificates_dir = known_certificates_dir or UNINITIALIZED_CONFIGURATION
|
||||
#
|
||||
# REST + TLS (Ursula)
|
||||
#
|
||||
self.rest_host = rest_host or self.DEFAULT_REST_HOST
|
||||
self.rest_port = rest_port or self.DEFAULT_REST_PORT
|
||||
self.tls_curve = tls_curve or self.__DEFAULT_TLS_CURVE
|
||||
self.certificate = certificate
|
||||
|
||||
self.interface_signature = interface_signature
|
||||
self.crypto_power = crypto_power
|
||||
|
||||
#
|
||||
# Keyring
|
||||
self.keyring = UNINITIALIZED_CONFIGURATION
|
||||
#
|
||||
self.keyring = keyring or NO_KEYRING_ATTACHED
|
||||
self.keyring_dir = keyring_dir or UNINITIALIZED_CONFIGURATION
|
||||
|
||||
# Contract Registry
|
||||
self.__registry_source = registry_source
|
||||
if import_seed_registry is True:
|
||||
registry_source = self.REGISTRY_SOURCE
|
||||
if not os.path.isfile(registry_source):
|
||||
message = "Seed contract registry does not exist at path {}.".format(registry_filepath)
|
||||
self.log.debug(message)
|
||||
raise RuntimeError(message)
|
||||
self.__registry_source = registry_source or self.REGISTRY_SOURCE
|
||||
self.registry_filepath = registry_filepath or UNINITIALIZED_CONFIGURATION
|
||||
|
||||
# Configuration Root Directory
|
||||
#
|
||||
# Configuration
|
||||
#
|
||||
self.config_file_location = config_file_location or UNINITIALIZED_CONFIGURATION
|
||||
self.config_root = UNINITIALIZED_CONFIGURATION
|
||||
self.__temp = temp
|
||||
if self.__temp:
|
||||
self.__temp_dir = UNINITIALIZED_CONFIGURATION
|
||||
self.node_storage = InMemoryNodeStorage(federated_only=federated_only,
|
||||
character_class=self.__class__)
|
||||
else:
|
||||
self.config_root = config_root
|
||||
self.__temp_dir = LIVE_CONFIGURATION
|
||||
from nucypher.characters.lawful import Ursula # TODO : Needs cleanup
|
||||
self.node_storage = node_storage or self.__DEFAULT_NODE_STORAGE(federated_only=federated_only,
|
||||
character_class=Ursula)
|
||||
self.__cache_runtime_filepaths()
|
||||
self.config_file_location = config_file_location
|
||||
|
||||
#
|
||||
# Mode
|
||||
#
|
||||
self.federated_only = federated_only
|
||||
self.__dev_mode = dev_mode
|
||||
|
||||
if self.__dev_mode:
|
||||
self.__temp_dir = UNINITIALIZED_CONFIGURATION
|
||||
self.node_storage = ForgetfulNodeStorage(federated_only=federated_only, character_class=self.__class__)
|
||||
else:
|
||||
self.__temp_dir = LIVE_CONFIGURATION
|
||||
self.config_root = config_root or DEFAULT_CONFIG_ROOT
|
||||
self._cache_runtime_filepaths()
|
||||
self.node_storage = node_storage or LocalFileBasedNodeStorage(federated_only=federated_only,
|
||||
config_root=self.config_root)
|
||||
#
|
||||
# Identity
|
||||
#
|
||||
self.federated_only = federated_only
|
||||
self.checksum_address = checksum_address
|
||||
self.is_me = is_me
|
||||
if self.is_me:
|
||||
#
|
||||
self.checksum_public_address = checksum_public_address
|
||||
|
||||
if self.is_me is True or dev_mode is True:
|
||||
# Self
|
||||
#
|
||||
if checksum_address and not self.__temp:
|
||||
self.read_keyring()
|
||||
if self.checksum_public_address and dev_mode is False:
|
||||
self.attach_keyring()
|
||||
self.network_middleware = network_middleware or self.__DEFAULT_NETWORK_MIDDLEWARE_CLASS()
|
||||
else:
|
||||
#
|
||||
# Stranger
|
||||
#
|
||||
self.known_nodes_dir = STRANGER_CONFIGURATION
|
||||
self.known_certificates_dir = STRANGER_CONFIGURATION
|
||||
self.node_storage = STRANGER_CONFIGURATION
|
||||
self.keyring_dir = STRANGER_CONFIGURATION
|
||||
self.keyring = STRANGER_CONFIGURATION
|
||||
self.network_middleware = STRANGER_CONFIGURATION
|
||||
if network_middleware:
|
||||
raise self.ConfigurationError("Cannot configure a stranger to use network middleware")
|
||||
raise self.ConfigurationError("Cannot configure a stranger to use network middleware.")
|
||||
|
||||
#
|
||||
# Learner
|
||||
#
|
||||
self.known_nodes = known_nodes or set()
|
||||
self.learn_on_same_thread = learn_on_same_thread
|
||||
self.abort_on_learning_error = abort_on_learning_error
|
||||
self.start_learning_now = start_learning_now
|
||||
self.save_metadata = save_metadata
|
||||
self.load_metadata = load_metadata
|
||||
self.reload_metadata = reload_metadata
|
||||
|
||||
self.__fleet_state = FleetStateTracker()
|
||||
known_nodes = known_nodes or set()
|
||||
if known_nodes:
|
||||
self.known_nodes._nodes.update({node.checksum_public_address: node for node in known_nodes})
|
||||
self.known_nodes.record_fleet_state()
|
||||
|
||||
#
|
||||
# Auto-Initialization
|
||||
# Blockchain
|
||||
#
|
||||
if auto_initialize:
|
||||
self.initialize(no_registry=not import_seed_registry or federated_only,
|
||||
wallet=auto_generate_keys and not federated_only,
|
||||
encrypting=auto_generate_keys,
|
||||
passphrase=passphrase)
|
||||
self.poa = poa
|
||||
self.provider_uri = provider_uri
|
||||
|
||||
self.blockchain = NO_BLOCKCHAIN_CONNECTION
|
||||
self.accounts = NO_BLOCKCHAIN_CONNECTION
|
||||
self.token_agent = NO_BLOCKCHAIN_CONNECTION
|
||||
self.miner_agent = NO_BLOCKCHAIN_CONNECTION
|
||||
self.policy_agent = NO_BLOCKCHAIN_CONNECTION
|
||||
|
||||
#
|
||||
# Development Mode
|
||||
#
|
||||
if dev_mode:
|
||||
|
||||
# Ephemeral dev settings
|
||||
self.abort_on_learning_error = True
|
||||
self.save_metadata = False
|
||||
self.reload_metadata = False
|
||||
|
||||
# Generate one-time alphanumeric development password
|
||||
alphabet = string.ascii_letters + string.digits
|
||||
password = ''.join(secrets.choice(alphabet) for _ in range(32))
|
||||
|
||||
# Auto-initialize
|
||||
self.initialize(password=password, import_registry=import_seed_registry)
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
return self.produce(*args, **kwargs)
|
||||
|
||||
def cleanup(self) -> None:
|
||||
if self.__temp:
|
||||
if self.__dev_mode:
|
||||
self.__temp_dir.cleanup()
|
||||
|
||||
@property
|
||||
def temp(self):
|
||||
return self.__temp
|
||||
def dev_mode(self):
|
||||
return self.__dev_mode
|
||||
|
||||
def produce(self, passphrase: str = None, **overrides):
|
||||
"""Initialize a new character instance and return it"""
|
||||
if not self.temp:
|
||||
self.read_keyring()
|
||||
self.keyring.unlock(passphrase=passphrase)
|
||||
@property
|
||||
def known_nodes(self):
|
||||
return self.__fleet_state
|
||||
|
||||
def connect_to_blockchain(self, provider_uri: str, poa: bool = False, compile_contracts: bool = False):
|
||||
if self.federated_only:
|
||||
raise NodeConfiguration.ConfigurationError("Cannot connect to blockchain in federated mode")
|
||||
|
||||
self.blockchain = Blockchain.connect(provider_uri=provider_uri, compile=compile_contracts)
|
||||
if poa is True:
|
||||
w3 = self.blockchain.interface.w3
|
||||
w3.middleware_stack.inject(geth_poa_middleware, layer=0)
|
||||
|
||||
self.accounts = self.blockchain.interface.w3.eth.accounts
|
||||
self.log.debug("Established connection to provider {}".format(self.blockchain.interface.provider_uri))
|
||||
|
||||
def connect_to_contracts(self) -> None:
|
||||
"""Initialize contract agency and set them on config"""
|
||||
self.token_agent = NucypherTokenAgent(blockchain=self.blockchain)
|
||||
self.miner_agent = MinerAgent(blockchain=self.blockchain)
|
||||
self.policy_agent = PolicyAgent(blockchain=self.blockchain)
|
||||
self.log.debug("Established connection to nucypher contracts")
|
||||
|
||||
def read_known_nodes(self):
|
||||
known_nodes = self.node_storage.all(federated_only=self.federated_only)
|
||||
known_nodes = {node.checksum_public_address: node for node in known_nodes}
|
||||
self.known_nodes._nodes.update(known_nodes)
|
||||
self.known_nodes.record_fleet_state()
|
||||
return self.known_nodes
|
||||
|
||||
def forget_nodes(self) -> None:
|
||||
self.node_storage.clear()
|
||||
message = "Removed all stored node node metadata and certificates"
|
||||
self.log.debug(message)
|
||||
|
||||
def destroy(self, force: bool = False, logs: bool = True) -> None:
|
||||
|
||||
# TODO: Further confirm this is a nucypher dir first! (in-depth measure)
|
||||
|
||||
if logs is True or force:
|
||||
shutil.rmtree(USER_LOG_DIR, ignore_errors=True)
|
||||
try:
|
||||
shutil.rmtree(self.config_root, ignore_errors=force)
|
||||
except FileNotFoundError:
|
||||
raise FileNotFoundError("No such directory {}".format(self.config_root))
|
||||
|
||||
def produce(self, **overrides):
|
||||
"""Initialize a new character instance and return it."""
|
||||
|
||||
# Build a merged dict of node parameters
|
||||
merged_parameters = {**self.static_payload, **self.dynamic_payload, **overrides}
|
||||
return self._character_class(**merged_parameters)
|
||||
|
||||
# Verify the configuration file refers to the same configuration root as this instance
|
||||
config_root_from_config_file = merged_parameters.pop('config_root')
|
||||
if config_root_from_config_file != self.config_root:
|
||||
message = "Configuration root mismatch {} and {}.".format(config_root_from_config_file, self.config_root)
|
||||
raise self.ConfigurationError(message)
|
||||
|
||||
character = self._CHARACTER_CLASS(**merged_parameters)
|
||||
return character
|
||||
|
||||
@staticmethod
|
||||
def _read_configuration_file(filepath) -> dict:
|
||||
with open(filepath, 'r') as file:
|
||||
payload = NodeConfiguration.__CONFIG_FILE_DESERIALIZER(file.read())
|
||||
def _read_configuration_file(filepath: str) -> dict:
|
||||
try:
|
||||
with open(filepath, 'r') as file:
|
||||
raw_contents = file.read()
|
||||
payload = NodeConfiguration.__CONFIG_FILE_DESERIALIZER(raw_contents)
|
||||
except FileNotFoundError as e:
|
||||
raise # TODO: Do we need better exception handling here?
|
||||
return payload
|
||||
|
||||
@classmethod
|
||||
def from_configuration_file(cls, filepath, **overrides) -> 'NodeConfiguration':
|
||||
def from_configuration_file(cls, filepath: str = None, **overrides) -> 'NodeConfiguration':
|
||||
"""Initialize a NodeConfiguration from a JSON file."""
|
||||
from nucypher.config.storages import NodeStorage # TODO: move
|
||||
NODE_STORAGES = {storage_class._name: storage_class for storage_class in NodeStorage.__subclasses__()}
|
||||
|
||||
from nucypher.config.storages import NodeStorage
|
||||
node_storage_subclasses = {storage._name: storage for storage in NodeStorage.__subclasses__()}
|
||||
|
||||
if filepath is None:
|
||||
filepath = cls.DEFAULT_CONFIG_FILE_LOCATION
|
||||
|
||||
# Read from disk
|
||||
payload = cls._read_configuration_file(filepath=filepath)
|
||||
|
||||
# Make NodeStorage
|
||||
# Initialize NodeStorage subclass from file (sub-configuration)
|
||||
storage_payload = payload['node_storage']
|
||||
storage_type = storage_payload[NodeStorage._TYPE_LABEL]
|
||||
storage_class = NODE_STORAGES[storage_type]
|
||||
storage_class = node_storage_subclasses[storage_type]
|
||||
node_storage = storage_class.from_payload(payload=storage_payload,
|
||||
character_class=cls._character_class,
|
||||
character_class=cls._CHARACTER_CLASS,
|
||||
federated_only=payload['federated_only'],
|
||||
serializer=cls.NODE_SERIALIZER,
|
||||
deserializer=cls.NODE_DESERIALIZER)
|
||||
|
@ -222,11 +367,12 @@ class NodeConfiguration:
|
|||
def to_configuration_file(self, filepath: str = None) -> str:
|
||||
"""Write the static_payload to a JSON file."""
|
||||
if filepath is None:
|
||||
filename = '{}{}'.format(self._name.lower(), self.__CONFIG_FILE_EXT)
|
||||
filename = '{}{}'.format(self._NAME.lower(), self.__CONFIG_FILE_EXT)
|
||||
filepath = os.path.join(self.config_root, filename)
|
||||
|
||||
payload = self.static_payload
|
||||
del payload['is_me'] # TODO
|
||||
|
||||
# Save node connection data
|
||||
payload.update(dict(node_storage=self.node_storage.payload()))
|
||||
|
||||
|
@ -254,12 +400,13 @@ class NodeConfiguration:
|
|||
def static_payload(self) -> dict:
|
||||
"""Exported static configuration values for initializing Ursula"""
|
||||
payload = dict(
|
||||
config_root=self.config_root,
|
||||
|
||||
# Identity
|
||||
is_me=self.is_me,
|
||||
federated_only=self.federated_only, # TODO: 466
|
||||
checksum_address=self.checksum_address,
|
||||
checksum_public_address=self.checksum_public_address,
|
||||
keyring_dir=self.keyring_dir,
|
||||
known_certificates_dir=self.known_certificates_dir,
|
||||
|
||||
# Behavior
|
||||
learn_on_same_thread=self.learn_on_same_thread,
|
||||
|
@ -272,8 +419,12 @@ class NodeConfiguration:
|
|||
@property
|
||||
def dynamic_payload(self, **overrides) -> dict:
|
||||
"""Exported dynamic configuration values for initializing Ursula"""
|
||||
if self.load_metadata:
|
||||
self.known_nodes.update(self.node_storage.all(federated_only=self.federated_only))
|
||||
if self.reload_metadata:
|
||||
known_nodes = self.node_storage.all(federated_only=self.federated_only)
|
||||
known_nodes = {node.checksum_public_address: node for node in known_nodes}
|
||||
self.known_nodes._nodes.update(known_nodes)
|
||||
self.known_nodes.record_fleet_state()
|
||||
|
||||
payload = dict(network_middleware=self.network_middleware or self.__DEFAULT_NETWORK_MIDDLEWARE_CLASS(),
|
||||
known_nodes=self.known_nodes,
|
||||
node_storage=self.node_storage,
|
||||
|
@ -287,22 +438,19 @@ class NodeConfiguration:
|
|||
def runtime_filepaths(self):
|
||||
filepaths = dict(config_root=self.config_root,
|
||||
keyring_dir=self.keyring_dir,
|
||||
known_certificates_dir=self.known_certificates_dir,
|
||||
registry_filepath=self.registry_filepath)
|
||||
return filepaths
|
||||
|
||||
@staticmethod
|
||||
def generate_runtime_filepaths(config_root: str) -> dict:
|
||||
@classmethod
|
||||
def generate_runtime_filepaths(cls, config_root: str) -> dict:
|
||||
"""Dynamically generate paths based on configuration root directory"""
|
||||
known_nodes_dir = os.path.join(config_root, 'known_nodes')
|
||||
filepaths = dict(config_root=config_root,
|
||||
config_file_location=os.path.join(config_root, cls.CONFIG_FILENAME),
|
||||
keyring_dir=os.path.join(config_root, 'keyring'),
|
||||
known_nodes_dir=known_nodes_dir,
|
||||
known_certificates_dir=os.path.join(known_nodes_dir, 'certificates'),
|
||||
registry_filepath=os.path.join(config_root, NodeConfiguration.__REGISTRY_NAME))
|
||||
return filepaths
|
||||
|
||||
def __cache_runtime_filepaths(self) -> None:
|
||||
def _cache_runtime_filepaths(self) -> None:
|
||||
"""Generate runtime filepaths and cache them on the config object"""
|
||||
filepaths = self.generate_runtime_filepaths(config_root=self.config_root)
|
||||
for field, filepath in filepaths.items():
|
||||
|
@ -311,28 +459,22 @@ class NodeConfiguration:
|
|||
|
||||
def derive_node_power_ups(self) -> List[CryptoPowerUp]:
|
||||
power_ups = list()
|
||||
if self.is_me and not self.temp:
|
||||
for power_class in self._character_class._default_crypto_powerups:
|
||||
if self.is_me and not self.dev_mode:
|
||||
for power_class in self._CHARACTER_CLASS._default_crypto_powerups:
|
||||
power_up = self.keyring.derive_crypto_power(power_class)
|
||||
power_ups.append(power_up)
|
||||
return power_ups
|
||||
|
||||
def initialize(self,
|
||||
passphrase: str,
|
||||
no_registry: bool = False,
|
||||
wallet: bool = False,
|
||||
encrypting: bool = False,
|
||||
tls: bool = False,
|
||||
host: str = None,
|
||||
curve=None,
|
||||
no_keys: bool = False
|
||||
password: str,
|
||||
import_registry: bool = True,
|
||||
) -> str:
|
||||
"""Write a new configuration to the disk, and with the configured node store."""
|
||||
"""Initialize a new configuration."""
|
||||
|
||||
#
|
||||
# Create Config Root
|
||||
#
|
||||
if self.__temp:
|
||||
if self.__dev_mode:
|
||||
self.__temp_dir = TemporaryDirectory(prefix=self.__TEMP_CONFIGURATION_DIR_PREFIX)
|
||||
self.config_root = self.__temp_dir.name
|
||||
else:
|
||||
|
@ -348,62 +490,60 @@ class NodeConfiguration:
|
|||
#
|
||||
# Create Config Subdirectories
|
||||
#
|
||||
self.__cache_runtime_filepaths()
|
||||
self._cache_runtime_filepaths()
|
||||
try:
|
||||
|
||||
# Directories
|
||||
os.mkdir(self.keyring_dir, mode=0o700) # keyring
|
||||
os.mkdir(self.known_nodes_dir, mode=0o755) # known_nodes
|
||||
os.mkdir(self.known_certificates_dir, mode=0o755) # known_certs
|
||||
self.node_storage.initialize() # TODO: default known dir
|
||||
# Node Storage
|
||||
self.node_storage.initialize()
|
||||
|
||||
if not self.temp and not no_keys:
|
||||
# Keyring
|
||||
self.write_keyring(passphrase=passphrase,
|
||||
wallet=wallet,
|
||||
encrypting=encrypting,
|
||||
tls=tls,
|
||||
host=host,
|
||||
tls_curve=curve)
|
||||
# Keyring
|
||||
if not self.dev_mode:
|
||||
os.mkdir(self.keyring_dir, mode=0o700) # keyring TODO: Keyring backend entry point
|
||||
self.write_keyring(password=password, host=self.rest_host, tls_curve=self.tls_curve)
|
||||
|
||||
# Registry
|
||||
if not no_registry and not self.federated_only:
|
||||
self.write_registry(output_filepath=self.registry_filepath,
|
||||
source=self.__registry_source,
|
||||
blank=no_registry)
|
||||
if import_registry and not self.federated_only:
|
||||
self.write_registry(output_filepath=self.registry_filepath, # type: str
|
||||
source=self.__registry_source, # type: str
|
||||
blank=import_registry) # type: bool
|
||||
|
||||
except FileExistsError:
|
||||
existing_paths = [os.path.join(self.config_root, f) for f in os.listdir(self.config_root)]
|
||||
message = "There are pre-existing nucypher installation files at {}: {}".format(self.config_root,
|
||||
existing_paths)
|
||||
message = "There are pre-existing files at {}: {}".format(self.config_root, existing_paths)
|
||||
self.log.critical(message)
|
||||
raise NodeConfiguration.ConfigurationError(message)
|
||||
|
||||
if not self.__temp:
|
||||
self.validate(config_root=self.config_root, no_registry=no_registry or self.federated_only)
|
||||
if not self.__dev_mode:
|
||||
self.validate(config_root=self.config_root, no_registry=import_registry or self.federated_only)
|
||||
|
||||
# Success
|
||||
message = "Created nucypher installation files at {}".format(self.config_root)
|
||||
self.log.debug(message)
|
||||
return self.config_root
|
||||
|
||||
def read_known_nodes(self):
|
||||
self.known_nodes.update(self.node_storage.all(federated_only=self.federated_only))
|
||||
return self.known_nodes
|
||||
def attach_keyring(self, checksum_address: str = None, *args, **kwargs) -> None:
|
||||
if self.keyring is not NO_KEYRING_ATTACHED:
|
||||
if self.keyring.checksum_address != (checksum_address or self.checksum_public_address):
|
||||
raise self.ConfigurationError("There is already a keyring attached to this configuration.")
|
||||
return
|
||||
|
||||
def read_keyring(self, *args, **kwargs):
|
||||
if self.checksum_address is None:
|
||||
if (checksum_address or self.checksum_public_address) is None:
|
||||
raise self.ConfigurationError("No account specified to unlock keyring")
|
||||
self.keyring = NucypherKeyring(keyring_root=self.keyring_dir,
|
||||
account=self.checksum_address,
|
||||
|
||||
self.keyring = NucypherKeyring(keyring_root=self.keyring_dir, # type: str
|
||||
account=checksum_address or self.checksum_public_address, # type: str
|
||||
*args, **kwargs)
|
||||
|
||||
def write_keyring(self,
|
||||
passphrase: str,
|
||||
encrypting: bool,
|
||||
wallet: bool,
|
||||
tls: bool,
|
||||
host: str,
|
||||
password: str,
|
||||
encrypting: bool = True,
|
||||
wallet: bool = False,
|
||||
tls: bool = True,
|
||||
tls_curve: EllipticCurve = None,
|
||||
) -> NucypherKeyring:
|
||||
|
||||
self.keyring = NucypherKeyring.generate(passphrase=passphrase,
|
||||
self.keyring = NucypherKeyring.generate(password=password,
|
||||
encrypting=encrypting,
|
||||
wallet=wallet,
|
||||
tls=tls,
|
||||
|
@ -413,11 +553,9 @@ class NodeConfiguration:
|
|||
|
||||
# TODO: Operating mode switch #466
|
||||
if self.federated_only or not wallet:
|
||||
self.checksum_address = self.keyring.federated_address
|
||||
self.checksum_public_address = self.keyring.federated_address
|
||||
else:
|
||||
self.checksum_address = self.keyring.checksum_address
|
||||
if tls:
|
||||
self.certificate_filepath = self.keyring.certificate_filepath
|
||||
self.checksum_public_address = self.keyring.checksum_address
|
||||
|
||||
return self.keyring
|
||||
|
||||
|
@ -434,7 +572,7 @@ class NodeConfiguration:
|
|||
output_filepath = output_filepath or self.registry_filepath
|
||||
source = source or self.REGISTRY_SOURCE
|
||||
|
||||
if not blank and not self.temp:
|
||||
if not blank and not self.dev_mode:
|
||||
# Validate Registry
|
||||
with open(source, 'r') as registry_file:
|
||||
try:
|
||||
|
@ -450,5 +588,5 @@ class NodeConfiguration:
|
|||
self.log.warn("Writing blank registry")
|
||||
open(output_filepath, 'w').close() # write blank
|
||||
|
||||
self.log.info("Successfully wrote registry to {}".format(output_filepath))
|
||||
self.log.debug("Successfully wrote registry to {}".format(output_filepath))
|
||||
return output_filepath
|
||||
|
|
|
@ -14,19 +14,28 @@ GNU General Public License for more details.
|
|||
You should have received a copy of the GNU General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
|
||||
import binascii
|
||||
import glob
|
||||
import os
|
||||
import tempfile
|
||||
from abc import abstractmethod, ABC
|
||||
from twisted.logger import Logger
|
||||
|
||||
import OpenSSL
|
||||
import boto3 as boto3
|
||||
import shutil
|
||||
from botocore.errorfactory import ClientError
|
||||
from constant_sorrow import constants
|
||||
from typing import Callable
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives.serialization import Encoding
|
||||
from cryptography.x509 import Certificate
|
||||
from twisted.logger import Logger
|
||||
from typing import Callable, Tuple, Union, Set, Any
|
||||
|
||||
from constant_sorrow.constants import NO_STORAGE_AVAILIBLE
|
||||
from nucypher.config.constants import DEFAULT_CONFIG_ROOT
|
||||
from nucypher.utilities.decorators import validate_checksum_address
|
||||
|
||||
|
||||
class NodeStorage(ABC):
|
||||
|
@ -35,6 +44,8 @@ class NodeStorage(ABC):
|
|||
_TYPE_LABEL = 'storage_type'
|
||||
NODE_SERIALIZER = binascii.hexlify
|
||||
NODE_DESERIALIZER = binascii.unhexlify
|
||||
TLS_CERTIFICATE_ENCODING = Encoding.PEM
|
||||
TLS_CERTIFICATE_EXTENSION = '.{}'.format(TLS_CERTIFICATE_ENCODING.name.lower())
|
||||
|
||||
class NodeStorageError(Exception):
|
||||
pass
|
||||
|
@ -43,23 +54,25 @@ class NodeStorage(ABC):
|
|||
pass
|
||||
|
||||
def __init__(self,
|
||||
character_class,
|
||||
federated_only: bool, # TODO# 466
|
||||
character_class=None,
|
||||
serializer: Callable = NODE_SERIALIZER,
|
||||
deserializer: Callable = NODE_DESERIALIZER,
|
||||
) -> None:
|
||||
|
||||
from nucypher.characters.lawful import Ursula
|
||||
|
||||
self.log = Logger(self.__class__.__name__)
|
||||
self.serializer = serializer
|
||||
self.deserializer = deserializer
|
||||
self.federated_only = federated_only
|
||||
self.character_class = character_class
|
||||
self.character_class = character_class or Ursula
|
||||
|
||||
def __getitem__(self, item):
|
||||
return self.get(checksum_address=item, federated_only=self.federated_only)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
return self.save(node=value)
|
||||
return self.store_node_metadata(node=value)
|
||||
|
||||
def __delitem__(self, key):
|
||||
self.remove(checksum_address=key)
|
||||
|
@ -67,29 +80,29 @@ class NodeStorage(ABC):
|
|||
def __iter__(self):
|
||||
return self.all(federated_only=self.federated_only)
|
||||
|
||||
def _read_common_name(self, certificate: Certificate):
|
||||
x509 = OpenSSL.crypto.X509.from_cryptography(certificate)
|
||||
subject_components = x509.get_subject().get_components()
|
||||
common_name_as_bytes = subject_components[0][1]
|
||||
common_name_from_cert = common_name_as_bytes.decode()
|
||||
return common_name_from_cert
|
||||
|
||||
@abstractmethod
|
||||
def all(self, federated_only: bool) -> set:
|
||||
"""Return s set of all stored nodes"""
|
||||
def store_node_certificate(self,
|
||||
host: str,
|
||||
checksum_address: str,
|
||||
certificate: Certificate,
|
||||
force: bool = False
|
||||
) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def get(self, checksum_address: str, federated_only: bool):
|
||||
"""Retrieve a single stored node"""
|
||||
def store_node_metadata(self, node):
|
||||
"""Save a single node's metadata and tls certificate"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def save(self, node):
|
||||
"""Save a single node"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def remove(self, checksum_address: str) -> bool:
|
||||
"""Remove a single stored node"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def clear(self) -> bool:
|
||||
"""Remove all stored nodes"""
|
||||
def generate_certificate_filepath(self, checksum_address: str) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
|
@ -107,72 +120,278 @@ class NodeStorage(ABC):
|
|||
"""One-time initialization steps to establish a node storage backend"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def all(self, federated_only: bool, certificates_only: bool = False) -> set:
|
||||
"""Return s set of all stored nodes"""
|
||||
raise NotImplementedError
|
||||
|
||||
class InMemoryNodeStorage(NodeStorage):
|
||||
@abstractmethod
|
||||
def get(self, checksum_address: str, federated_only: bool):
|
||||
"""Retrieve a single stored node"""
|
||||
raise NotImplementedError
|
||||
|
||||
_name = 'memory'
|
||||
@abstractmethod
|
||||
def remove(self, checksum_address: str) -> bool:
|
||||
"""Remove a single stored node"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def clear(self) -> bool:
|
||||
"""Remove all stored nodes"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class ForgetfulNodeStorage(NodeStorage):
|
||||
|
||||
_name = ':memory:'
|
||||
__base_prefix = 'nucypher-temp-cert-'
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.__known_nodes = dict()
|
||||
self.__metadata = dict()
|
||||
self.__certificates = dict()
|
||||
|
||||
def all(self, federated_only: bool) -> set:
|
||||
return set(self.__known_nodes.values())
|
||||
self.__rollover_certificates = list()
|
||||
|
||||
def get(self, checksum_address: str, federated_only: bool):
|
||||
try:
|
||||
return self.__known_nodes[checksum_address]
|
||||
except KeyError:
|
||||
raise self.UnknownNode
|
||||
def all(self, federated_only: bool, certificates_only: bool = False) -> set:
|
||||
return set(self.__metadata.values() if not certificates_only else self.__certificates.values())
|
||||
|
||||
def save(self, node):
|
||||
self.__known_nodes[node.checksum_public_address] = node
|
||||
return True
|
||||
@validate_checksum_address
|
||||
def get(self,
|
||||
federated_only: bool,
|
||||
host: str = None,
|
||||
checksum_address: str = None,
|
||||
certificate_only: bool = False):
|
||||
|
||||
def remove(self, checksum_address: str) -> bool:
|
||||
del self.__known_nodes[checksum_address]
|
||||
return True
|
||||
if not bool(checksum_address) ^ bool(host):
|
||||
message = "Either pass checksum_address or host; Not both. Got ({} {})".format(checksum_address, host)
|
||||
raise ValueError(message)
|
||||
|
||||
def clear(self):
|
||||
self.__known_nodes = dict()
|
||||
if certificate_only is True:
|
||||
try:
|
||||
return self.__certificates[checksum_address or host]
|
||||
except KeyError:
|
||||
raise self.UnknownNode
|
||||
else:
|
||||
try:
|
||||
return self.__metadata[checksum_address or host]
|
||||
except KeyError:
|
||||
raise self.UnknownNode
|
||||
|
||||
def forget(self, everything: bool = True) -> bool:
|
||||
for temp_certificate in self.__rollover_certificates:
|
||||
os.remove(temp_certificate)
|
||||
|
||||
if everything is True:
|
||||
pattern = '/tmp/{}*'.format(self.__base_prefix)
|
||||
for temp_certificate in glob.glob(pattern):
|
||||
os.remove(temp_certificate)
|
||||
return len(glob.glob(pattern)) == 0
|
||||
|
||||
return len(self.__rollover_certificates) == 0
|
||||
|
||||
def store_host_certificate(self, host: str, certificate: Certificate):
|
||||
self.__certificates[host] = certificate
|
||||
return self.generate_certificate_filepath(host=host)
|
||||
|
||||
@validate_checksum_address
|
||||
def store_node_certificate(self,
|
||||
certificate: Certificate,
|
||||
checksum_address: str,
|
||||
host: str = None,
|
||||
force: bool = False
|
||||
) -> str:
|
||||
|
||||
self.__certificates[checksum_address] = certificate
|
||||
return self.generate_certificate_filepath(checksum_address=checksum_address)
|
||||
|
||||
def store_node_metadata(self, node):
|
||||
self.__metadata[node.checksum_public_address] = node
|
||||
return self.__metadata[node.checksum_public_address]
|
||||
|
||||
@validate_checksum_address
|
||||
def generate_certificate_filepath(self,
|
||||
checksum_address: str = None,
|
||||
host: str = None) -> str:
|
||||
|
||||
if not bool(checksum_address) ^ bool(host):
|
||||
message = "Either pass checksum_address or host; Not both. Got ({} {})".format(checksum_address, host)
|
||||
raise ValueError(message)
|
||||
|
||||
prefix = '{}{}-'.format(self.__base_prefix, checksum_address or host)
|
||||
temp_file = tempfile.NamedTemporaryFile(prefix=prefix, suffix=self.TLS_CERTIFICATE_EXTENSION, delete=False)
|
||||
certificate = self.__certificates[checksum_address or host]
|
||||
certificate_bytes = certificate.public_bytes(self.TLS_CERTIFICATE_ENCODING)
|
||||
temp_file.write(certificate_bytes)
|
||||
|
||||
self.__rollover_certificates.append(temp_file.name)
|
||||
return temp_file.name
|
||||
|
||||
@validate_checksum_address
|
||||
def remove(self,
|
||||
checksum_address: str,
|
||||
metadata: bool = True,
|
||||
certificate: bool = True
|
||||
) -> Tuple[bool, str]:
|
||||
|
||||
if metadata is True:
|
||||
del self.__metadata[checksum_address]
|
||||
if certificate is True:
|
||||
del self.__certificates[checksum_address]
|
||||
return True, checksum_address
|
||||
|
||||
def clear(self, metadata: bool = True, certificates: bool = True) -> None:
|
||||
"""Forget all stored nodes and certificates"""
|
||||
if metadata is True:
|
||||
self.__metadata = dict()
|
||||
if certificates is True:
|
||||
self.__certificates = dict()
|
||||
|
||||
def payload(self) -> dict:
|
||||
payload = {self._TYPE_LABEL: self._name}
|
||||
return payload
|
||||
|
||||
@classmethod
|
||||
def from_payload(cls, payload: dict, *args, **kwargs) -> 'InMemoryNodeStorage':
|
||||
def from_payload(cls, payload: dict, *args, **kwargs) -> 'ForgetfulNodeStorage':
|
||||
"""Alternate constructor to create a storage instance from JSON-like configuration"""
|
||||
if payload[cls._TYPE_LABEL] != cls._name:
|
||||
raise cls.NodeStorageError
|
||||
return cls(*args, **kwargs)
|
||||
|
||||
def initialize(self) -> None:
|
||||
self.__known_nodes = dict()
|
||||
def initialize(self) -> bool:
|
||||
"""Returns True if initialization was successful"""
|
||||
self.__metadata = dict()
|
||||
self.__certificates = dict()
|
||||
return not bool(self.__metadata or self.__certificates)
|
||||
|
||||
|
||||
class LocalFileBasedNodeStorage(NodeStorage):
|
||||
|
||||
_name = 'local'
|
||||
__FILENAME_TEMPLATE = '{}.node'
|
||||
__DEFAULT_DIR = os.path.join(DEFAULT_CONFIG_ROOT, 'known_nodes', 'metadata')
|
||||
__METADATA_FILENAME_TEMPLATE = '{}.node'
|
||||
|
||||
class NoNodeMetadataFileFound(FileNotFoundError, NodeStorage.UnknownNode):
|
||||
pass
|
||||
|
||||
def __init__(self,
|
||||
known_metadata_dir: str = __DEFAULT_DIR,
|
||||
config_root: str = None,
|
||||
storage_root: str = None,
|
||||
metadata_dir: str = None,
|
||||
certificates_dir: str = None,
|
||||
*args, **kwargs
|
||||
) -> None:
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
self.log = Logger(self.__class__.__name__)
|
||||
self.known_metadata_dir = known_metadata_dir
|
||||
|
||||
def __generate_filepath(self, checksum_address: str) -> str:
|
||||
metadata_path = os.path.join(self.known_metadata_dir, self.__FILENAME_TEMPLATE.format(checksum_address))
|
||||
self.root_dir = storage_root
|
||||
self.metadata_dir = metadata_dir
|
||||
self.certificates_dir = certificates_dir
|
||||
self._cache_storage_filepaths(config_root=config_root)
|
||||
|
||||
@staticmethod
|
||||
def _generate_storage_filepaths(config_root: str = None,
|
||||
storage_root: str = None,
|
||||
metadata_dir: str = None,
|
||||
certificates_dir: str = None):
|
||||
|
||||
storage_root = storage_root or os.path.join(config_root or DEFAULT_CONFIG_ROOT, 'known_nodes')
|
||||
metadata_dir = metadata_dir or os.path.join(storage_root, 'metadata')
|
||||
certificates_dir = certificates_dir or os.path.join(storage_root, 'certificates')
|
||||
|
||||
payload = {'storage_root': storage_root,
|
||||
'metadata_dir': metadata_dir,
|
||||
'certificates_dir': certificates_dir}
|
||||
|
||||
return payload
|
||||
|
||||
def _cache_storage_filepaths(self, config_root: str = None):
|
||||
filepaths = self._generate_storage_filepaths(config_root=config_root,
|
||||
storage_root=self.root_dir,
|
||||
metadata_dir=self.metadata_dir,
|
||||
certificates_dir=self.certificates_dir)
|
||||
self.root_dir = filepaths['storage_root']
|
||||
self.metadata_dir = filepaths['metadata_dir']
|
||||
self.certificates_dir = filepaths['certificates_dir']
|
||||
|
||||
#
|
||||
# Certificates
|
||||
#
|
||||
|
||||
@validate_checksum_address
|
||||
def __get_certificate_filename(self, checksum_address: str):
|
||||
return '{}.{}'.format(checksum_address, Encoding.PEM.name.lower())
|
||||
|
||||
def __get_certificate_filepath(self, certificate_filename: str) -> str:
|
||||
return os.path.join(self.certificates_dir, certificate_filename)
|
||||
|
||||
@validate_checksum_address
|
||||
def generate_certificate_filepath(self, checksum_address: str) -> str:
|
||||
certificate_filename = self.__get_certificate_filename(checksum_address)
|
||||
certificate_filepath = self.__get_certificate_filepath(certificate_filename=certificate_filename)
|
||||
return certificate_filepath
|
||||
|
||||
@validate_checksum_address
|
||||
def __write_tls_certificate(self,
|
||||
checksum_address: str,
|
||||
certificate: Certificate,
|
||||
host: str = None,
|
||||
force: bool = False) -> str:
|
||||
|
||||
# Read
|
||||
x509 = OpenSSL.crypto.X509.from_cryptography(certificate)
|
||||
subject_components = x509.get_subject().get_components()
|
||||
common_name_as_bytes = subject_components[0][1]
|
||||
common_name_on_certificate = common_name_as_bytes.decode()
|
||||
if not host:
|
||||
host = common_name_on_certificate
|
||||
|
||||
# Validate
|
||||
# TODO: It's better for us to have checked this a while ago so that this situation is impossible. #443
|
||||
if host and (host != common_name_on_certificate):
|
||||
raise ValueError('You passed a hostname ("{}") that does not match the certificat\'s common name.'.format(host))
|
||||
|
||||
certificate_filepath = self.generate_certificate_filepath(checksum_address=checksum_address)
|
||||
certificate_already_exists = os.path.isfile(certificate_filepath)
|
||||
if force is False and certificate_already_exists:
|
||||
raise FileExistsError('A TLS certificate already exists at {}.'.format(certificate_filepath))
|
||||
|
||||
# Write
|
||||
with open(certificate_filepath, 'wb') as certificate_file:
|
||||
public_pem_bytes = certificate.public_bytes(self.TLS_CERTIFICATE_ENCODING)
|
||||
certificate_file.write(public_pem_bytes)
|
||||
|
||||
self.certificate_filepath = certificate_filepath
|
||||
self.log.info("Saved TLS certificate for {}: {}".format(self, certificate_filepath))
|
||||
|
||||
return certificate_filepath
|
||||
|
||||
@validate_checksum_address
|
||||
def __read_tls_public_certificate(self, filepath: str = None, checksum_address: str=None) -> Certificate:
|
||||
"""Deserialize an X509 certificate from a filepath"""
|
||||
if not bool(filepath) ^ bool(checksum_address):
|
||||
raise ValueError("Either pass filepath or checksum_address; Not both.")
|
||||
|
||||
if not filepath and checksum_address is not None:
|
||||
filepath = self.generate_certificate_filepath(checksum_address)
|
||||
|
||||
try:
|
||||
with open(filepath, 'rb') as certificate_file:
|
||||
cert = x509.load_pem_x509_certificate(certificate_file.read(), backend=default_backend())
|
||||
return cert
|
||||
except FileNotFoundError:
|
||||
raise FileNotFoundError("No SSL certificate found at {}".format(filepath))
|
||||
|
||||
#
|
||||
# Metadata
|
||||
#
|
||||
|
||||
@validate_checksum_address
|
||||
def __generate_metadata_filepath(self, checksum_address: str) -> str:
|
||||
metadata_path = os.path.join(self.metadata_dir, self.__METADATA_FILENAME_TEMPLATE.format(checksum_address))
|
||||
return metadata_path
|
||||
|
||||
def __read(self, filepath: str, federated_only: bool):
|
||||
def __read_metadata(self, filepath: str, federated_only: bool):
|
||||
from nucypher.characters.lawful import Ursula
|
||||
try:
|
||||
with open(filepath, "rb") as seed_file:
|
||||
|
@ -183,46 +402,107 @@ class LocalFileBasedNodeStorage(NodeStorage):
|
|||
raise self.UnknownNode
|
||||
return node
|
||||
|
||||
def __write(self, filepath: str, node):
|
||||
def __write_metadata(self, filepath: str, node):
|
||||
with open(filepath, "wb") as f:
|
||||
f.write(self.serializer(self.character_class.__bytes__(node)))
|
||||
self.log.info("Wrote new node metadata to filesystem {}".format(filepath))
|
||||
return filepath
|
||||
|
||||
def all(self, federated_only: bool) -> set:
|
||||
filenames = os.listdir(self.known_metadata_dir)
|
||||
self.log.info("Found {} known node metadata files at {}".format(len(filenames), self.known_metadata_dir))
|
||||
known_nodes = set()
|
||||
for filename in filenames:
|
||||
metadata_path = os.path.join(self.known_metadata_dir, filename)
|
||||
node = self.__read(filepath=metadata_path, federated_only=federated_only) # TODO: 466
|
||||
known_nodes.add(node)
|
||||
return known_nodes
|
||||
#
|
||||
# API
|
||||
#
|
||||
def all(self, federated_only: bool, certificates_only: bool = False) -> Set[Union[Any, Certificate]]:
|
||||
filenames = os.listdir(self.certificates_dir if certificates_only else self.metadata_dir)
|
||||
self.log.info("Found {} known node metadata files at {}".format(len(filenames), self.metadata_dir))
|
||||
|
||||
def get(self, checksum_address: str, federated_only: bool):
|
||||
metadata_path = self.__generate_filepath(checksum_address=checksum_address)
|
||||
node = self.__read(filepath=metadata_path, federated_only=federated_only) # TODO: 466
|
||||
known_certificates = set()
|
||||
if certificates_only:
|
||||
for filename in filenames:
|
||||
certificate = self.__read_tls_public_certificate(os.path.join(self.certificates_dir, filename))
|
||||
known_certificates.add(certificate)
|
||||
return known_certificates
|
||||
|
||||
else:
|
||||
known_nodes = set()
|
||||
for filename in filenames:
|
||||
metadata_path = os.path.join(self.metadata_dir, filename)
|
||||
node = self.__read_metadata(filepath=metadata_path, federated_only=federated_only) # TODO: 466
|
||||
known_nodes.add(node)
|
||||
return known_nodes
|
||||
|
||||
@validate_checksum_address
|
||||
def get(self, checksum_address: str, federated_only: bool, certificate_only: bool = False):
|
||||
if certificate_only is True:
|
||||
certificate = self.__read_tls_public_certificate(checksum_address=checksum_address)
|
||||
return certificate
|
||||
metadata_path = self.__generate_metadata_filepath(checksum_address=checksum_address)
|
||||
node = self.__read_metadata(filepath=metadata_path, federated_only=federated_only) # TODO: 466
|
||||
return node
|
||||
|
||||
def save(self, node):
|
||||
try:
|
||||
filepath = self.__generate_filepath(checksum_address=node.checksum_public_address)
|
||||
except AttributeError:
|
||||
raise AttributeError("{} does not have a rest_interface attached".format(self)) # TODO.. eh?
|
||||
self.__write(filepath=filepath, node=node)
|
||||
@validate_checksum_address
|
||||
def store_node_certificate(self,
|
||||
checksum_address: str,
|
||||
certificate: Certificate,
|
||||
host: str = None,
|
||||
force: bool = True
|
||||
) -> str:
|
||||
|
||||
def remove(self, checksum_address: str):
|
||||
filepath = self.__generate_filepath(checksum_address=checksum_address)
|
||||
self.log.debug("Delted {} from the filesystem".format(checksum_address))
|
||||
return os.remove(filepath)
|
||||
certificate_filepath = self.__write_tls_certificate(certificate=certificate,
|
||||
checksum_address=checksum_address,
|
||||
host=host,
|
||||
force=force)
|
||||
|
||||
def clear(self):
|
||||
self.__known_nodes = dict()
|
||||
return certificate_filepath
|
||||
|
||||
def store_node_metadata(self, node) -> str:
|
||||
filepath = self.__generate_metadata_filepath(checksum_address=node.checksum_public_address)
|
||||
self.__write_metadata(filepath=filepath, node=node)
|
||||
return filepath
|
||||
|
||||
def save_node(self, node, force) -> Tuple[str, str]:
|
||||
certificate_filepath = self.store_node_certificate(checksum_address=node.checksum_public_address,
|
||||
certificate=node.certificate,
|
||||
force=force)
|
||||
metadata_filepath = self.store_node_metadata(node=node)
|
||||
return metadata_filepath, certificate_filepath
|
||||
|
||||
@validate_checksum_address
|
||||
def remove(self, checksum_address: str, metadata: bool = True, certificate: bool = True) -> None:
|
||||
|
||||
if metadata is True:
|
||||
metadata_filepath = self.__generate_metadata_filepath(checksum_address=checksum_address)
|
||||
os.remove(metadata_filepath)
|
||||
self.log.debug("Deleted {} from the filesystem".format(checksum_address))
|
||||
|
||||
if certificate is True:
|
||||
certificate_filepath = self.generate_certificate_filepath(checksum_address=checksum_address)
|
||||
os.remove(certificate_filepath)
|
||||
self.log.debug("Deleted {} from the filesystem".format(checksum_address))
|
||||
|
||||
return
|
||||
|
||||
def clear(self, metadata: bool = True, certificates: bool = True) -> None:
|
||||
"""Forget all stored nodes and certificates"""
|
||||
|
||||
def __destroy_dir_contents(path):
|
||||
for file in os.listdir(path):
|
||||
file_path = os.path.join(path, file)
|
||||
if os.path.isfile(file_path):
|
||||
os.unlink(file_path)
|
||||
|
||||
if metadata is True:
|
||||
__destroy_dir_contents(self.metadata_dir)
|
||||
if certificates is True:
|
||||
__destroy_dir_contents(self.certificates_dir)
|
||||
|
||||
return
|
||||
|
||||
def payload(self) -> dict:
|
||||
payload = {
|
||||
'storage_type': self._name,
|
||||
'known_metadata_dir': self.known_metadata_dir
|
||||
'storage_root': self.root_dir,
|
||||
'metadata_dir': self.metadata_dir,
|
||||
'certificates_dir': self.certificates_dir
|
||||
}
|
||||
return payload
|
||||
|
||||
|
@ -231,35 +511,55 @@ class LocalFileBasedNodeStorage(NodeStorage):
|
|||
storage_type = payload[cls._TYPE_LABEL]
|
||||
if not storage_type == cls._name:
|
||||
raise cls.NodeStorageError("Wrong storage type. got {}".format(storage_type))
|
||||
return cls(known_metadata_dir=payload['known_metadata_dir'], *args, **kwargs)
|
||||
del payload['storage_type']
|
||||
|
||||
def initialize(self):
|
||||
return cls(*args, **payload, **kwargs)
|
||||
|
||||
def initialize(self) -> bool:
|
||||
try:
|
||||
os.mkdir(self.known_metadata_dir, mode=0o755) # known_metadata
|
||||
os.mkdir(self.root_dir, mode=0o755)
|
||||
os.mkdir(self.metadata_dir, mode=0o755)
|
||||
os.mkdir(self.certificates_dir, mode=0o755)
|
||||
except FileExistsError:
|
||||
message = "There are pre-existing metadata files at {}".format(self.known_metadata_dir)
|
||||
message = "There are pre-existing files at {}".format(self.root_dir)
|
||||
raise self.NodeStorageError(message)
|
||||
except FileNotFoundError:
|
||||
raise self.NodeStorageError("There is no existing configuration at {}".format(self.known_metadata_dir))
|
||||
raise self.NodeStorageError("There is no existing configuration at {}".format(self.root_dir))
|
||||
|
||||
return bool(all(map(os.path.isdir, (self.root_dir, self.metadata_dir, self.certificates_dir))))
|
||||
|
||||
|
||||
class TemporaryFileBasedNodeStorage(LocalFileBasedNodeStorage):
|
||||
_name = 'tmp'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.__temp_dir = constants.NO_STORAGE_AVAILIBLE
|
||||
super().__init__(known_metadata_dir=self.__temp_dir, *args, **kwargs)
|
||||
self.__temp_metadata_dir = None
|
||||
self.__temp_certificates_dir = None
|
||||
super().__init__(metadata_dir=self.__temp_metadata_dir,
|
||||
certificates_dir=self.__temp_certificates_dir,
|
||||
*args, **kwargs)
|
||||
|
||||
def __del__(self):
|
||||
if not self.__temp_dir is constants.NO_STORAGE_AVAILIBLE:
|
||||
shutil.rmtree(self.__temp_dir, ignore_errors=True)
|
||||
if self.__temp_metadata_dir is not None:
|
||||
shutil.rmtree(self.__temp_metadata_dir, ignore_errors=True)
|
||||
shutil.rmtree(self.__temp_certificates_dir, ignore_errors=True)
|
||||
|
||||
def initialize(self):
|
||||
self.__temp_dir = tempfile.mkdtemp(prefix="nucypher-tmp-nodes-")
|
||||
self.known_metadata_dir = self.__temp_dir
|
||||
def initialize(self) -> bool:
|
||||
|
||||
# Metadata
|
||||
self.__temp_metadata_dir = tempfile.mkdtemp(prefix="nucypher-tmp-nodes-")
|
||||
self.metadata_dir = self.__temp_metadata_dir
|
||||
|
||||
# Certificates
|
||||
self.__temp_certificates_dir = tempfile.mkdtemp(prefix="nucypher-tmp-certs-")
|
||||
self.certificates_dir = self.__temp_certificates_dir
|
||||
|
||||
return bool(os.path.isdir(self.metadata_dir) and os.path.isdir(self.certificates_dir))
|
||||
|
||||
|
||||
class S3NodeStorage(NodeStorage):
|
||||
|
||||
_name = 's3'
|
||||
S3_ACL = 'private' # Canned S3 Permissions
|
||||
|
||||
def __init__(self,
|
||||
|
@ -271,7 +571,7 @@ class S3NodeStorage(NodeStorage):
|
|||
self.__bucket_name = bucket_name
|
||||
self.__s3client = boto3.client('s3')
|
||||
self.__s3resource = s3_resource or boto3.resource('s3')
|
||||
self.__bucket = constants.NO_STORAGE_AVAILIBLE
|
||||
self.__bucket = NO_STORAGE_AVAILIBLE
|
||||
|
||||
@property
|
||||
def bucket(self):
|
||||
|
@ -290,12 +590,13 @@ class S3NodeStorage(NodeStorage):
|
|||
node = self.character_class.from_bytes(node_bytes)
|
||||
return node
|
||||
|
||||
@validate_checksum_address
|
||||
def generate_presigned_url(self, checksum_address: str) -> str:
|
||||
payload = {'Bucket': self.__bucket_name, 'Key': checksum_address}
|
||||
url = self.__s3client.generate_presigned_url('get_object', payload, ExpiresIn=900)
|
||||
return url
|
||||
|
||||
def all(self, federated_only: bool) -> set:
|
||||
def all(self, federated_only: bool, certificates_only: bool = False) -> set:
|
||||
node_objs = self.__bucket.objects.all()
|
||||
nodes = set()
|
||||
for node_obj in node_objs:
|
||||
|
@ -303,17 +604,19 @@ class S3NodeStorage(NodeStorage):
|
|||
nodes.add(node)
|
||||
return nodes
|
||||
|
||||
@validate_checksum_address
|
||||
def get(self, checksum_address: str, federated_only: bool):
|
||||
node_obj = self.__bucket.Object(checksum_address)
|
||||
node = self.__read(node_obj=node_obj)
|
||||
return node
|
||||
|
||||
def save(self, node):
|
||||
def store_node_metadata(self, node):
|
||||
self.__s3client.put_object(Bucket=self.__bucket_name,
|
||||
ACL=self.S3_ACL,
|
||||
Key=node.checksum_public_address,
|
||||
Body=self.serializer(bytes(node)))
|
||||
|
||||
@validate_checksum_address
|
||||
def remove(self, checksum_address: str) -> bool:
|
||||
node_obj = self.__bucket.Object(checksum_address)
|
||||
response = node_obj.delete()
|
||||
|
|
|
@ -14,20 +14,19 @@ GNU General Public License for more details.
|
|||
You should have received a copy of the GNU General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
|
||||
import binascii
|
||||
import os
|
||||
import random
|
||||
from collections import defaultdict, OrderedDict
|
||||
from collections import deque
|
||||
from collections import namedtuple
|
||||
from contextlib import suppress
|
||||
from logging import Logger
|
||||
from tempfile import TemporaryDirectory
|
||||
from typing import Set, Tuple
|
||||
|
||||
import OpenSSL
|
||||
import maya
|
||||
import requests
|
||||
import socket
|
||||
import time
|
||||
from bytestring_splitter import BytestringSplitter, VariableLengthBytestring, BytestringSplittingError
|
||||
from constant_sorrow import constants
|
||||
|
@ -39,19 +38,22 @@ from twisted.internet import reactor, defer
|
|||
from twisted.internet import task
|
||||
from twisted.internet.threads import deferToThread
|
||||
from twisted.logger import Logger
|
||||
from typing import Set, Tuple
|
||||
|
||||
from bytestring_splitter import BytestringSplitter
|
||||
from constant_sorrow import constants
|
||||
from constant_sorrow.constants import constant_or_bytes, GLOBAL_DOMAIN
|
||||
from nucypher.config.constants import SeednodeMetadata
|
||||
from nucypher.config.keyring import _write_tls_certificate
|
||||
from nucypher.config.storages import InMemoryNodeStorage
|
||||
from nucypher.config.storages import ForgetfulNodeStorage
|
||||
from nucypher.crypto.api import keccak_digest
|
||||
from nucypher.crypto.powers import BlockchainPower, SigningPower, EncryptingPower, NoSigningPower
|
||||
from nucypher.crypto.signing import signature_splitter
|
||||
from nucypher.network import LEARNING_LOOP_VERSION
|
||||
from nucypher.network.middleware import RestMiddleware
|
||||
from nucypher.network.nicknames import nickname_from_seed
|
||||
from nucypher.network.protocols import SuspiciousActivity
|
||||
from nucypher.network.protocols import SuspiciousActivity, parse_node_uri
|
||||
from nucypher.network.server import TLSHostingPower
|
||||
from nucypher.utilities.decorators import validate_checksum_address
|
||||
|
||||
|
||||
def icon_from_checksum(checksum,
|
||||
|
@ -144,10 +146,16 @@ class FleetStateTracker:
|
|||
def nickname_metadata(self):
|
||||
return self._nickname_metadata
|
||||
|
||||
@property
|
||||
def icon(self) -> str:
|
||||
if self.nickname_metadata is constants.NO_KNOWN_NODES:
|
||||
return str(constants.NO_KNOWN_NODES)
|
||||
return self.nickname_metadata[0][1]
|
||||
|
||||
def addresses(self):
|
||||
return self._nodes.keys()
|
||||
|
||||
def icon(self):
|
||||
def icon_html(self):
|
||||
return icon_from_checksum(checksum=self.checksum,
|
||||
number_of_nodes=len(self),
|
||||
nickname_metadata=self.nickname_metadata)
|
||||
|
@ -173,11 +181,18 @@ class FleetStateTracker:
|
|||
# For now we store the sorted node list. Someday we probably spin this out into
|
||||
# its own class, FleetState, and use it as the basis for partial updates.
|
||||
self.states[checksum] = self.state_template(nickname=self.nickname,
|
||||
nodes=sorted_nodes,
|
||||
icon=self.icon_html(),
|
||||
icon=self.icon(),
|
||||
nodes=sorted_nodes,
|
||||
updated=self.updated,
|
||||
)
|
||||
|
||||
def start_tracking_state(self, additional_nodes_to_track=[]):
|
||||
self.additional_nodes_to_track.extend(additional_nodes_to_track)
|
||||
self._tracking = True
|
||||
self.update_fleet_state()
|
||||
|
||||
def sorted(self):
|
||||
nodes_to_consider = list(self._nodes.values()) + self.additional_nodes_to_track
|
||||
return sorted(nodes_to_consider, key=lambda n: n.checksum_public_address)
|
||||
|
@ -202,7 +217,7 @@ class Learner:
|
|||
_ROUNDS_WITHOUT_NODES_AFTER_WHICH_TO_SLOW_DOWN = 10
|
||||
|
||||
# For Keeps
|
||||
__DEFAULT_NODE_STORAGE = InMemoryNodeStorage
|
||||
__DEFAULT_NODE_STORAGE = ForgetfulNodeStorage
|
||||
__DEFAULT_MIDDLEWARE_CLASS = RestMiddleware
|
||||
|
||||
LEARNER_VERSION = LEARNING_LOOP_VERSION
|
||||
|
@ -226,7 +241,6 @@ class Learner:
|
|||
learn_on_same_thread: bool = False,
|
||||
known_nodes: tuple = None,
|
||||
seed_nodes: Tuple[tuple] = None,
|
||||
known_certificates_dir: str = None,
|
||||
node_storage=None,
|
||||
save_metadata: bool = False,
|
||||
abort_on_learning_error: bool = False
|
||||
|
@ -244,7 +258,6 @@ class Learner:
|
|||
self._learning_listeners = defaultdict(list)
|
||||
self._node_ids_to_learn_about_immediately = set()
|
||||
|
||||
self.known_certificates_dir = known_certificates_dir or TemporaryDirectory("nucypher-tmp-certs-").name
|
||||
self.__known_nodes = FleetStateTracker()
|
||||
|
||||
self.done_seeding = False
|
||||
|
@ -263,8 +276,7 @@ class Learner:
|
|||
self.unresponsive_startup_nodes = list() # TODO: Attempt to use these again later
|
||||
for node in known_nodes:
|
||||
try:
|
||||
self.remember_node(
|
||||
node) # TODO: Need to test this better - do we ever init an Ursula-Learner with Node Storage?
|
||||
self.remember_node(node) # TODO: Need to test this better - do we ever init an Ursula-Learner with Node Storage?
|
||||
except self.UnresponsiveTeacher:
|
||||
self.unresponsive_startup_nodes.append(node)
|
||||
|
||||
|
@ -298,14 +310,12 @@ class Learner:
|
|||
def __attempt_seednode_learning(seednode_metadata, current_attempt=1):
|
||||
from nucypher.characters.lawful import Ursula
|
||||
self.log.debug(
|
||||
"Seeding from: {}|{}:{}".format(seednode_metadata.checksum_address,
|
||||
"Seeding from: {}|{}:{}".format(seednode_metadata.checksum_public_address,
|
||||
seednode_metadata.rest_host,
|
||||
seednode_metadata.rest_port))
|
||||
|
||||
seed_node = Ursula.from_seednode_metadata(seednode_metadata=seednode_metadata,
|
||||
network_middleware=self.network_middleware,
|
||||
certificates_directory=self.known_certificates_dir,
|
||||
timeout=timeout,
|
||||
federated_only=self.federated_only) # TODO: 466
|
||||
if seed_node is False:
|
||||
self.unresponsive_seed_nodes.add(seednode_metadata)
|
||||
|
@ -345,8 +355,11 @@ class Learner:
|
|||
# This node is already known. We can safely return.
|
||||
return False
|
||||
|
||||
node.save_certificate_to_disk(directory=self.known_certificates_dir, force=True) # TODO: Verify before force?
|
||||
certificate_filepath = node.get_certificate_filepath(certificates_dir=self.known_certificates_dir)
|
||||
# Store node's certificate - It has been seen.
|
||||
certificate_filepath = self.node_storage.store_node_certificate(checksum_address=node.checksum_public_address,
|
||||
certificate=node.certificate,
|
||||
host=node.rest_information()[0].host)
|
||||
|
||||
try:
|
||||
node.verify_node(force=force_verification_check,
|
||||
network_middleware=self.network_middleware,
|
||||
|
@ -354,6 +367,7 @@ class Learner:
|
|||
certificate_filepath=certificate_filepath)
|
||||
except SSLError:
|
||||
return False # TODO: Bucket this node as having bad TLS info - maybe it's an update that hasn't fully propagated?
|
||||
|
||||
except (requests.exceptions.ConnectionError, requests.exceptions.ReadTimeout):
|
||||
self.log.info("No Response while trying to verify node {}|{}".format(node.rest_interface, node))
|
||||
return False # TODO: Bucket this node as "ghost" or something: somebody else knows about it, but we can't get to it.
|
||||
|
@ -366,7 +380,7 @@ class Learner:
|
|||
raise RuntimeError
|
||||
|
||||
if self.save_metadata:
|
||||
self.write_node_metadata(node=node)
|
||||
self.node_storage.store_node_metadata(node=node)
|
||||
|
||||
self.log.info("Remembering {}, popping {} listeners.".format(node.checksum_public_address, len(listeners)))
|
||||
for listener in listeners:
|
||||
|
@ -593,7 +607,7 @@ class Learner:
|
|||
# Scenario 3: We don't know about this node, and neither does our friend.
|
||||
|
||||
def write_node_metadata(self, node, serializer=bytes) -> str:
|
||||
return self.node_storage.save(node=node)
|
||||
return self.node_storage.store_node_metadata(node=node)
|
||||
|
||||
def learn_from_teacher_node(self, eager=True):
|
||||
"""
|
||||
|
@ -617,6 +631,8 @@ class Learner:
|
|||
unresponsive_nodes = set()
|
||||
try:
|
||||
# TODO: Streamline path generation
|
||||
certificate_filepath = self.node_storage.generate_certificate_filepath(checksum_address=current_teacher.checksum_public_address)
|
||||
response = self.network_middleware.get_nodes_via_rest(url=rest_url,
|
||||
certificate_filepath = current_teacher.get_certificate_filepath(
|
||||
certificates_dir=self.known_certificates_dir)
|
||||
response = self.network_middleware.get_nodes_via_rest(url=teacher_uri,
|
||||
|
@ -669,8 +685,7 @@ class Learner:
|
|||
continue # This node is not serving any of our domains.
|
||||
try:
|
||||
if eager:
|
||||
certificate_filepath = current_teacher.get_certificate_filepath(
|
||||
certificates_dir=self.known_certificates_dir)
|
||||
certificate_filepath = self.node_storage.generate_certificate_filepath(checksum_address=current_teacher.checksum_public_address)
|
||||
node.verify_node(self.network_middleware,
|
||||
accept_federated_only=self.federated_only, # TODO: 466
|
||||
certificate_filepath=certificate_filepath)
|
||||
|
@ -712,7 +727,8 @@ class Teacher:
|
|||
verified_interface = False
|
||||
_verified_node = False
|
||||
_interface_info_splitter = (int, 4, {'byteorder': 'big'})
|
||||
log = Logger("network/nodes")
|
||||
log = Logger("teacher")
|
||||
__DEFAULT_MIN_SEED_STAKE = 0
|
||||
|
||||
def __init__(self,
|
||||
domains: Set,
|
||||
|
@ -753,17 +769,21 @@ class Teacher:
|
|||
Raised when deserializing a Character from a future version.
|
||||
"""
|
||||
|
||||
def seed_node_metadata(self):
|
||||
return SeednodeMetadata(self.checksum_public_address,
|
||||
self.rest_server.rest_interface.host,
|
||||
self.rest_server.rest_interface.port)
|
||||
|
||||
@classmethod
|
||||
def from_tls_hosting_power(cls, tls_hosting_power: TLSHostingPower, *args, **kwargs) -> 'Teacher':
|
||||
certificate_filepath = tls_hosting_power.keypair.certificate_filepath
|
||||
certificate = tls_hosting_power.keypair.certificate
|
||||
return cls(certificate=certificate, certificate_filepath=certificate_filepath, *args, **kwargs)
|
||||
|
||||
#
|
||||
# Known Nodes
|
||||
#
|
||||
|
||||
def seed_node_metadata(self):
|
||||
return SeednodeMetadata(self.checksum_public_address, # type: str
|
||||
self.rest_server.rest_interface.host, # type: str
|
||||
self.rest_server.rest_interface.port) # type: int
|
||||
|
||||
def sorted_nodes(self):
|
||||
nodes_to_consider = list(self.known_nodes.values()) + [self]
|
||||
return sorted(nodes_to_consider, key=lambda n: n.checksum_public_address)
|
||||
|
@ -787,6 +807,19 @@ class Teacher:
|
|||
self.fleet_state_updated = updated
|
||||
self.fleet_state_icon = icon_from_checksum(self.fleet_state_checksum,
|
||||
nickname_metadata=self.fleet_state_nickname_metadata)
|
||||
#
|
||||
# Stamp
|
||||
#
|
||||
|
||||
def _stamp_has_valid_wallet_signature(self):
|
||||
signature_bytes = self._evidence_of_decentralized_identity
|
||||
if signature_bytes is constants.NOT_SIGNED:
|
||||
return False
|
||||
else:
|
||||
signature = EthSignature(signature_bytes)
|
||||
proper_pubkey = signature.recover_public_key_from_msg(bytes(self.stamp))
|
||||
proper_address = proper_pubkey.to_checksum_address()
|
||||
return proper_address == self.checksum_public_address
|
||||
|
||||
def stamp_is_valid(self):
|
||||
"""
|
||||
|
@ -804,19 +837,6 @@ class Teacher:
|
|||
else:
|
||||
raise self.InvalidNode
|
||||
|
||||
def interface_is_valid(self):
|
||||
"""
|
||||
Checks that the interface info is valid for this node's canonical address.
|
||||
"""
|
||||
interface_info_message = self._signable_interface_info_message() # Contains canonical address.
|
||||
message = self.timestamp_bytes() + interface_info_message
|
||||
interface_is_valid = self._interface_signature.verify(message, self.public_keys(SigningPower))
|
||||
self.verified_interface = interface_is_valid
|
||||
if interface_is_valid:
|
||||
return True
|
||||
else:
|
||||
raise self.InvalidNode
|
||||
|
||||
def verify_id(self, ursula_id, digest_factory=bytes):
|
||||
self.verify()
|
||||
if not ursula_id == digest_factory(self.canonical_public_address):
|
||||
|
@ -879,12 +899,29 @@ class Teacher:
|
|||
else:
|
||||
self._verified_node = True
|
||||
|
||||
def substantiate_stamp(self, passphrase: str):
|
||||
def substantiate_stamp(self, password: str):
|
||||
blockchain_power = self._crypto_power.power_ups(BlockchainPower)
|
||||
blockchain_power.unlock_account(password=passphrase) # TODO: 349
|
||||
blockchain_power.unlock_account(password=password) # TODO: 349
|
||||
signature = blockchain_power.sign_message(bytes(self.stamp))
|
||||
self._evidence_of_decentralized_identity = signature
|
||||
|
||||
#
|
||||
# Interface
|
||||
#
|
||||
|
||||
def interface_is_valid(self):
|
||||
"""
|
||||
Checks that the interface info is valid for this node's canonical address.
|
||||
"""
|
||||
interface_info_message = self._signable_interface_info_message() # Contains canonical address.
|
||||
message = self.timestamp_bytes() + interface_info_message
|
||||
interface_is_valid = self._interface_signature.verify(message, self.public_keys(SigningPower))
|
||||
self.verified_interface = interface_is_valid
|
||||
if interface_is_valid:
|
||||
return True
|
||||
else:
|
||||
raise self.InvalidNode
|
||||
|
||||
def _signable_interface_info_message(self):
|
||||
message = self.canonical_public_address + self.rest_information()[0]
|
||||
return message
|
||||
|
@ -915,123 +952,15 @@ class Teacher:
|
|||
def timestamp_bytes(self):
|
||||
return self.timestamp.epoch.to_bytes(4, 'big')
|
||||
|
||||
@property
|
||||
def common_name(self):
|
||||
x509 = OpenSSL.crypto.X509.from_cryptography(self.certificate)
|
||||
subject_components = x509.get_subject().get_components()
|
||||
common_name_as_bytes = subject_components[0][1]
|
||||
common_name_from_cert = common_name_as_bytes.decode()
|
||||
return common_name_from_cert
|
||||
#
|
||||
# Nicknames
|
||||
#
|
||||
|
||||
@property
|
||||
def certificate_filename(self):
|
||||
return '{}.{}'.format(self.checksum_public_address, Encoding.PEM.name.lower()) # TODO: use cert's encoding..?
|
||||
|
||||
def get_certificate_filepath(self, certificates_dir: str) -> str:
|
||||
return os.path.join(certificates_dir, self.certificate_filename)
|
||||
|
||||
def save_certificate_to_disk(self, directory, force=False):
|
||||
x509 = OpenSSL.crypto.X509.from_cryptography(self.certificate)
|
||||
subject_components = x509.get_subject().get_components()
|
||||
common_name_as_bytes = subject_components[0][1]
|
||||
common_name_from_cert = common_name_as_bytes.decode()
|
||||
|
||||
if not self.rest_information()[0].host == common_name_from_cert:
|
||||
# TODO: It's better for us to have checked this a while ago so that this situation is impossible. #443
|
||||
raise ValueError("You passed a common_name that is not the same one as the cert. "
|
||||
"Common name is optional; the cert will be saved according to "
|
||||
"the name on the cert itself.")
|
||||
|
||||
certificate_filepath = self.get_certificate_filepath(certificates_dir=directory)
|
||||
_write_tls_certificate(self.certificate, full_filepath=certificate_filepath, force=force)
|
||||
self.certificate_filepath = certificate_filepath
|
||||
self.log.info("Saved TLS certificate for {}: {}".format(self, certificate_filepath))
|
||||
|
||||
@classmethod
|
||||
def from_seednode_metadata(cls,
|
||||
seednode_metadata,
|
||||
*args,
|
||||
**kwargs):
|
||||
"""
|
||||
Essentially another deserialization method, but this one doesn't reconstruct a complete
|
||||
node from bytes; instead it's just enough to connect to and verify a node.
|
||||
"""
|
||||
|
||||
return cls.from_seed_and_stake_info(checksum_address=seednode_metadata.checksum_address,
|
||||
host=seednode_metadata.rest_host,
|
||||
port=seednode_metadata.rest_port,
|
||||
*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def from_seed_and_stake_info(cls, host,
|
||||
certificates_directory,
|
||||
federated_only,
|
||||
port=9151,
|
||||
checksum_address=None,
|
||||
minimum_stake=0,
|
||||
network_middleware=None,
|
||||
*args,
|
||||
**kwargs
|
||||
):
|
||||
if network_middleware is None:
|
||||
network_middleware = RestMiddleware()
|
||||
|
||||
certificate = network_middleware.get_certificate(host=host, port=port)
|
||||
|
||||
real_host = certificate.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value
|
||||
# Write certificate; this is really only for temporary purposes. Ideally, we'd use
|
||||
# it in-memory here but there's no obvious way to do that.
|
||||
filename = '{}.{}'.format(checksum_address, Encoding.PEM.name.lower())
|
||||
certificate_filepath = os.path.join(certificates_directory, filename)
|
||||
_write_tls_certificate(certificate=certificate, full_filepath=certificate_filepath, force=True)
|
||||
cls.log.info("Saved seednode {} TLS certificate".format(checksum_address))
|
||||
|
||||
potential_seed_node = cls.from_rest_url(
|
||||
host=real_host,
|
||||
port=port,
|
||||
network_middleware=network_middleware,
|
||||
certificate_filepath=certificate_filepath,
|
||||
federated_only=True,
|
||||
*args,
|
||||
**kwargs) # TODO: 466
|
||||
|
||||
if checksum_address:
|
||||
if not checksum_address == potential_seed_node.checksum_public_address:
|
||||
raise potential_seed_node.SuspiciousActivity(
|
||||
"This seed node has a different wallet address: {} (was hoping for {}). Are you sure this is a seed node?".format(
|
||||
potential_seed_node.checksum_public_address,
|
||||
checksum_address))
|
||||
else:
|
||||
if minimum_stake > 0:
|
||||
# TODO: check the blockchain to verify that address has more then minimum_stake. #511
|
||||
raise NotImplementedError("Stake checking is not implemented yet.")
|
||||
try:
|
||||
potential_seed_node.verify_node(
|
||||
network_middleware=network_middleware,
|
||||
accept_federated_only=federated_only,
|
||||
certificate_filepath=certificate_filepath)
|
||||
except potential_seed_node.InvalidNode:
|
||||
raise # TODO: What if our seed node fails verification?
|
||||
return potential_seed_node
|
||||
|
||||
@classmethod
|
||||
def from_rest_url(cls,
|
||||
network_middleware: RestMiddleware,
|
||||
host: str,
|
||||
port: int,
|
||||
certificate_filepath,
|
||||
federated_only: bool = False,
|
||||
*args,
|
||||
**kwargs):
|
||||
|
||||
response = network_middleware.node_information(host, port, certificate_filepath=certificate_filepath)
|
||||
if not response.status_code == 200:
|
||||
raise RuntimeError("Got a bad response: {}".format(response))
|
||||
|
||||
stranger_ursula_from_public_keys = cls.from_bytes(response.content, federated_only=federated_only)
|
||||
return stranger_ursula_from_public_keys
|
||||
|
||||
def nickname_icon(self):
|
||||
return '{} {}'.format(self.nickname_metadata[0][1], self.nickname_metadata[1][1])
|
||||
|
||||
def nickname_icon_html(self):
|
||||
icon_template = """
|
||||
<div class="nucypher-nickname-icon" style="border-top-color:{first_color}; border-left-color:{first_color}; border-bottom-color:{second_color}; border-right-color:{second_color};">
|
||||
<span class="small">{node_class} v{version}</span>
|
||||
|
|
|
@ -14,6 +14,12 @@ GNU General Public License for more details.
|
|||
You should have received a copy of the GNU General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from eth_utils import is_checksum_address
|
||||
|
||||
from bytestring_splitter import VariableLengthBytestring
|
||||
|
||||
|
||||
|
@ -21,6 +27,26 @@ class SuspiciousActivity(RuntimeError):
|
|||
"""raised when an action appears to amount to malicious conduct."""
|
||||
|
||||
|
||||
def parse_node_uri(uri: str):
|
||||
from nucypher.config.characters import UrsulaConfiguration
|
||||
|
||||
if '@' in uri:
|
||||
checksum_address, uri = uri.split("@")
|
||||
if not is_checksum_address(checksum_address):
|
||||
raise ValueError("{} is not a valid checksum address.".format(checksum_address))
|
||||
else:
|
||||
checksum_address = None # federated
|
||||
|
||||
# HTTPS Explicit Required
|
||||
parsed_uri = urlparse(uri)
|
||||
if not parsed_uri.scheme == "https":
|
||||
raise ValueError("Invalid teacher URI. Is the hostname prefixed with 'https://' ?")
|
||||
|
||||
hostname = parsed_uri.hostname
|
||||
port = parsed_uri.port or UrsulaConfiguration.DEFAULT_REST_PORT
|
||||
return hostname, port, checksum_address
|
||||
|
||||
|
||||
class InterfaceInfo:
|
||||
expected_bytes_length = lambda: VariableLengthBytestring
|
||||
|
||||
|
|
|
@ -18,6 +18,8 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
|||
|
||||
import binascii
|
||||
import os
|
||||
from typing import Callable
|
||||
|
||||
from twisted.logger import Logger
|
||||
|
||||
from apistar import Route, App
|
||||
|
@ -26,6 +28,9 @@ from bytestring_splitter import VariableLengthBytestring
|
|||
from constant_sorrow import constants
|
||||
from constant_sorrow.constants import GLOBAL_DOMAIN
|
||||
from hendrix.experience import crosstown_traffic
|
||||
from nucypher.config.storages import ForgetfulNodeStorage
|
||||
from nucypher.crypto.signing import SignatureStamp
|
||||
from nucypher.network.middleware import RestMiddleware
|
||||
from umbral import pre
|
||||
from umbral.fragments import KFrag
|
||||
from umbral.keys import UmbralPublicKey
|
||||
|
@ -39,15 +44,16 @@ from nucypher.keystore.keystore import NotFound
|
|||
from nucypher.keystore.threading import ThreadedSession
|
||||
from nucypher.network import LEARNING_LOOP_VERSION
|
||||
from nucypher.network.protocols import InterfaceInfo
|
||||
from jinja2 import Template
|
||||
|
||||
from jinja2 import Template, TemplateError
|
||||
|
||||
HERE = BASE_DIR = os.path.abspath(os.path.dirname(__file__))
|
||||
TEMPLATES_DIR = os.path.join(HERE, "templates")
|
||||
|
||||
|
||||
class ProxyRESTServer:
|
||||
log = Logger("characters")
|
||||
SERVER_VERSION = LEARNING_LOOP_VERSION
|
||||
log = Logger("network-server")
|
||||
|
||||
def __init__(self,
|
||||
rest_host: str,
|
||||
|
@ -71,27 +77,28 @@ class ProxyRESTServer:
|
|||
|
||||
|
||||
class ProxyRESTRoutes:
|
||||
log = Logger("characters")
|
||||
log = Logger("network-server")
|
||||
|
||||
def __init__(self,
|
||||
db_name,
|
||||
db_filepath,
|
||||
network_middleware,
|
||||
federated_only,
|
||||
treasure_map_tracker,
|
||||
node_tracker,
|
||||
node_bytes_caster,
|
||||
work_order_tracker,
|
||||
node_recorder,
|
||||
stamp,
|
||||
verifier,
|
||||
suspicious_activity_tracker,
|
||||
certificate_dir,
|
||||
db_filepath: str,
|
||||
network_middleware: RestMiddleware,
|
||||
federated_only: bool,
|
||||
treasure_map_tracker: dict,
|
||||
node_tracker: 'FleetStateTracker',
|
||||
node_bytes_caster: Callable,
|
||||
work_order_tracker: list,
|
||||
node_recorder: Callable,
|
||||
stamp: SignatureStamp,
|
||||
verifier: Callable,
|
||||
suspicious_activity_tracker: dict,
|
||||
serving_domains,
|
||||
) -> None:
|
||||
|
||||
self.network_middleware = network_middleware
|
||||
self.federated_only = federated_only
|
||||
self.datastore = None
|
||||
|
||||
self.__forgetful_node_storage = ForgetfulNodeStorage(federated_only=federated_only)
|
||||
|
||||
self._treasure_map_tracker = treasure_map_tracker
|
||||
self._work_order_tracker = work_order_tracker
|
||||
|
@ -136,7 +143,6 @@ class ProxyRESTRoutes:
|
|||
]
|
||||
|
||||
self.rest_app = App(routes=routes)
|
||||
self.db_name = db_name
|
||||
self.db_filepath = db_filepath
|
||||
|
||||
from nucypher.keystore import keystore
|
||||
|
@ -144,7 +150,11 @@ class ProxyRESTRoutes:
|
|||
from sqlalchemy.engine import create_engine
|
||||
|
||||
self.log.info("Starting datastore {}".format(self.db_filepath))
|
||||
engine = create_engine('sqlite:///{}'.format(self.db_filepath))
|
||||
|
||||
# See: https://docs.sqlalchemy.org/en/rel_0_9/dialects/sqlite.html#connect-strings
|
||||
db_filepath = (self.db_filepath or '') # Capture None
|
||||
engine = create_engine('sqlite:///{}'.format(db_filepath))
|
||||
|
||||
Base.metadata.create_all(engine)
|
||||
self.datastore = keystore.KeyStore(engine)
|
||||
self.db_engine = engine
|
||||
|
@ -190,11 +200,10 @@ class ProxyRESTRoutes:
|
|||
signature = self._stamp(payload)
|
||||
return Response(bytes(signature) + payload, headers=headers, status_code=204)
|
||||
|
||||
nodes = self._node_class.batch_from_bytes(request.body,
|
||||
federated_only=self.federated_only, # TODO: 466
|
||||
)
|
||||
nodes = self._node_class.batch_from_bytes(request.body, federated_only=self.federated_only) # TODO: 466
|
||||
|
||||
# TODO: This logic is basically repeated in learn_from_teacher_node and remember_node. Let's find a better way. 555
|
||||
# TODO: This logic is basically repeated in learn_from_teacher_node and remember_node.
|
||||
# Let's find a better way. #555
|
||||
for node in nodes:
|
||||
if GLOBAL_DOMAIN not in self.serving_domains:
|
||||
if not self.serving_domains.intersection(node.serving_domains):
|
||||
|
@ -206,23 +215,35 @@ class ProxyRESTRoutes:
|
|||
@crosstown_traffic()
|
||||
def learn_about_announced_nodes():
|
||||
try:
|
||||
certificate_filepath = node.get_certificate_filepath(certificates_dir=self._certificate_dir) # TODO: integrate with recorder?
|
||||
node.save_certificate_to_disk(directory=self._certificate_dir, force=True)
|
||||
temp_certificate_filepath = self.__forgetful_node_storage.store_node_certificate(checksum_address=node.checksum_public_address,
|
||||
certificate=node.certificate)
|
||||
node.verify_node(self.network_middleware,
|
||||
accept_federated_only=self.federated_only, # TODO: 466
|
||||
certificate_filepath=certificate_filepath)
|
||||
certificate_filepath=temp_certificate_filepath)
|
||||
|
||||
# Suspicion
|
||||
except node.SuspiciousActivity:
|
||||
# TODO: Include data about caller?
|
||||
# TODO: Account for possibility that stamp, rather than interface, was bad.
|
||||
message = "Suspicious Activity: Discovered node with bad signature: {}. " \
|
||||
" Announced via REST." # TODO: Include data about caller?
|
||||
# TODO: Maybe also record the bytes representation separately to disk?
|
||||
message = "Suspicious Activity: Discovered node with bad signature: {}. Announced via REST."
|
||||
self.log.warn(message)
|
||||
self._suspicious_activity_tracker['vladimirs'].append(node) # TODO: Maybe also record the bytes representation separately to disk?
|
||||
self._suspicious_activity_tracker['vladimirs'].append(node)
|
||||
|
||||
# Async Sentinel
|
||||
except Exception as e:
|
||||
self.log.critical(str(e))
|
||||
raise # TODO
|
||||
raise
|
||||
|
||||
# Believable
|
||||
else:
|
||||
self.log.info("Learned about previously unknown node: {}".format(node))
|
||||
self._node_recorder(node)
|
||||
# TODO: Record new fleet state
|
||||
|
||||
# Cleanup
|
||||
finally:
|
||||
self.__forgetful_node_storage.forget(everything=True)
|
||||
|
||||
# TODO: What's the right status code here? 202? Different if we already knew about the node?
|
||||
return self.all_known_nodes(request)
|
||||
|
@ -391,13 +412,22 @@ class ProxyRESTRoutes:
|
|||
assert False
|
||||
|
||||
def status(self, request: Request):
|
||||
headers = {"Content-Type": "text/html", "charset":"utf-8"}
|
||||
# TODO: Seems very strange to deserialize *this node* when we can just pass it in. Might be a sign that we need to rethnk this composition.
|
||||
# TODO: Seems very strange to deserialize *this node* when we can just pass it in.
|
||||
# Might be a sign that we need to rethnk this composition.
|
||||
|
||||
headers = {"Content-Type": "text/html", "charset": "utf-8"}
|
||||
this_node = self._node_class.from_bytes(self._node_bytes_caster(), federated_only=self.federated_only)
|
||||
content = self._status_template.render(known_nodes=self._node_tracker,
|
||||
this_node=this_node,
|
||||
domains=[str(d) for d in self.serving_domains],
|
||||
previous_states=list(reversed(self._node_tracker.states.values()))[:5])
|
||||
|
||||
previous_states = list(reversed(self._node_tracker.states.values()))[:5]
|
||||
|
||||
try:
|
||||
content = self._status_template.render(this_node=this_node,
|
||||
known_nodes=self._node_tracker,
|
||||
previous_states=previous_states)
|
||||
except Exception as e:
|
||||
self.log.debug("Template Rendering Exception: ".format(str(e)))
|
||||
raise TemplateError(str(e)) from e
|
||||
|
||||
return Response(content=content, headers=headers)
|
||||
|
||||
|
||||
|
|
|
@ -66,6 +66,36 @@
|
|||
font-size: 2em;
|
||||
}
|
||||
|
||||
#known-nodes {
|
||||
float:left;
|
||||
clear:left;
|
||||
}
|
||||
.small-address {
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.state {
|
||||
float:left;
|
||||
}
|
||||
#previous-states {
|
||||
float:left;
|
||||
clear:left;
|
||||
}
|
||||
|
||||
#previous-states .state {
|
||||
margin:left: 10px;
|
||||
border-right: 3px solid black;
|
||||
}
|
||||
|
||||
#previous-states .nucypher-nickname-icon {
|
||||
height:75px;
|
||||
width: 75px;
|
||||
}
|
||||
|
||||
#previous-states .single-symbol {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
#known-nodes {
|
||||
float:left;
|
||||
clear:left;
|
||||
|
@ -74,13 +104,14 @@
|
|||
|
||||
<div id="this-node">
|
||||
<h2>{{ this_node.nickname}}</h2>
|
||||
{{ this_node.nickname_icon }}
|
||||
{{ this_node.nickname_icon() }}
|
||||
<h4>Domains: {% for domain in domains %}{{ domain }} {% endfor %}</h4>
|
||||
|
||||
<h3>Fleet State</h3>
|
||||
<div class="state">
|
||||
<h4>{{ known_nodes.nickname }}</h4>
|
||||
{{ known_nodes.icon() }}
|
||||
{{ known_nodes.icon }}
|
||||
<br/>
|
||||
<span class="small">{{ known_nodes.updated }}</span>
|
||||
</ul>
|
||||
|
@ -110,7 +141,7 @@
|
|||
</thead>
|
||||
{% for node in known_nodes -%}
|
||||
<tr>
|
||||
<td>{{ node.nickname_icon() }}</td>
|
||||
<td>{{ node.nickname_icon }}</td>
|
||||
<td>
|
||||
<a href="https://{{ node.rest_url()}}/status">{{ node.nickname }}</a>
|
||||
<br/><span class="small">{{ node.checksum_public_address }}</span>
|
||||
|
|
|
@ -256,7 +256,7 @@ class Policy:
|
|||
return self.publish(network_middleware)
|
||||
|
||||
def consider_arrangement(self, network_middleware, ursula, arrangement):
|
||||
certificate_filepath = ursula.get_certificate_filepath(certificates_dir=self.alice.known_certificates_dir)
|
||||
certificate_filepath = ursula.node_storage.generate_certificate_filepath(checksum_address=arrangement.ursula.checksum_public_address)
|
||||
try:
|
||||
ursula.verify_node(network_middleware,
|
||||
accept_federated_only=arrangement.federated,
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
import functools
|
||||
|
||||
from twisted.logger import Logger
|
||||
from typing import Callable
|
||||
import inspect
|
||||
import eth_utils
|
||||
|
||||
|
||||
def validate_checksum_address(func: Callable) -> Callable:
|
||||
"""
|
||||
EIP-55 Checksum address validation decorator.
|
||||
|
||||
Inspects the decorated function for an input parameter "checksum_address",
|
||||
then uses `eth_utils` to validate the address EIP-55 checksum,
|
||||
verifying the input type on failure; Raises TypeError
|
||||
or InvalidChecksumAddress if validation fails, respectively.
|
||||
|
||||
EIP-55 Specification: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-55.md
|
||||
ETH Utils Implementation: https://github.com/ethereum/eth-utils
|
||||
|
||||
"""
|
||||
|
||||
parameter_name = 'checksum_address'
|
||||
log = Logger('EIP-55-validator')
|
||||
|
||||
class InvalidChecksumAddress(eth_utils.exceptions.ValidationError):
|
||||
pass
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapped(*args, **kwargs):
|
||||
|
||||
# Check for the presence of a checksum address in this call
|
||||
params = inspect.getcallargs(func, *args, **kwargs)
|
||||
try:
|
||||
checksum_address = params[parameter_name]
|
||||
|
||||
# No checksum_address present in this call
|
||||
except KeyError:
|
||||
return func(*args, **kwargs) # ... don't mind me!
|
||||
|
||||
# Optional checksum_address present in this call
|
||||
signature = inspect.signature(func)
|
||||
checksum_address_is_optional = signature.parameters[parameter_name].default is None
|
||||
if checksum_address_is_optional and checksum_address is None:
|
||||
return func(*args, **kwargs) # ... nothing to validate
|
||||
|
||||
# Validate
|
||||
address_is_valid = eth_utils.is_checksum_address(checksum_address)
|
||||
|
||||
# OK!
|
||||
if address_is_valid:
|
||||
return func(*args, **kwargs)
|
||||
|
||||
# Invalid Type
|
||||
if not isinstance(checksum_address, str):
|
||||
actual_type_name = checksum_address.__class__.__name__
|
||||
message = '{} is an invalid type for parameter "{}".'.format(actual_type_name, parameter_name)
|
||||
raise TypeError(message)
|
||||
|
||||
# Invalid Value
|
||||
message = '"{}" is not a valid EIP-55 checksum address.'.format(checksum_address)
|
||||
log.debug(message)
|
||||
raise InvalidChecksumAddress(message)
|
||||
|
||||
return wrapped
|
|
@ -19,15 +19,32 @@ import datetime
|
|||
import pathlib
|
||||
|
||||
from sentry_sdk import capture_exception, add_breadcrumb
|
||||
from sentry_sdk.integrations.logging import LoggingIntegration
|
||||
from twisted.logger import FileLogObserver, jsonFileLogObserver
|
||||
from twisted.logger import ILogObserver
|
||||
from twisted.logger import LogLevel
|
||||
from twisted.python.logfile import DailyLogFile
|
||||
from zope.interface import provider
|
||||
|
||||
import nucypher
|
||||
from nucypher.config.constants import USER_LOG_DIR
|
||||
|
||||
|
||||
def initialize_sentry(dsn: str):
|
||||
import sentry_sdk
|
||||
import logging
|
||||
|
||||
sentry_logging = LoggingIntegration(
|
||||
level=logging.INFO, # Capture info and above as breadcrumbs
|
||||
event_level=logging.DEBUG # Send debug logs as events
|
||||
)
|
||||
sentry_sdk.init(
|
||||
dsn=dsn,
|
||||
integrations=[sentry_logging],
|
||||
release=nucypher.__version__
|
||||
)
|
||||
|
||||
|
||||
def formatUrsulaLogEvent(event):
|
||||
"""
|
||||
Format log lines for file logging.
|
||||
|
|
|
@ -24,8 +24,8 @@ from web3.middleware import geth_poa_middleware
|
|||
from nucypher.blockchain.eth import constants
|
||||
from nucypher.blockchain.eth.chains import Blockchain
|
||||
from nucypher.utilities.sandbox.constants import (DEVELOPMENT_ETH_AIRDROP_AMOUNT,
|
||||
DEFAULT_NUMBER_OF_URSULAS_IN_DEVELOPMENT_NETWORK,
|
||||
TEST_URSULA_INSECURE_DEVELOPMENT_PASSWORD)
|
||||
NUMBER_OF_URSULAS_IN_DEVELOPMENT_NETWORK,
|
||||
INSECURE_DEVELOPMENT_PASSWORD)
|
||||
|
||||
|
||||
def token_airdrop(token_agent, amount: int, origin: str, addresses: List[str]):
|
||||
|
@ -62,11 +62,11 @@ class TesterBlockchain(Blockchain):
|
|||
w3.middleware_stack.inject(geth_poa_middleware, layer=0)
|
||||
|
||||
# Generate additional ethereum accounts for testing
|
||||
enough_accounts = len(self.interface.w3.eth.accounts) >= DEFAULT_NUMBER_OF_URSULAS_IN_DEVELOPMENT_NETWORK
|
||||
enough_accounts = len(self.interface.w3.eth.accounts) >= NUMBER_OF_URSULAS_IN_DEVELOPMENT_NETWORK
|
||||
if test_accounts is not None and not enough_accounts:
|
||||
|
||||
accounts_to_make = DEFAULT_NUMBER_OF_URSULAS_IN_DEVELOPMENT_NETWORK - len(self.interface.w3.eth.accounts)
|
||||
test_accounts = test_accounts if test_accounts is not None else DEFAULT_NUMBER_OF_URSULAS_IN_DEVELOPMENT_NETWORK
|
||||
accounts_to_make = NUMBER_OF_URSULAS_IN_DEVELOPMENT_NETWORK - len(self.interface.w3.eth.accounts)
|
||||
test_accounts = test_accounts if test_accounts is not None else NUMBER_OF_URSULAS_IN_DEVELOPMENT_NETWORK
|
||||
|
||||
self.__generate_insecure_unlocked_accounts(quantity=accounts_to_make)
|
||||
|
||||
|
@ -84,14 +84,14 @@ class TesterBlockchain(Blockchain):
|
|||
Generate additional unlocked accounts transferring a balance to each account on creation.
|
||||
"""
|
||||
addresses = list()
|
||||
insecure_passphrase = TEST_URSULA_INSECURE_DEVELOPMENT_PASSWORD
|
||||
insecure_password = INSECURE_DEVELOPMENT_PASSWORD
|
||||
for _ in range(quantity):
|
||||
|
||||
umbral_priv_key = UmbralPrivateKey.gen_key()
|
||||
address = self.interface.w3.personal.importRawKey(private_key=umbral_priv_key.to_bytes(),
|
||||
passphrase=insecure_passphrase)
|
||||
password=insecure_password)
|
||||
|
||||
assert self.interface.unlock_account(address, password=insecure_passphrase, duration=None), 'Failed to unlock {}'.format(address)
|
||||
assert self.interface.unlock_account(address, password=insecure_password, duration=None), 'Failed to unlock {}'.format(address)
|
||||
addresses.append(address)
|
||||
self._test_account_cache.append(address)
|
||||
self.log.info('Generated new insecure account {}'.format(address))
|
||||
|
|
|
@ -14,16 +14,19 @@ GNU General Public License for more details.
|
|||
You should have received a copy of the GNU General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
|
||||
import os
|
||||
|
||||
from nucypher.blockchain.eth.constants import DISPATCHER_SECRET_LENGTH, M
|
||||
from nucypher.config.constants import DEFAULT_CONFIG_ROOT
|
||||
|
||||
TEST_KNOWN_URSULAS_CACHE = {}
|
||||
|
||||
TEST_URSULA_STARTING_PORT = 7468
|
||||
MOCK_KNOWN_URSULAS_CACHE = {}
|
||||
|
||||
DEFAULT_NUMBER_OF_URSULAS_IN_DEVELOPMENT_NETWORK = 10
|
||||
MOCK_URSULA_STARTING_PORT = 49152
|
||||
|
||||
NUMBER_OF_URSULAS_IN_DEVELOPMENT_NETWORK = 10
|
||||
|
||||
DEVELOPMENT_TOKEN_AIRDROP_AMOUNT = 1000000 * int(M)
|
||||
|
||||
|
@ -33,6 +36,14 @@ MINERS_ESCROW_DEPLOYMENT_SECRET = os.urandom(DISPATCHER_SECRET_LENGTH)
|
|||
|
||||
POLICY_MANAGER_DEPLOYMENT_SECRET = os.urandom(DISPATCHER_SECRET_LENGTH)
|
||||
|
||||
TEST_URSULA_INSECURE_DEVELOPMENT_PASSWORD = 'this-is-not-a-secure-password'
|
||||
INSECURE_DEVELOPMENT_PASSWORD = 'this-is-not-a-secure-password'
|
||||
|
||||
DEFAULT_SIMULATION_REGISTRY_FILEPATH = os.path.join(DEFAULT_CONFIG_ROOT, 'simulated_registry.json')
|
||||
MAX_TEST_SEEDER_ENTRIES = 20
|
||||
|
||||
MOCK_IP_ADDRESS = '0.0.0.0'
|
||||
|
||||
MOCK_IP_ADDRESS_2 = '10.10.10.10'
|
||||
|
||||
MOCK_URSULA_DB_FILEPATH = ':memory:'
|
||||
|
||||
MOCK_CUSTOM_INSTALLATION_PATH = '/tmp/nucypher-tmp-test-custom'
|
||||
|
|
|
@ -22,7 +22,7 @@ from bytestring_splitter import VariableLengthBytestring
|
|||
from nucypher.characters.lawful import Ursula
|
||||
from nucypher.crypto.kits import RevocationKit
|
||||
from nucypher.network.middleware import RestMiddleware
|
||||
from nucypher.utilities.sandbox.constants import TEST_KNOWN_URSULAS_CACHE
|
||||
from nucypher.utilities.sandbox.constants import MOCK_KNOWN_URSULAS_CACHE
|
||||
|
||||
|
||||
class MockRestMiddleware(RestMiddleware):
|
||||
|
@ -47,13 +47,12 @@ class MockRestMiddleware(RestMiddleware):
|
|||
|
||||
def _get_ursula_by_port(self, port):
|
||||
try:
|
||||
return TEST_KNOWN_URSULAS_CACHE[port]
|
||||
return MOCK_KNOWN_URSULAS_CACHE[port]
|
||||
except KeyError:
|
||||
raise RuntimeError(
|
||||
"Can't find an Ursula with port {} - did you spin up the right test ursulas?".format(port))
|
||||
|
||||
def get_certificate(self, host, port, timeout=3, retry_attempts: int = 3,
|
||||
retry_rate: int = 2, ):
|
||||
def get_certificate(self, host, port, timeout=3, retry_attempts: int = 3, retry_rate: int = 2, current_attempt: int = 0):
|
||||
ursula = self._get_ursula_by_port(port)
|
||||
return ursula.certificate
|
||||
|
||||
|
|
|
@ -14,45 +14,38 @@ GNU General Public License for more details.
|
|||
You should have received a copy of the GNU General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
import os
|
||||
|
||||
import random
|
||||
import sys
|
||||
|
||||
import maya
|
||||
import time
|
||||
from os import linesep
|
||||
|
||||
import click
|
||||
from eth_utils import to_checksum_address
|
||||
from twisted.internet import reactor
|
||||
from twisted.protocols.basic import LineReceiver
|
||||
from typing import Set, Union
|
||||
from typing import Union, Set
|
||||
|
||||
from nucypher.blockchain.eth import constants
|
||||
from nucypher.blockchain.eth.constants import MIN_ALLOWED_LOCKED, MIN_LOCKED_PERIODS, MAX_MINTING_PERIODS
|
||||
from nucypher.characters.lawful import Ursula
|
||||
from nucypher.config.characters import UrsulaConfiguration
|
||||
from nucypher.config.constants import SEEDNODES
|
||||
from nucypher.crypto.api import secure_random
|
||||
from nucypher.utilities.sandbox.constants import (DEFAULT_NUMBER_OF_URSULAS_IN_DEVELOPMENT_NETWORK,
|
||||
TEST_URSULA_STARTING_PORT,
|
||||
TEST_KNOWN_URSULAS_CACHE)
|
||||
from nucypher.utilities.sandbox.constants import (
|
||||
MOCK_KNOWN_URSULAS_CACHE,
|
||||
MOCK_URSULA_STARTING_PORT,
|
||||
NUMBER_OF_URSULAS_IN_DEVELOPMENT_NETWORK,
|
||||
MOCK_URSULA_DB_FILEPATH)
|
||||
|
||||
|
||||
def make_federated_ursulas(ursula_config: UrsulaConfiguration,
|
||||
quantity: int = DEFAULT_NUMBER_OF_URSULAS_IN_DEVELOPMENT_NETWORK,
|
||||
quantity: int = NUMBER_OF_URSULAS_IN_DEVELOPMENT_NETWORK,
|
||||
know_each_other: bool = True,
|
||||
**ursula_overrides) -> Set[Ursula]:
|
||||
|
||||
if not TEST_KNOWN_URSULAS_CACHE:
|
||||
starting_port = TEST_URSULA_STARTING_PORT
|
||||
if not MOCK_KNOWN_URSULAS_CACHE:
|
||||
starting_port = MOCK_URSULA_STARTING_PORT
|
||||
else:
|
||||
starting_port = max(TEST_KNOWN_URSULAS_CACHE.keys()) + 1
|
||||
starting_port = max(MOCK_KNOWN_URSULAS_CACHE.keys()) + 1
|
||||
|
||||
federated_ursulas = set()
|
||||
for port in range(starting_port, starting_port+quantity):
|
||||
|
||||
ursula = ursula_config.produce(rest_port=port + 100,
|
||||
db_name="test-{}".format(port),
|
||||
db_filepath=MOCK_URSULA_DB_FILEPATH,
|
||||
**ursula_overrides)
|
||||
|
||||
federated_ursulas.add(ursula)
|
||||
|
@ -60,7 +53,7 @@ def make_federated_ursulas(ursula_config: UrsulaConfiguration,
|
|||
# Store this Ursula in our global testing cache.
|
||||
|
||||
port = ursula.rest_information()[0].port
|
||||
TEST_KNOWN_URSULAS_CACHE[port] = ursula
|
||||
MOCK_KNOWN_URSULAS_CACHE[port] = ursula
|
||||
|
||||
if know_each_other:
|
||||
|
||||
|
@ -82,25 +75,25 @@ def make_decentralized_ursulas(ursula_config: UrsulaConfiguration,
|
|||
if isinstance(ether_addresses, int):
|
||||
ether_addresses = [to_checksum_address(secure_random(20)) for _ in range(ether_addresses)]
|
||||
|
||||
if not TEST_KNOWN_URSULAS_CACHE:
|
||||
starting_port = TEST_URSULA_STARTING_PORT
|
||||
if not MOCK_KNOWN_URSULAS_CACHE:
|
||||
starting_port = MOCK_URSULA_STARTING_PORT
|
||||
else:
|
||||
starting_port = max(TEST_KNOWN_URSULAS_CACHE.keys()) + 1
|
||||
starting_port = max(MOCK_KNOWN_URSULAS_CACHE.keys()) + 1
|
||||
|
||||
ursulas = set()
|
||||
for port, checksum_address in enumerate(ether_addresses, start=starting_port):
|
||||
|
||||
ursula = ursula_config.produce(checksum_address=checksum_address,
|
||||
db_name="test-{}".format(port),
|
||||
ursula = ursula_config.produce(checksum_public_address=checksum_address,
|
||||
db_filepath=MOCK_URSULA_DB_FILEPATH,
|
||||
rest_port=port + 100,
|
||||
**ursula_overrides)
|
||||
if stake is True:
|
||||
|
||||
min_stake, balance = int(constants.MIN_ALLOWED_LOCKED), ursula.token_balance
|
||||
min_stake, balance = MIN_ALLOWED_LOCKED, ursula.token_balance
|
||||
amount = random.randint(min_stake, balance)
|
||||
|
||||
# for a random lock duration
|
||||
min_locktime, max_locktime = int(constants.MIN_LOCKED_PERIODS), int(constants.MAX_MINTING_PERIODS)
|
||||
min_locktime, max_locktime = MIN_LOCKED_PERIODS, MAX_MINTING_PERIODS
|
||||
periods = random.randint(min_locktime, max_locktime)
|
||||
|
||||
ursula.initialize_stake(amount=amount, lock_periods=periods)
|
||||
|
@ -108,7 +101,7 @@ def make_decentralized_ursulas(ursula_config: UrsulaConfiguration,
|
|||
ursulas.add(ursula)
|
||||
# Store this Ursula in our global cache.
|
||||
port = ursula.rest_information()[0].port
|
||||
TEST_KNOWN_URSULAS_CACHE[port] = ursula
|
||||
MOCK_KNOWN_URSULAS_CACHE[port] = ursula
|
||||
|
||||
if know_each_other:
|
||||
|
||||
|
@ -119,116 +112,3 @@ def make_decentralized_ursulas(ursula_config: UrsulaConfiguration,
|
|||
|
||||
return ursulas
|
||||
|
||||
|
||||
class UrsulaCommandProtocol(LineReceiver):
|
||||
|
||||
delimiter = linesep.encode("ascii")
|
||||
encoding = 'utf-8'
|
||||
|
||||
width = 80
|
||||
height = 24
|
||||
|
||||
commands = (
|
||||
'status',
|
||||
'stop',
|
||||
|
||||
)
|
||||
|
||||
def __init__(self, ursula):
|
||||
self.ursula = ursula
|
||||
self.start_time = maya.now()
|
||||
super().__init__()
|
||||
|
||||
def _paint_known_nodes(self):
|
||||
# Gather Data
|
||||
known_nodes = self.ursula.known_nodes
|
||||
known_certificate_files = os.listdir(self.ursula.known_certificates_dir)
|
||||
number_of_known_nodes = len(known_nodes)
|
||||
seen_nodes = len(known_certificate_files)
|
||||
|
||||
# Operating Mode
|
||||
federated_only = self.ursula.federated_only
|
||||
if federated_only:
|
||||
click.secho("Configured in Federated Only mode", fg='green')
|
||||
|
||||
# Heading
|
||||
label = "Known Nodes (connected {} / seen {})".format(number_of_known_nodes, seen_nodes)
|
||||
heading = '\n' + label + " " * (45 - len(label)) + "Last Seen "
|
||||
click.secho(heading, bold=True, nl=False)
|
||||
|
||||
# Legend
|
||||
color_index = {
|
||||
'self': 'yellow',
|
||||
'known': 'white',
|
||||
'seednode': 'blue'
|
||||
}
|
||||
for node_type, color in color_index.items():
|
||||
click.secho('{0:<6} | '.format(node_type), fg=color, nl=False)
|
||||
click.echo('\n')
|
||||
|
||||
seednode_addresses = list(bn.checksum_address for bn in SEEDNODES)
|
||||
for address, node in known_nodes.items():
|
||||
row_template = "{} | {} | {} | {} | {}"
|
||||
node_type = 'known'
|
||||
if node.checksum_public_address == self.ursula.checksum_public_address:
|
||||
node_type = 'self'
|
||||
row_template += ' ({})'.format(node_type)
|
||||
elif node.checksum_public_address in seednode_addresses:
|
||||
node_type = 'seednode'
|
||||
row_template += ' ({})'.format(node_type)
|
||||
click.secho(row_template.format(node.checksum_public_address,
|
||||
node.rest_url().ljust(20), # TODO: Maybe put this 20 somewhere
|
||||
node.nickname.ljust(50),
|
||||
node.timestamp,
|
||||
node.last_seen,
|
||||
), fg=color_index[node_type])
|
||||
|
||||
def paintStatus(self):
|
||||
stats = ['Ursula {}'.format(self.ursula.checksum_public_address),
|
||||
'-'*50,
|
||||
'Uptime: {}'.format(maya.now() - self.start_time), # TODO
|
||||
'Learning Round: {}'.format(self.ursula._learning_round),
|
||||
'Operating Mode: {}'.format('Federated' if self.ursula.federated_only else 'Decentralized'), # TODO
|
||||
'Rest Interface {}'.format(self.ursula.rest_url()),
|
||||
'Node Storage Type {}:'.format(self.ursula.node_storage._name.capitalize()),
|
||||
'Known Nodes: {}'.format(len(self.ursula.known_nodes)),
|
||||
'Work Orders: {}'.format(len(self.ursula._work_orders))]
|
||||
|
||||
if self.ursula._current_teacher_node:
|
||||
teacher = 'Current Teacher: {}: ({})'.format(self.ursula._current_teacher_node,
|
||||
self.ursula._current_teacher_node.rest_url())
|
||||
stats.append(teacher)
|
||||
|
||||
click.echo('\n'+'\n'.join(stats))
|
||||
|
||||
def connectionMade(self):
|
||||
message = '\nConnected to node console {}@{}'.format(self.ursula.checksum_public_address,
|
||||
self.ursula.rest_url())
|
||||
click.secho(message, fg='yellow')
|
||||
click.secho("Type 'help' or '?' for help")
|
||||
self.transport.write(b'Ursula >>> ')
|
||||
|
||||
def lineReceived(self, line):
|
||||
line = line.decode(encoding=self.encoding).strip().lower()
|
||||
|
||||
commands = {
|
||||
'known_nodes': self._paint_known_nodes,
|
||||
'status': self.paintStatus,
|
||||
'stop': reactor.stop,
|
||||
'cycle_teacher': self.ursula.cycle_teacher_node
|
||||
}
|
||||
|
||||
try:
|
||||
commands[line]()
|
||||
except KeyError:
|
||||
click.secho("Invalid input. Options are {}".format(', '.join(commands)))
|
||||
|
||||
self.transport.write(b'Ursula >>> ')
|
||||
|
||||
def terminalSize(self, width, height):
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.terminal.eraseDisplay()
|
||||
self._redraw()
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
[aliases]
|
||||
test=pytest
|
||||
|
||||
[tool:pytest]
|
||||
python_files = tests/
|
10
setup.py
10
setup.py
|
@ -99,7 +99,7 @@ BENCHMARKS_REQUIRE = [
|
|||
'pytest-benchmark'
|
||||
]
|
||||
|
||||
EXTRAS_REQUIRE = {'testing': TESTS_REQUIRE,
|
||||
EXTRAS_REQUIRE = {'test': TESTS_REQUIRE,
|
||||
'deployment': DEPLOY_REQUIRES,
|
||||
'docs': DOCS_REQUIRE,
|
||||
'benchmark': BENCHMARKS_REQUIRE}
|
||||
|
@ -113,6 +113,8 @@ setup(name=ABOUT['__title__'],
|
|||
license=ABOUT['__license__'],
|
||||
long_description=long_description,
|
||||
|
||||
setup_requires=['pytest-runner'], # required for setup.py test
|
||||
tests_require=TESTS_REQUIRE,
|
||||
install_requires=INSTALL_REQUIRES,
|
||||
extras_require=EXTRAS_REQUIRE,
|
||||
|
||||
|
@ -127,7 +129,11 @@ setup(name=ABOUT['__title__'],
|
|||
'blockchain/eth/sol/source/zepellin/token/*']},
|
||||
include_package_data=True,
|
||||
|
||||
entry_points={'console_scripts': ['{0}={0}.cli:cli'.format(PACKAGE_NAME)]},
|
||||
# Entry Points
|
||||
entry_points={'console_scripts': [
|
||||
'{0} = {0}.cli.main:nucypher_cli'.format(PACKAGE_NAME),
|
||||
'{0}-deploy = {0}.cli.deploy:deploy'.format(PACKAGE_NAME),
|
||||
]},
|
||||
cmdclass={'verify': VerifyVersionCommand},
|
||||
|
||||
classifiers=[
|
||||
|
|
|
@ -14,34 +14,25 @@ GNU General Public License for more details.
|
|||
You should have received a copy of the GNU General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
# import pytest
|
||||
|
||||
|
||||
import pytest
|
||||
|
||||
from eth_tester.exceptions import TransactionFailed
|
||||
|
||||
TEST_MAX_SEEDS = 20
|
||||
|
||||
#
|
||||
# def test_seeder(testerchain):
|
||||
# origin, *everyone_else = testerchain.interface.w3.eth.accounts
|
||||
# deployer = SeederDeployer(deployer_address=origin)
|
||||
#
|
||||
# agent = deployer.make_agent()
|
||||
# direct_agent = SeederAgent()
|
||||
#
|
||||
# assert agent == direct_agent
|
||||
#
|
||||
from nucypher.blockchain.eth.constants import NULL_ADDRESS
|
||||
from nucypher.utilities.sandbox.constants import MOCK_IP_ADDRESS, MOCK_IP_ADDRESS_2, MAX_TEST_SEEDER_ENTRIES, \
|
||||
MOCK_URSULA_STARTING_PORT
|
||||
|
||||
|
||||
@pytest.mark.slow()
|
||||
def test_seeder(testerchain):
|
||||
origin, seed_address, another_seed_address, *everyone_else = testerchain.interface.w3.eth.accounts
|
||||
seed = ('0.0.0.0', 5757)
|
||||
another_seed = ('10.10.10.10', 9151)
|
||||
seed = (MOCK_IP_ADDRESS, MOCK_URSULA_STARTING_PORT)
|
||||
another_seed = (MOCK_IP_ADDRESS_2, MOCK_URSULA_STARTING_PORT + 1)
|
||||
|
||||
contract, _txhash = testerchain.interface.deploy_contract('Seeder', TEST_MAX_SEEDS)
|
||||
contract, _txhash = testerchain.interface.deploy_contract('Seeder', MAX_TEST_SEEDER_ENTRIES)
|
||||
|
||||
assert contract.functions.getSeedArrayLength().call() == TEST_MAX_SEEDS
|
||||
assert contract.functions.getSeedArrayLength().call() == MAX_TEST_SEEDER_ENTRIES
|
||||
assert contract.functions.owner().call() == origin
|
||||
|
||||
with pytest.raises((TransactionFailed, ValueError)):
|
||||
|
@ -55,13 +46,13 @@ def test_seeder(testerchain):
|
|||
testerchain.wait_for_receipt(txhash)
|
||||
assert contract.functions.seeds(seed_address).call() == [*seed]
|
||||
assert contract.functions.seedArray(0).call() == seed_address
|
||||
assert contract.functions.seedArray(1).call() == "0x" + "0" * 40
|
||||
assert contract.functions.seedArray(1).call() == NULL_ADDRESS
|
||||
txhash = contract.functions.enroll(another_seed_address, *another_seed).transact({'from': origin})
|
||||
testerchain.wait_for_receipt(txhash)
|
||||
assert contract.functions.seeds(another_seed_address).call() == [*another_seed]
|
||||
assert contract.functions.seedArray(0).call() == seed_address
|
||||
assert contract.functions.seedArray(1).call() == another_seed_address
|
||||
assert contract.functions.seedArray(2).call() == "0x" + "0" * 40
|
||||
assert contract.functions.seedArray(2).call() == NULL_ADDRESS
|
||||
|
||||
txhash = contract.functions.refresh(*another_seed).transact({'from': seed_address})
|
||||
testerchain.wait_for_receipt(txhash)
|
|
@ -14,6 +14,8 @@ GNU General Public License for more details.
|
|||
You should have received a copy of the GNU General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
|
||||
import maya
|
||||
import pytest
|
||||
|
||||
|
@ -100,7 +102,7 @@ def test_miner_collects_staking_reward(testerchain, miner, three_agents):
|
|||
initial_balance = miner.token_balance
|
||||
assert token_agent.get_balance(miner.checksum_public_address) == initial_balance
|
||||
|
||||
miner.initialize_stake(amount=int(constants.MIN_ALLOWED_LOCKED), # Lock the minimum amount of tokens
|
||||
miner.initialize_stake(amount=int(constants.MIN_ALLOWED_LOCKED), # Lock the minimum amount of tokens
|
||||
lock_periods=int(constants.MIN_LOCKED_PERIODS)) # ... for the fewest number of periods
|
||||
|
||||
# ...wait out the lock period...
|
||||
|
|
|
@ -16,27 +16,24 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
|||
"""
|
||||
|
||||
|
||||
import os
|
||||
|
||||
import datetime
|
||||
import maya
|
||||
import os
|
||||
import pytest
|
||||
|
||||
from apistar.test import TestClient
|
||||
from constant_sorrow import constants
|
||||
from nucypher.characters.lawful import Bob, Ursula
|
||||
from nucypher.characters.lawful import Bob
|
||||
from nucypher.config.characters import AliceConfiguration
|
||||
from nucypher.config.storages import LocalFileBasedNodeStorage
|
||||
from nucypher.crypto.api import keccak_digest
|
||||
from nucypher.crypto.kits import RevocationKit
|
||||
from nucypher.crypto.powers import SigningPower, DelegatingPower, EncryptingPower
|
||||
from nucypher.crypto.powers import SigningPower, EncryptingPower
|
||||
from nucypher.policy.models import Revocation
|
||||
from nucypher.utilities.sandbox.constants import TEST_URSULA_INSECURE_DEVELOPMENT_PASSWORD
|
||||
from nucypher.utilities.sandbox.constants import INSECURE_DEVELOPMENT_PASSWORD
|
||||
from nucypher.utilities.sandbox.middleware import MockRestMiddleware
|
||||
from nucypher.utilities.sandbox.policy import MockPolicyCreation
|
||||
from umbral.fragments import KFrag
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be implemented")
|
||||
@pytest.mark.skip(reason="to be implemented") # TODO
|
||||
@pytest.mark.usefixtures('blockchain_ursulas')
|
||||
def test_mocked_decentralized_grant(blockchain_alice, blockchain_bob, three_agents):
|
||||
|
||||
|
@ -135,33 +132,26 @@ def test_revocation(federated_alice, federated_bob):
|
|||
|
||||
def test_alices_powers_are_persistent(federated_ursulas, tmpdir):
|
||||
|
||||
passphrase = TEST_URSULA_INSECURE_DEVELOPMENT_PASSWORD
|
||||
|
||||
# Let's create an Alice from a Configuration.
|
||||
# This requires creating a local storage for her first.
|
||||
node_storage = LocalFileBasedNodeStorage(
|
||||
federated_only=True,
|
||||
character_class=Ursula, # Alice needs to store some info about Ursula
|
||||
known_metadata_dir=os.path.join(tmpdir, "known_metadata"),
|
||||
)
|
||||
|
||||
# Create a non-learning AliceConfiguration
|
||||
alice_config = AliceConfiguration(
|
||||
config_root=os.path.join(tmpdir, "config_root"),
|
||||
node_storage=node_storage,
|
||||
auto_initialize=True,
|
||||
auto_generate_keys=True,
|
||||
passphrase=passphrase,
|
||||
is_me=True,
|
||||
config_root=os.path.join(tmpdir, 'nucypher-custom-alice-config'),
|
||||
network_middleware=MockRestMiddleware(),
|
||||
known_nodes=federated_ursulas,
|
||||
start_learning_now=False,
|
||||
federated_only=True,
|
||||
save_metadata=False,
|
||||
load_metadata=False
|
||||
)
|
||||
alice = alice_config(passphrase=passphrase)
|
||||
reload_metadata=False)
|
||||
|
||||
# We will save Alice's config to a file for later use
|
||||
# Generate keys and write them the disk
|
||||
alice_config.initialize(password=INSECURE_DEVELOPMENT_PASSWORD)
|
||||
|
||||
# Unlock Alice's keyring
|
||||
alice_config.keyring.unlock(password=INSECURE_DEVELOPMENT_PASSWORD)
|
||||
|
||||
# Produce an Alice
|
||||
alice = alice_config() # or alice_config.produce()
|
||||
|
||||
# Save Alice's node configuration file to disk for later use
|
||||
alice_config_file = alice_config.to_configuration_file()
|
||||
|
||||
# Let's save Alice's public keys too to check they are correctly restored later
|
||||
|
@ -181,8 +171,7 @@ def test_alices_powers_are_persistent(federated_ursulas, tmpdir):
|
|||
|
||||
bob = Bob(federated_only=True,
|
||||
start_learning_now=False,
|
||||
network_middleware=MockRestMiddleware(),
|
||||
)
|
||||
network_middleware=MockRestMiddleware())
|
||||
|
||||
bob_policy = alice.grant(bob, label, m=m, n=n, expiration=policy_end_datetime)
|
||||
|
||||
|
@ -208,7 +197,9 @@ def test_alices_powers_are_persistent(federated_ursulas, tmpdir):
|
|||
start_learning_now=False,
|
||||
)
|
||||
|
||||
new_alice = new_alice_config(passphrase=passphrase)
|
||||
# Alice unlocks her restored keyring from disk
|
||||
new_alice_config.keyring.unlock(password=INSECURE_DEVELOPMENT_PASSWORD)
|
||||
new_alice = new_alice_config()
|
||||
|
||||
# First, we check that her public keys are correctly restored
|
||||
assert alices_verifying_key == new_alice.public_keys(SigningPower)
|
||||
|
@ -217,8 +208,7 @@ def test_alices_powers_are_persistent(federated_ursulas, tmpdir):
|
|||
# Bob's eldest brother, Roberto, appears too
|
||||
roberto = Bob(federated_only=True,
|
||||
start_learning_now=False,
|
||||
network_middleware=MockRestMiddleware(),
|
||||
)
|
||||
network_middleware=MockRestMiddleware())
|
||||
|
||||
# Alice creates a new policy for Roberto. Note how all the parameters
|
||||
# except for the label (i.e., recipient, m, n, policy_end) are different
|
||||
|
|
|
@ -80,7 +80,6 @@ def test_bob_can_follow_treasure_map_even_if_he_only_knows_of_one_node(enacted_f
|
|||
from nucypher.characters.lawful import Bob
|
||||
|
||||
bob = Bob(network_middleware=MockRestMiddleware(),
|
||||
known_certificates_dir=certificates_tempdir,
|
||||
start_learning_now=False,
|
||||
abort_on_learning_error=True,
|
||||
federated_only=True)
|
||||
|
|
|
@ -6,7 +6,7 @@ import pytest
|
|||
from nucypher.utilities.sandbox.middleware import MockRestMiddleware
|
||||
from nucypher.characters.lawful import Bob, Ursula
|
||||
from nucypher.data_sources import DataSource
|
||||
from nucypher.utilities.sandbox.constants import DEFAULT_NUMBER_OF_URSULAS_IN_DEVELOPMENT_NETWORK
|
||||
from nucypher.utilities.sandbox.constants import NUMBER_OF_URSULAS_IN_DEVELOPMENT_NETWORK
|
||||
from nucypher.keystore.keypairs import SigningKeypair
|
||||
|
||||
|
||||
|
@ -52,7 +52,6 @@ def test_bob_joins_policy_and_retrieves(federated_alice,
|
|||
bob = Bob(federated_only=True,
|
||||
start_learning_now=True,
|
||||
network_middleware=MockRestMiddleware(),
|
||||
known_certificates_dir=certificates_tempdir,
|
||||
abort_on_learning_error=True,
|
||||
known_nodes=a_couple_of_ursulas,
|
||||
)
|
||||
|
@ -62,7 +61,7 @@ def test_bob_joins_policy_and_retrieves(federated_alice,
|
|||
|
||||
# Alice creates a policy granting access to Bob
|
||||
# Just for fun, let's assume she distributes KFrags among Ursulas unknown to Bob
|
||||
n = DEFAULT_NUMBER_OF_URSULAS_IN_DEVELOPMENT_NETWORK - 2
|
||||
n = NUMBER_OF_URSULAS_IN_DEVELOPMENT_NETWORK - 2
|
||||
label = b'label://' + os.urandom(32)
|
||||
contract_end_datetime = maya.now() + datetime.timedelta(days=5)
|
||||
policy = federated_alice.grant(bob=bob,
|
||||
|
|
|
@ -99,7 +99,7 @@ def test_character_blockchain_power(testerchain):
|
|||
sig_privkey = testerchain.interface.providers[0].ethereum_tester.backend._key_lookup[eth_utils.to_canonical_address(eth_address)]
|
||||
sig_pubkey = sig_privkey.public_key
|
||||
|
||||
signer = Character(is_me=True, checksum_address=eth_address)
|
||||
signer = Character(is_me=True, checksum_public_address=eth_address)
|
||||
signer._crypto_power.consume_power_up(BlockchainPower(testerchain, eth_address))
|
||||
|
||||
# Due to testing backend, the account is already unlocked.
|
||||
|
|
|
@ -14,15 +14,21 @@ GNU General Public License for more details.
|
|||
You should have received a copy of the GNU General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from nucypher.characters.lawful import Ursula
|
||||
from nucypher.characters.unlawful import Vladimir
|
||||
from nucypher.crypto.powers import SigningPower, CryptoPower
|
||||
from nucypher.utilities.sandbox.constants import INSECURE_DEVELOPMENT_PASSWORD
|
||||
from nucypher.utilities.sandbox.middleware import MockRestMiddleware
|
||||
from nucypher.utilities.sandbox.ursula import make_federated_ursulas
|
||||
|
||||
|
||||
@pytest.mark.skip("To be implemented...?")
|
||||
@pytest.mark.skip("To be implemented.")
|
||||
def test_federated_ursula_substantiates_stamp():
|
||||
assert False
|
||||
|
||||
|
@ -47,3 +53,80 @@ def test_new_federated_ursula_announces_herself(ursula_federated_test_config):
|
|||
|
||||
assert ursula_with_a_mouse in ursula_in_a_house.known_nodes
|
||||
assert ursula_in_a_house in ursula_with_a_mouse.known_nodes
|
||||
|
||||
|
||||
def test_blockchain_ursula_substantiates_stamp(blockchain_ursulas):
|
||||
first_ursula = list(blockchain_ursulas)[0]
|
||||
signature_as_bytes = first_ursula._evidence_of_decentralized_identity
|
||||
signature = EthSignature(signature_bytes=signature_as_bytes)
|
||||
proper_public_key_for_first_ursula = signature.recover_public_key_from_msg(bytes(first_ursula.stamp))
|
||||
proper_address_for_first_ursula = proper_public_key_for_first_ursula.to_checksum_address()
|
||||
assert proper_address_for_first_ursula == first_ursula.checksum_public_address
|
||||
|
||||
# This method is a shortcut for the above.
|
||||
assert first_ursula._stamp_has_valid_wallet_signature
|
||||
|
||||
|
||||
def test_blockchain_ursula_verifies_stamp(blockchain_ursulas):
|
||||
first_ursula = list(blockchain_ursulas)[0]
|
||||
|
||||
# This Ursula does not yet have a verified stamp
|
||||
first_ursula.verified_stamp = False
|
||||
first_ursula.stamp_is_valid()
|
||||
|
||||
# ...but now it's verified.
|
||||
assert first_ursula.verified_stamp
|
||||
|
||||
|
||||
def test_vladimir_cannot_verify_interface_with_ursulas_signing_key(blockchain_ursulas):
|
||||
his_target = list(blockchain_ursulas)[4]
|
||||
|
||||
# Vladimir has his own ether address; he hopes to publish it along with Ursula's details
|
||||
# so that Alice (or whomever) pays him instead of Ursula, even though Ursula is providing the service.
|
||||
|
||||
# He finds a target and verifies that its interface is valid.
|
||||
assert his_target.interface_is_valid()
|
||||
|
||||
# Now Vladimir imitates Ursula - copying her public keys and interface info, but inserting his ether address.
|
||||
vladimir = Vladimir.from_target_ursula(his_target, claim_signing_key=True)
|
||||
|
||||
# Vladimir can substantiate the stamp using his own ether address...
|
||||
vladimir.substantiate_stamp(password=INSECURE_DEVELOPMENT_PASSWORD)
|
||||
vladimir.stamp_is_valid()
|
||||
|
||||
# Now, even though his public signing key matches Ursulas...
|
||||
assert vladimir.stamp == his_target.stamp
|
||||
|
||||
# ...he is unable to pretend that his interface is valid
|
||||
# because the interface validity check contains the canonical public address as part of its message.
|
||||
with pytest.raises(vladimir.InvalidNode):
|
||||
vladimir.interface_is_valid()
|
||||
|
||||
# Consequently, the metadata as a whole is also invalid.
|
||||
with pytest.raises(vladimir.InvalidNode):
|
||||
vladimir.validate_metadata()
|
||||
|
||||
|
||||
def test_vladimir_uses_his_own_signing_key(blockchain_alice, blockchain_ursulas):
|
||||
"""
|
||||
Similar to the attack above, but this time Vladimir makes his own interface signature
|
||||
using his own signing key, which he claims is Ursula's.
|
||||
"""
|
||||
his_target = list(blockchain_ursulas)[4]
|
||||
|
||||
fraduluent_keys = CryptoPower(power_ups=Ursula._default_crypto_powerups) # TODO: Why is this unused?
|
||||
|
||||
vladimir = Vladimir.from_target_ursula(target_ursula=his_target)
|
||||
|
||||
message = vladimir._signable_interface_info_message()
|
||||
signature = vladimir._crypto_power.power_ups(SigningPower).sign(vladimir.timestamp_bytes() + message)
|
||||
vladimir._interface_signature_object = signature
|
||||
|
||||
vladimir.substantiate_stamp(password=INSECURE_DEVELOPMENT_PASSWORD)
|
||||
|
||||
# With this slightly more sophisticated attack, his metadata does appear valid.
|
||||
vladimir.validate_metadata()
|
||||
|
||||
# However, the actual handshake proves him wrong.
|
||||
with pytest.raises(vladimir.InvalidNode):
|
||||
vladimir.verify_node(blockchain_alice.network_middleware)
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU 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 General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
|
||||
from nucypher.cli.deploy import deploy
|
||||
from nucypher.cli.main import nucypher_cli
|
||||
|
||||
|
||||
def test_nucypher_help_message(click_runner):
|
||||
help_args = ('--help', )
|
||||
result = click_runner.invoke(nucypher_cli, help_args, catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
||||
assert '[OPTIONS] COMMAND [ARGS]' in result.output, 'Missing or invalid help text was produced.'
|
||||
|
||||
|
||||
def test_nucypher_ursula_help_message(click_runner):
|
||||
help_args = ('ursula', '--help')
|
||||
result = click_runner.invoke(nucypher_cli, help_args, catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
||||
assert 'ursula [OPTIONS] ACTION' in result.output, 'Missing or invalid help text was produced.'
|
||||
|
||||
|
||||
def test_nucypher_deploy_help_message(click_runner):
|
||||
help_args = ('--help', )
|
||||
result = click_runner.invoke(deploy, help_args, catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
||||
assert 'deploy [OPTIONS] ACTION' in result.output, 'Missing or invalid help text was produced.'
|
|
@ -14,33 +14,30 @@ GNU General Public License for more details.
|
|||
You should have received a copy of the GNU General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
import time
|
||||
|
||||
|
||||
import pytest
|
||||
import pytest_twisted
|
||||
from click.testing import CliRunner
|
||||
import pytest_twisted as pt
|
||||
import time
|
||||
from twisted.internet import threads
|
||||
from twisted.internet.error import CannotListenError
|
||||
|
||||
from nucypher.cli import cli
|
||||
from nucypher.characters.base import Learner
|
||||
from nucypher.cli.main import nucypher_cli
|
||||
from nucypher.utilities.sandbox.constants import INSECURE_DEVELOPMENT_PASSWORD, MOCK_URSULA_STARTING_PORT
|
||||
from nucypher.utilities.sandbox.constants import TEST_URSULA_INSECURE_DEVELOPMENT_PASSWORD
|
||||
from nucypher.utilities.sandbox.ursula import UrsulaCommandProtocol
|
||||
|
||||
|
||||
@pytest.mark.skip()
|
||||
@pytest_twisted.inlineCallbacks
|
||||
def test_run_lone_federated_default_ursula():
|
||||
@pytest.mark.skip('Results in exception "ReactorAlreadyRunning"')
|
||||
@pt.inlineCallbacks
|
||||
def test_run_lone_federated_default_development_ursula(click_runner):
|
||||
args = ('ursula', 'run', '--rest-port', MOCK_URSULA_STARTING_PORT, '--dev')
|
||||
|
||||
args = ['--dev',
|
||||
'--federated-only',
|
||||
'ursula', 'run',
|
||||
'--rest-port', '9999', # TODO: use different port to avoid premature ConnectionError with many test runs?
|
||||
'--no-reactor'
|
||||
]
|
||||
|
||||
runner = CliRunner()
|
||||
result = yield threads.deferToThread(runner.invoke, cli, args, catch_exceptions=False, input=TEST_URSULA_INSECURE_DEVELOPMENT_PASSWORD+'\n')
|
||||
result = yield threads.deferToThread(click_runner.invoke,
|
||||
nucypher_cli, args,
|
||||
catch_exceptions=False,
|
||||
input=INSECURE_DEVELOPMENT_PASSWORD + '\n')
|
||||
|
||||
alone = "WARNING - Can't learn right now: Need some nodes to start learning from."
|
||||
time.sleep(Learner._SHORT_LEARNING_DELAY)
|
||||
|
@ -49,4 +46,4 @@ def test_run_lone_federated_default_ursula():
|
|||
|
||||
# Cannot start another Ursula on the same REST port
|
||||
with pytest.raises(CannotListenError):
|
||||
_result = runner.invoke(cli, args, catch_exceptions=False, input=TEST_URSULA_INSECURE_DEVELOPMENT_PASSWORD)
|
||||
_result = click_runner.invoke(nucypher_cli, args, catch_exceptions=False, input=INSECURE_DEVELOPMENT_PASSWORD)
|
|
@ -14,25 +14,24 @@ GNU General Public License for more details.
|
|||
You should have received a copy of the GNU General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
|
||||
import pytest
|
||||
from click.testing import CliRunner
|
||||
|
||||
from nucypher.cli import cli
|
||||
from nucypher.cli.main import nucypher_cli
|
||||
|
||||
|
||||
@pytest.mark.skip
|
||||
def test_stake_init():
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ['stake', 'init'], catch_exceptions=False)
|
||||
@pytest.mark.skip("To be implemented") # TODO
|
||||
def test_stake_init(click_runner):
|
||||
result = click_runner.invoke(nucypher_cli, ['stake', 'init'], catch_exceptions=False)
|
||||
|
||||
|
||||
@pytest.mark.skip
|
||||
def test_stake_info():
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ['stake', 'info'], catch_exceptions=False)
|
||||
@pytest.mark.skip("To be implemented") # TODO
|
||||
def test_stake_info(click_runner):
|
||||
result = click_runner.invoke(nucypher_cli, ['stake', 'info'], catch_exceptions=False)
|
||||
|
||||
|
||||
@pytest.mark.skip
|
||||
def test_stake_confirm():
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ['stake', 'confirm-activity'], catch_exceptions=False)
|
||||
@pytest.mark.skip("To be implemented") # TODO
|
||||
def test_stake_confirm(click_runner):
|
||||
result = click_runner.invoke(nucypher_cli, ['stake', 'confirm-activity'], catch_exceptions=False)
|
|
@ -0,0 +1,64 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU 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 General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from nucypher.cli.main import nucypher_cli
|
||||
from nucypher.config.characters import UrsulaConfiguration
|
||||
from nucypher.utilities.sandbox.constants import INSECURE_DEVELOPMENT_PASSWORD, MOCK_CUSTOM_INSTALLATION_PATH, \
|
||||
MOCK_IP_ADDRESS_2
|
||||
|
||||
|
||||
def test_initialize_configuration_files_and_directories(custom_filepath, click_runner):
|
||||
init_args = ('ursula', 'init', '--config-root', custom_filepath)
|
||||
|
||||
# Use a custom local filepath for configuration
|
||||
user_input = '{ip}\n{password}\n{password}\n'.format(password=INSECURE_DEVELOPMENT_PASSWORD, ip=MOCK_IP_ADDRESS_2)
|
||||
result = click_runner.invoke(nucypher_cli, init_args, input=user_input, catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
||||
|
||||
# CLI Output
|
||||
assert MOCK_CUSTOM_INSTALLATION_PATH in result.output, "Configuration not in system temporary directory"
|
||||
assert "nucypher ursula run" in result.output, 'Help message is missing suggested command'
|
||||
|
||||
# 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.CONFIG_FILENAME)
|
||||
assert os.path.isfile(custom_config_filepath), 'Configuration file does not exist'
|
||||
|
||||
# Auth
|
||||
assert 'Enter keyring password:' in result.output, 'WARNING: User was not prompted for password'
|
||||
assert 'Repeat for confirmation:' in result.output, 'User was not prompted to confirm password'
|
||||
|
||||
|
||||
def test_empty_federated_status(click_runner, custom_filepath):
|
||||
|
||||
custom_config_filepath = os.path.join(custom_filepath, UrsulaConfiguration.CONFIG_FILENAME)
|
||||
assert os.path.isfile(custom_config_filepath), 'Configuration file does not exist'
|
||||
|
||||
status_args = ('status', '--config-file', custom_config_filepath)
|
||||
result = click_runner.invoke(nucypher_cli, status_args, catch_exceptions=False)
|
||||
|
||||
assert result.exit_code == 0
|
||||
|
||||
assert 'Federated Only' in result.output
|
||||
heading = 'Known Nodes (connected 0 / seen 0)'
|
||||
assert heading in result.output
|
||||
assert 'password' not in result.output
|
|
@ -0,0 +1,225 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU 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 General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from json import JSONDecodeError
|
||||
|
||||
import pytest
|
||||
|
||||
from nucypher.cli.main import nucypher_cli
|
||||
from nucypher.config.characters import UrsulaConfiguration
|
||||
from nucypher.config.constants import APP_DIR, DEFAULT_CONFIG_ROOT
|
||||
from nucypher.utilities.sandbox.constants import (
|
||||
INSECURE_DEVELOPMENT_PASSWORD,
|
||||
MOCK_CUSTOM_INSTALLATION_PATH,
|
||||
MOCK_IP_ADDRESS,
|
||||
MOCK_URSULA_STARTING_PORT
|
||||
)
|
||||
|
||||
|
||||
def test_initialize_ursula_defaults(click_runner, mocker):
|
||||
|
||||
# Mock out filesystem writes
|
||||
mocker.patch.object(UrsulaConfiguration, 'initialize', autospec=True)
|
||||
mocker.patch.object(UrsulaConfiguration, 'to_configuration_file', autospec=True)
|
||||
|
||||
# Use default ursula init args
|
||||
init_args = ('ursula', 'init')
|
||||
user_input = '{ip}\n{password}\n{password}\n'.format(password=INSECURE_DEVELOPMENT_PASSWORD, ip=MOCK_IP_ADDRESS)
|
||||
result = click_runner.invoke(nucypher_cli, init_args, input=user_input, catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
||||
|
||||
# REST Host
|
||||
assert 'Enter Ursula\'s public-facing IPv4 address' in result.output
|
||||
|
||||
# Auth
|
||||
assert 'Enter keyring password:' in result.output, 'WARNING: User was not prompted for password'
|
||||
assert 'Repeat for confirmation:' in result.output, 'User was not prompted to confirm password'
|
||||
|
||||
|
||||
def test_initialize_custom_configuration_root(custom_filepath, click_runner):
|
||||
|
||||
# Use a custom local filepath for configuration
|
||||
init_args = ('ursula', 'init',
|
||||
'--config-root', custom_filepath,
|
||||
'--rest-host', MOCK_IP_ADDRESS,
|
||||
'--rest-port', MOCK_URSULA_STARTING_PORT)
|
||||
|
||||
user_input = '{password}\n{password}'.format(password=INSECURE_DEVELOPMENT_PASSWORD, ip=MOCK_IP_ADDRESS)
|
||||
result = click_runner.invoke(nucypher_cli, init_args, input=user_input, catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
||||
|
||||
# CLI Output
|
||||
assert MOCK_CUSTOM_INSTALLATION_PATH in result.output, "Configuration not in system temporary directory"
|
||||
assert "nucypher ursula run" in result.output, 'Help message is missing suggested command'
|
||||
assert 'IPv4' not in result.output
|
||||
|
||||
# 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.CONFIG_FILENAME)
|
||||
assert os.path.isfile(custom_config_filepath), 'Configuration file does not exist'
|
||||
|
||||
# Auth
|
||||
assert 'Enter keyring password:' in result.output, 'WARNING: User was not prompted for password'
|
||||
assert 'Repeat for confirmation:' in result.output, 'User was not prompted to confirm password'
|
||||
|
||||
|
||||
def test_configuration_file_contents(custom_filepath, nominal_configuration_fields):
|
||||
custom_config_filepath = os.path.join(custom_filepath, UrsulaConfiguration.CONFIG_FILENAME)
|
||||
assert os.path.isfile(custom_config_filepath), 'Configuration file does not exist'
|
||||
|
||||
# Check the contents of the configuration file
|
||||
with open(custom_config_filepath, 'r') as config_file:
|
||||
raw_contents = config_file.read()
|
||||
|
||||
try:
|
||||
data = json.loads(raw_contents)
|
||||
except JSONDecodeError:
|
||||
raise pytest.fail(msg="Invalid JSON configuration file {}".format(custom_config_filepath))
|
||||
|
||||
for field in nominal_configuration_fields:
|
||||
assert field in data, "Missing field '{}' from configuration file."
|
||||
if any(keyword in field for keyword in ('path', 'dir')):
|
||||
path = data[field]
|
||||
user_data_dir = APP_DIR.user_data_dir
|
||||
# assert os.path.exists(path), '{} does not exist'.format(path)
|
||||
assert user_data_dir not in path, '{} includes default appdir path {}'.format(field, user_data_dir)
|
||||
|
||||
assert os.path.isfile(custom_config_filepath), 'Configuration file does not exist'
|
||||
|
||||
|
||||
def test_password_prompt(click_runner, custom_filepath):
|
||||
|
||||
# Ensure the configuration file still exists
|
||||
custom_config_filepath = os.path.join(custom_filepath, UrsulaConfiguration.CONFIG_FILENAME)
|
||||
assert os.path.isfile(custom_config_filepath), 'Configuration file does not exist'
|
||||
|
||||
view_args = ('ursula', 'view', '--config-file', custom_config_filepath)
|
||||
|
||||
user_input = '{}\n'.format(INSECURE_DEVELOPMENT_PASSWORD)
|
||||
result = click_runner.invoke(nucypher_cli, view_args, input=user_input, catch_exceptions=False, env=dict())
|
||||
assert 'password' in result.output, 'WARNING: User was not prompted for password'
|
||||
assert result.exit_code == 0
|
||||
|
||||
envvars = {'NUCYPHER_KEYRING_PASSWORD': INSECURE_DEVELOPMENT_PASSWORD}
|
||||
result = click_runner.invoke(nucypher_cli, view_args, input=user_input, catch_exceptions=False, env=envvars)
|
||||
assert not 'password' in result.output, 'User was prompted for password'
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
def test_ursula_view_configuration(custom_filepath, click_runner, nominal_configuration_fields):
|
||||
|
||||
# Ensure the configuration file still exists
|
||||
custom_config_filepath = os.path.join(custom_filepath, UrsulaConfiguration.CONFIG_FILENAME)
|
||||
assert os.path.isfile(custom_config_filepath), 'Configuration file does not exist'
|
||||
|
||||
view_args = ('ursula', 'view', '--config-file', os.path.join(custom_filepath, UrsulaConfiguration.CONFIG_FILENAME))
|
||||
|
||||
# View the configuration
|
||||
result = click_runner.invoke(nucypher_cli, view_args,
|
||||
input='{}\n'.format(INSECURE_DEVELOPMENT_PASSWORD),
|
||||
catch_exceptions=False)
|
||||
|
||||
# CLI Output
|
||||
assert 'password' in result.output, 'WARNING: User was not prompted for password'
|
||||
assert MOCK_CUSTOM_INSTALLATION_PATH in result.output
|
||||
for field in nominal_configuration_fields:
|
||||
assert field in result.output, "Missing field '{}' from configuration file."
|
||||
|
||||
# Make sure nothing crazy is happening...
|
||||
assert os.path.isfile(custom_config_filepath), 'Configuration file does not exist'
|
||||
|
||||
|
||||
@pytest.mark.skip("Results in ReactorAlreadyRunning") # TODO: Find a way to execute this test (contains reactor.run call)
|
||||
def test_run_ursula(custom_filepath, click_runner):
|
||||
|
||||
# Ensure the configuration file still exists
|
||||
custom_config_filepath = os.path.join(custom_filepath, UrsulaConfiguration.CONFIG_FILENAME)
|
||||
assert os.path.isfile(custom_config_filepath), 'Configuration file does not exist'
|
||||
|
||||
# Run Ursula
|
||||
run_args = ('ursula', 'run', '--config-file', custom_config_filepath)
|
||||
result = click_runner.invoke(nucypher_cli, run_args,
|
||||
input='{}\nY\n'.format(INSECURE_DEVELOPMENT_PASSWORD),
|
||||
catch_exceptions=False)
|
||||
|
||||
# CLI Output
|
||||
assert result.exit_code == 0
|
||||
assert 'password' in result.output, 'WARNING: User was not prompted for password'
|
||||
assert '? [y/N]:' in result.output, 'WARNING: User was to run Ursula'
|
||||
assert '>>>' in result.output
|
||||
|
||||
|
||||
def test_ursula_init_does_not_overrides_existing_files(custom_filepath, click_runner):
|
||||
|
||||
# Ensure the configuration file still exists
|
||||
custom_config_filepath = os.path.join(custom_filepath, UrsulaConfiguration.CONFIG_FILENAME)
|
||||
assert os.path.isfile(custom_config_filepath), 'Configuration file does not exist'
|
||||
|
||||
init_args = ('ursula', 'init', '--config-root', custom_filepath, '--rest-host', MOCK_IP_ADDRESS)
|
||||
|
||||
# Ensure that an existing configuration directory cannot be overridden
|
||||
with pytest.raises(UrsulaConfiguration.ConfigurationError):
|
||||
_bad_result = click_runner.invoke(nucypher_cli, init_args,
|
||||
input='{}\n'.format(INSECURE_DEVELOPMENT_PASSWORD)*2,
|
||||
catch_exceptions=False)
|
||||
|
||||
assert 'password' in _bad_result.output, 'WARNING: User was not prompted for password'
|
||||
|
||||
# Really we want to keep this file until its destroyed
|
||||
assert os.path.isfile(custom_config_filepath), 'Configuration file does not exist'
|
||||
|
||||
|
||||
def test_ursula_destroy_configuration(custom_filepath, click_runner):
|
||||
|
||||
preexisting_live_configuration = os.path.isdir(DEFAULT_CONFIG_ROOT)
|
||||
preexisting_live_configuration_file = os.path.isfile(os.path.join(DEFAULT_CONFIG_ROOT, UrsulaConfiguration.CONFIG_FILENAME))
|
||||
|
||||
# Ensure the configuration file still exists
|
||||
custom_config_filepath = os.path.join(custom_filepath, UrsulaConfiguration.CONFIG_FILENAME)
|
||||
assert os.path.isfile(custom_config_filepath), 'Configuration file does not exist'
|
||||
|
||||
# Run the destroy command
|
||||
destruction_args = ('ursula', 'destroy', '--config-file', custom_config_filepath)
|
||||
result = click_runner.invoke(nucypher_cli, destruction_args,
|
||||
input='{}\nY\n'.format(INSECURE_DEVELOPMENT_PASSWORD),
|
||||
catch_exceptions=False)
|
||||
|
||||
# CLI Output
|
||||
assert not os.path.isfile(custom_config_filepath), 'Configuration file still exists'
|
||||
assert 'password' in result.output, 'WARNING: User was not prompted for password'
|
||||
assert '? [y/N]:' in result.output, 'WARNING: User was not asked to destroy files'
|
||||
assert custom_filepath in result.output, 'WARNING: Configuration path not in output. Deleting the wrong path?'
|
||||
assert 'Deleted' in result.output, '"Deleted" not in output'
|
||||
assert result.exit_code == 0, 'Destruction did not succeed'
|
||||
|
||||
# Ensure the files are deleted from the filesystem
|
||||
assert not os.path.isfile(custom_config_filepath), 'Files still exist' # ... it's gone...
|
||||
assert not os.path.isdir(custom_filepath), 'Nucypher files still exist' # it's all gone...
|
||||
|
||||
# If this test started off with a live configuration, ensure it still exists
|
||||
if preexisting_live_configuration:
|
||||
configuration_still_exists = os.path.isdir(DEFAULT_CONFIG_ROOT)
|
||||
assert configuration_still_exists
|
||||
|
||||
if preexisting_live_configuration_file:
|
||||
file_still_exists = os.path.isfile(os.path.join(DEFAULT_CONFIG_ROOT, UrsulaConfiguration.CONFIG_FILENAME))
|
||||
assert file_still_exists, 'WARNING: Test command deleted live non-test files'
|
|
@ -0,0 +1,51 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU 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 General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
import contextlib
|
||||
import shutil
|
||||
|
||||
import pytest
|
||||
from click.testing import CliRunner
|
||||
|
||||
from nucypher.config.characters import UrsulaConfiguration
|
||||
from nucypher.utilities.sandbox.constants import MOCK_CUSTOM_INSTALLATION_PATH
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def click_runner():
|
||||
runner = CliRunner()
|
||||
yield runner
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def nominal_configuration_fields():
|
||||
config = UrsulaConfiguration(dev_mode=True)
|
||||
config_fields = config.static_payload
|
||||
del config_fields['is_me']
|
||||
yield tuple(config_fields.keys())
|
||||
del config
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def custom_filepath():
|
||||
_custom_filepath = MOCK_CUSTOM_INSTALLATION_PATH
|
||||
with contextlib.suppress(FileNotFoundError):
|
||||
shutil.rmtree(_custom_filepath, ignore_errors=True)
|
||||
try:
|
||||
yield _custom_filepath
|
||||
finally:
|
||||
with contextlib.suppress(FileNotFoundError):
|
||||
shutil.rmtree(_custom_filepath, ignore_errors=True)
|
|
@ -0,0 +1,90 @@
|
|||
import sys
|
||||
from contextlib import contextmanager
|
||||
|
||||
import pytest
|
||||
from io import StringIO
|
||||
|
||||
from nucypher.cli.main import NucypherClickConfig
|
||||
from nucypher.cli.protocol import UrsulaCommandProtocol
|
||||
|
||||
# Disable click sentry and file logging
|
||||
|
||||
NucypherClickConfig.log_to_sentry = False
|
||||
NucypherClickConfig.log_to_file = False
|
||||
|
||||
|
||||
@contextmanager
|
||||
def capture_output():
|
||||
new_out, new_err = StringIO(), StringIO()
|
||||
old_out, old_err = sys.stdout, sys.stderr
|
||||
try:
|
||||
sys.stdout, sys.stderr = new_out, new_err
|
||||
yield sys.stdout, sys.stderr
|
||||
finally:
|
||||
sys.stdout, sys.stderr = old_out, old_err
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def ursula(federated_ursulas):
|
||||
ursula = federated_ursulas.pop()
|
||||
return ursula
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def protocol(ursula):
|
||||
protocol = UrsulaCommandProtocol(ursula=ursula)
|
||||
return protocol
|
||||
|
||||
|
||||
def test_ursula_command_protocol_creation(ursula):
|
||||
|
||||
protocol = UrsulaCommandProtocol(ursula=ursula)
|
||||
|
||||
assert protocol.ursula == ursula
|
||||
assert b'Ursula' in protocol.prompt
|
||||
|
||||
|
||||
def test_ursula_command_help(protocol, ursula):
|
||||
|
||||
class FakeTransport:
|
||||
"""This is a transport"""
|
||||
|
||||
mock_output = b''
|
||||
|
||||
@staticmethod
|
||||
def write(data: bytes):
|
||||
FakeTransport.mock_output += data
|
||||
|
||||
protocol.transport = FakeTransport
|
||||
|
||||
with capture_output() as (out, err):
|
||||
protocol.lineReceived(line=b'bananas')
|
||||
|
||||
# Ensure all commands are in the help text
|
||||
result = out.getvalue()
|
||||
for command in protocol.commands:
|
||||
assert command in result, '{} is missing from help text'.format(command)
|
||||
|
||||
# Blank lines are OK!
|
||||
with capture_output() as (out, err):
|
||||
protocol.lineReceived(line=b'')
|
||||
assert protocol.prompt in FakeTransport.mock_output
|
||||
|
||||
|
||||
def test_ursula_command_status(protocol, ursula):
|
||||
|
||||
with capture_output() as (out, err):
|
||||
protocol.paintStatus()
|
||||
result = out.getvalue()
|
||||
assert ursula.checksum_public_address in result
|
||||
assert '...' in result
|
||||
assert 'Known Nodes' in result
|
||||
|
||||
|
||||
def test_ursula_command_known_nodes(protocol, ursula):
|
||||
|
||||
with capture_output() as (out, err):
|
||||
protocol.paintKnownNodes()
|
||||
result = out.getvalue()
|
||||
assert 'Known Nodes' in result
|
||||
assert ursula.checksum_public_address not in result
|
|
@ -1,62 +0,0 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU 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 General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
|
||||
import pytest
|
||||
from click.testing import CliRunner
|
||||
|
||||
from nucypher.cli import cli
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("three_agents")
|
||||
def test_list(testerchain):
|
||||
runner = CliRunner()
|
||||
account = testerchain.interface.w3.eth.accounts[0]
|
||||
args = '--dev --federated-only --provider-uri tester://pyevm accounts list'.split()
|
||||
result = runner.invoke(cli, args, catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
||||
assert account in result.output
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("three_agents")
|
||||
def test_balance(testerchain):
|
||||
runner = CliRunner()
|
||||
account = testerchain.interface.w3.eth.accounts[0]
|
||||
args = '--dev --federated-only --provider-uri tester://pyevm accounts balance'.split()
|
||||
result = runner.invoke(cli, args, catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
||||
assert 'Tokens:' in result.output
|
||||
assert 'ETH:' in result.output
|
||||
assert account in result.output
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("three_agents")
|
||||
def test_transfer_eth(testerchain):
|
||||
runner = CliRunner()
|
||||
account = testerchain.interface.w3.eth.accounts[1]
|
||||
args = '--dev --federated-only --provider-uri tester://pyevm accounts transfer-eth'.split()
|
||||
result = runner.invoke(cli, args, catch_exceptions=False, input=account+'\n100\nY\n')
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("three_agents")
|
||||
def test_transfer_tokens(testerchain):
|
||||
runner = CliRunner()
|
||||
account = testerchain.interface.w3.eth.accounts[2]
|
||||
args = '--dev --federated-only --provider-uri tester://pyevm accounts transfer-tokens'.split()
|
||||
result = runner.invoke(cli, args, catch_exceptions=False, input=account+'\n100\nY\n')
|
||||
assert result.exit_code == 0
|
|
@ -1,109 +0,0 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU 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 General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
import contextlib
|
||||
import os
|
||||
|
||||
import pytest
|
||||
import shutil
|
||||
from click.testing import CliRunner
|
||||
|
||||
from nucypher.cli import cli
|
||||
from nucypher.config.node import NodeConfiguration
|
||||
from nucypher.utilities.sandbox.constants import TEST_URSULA_INSECURE_DEVELOPMENT_PASSWORD
|
||||
|
||||
|
||||
TEST_CUSTOM_INSTALLATION_PATH = '/tmp/nucypher-tmp-test-custom'
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def custom_filepath():
|
||||
custom_filepath = TEST_CUSTOM_INSTALLATION_PATH
|
||||
yield custom_filepath
|
||||
with contextlib.suppress(FileNotFoundError):
|
||||
shutil.rmtree(custom_filepath, ignore_errors=True)
|
||||
|
||||
|
||||
@pytest.mark.skip()
|
||||
def test_initialize_configuration_files_and_directories(custom_filepath):
|
||||
runner = CliRunner()
|
||||
|
||||
# Use the system temporary storage area
|
||||
args = ['--dev', '--federated-only', 'configure', 'install', '--ursula', '--force']
|
||||
result = runner.invoke(cli, args,
|
||||
input='{}\n{}'''.format(TEST_URSULA_INSECURE_DEVELOPMENT_PASSWORD,
|
||||
TEST_URSULA_INSECURE_DEVELOPMENT_PASSWORD),
|
||||
catch_exceptions=False)
|
||||
assert '/tmp' in result.output, "Configuration not in system temporary directory"
|
||||
assert NodeConfiguration._NodeConfiguration__TEMP_CONFIGURATION_DIR_PREFIX in result.output
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Use a custom local filepath
|
||||
args = ['--config-root', custom_filepath, '--federated-only', 'configure', 'install', '--ursula', '--force']
|
||||
result = runner.invoke(cli, args,
|
||||
input='{}\n{}'''.format(TEST_URSULA_INSECURE_DEVELOPMENT_PASSWORD,
|
||||
TEST_URSULA_INSECURE_DEVELOPMENT_PASSWORD),
|
||||
catch_exceptions=False)
|
||||
assert TEST_CUSTOM_INSTALLATION_PATH in result.output, "Configuration not in system temporary directory"
|
||||
assert 'Created' in result.output
|
||||
assert custom_filepath in result.output
|
||||
assert "'nucypher ursula run'" in result.output
|
||||
assert result.exit_code == 0
|
||||
assert os.path.isdir(custom_filepath)
|
||||
|
||||
# Ensure that there are not pre-existing configuration files at config_root
|
||||
_result = runner.invoke(cli, args,
|
||||
input='{}\n{}'''.format(TEST_URSULA_INSECURE_DEVELOPMENT_PASSWORD,
|
||||
TEST_URSULA_INSECURE_DEVELOPMENT_PASSWORD),
|
||||
catch_exceptions=False)
|
||||
assert "There are existing configuration files" in _result.output
|
||||
|
||||
# Destroy / Uninstall
|
||||
args = ['--config-root', custom_filepath, 'configure', 'destroy']
|
||||
result = runner.invoke(cli, args, input='Y', catch_exceptions=False)
|
||||
assert '[y/N]' in result.output
|
||||
assert TEST_CUSTOM_INSTALLATION_PATH in result.output, "Configuration not in system temporary directory"
|
||||
assert 'Deleted' in result.output
|
||||
assert custom_filepath in result.output
|
||||
assert result.exit_code == 0
|
||||
assert not os.path.isdir(custom_filepath)
|
||||
|
||||
# # TODO: Integrate with run ursula
|
||||
|
||||
|
||||
@pytest.mark.skip()
|
||||
def test_validate_runtime_filepaths(custom_filepath):
|
||||
runner = CliRunner()
|
||||
|
||||
args = ['--config-root', custom_filepath, 'configure', 'install', '--no-registry']
|
||||
result = runner.invoke(cli, args, input='Y', catch_exceptions=False)
|
||||
result = runner.invoke(cli, ['--config-root', custom_filepath,
|
||||
'configure', 'validate',
|
||||
'--filesystem',
|
||||
'--no-registry'], catch_exceptions=False)
|
||||
assert custom_filepath in result.output
|
||||
assert 'Valid' in result.output
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Remove the known nodes dir to "corrupt" the tree
|
||||
shutil.rmtree(os.path.join(custom_filepath, 'known_nodes'))
|
||||
result = runner.invoke(cli, ['--config-root', custom_filepath,
|
||||
'configure', 'validate',
|
||||
'--filesystem',
|
||||
'--no-registry'], catch_exceptions=False)
|
||||
assert custom_filepath in result.output
|
||||
assert 'Invalid' in result.output
|
||||
assert result.exit_code == 0 # TODO: exit differently for invalidity?
|
|
@ -14,6 +14,8 @@ GNU General Public License for more details.
|
|||
You should have received a copy of the GNU General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
|
||||
from functools import partial
|
||||
|
||||
import pytest_twisted
|
||||
|
@ -45,6 +47,7 @@ def test_get_cert_from_running_seed_node(ursula_federated_test_config):
|
|||
ursula_config=ursula_federated_test_config,
|
||||
quantity=1,
|
||||
know_each_other=False)
|
||||
|
||||
firstula = lonely_ursula_maker().pop()
|
||||
node_deployer = firstula.get_deployer()
|
||||
|
||||
|
@ -61,6 +64,11 @@ def test_get_cert_from_running_seed_node(ursula_federated_test_config):
|
|||
|
||||
def start_lonely_learning_loop():
|
||||
any_other_ursula.start_learning_loop()
|
||||
start = maya.now()
|
||||
while firstula not in any_other_ursula.known_nodes:
|
||||
passed = maya.now() - start
|
||||
if passed.seconds > 2:
|
||||
pytest.fail("Didn't find the seed node.")
|
||||
any_other_ursula.block_until_specific_nodes_are_known(set([firstula.checksum_public_address]), timeout=2)
|
||||
|
||||
yield deferToThread(start_lonely_learning_loop)
|
||||
|
|
|
@ -8,22 +8,22 @@ from nucypher.crypto.powers import DelegatingPower, EncryptingPower
|
|||
|
||||
|
||||
@pytest.mark.skip("Redacted and refactored for sensitive info leakage")
|
||||
def test_validate_passphrase():
|
||||
# Passphrase too short
|
||||
passphrase = 'x' * 5
|
||||
def test_validate_password():
|
||||
# Password too short
|
||||
password = 'x' * 5
|
||||
with pytest.raises(ValueError):
|
||||
_keyring = NucypherKeyring.generate(passphrase=passphrase)
|
||||
_keyring = NucypherKeyring.generate(password=password)
|
||||
|
||||
# Empty passphrase is provided
|
||||
# Empty password is provided
|
||||
with pytest.raises(ValueError):
|
||||
_keyring = NucypherKeyring.generate(passphrase="")
|
||||
_keyring = NucypherKeyring.generate(password="")
|
||||
|
||||
|
||||
def test_generate_alice_keyring(tmpdir):
|
||||
passphrase = 'x' * 16
|
||||
password = 'x' * 16
|
||||
|
||||
keyring = NucypherKeyring.generate(
|
||||
passphrase=passphrase,
|
||||
password=password,
|
||||
encrypting=True,
|
||||
wallet=False,
|
||||
tls=False,
|
||||
|
@ -36,7 +36,7 @@ def test_generate_alice_keyring(tmpdir):
|
|||
with pytest.raises(NucypherKeyring.KeyringLocked):
|
||||
_enc_keypair = keyring.derive_crypto_power(EncryptingPower).keypair
|
||||
|
||||
keyring.unlock(passphrase)
|
||||
keyring.unlock(password)
|
||||
enc_keypair = keyring.derive_crypto_power(EncryptingPower).keypair
|
||||
|
||||
assert enc_pubkey == enc_keypair.pubkey
|
||||
|
|
|
@ -27,11 +27,11 @@ from moto import mock_s3
|
|||
from nucypher.characters.lawful import Ursula
|
||||
from nucypher.config.storages import (
|
||||
S3NodeStorage,
|
||||
InMemoryNodeStorage,
|
||||
ForgetfulNodeStorage,
|
||||
TemporaryFileBasedNodeStorage,
|
||||
NodeStorage,
|
||||
LocalFileBasedNodeStorage
|
||||
NodeStorage
|
||||
)
|
||||
from nucypher.utilities.sandbox.constants import MOCK_URSULA_DB_FILEPATH
|
||||
|
||||
MOCK_S3_BUCKET_NAME = 'mock-seednodes'
|
||||
S3_DOMAIN_NAME = 's3.amazonaws.com'
|
||||
|
@ -41,26 +41,25 @@ class BaseTestNodeStorageBackends:
|
|||
|
||||
@pytest.fixture(scope='class')
|
||||
def light_ursula(temp_dir_path):
|
||||
db_name = 'ursula-{}.db'.format(10151)
|
||||
db_filepath = 'ursula-{}.db'.format(10151)
|
||||
try:
|
||||
node = Ursula(rest_host='127.0.0.1',
|
||||
rest_port=10151,
|
||||
db_filepath=db_name,
|
||||
db_name=db_name,
|
||||
db_filepath=MOCK_URSULA_DB_FILEPATH,
|
||||
federated_only=True)
|
||||
|
||||
yield node
|
||||
finally:
|
||||
with contextlib.suppress(Exception):
|
||||
os.remove(db_name)
|
||||
os.remove(db_filepath)
|
||||
|
||||
character_class = Ursula
|
||||
federated_only = True
|
||||
storage_backend = NotImplemented
|
||||
|
||||
def _read_and_write_to_storage(self, ursula, node_storage):
|
||||
def _read_and_write_metadata(self, ursula, node_storage):
|
||||
# Write Node
|
||||
node_storage.save(node=ursula)
|
||||
node_storage.store_node_metadata(node=ursula)
|
||||
|
||||
# Read Node
|
||||
node_from_storage = node_storage.get(checksum_address=ursula.checksum_public_address,
|
||||
|
@ -70,8 +69,8 @@ class BaseTestNodeStorageBackends:
|
|||
# Save more nodes
|
||||
all_known_nodes = set()
|
||||
for port in range(10152, 10251):
|
||||
node = Ursula(rest_host='127.0.0.1', rest_port=port, federated_only=True)
|
||||
node_storage.save(node=node)
|
||||
node = Ursula(rest_host='127.0.0.1', db_filepath=MOCK_URSULA_DB_FILEPATH, rest_port=port, federated_only=True)
|
||||
node_storage.store_node_metadata(node=node)
|
||||
all_known_nodes.add(node)
|
||||
|
||||
# Read all nodes from storage
|
||||
|
@ -89,12 +88,12 @@ class BaseTestNodeStorageBackends:
|
|||
|
||||
return True
|
||||
|
||||
def _write_and_delete_nodes_in_storage(self, ursula, node_storage):
|
||||
def _write_and_delete_metadata(self, ursula, node_storage):
|
||||
# Write Node
|
||||
node_storage.save(node=ursula)
|
||||
node_storage.store_node_metadata(node=ursula)
|
||||
|
||||
# Delete Node
|
||||
node_storage.remove(checksum_address=ursula.checksum_public_address)
|
||||
node_storage.remove(checksum_address=ursula.checksum_public_address, certificate=False)
|
||||
|
||||
# Read Node
|
||||
with pytest.raises(NodeStorage.UnknownNode):
|
||||
|
@ -112,15 +111,15 @@ class BaseTestNodeStorageBackends:
|
|||
#
|
||||
|
||||
def test_delete_node_in_storage(self, light_ursula):
|
||||
assert self._write_and_delete_nodes_in_storage(ursula=light_ursula, node_storage=self.storage_backend)
|
||||
assert self._write_and_delete_metadata(ursula=light_ursula, node_storage=self.storage_backend)
|
||||
|
||||
def test_read_and_write_to_storage(self, light_ursula):
|
||||
assert self._read_and_write_to_storage(ursula=light_ursula, node_storage=self.storage_backend)
|
||||
assert self._read_and_write_metadata(ursula=light_ursula, node_storage=self.storage_backend)
|
||||
|
||||
|
||||
class TestInMemoryNodeStorage(BaseTestNodeStorageBackends):
|
||||
storage_backend = InMemoryNodeStorage(character_class=BaseTestNodeStorageBackends.character_class,
|
||||
federated_only=BaseTestNodeStorageBackends.federated_only)
|
||||
storage_backend = ForgetfulNodeStorage(character_class=BaseTestNodeStorageBackends.character_class,
|
||||
federated_only=BaseTestNodeStorageBackends.federated_only)
|
||||
storage_backend.initialize()
|
||||
|
||||
|
||||
|
@ -149,7 +148,7 @@ class TestS3NodeStorageDirect(BaseTestNodeStorageBackends):
|
|||
@mock_s3
|
||||
def test_generate_presigned_url(self, light_ursula):
|
||||
s3_node_storage = self.s3_node_storage_factory()
|
||||
s3_node_storage.save(node=light_ursula)
|
||||
s3_node_storage.store_node_metadata(node=light_ursula)
|
||||
presigned_url = s3_node_storage.generate_presigned_url(checksum_address=light_ursula.checksum_public_address)
|
||||
|
||||
assert S3_DOMAIN_NAME in presigned_url
|
||||
|
@ -164,7 +163,7 @@ class TestS3NodeStorageDirect(BaseTestNodeStorageBackends):
|
|||
s3_node_storage = self.s3_node_storage_factory()
|
||||
|
||||
# Write Node
|
||||
s3_node_storage.save(node=light_ursula)
|
||||
s3_node_storage.store_node_metadata(node=light_ursula)
|
||||
|
||||
# Read Node
|
||||
node_from_storage = s3_node_storage.get(checksum_address=light_ursula.checksum_public_address,
|
||||
|
@ -175,7 +174,7 @@ class TestS3NodeStorageDirect(BaseTestNodeStorageBackends):
|
|||
all_known_nodes = set()
|
||||
for port in range(10152, 10251):
|
||||
node = Ursula(rest_host='127.0.0.1', rest_port=port, federated_only=True)
|
||||
s3_node_storage.save(node=node)
|
||||
s3_node_storage.store_node_metadata(node=node)
|
||||
all_known_nodes.add(node)
|
||||
|
||||
# Read all nodes from storage
|
||||
|
@ -198,7 +197,7 @@ class TestS3NodeStorageDirect(BaseTestNodeStorageBackends):
|
|||
s3_node_storage = self.s3_node_storage_factory()
|
||||
|
||||
# Write Node
|
||||
s3_node_storage.save(node=light_ursula)
|
||||
s3_node_storage.store_node_metadata(node=light_ursula)
|
||||
|
||||
# Delete Node
|
||||
s3_node_storage.remove(checksum_address=light_ursula.checksum_public_address)
|
||||
|
|
|
@ -18,15 +18,27 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
|||
import pytest
|
||||
from twisted.logger import globalLogPublisher
|
||||
|
||||
from nucypher.cli.main import NucypherClickConfig
|
||||
from nucypher.utilities.logging import SimpleObserver
|
||||
|
||||
#
|
||||
from nucypher.cli import NucypherClickConfig
|
||||
from nucypher.utilities.logging import SimpleObserver
|
||||
|
||||
# Logger Configuration
|
||||
NucypherClickConfig.log_to_sentry = False
|
||||
#
|
||||
|
||||
# Disable click sentry and file logging
|
||||
NucypherClickConfig.log_to_sentry = False
|
||||
NucypherClickConfig.log_to_file = False
|
||||
|
||||
|
||||
#
|
||||
# Pytest configuration
|
||||
#
|
||||
|
||||
pytest_plugins = [
|
||||
'tests.fixtures',
|
||||
'tests.fixtures', # Includes external fixtures module
|
||||
]
|
||||
|
||||
|
||||
|
@ -47,3 +59,8 @@ def pytest_collection_modifyitems(config, items):
|
|||
log_level_name = config.getoption("--log-level", "info", skip=True)
|
||||
observer = SimpleObserver(log_level_name)
|
||||
globalLogPublisher.addObserver(observer)
|
||||
|
||||
# Timber!
|
||||
log_level_name = config.getoption("--log-level", "info", skip=True)
|
||||
observer = SimpleObserver(log_level_name)
|
||||
globalLogPublisher.addObserver(observer)
|
||||
|
|
|
@ -14,23 +14,25 @@ GNU General Public License for more details.
|
|||
You should have received a copy of the GNU General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
|
||||
import contextlib
|
||||
import glob
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import datetime
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import tempfile
|
||||
import maya
|
||||
import pytest
|
||||
from constant_sorrow import constants
|
||||
import re
|
||||
import shutil
|
||||
from sqlalchemy.engine import create_engine
|
||||
|
||||
from constant_sorrow.constants import NON_PAYMENT
|
||||
from nucypher.blockchain.eth.constants import DISPATCHER_SECRET_LENGTH
|
||||
from nucypher.blockchain.eth.deployers import PolicyManagerDeployer, NucypherTokenDeployer, MinerEscrowDeployer
|
||||
from nucypher.blockchain.eth.interfaces import BlockchainDeployerInterface
|
||||
from nucypher.blockchain.eth.registry import TemporaryEthereumContractRegistry, InMemoryEthereumContractRegistry
|
||||
from nucypher.blockchain.eth.registry import InMemoryEthereumContractRegistry
|
||||
from nucypher.blockchain.eth.sol.compile import SolidityCompiler
|
||||
from nucypher.config.characters import UrsulaConfiguration, AliceConfiguration, BobConfiguration
|
||||
from nucypher.config.constants import BASE_DIR
|
||||
|
@ -40,9 +42,8 @@ from nucypher.keystore import keystore
|
|||
from nucypher.keystore.db import Base
|
||||
from nucypher.keystore.keypairs import SigningKeypair
|
||||
from nucypher.utilities.sandbox.blockchain import TesterBlockchain, token_airdrop
|
||||
from nucypher.utilities.sandbox.constants import (DEFAULT_NUMBER_OF_URSULAS_IN_DEVELOPMENT_NETWORK,
|
||||
DEVELOPMENT_TOKEN_AIRDROP_AMOUNT,
|
||||
TEST_URSULA_INSECURE_DEVELOPMENT_PASSWORD)
|
||||
from nucypher.utilities.sandbox.constants import (NUMBER_OF_URSULAS_IN_DEVELOPMENT_NETWORK,
|
||||
DEVELOPMENT_TOKEN_AIRDROP_AMOUNT)
|
||||
from nucypher.utilities.sandbox.middleware import MockRestMiddleware
|
||||
from nucypher.utilities.sandbox.ursula import make_federated_ursulas, make_decentralized_ursulas
|
||||
|
||||
|
@ -59,7 +60,7 @@ def cleanup():
|
|||
yield # we've got a lot of men and women here...
|
||||
|
||||
# Database teardown
|
||||
for f in glob.glob("./**/*.db"): # TODO: Needs cleanup
|
||||
for f in glob.glob("**/*.db"): # TODO: Needs cleanup
|
||||
os.remove(f)
|
||||
|
||||
# Temp Storage Teardown
|
||||
|
@ -90,8 +91,7 @@ def temp_config_root(temp_dir_path):
|
|||
"""
|
||||
User is responsible for closing the file given at the path.
|
||||
"""
|
||||
default_node_config = NodeConfiguration(temp=True,
|
||||
auto_initialize=False,
|
||||
default_node_config = NodeConfiguration(dev_mode=True,
|
||||
config_root=temp_dir_path,
|
||||
import_seed_registry=False)
|
||||
yield default_node_config.config_root
|
||||
|
@ -121,29 +121,23 @@ def certificates_tempdir():
|
|||
@pytest.fixture(scope="module")
|
||||
def ursula_federated_test_config():
|
||||
|
||||
ursula_config = UrsulaConfiguration(temp=True,
|
||||
auto_initialize=True,
|
||||
auto_generate_keys=True,
|
||||
passphrase=TEST_URSULA_INSECURE_DEVELOPMENT_PASSWORD,
|
||||
ursula_config = UrsulaConfiguration(dev_mode=True,
|
||||
is_me=True,
|
||||
start_learning_now=False,
|
||||
abort_on_learning_error=True,
|
||||
federated_only=True,
|
||||
network_middleware=MockRestMiddleware(),
|
||||
save_metadata=False,
|
||||
load_metadata=False)
|
||||
reload_metadata=False)
|
||||
yield ursula_config
|
||||
ursula_config.cleanup()
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
@pytest.mark.usefixtures('three_agents')
|
||||
def ursula_decentralized_test_config(three_agents):
|
||||
token_agent, miner_agent, policy_agent = three_agents
|
||||
|
||||
ursula_config = UrsulaConfiguration(temp=True,
|
||||
auto_initialize=True,
|
||||
auto_generate_keys=True,
|
||||
passphrase=TEST_URSULA_INSECURE_DEVELOPMENT_PASSWORD,
|
||||
ursula_config = UrsulaConfiguration(dev_mode=True,
|
||||
is_me=True,
|
||||
start_learning_now=False,
|
||||
abort_on_learning_error=True,
|
||||
|
@ -151,24 +145,21 @@ def ursula_decentralized_test_config(three_agents):
|
|||
network_middleware=MockRestMiddleware(),
|
||||
import_seed_registry=False,
|
||||
save_metadata=False,
|
||||
load_metadata=False)
|
||||
reload_metadata=False)
|
||||
yield ursula_config
|
||||
ursula_config.cleanup()
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def alice_federated_test_config(federated_ursulas):
|
||||
config = AliceConfiguration(temp=True,
|
||||
auto_initialize=True,
|
||||
auto_generate_keys=True,
|
||||
passphrase=TEST_URSULA_INSECURE_DEVELOPMENT_PASSWORD,
|
||||
config = AliceConfiguration(dev_mode=True,
|
||||
is_me=True,
|
||||
network_middleware=MockRestMiddleware(),
|
||||
known_nodes=federated_ursulas,
|
||||
federated_only=True,
|
||||
abort_on_learning_error=True,
|
||||
save_metadata=False,
|
||||
load_metadata=False)
|
||||
reload_metadata=False)
|
||||
yield config
|
||||
config.cleanup()
|
||||
|
||||
|
@ -178,34 +169,28 @@ def alice_blockchain_test_config(blockchain_ursulas, three_agents):
|
|||
token_agent, miner_agent, policy_agent = three_agents
|
||||
etherbase, alice_address, bob_address, *everyone_else = token_agent.blockchain.interface.w3.eth.accounts
|
||||
|
||||
config = AliceConfiguration(temp=True,
|
||||
config = AliceConfiguration(dev_mode=True,
|
||||
is_me=True,
|
||||
auto_initialize=True,
|
||||
auto_generate_keys=True,
|
||||
checksum_address=alice_address,
|
||||
passphrase=TEST_URSULA_INSECURE_DEVELOPMENT_PASSWORD,
|
||||
checksum_public_address=alice_address,
|
||||
network_middleware=MockRestMiddleware(),
|
||||
known_nodes=blockchain_ursulas,
|
||||
abort_on_learning_error=True,
|
||||
import_seed_registry=False,
|
||||
save_metadata=False,
|
||||
load_metadata=False)
|
||||
reload_metadata=False)
|
||||
yield config
|
||||
config.cleanup()
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def bob_federated_test_config():
|
||||
config = BobConfiguration(temp=True,
|
||||
auto_initialize=True,
|
||||
auto_generate_keys=True,
|
||||
passphrase=TEST_URSULA_INSECURE_DEVELOPMENT_PASSWORD,
|
||||
config = BobConfiguration(dev_mode=True,
|
||||
network_middleware=MockRestMiddleware(),
|
||||
start_learning_now=False,
|
||||
abort_on_learning_error=True,
|
||||
federated_only=True,
|
||||
save_metadata=False,
|
||||
load_metadata=False)
|
||||
reload_metadata=False)
|
||||
yield config
|
||||
config.cleanup()
|
||||
|
||||
|
@ -215,11 +200,8 @@ def bob_blockchain_test_config(blockchain_ursulas, three_agents):
|
|||
token_agent, miner_agent, policy_agent = three_agents
|
||||
etherbase, alice_address, bob_address, *everyone_else = token_agent.blockchain.interface.w3.eth.accounts
|
||||
|
||||
config = BobConfiguration(temp=True,
|
||||
auto_initialize=True,
|
||||
auto_generate_keys=True,
|
||||
checksum_address=bob_address,
|
||||
passphrase=TEST_URSULA_INSECURE_DEVELOPMENT_PASSWORD,
|
||||
config = BobConfiguration(dev_mode=True,
|
||||
checksum_public_address=bob_address,
|
||||
network_middleware=MockRestMiddleware(),
|
||||
known_nodes=blockchain_ursulas,
|
||||
start_learning_now=False,
|
||||
|
@ -227,7 +209,7 @@ def bob_blockchain_test_config(blockchain_ursulas, three_agents):
|
|||
federated_only=False,
|
||||
import_seed_registry=False,
|
||||
save_metadata=False,
|
||||
load_metadata=False)
|
||||
reload_metadata=False)
|
||||
yield config
|
||||
config.cleanup()
|
||||
|
||||
|
@ -241,7 +223,7 @@ def idle_federated_policy(federated_alice, federated_bob):
|
|||
"""
|
||||
Creates a Policy, in a manner typical of how Alice might do it, with a unique label
|
||||
"""
|
||||
n = DEFAULT_NUMBER_OF_URSULAS_IN_DEVELOPMENT_NETWORK
|
||||
n = NUMBER_OF_URSULAS_IN_DEVELOPMENT_NETWORK
|
||||
random_label = b'label://' + os.urandom(32)
|
||||
policy = federated_alice.create_policy(federated_bob, label=random_label, m=3, n=n, federated=True)
|
||||
return policy
|
||||
|
@ -250,7 +232,7 @@ def idle_federated_policy(federated_alice, federated_bob):
|
|||
@pytest.fixture(scope="module")
|
||||
def enacted_federated_policy(idle_federated_policy, federated_ursulas):
|
||||
# Alice has a policy in mind and knows of enough qualifies Ursulas; she crafts an offer for them.
|
||||
deposit = constants.NON_PAYMENT
|
||||
deposit = NON_PAYMENT
|
||||
contract_end_datetime = maya.now() + datetime.timedelta(days=5)
|
||||
network_middleware = MockRestMiddleware()
|
||||
|
||||
|
@ -277,7 +259,7 @@ def idle_blockchain_policy(blockchain_alice, blockchain_bob):
|
|||
@pytest.fixture(scope="module")
|
||||
def enacted_blockchain_policy(idle_blockchain_policy, blockchain_ursulas):
|
||||
# Alice has a policy in mind and knows of enough qualifies Ursulas; she crafts an offer for them.
|
||||
deposit = constants.NON_PAYMENT(b"0000000")
|
||||
deposit = NON_PAYMENT(b"0000000")
|
||||
contract_end_datetime = maya.now() + datetime.timedelta(days=5)
|
||||
network_middleware = MockRestMiddleware()
|
||||
|
||||
|
@ -331,7 +313,7 @@ def blockchain_bob(bob_blockchain_test_config):
|
|||
@pytest.fixture(scope="module")
|
||||
def federated_ursulas(ursula_federated_test_config):
|
||||
_ursulas = make_federated_ursulas(ursula_config=ursula_federated_test_config,
|
||||
quantity=DEFAULT_NUMBER_OF_URSULAS_IN_DEVELOPMENT_NETWORK)
|
||||
quantity=NUMBER_OF_URSULAS_IN_DEVELOPMENT_NETWORK)
|
||||
yield _ursulas
|
||||
|
||||
|
||||
|
@ -340,7 +322,7 @@ def blockchain_ursulas(three_agents, ursula_decentralized_test_config):
|
|||
token_agent, miner_agent, policy_agent = three_agents
|
||||
etherbase, alice, bob, *all_yall = token_agent.blockchain.interface.w3.eth.accounts
|
||||
|
||||
ursula_addresses = all_yall[:DEFAULT_NUMBER_OF_URSULAS_IN_DEVELOPMENT_NETWORK]
|
||||
ursula_addresses = all_yall[:NUMBER_OF_URSULAS_IN_DEVELOPMENT_NETWORK]
|
||||
|
||||
token_airdrop(origin=etherbase,
|
||||
addresses=ursula_addresses,
|
||||
|
@ -382,7 +364,7 @@ def testerchain(solidity_compiler):
|
|||
|
||||
# Create the blockchain
|
||||
testerchain = TesterBlockchain(interface=deployer_interface,
|
||||
test_accounts=DEFAULT_NUMBER_OF_URSULAS_IN_DEVELOPMENT_NETWORK,
|
||||
test_accounts=NUMBER_OF_URSULAS_IN_DEVELOPMENT_NETWORK,
|
||||
airdrop=False)
|
||||
|
||||
origin, *everyone = testerchain.interface.w3.eth.accounts
|
||||
|
|
|
@ -14,6 +14,8 @@ GNU General Public License for more details.
|
|||
You should have received a copy of the GNU General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
@ -26,7 +28,7 @@ from nucypher.characters.unlawful import Vladimir
|
|||
from nucypher.crypto.api import keccak_digest
|
||||
from nucypher.crypto.powers import SigningPower
|
||||
from nucypher.network.nicknames import nickname_from_seed
|
||||
from nucypher.utilities.sandbox.constants import TEST_URSULA_INSECURE_DEVELOPMENT_PASSWORD
|
||||
from nucypher.utilities.sandbox.constants import INSECURE_DEVELOPMENT_PASSWORD
|
||||
from nucypher.utilities.sandbox.middleware import MockRestMiddleware
|
||||
|
||||
|
||||
|
@ -41,8 +43,8 @@ def test_all_blockchain_ursulas_know_about_all_other_ursulas(blockchain_ursulas,
|
|||
if address == propagating_ursula.checksum_public_address:
|
||||
continue
|
||||
else:
|
||||
assert address in propagating_ursula.known_nodes, "{} did not know about {}".format(propagating_ursula,
|
||||
nickname_from_seed(address))
|
||||
assert address in propagating_ursula.known_nodes.addresses(), "{} did not know about {}".format(propagating_ursula,
|
||||
nickname_from_seed(address))
|
||||
|
||||
|
||||
@pytest.mark.slow()
|
||||
|
@ -191,11 +193,16 @@ def test_alice_refuses_to_make_arrangement_unless_ursula_is_valid(blockchain_ali
|
|||
message = vladimir._signable_interface_info_message()
|
||||
signature = vladimir._crypto_power.power_ups(SigningPower).sign(message)
|
||||
|
||||
vladimir.substantiate_stamp(passphrase=TEST_URSULA_INSECURE_DEVELOPMENT_PASSWORD)
|
||||
vladimir.substantiate_stamp(password=INSECURE_DEVELOPMENT_PASSWORD)
|
||||
vladimir._interface_signature_object = signature
|
||||
|
||||
class FakeArrangement:
|
||||
federated = False
|
||||
ursula = target
|
||||
|
||||
vladimir.node_storage.store_node_certificate(host=target.rest_information()[0].host,
|
||||
certificate=target.certificate,
|
||||
checksum_address=target.checksum_public_address)
|
||||
|
||||
with pytest.raises(vladimir.InvalidNode):
|
||||
idle_blockchain_policy.consider_arrangement(network_middleware=blockchain_alice.network_middleware,
|
||||
|
|
|
@ -26,8 +26,10 @@ from nucypher.utilities.sandbox.ursula import make_federated_ursulas
|
|||
|
||||
@pytest_twisted.inlineCallbacks
|
||||
def test_one_node_stores_a_bunch_of_others(federated_ursulas, ursula_federated_test_config):
|
||||
the_chosen_seednode = list(federated_ursulas)[2]
|
||||
|
||||
the_chosen_seednode = list(federated_ursulas)[2] # ...neo?
|
||||
seed_node = the_chosen_seednode.seed_node_metadata()
|
||||
|
||||
newcomer = make_federated_ursulas(
|
||||
ursula_config=ursula_federated_test_config,
|
||||
quantity=1,
|
||||
|
@ -41,9 +43,9 @@ def test_one_node_stores_a_bunch_of_others(federated_ursulas, ursula_federated_t
|
|||
newcomer.start_learning_loop()
|
||||
start = maya.now()
|
||||
# Loop until the_chosen_seednode is in storage.
|
||||
while not the_chosen_seednode in newcomer.node_storage.all(federated_only=True):
|
||||
while the_chosen_seednode not in newcomer.node_storage.all(federated_only=True):
|
||||
passed = maya.now() - start
|
||||
if passed.seconds > 2:
|
||||
if passed.seconds > 5:
|
||||
pytest.fail("Didn't find the seed node.")
|
||||
|
||||
yield deferToThread(start_lonely_learning_loop)
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
from nucypher.config.characters import UrsulaConfiguration
|
||||
|
||||
|
||||
def test_render_lonely_ursula_status_page(tmpdir):
|
||||
ursula_config = UrsulaConfiguration(dev_mode=True, federated_only=True)
|
||||
ursula = ursula_config()
|
||||
|
||||
template = ursula.rest_server.routes._status_template
|
||||
rendering = template.render(this_node=ursula, known_nodes=ursula.known_nodes)
|
||||
assert '<!DOCTYPE html>' in rendering
|
||||
assert ursula.nickname in rendering
|
||||
|
||||
|
||||
def test_render_ursula_status_page_with_known_nodes(tmpdir, federated_ursulas):
|
||||
ursula_config = UrsulaConfiguration(dev_mode=True, federated_only=True, known_nodes=federated_ursulas)
|
||||
ursula = ursula_config()
|
||||
|
||||
template = ursula.rest_server.routes._status_template
|
||||
rendering = template.render(this_node=ursula, known_nodes=ursula.known_nodes)
|
||||
assert '<!DOCTYPE html>' in rendering
|
||||
assert ursula.nickname in rendering
|
||||
|
||||
# Every known nodes address is rendered
|
||||
for known_ursula in federated_ursulas:
|
||||
assert known_ursula.checksum_public_address in rendering
|
|
@ -16,14 +16,25 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
|||
"""
|
||||
|
||||
|
||||
from click.testing import CliRunner
|
||||
import os
|
||||
|
||||
from nucypher.cli import cli
|
||||
import maya
|
||||
import pytest
|
||||
|
||||
|
||||
def test_help_message():
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ['--help'], catch_exceptions=False)
|
||||
class NucypherPytestRunner:
|
||||
TEST_PATH = os.path.join('tests', 'cli')
|
||||
PYTEST_ARGS = ['--verbose', TEST_PATH]
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert 'Usage: cli [OPTIONS] COMMAND [ARGS]' in result.output, 'Missing or invalid help text was produced.'
|
||||
def pytest_sessionstart(self):
|
||||
print("*** Running Nucypher CLI Tests ***")
|
||||
self.start_time = maya.now()
|
||||
|
||||
def pytest_sessionfinish(self):
|
||||
duration = maya.now() - self.start_time
|
||||
print("*** Nucypher Test Run Report ***")
|
||||
print("""Run Duration ... {}""".format(duration))
|
||||
|
||||
|
||||
def run():
|
||||
pytest.main(NucypherPytestRunner.PYTEST_ARGS, plugins=[NucypherPytestRunner()])
|
Loading…
Reference in New Issue