mirror of https://github.com/nucypher/nucypher.git
commit
9a262f3ab2
|
@ -1,5 +1,5 @@
|
|||
[bumpversion]
|
||||
current_version = 7.4.1
|
||||
current_version = 7.5.0
|
||||
commit = True
|
||||
tag = False
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(-(?P<stage>[^.]*)\.(?P<devnum>\d+))?
|
||||
|
|
|
@ -11,6 +11,11 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
|
|
|
@ -15,6 +15,9 @@ jobs:
|
|||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install latest Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
|
|
|
@ -6,7 +6,7 @@ repos:
|
|||
|
||||
- id: tests
|
||||
name: Run Nucypher Unit Tests
|
||||
entry: scripts/hooks/run_unit_tests.sh
|
||||
entry: scripts/run_unit_tests.sh
|
||||
language: system
|
||||
types: [python]
|
||||
stages: [push] # required additional setup: pre-commit install && pre-commit install -t pre-push
|
||||
|
|
|
@ -16,7 +16,9 @@ secrets management and dynamic access control.*
|
|||
TACo is end-to-end encrypted data sharing and communication, without the requirement of
|
||||
trusting a centralized authority, who might unilaterally deny service or even decrypt private user data. It is the only
|
||||
access control layer available to Web3 developers that can offer a decentralized service, through a live,
|
||||
well-collateralized and battle-tested network. See more here: [https://docs.threshold.network/applications/threshold-access-control](https://docs.threshold.network/applications/threshold-access-control)
|
||||
well-collateralized and battle-tested network.
|
||||
|
||||
See more in the [TACo docs](https://docs.taco.build/).
|
||||
|
||||
# Getting Involved
|
||||
|
||||
|
|
|
@ -13,6 +13,6 @@ We are happy to work together to use a more secure medium, such as Signal.
|
|||
Email security@nucypher.com and we will coordinate a communication channel that we're both comfortable with.
|
||||
|
||||
A great place to begin your research is by working on our testnet.
|
||||
Please see our [documentation](https://docs.threshold.network) to get started.
|
||||
Please see our [documentation](https://docs.taco.build) to get started.
|
||||
We ask that you please respect network machines and their owners.
|
||||
If you find a vulnerability that you suspect has given you access to a machine against the owner's permission, stop what you're doing and immediately email security@nucypher.com.
|
||||
|
|
|
@ -1,185 +1,183 @@
|
|||
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.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"
|
||||
abnf==2.3.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
aiohappyeyeballs==2.6.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
aiohttp==3.11.14 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
aiosignal==1.3.2 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
annotated-types==0.7.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
ape-solidity==0.8.5 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
appdirs==1.4.4 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
asttokens==3.0.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
async-timeout==5.0.1 ; python_version >= "3.9" and python_version < "3.11" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
attrs==25.3.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
atxm==0.5.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
autobahn==24.4.2 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
automat==24.8.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
base58==1.0.3 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
bitarray==3.2.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
blinker==1.9.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
bytestring-splitter==2.4.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
cached-property==2.0.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
certifi==2025.1.31 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
cffi==1.17.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
cfgv==3.4.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
charset-normalizer==3.4.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
ckzg==1.0.2 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
click==8.1.8 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
colorama==0.4.6 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
constant-sorrow==0.1.0a9 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
constantly==23.10.4 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
coverage==7.7.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
cryptography==43.0.3 ; python_version >= "3.9" and python_version < "3.11" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
cryptography==44.0.2 ; python_version >= "3.11" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
cytoolz==1.0.1 ; python_version >= "3.9" and python_version < "4" and implementation_name == "cpython"
|
||||
dataclassy==0.11.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
dateparser==1.2.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
decorator==5.2.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
distlib==0.3.9 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
eip712==0.2.11 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
eth-abi==5.2.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
eth-account==0.11.3 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
eth-ape==0.8.12 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
eth-bloom==3.1.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
eth-hash==0.7.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
eth-keyfile==0.9.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
eth-keys==0.6.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
eth-pydantic-types==0.1.3 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
eth-rlp==1.0.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
eth-tester==0.11.0b2 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
eth-typing==3.5.2 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
eth-utils==2.3.2 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
ethpm-types==0.6.24 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
eval-type-backport==0.2.2 ; python_version == "3.9" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
evm-trace==0.2.4 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
evmchains==0.0.13 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
exceptiongroup==1.2.2 ; python_version >= "3.9" and python_version < "3.11" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
executing==2.2.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
filelock==3.18.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
flask==3.1.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
frozenlist==1.5.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
greenlet==3.1.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
hendrix==5.0.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
hexbytes==0.3.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
humanize==4.12.2 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
hyperlink==21.0.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
identify==2.6.9 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
idna==3.10 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
ijson==3.3.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
importlib-metadata==8.6.1 ; python_version == "3.9" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
incremental==24.7.2 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
iniconfig==2.1.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
ipython==8.18.1 ; python_version >= "3.9" and python_version < "3.11" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
ipython==8.34.0 ; python_version >= "3.11" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
itsdangerous==2.2.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
jedi==0.19.2 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
jinja2==3.1.6 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
jsonpath-ng==1.7.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
jsonschema-specifications==2024.10.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
jsonschema==4.23.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
lazyasd==0.1.4 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
lru-dict==1.2.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
mako==1.3.9 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
markdown-it-py==3.0.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
markupsafe==3.0.2 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
marshmallow==3.26.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
matplotlib-inline==0.1.7 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
maya==0.6.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
mdurl==0.1.2 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
mnemonic==0.21 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
morphys==1.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
msgpack-python==0.5.6 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
msgspec==0.19.0 ; python_version >= "3.9" and python_version < "3.13" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
multidict==6.2.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
nodeenv==1.9.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
nucypher-core==0.13.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
numpy==1.26.4 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
packaging==23.2 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
pandas==2.2.3 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
parsimonious==0.10.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
parso==0.8.4 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
pendulum==3.0.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
pexpect==4.9.0 ; python_version >= "3.9" and python_version < "4" and (sys_platform != "win32" and sys_platform != "emscripten" or python_version < "3.11") and sys_platform != "win32" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
platformdirs==4.3.7 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
pluggy==1.5.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
ply==3.11 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
pre-commit==2.21.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
prometheus-client==0.21.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
prompt-toolkit==3.0.50 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
propcache==0.3.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
protobuf==6.30.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
ptyprocess==0.7.0 ; python_version >= "3.9" and python_version < "4" and (sys_platform != "win32" and sys_platform != "emscripten" or python_version < "3.11") and sys_platform != "win32" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
pure-eval==0.2.3 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
py-cid==0.3.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
py-ecc==7.0.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
py-evm==0.10.1b1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
py-geth==5.3.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
py-multibase==1.0.3 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
py-multicodec==0.2.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
py-multihash==0.2.3 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
py-solc-x==2.0.3 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
pyasn1-modules==0.4.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
pyasn1==0.6.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
pychalk @ git+https://github.com/nucypher/pychalk.git@b1a5c7562f9788f9c8722216b32019eeca281b69 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
pycparser==2.22 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
pycryptodome==3.22.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
pydantic-core==2.27.2 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
pydantic-settings==2.8.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
pydantic==2.10.6 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
pygments==2.19.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
pyjwt==2.10.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
pynacl==1.5.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
pyopenssl==25.0.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
pytest-cov==6.0.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
pytest-mock==3.14.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
pytest-timeout==2.3.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
pytest-twisted==1.14.3 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
pytest==8.3.5 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
python-baseconv==1.2.2 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
python-dateutil==2.9.0.post0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
python-dotenv==1.0.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
python-statemachine==2.3.4 ; python_version >= "3.9" and python_version < "4"
|
||||
pytz==2025.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
pyunormalize==16.0.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
pywin32==310 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy") and platform_system == "Windows"
|
||||
pyyaml==6.0.2 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
referencing==0.36.2 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
regex==2024.11.6 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
requests==2.32.3 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
rich==13.9.4 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
rlp==4.1.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
rpds-py==0.23.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
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.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"
|
||||
semantic-version==2.10.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
service-identity==24.2.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
setuptools==78.0.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
siwe==4.2.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
six==1.17.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
snaptime @ git+https://github.com/nucypher/snaptime.git@7edf0f861198b3689ccf82602e3f7b0a24acf6d5 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
sortedcontainers==2.4.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
sqlalchemy==2.0.39 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
stack-data==0.6.3 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
tabulate==0.9.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
time-machine==2.16.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
toml==0.10.2 ; python_version >= "3.9" and python_version < "3.11" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
tomli==2.2.1 ; python_version >= "3.9" and python_version < "3.11" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
toolz==1.0.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
tqdm==4.67.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
traitlets==5.14.3 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
trie==3.1.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
twisted==24.11.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
txaio==23.1.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
types-requests==2.32.0.20250306 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
typing-extensions==4.12.2 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
tzdata==2025.2 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
tzlocal==5.3.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
urllib3==2.3.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
varint==1.0.2 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
virtualenv==20.29.3 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
watchdog==3.0.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
wcwidth==0.2.13 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
web3==6.20.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
websockets==15.0.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
werkzeug==3.1.3 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
yarl==1.18.3 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
zipp==3.21.0 ; python_version == "3.9" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
zope-interface==7.2 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
|
|
|
@ -39,7 +39,7 @@ coordinator_agent = CoordinatorAgent(
|
|||
blockchain_endpoint=polygon_endpoint,
|
||||
registry=registry,
|
||||
)
|
||||
ritual_id = 0 # got this from a side channel
|
||||
ritual_id = 27 # got this from a side channel
|
||||
ritual = coordinator_agent.get_ritual(ritual_id)
|
||||
|
||||
# known authorized encryptor for ritual 3
|
||||
|
|
|
@ -40,7 +40,7 @@ coordinator_agent = CoordinatorAgent(
|
|||
blockchain_endpoint=polygon_endpoint,
|
||||
registry=registry,
|
||||
)
|
||||
ritual_id = 0 # got this from a side channel
|
||||
ritual_id = 27 # got this from a side channel
|
||||
ritual = coordinator_agent.get_ritual(ritual_id)
|
||||
|
||||
# known authorized encryptor for ritual 3
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Support for executing multiple conditions sequentially, where the outcome of one condition can be used as input for another.
|
|
@ -0,0 +1 @@
|
|||
Support for offchain JSON endpoint condition expression and evaluation
|
|
@ -0,0 +1 @@
|
|||
Expands recovery CLI to include audit and keystore identification features
|
|
@ -0,0 +1,2 @@
|
|||
Condition that allows for if-then-else branching based on underlying conditions i.e. IF ``CONDITION A`` THEN ``CONDITION B`` ELSE ``CONDITION_C``.
|
||||
The ELSE component can either be a Condition or a boolean value.
|
|
@ -0,0 +1 @@
|
|||
Enable support for Bearer authorization tokens (e.g., OAuth, JWT) within HTTP GET requests for ``JsonApiCondition``.
|
|
@ -0,0 +1 @@
|
|||
Enhance threshold decryption request efficiency by prioritizing nodes in the cohort with lower communication latency.
|
|
@ -0,0 +1 @@
|
|||
Added plumbing to support EVM condition evaluation on "any" (major) EVM chain outside of Ethereum and Polygon - only enabled on ``lynx`` testnet for now.
|
|
@ -0,0 +1 @@
|
|||
Support for conditions based on verification of JWT tokens.
|
|
@ -0,0 +1 @@
|
|||
Support for conditions based on APIs provided by off-chain JSON RPC 2.0 endpoints.
|
|
@ -0,0 +1 @@
|
|||
Add support for EIP1271 signature verification for smart contract wallets.
|
|
@ -0,0 +1 @@
|
|||
Allow BigInt values from ``taco-web`` typescript library to be provided as strings.
|
|
@ -0,0 +1 @@
|
|||
Introduce necessary changes to adapt agents methods to breaking changes in Coordinator contract. Previous methods are now deprecated from the API.
|
|
@ -16,7 +16,7 @@ __url__ = "https://github.com/nucypher/nucypher"
|
|||
|
||||
__summary__ = "A threshold access control application to empower privacy in decentralized systems."
|
||||
|
||||
__version__ = "7.4.1"
|
||||
__version__ = "7.5.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, Union
|
||||
from typing import Dict, List, Optional, Union
|
||||
|
||||
import maya
|
||||
from atxm.exceptions import InsufficientFunds
|
||||
|
@ -65,8 +65,10 @@ from nucypher.crypto.powers import (
|
|||
TransactingPower,
|
||||
)
|
||||
from nucypher.datastore.dkg import DKGStorage
|
||||
from nucypher.policy.conditions.evm import _CONDITION_CHAINS
|
||||
from nucypher.policy.conditions.utils import evaluate_condition_lingo
|
||||
from nucypher.policy.conditions.utils import (
|
||||
ConditionProviderManager,
|
||||
evaluate_condition_lingo,
|
||||
)
|
||||
from nucypher.policy.payment import ContractPayment
|
||||
from nucypher.types import PhaseId
|
||||
from nucypher.utilities.emitters import StdoutEmitter
|
||||
|
@ -248,7 +250,7 @@ class Operator(BaseActor):
|
|||
ThresholdRequestDecryptingPower
|
||||
) # used for secure decryption request channel
|
||||
|
||||
self.condition_providers = self.connect_condition_providers(
|
||||
self.condition_provider_manager = self.get_condition_provider_manager(
|
||||
condition_blockchain_endpoints
|
||||
)
|
||||
|
||||
|
@ -265,80 +267,79 @@ class Operator(BaseActor):
|
|||
)
|
||||
return receipt
|
||||
|
||||
@staticmethod
|
||||
def _is_permitted_condition_chain(chain_id: int) -> bool:
|
||||
return int(chain_id) in [int(cid) for cid in _CONDITION_CHAINS.keys()]
|
||||
|
||||
@staticmethod
|
||||
def _make_condition_provider(uri: str) -> HTTPProvider:
|
||||
provider = HTTPProvider(endpoint_uri=uri)
|
||||
return provider
|
||||
|
||||
def connect_condition_providers(
|
||||
self, endpoints: Dict[int, List[str]]
|
||||
) -> DefaultDict[int, List[HTTPProvider]]:
|
||||
providers = defaultdict(list) # use list to maintain order
|
||||
def get_condition_provider_manager(
|
||||
self, operator_configured_endpoints: Dict[int, List[str]]
|
||||
) -> ConditionProviderManager:
|
||||
|
||||
# check that we have endpoints for all condition chains
|
||||
if set(self.domain.condition_chain_ids) != set(endpoints):
|
||||
# check that we have mandatory user configured endpoints
|
||||
mandatory_configured_chains = {
|
||||
self.domain.eth_chain.id,
|
||||
self.domain.polygon_chain.id,
|
||||
}
|
||||
if mandatory_configured_chains != set(operator_configured_endpoints):
|
||||
raise self.ActorError(
|
||||
f"Missing blockchain endpoints for chains: "
|
||||
f"{set(self.domain.condition_chain_ids) - set(endpoints)}"
|
||||
f"Operator-configured condition endpoints for chains don't match mandatory chains: "
|
||||
f"{set(operator_configured_endpoints)} vs expected {mandatory_configured_chains}"
|
||||
)
|
||||
|
||||
providers = defaultdict(list) # use list to maintain order
|
||||
|
||||
# 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."
|
||||
# Operator-configured endpoints for chains
|
||||
for chain_id, chain_rpc_endpoints in operator_configured_endpoints.items():
|
||||
if not chain_rpc_endpoints:
|
||||
raise self.ActorError(
|
||||
f"Operator-configured condition endpoint is missing for the required chain {chain_id}"
|
||||
)
|
||||
|
||||
# connect to each endpoint and check that they are on the correct chain
|
||||
for uri in endpoints:
|
||||
for uri in chain_rpc_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"
|
||||
f"Operator-configured condition endpoint {uri} is duplicated 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}"
|
||||
f"Operator-configured RPC condition endpoint {uri} does not belong to chain {chain_id}"
|
||||
)
|
||||
healthy = rpc_endpoint_health_check(endpoint=uri)
|
||||
if not healthy:
|
||||
self.log.warn(
|
||||
f"user-supplied condition RPC endpoint {uri} is unhealthy"
|
||||
f"Operator-configured RPC condition 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:
|
||||
default_endpoints = get_healthy_default_rpc_endpoints(self.domain)
|
||||
for chain_id, chain_rpc_endpoints in default_endpoints.items():
|
||||
# randomize list so that the same fallback RPC endpoints aren't always used by all nodes
|
||||
random.shuffle(chain_rpc_endpoints)
|
||||
for uri in chain_rpc_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"
|
||||
f"Fallback blockchain endpoint, {uri}, is duplicated 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 {sum(len(v) for v in providers.values())} RPC endpoints for condition "
|
||||
f"checking on chain IDs {humanized_chain_ids}"
|
||||
f"checking on chain IDs {providers.keys()}"
|
||||
)
|
||||
|
||||
return providers
|
||||
return ConditionProviderManager(providers=providers)
|
||||
|
||||
def _resolve_ritual(self, ritual_id: int) -> Coordinator.Ritual:
|
||||
if not self.coordinator_agent.is_ritual_active(ritual_id=ritual_id):
|
||||
|
@ -749,7 +750,7 @@ class Operator(BaseActor):
|
|||
|
||||
return async_tx
|
||||
|
||||
def derive_decryption_share(
|
||||
def produce_decryption_share(
|
||||
self,
|
||||
ritual_id: int,
|
||||
ciphertext_header: CiphertextHeader,
|
||||
|
@ -761,7 +762,7 @@ class Operator(BaseActor):
|
|||
aggregated_transcript = AggregatedTranscript.from_bytes(
|
||||
bytes(ritual.aggregated_transcript)
|
||||
)
|
||||
decryption_share = self.ritual_power.derive_decryption_share(
|
||||
decryption_share = self.ritual_power.produce_decryption_share(
|
||||
nodes=validators,
|
||||
threshold=ritual.threshold,
|
||||
shares=ritual.shares,
|
||||
|
@ -809,10 +810,10 @@ class Operator(BaseActor):
|
|||
f"Node not part of ritual {decryption_request.ritual_id}",
|
||||
)
|
||||
|
||||
def _verify_ciphertext_authorization(
|
||||
def _verify_encryption_authorization(
|
||||
self, decryption_request: ThresholdDecryptionRequest
|
||||
) -> None:
|
||||
"""check that the ciphertext is authorized for this ritual"""
|
||||
"""Check that the encryption is authorized for this ritual"""
|
||||
ciphertext_header = decryption_request.ciphertext_header
|
||||
authorization = decryption_request.acp.authorization
|
||||
if not self.coordinator_agent.is_encryption_authorized(
|
||||
|
@ -847,7 +848,7 @@ class Operator(BaseActor):
|
|||
evaluate_condition_lingo(
|
||||
condition_lingo=condition_lingo,
|
||||
context=context,
|
||||
providers=self.condition_providers,
|
||||
providers=self.condition_provider_manager,
|
||||
)
|
||||
|
||||
def _verify_decryption_request_authorization(
|
||||
|
@ -855,10 +856,10 @@ class Operator(BaseActor):
|
|||
) -> None:
|
||||
"""check that the decryption request is authorized for this ritual"""
|
||||
self._verify_active_ritual(decryption_request)
|
||||
self._verify_ciphertext_authorization(decryption_request)
|
||||
self._verify_encryption_authorization(decryption_request)
|
||||
self._evaluate_conditions(decryption_request)
|
||||
|
||||
def _derive_decryption_share_for_request(
|
||||
def _produce_decryption_share_for_request(
|
||||
self,
|
||||
decryption_request: ThresholdDecryptionRequest,
|
||||
) -> Union[DecryptionShareSimple, DecryptionSharePrecomputed]:
|
||||
|
@ -867,7 +868,7 @@ class Operator(BaseActor):
|
|||
decryption_request=decryption_request
|
||||
)
|
||||
try:
|
||||
decryption_share = self.derive_decryption_share(
|
||||
decryption_share = self.produce_decryption_share(
|
||||
ritual_id=decryption_request.ritual_id,
|
||||
ciphertext_header=decryption_request.ciphertext_header,
|
||||
aad=decryption_request.acp.aad(),
|
||||
|
|
|
@ -593,6 +593,8 @@ class CoordinatorAgent(EthereumContractAgent):
|
|||
@contract_api(CONTRACT_CALL)
|
||||
def __rituals(self, ritual_id: int) -> Coordinator.Ritual:
|
||||
result = self.contract.functions.rituals(int(ritual_id)).call()
|
||||
# legacy rituals do not have a fee model entry - so None is appropriate
|
||||
fee_model = ChecksumAddress(result[12]) if len(result) >= 13 else None
|
||||
ritual = Coordinator.Ritual(
|
||||
id=ritual_id,
|
||||
initiator=ChecksumAddress(result[0]),
|
||||
|
@ -605,9 +607,10 @@ class CoordinatorAgent(EthereumContractAgent):
|
|||
threshold=result[7],
|
||||
aggregation_mismatch=result[8],
|
||||
access_controller=ChecksumAddress(result[9]),
|
||||
aggregated_transcript=bytes(result[11]),
|
||||
public_key=Ferveo.G1Point(result[10][0], result[10][1]),
|
||||
aggregated_transcript=bytes(result[11]),
|
||||
participants=[], # solidity does not return sub-structs
|
||||
fee_model=fee_model,
|
||||
)
|
||||
return ritual
|
||||
|
||||
|
@ -689,6 +692,7 @@ class CoordinatorAgent(EthereumContractAgent):
|
|||
result = self.contract.functions.numberOfRituals().call()
|
||||
return result
|
||||
|
||||
# TODO: Deprecate this method. Use IEncryptionAuthorizer contract directly - #3349
|
||||
@contract_api(CONTRACT_CALL)
|
||||
def is_encryption_authorized(
|
||||
self, ritual_id: int, evidence: bytes, ciphertext_header: bytes
|
||||
|
@ -697,14 +701,55 @@ class CoordinatorAgent(EthereumContractAgent):
|
|||
This contract read is relayed through coordinator to the access controller
|
||||
contract associated with a given ritual.
|
||||
"""
|
||||
result = self.contract.functions.isEncryptionAuthorized(
|
||||
ritual_id, evidence, ciphertext_header
|
||||
|
||||
# TODO this makes the test pass more easily - find a better way
|
||||
# for this access controller calling logic #3503
|
||||
ritual = self.get_ritual(ritual_id=ritual_id)
|
||||
abi = """[
|
||||
{
|
||||
"type": "function",
|
||||
"name": "isAuthorized",
|
||||
"stateMutability": "view",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "ritualId",
|
||||
"type": "uint32",
|
||||
"internalType": "uint32"
|
||||
},
|
||||
{
|
||||
"name": "evidence",
|
||||
"type": "bytes",
|
||||
"internalType": "bytes"
|
||||
},
|
||||
{
|
||||
"name": "ciphertextHeader",
|
||||
"type": "bytes",
|
||||
"internalType": "bytes"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "bool",
|
||||
"internalType": "bool"
|
||||
}
|
||||
]
|
||||
}
|
||||
]"""
|
||||
encryption_authorizer = self.blockchain.w3.eth.contract(
|
||||
address=ritual.access_controller, abi=abi
|
||||
)
|
||||
is_authorized = encryption_authorizer.functions.isAuthorized(
|
||||
ritual_id,
|
||||
evidence,
|
||||
ciphertext_header,
|
||||
).call()
|
||||
return result
|
||||
|
||||
return is_authorized
|
||||
|
||||
@contract_api(CONTRACT_CALL)
|
||||
def is_provider_public_key_set(self, staking_provider: ChecksumAddress) -> bool:
|
||||
result = self.contract.functions.isProviderPublicKeySet(staking_provider).call()
|
||||
result = self.contract.functions.isProviderKeySet(staking_provider).call()
|
||||
return result
|
||||
|
||||
@contract_api(TRANSACTION)
|
||||
|
@ -722,6 +767,7 @@ class CoordinatorAgent(EthereumContractAgent):
|
|||
@contract_api(TRANSACTION)
|
||||
def initiate_ritual(
|
||||
self,
|
||||
fee_model: ChecksumAddress,
|
||||
providers: List[ChecksumAddress],
|
||||
authority: ChecksumAddress,
|
||||
duration: int,
|
||||
|
@ -729,7 +775,7 @@ class CoordinatorAgent(EthereumContractAgent):
|
|||
transacting_power: TransactingPower,
|
||||
) -> TxReceipt:
|
||||
contract_function: ContractFunction = self.contract.functions.initiateRitual(
|
||||
providers, authority, duration, access_controller
|
||||
fee_model, providers, authority, duration, access_controller
|
||||
)
|
||||
receipt = self.blockchain.send_transaction(
|
||||
contract_function=contract_function, transacting_power=transacting_power
|
||||
|
@ -744,7 +790,8 @@ class CoordinatorAgent(EthereumContractAgent):
|
|||
transacting_power: TransactingPower,
|
||||
async_tx_hooks: BlockchainInterface.AsyncTxHooks,
|
||||
) -> AsyncTx:
|
||||
contract_function: ContractFunction = self.contract.functions.postTranscript(
|
||||
# See sprints/#145
|
||||
contract_function: ContractFunction = self.contract.functions.publishTranscript(
|
||||
ritualId=ritual_id, transcript=bytes(transcript)
|
||||
)
|
||||
async_tx = self.blockchain.send_async_transaction(
|
||||
|
@ -780,15 +827,6 @@ class CoordinatorAgent(EthereumContractAgent):
|
|||
)
|
||||
return async_tx
|
||||
|
||||
@contract_api(CONTRACT_CALL)
|
||||
def get_ritual_initiation_cost(
|
||||
self, providers: List[ChecksumAddress], duration: int
|
||||
) -> Wei:
|
||||
result = self.contract.functions.getRitualInitiationCost(
|
||||
providers, duration
|
||||
).call()
|
||||
return Wei(result)
|
||||
|
||||
@contract_api(CONTRACT_CALL)
|
||||
def get_ritual_id_from_public_key(self, public_key: DkgPublicKey) -> int:
|
||||
g1_point = Ferveo.G1Point.from_dkg_public_key(public_key)
|
||||
|
|
|
@ -54,4 +54,6 @@ POA_CHAINS = {
|
|||
80002, # "Polygon/Amoy"
|
||||
}
|
||||
|
||||
CHAINLIST_URL = "https://raw.githubusercontent.com/nucypher/chainlist/main/rpc.json"
|
||||
CHAINLIST_URL_TEMPLATE = (
|
||||
"https://raw.githubusercontent.com/nucypher/chainlist/main/{domain}.json"
|
||||
)
|
||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -2,7 +2,7 @@
|
|||
|
||||
import functools
|
||||
import inspect
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from typing import Callable, Optional, TypeVar, Union
|
||||
|
||||
import eth_utils
|
||||
|
@ -111,7 +111,7 @@ def save_receipt(actor_method) -> Callable: # TODO: rename to "save_result"?
|
|||
@functools.wraps(actor_method)
|
||||
def wrapped(self, *args, **kwargs) -> dict:
|
||||
receipt_or_txhash = actor_method(self, *args, **kwargs)
|
||||
self._saved_receipts.append((datetime.utcnow(), receipt_or_txhash))
|
||||
self._saved_receipts.append((datetime.now(timezone.utc), receipt_or_txhash))
|
||||
return receipt_or_txhash
|
||||
return wrapped
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
from enum import Enum
|
||||
from typing import Any, Dict, NamedTuple, Tuple
|
||||
|
||||
from cytoolz.functoolz import memoize
|
||||
from functools import cache
|
||||
from typing import Any, Dict, NamedTuple
|
||||
|
||||
|
||||
class UnrecognizedTacoDomain(Exception):
|
||||
|
@ -29,12 +28,10 @@ class TACoDomain:
|
|||
name: str,
|
||||
eth_chain: EthChain,
|
||||
polygon_chain: PolygonChain,
|
||||
condition_chains: Tuple[ChainInfo, ...],
|
||||
):
|
||||
self.name = name
|
||||
self.eth_chain = eth_chain
|
||||
self.polygon_chain = polygon_chain
|
||||
self.condition_chains = condition_chains
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<TACoDomain {self.name}>"
|
||||
|
@ -43,9 +40,7 @@ class TACoDomain:
|
|||
return self.name
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(
|
||||
(self.name, self.eth_chain, self.polygon_chain, self.condition_chains)
|
||||
)
|
||||
return hash((self.name, self.eth_chain, self.polygon_chain))
|
||||
|
||||
def __bytes__(self) -> bytes:
|
||||
return self.name.encode()
|
||||
|
@ -57,7 +52,6 @@ class TACoDomain:
|
|||
self.name == other.name
|
||||
and self.eth_chain == other.eth_chain
|
||||
and self.polygon_chain == other.polygon_chain
|
||||
and self.condition_chains == other.condition_chains
|
||||
)
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
|
@ -67,36 +61,24 @@ class TACoDomain:
|
|||
def is_testnet(self) -> bool:
|
||||
return self.eth_chain != EthChain.MAINNET
|
||||
|
||||
@property
|
||||
def condition_chain_ids(self) -> set:
|
||||
return set(chain.id for chain in self.condition_chains)
|
||||
|
||||
|
||||
|
||||
MAINNET = TACoDomain(
|
||||
name="mainnet",
|
||||
eth_chain=EthChain.MAINNET,
|
||||
polygon_chain=PolygonChain.MAINNET,
|
||||
condition_chains=(EthChain.MAINNET, PolygonChain.MAINNET),
|
||||
)
|
||||
|
||||
LYNX = TACoDomain(
|
||||
name="lynx",
|
||||
eth_chain=EthChain.SEPOLIA,
|
||||
polygon_chain=PolygonChain.AMOY,
|
||||
condition_chains=(
|
||||
EthChain.MAINNET,
|
||||
EthChain.SEPOLIA,
|
||||
PolygonChain.AMOY,
|
||||
PolygonChain.MAINNET,
|
||||
),
|
||||
)
|
||||
|
||||
TAPIR = TACoDomain(
|
||||
name="tapir",
|
||||
eth_chain=EthChain.SEPOLIA,
|
||||
polygon_chain=PolygonChain.AMOY,
|
||||
condition_chains=(EthChain.SEPOLIA, PolygonChain.AMOY),
|
||||
)
|
||||
|
||||
|
||||
|
@ -107,7 +89,7 @@ SUPPORTED_DOMAINS: Dict[str, TACoDomain] = {
|
|||
}
|
||||
|
||||
|
||||
@memoize
|
||||
@cache
|
||||
def get_domain(d: Any) -> TACoDomain:
|
||||
if not isinstance(d, str):
|
||||
raise TypeError(f"domain must be a string, not {type(d)}")
|
||||
|
|
|
@ -121,6 +121,7 @@ class Coordinator:
|
|||
init_timestamp: int
|
||||
end_timestamp: int
|
||||
threshold: int
|
||||
fee_model: ChecksumAddress = None
|
||||
total_transcripts: int = 0
|
||||
total_aggregations: int = 0
|
||||
public_key: Ferveo.G1Point = None
|
||||
|
@ -128,6 +129,7 @@ class Coordinator:
|
|||
aggregated_transcript: bytes = bytes()
|
||||
participants: List = field(default_factory=list)
|
||||
|
||||
|
||||
@property
|
||||
def providers(self):
|
||||
return [p.provider for p in self.participants]
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import time
|
||||
from decimal import Decimal
|
||||
from functools import cache
|
||||
from typing import Dict, List, Union
|
||||
|
||||
import requests
|
||||
|
@ -9,7 +10,8 @@ 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.blockchain.eth.constants import CHAINLIST_URL_TEMPLATE
|
||||
from nucypher.blockchain.eth.domains import TACoDomain
|
||||
from nucypher.utilities.logging import Logger
|
||||
|
||||
LOGGER = Logger("utility")
|
||||
|
@ -133,17 +135,17 @@ def rpc_endpoint_health_check(endpoint: str, max_drift_seconds: int = 60) -> boo
|
|||
return True # finally!
|
||||
|
||||
|
||||
def get_default_rpc_endpoints() -> Dict[int, List[str]]:
|
||||
@cache
|
||||
def get_default_rpc_endpoints(domain: TACoDomain) -> Dict[int, List[str]]:
|
||||
"""
|
||||
Fetches the default RPC endpoints for various chains
|
||||
For a given domain, 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}"
|
||||
)
|
||||
url = CHAINLIST_URL_TEMPLATE.format(domain=domain.name)
|
||||
LOGGER.debug(f"Fetching default RPC endpoints from remote chainlist {url}")
|
||||
|
||||
try:
|
||||
response = requests.get(CHAINLIST_URL)
|
||||
response = requests.get(url)
|
||||
except RequestException:
|
||||
LOGGER.warn("Failed to fetch default RPC endpoints: network error")
|
||||
return {}
|
||||
|
@ -159,23 +161,21 @@ def get_default_rpc_endpoints() -> Dict[int, List[str]]:
|
|||
return {}
|
||||
|
||||
|
||||
def get_healthy_default_rpc_endpoints(chain_id: int) -> List[str]:
|
||||
"""Returns a list of healthy RPC endpoints for a given chain ID."""
|
||||
def get_healthy_default_rpc_endpoints(domain: TACoDomain) -> Dict[int, List[str]]:
|
||||
"""Returns a mapping of chain id to healthy RPC endpoints for a given domain."""
|
||||
endpoints = get_default_rpc_endpoints(domain)
|
||||
|
||||
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}"
|
||||
)
|
||||
if not domain.is_testnet:
|
||||
# iterate over all chains and filter out unhealthy endpoints
|
||||
healthy = {
|
||||
chain_id: [
|
||||
endpoint
|
||||
for endpoint in endpoints[chain_id]
|
||||
if rpc_endpoint_health_check(endpoint)
|
||||
]
|
||||
for chain_id in endpoints
|
||||
}
|
||||
else:
|
||||
healthy = endpoints
|
||||
|
||||
return healthy
|
||||
|
|
|
@ -184,7 +184,7 @@ class DKGOmniscientDecryptionClient(ThresholdDecryptionClient):
|
|||
self._learner._dkg_insight.validator_keypairs,
|
||||
):
|
||||
# get decryption fragments/shares
|
||||
decryption_share = dkg.derive_decryption_share(
|
||||
decryption_share = dkg.produce_decryption_share(
|
||||
ritual_id=ritual_id,
|
||||
me=validator,
|
||||
shares=self._learner._dkg_insight.shares_num,
|
||||
|
|
|
@ -387,6 +387,8 @@ class Alice(Character, actors.PolicyAuthor):
|
|||
|
||||
|
||||
class Bob(Character):
|
||||
_TRACK_NODE_LATENCY_STATS = True
|
||||
|
||||
banner = BOB_BANNER
|
||||
_default_dkg_variant = FerveoVariant.Simple
|
||||
_default_crypto_powerups = [SigningPower, DecryptingPower]
|
||||
|
@ -1272,7 +1274,9 @@ class Ursula(Teacher, Character, Operator):
|
|||
decryption_request = self.decrypt_threshold_decryption_request(
|
||||
encrypted_decryption_request
|
||||
)
|
||||
decryption_share = self._derive_decryption_share_for_request(decryption_request)
|
||||
decryption_share = self._produce_decryption_share_for_request(
|
||||
decryption_request
|
||||
)
|
||||
encrypted_response = self._encrypt_decryption_share(
|
||||
decryption_share=decryption_share,
|
||||
ritual_id=decryption_request.ritual_id,
|
||||
|
|
|
@ -11,7 +11,6 @@ from nucypher.cli.literature import (
|
|||
GENERIC_PASSWORD_PROMPT,
|
||||
PASSWORD_COLLECTION_NOTICE,
|
||||
)
|
||||
from nucypher.config.base import CharacterConfiguration
|
||||
from nucypher.config.constants import NUCYPHER_ENVVAR_KEYSTORE_PASSWORD
|
||||
from nucypher.crypto.keystore import _WORD_COUNT, Keystore
|
||||
from nucypher.utilities.emitters import StdoutEmitter
|
||||
|
@ -36,7 +35,7 @@ def get_client_password(checksum_address: str, envvar: str = None, confirm: bool
|
|||
return client_password
|
||||
|
||||
|
||||
def unlock_signer_account(config: CharacterConfiguration, json_ipc: bool) -> None:
|
||||
def unlock_signer_account(config, json_ipc: bool) -> None:
|
||||
|
||||
# TODO: Remove this block after deprecating 'operator_address'
|
||||
from nucypher.config.characters import UrsulaConfiguration
|
||||
|
@ -67,7 +66,11 @@ def get_nucypher_password(emitter, confirm: bool = False, envvar=NUCYPHER_ENVVAR
|
|||
return keystore_password
|
||||
|
||||
|
||||
def unlock_nucypher_keystore(emitter: StdoutEmitter, password: str, character_configuration: CharacterConfiguration) -> bool:
|
||||
def unlock_nucypher_keystore(
|
||||
emitter: StdoutEmitter,
|
||||
password: str,
|
||||
character_configuration,
|
||||
) -> bool:
|
||||
"""Unlocks a nucypher keystore and attaches it to the supplied configuration if successful."""
|
||||
emitter.message(DECRYPTING_CHARACTER_KEYSTORE.format(name=character_configuration.NAME.capitalize()), color='yellow')
|
||||
|
||||
|
@ -80,15 +83,27 @@ def unlock_nucypher_keystore(emitter: StdoutEmitter, password: str, character_co
|
|||
return True
|
||||
|
||||
|
||||
def recover_keystore(emitter) -> None:
|
||||
def collect_mnemonic(emitter: StdoutEmitter) -> str:
|
||||
"""Collect nucypher mnemonic seed words interactively."""
|
||||
while True:
|
||||
__words = click.prompt("Enter nucypher keystore seed words")
|
||||
word_count = len(__words.split())
|
||||
if word_count != _WORD_COUNT:
|
||||
emitter.message(
|
||||
f"Invalid mnemonic - Number of words must be {str(_WORD_COUNT)}, but only got {word_count}"
|
||||
)
|
||||
continue
|
||||
break
|
||||
return __words
|
||||
|
||||
|
||||
def recover_keystore(emitter) -> Keystore:
|
||||
emitter.message('This procedure will recover your nucypher keystore from mnemonic seed words. '
|
||||
'You will need to provide the entire mnemonic (space seperated) in the correct '
|
||||
'order and choose a new password.', color='cyan')
|
||||
click.confirm('Do you want to continue', abort=True)
|
||||
__words = click.prompt("Enter nucypher keystore seed words")
|
||||
word_count = len(__words.split())
|
||||
if word_count != _WORD_COUNT:
|
||||
emitter.message(f'Invalid mnemonic - Number of words must be {str(_WORD_COUNT)}, but only got {word_count}')
|
||||
__words = collect_mnemonic(emitter)
|
||||
__password = get_nucypher_password(emitter=emitter, confirm=True)
|
||||
keystore = Keystore.restore(words=__words, password=__password)
|
||||
emitter.message(f'Recovered nucypher keystore {keystore.id} to \n {keystore.keystore_path}', color='green')
|
||||
return keystore
|
||||
|
|
|
@ -187,3 +187,14 @@ def perform_startup_ip_check(emitter: StdoutEmitter, ursula: Ursula, force: bool
|
|||
raise click.Abort()
|
||||
else:
|
||||
emitter.message('✓ External IP matches configuration', 'green')
|
||||
|
||||
|
||||
def update_config_keystore_path(keystore_path: Path, config_file: Path = None) -> None:
|
||||
"""Update the ursula.json configuration file to use the provided keystore path."""
|
||||
keystore_path = str(keystore_path.resolve())
|
||||
with open(config_file, "r+") as f:
|
||||
ursula_config = json.load(f)
|
||||
ursula_config["keystore_path"] = keystore_path
|
||||
f.seek(0)
|
||||
json.dump(ursula_config, f, indent=2)
|
||||
f.truncate() # rest of the file truncated
|
||||
|
|
|
@ -3,6 +3,7 @@ from pathlib import Path
|
|||
import click
|
||||
|
||||
from nucypher.cli.actions.auth import (
|
||||
collect_mnemonic,
|
||||
get_client_password,
|
||||
get_nucypher_password,
|
||||
recover_keystore,
|
||||
|
@ -13,6 +14,7 @@ from nucypher.cli.actions.configure import (
|
|||
get_or_update_configuration,
|
||||
handle_missing_configuration_file,
|
||||
perform_startup_ip_check,
|
||||
update_config_keystore_path,
|
||||
)
|
||||
from nucypher.cli.actions.migrate import migrate
|
||||
from nucypher.cli.actions.select import (
|
||||
|
@ -49,14 +51,22 @@ from nucypher.cli.options import (
|
|||
option_teacher_uri,
|
||||
)
|
||||
from nucypher.cli.painting.help import paint_new_installation_help
|
||||
from nucypher.cli.types import EIP55_CHECKSUM_ADDRESS, NETWORK_PORT, OPERATOR_IP
|
||||
from nucypher.cli.types import (
|
||||
EIP55_CHECKSUM_ADDRESS,
|
||||
EXISTING_READABLE_FILE,
|
||||
NETWORK_PORT,
|
||||
OPERATOR_IP,
|
||||
)
|
||||
from nucypher.cli.utils import make_cli_character, setup_emitter
|
||||
from nucypher.config.characters import UrsulaConfiguration
|
||||
from nucypher.config.constants import (
|
||||
DEFAULT_CONFIG_FILEPATH,
|
||||
NUCYPHER_ENVVAR_OPERATOR_ETH_PASSWORD,
|
||||
TEMPORARY_DOMAIN_NAME,
|
||||
)
|
||||
from nucypher.crypto.keystore import Keystore
|
||||
from nucypher.crypto.powers import RitualisticPower
|
||||
from nucypher.utilities.emitters import StdoutEmitter
|
||||
from nucypher.utilities.prometheus.metrics import PrometheusMetricsConfig
|
||||
|
||||
|
||||
|
@ -154,7 +164,7 @@ class UrsulaConfigOptions:
|
|||
# TODO: Exit codes (not only for this, but for other exceptions)
|
||||
return click.get_current_context().exit(1)
|
||||
|
||||
def generate_config(self, emitter, config_root, force, key_material):
|
||||
def generate_config(self, emitter, config_root, force, key_material, with_mnemonic):
|
||||
|
||||
if self.dev:
|
||||
raise RuntimeError(
|
||||
|
@ -183,6 +193,7 @@ class UrsulaConfigOptions:
|
|||
return UrsulaConfiguration.generate(
|
||||
password=get_nucypher_password(emitter=emitter, confirm=True),
|
||||
key_material=bytes.fromhex(key_material) if key_material else None,
|
||||
with_mnemonic=with_mnemonic,
|
||||
config_root=config_root,
|
||||
rest_host=self.rest_host,
|
||||
rest_port=self.rest_port,
|
||||
|
@ -313,7 +324,14 @@ def ursula():
|
|||
@option_config_root
|
||||
@group_general_config
|
||||
@option_key_material
|
||||
def init(general_config, config_options, force, config_root, key_material):
|
||||
@click.option(
|
||||
"--with-mnemonic",
|
||||
help="Initialize with a mnemonic phrase instead of generating a new keypair from scratch",
|
||||
is_flag=True,
|
||||
)
|
||||
def init(
|
||||
general_config, config_options, force, config_root, key_material, with_mnemonic
|
||||
):
|
||||
"""Create a new Ursula node configuration."""
|
||||
emitter = setup_emitter(general_config, config_options.operator_address)
|
||||
_pre_launch_warnings(emitter, dev=None, force=force)
|
||||
|
@ -339,16 +357,14 @@ def init(general_config, config_options, force, config_root, key_material):
|
|||
)
|
||||
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 key_material and with_mnemonic:
|
||||
raise click.BadOptionUsage(
|
||||
"--key-material",
|
||||
message=click.style(
|
||||
"--key-material is incompatible with --with-mnemonic",
|
||||
fg="red",
|
||||
),
|
||||
)
|
||||
|
||||
if not config_options.eth_endpoint:
|
||||
raise click.BadOptionUsage(
|
||||
|
@ -365,13 +381,39 @@ def init(general_config, config_options, force, config_root, key_material):
|
|||
fg="red",
|
||||
),
|
||||
)
|
||||
|
||||
click.clear()
|
||||
if with_mnemonic:
|
||||
emitter.echo(
|
||||
"Hello Operator, welcome :-) \n\n"
|
||||
"You are about to initialize a new Ursula node configuration using an existing mnemonic phrase.\n"
|
||||
"Have your mnemonic phrase ready and ensure you are in a secure environment.\n"
|
||||
"Please follow the prompts.",
|
||||
color="cyan",
|
||||
)
|
||||
else:
|
||||
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.domain:
|
||||
config_options.domain = select_domain(
|
||||
emitter,
|
||||
message="Select TACo Domain",
|
||||
)
|
||||
|
||||
ursula_config = config_options.generate_config(
|
||||
emitter=emitter, config_root=config_root, force=force, key_material=key_material
|
||||
emitter=emitter,
|
||||
config_root=config_root,
|
||||
force=force,
|
||||
key_material=key_material,
|
||||
with_mnemonic=with_mnemonic,
|
||||
)
|
||||
filepath = ursula_config.to_configuration_file()
|
||||
paint_new_installation_help(
|
||||
|
@ -380,13 +422,107 @@ def init(general_config, config_options, force, config_root, key_material):
|
|||
|
||||
|
||||
@ursula.command()
|
||||
@group_config_options
|
||||
@group_general_config
|
||||
def recover(general_config, config_options):
|
||||
# TODO: Combine with work in PR #2682
|
||||
# TODO: Integrate regeneration of configuration files
|
||||
emitter = setup_emitter(general_config, config_options.operator_address)
|
||||
recover_keystore(emitter=emitter)
|
||||
@option_config_file
|
||||
@click.option(
|
||||
"--keystore-filepath",
|
||||
help="Path to keystore .priv file",
|
||||
type=EXISTING_READABLE_FILE,
|
||||
required=False,
|
||||
)
|
||||
@click.option(
|
||||
"--view-mnemonic",
|
||||
help="View mnemonic seed words",
|
||||
is_flag=True,
|
||||
)
|
||||
def audit(config_file, keystore_filepath, view_mnemonic):
|
||||
"""Audit a mnemonic phrase against a local keystore or view mnemonic seed words."""
|
||||
emitter = StdoutEmitter()
|
||||
if keystore_filepath and config_file:
|
||||
raise click.BadOptionUsage(
|
||||
"--keystore-filepath",
|
||||
message=click.style(
|
||||
"--keystore-filepath is incompatible with --config-file",
|
||||
fg="red",
|
||||
),
|
||||
)
|
||||
|
||||
if keystore_filepath:
|
||||
keystore = Keystore(keystore_filepath)
|
||||
else:
|
||||
config_file = config_file or DEFAULT_CONFIG_FILEPATH
|
||||
if not config_file.exists():
|
||||
emitter.error(
|
||||
f"Ursula configuration file not found - {config_file.resolve()}"
|
||||
)
|
||||
raise click.Abort()
|
||||
|
||||
ursula_config = UrsulaConfiguration.from_configuration_file(
|
||||
filepath=config_file
|
||||
)
|
||||
keystore = ursula_config.keystore
|
||||
|
||||
password = get_nucypher_password(emitter=emitter, confirm=False)
|
||||
try:
|
||||
keystore.unlock(password=password)
|
||||
except Keystore.AuthenticationFailed:
|
||||
emitter.error("Password is incorrect.")
|
||||
raise click.Abort()
|
||||
|
||||
emitter.message("Password is correct.", color="green")
|
||||
|
||||
if view_mnemonic:
|
||||
mnemonic = keystore.get_mnemonic()
|
||||
emitter.message(f"\n{mnemonic}", color="cyan")
|
||||
return
|
||||
|
||||
try:
|
||||
correct = keystore.audit(words=collect_mnemonic(emitter), password=password)
|
||||
except Keystore.InvalidMnemonic:
|
||||
correct = False
|
||||
|
||||
if not correct:
|
||||
emitter.message("Mnemonic is incorrect.", color="red")
|
||||
raise click.Abort()
|
||||
|
||||
emitter.message("Mnemonic is correct.", color="green")
|
||||
|
||||
|
||||
@ursula.command()
|
||||
@option_config_file
|
||||
@click.option(
|
||||
"--keystore-filepath",
|
||||
help="Path to keystore .priv file Ursula should use",
|
||||
type=EXISTING_READABLE_FILE,
|
||||
required=False,
|
||||
)
|
||||
def recover(config_file, keystore_filepath):
|
||||
emitter = StdoutEmitter()
|
||||
config_file = config_file or DEFAULT_CONFIG_FILEPATH
|
||||
if not config_file.exists():
|
||||
emitter.error(f"Ursula configuration file not found - {config_file.resolve()}")
|
||||
raise click.Abort()
|
||||
|
||||
if keystore_filepath:
|
||||
# use available file
|
||||
keystore = Keystore(keystore_filepath)
|
||||
# ensure that the password for the keystore file is known
|
||||
password = get_nucypher_password(emitter=emitter, confirm=False)
|
||||
try:
|
||||
keystore.unlock(password=password)
|
||||
except Keystore.AuthenticationFailed:
|
||||
emitter.error("Password is incorrect.")
|
||||
raise click.Abort()
|
||||
else:
|
||||
# recovery keystore using user-provided mnemonic
|
||||
keystore = recover_keystore(emitter=emitter)
|
||||
|
||||
update_config_keystore_path(
|
||||
keystore_path=keystore.keystore_path, config_file=config_file
|
||||
)
|
||||
emitter.message(
|
||||
f"Updated {config_file} to use keystore filepath: {keystore.keystore_path.resolve()}",
|
||||
color="green",
|
||||
)
|
||||
|
||||
|
||||
@ursula.command()
|
||||
|
@ -402,6 +538,50 @@ def destroy(general_config, config_options, config_file, force):
|
|||
destroy_configuration(emitter, character_config=ursula_config, force=force)
|
||||
|
||||
|
||||
@ursula.command()
|
||||
@option_config_file
|
||||
@click.option(
|
||||
"--keystore-filepath",
|
||||
help="Path to keystore .priv file",
|
||||
type=EXISTING_READABLE_FILE,
|
||||
)
|
||||
@click.option(
|
||||
"--from-mnemonic",
|
||||
help="View TACo public keys from mnemonic seed words",
|
||||
is_flag=True,
|
||||
)
|
||||
def public_keys(config_file, keystore_filepath, from_mnemonic):
|
||||
"""Display the public keys of a keystore."""
|
||||
emitter = StdoutEmitter()
|
||||
|
||||
if sum(1 for i in (keystore_filepath, config_file, from_mnemonic) if i) > 1:
|
||||
raise click.BadOptionUsage(
|
||||
"--keystore-filepath",
|
||||
message=click.style(
|
||||
"At most, one of --keystore-filepath, --config-file, or --from-mnemonic must be specified",
|
||||
fg="red",
|
||||
),
|
||||
)
|
||||
|
||||
if from_mnemonic:
|
||||
keystore = Keystore.from_mnemonic(collect_mnemonic(emitter))
|
||||
else:
|
||||
keystore_path_to_use = keystore_filepath
|
||||
if not keystore_path_to_use:
|
||||
config_file = config_file or DEFAULT_CONFIG_FILEPATH
|
||||
ursula_config = UrsulaConfiguration.from_configuration_file(
|
||||
filepath=config_file
|
||||
)
|
||||
keystore_path_to_use = ursula_config.keystore.keystore_path
|
||||
|
||||
keystore = Keystore(keystore_path_to_use)
|
||||
keystore.unlock(get_nucypher_password(emitter=emitter, confirm=False))
|
||||
|
||||
ritualistic_power = keystore.derive_crypto_power(RitualisticPower)
|
||||
ferveo_public_key = bytes(ritualistic_power.public_key()).hex()
|
||||
emitter.message(f"\nFerveo Public Key: {ferveo_public_key}", color="cyan")
|
||||
|
||||
|
||||
@ursula.command()
|
||||
@group_character_options
|
||||
@option_config_file
|
||||
|
@ -508,7 +688,7 @@ def config(general_config, config_options, config_file, force, action):
|
|||
emitter.error(
|
||||
"--config-file <FILEPATH> is required to run a configuration file migration."
|
||||
)
|
||||
return click.Abort()
|
||||
raise click.Abort()
|
||||
config_file = select_config_file(
|
||||
emitter=emitter,
|
||||
checksum_address=config_options.operator_address,
|
||||
|
|
|
@ -48,9 +48,7 @@ Path to Logs: {USER_LOG_DIR}
|
|||
|
||||
- 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!
|
||||
|
||||
"""
|
||||
- Remember your password! Without the password, it's impossible to decrypt the key!"""
|
||||
)
|
||||
|
||||
if character_name == 'ursula':
|
||||
|
|
|
@ -24,6 +24,7 @@ from nucypher.blockchain.eth.registry import (
|
|||
)
|
||||
from nucypher.blockchain.eth.signers import Signer
|
||||
from nucypher.characters.lawful import Ursula
|
||||
from nucypher.cli.actions.auth import collect_mnemonic
|
||||
from nucypher.config import constants
|
||||
from nucypher.config.util import cast_paths_from
|
||||
from nucypher.crypto.keystore import Keystore
|
||||
|
@ -419,9 +420,7 @@ class CharacterConfiguration(BaseConfiguration):
|
|||
self.crypto_power = crypto_power
|
||||
if keystore_path and not keystore:
|
||||
keystore = Keystore(keystore_path=keystore_path)
|
||||
self.__keystore = self.__keystore = keystore or NO_KEYSTORE_ATTACHED.bool_value(
|
||||
False
|
||||
)
|
||||
self.__keystore = keystore or NO_KEYSTORE_ATTACHED.bool_value(False)
|
||||
self.keystore_dir = (
|
||||
Path(keystore.keystore_path).parent
|
||||
if keystore
|
||||
|
@ -583,18 +582,29 @@ class CharacterConfiguration(BaseConfiguration):
|
|||
|
||||
Warning: This method allows mutation and may result in an inconsistent configuration.
|
||||
"""
|
||||
# config file should exist and we we override -> no need for modifier
|
||||
# config file should exist and we override -> no need for modifier
|
||||
return super().update(filepath=self.config_file_location, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def generate(
|
||||
cls, password: str, key_material: Optional[bytes] = None, *args, **kwargs
|
||||
cls,
|
||||
password: str,
|
||||
key_material: Optional[bytes] = None,
|
||||
with_mnemonic: bool = False,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Generates local directories, private keys, and initial configuration for a new node.
|
||||
"""
|
||||
"""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)
|
||||
if key_material and with_mnemonic:
|
||||
raise ValueError(
|
||||
"Cannot provide key_material and with_mnemonic simultaneously"
|
||||
)
|
||||
node_config.initialize(
|
||||
key_material=key_material,
|
||||
with_mnemonic=with_mnemonic,
|
||||
password=password,
|
||||
)
|
||||
node_config.keystore.unlock(password)
|
||||
return node_config
|
||||
|
||||
|
@ -789,9 +799,19 @@ class CharacterConfiguration(BaseConfiguration):
|
|||
power_ups.append(power_up)
|
||||
return power_ups
|
||||
|
||||
def initialize(self, password: str, key_material: Optional[bytes] = None) -> Path:
|
||||
def initialize(
|
||||
self,
|
||||
password: str,
|
||||
key_material: Optional[bytes] = None,
|
||||
with_mnemonic: bool = False,
|
||||
) -> Path:
|
||||
"""Initialize a new configuration and write installation files to disk."""
|
||||
|
||||
if key_material and with_mnemonic:
|
||||
raise ValueError(
|
||||
"Cannot provide key_material and with_mnemonic simultaneously"
|
||||
)
|
||||
|
||||
# Development
|
||||
if self.dev_mode:
|
||||
self.__temp_dir = TemporaryDirectory(
|
||||
|
@ -806,6 +826,7 @@ class CharacterConfiguration(BaseConfiguration):
|
|||
key_material=key_material,
|
||||
password=password,
|
||||
interactive=self.MNEMONIC_KEYSTORE,
|
||||
with_mnemonic=with_mnemonic,
|
||||
)
|
||||
|
||||
self._cache_runtime_filepaths()
|
||||
|
@ -824,13 +845,26 @@ class CharacterConfiguration(BaseConfiguration):
|
|||
password: str,
|
||||
key_material: Optional[bytes] = None,
|
||||
interactive: bool = True,
|
||||
with_mnemonic: bool = False,
|
||||
) -> Keystore:
|
||||
|
||||
if key_material and with_mnemonic:
|
||||
raise ValueError(
|
||||
"Cannot provide key_material and with_mnemonic simultaneously"
|
||||
)
|
||||
|
||||
if key_material:
|
||||
self.__keystore = Keystore.import_secure(
|
||||
key_material=key_material,
|
||||
password=password,
|
||||
keystore_dir=self.keystore_dir,
|
||||
)
|
||||
elif with_mnemonic:
|
||||
self.__keystore = Keystore.restore(
|
||||
password=password,
|
||||
keystore_dir=self.keystore_dir,
|
||||
words=collect_mnemonic(self.emitter),
|
||||
)
|
||||
else:
|
||||
if interactive:
|
||||
self.__keystore = Keystore.generate(
|
||||
|
|
|
@ -24,6 +24,7 @@ BASE_DIR = NUCYPHER_PACKAGE.parent.resolve()
|
|||
# User Application Filepaths
|
||||
APP_DIR = AppDirs(nucypher.__title__, nucypher.__author__)
|
||||
DEFAULT_CONFIG_ROOT = Path(os.getenv('NUCYPHER_CONFIG_ROOT', default=APP_DIR.user_data_dir))
|
||||
DEFAULT_CONFIG_FILEPATH = DEFAULT_CONFIG_ROOT / "ursula.json"
|
||||
USER_LOG_DIR = Path(os.getenv('NUCYPHER_USER_LOG_DIR', default=APP_DIR.user_log_dir))
|
||||
DEFAULT_LOG_FILENAME = "nucypher.log"
|
||||
DEFAULT_JSON_LOG_FILENAME = "nucypher.json"
|
||||
|
|
|
@ -74,7 +74,7 @@ def verify_aggregate(
|
|||
pvss_aggregated.verify(shares, transcripts)
|
||||
|
||||
|
||||
def derive_decryption_share(
|
||||
def produce_decryption_share(
|
||||
nodes: List[Validator],
|
||||
aggregated_transcript: AggregatedTranscript,
|
||||
keypair: Keypair,
|
||||
|
|
|
@ -7,7 +7,7 @@ from json import JSONDecodeError
|
|||
from os.path import abspath
|
||||
from pathlib import Path
|
||||
from secrets import token_bytes
|
||||
from typing import Callable, ClassVar, Dict, List, Optional, Tuple, Union
|
||||
from typing import Callable, Dict, List, Optional, Tuple, Type, Union
|
||||
|
||||
import click
|
||||
from constant_sorrow.constants import KEYSTORE_LOCKED
|
||||
|
@ -253,12 +253,34 @@ class Keystore:
|
|||
class AuthenticationFailed(RuntimeError):
|
||||
pass
|
||||
|
||||
def __init__(self, keystore_path: Path):
|
||||
self.keystore_path = keystore_path
|
||||
self.__created, self.__id = _parse_path(keystore_path)
|
||||
class InvalidMnemonic(ValueError):
|
||||
pass
|
||||
|
||||
def __init__(self, keystore_path: Path = None):
|
||||
self.__secret = KEYSTORE_LOCKED
|
||||
self.keystore_path = keystore_path
|
||||
if self.keystore_path:
|
||||
self.__created, self.__id = _parse_path(keystore_path)
|
||||
|
||||
@classmethod
|
||||
def from_keystore_id(cls, filepath: Path) -> "Keystore":
|
||||
instance = cls(keystore_path=filepath)
|
||||
return instance
|
||||
|
||||
@classmethod
|
||||
def from_mnemonic(cls, words: str) -> "Keystore":
|
||||
__mnemonic = Mnemonic(_MNEMONIC_LANGUAGE)
|
||||
__secret = bytes(__mnemonic.to_entropy(words))
|
||||
keystore = cls()
|
||||
keystore.__secret = __secret
|
||||
return keystore
|
||||
|
||||
def __decrypt_keystore(self, path: Path, password: str) -> bool:
|
||||
if not self.keystore_path:
|
||||
raise Keystore.Invalid(
|
||||
"Keystore path not set, initialize with a valid path."
|
||||
)
|
||||
|
||||
payload = _read_keystore(path, deserializer=_deserialize_keystore)
|
||||
__password_material = derive_key_material_from_password(password=password.encode(),
|
||||
salt=payload['password_salt'])
|
||||
|
@ -346,12 +368,32 @@ class Keystore:
|
|||
@classmethod
|
||||
def restore(cls, words: str, password: str, keystore_dir: Optional[Path] = None) -> 'Keystore':
|
||||
"""Restore a keystore from seed words"""
|
||||
__mnemonic = Mnemonic(_MNEMONIC_LANGUAGE)
|
||||
__secret = bytes(__mnemonic.to_entropy(words))
|
||||
mnemonic = Mnemonic(_MNEMONIC_LANGUAGE)
|
||||
__secret = bytes(mnemonic.to_entropy(words))
|
||||
path = Keystore.__save(secret=__secret, password=password, keystore_dir=keystore_dir)
|
||||
keystore = cls(keystore_path=path)
|
||||
return keystore
|
||||
|
||||
def audit(self, words: str, password: str) -> bool:
|
||||
"""Check if a mnemonic phrase can derive the secret key for the local keystore"""
|
||||
mnemonic = Mnemonic(_MNEMONIC_LANGUAGE)
|
||||
try:
|
||||
__expected_secret = bytes(mnemonic.to_entropy(words))
|
||||
except ValueError as e:
|
||||
raise self.InvalidMnemonic(str(e))
|
||||
self.__decrypt_keystore(path=self.keystore_path, password=password)
|
||||
return self.__secret == __expected_secret
|
||||
|
||||
def get_mnemonic(self) -> str:
|
||||
"""Return the mnemonic phrase for the keystore"""
|
||||
if self.__secret is KEYSTORE_LOCKED:
|
||||
raise Keystore.Locked(
|
||||
f"{self.id} is locked and must be unlocked before use."
|
||||
)
|
||||
mnemonic = Mnemonic(_MNEMONIC_LANGUAGE)
|
||||
__words = mnemonic.to_mnemonic(self.__secret)
|
||||
return __words
|
||||
|
||||
@classmethod
|
||||
def generate(
|
||||
cls, password: str,
|
||||
|
@ -439,10 +481,9 @@ class Keystore:
|
|||
def unlock(self, password: str) -> None:
|
||||
self.__decrypt_keystore(path=self.keystore_path, password=password)
|
||||
|
||||
def derive_crypto_power(self,
|
||||
power_class: ClassVar[CryptoPowerUp],
|
||||
*power_args, **power_kwargs
|
||||
) -> Union[KeyPairBasedPower, DerivedKeyBasedPower]:
|
||||
def derive_crypto_power(
|
||||
self, power_class: Type[CryptoPowerUp], *power_args, **power_kwargs
|
||||
) -> Union[KeyPairBasedPower, DerivedKeyBasedPower]:
|
||||
|
||||
if not self.is_unlocked:
|
||||
raise Keystore.Locked(f"{self.id} is locked and must be unlocked before use.")
|
||||
|
|
|
@ -265,7 +265,7 @@ class RitualisticPower(KeyPairBasedPower):
|
|||
not_found_error = NoRitualisticPower
|
||||
provides = ("derive_decryption_share", "generate_transcript")
|
||||
|
||||
def derive_decryption_share(
|
||||
def produce_decryption_share(
|
||||
self,
|
||||
checksum_address: ChecksumAddress,
|
||||
ritual_id: int,
|
||||
|
@ -277,7 +277,7 @@ class RitualisticPower(KeyPairBasedPower):
|
|||
aad: bytes,
|
||||
variant: FerveoVariant,
|
||||
) -> Union[DecryptionShareSimple, DecryptionSharePrecomputed]:
|
||||
decryption_share = dkg.derive_decryption_share(
|
||||
decryption_share = dkg.produce_decryption_share(
|
||||
ritual_id=ritual_id,
|
||||
me=Validator(address=checksum_address, public_key=self.keypair.pubkey),
|
||||
shares=shares,
|
||||
|
|
|
@ -42,7 +42,7 @@ def generate_self_signed_certificate(
|
|||
private_key = ec.generate_private_key(curve(), default_backend())
|
||||
public_key = private_key.public_key()
|
||||
|
||||
now = datetime.datetime.utcnow()
|
||||
now = datetime.datetime.now(datetime.timezone.utc)
|
||||
fields = [x509.NameAttribute(NameOID.COMMON_NAME, host)]
|
||||
|
||||
subject = issuer = x509.Name(fields)
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import math
|
||||
from http import HTTPStatus
|
||||
from random import shuffle
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
from eth_typing import ChecksumAddress
|
||||
|
@ -80,15 +79,20 @@ class ThresholdDecryptionClient(ThresholdAccessControlClient):
|
|||
self.log.warn(message)
|
||||
raise self.ThresholdDecryptionRequestFailed(message)
|
||||
|
||||
# TODO: Find a better request order, perhaps based on latency data obtained from discovery loop - #3395
|
||||
requests = list(encrypted_requests)
|
||||
shuffle(requests)
|
||||
ursulas_to_contact = (
|
||||
self._learner.node_latency_collector.order_addresses_by_latency(
|
||||
list(encrypted_requests)
|
||||
)
|
||||
if self._learner.node_latency_collector
|
||||
else list(encrypted_requests)
|
||||
)
|
||||
|
||||
# Discussion about WorkerPool parameters:
|
||||
# "https://github.com/nucypher/nucypher/pull/3393#discussion_r1456307991"
|
||||
worker_pool = WorkerPool(
|
||||
worker=worker,
|
||||
value_factory=self.ThresholdDecryptionRequestFactory(
|
||||
ursulas_to_contact=requests,
|
||||
ursulas_to_contact=ursulas_to_contact,
|
||||
batch_size=math.ceil(threshold * 1.25),
|
||||
threshold=threshold,
|
||||
),
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import contextlib
|
||||
import time
|
||||
from collections import deque
|
||||
from contextlib import suppress
|
||||
|
@ -40,6 +41,7 @@ from nucypher.crypto.signing import InvalidSignature, SignatureStamp
|
|||
from nucypher.network.exceptions import NodeSeemsToBeDown
|
||||
from nucypher.network.middleware import RestMiddleware
|
||||
from nucypher.network.protocols import InterfaceInfo, SuspiciousActivity
|
||||
from nucypher.utilities.latency import NodeLatencyStatsCollector
|
||||
from nucypher.utilities.logging import Logger
|
||||
|
||||
TEACHER_NODES = {
|
||||
|
@ -218,6 +220,8 @@ class Learner:
|
|||
_ROUNDS_WITHOUT_NODES_AFTER_WHICH_TO_SLOW_DOWN = 10
|
||||
__DEFAULT_MIDDLEWARE_CLASS = RestMiddleware
|
||||
|
||||
_TRACK_NODE_LATENCY_STATS = False
|
||||
|
||||
_crashed = (
|
||||
False # moved from Character - why was this in Character and not Learner before
|
||||
)
|
||||
|
@ -261,6 +265,10 @@ class Learner:
|
|||
self.log = Logger("learning-loop") # type: Logger
|
||||
self.domain = domain
|
||||
|
||||
self.node_latency_collector = (
|
||||
NodeLatencyStatsCollector() if self._TRACK_NODE_LATENCY_STATS else None
|
||||
)
|
||||
|
||||
self.learning_deferred = Deferred()
|
||||
default_middleware = self.__DEFAULT_MIDDLEWARE_CLASS(
|
||||
registry=self.registry, eth_endpoint=self.eth_endpoint
|
||||
|
@ -827,11 +835,19 @@ class Learner:
|
|||
return RELAX
|
||||
|
||||
try:
|
||||
response = self.network_middleware.get_nodes_via_rest(
|
||||
node=current_teacher,
|
||||
announce_nodes=announce_nodes,
|
||||
fleet_state_checksum=self.known_nodes.checksum,
|
||||
optional_latency_context_manager = (
|
||||
self.node_latency_collector.get_latency_tracker(
|
||||
current_teacher.checksum_address
|
||||
)
|
||||
if self.node_latency_collector
|
||||
else contextlib.nullcontext()
|
||||
)
|
||||
with optional_latency_context_manager:
|
||||
response = self.network_middleware.get_nodes_via_rest(
|
||||
node=current_teacher,
|
||||
announce_nodes=announce_nodes,
|
||||
fleet_state_checksum=self.known_nodes.checksum,
|
||||
)
|
||||
# These except clauses apply to the current_teacher itself, not the learned-about nodes.
|
||||
except NodeSeemsToBeDown as e:
|
||||
unresponsive_nodes.add(current_teacher)
|
||||
|
|
|
@ -154,8 +154,9 @@ def _make_rest_app(this_node, log: Logger) -> Flask:
|
|||
"""
|
||||
# TODO: When non-evm chains are supported, bump the version.
|
||||
# this can return a list of chain names or other verifiable identifiers.
|
||||
|
||||
payload = {"version": 1.0, "evm": list(this_node.condition_providers)}
|
||||
providers = this_node.condition_provider_manager.providers
|
||||
sorted_chain_ids = sorted(list(providers))
|
||||
payload = {"version": 1.0, "evm": sorted_chain_ids}
|
||||
return Response(json.dumps(payload), mimetype="application/json")
|
||||
|
||||
@rest_app.route('/decrypt', methods=["POST"])
|
||||
|
@ -260,7 +261,7 @@ def _make_rest_app(this_node, log: Logger) -> Flask:
|
|||
try:
|
||||
evaluate_condition_lingo(
|
||||
condition_lingo=condition_lingo,
|
||||
providers=this_node.condition_providers,
|
||||
providers=this_node.condition_provider_manager,
|
||||
context=context,
|
||||
)
|
||||
except ConditionEvalError as error:
|
||||
|
|
|
@ -1,16 +1,22 @@
|
|||
from enum import Enum
|
||||
from typing import List
|
||||
from typing import List, Optional
|
||||
|
||||
import maya
|
||||
from eth_account.account import Account
|
||||
from eth_account.messages import HexBytes, encode_typed_data
|
||||
from eth_typing import ChecksumAddress
|
||||
from siwe import SiweMessage, VerificationError
|
||||
|
||||
from nucypher.policy.conditions.exceptions import NoConnectionToChain
|
||||
from nucypher.policy.conditions.utils import ConditionProviderManager
|
||||
from nucypher.utilities.logging import Logger
|
||||
|
||||
|
||||
class EvmAuth:
|
||||
class AuthScheme(Enum):
|
||||
EIP712 = "EIP712"
|
||||
EIP4361 = "EIP4361"
|
||||
EIP1271 = "EIP1271"
|
||||
|
||||
@classmethod
|
||||
def values(cls) -> List[str]:
|
||||
|
@ -26,7 +32,13 @@ class EvmAuth:
|
|||
"""The message is too old."""
|
||||
|
||||
@classmethod
|
||||
def authenticate(cls, data, signature, expected_address):
|
||||
def authenticate(
|
||||
cls,
|
||||
data,
|
||||
signature: str,
|
||||
expected_address: str,
|
||||
providers: Optional[ConditionProviderManager] = None,
|
||||
):
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
|
@ -35,13 +47,21 @@ class EvmAuth:
|
|||
return EIP712Auth
|
||||
elif scheme == cls.AuthScheme.EIP4361.value:
|
||||
return EIP4361Auth
|
||||
elif scheme == cls.AuthScheme.EIP1271.value:
|
||||
return EIP1271Auth
|
||||
|
||||
raise ValueError(f"Invalid authentication scheme: {scheme}")
|
||||
|
||||
|
||||
class EIP712Auth(EvmAuth):
|
||||
@classmethod
|
||||
def authenticate(cls, data, signature, expected_address):
|
||||
def authenticate(
|
||||
cls,
|
||||
data,
|
||||
signature: str,
|
||||
expected_address: str,
|
||||
providers: Optional[ConditionProviderManager] = None,
|
||||
):
|
||||
try:
|
||||
# convert hex data for byte fields - bytes are expected by underlying library
|
||||
# 1. salt
|
||||
|
@ -72,7 +92,13 @@ class EIP4361Auth(EvmAuth):
|
|||
FRESHNESS_IN_HOURS = 2
|
||||
|
||||
@classmethod
|
||||
def authenticate(cls, data, signature, expected_address):
|
||||
def authenticate(
|
||||
cls,
|
||||
data,
|
||||
signature: str,
|
||||
expected_address: str,
|
||||
providers: Optional[ConditionProviderManager] = None,
|
||||
):
|
||||
try:
|
||||
siwe_message = SiweMessage.from_message(message=data)
|
||||
except Exception as e:
|
||||
|
@ -106,3 +132,113 @@ class EIP4361Auth(EvmAuth):
|
|||
raise cls.AuthenticationFailed(
|
||||
f"Invalid EIP4361 signature; signature not valid for expected address, {expected_address}"
|
||||
)
|
||||
|
||||
|
||||
class EIP1271Auth(EvmAuth):
|
||||
EIP1271_ABI = """[
|
||||
{
|
||||
"inputs":[
|
||||
{
|
||||
"internalType":"bytes32",
|
||||
"name":"_hash",
|
||||
"type":"bytes32"
|
||||
},
|
||||
{
|
||||
"internalType":"bytes",
|
||||
"name":"_signature",
|
||||
"type":"bytes"
|
||||
}
|
||||
],
|
||||
"name":"isValidSignature",
|
||||
"outputs":[
|
||||
{
|
||||
"internalType":"bytes4",
|
||||
"name":"",
|
||||
"type":"bytes4"
|
||||
}
|
||||
],
|
||||
"stateMutability":"view",
|
||||
"type":"function"
|
||||
}
|
||||
]"""
|
||||
MAGIC_VALUE_BYTES = bytes(HexBytes("0x1626ba7e"))
|
||||
LOG = Logger("EIP1271Auth")
|
||||
|
||||
@classmethod
|
||||
def _extract_typed_data(cls, data):
|
||||
try:
|
||||
data_hash = bytes(HexBytes(data["dataHash"]))
|
||||
chain = data["chain"]
|
||||
return data_hash, chain
|
||||
except Exception as e:
|
||||
# data could not be processed
|
||||
raise cls.InvalidData(
|
||||
f"Invalid EIP1271 authentication data: {str(e) or e.__class__.__name__}"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _validate_auth_data(
|
||||
cls, data_hash, signature_bytes, expected_address, chain, providers
|
||||
):
|
||||
web3_endpoints = providers.web3_endpoints(chain_id=chain)
|
||||
last_error = None
|
||||
for web3_instance in web3_endpoints:
|
||||
try:
|
||||
# Interact with the EIP1271 contract
|
||||
eip1271_contract = web3_instance.eth.contract(
|
||||
address=expected_address, abi=cls.EIP1271_ABI
|
||||
)
|
||||
result = eip1271_contract.functions.isValidSignature(
|
||||
data_hash,
|
||||
signature_bytes,
|
||||
).call()
|
||||
if result == cls.MAGIC_VALUE_BYTES:
|
||||
return # Successful authentication
|
||||
|
||||
break
|
||||
except Exception as e:
|
||||
last_error = f"EIP1271 contract call failed ({expected_address}): {e}"
|
||||
cls.LOG.warn(f"{last_error}; attempting next provider")
|
||||
else:
|
||||
# If all providers fail
|
||||
if last_error:
|
||||
raise cls.AuthenticationFailed(
|
||||
f"EIP1271 verification failed; {last_error}"
|
||||
)
|
||||
|
||||
raise cls.AuthenticationFailed(
|
||||
f"EIP1271 verification failed; signature not valid for contract address, {expected_address}"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def authenticate(
|
||||
cls,
|
||||
data,
|
||||
signature: str,
|
||||
expected_address: ChecksumAddress,
|
||||
providers: Optional[ConditionProviderManager] = None,
|
||||
):
|
||||
if not providers:
|
||||
# should never happen
|
||||
raise cls.AuthenticationFailed(
|
||||
"EIP1271 verification failed; no endpoints provided"
|
||||
)
|
||||
|
||||
# Extract and validate input data
|
||||
data_hash, chain = cls._extract_typed_data(data)
|
||||
|
||||
# Validate the signature
|
||||
signature_bytes = bytes(HexBytes(signature))
|
||||
try:
|
||||
cls._validate_auth_data(
|
||||
data_hash, signature_bytes, expected_address, chain, providers
|
||||
)
|
||||
except NoConnectionToChain:
|
||||
raise cls.AuthenticationFailed(
|
||||
f"EIP1271 verification failed; No connection to chain ID {chain}"
|
||||
)
|
||||
except cls.AuthenticationFailed:
|
||||
raise
|
||||
except Exception as e:
|
||||
# catch all
|
||||
raise cls.AuthenticationFailed(f"EIP1271 verification failed; {e}")
|
||||
|
|
|
@ -1,14 +1,18 @@
|
|||
import json
|
||||
from abc import ABC, abstractmethod
|
||||
from base64 import b64decode, b64encode
|
||||
from typing import Any, Dict, Tuple
|
||||
from typing import Any, List, Optional, Tuple
|
||||
|
||||
from marshmallow import Schema, ValidationError
|
||||
from marshmallow import Schema, ValidationError, fields
|
||||
|
||||
from nucypher.policy.conditions.exceptions import (
|
||||
InvalidCondition,
|
||||
InvalidConditionLingo,
|
||||
)
|
||||
from nucypher.policy.conditions.utils import (
|
||||
CamelCaseSchema,
|
||||
extract_single_error_message_from_schema_errors,
|
||||
)
|
||||
|
||||
|
||||
class _Serializable:
|
||||
|
@ -51,31 +55,102 @@ class _Serializable:
|
|||
|
||||
|
||||
class AccessControlCondition(_Serializable, ABC):
|
||||
CONDITION_TYPE = NotImplemented
|
||||
|
||||
class Schema(Schema):
|
||||
name = NotImplemented
|
||||
class Schema(CamelCaseSchema):
|
||||
SKIP_VALUES = (None,)
|
||||
name = fields.Str(required=False, allow_none=True)
|
||||
condition_type = NotImplemented
|
||||
|
||||
def __init__(self, condition_type: str, name: Optional[str] = None):
|
||||
super().__init__()
|
||||
|
||||
self.condition_type = condition_type
|
||||
self.name = name
|
||||
|
||||
self._validate()
|
||||
|
||||
@abstractmethod
|
||||
def verify(self, *args, **kwargs) -> Tuple[bool, Any]:
|
||||
"""Returns the boolean result of the evaluation and the returned value in a two-tuple."""
|
||||
return NotImplemented
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def validate(cls, data: Dict) -> None:
|
||||
errors = cls.Schema().validate(data=data)
|
||||
def _validate(self, **kwargs):
|
||||
errors = self.Schema().validate(data=self.to_dict())
|
||||
if errors:
|
||||
raise InvalidCondition(f"Invalid {cls.__name__}: {errors}")
|
||||
error_message = extract_single_error_message_from_schema_errors(errors)
|
||||
raise InvalidCondition(
|
||||
f"Invalid {self.__class__.__name__}: {error_message}"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data) -> "AccessControlCondition":
|
||||
try:
|
||||
return super().from_dict(data)
|
||||
except ValidationError as e:
|
||||
raise InvalidConditionLingo(f"Invalid condition grammar: {e}")
|
||||
raise InvalidConditionLingo(f"Invalid condition grammar: {e}") from e
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, data) -> "AccessControlCondition":
|
||||
try:
|
||||
return super().from_json(data)
|
||||
except ValidationError as e:
|
||||
raise InvalidConditionLingo(f"Invalid condition grammar: {e}")
|
||||
raise InvalidConditionLingo(f"Invalid condition grammar: {e}") from e
|
||||
|
||||
|
||||
class MultiConditionAccessControl(AccessControlCondition):
|
||||
MAX_NUM_CONDITIONS = 5
|
||||
MAX_MULTI_CONDITION_NESTED_LEVEL = 2
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def conditions(self) -> List[AccessControlCondition]:
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def _validate_multi_condition_nesting(
|
||||
cls,
|
||||
conditions: List[AccessControlCondition],
|
||||
field_name: str,
|
||||
current_level: int = 1,
|
||||
):
|
||||
if len(conditions) > cls.MAX_NUM_CONDITIONS:
|
||||
raise ValidationError(
|
||||
field_name=field_name,
|
||||
message=f"Maximum of {cls.MAX_NUM_CONDITIONS} conditions are allowed",
|
||||
)
|
||||
|
||||
for condition in conditions:
|
||||
if not isinstance(condition, MultiConditionAccessControl):
|
||||
continue
|
||||
|
||||
level = current_level + 1
|
||||
if level > cls.MAX_MULTI_CONDITION_NESTED_LEVEL:
|
||||
raise ValidationError(
|
||||
field_name=field_name,
|
||||
message=f"Only {cls.MAX_MULTI_CONDITION_NESTED_LEVEL} nested levels of multi-conditions are allowed",
|
||||
)
|
||||
cls._validate_multi_condition_nesting(
|
||||
conditions=condition.conditions,
|
||||
field_name=field_name,
|
||||
current_level=level,
|
||||
)
|
||||
|
||||
|
||||
class ExecutionCall(_Serializable, ABC):
|
||||
class InvalidExecutionCall(ValueError):
|
||||
pass
|
||||
|
||||
class Schema(CamelCaseSchema):
|
||||
pass
|
||||
|
||||
def __init__(self):
|
||||
# validate call using marshmallow schema before creating
|
||||
errors = self.Schema().validate(data=self.to_dict())
|
||||
if errors:
|
||||
error_message = extract_single_error_message_from_schema_errors(errors)
|
||||
raise self.InvalidExecutionCall(f"{error_message}")
|
||||
|
||||
@abstractmethod
|
||||
def execute(self, *args, **kwargs) -> Any:
|
||||
raise NotImplementedError
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import re
|
||||
from functools import partial
|
||||
from typing import Any, List, Union
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
from eth_typing import ChecksumAddress
|
||||
from eth_utils import to_checksum_address
|
||||
|
@ -11,6 +11,10 @@ from nucypher.policy.conditions.exceptions import (
|
|||
InvalidContextVariableData,
|
||||
RequiredContextVariable,
|
||||
)
|
||||
from nucypher.policy.conditions.utils import (
|
||||
ConditionProviderManager,
|
||||
check_and_convert_big_int_string_to_int,
|
||||
)
|
||||
|
||||
USER_ADDRESS_CONTEXT = ":userAddress"
|
||||
USER_ADDRESS_EIP4361_EXTERNAL_CONTEXT = ":userAddressExternalEIP4361"
|
||||
|
@ -19,7 +23,7 @@ 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_CONTEXT: None, # allow any scheme (EIP4361, EIP1271, EIP712) for now; eventually EIP712 will be deprecated
|
||||
USER_ADDRESS_EIP4361_EXTERNAL_CONTEXT: EvmAuth.AuthScheme.EIP4361.value,
|
||||
}
|
||||
|
||||
|
@ -28,7 +32,11 @@ class UnexpectedScheme(Exception):
|
|||
pass
|
||||
|
||||
|
||||
def _resolve_user_address(user_address_context_variable, **context) -> ChecksumAddress:
|
||||
def _resolve_user_address(
|
||||
user_address_context_variable: str,
|
||||
providers: Optional[ConditionProviderManager] = None,
|
||||
**context,
|
||||
) -> ChecksumAddress:
|
||||
"""
|
||||
Recovers a checksum address from a signed message.
|
||||
|
||||
|
@ -38,7 +46,7 @@ def _resolve_user_address(user_address_context_variable, **context) -> ChecksumA
|
|||
{
|
||||
"signature": "<signature>",
|
||||
"address": "<address>",
|
||||
"scheme": "EIP4361" | ...
|
||||
"scheme": "EIP4361" | "EIP1271" | ...
|
||||
"typedData": ...
|
||||
}
|
||||
}
|
||||
|
@ -59,7 +67,10 @@ def _resolve_user_address(user_address_context_variable, **context) -> ChecksumA
|
|||
|
||||
auth = EvmAuth.from_scheme(scheme)
|
||||
auth.authenticate(
|
||||
data=typed_data, signature=signature, expected_address=expected_address
|
||||
data=typed_data,
|
||||
signature=signature,
|
||||
expected_address=expected_address,
|
||||
providers=providers,
|
||||
)
|
||||
except EvmAuth.InvalidData as e:
|
||||
raise InvalidContextVariableData(
|
||||
|
@ -90,18 +101,23 @@ _DIRECTIVES = {
|
|||
|
||||
|
||||
def is_context_variable(variable) -> bool:
|
||||
if isinstance(variable, str) and variable.startswith(CONTEXT_PREFIX):
|
||||
if CONTEXT_REGEX.fullmatch(variable):
|
||||
return True
|
||||
else:
|
||||
raise ValueError(f"Context variable name '{variable}' is not valid.")
|
||||
return False
|
||||
return isinstance(variable, str) and CONTEXT_REGEX.fullmatch(variable)
|
||||
|
||||
|
||||
def get_context_value(context_variable: str, **context) -> Any:
|
||||
def string_contains_context_variable(variable: str) -> bool:
|
||||
matches = re.findall(CONTEXT_REGEX, variable)
|
||||
return bool(matches)
|
||||
|
||||
|
||||
def get_context_value(
|
||||
context_variable: str,
|
||||
providers: Optional[ConditionProviderManager] = None,
|
||||
**context,
|
||||
) -> Any:
|
||||
try:
|
||||
# DIRECTIVES are special context vars that will pre-processed by ursula
|
||||
func = _DIRECTIVES[context_variable]
|
||||
value = func(providers=providers, **context) # required inputs here
|
||||
except KeyError:
|
||||
# fallback for context variable without directive - assume key,value pair
|
||||
# handles the case for user customized context variables
|
||||
|
@ -110,24 +126,44 @@ def get_context_value(context_variable: str, **context) -> Any:
|
|||
raise RequiredContextVariable(
|
||||
f'No value provided for unrecognized context variable "{context_variable}"'
|
||||
)
|
||||
else:
|
||||
value = func(**context) # required inputs here
|
||||
elif isinstance(value, str):
|
||||
# possible big int value
|
||||
value = check_and_convert_big_int_string_to_int(value)
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def _resolve_context_variable(param: Union[Any, List[Any]], **context):
|
||||
def resolve_any_context_variables(
|
||||
param: Union[Any, List[Any], Dict[Any, Any]],
|
||||
providers: Optional[ConditionProviderManager] = None,
|
||||
**context,
|
||||
):
|
||||
if isinstance(param, list):
|
||||
return [_resolve_context_variable(item, **context) for item in param]
|
||||
elif is_context_variable(param):
|
||||
return get_context_value(context_variable=param, **context)
|
||||
return [
|
||||
resolve_any_context_variables(item, providers, **context) for item in param
|
||||
]
|
||||
elif isinstance(param, dict):
|
||||
return {
|
||||
k: resolve_any_context_variables(v, providers, **context)
|
||||
for k, v in param.items()
|
||||
}
|
||||
elif isinstance(param, str):
|
||||
# either it is a context variable OR contains a context variable within it
|
||||
# TODO separating the two cases for now out of concern of regex searching
|
||||
# within strings (case 2)
|
||||
if is_context_variable(param):
|
||||
return get_context_value(
|
||||
context_variable=param, providers=providers, **context
|
||||
)
|
||||
else:
|
||||
matches = re.findall(CONTEXT_REGEX, param)
|
||||
for context_var in matches:
|
||||
# checking out of concern for faulty regex search within string
|
||||
if context_var in context:
|
||||
resolved_var = get_context_value(
|
||||
context_variable=context_var, providers=providers, **context
|
||||
)
|
||||
param = param.replace(context_var, str(resolved_var))
|
||||
return param
|
||||
else:
|
||||
return param
|
||||
|
||||
|
||||
def resolve_any_context_variables(parameters: List[Any], return_value_test, **context):
|
||||
processed_parameters = [
|
||||
_resolve_context_variable(param, **context) for param in parameters
|
||||
]
|
||||
processed_return_value_test = return_value_test.with_resolved_context(**context)
|
||||
return processed_parameters, processed_return_value_test
|
||||
|
|
|
@ -1,42 +1,52 @@
|
|||
from typing import (
|
||||
Any,
|
||||
Dict,
|
||||
Iterator,
|
||||
List,
|
||||
Optional,
|
||||
Set,
|
||||
Tuple,
|
||||
)
|
||||
|
||||
from eth_typing import ChecksumAddress
|
||||
from eth_utils import to_checksum_address
|
||||
from marshmallow import ValidationError, fields, post_load, validate, validates_schema
|
||||
from marshmallow import (
|
||||
ValidationError,
|
||||
fields,
|
||||
post_load,
|
||||
validate,
|
||||
validates,
|
||||
validates_schema,
|
||||
)
|
||||
from marshmallow.validate import OneOf
|
||||
from web3 import HTTPProvider, Web3
|
||||
from web3.contract.contract import ContractFunction
|
||||
from web3.middleware import geth_poa_middleware
|
||||
from web3.providers import BaseProvider
|
||||
from typing_extensions import override
|
||||
from web3 import Web3
|
||||
from web3.types import ABIFunction
|
||||
|
||||
from nucypher.policy.conditions import STANDARD_ABI_CONTRACT_TYPES, STANDARD_ABIS
|
||||
from nucypher.policy.conditions.base import AccessControlCondition
|
||||
from nucypher.policy.conditions import STANDARD_ABI_CONTRACT_TYPES
|
||||
from nucypher.policy.conditions.base import (
|
||||
ExecutionCall,
|
||||
)
|
||||
from nucypher.policy.conditions.context import (
|
||||
is_context_variable,
|
||||
resolve_any_context_variables,
|
||||
)
|
||||
from nucypher.policy.conditions.exceptions import (
|
||||
InvalidCondition,
|
||||
NoConnectionToChain,
|
||||
RequiredContextVariable,
|
||||
RPCExecutionFailed,
|
||||
)
|
||||
from nucypher.policy.conditions.lingo import ConditionType, ReturnValueTest
|
||||
from nucypher.policy.conditions.utils import CamelCaseSchema, camel_case_to_snake
|
||||
from nucypher.policy.conditions.lingo import (
|
||||
AnyField,
|
||||
ConditionType,
|
||||
ExecutionCallAccessControlCondition,
|
||||
ReturnValueTest,
|
||||
)
|
||||
from nucypher.policy.conditions.utils import (
|
||||
ConditionProviderManager,
|
||||
camel_case_to_snake,
|
||||
)
|
||||
from nucypher.policy.conditions.validation import (
|
||||
_align_comparator_value_with_abi,
|
||||
_get_abi_types,
|
||||
_validate_condition_abi,
|
||||
_validate_multiple_output_types,
|
||||
_validate_single_output_type,
|
||||
align_comparator_value_with_abi,
|
||||
get_unbound_contract_function,
|
||||
validate_contract_function_expected_return_type,
|
||||
validate_function_abi,
|
||||
)
|
||||
|
||||
# TODO: Move this to a more appropriate location,
|
||||
|
@ -44,87 +54,117 @@ from nucypher.policy.conditions.validation import (
|
|||
# Permitted blockchains for condition evaluation
|
||||
from nucypher.utilities import logging
|
||||
|
||||
_CONDITION_CHAINS = {
|
||||
1: "ethereum/mainnet",
|
||||
11155111: "ethereum/sepolia",
|
||||
137: "polygon/mainnet",
|
||||
80002: "polygon/amoy",
|
||||
# TODO: Permit support for these chains
|
||||
# 100: "gnosis/mainnet",
|
||||
# 10200: "gnosis/chiado",
|
||||
}
|
||||
|
||||
class RPCCall(ExecutionCall):
|
||||
LOG = logging.Logger(__name__)
|
||||
|
||||
def _resolve_abi(
|
||||
w3: Web3,
|
||||
method: str,
|
||||
standard_contract_type: Optional[str] = None,
|
||||
function_abi: Optional[ABIFunction] = None,
|
||||
) -> ABIFunction:
|
||||
"""Resolves the contract an/or function ABI from a standard contract name"""
|
||||
|
||||
if not (function_abi or standard_contract_type):
|
||||
raise InvalidCondition(
|
||||
f"Ambiguous ABI - Supply either an ABI or a standard contract type ({STANDARD_ABI_CONTRACT_TYPES})."
|
||||
)
|
||||
|
||||
if standard_contract_type:
|
||||
try:
|
||||
# Lookup the standard ABI given it's ERC standard name (standard contract type)
|
||||
contract_abi = STANDARD_ABIS[standard_contract_type]
|
||||
except KeyError:
|
||||
raise InvalidCondition(
|
||||
f"Invalid standard contract type {standard_contract_type}; Must be one of {STANDARD_ABI_CONTRACT_TYPES}"
|
||||
)
|
||||
|
||||
try:
|
||||
# Extract all function ABIs from the contract's ABI.
|
||||
# Will raise a ValueError if there is not exactly one match.
|
||||
function_abi = (
|
||||
w3.eth.contract(abi=contract_abi).get_function_by_name(method).abi
|
||||
)
|
||||
except ValueError as e:
|
||||
raise InvalidCondition(str(e))
|
||||
|
||||
return ABIFunction(function_abi)
|
||||
|
||||
|
||||
def _validate_chain(chain: int) -> None:
|
||||
if not isinstance(chain, int):
|
||||
raise ValueError(
|
||||
f'The "chain" field of a condition must be the '
|
||||
f'integer chain ID (got "{chain}").'
|
||||
)
|
||||
if chain not in _CONDITION_CHAINS:
|
||||
raise InvalidCondition(
|
||||
f"chain ID {chain} is not a permitted "
|
||||
f"blockchain for condition evaluation."
|
||||
)
|
||||
|
||||
|
||||
class RPCCondition(AccessControlCondition):
|
||||
ETH_PREFIX = "eth_"
|
||||
ALLOWED_METHODS = {
|
||||
# RPC
|
||||
"eth_getBalance": int,
|
||||
} # TODO other allowed methods (tDEC #64)
|
||||
LOG = logging.Logger(__name__)
|
||||
|
||||
class Schema(ExecutionCall.Schema):
|
||||
chain = fields.Int(required=True, strict=True)
|
||||
method = fields.Str(
|
||||
required=True,
|
||||
error_messages={
|
||||
"required": "Undefined method name",
|
||||
"null": "Undefined method name",
|
||||
},
|
||||
)
|
||||
parameters = fields.List(AnyField, required=False, allow_none=True)
|
||||
|
||||
@validates("method")
|
||||
def validate_method(self, value):
|
||||
if value not in RPCCall.ALLOWED_METHODS:
|
||||
raise ValidationError(
|
||||
f"'{value}' is not a permitted RPC endpoint for condition evaluation."
|
||||
)
|
||||
|
||||
@post_load
|
||||
def make(self, data, **kwargs):
|
||||
return RPCCall(**data)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
chain: int,
|
||||
method: str,
|
||||
parameters: Optional[List[Any]] = None,
|
||||
):
|
||||
self.chain = chain
|
||||
self.method = method
|
||||
self.parameters = parameters
|
||||
super().__init__()
|
||||
|
||||
def _get_web3_py_function(self, w3: Web3, rpc_method: str):
|
||||
web3_py_method = camel_case_to_snake(rpc_method)
|
||||
rpc_function = getattr(
|
||||
w3.eth, web3_py_method
|
||||
) # bind contract function (only exposes the eth API)
|
||||
return rpc_function
|
||||
|
||||
def execute(self, providers: ConditionProviderManager, **context) -> Any:
|
||||
resolved_parameters = []
|
||||
if self.parameters:
|
||||
resolved_parameters = resolve_any_context_variables(
|
||||
param=self.parameters, providers=providers, **context
|
||||
)
|
||||
|
||||
endpoints = providers.web3_endpoints(self.chain)
|
||||
|
||||
latest_error = ""
|
||||
for w3 in endpoints:
|
||||
try:
|
||||
result = self._execute(w3, resolved_parameters)
|
||||
break
|
||||
except RequiredContextVariable:
|
||||
raise
|
||||
except Exception as e:
|
||||
latest_error = f"RPC call '{self.method}' failed: {e}"
|
||||
self.LOG.warn(f"{latest_error}, attempting to try next endpoint.")
|
||||
# Something went wrong. Try the next endpoint.
|
||||
continue
|
||||
else:
|
||||
# Fuck.
|
||||
raise RPCExecutionFailed(
|
||||
f"RPC call '{self.method}' failed; latest error - {latest_error}"
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
def _execute(self, w3: Web3, resolved_parameters: List[Any]) -> Any:
|
||||
"""Execute onchain read and return result."""
|
||||
rpc_endpoint_, rpc_method = self.method.split("_", 1)
|
||||
rpc_function = self._get_web3_py_function(w3, rpc_method)
|
||||
rpc_result = rpc_function(*resolved_parameters) # RPC read
|
||||
return rpc_result
|
||||
|
||||
|
||||
class RPCCondition(ExecutionCallAccessControlCondition):
|
||||
EXECUTION_CALL_TYPE = RPCCall
|
||||
CONDITION_TYPE = ConditionType.RPC.value
|
||||
|
||||
class Schema(CamelCaseSchema):
|
||||
SKIP_VALUES = (None,)
|
||||
name = fields.Str(required=False)
|
||||
class Schema(ExecutionCallAccessControlCondition.Schema, RPCCall.Schema):
|
||||
condition_type = fields.Str(
|
||||
validate=validate.Equal(ConditionType.RPC.value), required=True
|
||||
)
|
||||
chain = fields.Int(
|
||||
required=True, strict=True, validate=OneOf(_CONDITION_CHAINS)
|
||||
)
|
||||
method = fields.Str(required=True)
|
||||
parameters = fields.List(fields.Field, attribute="parameters", required=False)
|
||||
return_value_test = fields.Nested(
|
||||
ReturnValueTest.ReturnValueTestSchema(), required=True
|
||||
)
|
||||
|
||||
@validates_schema()
|
||||
def validate_expected_return_type(self, data, **kwargs):
|
||||
method = data.get("method")
|
||||
return_value_test = data.get("return_value_test")
|
||||
|
||||
expected_return_type = RPCCall.ALLOWED_METHODS[method]
|
||||
comparator_value = return_value_test.value
|
||||
if is_context_variable(comparator_value):
|
||||
return
|
||||
|
||||
if not isinstance(return_value_test.value, expected_return_type):
|
||||
raise ValidationError(
|
||||
field_name="return_value_test",
|
||||
message=f"Return value comparison for '{method}' call output "
|
||||
f"should be '{expected_return_type}' and not '{type(comparator_value)}'.",
|
||||
)
|
||||
|
||||
@post_load
|
||||
def make(self, data, **kwargs):
|
||||
|
@ -139,113 +179,34 @@ class RPCCondition(AccessControlCondition):
|
|||
chain: int,
|
||||
method: str,
|
||||
return_value_test: ReturnValueTest,
|
||||
condition_type: str = CONDITION_TYPE,
|
||||
condition_type: str = ConditionType.RPC.value,
|
||||
name: Optional[str] = None,
|
||||
parameters: Optional[List[Any]] = None,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
# Validate input
|
||||
# TODO: Additional validation (function is valid for ABI, RVT validity, standard contract name validity, etc.)
|
||||
_validate_chain(chain=chain)
|
||||
super().__init__(
|
||||
chain=chain,
|
||||
method=method,
|
||||
return_value_test=return_value_test,
|
||||
condition_type=condition_type,
|
||||
name=name,
|
||||
parameters=parameters,
|
||||
*args,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
# internal
|
||||
if condition_type != self.CONDITION_TYPE:
|
||||
raise InvalidCondition(
|
||||
f"{self.__class__.__name__} must be instantiated with the {self.CONDITION_TYPE} type."
|
||||
)
|
||||
@property
|
||||
def method(self):
|
||||
return self.execution_call.method
|
||||
|
||||
self.condition_type = condition_type
|
||||
self.name = name
|
||||
self.chain = chain
|
||||
self.provider: Optional[BaseProvider] = None # set in _configure_provider
|
||||
self.method = self._validate_method(method=method)
|
||||
@property
|
||||
def chain(self):
|
||||
return self.execution_call.chain
|
||||
|
||||
# test
|
||||
# should not be set to None - we do list unpacking so cannot be None; use empty list
|
||||
self.parameters = parameters or []
|
||||
self.return_value_test = return_value_test # output
|
||||
|
||||
self._validate_expected_return_type()
|
||||
|
||||
def _validate_method(self, method):
|
||||
if not method:
|
||||
raise InvalidCondition("Undefined method name")
|
||||
|
||||
if method not in self.ALLOWED_METHODS:
|
||||
raise InvalidCondition(
|
||||
f"'{method}' is not a permitted RPC endpoint for condition evaluation."
|
||||
)
|
||||
return method
|
||||
|
||||
def _validate_expected_return_type(self):
|
||||
expected_return_type = self.ALLOWED_METHODS[self.method]
|
||||
comparator_value = self.return_value_test.value
|
||||
if is_context_variable(comparator_value):
|
||||
return
|
||||
|
||||
if not isinstance(self.return_value_test.value, expected_return_type):
|
||||
raise InvalidCondition(
|
||||
f"Return value comparison for '{self.method}' call output "
|
||||
f"should be '{expected_return_type}' and not '{type(comparator_value)}'."
|
||||
)
|
||||
|
||||
def _next_endpoint(
|
||||
self, providers: Dict[int, Set[HTTPProvider]]
|
||||
) -> Iterator[HTTPProvider]:
|
||||
"""Yields the next web3 provider to try for a given chain ID"""
|
||||
try:
|
||||
rpc_providers = providers[self.chain]
|
||||
|
||||
# if there are no entries for the chain ID, there
|
||||
# is no connection to that chain available.
|
||||
except KeyError:
|
||||
raise NoConnectionToChain(chain=self.chain)
|
||||
if not rpc_providers:
|
||||
raise NoConnectionToChain(chain=self.chain) # TODO: unreachable?
|
||||
for provider in rpc_providers:
|
||||
# Someday, we might make this whole function async, and then we can knock on
|
||||
# each endpoint here to see if it's alive and only yield it if it is.
|
||||
yield provider
|
||||
|
||||
def _configure_w3(self, provider: BaseProvider) -> Web3:
|
||||
# Instantiate a local web3 instance
|
||||
self.provider = provider
|
||||
w3 = Web3(provider)
|
||||
# inject web3 middleware to handle POA chain extra_data field.
|
||||
w3.middleware_onion.inject(geth_poa_middleware, layer=0, name="poa")
|
||||
|
||||
return w3
|
||||
|
||||
def _check_chain_id(self) -> None:
|
||||
"""
|
||||
Validates that the actual web3 provider is *actually*
|
||||
connected to the condition's chain ID by reading its RPC endpoint.
|
||||
"""
|
||||
provider_chain = self.w3.eth.chain_id
|
||||
if provider_chain != self.chain:
|
||||
raise InvalidCondition(
|
||||
f"This condition can only be evaluated on chain ID {self.chain} but the provider's "
|
||||
f"connection is to chain ID {provider_chain}"
|
||||
)
|
||||
|
||||
def _configure_provider(self, provider: BaseProvider):
|
||||
"""Binds the condition's contract function to a blockchain provider for evaluation"""
|
||||
self.w3 = self._configure_w3(provider=provider)
|
||||
self._check_chain_id()
|
||||
return provider
|
||||
|
||||
def _get_web3_py_function(self, rpc_method: str):
|
||||
web3_py_method = camel_case_to_snake(rpc_method)
|
||||
rpc_function = getattr(
|
||||
self.w3.eth, web3_py_method
|
||||
) # bind contract function (only exposes the eth API)
|
||||
return rpc_function
|
||||
|
||||
def _execute_call(self, parameters: List[Any]) -> Any:
|
||||
"""Execute onchain read and return result."""
|
||||
rpc_endpoint_, rpc_method = self.method.split("_", 1)
|
||||
rpc_function = self._get_web3_py_function(rpc_method)
|
||||
rpc_result = rpc_function(*parameters) # RPC read
|
||||
return rpc_result
|
||||
@property
|
||||
def parameters(self):
|
||||
return self.execution_call.parameters
|
||||
|
||||
def _align_comparator_value_with_abi(
|
||||
self, return_value_test: ReturnValueTest
|
||||
|
@ -253,123 +214,204 @@ class RPCCondition(AccessControlCondition):
|
|||
return return_value_test
|
||||
|
||||
def verify(
|
||||
self, providers: Dict[int, Set[HTTPProvider]], **context
|
||||
self, providers: ConditionProviderManager, **context
|
||||
) -> Tuple[bool, Any]:
|
||||
"""
|
||||
Verifies the onchain condition is met by performing a
|
||||
read operation and evaluating the return value test.
|
||||
"""
|
||||
parameters, return_value_test = resolve_any_context_variables(
|
||||
self.parameters, self.return_value_test, **context
|
||||
resolved_return_value_test = self.return_value_test.with_resolved_context(
|
||||
providers=providers, **context
|
||||
)
|
||||
return_value_test = self._align_comparator_value_with_abi(
|
||||
resolved_return_value_test
|
||||
)
|
||||
return_value_test = self._align_comparator_value_with_abi(return_value_test)
|
||||
|
||||
endpoints = self._next_endpoint(providers=providers)
|
||||
for provider in endpoints:
|
||||
self._configure_provider(provider=provider)
|
||||
try:
|
||||
result = self._execute_call(parameters=parameters)
|
||||
break
|
||||
except Exception as e:
|
||||
self.LOG.warn(
|
||||
f"RPC call '{self.method}' failed: {e}, attempting to try next endpoint."
|
||||
)
|
||||
# Something went wrong. Try the next endpoint.
|
||||
continue
|
||||
else:
|
||||
# Fuck.
|
||||
raise RPCExecutionFailed(f"Contract call '{self.method}' failed.")
|
||||
result = self.execution_call.execute(providers=providers, **context)
|
||||
|
||||
eval_result = return_value_test.eval(result) # test
|
||||
return eval_result, result
|
||||
|
||||
|
||||
class ContractCondition(RPCCondition):
|
||||
CONDITION_TYPE = ConditionType.CONTRACT.value
|
||||
|
||||
class Schema(RPCCondition.Schema):
|
||||
condition_type = fields.Str(
|
||||
validate=validate.Equal(ConditionType.CONTRACT.value), required=True
|
||||
)
|
||||
standard_contract_type = fields.Str(required=False)
|
||||
class ContractCall(RPCCall):
|
||||
class Schema(RPCCall.Schema):
|
||||
contract_address = fields.Str(required=True)
|
||||
function_abi = fields.Dict(required=False)
|
||||
standard_contract_type = fields.Str(
|
||||
required=False,
|
||||
validate=OneOf(
|
||||
STANDARD_ABI_CONTRACT_TYPES,
|
||||
error="Invalid standard contract type: {input}",
|
||||
),
|
||||
allow_none=True,
|
||||
)
|
||||
function_abi = fields.Dict(required=False, allow_none=True)
|
||||
|
||||
@post_load
|
||||
def make(self, data, **kwargs):
|
||||
return ContractCondition(**data)
|
||||
return ContractCall(**data)
|
||||
|
||||
@validates("contract_address")
|
||||
def validate_contract_address(self, value):
|
||||
try:
|
||||
to_checksum_address(value)
|
||||
except ValueError:
|
||||
raise ValidationError(f"Invalid checksum address: '{value}'")
|
||||
|
||||
@override
|
||||
@validates("method")
|
||||
def validate_method(self, value):
|
||||
return
|
||||
|
||||
@validates("function_abi")
|
||||
def validate_abi(self, value):
|
||||
# needs to be done before schema validation
|
||||
if value:
|
||||
try:
|
||||
validate_function_abi(value)
|
||||
except ValueError as e:
|
||||
raise ValidationError(
|
||||
field_name="function_abi", message=str(e)
|
||||
) from e
|
||||
|
||||
@validates_schema
|
||||
def check_standard_contract_type_or_function_abi(self, data, **kwargs):
|
||||
def validate_standard_contract_type_or_function_abi(self, data, **kwargs):
|
||||
method = data.get("method")
|
||||
standard_contract_type = data.get("standard_contract_type")
|
||||
function_abi = data.get("function_abi")
|
||||
|
||||
# validate xor of standard contract type and function abi
|
||||
if not (bool(standard_contract_type) ^ bool(function_abi)):
|
||||
raise ValidationError(
|
||||
field_name="standard_contract_type",
|
||||
message=f"Provide a standard contract type or function ABI; got ({standard_contract_type}, {function_abi}).",
|
||||
)
|
||||
|
||||
# validate function abi with method name (not available for field validation)
|
||||
if function_abi:
|
||||
try:
|
||||
validate_function_abi(function_abi, method_name=method)
|
||||
except ValueError as e:
|
||||
raise ValidationError(
|
||||
field_name="function_abi", message=str(e)
|
||||
) from e
|
||||
|
||||
# validate contract
|
||||
contract_address = to_checksum_address(data.get("contract_address"))
|
||||
try:
|
||||
_validate_condition_abi(
|
||||
standard_contract_type, function_abi, method_name=data.get("method")
|
||||
get_unbound_contract_function(
|
||||
contract_address=contract_address,
|
||||
method=method,
|
||||
standard_contract_type=standard_contract_type,
|
||||
function_abi=function_abi,
|
||||
)
|
||||
except ValueError as e:
|
||||
raise ValidationError(str(e))
|
||||
raise ValidationError(str(e)) from e
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
method: str,
|
||||
contract_address: ChecksumAddress,
|
||||
condition_type: str = CONDITION_TYPE,
|
||||
standard_contract_type: Optional[str] = None,
|
||||
function_abi: Optional[ABIFunction] = None,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
if not method:
|
||||
raise InvalidCondition("Undefined method name")
|
||||
try:
|
||||
_validate_condition_abi(
|
||||
standard_contract_type, function_abi, method_name=method
|
||||
)
|
||||
except ValueError as e:
|
||||
raise InvalidCondition(str(e))
|
||||
|
||||
self.method = method
|
||||
self.w3 = Web3() # used to instantiate contract function without a provider
|
||||
|
||||
# preprocessing
|
||||
contract_address = to_checksum_address(contract_address)
|
||||
|
||||
# spec
|
||||
self.contract_address = contract_address
|
||||
self.condition_type = condition_type
|
||||
self.standard_contract_type = standard_contract_type
|
||||
self.function_abi = function_abi
|
||||
|
||||
self.contract_function = self._get_unbound_contract_function()
|
||||
super().__init__(method=method, *args, **kwargs)
|
||||
|
||||
# call to super must be at the end for proper validation
|
||||
super().__init__(condition_type=condition_type, method=method, *args, **kwargs)
|
||||
|
||||
def _validate_method(self, method):
|
||||
# overrides validate method used by rpc superclass
|
||||
return method
|
||||
|
||||
def _validate_expected_return_type(self) -> None:
|
||||
output_abi_types = _get_abi_types(self.contract_function.contract_abi[0])
|
||||
comparator_value = self.return_value_test.value
|
||||
comparator_index = self.return_value_test.index
|
||||
index_string = (
|
||||
f"@index={comparator_index}" if comparator_index is not None else ""
|
||||
)
|
||||
failure_message = (
|
||||
f"Invalid return value comparison type '{type(comparator_value)}' for "
|
||||
f"'{self.contract_function.fn_name}'{index_string} based on ABI types {output_abi_types}"
|
||||
# contract function already validated - so should not raise an exception
|
||||
self.contract_function = get_unbound_contract_function(
|
||||
contract_address=self.contract_address,
|
||||
method=self.method,
|
||||
standard_contract_type=self.standard_contract_type,
|
||||
function_abi=self.function_abi,
|
||||
)
|
||||
|
||||
if len(output_abi_types) == 1:
|
||||
_validate_single_output_type(
|
||||
output_abi_types[0], comparator_value, comparator_index, failure_message
|
||||
)
|
||||
else:
|
||||
_validate_multiple_output_types(
|
||||
output_abi_types, comparator_value, comparator_index, failure_message
|
||||
)
|
||||
def _execute(self, w3: Web3, resolved_parameters: List[Any]) -> Any:
|
||||
"""Execute onchain read and return result."""
|
||||
self.contract_function.w3 = w3
|
||||
bound_contract_function = self.contract_function(
|
||||
*resolved_parameters
|
||||
) # bind contract function
|
||||
contract_result = bound_contract_function.call() # onchain read
|
||||
return contract_result
|
||||
|
||||
|
||||
class ContractCondition(RPCCondition):
|
||||
EXECUTION_CALL_TYPE = ContractCall
|
||||
CONDITION_TYPE = ConditionType.CONTRACT.value
|
||||
|
||||
class Schema(RPCCondition.Schema, ContractCall.Schema):
|
||||
condition_type = fields.Str(
|
||||
validate=validate.Equal(ConditionType.CONTRACT.value), required=True
|
||||
)
|
||||
|
||||
@validates_schema()
|
||||
def validate_expected_return_type(self, data, **kwargs):
|
||||
# validate that contract function is correct
|
||||
try:
|
||||
contract_function = get_unbound_contract_function(
|
||||
contract_address=data.get("contract_address"),
|
||||
method=data.get("method"),
|
||||
standard_contract_type=data.get("standard_contract_type"),
|
||||
function_abi=data.get("function_abi"),
|
||||
)
|
||||
except ValueError as e:
|
||||
raise ValidationError(str(e)) from e
|
||||
|
||||
# validate return type based on contract function
|
||||
return_value_test = data.get("return_value_test")
|
||||
try:
|
||||
validate_contract_function_expected_return_type(
|
||||
contract_function=contract_function,
|
||||
return_value_test=return_value_test,
|
||||
)
|
||||
except ValueError as e:
|
||||
raise ValidationError(
|
||||
field_name="return_value_test",
|
||||
message=str(e),
|
||||
) from e
|
||||
|
||||
@post_load
|
||||
def make(self, data, **kwargs):
|
||||
return ContractCondition(**data)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
method: str,
|
||||
contract_address: ChecksumAddress,
|
||||
condition_type: str = ConditionType.CONTRACT.value,
|
||||
standard_contract_type: Optional[str] = None,
|
||||
function_abi: Optional[ABIFunction] = None,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(
|
||||
method=method,
|
||||
condition_type=condition_type,
|
||||
contract_address=contract_address,
|
||||
standard_contract_type=standard_contract_type,
|
||||
function_abi=function_abi,
|
||||
*args,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@property
|
||||
def function_abi(self):
|
||||
return self.execution_call.function_abi
|
||||
|
||||
@property
|
||||
def standard_contract_type(self):
|
||||
return self.execution_call.standard_contract_type
|
||||
|
||||
@property
|
||||
def contract_function(self):
|
||||
return self.execution_call.contract_function
|
||||
|
||||
@property
|
||||
def contract_address(self):
|
||||
return self.execution_call.contract_address
|
||||
|
||||
def __repr__(self) -> str:
|
||||
r = (
|
||||
|
@ -379,41 +421,10 @@ class ContractCondition(RPCCondition):
|
|||
)
|
||||
return r
|
||||
|
||||
def _configure_provider(self, *args, **kwargs):
|
||||
super()._configure_provider(*args, **kwargs)
|
||||
self.contract_function.w3 = self.w3
|
||||
|
||||
def _get_unbound_contract_function(self) -> ContractFunction:
|
||||
"""Gets an unbound contract function to evaluate for this condition"""
|
||||
function_abi = _resolve_abi(
|
||||
w3=self.w3,
|
||||
standard_contract_type=self.standard_contract_type,
|
||||
method=self.method,
|
||||
function_abi=self.function_abi,
|
||||
)
|
||||
try:
|
||||
contract = self.w3.eth.contract(
|
||||
address=self.contract_address, abi=[function_abi]
|
||||
)
|
||||
contract_function = getattr(contract.functions, self.method)
|
||||
return contract_function
|
||||
except Exception as e:
|
||||
raise InvalidCondition(
|
||||
f"Unable to find contract function, '{self.method}', for condition: {e}"
|
||||
)
|
||||
|
||||
def _execute_call(self, parameters: List[Any]) -> Any:
|
||||
"""Execute onchain read and return result."""
|
||||
bound_contract_function = self.contract_function(
|
||||
*parameters
|
||||
) # bind contract function
|
||||
contract_result = bound_contract_function.call() # onchain read
|
||||
return contract_result
|
||||
|
||||
def _align_comparator_value_with_abi(
|
||||
self, return_value_test: ReturnValueTest
|
||||
) -> ReturnValueTest:
|
||||
return _align_comparator_value_with_abi(
|
||||
return align_comparator_value_with_abi(
|
||||
abi=self.contract_function.contract_abi[0],
|
||||
return_value_test=return_value_test,
|
||||
)
|
||||
|
|
|
@ -7,9 +7,22 @@ class InvalidConditionLingo(Exception):
|
|||
class NoConnectionToChain(RuntimeError):
|
||||
"""Raised when a node does not have an associated provider for a chain."""
|
||||
|
||||
def __init__(self, chain: int):
|
||||
def __init__(self, chain: int, message: str = None):
|
||||
self.chain = chain
|
||||
message = f"No connection to chain ID {chain}"
|
||||
message = message or f"No connection to chain ID {chain}"
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class InvalidConnectionToChain(RuntimeError):
|
||||
"""Raised when a node does not have a valid provider for a chain."""
|
||||
|
||||
def __init__(self, expected_chain: int, actual_chain: int, message: str = None):
|
||||
self.expected_chain = expected_chain
|
||||
self.actual_chain = actual_chain
|
||||
message = (
|
||||
message
|
||||
or f"Invalid blockchain connection; expected chain ID {expected_chain}, but detected {actual_chain}"
|
||||
)
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
|
@ -45,3 +58,11 @@ class ConditionEvaluationFailed(Exception):
|
|||
|
||||
class RPCExecutionFailed(ConditionEvaluationFailed):
|
||||
"""Raised when an exception is raised from an RPC call."""
|
||||
|
||||
|
||||
class JsonRequestException(ConditionEvaluationFailed):
|
||||
"""Raised when an exception is raised from a JSON request."""
|
||||
|
||||
|
||||
class JWTException(ConditionEvaluationFailed):
|
||||
"""Raised when an exception is raised when validating a JWT token"""
|
||||
|
|
|
@ -0,0 +1,121 @@
|
|||
from typing import Any, Optional
|
||||
|
||||
from marshmallow import ValidationError, fields, post_load, validate, validates
|
||||
from marshmallow.fields import Url
|
||||
from typing_extensions import override
|
||||
|
||||
from nucypher.policy.conditions.context import is_context_variable
|
||||
from nucypher.policy.conditions.json.base import (
|
||||
BaseJsonRequestCondition,
|
||||
HTTPMethod,
|
||||
JSONPathField,
|
||||
JsonRequestCall,
|
||||
)
|
||||
from nucypher.policy.conditions.lingo import (
|
||||
ConditionType,
|
||||
ExecutionCallAccessControlCondition,
|
||||
ReturnValueTest,
|
||||
)
|
||||
from nucypher.utilities.logging import Logger
|
||||
|
||||
|
||||
class JsonApiCall(JsonRequestCall):
|
||||
TIMEOUT = 5 # seconds
|
||||
|
||||
class Schema(JsonRequestCall.Schema):
|
||||
endpoint = Url(required=True, relative=False, schemes=["https"])
|
||||
parameters = fields.Dict(required=False, allow_none=True)
|
||||
query = JSONPathField(required=False, allow_none=True)
|
||||
authorization_token = fields.Str(required=False, allow_none=True)
|
||||
|
||||
@post_load
|
||||
def make(self, data, **kwargs):
|
||||
return JsonApiCall(**data)
|
||||
|
||||
@validates("authorization_token")
|
||||
def validate_auth_token(self, value):
|
||||
if value and not is_context_variable(value):
|
||||
raise ValidationError(
|
||||
f"Invalid value for authorization token; expected a context variable, but got '{value}'"
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
endpoint: str,
|
||||
parameters: Optional[dict] = None,
|
||||
query: Optional[str] = None,
|
||||
authorization_token: Optional[str] = None,
|
||||
):
|
||||
self.endpoint = endpoint
|
||||
super().__init__(
|
||||
http_method=HTTPMethod.GET,
|
||||
parameters=parameters,
|
||||
query=query,
|
||||
authorization_token=authorization_token,
|
||||
)
|
||||
|
||||
self.logger = Logger(__name__)
|
||||
|
||||
@override
|
||||
def execute(self, **context) -> Any:
|
||||
return super()._execute(endpoint=self.endpoint, **context)
|
||||
|
||||
|
||||
class JsonApiCondition(BaseJsonRequestCondition):
|
||||
"""
|
||||
A JSON API condition is a condition that can be evaluated by performing a GET on a JSON
|
||||
HTTPS endpoint. The response must return an HTTP 200 with valid JSON in the response body.
|
||||
The response will be deserialized as JSON and parsed using jsonpath.
|
||||
"""
|
||||
|
||||
EXECUTION_CALL_TYPE = JsonApiCall
|
||||
CONDITION_TYPE = ConditionType.JSONAPI.value
|
||||
|
||||
class Schema(ExecutionCallAccessControlCondition.Schema, JsonApiCall.Schema):
|
||||
condition_type = fields.Str(
|
||||
validate=validate.Equal(ConditionType.JSONAPI.value), required=True
|
||||
)
|
||||
|
||||
@post_load
|
||||
def make(self, data, **kwargs):
|
||||
return JsonApiCondition(**data)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
endpoint: str,
|
||||
return_value_test: ReturnValueTest,
|
||||
query: Optional[str] = None,
|
||||
parameters: Optional[dict] = None,
|
||||
authorization_token: Optional[str] = None,
|
||||
condition_type: Optional[str] = ConditionType.JSONAPI.value,
|
||||
name: Optional[str] = None,
|
||||
):
|
||||
super().__init__(
|
||||
endpoint=endpoint,
|
||||
return_value_test=return_value_test,
|
||||
query=query,
|
||||
parameters=parameters,
|
||||
authorization_token=authorization_token,
|
||||
condition_type=condition_type,
|
||||
name=name,
|
||||
)
|
||||
|
||||
@property
|
||||
def endpoint(self):
|
||||
return self.execution_call.endpoint
|
||||
|
||||
@property
|
||||
def query(self):
|
||||
return self.execution_call.query
|
||||
|
||||
@property
|
||||
def parameters(self):
|
||||
return self.execution_call.parameters
|
||||
|
||||
@property
|
||||
def timeout(self):
|
||||
return self.execution_call.timeout
|
||||
|
||||
@property
|
||||
def authorization_token(self):
|
||||
return self.execution_call.authorization_token
|
|
@ -0,0 +1,159 @@
|
|||
from abc import ABC
|
||||
from enum import Enum
|
||||
from http import HTTPStatus
|
||||
from typing import Any, Optional, Tuple
|
||||
|
||||
import requests
|
||||
from jsonpath_ng.exceptions import JsonPathLexerError, JsonPathParserError
|
||||
from jsonpath_ng.ext import parse
|
||||
from marshmallow.fields import Field
|
||||
|
||||
from nucypher.policy.conditions.base import ExecutionCall
|
||||
from nucypher.policy.conditions.context import (
|
||||
resolve_any_context_variables,
|
||||
string_contains_context_variable,
|
||||
)
|
||||
from nucypher.policy.conditions.exceptions import (
|
||||
ConditionEvaluationFailed,
|
||||
JsonRequestException,
|
||||
)
|
||||
from nucypher.policy.conditions.json.utils import process_result_for_condition_eval
|
||||
from nucypher.policy.conditions.lingo import ExecutionCallAccessControlCondition
|
||||
from nucypher.utilities.logging import Logger
|
||||
|
||||
|
||||
class HTTPMethod(Enum):
|
||||
GET = "GET"
|
||||
POST = "POST"
|
||||
|
||||
|
||||
class JsonRequestCall(ExecutionCall, ABC):
|
||||
TIMEOUT = 5 # seconds
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
http_method: HTTPMethod,
|
||||
parameters: Optional[dict] = None,
|
||||
query: Optional[str] = None,
|
||||
authorization_token: Optional[str] = None,
|
||||
):
|
||||
|
||||
self.http_method = http_method
|
||||
self.parameters = parameters or {}
|
||||
self.query = query
|
||||
self.authorization_token = authorization_token
|
||||
|
||||
self.timeout = self.TIMEOUT
|
||||
self.logger = Logger(__name__)
|
||||
|
||||
super().__init__()
|
||||
|
||||
def _execute(self, endpoint: str, **context) -> Any:
|
||||
data = self._fetch(endpoint, **context)
|
||||
result = self._query_response(data, **context)
|
||||
return result
|
||||
|
||||
def _fetch(self, endpoint: str, **context) -> Any:
|
||||
resolved_endpoint = resolve_any_context_variables(endpoint, **context)
|
||||
resolved_parameters = resolve_any_context_variables(self.parameters, **context)
|
||||
|
||||
headers = {"Content-Type": "application/json"}
|
||||
if self.authorization_token:
|
||||
resolved_authorization_token = resolve_any_context_variables(
|
||||
self.authorization_token, **context
|
||||
)
|
||||
headers["Authorization"] = f"Bearer {resolved_authorization_token}"
|
||||
|
||||
try:
|
||||
if self.http_method == HTTPMethod.GET:
|
||||
response = requests.get(
|
||||
resolved_endpoint,
|
||||
params=resolved_parameters,
|
||||
timeout=self.timeout,
|
||||
headers=headers,
|
||||
)
|
||||
else:
|
||||
# POST
|
||||
response = requests.post(
|
||||
resolved_endpoint,
|
||||
json=resolved_parameters,
|
||||
timeout=self.timeout,
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
response.raise_for_status()
|
||||
if response.status_code != HTTPStatus.OK:
|
||||
raise JsonRequestException(
|
||||
f"Failed to fetch from endpoint {resolved_endpoint}: {response.status_code}"
|
||||
)
|
||||
|
||||
except requests.exceptions.RequestException as request_error:
|
||||
raise JsonRequestException(
|
||||
f"Failed to fetch from endpoint {resolved_endpoint}: {request_error}"
|
||||
)
|
||||
|
||||
try:
|
||||
data = response.json()
|
||||
return data
|
||||
except (requests.exceptions.RequestException, ValueError) as json_error:
|
||||
raise JsonRequestException(
|
||||
f"Failed to extract JSON response from {resolved_endpoint}: {json_error}"
|
||||
)
|
||||
|
||||
def _query_response(self, response_json: Any, **context) -> Any:
|
||||
if not self.query:
|
||||
return response_json # primitive value
|
||||
|
||||
resolved_query = resolve_any_context_variables(self.query, **context)
|
||||
try:
|
||||
expression = parse(resolved_query)
|
||||
matches = expression.find(response_json)
|
||||
if not matches:
|
||||
message = f"No matches found for the JSONPath query: {resolved_query}"
|
||||
self.logger.info(message)
|
||||
raise ConditionEvaluationFailed(message)
|
||||
except (JsonPathLexerError, JsonPathParserError) as jsonpath_err:
|
||||
self.logger.error(f"JSONPath error occurred: {jsonpath_err}")
|
||||
raise ConditionEvaluationFailed(
|
||||
f"JSONPath error: {jsonpath_err}"
|
||||
) from jsonpath_err
|
||||
|
||||
if len(matches) > 1:
|
||||
message = f"Ambiguous JSONPath query - multiple matches found for: {resolved_query}"
|
||||
self.logger.info(message)
|
||||
raise JsonRequestException(message)
|
||||
|
||||
result = matches[0].value
|
||||
return result
|
||||
|
||||
|
||||
class JSONPathField(Field):
|
||||
default_error_messages = {
|
||||
"invalidType": "Expression of type {value} is not valid for JSONPath",
|
||||
"invalid": "'{value}' is not a valid JSONPath expression",
|
||||
}
|
||||
|
||||
def _deserialize(self, value, attr, data, **kwargs):
|
||||
if not isinstance(value, str):
|
||||
raise self.make_error("invalidType", value=type(value))
|
||||
try:
|
||||
if not string_contains_context_variable(value):
|
||||
parse(value)
|
||||
except (JsonPathLexerError, JsonPathParserError) as e:
|
||||
raise self.make_error("invalid", value=value) from e
|
||||
return value
|
||||
|
||||
|
||||
class BaseJsonRequestCondition(ExecutionCallAccessControlCondition, ABC):
|
||||
def verify(self, **context) -> Tuple[bool, Any]:
|
||||
"""
|
||||
Verifies the JSON condition.
|
||||
"""
|
||||
result = self.execution_call.execute(**context)
|
||||
result_for_eval = process_result_for_condition_eval(result)
|
||||
|
||||
resolved_return_value_test = self.return_value_test.with_resolved_context(
|
||||
**context
|
||||
)
|
||||
eval_result = resolved_return_value_test.eval(result_for_eval) # test
|
||||
return eval_result, result
|
|
@ -0,0 +1,179 @@
|
|||
from abc import ABC
|
||||
from typing import Any, Optional
|
||||
from uuid import uuid4
|
||||
|
||||
from marshmallow import ValidationError, fields, post_load, validate, validates
|
||||
from marshmallow.fields import Url
|
||||
from typing_extensions import override
|
||||
|
||||
from nucypher.policy.conditions.context import is_context_variable
|
||||
from nucypher.policy.conditions.exceptions import (
|
||||
JsonRequestException,
|
||||
)
|
||||
from nucypher.policy.conditions.json.base import (
|
||||
BaseJsonRequestCondition,
|
||||
HTTPMethod,
|
||||
JSONPathField,
|
||||
JsonRequestCall,
|
||||
)
|
||||
from nucypher.policy.conditions.lingo import (
|
||||
AnyField,
|
||||
ConditionType,
|
||||
ExecutionCallAccessControlCondition,
|
||||
ReturnValueTest,
|
||||
)
|
||||
|
||||
|
||||
class BaseJsonRPCCall(JsonRequestCall, ABC):
|
||||
class Schema(JsonRequestCall.Schema):
|
||||
method = fields.Str(required=True)
|
||||
params = AnyField(required=False, allow_none=True)
|
||||
query = JSONPathField(required=False, allow_none=True)
|
||||
authorization_token = fields.Str(required=False, allow_none=True)
|
||||
|
||||
@validates("authorization_token")
|
||||
def validate_auth_token(self, value):
|
||||
if value and not is_context_variable(value):
|
||||
raise ValidationError(
|
||||
f"Invalid value for authorization token; expected a context variable, but got '{value}'"
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
method: str,
|
||||
params: Optional[Any] = None,
|
||||
query: Optional[str] = None,
|
||||
authorization_token: Optional[str] = None,
|
||||
):
|
||||
self.method = method
|
||||
self.params = params or []
|
||||
|
||||
parameters = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": self.method,
|
||||
"params": self.params,
|
||||
"id": str(uuid4()), # any random id will do
|
||||
}
|
||||
super().__init__(
|
||||
http_method=HTTPMethod.POST,
|
||||
parameters=parameters,
|
||||
query=query,
|
||||
authorization_token=authorization_token,
|
||||
)
|
||||
|
||||
@override
|
||||
def _execute(self, endpoint, **context):
|
||||
data = self._fetch(endpoint, **context)
|
||||
|
||||
# response contains a value for either "result" or "error"
|
||||
error = data.get("error")
|
||||
if error:
|
||||
raise JsonRequestException(
|
||||
f"JSON RPC Request failed with error in response: code={error.get('code')}, msg={error.get('message')}"
|
||||
)
|
||||
|
||||
# obtain result first then perform query
|
||||
result = data.get("result")
|
||||
if not result:
|
||||
raise JsonRequestException(
|
||||
f"Malformed JSON RPC response, no 'result' field - data={data}"
|
||||
)
|
||||
query_result = self._query_response(result, **context)
|
||||
return query_result
|
||||
|
||||
|
||||
class JsonEndpointRPCCall(BaseJsonRPCCall):
|
||||
class Schema(BaseJsonRPCCall.Schema):
|
||||
endpoint = Url(required=True, relative=False, schemes=["https"])
|
||||
|
||||
@post_load
|
||||
def make(self, data, **kwargs):
|
||||
return JsonEndpointRPCCall(**data)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
endpoint: str,
|
||||
method: str,
|
||||
params: Optional[Any] = None,
|
||||
query: Optional[str] = None,
|
||||
authorization_token: Optional[str] = None,
|
||||
):
|
||||
self.endpoint = endpoint
|
||||
super().__init__(
|
||||
method=method,
|
||||
params=params,
|
||||
query=query,
|
||||
authorization_token=authorization_token,
|
||||
)
|
||||
|
||||
@override
|
||||
def execute(self, **context) -> Any:
|
||||
return super()._execute(endpoint=self.endpoint, **context)
|
||||
|
||||
|
||||
class JsonRpcCondition(BaseJsonRequestCondition):
|
||||
"""
|
||||
A JSON RPC condition is a condition that can be evaluated by performing a POST on a JSON
|
||||
HTTPS endpoint. The response must return an HTTP 200 with valid JSON RPC 2.0 response.
|
||||
The response will be deserialized as JSON and parsed using jsonpath.
|
||||
"""
|
||||
|
||||
EXECUTION_CALL_TYPE = JsonEndpointRPCCall
|
||||
CONDITION_TYPE = ConditionType.JSONRPC.value
|
||||
|
||||
class Schema(
|
||||
ExecutionCallAccessControlCondition.Schema, JsonEndpointRPCCall.Schema
|
||||
):
|
||||
condition_type = fields.Str(
|
||||
validate=validate.Equal(ConditionType.JSONRPC.value), required=True
|
||||
)
|
||||
|
||||
@post_load
|
||||
def make(self, data, **kwargs):
|
||||
return JsonRpcCondition(**data)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
endpoint: str,
|
||||
method: str,
|
||||
return_value_test: ReturnValueTest,
|
||||
params: Optional[Any] = None,
|
||||
query: Optional[str] = None,
|
||||
authorization_token: Optional[str] = None,
|
||||
condition_type: Optional[str] = ConditionType.JSONRPC.value,
|
||||
name: Optional[str] = None,
|
||||
):
|
||||
super().__init__(
|
||||
endpoint=endpoint,
|
||||
method=method,
|
||||
return_value_test=return_value_test,
|
||||
params=params,
|
||||
query=query,
|
||||
authorization_token=authorization_token,
|
||||
condition_type=condition_type,
|
||||
name=name,
|
||||
)
|
||||
|
||||
@property
|
||||
def endpoint(self):
|
||||
return self.execution_call.endpoint
|
||||
|
||||
@property
|
||||
def method(self):
|
||||
return self.execution_call.method
|
||||
|
||||
@property
|
||||
def params(self):
|
||||
return self.execution_call.params
|
||||
|
||||
@property
|
||||
def query(self):
|
||||
return self.execution_call.query
|
||||
|
||||
@property
|
||||
def authorization_token(self):
|
||||
return self.execution_call.authorization_token
|
||||
|
||||
@property
|
||||
def timeout(self):
|
||||
return self.execution_call.timeout
|
|
@ -0,0 +1,17 @@
|
|||
from typing import Any
|
||||
|
||||
|
||||
def process_result_for_condition_eval(result: Any):
|
||||
# strings that are not already quoted will cause a problem for literal_eval
|
||||
if not isinstance(result, str):
|
||||
return result
|
||||
|
||||
# check if already quoted; if not, quote it
|
||||
if not (
|
||||
(result.startswith("'") and result.endswith("'"))
|
||||
or (result.startswith('"') and result.endswith('"'))
|
||||
):
|
||||
quote_type_to_use = '"' if "'" in result else "'"
|
||||
result = f"{quote_type_to_use}{result}{quote_type_to_use}"
|
||||
|
||||
return result
|
|
@ -0,0 +1,154 @@
|
|||
from typing import Any, Optional, Tuple
|
||||
|
||||
import jwt
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives.asymmetric import ec, rsa
|
||||
from cryptography.hazmat.primitives.serialization import load_pem_public_key
|
||||
from marshmallow import ValidationError, fields, post_load, validate, validates
|
||||
|
||||
from nucypher.policy.conditions.base import AccessControlCondition, ExecutionCall
|
||||
from nucypher.policy.conditions.context import (
|
||||
is_context_variable,
|
||||
resolve_any_context_variables,
|
||||
)
|
||||
from nucypher.policy.conditions.exceptions import InvalidCondition, JWTException
|
||||
from nucypher.policy.conditions.lingo import ConditionType
|
||||
from nucypher.utilities.logging import Logger
|
||||
|
||||
|
||||
class JWTVerificationCall(ExecutionCall):
|
||||
|
||||
_valid_jwt_algorithms = (
|
||||
"ES256",
|
||||
"RS256",
|
||||
) # https://datatracker.ietf.org/doc/html/rfc7518#section-3.1
|
||||
|
||||
SECP_CURVE_FOR_ES256 = "secp256r1"
|
||||
|
||||
class Schema(ExecutionCall.Schema):
|
||||
jwt_token = fields.Str(required=True)
|
||||
# TODO: See #3572 for a discussion about deprecating this in favour of the expected issuer
|
||||
public_key = fields.Str(
|
||||
required=True
|
||||
) # required? maybe a valid PK certificate passed by requester?
|
||||
expected_issuer = fields.Str(required=False, allow_none=True)
|
||||
# TODO: StringOrURI as per the spec.
|
||||
|
||||
@post_load
|
||||
def make(self, data, **kwargs):
|
||||
return JWTVerificationCall(**data)
|
||||
|
||||
@validates("jwt_token")
|
||||
def validate_jwt_token(self, value):
|
||||
if not is_context_variable(value):
|
||||
raise ValidationError(
|
||||
f"Invalid value for JWT token; expected a context variable, but got '{value}'"
|
||||
)
|
||||
|
||||
@validates("public_key")
|
||||
def validate_public_key(self, value):
|
||||
try:
|
||||
public_key = load_pem_public_key(
|
||||
value.encode(), backend=default_backend()
|
||||
)
|
||||
if isinstance(public_key, rsa.RSAPublicKey):
|
||||
return
|
||||
elif isinstance(public_key, ec.EllipticCurvePublicKey):
|
||||
curve = public_key.curve
|
||||
if curve.name != JWTVerificationCall.SECP_CURVE_FOR_ES256:
|
||||
raise ValidationError(
|
||||
f"Invalid EC public key curve: {curve.name}"
|
||||
)
|
||||
except Exception as e:
|
||||
raise ValidationError(f"Invalid public key format: {str(e)}")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
jwt_token: str,
|
||||
public_key: str,
|
||||
expected_issuer: Optional[str] = None,
|
||||
):
|
||||
self.jwt_token = jwt_token
|
||||
self.public_key = public_key
|
||||
self.expected_issuer = expected_issuer
|
||||
|
||||
self.logger = Logger(__name__)
|
||||
|
||||
super().__init__()
|
||||
|
||||
def execute(self, **context) -> Any:
|
||||
jwt_token = resolve_any_context_variables(self.jwt_token, **context)
|
||||
|
||||
require = []
|
||||
if self.expected_issuer:
|
||||
require.append("iss")
|
||||
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
jwt=jwt_token,
|
||||
key=self.public_key,
|
||||
algorithms=self._valid_jwt_algorithms,
|
||||
options=dict(require=require),
|
||||
issuer=self.expected_issuer,
|
||||
)
|
||||
except jwt.exceptions.InvalidAlgorithmError:
|
||||
raise JWTException(f"valid algorithms: {self._valid_jwt_algorithms}")
|
||||
except jwt.exceptions.InvalidTokenError as e:
|
||||
raise JWTException(e)
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
class JWTCondition(AccessControlCondition):
|
||||
"""
|
||||
A JWT condition can be satisfied by presenting a valid JWT token, which not only is
|
||||
required to be cryptographically verifiable, but also must fulfill certain additional
|
||||
restrictions defined in the condition.
|
||||
"""
|
||||
|
||||
CONDITION_TYPE = ConditionType.JWT.value
|
||||
|
||||
class Schema(AccessControlCondition.Schema, JWTVerificationCall.Schema):
|
||||
condition_type = fields.Str(
|
||||
validate=validate.Equal(ConditionType.JWT.value), required=True
|
||||
)
|
||||
|
||||
@post_load
|
||||
def make(self, data, **kwargs):
|
||||
return JWTCondition(**data)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
jwt_token: str,
|
||||
public_key: str,
|
||||
condition_type: str = ConditionType.JWT.value,
|
||||
name: Optional[str] = None,
|
||||
expected_issuer: Optional[str] = None,
|
||||
):
|
||||
try:
|
||||
self.execution_call = JWTVerificationCall(
|
||||
jwt_token=jwt_token,
|
||||
public_key=public_key,
|
||||
expected_issuer=expected_issuer,
|
||||
)
|
||||
except ExecutionCall.InvalidExecutionCall as e:
|
||||
raise InvalidCondition(str(e)) from e
|
||||
|
||||
super().__init__(condition_type=condition_type, name=name)
|
||||
|
||||
@property
|
||||
def jwt_token(self):
|
||||
return self.execution_call.jwt_token
|
||||
|
||||
@property
|
||||
def public_key(self):
|
||||
return self.execution_call.public_key
|
||||
|
||||
@property
|
||||
def expected_issuer(self):
|
||||
return self.execution_call.expected_issuer
|
||||
|
||||
def verify(self, **context) -> Tuple[bool, Any]:
|
||||
payload = self.execution_call.execute(**context)
|
||||
result = True
|
||||
return result, payload
|
|
@ -20,10 +20,15 @@ from marshmallow import (
|
|||
from marshmallow.validate import OneOf, Range
|
||||
from packaging.version import parse as parse_version
|
||||
|
||||
from nucypher.policy.conditions.base import AccessControlCondition, _Serializable
|
||||
from nucypher.policy.conditions.base import (
|
||||
AccessControlCondition,
|
||||
ExecutionCall,
|
||||
MultiConditionAccessControl,
|
||||
_Serializable,
|
||||
)
|
||||
from nucypher.policy.conditions.context import (
|
||||
_resolve_context_variable,
|
||||
is_context_variable,
|
||||
resolve_any_context_variables,
|
||||
)
|
||||
from nucypher.policy.conditions.exceptions import (
|
||||
InvalidCondition,
|
||||
|
@ -31,7 +36,53 @@ from nucypher.policy.conditions.exceptions import (
|
|||
ReturnValueEvaluationError,
|
||||
)
|
||||
from nucypher.policy.conditions.types import ConditionDict, Lingo
|
||||
from nucypher.policy.conditions.utils import CamelCaseSchema
|
||||
from nucypher.policy.conditions.utils import (
|
||||
CamelCaseSchema,
|
||||
ConditionProviderManager,
|
||||
check_and_convert_big_int_string_to_int,
|
||||
)
|
||||
|
||||
|
||||
class AnyField(fields.Field):
|
||||
"""
|
||||
Catch all field for all data types received in JSON.
|
||||
However, `taco-web` will provide bigints as strings since typescript can't handle large
|
||||
numbers as integers, so those need converting to integers.
|
||||
"""
|
||||
|
||||
def _convert_any_big_ints_from_string(self, value):
|
||||
if isinstance(value, list):
|
||||
return [self._convert_any_big_ints_from_string(item) for item in value]
|
||||
elif isinstance(value, dict):
|
||||
return {
|
||||
k: self._convert_any_big_ints_from_string(v) for k, v in value.items()
|
||||
}
|
||||
elif isinstance(value, str):
|
||||
return check_and_convert_big_int_string_to_int(value)
|
||||
|
||||
return value
|
||||
|
||||
def _serialize(self, value, attr, obj, **kwargs):
|
||||
return value
|
||||
|
||||
def _deserialize(self, value, attr, data, **kwargs):
|
||||
return self._convert_any_big_ints_from_string(value)
|
||||
|
||||
|
||||
class AnyLargeIntegerField(fields.Int):
|
||||
"""
|
||||
Integer field that also allows for big int values for large numbers
|
||||
to be provided from `taco-web`. BigInts will be used for integer values > MAX_SAFE_INTEGER.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def _deserialize(self, value, attr, data, **kwargs):
|
||||
if isinstance(value, str):
|
||||
value = check_and_convert_big_int_string_to_int(value)
|
||||
|
||||
return super()._deserialize(value, attr, data, **kwargs)
|
||||
|
||||
|
||||
class _ConditionField(fields.Dict):
|
||||
|
@ -52,19 +103,8 @@ class _ConditionField(fields.Dict):
|
|||
instance = condition_class.from_dict(condition_data)
|
||||
return instance
|
||||
|
||||
#
|
||||
# CONDITION = BASE_CONDITION | COMPOUND_CONDITION
|
||||
#
|
||||
# BASE_CONDITION = {
|
||||
# // ..
|
||||
# }
|
||||
#
|
||||
# COMPOUND_CONDITION = {
|
||||
# "operator": OPERATOR,
|
||||
# "operands": [CONDITION*]
|
||||
# }
|
||||
|
||||
|
||||
# CONDITION = TIME | CONTRACT | RPC | JSON_API | JSON_RPC | JWT | COMPOUND | SEQUENTIAL | IF_THEN_ELSE_CONDITION
|
||||
class ConditionType(Enum):
|
||||
"""
|
||||
Defines the types of conditions that can be evaluated.
|
||||
|
@ -73,14 +113,32 @@ class ConditionType(Enum):
|
|||
TIME = "time"
|
||||
CONTRACT = "contract"
|
||||
RPC = "rpc"
|
||||
JSONAPI = "json-api"
|
||||
JSONRPC = "json-rpc"
|
||||
JWT = "jwt"
|
||||
COMPOUND = "compound"
|
||||
SEQUENTIAL = "sequential"
|
||||
IF_THEN_ELSE = "if-then-else"
|
||||
|
||||
@classmethod
|
||||
def values(cls) -> List[str]:
|
||||
return [condition.value for condition in cls]
|
||||
|
||||
|
||||
class CompoundAccessControlCondition(AccessControlCondition):
|
||||
class CompoundAccessControlCondition(MultiConditionAccessControl):
|
||||
"""
|
||||
A combination of two or more conditions connected by logical operators such as AND, OR, NOT.
|
||||
|
||||
CompoundCondition grammar:
|
||||
OPERATOR = AND | OR | NOT
|
||||
|
||||
COMPOUND_CONDITION = {
|
||||
"name": ... (Optional)
|
||||
"conditionType": "compound",
|
||||
"operator": OPERATOR,
|
||||
"operands": [CONDITION*]
|
||||
}
|
||||
"""
|
||||
AND_OPERATOR = "and"
|
||||
OR_OPERATOR = "or"
|
||||
NOT_OPERATOR = "not"
|
||||
|
@ -92,28 +150,35 @@ class CompoundAccessControlCondition(AccessControlCondition):
|
|||
def _validate_operator_and_operands(
|
||||
cls,
|
||||
operator: str,
|
||||
operands: List,
|
||||
exception_class: Union[Type[ValidationError], Type[InvalidCondition]],
|
||||
operands: List[AccessControlCondition],
|
||||
):
|
||||
if operator not in cls.OPERATORS:
|
||||
raise exception_class(f"{operator} is not a valid operator")
|
||||
|
||||
if operator == cls.NOT_OPERATOR:
|
||||
if len(operands) != 1:
|
||||
raise exception_class(
|
||||
f"Only 1 operand permitted for '{operator}' compound condition"
|
||||
)
|
||||
elif len(operands) < 2:
|
||||
raise exception_class(
|
||||
f"Minimum of 2 operand needed for '{operator}' compound condition"
|
||||
raise ValidationError(
|
||||
field_name="operator", message=f"{operator} is not a valid operator"
|
||||
)
|
||||
|
||||
class Schema(CamelCaseSchema):
|
||||
SKIP_VALUES = (None,)
|
||||
num_operands = len(operands)
|
||||
if operator == cls.NOT_OPERATOR:
|
||||
if num_operands != 1:
|
||||
raise ValidationError(
|
||||
field_name="operands",
|
||||
message=f"Only 1 operand permitted for '{operator}' compound condition",
|
||||
)
|
||||
elif num_operands < 2:
|
||||
raise ValidationError(
|
||||
field_name="operands",
|
||||
message=f"Minimum of 2 operand needed for '{operator}' compound condition",
|
||||
)
|
||||
elif num_operands > cls.MAX_NUM_CONDITIONS:
|
||||
raise ValidationError(
|
||||
field_name="operands",
|
||||
message="Maximum of {cls.MAX_NUM_CONDITIONS} operands allowed for '{operator}' compound condition",
|
||||
)
|
||||
|
||||
class Schema(AccessControlCondition.Schema):
|
||||
condition_type = fields.Str(
|
||||
validate=validate.Equal(ConditionType.COMPOUND.value), required=True
|
||||
)
|
||||
name = fields.Str(required=False)
|
||||
operator = fields.Str(required=True)
|
||||
operands = fields.List(_ConditionField, required=True)
|
||||
|
||||
|
@ -126,7 +191,10 @@ class CompoundAccessControlCondition(AccessControlCondition):
|
|||
operator = data["operator"]
|
||||
operands = data["operands"]
|
||||
CompoundAccessControlCondition._validate_operator_and_operands(
|
||||
operator, operands, ValidationError
|
||||
operator, operands
|
||||
)
|
||||
CompoundAccessControlCondition._validate_multi_condition_nesting(
|
||||
conditions=operands, field_name="operands"
|
||||
)
|
||||
|
||||
@post_load
|
||||
|
@ -146,18 +214,14 @@ class CompoundAccessControlCondition(AccessControlCondition):
|
|||
"operands": [CONDITION*]
|
||||
}
|
||||
"""
|
||||
if condition_type != self.CONDITION_TYPE:
|
||||
raise InvalidCondition(
|
||||
f"{self.__class__.__name__} must be instantiated with the {self.CONDITION_TYPE} type."
|
||||
)
|
||||
|
||||
self._validate_operator_and_operands(operator, operands, InvalidCondition)
|
||||
|
||||
self.condition_type = condition_type
|
||||
self.operator = operator
|
||||
self.operands = operands
|
||||
self.condition_type = condition_type
|
||||
self.name = name
|
||||
|
||||
super().__init__(
|
||||
condition_type=condition_type,
|
||||
name=name,
|
||||
)
|
||||
|
||||
self.id = md5(bytes(self)).hexdigest()[:6]
|
||||
|
||||
def __repr__(self):
|
||||
|
@ -185,6 +249,10 @@ class CompoundAccessControlCondition(AccessControlCondition):
|
|||
|
||||
return overall_result, values
|
||||
|
||||
@property
|
||||
def conditions(self):
|
||||
return self.operands
|
||||
|
||||
|
||||
class OrCompoundCondition(CompoundAccessControlCondition):
|
||||
def __init__(self, operands: List[AccessControlCondition]):
|
||||
|
@ -211,6 +279,275 @@ _COMPARATOR_FUNCTIONS = {
|
|||
}
|
||||
|
||||
|
||||
class ConditionVariable(_Serializable):
|
||||
class Schema(CamelCaseSchema):
|
||||
var_name = fields.Str(required=True) # TODO: should this be required?
|
||||
condition = _ConditionField(required=True)
|
||||
|
||||
@post_load
|
||||
def make(self, data, **kwargs):
|
||||
return ConditionVariable(**data)
|
||||
|
||||
def __init__(self, var_name: str, condition: AccessControlCondition):
|
||||
self.var_name = var_name
|
||||
self.condition = condition
|
||||
|
||||
|
||||
class SequentialAccessControlCondition(MultiConditionAccessControl):
|
||||
"""
|
||||
A series of conditions that are evaluated in a specific order, where the result of one
|
||||
condition can be used in subsequent conditions.
|
||||
|
||||
SequentialCondition grammar:
|
||||
CONDITION_VARIABLE = {
|
||||
"varName": STR,
|
||||
"condition": {
|
||||
CONDITION
|
||||
}
|
||||
}
|
||||
|
||||
SEQUENTIAL_CONDITION = {
|
||||
"name": ... (Optional)
|
||||
"conditionType": "sequential",
|
||||
"conditionVariables": [CONDITION_VARIABLE*]
|
||||
}
|
||||
"""
|
||||
|
||||
CONDITION_TYPE = ConditionType.SEQUENTIAL.value
|
||||
|
||||
@classmethod
|
||||
def _validate_condition_variables(
|
||||
cls,
|
||||
condition_variables: List[ConditionVariable],
|
||||
):
|
||||
if not condition_variables or len(condition_variables) < 2:
|
||||
raise ValidationError(
|
||||
field_name="condition_variables",
|
||||
message="At least two conditions must be specified",
|
||||
)
|
||||
|
||||
if len(condition_variables) > cls.MAX_NUM_CONDITIONS:
|
||||
raise ValidationError(
|
||||
field_name="condition_variables",
|
||||
message=f"Maximum of {cls.MAX_NUM_CONDITIONS} conditions are allowed",
|
||||
)
|
||||
|
||||
# check for duplicate var names
|
||||
var_names = set()
|
||||
for condition_variable in condition_variables:
|
||||
if condition_variable.var_name in var_names:
|
||||
raise ValidationError(
|
||||
field_name="condition_variables",
|
||||
message=f"Duplicate variable names are not allowed - {condition_variable.var_name}",
|
||||
)
|
||||
var_names.add(condition_variable.var_name)
|
||||
|
||||
class Schema(AccessControlCondition.Schema):
|
||||
condition_type = fields.Str(
|
||||
validate=validate.Equal(ConditionType.SEQUENTIAL.value), required=True
|
||||
)
|
||||
condition_variables = fields.List(
|
||||
fields.Nested(ConditionVariable.Schema(), required=True)
|
||||
)
|
||||
|
||||
# maintain field declaration ordering
|
||||
class Meta:
|
||||
ordered = True
|
||||
|
||||
@validates("condition_variables")
|
||||
def validate_condition_variables(self, value):
|
||||
SequentialAccessControlCondition._validate_condition_variables(value)
|
||||
conditions = [cv.condition for cv in value]
|
||||
SequentialAccessControlCondition._validate_multi_condition_nesting(
|
||||
conditions=conditions, field_name="condition_variables"
|
||||
)
|
||||
|
||||
@post_load
|
||||
def make(self, data, **kwargs):
|
||||
return SequentialAccessControlCondition(**data)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
condition_variables: List[ConditionVariable],
|
||||
condition_type: str = CONDITION_TYPE,
|
||||
name: Optional[str] = None,
|
||||
):
|
||||
self.condition_variables = condition_variables
|
||||
super().__init__(
|
||||
condition_type=condition_type,
|
||||
name=name,
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
r = f"{self.__class__.__name__}(num_condition_variables={len(self.condition_variables)})"
|
||||
return r
|
||||
|
||||
# TODO - think about not dereferencing context but using a dict;
|
||||
# may allows more freedom for params
|
||||
def verify(
|
||||
self, providers: ConditionProviderManager, **context
|
||||
) -> Tuple[bool, Any]:
|
||||
values = []
|
||||
latest_success = False
|
||||
inner_context = dict(context) # don't modify passed in context - use a copy
|
||||
# resolve variables
|
||||
for condition_variable in self.condition_variables:
|
||||
latest_success, result = condition_variable.condition.verify(
|
||||
providers=providers, **inner_context
|
||||
)
|
||||
values.append(result)
|
||||
if not latest_success:
|
||||
# short circuit due to failed condition
|
||||
break
|
||||
|
||||
inner_context[f":{condition_variable.var_name}"] = result
|
||||
|
||||
return latest_success, values
|
||||
|
||||
@property
|
||||
def conditions(self):
|
||||
return [
|
||||
condition_variable.condition
|
||||
for condition_variable in self.condition_variables
|
||||
]
|
||||
|
||||
|
||||
class _ElseConditionField(fields.Field):
|
||||
"""
|
||||
Serializes/Deserializes else conditions for IfThenElseCondition. This field represents either a
|
||||
Condition or a boolean value.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def _serialize(self, value, attr, obj, **kwargs):
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
|
||||
return value.to_dict()
|
||||
|
||||
def _deserialize(self, value, attr, data, **kwargs):
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
|
||||
lingo_version = self.context.get("lingo_version")
|
||||
condition_data = value
|
||||
condition_class = ConditionLingo.resolve_condition_class(
|
||||
condition=condition_data, version=lingo_version
|
||||
)
|
||||
instance = condition_class.from_dict(condition_data)
|
||||
return instance
|
||||
|
||||
|
||||
class IfThenElseCondition(MultiConditionAccessControl):
|
||||
"""
|
||||
A condition that represents simple if-then-else logic.
|
||||
|
||||
IF_THEN_ELSE_CONDITION = {
|
||||
"conditionType": "if-then-else",
|
||||
"ifCondition": CONDITION,
|
||||
"thenCondition": CONDITION,
|
||||
"elseCondition": CONDITION | true | false,
|
||||
}
|
||||
"""
|
||||
|
||||
CONDITION_TYPE = ConditionType.IF_THEN_ELSE.value
|
||||
|
||||
MAX_NUM_CONDITIONS = 3 # only ever max of 3 (if, then, else)
|
||||
|
||||
class Schema(AccessControlCondition.Schema):
|
||||
condition_type = fields.Str(
|
||||
validate=validate.Equal(ConditionType.IF_THEN_ELSE.value), required=True
|
||||
)
|
||||
if_condition = _ConditionField(required=True)
|
||||
then_condition = _ConditionField(required=True)
|
||||
else_condition = _ElseConditionField(required=True)
|
||||
|
||||
# maintain field declaration ordering
|
||||
class Meta:
|
||||
ordered = True
|
||||
|
||||
@staticmethod
|
||||
def _validate_nested_conditions(field_name, condition):
|
||||
IfThenElseCondition._validate_multi_condition_nesting(
|
||||
conditions=[condition],
|
||||
field_name=field_name,
|
||||
)
|
||||
|
||||
@validates("if_condition")
|
||||
def validate_if_condition(self, value):
|
||||
self._validate_nested_conditions("if_condition", value)
|
||||
|
||||
@validates("then_condition")
|
||||
def validate_then_condition(self, value):
|
||||
self._validate_nested_conditions("then_condition", value)
|
||||
|
||||
@validates("else_condition")
|
||||
def validate_else_condition(self, value):
|
||||
if isinstance(value, AccessControlCondition):
|
||||
self._validate_nested_conditions("else_condition", value)
|
||||
|
||||
@post_load
|
||||
def make(self, data, **kwargs):
|
||||
return IfThenElseCondition(**data)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
if_condition: AccessControlCondition,
|
||||
then_condition: AccessControlCondition,
|
||||
else_condition: Union[AccessControlCondition, bool],
|
||||
condition_type: str = CONDITION_TYPE,
|
||||
name: Optional[str] = None,
|
||||
):
|
||||
self.if_condition = if_condition
|
||||
self.then_condition = then_condition
|
||||
self.else_condition = else_condition
|
||||
super().__init__(condition_type=condition_type, name=name)
|
||||
|
||||
def __repr__(self):
|
||||
r = (
|
||||
f"{self.__class__.__name__}("
|
||||
f"if={self.if_condition.__class__.__name__}, "
|
||||
f"then={self.then_condition.__class__.__name__}, "
|
||||
f"else={self.else_condition.__class__.__name__}"
|
||||
f")"
|
||||
)
|
||||
return r
|
||||
|
||||
@property
|
||||
def conditions(self):
|
||||
values = [self.if_condition, self.then_condition]
|
||||
if isinstance(self.else_condition, AccessControlCondition):
|
||||
values.append(self.else_condition)
|
||||
|
||||
return values
|
||||
|
||||
def verify(self, *args, **kwargs) -> Tuple[bool, Any]:
|
||||
values = []
|
||||
|
||||
# if
|
||||
if_result, if_value = self.if_condition.verify(*args, **kwargs)
|
||||
values.append(if_value)
|
||||
|
||||
# then
|
||||
if if_result:
|
||||
then_result, then_value = self.then_condition.verify(*args, **kwargs)
|
||||
values.append(then_value)
|
||||
return then_result, values
|
||||
|
||||
# else
|
||||
if isinstance(self.else_condition, AccessControlCondition):
|
||||
# actual condition
|
||||
else_result, else_value = self.else_condition.verify(*args, **kwargs)
|
||||
else:
|
||||
# boolean value
|
||||
else_result, else_value = self.else_condition, self.else_condition
|
||||
|
||||
values.append(else_value)
|
||||
return else_result, values
|
||||
|
||||
|
||||
class ReturnValueTest:
|
||||
class InvalidExpression(ValueError):
|
||||
pass
|
||||
|
@ -220,10 +557,12 @@ class ReturnValueTest:
|
|||
class ReturnValueTestSchema(CamelCaseSchema):
|
||||
SKIP_VALUES = (None,)
|
||||
comparator = fields.Str(required=True, validate=OneOf(_COMPARATOR_FUNCTIONS))
|
||||
value = fields.Raw(
|
||||
value = AnyField(
|
||||
allow_none=False, required=True
|
||||
) # any valid type (excludes None)
|
||||
index = fields.Int(strict=True, required=False, validate=Range(min=0))
|
||||
index = fields.Int(
|
||||
strict=True, required=False, validate=Range(min=0), allow_none=True
|
||||
)
|
||||
|
||||
@post_load
|
||||
def make(self, data, **kwargs):
|
||||
|
@ -318,8 +657,10 @@ class ReturnValueTest:
|
|||
result = _COMPARATOR_FUNCTIONS[self.comparator](left_operand, right_operand)
|
||||
return result
|
||||
|
||||
def with_resolved_context(self, **context):
|
||||
value = _resolve_context_variable(self.value, **context)
|
||||
def with_resolved_context(
|
||||
self, providers: Optional[ConditionProviderManager] = None, **context
|
||||
):
|
||||
value = resolve_any_context_variables(self.value, providers, **context)
|
||||
return ReturnValueTest(self.comparator, value=value, index=self.index)
|
||||
|
||||
|
||||
|
@ -356,16 +697,6 @@ class ConditionLingo(_Serializable):
|
|||
"""
|
||||
|
||||
def __init__(self, condition: AccessControlCondition, version: str = VERSION):
|
||||
"""
|
||||
CONDITION = BASE_CONDITION | COMPOUND_CONDITION
|
||||
BASE_CONDITION = {
|
||||
// ..
|
||||
}
|
||||
COMPOUND_CONDITION = {
|
||||
"operator": OPERATOR,
|
||||
"operands": [CONDITION*]
|
||||
}
|
||||
"""
|
||||
self.condition = condition
|
||||
self.check_version_compatibility(version)
|
||||
self.version = version
|
||||
|
@ -411,11 +742,13 @@ class ConditionLingo(_Serializable):
|
|||
cls, condition: ConditionDict, version: int = None
|
||||
) -> Type[AccessControlCondition]:
|
||||
"""
|
||||
TODO: This feels like a jenky way to resolve data types from JSON blobs, but it works.
|
||||
Inspects a given bloc of JSON and attempts to resolve it's intended datatype within the
|
||||
Inspects a given block of JSON and attempts to resolve it's intended datatype within the
|
||||
conditions expression framework.
|
||||
"""
|
||||
from nucypher.policy.conditions.evm import ContractCondition, RPCCondition
|
||||
from nucypher.policy.conditions.json.api import JsonApiCondition
|
||||
from nucypher.policy.conditions.json.rpc import JsonRpcCondition
|
||||
from nucypher.policy.conditions.jwt import JWTCondition
|
||||
from nucypher.policy.conditions.time import TimeCondition
|
||||
|
||||
# version logical adjustments can be made here as required
|
||||
|
@ -426,12 +759,17 @@ class ConditionLingo(_Serializable):
|
|||
ContractCondition,
|
||||
RPCCondition,
|
||||
CompoundAccessControlCondition,
|
||||
JsonApiCondition,
|
||||
JsonRpcCondition,
|
||||
JWTCondition,
|
||||
SequentialAccessControlCondition,
|
||||
IfThenElseCondition,
|
||||
):
|
||||
if condition.CONDITION_TYPE == condition_type:
|
||||
return condition
|
||||
|
||||
raise InvalidConditionLingo(
|
||||
f"Cannot resolve condition lingo with condition type {condition_type}"
|
||||
f"Cannot resolve condition lingo, {condition}, with condition type {condition_type}"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
@ -440,3 +778,46 @@ class ConditionLingo(_Serializable):
|
|||
raise InvalidConditionLingo(
|
||||
f"Version provided, {version}, is incompatible with current version {cls.VERSION}"
|
||||
)
|
||||
|
||||
|
||||
class ExecutionCallAccessControlCondition(AccessControlCondition):
|
||||
"""
|
||||
Conditions that utilize underlying ExecutionCall objects.
|
||||
"""
|
||||
|
||||
EXECUTION_CALL_TYPE = NotImplemented
|
||||
|
||||
class Schema(AccessControlCondition.Schema):
|
||||
return_value_test = fields.Nested(
|
||||
ReturnValueTest.ReturnValueTestSchema(), required=True
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
condition_type: str,
|
||||
return_value_test: ReturnValueTest,
|
||||
name: Optional[str] = None,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
self.return_value_test = return_value_test
|
||||
|
||||
try:
|
||||
self.execution_call = self.EXECUTION_CALL_TYPE(*args, **kwargs)
|
||||
except ExecutionCall.InvalidExecutionCall as e:
|
||||
raise InvalidCondition(str(e)) from e
|
||||
|
||||
super().__init__(condition_type=condition_type, name=name)
|
||||
|
||||
def verify(self, *args, **kwargs) -> Tuple[bool, Any]:
|
||||
"""
|
||||
Verifies the condition is met by performing execution call and
|
||||
evaluating the return value test.
|
||||
"""
|
||||
result = self.execution_call.execute(*args, **kwargs)
|
||||
|
||||
resolved_return_value_test = self.return_value_test.with_resolved_context(
|
||||
**kwargs
|
||||
)
|
||||
eval_result = resolved_return_value_test.eval(result) # test
|
||||
return eval_result, result
|
||||
|
|
|
@ -1,33 +1,76 @@
|
|||
from typing import Any, List, Optional
|
||||
|
||||
from marshmallow import fields, post_load, validate
|
||||
from marshmallow.validate import Equal, OneOf
|
||||
from marshmallow import (
|
||||
ValidationError,
|
||||
fields,
|
||||
post_load,
|
||||
validate,
|
||||
validates,
|
||||
validates_schema,
|
||||
)
|
||||
from typing_extensions import override
|
||||
from web3 import Web3
|
||||
|
||||
from nucypher.policy.conditions.evm import _CONDITION_CHAINS, RPCCondition
|
||||
from nucypher.policy.conditions.exceptions import InvalidCondition
|
||||
from nucypher.policy.conditions.evm import RPCCall, RPCCondition
|
||||
from nucypher.policy.conditions.lingo import ConditionType, ReturnValueTest
|
||||
from nucypher.policy.conditions.utils import CamelCaseSchema
|
||||
|
||||
|
||||
class TimeRPCCall(RPCCall):
|
||||
METHOD = "blocktime"
|
||||
|
||||
class Schema(RPCCall.Schema):
|
||||
method = fields.Str(dump_default="blocktime", required=True)
|
||||
|
||||
@override
|
||||
@validates("method")
|
||||
def validate_method(self, value):
|
||||
if value != TimeRPCCall.METHOD:
|
||||
raise ValidationError(f"method name must be {TimeRPCCall.METHOD}.")
|
||||
|
||||
@validates("parameters")
|
||||
def validate_no_parameters(self, value):
|
||||
if value:
|
||||
raise ValidationError(
|
||||
f"{TimeRPCCall.METHOD}' does not take any parameters"
|
||||
)
|
||||
|
||||
@post_load
|
||||
def make(self, data, **kwargs):
|
||||
return TimeRPCCall(**data)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
chain: int,
|
||||
method: str = METHOD,
|
||||
parameters: Optional[List[Any]] = None,
|
||||
):
|
||||
super().__init__(chain=chain, method=method, parameters=parameters)
|
||||
|
||||
def _execute(self, w3: Web3, resolved_parameters: List[Any]) -> Any:
|
||||
"""Execute onchain read and return result."""
|
||||
# TODO may need to rethink as part of #3051 (multicall work).
|
||||
latest_block = w3.eth.get_block("latest")
|
||||
return latest_block.timestamp
|
||||
|
||||
|
||||
class TimeCondition(RPCCondition):
|
||||
METHOD = "blocktime"
|
||||
EXECUTION_CALL_TYPE = TimeRPCCall
|
||||
CONDITION_TYPE = ConditionType.TIME.value
|
||||
|
||||
class Schema(CamelCaseSchema):
|
||||
SKIP_VALUES = (None,)
|
||||
class Schema(RPCCondition.Schema, TimeRPCCall.Schema):
|
||||
condition_type = fields.Str(
|
||||
validate=validate.Equal(ConditionType.TIME.value), required=True
|
||||
)
|
||||
name = fields.Str(required=False)
|
||||
chain = fields.Int(
|
||||
required=True, strict=True, validate=OneOf(_CONDITION_CHAINS)
|
||||
)
|
||||
method = fields.Str(
|
||||
dump_default="blocktime", required=True, validate=Equal("blocktime")
|
||||
)
|
||||
return_value_test = fields.Nested(
|
||||
ReturnValueTest.ReturnValueTestSchema(), required=True
|
||||
)
|
||||
|
||||
@validates_schema
|
||||
def validate_expected_return_type(self, data, **kwargs):
|
||||
return_value_test = data.get("return_value_test")
|
||||
comparator_value = return_value_test.value
|
||||
if not isinstance(comparator_value, int):
|
||||
raise ValidationError(
|
||||
field_name="return_value_test",
|
||||
message=f"Invalid return value comparison type '{type(comparator_value)}'; must be an integer",
|
||||
)
|
||||
|
||||
@post_load
|
||||
def make(self, data, **kwargs):
|
||||
|
@ -41,40 +84,19 @@ class TimeCondition(RPCCondition):
|
|||
self,
|
||||
return_value_test: ReturnValueTest,
|
||||
chain: int,
|
||||
method: str = METHOD,
|
||||
condition_type: str = CONDITION_TYPE,
|
||||
method: str = TimeRPCCall.METHOD,
|
||||
condition_type: str = ConditionType.TIME.value,
|
||||
name: Optional[str] = None,
|
||||
):
|
||||
if method != self.METHOD:
|
||||
raise InvalidCondition(
|
||||
f"{self.__class__.__name__} must be instantiated with the {self.METHOD} method."
|
||||
)
|
||||
|
||||
# call to super must be at the end for proper validation
|
||||
super().__init__(
|
||||
return_value_test=return_value_test,
|
||||
chain=chain,
|
||||
method=method,
|
||||
return_value_test=return_value_test,
|
||||
name=name,
|
||||
condition_type=condition_type,
|
||||
name=name,
|
||||
)
|
||||
|
||||
def _validate_method(self, method):
|
||||
return method
|
||||
|
||||
def _validate_expected_return_type(self):
|
||||
comparator_value = self.return_value_test.value
|
||||
if not isinstance(comparator_value, int):
|
||||
raise InvalidCondition(
|
||||
f"Invalid return value comparison type '{type(comparator_value)}'; must be an integer"
|
||||
)
|
||||
|
||||
@property
|
||||
def timestamp(self):
|
||||
return self.return_value_test.value
|
||||
|
||||
def _execute_call(self, parameters: List[Any]) -> Any:
|
||||
"""Execute onchain read and return result."""
|
||||
# TODO may need to rethink as part of #3051 (multicall work).
|
||||
latest_block = self.w3.eth.get_block("latest")
|
||||
return latest_block.timestamp
|
||||
|
|
|
@ -34,16 +34,20 @@ class ReturnValueTestDict(TypedDict):
|
|||
key: NotRequired[Union[str, int]]
|
||||
|
||||
|
||||
# Conditions
|
||||
class _AccessControlCondition(TypedDict):
|
||||
name: NotRequired[str]
|
||||
|
||||
|
||||
class RPCConditionDict(_AccessControlCondition):
|
||||
conditionType: str
|
||||
|
||||
|
||||
class BaseExecConditionDict(_AccessControlCondition):
|
||||
returnValueTest: ReturnValueTestDict
|
||||
|
||||
|
||||
class RPCConditionDict(BaseExecConditionDict):
|
||||
chain: int
|
||||
method: str
|
||||
parameters: NotRequired[List[Any]]
|
||||
returnValueTest: ReturnValueTestDict
|
||||
|
||||
|
||||
class TimeConditionDict(RPCConditionDict):
|
||||
|
@ -56,17 +60,72 @@ class ContractConditionDict(RPCConditionDict):
|
|||
functionAbi: NotRequired[ABIFunction]
|
||||
|
||||
|
||||
class JsonApiConditionDict(BaseExecConditionDict):
|
||||
endpoint: str
|
||||
query: NotRequired[str]
|
||||
parameters: NotRequired[Dict]
|
||||
authorizationToken: NotRequired[str]
|
||||
|
||||
|
||||
class JsonRpcConditionDict(BaseExecConditionDict):
|
||||
endpoint: str
|
||||
method: str
|
||||
params: NotRequired[Any]
|
||||
query: NotRequired[str]
|
||||
authorizationToken: NotRequired[str]
|
||||
|
||||
|
||||
class JWTConditionDict(_AccessControlCondition):
|
||||
jwtToken: str
|
||||
publicKey: str # TODO: See #3572 for a discussion about deprecating this in favour of the expected issuer
|
||||
expectedIssuer: NotRequired[str]
|
||||
|
||||
|
||||
#
|
||||
# CompoundCondition represents:
|
||||
# {
|
||||
# "operator": ["and" | "or"]
|
||||
# "operands": List[AccessControlCondition | CompoundCondition]
|
||||
# "operator": ["and" | "or" | "not"]
|
||||
# "operands": List[AccessControlCondition]
|
||||
# }
|
||||
#
|
||||
class CompoundConditionDict(_AccessControlCondition):
|
||||
operator: Literal["and", "or", "not"]
|
||||
operands: List["ConditionDict"]
|
||||
|
||||
|
||||
#
|
||||
class CompoundConditionDict(TypedDict):
|
||||
conditionType: str
|
||||
operator: Literal["and", "or"]
|
||||
operands: List["Lingo"]
|
||||
# ConditionVariable represents:
|
||||
# {
|
||||
# varName: str
|
||||
# condition: AccessControlCondition
|
||||
# }
|
||||
#
|
||||
class ConditionVariableDict(TypedDict):
|
||||
varName: str
|
||||
condition: "ConditionDict"
|
||||
|
||||
|
||||
#
|
||||
# SequentialCondition represents:
|
||||
# {
|
||||
# "conditionVariables": List[ConditionVariable]
|
||||
# }
|
||||
#
|
||||
class SequentialConditionDict(_AccessControlCondition):
|
||||
conditionVariables = List[ConditionVariableDict]
|
||||
|
||||
|
||||
#
|
||||
# IfThenElseCondition represents:
|
||||
# {
|
||||
# "ifCondition": AccessControlCondition
|
||||
# "thenCondition": AccessControlCondition
|
||||
# "elseCondition": [AccessControlCondition | bool]
|
||||
# }
|
||||
class IfThenElseConditionDict(_AccessControlCondition):
|
||||
ifCondition: "ConditionDict"
|
||||
thenCondition: "ConditionDict"
|
||||
elseCondition: Union["ConditionDict", bool]
|
||||
|
||||
|
||||
#
|
||||
|
@ -74,9 +133,22 @@ class CompoundConditionDict(TypedDict):
|
|||
# - TimeCondition
|
||||
# - RPCCondition
|
||||
# - ContractCondition
|
||||
# - CompoundConditionDict
|
||||
# - CompoundCondition
|
||||
# - JsonApiCondition
|
||||
# - JsonRpcCondition
|
||||
# - JWTCondition
|
||||
# - SequentialCondition
|
||||
# - IfThenElseCondition
|
||||
ConditionDict = Union[
|
||||
TimeConditionDict, RPCConditionDict, ContractConditionDict, CompoundConditionDict
|
||||
TimeConditionDict,
|
||||
RPCConditionDict,
|
||||
ContractConditionDict,
|
||||
CompoundConditionDict,
|
||||
JsonApiConditionDict,
|
||||
JsonRpcConditionDict,
|
||||
JWTConditionDict,
|
||||
SequentialConditionDict,
|
||||
IfThenElseConditionDict,
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import re
|
||||
from http import HTTPStatus
|
||||
from typing import Dict, Optional, Set, Tuple
|
||||
from typing import Dict, Iterator, List, Optional, Tuple, Union
|
||||
|
||||
from marshmallow import Schema, post_dump
|
||||
from marshmallow.exceptions import SCHEMA
|
||||
from web3 import HTTPProvider, Web3
|
||||
from web3.middleware import geth_poa_middleware
|
||||
from web3.providers import BaseProvider
|
||||
|
||||
from nucypher.policy.conditions.exceptions import (
|
||||
|
@ -10,6 +13,7 @@ from nucypher.policy.conditions.exceptions import (
|
|||
ContextVariableVerificationFailed,
|
||||
InvalidCondition,
|
||||
InvalidConditionLingo,
|
||||
InvalidConnectionToChain,
|
||||
InvalidContextVariableData,
|
||||
NoConnectionToChain,
|
||||
RequiredContextVariable,
|
||||
|
@ -21,6 +25,57 @@ from nucypher.utilities.logging import Logger
|
|||
__LOGGER = Logger("condition-eval")
|
||||
|
||||
|
||||
class ConditionProviderManager:
|
||||
def __init__(self, providers: Dict[int, List[HTTPProvider]]):
|
||||
self.providers = providers
|
||||
self.logger = Logger(__name__)
|
||||
|
||||
def web3_endpoints(self, chain_id: int) -> Iterator[Web3]:
|
||||
rpc_providers = self.providers.get(chain_id, None)
|
||||
if not rpc_providers:
|
||||
raise NoConnectionToChain(chain=chain_id)
|
||||
|
||||
iterator_returned_at_least_one = False
|
||||
for provider in rpc_providers:
|
||||
try:
|
||||
w3 = self._configure_w3(provider=provider)
|
||||
self._check_chain_id(chain_id, w3)
|
||||
yield w3
|
||||
iterator_returned_at_least_one = True
|
||||
except InvalidConnectionToChain as e:
|
||||
# don't expect to happen but must account
|
||||
# for any misconfigurations of public endpoints
|
||||
self.logger.warn(str(e))
|
||||
|
||||
# if we get here, it is because there were endpoints, but issue with configuring them
|
||||
if not iterator_returned_at_least_one:
|
||||
raise NoConnectionToChain(
|
||||
chain=chain_id,
|
||||
message=f"Problematic provider endpoints for chain ID {chain_id}",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _configure_w3(provider: BaseProvider) -> Web3:
|
||||
# Instantiate a local web3 instance
|
||||
w3 = Web3(provider)
|
||||
# inject web3 middleware to handle POA chain extra_data field.
|
||||
w3.middleware_onion.inject(geth_poa_middleware, layer=0, name="poa")
|
||||
return w3
|
||||
|
||||
@staticmethod
|
||||
def _check_chain_id(chain_id: int, w3: Web3) -> None:
|
||||
"""
|
||||
Validates that the actual web3 provider is *actually*
|
||||
connected to the condition's chain ID by reading its RPC endpoint.
|
||||
"""
|
||||
provider_chain = w3.eth.chain_id
|
||||
if provider_chain != chain_id:
|
||||
raise InvalidConnectionToChain(
|
||||
expected_chain=chain_id,
|
||||
actual_chain=provider_chain,
|
||||
)
|
||||
|
||||
|
||||
class ConditionEvalError(Exception):
|
||||
"""Exception when execution condition evaluation."""
|
||||
def __init__(self, message: str, status_code: int):
|
||||
|
@ -57,7 +112,7 @@ class CamelCaseSchema(Schema):
|
|||
|
||||
def evaluate_condition_lingo(
|
||||
condition_lingo: Lingo,
|
||||
providers: Optional[Dict[int, Set[BaseProvider]]] = None,
|
||||
providers: Optional[ConditionProviderManager] = None,
|
||||
context: Optional[ContextDict] = None,
|
||||
log: Logger = __LOGGER,
|
||||
):
|
||||
|
@ -73,7 +128,7 @@ def evaluate_condition_lingo(
|
|||
|
||||
# Setup (don't use mutable defaults)
|
||||
context = context or dict()
|
||||
providers = providers or dict()
|
||||
providers = providers or ConditionProviderManager(providers=dict())
|
||||
error = None
|
||||
|
||||
# Evaluate
|
||||
|
@ -138,3 +193,48 @@ def evaluate_condition_lingo(
|
|||
if error:
|
||||
log.info(error.message) # log error message
|
||||
raise error
|
||||
|
||||
|
||||
def extract_single_error_message_from_schema_errors(
|
||||
errors: Dict[str, List[str]],
|
||||
) -> str:
|
||||
"""
|
||||
Extract single error message from Schema().validate() errors result.
|
||||
|
||||
The result is only for a single error type, and only the first message string for that type.
|
||||
If there are multiple error types, only one error type is used; the first field-specific (@validates)
|
||||
error type encountered is prioritized over any schema-level-specific (@validates_schema) error.
|
||||
"""
|
||||
if not errors:
|
||||
raise ValueError("Validation errors must be provided")
|
||||
|
||||
# extract error type - either field-specific (preferred) or schema-specific
|
||||
error_key_to_use = None
|
||||
for error_type in list(errors.keys()):
|
||||
error_key_to_use = error_type
|
||||
if error_key_to_use != SCHEMA:
|
||||
# actual field
|
||||
break
|
||||
|
||||
message = errors[error_key_to_use][0]
|
||||
message_prefix = (
|
||||
f"'{camel_case_to_snake(error_key_to_use)}' field - "
|
||||
if error_key_to_use != SCHEMA
|
||||
else ""
|
||||
)
|
||||
return f"{message_prefix}{message}"
|
||||
|
||||
|
||||
def check_and_convert_big_int_string_to_int(value: str) -> Union[str, int]:
|
||||
"""
|
||||
Check if a string is a big int string and convert it to an integer, otherwise return the string.
|
||||
"""
|
||||
if re.fullmatch(r"^-?\d+n$", value):
|
||||
try:
|
||||
result = int(value[:-1])
|
||||
return result
|
||||
except ValueError:
|
||||
# ignore
|
||||
pass
|
||||
|
||||
return value
|
||||
|
|
|
@ -7,42 +7,24 @@ from typing import (
|
|||
cast,
|
||||
)
|
||||
|
||||
from eth_typing import ChecksumAddress
|
||||
from web3 import Web3
|
||||
from web3.auto import w3
|
||||
from web3.contract.contract import ContractFunction
|
||||
from web3.types import ABIFunction
|
||||
|
||||
from nucypher.policy.conditions import STANDARD_ABI_CONTRACT_TYPES, STANDARD_ABIS
|
||||
from nucypher.policy.conditions.context import is_context_variable
|
||||
from nucypher.policy.conditions.exceptions import (
|
||||
InvalidCondition,
|
||||
)
|
||||
from nucypher.policy.conditions.lingo import ReturnValueTest
|
||||
|
||||
|
||||
def _validate_single_output_type(
|
||||
expected_type: str,
|
||||
comparator_value: Any,
|
||||
comparator_index: Optional[int],
|
||||
failure_message: str,
|
||||
) -> None:
|
||||
if comparator_index is not None and _is_tuple_type(expected_type):
|
||||
type_entries = _get_tuple_type_entries(expected_type)
|
||||
expected_type = type_entries[comparator_index]
|
||||
_validate_value_type(expected_type, comparator_value, failure_message)
|
||||
|
||||
#
|
||||
# Schema logic
|
||||
#
|
||||
|
||||
def _get_abi_types(abi: ABIFunction) -> List[str]:
|
||||
return [_collapse_if_tuple(cast(Dict[str, Any], arg)) for arg in abi["outputs"]]
|
||||
|
||||
|
||||
def _validate_value_type(
|
||||
expected_type: str, comparator_value: Any, failure_message: str
|
||||
) -> None:
|
||||
if is_context_variable(comparator_value):
|
||||
# context variable types cannot be known until execution time.
|
||||
return
|
||||
if not w3.is_encodable(expected_type, comparator_value):
|
||||
raise InvalidCondition(failure_message)
|
||||
|
||||
|
||||
def _collapse_if_tuple(abi: Dict[str, Any]) -> str:
|
||||
abi_type = abi["type"]
|
||||
if not abi_type.startswith("tuple"):
|
||||
|
@ -66,6 +48,28 @@ def _get_tuple_type_entries(tuple_type: str) -> List[str]:
|
|||
return result
|
||||
|
||||
|
||||
def _validate_value_type(
|
||||
expected_type: str, comparator_value: Any, failure_message: str
|
||||
) -> None:
|
||||
if is_context_variable(comparator_value):
|
||||
# context variable types cannot be known until execution time.
|
||||
return
|
||||
if not w3.is_encodable(expected_type, comparator_value):
|
||||
raise ValueError(failure_message)
|
||||
|
||||
|
||||
def _validate_single_output_type(
|
||||
expected_type: str,
|
||||
comparator_value: Any,
|
||||
comparator_index: Optional[int],
|
||||
failure_message: str,
|
||||
) -> None:
|
||||
if comparator_index is not None and _is_tuple_type(expected_type):
|
||||
type_entries = _get_tuple_type_entries(expected_type)
|
||||
expected_type = type_entries[comparator_index]
|
||||
_validate_value_type(expected_type, comparator_value, failure_message)
|
||||
|
||||
|
||||
def _validate_multiple_output_types(
|
||||
output_abi_types: List[str],
|
||||
comparator_value: Any,
|
||||
|
@ -82,15 +86,46 @@ def _validate_multiple_output_types(
|
|||
return
|
||||
|
||||
if not isinstance(comparator_value, Sequence):
|
||||
raise InvalidCondition(failure_message)
|
||||
raise ValueError(failure_message)
|
||||
|
||||
if len(output_abi_types) != len(comparator_value):
|
||||
raise InvalidCondition(failure_message)
|
||||
raise ValueError(failure_message)
|
||||
|
||||
for output_abi_type, component_value in zip(output_abi_types, comparator_value):
|
||||
_validate_value_type(output_abi_type, component_value, failure_message)
|
||||
|
||||
|
||||
def _resolve_abi(
|
||||
w3: Web3,
|
||||
method: str,
|
||||
standard_contract_type: Optional[str] = None,
|
||||
function_abi: Optional[ABIFunction] = None,
|
||||
) -> ABIFunction:
|
||||
"""Resolves the contract an/or function ABI from a standard contract name"""
|
||||
|
||||
if not (function_abi or standard_contract_type):
|
||||
raise ValueError(
|
||||
f"Ambiguous ABI - Supply either an ABI or a standard contract type ({STANDARD_ABI_CONTRACT_TYPES})."
|
||||
)
|
||||
|
||||
if standard_contract_type:
|
||||
try:
|
||||
# Lookup the standard ABI given it's ERC standard name (standard contract type)
|
||||
contract_abi = STANDARD_ABIS[standard_contract_type]
|
||||
except KeyError:
|
||||
raise ValueError(
|
||||
f"Invalid standard contract type {standard_contract_type}; Must be one of {STANDARD_ABI_CONTRACT_TYPES}"
|
||||
)
|
||||
|
||||
# Extract all function ABIs from the contract's ABI.
|
||||
# Will raise a ValueError if there is not exactly one match.
|
||||
function_abi = (
|
||||
w3.eth.contract(abi=contract_abi).get_function_by_name(method).abi
|
||||
)
|
||||
|
||||
return ABIFunction(function_abi)
|
||||
|
||||
|
||||
def _align_comparator_value_single_output(
|
||||
expected_type: str, comparator_value: Any, comparator_index: Optional[int]
|
||||
) -> Any:
|
||||
|
@ -99,7 +134,7 @@ def _align_comparator_value_single_output(
|
|||
expected_type = type_entries[comparator_index]
|
||||
|
||||
if not w3.is_encodable(expected_type, comparator_value):
|
||||
raise InvalidCondition(
|
||||
raise ValueError(
|
||||
f"Mismatched comparator type ({comparator_value} as {expected_type})"
|
||||
)
|
||||
return comparator_value
|
||||
|
@ -112,7 +147,7 @@ def _align_comparator_value_multiple_output(
|
|||
expected_type = output_abi_types[comparator_index]
|
||||
# ensure alignment
|
||||
if not w3.is_encodable(expected_type, comparator_value):
|
||||
raise InvalidCondition(
|
||||
raise ValueError(
|
||||
f"Mismatched comparator type ({comparator_value} as {expected_type})"
|
||||
)
|
||||
|
||||
|
@ -122,15 +157,20 @@ def _align_comparator_value_multiple_output(
|
|||
for output_abi_type, component_value in zip(output_abi_types, comparator_value):
|
||||
# ensure alignment
|
||||
if not w3.is_encodable(output_abi_type, component_value):
|
||||
raise InvalidCondition(
|
||||
raise ValueError(
|
||||
f"Mismatched comparator type ({component_value} as {output_abi_type})"
|
||||
)
|
||||
values.append(component_value)
|
||||
return values
|
||||
|
||||
|
||||
def _align_comparator_value_with_abi(
|
||||
abi, return_value_test: ReturnValueTest
|
||||
#
|
||||
# Public functions.
|
||||
#
|
||||
|
||||
|
||||
def align_comparator_value_with_abi(
|
||||
abi: ABIFunction, return_value_test: ReturnValueTest
|
||||
) -> ReturnValueTest:
|
||||
output_abi_types = _get_abi_types(abi)
|
||||
comparator = return_value_test.comparator
|
||||
|
@ -155,13 +195,19 @@ def _align_comparator_value_with_abi(
|
|||
)
|
||||
|
||||
|
||||
def _validate_condition_function_abi(function_abi: Dict, method_name: str) -> None:
|
||||
"""validates a dictionary as valid for use as a condition function ABI"""
|
||||
def validate_function_abi(
|
||||
function_abi: Dict, method_name: Optional[str] = None
|
||||
) -> None:
|
||||
"""
|
||||
Validates a dictionary as valid for use as a condition function ABI.
|
||||
|
||||
Optionally validates the method_name
|
||||
"""
|
||||
abi = ABIFunction(function_abi)
|
||||
|
||||
if not abi.get("name"):
|
||||
raise ValueError(f"Invalid ABI, no function name found {abi}")
|
||||
if abi.get("name") != method_name:
|
||||
if method_name and abi.get("name") != method_name:
|
||||
raise ValueError(f"Mismatched ABI for contract function {method_name} - {abi}")
|
||||
if abi.get("type") != "function":
|
||||
raise ValueError(f"Invalid ABI type {abi}")
|
||||
|
@ -171,14 +217,47 @@ def _validate_condition_function_abi(function_abi: Dict, method_name: str) -> No
|
|||
raise ValueError(f"Invalid ABI stateMutability {abi}")
|
||||
|
||||
|
||||
def _validate_condition_abi(
|
||||
standard_contract_type: str,
|
||||
function_abi: Dict,
|
||||
method_name: str,
|
||||
) -> None:
|
||||
if not (bool(standard_contract_type) ^ bool(function_abi)):
|
||||
def get_unbound_contract_function(
|
||||
contract_address: ChecksumAddress,
|
||||
method: str,
|
||||
standard_contract_type: Optional[str] = None,
|
||||
function_abi: Optional[ABIFunction] = None,
|
||||
) -> ContractFunction:
|
||||
"""Gets an unbound contract function to evaluate"""
|
||||
w3 = Web3()
|
||||
function_abi = _resolve_abi(
|
||||
w3=w3,
|
||||
standard_contract_type=standard_contract_type,
|
||||
method=method,
|
||||
function_abi=function_abi,
|
||||
)
|
||||
try:
|
||||
contract = w3.eth.contract(address=contract_address, abi=[function_abi])
|
||||
contract_function = getattr(contract.functions, method)
|
||||
return contract_function
|
||||
except Exception as e:
|
||||
raise ValueError(
|
||||
f"Provide 'standardContractType' or 'functionAbi'; got ({standard_contract_type}, {function_abi})."
|
||||
f"Unable to find contract function, '{method}', for condition: {e}"
|
||||
) from e
|
||||
|
||||
|
||||
def validate_contract_function_expected_return_type(
|
||||
contract_function: ContractFunction, return_value_test: ReturnValueTest
|
||||
) -> None:
|
||||
output_abi_types = _get_abi_types(contract_function.contract_abi[0])
|
||||
comparator_value = return_value_test.value
|
||||
comparator_index = return_value_test.index
|
||||
index_string = f"@index={comparator_index}" if comparator_index is not None else ""
|
||||
failure_message = (
|
||||
f"Invalid return value comparison type '{type(comparator_value)}' for "
|
||||
f"'{contract_function.fn_name}'{index_string} based on ABI types {output_abi_types}"
|
||||
)
|
||||
|
||||
if len(output_abi_types) == 1:
|
||||
_validate_single_output_type(
|
||||
output_abi_types[0], comparator_value, comparator_index, failure_message
|
||||
)
|
||||
else:
|
||||
_validate_multiple_output_types(
|
||||
output_abi_types, comparator_value, comparator_index, failure_message
|
||||
)
|
||||
if function_abi:
|
||||
_validate_condition_function_abi(function_abi, method_name=method_name)
|
||||
|
|
|
@ -188,7 +188,7 @@ class EventScanner:
|
|||
# minor chain reorganisation?
|
||||
return None
|
||||
last_time = block_info["timestamp"]
|
||||
return datetime.datetime.utcfromtimestamp(last_time)
|
||||
return datetime.datetime.fromtimestamp(last_time, tz=datetime.timezone.utc)
|
||||
|
||||
def get_suggested_scan_start_block(self):
|
||||
"""Get where we should start to scan for new token events.
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
import collections
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from threading import Lock
|
||||
from typing import List
|
||||
|
||||
from eth_typing import ChecksumAddress
|
||||
|
||||
|
||||
class NodeLatencyStatsCollector:
|
||||
"""
|
||||
Thread-safe utility that tracks latency statistics related to P2P connections with other nodes.
|
||||
"""
|
||||
|
||||
MAX_MOVING_AVERAGE_WINDOW = 5
|
||||
MAX_LATENCY = float(2**16) # just need a large number for sorting
|
||||
|
||||
class NodeLatencyContextManager:
|
||||
def __init__(
|
||||
self,
|
||||
stats_collector: "NodeLatencyStatsCollector",
|
||||
staker_address: ChecksumAddress,
|
||||
):
|
||||
self._stats_collector = stats_collector
|
||||
self.staker_address = staker_address
|
||||
|
||||
def __enter__(self):
|
||||
self.start_time = time.perf_counter()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
if exc_type:
|
||||
# exception occurred - reset stats since connectivity was compromised
|
||||
self._stats_collector.reset_stats(self.staker_address)
|
||||
else:
|
||||
# no exception
|
||||
end_time = time.perf_counter()
|
||||
execution_time = end_time - self.start_time
|
||||
self._stats_collector._update_stats(self.staker_address, execution_time)
|
||||
|
||||
def __init__(self, max_moving_average_window: int = MAX_MOVING_AVERAGE_WINDOW):
|
||||
self._node_stats = defaultdict(
|
||||
lambda: collections.deque([], maxlen=max_moving_average_window)
|
||||
)
|
||||
self._lock = Lock()
|
||||
|
||||
def _update_stats(self, staking_address: ChecksumAddress, latest_time_taken: float):
|
||||
with self._lock:
|
||||
self._node_stats[staking_address].append(latest_time_taken)
|
||||
|
||||
def reset_stats(self, staking_address: ChecksumAddress):
|
||||
with self._lock:
|
||||
self._node_stats[staking_address].clear()
|
||||
|
||||
def get_latency_tracker(
|
||||
self, staker_address: ChecksumAddress
|
||||
) -> NodeLatencyContextManager:
|
||||
return self.NodeLatencyContextManager(
|
||||
stats_collector=self, staker_address=staker_address
|
||||
)
|
||||
|
||||
def get_average_latency_time(self, staking_address: ChecksumAddress) -> float:
|
||||
with self._lock:
|
||||
readings = list(self._node_stats[staking_address])
|
||||
num_readings = len(readings)
|
||||
# just need a large number > 0
|
||||
return (
|
||||
self.MAX_LATENCY if num_readings == 0 else sum(readings) / num_readings
|
||||
)
|
||||
|
||||
def order_addresses_by_latency(
|
||||
self, staking_addresses: List[ChecksumAddress]
|
||||
) -> List[ChecksumAddress]:
|
||||
result = sorted(
|
||||
staking_addresses,
|
||||
key=lambda staking_address: self.get_average_latency_time(staking_address),
|
||||
)
|
||||
return result
|
|
@ -1,4 +1,5 @@
|
|||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
from contextlib import contextmanager
|
||||
from enum import Enum
|
||||
|
@ -15,6 +16,7 @@ from twisted.logger import (
|
|||
)
|
||||
from twisted.logger import Logger as TwistedLogger
|
||||
from twisted.python.logfile import LogFile
|
||||
from twisted.web import http
|
||||
|
||||
import nucypher
|
||||
from nucypher.config.constants import (
|
||||
|
@ -211,11 +213,35 @@ def get_text_file_observer(name=DEFAULT_LOG_FILENAME, path=USER_LOG_DIR):
|
|||
return observer
|
||||
|
||||
|
||||
class Logger(TwistedLogger):
|
||||
"""Drop-in replacement of Twisted's Logger, patching the emit() method to tolerate inputs with curly braces,
|
||||
i.e., not compliant with PEP 3101.
|
||||
def _redact_ip_address_when_logging_server_requests():
|
||||
"""
|
||||
Monkey-patch of twisted's HttpFactory log formatter so that logging of server requests
|
||||
will exclude (redact) the IP address of the requester.
|
||||
"""
|
||||
original_formatter = http.combinedLogFormatter
|
||||
ip_address_pattern = r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}"
|
||||
|
||||
See Issue #724 and, particularly, https://github.com/nucypher/nucypher/issues/724#issuecomment-600190455"""
|
||||
def redact_ip_address_formatter(timestamp, request):
|
||||
line = original_formatter(timestamp, request)
|
||||
# redact any ip address
|
||||
line = re.sub(ip_address_pattern, "<IP_REDACTED>", line)
|
||||
return line
|
||||
|
||||
http.combinedLogFormatter = redact_ip_address_formatter
|
||||
|
||||
|
||||
class Logger(TwistedLogger):
|
||||
"""Drop-in replacement of Twisted's Logger:
|
||||
1. patch the emit() method to tolerate inputs with curly braces,
|
||||
i.e., not compliant with PEP 3101. See Issue #724 and, particularly,
|
||||
https://github.com/nucypher/nucypher/issues/724#issuecomment-600190455
|
||||
2. redact IP addresses for http requests
|
||||
"""
|
||||
|
||||
_redact_ip_address_when_logging_server_requests()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def escape_format_string(cls, string):
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,11 +1,20 @@
|
|||
[tool.poetry]
|
||||
name = "nucypher"
|
||||
version = "7.4.1"
|
||||
version = "7.5.0"
|
||||
authors = ["NuCypher"]
|
||||
description = "A threshold access control application to empower privacy in decentralized systems."
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.9,<4"
|
||||
|
||||
# Special dependencies required because their setup.cfg files are incorrect.
|
||||
# Once these changes are incorporated in the origin projects, these lines can
|
||||
# be removed.
|
||||
# https://github.com/anthonyalmarza/chalk/pull/23
|
||||
# https://github.com/zartstrom/snaptime/pull/6
|
||||
pychalk = { git = "https://github.com/nucypher/pychalk.git", rev = "main" }
|
||||
snaptime = { git = "https://github.com/nucypher/snaptime.git", rev = "main" }
|
||||
|
||||
nucypher-core = "==0.13.0"
|
||||
cryptography = "*"
|
||||
pynacl = ">=1.4.0"
|
||||
|
@ -28,14 +37,16 @@ prometheus-client = '*'
|
|||
siwe = "^4.2.0"
|
||||
time-machine = "^2.13.0"
|
||||
twisted = "^24.2.0rc1"
|
||||
jsonpath-ng = "^1.6.1"
|
||||
pyjwt = {extras = ["crypto"], version = "^2.10.1"}
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
pytest = '<7'
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pytest = '*'
|
||||
pytest-cov = '*'
|
||||
pytest-mock = '*'
|
||||
pytest-timeout = '*'
|
||||
pytest-twisted = '*'
|
||||
eth-ape = ">=0.7"
|
||||
eth-ape = ">=0.8,<=0.8.12" # limit due to pytest not supporting newer version of eth-utils - also observed here: https://github.com/oceanprotocol/pdr-backend/issues/899
|
||||
ape-solidity = '*'
|
||||
coverage = '^7.3.2'
|
||||
pre-commit = '^2.12.1'
|
||||
|
|
209
requirements.txt
209
requirements.txt
|
@ -1,103 +1,108 @@
|
|||
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"
|
||||
abnf==2.3.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
aiohappyeyeballs==2.6.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
aiohttp==3.11.14 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
aiosignal==1.3.2 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
annotated-types==0.7.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
appdirs==1.4.4 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
async-timeout==5.0.1 ; python_version >= "3.9" and python_version < "3.11" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
attrs==25.3.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
atxm==0.5.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
autobahn==24.4.2 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
automat==24.8.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
bitarray==3.2.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
blinker==1.9.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
bytestring-splitter==2.4.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
cached-property==2.0.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
certifi==2025.1.31 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
cffi==1.17.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
charset-normalizer==3.4.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
ckzg==1.0.2 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
click==8.1.8 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
colorama==0.4.6 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
constant-sorrow==0.1.0a9 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
constantly==23.10.4 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
cryptography==43.0.3 ; python_version >= "3.9" and python_version < "3.11" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
cryptography==44.0.2 ; python_version >= "3.11" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
cytoolz==1.0.1 ; python_version >= "3.9" and python_version < "4" and implementation_name == "cpython"
|
||||
dateparser==1.2.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
eth-abi==5.2.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
eth-account==0.11.3 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
eth-hash==0.7.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
eth-keyfile==0.9.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
eth-keys==0.6.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
eth-rlp==1.0.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
eth-typing==3.5.2 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
eth-utils==2.3.2 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
flask==3.1.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
frozenlist==1.5.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
hendrix==5.0.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
hexbytes==0.3.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
humanize==4.12.2 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
hyperlink==21.0.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
idna==3.10 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
importlib-metadata==8.6.1 ; python_version == "3.9" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
incremental==24.7.2 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
itsdangerous==2.2.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
jinja2==3.1.6 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
jsonpath-ng==1.7.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
jsonschema-specifications==2024.10.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
jsonschema==4.23.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
lru-dict==1.2.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
mako==1.3.9 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
markupsafe==3.0.2 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
marshmallow==3.26.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
maya==0.6.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
mnemonic==0.21 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
msgpack-python==0.5.6 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
multidict==6.2.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
nucypher-core==0.13.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
packaging==23.2 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
parsimonious==0.10.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
pendulum==3.0.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
ply==3.11 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
prometheus-client==0.21.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
propcache==0.3.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
protobuf==6.30.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
py-ecc==7.0.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
pyasn1-modules==0.4.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
pyasn1==0.6.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
pychalk @ git+https://github.com/nucypher/pychalk.git@b1a5c7562f9788f9c8722216b32019eeca281b69 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
pycparser==2.22 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
pycryptodome==3.22.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
pydantic-core==2.27.2 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
pydantic==2.10.6 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
pyjwt==2.10.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
pynacl==1.5.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
pyopenssl==25.0.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
python-dateutil==2.9.0.post0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
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"
|
||||
pytz==2025.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
pyunormalize==16.0.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
pywin32==310 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy") and platform_system == "Windows"
|
||||
referencing==0.36.2 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
regex==2024.11.6 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
requests==2.32.3 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
rlp==4.1.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
rpds-py==0.23.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
service-identity==24.2.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
setuptools==78.0.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
siwe==4.2.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
six==1.17.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
snaptime @ git+https://github.com/nucypher/snaptime.git@7edf0f861198b3689ccf82602e3f7b0a24acf6d5 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
tabulate==0.9.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
time-machine==2.16.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
tomli==2.2.1 ; python_version >= "3.9" and python_version < "3.11" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
toolz==1.0.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
twisted==24.11.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
txaio==23.1.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
typing-extensions==4.12.2 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
tzdata==2025.2 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
tzlocal==5.3.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
urllib3==2.3.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
watchdog==3.0.0 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
web3==6.20.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
websockets==15.0.1 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
werkzeug==3.1.3 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
yarl==1.18.3 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
zipp==3.21.0 ; python_version == "3.9" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
zope-interface==7.2 ; python_version >= "3.9" and python_version < "4" and (implementation_name == "cpython" or implementation_name == "pypy")
|
||||
|
|
|
@ -0,0 +1,112 @@
|
|||
from typing import List, Optional
|
||||
|
||||
import click
|
||||
import requests
|
||||
from eth_typing import ChecksumAddress
|
||||
from nucypher_core import NodeMetadata
|
||||
from urllib3.exceptions import InsecureRequestWarning
|
||||
|
||||
from nucypher.blockchain.eth import domains
|
||||
from nucypher.blockchain.eth.agents import (
|
||||
ContractAgency,
|
||||
CoordinatorAgent,
|
||||
)
|
||||
from nucypher.blockchain.eth.registry import ContractRegistry
|
||||
from nucypher.utilities.emitters import StdoutEmitter
|
||||
from nucypher.utilities.logging import GlobalLoggerSettings
|
||||
|
||||
GlobalLoggerSettings.start_console_logging()
|
||||
emitter = StdoutEmitter(verbosity=2)
|
||||
|
||||
|
||||
def get_node_urls(participants: Optional[List[ChecksumAddress]] = None):
|
||||
participants_set = set(participants) if participants else None
|
||||
node_urls = {}
|
||||
try:
|
||||
response = requests.get(
|
||||
"https://mainnet.nucypher.network:9151/status?json=true",
|
||||
verify=False,
|
||||
timeout=5,
|
||||
)
|
||||
all_nodes = response.json().get("known_nodes", [])
|
||||
for node in all_nodes:
|
||||
staker_address = node["staker_address"]
|
||||
if participants_set and staker_address not in participants_set:
|
||||
continue
|
||||
node_urls[node["staker_address"]] = f"https://{node['rest_url']}"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return node_urls
|
||||
|
||||
|
||||
def get_current_ferveo_key(node_url):
|
||||
try:
|
||||
response = requests.get(
|
||||
f"{node_url}/public_information", verify=False, timeout=5
|
||||
)
|
||||
node_metadata = NodeMetadata.from_bytes(response.content)
|
||||
return node_metadata.payload.ferveo_public_key
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option(
|
||||
"--polygon-endpoint",
|
||||
"polygon_endpoint",
|
||||
help="Polygon network provider URI",
|
||||
type=click.STRING,
|
||||
required=True,
|
||||
)
|
||||
@click.option(
|
||||
"--ritual-id", help="Ritual ID", type=click.INT, required=False, default=None
|
||||
)
|
||||
def differing_ferveo_keys(
|
||||
polygon_endpoint,
|
||||
ritual_id,
|
||||
):
|
||||
# Suppress https verification warnings
|
||||
requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning)
|
||||
|
||||
domain = domains.get_domain(domains.MAINNET.name)
|
||||
registry = ContractRegistry.from_latest_publication(domain=domain)
|
||||
emitter.echo(f"NOTICE: Connecting to {domain} domain", color="yellow")
|
||||
|
||||
coordinator_agent = ContractAgency.get_agent(
|
||||
agent_class=CoordinatorAgent,
|
||||
registry=registry,
|
||||
blockchain_endpoint=polygon_endpoint,
|
||||
) # type: CoordinatorAgent
|
||||
|
||||
participants = None # None means all nodes
|
||||
if ritual_id:
|
||||
ritual = coordinator_agent.get_ritual(ritual_id)
|
||||
participants = ritual.providers # only nodes for ritual
|
||||
|
||||
node_urls = get_node_urls(participants)
|
||||
ritual_id_for_public_key_check = ritual_id or coordinator_agent.number_of_rituals()
|
||||
for provider, node_url in node_urls.items():
|
||||
if not node_url:
|
||||
print(f"Unable to determine public ip for {provider}")
|
||||
continue
|
||||
else:
|
||||
current_ferveo_key = get_current_ferveo_key(node_url)
|
||||
if not current_ferveo_key:
|
||||
print(
|
||||
f"Unable to obtain current public key from {node_url} for {provider}"
|
||||
)
|
||||
continue
|
||||
|
||||
reported_ferveo_key = coordinator_agent.get_provider_public_key(
|
||||
provider, ritual_id_for_public_key_check
|
||||
)
|
||||
|
||||
if bytes(current_ferveo_key) != bytes(reported_ferveo_key):
|
||||
print(f"[MISMATCH] {provider} is using different key than reported")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
differing_ferveo_keys()
|
|
@ -0,0 +1,53 @@
|
|||
import random
|
||||
from typing import List
|
||||
|
||||
from nucypher_core.ferveo import Transcript, Validator
|
||||
|
||||
from nucypher.blockchain.eth import domains
|
||||
from nucypher.blockchain.eth.agents import ContractAgency, CoordinatorAgent
|
||||
from nucypher.blockchain.eth.registry import ContractRegistry
|
||||
from nucypher.crypto.ferveo.dkg import aggregate_transcripts
|
||||
|
||||
ritual_id = 20
|
||||
domain = domains.MAINNET
|
||||
endpoint = ""
|
||||
|
||||
|
||||
registry = ContractRegistry.from_latest_publication(domain=domain)
|
||||
coordinator_agent = ContractAgency.get_agent(
|
||||
agent_class=CoordinatorAgent,
|
||||
registry=registry,
|
||||
blockchain_endpoint=endpoint,
|
||||
)
|
||||
|
||||
|
||||
def resolve_validators() -> List[Validator]:
|
||||
result = list()
|
||||
for staking_provider_address in ritual.providers:
|
||||
public_key = coordinator_agent.get_provider_public_key(
|
||||
provider=staking_provider_address, ritual_id=ritual.id
|
||||
)
|
||||
external_validator = Validator(
|
||||
address=staking_provider_address, public_key=public_key
|
||||
)
|
||||
result.append(external_validator)
|
||||
result = sorted(result, key=lambda x: x.address)
|
||||
return result
|
||||
|
||||
|
||||
ritual = coordinator_agent.get_ritual(
|
||||
ritual_id=ritual_id,
|
||||
transcripts=True,
|
||||
)
|
||||
|
||||
validators = resolve_validators()
|
||||
transcripts = [Transcript.from_bytes(bytes(t)) for t in ritual.transcripts]
|
||||
messages = list(zip(validators, transcripts))
|
||||
|
||||
aggregate_transcripts(
|
||||
transcripts=messages,
|
||||
shares=ritual.shares,
|
||||
threshold=ritual.threshold,
|
||||
me=random.choice(validators), # this is hacky
|
||||
ritual_id=ritual.id,
|
||||
)
|
|
@ -0,0 +1,49 @@
|
|||
import asyncio
|
||||
from collections import Counter
|
||||
from contextlib import suppress
|
||||
|
||||
import aiohttp
|
||||
import requests
|
||||
|
||||
TIMEOUT = 5
|
||||
template = "https://{host}/status?json=true"
|
||||
|
||||
requests.packages.urllib3.disable_warnings()
|
||||
|
||||
|
||||
async def fetch(session, url):
|
||||
with suppress(Exception):
|
||||
async with session.get(url, ssl=False, timeout=TIMEOUT) as response:
|
||||
response = await response.json()
|
||||
return response["version"]
|
||||
return "unknown"
|
||||
|
||||
|
||||
async def main():
|
||||
|
||||
url = template.format(host="mainnet.nucypher.network:9151")
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url, ssl=False, timeout=TIMEOUT) as response:
|
||||
status_data = await response.json()
|
||||
|
||||
nodes = status_data.get("known_nodes", [])
|
||||
total_nodes = len(nodes)
|
||||
print(f"Number of nodes: {total_nodes}")
|
||||
|
||||
tasks = set()
|
||||
for node in nodes:
|
||||
url = template.format(host=node["rest_url"])
|
||||
tasks.add(fetch(session, url))
|
||||
|
||||
results = Counter()
|
||||
for task in asyncio.as_completed(tasks):
|
||||
if task:
|
||||
result = await task
|
||||
results[result] += 1
|
||||
|
||||
items = sorted(results.items(), key=lambda result: result[1], reverse=True)
|
||||
for version, count in items:
|
||||
print(f"Version {version}: {count} nodes ({count*100/total_nodes:.1f}%)")
|
||||
|
||||
|
||||
asyncio.run(main())
|
|
@ -16,6 +16,7 @@ from nucypher.blockchain.eth.models import Coordinator
|
|||
from nucypher.blockchain.eth.registry import ContractRegistry
|
||||
from nucypher.blockchain.eth.signers import InMemorySigner, Signer
|
||||
from nucypher.characters.lawful import Bob, Enrico
|
||||
from nucypher.cli.types import EIP55_CHECKSUM_ADDRESS
|
||||
from nucypher.crypto.powers import TransactingPower
|
||||
from nucypher.policy.conditions.lingo import ConditionLingo, ConditionType
|
||||
from nucypher.utilities.emitters import StdoutEmitter
|
||||
|
@ -48,7 +49,7 @@ def get_transacting_power(signer: Signer):
|
|||
"--domain",
|
||||
"domain",
|
||||
help="TACo Domain",
|
||||
type=click.Choice([str(domains.TAPIR), str(domains.LYNX)]),
|
||||
type=click.Choice([str(domains.TAPIR), str(domains.LYNX), str(domains.MAINNET)]),
|
||||
default=str(domains.LYNX),
|
||||
)
|
||||
@click.option(
|
||||
|
@ -118,6 +119,13 @@ def get_transacting_power(signer: Signer):
|
|||
type=click.Choice([GLOBAL_ALLOW_LIST, "OpenAccessAuthorizer"]),
|
||||
required=False,
|
||||
)
|
||||
@click.option(
|
||||
"--fee-model",
|
||||
"-f",
|
||||
help="Fee model contract address",
|
||||
type=EIP55_CHECKSUM_ADDRESS,
|
||||
required=False,
|
||||
)
|
||||
def nucypher_dkg(
|
||||
domain,
|
||||
eth_endpoint,
|
||||
|
@ -129,6 +137,7 @@ def nucypher_dkg(
|
|||
ritual_duration,
|
||||
use_random_enrico,
|
||||
access_controller,
|
||||
fee_model,
|
||||
):
|
||||
if ritual_id < 0:
|
||||
# if creating ritual(s)
|
||||
|
@ -148,6 +157,14 @@ def nucypher_dkg(
|
|||
fg="red",
|
||||
),
|
||||
)
|
||||
if fee_model is None:
|
||||
raise click.BadOptionUsage(
|
||||
option_name="--fee-model",
|
||||
message=click.style(
|
||||
"--fee-model must be provided to create new ritual",
|
||||
fg="red",
|
||||
),
|
||||
)
|
||||
if num_rituals < 1:
|
||||
raise click.BadOptionUsage(
|
||||
option_name="--num-rituals",
|
||||
|
@ -164,7 +181,14 @@ def nucypher_dkg(
|
|||
fg="red",
|
||||
),
|
||||
)
|
||||
|
||||
if fee_model:
|
||||
raise click.BadOptionUsage(
|
||||
option_name="--fee-model",
|
||||
message=click.style(
|
||||
"--fee-model not needed since it is obtained from the Coordinator",
|
||||
fg="red",
|
||||
),
|
||||
)
|
||||
if num_rituals != 1:
|
||||
raise click.BadOptionUsage(
|
||||
option_name="--ritual-id, --num-rituals",
|
||||
|
@ -248,6 +272,7 @@ def nucypher_dkg(
|
|||
dkg_staking_providers.sort()
|
||||
emitter.echo(f"Using staking providers for DKG: {dkg_staking_providers}")
|
||||
receipt = coordinator_agent.initiate_ritual(
|
||||
fee_model=fee_model,
|
||||
providers=dkg_staking_providers,
|
||||
authority=account_address,
|
||||
duration=ritual_duration,
|
||||
|
|
|
@ -40,7 +40,7 @@ pip cache purge
|
|||
set -e
|
||||
|
||||
echo "Building Development Requirements"
|
||||
poetry lock
|
||||
poetry lock --regenerate
|
||||
poetry export -o dev-requirements.txt --without-hashes --with dev
|
||||
|
||||
echo "Building Standard Requirements"
|
||||
|
|
|
@ -2,6 +2,7 @@ import pytest
|
|||
import pytest_twisted
|
||||
from twisted.logger import globalLogPublisher
|
||||
|
||||
from nucypher.blockchain.eth.models import Coordinator
|
||||
from nucypher.blockchain.eth.signers import InMemorySigner
|
||||
from nucypher.crypto.keypairs import RitualisticKeypair
|
||||
from nucypher.crypto.powers import RitualisticPower
|
||||
|
@ -57,6 +58,7 @@ def test_dkg_failure_with_ferveo_key_mismatch(
|
|||
interval,
|
||||
testerchain,
|
||||
initiator,
|
||||
fee_model,
|
||||
global_allow_list,
|
||||
duration,
|
||||
accounts,
|
||||
|
@ -90,16 +92,15 @@ def test_dkg_failure_with_ferveo_key_mismatch(
|
|||
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
|
||||
)
|
||||
amount = fee_model.getRitualCost(len(cohort_staking_provider_addresses), duration)
|
||||
ritual_token.approve(
|
||||
coordinator_agent.contract_address,
|
||||
fee_model.address,
|
||||
amount,
|
||||
sender=accounts[initiator.transacting_power.account],
|
||||
)
|
||||
|
||||
receipt = coordinator_agent.initiate_ritual(
|
||||
fee_model=fee_model.address,
|
||||
providers=cohort_staking_provider_addresses,
|
||||
authority=initiator.transacting_power.account,
|
||||
duration=duration,
|
||||
|
@ -117,11 +118,22 @@ def test_dkg_failure_with_ferveo_key_mismatch(
|
|||
|
||||
globalLogPublisher.addObserver(log_trapper)
|
||||
|
||||
print("==================== AWAITING DKG FAILURE ====================")
|
||||
while len(log_messages) == 0:
|
||||
|
||||
print(
|
||||
"==================== AWAITING DKG FAILURE (FERVEO MISMATCH) ===================="
|
||||
)
|
||||
while (
|
||||
coordinator_agent.get_ritual_status(ritual_id)
|
||||
!= Coordinator.RitualStatus.DKG_TIMEOUT
|
||||
):
|
||||
# remains in awaiting transcripts because of the one bad ursula not submitting transcript
|
||||
assert (
|
||||
coordinator_agent.get_ritual_status(ritual_id)
|
||||
== Coordinator.RitualStatus.DKG_AWAITING_TRANSCRIPTS
|
||||
)
|
||||
yield clock.advance(interval)
|
||||
yield testerchain.time_travel(seconds=1)
|
||||
yield testerchain.time_travel(
|
||||
seconds=coordinator_agent.get_timeout() // 6
|
||||
) # min. 6 rounds before timeout
|
||||
|
||||
assert (
|
||||
render_ferveo_key_mismatch_warning(
|
||||
|
@ -130,6 +142,17 @@ def test_dkg_failure_with_ferveo_key_mismatch(
|
|||
in log_messages
|
||||
)
|
||||
|
||||
for ursula in cohort:
|
||||
participant = coordinator_agent.get_participant(
|
||||
ritual_id, ursula.checksum_address, True
|
||||
)
|
||||
if ursula.checksum_address == bad_ursula.checksum_address:
|
||||
assert (
|
||||
not participant.transcript
|
||||
), "transcript not submitted due to mismatched ferveo key"
|
||||
else:
|
||||
assert participant.transcript, "transcript submitted"
|
||||
|
||||
testerchain.tx_machine.stop()
|
||||
assert not testerchain.tx_machine.running
|
||||
globalLogPublisher.removeObserver(log_trapper)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import os
|
||||
import random
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import ANY, patch
|
||||
|
||||
import pytest
|
||||
import pytest_twisted
|
||||
|
@ -12,12 +12,15 @@ from nucypher.blockchain.eth.constants import NULL_ADDRESS
|
|||
from nucypher.blockchain.eth.models import Coordinator
|
||||
from nucypher.blockchain.eth.signers.software import InMemorySigner
|
||||
from nucypher.characters.lawful import Enrico, Ursula
|
||||
from nucypher.network.decryption import ThresholdDecryptionClient
|
||||
from nucypher.policy.conditions.evm import ContractCondition, RPCCondition
|
||||
from nucypher.policy.conditions.lingo import (
|
||||
ConditionLingo,
|
||||
ConditionVariable,
|
||||
NotCompoundCondition,
|
||||
OrCompoundCondition,
|
||||
ReturnValueTest,
|
||||
SequentialAccessControlCondition,
|
||||
)
|
||||
from nucypher.policy.conditions.time import TimeCondition
|
||||
from tests.constants import TEST_ETH_PROVIDER_URI, TESTERCHAIN_CHAIN_ID
|
||||
|
@ -93,7 +96,14 @@ def condition(test_registry):
|
|||
)
|
||||
|
||||
not_not_condition = NotCompoundCondition(
|
||||
operand=NotCompoundCondition(operand=and_condition)
|
||||
operand=NotCompoundCondition(operand=rpc_condition)
|
||||
)
|
||||
|
||||
sequential_condition = SequentialAccessControlCondition(
|
||||
condition_variables=[
|
||||
ConditionVariable("rpc", rpc_condition),
|
||||
ConditionVariable("contract", contract_condition),
|
||||
]
|
||||
)
|
||||
|
||||
conditions = [
|
||||
|
@ -103,6 +113,7 @@ def condition(test_registry):
|
|||
or_condition,
|
||||
and_condition,
|
||||
not_not_condition,
|
||||
sequential_condition,
|
||||
]
|
||||
|
||||
condition_to_use = random.choice(conditions)
|
||||
|
@ -137,6 +148,7 @@ def test_dkg_initiation(
|
|||
accounts,
|
||||
initiator,
|
||||
cohort,
|
||||
fee_model,
|
||||
global_allow_list,
|
||||
testerchain,
|
||||
ritual_token,
|
||||
|
@ -147,16 +159,15 @@ def test_dkg_initiation(
|
|||
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
|
||||
)
|
||||
amount = fee_model.getRitualCost(len(cohort_staking_provider_addresses), duration)
|
||||
ritual_token.approve(
|
||||
coordinator_agent.contract_address,
|
||||
fee_model.address,
|
||||
amount,
|
||||
sender=accounts[initiator.transacting_power.account],
|
||||
)
|
||||
|
||||
receipt = coordinator_agent.initiate_ritual(
|
||||
fee_model=fee_model.address,
|
||||
providers=cohort_staking_provider_addresses,
|
||||
authority=initiator.transacting_power.account,
|
||||
duration=duration,
|
||||
|
@ -243,8 +254,12 @@ def test_encrypt(
|
|||
|
||||
|
||||
@pytest_twisted.inlineCallbacks
|
||||
def test_unauthorized_decryption(bob, cohort, threshold_message_kit, ritual_id):
|
||||
def test_unauthorized_decryption(
|
||||
bob, cohort, threshold_message_kit, ritual_id, signer, global_allow_list
|
||||
):
|
||||
print("======== DKG DECRYPTION (UNAUTHORIZED) ========")
|
||||
assert not global_allow_list.isAddressAuthorized(ritual_id, signer.accounts[0])
|
||||
|
||||
bob.start_learning_loop(now=True)
|
||||
with pytest.raises(
|
||||
Ursula.NotEnoughUrsulas,
|
||||
|
@ -261,19 +276,88 @@ def test_unauthorized_decryption(bob, cohort, threshold_message_kit, ritual_id):
|
|||
yield
|
||||
|
||||
|
||||
def check_decrypt_without_any_cached_values(
|
||||
@pytest_twisted.inlineCallbacks
|
||||
def test_authorized_decryption(
|
||||
mocker,
|
||||
bob,
|
||||
global_allow_list,
|
||||
accounts,
|
||||
coordinator_agent,
|
||||
threshold_message_kit,
|
||||
signer,
|
||||
initiator,
|
||||
ritual_id,
|
||||
cohort,
|
||||
plaintext,
|
||||
):
|
||||
print("==================== DKG DECRYPTION (AUTHORIZED) ====================")
|
||||
# authorize Enrico to encrypt for ritual
|
||||
global_allow_list.authorize(
|
||||
ritual_id,
|
||||
[signer.accounts[0]],
|
||||
sender=accounts[initiator.transacting_power.account],
|
||||
)
|
||||
|
||||
# fake some latency stats
|
||||
latency_stats = {}
|
||||
for ursula in cohort:
|
||||
# reset all stats
|
||||
bob.node_latency_collector.reset_stats(ursula.checksum_address)
|
||||
# add a single data point for each ursula: some time between 0.1 and 4
|
||||
mock_latency = random.uniform(0.1, 4)
|
||||
bob.node_latency_collector._update_stats(ursula.checksum_address, mock_latency)
|
||||
latency_stats[ursula.checksum_address] = mock_latency
|
||||
|
||||
expected_ursula_request_ordering = sorted(
|
||||
list(latency_stats.keys()),
|
||||
key=lambda ursula_checksum: latency_stats[ursula_checksum],
|
||||
)
|
||||
value_factory_spy = mocker.spy(
|
||||
ThresholdDecryptionClient.ThresholdDecryptionRequestFactory, "__init__"
|
||||
)
|
||||
|
||||
# ritual_id, ciphertext, conditions are obtained from the side channel
|
||||
bob.start_learning_loop(now=True)
|
||||
cleartext = yield bob.threshold_decrypt(
|
||||
threshold_message_kit=threshold_message_kit,
|
||||
)
|
||||
assert bytes(cleartext) == plaintext.encode()
|
||||
|
||||
# check that proper ordering of ursulas used for worker pool factory for requests
|
||||
value_factory_spy.assert_called_once_with(
|
||||
ANY,
|
||||
ursulas_to_contact=expected_ursula_request_ordering,
|
||||
batch_size=ANY,
|
||||
threshold=ANY,
|
||||
)
|
||||
|
||||
# check prometheus metric for decryption requests
|
||||
# since all running on the same machine - the value is not per-ursula but rather all
|
||||
num_successes = REGISTRY.get_sample_value(
|
||||
"threshold_decryption_num_successes_total"
|
||||
)
|
||||
|
||||
ritual = coordinator_agent.get_ritual(ritual_id)
|
||||
# at least a threshold of ursulas were successful (concurrency)
|
||||
assert int(num_successes) >= ritual.threshold
|
||||
print("===================== DECRYPTION SUCCESSFUL =====================")
|
||||
yield
|
||||
|
||||
|
||||
@pytest_twisted.inlineCallbacks
|
||||
def test_decrypt_without_any_cached_values(
|
||||
threshold_message_kit, ritual_id, cohort, bob, coordinator_agent, plaintext
|
||||
):
|
||||
print("==================== DKG DECRYPTION NO CACHE ====================")
|
||||
original_validators = cohort[0].dkg_storage.get_validators(ritual_id)
|
||||
|
||||
for ursula in cohort:
|
||||
ursula.dkg_storage.clear(ritual_id)
|
||||
assert ursula.dkg_storage.get_validators(ritual_id) is None
|
||||
assert ursula.dkg_storage.get_active_ritual(ritual_id) is None
|
||||
|
||||
# perform threshold decryption
|
||||
bob.start_learning_loop(now=True)
|
||||
cleartext = bob.threshold_decrypt(
|
||||
cleartext = yield bob.threshold_decrypt(
|
||||
threshold_message_kit=threshold_message_kit,
|
||||
)
|
||||
assert bytes(cleartext) == plaintext.encode()
|
||||
|
@ -294,45 +378,7 @@ def check_decrypt_without_any_cached_values(
|
|||
assert v.public_key == original_validators[v_index].public_key
|
||||
|
||||
assert num_used_ursulas >= ritual.threshold
|
||||
print("===================== DECRYPTION SUCCESSFUL =====================")
|
||||
|
||||
|
||||
@pytest_twisted.inlineCallbacks
|
||||
def test_authorized_decryption(
|
||||
bob,
|
||||
global_allow_list,
|
||||
accounts,
|
||||
coordinator_agent,
|
||||
threshold_message_kit,
|
||||
signer,
|
||||
initiator,
|
||||
ritual_id,
|
||||
plaintext,
|
||||
):
|
||||
print("==================== DKG DECRYPTION (AUTHORIZED) ====================")
|
||||
# authorize Enrico to encrypt for ritual
|
||||
global_allow_list.authorize(
|
||||
ritual_id,
|
||||
[signer.accounts[0]],
|
||||
sender=accounts[initiator.transacting_power.account],
|
||||
)
|
||||
|
||||
# ritual_id, ciphertext, conditions are obtained from the side channel
|
||||
bob.start_learning_loop(now=True)
|
||||
cleartext = yield bob.threshold_decrypt(
|
||||
threshold_message_kit=threshold_message_kit,
|
||||
)
|
||||
assert bytes(cleartext) == plaintext.encode()
|
||||
|
||||
# check prometheus metric for decryption requests
|
||||
# since all running on the same machine - the value is not per-ursula but rather all
|
||||
num_successes = REGISTRY.get_sample_value(
|
||||
"threshold_decryption_num_successes_total"
|
||||
)
|
||||
|
||||
ritual = coordinator_agent.get_ritual(ritual_id)
|
||||
# at least a threshold of ursulas were successful (concurrency)
|
||||
assert int(num_successes) >= ritual.threshold
|
||||
print("===================== DECRYPTION NO CACHE SUCCESSFUL =====================")
|
||||
yield
|
||||
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ def test_ursula_dkg_rounds_fault_tolerance(
|
|||
ursulas,
|
||||
testerchain,
|
||||
ritual_token,
|
||||
fee_model,
|
||||
global_allow_list,
|
||||
coordinator_agent,
|
||||
accounts,
|
||||
|
@ -30,6 +31,7 @@ def test_ursula_dkg_rounds_fault_tolerance(
|
|||
initiator,
|
||||
testerchain,
|
||||
ritual_token,
|
||||
fee_model,
|
||||
global_allow_list,
|
||||
coordinator_agent,
|
||||
cohort_addresses,
|
||||
|
@ -61,21 +63,21 @@ def initiate_dkg(
|
|||
initiator,
|
||||
testerchain,
|
||||
ritual_token,
|
||||
fee_model,
|
||||
global_allow_list,
|
||||
coordinator_agent,
|
||||
cohort_addresses,
|
||||
):
|
||||
duration = 24 * 60 * 60
|
||||
# Approve the ritual token for the coordinator agent to spend
|
||||
amount = coordinator_agent.get_ritual_initiation_cost(
|
||||
providers=cohort_addresses, duration=duration
|
||||
)
|
||||
amount = fee_model.getRitualCost(len(cohort_addresses), duration)
|
||||
ritual_token.approve(
|
||||
coordinator_agent.contract_address,
|
||||
fee_model.address,
|
||||
amount,
|
||||
sender=accounts[initiator.transacting_power.account],
|
||||
)
|
||||
receipt = coordinator_agent.initiate_ritual(
|
||||
fee_model=fee_model.address,
|
||||
providers=cohort_addresses,
|
||||
authority=initiator.transacting_power.account,
|
||||
duration=duration,
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import os
|
||||
|
||||
import pytest
|
||||
import pytest_twisted
|
||||
from eth_utils import keccak
|
||||
|
@ -10,6 +8,7 @@ from twisted.internet.task import deferLater
|
|||
from nucypher.blockchain.eth.agents import CoordinatorAgent
|
||||
from nucypher.blockchain.eth.models import Coordinator
|
||||
from nucypher.crypto.powers import TransactingPower
|
||||
from tests.utils.dkg import generate_fake_ritual_transcript, threshold_from_shares
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
|
@ -17,11 +16,6 @@ def agent(coordinator_agent) -> CoordinatorAgent:
|
|||
return coordinator_agent
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def transcripts():
|
||||
return [os.urandom(32), os.urandom(32)]
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("ursulas")
|
||||
@pytest.fixture(scope="module")
|
||||
def cohort(staking_providers):
|
||||
|
@ -62,6 +56,7 @@ def test_initiate_ritual(
|
|||
agent,
|
||||
cohort,
|
||||
get_random_checksum_address,
|
||||
fee_model,
|
||||
global_allow_list,
|
||||
transacting_powers,
|
||||
ritual_token,
|
||||
|
@ -72,17 +67,18 @@ def test_initiate_ritual(
|
|||
assert number_of_rituals == 0
|
||||
|
||||
duration = 60 * 60 * 24
|
||||
amount = agent.get_ritual_initiation_cost(cohort, duration)
|
||||
amount = fee_model.getRitualCost(len(cohort), duration)
|
||||
|
||||
# Approve the ritual token for the coordinator agent to spend
|
||||
ritual_token.approve(
|
||||
agent.contract_address,
|
||||
fee_model.address,
|
||||
amount,
|
||||
sender=accounts[initiator.transacting_power.account],
|
||||
)
|
||||
|
||||
authority = get_random_checksum_address()
|
||||
receipt = agent.initiate_ritual(
|
||||
fee_model=fee_model.address,
|
||||
providers=cohort,
|
||||
authority=authority,
|
||||
duration=duration,
|
||||
|
@ -114,15 +110,20 @@ def test_initiate_ritual(
|
|||
|
||||
@pytest_twisted.inlineCallbacks
|
||||
def test_post_transcript(
|
||||
agent, transcripts, transacting_powers, testerchain, clock, mock_async_hooks
|
||||
agent, transacting_powers, testerchain, clock, mock_async_hooks
|
||||
):
|
||||
ritual_id = agent.number_of_rituals() - 1
|
||||
dkg_size = len(transacting_powers)
|
||||
threshold = threshold_from_shares(dkg_size)
|
||||
|
||||
txs = []
|
||||
for i, transacting_power in enumerate(transacting_powers):
|
||||
transcripts = []
|
||||
for transacting_power in transacting_powers:
|
||||
transcript = generate_fake_ritual_transcript(dkg_size, threshold)
|
||||
transcripts.append(transcript)
|
||||
async_tx = agent.post_transcript(
|
||||
ritual_id=ritual_id,
|
||||
transcript=transcripts[i],
|
||||
transcript=transcript,
|
||||
transacting_power=transacting_power,
|
||||
async_tx_hooks=mock_async_hooks,
|
||||
)
|
||||
|
@ -169,7 +170,6 @@ def test_post_transcript(
|
|||
@pytest_twisted.inlineCallbacks
|
||||
def test_post_aggregation(
|
||||
agent,
|
||||
aggregated_transcript,
|
||||
dkg_public_key,
|
||||
transacting_powers,
|
||||
cohort,
|
||||
|
@ -183,7 +183,11 @@ def test_post_aggregation(
|
|||
txs = []
|
||||
participant_public_key = SessionStaticSecret.random().public_key()
|
||||
|
||||
for i, transacting_power in enumerate(transacting_powers):
|
||||
dkg_size = len(transacting_powers)
|
||||
threshold = threshold_from_shares(dkg_size)
|
||||
aggregated_transcript = generate_fake_ritual_transcript(dkg_size, threshold)
|
||||
|
||||
for transacting_power in transacting_powers:
|
||||
async_tx = agent.post_aggregation(
|
||||
ritual_id=ritual_id,
|
||||
aggregated_transcript=aggregated_transcript,
|
||||
|
|
|
@ -11,11 +11,6 @@ dependencies:
|
|||
solidity:
|
||||
version: 0.8.23
|
||||
evm_version: paris
|
||||
import_remapping:
|
||||
- "@openzeppelin/contracts=openzeppelin/v5.0.0"
|
||||
- "@openzeppelin-upgradeable/contracts=openzeppelin-upgradeable/v5.0.0"
|
||||
- "@fx-portal/contracts=fx-portal/v1.0.5"
|
||||
- "@threshold/contracts=threshold/v1.2.1"
|
||||
|
||||
- name: openzeppelin
|
||||
github: OpenZeppelin/openzeppelin-contracts
|
||||
|
@ -24,11 +19,10 @@ dependencies:
|
|||
solidity:
|
||||
version: 0.8.23
|
||||
evm_version: paris
|
||||
import_remapping:
|
||||
- "@openzeppelin/contracts=openzeppelin/v5.0.0"
|
||||
|
||||
test:
|
||||
provider:
|
||||
chain_id: 131277322940537 # ensure ape doesn't change chain id to 1337
|
||||
mnemonic: test test test test test test test test test test test junk
|
||||
number_of_accounts: 30
|
||||
balance: 1_000_000 ETH
|
||||
|
|
|
@ -2,26 +2,26 @@ import pytest
|
|||
|
||||
from nucypher.blockchain.eth.agents import (
|
||||
ContractAgency,
|
||||
NucypherTokenAgent,
|
||||
SubscriptionManagerAgent,
|
||||
)
|
||||
from nucypher.policy.conditions.context import USER_ADDRESS_CONTEXT
|
||||
from nucypher.policy.conditions.evm import ContractCondition
|
||||
from nucypher.policy.conditions.lingo import (
|
||||
AndCompoundCondition,
|
||||
ConditionLingo,
|
||||
OrCompoundCondition,
|
||||
ReturnValueTest,
|
||||
)
|
||||
from nucypher.policy.conditions.utils import ConditionProviderManager
|
||||
from tests.constants import TEST_ETH_PROVIDER_URI, TESTERCHAIN_CHAIN_ID
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def condition_providers(testerchain):
|
||||
providers = {testerchain.client.chain_id: {testerchain.provider}}
|
||||
providers = ConditionProviderManager(
|
||||
{testerchain.client.chain_id: {testerchain.provider}}
|
||||
)
|
||||
return providers
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def compound_lingo(
|
||||
erc721_evm_condition_balanceof,
|
||||
|
@ -35,12 +35,8 @@ def compound_lingo(
|
|||
operands=[
|
||||
erc721_evm_condition_balanceof,
|
||||
time_condition,
|
||||
AndCompoundCondition(
|
||||
operands=[
|
||||
rpc_condition,
|
||||
erc20_evm_condition_balanceof,
|
||||
]
|
||||
),
|
||||
rpc_condition,
|
||||
erc20_evm_condition_balanceof,
|
||||
]
|
||||
)
|
||||
)
|
||||
|
@ -48,14 +44,9 @@ def compound_lingo(
|
|||
|
||||
|
||||
@pytest.fixture()
|
||||
def erc20_evm_condition_balanceof(testerchain, test_registry):
|
||||
token = ContractAgency.get_agent(
|
||||
NucypherTokenAgent,
|
||||
registry=test_registry,
|
||||
blockchain_endpoint=TEST_ETH_PROVIDER_URI,
|
||||
)
|
||||
def erc20_evm_condition_balanceof(testerchain, test_registry, ritual_token):
|
||||
condition = ContractCondition(
|
||||
contract_address=token.contract.address,
|
||||
contract_address=ritual_token.address,
|
||||
method="balanceOf",
|
||||
standard_contract_type="ERC20",
|
||||
chain=TESTERCHAIN_CHAIN_ID,
|
||||
|
@ -66,14 +57,12 @@ def erc20_evm_condition_balanceof(testerchain, test_registry):
|
|||
|
||||
|
||||
@pytest.fixture
|
||||
def erc721_contract(accounts, project):
|
||||
account = accounts[0]
|
||||
|
||||
def erc721_contract(project, deployer_account):
|
||||
# deploy contract
|
||||
deployed_contract = project.ConditionNFT.deploy(sender=account)
|
||||
deployed_contract = project.ConditionNFT.deploy(sender=deployer_account)
|
||||
|
||||
# mint nft with token id = 1
|
||||
deployed_contract.mint(account.address, 1, sender=account)
|
||||
deployed_contract.mint(deployer_account.address, 1, sender=deployer_account)
|
||||
return deployed_contract
|
||||
|
||||
|
||||
|
@ -152,15 +141,10 @@ def subscription_manager_is_active_policy_condition(testerchain, test_registry):
|
|||
|
||||
@pytest.fixture
|
||||
def custom_context_variable_erc20_condition(
|
||||
test_registry, testerchain, mock_condition_blockchains
|
||||
test_registry, testerchain, mock_condition_blockchains, ritual_token
|
||||
):
|
||||
token = ContractAgency.get_agent(
|
||||
NucypherTokenAgent,
|
||||
registry=test_registry,
|
||||
blockchain_endpoint=TEST_ETH_PROVIDER_URI,
|
||||
)
|
||||
condition = ContractCondition(
|
||||
contract_address=token.contract.address,
|
||||
contract_address=ritual_token.address,
|
||||
method="balanceOf",
|
||||
standard_contract_type="ERC20",
|
||||
chain=TESTERCHAIN_CHAIN_ID,
|
||||
|
@ -168,3 +152,11 @@ def custom_context_variable_erc20_condition(
|
|||
parameters=[":addressToUse"],
|
||||
)
|
||||
return condition
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def eip1271_contract_wallet(project, deployer_account):
|
||||
_eip1271_contract_wallet = deployer_account.deploy(
|
||||
project.SmartContractWallet, deployer_account.address
|
||||
)
|
||||
return _eip1271_contract_wallet
|
||||
|
|
|
@ -3,6 +3,7 @@ import os
|
|||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from eth_account.messages import defunct_hash_message, encode_defunct
|
||||
from hexbytes import HexBytes
|
||||
from web3 import Web3
|
||||
from web3.providers import BaseProvider
|
||||
|
@ -13,6 +14,7 @@ from nucypher.blockchain.eth.agents import (
|
|||
SubscriptionManagerAgent,
|
||||
)
|
||||
from nucypher.blockchain.eth.constants import NULL_ADDRESS
|
||||
from nucypher.policy.conditions.auth.evm import EvmAuth
|
||||
from nucypher.policy.conditions.context import (
|
||||
USER_ADDRESS_CONTEXT,
|
||||
get_context_value,
|
||||
|
@ -22,17 +24,18 @@ from nucypher.policy.conditions.evm import (
|
|||
RPCCondition,
|
||||
)
|
||||
from nucypher.policy.conditions.exceptions import (
|
||||
InvalidCondition,
|
||||
NoConnectionToChain,
|
||||
RequiredContextVariable,
|
||||
RPCExecutionFailed,
|
||||
)
|
||||
from nucypher.policy.conditions.json.rpc import JsonRpcCondition
|
||||
from nucypher.policy.conditions.lingo import (
|
||||
ConditionLingo,
|
||||
ConditionType,
|
||||
NotCompoundCondition,
|
||||
ReturnValueTest,
|
||||
)
|
||||
from nucypher.policy.conditions.utils import ConditionProviderManager
|
||||
from tests.constants import (
|
||||
TEST_ETH_PROVIDER_URI,
|
||||
TEST_POLYGON_PROVIDER_URI,
|
||||
|
@ -42,6 +45,28 @@ from tests.utils.policy import make_message_kits
|
|||
|
||||
GET_CONTEXT_VALUE_IMPORT_PATH = "nucypher.policy.conditions.context.get_context_value"
|
||||
|
||||
getActiveStakingProviders_abi_2_params = {
|
||||
"type": "function",
|
||||
"name": "getActiveStakingProviders",
|
||||
"stateMutability": "view",
|
||||
"inputs": [
|
||||
{"name": "_startIndex", "type": "uint256", "internalType": "uint256"},
|
||||
{
|
||||
"name": "_maxStakingProviders",
|
||||
"type": "uint256",
|
||||
"internalType": "uint256",
|
||||
},
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "allAuthorizedTokens", "type": "uint96", "internalType": "uint96"},
|
||||
{
|
||||
"name": "activeStakingProviders",
|
||||
"type": "bytes32[]",
|
||||
"internalType": "bytes32[]",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _dont_validate_user_address(context_variable: str, **context):
|
||||
if context_variable == USER_ADDRESS_CONTEXT:
|
||||
|
@ -67,11 +92,12 @@ def test_rpc_condition_evaluation_no_providers(
|
|||
):
|
||||
context = {USER_ADDRESS_CONTEXT: {"address": accounts.unassigned_accounts[0]}}
|
||||
with pytest.raises(NoConnectionToChain):
|
||||
_ = rpc_condition.verify(providers={}, **context)
|
||||
_ = rpc_condition.verify(providers=ConditionProviderManager({}), **context)
|
||||
|
||||
with pytest.raises(NoConnectionToChain):
|
||||
_ = rpc_condition.verify(
|
||||
providers={testerchain.client.chain_id: set()}, **context
|
||||
providers=ConditionProviderManager({testerchain.client.chain_id: list()}),
|
||||
**context,
|
||||
)
|
||||
|
||||
|
||||
|
@ -84,10 +110,11 @@ def test_rpc_condition_evaluation_invalid_provider_for_chain(
|
|||
):
|
||||
context = {USER_ADDRESS_CONTEXT: {"address": accounts.unassigned_accounts[0]}}
|
||||
new_chain = 23
|
||||
rpc_condition.chain = new_chain
|
||||
condition_providers = {new_chain: {testerchain.provider}}
|
||||
rpc_condition.execution_call.chain = new_chain
|
||||
condition_providers = ConditionProviderManager({new_chain: [testerchain.provider]})
|
||||
with pytest.raises(
|
||||
InvalidCondition, match=f"can only be evaluated on chain ID {new_chain}"
|
||||
NoConnectionToChain,
|
||||
match=f"Problematic provider endpoints for chain ID {new_chain}",
|
||||
):
|
||||
_ = rpc_condition.verify(providers=condition_providers, **context)
|
||||
|
||||
|
@ -118,13 +145,15 @@ def test_rpc_condition_evaluation_multiple_chain_providers(
|
|||
):
|
||||
context = {USER_ADDRESS_CONTEXT: {"address": accounts.unassigned_accounts[0]}}
|
||||
|
||||
condition_providers = {
|
||||
"1": {"fake1a", "fake1b"},
|
||||
"2": {"fake2"},
|
||||
"3": {"fake3"},
|
||||
"4": {"fake4"},
|
||||
TESTERCHAIN_CHAIN_ID: {testerchain.provider},
|
||||
}
|
||||
condition_providers = ConditionProviderManager(
|
||||
{
|
||||
"1": ["fake1a", "fake1b"],
|
||||
"2": ["fake2"],
|
||||
"3": ["fake3"],
|
||||
"4": ["fake4"],
|
||||
TESTERCHAIN_CHAIN_ID: [testerchain.provider],
|
||||
}
|
||||
)
|
||||
|
||||
condition_result, call_result = rpc_condition.verify(
|
||||
providers=condition_providers, **context
|
||||
|
@ -144,22 +173,17 @@ def test_rpc_condition_evaluation_multiple_providers_no_valid_fallback(
|
|||
):
|
||||
context = {USER_ADDRESS_CONTEXT: {"address": accounts.unassigned_accounts[0]}}
|
||||
|
||||
def my_configure_w3(provider: BaseProvider):
|
||||
return Web3(provider)
|
||||
|
||||
condition_providers = {
|
||||
TESTERCHAIN_CHAIN_ID: {
|
||||
mocker.Mock(spec=BaseProvider),
|
||||
mocker.Mock(spec=BaseProvider),
|
||||
mocker.Mock(spec=BaseProvider),
|
||||
condition_providers = ConditionProviderManager(
|
||||
{
|
||||
TESTERCHAIN_CHAIN_ID: [
|
||||
mocker.Mock(spec=BaseProvider),
|
||||
mocker.Mock(spec=BaseProvider),
|
||||
mocker.Mock(spec=BaseProvider),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
mocker.patch.object(
|
||||
rpc_condition, "_check_chain_id", return_value=None
|
||||
) # skip chain check
|
||||
mocker.patch.object(rpc_condition, "_configure_w3", my_configure_w3)
|
||||
)
|
||||
|
||||
mocker.patch.object(condition_providers, "_check_chain_id", return_value=None)
|
||||
with pytest.raises(RPCExecutionFailed):
|
||||
_ = rpc_condition.verify(providers=condition_providers, **context)
|
||||
|
||||
|
@ -173,22 +197,18 @@ def test_rpc_condition_evaluation_multiple_providers_valid_fallback(
|
|||
):
|
||||
context = {USER_ADDRESS_CONTEXT: {"address": accounts.unassigned_accounts[0]}}
|
||||
|
||||
def my_configure_w3(provider: BaseProvider):
|
||||
return Web3(provider)
|
||||
|
||||
condition_providers = {
|
||||
TESTERCHAIN_CHAIN_ID: {
|
||||
mocker.Mock(spec=BaseProvider),
|
||||
mocker.Mock(spec=BaseProvider),
|
||||
mocker.Mock(spec=BaseProvider),
|
||||
testerchain.provider,
|
||||
condition_providers = ConditionProviderManager(
|
||||
{
|
||||
TESTERCHAIN_CHAIN_ID: [
|
||||
mocker.Mock(spec=BaseProvider),
|
||||
mocker.Mock(spec=BaseProvider),
|
||||
mocker.Mock(spec=BaseProvider),
|
||||
testerchain.provider,
|
||||
]
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
mocker.patch.object(
|
||||
rpc_condition, "_check_chain_id", return_value=None
|
||||
) # skip chain check
|
||||
mocker.patch.object(rpc_condition, "_configure_w3", my_configure_w3)
|
||||
mocker.patch.object(condition_providers, "_check_chain_id", return_value=None)
|
||||
|
||||
condition_result, call_result = rpc_condition.verify(
|
||||
providers=condition_providers, **context
|
||||
|
@ -211,10 +231,12 @@ def test_rpc_condition_evaluation_no_connection_to_chain(
|
|||
context = {USER_ADDRESS_CONTEXT: {"address": accounts.unassigned_accounts[0]}}
|
||||
|
||||
# condition providers for other unrelated chains
|
||||
providers = {
|
||||
1: mock.Mock(), # mainnet
|
||||
11155111: mock.Mock(), # Sepolia
|
||||
}
|
||||
providers = ConditionProviderManager(
|
||||
{
|
||||
1: [mock.Mock()], # mainnet
|
||||
11155111: [mock.Mock()], # Sepolia
|
||||
}
|
||||
)
|
||||
|
||||
with pytest.raises(NoConnectionToChain):
|
||||
rpc_condition.verify(providers=providers, **context)
|
||||
|
@ -253,7 +275,10 @@ def test_rpc_condition_evaluation_with_context_var_in_return_value_test(
|
|||
invalid_balance = balance + 1
|
||||
context[":balanceContextVar"] = invalid_balance
|
||||
condition_result, call_result = rpc_condition.verify(
|
||||
providers={testerchain.client.chain_id: [testerchain.provider]}, **context
|
||||
providers=ConditionProviderManager(
|
||||
{testerchain.client.chain_id: [testerchain.provider]}
|
||||
),
|
||||
**context,
|
||||
)
|
||||
assert condition_result is False
|
||||
assert call_result != invalid_balance
|
||||
|
@ -770,30 +795,9 @@ def test_contract_condition_using_overloaded_function(
|
|||
#
|
||||
# valid overloaded function - 2 params
|
||||
#
|
||||
valid_abi_2_params = {
|
||||
"type": "function",
|
||||
"name": "getActiveStakingProviders",
|
||||
"stateMutability": "view",
|
||||
"inputs": [
|
||||
{"name": "_startIndex", "type": "uint256", "internalType": "uint256"},
|
||||
{
|
||||
"name": "_maxStakingProviders",
|
||||
"type": "uint256",
|
||||
"internalType": "uint256",
|
||||
},
|
||||
],
|
||||
"outputs": [
|
||||
{"name": "allAuthorizedTokens", "type": "uint96", "internalType": "uint96"},
|
||||
{
|
||||
"name": "activeStakingProviders",
|
||||
"type": "bytes32[]",
|
||||
"internalType": "bytes32[]",
|
||||
},
|
||||
],
|
||||
}
|
||||
condition = ContractCondition(
|
||||
contract_address=taco_child_application_agent.contract.address,
|
||||
function_abi=ABIFunction(valid_abi_2_params),
|
||||
function_abi=ABIFunction(getActiveStakingProviders_abi_2_params),
|
||||
method="getActiveStakingProviders",
|
||||
chain=TESTERCHAIN_CHAIN_ID,
|
||||
return_value_test=ReturnValueTest("==", ":expectedStakingProviders"),
|
||||
|
@ -897,3 +901,145 @@ def test_contract_condition_using_overloaded_function(
|
|||
)
|
||||
with pytest.raises(RPCExecutionFailed):
|
||||
_ = condition.verify(providers=condition_providers, **context)
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason="This test uses a public rpc endpoint")
|
||||
def test_json_rpc_condition_non_evm_prototyping_example():
|
||||
condition = JsonRpcCondition(
|
||||
endpoint="https://api.mainnet-beta.solana.com",
|
||||
method="getBlockTime",
|
||||
params=[308103883],
|
||||
return_value_test=ReturnValueTest(">=", 1734461499),
|
||||
)
|
||||
success, _ = condition.verify()
|
||||
assert success
|
||||
|
||||
condition = JsonRpcCondition(
|
||||
endpoint="https://api.mainnet-beta.solana.com",
|
||||
method="getBalance",
|
||||
params=["83astBRguLMdt2h5U1Tpdq5tjFoJ6noeGwaY3mDLVcri"],
|
||||
query="$.value",
|
||||
return_value_test=ReturnValueTest(">=", 0),
|
||||
)
|
||||
success, _ = condition.verify()
|
||||
assert success
|
||||
|
||||
condition = JsonRpcCondition(
|
||||
endpoint="https://bitcoin.drpc.org",
|
||||
method="getblock",
|
||||
params=["00000000000000000001ed4d40e6b602d7f09b9d47d5e046d52339cc6673a486"],
|
||||
query="$.time",
|
||||
return_value_test=ReturnValueTest(">=", 1734461294),
|
||||
)
|
||||
success, _ = condition.verify()
|
||||
assert success
|
||||
|
||||
|
||||
def test_rpc_condition_using_eip1271(
|
||||
deployer_account, eip1271_contract_wallet, condition_providers
|
||||
):
|
||||
# send some ETH to the smart contract wallet
|
||||
eth_amount = Web3.to_wei(2.25, "ether")
|
||||
|
||||
encoded_deposit_function = eip1271_contract_wallet.deposit.encode_input().hex()
|
||||
deployer_account.transfer(
|
||||
account=eip1271_contract_wallet.address,
|
||||
value=eth_amount,
|
||||
data=encoded_deposit_function,
|
||||
)
|
||||
|
||||
rpc_condition = RPCCondition(
|
||||
method="eth_getBalance",
|
||||
chain=TESTERCHAIN_CHAIN_ID,
|
||||
parameters=[USER_ADDRESS_CONTEXT],
|
||||
return_value_test=ReturnValueTest("==", eth_amount),
|
||||
)
|
||||
|
||||
data = f"I'm the owner of the smart contract wallet address {eip1271_contract_wallet.address}"
|
||||
signable_message = encode_defunct(text=data)
|
||||
hash = defunct_hash_message(text=data)
|
||||
message_signature = deployer_account.sign_message(signable_message)
|
||||
hex_signature = HexBytes(message_signature.encode_rsv()).hex()
|
||||
|
||||
typedData = {"chain": TESTERCHAIN_CHAIN_ID, "dataHash": hash.hex()}
|
||||
auth_message = {
|
||||
"signature": f"{hex_signature}",
|
||||
"address": f"{eip1271_contract_wallet.address}",
|
||||
"scheme": EvmAuth.AuthScheme.EIP1271.value,
|
||||
"typedData": typedData,
|
||||
}
|
||||
context = {
|
||||
USER_ADDRESS_CONTEXT: auth_message,
|
||||
}
|
||||
condition_result, call_result = rpc_condition.verify(
|
||||
providers=condition_providers, **context
|
||||
)
|
||||
assert condition_result is True
|
||||
assert call_result == eth_amount
|
||||
|
||||
# withdraw some ETH and check condition again
|
||||
withdraw_amount = Web3.to_wei(1, "ether")
|
||||
eip1271_contract_wallet.withdraw(withdraw_amount, sender=deployer_account)
|
||||
condition_result, call_result = rpc_condition.verify(
|
||||
providers=condition_providers, **context
|
||||
)
|
||||
assert condition_result is False
|
||||
assert call_result != eth_amount
|
||||
assert call_result == (eth_amount - withdraw_amount)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("staking_providers")
|
||||
def test_big_int_string_handling(
|
||||
accounts, taco_child_application_agent, bob, condition_providers
|
||||
):
|
||||
(
|
||||
total_staked,
|
||||
providers,
|
||||
) = taco_child_application_agent._get_active_staking_providers_raw(0, 10, 0)
|
||||
expected_result = [
|
||||
total_staked,
|
||||
[
|
||||
HexBytes(provider_bytes).hex() for provider_bytes in providers
|
||||
], # must be json serializable
|
||||
]
|
||||
|
||||
context = {
|
||||
":expectedStakingProviders": expected_result,
|
||||
} # user-defined context vars
|
||||
|
||||
contract_condition = {
|
||||
"conditionType": ConditionType.CONTRACT.value,
|
||||
"contractAddress": taco_child_application_agent.contract.address,
|
||||
"functionAbi": getActiveStakingProviders_abi_2_params,
|
||||
"chain": TESTERCHAIN_CHAIN_ID,
|
||||
"method": "getActiveStakingProviders",
|
||||
"parameters": ["0n", "10n"], # use bigint notation
|
||||
"returnValueTest": {
|
||||
"comparator": "==",
|
||||
"value": ":expectedStakingProviders",
|
||||
},
|
||||
}
|
||||
rpc_condition = {
|
||||
"conditionType": ConditionType.RPC.value,
|
||||
"chain": TESTERCHAIN_CHAIN_ID,
|
||||
"method": "eth_getBalance",
|
||||
"parameters": [bob.checksum_address, "latest"],
|
||||
"returnValueTest": {
|
||||
"comparator": ">=",
|
||||
"value": "10000000000000n",
|
||||
}, # use bigint notation
|
||||
}
|
||||
compound_condition = {
|
||||
"version": ConditionLingo.VERSION,
|
||||
"condition": {
|
||||
"conditionType": ConditionType.COMPOUND.value,
|
||||
"operator": "and",
|
||||
"operands": [contract_condition, rpc_condition],
|
||||
},
|
||||
}
|
||||
|
||||
compound_condition_json = json.dumps(compound_condition)
|
||||
condition_result = ConditionLingo.from_json(compound_condition_json).eval(
|
||||
providers=condition_providers, **context
|
||||
)
|
||||
assert condition_result, "condition executed and passes"
|
||||
|
|
|
@ -2,8 +2,15 @@ from collections import defaultdict
|
|||
|
||||
import pytest
|
||||
|
||||
from nucypher.policy.conditions.evm import RPCCondition
|
||||
from nucypher.policy.conditions.lingo import ConditionLingo, ConditionType
|
||||
from nucypher.policy.conditions.evm import RPCCall, RPCCondition
|
||||
from nucypher.policy.conditions.lingo import (
|
||||
CompoundAccessControlCondition,
|
||||
ConditionLingo,
|
||||
ConditionType,
|
||||
ReturnValueTest,
|
||||
)
|
||||
from nucypher.policy.conditions.time import TimeCondition
|
||||
from nucypher.policy.conditions.utils import ConditionProviderManager
|
||||
from nucypher.utilities.logging import GlobalLoggerSettings
|
||||
from tests.utils.policy import make_message_kits
|
||||
|
||||
|
@ -14,22 +21,28 @@ def make_multichain_evm_conditions(bob, chain_ids):
|
|||
"""This is a helper function to make a set of conditions that are valid on multiple chains."""
|
||||
operands = list()
|
||||
for chain_id in chain_ids:
|
||||
operand = [
|
||||
{
|
||||
"conditionType": ConditionType.TIME.value,
|
||||
"returnValueTest": {"value": 0, "comparator": ">"},
|
||||
"method": "blocktime",
|
||||
"chain": chain_id,
|
||||
},
|
||||
{
|
||||
"conditionType": ConditionType.RPC.value,
|
||||
"chain": chain_id,
|
||||
"method": "eth_getBalance",
|
||||
"parameters": [bob.checksum_address, "latest"],
|
||||
"returnValueTest": {"comparator": ">=", "value": 10000000000000},
|
||||
},
|
||||
]
|
||||
operands.extend(operand)
|
||||
compound_and_condition = CompoundAccessControlCondition(
|
||||
operator="and",
|
||||
operands=[
|
||||
TimeCondition(
|
||||
chain=chain_id,
|
||||
return_value_test=ReturnValueTest(
|
||||
comparator=">",
|
||||
value=0,
|
||||
),
|
||||
),
|
||||
RPCCondition(
|
||||
chain=chain_id,
|
||||
method="eth_getBalance",
|
||||
parameters=[bob.checksum_address, "latest"],
|
||||
return_value_test=ReturnValueTest(
|
||||
comparator=">=",
|
||||
value=10000000000000,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
operands.append(compound_and_condition.to_dict())
|
||||
|
||||
_conditions = {
|
||||
"version": ConditionLingo.VERSION,
|
||||
|
@ -49,7 +62,7 @@ def conditions(bob, multichain_ids):
|
|||
|
||||
|
||||
def test_single_retrieve_with_multichain_conditions(
|
||||
enacted_policy, bob, multichain_ursulas, conditions, mock_rpc_condition
|
||||
enacted_policy, bob, multichain_ursulas, conditions, monkeymodule, testerchain
|
||||
):
|
||||
bob.remember_node(multichain_ursulas[0])
|
||||
bob.start_learning_loop()
|
||||
|
@ -59,6 +72,11 @@ def test_single_retrieve_with_multichain_conditions(
|
|||
encrypted_treasure_map=enacted_policy.treasure_map,
|
||||
alice_verifying_key=enacted_policy.publisher_verifying_key,
|
||||
)
|
||||
monkeymodule.setattr(
|
||||
ConditionProviderManager,
|
||||
"web3_endpoints",
|
||||
lambda *args, **kwargs: [testerchain.w3],
|
||||
)
|
||||
|
||||
cleartexts = bob.retrieve_and_decrypt(
|
||||
message_kits=message_kits,
|
||||
|
@ -69,7 +87,7 @@ def test_single_retrieve_with_multichain_conditions(
|
|||
|
||||
|
||||
def test_single_decryption_request_with_faulty_rpc_endpoint(
|
||||
enacted_policy, bob, multichain_ursulas, conditions, mock_rpc_condition
|
||||
monkeymodule, testerchain, enacted_policy, bob, multichain_ursulas, conditions
|
||||
):
|
||||
bob.remember_node(multichain_ursulas[0])
|
||||
bob.start_learning_loop()
|
||||
|
@ -80,30 +98,30 @@ def test_single_decryption_request_with_faulty_rpc_endpoint(
|
|||
alice_verifying_key=enacted_policy.publisher_verifying_key,
|
||||
)
|
||||
|
||||
calls = defaultdict(int)
|
||||
original_execute_call = RPCCondition._execute_call
|
||||
monkeymodule.setattr(
|
||||
ConditionProviderManager,
|
||||
"web3_endpoints",
|
||||
lambda *args, **kwargs: [testerchain.w3, testerchain.w3],
|
||||
) # a base, and fallback
|
||||
|
||||
def faulty_execute_call(*args, **kwargs):
|
||||
rpc_calls = defaultdict(int)
|
||||
original_execute_call = RPCCall._execute
|
||||
|
||||
def faulty_rpc_execute_call(*args, **kwargs):
|
||||
"""Intercept the call to the RPC endpoint and raise an exception on the second call."""
|
||||
nonlocal calls
|
||||
rpc_call = args[0]
|
||||
calls[rpc_call.chain] += 1
|
||||
if (
|
||||
calls[rpc_call.chain] == 2
|
||||
and "tester://multichain.0" in rpc_call.provider.endpoint_uri
|
||||
):
|
||||
nonlocal rpc_calls
|
||||
rpc_call_object = args[0]
|
||||
rpc_calls[rpc_call_object.chain] += 1
|
||||
if rpc_calls[rpc_call_object.chain] % 2 == 0:
|
||||
# simulate a network error
|
||||
raise ConnectionError("Something went wrong with the network")
|
||||
elif calls[rpc_call.chain] == 3:
|
||||
# check the provider is the fallback
|
||||
this_uri = rpc_call.provider.endpoint_uri
|
||||
assert "fallback" in this_uri
|
||||
|
||||
# make original call
|
||||
return original_execute_call(*args, **kwargs)
|
||||
|
||||
RPCCondition._execute_call = faulty_execute_call
|
||||
monkeymodule.setattr(RPCCall, "_execute", faulty_rpc_execute_call)
|
||||
cleartexts = bob.retrieve_and_decrypt(
|
||||
message_kits=message_kits,
|
||||
**policy_info_kwargs,
|
||||
)
|
||||
assert cleartexts == messages
|
||||
RPCCondition._execute_call = original_execute_call
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import random
|
||||
|
||||
import maya
|
||||
import pytest
|
||||
from web3 import Web3
|
||||
|
||||
|
@ -14,14 +13,12 @@ from nucypher.blockchain.eth.agents import (
|
|||
)
|
||||
from nucypher.blockchain.eth.interfaces import BlockchainInterfaceFactory
|
||||
from nucypher.blockchain.eth.registry import ContractRegistry, RegistrySourceManager
|
||||
from nucypher.policy.conditions.evm import RPCCondition
|
||||
from nucypher.utilities.logging import Logger
|
||||
from tests.constants import (
|
||||
BONUS_TOKENS_FOR_TESTS,
|
||||
MIN_OPERATOR_SECONDS,
|
||||
TEMPORARY_DOMAIN,
|
||||
TEST_ETH_PROVIDER_URI,
|
||||
TESTERCHAIN_CHAIN_ID,
|
||||
)
|
||||
from tests.utils.blockchain import ReservedTestAccountManager, TesterBlockchain
|
||||
from tests.utils.registry import ApeRegistrySource
|
||||
|
@ -32,6 +29,7 @@ from tests.utils.ursula import (
|
|||
|
||||
test_logger = Logger("acceptance-test-logger")
|
||||
|
||||
ONE_DAY = 24 * 60 * 60
|
||||
|
||||
# ERC-20
|
||||
TOTAL_SUPPLY = Web3.to_wei(11_000_000_000, "ether")
|
||||
|
@ -42,17 +40,12 @@ NU_TOTAL_SUPPLY = Web3.to_wei(
|
|||
# TACo Application
|
||||
MIN_AUTHORIZATION = Web3.to_wei(40_000, "ether")
|
||||
|
||||
REWARD_DURATION = 60 * 60 * 24 * 7 # one week in seconds
|
||||
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 deployment
|
||||
REWARD_DURATION = 7 * ONE_DAY # one week in seconds
|
||||
DEAUTHORIZATION_DURATION = 60 * ONE_DAY # 60 days in seconds
|
||||
|
||||
PENALTY_DEFAULT = 1000 # 10% penalty
|
||||
PENALTY_INCREMENT = 2500 # 25% penalty increment
|
||||
PENALTY_DURATION = 60 * 60 * 24 # 1 day in seconds
|
||||
PENALTY_DURATION = ONE_DAY # 1 day in seconds
|
||||
|
||||
|
||||
# Coordinator
|
||||
|
@ -138,14 +131,6 @@ def t_token(nucypher_dependency, deployer_account):
|
|||
return _t_token
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def nu_token(nucypher_dependency, deployer_account):
|
||||
_nu_token = deployer_account.deploy(
|
||||
nucypher_dependency.NuCypherToken, NU_TOTAL_SUPPLY
|
||||
)
|
||||
return _nu_token
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def threshold_staking(nucypher_dependency, deployer_account):
|
||||
_threshold_staking = deployer_account.deploy(
|
||||
|
@ -170,8 +155,6 @@ def taco_application(
|
|||
MIN_OPERATOR_SECONDS,
|
||||
REWARD_DURATION,
|
||||
DEAUTHORIZATION_DURATION,
|
||||
[COMMITMENT_DURATION_1, COMMITMENT_DURATION_2],
|
||||
maya.now().epoch + COMMITMENT_DEADLINE,
|
||||
PENALTY_DEFAULT,
|
||||
PENALTY_DURATION,
|
||||
PENALTY_INCREMENT,
|
||||
|
@ -237,8 +220,6 @@ def coordinator(
|
|||
_coordinator = deployer_account.deploy(
|
||||
nucypher_dependency.Coordinator,
|
||||
taco_child_application.address,
|
||||
ritual_token.address,
|
||||
FEE_RATE,
|
||||
)
|
||||
|
||||
encoded_initializer_function = _coordinator.initialize.encode_input(
|
||||
|
@ -252,13 +233,33 @@ def coordinator(
|
|||
)
|
||||
|
||||
proxy_contract = nucypher_dependency.Coordinator.at(proxy.address)
|
||||
proxy_contract.makeInitiationPublic(sender=deployer_account)
|
||||
taco_child_application.initialize(
|
||||
proxy_contract.address, adjudicator.address, sender=deployer_account
|
||||
)
|
||||
return proxy_contract
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def fee_model(nucypher_dependency, deployer_account, coordinator, ritual_token):
|
||||
contract = deployer_account.deploy(
|
||||
nucypher_dependency.FlatRateFeeModel,
|
||||
coordinator.address,
|
||||
ritual_token.address,
|
||||
FEE_RATE,
|
||||
)
|
||||
coordinator.grantRole(
|
||||
coordinator.TREASURY_ROLE(), deployer_account.address, sender=deployer_account
|
||||
)
|
||||
coordinator.grantRole(
|
||||
coordinator.FEE_MODEL_MANAGER_ROLE(),
|
||||
deployer_account.address,
|
||||
sender=deployer_account,
|
||||
)
|
||||
coordinator.approveFeeModel(contract.address, sender=deployer_account)
|
||||
|
||||
return contract
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def global_allow_list(nucypher_dependency, deployer_account, coordinator):
|
||||
contract = deployer_account.deploy(
|
||||
|
@ -285,11 +286,11 @@ def subscription_manager(nucypher_dependency, deployer_account):
|
|||
def deployed_contracts(
|
||||
ritual_token,
|
||||
t_token,
|
||||
nu_token,
|
||||
threshold_staking,
|
||||
taco_application,
|
||||
taco_child_application,
|
||||
coordinator,
|
||||
fee_model,
|
||||
global_allow_list,
|
||||
subscription_manager,
|
||||
):
|
||||
|
@ -297,11 +298,11 @@ def deployed_contracts(
|
|||
deployments = [
|
||||
ritual_token,
|
||||
t_token,
|
||||
nu_token,
|
||||
threshold_staking,
|
||||
taco_application,
|
||||
taco_child_application,
|
||||
coordinator,
|
||||
fee_model,
|
||||
global_allow_list,
|
||||
subscription_manager,
|
||||
]
|
||||
|
@ -412,19 +413,6 @@ def taco_child_application_agent(testerchain, test_registry):
|
|||
# Conditions
|
||||
#
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def mock_rpc_condition(module_mocker, testerchain, monkeymodule):
|
||||
def configure_mock(condition, provider, *args, **kwargs):
|
||||
condition.provider = provider
|
||||
return testerchain.w3
|
||||
|
||||
monkeymodule.setattr(RPCCondition, "_configure_w3", configure_mock)
|
||||
configure_spy = module_mocker.spy(RPCCondition, "_configure_w3")
|
||||
|
||||
chain_id_check_mock = module_mocker.patch.object(RPCCondition, "_check_chain_id")
|
||||
return configure_spy, chain_id_check_mock
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def multichain_ids(module_mocker):
|
||||
ids = mock_permitted_multichain_connections(mocker=module_mocker)
|
||||
|
@ -432,7 +420,7 @@ def multichain_ids(module_mocker):
|
|||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def multichain_ursulas(ursulas, multichain_ids, mock_rpc_condition):
|
||||
def multichain_ursulas(ursulas, multichain_ids):
|
||||
setup_multichain_ursulas(ursulas=ursulas, chain_ids=multichain_ids)
|
||||
return ursulas
|
||||
|
||||
|
@ -440,10 +428,6 @@ def multichain_ursulas(ursulas, multichain_ids, mock_rpc_condition):
|
|||
@pytest.fixture(scope="module", autouse=True)
|
||||
def mock_condition_blockchains(module_mocker):
|
||||
"""adds testerchain's chain ID to permitted conditional chains"""
|
||||
module_mocker.patch.dict(
|
||||
"nucypher.policy.conditions.evm._CONDITION_CHAINS",
|
||||
{TESTERCHAIN_CHAIN_ID: "eth-tester/pyevm"},
|
||||
)
|
||||
|
||||
module_mocker.patch(
|
||||
"nucypher.blockchain.eth.domains.get_domain", return_value=TEMPORARY_DOMAIN
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue