Merge pull request #3446 from nucypher/v7.4.x

v7.4.x
v7.4.1-hotfix v7.4.0
Derek Pierre 2024-08-12 09:46:38 -04:00 committed by GitHub
commit 42c3ac528e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
62 changed files with 3164 additions and 2023 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 7.3.0
current_version = 7.4.0
commit = True
tag = False
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(-(?P<stage>[^.]*)\.(?P<devnum>\d+))?

View File

@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [ "3.8", "3.12" ]
python-version: [ "3.9", "3.12" ]
steps:
- name: Checkout repo
@ -142,7 +142,7 @@ jobs:
# Only upload coverage files after all tests have passed
- name: Upload unit tests coverage to Codecov
if: matrix.python-version == '3.12'
uses: codecov/codecov-action@v4.3.0
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: unit-coverage.xml
@ -152,7 +152,7 @@ jobs:
- name: Upload integration tests coverage to Codecov
if: matrix.python-version == '3.12'
uses: codecov/codecov-action@v4.3.0
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: integration-coverage.xml
@ -162,7 +162,7 @@ jobs:
- name: Upload acceptance tests coverage to Codecov
if: matrix.python-version == '3.12'
uses: codecov/codecov-action@v4.3.0
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
directory: tests/acceptance

View File

@ -27,4 +27,4 @@ jobs:
pip install .
- name: Lint with Ruff
run: ruff --output-format=github nucypher
run: ruff check --output-format=github nucypher

View File

@ -12,7 +12,7 @@ repos:
stages: [push] # required additional setup: pre-commit install && pre-commit install -t pre-push
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.3.0
rev: v4.5.0
hooks:
# Git
@ -36,7 +36,7 @@ repos:
- id: detect-private-key
- repo: https://github.com/akaihola/darker
rev: 1.7.2
rev: v2.1.1
hooks:
- id: darker
args: ["--check"]
@ -45,6 +45,6 @@ repos:
stages: [commit]
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: 'v0.1.4'
rev: v0.4.5
hooks:
- id: ruff

View File

@ -13,4 +13,4 @@ recursive-include nucypher/blockchain/eth/contract_registry *.json
recursive-include nucypher/policy/conditions *.json
recursive-include nucypher/network/templates *.html *.mako
recursive-exclude nucypher/utilities/templates *.html *.mako
recursive-include nucypher/acumen/ *json
recursive-include nucypher/acumen *.json

View File

@ -1,5 +1,3 @@
version: '3'
services:
nucypher:
image: nucypher:latest

View File

@ -1,191 +1,185 @@
aiohttp==3.9.4rc0 ; python_version >= "3.8" and python_version < "4"
aiosignal==1.3.1 ; python_version >= "3.8" and python_version < "4"
annotated-types==0.6.0 ; python_version >= "3.8" and python_version < "4"
ape-solidity==0.7.1 ; python_version >= "3.8" and python_version < "4"
appdirs==1.4.4 ; python_version >= "3.8" and python_version < "4"
appnope==0.1.4 ; python_version >= "3.8" and python_version < "4" and sys_platform == "darwin"
asttokens==2.4.1 ; python_version >= "3.8" and python_version < "4"
async-timeout==4.0.3 ; python_version >= "3.8" and python_version < "3.11"
atomicwrites==1.4.1 ; python_version >= "3.8" and python_version < "4" and sys_platform == "win32"
attrs==23.2.0 ; python_version >= "3.8" and python_version < "4"
atxm==0.3.0 ; python_version >= "3.8" and python_version < "4"
autobahn==23.1.2 ; python_version >= "3.8" and python_version < "4"
automat==22.10.0 ; python_version >= "3.8" and python_version < "4"
backcall==0.2.0 ; python_version >= "3.8" and python_version < "4"
backports-zoneinfo==0.2.1 ; python_version >= "3.8" and python_version < "3.9"
base58==1.0.3 ; python_version >= "3.8" and python_version < "4"
bitarray==2.9.2 ; python_version >= "3.8" and python_version < "4"
blinker==1.7.0 ; python_version >= "3.8" and python_version < "4"
bytestring-splitter==2.4.1 ; python_version >= "3.8" and python_version < "4"
cached-property==1.5.2 ; python_version >= "3.8" and python_version < "4"
certifi==2024.2.2 ; python_version >= "3.8" and python_version < "4"
cffi==1.16.0 ; python_version >= "3.8" and python_version < "4"
cfgv==3.4.0 ; python_version >= "3.8" and python_version < "4"
charset-normalizer==3.3.2 ; python_version >= "3.8" and python_version < "4"
click==8.1.7 ; python_version >= "3.8" and python_version < "4"
colorama==0.4.6 ; python_version >= "3.8" and python_version < "4"
constant-sorrow==0.1.0a9 ; python_version >= "3.8" and python_version < "4"
constantly==23.10.4 ; python_version >= "3.8" and python_version < "4"
coverage==7.4.4 ; python_version >= "3.8" and python_version < "4"
coverage[toml]==7.4.4 ; python_version >= "3.8" and python_version < "4"
cryptography==42.0.5 ; python_version >= "3.8" and python_version < "4"
cytoolz==0.12.3 ; python_version >= "3.8" and python_version < "4" and implementation_name == "cpython"
dataclassy==0.11.1 ; python_version >= "3.8" and python_version < "4"
dateparser==1.2.0 ; python_version >= "3.8" and python_version < "4"
decorator==5.1.1 ; python_version >= "3.8" and python_version < "4"
deprecated==1.2.14 ; python_version >= "3.8" and python_version < "4"
distlib==0.3.8 ; python_version >= "3.8" and python_version < "4"
eip712==0.2.5 ; python_version >= "3.8" and python_version < "4"
eth-abi==4.2.1 ; python_version >= "3.8" and python_version < "4"
eth-account==0.10.0 ; python_version >= "3.8" and python_version < "4"
eth-ape==0.7.13 ; python_version >= "3.8" and python_version < "4"
eth-bloom==3.0.0 ; python_version >= "3.8" and python_version < "4"
eth-hash==0.7.0 ; python_version >= "3.8" and python_version < "4"
eth-hash[pycryptodome]==0.7.0 ; python_version >= "3.8" and python_version < "4"
eth-hash[pysha3]==0.7.0 ; python_version >= "3.8" and python_version < "4" and implementation_name == "cpython"
eth-keyfile==0.8.0 ; python_version >= "3.8" and python_version < "4"
eth-keys==0.4.0 ; python_version >= "3.8" and python_version < "4"
eth-pydantic-types==0.1.0 ; python_version >= "3.8" and python_version < "4"
eth-rlp==1.0.1 ; python_version >= "3.8" and python_version < "4"
eth-tester[py-evm]==0.9.1b2 ; python_version >= "3.8" and python_version < "4"
eth-typing==3.5.2 ; python_version >= "3.8" and python_version < "4"
eth-utils==2.3.1 ; python_version >= "3.8" and python_version < "4"
ethpm-types==0.6.9 ; python_version >= "3.8" and python_version < "4"
evm-trace==0.1.3 ; python_version >= "3.8" and python_version < "4"
evmchains==0.0.6 ; python_version >= "3.8" and python_version < "4"
executing==2.0.1 ; python_version >= "3.8" and python_version < "4"
filelock==3.13.4 ; python_version >= "3.8" and python_version < "4"
flask==3.0.3 ; python_version >= "3.8" and python_version < "4"
frozenlist==1.4.1 ; python_version >= "3.8" and python_version < "4"
greenlet==3.0.3 ; python_version >= "3.8" and python_version < "4"
hendrix==5.0.0 ; python_version >= "3.8" and python_version < "4"
hexbytes==0.3.1 ; python_version >= "3.8" and python_version < "4"
humanize==4.9.0 ; python_version >= "3.8" and python_version < "4"
hyperlink==21.0.0 ; python_version >= "3.8" and python_version < "4"
identify==2.5.35 ; python_version >= "3.8" and python_version < "4"
idna==3.7 ; python_version >= "3.8" and python_version < "4"
ijson==3.2.3 ; python_version >= "3.8" and python_version < "4"
importlib-metadata==7.1.0 ; python_version >= "3.8" and python_version < "4"
importlib-resources==6.4.0 ; python_version >= "3.8" and python_version < "3.9"
incremental==22.10.0 ; python_version >= "3.8" and python_version < "4"
iniconfig==2.0.0 ; python_version >= "3.8" and python_version < "4"
ipython==8.12.3 ; python_version >= "3.8" and python_version < "4"
itsdangerous==2.1.2 ; python_version >= "3.8" and python_version < "4"
jedi==0.19.1 ; python_version >= "3.8" and python_version < "4"
jinja2==3.1.3 ; python_version >= "3.8" and python_version < "4"
jsonschema-specifications==2023.12.1 ; python_version >= "3.8" and python_version < "4"
jsonschema==4.21.1 ; python_version >= "3.8" and python_version < "4"
lazyasd==0.1.4 ; python_version >= "3.8" and python_version < "4"
lru-dict==1.2.0 ; python_version >= "3.8" and python_version < "4"
mako==1.3.3 ; python_version >= "3.8" and python_version < "4"
markdown-it-py==3.0.0 ; python_version >= "3.8" and python_version < "4"
markupsafe==2.1.5 ; python_version >= "3.8" and python_version < "4"
marshmallow==3.21.1 ; python_version >= "3.8" and python_version < "4"
matplotlib-inline==0.1.7 ; python_version >= "3.8" and python_version < "4"
maya==0.6.1 ; python_version >= "3.8" and python_version < "4"
mdurl==0.1.2 ; python_version >= "3.8" and python_version < "4"
mnemonic==0.20 ; python_version >= "3.8" and python_version < "4"
morphys==1.0 ; python_version >= "3.8" and python_version < "4"
msgpack-python==0.5.6 ; python_version >= "3.8" and python_version < "4"
msgspec==0.18.6 ; python_version >= "3.8" and python_version < "4"
multidict==6.0.5 ; python_version >= "3.8" and python_version < "4"
mypy-extensions==1.0.0 ; python_version >= "3.8" and python_version < "4"
nodeenv==1.8.0 ; python_version >= "3.8" and python_version < "4"
nucypher-core==0.13.0 ; python_version >= "3.8" and python_version < "4"
numpy==1.24.4 ; python_version >= "3.8" and python_version < "3.9"
abnf==2.2.0 ; python_version >= "3.9" and python_version < "4.0"
aiohappyeyeballs==2.3.2 ; python_version >= "3.9" and python_version < "4.0"
aiohttp==3.10.0 ; python_version >= "3.9" and python_version < "4"
aiosignal==1.3.1 ; python_version >= "3.9" and python_version < "4"
annotated-types==0.7.0 ; python_version >= "3.9" and python_version < "4.0"
ape-solidity==0.7.3 ; python_version >= "3.9" and python_version < "4"
appdirs==1.4.4 ; python_version >= "3.9" and python_version < "4"
asttokens==2.4.1 ; python_version >= "3.9" and python_version < "4"
async-timeout==4.0.3 ; python_version >= "3.9" and python_version < "3.11"
atomicwrites==1.4.1 ; python_version >= "3.9" and python_version < "4" and sys_platform == "win32"
attrs==23.2.0 ; python_version >= "3.9" and python_version < "4"
atxm==0.5.0 ; python_version >= "3.9" and python_version < "4"
autobahn==23.6.2 ; python_version >= "3.9" and python_version < "4"
automat==22.10.0 ; python_version >= "3.9" and python_version < "4"
base58==1.0.3 ; python_version >= "3.9" and python_version < "4"
bitarray==2.9.2 ; python_version >= "3.9" and python_version < "4"
blinker==1.8.2 ; python_version >= "3.9" and python_version < "4"
bytestring-splitter==2.4.1 ; python_version >= "3.9" and python_version < "4"
cached-property==1.5.2 ; python_version >= "3.9" and python_version < "4"
certifi==2024.7.4 ; python_version >= "3.9" and python_version < "4"
cffi==1.16.0 ; python_version >= "3.9" and python_version < "4"
cfgv==3.4.0 ; python_version >= "3.9" and python_version < "4"
charset-normalizer==3.3.2 ; python_version >= "3.9" and python_version < "4"
ckzg==1.0.2 ; python_version >= "3.9" and python_version < "4"
click==8.1.7 ; python_version >= "3.9" and python_version < "4"
colorama==0.4.6 ; python_version >= "3.9" and python_version < "4"
constant-sorrow==0.1.0a9 ; python_version >= "3.9" and python_version < "4"
constantly==23.10.4 ; python_version >= "3.9" and python_version < "4"
coverage==7.6.0 ; python_version >= "3.9" and python_version < "4"
coverage[toml]==7.6.0 ; python_version >= "3.9" and python_version < "4"
cryptography==43.0.0 ; python_version >= "3.9" and python_version < "4"
cytoolz==0.12.3 ; python_version >= "3.9" and python_version < "4" and implementation_name == "cpython"
dataclassy==0.11.1 ; python_version >= "3.9" and python_version < "4"
dateparser==1.2.0 ; python_version >= "3.9" and python_version < "4"
decorator==5.1.1 ; python_version >= "3.9" and python_version < "4"
deprecated==1.2.14 ; python_version >= "3.9" and python_version < "4"
distlib==0.3.8 ; python_version >= "3.9" and python_version < "4"
eip712==0.2.7 ; python_version >= "3.9" and python_version < "4"
eth-abi==5.1.0 ; python_version >= "3.9" and python_version < "4"
eth-account==0.11.2 ; python_version >= "3.9" and python_version < "4"
eth-ape==0.7.23 ; python_version >= "3.9" and python_version < "4"
eth-bloom==3.0.1 ; python_version >= "3.9" and python_version < "4"
eth-hash==0.7.0 ; python_version >= "3.9" and python_version < "4"
eth-hash[pycryptodome]==0.7.0 ; python_version >= "3.9" and python_version < "4"
eth-hash[pysha3]==0.7.0 ; python_version >= "3.9" and python_version < "4" and implementation_name == "cpython"
eth-keyfile==0.8.1 ; python_version >= "3.9" and python_version < "4"
eth-keys==0.5.1 ; python_version >= "3.9" and python_version < "4"
eth-pydantic-types==0.1.0 ; python_version >= "3.9" and python_version < "4"
eth-rlp==1.0.1 ; python_version >= "3.9" and python_version < "4"
eth-tester[py-evm]==0.11.0b2 ; python_version >= "3.9" and python_version < "4"
eth-typing==3.5.2 ; python_version >= "3.9" and python_version < "4"
eth-utils==2.3.1 ; python_version >= "3.9" and python_version < "4"
ethpm-types==0.6.14 ; python_version >= "3.9" and python_version < "4"
evm-trace==0.1.5 ; python_version >= "3.9" and python_version < "4"
evmchains==0.0.11 ; python_version >= "3.9" and python_version < "4"
exceptiongroup==1.2.2 ; python_version >= "3.9" and python_version < "3.11"
executing==2.0.1 ; python_version >= "3.9" and python_version < "4"
filelock==3.15.4 ; python_version >= "3.9" and python_version < "4"
flask==3.0.3 ; python_version >= "3.9" and python_version < "4"
frozenlist==1.4.1 ; python_version >= "3.9" and python_version < "4"
greenlet==3.0.3 ; python_version >= "3.9" and python_version < "4"
hendrix==5.0.0 ; python_version >= "3.9" and python_version < "4"
hexbytes==0.3.1 ; python_version >= "3.9" and python_version < "4"
humanize==4.10.0 ; python_version >= "3.9" and python_version < "4"
hyperlink==21.0.0 ; python_version >= "3.9" and python_version < "4"
identify==2.6.0 ; python_version >= "3.9" and python_version < "4"
idna==3.7 ; python_version >= "3.9" and python_version < "4"
ijson==3.3.0 ; python_version >= "3.9" and python_version < "4"
importlib-metadata==8.2.0 ; python_version >= "3.9" and python_version < "3.10"
incremental==24.7.2 ; python_version >= "3.9" and python_version < "4"
iniconfig==2.0.0 ; python_version >= "3.9" and python_version < "4"
ipython==8.18.1 ; python_version >= "3.9" and python_version < "4"
itsdangerous==2.2.0 ; python_version >= "3.9" and python_version < "4"
jedi==0.19.1 ; python_version >= "3.9" and python_version < "4"
jinja2==3.1.4 ; python_version >= "3.9" and python_version < "4"
jsonschema-specifications==2023.12.1 ; python_version >= "3.9" and python_version < "4"
jsonschema==4.23.0 ; python_version >= "3.9" and python_version < "4"
lazyasd==0.1.4 ; python_version >= "3.9" and python_version < "4"
lru-dict==1.2.0 ; python_version >= "3.9" and python_version < "4"
mako==1.3.5 ; python_version >= "3.9" and python_version < "4"
markdown-it-py==3.0.0 ; python_version >= "3.9" and python_version < "4"
markupsafe==2.1.5 ; python_version >= "3.9" and python_version < "4"
marshmallow==3.21.3 ; python_version >= "3.9" and python_version < "4"
matplotlib-inline==0.1.7 ; python_version >= "3.9" and python_version < "4"
maya==0.6.1 ; python_version >= "3.9" and python_version < "4"
mdurl==0.1.2 ; python_version >= "3.9" and python_version < "4"
mnemonic==0.21 ; python_version >= "3.9" and python_version < "4"
morphys==1.0 ; python_version >= "3.9" and python_version < "4"
msgpack-python==0.5.6 ; python_version >= "3.9" and python_version < "4"
msgspec==0.18.6 ; python_version >= "3.9" and python_version < "4"
multidict==6.0.5 ; python_version >= "3.9" and python_version < "4"
nodeenv==1.9.1 ; python_version >= "3.9" and python_version < "4"
nucypher-core==0.13.0 ; python_version >= "3.9" and python_version < "4"
numpy==1.26.4 ; python_version >= "3.9" and python_version < "4"
packaging==23.2 ; python_version >= "3.8" and python_version < "4"
pandas==1.5.3 ; python_version >= "3.8" and python_version < "4"
parsimonious==0.9.0 ; python_version >= "3.8" and python_version < "4"
parso==0.8.4 ; python_version >= "3.8" and python_version < "4"
pendulum==3.0.0 ; python_version >= "3.8" and python_version < "4"
pexpect==4.9.0 ; python_version >= "3.8" and python_version < "4" and sys_platform != "win32"
pickleshare==0.7.5 ; python_version >= "3.8" and python_version < "4"
pkgutil-resolve-name==1.3.10 ; python_version >= "3.8" and python_version < "3.9"
platformdirs==4.2.0 ; python_version >= "3.8" and python_version < "4"
pluggy==1.4.0 ; python_version >= "3.8" and python_version < "4"
pre-commit==2.21.0 ; python_version >= "3.8" and python_version < "4"
prometheus-client==0.20.0 ; python_version >= "3.8" and python_version < "4"
prompt-toolkit==3.0.43 ; python_version >= "3.8" and python_version < "4"
protobuf==5.26.1 ; python_version >= "3.8" and python_version < "4"
ptyprocess==0.7.0 ; python_version >= "3.8" and python_version < "4" and sys_platform != "win32"
pure-eval==0.2.2 ; python_version >= "3.8" and python_version < "4"
py-cid==0.3.0 ; python_version >= "3.8" and python_version < "4"
py-ecc==6.0.0 ; python_version >= "3.8" and python_version < "4"
py-evm==0.7.0a4 ; python_version >= "3.8" and python_version < "4"
py-geth==4.4.0 ; python_version >= "3.8" and python_version < "4"
py-multibase==1.0.3 ; python_version >= "3.8" and python_version < "4"
py-multicodec==0.2.1 ; python_version >= "3.8" and python_version < "4"
py-multihash==0.2.3 ; python_version >= "3.8" and python_version < "4"
py-solc-x==2.0.2 ; python_version >= "3.8" and python_version < "4"
py==1.11.0 ; python_version >= "3.8" and python_version < "4"
pyasn1-modules==0.4.0 ; python_version >= "3.8" and python_version < "4"
pyasn1==0.6.0 ; python_version >= "3.8" and python_version < "4"
pychalk==2.0.1 ; python_version >= "3.8" and python_version < "4"
pycparser==2.22 ; python_version >= "3.8" and python_version < "4"
pycryptodome==3.20.0 ; python_version >= "3.8" and python_version < "4"
pydantic-core==2.14.6 ; python_version >= "3.8" and python_version < "4"
pydantic-settings==2.2.1 ; python_version >= "3.8" and python_version < "4"
pydantic==2.5.3 ; python_version >= "3.8" and python_version < "4"
pyethash==0.1.27 ; python_version >= "3.8" and python_version < "4"
pygithub==1.59.1 ; python_version >= "3.8" and python_version < "4"
pygments==2.17.2 ; python_version >= "3.8" and python_version < "4"
pyjwt[crypto]==2.8.0 ; python_version >= "3.8" and python_version < "4"
pynacl==1.5.0 ; python_version >= "3.8" and python_version < "4"
pyopenssl==24.1.0 ; python_version >= "3.8" and python_version < "4"
pysha3==1.0.2 ; python_version < "3.9" and python_version >= "3.8" and implementation_name == "cpython"
pytest-cov==5.0.0 ; python_version >= "3.8" and python_version < "4"
pytest-mock==3.14.0 ; python_version >= "3.8" and python_version < "4"
pytest-timeout==2.2.0 ; python_version >= "3.8" and python_version < "4"
pytest-twisted==1.14.1 ; python_version >= "3.8" and python_version < "4"
pytest==6.2.5 ; python_version >= "3.8" and python_version < "4"
python-baseconv==1.2.2 ; python_version >= "3.8" and python_version < "4"
python-dateutil==2.9.0.post0 ; python_version >= "3.8" and python_version < "4"
python-dotenv==1.0.1 ; python_version >= "3.8" and python_version < "4"
python-statemachine==2.1.2 ; python_version >= "3.8" and python_version < "3.13"
pytz==2024.1 ; python_version >= "3.8" and python_version < "4"
pyunormalize==15.1.0 ; python_version >= "3.8" and python_version < "4"
pywin32==306 ; python_version >= "3.8" and python_version < "4" and platform_system == "Windows"
pyyaml==6.0.1 ; python_version >= "3.8" and python_version < "4"
referencing==0.34.0 ; python_version >= "3.8" and python_version < "4"
regex==2023.12.25 ; python_version >= "3.8" and python_version < "4"
requests==2.31.0 ; python_version >= "3.8" and python_version < "4"
rich==13.7.1 ; python_version >= "3.8" and python_version < "4"
rlp==3.0.0 ; python_version >= "3.8" and python_version < "4"
rpds-py==0.18.0 ; python_version >= "3.8" and python_version < "4"
packaging==23.2 ; python_version >= "3.9" and python_version < "4"
pandas==1.5.3 ; python_version >= "3.9" and python_version < "4"
parsimonious==0.10.0 ; python_version >= "3.9" and python_version < "4"
parso==0.8.4 ; python_version >= "3.9" and python_version < "4"
pendulum==3.0.0 ; python_version >= "3.9" and python_version < "4"
pexpect==4.9.0 ; python_version >= "3.9" and python_version < "4" and sys_platform != "win32"
platformdirs==4.2.2 ; python_version >= "3.9" and python_version < "4"
pluggy==1.5.0 ; python_version >= "3.9" and python_version < "4"
pre-commit==2.21.0 ; python_version >= "3.9" and python_version < "4"
prometheus-client==0.20.0 ; python_version >= "3.9" and python_version < "4"
prompt-toolkit==3.0.47 ; python_version >= "3.9" and python_version < "4"
protobuf==5.27.2 ; python_version >= "3.9" and python_version < "4"
ptyprocess==0.7.0 ; python_version >= "3.9" and python_version < "4" and sys_platform != "win32"
pure-eval==0.2.3 ; python_version >= "3.9" and python_version < "4"
py-cid==0.3.0 ; python_version >= "3.9" and python_version < "4"
py-ecc==7.0.1 ; python_version >= "3.9" and python_version < "4"
py-evm==0.10.1b1 ; python_version >= "3.9" and python_version < "4"
py-geth==4.4.0 ; python_version >= "3.9" and python_version < "4"
py-multibase==1.0.3 ; python_version >= "3.9" and python_version < "4"
py-multicodec==0.2.1 ; python_version >= "3.9" and python_version < "4"
py-multihash==0.2.3 ; python_version >= "3.9" and python_version < "4"
py-solc-x==2.0.3 ; python_version >= "3.9" and python_version < "4"
py==1.11.0 ; python_version >= "3.9" and python_version < "4"
pyasn1-modules==0.4.0 ; python_version >= "3.9" and python_version < "4"
pyasn1==0.6.0 ; python_version >= "3.9" and python_version < "4"
pychalk==2.0.1 ; python_version >= "3.9" and python_version < "4"
pycparser==2.22 ; python_version >= "3.9" and python_version < "4"
pycryptodome==3.20.0 ; python_version >= "3.9" and python_version < "4"
pydantic-core==2.20.1 ; python_version >= "3.9" and python_version < "4.0"
pydantic-settings==2.4.0 ; python_version >= "3.9" and python_version < "4"
pydantic==2.8.2 ; python_version >= "3.9" and python_version < "4.0"
pygithub==1.59.1 ; python_version >= "3.9" and python_version < "4"
pygments==2.18.0 ; python_version >= "3.9" and python_version < "4"
pyjwt[crypto]==2.8.0 ; python_version >= "3.9" and python_version < "4"
pynacl==1.5.0 ; python_version >= "3.9" and python_version < "4"
pyopenssl==24.2.1 ; python_version >= "3.9" and python_version < "4"
pytest-cov==5.0.0 ; python_version >= "3.9" and python_version < "4"
pytest-mock==3.14.0 ; python_version >= "3.9" and python_version < "4"
pytest-timeout==2.2.0 ; python_version >= "3.9" and python_version < "4"
pytest-twisted==1.14.2 ; python_version >= "3.9" and python_version < "4"
pytest==6.2.5 ; python_version >= "3.9" and python_version < "4"
python-baseconv==1.2.2 ; python_version >= "3.9" and python_version < "4"
python-dateutil==2.9.0.post0 ; python_version >= "3.9" and python_version < "4"
python-dotenv==1.0.1 ; python_version >= "3.9" and python_version < "4"
pytz==2024.1 ; python_version >= "3.9" and python_version < "4"
pyunormalize==15.1.0 ; python_version >= "3.9" and python_version < "4"
pywin32==306 ; python_version >= "3.9" and python_version < "4" and platform_system == "Windows"
pyyaml==6.0.1 ; python_version >= "3.9" and python_version < "4"
referencing==0.35.1 ; python_version >= "3.9" and python_version < "4"
regex==2024.7.24 ; python_version >= "3.9" and python_version < "4"
requests==2.32.3 ; python_version >= "3.9" and python_version < "4"
rich==13.7.1 ; python_version >= "3.9" and python_version < "4"
rlp==4.0.1 ; python_version >= "3.9" and python_version < "4"
rpds-py==0.19.1 ; python_version >= "3.9" and python_version < "4"
safe-pysha3==1.0.4 ; python_version >= "3.9" and python_version < "4" and implementation_name == "cpython"
semantic-version==2.10.0 ; python_version >= "3.8" and python_version < "4"
service-identity==24.1.0 ; python_version >= "3.8" and python_version < "4"
setuptools==69.2.0 ; python_version >= "3.8" and python_version < "4"
six==1.16.0 ; python_version >= "3.8" and python_version < "4"
snaptime==0.2.4 ; python_version >= "3.8" and python_version < "4"
sortedcontainers==2.4.0 ; python_version >= "3.8" and python_version < "4"
sqlalchemy==2.0.29 ; python_version >= "3.8" and python_version < "4"
stack-data==0.6.3 ; python_version >= "3.8" and python_version < "4"
tabulate==0.9.0 ; python_version >= "3.8" and python_version < "4"
time-machine==2.14.1 ; python_version >= "3.8" and python_version < "4"
toml==0.10.2 ; python_version >= "3.8" and python_version < "4"
tomli==2.0.1 ; python_full_version <= "3.11.0a6" and python_version >= "3.8"
toolz==0.12.1 ; python_version >= "3.8" and python_version < "4"
tqdm==4.66.2 ; python_version >= "3.8" and python_version < "4"
traitlets==5.14.2 ; python_version >= "3.8" and python_version < "4"
trie==2.2.0 ; python_version >= "3.8" and python_version < "4"
twisted-iocpsupport==1.0.4 ; python_version >= "3.8" and python_version < "4" and platform_system == "Windows"
twisted==24.3.0 ; python_version >= "3.8" and python_version < "4"
txaio==23.1.1 ; python_version >= "3.8" and python_version < "4"
typing-extensions==4.11.0 ; python_version >= "3.8" and python_version < "4"
tzdata==2024.1 ; python_version >= "3.8" and python_version < "4"
tzlocal==5.2 ; python_version >= "3.8" and python_version < "4"
urllib3==2.2.0 ; python_version >= "3.8" and python_version < "4"
varint==1.0.2 ; python_version >= "3.8" and python_version < "4"
virtualenv==20.25.1 ; python_version >= "3.8" and python_version < "4"
watchdog==3.0.0 ; python_version >= "3.8" and python_version < "4"
wcwidth==0.2.13 ; python_version >= "3.8" and python_version < "4"
web3==6.15.1 ; python_version >= "3.8" and python_version < "4"
web3[tester]==6.15.1 ; python_version >= "3.8" and python_version < "4"
websockets==12.0 ; python_version >= "3.8" and python_version < "4"
werkzeug==3.0.2 ; python_version >= "3.8" and python_version < "4"
wrapt==1.16.0 ; python_version >= "3.8" and python_version < "4"
yarl==1.9.4 ; python_version >= "3.8" and python_version < "4"
zipp==3.18.1 ; python_version >= "3.8" and python_version < "4"
zope-interface==6.2 ; python_version >= "3.8" and python_version < "4"
semantic-version==2.10.0 ; python_version >= "3.9" and python_version < "4"
service-identity==24.1.0 ; python_version >= "3.9" and python_version < "4"
setuptools==72.1.0 ; python_version >= "3.9" and python_version < "4"
siwe==4.2.0 ; python_version >= "3.9" and python_version < "4.0"
six==1.16.0 ; python_version >= "3.9" and python_version < "4"
snaptime==0.2.4 ; python_version >= "3.9" and python_version < "4"
sortedcontainers==2.4.0 ; python_version >= "3.9" and python_version < "4"
sqlalchemy==2.0.31 ; python_version >= "3.9" and python_version < "4"
stack-data==0.6.3 ; python_version >= "3.9" and python_version < "4"
tabulate==0.9.0 ; python_version >= "3.9" and python_version < "4"
time-machine==2.14.2 ; python_version >= "3.9" and python_version < "4"
toml==0.10.2 ; python_version >= "3.9" and python_version < "4"
tomli==2.0.1 ; python_version >= "3.9" and python_full_version <= "3.11.0a6"
toolz==0.12.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "pypy" or implementation_name == "cpython")
tqdm==4.66.4 ; python_version >= "3.9" and python_version < "4"
traitlets==5.14.3 ; python_version >= "3.9" and python_version < "4"
trie==3.0.1 ; python_version >= "3.9" and python_version < "4"
twisted-iocpsupport==1.0.4 ; python_version >= "3.9" and python_version < "4" and platform_system == "Windows"
twisted==24.3.0 ; python_version >= "3.9" and python_version < "4"
txaio==23.1.1 ; python_version >= "3.9" and python_version < "4"
typing-extensions==4.12.2 ; python_version >= "3.9" and python_version < "4"
tzdata==2024.1 ; python_version >= "3.9" and python_version < "4"
tzlocal==5.2 ; python_version >= "3.9" and python_version < "4"
urllib3==2.2.2 ; python_version >= "3.9" and python_version < "4"
varint==1.0.2 ; python_version >= "3.9" and python_version < "4"
virtualenv==20.26.3 ; python_version >= "3.9" and python_version < "4"
watchdog==3.0.0 ; python_version >= "3.9" and python_version < "4"
wcwidth==0.2.13 ; python_version >= "3.9" and python_version < "4"
web3==6.20.1 ; python_version >= "3.9" and python_version < "4"
web3[tester]==6.20.1 ; python_version >= "3.9" and python_version < "4"
websockets==12.0 ; python_version >= "3.9" and python_version < "4"
werkzeug==3.0.3 ; python_version >= "3.9" and python_version < "4"
wrapt==1.16.0 ; python_version >= "3.9" and python_version < "4"
yarl==1.9.4 ; python_version >= "3.9" and python_version < "4"
zipp==3.19.2 ; python_version >= "3.9" and python_version < "3.10"
zope-interface==6.4.post2 ; python_version >= "3.9" and python_version < "4"

