mirror of https://github.com/nucypher/nucypher.git
Merge pull request #2664 from nucypher/porter
[EPIC] Porter MVP - "Infura for NuCypher"pull/2771/head
commit
47d281a30e
|
@ -84,6 +84,14 @@ workflows:
|
|||
requires:
|
||||
- unit
|
||||
- integration
|
||||
- porter:
|
||||
context: "NuCypher Tests"
|
||||
filters:
|
||||
tags:
|
||||
only: /.*/
|
||||
requires:
|
||||
- unit
|
||||
- integration
|
||||
- tests_ok:
|
||||
filters:
|
||||
tags:
|
||||
|
@ -96,6 +104,7 @@ workflows:
|
|||
- cli
|
||||
- deployers
|
||||
- utilities
|
||||
- porter
|
||||
- build_dev_docker_images:
|
||||
filters:
|
||||
tags:
|
||||
|
@ -136,6 +145,14 @@ workflows:
|
|||
only: main
|
||||
requires:
|
||||
- test_build
|
||||
- build_porter_docker:
|
||||
filters:
|
||||
tags:
|
||||
only: /v[0-9]+.*/
|
||||
branches:
|
||||
only: main
|
||||
requires:
|
||||
- test_build
|
||||
- publish_docker_experimental:
|
||||
context: "NuCypher Docker"
|
||||
requires:
|
||||
|
@ -149,6 +166,7 @@ workflows:
|
|||
type: approval
|
||||
requires:
|
||||
- build_docker
|
||||
- build_porter_docker
|
||||
filters:
|
||||
tags:
|
||||
only: /v[0-9]+.*/
|
||||
|
@ -172,6 +190,15 @@ workflows:
|
|||
only: /v[0-9]+.*/
|
||||
branches:
|
||||
ignore: /.*/
|
||||
- publish_porter_docker:
|
||||
context: "NuCypher Porter Docker"
|
||||
requires:
|
||||
- request_publication_approval
|
||||
filters:
|
||||
tags:
|
||||
only: /v[0-9]+.*/
|
||||
branches:
|
||||
ignore: /.*/
|
||||
nightly:
|
||||
triggers:
|
||||
- schedule:
|
||||
|
@ -279,6 +306,13 @@ workflows:
|
|||
only: /.*/
|
||||
requires:
|
||||
- pip_install_37
|
||||
- porter:
|
||||
context: "Nightly"
|
||||
filters:
|
||||
tags:
|
||||
only: /.*/
|
||||
requires:
|
||||
- pip_install_37
|
||||
- tests_ok:
|
||||
filters:
|
||||
tags:
|
||||
|
@ -291,6 +325,7 @@ workflows:
|
|||
- cli
|
||||
- deployers
|
||||
- utilities
|
||||
- porter
|
||||
- build_dev_docker_images:
|
||||
filters:
|
||||
tags:
|
||||
|
@ -337,6 +372,14 @@ workflows:
|
|||
only: main
|
||||
requires:
|
||||
- test_build
|
||||
- build_porter_docker:
|
||||
filters:
|
||||
tags:
|
||||
only: /v[0-9]+.*/
|
||||
branches:
|
||||
only: main
|
||||
requires:
|
||||
- test_build
|
||||
|
||||
|
||||
python_36_base: &python_36_base
|
||||
|
@ -419,7 +462,7 @@ commands:
|
|||
- run: sudo chown -R circleci:circleci /usr/local/bin
|
||||
- run: sudo chown -R circleci:circleci /usr/local/lib/python3.7/site-packages
|
||||
- save_cache:
|
||||
key: pip-v7-{{ .Branch }}-{{ checksum "Pipfile.lock" }}
|
||||
key: pip-v8-{{ .Branch }}-{{ checksum "Pipfile.lock" }}
|
||||
paths:
|
||||
- "~/.local/bin"
|
||||
- "~/.local/lib/python3.7/site-packages"
|
||||
|
@ -434,7 +477,7 @@ commands:
|
|||
- run: sudo chown -R circleci:circleci /usr/local/bin
|
||||
- run: sudo chown -R circleci:circleci /usr/local/lib/python3.7/site-packages
|
||||
- restore_cache: # ensure this step occurs *before* installing dependencies
|
||||
key: pip-v7-{{ .Branch }}-{{ checksum "Pipfile.lock" }}
|
||||
key: pip-v8-{{ .Branch }}-{{ checksum "Pipfile.lock" }}
|
||||
- restore_cache:
|
||||
key: solc-v2-{{ checksum "nucypher/blockchain/eth/sol/__conf__.py" }}
|
||||
|
||||
|
@ -681,6 +724,18 @@ jobs:
|
|||
- run_test_suite
|
||||
- capture_test_results
|
||||
|
||||
porter:
|
||||
<<: *python_37_base
|
||||
parallelism: 1
|
||||
steps:
|
||||
- prepare_environment
|
||||
- run:
|
||||
name: Preparing Nucypher Porter Tests
|
||||
command: |
|
||||
circleci tests glob "tests/acceptance/porter/**/test_*.py" | circleci tests split --split-by=timings | tee test-names.tmp
|
||||
- run_test_suite
|
||||
- capture_test_results
|
||||
|
||||
tests_ok:
|
||||
<<: *python_37_base
|
||||
steps:
|
||||
|
@ -805,6 +860,37 @@ jobs:
|
|||
paths:
|
||||
- ~/docker/nucypher.tar
|
||||
|
||||
build_porter_docker:
|
||||
working_directory: ~/nucypher
|
||||
docker:
|
||||
- image: cimg/python:3.8.6
|
||||
steps:
|
||||
- checkout
|
||||
- setup_remote_docker
|
||||
- restore_cache:
|
||||
keys:
|
||||
- v2-{{ .Branch }}-{{ arch }}
|
||||
paths:
|
||||
- ~/docker/nucypher-porter.tar
|
||||
- run:
|
||||
name: Load Porter Docker Image Layer Cache
|
||||
command: |
|
||||
set +o pipefail
|
||||
docker load -i ~/docker/nucypher-porter.tar | true
|
||||
- run:
|
||||
name: Build Porter Docker Image
|
||||
command: |
|
||||
docker build -f deploy/docker/porter/Dockerfile --cache-from=nucypher-porter -t nucypher/porter:circle .
|
||||
- run:
|
||||
name: Save Porter Docker Image Layer Cache
|
||||
command: |
|
||||
mkdir -p ~/docker
|
||||
docker save -o ~/docker/nucypher-porter.tar nucypher/porter:circle
|
||||
- save_cache:
|
||||
key: v2-{{ .Branch }}-{{ arch }}
|
||||
paths:
|
||||
- ~/docker/nucypher-porter.tar
|
||||
|
||||
publish_pypi:
|
||||
<<: *python_37_base
|
||||
steps:
|
||||
|
@ -882,6 +968,32 @@ jobs:
|
|||
docker push nucypher/nucypher:$CIRCLE_TAG
|
||||
docker push nucypher/nucypher:latest
|
||||
|
||||
publish_porter_docker:
|
||||
working_directory: ~/nucypher
|
||||
docker:
|
||||
- image: cimg/python:3.8.6
|
||||
steps:
|
||||
- checkout
|
||||
- setup_remote_docker
|
||||
- restore_cache:
|
||||
keys:
|
||||
- v2-{{ .Branch }}-{{ arch }}
|
||||
paths:
|
||||
- ~/docker/nucypher-porter.tar
|
||||
- run:
|
||||
name: Load Porter Docker Image Layer Cache
|
||||
command: |
|
||||
set +o pipefail
|
||||
docker load -i ~/docker/nucypher-porter.tar | true
|
||||
- deploy:
|
||||
name: Push Tagged NuCypher Porter Docker Images
|
||||
command: |
|
||||
echo $DOCKER_PASSWORD | docker login -u $DOCKER_USERNAME --password-stdin
|
||||
docker tag nucypher/porter:circle nucypher/porter:$CIRCLE_TAG
|
||||
docker tag nucypher/porter:circle nucypher/porter:latest
|
||||
docker push nucypher/porter:$CIRCLE_TAG
|
||||
docker push nucypher/porter:latest
|
||||
|
||||
statistical_tests:
|
||||
<<: *python_37_base
|
||||
parallelism: 1
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
FROM python:3.8.7-slim
|
||||
|
||||
# Update
|
||||
RUN apt update -y && apt upgrade -y
|
||||
RUN apt install patch gcc libffi-dev wget -y
|
||||
|
||||
WORKDIR /code
|
||||
COPY . /code
|
||||
|
||||
# Porter requirements
|
||||
RUN pip3 install .[porter]
|
||||
|
||||
CMD ["/bin/bash"]
|
|
@ -0,0 +1,55 @@
|
|||
version: '3'
|
||||
|
||||
services:
|
||||
porter-http:
|
||||
restart: on-failure
|
||||
image: porter:latest
|
||||
container_name: porter-http
|
||||
build:
|
||||
context: ../../..
|
||||
dockerfile: deploy/docker/porter/Dockerfile
|
||||
ports:
|
||||
# Default Porter port
|
||||
- "80:9155"
|
||||
volumes:
|
||||
- .:/code
|
||||
- ~/.local/share/nucypher:/nucypher
|
||||
command: ["nucypher", "porter", "run",
|
||||
"--provider", "${WEB3_PROVIDER_URI}",
|
||||
"--network", "${NUCYPHER_NETWORK}"]
|
||||
|
||||
porter-https:
|
||||
restart: on-failure
|
||||
image: porter:latest
|
||||
container_name: porter-https
|
||||
ports:
|
||||
# Default Porter port
|
||||
- "443:9155"
|
||||
volumes:
|
||||
- .:/code
|
||||
- ~/.local/share/nucypher:/nucypher
|
||||
- "${TLS_DIR}:/etc/porter/tls/"
|
||||
command: [ "nucypher", "porter", "run",
|
||||
"--provider", "${WEB3_PROVIDER_URI}",
|
||||
"--network", "${NUCYPHER_NETWORK}",
|
||||
"--tls-key-filepath", "/etc/porter/tls/key.pem",
|
||||
"--tls-certificate-filepath", "/etc/porter/tls/cert.pem"]
|
||||
|
||||
porter-https-auth:
|
||||
restart: on-failure
|
||||
image: porter:latest
|
||||
container_name: porter-https-auth
|
||||
ports:
|
||||
# Default Porter port
|
||||
- "443:9155"
|
||||
volumes:
|
||||
- .:/code
|
||||
- ~/.local/share/nucypher:/nucypher
|
||||
- "${TLS_DIR}:/etc/porter/tls/"
|
||||
- "${HTPASSWD_FILE}:/etc/porter/auth/htpasswd"
|
||||
command: [ "nucypher", "porter", "run",
|
||||
"--provider", "${WEB3_PROVIDER_URI}",
|
||||
"--network", "${NUCYPHER_NETWORK}",
|
||||
"--tls-key-filepath", "/etc/porter/tls/key.pem",
|
||||
"--tls-certificate-filepath", "/etc/porter/tls/cert.pem",
|
||||
"--basic-auth-filepath", "/etc/porter/auth/htpasswd"]
|
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 153 KiB |
|
@ -0,0 +1,633 @@
|
|||
.. _porter:
|
||||
|
||||
Porter Service
|
||||
==============
|
||||
|
||||
Overview
|
||||
--------
|
||||
NuCypher Porter can be described as the *“Infura for NuCypher”*. Porter is a web-based service that performs
|
||||
nucypher-based protocol operations on behalf of applications.
|
||||
|
||||
Its goal is to simplify and abstract the complexities surrounding the nucypher protocol to negate the need for
|
||||
applications to interact with it via a python client. Porter introduces the nucypher protocol to cross-platform
|
||||
functionality including web and mobile applications. By leveraging ``rust-umbral`` and its associated javascript
|
||||
bindings for cryptography, and Porter for communication with the network, a lightweight, richer and full-featured
|
||||
web and mobile experience is accessible to application developers.
|
||||
|
||||
.. image:: ../.static/img/porter_diagram.svg
|
||||
:target: ../.static/img/porter_diagram.svg
|
||||
|
||||
|
||||
Running Porter
|
||||
--------------
|
||||
|
||||
.. note::
|
||||
|
||||
If running the Porter service using Docker or Docker Compose, it will run on port 80 (HTTP) or 443 (HTTPS). If
|
||||
running via the CLI the default port is 9155, unless specified otherwise via the ``--http-port`` option.
|
||||
|
||||
Security
|
||||
^^^^^^^^
|
||||
|
||||
HTTPS
|
||||
+++++
|
||||
To run the Porter service over HTTPS, it will require a TLS key (``--tls-key-filepath`` option) and a TLS certificate.
|
||||
|
||||
If desired, keys and self-signed certificates can be created for the localhost using the ``openssl`` command:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ openssl req -x509 -out cert.pem -keyout key.pem \
|
||||
-newkey rsa:2048 -nodes -sha256 \
|
||||
-subj '/CN=localhost' -extensions EXT -config <( \
|
||||
printf "[dn]\nCN=localhost\n[req]\ndistinguished_name = dn\n[EXT]\nsubjectAltName=DNS:localhost\nkeyUsage=digitalSignature\nextendedKeyUsage=serverAuth")
|
||||
|
||||
.. important::
|
||||
|
||||
Self-signed certificates are not recommended, other than for testing.
|
||||
|
||||
|
||||
Authentication
|
||||
++++++++++++++
|
||||
Porter will allow the configuration of Basic Authentication out of the box via
|
||||
an `htpasswd <https://httpd.apache.org/docs/2.4/programs/htpasswd.html>`_ file. The use of Basic Authentication
|
||||
necessitates HTTPS since user credentials will be passed over the network as cleartext.
|
||||
|
||||
Alternative authentication mechanisms can be implemented outside of Porter via an intermediary proxy service, for
|
||||
example an Nginx HTTPS reverse proxy.
|
||||
|
||||
|
||||
via Docker
|
||||
^^^^^^^^^^
|
||||
|
||||
Run Porter within Docker without acquiring or installing the ``nucypher`` codebase.
|
||||
|
||||
#. Get the latest ``nucypher`` image:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ docker pull nucypher/porter:latest
|
||||
|
||||
#. Run Porter service
|
||||
|
||||
For HTTP service (on default port 80):
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ docker run -d --rm \
|
||||
--name porter-http \
|
||||
-v ~/.local/share/nucypher/:/root/.local/share/nucypher \
|
||||
-p 80:9155 \
|
||||
nucypher/porter:latest \
|
||||
nucypher porter run \
|
||||
--provider <YOUR WEB3 PROVIDER URI> \
|
||||
--network <NETWORK NAME>
|
||||
|
||||
For HTTPS service (on default port 443):
|
||||
|
||||
* Without Basic Authentication:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ docker run -d --rm \
|
||||
--name porter-https \
|
||||
-v ~/.local/share/nucypher/:/root/.local/share/nucypher \
|
||||
-v <TLS DIRECTORY>:/etc/porter/tls \
|
||||
-p 443:9155 \
|
||||
nucypher/porter:latest \
|
||||
nucypher porter run \
|
||||
--provider <YOUR WEB3 PROVIDER URI> \
|
||||
--network <NETWORK NAME> \
|
||||
--tls-key-filepath /etc/porter/tls/<KEY FILENAME> \
|
||||
--tls-certificate-filepath /etc/porter/tls/<CERT FILENAME>
|
||||
|
||||
* With Basic Authentication:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ docker run -d --rm \
|
||||
--name porter-https-auth \
|
||||
-v ~/.local/share/nucypher/:/root/.local/share/nucypher \
|
||||
-v <TLS DIRECTORY>:/etc/porter/tls \
|
||||
-v <HTPASSWD FILE>:/etc/porter/auth/htpasswd \
|
||||
-p 443:9155 \
|
||||
nucypher/porter:latest \
|
||||
nucypher porter run \
|
||||
--provider <YOUR WEB3 PROVIDER URI> \
|
||||
--network <NETWORK NAME> \
|
||||
--tls-key-filepath /etc/porter/tls/<KEY FILENAME> \
|
||||
--tls-certificate-filepath /etc/porter/tls/<CERT FILENAME> \
|
||||
--basic-auth-filepath /etc/porter/auth/htpasswd
|
||||
|
||||
The ``<TLS DIRECTORY>`` is expected to contain the TLS key file (``<KEY FILENAME>``) and the
|
||||
certificate (``<CERT FILENAME>``) to run Porter over HTTPS.
|
||||
|
||||
|
||||
#. Porter will be available on default ports 80 (HTTP) or 443 (HTTPS). The porter service running will be one of
|
||||
the following depending on the mode chosen:
|
||||
|
||||
* ``porter-http``
|
||||
* ``porter-https``
|
||||
* ``porter-https-auth``
|
||||
|
||||
#. View Porter logs
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ docker logs -f <PORTER SERVICE>
|
||||
|
||||
#. Stop Porter service
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ docker stop <PORTER SERVICE>
|
||||
|
||||
|
||||
via Docker Compose
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Docker Compose will start the Porter service within a Docker container.
|
||||
|
||||
#. Acquire the ``nucypher`` codebase - see :ref:`acquire_codebase`. There is no need
|
||||
to install ``nucypher`` after acquiring the codebase since Docker will be used.
|
||||
|
||||
#. Set the required environment variables:
|
||||
|
||||
* Web3 Provider URI environment variable
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ export WEB3_PROVIDER_URI=<YOUR WEB3 PROVIDER URI>
|
||||
|
||||
.. note::
|
||||
|
||||
Local ipc is not supported when running via Docker.
|
||||
|
||||
|
||||
* Network Name environment variable
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ export NUCYPHER_NETWORK=<NETWORK NAME>
|
||||
|
||||
* *(Optional)* TLS directory variable containing the TLS key and the certificate to run Porter over HTTPS. The directory is expected to contain two files:
|
||||
|
||||
* ``key.pem`` - the TLS key
|
||||
* ``cert.pem`` - the TLS certificate
|
||||
|
||||
Set the TLS directory environment variable
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ export TLS_DIR=<ABSOLUTE PATH TO TLS DIRECTORY>
|
||||
|
||||
* *(Optional)* Filepath to the htpasswd file for Basic Authentication
|
||||
|
||||
Set the htpasswd filepath environment variable
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ export HTPASSWD_FILE=<ABSOLUTE PATH TO HTPASSWD FILE>
|
||||
|
||||
#. Run Porter service
|
||||
|
||||
For HTTP service (on default port 80):
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ docker-compose -f deploy/docker/porter/docker-compose.yml up -d porter-http
|
||||
|
||||
For HTTPS service (on default port 443):
|
||||
|
||||
* Without Basic Authentication
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ docker-compose -f deploy/docker/porter/docker-compose.yml up -d porter-https
|
||||
|
||||
* With Basic Authentication
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ docker-compose -f deploy/docker/porter/docker-compose.yml up -d porter-https-auth
|
||||
|
||||
#. Porter will be available on default ports 80 (HTTP) or 443 (HTTPS). The porter service running will be one of
|
||||
the following depending on the mode chosen:
|
||||
|
||||
* ``porter-http``
|
||||
* ``porter-https``
|
||||
* ``porter-https-auth``
|
||||
|
||||
#. View Porter logs
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ docker-compose -f deploy/docker/porter/docker-compose.yml logs -f <PORTER SERVICE>
|
||||
|
||||
#. Stop Porter service
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ docker-compose -f deploy/docker/porter/docker-compose.yml down
|
||||
|
||||
|
||||
via CLI
|
||||
^^^^^^^
|
||||
|
||||
Install ``nucypher`` - see :doc:`/references/pip-installation`.
|
||||
|
||||
For a full list of CLI options, run:
|
||||
|
||||
.. code:: console
|
||||
|
||||
$ nucypher porter run --help
|
||||
|
||||
|
||||
* Run Porter service
|
||||
|
||||
* Run via HTTP
|
||||
|
||||
.. code:: console
|
||||
|
||||
$ nucypher porter run --provider <YOUR WEB3 PROVIDER URI> --network <NETWORK NAME>
|
||||
|
||||
|
||||
______
|
||||
(_____ \ _
|
||||
_____) )__ ____| |_ ____ ____
|
||||
| ____/ _ \ / ___) _)/ _ )/ ___)
|
||||
| | | |_| | | | |_( (/ /| |
|
||||
|_| \___/|_| \___)____)_|
|
||||
|
||||
the Pipe for nucypher network operations
|
||||
|
||||
Reading Latest Chaindata...
|
||||
Network: <NETWORK NAME>
|
||||
Provider: ...
|
||||
Running Porter Web Controller at http://127.0.0.1:9155
|
||||
|
||||
* Run via HTTPS
|
||||
|
||||
To run via HTTPS use the ``--tls-key-filepath`` and ``--tls-certificate-filepath`` options:
|
||||
|
||||
.. code:: console
|
||||
|
||||
$ nucypher porter run --provider <YOUR WEB3 PROVIDER URI> --network <NETWORK NAME> --tls-key-filepath <TLS KEY FILEPATH> --tls-certificate-filepath <CERT FILEPATH>
|
||||
|
||||
|
||||
______
|
||||
(_____ \ _
|
||||
_____) )__ ____| |_ ____ ____
|
||||
| ____/ _ \ / ___) _)/ _ )/ ___)
|
||||
| | | |_| | | | |_( (/ /| |
|
||||
|_| \___/|_| \___)____)_|
|
||||
|
||||
the Pipe for nucypher network operations
|
||||
|
||||
Reading Latest Chaindata...
|
||||
Network: <NETWORK NAME>
|
||||
Provider: ...
|
||||
Running Porter Web Controller at https://127.0.0.1:9155
|
||||
|
||||
For HTTPS with Basic Authentication, add the ``--basic-auth-filepath`` option:
|
||||
|
||||
.. code:: console
|
||||
|
||||
$ nucypher porter run --provider <YOUR WEB3 PROVIDER URI> --network <NETWORK NAME> --tls-key-filepath <TLS KEY FILEPATH> --tls-certificate-filepath <CERT FILEPATH> --basic-auth-filepath <HTPASSWD FILE>
|
||||
|
||||
|
||||
______
|
||||
(_____ \ _
|
||||
_____) )__ ____| |_ ____ ____
|
||||
| ____/ _ \ / ___) _)/ _ )/ ___)
|
||||
| | | |_| | | | |_( (/ /| |
|
||||
|_| \___/|_| \___)____)_|
|
||||
|
||||
the Pipe for nucypher network operations
|
||||
|
||||
Reading Latest Chaindata...
|
||||
Network: <NETWORK NAME>
|
||||
Provider: ...
|
||||
Basic Authentication enabled
|
||||
Running Porter Web Controller at https://127.0.0.1:9155
|
||||
|
||||
|
||||
API
|
||||
---
|
||||
|
||||
Status Codes
|
||||
^^^^^^^^^^^^
|
||||
All documented API endpoints use JSON and are REST-like.
|
||||
|
||||
Some common returned status codes you may encounter are:
|
||||
|
||||
- ``200 OK`` -- The request has succeeded.
|
||||
- ``400 BAD REQUEST`` -- The server cannot or will not process the request due to something that is perceived to
|
||||
be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).
|
||||
- ``401 UNAUTHORIZED`` -- Authentication is required and the request has failed to provide valid authentication credentials.
|
||||
- ``500 INTERNAL SERVER ERROR`` -- The server encountered an unexpected condition that prevented it from
|
||||
fulfilling the request.
|
||||
|
||||
Typically, you will want to ensure that any given response results in a 200 status code.
|
||||
This indicates that the server successfully completed the call.
|
||||
|
||||
If a 400 status code is returned, double-check the request data being sent to the server. The text provided in the
|
||||
error response should describe the nature of the problem.
|
||||
|
||||
If a 401 status code is returned, ensure that valid authentication credentials are being used in the request e.g. if
|
||||
Basic authentication is enabled.
|
||||
|
||||
If a 500 status code, note the reason provided. If the error is ambiguous or unexpected, we'd like to
|
||||
know about it! The text provided in the error response should describe the nature of the problem.
|
||||
|
||||
For any bugs/un expected errors, see our :ref:`Contribution Guide <contribution-guide>` for issue reporting and
|
||||
getting involved. Please include contextual information about the sequence of steps that caused the 500 error in the
|
||||
GitHub issue. For any questions, message us in our `Discord <https://discord.gg/7rmXa3S>`_.
|
||||
|
||||
|
||||
URL Query Parameters
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
All parameters can be passed as either JSON data within the request or as query parameter strings in the URL.
|
||||
Query parameters used within the URL will need to be URL encoded e.g. ``/`` in a base64 string becomes ``%2F`` etc.
|
||||
|
||||
For ``List`` data types to be passed via a URL query parameter, the value should be provided as a comma-delimited
|
||||
String. For example, if a parameter is of type ``List[String]`` either a JSON list of Strings can be provided e.g.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
curl -X GET <PORTER URI>/<ENDPOINT> \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"parameter_with_list_of_values": ["value1", "value2", "value3"]}'
|
||||
|
||||
OR it can be provided via a URL query parameter
|
||||
|
||||
.. code:: bash
|
||||
|
||||
curl -X GET <PORTER URI>/<ENDPOINT>?parameter_with_list_of_values=value1,value2,value3
|
||||
|
||||
More examples shown below.
|
||||
|
||||
.. important::
|
||||
|
||||
If URL query parameters are used and the URL becomes too long, the request will fail. There is no official limit
|
||||
and it is dependent on the tool being used.
|
||||
|
||||
|
||||
GET /get_ursulas
|
||||
^^^^^^^^^^^^^^^^
|
||||
Sample available Ursulas for a policy as part of Alice's ``grant`` workflow. Returns a list of Ursulas
|
||||
and their associated information that is used for the policy.
|
||||
|
||||
Parameters
|
||||
++++++++++
|
||||
+----------------------------------+---------------+-----------------------------------------------+
|
||||
| **Parameter** | **Type** | **Description** |
|
||||
+==================================+===============+===============================================+
|
||||
| ``quantity`` | Integer | Number of total Ursulas to return. |
|
||||
+----------------------------------+---------------+-----------------------------------------------+
|
||||
| ``duration_periods`` | Integer | Number of periods required for the policy. |
|
||||
+----------------------------------+---------------+-----------------------------------------------+
|
||||
| ``include_ursulas`` *(Optional)* | List[Strings] | | List of Ursula checksum addresses to |
|
||||
| | | | give preference to. If any of these Ursulas |
|
||||
| | | | are unavailable, they will not be included |
|
||||
| | | | in result. |
|
||||
+----------------------------------+---------------+-----------------------------------------------+
|
||||
| ``exclude_ursulas`` *(Optional)* | List[Strings] | | List of Ursula checksum addresses to not |
|
||||
| | | | include in the result. |
|
||||
+----------------------------------+---------------+-----------------------------------------------+
|
||||
|
||||
|
||||
Returns
|
||||
+++++++
|
||||
List of Ursulas with associated information:
|
||||
|
||||
* ``encrypting_key`` - Ursula's encrypting key encoded as hex
|
||||
* ``checksum_address`` - Ursula's checksum address
|
||||
* ``uri`` - Ursula's URI
|
||||
|
||||
Example Request
|
||||
+++++++++++++++
|
||||
.. code:: bash
|
||||
|
||||
curl -X GET <PORTER URI>/get_ursulas \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"quantity": 5,
|
||||
"duration_periods": 4,
|
||||
"include_ursulas": ["0xB04FcDF9327f65AB0107Ea95b78BB200C07FA752"],
|
||||
"exclude_ursulas": ["0x5cF1703A1c99A4b42Eb056535840e93118177232", "0x9919C9f5CbBAA42CB3bEA153E14E16F85fEA5b5D"]}'
|
||||
|
||||
OR
|
||||
|
||||
.. code:: bash
|
||||
|
||||
curl -X GET "<PORTER URI>/get_ursulas?quantity=5&duration_periods=4&include_ursulas=0xB04FcDF9327f65AB0107Ea95b78BB200C07FA752&exclude_ursulas=0x5cF1703A1c99A4b42Eb056535840e93118177232,0x9919C9f5CbBAA42CB3bEA153E14E16F85fEA5b5D"
|
||||
|
||||
|
||||
Example Response
|
||||
++++++++++++++++
|
||||
.. code::
|
||||
|
||||
Status: 200 OK
|
||||
|
||||
|
||||
.. code:: json
|
||||
|
||||
{
|
||||
"result": {
|
||||
"ursulas": [
|
||||
{
|
||||
"encrypting_key": "025a335eca37edce8191d43c156e7bc6b451b21e5258759966bbfe0e6ce44543cb",
|
||||
"checksum_address": "0x5cF1703A1c99A4b42Eb056535840e93118177232",
|
||||
"uri": "https://3.236.144.36:9151"
|
||||
},
|
||||
{
|
||||
"encrypting_key": "02b0a0099ee180b531b4937bd7446972296447b2479ca6259cb6357ed98b90da3a",
|
||||
"checksum_address": "0x7fff551249D223f723557a96a0e1a469C79cC934",
|
||||
"uri": "https://54.218.83.166:9151"
|
||||
},
|
||||
{
|
||||
"encrypting_key": "02761c765e2f101df39a5f680f3943d0d993ef9576de8a3e0e5fbc040d6f8c15a5",
|
||||
"checksum_address": "0x9C7C824239D3159327024459Ad69bB215859Bd25",
|
||||
"uri": "https://92.53.84.156:9151"
|
||||
},
|
||||
{
|
||||
"encrypting_key": "0258b7c79fe73f3499de91dd5a5341387184035d0555b10e6ac762d211a39684c0",
|
||||
"checksum_address": "0x9919C9f5CbBAA42CB3bEA153E14E16F85fEA5b5D",
|
||||
"uri": "https://3.36.66.164:9151"
|
||||
},
|
||||
{
|
||||
"encrypting_key": "02e43a623c24db4f62565f82b6081044c1968277edfdca494a81c8fd0826e0adf6",
|
||||
"checksum_address": "0xfBeb3368735B3F0A65d1F1E02bf1d188bb5F5BE6",
|
||||
"uri": "https://128.199.124.254:9151"
|
||||
}
|
||||
]
|
||||
},
|
||||
"version": "6.0.0"
|
||||
}
|
||||
|
||||
|
||||
POST /publish_treasure_map
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
Publish a treasure map to the network as part of Alice's ``grant`` workflow. The treasure map associated
|
||||
with the policy is stored by the network.
|
||||
|
||||
Parameters
|
||||
++++++++++
|
||||
+----------------------------------+---------------+----------------------------------------+
|
||||
| **Parameter** | **Type** | **Description** |
|
||||
+==================================+===============+========================================+
|
||||
| ``treasure_map`` | String | Treasure map bytes encoded as base64. |
|
||||
+----------------------------------+---------------+----------------------------------------+
|
||||
| ``bob_encrypting_key`` | String | Bob's encrypting key encoded as hex. |
|
||||
+----------------------------------+---------------+----------------------------------------+
|
||||
|
||||
Returns
|
||||
+++++++
|
||||
Confirmation that the treasure map was published:
|
||||
|
||||
* ``published`` - Value of ``true``.
|
||||
|
||||
If publishing the treasure map fails, an error status code is returned.
|
||||
|
||||
Example Request
|
||||
+++++++++++++++
|
||||
.. code:: bash
|
||||
|
||||
curl -X POST <PORTER URI>/publish_treasure_map \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"treasure_map": "Qld7S8sbKFCv2B8KxfJo4oxiTOjZ4VPyqTK5K1xK6DND6TbLg2hvlGaMV69aiiC5QfadB82w/5q1Sw+SNFHN2e ...",
|
||||
"bob_encrypting_key": "026d1f4ce5b2474e0dae499d6737a8d987ed3c9ab1a55e00f57ad2d8e81fe9e9ac"}'
|
||||
|
||||
OR
|
||||
|
||||
.. code:: bash
|
||||
|
||||
curl -X POST "<PORTER URI>/publish_treasure_map?treasure_map=Qld7S8sbKFCv2B8KxfJo4oxiTOjZ4VPyqTK5K1xK6DND6TbLg2hvlGaMV69aiiC5QfadB82w%2F5q1Sw%2BSNFHN2e ...&bob_encrypting_key=026d1f4ce5b2474e0dae499d6737a8d987ed3c9ab1a55e00f57ad2d8e81fe9e9ac"
|
||||
|
||||
|
||||
Example Response
|
||||
++++++++++++++++
|
||||
.. code::
|
||||
|
||||
Status: 200 OK
|
||||
|
||||
|
||||
.. code:: json
|
||||
|
||||
{
|
||||
"result": {
|
||||
"published": true
|
||||
},
|
||||
"version": "6.0.0"
|
||||
}
|
||||
|
||||
|
||||
GET /get_treasure_map
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
Retrieve a treasure map from the network as part of Bob's ``retrieve`` workflow. Bob needs to obtain the treasure map
|
||||
associated with a policy, to learn which Ursulas were assigned to service the policy.
|
||||
|
||||
Parameters
|
||||
++++++++++
|
||||
+----------------------------------+---------------+----------------------------------------+
|
||||
| **Parameter** | **Type** | **Description** |
|
||||
+==================================+===============+========================================+
|
||||
| ``treasure_map_id`` | String | Treasure map identifier. |
|
||||
+----------------------------------+---------------+----------------------------------------+
|
||||
| ``bob_encrypting_key`` | String | Bob's encrypting key encoded as hex. |
|
||||
+----------------------------------+---------------+----------------------------------------+
|
||||
|
||||
|
||||
Returns
|
||||
+++++++
|
||||
The requested treasure map:
|
||||
|
||||
* ``treasure_map`` - Treasure map bytes encoded as base64
|
||||
|
||||
Example Request
|
||||
+++++++++++++++
|
||||
.. code:: bash
|
||||
|
||||
curl -X GET <PORTER URI>/get_treasure_map \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"treasure_map_id": "f6ec73c93084ce91d5542a4ba6070071f5565112fe19b26ae9c960f9d658903a",
|
||||
"bob_encrypting_key": "026d1f4ce5b2474e0dae499d6737a8d987ed3c9ab1a55e00f57ad2d8e81fe9e9ac"}'
|
||||
|
||||
OR
|
||||
|
||||
.. code:: bash
|
||||
|
||||
curl -X GET "<PORTER URI>/get_treasure_map?treasure_map_id=f6ec73c93084ce91d5542a4ba6070071f5565112fe19b26ae9c960f9d658903a&bob_encrypting_key=026d1f4ce5b2474e0dae499d6737a8d987ed3c9ab1a55e00f57ad2d8e81fe9e9ac"
|
||||
|
||||
Example Response
|
||||
++++++++++++++++
|
||||
.. code::
|
||||
|
||||
Status: 200 OK
|
||||
|
||||
|
||||
.. code:: json
|
||||
|
||||
{
|
||||
"result": {
|
||||
"treasure_map": "Qld7S8sbKFCv2B8KxfJo4oxiTOjZ4VPyqTK5K1xK6DND6TbLg2hvlGaMV69aiiC5QfadB82w/5q1Sw+SNFHN2esWgAbs38QuUVUGCzDoWzQAAAGIAuhw12ZiPMNV8LaeWV8uUN+au2HGOjWilqtKsaP9fmnLAzFiTUAu9/VCxOLOQE88BPoWk1H7OxRLDEhnBVYyflpifKbOYItwLLTtWYVFRY90LtNSAzS8d3vNH4c3SHSZwYsCKY+5LvJ68GD0CqhydSxCcGckh0unttHrYGSOQsURUI4AAAEBsSMlukjA1WyYA+FouqkuRtk8bVHcYLqRUkK2n6dShEUGMuY1SzcAbBINvJYmQp+hhzK5m47AzCl463emXepYZQC/evytktG7yXxd3k8Ak+Qr7T4+G2VgJl4YrafTpIT6wowd+8u/SMSrrf/M41OhtLeBC4uDKjO3rYBQfVLTpEAgiX/9jxB80RtNMeCwgcieviAR5tlw2IlxVTEhxXbFeopcOZmfEuhVWqgBUfIakqsNCXkkubV0XS2l5G1vtTM8oNML0rP8PyKd4+0M5N6P/EQqFkHH93LCDD0IQBq9usm3MoJp0eT8N3m5gprI05drDh2xe/W6qnQfw3YXnjdvf2A="
|
||||
},
|
||||
"version": "6.0.0"
|
||||
}
|
||||
|
||||
|
||||
POST /exec_work_order
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
Use a work order to execute a re-encrypt operation on the network network as part of Bob's ``retrieve`` workflow.
|
||||
|
||||
Parameters
|
||||
++++++++++
|
||||
+----------------------------------+---------------+----------------------------------------+
|
||||
| **Parameter** | **Type** | **Description** |
|
||||
+==================================+===============+========================================+
|
||||
| ``ursula`` | String | | Checksum address corresponding to |
|
||||
| | | | the Ursula to execute the work order.|
|
||||
+----------------------------------+---------------+----------------------------------------+
|
||||
| ``work_order_payload`` | String | Work order payload encoded as base64. |
|
||||
+----------------------------------+---------------+----------------------------------------+
|
||||
|
||||
|
||||
Returns
|
||||
+++++++
|
||||
The result of the re-encryption operation performed on the work order payload:
|
||||
|
||||
* ``work_order_result`` - The result of the re-encryption operation returning combined cFrag and
|
||||
associated signature bytes encoded as base64, i.e. it is of the format
|
||||
``base64(<cfrag_1> || <cfrag_1_signature> || <cfrag_2> || <cfrag_2_signature> ...)``.
|
||||
|
||||
Example Request
|
||||
+++++++++++++++
|
||||
.. code:: bash
|
||||
|
||||
curl -X POST <PORTER URI>/exec_work_order \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"ursula": "0xE57bFE9F44b819898F47BF37E5AF72a0783e1141",
|
||||
"work_order_payload": "QoQgOCRvtT4qG0nb5eDbfJ3vO6jMeoy9yp7lvezWKyNF0I6f/uQBPJed9FM7oc7jDAzqyDYD1C/1Cnab+kdobAJT7a3Z/KcOot4SwhgZ0eLGYVuhiAnXP9F7lBDosmvd2gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1I8O2JB/65y0K6m0dxmCpYJfsbpV63dMqcmTTPZAuWuA6LDyDa9JOqPF1OYQWHYi93wNLVLyHCLH6UEf5JOSgmgZOCPIzOaUpQMlr1rIDGExrF6zrLGpAwpPMMOzqa8tjiWHFaHWyLsUMyVKT8v1Psa/iIQ4NZDG+gKDSjgp33MbLml/ti+1p75M2ewuUbCjCWq5Mkf5ycqyEQUMt0IcTgNA6vxewmaBt7UTsYaUzkeTkNz/hLO9+ZFJ1NzJLxeweSqAiNQtfBvG7Fih6oaQT9uslu4QAwOTgolqkinEsoNx9XRL8Ocb8pO/5POBfPxiH8c2v5lrr6HhkAKAC5QODfegIToy29k0KIf3bCoqaVYncvjLJcum0AatnyOkYoV9Zf5wojvyFJE+MZ/homke4Yd8irUdoLSgxDuEDtyRNMuTpcHA37Z+npgp/zi0DQUvK35xZE+DmGYhaHTOPQesiTqyJc/Az22wtTfA3n9JwjSl6CjADGjHWgUPMQWzW8fqICo1iek2z7oFHM24yCtyvsEbC2Mm25LEZi/k2mfbgpNRg5PqW9qj/hTK19Cm4s0rlK7e2odCD5T3Iy0s6eg0KgR0RhT/ayH42be1FHgXFBFeABhm0fM8ZxorhMF1ce/yDPOaRZ8"}'
|
||||
|
||||
OR
|
||||
|
||||
.. code:: bash
|
||||
|
||||
curl -X POST "<PORTER URI>/exec_work_order?ursula=0xE57bFE9F44b819898F47BF37E5AF72a0783e1141&work_order_payload=QoQgOCRvtT4qG0nb5eDbfJ3vO6jMeoy9yp7lvezWKyNF0I6f%2FuQBPJed9FM7oc7jDAzqyDYD1C%2F1Cnab%2BkdobAJT7a3Z%2FKcOot4SwhgZ0eLGYVuhiAnXP9F7lBDosmvd2gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1I8O2JB%2F65y0K6m0dxmCpYJfsbpV63dMqcmTTPZAuWuA6LDyDa9JOqPF1OYQWHYi93wNLVLyHCLH6UEf5JOSgmgZOCPIzOaUpQMlr1rIDGExrF6zrLGpAwpPMMOzqa8tjiWHFaHWyLsUMyVKT8v1Psa%2FiIQ4NZDG%2BgKDSjgp33MbLml%2Fti%2B1p75M2ewuUbCjCWq5Mkf5ycqyEQUMt0IcTgNA6vxewmaBt7UTsYaUzkeTkNz%2FhLO9%2BZFJ1NzJLxeweSqAiNQtfBvG7Fih6oaQT9uslu4QAwOTgolqkinEsoNx9XRL8Ocb8pO%2F5POBfPxiH8c2v5lrr6HhkAKAC5QODfegIToy29k0KIf3bCoqaVYncvjLJcum0AatnyOkYoV9Zf5wojvyFJE%2BMZ%2Fhomke4Yd8irUdoLSgxDuEDtyRNMuTpcHA37Z%2Bnpgp%2Fzi0DQUvK35xZE%2BDmGYhaHTOPQesiTqyJc%2FAz22wtTfA3n9JwjSl6CjADGjHWgUPMQWzW8fqICo1iek2z7oFHM24yCtyvsEbC2Mm25LEZi%2Fk2mfbgpNRg5PqW9qj%2FhTK19Cm4s0rlK7e2odCD5T3Iy0s6eg0KgR0RhT%2FayH42be1FHgXFBFeABhm0fM8ZxorhMF1ce%2FyDPOaRZ8"
|
||||
|
||||
|
||||
Example Response
|
||||
++++++++++++++++
|
||||
.. code::
|
||||
|
||||
Status: 200 OK
|
||||
|
||||
|
||||
.. code:: json
|
||||
|
||||
{
|
||||
"result": {
|
||||
"work_order_result": "AAABpwIE30NxwNdRKKYbQ8g0/smtFuDoTPy8wrkJykX80A4LKAMQ4B9nUoyq9JHyDvnLXf314LrLA4roe/HuUXXNsF+6muWUZPwe8IA/SwkPJnpggGu0xQdVl3eMGpgHYL9pW3jWA0ztwFmQ6qpgJkXxdkK7j62kBSjzWTziWRaWzgd0bXRqA71fJSvQp/q2V5do3/g2BvqN8R22ZBxzn0s77p0s7LyIA+K1a1aMbR22OtpGdmUTbl3SK7gSYAVsHtpBbvok/FstA78AbixycMh5OgOXze62RMFXFvNeK5aw8vld0YefHkoWA+4YKNw8zddlhhH4jv8gXKxZQcdxA4tpxYiigTFuJFb4B/WzU9MEvZQUDfVVxtAgtpyTQaw+EFVc313bFrPjfZIhbodl2FBSa8HHbyU+zyuQbx3xIUcTXrkWCLV7+J6mrvjrJkGWH+AJAceN8P7uPvK101P5OKs6oiO1/voDPIOr0boQB6pE+gHGH56Eb8Q3W5uGJ9p2e2Ul90vMFRmMRLVvHToNrMgrMOLNa/TenaiAxK+xfiOnNNE0Mi3LQTAj0c+mTLM1fZ81zIgT0yQXJzfKfiiJuAN35e1JlgYISnxchLqv6gYldeRxx1Xz2v5TZtTvRjlP9PCEEaW4sQaqW+0AAAGnAghcC66v3rlE4nN8utPifyLlG6dwwg25KVfTwfjiqOagAsrrQEi1CHnTyFl50DSruaygVnPj+sCv/G9onIExiRv6dcjhZhASDM/2P67XlNiwn074GI5f8e1orNVcVXpvS+0DzdQxL+pbkmNDH6XkZjbAyYcJ+B8zPtcMeIlEhPEJz7ICxJ0agnJr6DgGNvsNXeGSDQWVHRAwPlTOvvh0lZnOvnICfMParvaZkDVft9z4x7HUTuvkJmEnp1qAGefyDx57MzACBbvqNTHVDhv4qDmfe0ynjIKO+1bYT8G4s7xmgmS9y8ECOO+/cX1z9MgijH4wZLG36kJq50BXXOU7U5yRvzQe4R0W2noI4lfmPg3bTqXQyZrvINVImvwuyvQMoQTDM9HFM4dUyN4vLDpSe3bHZmpTIpI8VtOMNUAo+wGzfBMEsSPAxJ/fOZCL33HTMIe0p+q4F9ksDNxO3XFHwEuMm2FuSRuQbnKUtxqXp4YiDdF2PKPKthfZ7WUZN+Z+wAjMzwE28Bf3m5SGZsXDQxaKTioWJUxDbzdHnpcI28+4srZVr2SU6GKR5yGDrAJxLf2Of4+UHK/UNakH45bWJfYq6Rr+iWe0aFHDtQHqkBtWWmvWJBLhyzsXnqBUhyKQr3DEZ3/R0wAAAacCq+RZNiJQx5WYQLVnrQefNU+A55LL75iRMOJRxukGlEoD5D30UIArtCv4d+iN6XLZhEM6Jsqg5ltPWfFZyRzHbb+StN+TFlQ/LDnFRPQ0MQpGMDjGckCqklZdn0NUGDkTcgOmyY2bhoGcgkob3QvlhSOYS/LPLRCnyn5fn0kABoNKagKuITPL9Tmg87k+yK9PQFu9P3cAOCvL/ZUPJSKI5r47HANuLdqh4zhfyAe5PW3qi7GnzRXRySA+6lImyD/nWqbb2gNv9MYrA2ioheO5jXA2uArwgZObnLApPsIc7Jwl1ko7KgJXN+EsBMdBHwz2Ps6LJt9z73y/HnuivgSNkssxZ8pIXqk9NGRwgBhP2DW0Ctl30bRNX6SIEcD/b0yzsfKIUQk3L7QwPYxE9iKicz608hLzrCUwmHoa8oWA/9dQks8KBD1tTrm0KM5fJcXonZHzeDFIYcHMDMu1PJkwFsLWRVTXJJBgDMUQJix9mpb3px0jMPjAZuDlpherQsLNNRQJq75pVu7hyKmeDAPBGhBNND2NxB3pg3RrFhShdosjpRP/gCJhuI0IdAnajYCG+vmIilyS/8oZomMEg6b8KChUOPVkp/VPFXUKow4jzLXjf7R9DJM/41Yn5ut7IaLi37X/jwwa"
|
||||
},
|
||||
"version": "6.0.0"
|
||||
}
|
|
@ -56,6 +56,10 @@ Glossary
|
|||
PKE
|
||||
Public-key encryption.
|
||||
|
||||
Porter
|
||||
A web service that is the conduit between applications (platform-agnostic) and the nucypher network, that
|
||||
performs nucypher protocol operations on behalf of Alice and Bob.
|
||||
|
||||
PRE
|
||||
Proxy re-encryption.
|
||||
|
||||
|
|
|
@ -98,6 +98,7 @@ Whitepapers
|
|||
:caption: Application Development
|
||||
|
||||
application_development/getting_started
|
||||
application_development/porter
|
||||
application_development/http_character_control
|
||||
application_development/cli_examples
|
||||
application_development/local_fleet_demo
|
||||
|
|
|
@ -19,6 +19,13 @@ General
|
|||
Password for the `nucypher` Keystore.
|
||||
* `NUCYPHER_PROVIDER_URI`
|
||||
Default Web3 node provider URI.
|
||||
* `NUCYPHER_STAKERS_PAGINATION_SIZE`
|
||||
Default pagination size for the maximum number of active stakers to retrieve from StakingEscrow in
|
||||
one contract call.
|
||||
* `NUCYPHER_STAKERS_PAGINATION_SIZE_LIGHT_NODE`
|
||||
Default pagination size for the maximum number of active stakers to retrieve from StakingEscrow in
|
||||
one contract call when a light node provider is being used.
|
||||
|
||||
|
||||
Alice
|
||||
-----
|
||||
|
|
|
@ -15,6 +15,8 @@ the nucypher codebase.
|
|||
|
||||
Before continuing, ensure you have ``git`` installed (\ `Git Documentation <https://git-scm.com/doc>`_\ ).
|
||||
|
||||
.. _acquire_codebase:
|
||||
|
||||
Acquire NuCypher Codebase
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Introduction of NuCypher Porter - a web-based service that performs ``nucypher`` protocol operations on behalf of applications for cross-platform functionality.
|
|
@ -102,7 +102,7 @@ from nucypher.blockchain.eth.utils import (
|
|||
prettify_eth_amount
|
||||
)
|
||||
from nucypher.characters.banners import STAKEHOLDER_BANNER
|
||||
from nucypher.characters.control.emitters import StdoutEmitter
|
||||
from nucypher.control.emitters import StdoutEmitter
|
||||
from nucypher.config.constants import DEFAULT_CONFIG_ROOT
|
||||
from nucypher.crypto.powers import TransactingPower
|
||||
from nucypher.types import NuNits, Period
|
||||
|
@ -1298,12 +1298,6 @@ class BlockchainPolicyAuthor(NucypherTokenActor):
|
|||
payload = {**blockchain_payload, **policy_end_time}
|
||||
return payload
|
||||
|
||||
def get_stakers_reservoir(self, **options) -> StakersReservoir:
|
||||
"""
|
||||
Get a sampler object containing the currently registered stakers.
|
||||
"""
|
||||
return self.staking_agent.get_stakers_reservoir(**options)
|
||||
|
||||
def create_policy(self, *args, **kwargs):
|
||||
"""
|
||||
Hence the name, a BlockchainPolicyAuthor can create
|
||||
|
|
|
@ -14,7 +14,7 @@ GNU Affero General Public License for more details.
|
|||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
import random
|
||||
import sys
|
||||
|
@ -57,6 +57,11 @@ from nucypher.blockchain.eth.interfaces import BlockchainInterfaceFactory
|
|||
from nucypher.blockchain.eth.registry import BaseContractRegistry
|
||||
from nucypher.crypto.powers import TransactingPower
|
||||
from nucypher.crypto.utils import sha256_digest
|
||||
from nucypher.config.constants import (
|
||||
NUCYPHER_ENVVAR_STAKERS_PAGINATION_SIZE_LIGHT_NODE,
|
||||
NUCYPHER_ENVVAR_STAKERS_PAGINATION_SIZE
|
||||
)
|
||||
from nucypher.crypto.powers import TransactingPower
|
||||
from nucypher.types import (
|
||||
Agent,
|
||||
NuNits,
|
||||
|
@ -252,7 +257,10 @@ class StakingEscrowAgent(EthereumContractAgent):
|
|||
'finishUpgrade'
|
||||
)
|
||||
|
||||
DEFAULT_PAGINATION_SIZE: int = 30 # TODO: Use dynamic pagination size (see #1424)
|
||||
DEFAULT_STAKER_PAGINATION_SIZE_LIGHT_NODE: int = int(os.environ.get(
|
||||
NUCYPHER_ENVVAR_STAKERS_PAGINATION_SIZE_LIGHT_NODE, default=30))
|
||||
|
||||
DEFAULT_STAKER_PAGINATION_SIZE: int = int(os.environ.get(NUCYPHER_ENVVAR_STAKERS_PAGINATION_SIZE, default=1000))
|
||||
|
||||
class NotEnoughStakers(Exception):
|
||||
"""Raised when the are not enough stakers available to complete an operation"""
|
||||
|
@ -316,7 +324,8 @@ class StakingEscrowAgent(EthereumContractAgent):
|
|||
raise ValueError("Period must be > 0")
|
||||
|
||||
if pagination_size is None:
|
||||
pagination_size = StakingEscrowAgent.DEFAULT_PAGINATION_SIZE if self.blockchain.is_light else 0
|
||||
pagination_size = self.DEFAULT_STAKER_PAGINATION_SIZE_LIGHT_NODE if self.blockchain.is_light else self.DEFAULT_STAKER_PAGINATION_SIZE
|
||||
self.log.debug(f"Defaulting to pagination size {pagination_size}")
|
||||
elif pagination_size < 0:
|
||||
raise ValueError("Pagination size must be >= 0")
|
||||
|
||||
|
@ -325,15 +334,34 @@ class StakingEscrowAgent(EthereumContractAgent):
|
|||
start_index: int = 0
|
||||
n_tokens: int = 0
|
||||
stakers: Dict[int, int] = dict()
|
||||
active_stakers: Tuple[NuNits, List[List[int]]]
|
||||
attempts = 0
|
||||
while start_index < num_stakers:
|
||||
active_stakers = self.contract.functions.getActiveStakers(periods, start_index, pagination_size).call()
|
||||
temp_locked_tokens, temp_stakers = active_stakers
|
||||
# temp_stakers is a list of length-2 lists (address -> locked tokens)
|
||||
temp_stakers_map = {address: locked_tokens for address, locked_tokens in temp_stakers}
|
||||
n_tokens = n_tokens + temp_locked_tokens
|
||||
stakers.update(temp_stakers_map)
|
||||
start_index += pagination_size
|
||||
try:
|
||||
attempts += 1
|
||||
active_stakers = self.contract.functions.getActiveStakers(periods,
|
||||
start_index,
|
||||
pagination_size).call()
|
||||
except Exception as e:
|
||||
if 'timeout' not in str(e):
|
||||
# exception unrelated to pagination size and timeout
|
||||
raise e
|
||||
elif pagination_size == 1 or attempts >= 3:
|
||||
# we tried
|
||||
raise e
|
||||
else:
|
||||
# reduce pagination size and retry
|
||||
old_pagination_size = pagination_size
|
||||
pagination_size = old_pagination_size // 2
|
||||
self.log.debug(f"Failed stakers sampling using pagination size = {old_pagination_size}. "
|
||||
f"Retrying with size {pagination_size}")
|
||||
else:
|
||||
temp_locked_tokens, temp_stakers = active_stakers
|
||||
# temp_stakers is a list of length-2 lists (address -> locked tokens)
|
||||
temp_stakers_map = {address: locked_tokens for address, locked_tokens in temp_stakers}
|
||||
n_tokens = n_tokens + temp_locked_tokens
|
||||
stakers.update(temp_stakers_map)
|
||||
start_index += pagination_size
|
||||
|
||||
else:
|
||||
n_tokens, temp_stakers = self.contract.functions.getActiveStakers(periods, 0, 0).call()
|
||||
stakers = {address: locked_tokens for address, locked_tokens in temp_stakers}
|
||||
|
|
|
@ -61,7 +61,7 @@ from nucypher.blockchain.eth.sol.compile.compile import multiversion_compile
|
|||
from nucypher.blockchain.eth.sol.compile.constants import SOLIDITY_SOURCE_ROOT
|
||||
from nucypher.blockchain.eth.sol.compile.types import SourceBundle
|
||||
from nucypher.blockchain.eth.utils import get_transaction_name, prettify_eth_amount
|
||||
from nucypher.characters.control.emitters import JSONRPCStdoutEmitter, StdoutEmitter
|
||||
from nucypher.control.emitters import StdoutEmitter, JSONRPCStdoutEmitter
|
||||
from nucypher.utilities.ethereum import encode_constructor_arguments
|
||||
from nucypher.utilities.gas_strategies import (
|
||||
construct_datafeed_median_strategy,
|
||||
|
|
|
@ -33,7 +33,7 @@ from web3 import Web3
|
|||
|
||||
from nucypher.blockchain.eth.decorators import validate_checksum_address
|
||||
from nucypher.blockchain.eth.signers.base import Signer
|
||||
from nucypher.characters.control.emitters import StdoutEmitter
|
||||
from nucypher.control.emitters import StdoutEmitter
|
||||
|
||||
|
||||
def handle_trezor_call(device_func):
|
||||
|
|
|
@ -75,16 +75,6 @@ the Encryptor.
|
|||
"""
|
||||
|
||||
|
||||
MOE_BANNER = r"""
|
||||
_______
|
||||
| | |.-----..-----.
|
||||
| || _ || -__|
|
||||
|__|_|__||_____||_____|
|
||||
|
||||
the Monitor.
|
||||
"""
|
||||
|
||||
|
||||
URSULA_BANNER = r'''
|
||||
|
||||
|
||||
|
|
|
@ -39,7 +39,8 @@ from eth_utils import to_canonical_address, to_checksum_address
|
|||
from nucypher.acumen.nicknames import Nickname
|
||||
from nucypher.blockchain.eth.registry import BaseContractRegistry, InMemoryContractRegistry
|
||||
from nucypher.blockchain.eth.signers.base import Signer
|
||||
from nucypher.characters.control.controllers import CLIController, JSONRPCController
|
||||
from nucypher.characters.control.controllers import CharacterCLIController
|
||||
from nucypher.control.controllers import JSONRPCController
|
||||
from nucypher.crypto.keystore import Keystore
|
||||
from nucypher.crypto.kits import UmbralMessageKit
|
||||
from nucypher.crypto.powers import (
|
||||
|
@ -67,7 +68,6 @@ class Character(Learner):
|
|||
_display_name_template = "({})⇀{}↽ ({})" # Used in __repr__ and in cls.from_bytes
|
||||
_default_crypto_powerups = None
|
||||
_stamp = None
|
||||
_crashed = False
|
||||
|
||||
def __init__(self,
|
||||
domain: str = None,
|
||||
|
@ -512,9 +512,9 @@ class Character(Learner):
|
|||
|
||||
def make_cli_controller(self, crash_on_error: bool = False):
|
||||
app_name = bytes(self.stamp).hex()[:6]
|
||||
controller = CLIController(app_name=app_name,
|
||||
crash_on_error=crash_on_error,
|
||||
interface=self.interface)
|
||||
controller = CharacterCLIController(app_name=app_name,
|
||||
crash_on_error=crash_on_error,
|
||||
interface=self.interface)
|
||||
|
||||
self.controller = controller
|
||||
return controller
|
||||
|
|
|
@ -15,353 +15,24 @@
|
|||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import json
|
||||
from json import JSONDecodeError
|
||||
|
||||
import inspect
|
||||
import maya
|
||||
from abc import ABC, abstractmethod
|
||||
from flask import Flask, Response
|
||||
from hendrix.deploy.base import HendrixDeploy
|
||||
from twisted.internet import reactor, stdio
|
||||
|
||||
from nucypher.characters.control.emitters import JSONRPCStdoutEmitter, StdoutEmitter, WebEmitter
|
||||
from nucypher.characters.control.interfaces import CharacterPublicInterface
|
||||
from nucypher.characters.control.specifications.exceptions import SpecificationError
|
||||
from nucypher.cli.processes import JSONRPCLineReceiver
|
||||
from nucypher.config.constants import MAX_UPLOAD_CONTENT_LENGTH
|
||||
from nucypher.exceptions import DevelopmentInstallationRequired
|
||||
from nucypher.network.resources import get_static_resources
|
||||
from nucypher.utilities.logging import Logger, GlobalLoggerSettings
|
||||
from nucypher.control.controllers import CLIController
|
||||
from nucypher.control.emitters import StdoutEmitter
|
||||
|
||||
|
||||
class CharacterControllerBase(ABC):
|
||||
"""
|
||||
A transactional interface for a human to interact with all
|
||||
of one characters entry and exit points.
|
||||
|
||||
Subclasses of CharacterControllerBase handle a character's public interface I/O,
|
||||
serialization, interface specification, validation, and transport.
|
||||
|
||||
(stdio, http, in-memory python containers, other IPC, or another protocol.)
|
||||
"""
|
||||
_emitter_class = NotImplemented
|
||||
|
||||
def __init__(self):
|
||||
|
||||
# Control Emitter
|
||||
self.emitter = self._emitter_class()
|
||||
|
||||
def _perform_action(self, action: str, request: dict) -> dict:
|
||||
"""
|
||||
This method is where input validation and method invocation
|
||||
happens for all character actions.
|
||||
"""
|
||||
request = request or {} # for requests with no input params request can be ''
|
||||
method = getattr(self.interface, action, None)
|
||||
serializer = method._schema
|
||||
params = serializer.load(request) # input validation will occur here.
|
||||
response = method(**params) # < ---- INLET
|
||||
|
||||
response_data = serializer.dump(response)
|
||||
return response_data
|
||||
|
||||
|
||||
class CharacterControlServer(CharacterControllerBase):
|
||||
class CharacterCLIController(CLIController):
|
||||
|
||||
_emitter_class = StdoutEmitter
|
||||
_crash_on_error_default = False
|
||||
|
||||
def __init__(self,
|
||||
app_name: str,
|
||||
interface: CharacterPublicInterface,
|
||||
start_learning: bool = True,
|
||||
crash_on_error: bool = _crash_on_error_default,
|
||||
*args, **kwargs):
|
||||
|
||||
self.app_name = app_name
|
||||
|
||||
# Configuration
|
||||
self.start_learning = start_learning
|
||||
self.crash_on_error = crash_on_error
|
||||
|
||||
# Control Cycle Handler
|
||||
self.emitter = self._emitter_class()
|
||||
|
||||
# Internals
|
||||
self._transport = None
|
||||
|
||||
self.interface = interface
|
||||
|
||||
def set_method(name):
|
||||
|
||||
def wrapper(request=None, **kwargs):
|
||||
request = request or kwargs
|
||||
return self.handle_request(name, request=request)
|
||||
setattr(self, name, wrapper)
|
||||
|
||||
for method_name in self._get_interfaces().keys():
|
||||
set_method(method_name)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.log = Logger(app_name)
|
||||
|
||||
def _get_interfaces(self):
|
||||
return {
|
||||
name: method for name, method in
|
||||
inspect.getmembers(
|
||||
self.interface,
|
||||
predicate=inspect.ismethod)
|
||||
if hasattr(method, '_schema')
|
||||
}
|
||||
|
||||
def stop_character(self):
|
||||
self.interface.character.disenchant()
|
||||
|
||||
@abstractmethod
|
||||
def make_control_transport(self):
|
||||
return NotImplemented
|
||||
|
||||
@abstractmethod
|
||||
def handle_request(self, method_name, control_request):
|
||||
return NotImplemented
|
||||
|
||||
@abstractmethod
|
||||
def test_client(self):
|
||||
return NotImplemented
|
||||
|
||||
|
||||
class CLIController(CharacterControlServer):
|
||||
|
||||
_emitter_class = StdoutEmitter
|
||||
|
||||
def make_control_transport(self):
|
||||
return
|
||||
|
||||
def test_client(self):
|
||||
return
|
||||
|
||||
def handle_request(self, method_name, request) -> dict:
|
||||
response = self._perform_action(action=method_name, request=request)
|
||||
if GlobalLoggerSettings._json_ipc:
|
||||
# support for --json-ipc flag, for JSON *responses* from CLI commands-as-requests.
|
||||
start = maya.now()
|
||||
self.emitter.ipc(response=response, request_id=start.epoch, duration=maya.now() - start)
|
||||
else:
|
||||
self.emitter.pretty(response)
|
||||
return response
|
||||
interface: 'CharacterPublicInterface',
|
||||
*args,
|
||||
**kwargs):
|
||||
super().__init__(interface=interface, *args, **kwargs)
|
||||
|
||||
def _perform_action(self, *args, **kwargs) -> dict:
|
||||
try:
|
||||
response_data = super()._perform_action(*args, **kwargs)
|
||||
finally:
|
||||
self.log.debug(f"Finished action '{kwargs['action']}', stopping {self.interface.character}")
|
||||
self.stop_character()
|
||||
self.log.debug(f"Finished action '{kwargs['action']}', stopping {self.interface.implementer}")
|
||||
self.interface.implementer.disenchant()
|
||||
return response_data
|
||||
|
||||
|
||||
class JSONRPCController(CharacterControlServer):
|
||||
|
||||
_emitter_class = JSONRPCStdoutEmitter
|
||||
|
||||
def start(self):
|
||||
_transport = self.make_control_transport()
|
||||
reactor.run() # < ------ Blocking Call (Reactor)
|
||||
|
||||
def test_client(self):
|
||||
try:
|
||||
from tests.utils.controllers import JSONRPCTestClient
|
||||
except ImportError:
|
||||
raise DevelopmentInstallationRequired(importable_name='tests.utils.controllers.JSONRPCTestClient')
|
||||
|
||||
test_client = JSONRPCTestClient(rpc_controller=self)
|
||||
return test_client
|
||||
|
||||
def make_control_transport(self):
|
||||
transport = stdio.StandardIO(JSONRPCLineReceiver(rpc_controller=self))
|
||||
return transport
|
||||
|
||||
def handle_procedure_call(self, control_request) -> int:
|
||||
|
||||
# Validate request and read request metadata
|
||||
jsonrpc2 = control_request['jsonrpc']
|
||||
if jsonrpc2 != '2.0':
|
||||
raise self.emitter.InvalidRequest
|
||||
|
||||
request_id = control_request['id']
|
||||
|
||||
# Read the interface's signature metadata
|
||||
method_name = control_request['method']
|
||||
method_params = control_request.get('params', dict()) # optional
|
||||
if method_name not in self._get_interfaces():
|
||||
raise self.emitter.MethodNotFound(f'No method called {method_name}')
|
||||
|
||||
return self.call_interface(method_name=method_name,
|
||||
request=method_params,
|
||||
request_id=request_id)
|
||||
|
||||
def handle_message(self, message: dict, *args, **kwargs) -> int:
|
||||
"""Handle single JSON RPC message"""
|
||||
|
||||
try:
|
||||
_request_id = message['id']
|
||||
|
||||
except KeyError: # Notification
|
||||
raise self.emitter.InvalidRequest('No request id')
|
||||
except TypeError:
|
||||
raise self.emitter.InvalidRequest(f'Request object not valid: {type(message)}')
|
||||
else: # RPC
|
||||
return self.handle_procedure_call(control_request=message)
|
||||
|
||||
def handle_batch(self, control_requests: list) -> int:
|
||||
|
||||
if not control_requests:
|
||||
e = self.emitter.InvalidRequest()
|
||||
return self.emitter.error(e)
|
||||
|
||||
batch_size = 0
|
||||
for request in control_requests: # TODO: parallelism
|
||||
response_size = self.handle_message(message=request)
|
||||
batch_size += response_size
|
||||
return batch_size
|
||||
|
||||
def handle_request(self, control_request: bytes, *args, **kwargs) -> int:
|
||||
|
||||
try:
|
||||
control_request = json.loads(control_request)
|
||||
except JSONDecodeError:
|
||||
e = self.emitter.ParseError()
|
||||
return self.emitter.error(e)
|
||||
|
||||
# Handle batch of messages
|
||||
if isinstance(control_request, list):
|
||||
return self.handle_batch(control_requests=control_request)
|
||||
|
||||
# Handle single message
|
||||
try:
|
||||
return self.handle_message(message=control_request, *args, **kwargs)
|
||||
|
||||
except self.emitter.JSONRPCError as e:
|
||||
return self.emitter.error(e)
|
||||
|
||||
except Exception as e:
|
||||
if self.crash_on_error:
|
||||
raise
|
||||
return self.emitter.error(e)
|
||||
|
||||
def call_interface(self, method_name, request, request_id: int = None):
|
||||
received = maya.now()
|
||||
internal_request_id = received.epoch
|
||||
if request_id is None:
|
||||
request_id = internal_request_id
|
||||
response = self._perform_action(action=method_name, request=request)
|
||||
responded = maya.now()
|
||||
duration = responded - received
|
||||
return self.emitter.ipc(response=response, request_id=request_id, duration=duration)
|
||||
|
||||
|
||||
class WebController(CharacterControlServer):
|
||||
"""
|
||||
A wrapper around a JSON control interface that
|
||||
handles web requests to exert control over a character.
|
||||
"""
|
||||
|
||||
_emitter_class = WebEmitter
|
||||
_crash_on_error_default = False
|
||||
|
||||
_captured_status_codes = {200: 'OK',
|
||||
400: 'BAD REQUEST',
|
||||
500: 'INTERNAL SERVER ERROR'}
|
||||
|
||||
def test_client(self):
|
||||
test_client = self._transport.test_client()
|
||||
|
||||
# ease your mind
|
||||
self._transport.config.update(TESTING=self.crash_on_error, PROPOGATE_EXCEPTION=self.crash_on_error)
|
||||
|
||||
return test_client
|
||||
|
||||
def make_control_transport(self):
|
||||
|
||||
self._transport = Flask(self.app_name)
|
||||
self._transport.config['MAX_CONTENT_LENGTH'] = MAX_UPLOAD_CONTENT_LENGTH
|
||||
|
||||
# Return FlaskApp decorator
|
||||
return self._transport
|
||||
|
||||
def start(self, http_port: int, dry_run: bool = False):
|
||||
|
||||
self.log.info("Starting HTTP Character Control...")
|
||||
if dry_run:
|
||||
return
|
||||
|
||||
# TODO #845: Make non-blocking web control startup
|
||||
hx_deployer = HendrixDeploy(action="start", options={
|
||||
"wsgi": self._transport, "http_port": http_port, "resources": get_static_resources()})
|
||||
hx_deployer.run() # <--- Blocking Call to Reactor
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
return self.handle_request(*args, **kwargs)
|
||||
|
||||
def handle_request(self, method_name, control_request, *args, **kwargs) -> Response:
|
||||
|
||||
_400_exceptions = (SpecificationError,
|
||||
TypeError,
|
||||
JSONDecodeError,
|
||||
self.emitter.MethodNotFound)
|
||||
|
||||
try:
|
||||
request_body = control_request.data or dict()
|
||||
if request_body:
|
||||
request_body = json.loads(request_body)
|
||||
request_body.update(kwargs)
|
||||
|
||||
if method_name not in self._get_interfaces():
|
||||
raise self.emitter.MethodNotFound(f'No method called {method_name}')
|
||||
|
||||
response = self._perform_action(action=method_name, request=request_body)
|
||||
|
||||
#
|
||||
# Client Errors
|
||||
#
|
||||
except _400_exceptions as e:
|
||||
__exception_code = 400
|
||||
return self.emitter.exception(
|
||||
e=e,
|
||||
log_level='debug',
|
||||
response_code=__exception_code,
|
||||
error_message=WebController._captured_status_codes[__exception_code])
|
||||
|
||||
#
|
||||
# Server Errors
|
||||
#
|
||||
except SpecificationError as e:
|
||||
__exception_code = 500
|
||||
if self.crash_on_error:
|
||||
raise
|
||||
return self.emitter.exception(
|
||||
e=e,
|
||||
log_level='critical',
|
||||
response_code=__exception_code,
|
||||
error_message=WebController._captured_status_codes[__exception_code])
|
||||
|
||||
#
|
||||
# Unhandled Server Errors
|
||||
#
|
||||
except Exception as e:
|
||||
__exception_code = 500
|
||||
if self.crash_on_error:
|
||||
raise
|
||||
return self.emitter.exception(
|
||||
e=e,
|
||||
log_level='debug',
|
||||
response_code=__exception_code,
|
||||
error_message=WebController._captured_status_codes[__exception_code])
|
||||
|
||||
#
|
||||
# Send to WebEmitter
|
||||
#
|
||||
else:
|
||||
self.log.debug(f"{method_name} [200 - OK]")
|
||||
return self.emitter.respond(response=response)
|
||||
|
|
|
@ -15,12 +15,12 @@
|
|||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import functools
|
||||
import maya
|
||||
from typing import Union
|
||||
|
||||
import maya
|
||||
|
||||
from nucypher.characters.base import Character
|
||||
from nucypher.characters.control.specifications import alice, bob, enrico
|
||||
from nucypher.control.interfaces import attach_schema, ControlInterface
|
||||
from nucypher.crypto.kits import UmbralMessageKit
|
||||
from nucypher.crypto.powers import DecryptingPower, SigningPower
|
||||
from nucypher.crypto.umbral_adapter import PublicKey
|
||||
|
@ -28,41 +28,10 @@ from nucypher.crypto.utils import construct_policy_id
|
|||
from nucypher.network.middleware import RestMiddleware
|
||||
|
||||
|
||||
def attach_schema(schema):
|
||||
def callable(func):
|
||||
func._schema = schema()
|
||||
class CharacterPublicInterface(ControlInterface):
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapped(*args, **kwargs):
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapped
|
||||
|
||||
return callable
|
||||
|
||||
|
||||
class CharacterPublicInterface:
|
||||
|
||||
def __init__(self, character=None, *args, **kwargs):
|
||||
self.character = character
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def connect_cli(cls, action):
|
||||
schema = getattr(cls, action)._schema
|
||||
|
||||
def callable(func):
|
||||
c = func
|
||||
for f in [f for f in schema.load_fields.values() if f.click]:
|
||||
c = f.click(c)
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapped(*args, **kwargs):
|
||||
return c(*args, **kwargs)
|
||||
|
||||
return wrapped
|
||||
|
||||
return callable
|
||||
def __init__(self, character: Character = None, *args, **kwargs):
|
||||
super().__init__(implementer=character, *args, **kwargs)
|
||||
|
||||
|
||||
class AliceInterface(CharacterPublicInterface):
|
||||
|
@ -82,7 +51,7 @@ class AliceInterface(CharacterPublicInterface):
|
|||
bob = Bob.from_public_keys(encrypting_key=bob_encrypting_key,
|
||||
verifying_key=bob_verifying_key)
|
||||
|
||||
new_policy = self.character.create_policy(
|
||||
new_policy = self.implementer.create_policy(
|
||||
bob=bob,
|
||||
label=label,
|
||||
m=m,
|
||||
|
@ -95,7 +64,7 @@ class AliceInterface(CharacterPublicInterface):
|
|||
|
||||
@attach_schema(alice.DerivePolicyEncryptionKey)
|
||||
def derive_policy_encrypting_key(self, label: bytes) -> dict:
|
||||
policy_encrypting_key = self.character.get_policy_encrypting_key_from_label(label)
|
||||
policy_encrypting_key = self.implementer.get_policy_encrypting_key_from_label(label)
|
||||
response_data = {'policy_encrypting_key': policy_encrypting_key, 'label': label}
|
||||
return response_data
|
||||
|
||||
|
@ -115,13 +84,13 @@ class AliceInterface(CharacterPublicInterface):
|
|||
bob = Bob.from_public_keys(encrypting_key=bob_encrypting_key,
|
||||
verifying_key=bob_verifying_key)
|
||||
|
||||
new_policy = self.character.grant(bob=bob,
|
||||
label=label,
|
||||
m=m,
|
||||
n=n,
|
||||
value=value,
|
||||
rate=rate,
|
||||
expiration=expiration)
|
||||
new_policy = self.implementer.grant(bob=bob,
|
||||
label=label,
|
||||
m=m,
|
||||
n=n,
|
||||
value=value,
|
||||
rate=rate,
|
||||
expiration=expiration)
|
||||
|
||||
new_policy.treasure_map_publisher.block_until_success_is_reasonably_likely()
|
||||
|
||||
|
@ -136,16 +105,16 @@ class AliceInterface(CharacterPublicInterface):
|
|||
|
||||
# TODO: Move deeper into characters
|
||||
policy_id = construct_policy_id(label, bob_verifying_key)
|
||||
policy = self.character.active_policies[policy_id]
|
||||
policy = self.implementer.active_policies[policy_id]
|
||||
|
||||
receipt, failed_revocations = self.character.revoke(policy)
|
||||
receipt, failed_revocations = self.implementer.revoke(policy)
|
||||
if len(failed_revocations) > 0:
|
||||
for node_id, attempt in failed_revocations.items():
|
||||
revocation, fail_reason = attempt
|
||||
if fail_reason == RestMiddleware.NotFound:
|
||||
del (failed_revocations[node_id])
|
||||
if len(failed_revocations) <= (policy.n - policy.treasure_map.m + 1):
|
||||
del (self.character.active_policies[policy_id])
|
||||
del (self.implementer.active_policies[policy_id])
|
||||
|
||||
response_data = {'failed_revocations': len(failed_revocations)}
|
||||
return response_data
|
||||
|
@ -157,7 +126,7 @@ class AliceInterface(CharacterPublicInterface):
|
|||
"""
|
||||
|
||||
from nucypher.characters.lawful import Enrico
|
||||
policy_encrypting_key = self.character.get_policy_encrypting_key_from_label(label)
|
||||
policy_encrypting_key = self.implementer.get_policy_encrypting_key_from_label(label)
|
||||
|
||||
# TODO #846: May raise UnknownOpenSSLError and InvalidTag.
|
||||
message_kit = UmbralMessageKit.from_bytes(message_kit)
|
||||
|
@ -168,7 +137,7 @@ class AliceInterface(CharacterPublicInterface):
|
|||
label=label
|
||||
)
|
||||
|
||||
plaintexts = self.character.decrypt_message_kit(
|
||||
plaintexts = self.implementer.decrypt_message_kit(
|
||||
message_kit=message_kit,
|
||||
data_source=enrico,
|
||||
label=label
|
||||
|
@ -182,7 +151,7 @@ class AliceInterface(CharacterPublicInterface):
|
|||
"""
|
||||
Character control endpoint for getting Alice's public keys.
|
||||
"""
|
||||
verifying_key = self.character.public_keys(SigningPower)
|
||||
verifying_key = self.implementer.public_keys(SigningPower)
|
||||
response_data = {'alice_verifying_key': verifying_key}
|
||||
return response_data
|
||||
|
||||
|
@ -194,7 +163,7 @@ class BobInterface(CharacterPublicInterface):
|
|||
"""
|
||||
Character control endpoint for joining a policy on the network.
|
||||
"""
|
||||
self.character.join_policy(label=label, publisher_verifying_key=alice_verifying_key)
|
||||
self.implementer.join_policy(label=label, publisher_verifying_key=alice_verifying_key)
|
||||
response = {'policy_encrypting_key': 'OK'} # FIXME
|
||||
return response
|
||||
|
||||
|
@ -218,13 +187,13 @@ class BobInterface(CharacterPublicInterface):
|
|||
policy_encrypting_key=policy_encrypting_key,
|
||||
label=label)
|
||||
|
||||
self.character.join_policy(label=label, publisher_verifying_key=alice_verifying_key)
|
||||
self.implementer.join_policy(label=label, publisher_verifying_key=alice_verifying_key)
|
||||
|
||||
plaintexts = self.character.retrieve(message_kit,
|
||||
enrico=enrico,
|
||||
alice_verifying_key=alice_verifying_key,
|
||||
label=label,
|
||||
treasure_map=treasure_map)
|
||||
plaintexts = self.implementer.retrieve(message_kit,
|
||||
enrico=enrico,
|
||||
alice_verifying_key=alice_verifying_key,
|
||||
label=label,
|
||||
treasure_map=treasure_map)
|
||||
|
||||
response_data = {'cleartexts': plaintexts}
|
||||
return response_data
|
||||
|
@ -234,8 +203,8 @@ class BobInterface(CharacterPublicInterface):
|
|||
"""
|
||||
Character control endpoint for getting Bob's encrypting and signing public keys
|
||||
"""
|
||||
verifying_key = self.character.public_keys(SigningPower)
|
||||
encrypting_key = self.character.public_keys(DecryptingPower)
|
||||
verifying_key = self.implementer.public_keys(SigningPower)
|
||||
encrypting_key = self.implementer.public_keys(DecryptingPower)
|
||||
response_data = {'bob_encrypting_key': encrypting_key, 'bob_verifying_key': verifying_key}
|
||||
return response_data
|
||||
|
||||
|
@ -248,6 +217,6 @@ class EnricoInterface(CharacterPublicInterface):
|
|||
Character control endpoint for encrypting data for a policy and
|
||||
receiving the messagekit (and signature) to give to Bob.
|
||||
"""
|
||||
message_kit, signature = self.character.encrypt_message(plaintext=plaintext)
|
||||
message_kit, signature = self.implementer.encrypt_message(plaintext=plaintext)
|
||||
response_data = {'message_kit': message_kit, 'signature': signature}
|
||||
return response_data
|
||||
|
|
|
@ -19,36 +19,36 @@
|
|||
import click
|
||||
from marshmallow import validates_schema
|
||||
|
||||
from nucypher.characters.control.specifications.fields.treasuremap import TreasureMap
|
||||
from nucypher.characters.control.specifications import fields
|
||||
from nucypher.characters.control.specifications.base import BaseSchema
|
||||
from nucypher.characters.control.specifications.exceptions import InvalidArgumentCombo
|
||||
from nucypher.characters.control.specifications import fields as character_fields
|
||||
from nucypher.control.specifications import fields as base_fields
|
||||
from nucypher.control.specifications.base import BaseSchema
|
||||
from nucypher.control.specifications.exceptions import InvalidArgumentCombo
|
||||
from nucypher.cli import options, types
|
||||
|
||||
|
||||
class PolicyBaseSchema(BaseSchema):
|
||||
|
||||
bob_encrypting_key = fields.Key(
|
||||
bob_encrypting_key = character_fields.Key(
|
||||
required=True, load_only=True,
|
||||
click=click.option(
|
||||
'--bob-encrypting-key',
|
||||
'-bek',
|
||||
help="Bob's encrypting key as a hexadecimal string",
|
||||
type=click.STRING, required=False))
|
||||
bob_verifying_key = fields.Key(
|
||||
bob_verifying_key = character_fields.Key(
|
||||
required=True, load_only=True,
|
||||
click=click.option(
|
||||
'--bob-verifying-key',
|
||||
'-bvk',
|
||||
help="Bob's verifying key as a hexadecimal string",
|
||||
type=click.STRING, required=False))
|
||||
m = fields.M(
|
||||
m = character_fields.M(
|
||||
required=True, load_only=True,
|
||||
click=options.option_m)
|
||||
n = fields.N(
|
||||
n = character_fields.N(
|
||||
required=True, load_only=True,
|
||||
click=options.option_n)
|
||||
expiration = fields.DateTime(
|
||||
expiration = character_fields.DateTime(
|
||||
required=True, load_only=True,
|
||||
click=click.option(
|
||||
'--expiration',
|
||||
|
@ -57,18 +57,18 @@ class PolicyBaseSchema(BaseSchema):
|
|||
)
|
||||
|
||||
# optional input
|
||||
value = fields.Wei(
|
||||
value = character_fields.Wei(
|
||||
load_only=True,
|
||||
click=click.option('--value', help="Total policy value (in Wei)", type=types.WEI))
|
||||
|
||||
rate = fields.Wei(
|
||||
rate = character_fields.Wei(
|
||||
load_only=True,
|
||||
required=False,
|
||||
click=options.option_rate
|
||||
)
|
||||
|
||||
# output
|
||||
policy_encrypting_key = fields.Key(dump_only=True)
|
||||
policy_encrypting_key = character_fields.Key(dump_only=True)
|
||||
|
||||
@validates_schema
|
||||
def check_valid_n_and_m(self, data, **kwargs):
|
||||
|
@ -89,38 +89,40 @@ class PolicyBaseSchema(BaseSchema):
|
|||
|
||||
class CreatePolicy(PolicyBaseSchema):
|
||||
|
||||
label = fields.Label(
|
||||
label = character_fields.Label(
|
||||
required=True,
|
||||
click=options.option_label(required=True))
|
||||
|
||||
|
||||
class GrantPolicy(PolicyBaseSchema):
|
||||
|
||||
label = fields.Label(
|
||||
label = character_fields.Label(
|
||||
load_only=True, required=True,
|
||||
click=options.option_label(required=False))
|
||||
|
||||
# output fields
|
||||
treasure_map = TreasureMap(dump_only=True)
|
||||
alice_verifying_key = fields.Key(dump_only=True)
|
||||
# treasure map only used for serialization so no need to provide federated/non-federated context
|
||||
treasure_map = character_fields.TreasureMap(dump_only=True)
|
||||
|
||||
alice_verifying_key = character_fields.Key(dump_only=True)
|
||||
|
||||
|
||||
class DerivePolicyEncryptionKey(BaseSchema):
|
||||
|
||||
label = fields.Label(
|
||||
label = character_fields.Label(
|
||||
required=True,
|
||||
click=options.option_label(required=True))
|
||||
|
||||
# output
|
||||
policy_encrypting_key = fields.Key(dump_only=True)
|
||||
policy_encrypting_key = character_fields.Key(dump_only=True)
|
||||
|
||||
|
||||
class Revoke(BaseSchema):
|
||||
|
||||
label = fields.Label(
|
||||
label = character_fields.Label(
|
||||
required=True, load_only=True,
|
||||
click=options.option_label(required=True))
|
||||
bob_verifying_key = fields.Key(
|
||||
bob_verifying_key = character_fields.Key(
|
||||
required=True, load_only=True,
|
||||
click=click.option(
|
||||
'--bob-verifying-key',
|
||||
|
@ -129,21 +131,21 @@ class Revoke(BaseSchema):
|
|||
required=True))
|
||||
|
||||
# output
|
||||
failed_revocations = fields.Integer(dump_only=True)
|
||||
failed_revocations = base_fields.Integer(dump_only=True)
|
||||
|
||||
|
||||
class Decrypt(BaseSchema):
|
||||
label = fields.Label(
|
||||
label = character_fields.Label(
|
||||
required=True, load_only=True,
|
||||
click=options.option_label(required=True))
|
||||
message_kit = fields.UmbralMessageKit(
|
||||
message_kit = character_fields.UmbralMessageKit(
|
||||
load_only=True,
|
||||
click=options.option_message_kit(required=True))
|
||||
|
||||
# output
|
||||
cleartexts = fields.List(fields.Cleartext(), dump_only=True)
|
||||
cleartexts = base_fields.List(character_fields.Cleartext(), dump_only=True)
|
||||
|
||||
|
||||
class PublicKeys(BaseSchema):
|
||||
|
||||
alice_verifying_key = fields.Key(dump_only=True)
|
||||
alice_verifying_key = character_fields.Key(dump_only=True)
|
||||
|
|
|
@ -17,17 +17,18 @@
|
|||
|
||||
import click
|
||||
|
||||
from nucypher.characters.control.specifications import fields
|
||||
from nucypher.characters.control.specifications.base import BaseSchema
|
||||
import nucypher.control.specifications.fields as base_fields
|
||||
from nucypher.characters.control.specifications import fields as character_fields
|
||||
from nucypher.control.specifications.base import BaseSchema
|
||||
from nucypher.cli import options
|
||||
|
||||
|
||||
class JoinPolicy(BaseSchema): #TODO: this doesn't have a cli implementation
|
||||
|
||||
label = fields.Label(
|
||||
label = character_fields.Label(
|
||||
load_only=True, required=True,
|
||||
click=options.option_label(required=True))
|
||||
alice_verifying_key = fields.Key(
|
||||
alice_verifying_key = character_fields.Key(
|
||||
load_only=True, required=True,
|
||||
click=click.option(
|
||||
'--alice-verifying-key',
|
||||
|
@ -35,23 +36,23 @@ class JoinPolicy(BaseSchema): #TODO: this doesn't have a cli implementation
|
|||
help="Alice's verifying key as a hexadecimal string",
|
||||
required=False, type=click.STRING,))
|
||||
|
||||
policy_encrypting_key = fields.String(dump_only=True)
|
||||
policy_encrypting_key = base_fields.String(dump_only=True)
|
||||
# this should be a Key Field
|
||||
# but bob.join_policy outputs {'policy_encrypting_key': 'OK'}
|
||||
|
||||
|
||||
class Retrieve(BaseSchema):
|
||||
label = fields.Label(
|
||||
label = character_fields.Label(
|
||||
required=True,
|
||||
load_only=True,
|
||||
click=options.option_label(required=False)
|
||||
)
|
||||
policy_encrypting_key = fields.Key(
|
||||
policy_encrypting_key = character_fields.Key(
|
||||
required=True,
|
||||
load_only=True,
|
||||
click=options.option_policy_encrypting_key(required=False)
|
||||
)
|
||||
alice_verifying_key = fields.Key(
|
||||
alice_verifying_key = character_fields.Key(
|
||||
required=False,
|
||||
load_only=True,
|
||||
click=click.option(
|
||||
|
@ -61,15 +62,15 @@ class Retrieve(BaseSchema):
|
|||
type=click.STRING,
|
||||
required=False)
|
||||
)
|
||||
message_kit = fields.UmbralMessageKit(
|
||||
message_kit = character_fields.UmbralMessageKit(
|
||||
required=True,
|
||||
load_only=True,
|
||||
click=options.option_message_kit(required=False)
|
||||
)
|
||||
|
||||
cleartexts = fields.List(fields.Cleartext(), dump_only=True)
|
||||
cleartexts = base_fields.List(character_fields.Cleartext(), dump_only=True)
|
||||
|
||||
|
||||
class PublicKeys(BaseSchema):
|
||||
bob_encrypting_key = fields.Key(dump_only=True)
|
||||
bob_verifying_key = fields.Key(dump_only=True)
|
||||
bob_encrypting_key = character_fields.Key(dump_only=True)
|
||||
bob_verifying_key = character_fields.Key(dump_only=True)
|
||||
|
|
|
@ -18,10 +18,11 @@
|
|||
import click
|
||||
from marshmallow import post_load
|
||||
|
||||
from nucypher.characters.control.specifications import fields, exceptions
|
||||
import nucypher.control.specifications.exceptions
|
||||
from nucypher.characters.control.specifications import fields
|
||||
from nucypher.cli import options
|
||||
from nucypher.cli.types import EXISTING_READABLE_FILE
|
||||
from nucypher.characters.control.specifications.base import BaseSchema
|
||||
from nucypher.control.specifications.base import BaseSchema
|
||||
|
||||
|
||||
class EncryptMessage(BaseSchema):
|
||||
|
@ -53,7 +54,8 @@ class EncryptMessage(BaseSchema):
|
|||
"""
|
||||
|
||||
if data.get('message') and data.get('file'):
|
||||
raise exceptions.InvalidArgumentCombo("choose either a message or a filepath but not both.")
|
||||
raise nucypher.control.specifications.exceptions.InvalidArgumentCombo(
|
||||
"Choose either a message or a filepath but not both.")
|
||||
|
||||
if data.get('message'):
|
||||
data = bytes(data['message'], encoding='utf-8')
|
||||
|
|
|
@ -18,26 +18,5 @@
|
|||
from bytestring_splitter import BytestringSplittingError
|
||||
from cryptography.exceptions import InternalError
|
||||
|
||||
|
||||
class SpecificationError(ValueError):
|
||||
"""The protocol request is completely unusable"""
|
||||
|
||||
|
||||
class MissingField(SpecificationError):
|
||||
"""The protocol request cannot be deserialized because it is missing required fields"""
|
||||
|
||||
|
||||
class InvalidInputData(SpecificationError):
|
||||
"""Input data does not match the input specification"""
|
||||
|
||||
|
||||
class InvalidOutputData(SpecificationError):
|
||||
"""Response data does not match the output specification"""
|
||||
|
||||
|
||||
class InvalidArgumentCombo(SpecificationError):
|
||||
"""Arguments specified are incompatible"""
|
||||
|
||||
|
||||
# TODO: catch cryptography.exceptions.InternalError in PyUmbral
|
||||
InvalidNativeDataTypes = (ValueError, TypeError, BytestringSplittingError, InternalError)
|
||||
|
|
|
@ -19,12 +19,12 @@ from base64 import b64encode
|
|||
|
||||
from marshmallow import fields
|
||||
|
||||
from nucypher.characters.control.specifications.fields.base import BaseField
|
||||
from nucypher.control.specifications.fields.base import BaseField
|
||||
|
||||
|
||||
class Cleartext(BaseField, fields.String):
|
||||
|
||||
def _serialize(self, value, attr, data, **kwargs):
|
||||
def _serialize(self, value, attr, obj, **kwargs):
|
||||
return value.decode()
|
||||
|
||||
def _deserialize(self, value, attr, data, **kwargs):
|
||||
|
|
|
@ -18,7 +18,8 @@
|
|||
import maya
|
||||
from marshmallow import fields
|
||||
|
||||
from nucypher.characters.control.specifications.fields.base import BaseField
|
||||
from nucypher.control.specifications.exceptions import InvalidInputData
|
||||
from nucypher.control.specifications.fields.base import BaseField
|
||||
|
||||
|
||||
class DateTime(BaseField, fields.Field):
|
||||
|
@ -27,4 +28,7 @@ class DateTime(BaseField, fields.Field):
|
|||
return value.iso8601()
|
||||
|
||||
def _deserialize(self, value, attr, data, **kwargs):
|
||||
return maya.MayaDT.from_iso8601(iso8601_string=value)
|
||||
try:
|
||||
return maya.MayaDT.from_iso8601(iso8601_string=value)
|
||||
except maya.pendulum.parsing.ParserError as e:
|
||||
raise InvalidInputData(f"Could not convert input for {self.name} to a valid date time: {e}")
|
||||
|
|
|
@ -15,19 +15,22 @@
|
|||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from marshmallow import fields
|
||||
|
||||
from nucypher.characters.control.specifications.exceptions import InvalidInputData, InvalidNativeDataTypes
|
||||
from nucypher.characters.control.specifications.fields.base import BaseField
|
||||
from nucypher.control.specifications.exceptions import InvalidInputData
|
||||
from nucypher.control.specifications.fields.base import BaseField
|
||||
|
||||
|
||||
class FileField(BaseField, fields.String):
|
||||
|
||||
def _deserialize(self, value, attr, data, **kwargs):
|
||||
with open(value, 'rb') as plaintext_file:
|
||||
p = Path(value)
|
||||
if not p.exists():
|
||||
raise InvalidInputData(f"Filepath {value} does not exist")
|
||||
if not p.is_file():
|
||||
raise InvalidInputData(f"Filepath {value} does not map to a file")
|
||||
|
||||
with p.open(mode='rb') as plaintext_file:
|
||||
plaintext = plaintext_file.read() # TODO: #2106 Handle large files
|
||||
return plaintext
|
||||
|
||||
def _validate(self, value):
|
||||
return os.path.exists(value) and os.path.isfile(value)
|
||||
|
|
|
@ -17,8 +17,9 @@
|
|||
|
||||
from marshmallow import fields
|
||||
|
||||
from nucypher.characters.control.specifications.exceptions import InvalidInputData, InvalidNativeDataTypes
|
||||
from nucypher.characters.control.specifications.fields.base import BaseField
|
||||
from nucypher.characters.control.specifications.exceptions import InvalidNativeDataTypes
|
||||
from nucypher.control.specifications.exceptions import InvalidInputData
|
||||
from nucypher.control.specifications.fields.base import BaseField
|
||||
from nucypher.crypto.umbral_adapter import PublicKey
|
||||
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
|
||||
from marshmallow import fields
|
||||
|
||||
from nucypher.characters.control.specifications.fields.base import BaseField
|
||||
from nucypher.control.specifications.fields.base import BaseField
|
||||
|
||||
|
||||
class Label(BaseField, fields.Field):
|
||||
|
|
|
@ -19,8 +19,9 @@ from base64 import b64decode, b64encode
|
|||
|
||||
from marshmallow import fields
|
||||
|
||||
from nucypher.characters.control.specifications.exceptions import InvalidInputData, InvalidNativeDataTypes
|
||||
from nucypher.characters.control.specifications.fields.base import BaseField
|
||||
from nucypher.characters.control.specifications.exceptions import InvalidNativeDataTypes
|
||||
from nucypher.control.specifications.exceptions import InvalidInputData
|
||||
from nucypher.control.specifications.fields.base import BaseField
|
||||
from nucypher.crypto.kits import UmbralMessageKit as UmbralMessageKitClass
|
||||
|
||||
|
||||
|
|
|
@ -15,33 +15,10 @@
|
|||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import click
|
||||
from marshmallow import fields
|
||||
|
||||
from nucypher.characters.control.specifications.exceptions import InvalidInputData
|
||||
from nucypher.characters.control.specifications.fields.base import BaseField
|
||||
from nucypher.control.specifications.fields.base import Integer, PositiveInteger
|
||||
from nucypher.cli import types
|
||||
|
||||
|
||||
class String(BaseField, fields.String):
|
||||
pass
|
||||
|
||||
|
||||
class List(BaseField, fields.List):
|
||||
pass
|
||||
|
||||
|
||||
class Integer(BaseField, fields.Integer):
|
||||
click_type = click.INT
|
||||
|
||||
|
||||
class PositiveInteger(Integer):
|
||||
|
||||
def _validate(self, value):
|
||||
if not value > 0:
|
||||
raise InvalidInputData(f"{self.name} must be a positive integer.")
|
||||
|
||||
|
||||
class M(PositiveInteger):
|
||||
pass
|
||||
|
||||
|
@ -52,9 +29,3 @@ class N(PositiveInteger):
|
|||
|
||||
class Wei(Integer):
|
||||
click_type = types.WEI
|
||||
|
||||
|
||||
class click:
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
|
|
|
@ -19,8 +19,9 @@ from base64 import b64decode, b64encode
|
|||
|
||||
from marshmallow import fields
|
||||
|
||||
from nucypher.characters.control.specifications.exceptions import InvalidInputData, InvalidNativeDataTypes
|
||||
from nucypher.characters.control.specifications.fields.base import BaseField
|
||||
from nucypher.characters.control.specifications.exceptions import InvalidNativeDataTypes
|
||||
from nucypher.control.specifications.exceptions import InvalidInputData
|
||||
from nucypher.control.specifications.fields.base import BaseField
|
||||
from nucypher.crypto.umbral_adapter import Signature
|
||||
|
||||
|
||||
|
|
|
@ -19,11 +19,32 @@ from base64 import b64decode, b64encode
|
|||
|
||||
from marshmallow import fields
|
||||
|
||||
from nucypher.characters.control.specifications.exceptions import InvalidInputData, InvalidNativeDataTypes
|
||||
from nucypher.characters.control.specifications.fields.base import BaseField
|
||||
from nucypher.characters.control.specifications.exceptions import InvalidNativeDataTypes
|
||||
from nucypher.control.specifications.exceptions import InvalidInputData
|
||||
from nucypher.control.specifications.fields.base import BaseField
|
||||
|
||||
|
||||
class TreasureMap(BaseField, fields.Field):
|
||||
"""
|
||||
JSON Parameter representation of TreasureMap.
|
||||
|
||||
Requires that either federated or non-federated (blockchain) treasure maps are expected to function correcty. This
|
||||
information is indicated either:
|
||||
- At creation time of the field via constructor parameter 'federated_only' (takes precedence)
|
||||
OR
|
||||
- Via the parent Schema context it is running in. In this case, the parent Schema context dictionary should have a
|
||||
key-value entry in it with the IS_FEDERATED_CONTEXT_KEY class constant as the key, and a value of True/False.
|
||||
|
||||
If neither is provided, the TreasureMap is assumed to be a SignedTreasureMap. The federated/non-federated context
|
||||
of the TreasureMap only applies to deserialization and validation since the received value is in base64 encoded
|
||||
bytes.
|
||||
"""
|
||||
IS_FEDERATED_CONTEXT_KEY = 'federated'
|
||||
|
||||
def __init__(self, federated_only=None, *args, **kwargs):
|
||||
self.federated_only = federated_only
|
||||
BaseField.__init__(self, *args, **kwargs)
|
||||
fields.Field.__init__(self, *args, **kwargs)
|
||||
|
||||
def _serialize(self, value, attr, obj, **kwargs):
|
||||
return b64encode(bytes(value)).decode()
|
||||
|
@ -35,17 +56,23 @@ class TreasureMap(BaseField, fields.Field):
|
|||
raise InvalidInputData(f"Could not parse {self.name}: {e}")
|
||||
|
||||
def _validate(self, value):
|
||||
from nucypher.policy.maps import SignedTreasureMap
|
||||
from nucypher.policy.maps import TreasureMap as UnsignedTreasureMap
|
||||
|
||||
# determine context: federated or non-federated defined by field or schema
|
||||
is_federated_context = False # default to non-federated
|
||||
if self.federated_only is not None:
|
||||
# defined by field itself
|
||||
is_federated_context = self.federated_only
|
||||
else:
|
||||
# defined by schema
|
||||
if self.parent is not None and self.parent.context.get('federated') is not None:
|
||||
is_federated_context = self.context.get('federated')
|
||||
|
||||
try:
|
||||
# Unsigned TreasureMap (Federated)
|
||||
from nucypher.policy.maps import TreasureMap as UnsignedTreasureMap
|
||||
splitter = UnsignedTreasureMap.get_splitter(value)
|
||||
splitter(value)
|
||||
except InvalidNativeDataTypes:
|
||||
try:
|
||||
# Signed TreasureMap (Blockchain)
|
||||
from nucypher.policy.maps import SignedTreasureMap
|
||||
splitter = SignedTreasureMap.get_splitter(value)
|
||||
splitter(value)
|
||||
except InvalidNativeDataTypes as e:
|
||||
raise InvalidInputData(f"Could not parse {self.name}: {e}")
|
||||
return True
|
||||
splitter = SignedTreasureMap.get_splitter(value) if not is_federated_context else UnsignedTreasureMap.get_splitter(value)
|
||||
_ = splitter(value)
|
||||
return True
|
||||
except InvalidNativeDataTypes as e:
|
||||
# store exception
|
||||
raise InvalidInputData(f"Could not parse {self.name} (federated={is_federated_context}): {e}")
|
||||
|
|
|
@ -69,12 +69,12 @@ from nucypher.blockchain.eth.registry import BaseContractRegistry
|
|||
from nucypher.blockchain.eth.signers.software import Web3Signer
|
||||
from nucypher.characters.banners import ALICE_BANNER, BOB_BANNER, ENRICO_BANNER, URSULA_BANNER
|
||||
from nucypher.characters.base import Character, Learner
|
||||
from nucypher.characters.control.controllers import WebController
|
||||
from nucypher.characters.control.emitters import StdoutEmitter
|
||||
from nucypher.characters.control.interfaces import AliceInterface, BobInterface, EnricoInterface
|
||||
from nucypher.cli.processes import UrsulaCommandProtocol
|
||||
from nucypher.config.constants import END_OF_POLICIES_PROBATIONARY_PERIOD
|
||||
from nucypher.config.storages import ForgetfulNodeStorage, NodeStorage
|
||||
from nucypher.control.controllers import WebController
|
||||
from nucypher.control.emitters import StdoutEmitter
|
||||
from nucypher.crypto.constants import HRAC_LENGTH, WRIT_CHECKSUM_SIZE
|
||||
from nucypher.crypto.keypairs import HostingKeypair
|
||||
from nucypher.crypto.kits import UmbralMessageKit
|
||||
|
@ -99,6 +99,7 @@ from nucypher.crypto.umbral_adapter import (
|
|||
from nucypher.crypto.utils import keccak_digest, encrypt_and_sign
|
||||
from nucypher.datastore.datastore import DatastoreTransactionError, RecordNotFound
|
||||
from nucypher.datastore.queries import find_expired_policies, find_expired_treasure_maps
|
||||
from nucypher.network import treasuremap
|
||||
from nucypher.network.exceptions import NodeSeemsToBeDown
|
||||
from nucypher.network.middleware import RestMiddleware
|
||||
from nucypher.network.nodes import NodeSprout, TEACHER_NODES, Teacher
|
||||
|
@ -642,7 +643,7 @@ class Bob(Character):
|
|||
if not self.known_nodes:
|
||||
raise self.NotEnoughTeachers("Can't retrieve without knowing about any nodes at all. Pass a teacher or seed node.")
|
||||
|
||||
treasure_map = self.get_treasure_map_from_known_ursulas(self.network_middleware, map_identifier)
|
||||
treasure_map = self.get_treasure_map_from_known_ursulas(map_identifier)
|
||||
|
||||
self._try_orient(treasure_map, publisher_verifying_key)
|
||||
self.treasure_maps[map_identifier] = treasure_map # TODO: make a part of _try_orient()?
|
||||
|
@ -666,47 +667,16 @@ class Bob(Character):
|
|||
|
||||
return map_id
|
||||
|
||||
def get_treasure_map_from_known_ursulas(self, network_middleware, map_identifier, timeout=3):
|
||||
def get_treasure_map_from_known_ursulas(self, map_identifier: str, timeout=3):
|
||||
"""
|
||||
Iterate through the nodes we know, asking for the TreasureMap.
|
||||
Return the first one who has it.
|
||||
"""
|
||||
if self.federated_only:
|
||||
from nucypher.policy.maps import TreasureMap as _MapClass
|
||||
else:
|
||||
from nucypher.policy.maps import SignedTreasureMap as _MapClass
|
||||
|
||||
start = maya.now()
|
||||
|
||||
# Spend no more than half the timeout finding the nodes. 8 nodes is arbitrary. Come at me.
|
||||
self.block_until_number_of_known_nodes_is(8, timeout=timeout/2, learn_on_this_thread=True)
|
||||
while True:
|
||||
nodes_with_map = self.matching_nodes_among(self.known_nodes)
|
||||
random.shuffle(nodes_with_map)
|
||||
|
||||
for node in nodes_with_map:
|
||||
try:
|
||||
response = network_middleware.get_treasure_map_from_node(node, map_identifier)
|
||||
except (*NodeSeemsToBeDown, self.NotEnoughNodes):
|
||||
continue
|
||||
except network_middleware.NotFound:
|
||||
self.log.info(f"Node {node} claimed not to have TreasureMap {map_identifier}")
|
||||
continue
|
||||
|
||||
if response.status_code == 200 and response.content:
|
||||
try:
|
||||
treasure_map = _MapClass.from_bytes(response.content)
|
||||
return treasure_map
|
||||
except InvalidSignature:
|
||||
# TODO: What if a node gives a bunk TreasureMap? NRN
|
||||
raise
|
||||
else:
|
||||
continue # TODO: Actually, handle error case here. NRN
|
||||
else:
|
||||
self.learn_from_teacher_node()
|
||||
|
||||
if (start - maya.now()).seconds > timeout:
|
||||
raise _MapClass.NowhereToBeFound(f"Asked {len(self.known_nodes)} nodes, but none had map {map_identifier} ")
|
||||
bob_encrypting_key = self.public_keys(DecryptingPower)
|
||||
return treasuremap.get_treasure_map_from_known_ursulas(learner=self,
|
||||
map_identifier=map_identifier,
|
||||
bob_encrypting_key=bob_encrypting_key,
|
||||
timeout=timeout)
|
||||
|
||||
def work_orders_for_capsules(self,
|
||||
*capsules,
|
||||
|
@ -1051,41 +1021,12 @@ class Bob(Character):
|
|||
def matching_nodes_among(self,
|
||||
nodes: FleetSensor,
|
||||
no_less_than=7): # Somewhat arbitrary floor here.
|
||||
# Look for nodes whose checksum address has the second character of Bob's encrypting key in the first
|
||||
# few characters.
|
||||
# Think of it as a cheap knockoff hamming distance.
|
||||
# The good news is that Bob can construct the list easily.
|
||||
# And - famous last words incoming - there's no cognizable attack surface.
|
||||
# Sure, Bob can mine encrypting keypairs until he gets the set of target Ursulas on which Alice can
|
||||
# store a TreasureMap. And then... ???... profit?
|
||||
|
||||
# Sanity check - do we even have enough nodes?
|
||||
if len(nodes) < no_less_than:
|
||||
raise ValueError(f"Can't select {no_less_than} from {len(nodes)} (Fleet state: {nodes.FleetState})")
|
||||
|
||||
search_boundary = 2
|
||||
target_nodes = []
|
||||
target_hex_match = bytes(self.public_keys(DecryptingPower)).hex()[1]
|
||||
while len(target_nodes) < no_less_than:
|
||||
target_nodes = []
|
||||
search_boundary += 2
|
||||
|
||||
if search_boundary > 42: # We've searched the entire string and can't match any. TODO: Portable learning is a nice idea here.
|
||||
# Not enough matching nodes. Fine, we'll just publish to the first few.
|
||||
try:
|
||||
# TODO: This is almost certainly happening in a test. If it does happen in production, it's a bit of a problem. Need to fix #2124 to mitigate.
|
||||
target_nodes = list(nodes.values())[0:6]
|
||||
return target_nodes
|
||||
except IndexError:
|
||||
raise self.NotEnoughNodes("There aren't enough nodes on the network to enact this policy. Unless this is day one of the network and nodes are still getting spun up, something is bonkers.")
|
||||
|
||||
# TODO: 1995 all throughout here (we might not (need to) know the checksum address yet; canonical will do.)
|
||||
# This might be a performance issue above a few thousand nodes.
|
||||
target_nodes = [node for node in nodes if target_hex_match in node.checksum_address[2:search_boundary]]
|
||||
return target_nodes
|
||||
bob_encrypting_key = self.public_keys(DecryptingPower)
|
||||
return treasuremap.find_matching_nodes(known_nodes=nodes,
|
||||
bob_encrypting_key=bob_encrypting_key,
|
||||
no_less_than=no_less_than)
|
||||
|
||||
def make_web_controller(drone_bob, crash_on_error: bool = False):
|
||||
|
||||
app_name = bytes(drone_bob.stamp).hex()[:6]
|
||||
controller = WebController(app_name=app_name,
|
||||
crash_on_error=crash_on_error,
|
||||
|
|
|
@ -23,7 +23,7 @@ from constant_sorrow.constants import NO_PASSWORD
|
|||
|
||||
from nucypher.blockchain.eth.decorators import validate_checksum_address
|
||||
from nucypher.blockchain.eth.signers.software import ClefSigner
|
||||
from nucypher.characters.control.emitters import StdoutEmitter
|
||||
from nucypher.control.emitters import StdoutEmitter
|
||||
from nucypher.cli.literature import (
|
||||
COLLECT_ETH_PASSWORD,
|
||||
COLLECT_NUCYPHER_PASSWORD,
|
||||
|
|
|
@ -24,7 +24,7 @@ from datetime import timedelta
|
|||
from typing import Tuple
|
||||
from web3.main import Web3
|
||||
|
||||
from nucypher.characters.control.emitters import StdoutEmitter
|
||||
from nucypher.control.emitters import StdoutEmitter
|
||||
from nucypher.characters.lawful import Bob, Alice
|
||||
from nucypher.cli.painting.help import enforce_probationary_period, paint_probationary_period_disclaimer
|
||||
from nucypher.cli.painting.policies import paint_single_card
|
||||
|
|
|
@ -21,7 +21,7 @@ from typing import Optional, Type, List
|
|||
|
||||
import click
|
||||
|
||||
from nucypher.characters.control.emitters import StdoutEmitter
|
||||
from nucypher.control.emitters import StdoutEmitter
|
||||
from nucypher.characters.lawful import Ursula
|
||||
from nucypher.cli.actions.confirm import confirm_destroy_configuration
|
||||
from nucypher.cli.literature import (
|
||||
|
|
|
@ -30,7 +30,7 @@ from nucypher.blockchain.eth.interfaces import BlockchainDeployerInterface, Vers
|
|||
from nucypher.blockchain.eth.registry import LocalContractRegistry
|
||||
from nucypher.blockchain.eth.token import NU
|
||||
from nucypher.blockchain.eth.utils import calculate_period_duration
|
||||
from nucypher.characters.control.emitters import StdoutEmitter
|
||||
from nucypher.control.emitters import StdoutEmitter
|
||||
from nucypher.cli.literature import (
|
||||
ABORT_DEPLOYMENT,
|
||||
CHARACTER_DESTRUCTION,
|
||||
|
|
|
@ -31,7 +31,7 @@ from nucypher.blockchain.eth.networks import NetworksInventory
|
|||
from nucypher.blockchain.eth.registry import InMemoryContractRegistry, BaseContractRegistry
|
||||
from nucypher.blockchain.eth.signers.base import Signer
|
||||
from nucypher.blockchain.eth.token import NU, Stake
|
||||
from nucypher.characters.control.emitters import StdoutEmitter
|
||||
from nucypher.control.emitters import StdoutEmitter
|
||||
from nucypher.cli.actions.configure import get_config_filepaths
|
||||
from nucypher.cli.literature import (
|
||||
GENERIC_SELECT_ACCOUNT,
|
||||
|
|
|
@ -19,7 +19,7 @@ from collections import namedtuple
|
|||
import click
|
||||
import maya
|
||||
|
||||
from nucypher.characters.control.emitters import StdoutEmitter
|
||||
from nucypher.control.emitters import StdoutEmitter
|
||||
from nucypher.characters.lawful import Alice
|
||||
|
||||
Precondition = namedtuple('Precondition', 'options condition')
|
||||
|
|
|
@ -18,7 +18,7 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
|||
|
||||
import click
|
||||
|
||||
from nucypher.characters.control.emitters import StdoutEmitter
|
||||
from nucypher.control.emitters import StdoutEmitter
|
||||
from nucypher.characters.control.interfaces import AliceInterface
|
||||
from nucypher.cli.actions.auth import get_nucypher_password
|
||||
from nucypher.cli.actions.collect import collect_bob_public_keys, collect_policy_parameters
|
||||
|
@ -362,7 +362,7 @@ def run(general_config, character_options, config_file, controller_port, dry_run
|
|||
controller = ALICE.make_web_controller(crash_on_error=general_config.debug)
|
||||
ALICE.log.info('Starting HTTP Character Web Controller')
|
||||
emitter.message(f'Running HTTP Alice Controller at http://localhost:{controller_port}')
|
||||
return controller.start(http_port=controller_port, dry_run=dry_run)
|
||||
return controller.start(port=controller_port, dry_run=dry_run)
|
||||
|
||||
# Handle Crash
|
||||
except Exception as e:
|
||||
|
|
|
@ -18,7 +18,7 @@ from base64 import b64decode
|
|||
|
||||
import click
|
||||
|
||||
from nucypher.characters.control.emitters import StdoutEmitter
|
||||
from nucypher.control.emitters import StdoutEmitter
|
||||
from nucypher.characters.control.interfaces import BobInterface
|
||||
from nucypher.characters.lawful import Alice
|
||||
from nucypher.cli.actions.auth import get_nucypher_password
|
||||
|
@ -265,7 +265,7 @@ def run(general_config, character_options, config_file, controller_port, dry_run
|
|||
# Start Controller
|
||||
controller = BOB.make_web_controller(crash_on_error=general_config.debug)
|
||||
BOB.log.info('Starting HTTP Character Web Controller')
|
||||
return controller.start(http_port=controller_port, dry_run=dry_run)
|
||||
return controller.start(port=controller_port, dry_run=dry_run)
|
||||
|
||||
|
||||
@bob.command()
|
||||
|
|
|
@ -19,7 +19,7 @@ import click
|
|||
import os
|
||||
import shutil
|
||||
|
||||
from nucypher.characters.control.emitters import StdoutEmitter
|
||||
from nucypher.control.emitters import StdoutEmitter
|
||||
from nucypher.cli.actions.select import select_card
|
||||
from nucypher.cli.options import option_force
|
||||
from nucypher.cli.painting.policies import paint_single_card, paint_cards
|
||||
|
|
|
@ -37,7 +37,7 @@ from nucypher.blockchain.eth.registry import (
|
|||
from nucypher.blockchain.eth.signers.base import Signer
|
||||
from nucypher.blockchain.eth.signers.software import ClefSigner
|
||||
from nucypher.blockchain.eth.sol.__conf__ import SOLIDITY_COMPILER_VERSION
|
||||
from nucypher.characters.control.emitters import StdoutEmitter
|
||||
from nucypher.control.emitters import StdoutEmitter
|
||||
from nucypher.cli.actions.auth import get_client_password
|
||||
from nucypher.cli.actions.confirm import confirm_deployment, verify_upgrade_details
|
||||
from nucypher.cli.actions.select import select_client_account
|
||||
|
|
|
@ -53,7 +53,7 @@ def run(general_config, policy_encrypting_key, dry_run, http_port):
|
|||
|
||||
ENRICO.log.info('Starting HTTP Character Web Controller')
|
||||
controller = ENRICO.make_web_controller()
|
||||
return controller.start(http_port=http_port, dry_run=dry_run)
|
||||
return controller.start(port=http_port, dry_run=dry_run)
|
||||
|
||||
|
||||
@enrico.command()
|
||||
|
|
|
@ -19,7 +19,7 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
|||
import click
|
||||
import os
|
||||
|
||||
from nucypher.characters.control.emitters import StdoutEmitter
|
||||
from nucypher.control.emitters import StdoutEmitter
|
||||
from nucypher.cli.actions.auth import (
|
||||
get_client_password,
|
||||
get_nucypher_password,
|
||||
|
|
|
@ -0,0 +1,149 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
|
||||
from nucypher.blockchain.eth.networks import NetworksInventory
|
||||
from nucypher.characters.lawful import Ursula
|
||||
from nucypher.cli.config import group_general_config
|
||||
from nucypher.cli.literature import BOTH_TLS_KEY_AND_CERTIFICATION_MUST_BE_PROVIDED, PORTER_RUN_MESSAGE, \
|
||||
BASIC_AUTH_REQUIRES_HTTPS
|
||||
from nucypher.cli.options import (
|
||||
option_network,
|
||||
option_provider_uri,
|
||||
option_federated_only,
|
||||
option_teacher_uri,
|
||||
option_registry_filepath,
|
||||
option_min_stake
|
||||
)
|
||||
from nucypher.cli.types import NETWORK_PORT
|
||||
from nucypher.cli.utils import setup_emitter, get_registry
|
||||
from nucypher.config.constants import TEMPORARY_DOMAIN
|
||||
from nucypher.utilities.porter.control.interfaces import PorterInterface
|
||||
from nucypher.utilities.porter.porter import Porter
|
||||
|
||||
|
||||
@click.group()
|
||||
def porter():
|
||||
"""
|
||||
Porter management commands. Porter is a web-service that is the conduit between applications and the
|
||||
nucypher network, that performs actions on behalf of Alice and Bob.
|
||||
"""
|
||||
|
||||
|
||||
@porter.command()
|
||||
@group_general_config
|
||||
@option_network(default=NetworksInventory.DEFAULT, validate=True, required=False)
|
||||
@option_provider_uri(required=False)
|
||||
@option_federated_only
|
||||
@option_teacher_uri
|
||||
@option_registry_filepath
|
||||
@option_min_stake
|
||||
@click.option('--http-port', help="Porter HTTP/HTTPS port for JSON endpoint", type=NETWORK_PORT, default=Porter.DEFAULT_PORT)
|
||||
@click.option('--tls-certificate-filepath', help="Pre-signed TLS certificate filepath", type=click.Path(dir_okay=False, exists=True, path_type=Path))
|
||||
@click.option('--tls-key-filepath', help="TLS private key filepath", type=click.Path(dir_okay=False, exists=True, path_type=Path))
|
||||
@click.option('--basic-auth-filepath', help="htpasswd filepath for basic authentication", type=click.Path(dir_okay=False, exists=True, resolve_path=True, path_type=Path))
|
||||
@click.option('--dry-run', '-x', help="Execute normally without actually starting Porter", is_flag=True)
|
||||
@click.option('--eager', help="Start learning and scraping the network before starting up other services", is_flag=True, default=True)
|
||||
def run(general_config,
|
||||
network,
|
||||
provider_uri,
|
||||
federated_only,
|
||||
teacher_uri,
|
||||
registry_filepath,
|
||||
min_stake,
|
||||
http_port,
|
||||
tls_certificate_filepath,
|
||||
tls_key_filepath,
|
||||
basic_auth_filepath,
|
||||
dry_run,
|
||||
eager):
|
||||
"""Start Porter's Web controller."""
|
||||
emitter = setup_emitter(general_config, banner=Porter.BANNER)
|
||||
|
||||
# HTTP/HTTPS
|
||||
if bool(tls_key_filepath) ^ bool(tls_certificate_filepath):
|
||||
raise click.BadOptionUsage(option_name='--tls-key-filepath, --tls-certificate-filepath',
|
||||
message=BOTH_TLS_KEY_AND_CERTIFICATION_MUST_BE_PROVIDED)
|
||||
|
||||
is_https = (tls_key_filepath and tls_certificate_filepath)
|
||||
|
||||
# check authentication
|
||||
if basic_auth_filepath and not is_https:
|
||||
raise click.BadOptionUsage(option_name='--basic-auth-filepath',
|
||||
message=BASIC_AUTH_REQUIRES_HTTPS)
|
||||
|
||||
if federated_only:
|
||||
if not teacher_uri:
|
||||
raise click.BadOptionUsage(option_name='--teacher',
|
||||
message="--teacher is required for federated porter.")
|
||||
|
||||
teacher = Ursula.from_teacher_uri(teacher_uri=teacher_uri,
|
||||
federated_only=True,
|
||||
min_stake=min_stake) # min stake is irrelevant for federated
|
||||
PORTER = Porter(domain=TEMPORARY_DOMAIN,
|
||||
start_learning_now=eager,
|
||||
known_nodes={teacher},
|
||||
verify_node_bonding=False,
|
||||
federated_only=True)
|
||||
else:
|
||||
# decentralized/blockchain
|
||||
if not provider_uri:
|
||||
raise click.BadOptionUsage(option_name='--provider',
|
||||
message="--provider is required for decentralized porter.")
|
||||
if not network:
|
||||
# should never happen - network defaults to 'mainnet' if not specified
|
||||
raise click.BadOptionUsage(option_name='--network',
|
||||
message="--network is required for decentralized porter.")
|
||||
|
||||
registry = get_registry(network=network, registry_filepath=registry_filepath)
|
||||
teacher = None
|
||||
if teacher_uri:
|
||||
teacher = Ursula.from_teacher_uri(teacher_uri=teacher_uri,
|
||||
federated_only=False, # always False
|
||||
min_stake=min_stake,
|
||||
registry=registry)
|
||||
|
||||
PORTER = Porter(domain=network,
|
||||
known_nodes={teacher} if teacher else None,
|
||||
registry=registry,
|
||||
start_learning_now=eager,
|
||||
provider_uri=provider_uri)
|
||||
|
||||
# RPC
|
||||
if general_config.json_ipc:
|
||||
rpc_controller = PORTER.make_rpc_controller()
|
||||
_transport = rpc_controller.make_control_transport()
|
||||
rpc_controller.start()
|
||||
return
|
||||
|
||||
emitter.message(f"Network: {PORTER.domain.capitalize()}", color='green')
|
||||
if not federated_only:
|
||||
emitter.message(f"Provider: {provider_uri}", color='green')
|
||||
|
||||
if basic_auth_filepath:
|
||||
emitter.message("Basic Authentication enabled", color='green')
|
||||
|
||||
controller = PORTER.make_web_controller(htpasswd_filepath=basic_auth_filepath, crash_on_error=False)
|
||||
http_scheme = "https" if is_https else "http"
|
||||
message = PORTER_RUN_MESSAGE.format(http_scheme=http_scheme, http_port=http_port)
|
||||
emitter.message(message, color='green', bold=True)
|
||||
return controller.start(port=http_port,
|
||||
tls_key_filepath=tls_key_filepath,
|
||||
tls_certificate_filepath=tls_certificate_filepath,
|
||||
dry_run=dry_run)
|
|
@ -19,7 +19,7 @@
|
|||
import click
|
||||
import os
|
||||
|
||||
from nucypher.characters.control.emitters import JSONRPCStdoutEmitter, StdoutEmitter
|
||||
from nucypher.control.emitters import StdoutEmitter, JSONRPCStdoutEmitter
|
||||
from nucypher.cli.utils import get_env_bool
|
||||
from nucypher.cli.options import group_options
|
||||
from nucypher.config.constants import NUCYPHER_SENTRY_ENDPOINT
|
||||
|
|
|
@ -714,3 +714,14 @@ CONFIRMING_ACTIVITY_NOW = "Making a commitment to period {committed_period}"
|
|||
SUCCESSFUL_CONFIRM_ACTIVITY = '\nCommitment was made to period #{committed_period} (starting at {date})'
|
||||
|
||||
SUCCESSFUL_MANUALLY_SAVE_METADATA = "Successfully saved node metadata to {metadata_path}."
|
||||
|
||||
|
||||
#
|
||||
# Porter
|
||||
#
|
||||
|
||||
PORTER_RUN_MESSAGE = "Running Porter Web Controller at {http_scheme}://127.0.0.1:{http_port}"
|
||||
|
||||
BOTH_TLS_KEY_AND_CERTIFICATION_MUST_BE_PROVIDED = "Both --tls-key-filepath and --tls-certificate-filepath must be provided to launch porter with TLS; only one specified"
|
||||
|
||||
BASIC_AUTH_REQUIRES_HTTPS = "Basic authentication can only be used with HTTPS. --tls-key-filepath and --tls-certificate-filepath must also be provided"
|
||||
|
|
|
@ -29,7 +29,8 @@ from nucypher.cli.commands import (
|
|||
ursula,
|
||||
worklock,
|
||||
cloudworkers,
|
||||
contacts
|
||||
contacts,
|
||||
porter
|
||||
)
|
||||
from nucypher.cli.painting.help import echo_version, echo_config_root_path, echo_logging_root_path
|
||||
|
||||
|
@ -86,6 +87,7 @@ ENTRY_POINTS = (
|
|||
felix.felix, # Faucet
|
||||
cloudworkers.cloudworkers, # Remote Worker node management
|
||||
contacts.contacts, # Character "card" management
|
||||
porter.porter
|
||||
)
|
||||
|
||||
for entry_point in ENTRY_POINTS:
|
||||
|
|
|
@ -84,7 +84,7 @@ def option_contract_name(required: bool = False):
|
|||
def option_controller_port(default=None):
|
||||
return click.option(
|
||||
'--controller-port',
|
||||
help="The host port to run Alice HTTP services on",
|
||||
help="The host port to run HTTP services on",
|
||||
type=NETWORK_PORT,
|
||||
default=default)
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ from nucypher.blockchain.eth.constants import STAKING_ESCROW_CONTRACT_NAME, NULL
|
|||
from nucypher.blockchain.eth.interfaces import BlockchainInterfaceFactory
|
||||
from nucypher.blockchain.eth.token import NU, Stake
|
||||
from nucypher.blockchain.eth.utils import datetime_at_period, estimate_block_number_for_period, prettify_eth_amount
|
||||
from nucypher.characters.control.emitters import StdoutEmitter
|
||||
from nucypher.control.emitters import StdoutEmitter
|
||||
from nucypher.cli.literature import (
|
||||
POST_STAKING_ADVICE,
|
||||
TOKEN_REWARD_CURRENT,
|
||||
|
|
|
@ -39,7 +39,7 @@ from nucypher.blockchain.eth.registry import (
|
|||
LocalContractRegistry
|
||||
)
|
||||
from nucypher.characters.base import Character
|
||||
from nucypher.characters.control.emitters import StdoutEmitter
|
||||
from nucypher.control.emitters import StdoutEmitter
|
||||
from nucypher.cli.actions.auth import (
|
||||
get_nucypher_password,
|
||||
unlock_nucypher_keystore,
|
||||
|
|
|
@ -34,6 +34,9 @@ NUCYPHER_ENVVAR_ALICE_ETH_PASSWORD = "NUCYPHER_ALICE_ETH_PASSWORD"
|
|||
NUCYPHER_ENVVAR_BOB_ETH_PASSWORD = "NUCYPHER_BOB_ETH_PASSWORD"
|
||||
NUCYPHER_ENVVAR_PROVIDER_URI = "NUCYPHER_PROVIDER_URI"
|
||||
|
||||
NUCYPHER_ENVVAR_STAKERS_PAGINATION_SIZE_LIGHT_NODE = "NUCYPHER_STAKERS_PAGINATION_SIZE_LIGHT_NODE"
|
||||
NUCYPHER_ENVVAR_STAKERS_PAGINATION_SIZE = "NUCYPHER_STAKERS_PAGINATION_SIZE"
|
||||
|
||||
# Base Filepaths
|
||||
NUCYPHER_PACKAGE = Path(nucypher.__file__).parent.resolve()
|
||||
BASE_DIR = NUCYPHER_PACKAGE.parent.resolve()
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
|
@ -0,0 +1,368 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
|
||||
import inspect
|
||||
import json
|
||||
from abc import ABC, abstractmethod
|
||||
from json import JSONDecodeError
|
||||
from typing import Optional
|
||||
|
||||
import maya
|
||||
from flask import Flask, Response
|
||||
from hendrix.deploy.base import HendrixDeploy
|
||||
from hendrix.deploy.tls import HendrixDeployTLS
|
||||
from twisted.internet import reactor, stdio
|
||||
|
||||
from nucypher.cli.processes import JSONRPCLineReceiver
|
||||
from nucypher.config.constants import MAX_UPLOAD_CONTENT_LENGTH
|
||||
from nucypher.control.emitters import StdoutEmitter, JSONRPCStdoutEmitter, WebEmitter
|
||||
from nucypher.control.interfaces import ControlInterface
|
||||
from nucypher.control.specifications.exceptions import SpecificationError
|
||||
from nucypher.exceptions import DevelopmentInstallationRequired
|
||||
from nucypher.network.resources import get_static_resources
|
||||
from nucypher.utilities.logging import Logger, GlobalLoggerSettings
|
||||
|
||||
|
||||
class ControllerBase(ABC):
|
||||
"""
|
||||
A transactional interface for a human to interact with.
|
||||
"""
|
||||
_emitter_class = NotImplemented
|
||||
|
||||
def __init__(self, interface: ControlInterface):
|
||||
# Control Emitter
|
||||
self.emitter = self._emitter_class()
|
||||
|
||||
# Interface
|
||||
self.interface = interface
|
||||
|
||||
def _perform_action(self, action: str, request: Optional[dict] = None) -> dict:
|
||||
"""
|
||||
This method is where input validation and method invocation
|
||||
happens for all interface actions.
|
||||
"""
|
||||
request = request or {} # for requests with no input params request can be ''
|
||||
method = getattr(self.interface, action, None)
|
||||
serializer = method._schema
|
||||
params = serializer.load(request) # input validation will occur here.
|
||||
response = method(**params) # < ---- INLET
|
||||
|
||||
response_data = serializer.dump(response)
|
||||
return response_data
|
||||
|
||||
|
||||
class InterfaceControlServer(ControllerBase):
|
||||
_emitter_class = StdoutEmitter
|
||||
_crash_on_error_default = False
|
||||
|
||||
def __init__(self,
|
||||
app_name: str,
|
||||
crash_on_error: bool = _crash_on_error_default,
|
||||
*args,
|
||||
**kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.app_name = app_name
|
||||
|
||||
# Configuration
|
||||
self.crash_on_error = crash_on_error
|
||||
|
||||
def set_method(name):
|
||||
def wrapper(request=None, **kwargs):
|
||||
request = request or kwargs
|
||||
return self.handle_request(name, request)
|
||||
|
||||
setattr(self, name, wrapper)
|
||||
|
||||
for method_name in self._get_interfaces().keys():
|
||||
set_method(method_name)
|
||||
set_method(method_name)
|
||||
|
||||
self.log = Logger(app_name)
|
||||
|
||||
def _get_interfaces(self):
|
||||
return {
|
||||
name: method for name, method in
|
||||
inspect.getmembers(
|
||||
self.interface,
|
||||
predicate=inspect.ismethod)
|
||||
if hasattr(method, '_schema')
|
||||
}
|
||||
|
||||
@abstractmethod
|
||||
def make_control_transport(self):
|
||||
return NotImplemented
|
||||
|
||||
@abstractmethod
|
||||
def handle_request(self, method_name, control_request):
|
||||
return NotImplemented
|
||||
|
||||
@abstractmethod
|
||||
def test_client(self):
|
||||
return NotImplemented
|
||||
|
||||
|
||||
class CLIController(InterfaceControlServer):
|
||||
|
||||
_emitter_class = StdoutEmitter
|
||||
|
||||
def make_control_transport(self):
|
||||
return
|
||||
|
||||
def test_client(self):
|
||||
return
|
||||
|
||||
def handle_request(self, method_name, request) -> dict:
|
||||
response = self._perform_action(action=method_name, request=request)
|
||||
if GlobalLoggerSettings._json_ipc:
|
||||
# support for --json-ipc flag, for JSON *responses* from CLI commands-as-requests.
|
||||
start = maya.now()
|
||||
self.emitter.ipc(response=response, request_id=start.epoch, duration=maya.now() - start)
|
||||
else:
|
||||
self.emitter.pretty(response)
|
||||
return response
|
||||
|
||||
|
||||
class JSONRPCController(InterfaceControlServer):
|
||||
|
||||
_emitter_class = JSONRPCStdoutEmitter
|
||||
|
||||
def start(self):
|
||||
_transport = self.make_control_transport()
|
||||
reactor.run() # < ------ Blocking Call (Reactor)
|
||||
|
||||
def test_client(self):
|
||||
try:
|
||||
from tests.utils.controllers import JSONRPCTestClient
|
||||
except ImportError:
|
||||
raise DevelopmentInstallationRequired(importable_name='tests.utils.controllers.JSONRPCTestClient')
|
||||
|
||||
test_client = JSONRPCTestClient(rpc_controller=self)
|
||||
return test_client
|
||||
|
||||
def make_control_transport(self):
|
||||
transport = stdio.StandardIO(JSONRPCLineReceiver(rpc_controller=self))
|
||||
return transport
|
||||
|
||||
def handle_procedure_call(self, control_request) -> int:
|
||||
|
||||
# Validate request and read request metadata
|
||||
jsonrpc2 = control_request['jsonrpc']
|
||||
if jsonrpc2 != '2.0':
|
||||
raise self.emitter.InvalidRequest
|
||||
|
||||
request_id = control_request['id']
|
||||
|
||||
# Read the interface's signature metadata
|
||||
method_name = control_request['method']
|
||||
method_params = control_request.get('params', dict()) # optional
|
||||
if method_name not in self._get_interfaces():
|
||||
raise self.emitter.MethodNotFound(f'No method called {method_name}')
|
||||
|
||||
return self.call_interface(method_name=method_name,
|
||||
request=method_params,
|
||||
request_id=request_id)
|
||||
|
||||
def handle_message(self, message: dict, *args, **kwargs) -> int:
|
||||
"""Handle single JSON RPC message"""
|
||||
|
||||
try:
|
||||
_request_id = message['id']
|
||||
|
||||
except KeyError: # Notification
|
||||
raise self.emitter.InvalidRequest('No request id')
|
||||
except TypeError:
|
||||
raise self.emitter.InvalidRequest(f'Request object not valid: {type(message)}')
|
||||
else: # RPC
|
||||
return self.handle_procedure_call(control_request=message)
|
||||
|
||||
def handle_batch(self, control_requests: list) -> int:
|
||||
|
||||
if not control_requests:
|
||||
e = self.emitter.InvalidRequest()
|
||||
return self.emitter.error(e)
|
||||
|
||||
batch_size = 0
|
||||
for request in control_requests: # TODO: parallelism
|
||||
response_size = self.handle_message(message=request)
|
||||
batch_size += response_size
|
||||
return batch_size
|
||||
|
||||
def handle_request(self, control_request: bytes, *args, **kwargs) -> int:
|
||||
|
||||
try:
|
||||
control_request = json.loads(control_request)
|
||||
except JSONDecodeError:
|
||||
e = self.emitter.ParseError()
|
||||
return self.emitter.error(e)
|
||||
|
||||
# Handle batch of messages
|
||||
if isinstance(control_request, list):
|
||||
return self.handle_batch(control_requests=control_request)
|
||||
|
||||
# Handle single message
|
||||
try:
|
||||
return self.handle_message(message=control_request, *args, **kwargs)
|
||||
|
||||
except self.emitter.JSONRPCError as e:
|
||||
return self.emitter.error(e)
|
||||
|
||||
except Exception as e:
|
||||
if self.crash_on_error:
|
||||
raise
|
||||
return self.emitter.error(e)
|
||||
|
||||
def call_interface(self, method_name, request, request_id: int = None):
|
||||
received = maya.now()
|
||||
internal_request_id = received.epoch
|
||||
if request_id is None:
|
||||
request_id = internal_request_id
|
||||
response = self._perform_action(action=method_name, request=request)
|
||||
responded = maya.now()
|
||||
duration = responded - received
|
||||
return self.emitter.ipc(response=response, request_id=request_id, duration=duration)
|
||||
|
||||
|
||||
class WebController(InterfaceControlServer):
|
||||
"""
|
||||
A wrapper around a JSON control interface that
|
||||
handles web requests to exert control over an implemented interface.
|
||||
"""
|
||||
|
||||
_emitter_class = WebEmitter
|
||||
_crash_on_error_default = False
|
||||
|
||||
_captured_status_codes = {200: 'OK',
|
||||
400: 'BAD REQUEST',
|
||||
500: 'INTERNAL SERVER ERROR'}
|
||||
|
||||
def test_client(self):
|
||||
test_client = self._transport.test_client()
|
||||
|
||||
# ease your mind
|
||||
self._transport.config.update(TESTING=self.crash_on_error, PROPOGATE_EXCEPTION=self.crash_on_error)
|
||||
|
||||
return test_client
|
||||
|
||||
def make_control_transport(self):
|
||||
self._transport = Flask(self.app_name)
|
||||
self._transport.config['MAX_CONTENT_LENGTH'] = MAX_UPLOAD_CONTENT_LENGTH
|
||||
|
||||
# Return FlaskApp decorator
|
||||
return self._transport
|
||||
|
||||
def start(self,
|
||||
port: int,
|
||||
tls_key_filepath: str = None,
|
||||
tls_certificate_filepath: str = None,
|
||||
dry_run: bool = False):
|
||||
if dry_run:
|
||||
return
|
||||
|
||||
if tls_key_filepath and tls_certificate_filepath:
|
||||
self.log.info("Starting HTTPS Control...")
|
||||
# HTTPS endpoint
|
||||
hx_deployer = HendrixDeployTLS(action="start",
|
||||
key=tls_key_filepath,
|
||||
cert=tls_certificate_filepath,
|
||||
options={
|
||||
"wsgi": self._transport,
|
||||
"https_port": port,
|
||||
"resources": get_static_resources()
|
||||
})
|
||||
else:
|
||||
# HTTP endpoint
|
||||
# TODO #845: Make non-blocking web control startup
|
||||
self.log.info("Starting HTTP Control...")
|
||||
hx_deployer = HendrixDeploy(action="start",
|
||||
options={
|
||||
"wsgi": self._transport,
|
||||
"http_port": port,
|
||||
"resources": get_static_resources()
|
||||
})
|
||||
|
||||
hx_deployer.run() # <--- Blocking Call to Reactor
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
return self.handle_request(*args, **kwargs)
|
||||
|
||||
def handle_request(self, method_name, control_request, *args, **kwargs) -> Response:
|
||||
|
||||
_400_exceptions = (SpecificationError,
|
||||
TypeError,
|
||||
JSONDecodeError,
|
||||
self.emitter.MethodNotFound)
|
||||
|
||||
try:
|
||||
request_data = control_request.data
|
||||
request_body = json.loads(request_data) if request_data else dict()
|
||||
|
||||
# handle query string parameters
|
||||
if hasattr(control_request, 'args'):
|
||||
request_body.update(control_request.args)
|
||||
|
||||
request_body.update(kwargs)
|
||||
|
||||
if method_name not in self._get_interfaces():
|
||||
raise self.emitter.MethodNotFound(f'No method called {method_name}')
|
||||
|
||||
response = self._perform_action(action=method_name, request=request_body)
|
||||
|
||||
#
|
||||
# Client Errors
|
||||
#
|
||||
except _400_exceptions as e:
|
||||
__exception_code = 400
|
||||
return self.emitter.exception(
|
||||
e=e,
|
||||
log_level='debug',
|
||||
response_code=__exception_code,
|
||||
error_message=WebController._captured_status_codes[__exception_code])
|
||||
|
||||
#
|
||||
# Server Errors
|
||||
#
|
||||
except SpecificationError as e:
|
||||
__exception_code = 500
|
||||
if self.crash_on_error:
|
||||
raise
|
||||
return self.emitter.exception(
|
||||
e=e,
|
||||
log_level='critical',
|
||||
response_code=__exception_code,
|
||||
error_message=WebController._captured_status_codes[__exception_code])
|
||||
|
||||
#
|
||||
# Unhandled Server Errors
|
||||
#
|
||||
except Exception as e:
|
||||
__exception_code = 500
|
||||
if self.crash_on_error:
|
||||
raise
|
||||
return self.emitter.exception(
|
||||
e=e,
|
||||
log_level='debug',
|
||||
response_code=__exception_code,
|
||||
error_message=WebController._captured_status_codes[__exception_code])
|
||||
|
||||
#
|
||||
# Send to WebEmitter
|
||||
#
|
||||
else:
|
||||
self.log.debug(f"{method_name} [200 - OK]")
|
||||
return self.emitter.respond(response=response)
|
|
@ -15,14 +15,15 @@
|
|||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
import click
|
||||
import json
|
||||
import os
|
||||
from flask import Response
|
||||
from functools import partial
|
||||
from typing import Callable, Union
|
||||
|
||||
import click
|
||||
from flask import Response
|
||||
|
||||
import nucypher
|
||||
from nucypher.utilities.logging import Logger
|
||||
|
||||
|
@ -226,7 +227,6 @@ class WebEmitter:
|
|||
class MethodNotFound(BaseException):
|
||||
"""Cannot find interface method to handle request"""
|
||||
|
||||
|
||||
_crash_on_error_default = False
|
||||
transport_serializer = json.dumps
|
||||
_default_sink_callable = Response
|
||||
|
@ -248,27 +248,29 @@ class WebEmitter:
|
|||
'version': str(nucypher.__version__)}
|
||||
return response_data
|
||||
|
||||
def exception(drone_character,
|
||||
def exception(self,
|
||||
e,
|
||||
error_message: str,
|
||||
log_level: str = 'info',
|
||||
response_code: int = 500):
|
||||
|
||||
message = f"{drone_character} [{str(response_code)} - {error_message}] | ERROR: {str(e)}"
|
||||
logger = getattr(drone_character.log, log_level)
|
||||
message = f"{self} [{str(response_code)} - {error_message}] | ERROR: {str(e) or type(e).__name__}"
|
||||
logger = getattr(self.log, log_level)
|
||||
# See #724 / 2156
|
||||
message_cleaned_for_logger = message.replace("{", "<^<").replace("}", ">^>")
|
||||
logger(message_cleaned_for_logger)
|
||||
if drone_character.crash_on_error:
|
||||
if self.crash_on_error:
|
||||
raise e
|
||||
return drone_character.sink(str(e), status=response_code)
|
||||
|
||||
def respond(drone_character, response) -> Response:
|
||||
assembled_response = drone_character.assemble_response(response=response)
|
||||
response_message = str(e) or type(e).__name__
|
||||
return self.sink(response_message, status=response_code)
|
||||
|
||||
def respond(self, response) -> Response:
|
||||
assembled_response = self.assemble_response(response=response)
|
||||
serialized_response = WebEmitter.transport_serializer(assembled_response)
|
||||
|
||||
# ---------- HTTP OUTPUT
|
||||
response = drone_character.sink(response=serialized_response, status=200, content_type="application/javascript")
|
||||
response = self.sink(response=serialized_response, status=200, content_type="application/javascript")
|
||||
return response
|
||||
|
||||
def get_stream(self, *args, **kwargs):
|
|
@ -0,0 +1,56 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
|
||||
import functools
|
||||
|
||||
|
||||
def attach_schema(schema):
|
||||
def callable(func):
|
||||
func._schema = schema()
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapped(*args, **kwargs):
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapped
|
||||
|
||||
return callable
|
||||
|
||||
|
||||
class ControlInterface:
|
||||
|
||||
def __init__(self, implementer=None, *args, **kwargs):
|
||||
self.implementer = implementer
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def connect_cli(cls, action):
|
||||
schema = getattr(cls, action)._schema
|
||||
|
||||
def callable(func):
|
||||
c = func
|
||||
for f in [f for f in schema.load_fields.values() if f.click]:
|
||||
c = f.click(c)
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapped(*args, **kwargs):
|
||||
return c(*args, **kwargs)
|
||||
|
||||
return wrapped
|
||||
|
||||
return callable
|
|
@ -0,0 +1,16 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
|
@ -17,13 +17,12 @@
|
|||
|
||||
from marshmallow import INCLUDE, Schema
|
||||
|
||||
from nucypher.characters.control.specifications.exceptions import InvalidInputData
|
||||
from nucypher.control.specifications.exceptions import InvalidInputData
|
||||
|
||||
|
||||
class BaseSchema(Schema):
|
||||
|
||||
class Meta:
|
||||
|
||||
unknown = INCLUDE # pass through any data that isn't defined as a field
|
||||
|
||||
def handle_error(self, error, data, many, **kwargs):
|
|
@ -0,0 +1,36 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
|
||||
class SpecificationError(ValueError):
|
||||
"""The protocol request is completely unusable"""
|
||||
|
||||
|
||||
class MissingField(SpecificationError):
|
||||
"""The protocol request cannot be deserialized because it is missing required fields"""
|
||||
|
||||
|
||||
class InvalidInputData(SpecificationError):
|
||||
"""Input data does not match the input specification"""
|
||||
|
||||
|
||||
class InvalidOutputData(SpecificationError):
|
||||
"""Response data does not match the output specification"""
|
||||
|
||||
|
||||
class InvalidArgumentCombo(SpecificationError):
|
||||
"""Arguments specified are incompatible"""
|
|
@ -15,13 +15,5 @@
|
|||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import click
|
||||
|
||||
|
||||
class BaseField:
|
||||
|
||||
click_type = click.STRING
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.click = kwargs.pop('click', None)
|
||||
super().__init__(*args, **kwargs)
|
||||
from nucypher.control.specifications.fields.base import *
|
|
@ -0,0 +1,81 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from base64 import b64encode, b64decode
|
||||
|
||||
import click
|
||||
from marshmallow import fields
|
||||
|
||||
from nucypher.control.specifications.exceptions import InvalidInputData
|
||||
|
||||
|
||||
class BaseField:
|
||||
|
||||
click_type = click.STRING
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.click = kwargs.pop('click', None)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
#
|
||||
# Very common, simple field types to build on.
|
||||
#
|
||||
|
||||
class String(BaseField, fields.String):
|
||||
pass
|
||||
|
||||
|
||||
class List(BaseField, fields.List):
|
||||
pass
|
||||
|
||||
|
||||
class StringList(List):
|
||||
"""
|
||||
Expects a delimited string, if input is not already a list. The string is split using the delimiter arg
|
||||
(defaults to ',' if not provided) and returns a corresponding List of object.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.delimiter = kwargs.pop('delimiter', ',')
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def _deserialize(self, value, attr, data, **kwargs):
|
||||
if not isinstance(value, list):
|
||||
value = value.split(self.delimiter)
|
||||
return super()._deserialize(value, attr, data, **kwargs)
|
||||
|
||||
|
||||
class Integer(BaseField, fields.Integer):
|
||||
click_type = click.INT
|
||||
|
||||
|
||||
class PositiveInteger(Integer):
|
||||
def _validate(self, value):
|
||||
if not value > 0:
|
||||
raise InvalidInputData(f"{self.name} must be a positive integer.")
|
||||
|
||||
|
||||
class Base64BytesRepresentation(BaseField, fields.Field):
|
||||
"""Serializes/Deserializes any object's byte representation to/from bae64."""
|
||||
def _serialize(self, value, attr, obj, **kwargs):
|
||||
value_bytes = value if isinstance(value, bytes) else bytes(value)
|
||||
return b64encode(value_bytes).decode()
|
||||
|
||||
def _deserialize(self, value, attr, data, **kwargs):
|
||||
try:
|
||||
return b64decode(value)
|
||||
except ValueError as e:
|
||||
raise InvalidInputData(f"Could not parse {self.name}: {e}")
|
|
@ -25,7 +25,7 @@ from constant_sorrow import constants
|
|||
from cryptography.hazmat.primitives.asymmetric import ec
|
||||
from hendrix.deploy.tls import HendrixDeployTLS
|
||||
from hendrix.facilities.services import ExistingKeyTLSContextFactory
|
||||
from umbral.signing import Signer
|
||||
from nucypher.crypto.umbral_adapter import Signer
|
||||
|
||||
from nucypher.config.constants import MAX_UPLOAD_CONTENT_LENGTH
|
||||
from nucypher.crypto.kits import MessageKit
|
||||
|
|
|
@ -31,8 +31,8 @@ import click
|
|||
from constant_sorrow.constants import KEYSTORE_LOCKED
|
||||
from mnemonic.mnemonic import Mnemonic
|
||||
|
||||
from nucypher.characters.control.emitters import StdoutEmitter
|
||||
from nucypher.config.constants import DEFAULT_CONFIG_ROOT
|
||||
from nucypher.control.emitters import StdoutEmitter
|
||||
from nucypher.crypto.keypairs import HostingKeypair
|
||||
from nucypher.crypto.passwords import (
|
||||
secret_box_decrypt,
|
||||
|
|
|
@ -25,7 +25,6 @@ from constant_sorrow.constants import CERTIFICATE_NOT_SAVED, EXEMPT_FROM_VERIFIC
|
|||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
|
||||
from nucypher.blockchain.eth.networks import NetworksInventory
|
||||
from nucypher.crypto.splitters import cfrag_splitter, signature_splitter
|
||||
from nucypher.utilities.logging import Logger
|
||||
|
||||
|
@ -194,7 +193,8 @@ class RestMiddleware:
|
|||
return response
|
||||
|
||||
def reencrypt(self, work_order):
|
||||
ursula_rest_response = self.send_work_order_payload_to_ursula(work_order)
|
||||
ursula_rest_response = self.send_work_order_payload_to_ursula(ursula=work_order.ursula,
|
||||
work_order_payload=work_order.payload())
|
||||
splitter = cfrag_splitter + signature_splitter
|
||||
cfrags_and_signatures = splitter.repeat(ursula_rest_response.content)
|
||||
return cfrags_and_signatures
|
||||
|
@ -221,12 +221,11 @@ class RestMiddleware:
|
|||
timeout=2)
|
||||
return response
|
||||
|
||||
def send_work_order_payload_to_ursula(self, work_order):
|
||||
payload = work_order.payload()
|
||||
def send_work_order_payload_to_ursula(self, ursula: 'Ursula', work_order_payload: bytes):
|
||||
response = self.client.post(
|
||||
node_or_sprout=work_order.ursula,
|
||||
node_or_sprout=ursula,
|
||||
path=f"reencrypt",
|
||||
data=payload,
|
||||
data=work_order_payload,
|
||||
timeout=2
|
||||
)
|
||||
return response
|
||||
|
|
|
@ -195,6 +195,8 @@ class Learner:
|
|||
__DEFAULT_NODE_STORAGE = ForgetfulNodeStorage
|
||||
__DEFAULT_MIDDLEWARE_CLASS = RestMiddleware
|
||||
|
||||
_crashed = False # moved from Character - why was this in Character and not Learner before
|
||||
|
||||
LEARNER_VERSION = LEARNING_LOOP_VERSION
|
||||
LOWEST_COMPATIBLE_VERSION = 2 # Disallow versions lower than this
|
||||
|
||||
|
@ -658,8 +660,7 @@ class Learner:
|
|||
addresses: Set,
|
||||
timeout=LEARNING_TIMEOUT,
|
||||
allow_missing=0,
|
||||
learn_on_this_thread=False,
|
||||
verify_now=False):
|
||||
learn_on_this_thread=False):
|
||||
start = maya.now()
|
||||
starting_round = self._learning_round
|
||||
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from random import shuffle
|
||||
|
||||
import maya
|
||||
from nucypher.crypto.umbral_adapter import PublicKey
|
||||
|
||||
from nucypher.acumen.perception import FleetSensor
|
||||
from nucypher.crypto.signing import InvalidSignature
|
||||
from nucypher.network.exceptions import NodeSeemsToBeDown
|
||||
from nucypher.network.nodes import Learner
|
||||
|
||||
|
||||
def get_treasure_map_from_known_ursulas(learner: Learner,
|
||||
map_identifier: str,
|
||||
bob_encrypting_key: PublicKey,
|
||||
timeout=3):
|
||||
"""
|
||||
Iterate through the nodes we know, asking for the TreasureMap.
|
||||
Return the first one who has it.
|
||||
"""
|
||||
if learner.federated_only:
|
||||
from nucypher.policy.maps import TreasureMap as _MapClass
|
||||
else:
|
||||
from nucypher.policy.maps import SignedTreasureMap as _MapClass
|
||||
|
||||
start = maya.now()
|
||||
|
||||
# Spend no more than half the timeout finding the nodes. 8 nodes is arbitrary. Come at me.
|
||||
learner.block_until_number_of_known_nodes_is(8, timeout=timeout / 2, learn_on_this_thread=True)
|
||||
while True:
|
||||
nodes_with_map = find_matching_nodes(known_nodes=learner.known_nodes, bob_encrypting_key=bob_encrypting_key)
|
||||
# TODO nodes_with_map can be large - what if treasure map not present in any of them? Without checking the
|
||||
# timeout within the loop, this could take a long time.
|
||||
shuffle(nodes_with_map)
|
||||
|
||||
for node in nodes_with_map:
|
||||
try:
|
||||
response = learner.network_middleware.get_treasure_map_from_node(node, map_identifier)
|
||||
except (*NodeSeemsToBeDown, learner.NotEnoughNodes):
|
||||
continue
|
||||
except learner.network_middleware.NotFound:
|
||||
learner.log.info(f"Node {node} claimed not to have TreasureMap {map_identifier}")
|
||||
continue
|
||||
except node.NotStaking:
|
||||
# TODO this wasn't here before - check with myles
|
||||
learner.log.info(f"Node {node} not staking")
|
||||
continue
|
||||
|
||||
if response.status_code == 200 and response.content:
|
||||
try:
|
||||
treasure_map = _MapClass.from_bytes(response.content)
|
||||
return treasure_map
|
||||
except InvalidSignature:
|
||||
# TODO: What if a node gives a bunk TreasureMap? NRN
|
||||
raise
|
||||
else:
|
||||
continue # TODO: Actually, handle error case here. NRN
|
||||
else:
|
||||
learner.learn_from_teacher_node()
|
||||
|
||||
if (start - maya.now()).seconds > timeout:
|
||||
raise _MapClass.NowhereToBeFound(f"Asked {len(learner.known_nodes)} nodes, "
|
||||
f"but none had map {map_identifier}")
|
||||
|
||||
|
||||
def find_matching_nodes(known_nodes: FleetSensor,
|
||||
bob_encrypting_key: PublicKey,
|
||||
no_less_than=7): # Somewhat arbitrary floor here.
|
||||
# Look for nodes whose checksum address has the second character of Bob's encrypting key in the first
|
||||
# few characters.
|
||||
# Think of it as a cheap knockoff hamming distance.
|
||||
# The good news is that Bob can construct the list easily.
|
||||
# And - famous last words incoming - there's no cognizable attack surface.
|
||||
# Sure, Bob can mine encrypting keypairs until he gets the set of target Ursulas on which Alice can
|
||||
# store a TreasureMap. And then... ???... profit?
|
||||
|
||||
# Sanity check - do we even have enough nodes?
|
||||
if len(known_nodes) < no_less_than:
|
||||
raise ValueError(f"Can't select {no_less_than} from {len(known_nodes)} (Fleet state: {known_nodes.FleetState})")
|
||||
|
||||
search_boundary = 2
|
||||
target_nodes = []
|
||||
target_hex_match = bytes(bob_encrypting_key).hex()[1]
|
||||
while len(target_nodes) < no_less_than:
|
||||
search_boundary += 2
|
||||
if search_boundary > 42: # We've searched the entire string and can't match any. TODO: Portable learning is a nice idea here.
|
||||
# Not enough matching nodes. Fine, we'll just publish to the first few.
|
||||
try:
|
||||
# TODO: This is almost certainly happening in a test. If it does happen in production, it's a
|
||||
# bit of a problem. Need to fix #2124 to mitigate.
|
||||
target_nodes = list(known_nodes.values())[0:6]
|
||||
return target_nodes
|
||||
except IndexError:
|
||||
raise Learner.NotEnoughNodes(
|
||||
"There aren't enough nodes on the network to enact this policy. Unless this is day "
|
||||
"one of the network and nodes are still getting spun up, something is bonkers.")
|
||||
|
||||
# TODO: 1995 all throughout here (we might not (need to) know the checksum address yet; canonical will do.)
|
||||
# This might be a performance issue above a few thousand nodes.
|
||||
target_nodes = [node for node in known_nodes if target_hex_match in node.checksum_address[2:search_boundary]]
|
||||
return target_nodes
|
|
@ -25,7 +25,6 @@ from bytestring_splitter import BytestringSplitter, VariableLengthBytestring
|
|||
from eth_typing.evm import ChecksumAddress
|
||||
from twisted.internet import reactor
|
||||
|
||||
from nucypher.blockchain.eth.agents import StakersReservoir, StakingEscrowAgent
|
||||
from nucypher.blockchain.eth.constants import POLICY_ID_LENGTH
|
||||
from nucypher.crypto.constants import HRAC_LENGTH
|
||||
from nucypher.crypto.kits import RevocationKit
|
||||
|
@ -35,6 +34,12 @@ from nucypher.crypto.utils import keccak_digest
|
|||
from nucypher.crypto.umbral_adapter import PublicKey, KeyFrag, Signature
|
||||
from nucypher.crypto.utils import construct_policy_id
|
||||
from nucypher.network.middleware import RestMiddleware
|
||||
from nucypher.policy.reservoir import (
|
||||
make_federated_staker_reservoir,
|
||||
MergedReservoir,
|
||||
PrefetchStrategy,
|
||||
make_decentralized_staker_reservoir
|
||||
)
|
||||
from nucypher.utilities.concurrency import WorkerPool, AllAtOnceFactory
|
||||
from nucypher.utilities.logging import Logger
|
||||
|
||||
|
@ -76,15 +81,31 @@ class TreasureMapPublisher:
|
|||
log = Logger('TreasureMapPublisher')
|
||||
|
||||
def __init__(self,
|
||||
worker,
|
||||
nodes,
|
||||
percent_to_complete_before_release=5,
|
||||
threadpool_size=120,
|
||||
timeout=20):
|
||||
treasure_map_bytes: bytes,
|
||||
nodes: Sequence['Ursula'],
|
||||
network_middleware: RestMiddleware,
|
||||
percent_to_complete_before_release: int = 5,
|
||||
threadpool_size: int = 120,
|
||||
timeout: float = 20):
|
||||
|
||||
self._total = len(nodes)
|
||||
self._block_until_this_many_are_complete = math.ceil(len(nodes) * percent_to_complete_before_release / 100)
|
||||
self._worker_pool = WorkerPool(worker=worker,
|
||||
|
||||
def put_treasure_map_on_node(node: 'Ursula'):
|
||||
try:
|
||||
response = network_middleware.put_treasure_map_on_node(node=node,
|
||||
map_payload=treasure_map_bytes)
|
||||
except Exception as e:
|
||||
self.log.warn(f"Putting treasure map on {node} failed: {e}")
|
||||
raise
|
||||
|
||||
# Received an HTTP response
|
||||
if response.status_code != 201:
|
||||
message = f"Putting treasure map on {node} failed with response status: {response.status}"
|
||||
self.log.warn(message)
|
||||
return response
|
||||
|
||||
self._worker_pool = WorkerPool(worker=put_treasure_map_on_node,
|
||||
value_factory=AllAtOnceFactory(nodes),
|
||||
target_successes=self._block_until_this_many_are_complete,
|
||||
timeout=timeout,
|
||||
|
@ -131,49 +152,6 @@ class TreasureMapPublisher:
|
|||
self._worker_pool.join()
|
||||
|
||||
|
||||
class MergedReservoir:
|
||||
"""
|
||||
A reservoir made of a list of addresses and a StakersReservoir.
|
||||
Draws the values from the list first, then from StakersReservoir,
|
||||
then returns None on subsequent calls.
|
||||
"""
|
||||
|
||||
def __init__(self, values: Iterable, reservoir: StakersReservoir):
|
||||
self.values = list(values)
|
||||
self.reservoir = reservoir
|
||||
|
||||
def __call__(self) -> Optional[ChecksumAddress]:
|
||||
if self.values:
|
||||
return self.values.pop(0)
|
||||
elif len(self.reservoir) > 0:
|
||||
return self.reservoir.draw(1)[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
class PrefetchStrategy:
|
||||
"""
|
||||
Encapsulates the batch draw strategy from a reservoir.
|
||||
Determines how many values to draw based on the number of values
|
||||
that have already led to successes.
|
||||
"""
|
||||
|
||||
def __init__(self, reservoir: MergedReservoir, need_successes: int):
|
||||
self.reservoir = reservoir
|
||||
self.need_successes = need_successes
|
||||
|
||||
def __call__(self, successes: int) -> Optional[List[ChecksumAddress]]:
|
||||
batch = []
|
||||
for i in range(self.need_successes - successes):
|
||||
value = self.reservoir()
|
||||
if value is None:
|
||||
break
|
||||
batch.append(value)
|
||||
if not batch:
|
||||
return None
|
||||
return batch
|
||||
|
||||
|
||||
class Policy(ABC):
|
||||
"""
|
||||
An edict by Alice, arranged with n Ursulas, to perform re-encryption for a specific Bob.
|
||||
|
@ -384,22 +362,11 @@ class Policy(ABC):
|
|||
# TODO (#2516): remove hardcoding of 8 nodes
|
||||
self.alice.block_until_number_of_known_nodes_is(8, timeout=2, learn_on_this_thread=True)
|
||||
target_nodes = self.bob.matching_nodes_among(self.alice.known_nodes)
|
||||
treasure_map_bytes = bytes(treasure_map) # prevent the closure from holding the reference
|
||||
treasure_map_bytes = bytes(treasure_map) # prevent holding of the reference
|
||||
|
||||
def put_treasure_map_on_node(node):
|
||||
try:
|
||||
response = network_middleware.put_treasure_map_on_node(node=node, map_payload=treasure_map_bytes)
|
||||
except Exception as e:
|
||||
self.log.warn(f"Putting treasure map on {node} failed: {e}")
|
||||
raise
|
||||
|
||||
# Received an HTTP response
|
||||
if response.status_code != 201:
|
||||
message = f"Putting treasure map on {node} failed with response status: {response.status}"
|
||||
self.log.warn(message)
|
||||
return response
|
||||
|
||||
return TreasureMapPublisher(worker=put_treasure_map_on_node, nodes=target_nodes)
|
||||
return TreasureMapPublisher(treasure_map_bytes=treasure_map_bytes,
|
||||
nodes=target_nodes,
|
||||
network_middleware=network_middleware)
|
||||
|
||||
def enact(self,
|
||||
network_middleware: RestMiddleware,
|
||||
|
@ -458,7 +425,6 @@ class Policy(ABC):
|
|||
|
||||
|
||||
class FederatedPolicy(Policy):
|
||||
|
||||
from nucypher.policy.maps import TreasureMap as __map_class
|
||||
_treasure_map_class = __map_class
|
||||
|
||||
|
@ -466,11 +432,8 @@ class FederatedPolicy(Policy):
|
|||
return Policy.NotEnoughUrsulas
|
||||
|
||||
def _make_reservoir(self, handpicked_addresses):
|
||||
addresses = {
|
||||
ursula.checksum_address: 1 for ursula in self.alice.known_nodes
|
||||
if ursula.checksum_address not in handpicked_addresses}
|
||||
|
||||
return MergedReservoir(handpicked_addresses, StakersReservoir(addresses))
|
||||
return make_federated_staker_reservoir(known_nodes=self.alice.known_nodes,
|
||||
include_addresses=handpicked_addresses)
|
||||
|
||||
def _make_enactment_payload(self, kfrag) -> bytes:
|
||||
return bytes(kfrag)
|
||||
|
@ -551,14 +514,10 @@ class BlockchainPolicy(Policy):
|
|||
return params
|
||||
|
||||
def _make_reservoir(self, handpicked_addresses):
|
||||
try:
|
||||
reservoir = self.alice.get_stakers_reservoir(duration=self.payment_periods,
|
||||
without=handpicked_addresses)
|
||||
except StakingEscrowAgent.NotEnoughStakers:
|
||||
# TODO: do that in `get_stakers_reservoir()`?
|
||||
reservoir = StakersReservoir({})
|
||||
|
||||
return MergedReservoir(handpicked_addresses, reservoir)
|
||||
staker_reservoir = make_decentralized_staker_reservoir(staking_agent=self.alice.staking_agent,
|
||||
duration_periods=self.payment_periods,
|
||||
include_addresses=handpicked_addresses)
|
||||
return staker_reservoir
|
||||
|
||||
def _publish_to_blockchain(self, ursulas) -> dict:
|
||||
|
||||
|
|
|
@ -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 Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from typing import Iterable, Optional, List
|
||||
|
||||
from eth_typing import ChecksumAddress
|
||||
|
||||
from nucypher.acumen.perception import FleetSensor
|
||||
from nucypher.blockchain.eth.agents import StakersReservoir, StakingEscrowAgent
|
||||
|
||||
|
||||
def make_federated_staker_reservoir(known_nodes: FleetSensor,
|
||||
exclude_addresses: Optional[Iterable[ChecksumAddress]] = None,
|
||||
include_addresses: Optional[Iterable[ChecksumAddress]] = None):
|
||||
"""
|
||||
Get a sampler object containing the federated stakers.
|
||||
"""
|
||||
# needs to not include both exclude and include addresses
|
||||
# so that they aren't included in reservoir, include_address will be re-added to reservoir afterwards
|
||||
include_addresses = include_addresses or ()
|
||||
exclusion_set = set(include_addresses) | set(exclude_addresses or ())
|
||||
addresses = {}
|
||||
for ursula in known_nodes:
|
||||
if ursula.checksum_address in exclusion_set:
|
||||
continue
|
||||
addresses[ursula.checksum_address] = 1
|
||||
|
||||
# add include addresses
|
||||
return MergedReservoir(include_addresses, StakersReservoir(addresses))
|
||||
|
||||
|
||||
def make_decentralized_staker_reservoir(staking_agent: StakingEscrowAgent,
|
||||
duration_periods: int,
|
||||
exclude_addresses: Optional[Iterable[ChecksumAddress]] = None,
|
||||
include_addresses: Optional[Iterable[ChecksumAddress]] = None,
|
||||
pagination_size: int = None):
|
||||
"""
|
||||
Get a sampler object containing the currently registered stakers.
|
||||
"""
|
||||
|
||||
# needs to not include both exclude and include addresses
|
||||
# so that they aren't included in reservoir, include_address will be re-added to reservoir afterwards
|
||||
include_addresses = include_addresses or ()
|
||||
without_set = set(include_addresses) | set(exclude_addresses or ())
|
||||
try:
|
||||
reservoir = staking_agent.get_stakers_reservoir(duration=duration_periods,
|
||||
without=without_set,
|
||||
pagination_size=pagination_size)
|
||||
except StakingEscrowAgent.NotEnoughStakers:
|
||||
# TODO: do that in `get_stakers_reservoir()`?
|
||||
reservoir = StakersReservoir({})
|
||||
|
||||
# add include addresses
|
||||
return MergedReservoir(include_addresses, reservoir)
|
||||
|
||||
|
||||
class MergedReservoir:
|
||||
"""
|
||||
A reservoir made of a list of addresses and a StakersReservoir.
|
||||
Draws the values from the list first, then from StakersReservoir,
|
||||
then returns None on subsequent calls.
|
||||
"""
|
||||
|
||||
def __init__(self, values: Iterable, reservoir: StakersReservoir):
|
||||
self.values = list(values)
|
||||
self.reservoir = reservoir
|
||||
|
||||
def __call__(self) -> Optional[ChecksumAddress]:
|
||||
if self.values:
|
||||
return self.values.pop(0)
|
||||
elif len(self.reservoir) > 0:
|
||||
return self.reservoir.draw(1)[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
class PrefetchStrategy:
|
||||
"""
|
||||
Encapsulates the batch draw strategy from a reservoir.
|
||||
Determines how many values to draw based on the number of values
|
||||
that have already led to successes.
|
||||
"""
|
||||
|
||||
def __init__(self, reservoir: MergedReservoir, need_successes: int):
|
||||
self.reservoir = reservoir
|
||||
self.need_successes = need_successes
|
||||
|
||||
def __call__(self, successes: int) -> Optional[List[ChecksumAddress]]:
|
||||
batch = []
|
||||
for i in range(self.need_successes - successes):
|
||||
value = self.reservoir()
|
||||
if value is None:
|
||||
break
|
||||
batch.append(value)
|
||||
if not batch:
|
||||
return None
|
||||
return batch
|
|
@ -0,0 +1,16 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
|
@ -0,0 +1,16 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
|
@ -0,0 +1,38 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
from nucypher.control.controllers import CLIController
|
||||
from nucypher.control.emitters import StdoutEmitter
|
||||
|
||||
|
||||
class PorterCLIController(CLIController):
|
||||
|
||||
_emitter_class = StdoutEmitter
|
||||
|
||||
def __init__(self,
|
||||
interface: 'PorterInterface',
|
||||
*args,
|
||||
**kwargs):
|
||||
super().__init__(interface=interface, *args, **kwargs)
|
||||
|
||||
def _perform_action(self, *args, **kwargs) -> dict:
|
||||
try:
|
||||
response_data = super()._perform_action(*args, **kwargs)
|
||||
finally:
|
||||
self.log.debug(f"Finished action '{kwargs['action']}', stopping {self.interface.implementer}")
|
||||
self.interface.implementer.disenchant()
|
||||
return response_data
|
|
@ -0,0 +1,90 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from typing import List, Optional
|
||||
|
||||
from eth_typing import ChecksumAddress
|
||||
from nucypher.crypto.umbral_adapter import PublicKey
|
||||
|
||||
from nucypher.characters.control.specifications.fields import TreasureMap
|
||||
from nucypher.control.interfaces import ControlInterface, attach_schema
|
||||
from nucypher.utilities.porter.control.specifications import porter_schema
|
||||
|
||||
|
||||
class PorterInterface(ControlInterface):
|
||||
def __init__(self, porter: 'Porter' = None, *args, **kwargs):
|
||||
super().__init__(implementer=porter, *args, **kwargs)
|
||||
# set federated/non-federated context for publish treasure map schema
|
||||
PorterInterface.publish_treasure_map._schema.context[TreasureMap.IS_FEDERATED_CONTEXT_KEY] = porter.federated_only
|
||||
|
||||
#
|
||||
# Alice Endpoints
|
||||
#
|
||||
@attach_schema(porter_schema.AliceGetUrsulas)
|
||||
def get_ursulas(self,
|
||||
quantity: int,
|
||||
duration_periods: int,
|
||||
exclude_ursulas: Optional[List[ChecksumAddress]] = None,
|
||||
include_ursulas: Optional[List[ChecksumAddress]] = None) -> dict:
|
||||
ursulas_info = self.implementer.get_ursulas(quantity=quantity,
|
||||
duration_periods=duration_periods,
|
||||
exclude_ursulas=exclude_ursulas,
|
||||
include_ursulas=include_ursulas)
|
||||
|
||||
response_data = {
|
||||
"ursulas": ursulas_info
|
||||
}
|
||||
return response_data
|
||||
|
||||
@attach_schema(porter_schema.AlicePublishTreasureMap)
|
||||
def publish_treasure_map(self,
|
||||
treasure_map: bytes,
|
||||
bob_encrypting_key: bytes) -> dict:
|
||||
bob_enc_key = PublicKey.from_bytes(bob_encrypting_key)
|
||||
self.implementer.publish_treasure_map(treasure_map_bytes=treasure_map,
|
||||
bob_encrypting_key=bob_enc_key)
|
||||
response_data = {'published': True} # always True - if publish failed, an exception is raised by implementer
|
||||
return response_data
|
||||
|
||||
@attach_schema(porter_schema.AliceRevoke)
|
||||
def revoke(self) -> dict:
|
||||
# Steps (analogous to nucypher.character.control.interfaces):
|
||||
# 1. creation of objects / setup
|
||||
# 2. call self.implementer.some_function() i.e. Porter learner has an associated function to call
|
||||
# 3. create response
|
||||
pass
|
||||
|
||||
#
|
||||
# Bob Endpoints
|
||||
#
|
||||
@attach_schema(porter_schema.BobGetTreasureMap)
|
||||
def get_treasure_map(self,
|
||||
treasure_map_id: str,
|
||||
bob_encrypting_key: bytes) -> dict:
|
||||
bob_enc_key = PublicKey.from_bytes(bob_encrypting_key)
|
||||
treasure_map = self.implementer.get_treasure_map(map_identifier=treasure_map_id,
|
||||
bob_encrypting_key=bob_enc_key)
|
||||
response_data = {'treasure_map': treasure_map}
|
||||
return response_data
|
||||
|
||||
@attach_schema(porter_schema.BobExecWorkOrder)
|
||||
def exec_work_order(self,
|
||||
ursula: ChecksumAddress,
|
||||
work_order_payload: bytes) -> dict:
|
||||
work_order_result = self.implementer.exec_work_order(ursula_address=ursula,
|
||||
work_order_payload=work_order_payload)
|
||||
response_data = {'work_order_result': work_order_result}
|
||||
return response_data
|
|
@ -0,0 +1,16 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
|
@ -0,0 +1,20 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
from nucypher.utilities.porter.control.specifications.fields.ursula import *
|
||||
from nucypher.utilities.porter.control.specifications.fields.treasuremapid import *
|
||||
from nucypher.utilities.porter.control.specifications.fields.workorder import *
|
|
@ -0,0 +1,31 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
from marshmallow import fields
|
||||
|
||||
from nucypher.control.specifications.exceptions import InvalidInputData
|
||||
from nucypher.control.specifications.fields.base import BaseField
|
||||
from nucypher.crypto.constants import HRAC_LENGTH, KECCAK_DIGEST_LENGTH
|
||||
|
||||
|
||||
class TreasureMapID(BaseField, fields.String):
|
||||
|
||||
def _validate(self, value):
|
||||
treasure_map_id = bytes.fromhex(value)
|
||||
# FIXME federated has map id length 32 bytes but decentralized has length 16 bytes ... huh? - #2725
|
||||
if len(treasure_map_id) != KECCAK_DIGEST_LENGTH and len(treasure_map_id) != HRAC_LENGTH:
|
||||
raise InvalidInputData(f"Could not convert input for {self.name} to a valid TreasureMap ID: invalid length")
|
|
@ -0,0 +1,43 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
from eth_utils import to_checksum_address
|
||||
from marshmallow import fields
|
||||
|
||||
from nucypher.characters.control.specifications.fields import Key
|
||||
from nucypher.cli import types
|
||||
from nucypher.control.specifications.base import BaseSchema
|
||||
from nucypher.control.specifications.exceptions import InvalidInputData
|
||||
from nucypher.control.specifications.fields import String
|
||||
|
||||
|
||||
class UrsulaChecksumAddress(String):
|
||||
"""Ursula checksum address."""
|
||||
click_type = types.EIP55_CHECKSUM_ADDRESS
|
||||
|
||||
def _deserialize(self, value, attr, data, **kwargs):
|
||||
try:
|
||||
return to_checksum_address(value=value)
|
||||
except ValueError as e:
|
||||
raise InvalidInputData(f"Could not convert input for {self.name} to a valid checksum address: {e}")
|
||||
|
||||
|
||||
class UrsulaInfoSchema(BaseSchema):
|
||||
"""Schema for the result of sampling of Ursulas."""
|
||||
checksum_address = UrsulaChecksumAddress()
|
||||
uri = fields.URL()
|
||||
encrypting_key = Key()
|
|
@ -0,0 +1,28 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
from nucypher.control.specifications.fields import Base64BytesRepresentation
|
||||
from nucypher.policy.orders import WorkOrder as WorkOrderClass
|
||||
|
||||
|
||||
class WorkOrder(Base64BytesRepresentation):
|
||||
def _serialize(self, value: WorkOrderClass, attr, obj, **kwargs):
|
||||
return super()._serialize(value.payload(), attr, obj, **kwargs)
|
||||
|
||||
|
||||
class WorkOrderResult(Base64BytesRepresentation):
|
||||
pass
|
|
@ -0,0 +1,177 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
import click
|
||||
from marshmallow import validates_schema
|
||||
from marshmallow import fields as marshmallow_fields
|
||||
|
||||
from nucypher.control.specifications.base import BaseSchema
|
||||
from nucypher.control.specifications import fields as base_fields
|
||||
from nucypher.control.specifications.exceptions import InvalidArgumentCombo
|
||||
from nucypher.utilities.porter.control.specifications import fields
|
||||
from nucypher.characters.control.specifications import fields as character_fields
|
||||
from nucypher.cli import types
|
||||
|
||||
|
||||
def option_ursula():
|
||||
return click.option(
|
||||
'--ursula',
|
||||
'-u',
|
||||
help="Ursula checksum address",
|
||||
type=types.EIP55_CHECKSUM_ADDRESS,
|
||||
required=True)
|
||||
|
||||
|
||||
def option_bob_encrypting_key():
|
||||
return click.option(
|
||||
'--bob-encrypting-key',
|
||||
'-bek',
|
||||
help="Bob's encrypting key as a hexadecimal string",
|
||||
type=click.STRING,
|
||||
required=True)
|
||||
|
||||
|
||||
#
|
||||
# Alice Endpoints
|
||||
#
|
||||
class AliceGetUrsulas(BaseSchema):
|
||||
quantity = base_fields.PositiveInteger(
|
||||
required=True,
|
||||
load_only=True,
|
||||
click=click.option(
|
||||
'--quantity',
|
||||
'-n',
|
||||
help="Total number of Ursulas needed",
|
||||
type=click.INT, required=True))
|
||||
duration_periods = base_fields.PositiveInteger(
|
||||
required=True,
|
||||
load_only=True,
|
||||
click=click.option(
|
||||
'--periods',
|
||||
'-p',
|
||||
help="Required duration of service for Ursulas",
|
||||
type=click.INT, required=True))
|
||||
|
||||
# optional
|
||||
exclude_ursulas = base_fields.StringList(
|
||||
fields.UrsulaChecksumAddress(),
|
||||
click=click.option(
|
||||
'--exclude-ursula',
|
||||
'-e',
|
||||
help="Ursula checksum address to exclude from sample",
|
||||
multiple=True,
|
||||
type=types.EIP55_CHECKSUM_ADDRESS,
|
||||
required=False,
|
||||
default=[]),
|
||||
required=False,
|
||||
load_only=True)
|
||||
|
||||
include_ursulas = base_fields.StringList(
|
||||
fields.UrsulaChecksumAddress(),
|
||||
click=click.option(
|
||||
'--include-ursula',
|
||||
'-i',
|
||||
help="Ursula checksum address to include in sample",
|
||||
multiple=True,
|
||||
type=types.EIP55_CHECKSUM_ADDRESS,
|
||||
required=False,
|
||||
default=[]),
|
||||
required=False,
|
||||
load_only=True)
|
||||
|
||||
# output
|
||||
ursulas = marshmallow_fields.List(marshmallow_fields.Nested(fields.UrsulaInfoSchema), dump_only=True)
|
||||
|
||||
@validates_schema
|
||||
def check_valid_quantity_and_include_ursulas(self, data, **kwargs):
|
||||
# TODO does this make sense - perhaps having extra ursulas could be a good thing if some are down or can't
|
||||
# be contacted at that time
|
||||
ursulas_to_include = data.get('include_ursulas')
|
||||
if ursulas_to_include and len(ursulas_to_include) > data['quantity']:
|
||||
raise InvalidArgumentCombo(f"Ursulas to include is greater than quantity requested")
|
||||
|
||||
@validates_schema
|
||||
def check_include_and_exclude_are_mutually_exclusive(self, data, **kwargs):
|
||||
ursulas_to_include = data.get('include_ursulas') or []
|
||||
ursulas_to_exclude = data.get('exclude_ursulas') or []
|
||||
common_ursulas = set(ursulas_to_include).intersection(ursulas_to_exclude)
|
||||
if len(common_ursulas) > 0:
|
||||
raise InvalidArgumentCombo(f"Ursulas to include and exclude are not mutually exclusive; "
|
||||
f"common entries {common_ursulas}")
|
||||
|
||||
|
||||
class AlicePublishTreasureMap(BaseSchema):
|
||||
treasure_map = character_fields.TreasureMap(
|
||||
required=True,
|
||||
load_only=True,
|
||||
click=click.option(
|
||||
'--treasure-map',
|
||||
'-t',
|
||||
help="Treasure Map to publish",
|
||||
type=click.STRING,
|
||||
required=True))
|
||||
bob_encrypting_key = character_fields.Key(
|
||||
required=True,
|
||||
load_only=True,
|
||||
click=option_bob_encrypting_key())
|
||||
|
||||
# output
|
||||
published = marshmallow_fields.Bool(dump_only=True)
|
||||
|
||||
|
||||
class AliceRevoke(BaseSchema):
|
||||
pass # TODO need to understand revoke process better
|
||||
|
||||
|
||||
#
|
||||
# Bob Endpoints
|
||||
#
|
||||
class BobGetTreasureMap(BaseSchema):
|
||||
treasure_map_id = fields.TreasureMapID(
|
||||
required=True,
|
||||
load_only=True,
|
||||
click=click.option(
|
||||
'--treasure-map-id',
|
||||
'-tid',
|
||||
help="Treasure Map ID as hex",
|
||||
type=click.STRING,
|
||||
required=True))
|
||||
bob_encrypting_key = character_fields.Key(
|
||||
required=True,
|
||||
load_only=True,
|
||||
click=option_bob_encrypting_key())
|
||||
|
||||
# output
|
||||
# treasure map only used for serialization so no need to provide federated/non-federated context
|
||||
treasure_map = character_fields.TreasureMap(dump_only=True)
|
||||
|
||||
|
||||
class BobExecWorkOrder(BaseSchema):
|
||||
ursula = fields.UrsulaChecksumAddress(
|
||||
required=True,
|
||||
load_only=True,
|
||||
click=option_ursula())
|
||||
work_order_payload = fields.WorkOrder(
|
||||
required=True,
|
||||
load_only=True,
|
||||
click=click.option(
|
||||
'--work-order',
|
||||
'-w',
|
||||
help="Re-encryption work order",
|
||||
type=click.STRING, required=True))
|
||||
|
||||
# output
|
||||
work_order_result = fields.WorkOrderResult(dump_only=True)
|
|
@ -0,0 +1,272 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from typing import List, Optional, Sequence, NamedTuple
|
||||
|
||||
from constant_sorrow.constants import NO_CONTROL_PROTOCOL, NO_BLOCKCHAIN_CONNECTION
|
||||
from eth_typing import ChecksumAddress
|
||||
from flask import request, Response
|
||||
from nucypher.crypto.umbral_adapter import PublicKey
|
||||
|
||||
from nucypher.blockchain.eth.agents import ContractAgency, StakingEscrowAgent
|
||||
from nucypher.blockchain.eth.interfaces import BlockchainInterfaceFactory
|
||||
from nucypher.blockchain.eth.registry import BaseContractRegistry, InMemoryContractRegistry
|
||||
|
||||
from nucypher.characters.lawful import Ursula
|
||||
|
||||
from nucypher.control.controllers import WebController, JSONRPCController
|
||||
from nucypher.crypto.powers import DecryptingPower
|
||||
from nucypher.network.nodes import Learner
|
||||
from nucypher.network import treasuremap
|
||||
from nucypher.policy.policies import TreasureMapPublisher
|
||||
from nucypher.policy.reservoir import (
|
||||
make_federated_staker_reservoir,
|
||||
make_decentralized_staker_reservoir,
|
||||
PrefetchStrategy
|
||||
)
|
||||
from nucypher.utilities.concurrency import WorkerPool
|
||||
from nucypher.utilities.logging import Logger
|
||||
from nucypher.utilities.porter.control.controllers import PorterCLIController
|
||||
from nucypher.utilities.porter.control.interfaces import PorterInterface
|
||||
|
||||
|
||||
class Porter(Learner):
|
||||
|
||||
BANNER = r"""
|
||||
|
||||
______
|
||||
(_____ \ _
|
||||
_____) )__ ____| |_ ____ ____
|
||||
| ____/ _ \ / ___) _)/ _ )/ ___)
|
||||
| | | |_| | | | |_( (/ /| |
|
||||
|_| \___/|_| \___)____)_|
|
||||
|
||||
the Pipe for nucypher network operations
|
||||
"""
|
||||
|
||||
APP_NAME = "Porter"
|
||||
|
||||
_SHORT_LEARNING_DELAY = 2
|
||||
_LONG_LEARNING_DELAY = 30
|
||||
_ROUNDS_WITHOUT_NODES_AFTER_WHICH_TO_SLOW_DOWN = 25
|
||||
|
||||
DEFAULT_EXECUTION_TIMEOUT = 10 # 10s
|
||||
|
||||
DEFAULT_PORT = 9155
|
||||
|
||||
_interface_class = PorterInterface
|
||||
|
||||
class UrsulaInfo(NamedTuple):
|
||||
"""Simple object that stores relevant Ursula information resulting from sampling."""
|
||||
checksum_address: ChecksumAddress
|
||||
uri: str
|
||||
encrypting_key: PublicKey
|
||||
|
||||
def __init__(self,
|
||||
domain: str = None,
|
||||
registry: BaseContractRegistry = None,
|
||||
controller: bool = True,
|
||||
federated_only: bool = False,
|
||||
node_class: object = Ursula,
|
||||
provider_uri: str = None,
|
||||
*args, **kwargs):
|
||||
self.federated_only = federated_only
|
||||
|
||||
if not self.federated_only:
|
||||
if not provider_uri:
|
||||
raise ValueError('Provider URI is required for decentralized Porter.')
|
||||
|
||||
if not BlockchainInterfaceFactory.is_interface_initialized(provider_uri=provider_uri):
|
||||
BlockchainInterfaceFactory.initialize_interface(provider_uri=provider_uri)
|
||||
|
||||
self.registry = registry or InMemoryContractRegistry.from_latest_publication(network=domain)
|
||||
self.staking_agent = ContractAgency.get_agent(StakingEscrowAgent, registry=self.registry)
|
||||
else:
|
||||
self.registry = NO_BLOCKCHAIN_CONNECTION.bool_value(False)
|
||||
node_class.set_federated_mode(federated_only)
|
||||
|
||||
super().__init__(save_metadata=True, domain=domain, node_class=node_class, *args, **kwargs)
|
||||
|
||||
self.log = Logger(self.__class__.__name__)
|
||||
|
||||
# Controller Interface
|
||||
self.interface = self._interface_class(porter=self)
|
||||
self.controller = NO_CONTROL_PROTOCOL
|
||||
if controller:
|
||||
# TODO need to understand this better - only made it analogous to what was done for characters
|
||||
self.make_cli_controller()
|
||||
self.log.info(self.BANNER)
|
||||
|
||||
def get_treasure_map(self, map_identifier: str, bob_encrypting_key: PublicKey):
|
||||
return treasuremap.get_treasure_map_from_known_ursulas(learner=self,
|
||||
map_identifier=map_identifier,
|
||||
bob_encrypting_key=bob_encrypting_key,
|
||||
timeout=self.DEFAULT_EXECUTION_TIMEOUT)
|
||||
|
||||
def publish_treasure_map(self, treasure_map_bytes: bytes, bob_encrypting_key: PublicKey) -> None:
|
||||
# TODO (#2516): remove hardcoding of 8 nodes
|
||||
self.block_until_number_of_known_nodes_is(8, timeout=self.DEFAULT_EXECUTION_TIMEOUT, learn_on_this_thread=True)
|
||||
target_nodes = treasuremap.find_matching_nodes(known_nodes=self.known_nodes,
|
||||
bob_encrypting_key=bob_encrypting_key)
|
||||
treasure_map_publisher = TreasureMapPublisher(treasure_map_bytes=treasure_map_bytes,
|
||||
nodes=target_nodes,
|
||||
network_middleware=self.network_middleware,
|
||||
timeout=self.DEFAULT_EXECUTION_TIMEOUT)
|
||||
treasure_map_publisher.start() # let's do this
|
||||
treasure_map_publisher.block_until_success_is_reasonably_likely()
|
||||
|
||||
def get_ursulas(self,
|
||||
quantity: int,
|
||||
duration_periods: int = None, # optional for federated mode
|
||||
exclude_ursulas: Optional[Sequence[ChecksumAddress]] = None,
|
||||
include_ursulas: Optional[Sequence[ChecksumAddress]] = None) -> List[UrsulaInfo]:
|
||||
reservoir = self._make_staker_reservoir(quantity, duration_periods, exclude_ursulas, include_ursulas)
|
||||
value_factory = PrefetchStrategy(reservoir, quantity)
|
||||
|
||||
def get_ursula_info(ursula_address) -> Porter.UrsulaInfo:
|
||||
if ursula_address not in self.known_nodes:
|
||||
raise ValueError(f"{ursula_address} is not known")
|
||||
|
||||
ursula = self.known_nodes[ursula_address]
|
||||
try:
|
||||
# verify node is valid
|
||||
self.network_middleware.client.verify_and_parse_node_or_host_and_port(node_or_sprout=ursula,
|
||||
host=None,
|
||||
port=None)
|
||||
|
||||
return Porter.UrsulaInfo(checksum_address=ursula_address,
|
||||
uri=f"{ursula.rest_interface.formal_uri}",
|
||||
encrypting_key=ursula.public_keys(DecryptingPower))
|
||||
except Exception as e:
|
||||
self.log.debug(f"Unable to obtain Ursula information ({ursula_address}): {str(e)}")
|
||||
raise
|
||||
|
||||
self.block_until_number_of_known_nodes_is(quantity,
|
||||
timeout=self.DEFAULT_EXECUTION_TIMEOUT,
|
||||
learn_on_this_thread=True,
|
||||
eager=True)
|
||||
|
||||
worker_pool = WorkerPool(worker=get_ursula_info,
|
||||
value_factory=value_factory,
|
||||
target_successes=quantity,
|
||||
timeout=self.DEFAULT_EXECUTION_TIMEOUT,
|
||||
stagger_timeout=1,
|
||||
threadpool_size=quantity)
|
||||
worker_pool.start()
|
||||
successes = worker_pool.block_until_target_successes()
|
||||
ursulas_info = successes.values()
|
||||
return list(ursulas_info)
|
||||
|
||||
def exec_work_order(self, ursula_address: ChecksumAddress, work_order_payload: bytes) -> bytes:
|
||||
self.block_until_specific_nodes_are_known(addresses={ursula_address}, learn_on_this_thread=True)
|
||||
ursula = self.known_nodes[ursula_address]
|
||||
ursula_rest_response = self.network_middleware.send_work_order_payload_to_ursula(
|
||||
ursula=ursula,
|
||||
work_order_payload=work_order_payload)
|
||||
result = ursula_rest_response.content
|
||||
return result
|
||||
|
||||
def _make_staker_reservoir(self,
|
||||
quantity: int,
|
||||
duration_periods: int = None, # optional for federated mode
|
||||
exclude_ursulas: Optional[Sequence[ChecksumAddress]] = None,
|
||||
include_ursulas: Optional[Sequence[ChecksumAddress]] = None):
|
||||
if self.federated_only:
|
||||
sample_size = quantity - (len(include_ursulas) if include_ursulas else 0)
|
||||
if not self.block_until_number_of_known_nodes_is(sample_size,
|
||||
timeout=self.DEFAULT_EXECUTION_TIMEOUT,
|
||||
learn_on_this_thread=True):
|
||||
raise ValueError("Unable to learn about sufficient Ursulas")
|
||||
return make_federated_staker_reservoir(known_nodes=self.known_nodes,
|
||||
exclude_addresses=exclude_ursulas,
|
||||
include_addresses=include_ursulas)
|
||||
else:
|
||||
if not duration_periods:
|
||||
raise ValueError("Duration periods must be provided in decentralized mode")
|
||||
return make_decentralized_staker_reservoir(staking_agent=self.staking_agent,
|
||||
duration_periods=duration_periods,
|
||||
exclude_addresses=exclude_ursulas,
|
||||
include_addresses=include_ursulas)
|
||||
|
||||
def make_cli_controller(self, crash_on_error: bool = False):
|
||||
controller = PorterCLIController(app_name=self.APP_NAME,
|
||||
crash_on_error=crash_on_error,
|
||||
interface=self.interface)
|
||||
self.controller = controller
|
||||
return controller
|
||||
|
||||
def make_rpc_controller(self, crash_on_error: bool = False):
|
||||
controller = JSONRPCController(app_name=self.APP_NAME,
|
||||
crash_on_error=crash_on_error,
|
||||
interface=self.interface)
|
||||
|
||||
self.controller = controller
|
||||
return controller
|
||||
|
||||
def make_web_controller(self, crash_on_error: bool = False, htpasswd_filepath: str = None):
|
||||
controller = WebController(app_name=self.APP_NAME,
|
||||
crash_on_error=crash_on_error,
|
||||
interface=self._interface_class(porter=self))
|
||||
self.controller = controller
|
||||
|
||||
# Register Flask Decorator
|
||||
porter_flask_control = controller.make_control_transport()
|
||||
if htpasswd_filepath:
|
||||
try:
|
||||
from flask_htpasswd import HtPasswdAuth
|
||||
except ImportError:
|
||||
raise ImportError('Porter installation is required for basic authentication '
|
||||
'- run "pip install nucypher[porter]" and try again.')
|
||||
|
||||
porter_flask_control.config['FLASK_HTPASSWD_PATH'] = htpasswd_filepath
|
||||
# ensure basic auth required for all endpoints
|
||||
porter_flask_control.config['FLASK_AUTH_ALL'] = True
|
||||
_ = HtPasswdAuth(app=porter_flask_control)
|
||||
|
||||
#
|
||||
# Porter Control HTTP Endpoints
|
||||
#
|
||||
@porter_flask_control.route('/get_ursulas', methods=['GET'])
|
||||
def get_ursulas() -> Response:
|
||||
"""Porter control endpoint for sampling Ursulas on behalf of Alice."""
|
||||
response = controller(method_name='get_ursulas', control_request=request)
|
||||
return response
|
||||
|
||||
@porter_flask_control.route("/publish_treasure_map", methods=['POST'])
|
||||
def publish_treasure_map() -> Response:
|
||||
"""Porter control endpoint for publishing a treasure map on behalf of Alice."""
|
||||
response = controller(method_name='publish_treasure_map', control_request=request)
|
||||
return response
|
||||
|
||||
@porter_flask_control.route("/revoke", methods=['POST'])
|
||||
def revoke():
|
||||
"""Porter control endpoint for off-chain revocation of a policy on behalf of Alice."""
|
||||
response = controller(method_name='revoke', control_request=request)
|
||||
return response
|
||||
|
||||
@porter_flask_control.route('/get_treasure_map', methods=['GET'])
|
||||
def get_treasure_map() -> Response:
|
||||
"""Porter control endpoint for retrieving a treasure map on behalf of Bob."""
|
||||
response = controller(method_name='get_treasure_map', control_request=request)
|
||||
return response
|
||||
|
||||
@porter_flask_control.route("/exec_work_order", methods=['POST'])
|
||||
def exec_work_order() -> Response:
|
||||
"""Porter control endpoint for executing a PRE work order on behalf of Bob."""
|
||||
response = controller(method_name='exec_work_order', control_request=request)
|
||||
return response
|
||||
|
||||
return controller
|
|
@ -26,7 +26,7 @@ from nucypher.config.constants import NUCYPHER_ENVVAR_PROVIDER_URI
|
|||
from nucypher.blockchain.eth.interfaces import BlockchainInterfaceFactory
|
||||
from nucypher.blockchain.eth.registry import InMemoryContractRegistry
|
||||
|
||||
from nucypher.characters.control.emitters import StdoutEmitter
|
||||
from nucypher.control.emitters import StdoutEmitter
|
||||
from nucypher.utilities.logging import GlobalLoggerSettings
|
||||
|
||||
from constant_sorrow.constants import NO_BLOCKCHAIN_CONNECTION
|
||||
|
|
6
setup.py
6
setup.py
|
@ -135,18 +135,20 @@ DEPLOY_REQUIRES = [
|
|||
URSULA_REQUIRES = ['prometheus_client', 'sentry-sdk'] # TODO: Consider renaming to 'monitor', etc.
|
||||
ALICE_REQUIRES = ['qrcode']
|
||||
BOB_REQUIRES = ['qrcode']
|
||||
PORTER_REQUIRES = ['flask-htpasswd'] # needed for basic authentication
|
||||
|
||||
EXTRAS = {
|
||||
|
||||
# Admin
|
||||
'dev': DEV_REQUIRES + URSULA_REQUIRES + ALICE_REQUIRES,
|
||||
'dev': DEV_REQUIRES + URSULA_REQUIRES + ALICE_REQUIRES + PORTER_REQUIRES,
|
||||
'benchmark': DEV_REQUIRES + BENCHMARK_REQUIRES,
|
||||
'deploy': DEPLOY_REQUIRES,
|
||||
|
||||
# User
|
||||
'ursula': URSULA_REQUIRES,
|
||||
'alice': ALICE_REQUIRES,
|
||||
'bob': BOB_REQUIRES
|
||||
'bob': BOB_REQUIRES,
|
||||
'porter': PORTER_REQUIRES
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from base64 import b64encode
|
||||
|
||||
import pytest
|
||||
|
||||
from nucypher.characters.control.specifications.fields import TreasureMap
|
||||
from nucypher.control.specifications.exceptions import InvalidInputData
|
||||
|
||||
|
||||
def test_treasure_map(enacted_blockchain_policy):
|
||||
treasure_map = enacted_blockchain_policy.treasure_map
|
||||
|
||||
field = TreasureMap(federated_only=False) # decentralized context
|
||||
serialized = field._serialize(value=treasure_map, attr=None, obj=None)
|
||||
assert serialized == b64encode(bytes(treasure_map)).decode()
|
||||
|
||||
deserialized = field._deserialize(value=serialized, attr=None, data=None)
|
||||
assert deserialized == bytes(treasure_map)
|
||||
|
||||
field._validate(value=bytes(treasure_map))
|
||||
|
||||
with pytest.raises(InvalidInputData):
|
||||
field._validate(value=b"TreasureMap")
|
|
@ -22,7 +22,7 @@ from unittest import mock
|
|||
import os
|
||||
import pytest
|
||||
|
||||
from nucypher.characters.control.emitters import JSONRPCStdoutEmitter
|
||||
from nucypher.control.emitters import JSONRPCStdoutEmitter
|
||||
from nucypher.characters.lawful import Ursula
|
||||
from nucypher.cli import utils
|
||||
from nucypher.cli.literature import SUCCESSFUL_DESTRUCTION, COLLECT_NUCYPHER_PASSWORD
|
||||
|
|
|
@ -0,0 +1,320 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from nucypher.characters.lawful import Ursula
|
||||
from nucypher.cli.literature import (
|
||||
PORTER_RUN_MESSAGE,
|
||||
BOTH_TLS_KEY_AND_CERTIFICATION_MUST_BE_PROVIDED,
|
||||
BASIC_AUTH_REQUIRES_HTTPS
|
||||
)
|
||||
from nucypher.cli.main import nucypher_cli
|
||||
from nucypher.config.constants import TEMPORARY_DOMAIN
|
||||
from nucypher.utilities.porter.porter import Porter
|
||||
from tests.constants import TEST_PROVIDER_URI
|
||||
from tests.utils.ursula import select_test_port
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def federated_teacher_uri(mocker, federated_ursulas):
|
||||
teacher = list(federated_ursulas)[0]
|
||||
teacher_uri = teacher.seed_node_metadata(as_teacher_uri=True)
|
||||
mocker.patch.object(Ursula, 'from_teacher_uri', return_value=teacher)
|
||||
yield teacher_uri
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def blockchain_teacher_uri(mocker, blockchain_ursulas):
|
||||
teacher = list(blockchain_ursulas)[0]
|
||||
teacher_uri = teacher.seed_node_metadata(as_teacher_uri=True)
|
||||
mocker.patch.object(Ursula, 'from_teacher_uri', return_value=teacher)
|
||||
yield teacher_uri
|
||||
|
||||
|
||||
def test_federated_porter_cli_run_simple(click_runner, federated_ursulas, federated_teacher_uri):
|
||||
porter_run_command = ('porter', 'run',
|
||||
'--dry-run',
|
||||
'--federated-only',
|
||||
'--teacher', federated_teacher_uri)
|
||||
result = click_runner.invoke(nucypher_cli, porter_run_command, catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
||||
output = result.output
|
||||
assert f"Network: {TEMPORARY_DOMAIN}" in output
|
||||
assert PORTER_RUN_MESSAGE.format(http_scheme="http", http_port=Porter.DEFAULT_PORT) in output
|
||||
|
||||
# Non-default port
|
||||
non_default_port = select_test_port()
|
||||
porter_run_command = ('porter', 'run',
|
||||
'--dry-run',
|
||||
'--federated-only',
|
||||
'--http-port', non_default_port,
|
||||
'--teacher', federated_teacher_uri)
|
||||
result = click_runner.invoke(nucypher_cli, porter_run_command, catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
||||
output = result.output
|
||||
assert f"Network: {TEMPORARY_DOMAIN}" in output
|
||||
assert PORTER_RUN_MESSAGE.format(http_scheme="http", http_port=non_default_port) in output
|
||||
|
||||
|
||||
def test_federated_porter_cli_run_teacher_must_be_provided(click_runner, federated_ursulas):
|
||||
porter_run_command = ('porter', 'run',
|
||||
'--dry-run',
|
||||
'--federated-only')
|
||||
|
||||
result = click_runner.invoke(nucypher_cli, porter_run_command, catch_exceptions=False)
|
||||
assert result.exit_code != 0
|
||||
assert f"--teacher is required" in result.output
|
||||
|
||||
|
||||
def test_federated_porter_cli_run_tls_filepath_and_certificate(click_runner,
|
||||
federated_ursulas,
|
||||
tempfile_path,
|
||||
temp_dir_path,
|
||||
federated_teacher_uri):
|
||||
porter_run_command = ('porter', 'run',
|
||||
'--dry-run',
|
||||
'--federated-only',
|
||||
'--teacher', federated_teacher_uri,
|
||||
'--tls-key-filepath', tempfile_path) # only tls-key provided
|
||||
result = click_runner.invoke(nucypher_cli, porter_run_command, catch_exceptions=False)
|
||||
assert result.exit_code != 0 # both --tls-key-filepath and --tls-certificate-filepath must be provided for TLS
|
||||
assert BOTH_TLS_KEY_AND_CERTIFICATION_MUST_BE_PROVIDED in result.output
|
||||
|
||||
porter_run_command = ('porter', 'run',
|
||||
'--dry-run',
|
||||
'--federated-only',
|
||||
'--teacher', federated_teacher_uri,
|
||||
'--tls-certificate-filepath', tempfile_path) # only certificate provided
|
||||
result = click_runner.invoke(nucypher_cli, porter_run_command, catch_exceptions=False)
|
||||
assert result.exit_code != 0 # both --tls-key-filepath and --tls-certificate-filepath must be provided for TLS
|
||||
assert BOTH_TLS_KEY_AND_CERTIFICATION_MUST_BE_PROVIDED in result.output
|
||||
|
||||
#
|
||||
# tls-key and certificate filepaths must exist
|
||||
#
|
||||
assert Path(tempfile_path).exists() # temp file exists
|
||||
|
||||
non_existent_path = (Path(temp_dir_path) / 'non_existent_file')
|
||||
assert not non_existent_path.exists()
|
||||
# tls-key-filepath does not exist
|
||||
porter_run_command = ('porter', 'run',
|
||||
'--dry-run',
|
||||
'--federated-only',
|
||||
'--teacher', federated_teacher_uri,
|
||||
'--tls-certificate-filepath', tempfile_path,
|
||||
'--tls-key-filepath', str(non_existent_path.absolute()))
|
||||
result = click_runner.invoke(nucypher_cli, porter_run_command, catch_exceptions=False)
|
||||
assert result.exit_code != 0
|
||||
output = result.output
|
||||
assert f"'--tls-key-filepath': File '{non_existent_path.absolute()}' does not exist" in output
|
||||
|
||||
# tls-certificate-filepath does not exist
|
||||
porter_run_command = ('porter', 'run',
|
||||
'--dry-run',
|
||||
'--federated-only',
|
||||
'--teacher', federated_teacher_uri,
|
||||
'--tls-certificate-filepath', str(non_existent_path.absolute()),
|
||||
'--tls-key-filepath', tempfile_path)
|
||||
result = click_runner.invoke(nucypher_cli, porter_run_command, catch_exceptions=False)
|
||||
assert result.exit_code != 0
|
||||
output = result.output
|
||||
assert f"'--tls-certificate-filepath': File '{non_existent_path.absolute()}' does not exist" in output
|
||||
|
||||
|
||||
def test_federated_cli_run_https(click_runner, federated_ursulas, temp_dir_path, federated_teacher_uri):
|
||||
tls_key_path = Path(temp_dir_path) / 'key.pem'
|
||||
_write_random_data(tls_key_path)
|
||||
certificate_file_path = Path(temp_dir_path) / 'fullchain.pem'
|
||||
_write_random_data(certificate_file_path)
|
||||
|
||||
porter_run_command = ('porter', 'run',
|
||||
'--dry-run',
|
||||
'--federated-only',
|
||||
'--teacher', federated_teacher_uri,
|
||||
'--tls-key-filepath', tls_key_path,
|
||||
'--tls-certificate-filepath', certificate_file_path)
|
||||
result = click_runner.invoke(nucypher_cli, porter_run_command, catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
||||
assert PORTER_RUN_MESSAGE.format(http_scheme="https", http_port=Porter.DEFAULT_PORT) in result.output
|
||||
|
||||
|
||||
def test_federated_cli_run_https_basic_auth(click_runner,
|
||||
federated_ursulas,
|
||||
federated_teacher_uri,
|
||||
temp_dir_path,
|
||||
basic_auth_file):
|
||||
tls_key_path = Path(temp_dir_path) / 'key.pem'
|
||||
_write_random_data(tls_key_path)
|
||||
certificate_file_path = Path(temp_dir_path) / 'fullchain.pem'
|
||||
_write_random_data(certificate_file_path)
|
||||
|
||||
porter_run_command = ('porter', 'run',
|
||||
'--dry-run',
|
||||
'--federated-only',
|
||||
'--teacher', federated_teacher_uri,
|
||||
'--tls-key-filepath', tls_key_path,
|
||||
'--tls-certificate-filepath', certificate_file_path,
|
||||
'--basic-auth-filepath', basic_auth_file)
|
||||
result = click_runner.invoke(nucypher_cli, porter_run_command, catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
||||
assert "Basic Authentication enabled" in result.output
|
||||
|
||||
|
||||
def test_blockchain_porter_cli_run_simple(click_runner,
|
||||
blockchain_ursulas,
|
||||
testerchain,
|
||||
agency_local_registry,
|
||||
blockchain_teacher_uri):
|
||||
porter_run_command = ('porter', 'run',
|
||||
'--dry-run',
|
||||
'--network', TEMPORARY_DOMAIN,
|
||||
'--provider', TEST_PROVIDER_URI,
|
||||
'--registry-filepath', agency_local_registry.filepath,
|
||||
'--teacher', blockchain_teacher_uri)
|
||||
result = click_runner.invoke(nucypher_cli, porter_run_command, catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
||||
output = result.output
|
||||
assert f"Network: {TEMPORARY_DOMAIN}" in output
|
||||
assert f"Provider: {TEST_PROVIDER_URI}" in output
|
||||
assert PORTER_RUN_MESSAGE.format(http_scheme="http", http_port=Porter.DEFAULT_PORT) in output
|
||||
|
||||
# Non-default port
|
||||
non_default_port = select_test_port()
|
||||
porter_run_command = ('porter', 'run',
|
||||
'--dry-run',
|
||||
'--network', TEMPORARY_DOMAIN,
|
||||
'--provider', TEST_PROVIDER_URI,
|
||||
'--registry-filepath', agency_local_registry.filepath,
|
||||
'--http-port', non_default_port,
|
||||
'--teacher', blockchain_teacher_uri)
|
||||
result = click_runner.invoke(nucypher_cli, porter_run_command, catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
||||
output = result.output
|
||||
assert f"Network: {TEMPORARY_DOMAIN}" in output
|
||||
assert f"Provider: {TEST_PROVIDER_URI}" in output
|
||||
assert PORTER_RUN_MESSAGE.format(http_scheme="http", http_port=non_default_port) in output
|
||||
|
||||
|
||||
def test_blockchain_porter_cli_run_provider_required(click_runner,
|
||||
blockchain_ursulas,
|
||||
testerchain,
|
||||
agency_local_registry,
|
||||
blockchain_teacher_uri):
|
||||
porter_run_command = ('porter', 'run',
|
||||
'--dry-run',
|
||||
'--network', TEMPORARY_DOMAIN,
|
||||
'--registry-filepath', agency_local_registry.filepath,
|
||||
'--teacher', blockchain_teacher_uri)
|
||||
|
||||
result = click_runner.invoke(nucypher_cli, porter_run_command, catch_exceptions=False)
|
||||
|
||||
assert result.exit_code != 0
|
||||
assert "--provider is required" in result.output
|
||||
|
||||
|
||||
def test_blockchain_porter_cli_run_network_defaults_to_mainnet(click_runner,
|
||||
blockchain_ursulas,
|
||||
testerchain,
|
||||
agency_local_registry,
|
||||
blockchain_teacher_uri):
|
||||
porter_run_command = ('porter', 'run',
|
||||
'--dry-run',
|
||||
'--provider', TEST_PROVIDER_URI,
|
||||
'--registry-filepath', agency_local_registry.filepath,
|
||||
'--teacher', blockchain_teacher_uri)
|
||||
|
||||
result = click_runner.invoke(nucypher_cli, porter_run_command, catch_exceptions=False)
|
||||
|
||||
assert result.exit_code != 0
|
||||
# there is no 'mainnet' network for decentralized testing
|
||||
assert "'mainnet' is not a NuCypher Network" in result.output
|
||||
|
||||
|
||||
def test_blockchain_porter_cli_run_https(click_runner,
|
||||
blockchain_ursulas,
|
||||
testerchain,
|
||||
agency_local_registry,
|
||||
temp_dir_path,
|
||||
blockchain_teacher_uri):
|
||||
tls_key_path = Path(temp_dir_path) / 'key.pem'
|
||||
_write_random_data(tls_key_path)
|
||||
certificate_file_path = Path(temp_dir_path) / 'fullchain.pem'
|
||||
_write_random_data(certificate_file_path)
|
||||
|
||||
porter_run_command = ('porter', 'run',
|
||||
'--dry-run',
|
||||
'--network', TEMPORARY_DOMAIN,
|
||||
'--provider', TEST_PROVIDER_URI,
|
||||
'--registry-filepath', agency_local_registry.filepath,
|
||||
'--teacher', blockchain_teacher_uri,
|
||||
'--tls-key-filepath', tls_key_path,
|
||||
'--tls-certificate-filepath', certificate_file_path)
|
||||
|
||||
result = click_runner.invoke(nucypher_cli, porter_run_command, catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
||||
assert PORTER_RUN_MESSAGE.format(http_scheme="https", http_port=Porter.DEFAULT_PORT) in result.output
|
||||
|
||||
|
||||
def test_blockchain_porter_cli_run_https_basic_auth(click_runner,
|
||||
blockchain_ursulas,
|
||||
blockchain_teacher_uri,
|
||||
testerchain,
|
||||
agency_local_registry,
|
||||
temp_dir_path,
|
||||
basic_auth_file
|
||||
):
|
||||
tls_key_path = Path(temp_dir_path) / 'key.pem'
|
||||
_write_random_data(tls_key_path)
|
||||
certificate_file_path = Path(temp_dir_path) / 'fullchain.pem'
|
||||
_write_random_data(certificate_file_path)
|
||||
|
||||
# Basic Auth requires https - missing both tls parameters
|
||||
porter_run_command = ('porter', 'run',
|
||||
'--dry-run',
|
||||
'--network', TEMPORARY_DOMAIN,
|
||||
'--provider', TEST_PROVIDER_URI,
|
||||
'--registry-filepath', agency_local_registry.filepath,
|
||||
'--teacher', blockchain_teacher_uri,
|
||||
'--basic-auth-filepath', basic_auth_file)
|
||||
|
||||
result = click_runner.invoke(nucypher_cli, porter_run_command, catch_exceptions=False)
|
||||
assert result.exit_code != 0
|
||||
assert BASIC_AUTH_REQUIRES_HTTPS in result.output
|
||||
|
||||
# Basic Auth
|
||||
porter_run_command = ('porter', 'run',
|
||||
'--dry-run',
|
||||
'--network', TEMPORARY_DOMAIN,
|
||||
'--provider', TEST_PROVIDER_URI,
|
||||
'--registry-filepath', agency_local_registry.filepath,
|
||||
'--teacher', blockchain_teacher_uri,
|
||||
'--tls-key-filepath', tls_key_path,
|
||||
'--tls-certificate-filepath', certificate_file_path,
|
||||
'--basic-auth-filepath', basic_auth_file)
|
||||
|
||||
result = click_runner.invoke(nucypher_cli, porter_run_command, catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
||||
assert "Basic Authentication enabled" in result.output
|
||||
|
||||
|
||||
def _write_random_data(filepath: Path):
|
||||
with filepath.open('wb') as file:
|
||||
file.write(os.urandom(24))
|
|
@ -21,7 +21,7 @@ import pytest
|
|||
import sys
|
||||
from io import StringIO
|
||||
|
||||
from nucypher.characters.control.emitters import StdoutEmitter
|
||||
from nucypher.control.emitters import StdoutEmitter
|
||||
from nucypher.cli.processes import UrsulaCommandProtocol
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
#
|
||||
# Web
|
||||
#
|
||||
@pytest.fixture(scope='module')
|
||||
def blockchain_porter_web_controller(blockchain_porter):
|
||||
web_controller = blockchain_porter.make_web_controller(crash_on_error=True)
|
||||
yield web_controller.test_client()
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def blockchain_porter_basic_auth_web_controller(blockchain_porter, basic_auth_file):
|
||||
web_controller = blockchain_porter.make_web_controller(crash_on_error=True, htpasswd_filepath=basic_auth_file)
|
||||
yield web_controller.test_client()
|
||||
|
||||
|
||||
#
|
||||
# RPC
|
||||
#
|
||||
@pytest.fixture(scope='module')
|
||||
def blockchain_porter_rpc_controller(blockchain_porter):
|
||||
rpc_controller = blockchain_porter.make_rpc_controller(crash_on_error=True)
|
||||
yield rpc_controller.test_client()
|
|
@ -0,0 +1,163 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
from base64 import b64encode, b64decode
|
||||
|
||||
import pytest
|
||||
from nucypher.crypto.umbral_adapter import PublicKey
|
||||
|
||||
from nucypher.crypto.constants import HRAC_LENGTH
|
||||
from nucypher.crypto.powers import DecryptingPower
|
||||
from nucypher.network.nodes import Learner
|
||||
from nucypher.policy.maps import TreasureMap
|
||||
from tests.utils.middleware import MockRestMiddleware
|
||||
|
||||
|
||||
# should always be first test due to checks on response id
|
||||
from tests.utils.policy import work_order_setup
|
||||
|
||||
|
||||
def test_get_ursulas(blockchain_porter_rpc_controller, blockchain_ursulas):
|
||||
method = 'get_ursulas'
|
||||
expected_response_id = 0
|
||||
|
||||
quantity = 4
|
||||
duration = 2
|
||||
blockchain_ursulas_list = list(blockchain_ursulas)
|
||||
include_ursulas = [blockchain_ursulas_list[0].checksum_address, blockchain_ursulas_list[1].checksum_address]
|
||||
exclude_ursulas = [blockchain_ursulas_list[2].checksum_address, blockchain_ursulas_list[3].checksum_address]
|
||||
|
||||
get_ursulas_params = {
|
||||
'quantity': quantity,
|
||||
'duration_periods': duration,
|
||||
'include_ursulas': include_ursulas,
|
||||
'exclude_ursulas': exclude_ursulas
|
||||
}
|
||||
|
||||
#
|
||||
# Success
|
||||
#
|
||||
request_data = {'method': method, 'params': get_ursulas_params}
|
||||
response = blockchain_porter_rpc_controller.send(request_data)
|
||||
expected_response_id += 1
|
||||
assert response.success
|
||||
assert response.id == expected_response_id
|
||||
ursulas_info = response.data['result']['ursulas']
|
||||
returned_ursula_addresses = {ursula_info['checksum_address'] for ursula_info in ursulas_info} # ensure no repeats
|
||||
assert len(returned_ursula_addresses) == quantity
|
||||
for address in include_ursulas:
|
||||
assert address in returned_ursula_addresses
|
||||
for address in exclude_ursulas:
|
||||
assert address not in returned_ursula_addresses
|
||||
|
||||
# Confirm the same message send works again, with a unique ID
|
||||
request_data = {'method': method, 'params': get_ursulas_params}
|
||||
rpc_response = blockchain_porter_rpc_controller.send(request=request_data)
|
||||
expected_response_id += 1
|
||||
assert rpc_response.success
|
||||
assert rpc_response.id == expected_response_id
|
||||
|
||||
#
|
||||
# Failure case
|
||||
#
|
||||
failed_ursula_params = dict(get_ursulas_params)
|
||||
failed_ursula_params['quantity'] = len(blockchain_ursulas_list) + 1 # too many to get
|
||||
request_data = {'method': method, 'params': failed_ursula_params}
|
||||
with pytest.raises(Learner.NotEnoughNodes):
|
||||
blockchain_porter_rpc_controller.send(request_data)
|
||||
|
||||
|
||||
def test_publish_and_get_treasure_map(blockchain_porter_rpc_controller,
|
||||
blockchain_alice,
|
||||
blockchain_bob,
|
||||
idle_blockchain_policy):
|
||||
# ensure that random treasure map cannot be obtained since not available
|
||||
with pytest.raises(TreasureMap.NowhereToBeFound):
|
||||
random_bob_encrypting_key = PublicKey.from_bytes(
|
||||
bytes.fromhex("026d1f4ce5b2474e0dae499d6737a8d987ed3c9ab1a55e00f57ad2d8e81fe9e9ac"))
|
||||
random_treasure_map_id = "93a9482bdf3b4f2e9df906a35144ca84"
|
||||
assert len(bytes.fromhex(random_treasure_map_id)) == HRAC_LENGTH # non-federated is 16 bytes
|
||||
get_treasure_map_params = {
|
||||
'treasure_map_id': random_treasure_map_id,
|
||||
'bob_encrypting_key': bytes(random_bob_encrypting_key).hex()
|
||||
}
|
||||
request_data = {'method': 'get_treasure_map', 'params': get_treasure_map_params}
|
||||
blockchain_porter_rpc_controller.send(request_data)
|
||||
|
||||
blockchain_bob_encrypting_key = blockchain_bob.public_keys(DecryptingPower)
|
||||
# try publishing a new policy
|
||||
network_middleware = MockRestMiddleware()
|
||||
enacted_policy = idle_blockchain_policy.enact(network_middleware=network_middleware,
|
||||
publish_treasure_map=False) # enact but don't publish
|
||||
treasure_map = enacted_policy.treasure_map
|
||||
publish_treasure_map_params = {
|
||||
'treasure_map': b64encode(bytes(treasure_map)).decode(),
|
||||
'bob_encrypting_key': bytes(blockchain_bob_encrypting_key).hex()
|
||||
}
|
||||
request_data = {'method': 'publish_treasure_map', 'params': publish_treasure_map_params}
|
||||
response = blockchain_porter_rpc_controller.send(request_data)
|
||||
assert response.success
|
||||
|
||||
# try getting the recently published treasure map
|
||||
map_id = blockchain_bob.construct_map_id(blockchain_alice.stamp,
|
||||
enacted_policy.label)
|
||||
get_treasure_map_params = {
|
||||
'treasure_map_id': map_id,
|
||||
'bob_encrypting_key': bytes(blockchain_bob_encrypting_key).hex()
|
||||
}
|
||||
request_data = {'method': 'get_treasure_map', 'params': get_treasure_map_params}
|
||||
response = blockchain_porter_rpc_controller.send(request_data)
|
||||
assert response.success
|
||||
assert response.content['treasure_map'] == b64encode(bytes(treasure_map)).decode()
|
||||
|
||||
|
||||
def test_exec_work_order(blockchain_porter_rpc_controller,
|
||||
random_blockchain_policy,
|
||||
blockchain_ursulas,
|
||||
blockchain_bob,
|
||||
blockchain_alice,
|
||||
get_random_checksum_address):
|
||||
method = 'exec_work_order'
|
||||
# Setup
|
||||
network_middleware = MockRestMiddleware()
|
||||
# enact new random policy since idle_blockchain_policy/enacted_blockchain_policy already modified in previous tests
|
||||
enacted_policy = random_blockchain_policy.enact(network_middleware=network_middleware,
|
||||
publish_treasure_map=False) # enact but don't publish
|
||||
ursula_address, work_order = work_order_setup(enacted_policy,
|
||||
blockchain_ursulas,
|
||||
blockchain_bob,
|
||||
blockchain_alice)
|
||||
work_order_payload_b64 = b64encode(work_order.payload()).decode()
|
||||
|
||||
exec_work_order_params = {
|
||||
'ursula': ursula_address,
|
||||
'work_order_payload': work_order_payload_b64
|
||||
}
|
||||
request_data = {'method': method, 'params': exec_work_order_params}
|
||||
response = blockchain_porter_rpc_controller.send(request_data)
|
||||
assert response.success
|
||||
work_order_result = response.content['work_order_result']
|
||||
assert work_order_result
|
||||
|
||||
# Failure
|
||||
exec_work_order_params = {
|
||||
'ursula': get_random_checksum_address(), # unknown ursula
|
||||
'work_order_payload': work_order_payload_b64
|
||||
}
|
||||
with pytest.raises(Learner.NotEnoughNodes):
|
||||
request_data = {'method': method, 'params': exec_work_order_params}
|
||||
blockchain_porter_rpc_controller.send(request_data)
|
|
@ -0,0 +1,218 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import json
|
||||
from urllib.parse import urlencode
|
||||
from base64 import b64encode
|
||||
|
||||
import pytest
|
||||
from nucypher.crypto.umbral_adapter import PublicKey
|
||||
|
||||
from nucypher.crypto.constants import HRAC_LENGTH
|
||||
from nucypher.crypto.powers import DecryptingPower
|
||||
from nucypher.network.nodes import Learner
|
||||
from nucypher.policy.maps import TreasureMap
|
||||
from tests.utils.middleware import MockRestMiddleware
|
||||
from tests.utils.policy import work_order_setup
|
||||
|
||||
|
||||
def test_get_ursulas(blockchain_porter_web_controller, blockchain_ursulas):
|
||||
# Send bad data to assert error return
|
||||
response = blockchain_porter_web_controller.get('/get_ursulas', data=json.dumps({'bad': 'input'}))
|
||||
assert response.status_code == 400
|
||||
|
||||
quantity = 4
|
||||
duration = 2
|
||||
blockchain_ursulas_list = list(blockchain_ursulas)
|
||||
include_ursulas = [blockchain_ursulas_list[0].checksum_address, blockchain_ursulas_list[1].checksum_address]
|
||||
exclude_ursulas = [blockchain_ursulas_list[2].checksum_address, blockchain_ursulas_list[3].checksum_address]
|
||||
|
||||
get_ursulas_params = {
|
||||
'quantity': quantity,
|
||||
'duration_periods': duration,
|
||||
'include_ursulas': include_ursulas,
|
||||
'exclude_ursulas': exclude_ursulas
|
||||
}
|
||||
|
||||
#
|
||||
# Success
|
||||
#
|
||||
response = blockchain_porter_web_controller.get('/get_ursulas', data=json.dumps(get_ursulas_params))
|
||||
assert response.status_code == 200
|
||||
|
||||
response_data = json.loads(response.data)
|
||||
ursulas_info = response_data['result']['ursulas']
|
||||
returned_ursula_addresses = {ursula_info['checksum_address'] for ursula_info in ursulas_info} # ensure no repeats
|
||||
assert len(returned_ursula_addresses) == quantity
|
||||
for address in include_ursulas:
|
||||
assert address in returned_ursula_addresses
|
||||
for address in exclude_ursulas:
|
||||
assert address not in returned_ursula_addresses
|
||||
|
||||
#
|
||||
# Test Query parameters
|
||||
#
|
||||
response = blockchain_porter_web_controller.get(f'/get_ursulas?quantity={quantity}'
|
||||
f'&duration_periods={duration}'
|
||||
f'&include_ursulas={",".join(include_ursulas)}'
|
||||
f'&exclude_ursulas={",".join(exclude_ursulas)}')
|
||||
assert response.status_code == 200
|
||||
|
||||
response_data = json.loads(response.data)
|
||||
ursulas_info = response_data['result']['ursulas']
|
||||
returned_ursula_addresses = {ursula_info['checksum_address'] for ursula_info in ursulas_info} # ensure no repeats
|
||||
assert len(returned_ursula_addresses) == quantity
|
||||
for address in include_ursulas:
|
||||
assert address in returned_ursula_addresses
|
||||
for address in exclude_ursulas:
|
||||
assert address not in returned_ursula_addresses
|
||||
|
||||
#
|
||||
# Failure case
|
||||
#
|
||||
failed_ursula_params = dict(get_ursulas_params)
|
||||
failed_ursula_params['quantity'] = len(blockchain_ursulas_list) + 1 # too many to get
|
||||
with pytest.raises(Learner.NotEnoughNodes):
|
||||
blockchain_porter_web_controller.get('/get_ursulas', data=json.dumps(failed_ursula_params))
|
||||
|
||||
|
||||
def test_publish_and_get_treasure_map(blockchain_porter_web_controller,
|
||||
blockchain_alice,
|
||||
blockchain_bob,
|
||||
idle_blockchain_policy):
|
||||
# Send bad data to assert error return
|
||||
response = blockchain_porter_web_controller.get('/get_treasure_map', data=json.dumps({'bad': 'input'}))
|
||||
assert response.status_code == 400
|
||||
|
||||
response = blockchain_porter_web_controller.post('/publish_treasure_map', data=json.dumps({'bad': 'input'}))
|
||||
assert response.status_code == 400
|
||||
|
||||
# ensure that random treasure map cannot be obtained since not available
|
||||
with pytest.raises(TreasureMap.NowhereToBeFound):
|
||||
random_bob_encrypting_key = PublicKey.from_bytes(
|
||||
bytes.fromhex("026d1f4ce5b2474e0dae499d6737a8d987ed3c9ab1a55e00f57ad2d8e81fe9e9ac"))
|
||||
random_treasure_map_id = "93a9482bdf3b4f2e9df906a35144ca84"
|
||||
assert len(bytes.fromhex(random_treasure_map_id)) == HRAC_LENGTH # non-federated is 16 bytes
|
||||
get_treasure_map_params = {
|
||||
'treasure_map_id': random_treasure_map_id,
|
||||
'bob_encrypting_key': bytes(random_bob_encrypting_key).hex()
|
||||
}
|
||||
blockchain_porter_web_controller.get('/get_treasure_map',
|
||||
data=json.dumps(get_treasure_map_params))
|
||||
|
||||
blockchain_bob_encrypting_key = blockchain_bob.public_keys(DecryptingPower)
|
||||
# try publishing a new policy
|
||||
network_middleware = MockRestMiddleware()
|
||||
enacted_policy = idle_blockchain_policy.enact(network_middleware=network_middleware,
|
||||
publish_treasure_map=False) # enact but don't publish
|
||||
treasure_map = enacted_policy.treasure_map
|
||||
publish_treasure_map_params = {
|
||||
'treasure_map': b64encode(bytes(treasure_map)).decode(),
|
||||
'bob_encrypting_key': bytes(blockchain_bob_encrypting_key).hex()
|
||||
}
|
||||
# this query string is long (~6840 characters), but still seems to work ...
|
||||
# json data payload is tested in federated tests
|
||||
response = blockchain_porter_web_controller.post(f'/publish_treasure_map'
|
||||
f'?{urlencode(publish_treasure_map_params)}')
|
||||
|
||||
assert response.status_code == 200
|
||||
response_data = json.loads(response.data)
|
||||
assert response_data['result']['published']
|
||||
|
||||
# try getting the recently published treasure map
|
||||
map_id = blockchain_bob.construct_map_id(blockchain_alice.stamp,
|
||||
enacted_policy.label)
|
||||
get_treasure_map_params = {
|
||||
'treasure_map_id': map_id,
|
||||
'bob_encrypting_key': bytes(blockchain_bob_encrypting_key).hex()
|
||||
}
|
||||
response = blockchain_porter_web_controller.get('/get_treasure_map',
|
||||
data=json.dumps(get_treasure_map_params))
|
||||
assert response.status_code == 200
|
||||
response_data = json.loads(response.data)
|
||||
assert response_data['result']['treasure_map'] == b64encode(bytes(treasure_map)).decode()
|
||||
|
||||
# try getting recently published treasure map using query parameters
|
||||
response = blockchain_porter_web_controller.get(f'/get_treasure_map'
|
||||
f'?{urlencode(get_treasure_map_params)}')
|
||||
assert response.status_code == 200
|
||||
response_data = json.loads(response.data)
|
||||
assert response_data['result']['treasure_map'] == b64encode(bytes(treasure_map)).decode()
|
||||
|
||||
|
||||
def test_exec_work_order(blockchain_porter_web_controller,
|
||||
random_blockchain_policy,
|
||||
blockchain_ursulas,
|
||||
blockchain_bob,
|
||||
blockchain_alice,
|
||||
get_random_checksum_address):
|
||||
# Send bad data to assert error return
|
||||
response = blockchain_porter_web_controller.post('/exec_work_order', data=json.dumps({'bad': 'input'}))
|
||||
assert response.status_code == 400
|
||||
|
||||
# Setup
|
||||
network_middleware = MockRestMiddleware()
|
||||
# enact new random policy since idle_blockchain_policy/enacted_blockchain_policy already modified in previous tests
|
||||
enacted_policy = random_blockchain_policy.enact(network_middleware=network_middleware,
|
||||
publish_treasure_map=False) # enact but don't publish
|
||||
ursula_address, work_order = work_order_setup(enacted_policy,
|
||||
blockchain_ursulas,
|
||||
blockchain_bob,
|
||||
blockchain_alice)
|
||||
work_order_payload_b64 = b64encode(work_order.payload()).decode()
|
||||
|
||||
exec_work_order_params = {
|
||||
'ursula': ursula_address,
|
||||
'work_order_payload': work_order_payload_b64
|
||||
}
|
||||
response = blockchain_porter_web_controller.post(f'/exec_work_order'
|
||||
f'?{urlencode(exec_work_order_params)}')
|
||||
assert response.status_code == 200
|
||||
|
||||
response_data = json.loads(response.data)
|
||||
work_order_result = response_data['result']['work_order_result']
|
||||
assert work_order_result
|
||||
|
||||
# Failure
|
||||
exec_work_order_params = {
|
||||
'ursula': get_random_checksum_address(), # unknown ursula
|
||||
'work_order_payload': work_order_payload_b64
|
||||
}
|
||||
with pytest.raises(Learner.NotEnoughNodes):
|
||||
blockchain_porter_web_controller.post('/exec_work_order', data=json.dumps(exec_work_order_params))
|
||||
|
||||
|
||||
def test_get_ursulas_basic_auth(blockchain_porter_basic_auth_web_controller):
|
||||
quantity = 4
|
||||
duration = 2
|
||||
get_ursulas_params = {
|
||||
'quantity': quantity,
|
||||
'duration_periods': duration,
|
||||
}
|
||||
|
||||
response = blockchain_porter_basic_auth_web_controller.get('/get_ursulas', data=json.dumps(get_ursulas_params))
|
||||
assert response.status_code == 401 # user is unauthorized
|
||||
|
||||
credentials = b64encode(b"admin:admin").decode('utf-8')
|
||||
response = blockchain_porter_basic_auth_web_controller.get('/get_ursulas',
|
||||
data=json.dumps(get_ursulas_params),
|
||||
headers={"Authorization": f"Basic {credentials}"})
|
||||
assert response.status_code == 200 # success - access allowed
|
||||
response_data = json.loads(response.data)
|
||||
ursulas_info = response_data['result']['ursulas']
|
||||
returned_ursula_addresses = {ursula_info['checksum_address'] for ursula_info in ursulas_info} # ensure no repeats
|
||||
assert len(returned_ursula_addresses) == quantity
|
|
@ -0,0 +1,123 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from nucypher.crypto.umbral_adapter import PublicKey
|
||||
|
||||
from nucypher.crypto.constants import HRAC_LENGTH
|
||||
from nucypher.crypto.powers import DecryptingPower
|
||||
from nucypher.policy.maps import TreasureMap
|
||||
from tests.utils.middleware import MockRestMiddleware
|
||||
from tests.utils.policy import work_order_setup
|
||||
|
||||
|
||||
def test_get_ursulas(blockchain_porter, blockchain_ursulas):
|
||||
# simple
|
||||
quantity = 4
|
||||
duration = 2
|
||||
ursulas_info = blockchain_porter.get_ursulas(quantity=quantity, duration_periods=duration)
|
||||
returned_ursula_addresses = {ursula_info.checksum_address for ursula_info in ursulas_info}
|
||||
assert len(returned_ursula_addresses) == quantity # ensure no repeats
|
||||
|
||||
blockchain_ursulas_list = list(blockchain_ursulas)
|
||||
|
||||
# include specific ursulas
|
||||
include_ursulas = [blockchain_ursulas_list[0].checksum_address, blockchain_ursulas_list[1].checksum_address]
|
||||
ursulas_info = blockchain_porter.get_ursulas(quantity=quantity,
|
||||
duration_periods=duration,
|
||||
include_ursulas=include_ursulas)
|
||||
returned_ursula_addresses = {ursula_info.checksum_address for ursula_info in ursulas_info}
|
||||
assert len(returned_ursula_addresses) == quantity
|
||||
for address in include_ursulas:
|
||||
assert address in returned_ursula_addresses
|
||||
|
||||
# exclude specific ursulas
|
||||
number_to_exclude = len(blockchain_ursulas_list) - 4
|
||||
exclude_ursulas = []
|
||||
for i in range(number_to_exclude):
|
||||
exclude_ursulas.append(blockchain_ursulas_list[i].checksum_address)
|
||||
ursulas_info = blockchain_porter.get_ursulas(quantity=quantity,
|
||||
duration_periods=duration,
|
||||
exclude_ursulas=exclude_ursulas)
|
||||
returned_ursula_addresses = {ursula_info.checksum_address for ursula_info in ursulas_info}
|
||||
assert len(returned_ursula_addresses) == quantity
|
||||
for address in exclude_ursulas:
|
||||
assert address not in returned_ursula_addresses
|
||||
|
||||
# include and exclude
|
||||
include_ursulas = [blockchain_ursulas_list[0].checksum_address, blockchain_ursulas_list[1].checksum_address]
|
||||
exclude_ursulas = [blockchain_ursulas_list[2].checksum_address, blockchain_ursulas_list[3].checksum_address]
|
||||
ursulas_info = blockchain_porter.get_ursulas(quantity=quantity,
|
||||
duration_periods=duration,
|
||||
include_ursulas=include_ursulas,
|
||||
exclude_ursulas=exclude_ursulas)
|
||||
returned_ursula_addresses = {ursula_info.checksum_address for ursula_info in ursulas_info}
|
||||
assert len(returned_ursula_addresses) == quantity
|
||||
for address in include_ursulas:
|
||||
assert address in returned_ursula_addresses
|
||||
for address in exclude_ursulas:
|
||||
assert address not in returned_ursula_addresses
|
||||
|
||||
|
||||
def test_publish_and_get_treasure_map(blockchain_porter,
|
||||
blockchain_alice,
|
||||
blockchain_bob,
|
||||
idle_blockchain_policy):
|
||||
# ensure that random treasure map cannot be obtained since not available
|
||||
with pytest.raises(TreasureMap.NowhereToBeFound):
|
||||
random_bob_encrypting_key = PublicKey.from_bytes(
|
||||
bytes.fromhex("026d1f4ce5b2474e0dae499d6737a8d987ed3c9ab1a55e00f57ad2d8e81fe9e9ac"))
|
||||
random_treasure_map_id = "93a9482bdf3b4f2e9df906a35144ca84"
|
||||
assert len(bytes.fromhex(random_treasure_map_id)) == HRAC_LENGTH # non-federated is 16 bytes
|
||||
blockchain_porter.get_treasure_map(map_identifier=random_treasure_map_id,
|
||||
bob_encrypting_key=random_bob_encrypting_key)
|
||||
|
||||
blockchain_bob_encrypting_key = blockchain_bob.public_keys(DecryptingPower)
|
||||
|
||||
# try publishing a new policy
|
||||
network_middleware = MockRestMiddleware()
|
||||
enacted_policy = idle_blockchain_policy.enact(network_middleware=network_middleware,
|
||||
publish_treasure_map=False) # enact but don't publish
|
||||
treasure_map = enacted_policy.treasure_map
|
||||
blockchain_porter.publish_treasure_map(bytes(treasure_map), blockchain_bob_encrypting_key)
|
||||
|
||||
# try getting the recently published treasure map
|
||||
map_id = blockchain_bob.construct_map_id(blockchain_alice.stamp,
|
||||
enacted_policy.label)
|
||||
retrieved_treasure_map = blockchain_porter.get_treasure_map(map_identifier=map_id,
|
||||
bob_encrypting_key=blockchain_bob_encrypting_key)
|
||||
assert retrieved_treasure_map == treasure_map
|
||||
|
||||
|
||||
def test_exec_work_order(blockchain_porter,
|
||||
random_blockchain_policy,
|
||||
blockchain_ursulas,
|
||||
blockchain_bob,
|
||||
blockchain_alice):
|
||||
# Setup
|
||||
network_middleware = MockRestMiddleware()
|
||||
# enact new random policy since idle_blockchain_policy/enacted_blockchain_policy already modified in previous tests
|
||||
enacted_policy = random_blockchain_policy.enact(network_middleware=network_middleware,
|
||||
publish_treasure_map=False) # enact but don't publish
|
||||
ursula_address, work_order = work_order_setup(enacted_policy,
|
||||
blockchain_ursulas,
|
||||
blockchain_bob,
|
||||
blockchain_alice)
|
||||
# use porter
|
||||
result = blockchain_porter.exec_work_order(ursula_address=ursula_address,
|
||||
work_order_payload=work_order.payload())
|
||||
assert result
|
|
@ -0,0 +1,92 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
from base64 import b64encode
|
||||
|
||||
import pytest
|
||||
|
||||
from nucypher.characters.control.specifications.fields import TreasureMap
|
||||
from nucypher.control.specifications.exceptions import InvalidInputData
|
||||
from nucypher.crypto.powers import DecryptingPower
|
||||
from nucypher.utilities.porter.control.specifications.porter_schema import AlicePublishTreasureMap
|
||||
|
||||
|
||||
def test_alice_publish_treasure_map_schema_blockchain_context_default(enacted_blockchain_policy, blockchain_bob):
|
||||
alice_publish_treasure_map_schema = AlicePublishTreasureMap() # default is decentralized
|
||||
run_publish_treasuremap_schema_tests(alice_publish_treasure_map_schema=alice_publish_treasure_map_schema,
|
||||
enacted_blockchain_policy=enacted_blockchain_policy,
|
||||
blockchain_bob=blockchain_bob)
|
||||
|
||||
|
||||
def test_alice_publish_treasure_map_schema_blockchain_context_set_false(enacted_blockchain_policy, blockchain_bob):
|
||||
# since non-federated, schema's context doesn't have to be set, but set it anyway to ensure that setting to
|
||||
# False still works as expected.
|
||||
alice_publish_treasure_map_schema = AlicePublishTreasureMap() # default is decentralized
|
||||
alice_publish_treasure_map_schema.context[TreasureMap.IS_FEDERATED_CONTEXT_KEY] = False
|
||||
run_publish_treasuremap_schema_tests(alice_publish_treasure_map_schema=alice_publish_treasure_map_schema,
|
||||
enacted_blockchain_policy=enacted_blockchain_policy,
|
||||
blockchain_bob=blockchain_bob)
|
||||
|
||||
|
||||
def run_publish_treasuremap_schema_tests(alice_publish_treasure_map_schema, enacted_blockchain_policy, blockchain_bob):
|
||||
# no args
|
||||
with pytest.raises(InvalidInputData):
|
||||
alice_publish_treasure_map_schema.load({})
|
||||
|
||||
treasure_map_b64 = b64encode(bytes(enacted_blockchain_policy.treasure_map)).decode()
|
||||
bob_encrypting_key = blockchain_bob.public_keys(DecryptingPower)
|
||||
bob_encrypting_key_hex = bytes(bob_encrypting_key).hex()
|
||||
|
||||
required_data = {
|
||||
'treasure_map': treasure_map_b64,
|
||||
'bob_encrypting_key': bob_encrypting_key_hex
|
||||
}
|
||||
|
||||
# required args
|
||||
alice_publish_treasure_map_schema.load(required_data)
|
||||
|
||||
# missing required args
|
||||
updated_data = {k: v for k, v in required_data.items() if k != 'treasure_map'}
|
||||
with pytest.raises(InvalidInputData):
|
||||
alice_publish_treasure_map_schema.load(updated_data)
|
||||
|
||||
updated_data = {k: v for k, v in required_data.items() if k != 'bob_encrypting_key'}
|
||||
with pytest.raises(InvalidInputData):
|
||||
alice_publish_treasure_map_schema.load(updated_data)
|
||||
|
||||
# invalid treasure map
|
||||
updated_data = dict(required_data)
|
||||
updated_data['treasure_map'] = b64encode(b"testing").decode()
|
||||
with pytest.raises(InvalidInputData):
|
||||
alice_publish_treasure_map_schema.load(updated_data)
|
||||
|
||||
# invalid encrypting key
|
||||
updated_data = dict(required_data)
|
||||
updated_data['bob_encrypting_key'] = b'123456'.hex()
|
||||
with pytest.raises(InvalidInputData):
|
||||
alice_publish_treasure_map_schema.load(updated_data)
|
||||
|
||||
# Test Output - test only true since there is no false ever returned
|
||||
response_data = {'published': True}
|
||||
output = alice_publish_treasure_map_schema.dump(obj=response_data)
|
||||
assert output == response_data
|
||||
|
||||
# setting federated context to True
|
||||
alice_publish_treasure_map_schema.context[TreasureMap.IS_FEDERATED_CONTEXT_KEY] = True
|
||||
with pytest.raises(InvalidInputData):
|
||||
# failed because federated treasure map expected, but instead non-federated (blockchain) treasure map provided
|
||||
alice_publish_treasure_map_schema.load(required_data)
|
|
@ -21,7 +21,7 @@ import lmdb
|
|||
import pytest
|
||||
from eth_utils.crypto import keccak
|
||||
|
||||
from nucypher.characters.control.emitters import WebEmitter
|
||||
from nucypher.control.emitters import WebEmitter
|
||||
from nucypher.crypto.powers import TransactingPower
|
||||
from nucypher.network.nodes import Learner
|
||||
from nucypher.network.trackers import AvailabilityTracker
|
||||
|
|
|
@ -23,6 +23,7 @@ import shutil
|
|||
import tempfile
|
||||
from datetime import datetime, timedelta
|
||||
from functools import partial
|
||||
from pathlib import Path
|
||||
from typing import Callable, Tuple
|
||||
|
||||
import maya
|
||||
|
@ -49,7 +50,7 @@ from nucypher.blockchain.eth.interfaces import BlockchainInterfaceFactory
|
|||
from nucypher.blockchain.eth.registry import InMemoryContractRegistry, LocalContractRegistry
|
||||
from nucypher.blockchain.eth.signers.software import Web3Signer
|
||||
from nucypher.blockchain.eth.token import NU
|
||||
from nucypher.characters.control.emitters import StdoutEmitter
|
||||
from nucypher.control.emitters import StdoutEmitter
|
||||
from nucypher.characters.lawful import Enrico
|
||||
from nucypher.config.characters import (
|
||||
AliceConfiguration,
|
||||
|
@ -63,6 +64,7 @@ from nucypher.crypto.powers import TransactingPower
|
|||
from nucypher.datastore import datastore
|
||||
from nucypher.network.nodes import TEACHER_NODES
|
||||
from nucypher.utilities.logging import GlobalLoggerSettings, Logger
|
||||
from nucypher.utilities.porter.porter import Porter
|
||||
from tests.constants import (
|
||||
BASE_TEMP_DIR,
|
||||
BASE_TEMP_PREFIX,
|
||||
|
@ -278,6 +280,23 @@ def enacted_blockchain_policy(idle_blockchain_policy, blockchain_ursulas):
|
|||
return enacted_policy
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def random_blockchain_policy(testerchain, blockchain_alice, blockchain_bob, token_economics):
|
||||
random_label = generate_random_label()
|
||||
periods = token_economics.minimum_locked_periods // 2
|
||||
days = periods * (token_economics.hours_per_period // 24)
|
||||
now = testerchain.w3.eth.getBlock('latest').timestamp
|
||||
expiration = maya.MayaDT(now).add(days=days - 1)
|
||||
n = 3
|
||||
m = 2
|
||||
policy = blockchain_alice.create_policy(blockchain_bob,
|
||||
label=random_label,
|
||||
m=m, n=n,
|
||||
value=n * periods * 100,
|
||||
expiration=expiration)
|
||||
return policy
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def capsule_side_channel(enacted_federated_policy):
|
||||
class _CapsuleSideChannel:
|
||||
|
@ -418,6 +437,35 @@ def lonely_ursula_maker(ursula_federated_test_config):
|
|||
_maker.clean()
|
||||
|
||||
|
||||
#
|
||||
# Porter
|
||||
#
|
||||
@pytest.fixture(scope="module")
|
||||
def federated_porter(federated_ursulas):
|
||||
porter = Porter(domain=TEMPORARY_DOMAIN,
|
||||
abort_on_learning_error=True,
|
||||
start_learning_now=True,
|
||||
known_nodes=federated_ursulas,
|
||||
verify_node_bonding=False,
|
||||
federated_only=True,
|
||||
network_middleware=MockRestMiddleware())
|
||||
yield porter
|
||||
porter.stop_learning_loop()
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def blockchain_porter(blockchain_ursulas, testerchain, test_registry):
|
||||
porter = Porter(domain=TEMPORARY_DOMAIN,
|
||||
abort_on_learning_error=True,
|
||||
start_learning_now=True,
|
||||
known_nodes=blockchain_ursulas,
|
||||
provider_uri=TEST_PROVIDER_URI,
|
||||
registry=test_registry,
|
||||
network_middleware=MockRestMiddleware())
|
||||
yield porter
|
||||
porter.stop_learning_loop()
|
||||
|
||||
|
||||
#
|
||||
# Blockchain
|
||||
#
|
||||
|
@ -1057,3 +1105,16 @@ def mock_teacher_nodes(mocker):
|
|||
def disable_interactive_keystore_generation(mocker):
|
||||
# Do not notify or confirm mnemonic seed words during tests normally
|
||||
mocker.patch.object(Keystore, '_confirm_generate')
|
||||
|
||||
|
||||
#
|
||||
# Web Auth
|
||||
#
|
||||
@pytest.fixture(scope='module')
|
||||
def basic_auth_file(temp_dir_path):
|
||||
basic_auth = Path(temp_dir_path) / 'htpasswd'
|
||||
with basic_auth.open("w") as f:
|
||||
# username: "admin", password: "admin"
|
||||
f.write("admin:$apr1$hlEpWVoI$0qjykXrvdZ0yO2TnBggQO0\n")
|
||||
yield basic_auth
|
||||
basic_auth.unlink()
|
||||
|
|
|
@ -25,13 +25,8 @@ import pytest
|
|||
from nucypher.characters.control.specifications.fields.treasuremap import TreasureMap
|
||||
from nucypher.characters.control.specifications import fields
|
||||
from nucypher.characters.control.specifications.alice import GrantPolicy
|
||||
from nucypher.characters.control.specifications.base import BaseSchema
|
||||
from nucypher.characters.control.specifications.exceptions import (
|
||||
InvalidArgumentCombo,
|
||||
InvalidInputData,
|
||||
SpecificationError
|
||||
)
|
||||
|
||||
from nucypher.control.specifications.base import BaseSchema
|
||||
from nucypher.control.specifications.exceptions import SpecificationError, InvalidInputData, InvalidArgumentCombo
|
||||
from nucypher.crypto.powers import DecryptingPower
|
||||
|
||||
|
||||
|
@ -78,7 +73,7 @@ def test_treasuremap_validation(enacted_federated_policy):
|
|||
"""Tell people exactly what's wrong with their treasuremaps"""
|
||||
|
||||
class TreasureMapsOnly(BaseSchema):
|
||||
tmap = TreasureMap()
|
||||
tmap = TreasureMap(federated_only=True)
|
||||
|
||||
# this will raise a base64 error
|
||||
with pytest.raises(SpecificationError) as e:
|
||||
|
|
|
@ -22,7 +22,7 @@ from constant_sorrow.constants import NO_PASSWORD
|
|||
from mnemonic.mnemonic import Mnemonic
|
||||
|
||||
from nucypher.blockchain.eth.decorators import InvalidChecksumAddress
|
||||
from nucypher.characters.control.emitters import StdoutEmitter
|
||||
from nucypher.control.emitters import StdoutEmitter
|
||||
from nucypher.cli.actions.auth import (
|
||||
get_client_password,
|
||||
get_nucypher_password,
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from base64 import b64decode
|
||||
|
||||
import pytest
|
||||
|
||||
from nucypher.crypto.umbral_adapter import PublicKey
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def random_federated_treasure_map_data():
|
||||
#
|
||||
# These values were obtained from running the federated heartbeat demo and printing the relevant values.
|
||||
#
|
||||
random_bob_encrypting_key = PublicKey.from_bytes(
|
||||
bytes.fromhex("026d1f4ce5b2474e0dae499d6737a8d987ed3c9ab1a55e00f57ad2d8e81fe9e9ac"))
|
||||
random_treasure_map_id = "cc1c29dd2305483cc838cbcc5ecb5ef6edfd69ecadb0aa52b6b084b630989187" # federated is 32 bytes
|
||||
random_treasure_map = b64decode("VE0AAZ3jsXPynMD9dm+Fjwi49bxkOzUjwsNI0Y0p8bGB9F60OXGmJqibK0Ki4FSWti2Y"
|
||||
"vUDuxMBrx8BidK00ITuDVoz037tyvCyOL+5Wcy5/LRD8AAAIgQOfxCS16Gu9aw0iy/G9"
|
||||
"9JUW/hfj6Mt3lM7+hIrLASpMZgJO1o5GbBumlzv0w90HAwXGNrJkhbTgUpRgO0vsGqtr"
|
||||
"OtuWe0lUNvLqPwvLbq1r5FmBD6FmvlfsiKI3aoKUgChEAlXU5d/KqJjvMHIih/yQNpZv"
|
||||
"5RFBK52YiOEAb8EO4FqyAAAH+tzB7Fk7m8iVP/UILgco37l8EF2edLJyZloxggpe9cN7"
|
||||
"fs5hHwAxTgRBCI3fZCyKxkZxVyFlPnRuih1A6dZJLOqeQGtDCsPDA/3wi1ND1swM31ti"
|
||||
"2PopqoLTmhqWJvu+dqTqeqOAMXehbx/e5gpYEeyIFbt5dQyp7MLGHlGzvvbhh7SLqDN7"
|
||||
"vGY4l9lnwNDfyVMZ2t0Q43oKYvv9YTSDkxJqeooTT21vcpLB3DMcjs2Geq3Zapcn2bfp"
|
||||
"QoRh0ZyAjKTR650PDOA2DgwkuWfkTWJk8E9a1EVnV5zeWNQB3nH5jfP5Tq/tXVs/4I8A"
|
||||
"nvALP74PjDYmzwTkPWsZ4sKMe/3vrvu1cZMblv/3c006nhWTqGlYKP52d8mGzgORWCqA"
|
||||
"Pn+to2xEIZZETUl4uWgJfMRlih0/kq1T6aHoIyr16hQKv9uJKnIbsdBE/D4eQUEvAUk/"
|
||||
"YVQhoETgznQK04vMgLHErhiT/JquA2DZNKg7/Nw8L4Z49anwoYPxqbP9djcOnxzy2Iupv"
|
||||
"HZrMX5D3tZHBszSwxQ7NiZRa38Hn/ed1Jodgv7j2nhRQVc+HZSJmj522FISk3wKMgNaHq"
|
||||
"RbtmNot/4bRCARaDB1spOvnxqemq+RfsmCUJcsTjyjEfwHm74UD+G4Hv/3h9DEmWPnJ23"
|
||||
"5q0x2LoodOnMJ3QjjN5qZoQuU3vk8f6zdKNkXWqPrnRPx89okX/N7sW6wk2lJESRO53I2"
|
||||
"+IirUIlVYfWmUuTvleeH15p+kjIuzaO+xGVHuOT/r6onc5+CJDUYih5NuAzzoAcThi8l/"
|
||||
"6ZLTDi+uIj9hcqylBU1lf/ZB3TY7h03eMwEimflpti/DBqArZ1i81l9grTU+Rzx86p9rk"
|
||||
"VaS+B7v3oft7Zm/UTvLk7BZIsjrMmAhLTJKUNY2svzA2dlXlEDmlmAJzrz/gsWph1u7ds"
|
||||
"WpN3xQytHwRpCgfin4Ndzag9rZg2Gpy0IqS3x/csxp42HTHoeJ6xAp6UX5PEfY6MrWTqf"
|
||||
"jCfLBnhli4+1Jb+kum1o7sD4htKnTezKndZuEIYDmLw3C0uPxDZGpckr05ZGBBDnLi8cp"
|
||||
"KSs+WUJccdUQazo6JkHKbejCtneTctqjAgKHTXb5ReYqcTiQ4Z0OXFUNzQvYcXrOqhb3r"
|
||||
"OqZIDtnqIkjkUqmT1DBOjGazuLCX4rkbwCx5h+D2/+L66KShwh5oVPIkKOHf/DvGVq1E/"
|
||||
"skIFPWtNOyBCwZS1OWo2zOEi19t/TP7aCn/HjLrZdlOf5X+6Yoh9VytCgnX+Pc4cvzMhE"
|
||||
"9o4TurtPxmOfQs5y7EhFY+3leF+x0RHaOLPNEtr1cjNpLguvm5GGM0rcFkpZh1Zr8UBcf"
|
||||
"poLCWvupdvkzCyS78gda637+57M6ZNiaE9oNuvXaiG2MXUzyBx9DBdXKieJsKjhZZ9VTP"
|
||||
"7ceJuM70USFTV3K2yRCsHpoxX7qql9k1+ZChFQNP0LYHuo7FAMMIq4nu7B2R9yjKDON1Z"
|
||||
"5JxcsKxZrFR90kH3oogVQP0gegF1qGGdfT87cLZmpFHB1Vuzsu7AcpjRay+nDhi+HdG/+"
|
||||
"PeeobJwgpA3L5/0JKoB2cpXQY4p2bCzFBInG64Bl6AEQYPEB13u3D7iC1k4j9xxUgRX+t"
|
||||
"fX0Kp3VojnAaawAc6Et/vJ/13p7DHvPvWXz7A9ZNHoTpZV7rp9ZhZCXrDCfqPc9Q9+Cwa"
|
||||
"LU8m/9aEz/VKN/TyZdyZJlGBJ7NXCQf0qnZh7rgA0I/lhvJ4SFkLiqA8OuouVGDGgrvm/"
|
||||
"ySiNcVOlwdXDDqCYn4vNA3PwpDdK6XjA4btlvTK1Xm6cnPMrJ/Yk65qfEnaCGspmSK8pG"
|
||||
"SIyuvECAktRCg/IETZTqFo43ewt0wWlROR1Veib/+ZbjbPbVmphGSUaLakG+NKEnDdyGZ"
|
||||
"JJ5ZxGnA6V7P1SwM+1MuNZaovJWbX8Kk1jIg0Y9fCpOcB8nDiwpgcapi5YGS3kgP1Hwny"
|
||||
"OvAQwYjb6xdYTC8hsLp9gnRFYyDGSyGeCFJ1yxwC6u+o4ex4hetAMl7Ce3sVc66XKVZWf"
|
||||
"IEjmODRE3ztghuSGRWmVG20wTS4+Iya69WAynWv1DYXfzQ5h/5NK5JjuJftvod8uq5UReo"
|
||||
"98bH2OIrnTfpGzDDcl48AYzRQJ4/lokBVgWrdKfscv7Z55RVxyjR7eOoXZGzFlmkj7YKG+"
|
||||
"NJmZsC1Glrtz6sc7xmldEWFfk+Zb2j2HKLzJ99ekqGJpcrQSJAs6nXINplVnI8psrpOG8/"
|
||||
"cTIqMsliL+i9qZxUX6sl//fj2eR4nzb0W1qItXyS2UOMC1t7MgzICsfn0VWzIT0da4f6za"
|
||||
"EQEBjtYWEmj7UJTlVm+L+utlMFFtunRD6uCibBVTtqZY5oTN8IUyTYyGV+K7w5jpm1ceUm"
|
||||
"0kSeqivSArWInnpk7S098lz8DtFsXxJwhsKUXyOjWXAyYOTzIuB8HxvQ92KyPKrD927iZL"
|
||||
"5q8DYgC8q5fozFbWFJ6Do+6STVtqYtngNCizrIoBg2/OMx0pUUGkH+S9b7wfordn+czd6s"
|
||||
"t8NoCij4F3nHx8dvA2ZKEoC5YlrEsJBjdKHZwRPtQ6H8bDj2C20K48t4jQ26GPqithBkYE"
|
||||
"ogU/kE00AE+L0JJQAkgWotyF28V/+awu+rpeuJ4eKzUS0ig0YiLYSfSAsqCARahH25QljG"
|
||||
"pYabC2hGmPwkD9pGEymtCcFnf47Zhi6v7LkrbatbLk8ebIMm21a6WLy")
|
||||
yield random_bob_encrypting_key, random_treasure_map_id, random_treasure_map
|
|
@ -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 Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
import pytest
|
||||
|
||||
|
||||
#
|
||||
# Web
|
||||
#
|
||||
@pytest.fixture(scope='module')
|
||||
def federated_porter_web_controller(federated_porter):
|
||||
web_controller = federated_porter.make_web_controller(crash_on_error=True)
|
||||
yield web_controller.test_client()
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def federated_porter_basic_auth_web_controller(federated_porter, basic_auth_file):
|
||||
web_controller = federated_porter.make_web_controller(crash_on_error=True, htpasswd_filepath=basic_auth_file)
|
||||
yield web_controller.test_client()
|
||||
|
||||
|
||||
#
|
||||
# RPC
|
||||
#
|
||||
@pytest.fixture(scope='module')
|
||||
def federated_porter_rpc_controller(federated_porter):
|
||||
rpc_controller = federated_porter.make_rpc_controller(crash_on_error=True)
|
||||
yield rpc_controller.test_client()
|
|
@ -0,0 +1,162 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
from base64 import b64encode, b64decode
|
||||
|
||||
import pytest
|
||||
from nucypher.crypto.umbral_adapter import PublicKey
|
||||
|
||||
from nucypher.crypto.powers import DecryptingPower
|
||||
from nucypher.network.nodes import Learner
|
||||
from nucypher.policy.maps import TreasureMap
|
||||
|
||||
|
||||
# should always be first test due to checks on response id
|
||||
from tests.utils.policy import work_order_setup
|
||||
|
||||
|
||||
def test_get_ursulas(federated_porter_rpc_controller, federated_ursulas):
|
||||
method = 'get_ursulas'
|
||||
expected_response_id = 0
|
||||
|
||||
quantity = 4
|
||||
duration = 2 # irrelevant for federated (but required)
|
||||
federated_ursulas_list = list(federated_ursulas)
|
||||
include_ursulas = [federated_ursulas_list[0].checksum_address, federated_ursulas_list[1].checksum_address]
|
||||
exclude_ursulas = [federated_ursulas_list[2].checksum_address, federated_ursulas_list[3].checksum_address]
|
||||
|
||||
get_ursulas_params = {
|
||||
'quantity': quantity,
|
||||
'duration_periods': duration, # irrelevant for federated (but required)
|
||||
'include_ursulas': include_ursulas,
|
||||
'exclude_ursulas': exclude_ursulas
|
||||
}
|
||||
|
||||
#
|
||||
# Success
|
||||
#
|
||||
request_data = {'method': method, 'params': get_ursulas_params}
|
||||
response = federated_porter_rpc_controller.send(request_data)
|
||||
expected_response_id += 1
|
||||
assert response.success
|
||||
assert response.id == expected_response_id
|
||||
ursulas_info = response.data['result']['ursulas']
|
||||
returned_ursula_addresses = {ursula_info['checksum_address'] for ursula_info in ursulas_info} # ensure no repeats
|
||||
assert len(returned_ursula_addresses) == quantity
|
||||
for address in include_ursulas:
|
||||
assert address in returned_ursula_addresses
|
||||
for address in exclude_ursulas:
|
||||
assert address not in returned_ursula_addresses
|
||||
|
||||
# Confirm the same message send works again, with a unique ID
|
||||
request_data = {'method': method, 'params': get_ursulas_params}
|
||||
rpc_response = federated_porter_rpc_controller.send(request=request_data)
|
||||
expected_response_id += 1
|
||||
assert rpc_response.success
|
||||
assert rpc_response.id == expected_response_id
|
||||
|
||||
#
|
||||
# Failure case
|
||||
#
|
||||
failed_ursula_params = dict(get_ursulas_params)
|
||||
failed_ursula_params['quantity'] = len(federated_ursulas_list) + 1 # too many to get
|
||||
request_data = {'method': method, 'params': failed_ursula_params}
|
||||
with pytest.raises(Learner.NotEnoughNodes):
|
||||
federated_porter_rpc_controller.send(request_data)
|
||||
|
||||
|
||||
def test_publish_and_get_treasure_map(federated_porter_rpc_controller,
|
||||
federated_alice,
|
||||
federated_bob,
|
||||
enacted_federated_policy,
|
||||
random_federated_treasure_map_data):
|
||||
random_bob_encrypting_key, random_treasure_map_id, random_treasure_map = random_federated_treasure_map_data
|
||||
|
||||
# ensure that random treasure map cannot be obtained since not available
|
||||
with pytest.raises(TreasureMap.NowhereToBeFound):
|
||||
get_treasure_map_params = {
|
||||
'treasure_map_id': random_treasure_map_id,
|
||||
'bob_encrypting_key': bytes(random_bob_encrypting_key).hex()
|
||||
}
|
||||
request_data = {'method': 'get_treasure_map', 'params': get_treasure_map_params}
|
||||
federated_porter_rpc_controller.send(request_data)
|
||||
|
||||
# publish the random treasure map
|
||||
publish_treasure_map_params = {
|
||||
'treasure_map': b64encode(bytes(random_treasure_map)).decode(),
|
||||
'bob_encrypting_key': bytes(random_bob_encrypting_key).hex()
|
||||
}
|
||||
request_data = {'method': 'publish_treasure_map', 'params': publish_treasure_map_params}
|
||||
response = federated_porter_rpc_controller.send(request_data)
|
||||
assert response.success
|
||||
|
||||
# try getting the random treasure map now
|
||||
get_treasure_map_params = {
|
||||
'treasure_map_id': random_treasure_map_id,
|
||||
'bob_encrypting_key': bytes(random_bob_encrypting_key).hex()
|
||||
}
|
||||
request_data = {'method': 'get_treasure_map', 'params': get_treasure_map_params}
|
||||
response = federated_porter_rpc_controller.send(request_data)
|
||||
assert response.success
|
||||
assert response.content['treasure_map'] == b64encode(bytes(random_treasure_map)).decode()
|
||||
|
||||
# try getting an already existing policy
|
||||
map_id = federated_bob.construct_map_id(federated_alice.stamp,
|
||||
enacted_federated_policy.label)
|
||||
get_treasure_map_params = {
|
||||
'treasure_map_id': map_id,
|
||||
'bob_encrypting_key': bytes(federated_bob.public_keys(DecryptingPower)).hex()
|
||||
}
|
||||
request_data = {'method': 'get_treasure_map', 'params': get_treasure_map_params}
|
||||
response = federated_porter_rpc_controller.send(request_data)
|
||||
assert response.success
|
||||
assert response.content['treasure_map'] == b64encode(bytes(enacted_federated_policy.treasure_map)).decode()
|
||||
|
||||
|
||||
def test_exec_work_order(federated_porter_rpc_controller,
|
||||
enacted_federated_policy,
|
||||
federated_ursulas,
|
||||
federated_bob,
|
||||
federated_alice,
|
||||
get_random_checksum_address):
|
||||
method = 'exec_work_order'
|
||||
# Setup
|
||||
ursula_address, work_order = work_order_setup(enacted_federated_policy,
|
||||
federated_ursulas,
|
||||
federated_bob,
|
||||
federated_alice)
|
||||
|
||||
work_order_payload_b64 = b64encode(work_order.payload()).decode()
|
||||
|
||||
exec_work_order_params = {
|
||||
'ursula': ursula_address,
|
||||
'work_order_payload': work_order_payload_b64
|
||||
}
|
||||
request_data = {'method': method, 'params': exec_work_order_params}
|
||||
response = federated_porter_rpc_controller.send(request_data)
|
||||
assert response.success
|
||||
work_order_result = response.content['work_order_result']
|
||||
assert work_order_result
|
||||
|
||||
# Failure
|
||||
exec_work_order_params = {
|
||||
'ursula': get_random_checksum_address(), # unknown ursula
|
||||
'work_order_payload': work_order_payload_b64
|
||||
}
|
||||
with pytest.raises(Learner.NotEnoughNodes):
|
||||
request_data = {'method': method, 'params': exec_work_order_params}
|
||||
federated_porter_rpc_controller.send(request_data)
|
|
@ -0,0 +1,239 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import json
|
||||
from urllib.parse import urlencode
|
||||
from base64 import b64encode
|
||||
|
||||
import pytest
|
||||
|
||||
from nucypher.crypto.powers import DecryptingPower
|
||||
from nucypher.network.nodes import Learner
|
||||
from nucypher.policy.maps import TreasureMap
|
||||
from tests.utils.policy import work_order_setup
|
||||
|
||||
|
||||
def test_get_ursulas(federated_porter_web_controller, federated_ursulas):
|
||||
# Send bad data to assert error return
|
||||
response = federated_porter_web_controller.get('/get_ursulas', data=json.dumps({'bad': 'input'}))
|
||||
assert response.status_code == 400
|
||||
|
||||
quantity = 4
|
||||
duration = 2 # irrelevant for federated (but required)
|
||||
federated_ursulas_list = list(federated_ursulas)
|
||||
include_ursulas = [federated_ursulas_list[0].checksum_address, federated_ursulas_list[1].checksum_address]
|
||||
exclude_ursulas = [federated_ursulas_list[2].checksum_address, federated_ursulas_list[3].checksum_address]
|
||||
|
||||
get_ursulas_params = {
|
||||
'quantity': quantity,
|
||||
'duration_periods': duration, # irrelevant for federated (but required)
|
||||
'include_ursulas': include_ursulas,
|
||||
'exclude_ursulas': exclude_ursulas
|
||||
}
|
||||
|
||||
#
|
||||
# Success
|
||||
#
|
||||
response = federated_porter_web_controller.get('/get_ursulas', data=json.dumps(get_ursulas_params))
|
||||
assert response.status_code == 200
|
||||
|
||||
response_data = json.loads(response.data)
|
||||
ursulas_info = response_data['result']['ursulas']
|
||||
returned_ursula_addresses = {ursula_info['checksum_address'] for ursula_info in ursulas_info} # ensure no repeats
|
||||
assert len(returned_ursula_addresses) == quantity
|
||||
for address in include_ursulas:
|
||||
assert address in returned_ursula_addresses
|
||||
for address in exclude_ursulas:
|
||||
assert address not in returned_ursula_addresses
|
||||
|
||||
#
|
||||
# Test Query parameters
|
||||
#
|
||||
response = federated_porter_web_controller.get(f'/get_ursulas?quantity={quantity}'
|
||||
f'&duration_periods={duration}'
|
||||
f'&include_ursulas={",".join(include_ursulas)}'
|
||||
f'&exclude_ursulas={",".join(exclude_ursulas)}')
|
||||
assert response.status_code == 200
|
||||
response_data = json.loads(response.data)
|
||||
ursulas_info = response_data['result']['ursulas']
|
||||
returned_ursula_addresses = {ursula_info['checksum_address'] for ursula_info in ursulas_info} # ensure no repeats
|
||||
assert len(returned_ursula_addresses) == quantity
|
||||
for address in include_ursulas:
|
||||
assert address in returned_ursula_addresses
|
||||
for address in exclude_ursulas:
|
||||
assert address not in returned_ursula_addresses
|
||||
|
||||
#
|
||||
# Failure case
|
||||
#
|
||||
failed_ursula_params = dict(get_ursulas_params)
|
||||
failed_ursula_params['quantity'] = len(federated_ursulas_list) + 1 # too many to get
|
||||
with pytest.raises(Learner.NotEnoughNodes):
|
||||
federated_porter_web_controller.get('/get_ursulas', data=json.dumps(failed_ursula_params))
|
||||
|
||||
|
||||
def test_publish_and_get_treasure_map(federated_porter_web_controller,
|
||||
federated_alice,
|
||||
federated_bob,
|
||||
enacted_federated_policy,
|
||||
random_federated_treasure_map_data):
|
||||
# Send bad data to assert error return
|
||||
response = federated_porter_web_controller.get('/get_treasure_map', data=json.dumps({'bad': 'input'}))
|
||||
assert response.status_code == 400
|
||||
|
||||
response = federated_porter_web_controller.post('/publish_treasure_map', data=json.dumps({'bad': 'input'}))
|
||||
assert response.status_code == 400
|
||||
|
||||
random_bob_encrypting_key, random_treasure_map_id, random_treasure_map = random_federated_treasure_map_data
|
||||
|
||||
# ensure that random treasure map cannot be obtained since not available
|
||||
with pytest.raises(TreasureMap.NowhereToBeFound):
|
||||
get_treasure_map_params = {
|
||||
'treasure_map_id': random_treasure_map_id,
|
||||
'bob_encrypting_key': bytes(random_bob_encrypting_key).hex()
|
||||
}
|
||||
federated_porter_web_controller.get('/get_treasure_map', data=json.dumps(get_treasure_map_params))
|
||||
|
||||
# publish the random treasure map
|
||||
publish_treasure_map_params = {
|
||||
'treasure_map': b64encode(bytes(random_treasure_map)).decode(),
|
||||
'bob_encrypting_key': bytes(random_bob_encrypting_key).hex()
|
||||
}
|
||||
response = federated_porter_web_controller.post('/publish_treasure_map',
|
||||
data=json.dumps(publish_treasure_map_params))
|
||||
assert response.status_code == 200
|
||||
response_data = json.loads(response.data)
|
||||
assert response_data['result']['published']
|
||||
|
||||
# try getting the random treasure map now
|
||||
get_treasure_map_params = {
|
||||
'treasure_map_id': random_treasure_map_id,
|
||||
'bob_encrypting_key': bytes(random_bob_encrypting_key).hex()
|
||||
}
|
||||
response = federated_porter_web_controller.get('/get_treasure_map',
|
||||
data=json.dumps(get_treasure_map_params))
|
||||
assert response.status_code == 200
|
||||
response_data = json.loads(response.data)
|
||||
assert response_data['result']['treasure_map'] == b64encode(bytes(random_treasure_map)).decode()
|
||||
|
||||
# try getting random treasure map using query parameters
|
||||
response = federated_porter_web_controller.get(f'/get_treasure_map'
|
||||
f'?{urlencode(get_treasure_map_params)}')
|
||||
assert response.status_code == 200
|
||||
response_data = json.loads(response.data)
|
||||
assert response_data['result']['treasure_map'] == b64encode(bytes(random_treasure_map)).decode()
|
||||
|
||||
# try getting an already existing policy
|
||||
map_id = federated_bob.construct_map_id(federated_alice.stamp,
|
||||
enacted_federated_policy.label)
|
||||
get_treasure_map_params = {
|
||||
'treasure_map_id': map_id,
|
||||
'bob_encrypting_key': bytes(federated_bob.public_keys(DecryptingPower)).hex()
|
||||
}
|
||||
response = federated_porter_web_controller.get('/get_treasure_map',
|
||||
data=json.dumps(get_treasure_map_params))
|
||||
assert response.status_code == 200
|
||||
response_data = json.loads(response.data)
|
||||
assert response_data['result']['treasure_map'] == b64encode(bytes(enacted_federated_policy.treasure_map)).decode()
|
||||
|
||||
|
||||
def test_exec_work_order(federated_porter_web_controller,
|
||||
enacted_federated_policy,
|
||||
federated_ursulas,
|
||||
federated_bob,
|
||||
federated_alice,
|
||||
get_random_checksum_address):
|
||||
# Send bad data to assert error return
|
||||
response = federated_porter_web_controller.post('/exec_work_order', data=json.dumps({'bad': 'input'}))
|
||||
assert response.status_code == 400
|
||||
|
||||
# Setup
|
||||
ursula_address, work_order = work_order_setup(enacted_federated_policy,
|
||||
federated_ursulas,
|
||||
federated_bob,
|
||||
federated_alice)
|
||||
work_order_payload_b64 = b64encode(work_order.payload()).decode()
|
||||
|
||||
# Success
|
||||
exec_work_order_params = {
|
||||
'ursula': ursula_address,
|
||||
'work_order_payload': work_order_payload_b64
|
||||
}
|
||||
response = federated_porter_web_controller.post('/exec_work_order', data=json.dumps(exec_work_order_params))
|
||||
assert response.status_code == 200
|
||||
|
||||
response_data = json.loads(response.data)
|
||||
work_order_result = response_data['result']['work_order_result']
|
||||
assert work_order_result
|
||||
|
||||
# Failure
|
||||
exec_work_order_params = {
|
||||
'ursula': get_random_checksum_address(), # unknown ursula
|
||||
'work_order_payload': work_order_payload_b64
|
||||
}
|
||||
with pytest.raises(Learner.NotEnoughNodes):
|
||||
federated_porter_web_controller.post('/exec_work_order', data=json.dumps(exec_work_order_params))
|
||||
|
||||
|
||||
def test_endpoints_basic_auth(federated_porter_basic_auth_web_controller,
|
||||
random_federated_treasure_map_data,
|
||||
get_random_checksum_address):
|
||||
# /get_ursulas
|
||||
quantity = 4
|
||||
duration = 2 # irrelevant for federated (but required)
|
||||
get_ursulas_params = {
|
||||
'quantity': quantity,
|
||||
'duration_periods': duration, # irrelevant for federated (but required)
|
||||
}
|
||||
response = federated_porter_basic_auth_web_controller.get('/get_ursulas', data=json.dumps(get_ursulas_params))
|
||||
assert response.status_code == 401 # user unauthorized
|
||||
|
||||
random_bob_encrypting_key, random_treasure_map_id, random_treasure_map = random_federated_treasure_map_data
|
||||
|
||||
# /get_treasure_map
|
||||
get_treasure_map_params = {
|
||||
'treasure_map_id': random_treasure_map_id,
|
||||
'bob_encrypting_key': bytes(random_bob_encrypting_key).hex()
|
||||
}
|
||||
response = federated_porter_basic_auth_web_controller.get('/get_treasure_map',
|
||||
data=json.dumps(get_treasure_map_params))
|
||||
assert response.status_code == 401 # user not authenticated
|
||||
|
||||
# /publish_treasure_map
|
||||
publish_treasure_map_params = {
|
||||
'treasure_map': b64encode(bytes(random_treasure_map)).decode(),
|
||||
'bob_encrypting_key': bytes(random_bob_encrypting_key).hex()
|
||||
}
|
||||
response = federated_porter_basic_auth_web_controller.post('/publish_treasure_map',
|
||||
data=json.dumps(publish_treasure_map_params))
|
||||
assert response.status_code == 401 # user not authenticated
|
||||
|
||||
# /exec_work_order
|
||||
exec_work_order_params = {
|
||||
'ursula': get_random_checksum_address(),
|
||||
'work_order_payload': b64encode(b"some data").decode()
|
||||
}
|
||||
response = federated_porter_basic_auth_web_controller.post('/exec_work_order',
|
||||
data=json.dumps(exec_work_order_params))
|
||||
assert response.status_code == 401 # user not authenticated
|
||||
|
||||
# try get_ursulas with authentication
|
||||
credentials = b64encode(b"admin:admin").decode('utf-8')
|
||||
response = federated_porter_basic_auth_web_controller.get('/get_ursulas',
|
||||
data=json.dumps(get_ursulas_params),
|
||||
headers={"Authorization": f"Basic {credentials}"})
|
||||
assert response.status_code == 200 # success
|
|
@ -0,0 +1,113 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
import os
|
||||
from base64 import b64decode
|
||||
|
||||
import pytest
|
||||
from nucypher.crypto.powers import DecryptingPower
|
||||
from nucypher.crypto.umbral_adapter import PublicKey
|
||||
from nucypher.policy.maps import TreasureMap
|
||||
from tests.utils.policy import work_order_setup
|
||||
|
||||
|
||||
def test_get_ursulas(federated_porter, federated_ursulas):
|
||||
# simple
|
||||
quantity = 4
|
||||
ursulas_info = federated_porter.get_ursulas(quantity=quantity)
|
||||
returned_ursula_addresses = {ursula_info.checksum_address for ursula_info in ursulas_info}
|
||||
assert len(returned_ursula_addresses) == quantity # ensure no repeats
|
||||
|
||||
federated_ursulas_list = list(federated_ursulas)
|
||||
|
||||
# include specific ursulas
|
||||
include_ursulas = [federated_ursulas_list[0].checksum_address, federated_ursulas_list[1].checksum_address]
|
||||
ursulas_info = federated_porter.get_ursulas(quantity=quantity,
|
||||
include_ursulas=include_ursulas)
|
||||
returned_ursula_addresses = {ursula_info.checksum_address for ursula_info in ursulas_info}
|
||||
assert len(returned_ursula_addresses) == quantity
|
||||
for address in include_ursulas:
|
||||
assert address in returned_ursula_addresses
|
||||
|
||||
# exclude specific ursulas
|
||||
number_to_exclude = len(federated_ursulas_list) - 4
|
||||
exclude_ursulas = []
|
||||
for i in range(number_to_exclude):
|
||||
exclude_ursulas.append(federated_ursulas_list[i].checksum_address)
|
||||
ursulas_info = federated_porter.get_ursulas(quantity=quantity,
|
||||
exclude_ursulas=exclude_ursulas)
|
||||
returned_ursula_addresses = {ursula_info.checksum_address for ursula_info in ursulas_info}
|
||||
assert len(returned_ursula_addresses) == quantity
|
||||
for address in exclude_ursulas:
|
||||
assert address not in returned_ursula_addresses
|
||||
|
||||
# include and exclude
|
||||
include_ursulas = [federated_ursulas_list[0].checksum_address, federated_ursulas_list[1].checksum_address]
|
||||
exclude_ursulas = [federated_ursulas_list[2].checksum_address, federated_ursulas_list[3].checksum_address]
|
||||
ursulas_info = federated_porter.get_ursulas(quantity=quantity,
|
||||
include_ursulas=include_ursulas,
|
||||
exclude_ursulas=exclude_ursulas)
|
||||
returned_ursula_addresses = {ursula_info.checksum_address for ursula_info in ursulas_info}
|
||||
assert len(returned_ursula_addresses) == quantity
|
||||
for address in include_ursulas:
|
||||
assert address in returned_ursula_addresses
|
||||
for address in exclude_ursulas:
|
||||
assert address not in returned_ursula_addresses
|
||||
|
||||
|
||||
def test_publish_and_get_treasure_map(federated_porter,
|
||||
federated_alice,
|
||||
federated_bob,
|
||||
enacted_federated_policy,
|
||||
random_federated_treasure_map_data):
|
||||
random_bob_encrypting_key, random_treasure_map_id, random_treasure_map = random_federated_treasure_map_data
|
||||
|
||||
# ensure that random treasure map cannot be obtained since not available
|
||||
with pytest.raises(TreasureMap.NowhereToBeFound):
|
||||
federated_porter.get_treasure_map(map_identifier=random_treasure_map_id,
|
||||
bob_encrypting_key=random_bob_encrypting_key)
|
||||
|
||||
# publish the random treasure map
|
||||
federated_porter.publish_treasure_map(treasure_map_bytes=random_treasure_map,
|
||||
bob_encrypting_key=random_bob_encrypting_key)
|
||||
|
||||
# try getting the random treasure map now
|
||||
treasure_map = federated_porter.get_treasure_map(map_identifier=random_treasure_map_id,
|
||||
bob_encrypting_key=random_bob_encrypting_key)
|
||||
assert treasure_map.public_id() == random_treasure_map_id
|
||||
|
||||
# try getting an already existing policy
|
||||
map_id = federated_bob.construct_map_id(federated_alice.stamp,
|
||||
enacted_federated_policy.label)
|
||||
treasure_map = federated_porter.get_treasure_map(map_identifier=map_id,
|
||||
bob_encrypting_key=federated_bob.public_keys(DecryptingPower))
|
||||
assert treasure_map == enacted_federated_policy.treasure_map
|
||||
|
||||
|
||||
def test_exec_work_order(federated_porter,
|
||||
federated_ursulas,
|
||||
federated_bob,
|
||||
federated_alice,
|
||||
enacted_federated_policy):
|
||||
# Setup
|
||||
ursula_address, work_order = work_order_setup(enacted_federated_policy,
|
||||
federated_ursulas,
|
||||
federated_bob,
|
||||
federated_alice)
|
||||
|
||||
result = federated_porter.exec_work_order(ursula_address=ursula_address,
|
||||
work_order_payload=work_order.payload())
|
||||
assert result, "valid result returned"
|
|
@ -0,0 +1,335 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
import os
|
||||
from base64 import b64encode
|
||||
|
||||
import pytest
|
||||
|
||||
from nucypher.characters.control.specifications.fields import TreasureMap
|
||||
from nucypher.control.specifications.exceptions import InvalidArgumentCombo, InvalidInputData
|
||||
from nucypher.crypto.constants import ENCRYPTED_KFRAG_PAYLOAD_LENGTH
|
||||
from nucypher.crypto.powers import DecryptingPower
|
||||
from nucypher.crypto.umbral_adapter import SecretKey
|
||||
from nucypher.policy.orders import WorkOrder as WorkOrderClass
|
||||
from nucypher.utilities.porter.control.specifications.fields import UrsulaInfoSchema
|
||||
from nucypher.utilities.porter.control.specifications.porter_schema import (
|
||||
AliceGetUrsulas,
|
||||
AlicePublishTreasureMap,
|
||||
BobGetTreasureMap,
|
||||
BobExecWorkOrder
|
||||
)
|
||||
from nucypher.utilities.porter.porter import Porter
|
||||
|
||||
|
||||
def test_alice_get_ursulas_schema(get_random_checksum_address):
|
||||
#
|
||||
# Input i.e. load
|
||||
#
|
||||
|
||||
# no args
|
||||
with pytest.raises(InvalidInputData):
|
||||
AliceGetUrsulas().load({})
|
||||
|
||||
quantity = 10
|
||||
required_data = {
|
||||
'quantity': quantity,
|
||||
'duration_periods': 4,
|
||||
}
|
||||
|
||||
# required args
|
||||
AliceGetUrsulas().load(required_data)
|
||||
|
||||
# missing required args
|
||||
updated_data = {k: v for k, v in required_data.items() if k != 'quantity'}
|
||||
with pytest.raises(InvalidInputData):
|
||||
AliceGetUrsulas().load(updated_data)
|
||||
|
||||
updated_data = {k: v for k, v in required_data.items() if k != 'duration_periods'}
|
||||
with pytest.raises(InvalidInputData):
|
||||
AliceGetUrsulas().load(updated_data)
|
||||
|
||||
# optional components
|
||||
|
||||
# only exclude
|
||||
updated_data = dict(required_data)
|
||||
exclude_ursulas = []
|
||||
for i in range(2):
|
||||
exclude_ursulas.append(get_random_checksum_address())
|
||||
updated_data['exclude_ursulas'] = exclude_ursulas
|
||||
AliceGetUrsulas().load(updated_data)
|
||||
|
||||
# only include
|
||||
updated_data = dict(required_data)
|
||||
include_ursulas = []
|
||||
for i in range(3):
|
||||
include_ursulas.append(get_random_checksum_address())
|
||||
updated_data['include_ursulas'] = include_ursulas
|
||||
AliceGetUrsulas().load(updated_data)
|
||||
|
||||
# both exclude and include
|
||||
updated_data = dict(required_data)
|
||||
updated_data['exclude_ursulas'] = exclude_ursulas
|
||||
updated_data['include_ursulas'] = include_ursulas
|
||||
AliceGetUrsulas().load(updated_data)
|
||||
|
||||
# list input formatted as ',' separated strings
|
||||
updated_data = dict(required_data)
|
||||
updated_data['exclude_ursulas'] = ','.join(exclude_ursulas)
|
||||
updated_data['include_ursulas'] = ','.join(include_ursulas)
|
||||
data = AliceGetUrsulas().load(updated_data)
|
||||
assert data['exclude_ursulas'] == exclude_ursulas
|
||||
assert data['include_ursulas'] == include_ursulas
|
||||
|
||||
# single value as string cast to list
|
||||
updated_data = dict(required_data)
|
||||
updated_data['exclude_ursulas'] = exclude_ursulas[0]
|
||||
updated_data['include_ursulas'] = include_ursulas[0]
|
||||
data = AliceGetUrsulas().load(updated_data)
|
||||
assert data['exclude_ursulas'] == [exclude_ursulas[0]]
|
||||
assert data['include_ursulas'] == [include_ursulas[0]]
|
||||
|
||||
# invalid include entry
|
||||
updated_data = dict(required_data)
|
||||
updated_data['exclude_ursulas'] = exclude_ursulas
|
||||
updated_data['include_ursulas'] = list(include_ursulas) # make copy to modify
|
||||
updated_data['include_ursulas'].append("0xdeadbeef")
|
||||
with pytest.raises(InvalidInputData):
|
||||
AliceGetUrsulas().load(updated_data)
|
||||
|
||||
# invalid exclude entry
|
||||
updated_data = dict(required_data)
|
||||
updated_data['exclude_ursulas'] = list(exclude_ursulas) # make copy to modify
|
||||
updated_data['exclude_ursulas'].append("0xdeadbeef")
|
||||
updated_data['include_ursulas'] = include_ursulas
|
||||
with pytest.raises(InvalidInputData):
|
||||
AliceGetUrsulas().load(updated_data)
|
||||
|
||||
# too many ursulas to include
|
||||
updated_data = dict(required_data)
|
||||
too_many_ursulas_to_include = []
|
||||
while len(too_many_ursulas_to_include) <= quantity:
|
||||
too_many_ursulas_to_include.append(get_random_checksum_address())
|
||||
updated_data['include_ursulas'] = too_many_ursulas_to_include
|
||||
with pytest.raises(InvalidArgumentCombo):
|
||||
# number of ursulas to include exceeds quantity to sample
|
||||
AliceGetUrsulas().load(updated_data)
|
||||
|
||||
# include and exclude addresses are not mutually exclusive - include has common entry
|
||||
updated_data = dict(required_data)
|
||||
updated_data['exclude_ursulas'] = exclude_ursulas
|
||||
updated_data['include_ursulas'] = list(include_ursulas) # make copy to modify
|
||||
updated_data['include_ursulas'].append(exclude_ursulas[0]) # one address that overlaps
|
||||
with pytest.raises(InvalidArgumentCombo):
|
||||
# 1 address in both include and exclude lists
|
||||
AliceGetUrsulas().load(updated_data)
|
||||
|
||||
# include and exclude addresses are not mutually exclusive - exclude has common entry
|
||||
updated_data = dict(required_data)
|
||||
updated_data['exclude_ursulas'] = list(exclude_ursulas) # make copy to modify
|
||||
updated_data['exclude_ursulas'].append(include_ursulas[0]) # on address that overlaps
|
||||
updated_data['include_ursulas'] = include_ursulas
|
||||
with pytest.raises(InvalidArgumentCombo):
|
||||
# 1 address in both include and exclude lists
|
||||
AliceGetUrsulas().load(updated_data)
|
||||
|
||||
#
|
||||
# Output i.e. dump
|
||||
#
|
||||
ursulas_info = []
|
||||
expected_ursulas_info = []
|
||||
port = 11500
|
||||
for i in range(3):
|
||||
ursula_info = Porter.UrsulaInfo(get_random_checksum_address(),
|
||||
f"https://127.0.0.1:{port+i}",
|
||||
SecretKey.random().public_key())
|
||||
ursulas_info.append(ursula_info)
|
||||
|
||||
# use schema to determine expected output (encrypting key gets changed to hex)
|
||||
expected_ursulas_info.append(UrsulaInfoSchema().dump(ursula_info))
|
||||
|
||||
output = AliceGetUrsulas().dump(obj={'ursulas': ursulas_info})
|
||||
assert output == {"ursulas": expected_ursulas_info}
|
||||
|
||||
|
||||
def test_alice_publish_treasure_map_schema_federated_context(enacted_federated_policy, federated_bob):
|
||||
# since federated, schema's context must be set - so create one schema
|
||||
# and reuse (it doesn't hold state other than the context)
|
||||
alice_publish_treasure_map_schema = AlicePublishTreasureMap()
|
||||
alice_publish_treasure_map_schema.context[TreasureMap.IS_FEDERATED_CONTEXT_KEY] = True
|
||||
|
||||
# no args
|
||||
with pytest.raises(InvalidInputData):
|
||||
alice_publish_treasure_map_schema.load({})
|
||||
|
||||
treasure_map_b64 = b64encode(bytes(enacted_federated_policy.treasure_map)).decode()
|
||||
bob_encrypting_key = federated_bob.public_keys(DecryptingPower)
|
||||
bob_encrypting_key_hex = bytes(bob_encrypting_key).hex()
|
||||
|
||||
required_data = {
|
||||
'treasure_map': treasure_map_b64,
|
||||
'bob_encrypting_key': bob_encrypting_key_hex
|
||||
}
|
||||
|
||||
# required args
|
||||
alice_publish_treasure_map_schema.load(required_data)
|
||||
|
||||
# missing required args
|
||||
updated_data = {k: v for k, v in required_data.items() if k != 'treasure_map'}
|
||||
with pytest.raises(InvalidInputData):
|
||||
alice_publish_treasure_map_schema.load(updated_data)
|
||||
|
||||
updated_data = {k: v for k, v in required_data.items() if k != 'bob_encrypting_key'}
|
||||
with pytest.raises(InvalidInputData):
|
||||
alice_publish_treasure_map_schema.load(updated_data)
|
||||
|
||||
# invalid treasure map
|
||||
updated_data = dict(required_data)
|
||||
updated_data['treasure_map'] = b64encode(b"testing").decode()
|
||||
with pytest.raises(InvalidInputData):
|
||||
alice_publish_treasure_map_schema.load(updated_data)
|
||||
|
||||
# invalid encrypting key
|
||||
updated_data = dict(required_data)
|
||||
updated_data['bob_encrypting_key'] = b'123456'.hex()
|
||||
with pytest.raises(InvalidInputData):
|
||||
alice_publish_treasure_map_schema.load(updated_data)
|
||||
|
||||
# Test Output - test only true since there is no false ever returned
|
||||
response_data = {'published': True}
|
||||
output = alice_publish_treasure_map_schema.dump(obj=response_data)
|
||||
assert output == response_data
|
||||
|
||||
# setting federated context to False fails
|
||||
alice_publish_treasure_map_schema.context[TreasureMap.IS_FEDERATED_CONTEXT_KEY] = False
|
||||
with pytest.raises(InvalidInputData):
|
||||
# failed because non-federated (blockchain) treasure map expected, but instead federated treasure map provided
|
||||
alice_publish_treasure_map_schema.load(required_data)
|
||||
|
||||
|
||||
def test_alice_revoke():
|
||||
pass # TODO
|
||||
|
||||
|
||||
def test_bob_get_treasure_map(enacted_federated_policy, federated_alice, federated_bob):
|
||||
#
|
||||
# Input i.e. load
|
||||
#
|
||||
|
||||
# no args
|
||||
with pytest.raises(InvalidInputData):
|
||||
BobGetTreasureMap().load({})
|
||||
|
||||
treasure_map_id = federated_bob.construct_map_id(federated_alice.stamp, enacted_federated_policy.label)
|
||||
bob_encrypting_key = federated_bob.public_keys(DecryptingPower)
|
||||
bob_encrypting_key_hex = bytes(bob_encrypting_key).hex()
|
||||
|
||||
required_data = {
|
||||
'treasure_map_id': treasure_map_id,
|
||||
'bob_encrypting_key': bob_encrypting_key_hex
|
||||
}
|
||||
|
||||
# required args
|
||||
BobGetTreasureMap().load(required_data)
|
||||
|
||||
# random 16-byte length map id
|
||||
updated_data = dict(required_data)
|
||||
updated_data['treasure_map_id'] = "93a9482bdf3b4f2e9df906a35144ca93"
|
||||
BobGetTreasureMap().load(updated_data)
|
||||
|
||||
# missing required args
|
||||
updated_data = {k: v for k, v in required_data.items() if k != 'treasure_map_id'}
|
||||
with pytest.raises(InvalidInputData):
|
||||
BobGetTreasureMap().load(updated_data)
|
||||
|
||||
updated_data = {k: v for k, v in required_data.items() if k != 'bob_encrypting_key'}
|
||||
with pytest.raises(InvalidInputData):
|
||||
BobGetTreasureMap().load(updated_data)
|
||||
|
||||
# invalid treasure map id
|
||||
updated_data = dict(required_data)
|
||||
updated_data['treasure_map_id'] = b'fake_id'.hex()
|
||||
with pytest.raises(InvalidInputData):
|
||||
BobGetTreasureMap().load(updated_data)
|
||||
|
||||
# invalid encrypting key
|
||||
updated_data = dict(required_data)
|
||||
updated_data['bob_encrypting_key'] = b'123456'.hex()
|
||||
with pytest.raises(InvalidInputData):
|
||||
BobGetTreasureMap().load(updated_data)
|
||||
|
||||
#
|
||||
# Output i.e. dump
|
||||
#
|
||||
treasure_map = enacted_federated_policy.treasure_map
|
||||
result = {'treasure_map': treasure_map}
|
||||
output = BobGetTreasureMap().dump(obj=result)
|
||||
assert output == {'treasure_map': b64encode(bytes(treasure_map)).decode()}
|
||||
|
||||
|
||||
def test_bob_exec_work_order(mock_ursula_reencrypts,
|
||||
federated_ursulas,
|
||||
get_random_checksum_address,
|
||||
federated_bob,
|
||||
federated_alice,
|
||||
random_policy_label):
|
||||
# Setup
|
||||
ursula = list(federated_ursulas)[0]
|
||||
tasks = [mock_ursula_reencrypts(ursula) for _ in range(3)]
|
||||
material = [(task.capsule, task.signature, task.cfrag, task.cfrag_signature) for task in tasks]
|
||||
capsules, signatures, cfrags, cfrag_signatures = zip(*material)
|
||||
|
||||
mock_kfrag = os.urandom(ENCRYPTED_KFRAG_PAYLOAD_LENGTH)
|
||||
|
||||
# Test construction of WorkOrders by Bob
|
||||
work_order = WorkOrderClass.construct_by_bob(encrypted_kfrag=mock_kfrag,
|
||||
bob=federated_bob,
|
||||
publisher_verifying_key=federated_alice.stamp.as_umbral_pubkey(),
|
||||
alice_verifying_key=federated_alice.stamp.as_umbral_pubkey(),
|
||||
ursula=ursula,
|
||||
capsules=capsules,
|
||||
label=random_policy_label)
|
||||
|
||||
# Test Work Order
|
||||
work_order_bytes = work_order.payload()
|
||||
|
||||
# no args
|
||||
with pytest.raises(InvalidInputData):
|
||||
BobExecWorkOrder().load({})
|
||||
|
||||
work_order_b64 = b64encode(work_order_bytes).decode()
|
||||
required_data = {
|
||||
'ursula': ursula.checksum_address,
|
||||
'work_order_payload': work_order_b64
|
||||
}
|
||||
|
||||
# required args
|
||||
BobExecWorkOrder().load(required_data)
|
||||
|
||||
# missing required args
|
||||
updated_data = {k: v for k, v in required_data.items() if k != 'ursula'}
|
||||
with pytest.raises(InvalidInputData):
|
||||
BobExecWorkOrder().load(updated_data)
|
||||
|
||||
updated_data = {k: v for k, v in required_data.items() if k != 'work_order_payload'}
|
||||
with pytest.raises(InvalidInputData):
|
||||
BobExecWorkOrder().load(updated_data)
|
||||
|
||||
# invalid ursula checksum address
|
||||
updated_data = dict(required_data)
|
||||
updated_data['ursula'] = "0xdeadbeef"
|
||||
with pytest.raises(InvalidInputData):
|
||||
BobExecWorkOrder().load(updated_data)
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue