Merge pull request #2664 from nucypher/porter

[EPIC] Porter MVP - "Infura for NuCypher"
pull/2771/head
KPrasch 2021-08-04 10:53:18 -07:00 committed by GitHub
commit 47d281a30e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
105 changed files with 6491 additions and 784 deletions

View File

@ -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

View File

@ -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"]

View File

@ -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

View File

@ -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"
}

View File

@ -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.

View File

@ -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

View File

@ -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
-----

View File

@ -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
^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -0,0 +1 @@
Introduction of NuCypher Porter - a web-based service that performs ``nucypher`` protocol operations on behalf of applications for cross-platform functionality.

View File

@ -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

View File

@ -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}

View File

@ -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,

View File

@ -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):

View File

@ -75,16 +75,6 @@ the Encryptor.
"""
MOE_BANNER = r"""
_______
| | |.-----..-----.
| || _ || -__|
|__|_|__||_____||_____|
the Monitor.
"""
URSULA_BANNER = r'''

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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')

View File

@ -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)

View File

@ -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):

View File

@ -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}")

View File

@ -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)

View File

@ -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

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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}")

View File

@ -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,

View File

@ -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,

View File

@ -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

View File

@ -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 (

View File

@ -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,

View File

@ -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,

View File

@ -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')

View File

@ -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:

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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,

View File

@ -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)

View File

@ -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

View File

@ -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"

View File

@ -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:

View File

@ -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)

View File

@ -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,

View File

@ -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,

View File

@ -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()

View File

@ -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/>.
"""

View File

@ -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)

View File

@ -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):

View File

@ -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

View File

@ -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/>.
"""

View File

@ -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):

View File

@ -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"""

View File

@ -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 *

View File

@ -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}")

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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/>.
"""

View File

@ -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/>.
"""

View File

@ -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

View File

@ -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

View File

@ -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/>.
"""

View File

@ -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 *

View File

@ -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")

View File

@ -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()

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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")

View File

@ -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

View File

@ -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))

View File

@ -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

View File

@ -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()

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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()

View File

@ -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:

View File

@ -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,

View File

@ -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

View File

@ -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()

View File

@ -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)

View File

@ -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

View File

@ -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"

View File

@ -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