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