mirror of https://github.com/nucypher/nucypher.git
commit
42c3ac528e
|
@ -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+))?
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -27,4 +27,4 @@ jobs:
|
|||
pip install .
|
||||
|
||||
- name: Lint with Ruff
|
||||
run: ruff --output-format=github nucypher
|
||||
run: ruff check --output-format=github nucypher
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
version: '3'
|
||||
|
||||
services:
|
||||
nucypher:
|
||||
image: nucypher:latest
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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!"""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
# noinspection Mypy
|
||||
|
||||
|
||||
|
||||
import os
|
||||
|
||||
import click
|
||||
|
|
|
@ -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})"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
class FerveoKeyMismatch(Exception):
|
||||
"""
|
||||
Raised when a local ferveo public key does not match the
|
||||
public key published to the Coordinator.
|
||||
"""
|
|
@ -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:
|
||||
|
|
|
@ -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}"
|
||||
)
|
|
@ -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,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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 = ""
|
||||
|
|
|
@ -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
|
File diff suppressed because it is too large
Load Diff
|
@ -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"]
|
||||
|
|
36
releases.rst
36
releases.rst
|
@ -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)
|
||||
-------------------
|
||||
|
||||
|
|
201
requirements.txt
201
requirements.txt
|
@ -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"
|
||||
|
|
|
@ -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
|
|
@ -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()
|
|
@ -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
|
||||
|
|
@ -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()
|
6
setup.py
6
setup.py
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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)
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
#
|
||||
|
|
|
@ -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]},
|
||||
)
|
||||
|
|
|
@ -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):
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
)
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 == []
|
|
@ -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()
|
||||
|
||||
|
|
Loading…
Reference in New Issue