View File

@ -16,7 +16,7 @@ __url__ = "https://github.com/nucypher/nucypher"
__summary__ = "A threshold access control application to empower privacy in decentralized systems."
__version__ = "7.3.0"
__version__ = "7.4.0"
__author__ = "NuCypher"

View File

@ -4,7 +4,7 @@ import time
import traceback
from collections import defaultdict
from decimal import Decimal
from typing import DefaultDict, Dict, List, Optional, Set, Union
from typing import DefaultDict, Dict, List, Optional, Union
import maya
from atxm.exceptions import InsufficientFunds
@ -37,8 +37,10 @@ from nucypher.blockchain.eth.agents import (
TACoApplicationAgent,
TACoChildApplicationAgent,
)
from nucypher.blockchain.eth.clients import PUBLIC_CHAINS
from nucypher.blockchain.eth.constants import NULL_ADDRESS
from nucypher.blockchain.eth.constants import (
NULL_ADDRESS,
PUBLIC_CHAINS,
)
from nucypher.blockchain.eth.decorators import validate_checksum_address
from nucypher.blockchain.eth.domains import TACoDomain
from nucypher.blockchain.eth.interfaces import (
@ -50,7 +52,12 @@ from nucypher.blockchain.eth.registry import ContractRegistry
from nucypher.blockchain.eth.signers import Signer
from nucypher.blockchain.eth.trackers import dkg
from nucypher.blockchain.eth.trackers.bonding import OperatorBondedTracker
from nucypher.blockchain.eth.utils import truncate_checksum_address
from nucypher.blockchain.eth.utils import (
get_healthy_default_rpc_endpoints,
rpc_endpoint_health_check,
truncate_checksum_address,
)
from nucypher.crypto.ferveo.exceptions import FerveoKeyMismatch
from nucypher.crypto.powers import (
CryptoPower,
RitualisticPower,
@ -64,6 +71,7 @@ from nucypher.policy.payment import ContractPayment
from nucypher.types import PhaseId
from nucypher.utilities.emitters import StdoutEmitter
from nucypher.utilities.logging import Logger
from nucypher.utilities.warnings import render_ferveo_key_mismatch_warning
class BaseActor:
@ -268,37 +276,66 @@ class Operator(BaseActor):
def connect_condition_providers(
self, endpoints: Dict[int, List[str]]
) -> DefaultDict[int, Set[HTTPProvider]]:
providers = defaultdict(set)
) -> DefaultDict[int, List[HTTPProvider]]:
providers = defaultdict(list) # use list to maintain order
# check that we have endpoints for all condition chains
if self.domain.condition_chain_ids != set(endpoints):
if set(self.domain.condition_chain_ids) != set(endpoints):
raise self.ActorError(
f"Missing blockchain endpoints for chains: "
f"{self.domain.condition_chain_ids - set(endpoints)}"
f"{set(self.domain.condition_chain_ids) - set(endpoints)}"
)
# check that each chain id is supported
# ensure that no endpoint uri for a specific chain is repeated
duplicated_endpoint_check = defaultdict(set)
# User-defined endpoints for chains
for chain_id, endpoints in endpoints.items():
if not self._is_permitted_condition_chain(chain_id):
raise NotImplementedError(
f"Chain ID {chain_id} is not supported for condition evaluation by this Operator."
f"Chain ID {chain_id} is not supported for condition evaluation by this operator."
)
# connect to each endpoint and check that they are on the correct chain
for uri in endpoints:
if uri in duplicated_endpoint_check[chain_id]:
self.log.warn(
f"Duplicated user-supplied blockchain uri, {uri}, for condition evaluation on chain {chain_id}; skipping"
)
continue
provider = self._make_condition_provider(uri)
if int(Web3(provider).eth.chain_id) != int(chain_id):
raise self.ActorError(
f"Condition blockchain endpoint {uri} is not on chain {chain_id}"
)
providers[int(chain_id)].add(provider)
healthy = rpc_endpoint_health_check(endpoint=uri)
if not healthy:
self.log.warn(
f"user-supplied condition RPC endpoint {uri} is unhealthy"
)
providers[int(chain_id)].append(provider)
duplicated_endpoint_check[chain_id].add(uri)
# Ingest default/fallback RPC providers for each chain
for chain_id in self.domain.condition_chain_ids:
default_endpoints = get_healthy_default_rpc_endpoints(chain_id)
for uri in default_endpoints:
if uri in duplicated_endpoint_check[chain_id]:
self.log.warn(
f"Duplicated fallback blockchain uri, {uri}, for condition evaluation on chain {chain_id}; skipping"
)
continue
provider = self._make_condition_provider(uri)
providers[chain_id].append(provider)
duplicated_endpoint_check[chain_id].add(uri)
humanized_chain_ids = ", ".join(
_CONDITION_CHAINS[chain_id] for chain_id in providers
)
self.log.info(
f"Connected to {len(providers)} blockchains for condition checking: {humanized_chain_ids}"
f"Connected to {sum(len(v) for v in providers.values())} RPC endpoints for condition "
f"checking on chain IDs {humanized_chain_ids}"
)
return providers
@ -535,6 +572,14 @@ class Operator(BaseActor):
Errors raised by this method are not explicitly caught and are expected
to be handled by the EventActuator.
"""
try:
self.check_ferveo_public_key_match()
except FerveoKeyMismatch:
# crash this node
self.stop(halt_reactor=True)
return
if self.checksum_address not in participants:
message = (
f"{self.checksum_address}|{self.wallet_address} "
@ -588,11 +633,11 @@ class Operator(BaseActor):
ritual_id=ritual.id,
)
except Exception as e:
# TODO: Handle this better #3096
stack_trace = traceback.format_stack()
self.log.critical(
f"Failed to generate a transcript for ritual #{ritual.id}: {str(e)}"
f"Failed to generate a transcript for ritual #{ritual.id}: {str(e)}\n{stack_trace}"
)
raise e
return
# publish the transcript and store the receipt
self.dkg_storage.store_validators(ritual_id=ritual.id, validators=validators)
@ -679,10 +724,11 @@ class Operator(BaseActor):
transcripts=messages,
)
except Exception as e:
self.log.debug(
f"Failed to aggregate transcripts for ritual #{ritual.id}: {str(e)}"
stack_trace = traceback.format_stack()
self.log.critical(
f"Failed to aggregate transcripts for ritual #{ritual.id}: {str(e)}\n{stack_trace}"
)
raise e
return
# publish the transcript with network-wide jitter to avoid tx congestion
time.sleep(random.randint(0, self.AGGREGATION_SUBMISSION_MAX_DELAY))
@ -962,13 +1008,31 @@ class Operator(BaseActor):
f" for {self.staking_provider_address} on {taco_child_pretty_chain_name} with txhash {txhash})",
color="green",
)
else:
# this node's ferveo public key is already published
self.check_ferveo_public_key_match()
emitter.message(
f"✓ Provider's DKG participation public key already set for "
f"{self.staking_provider_address} on {taco_child_pretty_chain_name} at Coordinator {coordinator_address}",
f"{self.staking_provider_address} on Coordinator {coordinator_address}",
color="green",
)
def check_ferveo_public_key_match(self) -> None:
latest_ritual_id = self.coordinator_agent.number_of_rituals()
local_ferveo_key = self.ritual_power.public_key()
onchain_ferveo_key = self.coordinator_agent.get_provider_public_key(
ritual_id=latest_ritual_id, provider=self.staking_provider_address
)
if bytes(local_ferveo_key) != bytes(onchain_ferveo_key):
message = render_ferveo_key_mismatch_warning(
local_key=local_ferveo_key,
onchain_key=onchain_ferveo_key,
)
self.log.critical(message)
raise FerveoKeyMismatch(message)
class PolicyAuthor(NucypherTokenActor):
"""Alice base class for blockchain operations, mocking up new policies!"""

View File

@ -8,9 +8,14 @@ from web3 import Web3
from web3._utils.threads import Timeout
from web3.contract.contract import Contract
from web3.exceptions import TimeExhausted, TransactionNotFound
from web3.middleware import geth_poa_middleware, simple_cache_middleware
from web3.types import TxReceipt, Wei
from nucypher.blockchain.eth.constants import AVERAGE_BLOCK_TIME_IN_SECONDS
from nucypher.blockchain.eth.constants import (
AVERAGE_BLOCK_TIME_IN_SECONDS,
POA_CHAINS,
PUBLIC_CHAINS,
)
from nucypher.blockchain.middleware.retry import (
AlchemyRetryRequestMiddleware,
InfuraRetryRequestMiddleware,
@ -33,28 +38,6 @@ class Web3ClientUnexpectedVersionString(Web3ClientError):
pass
PUBLIC_CHAINS = {
1: "Mainnet",
137: "Polygon/Mainnet",
11155111: "Sepolia",
80002: "Polygon/Amoy",
}
# This list is not exhaustive,
# but is sufficient for the current needs of the project.
POA_CHAINS = {
4, # Rinkeby
5, # Goerli
42, # Kovan
77, # Sokol
100, # xDAI
10200, # gnosis/chiado,
137, # Polygon/Mainnet
80001, # "Polygon/Mumbai"
80002, # "Polygon/Amoy"
}
class EthereumClient:
BLOCK_CONFIRMATIONS_POLLING_TIME = 3 # seconds
TRANSACTION_POLLING_TIME = 0.5 # seconds
@ -98,6 +81,7 @@ class EthereumClient:
self._add_default_middleware()
def _add_default_middleware(self):
# retry request middleware
endpoint_uri = getattr(self.w3.provider, "endpoint_uri", "")
if "infura" in endpoint_uri:
self.log.debug("Adding Infura RPC retry middleware to client")
@ -109,6 +93,22 @@ class EthereumClient:
self.log.debug("Adding RPC retry middleware to client")
self.add_middleware(RetryRequestMiddleware)
# poa middleware
chain_id = self.chain_id
is_poa = chain_id in POA_CHAINS
self.log.debug(
f"Blockchain: {self.chain_name} (chain_id={chain_id}, poa={is_poa})"
)
if is_poa:
# proof-of-authority blockchain
self.log.info("Injecting POA middleware at layer 0")
self.inject_middleware(geth_poa_middleware, layer=0, name="poa")
# simple cache middleware
self.log.debug("Adding simple_cache_middleware")
self.add_middleware(simple_cache_middleware)
@property
def chain_name(self) -> str:
name = PUBLIC_CHAINS.get(self.chain_id, UNKNOWN_DEVELOPMENT_CHAIN_ID)
@ -272,7 +272,7 @@ class EthereumClient:
return (time.time() - self.get_blocktime()) < self.STALECHECK_ALLOWABLE_DELAY
@classmethod
def _get_chain_id(cls, w3: Web3):
def _get_chain_id(cls, w3: Web3) -> int:
result = w3.eth.chain_id
try:
# from hex-str

View File

@ -1,5 +1,3 @@
#
# Contract Names
#
@ -15,7 +13,6 @@ TACO_CHILD_APPLICATION_CONTRACT_NAME = "TACoChildApplication"
COORDINATOR_CONTRACT_NAME = "Coordinator"
SUBSCRIPTION_MANAGER_CONTRACT_NAME = "SubscriptionManager"
TACO_CONTRACT_NAMES = (
TACO_APPLICATION_CONTRACT_NAME,
TACO_CHILD_APPLICATION_CONTRACT_NAME,
@ -23,7 +20,6 @@ TACO_CONTRACT_NAMES = (
SUBSCRIPTION_MANAGER_CONTRACT_NAME
)
# Ethereum
AVERAGE_BLOCK_TIME_IN_SECONDS = 14
@ -37,3 +33,25 @@ NULL_ADDRESS = '0x' + '0' * 40
# NuCypher
# TODO: this is equal to HRAC.SIZE.
POLICY_ID_LENGTH = 16
PUBLIC_CHAINS = {
1: "Mainnet",
137: "Polygon/Mainnet",
11155111: "Sepolia",
80002: "Polygon/Amoy",
}
POA_CHAINS = {
4, # Rinkeby
5, # Goerli
42, # Kovan
77, # Sokol
100, # xDAI
10200, # gnosis/chiado,
137, # Polygon/Mainnet
80001, # "Polygon/Mumbai"
80002, # "Polygon/Amoy"
}
CHAINLIST_URL = "https://raw.githubusercontent.com/nucypher/chainlist/main/rpc.json"

View File

@ -17,11 +17,10 @@ from eth_utils import to_checksum_address
from web3 import HTTPProvider, IPCProvider, Web3, WebsocketProvider
from web3.contract.contract import Contract, ContractConstructor, ContractFunction
from web3.exceptions import TimeExhausted
from web3.middleware import geth_poa_middleware, simple_cache_middleware
from web3.providers import BaseProvider
from web3.types import TxParams, TxReceipt
from nucypher.blockchain.eth.clients import POA_CHAINS, EthereumClient
from nucypher.blockchain.eth.clients import EthereumClient
from nucypher.blockchain.eth.decorators import validate_checksum_address
from nucypher.blockchain.eth.providers import (
_get_http_provider,
@ -240,14 +239,7 @@ class BlockchainInterface:
self.w3 = NO_BLOCKCHAIN_CONNECTION
self.client: EthereumClient = NO_BLOCKCHAIN_CONNECTION
self.is_light = light
speedup_strategy = ExponentialSpeedupStrategy(
w3=self.w3,
min_time_between_speedups=120,
) # speedup txs if not mined after 2 mins.
self.tx_machine = AutomaticTxMachine(
w3=self.w3, tx_exec_timeout=self.TIMEOUT, strategies=[speedup_strategy]
)
self.tx_machine = None
# TODO: Not ready to give users total flexibility. Let's stick for the moment to known values. See #2447
if gas_strategy not in (
@ -291,24 +283,6 @@ class BlockchainInterface:
gas_strategy = cls.GAS_STRATEGIES[cls.DEFAULT_GAS_STRATEGY]
return gas_strategy
def attach_middleware(self):
chain_id = int(self.client.chain_id)
self.poa = chain_id in POA_CHAINS
self.log.debug(
f"Blockchain: {self.client.chain_name} (chain_id={chain_id}, poa={self.poa})"
)
# For use with Proof-Of-Authority test-blockchains
if self.poa is True:
self.log.debug("Injecting POA middleware at layer 0")
self.client.inject_middleware(geth_poa_middleware, layer=0)
self.log.debug("Adding simple_cache_middleware")
self.client.add_middleware(simple_cache_middleware)
# TODO: See #2770
# self.configure_gas_strategy()
def configure_gas_strategy(self, gas_strategy: Optional[Callable] = None) -> None:
if gas_strategy:
@ -336,6 +310,10 @@ class BlockchainInterface:
# self.log.debug(f"Gas strategy currently reports a gas price of {gwei_gas_price} gwei.")
def connect(self):
if self.is_connected:
# safety check - connect was already previously called
return
endpoint = self.endpoint
self.log.info(f"Using external Web3 Provider '{self.endpoint}'")
@ -348,11 +326,19 @@ class BlockchainInterface:
if self._provider is NO_BLOCKCHAIN_CONNECTION:
raise self.NoProvider("There are no configured blockchain providers")
# Connect if not connected
try:
self.w3 = self.Web3(provider=self._provider)
self.tx_machine.w3 = self.w3 # share this web3 instance with the tracker
# client mutates w3 instance (configures middleware etc.)
self.client = EthereumClient(w3=self.w3)
# web3 instance fully configured; share instance with ATxM and respective strategies
speedup_strategy = ExponentialSpeedupStrategy(
w3=self.w3,
min_time_between_speedups=120,
) # speedup txs if not mined after 2 mins.
self.tx_machine = AutomaticTxMachine(
w3=self.w3, tx_exec_timeout=self.TIMEOUT, strategies=[speedup_strategy]
)
except requests.ConnectionError: # RPC
raise self.ConnectionFailed(
f"Connection Failed - {str(self.endpoint)} - is RPC enabled?"
@ -361,8 +347,6 @@ class BlockchainInterface:
raise self.ConnectionFailed(
f"Connection Failed - {str(self.endpoint)} - is IPC enabled?"
)
else:
self.attach_middleware()
return self.is_connected

View File

@ -1,6 +1,6 @@
from _pydecimal import Decimal
from typing import Union
from _pydecimal import Decimal
from eth_utils import currency
from nucypher.types import ERC20UNits, NuNits, TuNits

View File

@ -1,11 +1,19 @@
import time
from decimal import Decimal
from typing import Union
from typing import Dict, List, Union
import requests
from eth_typing import ChecksumAddress
from requests import RequestException
from web3 import Web3
from web3.contract.contract import ContractConstructor, ContractFunction
from web3.types import TxParams
from nucypher.blockchain.eth.constants import CHAINLIST_URL
from nucypher.utilities.logging import Logger
LOGGER = Logger("utility")
def prettify_eth_amount(amount, original_denomination: str = 'wei') -> str:
"""
@ -62,3 +70,112 @@ def get_tx_cost_data(transaction_dict: TxParams):
max_cost_wei = max_unit_price * transaction_dict["gas"]
max_cost = Web3.from_wei(max_cost_wei, "ether")
return max_cost, max_price_gwei, tx_type
def rpc_endpoint_health_check(endpoint: str, max_drift_seconds: int = 60) -> bool:
"""
Checks the health of an Ethereum RPC endpoint by comparing the timestamp of the latest block
with the system time. The maximum drift allowed is `max_drift_seconds`.
"""
query = {
"jsonrpc": "2.0",
"method": "eth_getBlockByNumber",
"params": ["latest", False],
"id": 1,
}
LOGGER.debug(f"Checking health of RPC endpoint {endpoint}")
try:
response = requests.post(
endpoint,
json=query,
headers={"Content-Type": "application/json"},
timeout=5,
)
except requests.exceptions.RequestException:
LOGGER.debug(f"RPC endpoint {endpoint} is unhealthy: network error")
return False
if response.status_code != 200:
LOGGER.debug(
f"RPC endpoint {endpoint} is unhealthy: {response.status_code} | {response.text}"
)
return False
try:
data = response.json()
if "result" not in data:
LOGGER.debug(f"RPC endpoint {endpoint} is unhealthy: no response data")
return False
except requests.exceptions.RequestException:
LOGGER.debug(f"RPC endpoint {endpoint} is unhealthy: {response.text}")
return False
if data["result"] is None:
LOGGER.debug(f"RPC endpoint {endpoint} is unhealthy: no block data")
return False
block_data = data["result"]
try:
timestamp = int(block_data.get("timestamp"), 16)
except TypeError:
LOGGER.debug(f"RPC endpoint {endpoint} is unhealthy: invalid block data")
return False
system_time = time.time()
drift = abs(system_time - timestamp)
if drift > max_drift_seconds:
LOGGER.debug(
f"RPC endpoint {endpoint} is unhealthy: drift too large ({drift} seconds)"
)
return False
LOGGER.debug(f"RPC endpoint {endpoint} is healthy")
return True # finally!
def get_default_rpc_endpoints() -> Dict[int, List[str]]:
"""
Fetches the default RPC endpoints for various chains
from the nucypher/chainlist repository.
"""
LOGGER.debug(
f"Fetching default RPC endpoints from remote chainlist {CHAINLIST_URL}"
)
try:
response = requests.get(CHAINLIST_URL)
except RequestException:
LOGGER.warn("Failed to fetch default RPC endpoints: network error")
return {}
if response.status_code == 200:
return {
int(chain_id): endpoints for chain_id, endpoints in response.json().items()
}
else:
LOGGER.error(
f"Failed to fetch default RPC endpoints: {response.status_code} | {response.text}"
)
return {}
def get_healthy_default_rpc_endpoints(chain_id: int) -> List[str]:
"""Returns a list of healthy RPC endpoints for a given chain ID."""
endpoints = get_default_rpc_endpoints()
chain_endpoints = endpoints.get(chain_id)
if not chain_endpoints:
LOGGER.error(f"No default RPC endpoints found for chain ID {chain_id}")
return list()
healthy = [
endpoint for endpoint in chain_endpoints if rpc_endpoint_health_check(endpoint)
]
LOGGER.info(f"Healthy default RPC endpoints for chain ID {chain_id}: {healthy}")
if not healthy:
LOGGER.warn(
f"No healthy default RPC endpoints available for chain ID {chain_id}"
)
return healthy

View File

@ -994,7 +994,11 @@ class Ursula(Teacher, Character, Operator):
if self._prometheus_metrics_tracker:
self._prometheus_metrics_tracker.stop()
if halt_reactor:
reactor.stop()
self.halt_reactor()
@staticmethod
def halt_reactor() -> None:
reactor.stop()
def _finalize(self):
"""
@ -1252,6 +1256,7 @@ class Ursula(Teacher, Character, Operator):
known_nodes=known_nodes_info,
balance_eth=balance_eth,
block_height=self.ritual_tracker.scanner.get_last_scanned_block(),
ferveo_public_key=bytes(self.public_keys(RitualisticPower)).hex(),
)
def as_external_validator(self) -> Validator:
@ -1289,6 +1294,7 @@ class LocalUrsulaStatus(NamedTuple):
known_nodes: Optional[List[RemoteUrsulaStatus]]
balance_eth: float
block_height: int
ferveo_public_key: str
def to_json(self) -> Dict[str, Any]:
if self.known_nodes is None:
@ -1310,6 +1316,7 @@ class LocalUrsulaStatus(NamedTuple):
known_nodes=known_nodes_json,
balance_eth=self.balance_eth,
block_height=self.block_height,
ferveo_public_key=self.ferveo_public_key,
)

View File

@ -317,8 +317,39 @@ def init(general_config, config_options, force, config_root, key_material):
"""Create a new Ursula node configuration."""
emitter = setup_emitter(general_config, config_options.operator_address)
_pre_launch_warnings(emitter, dev=None, force=force)
if not config_root:
config_root = general_config.config_root
keystore_path = Path(config_root) / Keystore._DIR_NAME
if keystore_path.exists() and any(keystore_path.iterdir()):
click.clear()
emitter.echo(
f"There are existing secret keys in '{keystore_path}'.\n"
"The 'init' command is a one-time operation, do not run it again.\n",
color="red",
)
emitter.echo(
"To review your existing configuration, run:\n\n"
"nucypher ursula config\n\n"
"To run your node with the existing configuration, run:\n\n"
"nucypher ursula run\n",
color="cyan",
)
return click.get_current_context().exit(1)
click.clear()
emitter.echo(
"Hello Operator, welcome on board :-) \n\n"
"NOTE: Initializing a new Ursula node configuration is a one-time operation\n"
"for the lifetime of your node. This is a two-step process:\n\n"
"1. Creating a password to encrypt your operator keys\n"
"2. Securing a taco node seed phase\n\n"
"Please follow the prompts.",
color="cyan",
)
if not config_options.eth_endpoint:
raise click.BadOptionUsage(
"--eth-endpoint",

View File

@ -1,7 +1,5 @@
# noinspection Mypy
import os
import click

View File

@ -53,8 +53,8 @@ DEFAULT_TO_LONE_CONFIG_FILE = "Defaulting to {config_class} configuration file:
# Authentication
PASSWORD_COLLECTION_NOTICE = """
Please provide a password to lock Operator keys.
Do not forget this password, and ideally store it using a password manager.
Please provide a password to encrypt your node's private keys.
Do not forget this password. Ideally generate and store this password using a password manager.
"""
COLLECT_ETH_PASSWORD = "Enter ethereum account password ({checksum_address})"

View File

@ -3,6 +3,7 @@ from constant_sorrow.constants import NO_KEYSTORE_ATTACHED
from nucypher.characters.banners import NUCYPHER_BANNER
from nucypher.config.constants import DEFAULT_CONFIG_ROOT, USER_LOG_DIR
from nucypher.crypto.powers import RitualisticPower
def echo_version(ctx, param, value):
@ -30,17 +31,21 @@ def paint_new_installation_help(emitter, new_configuration, filepath):
character_config_class = new_configuration.__class__
character_name = character_config_class.NAME.lower()
if new_configuration.keystore != NO_KEYSTORE_ATTACHED:
maybe_public_key = new_configuration.keystore.id
ritual_power = new_configuration.keystore.derive_crypto_power(RitualisticPower)
ferveo_public_key = bytes(ritual_power.public_key()).hex()
maybe_public_key = f"{ferveo_public_key[:8]}...{ferveo_public_key[-8:]}"
else:
maybe_public_key = "(no keystore attached)"
emitter.message("Generated keystore", color="green")
emitter.message(
f"""
Public Key: {maybe_public_key}
DKG Public Key: {maybe_public_key}
Path to Keystore: {new_configuration.keystore_dir}
Path to Config: {filepath}
Path to Logs: {USER_LOG_DIR}
- You can share your public key with anyone. Others need it to interact with you.
- Never share secret keys with anyone!
- Backup your keystore! Character keys are required to interact with the protocol!
- Remember your password! Without the password, it's impossible to decrypt the key!
@ -48,18 +53,6 @@ Path to Keystore: {new_configuration.keystore_dir}
"""
)
default_config_filepath = True
if new_configuration.default_filepath() != filepath:
default_config_filepath = False
emitter.message(f'Generated configuration file at {"default" if default_config_filepath else "non-default"} '
f'filepath {filepath}', color='green')
# add hint about --config-file
if not default_config_filepath:
emitter.message(f'* NOTE: for a non-default configuration filepath use `--config-file "{filepath}"` '
f'with subsequent `{character_name}` CLI commands', color='yellow')
# Ursula
if character_name == 'ursula':
hint = '''
* Review configuration -> nucypher ursula config

View File

@ -590,9 +590,12 @@ class CharacterConfiguration(BaseConfiguration):
def generate(
cls, password: str, key_material: Optional[bytes] = None, *args, **kwargs
):
"""Shortcut: Hook-up a new initial installation and configuration."""
"""
Generates local directories, private keys, and initial configuration for a new node.
"""
node_config = cls(dev_mode=False, *args, **kwargs)
node_config.initialize(key_material=key_material, password=password)
node_config.keystore.unlock(password)
return node_config
def cleanup(self) -> None:
@ -786,7 +789,7 @@ class CharacterConfiguration(BaseConfiguration):
power_ups.append(power_up)
return power_ups
def initialize(self, password: str, key_material: Optional[bytes] = None) -> str:
def initialize(self, password: str, key_material: Optional[bytes] = None) -> Path:
"""Initialize a new configuration and write installation files to disk."""
# Development

View File

@ -5,13 +5,22 @@ from typing import Dict, List, Optional
from cryptography.x509 import Certificate
from eth_utils import is_checksum_address
from nucypher.blockchain.eth.agents import (
ContractAgency,
CoordinatorAgent,
TACoChildApplicationAgent,
)
from nucypher.blockchain.eth.constants import NULL_ADDRESS
from nucypher.blockchain.eth.interfaces import BlockchainInterfaceFactory
from nucypher.config.base import CharacterConfiguration
from nucypher.config.constants import (
NUCYPHER_ENVVAR_ALICE_ETH_PASSWORD,
NUCYPHER_ENVVAR_BOB_ETH_PASSWORD,
NUCYPHER_ENVVAR_OPERATOR_ETH_PASSWORD,
)
from nucypher.utilities.emitters import StdoutEmitter
from nucypher.utilities.networking import LOOPBACK_ADDRESS
from nucypher.utilities.warnings import render_lost_seed_phrase_message
class UrsulaConfiguration(CharacterConfiguration):
@ -67,6 +76,53 @@ class UrsulaConfiguration(CharacterConfiguration):
self.condition_blockchain_endpoints[int(chain)] = blockchain_endpoint
self.configure_condition_blockchain_endpoints()
def initialize(self, *args, **kwargs) -> Path:
"""
Check if the coordinator public key is set and prevent the creation of a new node if it is.
"""
emitter = StdoutEmitter()
emitter.echo("Checking operator account status...")
BlockchainInterfaceFactory.get_or_create_interface(
endpoint=self.polygon_endpoint
)
coordinator_agent = ContractAgency.get_agent(
CoordinatorAgent,
blockchain_endpoint=self.polygon_endpoint,
registry=self.registry,
)
application_agent = ContractAgency.get_agent(
TACoChildApplicationAgent,
blockchain_endpoint=self.polygon_endpoint,
registry=self.registry,
)
if self.operator_address:
staking_provider_address = application_agent.staking_provider_from_operator(
self.operator_address
)
if staking_provider_address and staking_provider_address != NULL_ADDRESS:
if coordinator_agent.is_provider_public_key_set(
staking_provider_address
):
message = (
f"Operator {self.operator_address} has already published a public key.\n"
f"It is not permitted to create a new node with this operator address."
f"{render_lost_seed_phrase_message()}"
)
self.log.critical(message)
raise self.ConfigurationError(message)
else:
emitter.echo(
"NOTE: Your operator is not bonded to a staking provider. \n"
"Bond the operator to a staking provider on the threshold dashboard.",
color="cyan",
)
return super().initialize(*args, **kwargs)
def configure_condition_blockchain_endpoints(self) -> None:
"""Configure default condition provider URIs for eth and polygon network."""
# Polygon

View File

@ -0,0 +1,5 @@
class FerveoKeyMismatch(Exception):
"""
Raised when a local ferveo public key does not match the
public key published to the Coordinator.
"""

View File

@ -223,7 +223,8 @@ class Keystore:
_ID_SIZE = 32
# Filepath
_DEFAULT_DIR: Path = DEFAULT_CONFIG_ROOT / 'keystore'
_DIR_NAME = "keystore"
_DEFAULT_DIR: Path = DEFAULT_CONFIG_ROOT / _DIR_NAME
_DELIMITER = '-'
_SUFFIX = 'priv'
@ -380,20 +381,49 @@ class Keystore:
# notification
emitter = StdoutEmitter()
emitter.message(
"Backup your seed words, you will not be able to view them again.\n"
emitter.echo(
"\nNOTE: Next, you will be assigned a taco node seed phase. This seed phase is used to\n"
"generate your keystore. You will need this seed phase to recover your keystore\n"
"in the future. Please write down the seed phase and keep it in a safe place.\n",
color="cyan",
)
emitter.message(f"{__words}\n", color="cyan")
emitter.message(
"IMPORTANT: Backup your seed phrase, you will not be able to view them again.\n"
"You can use these words to restore your keystore in the future in case of loss of\n"
"your keystore files or password. Do not share these words with anyone.\n",
color="yellow",
)
emitter.message(
"WARNING: If you lose your seed phase and also lose access to your keystore/password "
"your stake will be slashed.\n",
color="red",
)
click.confirm("Reveal seed phase?", default=False, abort=True)
click.clear()
formatted_words = "\n".join(
f"{i} {w}" for i, w in enumerate(__words.split(), start=1)
)
emitter.message(f"{formatted_words}\n", color="green")
if not click.confirm("Have you backed up your seed phrase?"):
emitter.message('Keystore generation aborted.', color='red')
raise click.Abort()
click.clear()
# confirmation
__response = click.prompt("Confirm seed words")
if __response != __words:
raise ValueError('Incorrect seed word confirmation. No keystore has been created, try again.')
while True:
__response = click.prompt("Confirm seed words (space separated)")
if __response != __words:
emitter.message(
"Seed words do not match. Please try again.", color="red"
)
continue
break
click.clear()
emitter.echo("Seed phrase confirmed. Generating keystore...", color="green")
@property
def id(self) -> str:

View File

@ -0,0 +1,108 @@
from enum import Enum
from typing import List
import maya
from eth_account.account import Account
from eth_account.messages import HexBytes, encode_typed_data
from siwe import SiweMessage, VerificationError
class EvmAuth:
class AuthScheme(Enum):
EIP712 = "EIP712"
EIP4361 = "EIP4361"
@classmethod
def values(cls) -> List[str]:
return [scheme.value for scheme in cls]
class InvalidData(Exception):
pass
class AuthenticationFailed(Exception):
pass
class StaleMessage(AuthenticationFailed):
"""The message is too old."""
@classmethod
def authenticate(cls, data, signature, expected_address):
raise NotImplementedError
@classmethod
def from_scheme(cls, scheme: str):
if scheme == cls.AuthScheme.EIP712.value:
return EIP712Auth
elif scheme == cls.AuthScheme.EIP4361.value:
return EIP4361Auth
raise ValueError(f"Invalid authentication scheme: {scheme}")
class EIP712Auth(EvmAuth):
@classmethod
def authenticate(cls, data, signature, expected_address):
try:
# convert hex data for byte fields - bytes are expected by underlying library
# 1. salt
salt = data["domain"]["salt"]
data["domain"]["salt"] = HexBytes(salt)
# 2. blockHash
blockHash = data["message"]["blockHash"]
data["message"]["blockHash"] = HexBytes(blockHash)
signable_message = encode_typed_data(full_message=data)
address_for_signature = Account.recover_message(
signable_message=signable_message, signature=signature
)
except Exception as e:
# data could not be processed
raise cls.InvalidData(
f"Invalid EIP712 message: {str(e) or e.__class__.__name__}"
)
if address_for_signature != expected_address:
# verification failed - addresses don't match
raise cls.AuthenticationFailed(
f"EIP712 verification failed; signature not valid for expected address, {expected_address}"
)
class EIP4361Auth(EvmAuth):
FRESHNESS_IN_HOURS = 2
@classmethod
def authenticate(cls, data, signature, expected_address):
try:
siwe_message = SiweMessage.from_message(message=data)
except Exception as e:
raise cls.InvalidData(
f"Invalid EIP4361 message - {str(e) or e.__class__.__name__}"
)
try:
# performs various validation checks on message eg. expiration, not-before, signature etc.
siwe_message.verify(signature=signature)
except VerificationError as e:
raise cls.AuthenticationFailed(
f"EIP4361 verification failed - {str(e) or e.__class__.__name__}"
)
# enforce a freshness check - reference point is issued at
issued_at = maya.MayaDT.from_iso8601(siwe_message.issued_at)
now = maya.now()
if issued_at > now:
raise cls.AuthenticationFailed(
f"EIP4361 issued-at datetime is in the future: {issued_at.iso8601()}"
)
if now > issued_at.add(hours=cls.FRESHNESS_IN_HOURS):
raise cls.StaleMessage(
f"EIP4361 message is more than {cls.FRESHNESS_IN_HOURS} "
f"hours old (issued at {issued_at.iso8601()})"
)
if siwe_message.address != expected_address:
# verification failed - addresses don't match
raise cls.AuthenticationFailed(
f"Invalid EIP4361 signature; signature not valid for expected address, {expected_address}"
)

View File

@ -1,11 +1,11 @@
import re
from functools import partial
from typing import Any, List, Union
from eth_account.account import Account
from eth_account.messages import HexBytes, encode_structured_data
from eth_typing import ChecksumAddress
from eth_utils import to_checksum_address
from nucypher.policy.conditions.auth.evm import EvmAuth
from nucypher.policy.conditions.exceptions import (
ContextVariableVerificationFailed,
InvalidContextVariableData,
@ -13,69 +13,79 @@ from nucypher.policy.conditions.exceptions import (
)
USER_ADDRESS_CONTEXT = ":userAddress"
USER_ADDRESS_EIP4361_EXTERNAL_CONTEXT = ":userAddressExternalEIP4361"
CONTEXT_PREFIX = ":"
CONTEXT_REGEX = re.compile(":[a-zA-Z_][a-zA-Z0-9_]*")
USER_ADDRESS_SCHEMES = {
USER_ADDRESS_CONTEXT: None, # allow any scheme (EIP4361, EIP712) for now; eventually EIP712 will be deprecated
USER_ADDRESS_EIP4361_EXTERNAL_CONTEXT: EvmAuth.AuthScheme.EIP4361.value,
}
def _recover_user_address(**context) -> ChecksumAddress:
class UnexpectedScheme(Exception):
pass
def _resolve_user_address(user_address_context_variable, **context) -> ChecksumAddress:
"""
Recovers a checksum address from a signed EIP712 message.
Recovers a checksum address from a signed message.
Expected format:
{
":userAddress":
":userAddress...":
{
"signature": "<signature>",
"address": "<address>",
"typedData": "<a complicated EIP712 data structure>"
"scheme": "EIP4361" | ...
"typedData": ...
}
}
"""
# setup
try:
user_address_info = context[USER_ADDRESS_CONTEXT]
user_address_info = context[user_address_context_variable]
signature = user_address_info["signature"]
user_address = to_checksum_address(user_address_info["address"])
eip712_message = user_address_info["typedData"]
expected_address = to_checksum_address(user_address_info["address"])
typed_data = user_address_info["typedData"]
# convert hex data for byte fields - bytes are expected by underlying library
# 1. salt
salt = eip712_message["domain"]["salt"]
eip712_message["domain"]["salt"] = HexBytes(salt)
# 2. blockHash
blockHash = eip712_message["message"]["blockHash"]
eip712_message["message"]["blockHash"] = HexBytes(blockHash)
# if empty assume EIP712, although EIP712 will eventually be deprecated
scheme = user_address_info.get("scheme", EvmAuth.AuthScheme.EIP712.value)
expected_scheme = USER_ADDRESS_SCHEMES[user_address_context_variable]
if expected_scheme and scheme != expected_scheme:
raise UnexpectedScheme(
f"Expected {expected_scheme} authentication scheme, but received {scheme}"
)
signable_message = encode_structured_data(primitive=eip712_message)
auth = EvmAuth.from_scheme(scheme)
auth.authenticate(
data=typed_data, signature=signature, expected_address=expected_address
)
except EvmAuth.InvalidData as e:
raise InvalidContextVariableData(
f"Invalid context variable data for '{user_address_context_variable}'; {e}"
)
except EvmAuth.AuthenticationFailed as e:
raise ContextVariableVerificationFailed(
f"Authentication failed for '{user_address_context_variable}'; {e}"
)
except Exception as e:
# data could not be processed
raise InvalidContextVariableData(
f'Invalid data provided for "{USER_ADDRESS_CONTEXT}"; {e.__class__.__name__} - {e}'
f"Invalid context variable data for '{user_address_context_variable}'; {e.__class__.__name__} - {e}"
)
# actual verification
try:
address_for_signature = Account.recover_message(
signable_message=signable_message, signature=signature
)
if address_for_signature == user_address:
return user_address
except Exception as e:
# exception during verification
raise ContextVariableVerificationFailed(
f"Could not determine address of signature for '{USER_ADDRESS_CONTEXT}'; {e.__class__.__name__} - {e}"
)
# verification failed - addresses don't match
raise ContextVariableVerificationFailed(
f"Signer address for '{USER_ADDRESS_CONTEXT}' signature does not match; expected {user_address}"
)
return expected_address
_DIRECTIVES = {
USER_ADDRESS_CONTEXT: _recover_user_address,
USER_ADDRESS_CONTEXT: partial(
_resolve_user_address, user_address_context_variable=USER_ADDRESS_CONTEXT
),
USER_ADDRESS_EIP4361_EXTERNAL_CONTEXT: partial(
_resolve_user_address,
user_address_context_variable=USER_ADDRESS_EIP4361_EXTERNAL_CONTEXT,
),
}

View File

@ -18,7 +18,7 @@ from web3.middleware import geth_poa_middleware
from web3.providers import BaseProvider
from web3.types import ABIFunction
from nucypher.blockchain.eth.clients import POA_CHAINS
from nucypher.blockchain.eth.constants import POA_CHAINS
from nucypher.policy.conditions import STANDARD_ABI_CONTRACT_TYPES, STANDARD_ABIS
from nucypher.policy.conditions.base import AccessControlCondition
from nucypher.policy.conditions.context import (

View File

@ -1,9 +1,9 @@
import ssl
import time
from _socket import gethostbyname
from typing import Dict, NamedTuple
from urllib.parse import urlparse, urlunparse
from _socket import gethostbyname
from requests import PreparedRequest, Response, Session
from requests.adapters import HTTPAdapter
from requests.exceptions import RequestException
@ -107,6 +107,9 @@ class SelfSignedCertificateAdapter(HTTPAdapter):
self.certificate_cache, *args, **kwargs
)
def get_connection_with_tls_context(self, request, verify, proxies=None, cert=None):
return self.get_connection(request.url, proxies)
class P2PSession(Session):
_DEFAULT_HOSTNAME = ""

View File

@ -0,0 +1,33 @@
from nucypher.config.constants import DEFAULT_CONFIG_ROOT
def render_lost_seed_phrase_message():
message = f"""
To relocate your node to a new host copy the configuration directory ({DEFAULT_CONFIG_ROOT}) to the new host.
If you do not have a backup of the original keystore or have lost your password, you will need to recover your
node using the recovery phrase assigned during the initial setup by running:
nucypher ursula recover
If you have lost your recovery phrase: Open a support ticket in the Threshold Discord server (#taco).
Disclose the loss immediately to minimize penalties. Your stake may be slashed, but the punishment will be significantly
reduced if a key material handover is completed quickly, ensuring the node's service is not disrupted.
"""
return message
def render_ferveo_key_mismatch_warning(local_key, onchain_key):
message = f"""
ERROR: The local Ferveo public key {bytes(local_key).hex()[:8]} does not match the on-chain public key {bytes(onchain_key).hex()[:8]}!
This is a critical error. Without the original private keys, your node cannot service existing DKGs.
IMPORTANT: Running `nucypher ursula init` will generate new private keys, which is not the correct procedure
for relocating or restoring a TACo node.
{render_lost_seed_phrase_message()}
"""
return message

2437
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,18 +1,18 @@
[tool.poetry]
name = "nucypher"
version = "7.3.0"
version = "7.4.0"
authors = ["NuCypher"]
description = "A threshold access control application to empower privacy in decentralized systems."
[tool.poetry.dependencies]
python = ">=3.8,<4"
python = ">=3.9,<4"
nucypher-core = "==0.13.0"
cryptography = "*"
pynacl = ">=1.4.0"
mnemonic = "*"
pyopenssl = "*"
web3 = '^6.15.1'
atxm = "*"
atxm = "^0.5.0"
flask = "*"
hendrix = "*"
requests = "*"
@ -25,6 +25,7 @@ marshmallow = '*'
appdirs = '*'
constant-sorrow = '^0.1.0a9'
prometheus-client = '*'
siwe = "^4.2.0"
time-machine = "^2.13.0"
twisted = "^24.2.0rc1"
@ -34,10 +35,11 @@ pytest-cov = '*'
pytest-mock = '*'
pytest-timeout = '*'
pytest-twisted = '*'
eth-ape = "*"
eth-ape = ">=0.7"
ape-solidity = '*'
coverage = '^7.3.2'
pre-commit = '^2.12.1'
numpy = '^1.26.0'
[tool.towncrier]
@ -83,8 +85,8 @@ pre-commit = '^2.12.1'
showcontent = true
[tool.ruff]
select = ["E", "F", "I"]
ignore = ["E501"]
lint.select = ["E", "F", "I"]
lint.ignore = ["E501"]
[tool.ruff.isort]
[tool.ruff.lint.isort]
known-first-party = ["nucypher"]

View File

@ -4,6 +4,42 @@ Releases
.. towncrier release notes start
v7.4.0 (2024-08-12)
-------------------
Features
~~~~~~~~
- Support for default/fallback RPC endpoints from remote sources as a backup for operator-supplied RPC endpoints for condition evaluation. (`#3496 <https://github.com/nucypher/nucypher/issues/3496>`__)
- Add support for Sign-in With Ethereum (SIWE) messages to be used when verifying wallet address ownership for the ``:userAddress`` special context variable during decryption requests. (`#3502 <https://github.com/nucypher/nucypher/issues/3502>`__)
- Add functionality for ``:userAddressEIP712`` and ``:userAddressEIP4361`` to provide specific authentication
support for user address context values for conditions. ``:userAddress`` will allow any valid authentication scheme. (`#3508 <https://github.com/nucypher/nucypher/issues/3508>`__)
- Add ability for special context variable to handle Sign-In With Ethereum (EIP-4361)
pre-existing sign-on signature to be reused as proof for validating a user address in conditions. (`#3513 <https://github.com/nucypher/nucypher/issues/3513>`__)
- Prevents nodes from starting up or participating in DKGs if there is a local vs. onchain ferveo key mismatch. This will assist in alerting node operators who need to relocate or recover their hosts about the correct procedure. (`#3529 <https://github.com/nucypher/nucypher/issues/3529>`__)
- Prevent new nodes from initialization if the operator already has published a ferveo public key onchain.
Improves information density and communication of keystore security obligations while using the init CLI. (`#3533 <https://github.com/nucypher/nucypher/issues/3533>`__)
Bugfixes
~~~~~~~~
- Do not continuously retry ritual actions when unrecoverable ferveo error occurs during ritual ceremony. (`#3524 <https://github.com/nucypher/nucypher/issues/3524>`__)
- ATxM instance did not pass correct web3 instance to underlying strategies. (`#3531 <https://github.com/nucypher/nucypher/issues/3531>`__)
Deprecations and Removals
~~~~~~~~~~~~~~~~~~~~~~~~~
- Drop support for Python 3.8. (`#3521 <https://github.com/nucypher/nucypher/issues/3521>`__)
Internal Development Tasks
~~~~~~~~~~~~~~~~~~~~~~~~~~
- `#3446 <https://github.com/nucypher/nucypher/issues/3446>`__, `#3498 <https://github.com/nucypher/nucypher/issues/3498>`__, `#3499 <https://github.com/nucypher/nucypher/issues/3499>`__, `#3507 <https://github.com/nucypher/nucypher/issues/3507>`__, `#3509 <https://github.com/nucypher/nucypher/issues/3509>`__, `#3510 <https://github.com/nucypher/nucypher/issues/3510>`__, `#3519 <https://github.com/nucypher/nucypher/issues/3519>`__, `#3521 <https://github.com/nucypher/nucypher/issues/3521>`__, `#3522 <https://github.com/nucypher/nucypher/issues/3522>`__, `#3532 <https://github.com/nucypher/nucypher/issues/3532>`__
v7.3.0 (2024-05-07)
-------------------

View File

@ -1,98 +1,103 @@
aiohttp==3.9.4rc0 ; python_version >= "3.8" and python_version < "4"
aiosignal==1.3.1 ; python_version >= "3.8" and python_version < "4"
appdirs==1.4.4 ; python_version >= "3.8" and python_version < "4"
async-timeout==4.0.3 ; python_version >= "3.8" and python_version < "3.11"
attrs==23.2.0 ; python_version >= "3.8" and python_version < "4"
atxm==0.3.0 ; python_version >= "3.8" and python_version < "4"
autobahn==23.1.2 ; python_version >= "3.8" and python_version < "4"
automat==22.10.0 ; python_version >= "3.8" and python_version < "4"
backports-zoneinfo==0.2.1 ; python_version >= "3.8" and python_version < "3.9"
bitarray==2.9.2 ; python_version >= "3.8" and python_version < "4"
blinker==1.7.0 ; python_version >= "3.8" and python_version < "4"
bytestring-splitter==2.4.1 ; python_version >= "3.8" and python_version < "4"
certifi==2024.2.2 ; python_version >= "3.8" and python_version < "4"
cffi==1.16.0 ; python_version >= "3.8" and python_version < "4"
charset-normalizer==3.3.2 ; python_version >= "3.8" and python_version < "4"
click==8.1.7 ; python_version >= "3.8" and python_version < "4"
colorama==0.4.6 ; python_version >= "3.8" and python_version < "4"
constant-sorrow==0.1.0a9 ; python_version >= "3.8" and python_version < "4"
constantly==23.10.4 ; python_version >= "3.8" and python_version < "4"
cryptography==42.0.5 ; python_version >= "3.8" and python_version < "4"
cytoolz==0.12.3 ; python_version >= "3.8" and python_version < "4" and implementation_name == "cpython"
dateparser==1.2.0 ; python_version >= "3.8" and python_version < "4"
eth-abi==4.2.1 ; python_version >= "3.8" and python_version < "4"
eth-account==0.10.0 ; python_version >= "3.8" and python_version < "4"
eth-hash==0.7.0 ; python_version >= "3.8" and python_version < "4"
eth-hash[pycryptodome]==0.7.0 ; python_version >= "3.8" and python_version < "4"
eth-keyfile==0.8.0 ; python_version >= "3.8" and python_version < "4"
eth-keys==0.4.0 ; python_version >= "3.8" and python_version < "4"
eth-rlp==1.0.1 ; python_version >= "3.8" and python_version < "4"
eth-typing==3.5.2 ; python_version >= "3.8" and python_version < "4"
eth-utils==2.3.1 ; python_version >= "3.8" and python_version < "4"
flask==3.0.3 ; python_version >= "3.8" and python_version < "4"
frozenlist==1.4.1 ; python_version >= "3.8" and python_version < "4"
hendrix==5.0.0 ; python_version >= "3.8" and python_version < "4"
hexbytes==0.3.1 ; python_version >= "3.8" and python_version < "4"
humanize==4.9.0 ; python_version >= "3.8" and python_version < "4"
hyperlink==21.0.0 ; python_version >= "3.8" and python_version < "4"
idna==3.7 ; python_version >= "3.8" and python_version < "4"
importlib-metadata==7.1.0 ; python_version >= "3.8" and python_version < "3.10"
importlib-resources==6.4.0 ; python_version >= "3.8" and python_version < "3.9"
incremental==22.10.0 ; python_version >= "3.8" and python_version < "4"
itsdangerous==2.1.2 ; python_version >= "3.8" and python_version < "4"
jinja2==3.1.3 ; python_version >= "3.8" and python_version < "4"
jsonschema-specifications==2023.12.1 ; python_version >= "3.8" and python_version < "4"
jsonschema==4.21.1 ; python_version >= "3.8" and python_version < "4"
lru-dict==1.2.0 ; python_version >= "3.8" and python_version < "4"
mako==1.3.3 ; python_version >= "3.8" and python_version < "4"
markupsafe==2.1.5 ; python_version >= "3.8" and python_version < "4"
marshmallow==3.21.1 ; python_version >= "3.8" and python_version < "4"
maya==0.6.1 ; python_version >= "3.8" and python_version < "4"
mnemonic==0.20 ; python_version >= "3.8" and python_version < "4"
msgpack-python==0.5.6 ; python_version >= "3.8" and python_version < "4"
multidict==6.0.5 ; python_version >= "3.8" and python_version < "4"
nucypher-core==0.13.0 ; python_version >= "3.8" and python_version < "4"
packaging==23.2 ; python_version >= "3.8" and python_version < "4"
parsimonious==0.9.0 ; python_version >= "3.8" and python_version < "4"
pendulum==3.0.0 ; python_version >= "3.8" and python_version < "4"
pkgutil-resolve-name==1.3.10 ; python_version >= "3.8" and python_version < "3.9"
prometheus-client==0.20.0 ; python_version >= "3.8" and python_version < "4"
protobuf==5.26.1 ; python_version >= "3.8" and python_version < "4"
pyasn1-modules==0.4.0 ; python_version >= "3.8" and python_version < "4"
pyasn1==0.6.0 ; python_version >= "3.8" and python_version < "4"
pychalk==2.0.1 ; python_version >= "3.8" and python_version < "4"
pycparser==2.22 ; python_version >= "3.8" and python_version < "4"
pycryptodome==3.20.0 ; python_version >= "3.8" and python_version < "4"
pynacl==1.5.0 ; python_version >= "3.8" and python_version < "4"
pyopenssl==24.1.0 ; python_version >= "3.8" and python_version < "4"
python-dateutil==2.9.0.post0 ; python_version >= "3.8" and python_version < "4"
python-statemachine==2.1.2 ; python_version >= "3.8" and python_version < "3.13"
pytz==2024.1 ; python_version >= "3.8" and python_version < "4"
pyunormalize==15.1.0 ; python_version >= "3.8" and python_version < "4"
pywin32==306 ; python_version >= "3.8" and python_version < "4" and platform_system == "Windows"
referencing==0.34.0 ; python_version >= "3.8" and python_version < "4"
regex==2023.12.25 ; python_version >= "3.8" and python_version < "4"
requests==2.31.0 ; python_version >= "3.8" and python_version < "4"
rlp==3.0.0 ; python_version >= "3.8" and python_version < "4"
rpds-py==0.18.0 ; python_version >= "3.8" and python_version < "4"
service-identity==24.1.0 ; python_version >= "3.8" and python_version < "4"
setuptools==69.2.0 ; python_version >= "3.8" and python_version < "4"
six==1.16.0 ; python_version >= "3.8" and python_version < "4"
snaptime==0.2.4 ; python_version >= "3.8" and python_version < "4"
tabulate==0.9.0 ; python_version >= "3.8" and python_version < "4"
time-machine==2.14.1 ; python_version >= "3.8" and python_version < "4"
toolz==0.12.1 ; python_version >= "3.8" and python_version < "4"
twisted-iocpsupport==1.0.4 ; python_version >= "3.8" and python_version < "4" and platform_system == "Windows"
twisted==24.3.0 ; python_version >= "3.8" and python_version < "4"
txaio==23.1.1 ; python_version >= "3.8" and python_version < "4"
typing-extensions==4.11.0 ; python_version >= "3.8" and python_version < "4"
tzdata==2024.1 ; python_version >= "3.8" and python_version < "4"
tzlocal==5.2 ; python_version >= "3.8" and python_version < "4"
urllib3==2.2.0 ; python_version >= "3.8" and python_version < "4"
watchdog==3.0.0 ; python_version >= "3.8" and python_version < "4"
web3==6.15.1 ; python_version >= "3.8" and python_version < "4"
websockets==12.0 ; python_version >= "3.8" and python_version < "4"
werkzeug==3.0.2 ; python_version >= "3.8" and python_version < "4"
yarl==1.9.4 ; python_version >= "3.8" and python_version < "4"
zipp==3.18.1 ; python_version >= "3.8" and python_version < "3.10"
zope-interface==6.2 ; python_version >= "3.8" and python_version < "4"
abnf==2.2.0 ; python_version >= "3.9" and python_version < "4.0"
aiohappyeyeballs==2.3.2 ; python_version >= "3.9" and python_version < "4.0"
aiohttp==3.10.0 ; python_version >= "3.9" and python_version < "4"
aiosignal==1.3.1 ; python_version >= "3.9" and python_version < "4"
annotated-types==0.7.0 ; python_version >= "3.9" and python_version < "4.0"
appdirs==1.4.4 ; python_version >= "3.9" and python_version < "4"
async-timeout==4.0.3 ; python_version >= "3.9" and python_version < "3.11"
attrs==23.2.0 ; python_version >= "3.9" and python_version < "4"
atxm==0.5.0 ; python_version >= "3.9" and python_version < "4"
autobahn==23.6.2 ; python_version >= "3.9" and python_version < "4"
automat==22.10.0 ; python_version >= "3.9" and python_version < "4"
bitarray==2.9.2 ; python_version >= "3.9" and python_version < "4"
blinker==1.8.2 ; python_version >= "3.9" and python_version < "4"
bytestring-splitter==2.4.1 ; python_version >= "3.9" and python_version < "4"
certifi==2024.7.4 ; python_version >= "3.9" and python_version < "4"
cffi==1.16.0 ; python_version >= "3.9" and python_version < "4"
charset-normalizer==3.3.2 ; python_version >= "3.9" and python_version < "4"
ckzg==1.0.2 ; python_version >= "3.9" and python_version < "4"
click==8.1.7 ; python_version >= "3.9" and python_version < "4"
colorama==0.4.6 ; python_version >= "3.9" and python_version < "4"
constant-sorrow==0.1.0a9 ; python_version >= "3.9" and python_version < "4"
constantly==23.10.4 ; python_version >= "3.9" and python_version < "4"
cryptography==43.0.0 ; python_version >= "3.9" and python_version < "4"
cytoolz==0.12.3 ; python_version >= "3.9" and python_version < "4" and implementation_name == "cpython"
dateparser==1.2.0 ; python_version >= "3.9" and python_version < "4"
eth-abi==5.1.0 ; python_version >= "3.9" and python_version < "4"
eth-account==0.11.2 ; python_version >= "3.9" and python_version < "4"
eth-hash==0.7.0 ; python_version >= "3.9" and python_version < "4"
eth-hash[pycryptodome]==0.7.0 ; python_version >= "3.9" and python_version < "4"
eth-keyfile==0.8.1 ; python_version >= "3.9" and python_version < "4"
eth-keys==0.5.1 ; python_version >= "3.9" and python_version < "4"
eth-rlp==1.0.1 ; python_version >= "3.9" and python_version < "4"
eth-typing==3.5.2 ; python_version >= "3.9" and python_version < "4"
eth-utils==2.3.1 ; python_version >= "3.9" and python_version < "4"
flask==3.0.3 ; python_version >= "3.9" and python_version < "4"
frozenlist==1.4.1 ; python_version >= "3.9" and python_version < "4"
hendrix==5.0.0 ; python_version >= "3.9" and python_version < "4"
hexbytes==0.3.1 ; python_version >= "3.9" and python_version < "4"
humanize==4.10.0 ; python_version >= "3.9" and python_version < "4"
hyperlink==21.0.0 ; python_version >= "3.9" and python_version < "4"
idna==3.7 ; python_version >= "3.9" and python_version < "4"
importlib-metadata==8.2.0 ; python_version >= "3.9" and python_version < "3.10"
incremental==24.7.2 ; python_version >= "3.9" and python_version < "4"
itsdangerous==2.2.0 ; python_version >= "3.9" and python_version < "4"
jinja2==3.1.4 ; python_version >= "3.9" and python_version < "4"
jsonschema-specifications==2023.12.1 ; python_version >= "3.9" and python_version < "4"
jsonschema==4.23.0 ; python_version >= "3.9" and python_version < "4"
lru-dict==1.2.0 ; python_version >= "3.9" and python_version < "4"
mako==1.3.5 ; python_version >= "3.9" and python_version < "4"
markupsafe==2.1.5 ; python_version >= "3.9" and python_version < "4"
marshmallow==3.21.3 ; python_version >= "3.9" and python_version < "4"
maya==0.6.1 ; python_version >= "3.9" and python_version < "4"
mnemonic==0.21 ; python_version >= "3.9" and python_version < "4"
msgpack-python==0.5.6 ; python_version >= "3.9" and python_version < "4"
multidict==6.0.5 ; python_version >= "3.9" and python_version < "4"
nucypher-core==0.13.0 ; python_version >= "3.9" and python_version < "4"
packaging==23.2 ; python_version >= "3.9" and python_version < "4"
parsimonious==0.10.0 ; python_version >= "3.9" and python_version < "4"
pendulum==3.0.0 ; python_version >= "3.9" and python_version < "4"
prometheus-client==0.20.0 ; python_version >= "3.9" and python_version < "4"
protobuf==5.27.2 ; python_version >= "3.9" and python_version < "4"
pyasn1-modules==0.4.0 ; python_version >= "3.9" and python_version < "4"
pyasn1==0.6.0 ; python_version >= "3.9" and python_version < "4"
pychalk==2.0.1 ; python_version >= "3.9" and python_version < "4"
pycparser==2.22 ; python_version >= "3.9" and python_version < "4"
pycryptodome==3.20.0 ; python_version >= "3.9" and python_version < "4"
pydantic-core==2.20.1 ; python_version >= "3.9" and python_version < "4.0"
pydantic==2.8.2 ; python_version >= "3.9" and python_version < "4.0"
pynacl==1.5.0 ; python_version >= "3.9" and python_version < "4"
pyopenssl==24.2.1 ; python_version >= "3.9" and python_version < "4"
python-dateutil==2.9.0.post0 ; python_version >= "3.9" and python_version < "4"
python-statemachine==2.3.4 ; python_version >= "3.9" and python_version < "4"
pytz==2024.1 ; python_version >= "3.9" and python_version < "4"
pyunormalize==15.1.0 ; python_version >= "3.9" and python_version < "4"
pywin32==306 ; python_version >= "3.9" and python_version < "4" and platform_system == "Windows"
referencing==0.35.1 ; python_version >= "3.9" and python_version < "4"
regex==2024.7.24 ; python_version >= "3.9" and python_version < "4"
requests==2.32.3 ; python_version >= "3.9" and python_version < "4"
rlp==4.0.1 ; python_version >= "3.9" and python_version < "4"
rpds-py==0.19.1 ; python_version >= "3.9" and python_version < "4"
service-identity==24.1.0 ; python_version >= "3.9" and python_version < "4"
setuptools==72.1.0 ; python_version >= "3.9" and python_version < "4"
siwe==4.2.0 ; python_version >= "3.9" and python_version < "4.0"
six==1.16.0 ; python_version >= "3.9" and python_version < "4"
snaptime==0.2.4 ; python_version >= "3.9" and python_version < "4"
tabulate==0.9.0 ; python_version >= "3.9" and python_version < "4"
time-machine==2.14.2 ; python_version >= "3.9" and python_version < "4"
tomli==2.0.1 ; python_version >= "3.9" and python_version < "3.11"
toolz==0.12.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "pypy" or implementation_name == "cpython")
twisted-iocpsupport==1.0.4 ; python_version >= "3.9" and python_version < "4" and platform_system == "Windows"
twisted==24.3.0 ; python_version >= "3.9" and python_version < "4"
txaio==23.1.1 ; python_version >= "3.9" and python_version < "4"
typing-extensions==4.12.2 ; python_version >= "3.9" and python_version < "4"
tzdata==2024.1 ; python_version >= "3.9" and python_version < "4"
tzlocal==5.2 ; python_version >= "3.9" and python_version < "4"
urllib3==2.2.2 ; python_version >= "3.9" and python_version < "4"
watchdog==3.0.0 ; python_version >= "3.9" and python_version < "4"
web3==6.20.1 ; python_version >= "3.9" and python_version < "4"
websockets==12.0 ; python_version >= "3.9" and python_version < "4"
werkzeug==3.0.3 ; python_version >= "3.9" and python_version < "4"
yarl==1.9.4 ; python_version >= "3.9" and python_version < "4"
zipp==3.19.2 ; python_version >= "3.9" and python_version < "3.10"
zope-interface==6.4.post2 ; python_version >= "3.9" and python_version < "4"

View File

@ -1,19 +1,3 @@
"""
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 code
import readline
import rlcompleter

View File

@ -1,75 +0,0 @@
"""
This file is part of nucypher.
nucypher is free software: you can redistribute it and/or modify
it under the terms of the GNU 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 subprocess
import sys
import venv
from pathlib import (
Path,
)
from tempfile import (
TemporaryDirectory,
)
from typing import (
Tuple,
)
def create_venv(parent_path: Path) -> Path:
if hasattr(sys, 'real_prefix'):
# python is currently running inside a venv
# pip_path = Path(sys.executable).parent
raise RuntimeError("Disable venv and try again.")
venv_path = parent_path / 'package-smoke-test'
pip_path = venv_path / 'bin' / 'pip'
venv.create(venv_path, with_pip=True)
assert Path.exists(venv_path), f'venv path "{venv_path}" does not exist.'
assert Path.exists(pip_path), f'pip executable not found at "{pip_path}"'
subprocess.run([pip_path, 'install', '-U', 'pip', 'setuptools'], check=True)
return venv_path
def find_wheel(project_path: Path) -> Path:
wheels = list(project_path.glob('dist/*.whl'))
if len(wheels) != 1:
raise Exception(f"Expected one wheel. Instead found: {wheels} in project {project_path.absolute()}")
return wheels[0]
def install_wheel(venv_path: Path, wheel_path: Path, extras: Tuple[str, ...] = ()) -> None:
if extras:
extra_suffix = f"[{','.join(extras)}]"
else:
extra_suffix = ""
subprocess.run([venv_path / 'bin' / 'pip', 'install', f"{wheel_path}{extra_suffix}"], check=True)
def test_install_local_wheel() -> None:
with TemporaryDirectory() as tmpdir:
venv_path = create_venv(Path(tmpdir))
wheel_path = find_wheel(Path('.'))
install_wheel(venv_path, wheel_path)
print("Installed", wheel_path.absolute(), "to", venv_path)
print(f"Activate with `source {venv_path}/bin/activate`")
input("Press enter when the test has completed. The directory will be deleted.")
if __name__ == '__main__':
test_install_local_wheel()

View File

@ -43,7 +43,6 @@ echo "Building Development Requirements"
poetry lock
poetry export -o dev-requirements.txt --without-hashes --with dev
echo "Building Standard Requirements"
poetry export -o requirements.txt --without-hashes --without dev

View File

@ -0,0 +1,65 @@
import subprocess
import sys
import venv
from pathlib import (
Path,
)
from tempfile import (
TemporaryDirectory,
)
from typing import (
Tuple,
)
def create_venv(parent_path: Path) -> Path:
if hasattr(sys, "real_prefix"):
# python is currently running inside a venv
# pip_path = Path(sys.executable).parent
raise RuntimeError("Disable venv and try again.")
venv_path = parent_path / "package-smoke-test"
pip_path = venv_path / "bin" / "pip"
venv.create(venv_path, with_pip=True)
assert Path.exists(venv_path), f'venv path "{venv_path}" does not exist.'
assert Path.exists(pip_path), f'pip executable not found at "{pip_path}"'
subprocess.run([pip_path, "install", "-U", "pip", "setuptools"], check=True)
return venv_path
def find_wheel(project_path: Path) -> Path:
wheels = list(project_path.glob("dist/*.whl"))
if len(wheels) != 1:
raise Exception(
f"Expected one wheel. Instead found: {wheels} in project {project_path.absolute()}"
)
return wheels[0]
def install_wheel(
venv_path: Path, wheel_path: Path, extras: Tuple[str, ...] = ()
) -> None:
if extras:
extra_suffix = f"[{','.join(extras)}]"
else:
extra_suffix = ""
subprocess.run(
[venv_path / "bin" / "pip", "install", f"{wheel_path}{extra_suffix}"],
check=True,
)
def test_install_local_wheel() -> None:
with TemporaryDirectory() as tmpdir:
venv_path = create_venv(Path(tmpdir))
wheel_path = find_wheel(Path("release"))
install_wheel(venv_path, wheel_path)
print("Installed", wheel_path.absolute(), "to", venv_path)
print(f"Activate with `source {venv_path}/bin/activate`")
input("Press enter when the test has completed. The directory will be deleted.")
if __name__ == "__main__":
test_install_local_wheel()

View File

@ -17,7 +17,6 @@ PYPI_CLASSIFIERS = [
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
@ -43,6 +42,9 @@ EXTRAS = {
"dev": DEV_REQUIRES,
}
# read the contents of your README file
long_description = (Path(__file__).parent / "README.md").read_text()
setup(
# Requirements
@ -75,6 +77,8 @@ setup(
author_email=ABOUT['__email__'],
description=ABOUT['__summary__'],
license=ABOUT['__license__'],
long_description_content_type="text/markdown",
long_description=long_description,
keywords="threshold access control, distributed key generation",
classifiers=PYPI_CLASSIFIERS,
)

View File

@ -0,0 +1,135 @@
import pytest
import pytest_twisted
from twisted.logger import globalLogPublisher
from nucypher.blockchain.eth.signers import InMemorySigner
from nucypher.crypto.keypairs import RitualisticKeypair
from nucypher.crypto.powers import RitualisticPower
from nucypher.utilities.warnings import render_ferveo_key_mismatch_warning
@pytest.fixture(scope="module")
def ritual_id():
return 0
@pytest.fixture(scope="module")
def dkg_size():
return 4
@pytest.fixture(scope="module")
def duration():
return 48 * 60 * 60
@pytest.fixture(scope="module")
def plaintext():
return "peace at dawn"
@pytest.fixture(scope="module")
def interval(testerchain):
return testerchain.tx_machine._task.interval
@pytest.fixture(scope="module")
def signer():
return InMemorySigner()
@pytest.fixture(scope="module")
def cohort(testerchain, clock, coordinator_agent, ursulas, dkg_size):
nodes = list(sorted(ursulas[:dkg_size], key=lambda x: int(x.checksum_address, 16)))
assert len(nodes) == dkg_size
for node in nodes:
node.ritual_tracker.task._task.clock = clock
node.ritual_tracker.start()
return nodes
@pytest_twisted.inlineCallbacks
def test_dkg_failure_with_ferveo_key_mismatch(
coordinator_agent,
ritual_id,
cohort,
clock,
interval,
testerchain,
initiator,
global_allow_list,
duration,
accounts,
ritual_token,
):
bad_ursula = cohort[0]
old_public_key = bad_ursula.public_keys(RitualisticPower)
new_keypair = RitualisticKeypair()
new_public_key = new_keypair.pubkey
bad_ursula._crypto_power._CryptoPower__power_ups[RitualisticPower].keypair = (
new_keypair
)
assert bytes(old_public_key) != bytes(new_public_key)
assert bytes(old_public_key) != bytes(bad_ursula.public_keys(RitualisticPower))
assert bytes(new_public_key) == bytes(bad_ursula.public_keys(RitualisticPower))
onchain_public_key = coordinator_agent.get_provider_public_key(
ritual_id=ritual_id, provider=bad_ursula.checksum_address
)
assert bytes(onchain_public_key) == bytes(old_public_key)
assert bytes(onchain_public_key) != bytes(new_public_key)
assert bytes(onchain_public_key) != bytes(bad_ursula.public_keys(RitualisticPower))
print(f"BAD URSULA: {bad_ursula.checksum_address}")
print("==================== INITIALIZING ====================")
cohort_staking_provider_addresses = list(u.checksum_address for u in cohort)
# Approve the ritual token for the coordinator agent to spend
amount = coordinator_agent.get_ritual_initiation_cost(
providers=cohort_staking_provider_addresses, duration=duration
)
ritual_token.approve(
coordinator_agent.contract_address,
amount,
sender=accounts[initiator.transacting_power.account],
)
receipt = coordinator_agent.initiate_ritual(
providers=cohort_staking_provider_addresses,
authority=initiator.transacting_power.account,
duration=duration,
access_controller=global_allow_list.address,
transacting_power=initiator.transacting_power,
)
testerchain.time_travel(seconds=1)
testerchain.wait_for_receipt(receipt["transactionHash"])
log_messages = []
def log_trapper(event):
log_messages.append(event["log_format"])
globalLogPublisher.addObserver(log_trapper)
print("==================== AWAITING DKG FAILURE ====================")
while len(log_messages) == 0:
yield clock.advance(interval)
yield testerchain.time_travel(seconds=1)
assert (
render_ferveo_key_mismatch_warning(
bytes(new_public_key), bytes(onchain_public_key)
)
in log_messages
)
testerchain.tx_machine.stop()
assert not testerchain.tx_machine.running
globalLogPublisher.removeObserver(log_trapper)

View File

@ -6,6 +6,11 @@ import pytest_twisted as pt
from eth_account import Account
from twisted.internet import threads
from nucypher.blockchain.eth.agents import (
CoordinatorAgent,
TACoApplicationAgent,
TACoChildApplicationAgent,
)
from nucypher.characters.base import Learner
from nucypher.cli.literature import NO_CONFIGURATIONS_ON_DISK
from nucypher.cli.main import nucypher_cli
@ -13,6 +18,8 @@ from nucypher.config.characters import UrsulaConfiguration
from nucypher.config.constants import (
TEMPORARY_DOMAIN_NAME,
)
from nucypher.crypto.ferveo.exceptions import FerveoKeyMismatch
from nucypher.crypto.powers import RitualisticPower
from nucypher.utilities.networking import LOOPBACK_ADDRESS
from tests.constants import (
INSECURE_DEVELOPMENT_PASSWORD,
@ -32,9 +39,25 @@ def test_missing_configuration_file(_default_filepath_mock, click_runner):
@pt.inlineCallbacks
def test_run_lone_default_development_ursula(click_runner, mocker, ursulas, accounts):
def test_ursula_startup(click_runner, mocker, accounts, testerchain):
deploy_port = select_test_port()
operator_address = ursulas[0].operator_address
operator_address = accounts[-1].address
mocker.patch.object(
TACoApplicationAgent,
"get_staking_provider_from_operator",
return_value=accounts[-2].address,
)
mocker.patch.object(
TACoChildApplicationAgent,
"staking_provider_from_operator",
return_value=accounts[-2].address,
)
mocker.patch.object(CoordinatorAgent, "set_provider_public_key", return_value=None)
account = Account.from_key(private_key=accounts[operator_address].private_key)
mocker.patch.object(Account, "create", return_value=account)
args = (
"ursula",
"run", # Stat Ursula Command
@ -54,9 +77,19 @@ def test_run_lone_default_development_ursula(click_runner, mocker, ursulas, acco
"memory://",
)
account = Account.from_key(private_key=accounts[operator_address].private_key)
mocker.patch.object(Account, "create", return_value=account)
# Trigger a ferveo key mismatch
mocker.patch.object(CoordinatorAgent, "get_provider_public_key", return_value=42)
with pytest.raises(FerveoKeyMismatch):
result = yield threads.deferToThread(
click_runner.invoke,
nucypher_cli,
args,
catch_exceptions=False,
input=INSECURE_DEVELOPMENT_PASSWORD + "\n",
)
# Normal startup
mocker.patch.object(RitualisticPower, "public_key", return_value=42)
result = yield threads.deferToThread(
click_runner.invoke,
nucypher_cli,

View File

@ -1,4 +1,3 @@
import copy
import json
import os
from unittest import mock
@ -23,9 +22,7 @@ from nucypher.policy.conditions.evm import (
RPCCondition,
)
from nucypher.policy.conditions.exceptions import (
ContextVariableVerificationFailed,
InvalidCondition,
InvalidContextVariableData,
NoConnectionToChain,
RequiredContextVariable,
RPCExecutionFailed,
@ -61,61 +58,6 @@ def test_required_context_variable(
) # no context
@pytest.mark.parametrize("expected_entry", ["address", "signature", "typedData"])
def test_user_address_context_missing_required_entries(expected_entry, valid_user_address_context):
context = copy.deepcopy(valid_user_address_context)
del context[USER_ADDRESS_CONTEXT][expected_entry]
with pytest.raises(InvalidContextVariableData):
get_context_value(USER_ADDRESS_CONTEXT, **context)
def test_user_address_context_invalid_eip712_typed_data(valid_user_address_context):
# invalid typed data
context = copy.deepcopy(valid_user_address_context)
context[USER_ADDRESS_CONTEXT]["typedData"] = dict(
randomSaying="Comparison is the thief of joy." # - Theodore Roosevelt
)
with pytest.raises(InvalidContextVariableData):
get_context_value(USER_ADDRESS_CONTEXT, **context)
def test_user_address_context_variable_verification(
valid_user_address_context, accounts
):
# valid user address context - signature matches address
address = get_context_value(USER_ADDRESS_CONTEXT, **valid_user_address_context)
assert address == valid_user_address_context[USER_ADDRESS_CONTEXT]["address"]
# invalid user address context - signature does not match address
# internals are mutable - deepcopy
mismatch_with_address_context = copy.deepcopy(valid_user_address_context)
mismatch_with_address_context[USER_ADDRESS_CONTEXT][
"address"
] = accounts.etherbase_account
with pytest.raises(ContextVariableVerificationFailed):
get_context_value(USER_ADDRESS_CONTEXT, **mismatch_with_address_context)
# invalid user address context - signature does not match address
# internals are mutable - deepcopy
mismatch_with_address_context = copy.deepcopy(valid_user_address_context)
signature = (
"0x93252ddff5f90584b27b5eef1915b23a8b01a703be56c8bf0660647c15cb75e9"
"1983bde9877eaad11da5a3ebc9b64957f1c182536931f9844d0c600f0c41293d1b"
)
mismatch_with_address_context[USER_ADDRESS_CONTEXT]["signature"] = signature
with pytest.raises(ContextVariableVerificationFailed):
get_context_value(USER_ADDRESS_CONTEXT, **mismatch_with_address_context)
# invalid signature
# internals are mutable - deepcopy
invalid_signature_context = copy.deepcopy(valid_user_address_context)
invalid_signature_context[USER_ADDRESS_CONTEXT][
"signature"
] = "0xdeadbeef" # invalid signature
with pytest.raises(ContextVariableVerificationFailed):
get_context_value(USER_ADDRESS_CONTEXT, **invalid_signature_context)
@mock.patch(
GET_CONTEXT_VALUE_IMPORT_PATH,
side_effect=_dont_validate_user_address,

View File

@ -48,7 +48,12 @@ DEAUTHORIZATION_DURATION = 60 * 60 * 24 * 60 # 60 days in seconds
COMMITMENT_DURATION_1 = 182 * 60 * 24 * 60 # 182 days in seconds
COMMITMENT_DURATION_2 = 2 * COMMITMENT_DURATION_1 # 365 days in seconds
COMMITMENT_DEADLINE = 60 * 60 * 24 * 100 # 100 days after deploymwent
COMMITMENT_DEADLINE = 60 * 60 * 24 * 100 # 100 days after deployment
PENALTY_DEFAULT = 1000 # 10% penalty
PENALTY_INCREMENT = 2500 # 25% penalty increment
PENALTY_DURATION = 60 * 60 * 24 # 1 day in seconds
# Coordinator
TIMEOUT = 3600
@ -167,6 +172,9 @@ def taco_application(
DEAUTHORIZATION_DURATION,
[COMMITMENT_DURATION_1, COMMITMENT_DURATION_2],
maya.now().epoch + COMMITMENT_DEADLINE,
PENALTY_DEFAULT,
PENALTY_DURATION,
PENALTY_INCREMENT,
)
proxy = deployer_account.deploy(
@ -210,6 +218,13 @@ def taco_child_application(
return proxy_contract
@pytest.fixture(scope="module")
def adjudicator(module_mocker, get_random_checksum_address):
_adjudicator = module_mocker.Mock()
_adjudicator.address = get_random_checksum_address()
return _adjudicator
@pytest.fixture(scope="module")
def coordinator(
oz_dependency,
@ -217,6 +232,7 @@ def coordinator(
deployer_account,
taco_child_application,
ritual_token,
adjudicator,
):
_coordinator = deployer_account.deploy(
nucypher_dependency.Coordinator,
@ -237,7 +253,9 @@ def coordinator(
proxy_contract = nucypher_dependency.Coordinator.at(proxy.address)
proxy_contract.makeInitiationPublic(sender=deployer_account)
taco_child_application.initialize(proxy_contract.address, sender=deployer_account)
taco_child_application.initialize(
proxy_contract.address, adjudicator.address, sender=deployer_account
)
return proxy_contract
@ -258,7 +276,6 @@ def subscription_manager(nucypher_dependency, deployer_account):
)
return _subscription_manager
#
# Deployment/Blockchains
#

View File

@ -13,8 +13,10 @@ import maya
import pytest
from click.testing import CliRunner
from eth_account import Account
from eth_account.messages import encode_typed_data
from eth_utils import to_checksum_address
from nucypher_core.ferveo import AggregatedTranscript, DkgPublicKey, Keypair, Validator
from siwe import SiweMessage
from twisted.internet.task import Clock
from web3 import Web3
@ -24,17 +26,22 @@ from nucypher.blockchain.eth.interfaces import (
BlockchainInterface,
BlockchainInterfaceFactory,
)
from nucypher.blockchain.eth.signers.software import KeystoreSigner
from nucypher.blockchain.eth.signers.software import InMemorySigner, KeystoreSigner
from nucypher.characters.lawful import Enrico, Ursula
from nucypher.cli.config import GroupGeneralConfig
from nucypher.config.characters import (
AliceConfiguration,
BobConfiguration,
UrsulaConfiguration,
)
from nucypher.config.constants import TEMPORARY_DOMAIN_NAME
from nucypher.config.constants import (
APP_DIR,
TEMPORARY_DOMAIN_NAME,
)
from nucypher.crypto.ferveo import dkg
from nucypher.crypto.keystore import Keystore
from nucypher.network.nodes import TEACHER_NODES
from nucypher.policy.conditions.auth.evm import EvmAuth
from nucypher.policy.conditions.context import USER_ADDRESS_CONTEXT
from nucypher.policy.conditions.evm import RPCCondition
from nucypher.policy.conditions.lingo import (
@ -646,43 +653,78 @@ def rpc_condition():
return condition
@pytest.fixture(scope="module")
def valid_user_address_context():
return {
USER_ADDRESS_CONTEXT: {
"signature": "0x488a7acefdc6d098eedf73cdfd379777c0f4a4023a660d350d3bf309a51dd4251abaad9cdd11b71c400cfb4625c14ca142f72b39165bd980c8da1ea32892ff071c",
"address": "0x5ce9454909639D2D17A3F753ce7d93fa0b9aB12E",
"typedData": {
"primaryType": "Wallet",
"types": {
"EIP712Domain": [
{"name": "name", "type": "string"},
{"name": "version", "type": "string"},
{"name": "chainId", "type": "uint256"},
{"name": "salt", "type": "bytes32"},
],
"Wallet": [
{"name": "address", "type": "string"},
{"name": "blockNumber", "type": "uint256"},
{"name": "blockHash", "type": "bytes32"},
{"name": "signatureText", "type": "string"},
],
},
"domain": {
"name": "tDec",
"version": "1",
"chainId": 80001,
"salt": "0x3e6365d35fd4e53cbc00b080b0742b88f8b735352ea54c0534ed6a2e44a83ff0",
},
"message": {
"address": "0x5ce9454909639D2D17A3F753ce7d93fa0b9aB12E",
"blockNumber": 28117088,
"blockHash": "0x104dfae58be4a9b15d59ce447a565302d5658914f1093f10290cd846fbe258b7",
"signatureText": "I'm the owner of address 0x5ce9454909639D2D17A3F753ce7d93fa0b9aB12E as of block number 28117088",
},
},
}
@pytest.fixture(scope="function")
def valid_eip712_auth_message():
signer = Account.create()
account = signer.address
data = {
"primaryType": "Wallet",
"types": {
"EIP712Domain": [
{"name": "name", "type": "string"},
{"name": "version", "type": "string"},
{"name": "chainId", "type": "uint256"},
{"name": "salt", "type": "bytes32"},
],
"Wallet": [
{"name": "address", "type": "string"},
{"name": "blockNumber", "type": "uint256"},
{"name": "blockHash", "type": "bytes32"},
{"name": "signatureText", "type": "string"},
],
},
"domain": {
"name": "tDec",
"version": "1",
"chainId": 80001,
"salt": "0x3e6365d35fd4e53cbc00b080b0742b88f8b735352ea54c0534ed6a2e44a83ff0",
},
"message": {
"address": f"{account}",
"blockNumber": 28117088,
"blockHash": "0x104dfae58be4a9b15d59ce447a565302d5658914f1093f10290cd846fbe258b7",
"signatureText": f"I'm the owner of address {account} as of block number 28117088",
},
}
signable_message = encode_typed_data(full_message=data)
signature = signer.sign_message(signable_message=signable_message)
auth_message = {
"signature": f"{signature.signature.hex()}",
"address": f"{account}",
"scheme": "EIP712",
"typedData": data,
}
return auth_message
@pytest.fixture(scope="function")
def valid_eip4361_auth_message():
signer = InMemorySigner()
siwe_message_data = {
"domain": "login.xyz",
"address": f"{signer.accounts[0]}",
"statement": "Sign-In With Ethereum Example Statement",
"uri": "https://login.xyz",
"version": "1",
"nonce": "bTyXgcQxn2htgkjJn",
"chain_id": 1,
"issued_at": f"{maya.now().iso8601()}",
}
siwe_message = SiweMessage(**siwe_message_data).prepare_message()
signature = signer.sign_message(
account=signer.accounts[0], message=siwe_message.encode()
)
auth_message = {
"signature": f"{signature.hex()}",
"address": f"{signer.accounts[0]}",
"scheme": f"{EvmAuth.AuthScheme.EIP4361.value}",
"typedData": f"{siwe_message}",
}
return auth_message
@pytest.fixture(scope="session", autouse=True)
@ -798,3 +840,50 @@ def mock_async_hooks(mocker):
)
return hooks
@pytest.fixture(scope="session", autouse=True)
def mock_halt_reactor(session_mocker):
session_mocker.patch.object(Ursula, "halt_reactor")
@pytest.fixture(scope="session")
def temp_config_root():
return Path("/tmp/nucypher-test")
@pytest.fixture(scope="session", autouse=True)
def mock_default_config_root(session_mocker, temp_config_root):
real_default_config_root = Path(APP_DIR.user_data_dir)
if real_default_config_root.exists():
if os.getenv("GITHUB_ACTIONS") == "true":
shutil.rmtree(real_default_config_root)
else:
raise RuntimeError(
f"{real_default_config_root} already exists. It is not permitted to run tests in an (production) "
f"environment where this directory exists. Please remove it before running tests."
)
session_mocker.patch(
"nucypher.config.constants.DEFAULT_CONFIG_ROOT", temp_config_root
)
session_mocker.patch.object(GroupGeneralConfig, "config_root", temp_config_root)
@pytest.fixture(scope="function", autouse=True)
def clear_config_root(temp_config_root):
if temp_config_root.exists():
print(f"Removing {temp_config_root}")
shutil.rmtree(Path("/tmp/nucypher-test"))
yield
if Path(APP_DIR.user_data_dir).exists():
raise RuntimeError(
f"{APP_DIR.user_data_dir} was used by a test. This is not permitted, please mock."
)
@pytest.fixture(scope="session", autouse=True)
def mock_default_rpc_endpoint_fetch(session_mocker):
session_mocker.patch(
"nucypher.blockchain.eth.utils.get_default_rpc_endpoints",
return_value={TESTERCHAIN_CHAIN_ID: [TEST_ETH_PROVIDER_URI]},
)

View File

@ -6,6 +6,7 @@ from web3 import Web3
from nucypher.blockchain.eth.actors import BaseActor, Operator
from nucypher.blockchain.eth.clients import EthereumClient
from nucypher.blockchain.eth.constants import NULL_ADDRESS
from nucypher.crypto.powers import RitualisticPower
@pytest.fixture(scope="function")
@ -117,6 +118,13 @@ def test_operator_block_until_ready_success(
ursula.checksum_address,
]
# mock key commitment
mocker.patch.object(
ursula.coordinator_agent,
"get_provider_public_key",
return_value=bytes(ursula.public_keys(RitualisticPower)),
)
log_messages = []
def log_trapper(event):

View File

@ -13,6 +13,7 @@ from nucypher.blockchain.eth.agents import CoordinatorAgent
from nucypher.blockchain.eth.models import Coordinator
from nucypher.blockchain.eth.signers.software import InMemorySigner
from nucypher.characters.lawful import Enrico, Ursula
from nucypher.crypto.keypairs import RitualisticKeypair
from nucypher.crypto.powers import RitualisticPower
from nucypher.policy.conditions.lingo import ConditionLingo, ConditionType
from tests.constants import TESTERCHAIN_CHAIN_ID
@ -37,24 +38,19 @@ ROUND_1_EVENT_NAME = "StartRitual"
ROUND_2_EVENT_NAME = "StartAggregationRound"
PARAMS = [ # dkg_size, ritual_id, variant
(2, 0, FerveoVariant.Precomputed),
(5, 1, FerveoVariant.Precomputed),
(8, 2, FerveoVariant.Precomputed),
(2, 3, FerveoVariant.Simple),
(5, 4, FerveoVariant.Simple),
(8, 5, FerveoVariant.Simple),
(2, 0, FerveoVariant.Simple),
(5, 1, FerveoVariant.Simple),
(8, 2, FerveoVariant.Simple),
# TODO: slow and need additional accounts for testing
# (16, 6, FerveoVariant.Precomputed),
# (16, 7, FerveoVariant.Simple),
# (32, 8, FerveoVariant.Precomputed),
# (32, 9, FerveoVariant.Simple),
# (16, 3, FerveoVariant.Simple),
# (32, 4, FerveoVariant.Simple),
]
BLOCKS = list(reversed(range(1, 1000)))
COORDINATOR = MockCoordinatorAgent(MockBlockchain())
@pytest.fixture(scope="function", autouse=True)
@pytest.fixture(scope="function")
def mock_coordinator_agent(testerchain, mock_contract_agency):
mock_contract_agency._MockContractAgency__agents[CoordinatorAgent] = COORDINATOR
@ -64,7 +60,7 @@ def mock_coordinator_agent(testerchain, mock_contract_agency):
@pytest.fixture(scope="function")
def cohort(ursulas, mock_coordinator_agent):
"""Creates a cohort of Ursulas"""
for u in ursulas:
# set mapping in coordinator agent
mock_coordinator_agent._add_operator_to_staking_provider_mapping(
@ -73,6 +69,7 @@ def cohort(ursulas, mock_coordinator_agent):
mock_coordinator_agent.set_provider_public_key(
u.public_keys(RitualisticPower), u.transacting_power
)
u.coordinator_agent = mock_coordinator_agent
u.ritual_tracker.coordinator_agent = mock_coordinator_agent
@ -123,12 +120,9 @@ def execute_round_2(ritual_id: int, cohort: List[Ursula]):
)
@pytest.mark.parametrize("dkg_size, ritual_id, variant", PARAMS)
@pytest_twisted.inlineCallbacks()
def test_ursula_ritualist(
testerchain,
def run_test(
mock_coordinator_agent,
cohort,
bad_cohort,
alice,
bob,
dkg_size,
@ -137,14 +131,10 @@ def test_ursula_ritualist(
get_random_checksum_address,
):
"""Tests the DKG and the encryption/decryption of a message"""
cohort = cohort[:dkg_size]
cohort = bad_cohort[:dkg_size]
# adjust threshold since we are testing with pre-computed (simple is the default)
threshold = mock_coordinator_agent.get_threshold_for_ritual_size(
dkg_size
) # default is simple
if variant == FerveoVariant.Precomputed:
threshold = dkg_size
threshold = mock_coordinator_agent.get_threshold_for_ritual_size(dkg_size)
with patch.object(
mock_coordinator_agent, "get_threshold_for_ritual_size", return_value=threshold
@ -152,7 +142,10 @@ def test_ursula_ritualist(
def initialize():
"""Initiates the ritual"""
print("==================== INITIALIZING ====================")
print(
f"==================== INITIALIZING {dkg_size} {variant} ===================="
)
cohort_staking_provider_addresses = list(u.checksum_address for u in cohort)
mock_coordinator_agent.initiate_ritual(
providers=cohort_staking_provider_addresses,
@ -161,6 +154,9 @@ def test_ursula_ritualist(
access_controller=get_random_checksum_address(),
transacting_power=alice.transacting_power,
)
print(
f"cohort_staking_provider_addresses: {cohort_staking_provider_addresses}"
)
assert mock_coordinator_agent.number_of_rituals() == ritual_id + 1
def round_1(_):
@ -356,3 +352,66 @@ def test_ursula_ritualist(
d.addCallback(callback)
d.addErrback(error_handler)
yield d
@pytest.mark.parametrize("dkg_size, ritual_id, variant", PARAMS)
@pytest_twisted.inlineCallbacks()
def test_ursula_ritualist_good_cohort(
testerchain,
mock_coordinator_agent,
cohort,
alice,
bob,
dkg_size,
ritual_id,
variant,
get_random_checksum_address,
):
yield from run_test(
mock_coordinator_agent,
cohort,
alice,
bob,
dkg_size,
ritual_id,
variant,
get_random_checksum_address,
)
@pytest.mark.xfail(reason="This is not fixed yet")
@pytest_twisted.inlineCallbacks()
def test_ursula_ritualist_bad_cohort(
mock_coordinator_agent,
cohort,
alice,
bob,
get_random_checksum_address,
):
"""Modify the first Ursula's keystore to be different"""
bad_ursula = cohort[0]
old_public_key = bad_ursula.public_keys(RitualisticPower)
new_keypair = RitualisticKeypair()
new_public_key = new_keypair.pubkey
# Modify the first Ursula's keystore to be different
bad_ursula._crypto_power._CryptoPower__power_ups[RitualisticPower].keypair = (
new_keypair
)
assert old_public_key != new_public_key
assert old_public_key != bad_ursula.public_keys(RitualisticPower)
assert new_public_key == bad_ursula.public_keys(RitualisticPower)
print(f"BAD URSULA: {bad_ursula.checksum_address}")
yield from run_test(
mock_coordinator_agent,
cohort,
alice,
bob,
2,
3,
FerveoVariant.Precomputed,
get_random_checksum_address,
)

View File

@ -1,5 +1,6 @@
import json
import pytest
from nucypher_core import Address, Conditions, RetrievalKit
from nucypher_core._nucypher_core import MessageKit
@ -15,6 +16,7 @@ def _policy_info_kwargs(enacted_policy):
)
@pytest.mark.usefixtures("mock_payment_method")
def test_retrieval_kit(enacted_policy, ursulas):
messages, message_kits = make_message_kits(enacted_policy.public_key)
@ -29,7 +31,9 @@ def test_retrieval_kit(enacted_policy, ursulas):
assert retrieval_kit.queried_addresses == retrieval_kit_back.queried_addresses
def test_single_retrieve(enacted_policy, bob, ursulas):
@pytest.mark.usefixtures("mock_payment_method")
def test_single_retrieve(enacted_policy, bob, ursulas, mocker):
bob.remember_node(ursulas[0])
bob.start_learning_loop()
messages, message_kits = make_message_kits(enacted_policy.public_key)
@ -42,6 +46,7 @@ def test_single_retrieve(enacted_policy, bob, ursulas):
assert cleartexts == messages
@pytest.mark.usefixtures("mock_payment_method")
def test_single_retrieve_conditions_set_directly_to_none(enacted_policy, bob, ursulas):
bob.start_learning_loop()
message = b"plaintext1"
@ -59,6 +64,7 @@ def test_single_retrieve_conditions_set_directly_to_none(enacted_policy, bob, ur
assert cleartexts == [message]
@pytest.mark.usefixtures("mock_payment_method")
def test_single_retrieve_conditions_empty_list(enacted_policy, bob, ursulas):
bob.start_learning_loop()
message = b"plaintext1"
@ -76,6 +82,7 @@ def test_single_retrieve_conditions_empty_list(enacted_policy, bob, ursulas):
assert cleartexts == [message]
@pytest.mark.usefixtures("mock_payment_method")
def test_use_external_cache(enacted_policy, bob, ursulas):
bob.start_learning_loop()

View File

@ -12,6 +12,7 @@ from tests.constants import MOCK_ETH_PROVIDER_URI
from tests.utils.middleware import MockRestMiddleware
@pytest.mark.usefixtures("mock_payment_method")
def test_bob_full_retrieve_flow(
ursulas, bob, alice, capsule_side_channel, treasure_map, enacted_policy
):
@ -35,6 +36,7 @@ def test_bob_full_retrieve_flow(
assert b"Welcome to flippering number 0." == delivered_cleartexts[0]
@pytest.mark.usefixtures("mock_payment_method")
def test_bob_retrieves(accounts, alice, ursulas):
"""A test to show that Bob can retrieve data from Ursula"""
@ -96,6 +98,7 @@ def test_bob_retrieves(accounts, alice, ursulas):
bob.disenchant()
@pytest.mark.usefixtures("mock_payment_method")
def test_bob_retrieves_with_treasure_map(
bob, ursulas, enacted_policy, capsule_side_channel
):
@ -118,6 +121,7 @@ def test_bob_retrieves_with_treasure_map(
# TODO: #2813 Without kfrag and arrangement storage by nodes,
@pytest.mark.skip()
@pytest.mark.usefixtures("mock_payment_method")
def test_bob_retrieves_too_late(bob, ursulas, enacted_policy, capsule_side_channel):
clock = Clock()
clock.advance(time.time())

View File

@ -26,7 +26,10 @@ def _policy_info_kwargs(enacted_policy):
)
def test_single_retrieve_with_truthy_conditions(enacted_policy, bob, ursulas, mocker):
@pytest.mark.usefixtures("mock_payment_method")
def test_single_retrieve_with_truthy_conditions(
enacted_policy, bob, ursulas, mocker, mock_payment_method
):
from nucypher_core import MessageKit
reencrypt_spy = mocker.spy(Ursula, '_reencrypt')
@ -68,7 +71,10 @@ def test_single_retrieve_with_truthy_conditions(enacted_policy, bob, ursulas, mo
assert reencrypt_spy.call_count == enacted_policy.threshold
def test_single_retrieve_with_falsy_conditions(enacted_policy, bob, ursulas, mocker):
@pytest.mark.usefixtures("mock_payment_method")
def test_single_retrieve_with_falsy_conditions(
enacted_policy, bob, ursulas, mocker, mock_payment_method
):
from nucypher_core import MessageKit
reencrypt_spy = mocker.spy(Ursula, '_reencrypt')
@ -124,6 +130,7 @@ FAILURE_CASE_EXCEPTION_CODE_MATCHING = [
"eval_failure_exception_class, middleware_exception_class",
FAILURE_CASE_EXCEPTION_CODE_MATCHING,
)
@pytest.mark.usefixtures("mock_payment_method")
def test_middleware_handling_of_failed_condition_responses(
eval_failure_exception_class,
middleware_exception_class,
@ -131,6 +138,7 @@ def test_middleware_handling_of_failed_condition_responses(
enacted_policy,
bob,
mock_rest_middleware,
mock_payment_method,
):
# we use a failed condition for reencryption to test conversion of response codes to middleware exceptions
from nucypher_core import MessageKit

View File

@ -1,8 +1,7 @@
import datetime
import maya
import pytest
from nucypher_core import EncryptedKeyFrag, RevocationOrder
from nucypher_core import EncryptedKeyFrag
from nucypher.characters.lawful import Enrico

View File

@ -62,49 +62,6 @@ def test_auto_select_config_file(
config_file=str(config_path)) in captured.out
@pytest.mark.skip(reason="planned for removal")
def test_interactive_select_config_file(
test_emitter,
capsys,
alice_test_config,
temp_dir_path,
mock_stdin,
mock_accounts,
patch_keystore,
):
"""Multiple configurations found - Prompt the user for a selection"""
user_input = 0
config = alice_test_config
config_class = config.__class__
# Make one configuration...
config_path = temp_dir_path / config_class.generate_filename()
config.to_configuration_file(filepath=config_path)
assert config_path.exists()
select_config_file(emitter=test_emitter,
config_class=config_class,
config_root=temp_dir_path)
# ... and then a bunch more
accounts = list(mock_accounts.items())
filenames = dict()
for filename, account in accounts:
config.checksum_address = account.address
config_path = temp_dir_path / config.generate_filename(modifier=account.address)
path = config.to_configuration_file(filepath=config_path, modifier=account.address)
filenames[path] = account.address
assert config_path.exists()
mock_stdin.line(str(user_input))
captured = capsys.readouterr()
for filename, account in accounts:
assert account.address in captured.out
assert mock_stdin.empty()
def test_confirm_prompt_to_migrate_select_config_file(
test_emitter, capsys, alice_test_config, temp_dir_path, mock_stdin
):

View File

@ -1,3 +1,5 @@
import shutil
import pytest
from nucypher.blockchain.eth.actors import Operator
@ -18,7 +20,7 @@ from tests.constants import (
@pytest.mark.usefixtures("mock_registry_sources")
def test_ursula_startup_ip_checkup(click_runner, mocker):
def test_ursula_startup_ip_checkup(click_runner, mocker, temp_config_root):
target = "nucypher.cli.actions.configure.determine_external_ip_address"
# Patch the get_external_ip call
@ -48,6 +50,7 @@ def test_ursula_startup_ip_checkup(click_runner, mocker):
)
assert result.exit_code == 0, result.output
assert MOCK_IP_ADDRESS in result.output
shutil.rmtree(str(temp_config_root.absolute()))
args = (
"ursula",
@ -64,6 +67,7 @@ def test_ursula_startup_ip_checkup(click_runner, mocker):
nucypher_cli, args, catch_exceptions=False, input=FAKE_PASSWORD_CONFIRMED
)
assert result.exit_code == 0, result.output
shutil.rmtree(str(temp_config_root.absolute()))
# Patch get_external_ip call to error output
mocker.patch(target, side_effect=UnknownIPAddress)

View File

@ -26,6 +26,7 @@ from nucypher.cli.types import ChecksumAddress
from nucypher.config.characters import UrsulaConfiguration
from nucypher.crypto.powers import TransactingPower
from nucypher.network.nodes import Teacher
from nucypher.policy.payment import SubscriptionManagerPayment
from tests.constants import (
KEYFILE_NAME_TEMPLATE,
MOCK_KEYSTORE_PATH,
@ -33,6 +34,7 @@ from tests.constants import (
TEMPORARY_DOMAIN,
TESTERCHAIN_CHAIN_ID,
)
from tests.mock.agents import MockContractAgency
from tests.mock.interfaces import MockBlockchain
from tests.mock.io import MockStdinWrapper
from tests.utils.registry import MockRegistrySource, mock_registry_sources
@ -133,7 +135,6 @@ def test_registry(module_mocker):
@pytest.fixture(scope='module', autouse=True)
def mock_contract_agency():
# Patch
from tests.mock.agents import MockContractAgency
# Monkeypatch # TODO: Use better tooling for this monkeypatch?
get_agent = ContractAgency.get_agent
@ -295,3 +296,8 @@ def multichain_ursulas(ursulas, multichain_ids):
@pytest.fixture(scope="module")
def mock_prometheus(module_mocker):
return module_mocker.patch("nucypher.characters.lawful.start_prometheus_exporter")
@pytest.fixture(scope="module")
def mock_payment_method(module_mocker):
module_mocker.patch.object(SubscriptionManagerPayment, "verify", return_value=True)

View File

@ -19,7 +19,7 @@ BlockchainInterfaceFactory._interfaces[MOCK_ETH_PROVIDER_URI] = CACHED_MOCK_TEST
class MockContractAgent:
FAKE_CALL_RESULT = 1
FAKE_CALL_RESULT = 0
# Internal
__COLLECTION_MARKER = "contract_api" # decorator attribute
@ -53,7 +53,8 @@ class MockContractAgent:
def __setup_mock(self, agent_class: Type[Agent]) -> None:
api_methods: Iterable[Callable] = list(self.__collect_contract_api(agent_class=agent_class))
mock_methods, mock_properties = list(), dict()
mock_methods = list()
# mock_properties = dict()
for agent_interface in api_methods:
@ -88,7 +89,8 @@ class MockContractAgent:
self._REAL_METHODS = api_methods
def __get_interface_calls(self, interface: Enum) -> List[Callable]:
predicate = lambda method: bool(method.contract_api == interface)
def predicate(method):
return bool(method.contract_api == interface)
interface_calls = list(filter(predicate, self._MOCK_METHODS))
return interface_calls
@ -127,9 +129,13 @@ class MockContractAgent:
def get_unexpected_transactions(self, allowed: Union[Iterable[Callable], None]) -> List[Callable]:
if allowed:
predicate = lambda tx: tx not in allowed and tx.called
def predicate(tx):
return tx not in allowed and tx.called
else:
predicate = lambda tx: tx.called
def predicate(tx):
return tx.called
unexpected_transactions = list(filter(predicate, self.all_transactions))
return unexpected_transactions

View File

@ -1,14 +1,26 @@
import copy
import itertools
import re
import pytest
from nucypher.policy.conditions.context import (
USER_ADDRESS_CONTEXT,
USER_ADDRESS_EIP4361_EXTERNAL_CONTEXT,
_resolve_context_variable,
_resolve_user_address,
get_context_value,
is_context_variable,
resolve_any_context_variables,
)
from nucypher.policy.conditions.lingo import ReturnValueTest
from nucypher.policy.conditions.exceptions import (
ContextVariableVerificationFailed,
InvalidConditionContext,
InvalidContextVariableData,
)
from nucypher.policy.conditions.lingo import (
ReturnValueTest,
)
INVALID_CONTEXT_PARAM_NAMES = [
":",
@ -81,3 +93,128 @@ def test_resolve_any_context_variables():
assert resolved_return_value.comparator == return_value_test.comparator
assert resolved_return_value.index == return_value_test.index
assert resolved_return_value.value == resolved_value
@pytest.mark.parametrize("expected_entry", ["address", "signature", "typedData"])
@pytest.mark.parametrize(
"context_variable_name, valid_user_address_fixture",
[
(USER_ADDRESS_CONTEXT, "valid_eip4361_auth_message"),
(USER_ADDRESS_CONTEXT, "valid_eip712_auth_message"), # allowed for now
(USER_ADDRESS_EIP4361_EXTERNAL_CONTEXT, "valid_eip4361_auth_message"),
],
)
def test_user_address_context_missing_required_entries(
expected_entry, context_variable_name, valid_user_address_fixture, request
):
valid_user_address_auth_message = request.getfixturevalue(
valid_user_address_fixture
)
context = {context_variable_name: valid_user_address_auth_message}
del context[context_variable_name][expected_entry]
with pytest.raises(InvalidContextVariableData):
get_context_value(context_variable_name, **context)
@pytest.mark.parametrize(
"context_variable_name, valid_user_address_fixture",
[
(USER_ADDRESS_CONTEXT, "valid_eip4361_auth_message"),
(USER_ADDRESS_CONTEXT, "valid_eip712_auth_message"), # allowed for now
(USER_ADDRESS_EIP4361_EXTERNAL_CONTEXT, "valid_eip4361_auth_message"),
],
)
def test_user_address_context_invalid_typed_data(
context_variable_name, valid_user_address_fixture, request
):
valid_user_address_auth_message = request.getfixturevalue(
valid_user_address_fixture
)
# invalid typed data
context = {context_variable_name: valid_user_address_auth_message}
context[context_variable_name]["typedData"] = dict(
randomSaying="Comparison is the thief of joy." # - Theodore Roosevelt
)
with pytest.raises(InvalidContextVariableData):
get_context_value(context_variable_name, **context)
@pytest.mark.parametrize(
"context_variable_name, valid_user_address_fixture",
[
# EIP712 message not compatible with EIP4361 context variable
(USER_ADDRESS_EIP4361_EXTERNAL_CONTEXT, "valid_eip712_auth_message"),
],
)
def test_user_address_context_variable_with_incompatible_auth_message(
context_variable_name, valid_user_address_fixture, request
):
valid_user_address_auth_message = request.getfixturevalue(
valid_user_address_fixture
)
# scheme in message is unexpected for context variable name
context = {context_variable_name: valid_user_address_auth_message}
with pytest.raises(InvalidContextVariableData, match="UnexpectedScheme"):
get_context_value(context_variable_name, **context)
@pytest.mark.parametrize(
"context_variable_name, valid_user_address_fixture",
[
(USER_ADDRESS_CONTEXT, "valid_eip4361_auth_message"),
(USER_ADDRESS_CONTEXT, "valid_eip712_auth_message"), # allowed for now
(USER_ADDRESS_EIP4361_EXTERNAL_CONTEXT, "valid_eip4361_auth_message"),
],
)
def test_user_address_context_variable_verification(
context_variable_name,
valid_user_address_fixture,
get_random_checksum_address,
request,
):
valid_user_address_auth_message = request.getfixturevalue(
valid_user_address_fixture
)
valid_user_address_context = {
context_variable_name: valid_user_address_auth_message
}
# call underlying directive directly (appease codecov)
address = _resolve_user_address(
user_address_context_variable=context_variable_name,
**valid_user_address_context,
)
assert address == valid_user_address_context[context_variable_name]["address"]
# valid user address context
address = get_context_value(context_variable_name, **valid_user_address_context)
assert address == valid_user_address_context[context_variable_name]["address"]
# invalid user address context - signature does not match address
# internals are mutable - deepcopy
mismatch_with_address_context = copy.deepcopy(valid_user_address_context)
mismatch_with_address_context[context_variable_name][
"address"
] = get_random_checksum_address()
with pytest.raises(ContextVariableVerificationFailed):
get_context_value(context_variable_name, **mismatch_with_address_context)
# invalid user address context - signature does not match address
# internals are mutable - deepcopy
mismatch_with_address_context = copy.deepcopy(valid_user_address_context)
signature = (
"0x93252ddff5f90584b27b5eef1915b23a8b01a703be56c8bf0660647c15cb75e9"
"1983bde9877eaad11da5a3ebc9b64957f1c182536931f9844d0c600f0c41293d1b"
)
mismatch_with_address_context[context_variable_name]["signature"] = signature
with pytest.raises(ContextVariableVerificationFailed):
get_context_value(context_variable_name, **mismatch_with_address_context)
# invalid signature
# internals are mutable - deepcopy
invalid_signature_context = copy.deepcopy(valid_user_address_context)
invalid_signature_context[context_variable_name][
"signature"
] = "0xdeadbeef" # invalid signature
with pytest.raises(InvalidConditionContext):
get_context_value(context_variable_name, **invalid_signature_context)

View File

@ -0,0 +1,281 @@
import maya
import pytest
from siwe import SiweMessage
from nucypher.blockchain.eth.signers import InMemorySigner
from nucypher.policy.conditions.auth.evm import EIP712Auth, EIP4361Auth, EvmAuth
def test_auth_scheme():
for scheme in EvmAuth.AuthScheme:
expected_scheme = (
EIP712Auth if scheme == EvmAuth.AuthScheme.EIP712 else EIP4361Auth
)
assert EvmAuth.from_scheme(scheme=scheme.value) == expected_scheme
# non-existent scheme
with pytest.raises(ValueError):
_ = EvmAuth.from_scheme(scheme="rando")
def test_authenticate_eip712(valid_eip712_auth_message, get_random_checksum_address):
data = valid_eip712_auth_message["typedData"]
signature = valid_eip712_auth_message["signature"]
address = valid_eip712_auth_message["address"]
# invalid data
invalid_data = dict(data) # make a copy
del invalid_data["domain"]
with pytest.raises(EvmAuth.InvalidData):
EIP712Auth.authenticate(
data=invalid_data, signature=signature, expected_address=address
)
invalid_data = dict(data) # make a copy
del invalid_data["message"]
with pytest.raises(EvmAuth.InvalidData):
EIP712Auth.authenticate(
data=invalid_data, signature=signature, expected_address=address
)
# signature not for expected address
incorrect_signature = (
"0x93252ddff5f90584b27b5eef1915b23a8b01a703be56c8bf0660647c15cb75e9"
"1983bde9877eaad11da5a3ebc9b64957f1c182536931f9844d0c600f0c41293d1b"
)
with pytest.raises(EvmAuth.AuthenticationFailed):
EIP712Auth.authenticate(
data=data, signature=incorrect_signature, expected_address=address
)
# invalid signature
invalid_signature = "0xdeadbeef"
with pytest.raises(EvmAuth.InvalidData):
EIP712Auth.authenticate(
data=data, signature=invalid_signature, expected_address=address
)
# mismatch with expected address
with pytest.raises(
EvmAuth.AuthenticationFailed, match="signature not valid for expected address"
):
EIP712Auth.authenticate(
data=data,
signature=signature,
expected_address=get_random_checksum_address(),
)
# everything valid
EIP712Auth.authenticate(data, signature, address)
def test_authenticate_eip4361(get_random_checksum_address):
signer = InMemorySigner()
siwe_message_data = {
"domain": "login.xyz",
"address": f"{signer.accounts[0]}",
"statement": "Sign-In With Ethereum Example Statement",
"uri": "did:key:z6Mkf55NiCvhxbLg6waBsJ58Hq4Nx6diedT7MGv1189gxV4i",
"version": "1",
"nonce": "bTyXgcQxn2htgkjJn",
"chain_id": 1,
"issued_at": f"{maya.now().iso8601()}",
"resources": ["ceramic://*"],
}
valid_message = SiweMessage(**siwe_message_data).prepare_message()
valid_message_signature = signer.sign_message(
account=signer.accounts[0], message=valid_message.encode()
)
valid_address_for_signature = signer.accounts[0]
# everything valid
EIP4361Auth.authenticate(
valid_message, valid_message_signature, valid_address_for_signature
)
# invalid data
invalid_data = "just a regular old string"
with pytest.raises(EvmAuth.InvalidData):
EIP4361Auth.authenticate(
data=invalid_data,
signature=valid_message_signature,
expected_address=valid_address_for_signature,
)
# signature not for expected address
incorrect_signature = (
"0x93252ddff5f90584b27b5eef1915b23a8b01a703be56c8bf0660647c15cb75e9"
"1983bde9877eaad11da5a3ebc9b64957f1c182536931f9844d0c600f0c41293d1b"
)
with pytest.raises(
EvmAuth.AuthenticationFailed,
match="EIP4361 verification failed - InvalidSignature",
):
EIP4361Auth.authenticate(
data=valid_message,
signature=incorrect_signature,
expected_address=valid_address_for_signature,
)
# invalid signature
invalid_signature = "0xdeadbeef"
with pytest.raises(
EvmAuth.AuthenticationFailed,
match="EIP4361 verification failed - InvalidSignature",
):
EIP4361Auth.authenticate(
data=valid_message,
signature=invalid_signature,
expected_address=valid_address_for_signature,
)
# mismatch with expected address
with pytest.raises(
EvmAuth.AuthenticationFailed, match="signature not valid for expected address"
):
EIP4361Auth.authenticate(
data=valid_message,
signature=valid_message_signature,
expected_address=get_random_checksum_address(),
)
# expiration provided - not yet reached
expiration_message_data = dict(siwe_message_data)
expiration_message_data["expiration_time"] = maya.now().add(hours=1).iso8601()
expiration_message = SiweMessage(**expiration_message_data).prepare_message()
expiration_message_signature = signer.sign_message(
account=valid_address_for_signature, message=expiration_message.encode()
)
EIP4361Auth.authenticate(
expiration_message,
expiration_message_signature.hex(),
valid_address_for_signature,
) # authentication works
# expiration provided - already expired
already_expired_message_data = dict(siwe_message_data)
already_expired_message_data["expiration_time"] = (
maya.now().subtract(minutes=45).iso8601()
)
already_expired_message_data["issued_at"] = (
maya.now().subtract(minutes=60).iso8601()
)
already_expired_message = SiweMessage(
**already_expired_message_data
).prepare_message()
already_expired_message_signature = signer.sign_message(
account=valid_address_for_signature, message=already_expired_message.encode()
)
with pytest.raises(
EvmAuth.AuthenticationFailed,
match="EIP4361 verification failed - ExpiredMessage",
):
EIP4361Auth.authenticate(
already_expired_message,
already_expired_message_signature.hex(),
valid_address_for_signature,
) # authentication fails
# not_before not yet reached
not_before_message_data = dict(siwe_message_data)
not_before_message_data["not_before"] = maya.now().add(hours=1).iso8601()
not_before_message = SiweMessage(**not_before_message_data).prepare_message()
not_before_signature = signer.sign_message(
account=valid_address_for_signature, message=not_before_message.encode()
)
with pytest.raises(
EvmAuth.AuthenticationFailed,
match="EIP4361 verification failed - NotYetValidMessage",
):
EIP4361Auth.authenticate(
not_before_message, not_before_signature.hex(), valid_address_for_signature
)
# not_before already reached
not_before_message_data = dict(siwe_message_data)
not_before_message_data["not_before"] = maya.now().subtract(hours=1).iso8601()
not_before_message = SiweMessage(**not_before_message_data).prepare_message()
not_before_signature = signer.sign_message(
account=valid_address_for_signature, message=not_before_message.encode()
)
EIP4361Auth.authenticate(
not_before_message, not_before_signature.hex(), valid_address_for_signature
) # all is well
# issued at in the future (sneaky!)
futuristic_issued_at_message_data = dict(siwe_message_data)
futuristic_issued_at_message_data["issued_at"] = (
f"{maya.now().add(minutes=30).iso8601()}"
)
futuristic_issued_at_message = SiweMessage(
**futuristic_issued_at_message_data
).prepare_message()
futuristic_issued_at_message_signature = signer.sign_message(
account=valid_address_for_signature,
message=futuristic_issued_at_message.encode(),
)
with pytest.raises(
EvmAuth.AuthenticationFailed,
match="EIP4361 issued-at datetime is in the future",
):
EIP4361Auth.authenticate(
futuristic_issued_at_message,
futuristic_issued_at_message_signature.hex(),
valid_address_for_signature,
)
# stale message - issued_at
stale_message_data = dict(siwe_message_data)
stale_message_data["issued_at"] = (
f"{maya.now().subtract(hours=EIP4361Auth.FRESHNESS_IN_HOURS + 1).iso8601()}"
)
stale_message = SiweMessage(**stale_message_data).prepare_message()
stale_message_signature = signer.sign_message(
account=valid_address_for_signature, message=stale_message.encode()
)
with pytest.raises(
EvmAuth.StaleMessage,
match=f"EIP4361 message is more than {EIP4361Auth.FRESHNESS_IN_HOURS} hours old",
):
EIP4361Auth.authenticate(
stale_message, stale_message_signature.hex(), valid_address_for_signature
)
# old, but not stale and still valid
old_but_not_stale_message_data = dict(siwe_message_data)
old_but_not_stale_message_data["issued_at"] = (
f"{maya.now().subtract(hours=EIP4361Auth.FRESHNESS_IN_HOURS - 1).iso8601()}"
)
old_but_not_stale_message = SiweMessage(
**old_but_not_stale_message_data
).prepare_message()
old_not_stale_message_signature = signer.sign_message(
account=valid_address_for_signature, message=old_but_not_stale_message.encode()
)
EIP4361Auth.authenticate(
old_but_not_stale_message,
old_not_stale_message_signature.hex(),
valid_address_for_signature,
)
# old but not stale; fails due to expiry time used in message itself
not_stale_but_past_expiry = dict(old_but_not_stale_message_data)
not_stale_but_past_expiry["expiration_time"] = (
f"{maya.now().subtract(seconds=30).iso8601()}"
)
not_stale_but_past_expiry_message = SiweMessage(
**not_stale_but_past_expiry
).prepare_message()
not_stale_but_past_expiry_signature = signer.sign_message(
account=valid_address_for_signature,
message=not_stale_but_past_expiry_message.encode(),
)
with pytest.raises(
EvmAuth.AuthenticationFailed,
match="EIP4361 verification failed - ExpiredMessage",
):
EIP4361Auth.authenticate(
not_stale_but_past_expiry_message,
not_stale_but_past_expiry_signature.hex(),
valid_address_for_signature,
)

View File

@ -226,8 +226,8 @@ def test_restore_keystore_from_mnemonic(tmpdir, mocker):
_keystore = Keystore(keystore_path=keystore_path)
# Restore with user-supplied words and a new password
keystore = Keystore.restore(words=words, password='ANewHope')
keystore.unlock(password='ANewHope')
keystore = Keystore.restore(words=words, password="ANewHope", keystore_dir=tmpdir)
keystore.unlock(password="ANewHope")
assert keystore._Keystore__secret == secret

View File

@ -10,11 +10,11 @@ CHAIN_ID = 23
@pytest.mark.parametrize("chain_id_return_value", [hex(CHAIN_ID), CHAIN_ID])
def test_cached_chain_id(mocker, chain_id_return_value):
web3_mock = mocker.MagicMock()
mock_client = EthereumClient(w3=web3_mock)
chain_id_property_mock = PropertyMock(return_value=chain_id_return_value)
type(web3_mock.eth).chain_id = chain_id_property_mock
mock_client = EthereumClient(w3=web3_mock)
assert mock_client.chain_id == CHAIN_ID
chain_id_property_mock.assert_called_once()

View File

@ -1,3 +1,5 @@
from unittest.mock import patch
import pytest
from atxm.exceptions import Fault, InsufficientFunds
@ -139,8 +141,18 @@ def test_perform_round_1(
lambda *args, **kwargs: Coordinator.RitualStatus.DKG_AWAITING_TRANSCRIPTS
)
phase_id = PhaseId(ritual_id=0, phase=PHASE1)
# cryptographic issue does not raise exception
with patch(
"nucypher.crypto.ferveo.dkg.generate_transcript",
side_effect=Exception("transcript cryptography failed"),
):
async_tx = ursula.perform_round_1(
ritual_id=0, authority=random_address, participants=cohort, timestamp=0
)
# exception not raised, but None returned
assert async_tx is None
phase_id = PhaseId(ritual_id=0, phase=PHASE1)
assert (
ursula.dkg_storage.get_ritual_phase_async_tx(phase_id=phase_id) is None
), "no tx data as yet"
@ -244,8 +256,16 @@ def test_perform_round_2(
lambda *args, **kwargs: Coordinator.RitualStatus.DKG_AWAITING_AGGREGATIONS
)
phase_2_id = PhaseId(ritual_id=0, phase=PHASE2)
# cryptographic issue does not raise exception
with patch(
"nucypher.crypto.ferveo.dkg.verify_aggregate",
side_effect=Exception("aggregate cryptography failed"),
):
async_tx = ursula.perform_round_2(ritual_id=0, timestamp=0)
# exception not raised, but None returned
assert async_tx is None
phase_2_id = PhaseId(ritual_id=0, phase=PHASE2)
assert (
ursula.dkg_storage.get_ritual_phase_async_tx(phase_id=phase_2_id) is None
), "no tx data as yet"

View File

@ -0,0 +1,84 @@
import requests
from nucypher.blockchain.eth.utils import (
get_default_rpc_endpoints,
get_healthy_default_rpc_endpoints,
rpc_endpoint_health_check,
)
def test_rpc_endpoint_health_check(mocker):
mock_time = mocker.patch("time.time", return_value=1625247600)
mock_post = mocker.patch("requests.post")
mock_response = mocker.Mock()
mock_response.status_code = 200
mock_response.json.return_value = {
"jsonrpc": "2.0",
"id": 1,
"result": {"timestamp": hex(1625247600)},
}
mock_post.return_value = mock_response
# Test a healthy endpoint
assert rpc_endpoint_health_check("http://mockendpoint") is True
# Test an unhealthy endpoint (drift too large)
mock_time.return_value = 1625247600 + 100 # System time far ahead
assert rpc_endpoint_health_check("http://mockendpoint") is False
# Test request exception
mock_post.side_effect = requests.exceptions.RequestException
assert rpc_endpoint_health_check("http://mockendpoint") is False
def test_get_default_rpc_endpoints(mocker):
mock_get = mocker.patch("requests.get")
mock_response = mocker.Mock()
mock_response.status_code = 200
mock_response.json.return_value = {
"1": ["http://endpoint1", "http://endpoint2"],
"2": ["http://endpoint3", "http://endpoint4"],
}
mock_get.return_value = mock_response
expected_result = {
1: ["http://endpoint1", "http://endpoint2"],
2: ["http://endpoint3", "http://endpoint4"],
}
assert get_default_rpc_endpoints() == expected_result
# Mock a failed response
mock_get.return_value.status_code = 500
assert get_default_rpc_endpoints() == {}
def test_get_healthy_default_rpc_endpoints(mocker):
mock_get_endpoints = mocker.patch(
"nucypher.blockchain.eth.utils.get_default_rpc_endpoints"
)
mock_get_endpoints.return_value = {
1: ["http://endpoint1", "http://endpoint2"],
2: ["http://endpoint3", "http://endpoint4"],
}
mock_health_check = mocker.patch(
"nucypher.blockchain.eth.utils.rpc_endpoint_health_check"
)
mock_health_check.side_effect = (
lambda endpoint: endpoint == "http://endpoint1"
or endpoint == "http://endpoint3"
)
# Test chain ID 1
healthy_endpoints = get_healthy_default_rpc_endpoints(1)
assert healthy_endpoints == ["http://endpoint1"]
# Test chain ID 2
healthy_endpoints = get_healthy_default_rpc_endpoints(2)
assert healthy_endpoints == ["http://endpoint3"]
# Test chain ID with no healthy endpoints
healthy_endpoints = get_healthy_default_rpc_endpoints(3)
assert healthy_endpoints == []

View File

@ -15,11 +15,11 @@ CHAIN_ID = 11155111 # pretend to be sepolia
@pytest.mark.parametrize("chain_id_return_value", [hex(CHAIN_ID), CHAIN_ID])
def test_cached_chain_id(mocker, chain_id_return_value):
web3_mock = mocker.MagicMock()
mock_client = EthereumClient(w3=web3_mock)
chain_id_property_mock = PropertyMock(return_value=chain_id_return_value)
type(web3_mock.eth).chain_id = chain_id_property_mock
mock_client = EthereumClient(w3=web3_mock)
assert mock_client.chain_id == CHAIN_ID
chain_id_property_mock.assert_called_once()