Merge pull request #2960 from nucypher/tdec

[EPIC] Condition-Based Decryption PoC
pull/2985/head
KPrasch 2022-10-18 20:21:58 +02:00 committed by GitHub
commit 723cc00afc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
96 changed files with 6730 additions and 1283 deletions

View File

@ -9,6 +9,7 @@ recursive-exclude * __pycache__
global-exclude *.py[cod]
recursive-include nucypher/blockchain/eth/contract_registry *.json *.md
recursive-include nucypher/policy/conditions *.json
prune nucypher/blockchain/eth/contract_registry/historical
recursive-include nucypher/network/templates *.html *.mako
recursive-include nucypher/utilities/templates *.html *.mako

View File

@ -12,7 +12,7 @@ constant-sorrow = ">=0.1.0a9"
bytestring-splitter = ">=2.4.0"
hendrix = ">=3.4"
lmdb = "*"
nucypher-core = ">=0.2"
nucypher-core = ">=0.4.0"
# Cryptography
pyopenssl = "*"
cryptography = ">=3.2"
@ -38,6 +38,7 @@ py-evm = "*"
eth-tester = "*"
web3 = "*"
eth-utils = "*"
eip712-structs = "*"
# CLI / Configuration
appdirs = "*"
click = ">=7.0"

1760
Pipfile.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
FROM python:3.8.12-slim
FROM nucypher/rust-python:3.8.12
# Update
RUN apt-get update -y && apt upgrade -y

View File

@ -1,49 +1,50 @@
-i https://pypi.python.org/simple
attrs==21.4.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
attrs==22.1.0; python_version >= '3.5'
bandit==1.7.4
certifi==2021.10.8
certifi==2022.9.24; python_version >= '3.6'
cfgv==3.3.1; python_full_version >= '3.6.1'
charset-normalizer==2.0.12; python_version >= '3'
coverage==6.3.2
charset-normalizer==2.1.1; python_version >= '3.6'
coverage==6.5.0
decorator==5.1.1; python_version >= '3.5'
distlib==0.3.4
distlib==0.3.6
exceptiongroup==1.0.0rc9; python_version < '3.11'
execnet==1.9.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
filelock==3.6.0; python_version >= '3.7'
filelock==3.8.0; python_version >= '3.7'
gitdb==4.0.9; python_version >= '3.6'
gitpython==3.1.27; python_version >= '3.7'
greenlet==2.0.0a2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
hypothesis==6.45.0
identify==2.4.12; python_version >= '3.7'
idna==3.3; python_version >= '3'
hypothesis==6.56.0
identify==2.5.5; python_version >= '3.7'
idna==3.4; python_version >= '3.5'
iniconfig==1.1.1
mypy-extensions==0.4.3
mypy==0.942
nodeenv==1.6.0
mypy==0.981
nodeenv==1.7.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'
packaging==21.3; python_version >= '3.6'
pbr==5.8.1; python_version >= '2.6'
pbr==5.10.0; python_version >= '2.6'
platformdirs==2.5.2; python_version >= '3.7'
pluggy==1.0.0; python_version >= '3.6'
pre-commit==2.18.1
pre-commit==2.20.0
py-solc-x==0.10.1
py==1.11.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
pyflakes==2.4.0
pyparsing==3.0.8; python_full_version >= '3.6.8'
pytest-cov==3.0.0
pyflakes==2.5.0
pyparsing==3.0.9; python_full_version >= '3.6.8'
pytest-cov==4.0.0
pytest-forked==1.4.0; python_version >= '3.6'
pytest-mock==3.7.0
pytest-mock==3.9.0
pytest-timeout==2.1.0
pytest-twisted==1.13.4
pytest-xdist==2.5.0
pytest==6.2.5
pyyaml==6.0; python_version >= '3.6'
requests==2.27.1
semantic-version==2.9.0; python_version >= '2.7'
six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'
requests==2.28.1
semantic-version==2.10.0; python_version >= '2.7'
setuptools==65.4.1; python_version >= '3.7'
smmap==5.0.0; python_version >= '3.6'
sortedcontainers==2.4.0
stevedore==3.5.0; python_version >= '3.6'
stevedore==4.0.0; python_version >= '3.8'
toml==0.10.2; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'
tomli==2.0.1; python_version >= '3.7'
typing-extensions==3.10.0.2
urllib3==1.26.9; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'
virtualenv==20.14.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
tomli==2.0.1; python_version < '3.11'
typing-extensions==4.3.0; python_version >= '3.7'
urllib3==1.26.12; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4'
virtualenv==20.16.5; python_version >= '3.6'

View File

@ -1,19 +1,19 @@
-i https://pypi.python.org/simple
alabaster==0.7.12
attrs==21.4.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
attrs==22.1.0; python_version >= '3.5'
autosemver==0.5.5
babel==2.10.1; python_version >= '3.6'
certifi==2021.10.8
charset-normalizer==2.0.12; python_version >= '3'
babel==2.10.3; python_version >= '3.6'
certifi==2022.9.24; python_version >= '3.6'
charset-normalizer==2.1.1; python_version >= '3.6'
click-default-group==1.2.2
click==8.1.2; python_version >= '3.7'
coverage==6.3.2; python_version >= '3.7'
click==8.1.3; python_version >= '3.7'
coverage==6.5.0; python_version >= '3.7'
docutils==0.17.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
dulwich==0.19.16
execnet==1.9.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
git+https://github.com/inspirehep/jsonschema2rst.git@d211d9709046431a6b6a4594c97c22d8713d854b#egg=jsonschema2rst
idna==3.3; python_version >= '3'
imagesize==1.3.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
idna==3.4; python_version >= '3.5'
imagesize==1.4.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
incremental==21.3.0
iniconfig==1.1.1
isort==5.10.1; python_full_version >= '3.6.1' and python_version < '4'
@ -25,17 +25,17 @@ pep8==1.7.1
pluggy==1.0.0; python_version >= '3.6'
py-solc-x==0.10.1
py==1.11.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
pygments==2.12.0; python_version >= '3.6'
pyparsing==3.0.8; python_full_version >= '3.6.8'
pygments==2.13.0; python_version >= '3.6'
pyparsing==3.0.9; python_full_version >= '3.6.8'
pytest-cache==1.0
pytest-cov==3.0.0; python_version >= '3.6'
pytest-cov==4.0.0; python_version >= '3.6'
pytest-pep8==1.0.6
pytest==7.1.2; python_version >= '3.7'
pytz==2022.1
pytest==7.1.3; python_version >= '3.7'
pytz==2022.4
pyyaml==6.0; python_version >= '3.6'
requests==2.27.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'
semantic-version==2.9.0; python_version >= '2.7'
setuptools==62.1.0; python_version >= '3.7'
requests==2.28.1; python_version >= '3.7' and python_version < '4'
semantic-version==2.10.0; python_version >= '2.7'
setuptools==65.4.1; python_version >= '3.7'
snowballstemmer==2.2.0
sphinx-rtd-theme==1.0.0
sphinx==3.0.1
@ -45,6 +45,6 @@ sphinxcontrib-htmlhelp==2.0.0; python_version >= '3.6'
sphinxcontrib-jsmath==1.0.1; python_version >= '3.5'
sphinxcontrib-qthelp==1.0.3; python_version >= '3.5'
sphinxcontrib-serializinghtml==1.1.5; python_version >= '3.5'
tomli==2.0.1; python_version >= '3.6'
towncrier==21.9.0
urllib3==1.26.9; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'
tomli==2.0.1; python_version >= '3.7'
towncrier==22.8.0
urllib3==1.26.12; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4'

View File

@ -649,7 +649,7 @@ Example Response
}
]
},
"version": "6.0.0"
"version": "6.1.0"
}
@ -674,6 +674,11 @@ Parameters
+-------------------------------------------+---------------+----------------------------------------+
| ``bob_verifying_key`` | String | Bob's verifying key encoded as hex. |
+-------------------------------------------+---------------+----------------------------------------+
| ``context`` *(Optional)* | String | | Associated JSON dictionary required |
| | | | during re-encryption e.g. data to |
| | | | satisfy re-encryption conditions. |
+-------------------------------------------+---------------+----------------------------------------+
* A single *retrieval kit* is an encapsulation of the information necessary to obtain cfrags from Ursulas.
It contains a capsule and the checksum addresses of the Ursulas from which the requester has
@ -698,6 +703,10 @@ Parameters
because some Ursulas may have experienced a blip in connectivity. This is an optional optimization that provides
retry functionality that skips previously successful reencryption operations.
* A *context* is an associated JSON dictionary of data required during re-encryption. One such example is when a condition for re-encryption
requires proof of ownership of a wallet address; the *context* is used to provide the data and signature required for the proof.
Returns
^^^^^^^
The result of the re-encryption operations performed:
@ -720,12 +729,6 @@ Example Request
"bob_verifying_key": "039c19e5d44b016af126d89488c4ae5599e0fde9ea30047754d1fe173d05eee468",
"policy_encrypting_key": "02cdb2cec70b568c0624b72450c2043836aa831b06b196a50db461e87acddb791e"}'
OR
.. code:: bash
curl -X POST "<PORTER URI>/retrieve_crags?retrieval_kits=%5B%27gANDYgMKitDPd%2FQttLGy%2Bs7Oacnm8pfbl3Qs2UD3IS1d9wF3awJsXnjFq7OkRQE45DV4%2BMa2lDSJ5SeKEBqJK5GdPMB6CRwJ1hX7Y5SYgzpZtr%2FZ5%2FS3DHgVKn%2B8fWX92FaqEXIGcQBjYnVpbHRpbnMKc2V0CnEBXXEChXEDUnEEhnEFLg%3D%3D%27%5D&alice_verifying_key=02d3389864e9e7206ae1d18301bbd67ad8e0bdf257b3085c9aa13e9438ff9133f2&bob_encrypting_key=03d41cb7aa2df98cb9fb1591b5556363862a367faae6d0e4874a860321141788cb&bob_verifying_key=039c19e5d44b016af126d89488c4ae5599e0fde9ea30047754d1fe173d05eee468&policy_encrypting_key=02cdb2cec70b568c0624b72450c2043836aa831b06b196a50db461e87acddb791e&treasure_map=ivOS2%2FMarBpkLAksM0O%2BpgLUHAV%2F0ceIBarBKwqUpAXARhpvuwAAAm0DoDAtioScWJSHWNGzQd9pMGW2dRF4IvJX%2FExALF6AcLICLCBP%2Btte8QR4l0GLNy3YwK4oO8f8Ht0Ij%2Bv0feWWwgeo3R7FVeC4ExDuYvgdsV6jCP3vqZnLphIPk8LQeo1XVAABAtM4mGTp5yBq4dGDAbvWetjgvfJXswhcmqE%2BlDj%2FkTPyAAAB5H0rD40N1u5Ct455sh4SicbHTGsXcRSt%2FadeHVl3zylNpWDsFbeon7VI5fGGmWLAKmCJ5ARU1Mgfwg0pfsXDgHTky6XOeXnNw630z9muBE4NMUiESOQm%2FRAsphMR%2FDEIMRaCgjhaE2diVdVAm15JjRXV9JN5gAp58Y1ecPcWR2lMcgAMHBFMX60bpbgjySha94Hwb0kR2SKIFkPQnuMljoQxutTDAyh55eE2sHf9ZOAVZkpKQu8NkaWy7adx%2F1QefezNbngX9c2yYml133Al4oGrLWYA3fnbod2Y6F1oeG5As5ZIW%2FO8k7Rf%2B3i9a%2BDS1i%2BKbgETHQGxOkQSpNPUmwJjtzDJQ1xFMmKkxgwUtXenfyrzDDPU6EQloWK2PmyTD%2FhSKHLpkLyzYp95gadzDiS8RlOnNw%2FuP8vfMPSrXYGZSKXvHvlrQxKOjnF7FrheauwwRPjM0yYTftPs3jNkZwCTl%2BEwn6NdLur927SeGyAB3gHCjHenje%2B3hU1jsn%2FmwfwLJwSMT7V0rbXV6I0NYhjQy2Ajj%2B7ev%2FNSvRdeneeYTU3iHoO6nIhWHBLVExWafu59B6hhsm261kvXw718eiUcL%2B1X1eZ5WApplCuXGQV7L6DZxlQPanRJy7BZZQmFwEUoMCnx9mGbOKmNbeCADx3vwKY5nrbTDAAAAm0Cccv5a3jS2QiICCzCyA0Ot3U7VT1F3d%2BB3cHcmv8DaCwDODb8IadnsiVK%2BdfbPLn3ne%2Blm8d9yqhh6bLi6KNDb6yiWrjWnd4Irnnh3amMwik00vdyQKYvdsaSEJqtVLmtcQABAtM4mGTp5yBq4dGDAbvWetjgvfJXswhcmqE%2BlDj%2FkTPyAAAB5Do%2Feww%2BG709VPQwkxd0tRFyJh97Wcb5uSGs%2B4fK9O%2B5CTf5rDQSO3ueWLRF4ytRzd3QjK6%2B8FlXsJQM5n5pGLUNNWpUlimk2MmPaLehC9uGBfQzoTfQof%2BU8CBXkTTnRi0IeAYMq8eiuEnNR%2FoOJjpjuwgZH4gue%2FsSDF8FyhFU4SwF%2FWdjLg0FgmZzRlqABNXeE8vOofydEMYgUMPd8qxjimAGhkYlBUNjlme4BUdA2AqndMttpc3y9ILTobaGSnjgWfq9Ztw%2Fn72scPI11T%2BYMaaXd33dacNPx%2BpVzcgqi358PT8WQ6U3n%2B1be8mhF8VGEO7%2F5zLFHECRCv06erER8ChTZvr4rb8Y0xRCz%2FpatllLqvWZkGSmotmsi9qAptgG%2FXkozOZIqmBuM2AuQTwaePyuJzelc5xD51OlkQRahV6%2Bok3CokckwtOXtC6dzq4dmh03Uj5ZeKj8IgITDPN6jCf5TwLmXSuEGl5W%2FxmrEUeNlrthlJm7Cdd1NpLn3RZNCgSS4%2BPw9cpY6fj%2FmF8yR0erf9Tkrxr7FXzSe%2FUWkfeB3aQPulP4U3nM7vJIz9DBcJxtdozfqHchZ%2FK%2BVnaW%2F7IlNhvu3Cwk%2BN3D9sUwf%2FuHQuE%2FQSsYZ0fjUCnB1UgJMjho5Sd4CHLNoCFroNj71YtnpdXjUQAAAm0D5ITdM1U28%2B6%2FLU%2B%2BJw%2FUTMOefScVULkEyaojkyJK574Dc96zie3HtMN0ahALfOg5yn2z2zZpwqsLk9mpT23GD8AYR55RcvLHGIjJTubtuMINy7ZBgbZmisPDt5DvHVKj1wABAtM4mGTp5yBq4dGDAbvWetjgvfJXswhcmqE%2BlDj%2FkTPyAAAB5B9Wn5rfJ8LS81DIkZj6By39KZPYLoNSpR%2BVEZsLaSEo%2FHTMG43Q%2FgG%2FYjZMQHBEZwleE1H35P3EuDlWOriEQxveH7ihmHVSDfj8R%2B6xo%2F263QCLqtg9djSFPW7h6QRx5JBM%2BWABcmIZQrAvMDe1q7F8VOGRDMf8tW%2F7sySMFn9pQ7735kasw8iNsGPX9gVNcncSuh8hmgWGzwciUU%2FY5SYmQvl0Oc15G5%2FkFhIA9nDVfZR4sMBRB9ApYbnNYsxtH12wWhTo04hPEGfzsqKK10muLy%2Bqpo3VBhX24HPTBAvYm68f0UVD%2Ba0cZWmgYKypmMqApJ87RnPvXbE3rmKepJM8u02O4X1OBlfDZBrTsbCbMxeniS6bzE6VPE62jOW6GIuyV6%2BNQS3PZTuTWG%2Fp7T5n2EC%2FPf%2FCvGLq41gQDU9VT2aCbHkbr9C0klVJfUwqdE%2F51zLmcY8wpx3P%2BOS%2BlrIjxQzOpWSKQfsNyt1DhKpKb5Y1wWrUGm6s0sBEG7FQK2SmWMhpjB36ZRdmtQ8%2Fmvh20KELR6W%2BocGosR20TXdGINzJEnobbTkkGNz2sqzePvL7Ql5Utc%2FGCaZYC2yIvJEGBOSBVtKvwqTOaMOFTaCIx4R5f3X17umkMD1YCvir39cREkU%3D"
Example Response
^^^^^^^^^^^^^^^^
@ -747,7 +750,7 @@ Example Response
}
]
},
"version": "6.0.0"
"version": "6.1.0"
}

View File

@ -32,7 +32,7 @@ class Economics:
_default_min_authorization = TToken(40_000, 'T').to_units()
_default_min_operator_seconds = 60 * 60 * 24 # one day in seconds
_default_fee_rate = Wei(Web3.toWei(1, 'gwei'))
_default_fee_rate = Wei(Web3.to_wei(1, 'gwei'))
# TODO: Reintroduce Adjudicator
# Slashing parameters

View File

@ -110,7 +110,7 @@ class BaseActor:
"""Return this actor's current ETH balance"""
blockchain = BlockchainInterfaceFactory.get_interface() # TODO: EthAgent? #1509
balance = blockchain.client.get_balance(self.wallet_address)
return Web3.fromWei(balance, 'ether')
return Web3.from_wei(balance, 'ether')
@property
def wallet_address(self):
@ -362,7 +362,7 @@ class Operator(BaseActor):
ether_balance = client.get_balance(self.operator_address)
if ether_balance:
# funds found
funded, balance = True, Web3.fromWei(ether_balance, 'ether')
funded, balance = True, Web3.from_wei(ether_balance, 'ether')
emitter.message(f"✓ Operator {self.operator_address} is funded with {balance} ETH", color='green')
else:
emitter.message(f"! Operator {self.operator_address} is not funded with ETH", color="yellow")

View File

@ -227,7 +227,7 @@ class EthereumClient:
@property
def is_connected(self):
return self.w3.isConnected()
return self.w3.is_connected()
@property
def etherbase(self) -> str:
@ -238,7 +238,7 @@ class EthereumClient:
return self.w3.eth.accounts
def get_balance(self, account):
return self.w3.eth.getBalance(account)
return self.w3.eth.get_balance(account)
def inject_middleware(self, middleware, **kwargs):
self.w3.middleware_onion.inject(middleware, **kwargs)
@ -247,16 +247,16 @@ class EthereumClient:
self.w3.middleware_onion.add(middleware)
def set_gas_strategy(self, gas_strategy):
self.w3.eth.setGasPriceStrategy(gas_strategy)
self.w3.eth.set_gas_price_strategy(gas_strategy)
@property
def chain_id(self) -> int:
try:
# from hex-str
return int(self.w3.eth.chainId, 16)
return int(self.w3.eth.chain_id, 16)
except TypeError:
# from str
return int(self.w3.eth.chainId)
return int(self.w3.eth.chain_id)
@property
def net_version(self) -> int:
@ -270,18 +270,18 @@ class EthereumClient:
"""
Returns client's gas price. Underneath, it uses the eth_gasPrice JSON-RPC method
"""
return self.w3.eth.gasPrice
return self.w3.eth.gas_price
def gas_price_for_transaction(self, transaction=None) -> Wei:
"""
Obtains a gas price via the current gas strategy, if any; otherwise, it resorts to the client's gas price.
This method mirrors the behavior of web3._utils.transactions when building transactions.
"""
return self.w3.eth.generateGasPrice(transaction) or self.gas_price
return self.w3.eth.generate_gas_price(transaction) or self.gas_price
@property
def block_number(self) -> BlockNumber:
return self.w3.eth.blockNumber
return self.w3.eth.block_number
@property
def coinbase(self) -> ChecksumAddress:
@ -310,7 +310,7 @@ class EthereumClient:
else:
# If not asking for confirmations, just use web3 and assume the returned receipt is final
try:
receipt = self.w3.eth.waitForTransactionReceipt(transaction_hash=transaction_hash,
receipt = self.w3.eth.wait_for_transaction_receipt(transaction_hash=transaction_hash,
timeout=timeout,
poll_latency=self.TRANSACTION_POLLING_TIME)
except TimeExhausted:
@ -320,9 +320,11 @@ class EthereumClient:
def block_until_enough_confirmations(self, transaction_hash: str, timeout: float, confirmations: int) -> dict:
receipt: TxReceipt = self.w3.eth.waitForTransactionReceipt(transaction_hash=transaction_hash,
timeout=timeout,
poll_latency=self.TRANSACTION_POLLING_TIME)
receipt: TxReceipt = self.w3.eth.wait_for_transaction_receipt(
transaction_hash=transaction_hash,
timeout=timeout,
poll_latency=self.TRANSACTION_POLLING_TIME
)
preliminary_block_hash = Web3.toHex(receipt['blockHash'])
tx_block_number = Web3.toInt(receipt['blockNumber'])
@ -348,7 +350,7 @@ class EthereumClient:
def check_transaction_is_on_chain(self, receipt: TxReceipt) -> bool:
transaction_hash = Web3.toHex(receipt['transactionHash'])
try:
new_receipt = self.w3.eth.getTransactionReceipt(transaction_hash)
new_receipt = self.w3.eth.get_transaction_receipt(transaction_hash)
except TransactionNotFound:
reorg_detected = True
else:
@ -365,20 +367,20 @@ class EthereumClient:
raise NotImplementedError
def get_transaction(self, transaction_hash) -> dict:
return self.w3.eth.getTransaction(transaction_hash)
return self.w3.eth.get_transaction(transaction_hash)
def get_transaction_receipt(self, transaction_hash) -> Union[dict, None]:
return self.w3.eth.getTransactionReceipt(transaction_hash)
return self.w3.eth.get_transaction_receipt(transaction_hash)
def get_transaction_count(self, account: str, pending: bool) -> int:
block_identifier = 'pending' if pending else 'latest'
return self.w3.eth.getTransactionCount(account, block_identifier)
return self.w3.eth.get_transaction_count(account, block_identifier)
def send_transaction(self, transaction_dict: dict) -> str:
return self.w3.eth.sendTransaction(transaction_dict)
return self.w3.eth.send_transaction(transaction_dict)
def send_raw_transaction(self, transaction_bytes: bytes) -> str:
return self.w3.eth.sendRawTransaction(transaction_bytes)
return self.w3.eth.send_raw_transaction(transaction_bytes)
def sign_message(self, account: str, message: bytes) -> str:
"""
@ -389,12 +391,12 @@ class EthereumClient:
return self.w3.eth.sign(account, data=message)
def get_blocktime(self):
highest_block = self.w3.eth.getBlock('latest')
highest_block = self.w3.eth.get_block('latest')
now = highest_block['timestamp']
return now
def get_block(self, block_identifier):
return self.w3.eth.getBlock(block_identifier)
return self.w3.eth.get_block(block_identifier)
def _has_latest_block(self) -> bool:
# TODO: Investigate using `web3.middleware.make_stalecheck_middleware` #2060
@ -457,7 +459,7 @@ class GethClient(EthereumClient):
transaction_dict = dissoc(transaction_dict, 'to')
# Sign
result = self.w3.eth.signTransaction(transaction_dict)
result = self.w3.eth.sign_transaction(transaction_dict)
# Return RLP bytes
rlp_encoded_transaction = result.raw

View File

@ -34,7 +34,7 @@ class EventRecord:
except BlockchainInterfaceFactory.NoRegisteredInterfaces:
self.timestamp = None
else:
self.timestamp = blockchain.client.w3.eth.getBlock(self.block_number)['timestamp']
self.timestamp = blockchain.client.w3.eth.get_block(self.block_number)['timestamp']
def __repr__(self):
pairs_to_show = dict(self.args.items())

View File

@ -308,7 +308,7 @@ class BlockchainInterface:
configuration_message = f"Using gas strategy '{reported_gas_strategy}'"
if self.max_gas_price:
__price = Web3.toWei(self.max_gas_price, 'gwei') # from gwei to wei
__price = Web3.to_wei(self.max_gas_price, 'gwei') # from gwei to wei
gas_strategy = max_price_gas_strategy_wrapper(gas_strategy=gas_strategy, max_gas_price_wei=__price)
configuration_message += f", with a max price of {self.max_gas_price} gwei."
@ -316,7 +316,7 @@ class BlockchainInterface:
# TODO: This line must not be called prior to establishing a connection
# Move it down to a lower layer, near the client.
# gwei_gas_price = Web3.fromWei(self.client.gas_price_for_transaction(), 'gwei')
# gwei_gas_price = Web3.from_wei(self.client.gas_price_for_transaction(), 'gwei')
self.log.info(configuration_message)
# self.log.debug(f"Gas strategy currently reports a gas price of {gwei_gas_price} gwei.")
@ -505,11 +505,11 @@ class BlockchainInterface:
self.__log_transaction(transaction_dict=payload, contract_function=contract_function)
try:
if 'gas' not in payload: # i.e., transaction_gas_limit is not None
# As web3 buildTransaction() will estimate gas with block identifier "pending" by default,
# As web3 build_transaction() will estimate gas with block identifier "pending" by default,
# explicitly estimate gas here with block identifier 'latest' if not otherwise specified
# as a pending transaction can cause gas estimation to fail, notably in case of worklock refunds.
payload['gas'] = contract_function.estimateGas(payload, block_identifier='latest')
transaction_dict = contract_function.buildTransaction(payload)
payload['gas'] = contract_function.estimate_gas(payload, block_identifier='latest')
transaction_dict = contract_function.build_transaction(payload)
except (TestTransactionFailed, ValidationError, ValueError) as error:
# Note: Geth (1.9.15) raises ValueError in the same condition that pyevm raises ValidationError here.
# Treat this condition as "Transaction Failed" during gas estimation.
@ -570,9 +570,9 @@ class BlockchainInterface:
max_unit_price = transaction_dict['gasPrice']
tx_type = 'Legacy'
max_price_gwei = Web3.fromWei(max_unit_price, 'gwei')
max_price_gwei = Web3.from_wei(max_unit_price, 'gwei')
max_cost_wei = max_unit_price * transaction_dict['gas']
max_cost = Web3.fromWei(max_cost_wei, 'ether')
max_cost = Web3.from_wei(max_cost_wei, 'ether')
if transacting_power.is_device:
emitter.message(f'Confirm transaction {transaction_name} on hardware wallet... '

View File

@ -128,11 +128,11 @@ def prettify_eth_amount(amount, original_denomination: str = 'wei') -> str:
"""
try:
# First obtain canonical representation in wei. Works for int, float, Decimal and str amounts
amount_in_wei = Web3.toWei(Decimal(amount), original_denomination)
amount_in_wei = Web3.to_wei(Decimal(amount), original_denomination)
common_denominations = ('wei', 'gwei', 'ether')
options = [str(Web3.fromWei(amount_in_wei, d)) for d in common_denominations]
options = [str(Web3.from_wei(amount_in_wei, d)) for d in common_denominations]
best_option = min(zip(map(len, options), options, common_denominations))
_length, pretty_amount, denomination = best_option

View File

@ -24,16 +24,27 @@ from http import HTTPStatus
from json.decoder import JSONDecodeError
from pathlib import Path
from queue import Queue
from typing import Dict, Iterable, List, NamedTuple, Tuple, Union, Optional, Sequence, Set, Any
from typing import (
Any,
Dict,
Iterable,
List,
NamedTuple,
Optional,
Sequence,
Set,
Tuple,
Union,
)
import maya
from constant_sorrow import constants
from constant_sorrow.constants import (
PUBLIC_ONLY,
STRANGER_ALICE,
READY,
INVALIDATED,
NOT_SIGNED
NOT_SIGNED,
PUBLIC_ONLY,
READY,
STRANGER_ALICE,
)
from cryptography.hazmat.primitives.serialization import Encoding
from cryptography.x509 import Certificate, NameOID
@ -41,35 +52,42 @@ from eth_typing.evm import ChecksumAddress
from eth_utils import to_checksum_address
from flask import Response, request
from nucypher_core import (
MessageKit,
Address,
HRAC,
Conditions,
EncryptedKeyFrag,
TreasureMap,
EncryptedTreasureMap,
ReencryptionResponse,
MessageKit,
NodeMetadata,
NodeMetadataPayload,
HRAC,
)
from nucypher_core.umbral import (
PublicKey, VerifiedKeyFrag, reencrypt,
ReencryptionResponse,
TreasureMap,
)
from nucypher_core.umbral import PublicKey, VerifiedKeyFrag, reencrypt
from twisted.internet import reactor, stdio
from twisted.internet.defer import Deferred
from twisted.logger import Logger
from web3.types import TxReceipt
import nucypher
from nucypher.acumen.nicknames import Nickname
from nucypher.acumen.perception import ArchivedFleetState, RemoteUrsulaStatus
from nucypher.blockchain.eth.actors import Operator, BlockchainPolicyAuthor
from nucypher.blockchain.eth.actors import BlockchainPolicyAuthor, Operator
from nucypher.blockchain.eth.agents import ContractAgency, PREApplicationAgent
from nucypher.blockchain.eth.interfaces import BlockchainInterfaceFactory
from nucypher.blockchain.eth.registry import BaseContractRegistry
from nucypher.blockchain.eth.signers.software import Web3Signer
from nucypher.characters.banners import ALICE_BANNER, BOB_BANNER, ENRICO_BANNER, URSULA_BANNER
from nucypher.characters.banners import (
ALICE_BANNER,
BOB_BANNER,
ENRICO_BANNER,
URSULA_BANNER,
)
from nucypher.characters.base import Character, Learner
from nucypher.characters.control.interfaces import AliceInterface, BobInterface, EnricoInterface
from nucypher.characters.control.interfaces import (
AliceInterface,
BobInterface,
EnricoInterface,
)
from nucypher.cli.processes import UrsulaCommandProtocol
from nucypher.config.storages import NodeStorage
from nucypher.control.controllers import WebController
@ -80,19 +98,19 @@ from nucypher.crypto.powers import (
DelegatingPower,
PowerUpError,
SigningPower,
TransactingPower,
TLSHostingPower,
TransactingPower,
)
from nucypher.network.exceptions import NodeSeemsToBeDown
from nucypher.network.middleware import RestMiddleware
from nucypher.network.nodes import NodeSprout, TEACHER_NODES, Teacher
from nucypher.network.nodes import TEACHER_NODES, NodeSprout, Teacher
from nucypher.network.protocols import parse_node_uri
from nucypher.network.retrieval import RetrievalClient
from nucypher.network.server import ProxyRESTServer, make_rest_app
from nucypher.network.trackers import AvailabilityTracker, OperatorBondedTracker
from nucypher.policy.kits import PolicyMessageKit
from nucypher.policy.payment import PaymentMethod, FreeReencryptions
from nucypher.policy.policies import Policy, BlockchainPolicy, FederatedPolicy
from nucypher.policy.payment import FreeReencryptions, PaymentMethod
from nucypher.policy.policies import BlockchainPolicy, FederatedPolicy, Policy
from nucypher.utilities.logging import Logger
from nucypher.utilities.networking import validate_operator_ip
@ -553,9 +571,10 @@ class Bob(Character):
def retrieve(
self,
message_kits: Sequence[Union[MessageKit, PolicyMessageKit]],
alice_verifying_key: PublicKey, # KeyFrag signer's key
alice_verifying_key: PublicKey, # KeyFrag signer's key
encrypted_treasure_map: EncryptedTreasureMap,
publisher_verifying_key: Optional[PublicKey] = None,
**context, # TODO: dont use one context to rule them all
) -> List[PolicyMessageKit]:
"""
Attempts to retrieve reencrypted capsule fragments
@ -581,7 +600,7 @@ class Bob(Character):
treasure_map = self._treasure_maps[map_hash]
else:
# Have to decrypt the treasure map first to find out what the threshold is.
# Otherwise we could check the message kits for completeness right away.
# Otherwise, we could check the message kits for completeness right away.
treasure_map = self._decrypt_treasure_map(encrypted_treasure_map, publisher_verifying_key)
self._treasure_maps[map_hash] = treasure_map
@ -597,12 +616,14 @@ class Bob(Character):
# Retrieve capsule frags
client = RetrievalClient(learner=self)
retrieval_results = client.retrieve_cfrags(
retrieval_results, _ = client.retrieve_cfrags(
treasure_map=treasure_map,
retrieval_kits=retrieval_kits,
alice_verifying_key=alice_verifying_key,
bob_encrypting_key=self.public_keys(DecryptingPower),
bob_verifying_key=self.stamp.as_umbral_pubkey())
bob_verifying_key=self.stamp.as_umbral_pubkey(),
**context
)
# Refill message kits with newly retrieved capsule frags
results = []
@ -952,6 +973,7 @@ class Ursula(Teacher, Character, Operator):
if prometheus_config:
# Locally scoped to prevent import without prometheus explicitly installed
from nucypher.utilities.prometheus.metrics import start_prometheus_exporter
start_prometheus_exporter(ursula=self, prometheus_config=prometheus_config)
if emitter:
emitter.message(f"✓ Prometheus Exporter", color='green')
@ -1045,7 +1067,7 @@ class Ursula(Teacher, Character, Operator):
operator_signature = None
else:
operator_signature = self.operator_signature
payload = NodeMetadataPayload(staking_provider_address=self.canonical_address,
payload = NodeMetadataPayload(staking_provider_address=Address(self.canonical_address),
domain=self.domain,
timestamp_epoch=timestamp.epoch,
operator_signature=operator_signature,
@ -1235,10 +1257,8 @@ class Ursula(Teacher, Character, Operator):
cfrag = reencrypt(capsule, kfrag)
cfrags.append(cfrag)
self.log.info(f"Re-encrypted capsule {capsule} -> made {cfrag}.")
return ReencryptionResponse(signer=self.stamp.as_umbral_signer(),
capsules=capsules,
vcfrags=cfrags)
results = list(zip(capsules, cfrags))
return ReencryptionResponse(signer=self.stamp.as_umbral_signer(), capsules_and_vcfrags=results)
def status_info(self, omit_known_nodes: bool = False) -> 'LocalUrsulaStatus':
@ -1331,10 +1351,12 @@ class Enrico(Character):
if is_me:
self.log.info(self.banner.format(policy_encrypting_key))
def encrypt_message(self, plaintext: bytes) -> MessageKit:
def encrypt_message(self, plaintext: bytes, conditions: Optional[Dict[str, Union[str, int]]] = None) -> MessageKit:
# TODO: #2107 Rename to "encrypt"
conditions = Conditions(json.dumps(conditions or list()))
message_kit = MessageKit(policy_encrypting_key=self.policy_pubkey,
plaintext=plaintext)
plaintext=plaintext,
conditions=conditions)
return message_kit
@classmethod

View File

@ -117,7 +117,7 @@ def collect_policy_rate_and_value(alice: Alice, rate: int, value: int, shares: i
rate = alice.payment_method.rate # wei
if not force:
default_gwei = Web3.fromWei(rate, 'gwei') # wei -> gwei
default_gwei = Web3.from_wei(rate, 'gwei') # wei -> gwei
prompt = "Confirm rate of {node_rate} gwei * {shares} nodes ({period_rate} gwei per period)?"
if not click.confirm(prompt.format(node_rate=default_gwei, period_rate=default_gwei * shares, shares=shares), default=True):
@ -125,7 +125,7 @@ def collect_policy_rate_and_value(alice: Alice, rate: int, value: int, shares: i
# TODO: Interactive rate sampling & validation (#1709)
interactive_prompt = prompt.format(node_rate=interactive_rate, period_rate=interactive_rate * shares, shares=shares)
click.confirm(interactive_prompt, default=True, abort=True)
rate = Web3.toWei(interactive_rate, 'gwei') # gwei -> wei
rate = Web3.to_wei(interactive_rate, 'gwei') # gwei -> wei
return rate, value

View File

@ -103,7 +103,7 @@ def confirm_staged_grant(emitter, grant_request: Dict, federated: bool, seconds_
emitter.echo(tabulate(table, tablefmt="simple"))
return
period_rate = Web3.fromWei(pretty_request['shares'] * pretty_request['rate'], 'gwei')
period_rate = Web3.from_wei(pretty_request['shares'] * pretty_request['rate'], 'gwei')
pretty_request['rate'] = f"{pretty_request['rate']} wei/period * {pretty_request['shares']} nodes"
expiration = pretty_request['expiration']

View File

@ -117,7 +117,7 @@ def select_client_account(emitter,
is_staking = 'Yes' if bool(staker.stakes) else 'No'
row.append(is_staking)
if show_eth_balance:
ether_balance = Web3.fromWei(blockchain.client.get_balance(account), 'ether')
ether_balance = Web3.from_wei(blockchain.client.get_balance(account), 'ether')
row.append(f'{ether_balance} ETH')
if show_nu_balance:
token_agent = ContractAgency.get_agent(NucypherTokenAgent, registry=registry)

View File

@ -114,7 +114,7 @@ Registry ................ {registry.filepath}
token_contract_info = f"""
{token_agent.contract_name} ........... {token_agent.contract_address}
~ Ethers ............ {Web3.fromWei(blockchain.client.get_balance(token_agent.contract_address), 'ether')} ETH
~ Ethers ............ {Web3.from_wei(blockchain.client.get_balance(token_agent.contract_address), 'ether')} ETH
~ Tokens ............ {NU.from_units(token_agent.get_balance(token_agent.contract_address))}"""
except BaseContractRegistry.UnknownContract:
message = f"\n{NucypherTokenAgent.contract_name} is not enrolled in {registry.filepath}"
@ -146,12 +146,12 @@ Registry ................ {registry.filepath}
{agent.contract_name} .... {bare_contract.address}
~ Version ............ {bare_contract.version}
~ Owner .............. {bare_contract.functions.owner().call()}
~ Ethers ............. {Web3.fromWei(blockchain.client.get_balance(bare_contract.address), 'ether')} ETH
~ Ethers ............. {Web3.from_wei(blockchain.client.get_balance(bare_contract.address), 'ether')} ETH
~ Tokens ............. {NU.from_units(token_agent.get_balance(bare_contract.address))}
~ Dispatcher ......... {dispatcher_deployer.contract_address}
~ Owner .......... {dispatcher_deployer.contract.functions.owner().call()}
~ Target ......... {dispatcher_deployer.contract.functions.target().call()}
~ Ethers ......... {Web3.fromWei(blockchain.client.get_balance(dispatcher_deployer.contract_address), 'ether')} ETH
~ Ethers ......... {Web3.from_wei(blockchain.client.get_balance(dispatcher_deployer.contract_address), 'ether')} ETH
~ Tokens ......... {NU.from_units(token_agent.get_balance(dispatcher_deployer.contract_address))}"""
emitter.echo(proxy_payload)
emitter.echo(sep, nl=False)

View File

@ -35,7 +35,7 @@ def paint_contract_status(registry, emitter):
blockchain = f"""
| '{blockchain.client.chain_name}' Blockchain Network |
Gas Price ................ {Web3.fromWei(blockchain.client.gas_price, 'gwei')} Gwei
Gas Price ................ {Web3.from_wei(blockchain.client.gas_price, 'gwei')} Gwei
ETH Provider URI ......... {blockchain.eth_provider_uri}
Registry ................. {registry.filepath}
"""

View File

@ -28,9 +28,9 @@ from nucypher_core.umbral import PublicKey
from nucypher.blockchain.economics import Economics
from nucypher.blockchain.eth.interfaces import BlockchainInterface
from nucypher.blockchain.eth.networks import NetworksInventory
from nucypher.blockchain.eth.token import NU, TToken
from nucypher.blockchain.eth.token import TToken
from nucypher.policy.payment import PAYMENT_METHODS
from nucypher.utilities.networking import InvalidOperatorIP, validate_operator_ip
from nucypher.utilities.networking import validate_operator_ip, InvalidOperatorIP
class ChecksumAddress(click.ParamType):

View File

@ -74,4 +74,4 @@ TEMPORARY_DOMAIN = ":temporary-domain:" # for use with `--dev` node runtimes
NUCYPHER_EVENTS_THROTTLE_MAX_BLOCKS = 'NUCYPHER_EVENTS_THROTTLE_MAX_BLOCKS'
# Probationary period
END_OF_POLICIES_PROBATIONARY_PERIOD = MayaDT.from_iso8601("2022-08-31T23:59:59.0Z")
END_OF_POLICIES_PROBATIONARY_PERIOD = MayaDT.from_iso8601('2023-4-20T23:59:59.0Z')

View File

@ -14,7 +14,8 @@
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
from base64 import b64encode, b64decode
import json
from base64 import b64decode, b64encode
import click
from marshmallow import fields
@ -71,11 +72,49 @@ class PositiveInteger(Integer):
class Base64BytesRepresentation(BaseField, fields.Field):
"""Serializes/Deserializes any object's byte representation to/from bae64."""
def _serialize(self, value, attr, obj, **kwargs):
value_bytes = value if isinstance(value, bytes) else bytes(value)
return b64encode(value_bytes).decode()
try:
value_bytes = value if isinstance(value, bytes) else bytes(value)
return b64encode(value_bytes).decode()
except Exception as e:
raise InvalidInputData(
f"Provided object type, {type(value)}, is not serializable: {e}"
)
def _deserialize(self, value, attr, data, **kwargs):
try:
return b64decode(value)
except ValueError as e:
raise InvalidInputData(f"Could not parse {self.name}: {e}")
class JSON(BaseField, fields.Field):
"""Serializes/Deserializes objects to/from JSON strings."""
def __init__(self, expected_type=None, *args, **kwargs):
# enforce type-safety (TODO too strict?)
self.expected_type = expected_type
super().__init__(*args, **kwargs)
def _serialize(self, value, attr, obj, **kwargs):
if self.expected_type and (type(value) != self.expected_type):
raise InvalidInputData(
f"Unexpected object type, {type(value)}; expected {self.expected_type}")
try:
value_json = json.dumps(value)
return value_json
except Exception as e:
raise InvalidInputData(
f"Provided object type, {type(value)}, is not JSON serializable: {e}"
)
def _deserialize(self, value, attr, data, **kwargs):
try:
result = json.loads(value)
except Exception as e:
raise InvalidInputData(f"Invalid JSON: {e}")
else:
if self.expected_type and (type(result) != self.expected_type):
raise InvalidInputData(
f"Unexpected object type, {type(result)}; expected {self.expected_type}")
return result

View File

@ -14,34 +14,41 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
from collections import defaultdict
import json
import random
from typing import Dict, Sequence, List
from collections import defaultdict
from typing import Dict, List, Optional, Sequence, Tuple, Union
from eth_typing.evm import ChecksumAddress
from eth_utils import to_checksum_address
from twisted.logger import Logger
from nucypher_core import (
TreasureMap,
Conditions,
Context,
ReencryptionResponse,
ReencryptionRequest,
RetrievalKit,
)
TreasureMap,
)
from nucypher_core.umbral import (
Capsule,
PublicKey,
VerifiedCapsuleFrag,
VerificationError,
VerifiedCapsuleFrag,
)
from twisted.logger import Logger
from nucypher.crypto.signing import InvalidSignature
from nucypher.network.exceptions import NodeSeemsToBeDown
from nucypher.network.nodes import Learner
from nucypher.policy.conditions.lingo import ConditionLingo
from nucypher.policy.kits import RetrievalResult
class RetrievalError:
def __init__(self, errors: Dict[ChecksumAddress, str]):
self.errors = errors
class RetrievalPlan:
"""
An emphemeral object providing a service of selecting Ursulas for reencryption requests
@ -51,13 +58,23 @@ class RetrievalPlan:
def __init__(self, treasure_map: TreasureMap, retrieval_kits: Sequence[RetrievalKit]):
# Record the retrieval kits order
self._capsules = [retrieval_kit.capsule for retrieval_kit in retrieval_kits]
self._capsules, rust_conditions = tuple(zip(*((rk.capsule, rk.conditions) for rk in retrieval_kits)))
# Transform Conditions -> ConditionsLingo
json_conditions = (json.loads(str(c)) for c in rust_conditions)
self._conditions = list(ConditionLingo.from_list(lingo) if lingo else None for lingo in json_conditions)
self._threshold = treasure_map.threshold
# Records the retrieval results, indexed by capsule
self._results = {retrieval_kit.capsule: {}
for retrieval_kit in retrieval_kits} # {capsule: {ursula_address: cfrag}}
self._results = {
retrieval_kit.capsule: {} for retrieval_kit in retrieval_kits
} # {capsule: {ursula_address: cfrag}}
# Records the retrieval result errors, indexed by capsule
self._errors = {
retrieval_kit.capsule: {} for retrieval_kit in retrieval_kits
} # {capsule: {ursula_address: error}}
# Records the addresses of Ursulas that were already queried, indexed by capsule.
self._queried_addresses = {retrieval_kit.capsule: set(retrieval_kit.queried_addresses)
@ -94,12 +111,14 @@ class RetrievalPlan:
# Only request reencryption for capsules that:
# - haven't been processed by this Ursula
# - don't already have cfrags from `threshold` Ursulas
capsules = [capsule for capsule in self._capsules
packets = [(capsule, lingo) for capsule, lingo in zip(self._capsules, self._conditions)
if (capsule not in self._processed_capsules.get(ursula_address, set())
and len(self._queried_addresses[capsule]) < self._threshold)]
if len(capsules) > 0:
if len(packets) > 0:
capsules, conditions = list(zip(*packets))
return RetrievalWorkOrder(ursula_address=ursula_address,
capsules=capsules)
capsules=list(capsules),
lingo=list(conditions))
# Execution will not reach this point if `is_complete()` returned `False` before this call.
raise RuntimeError("No Ursulas left")
@ -113,6 +132,13 @@ class RetrievalPlan:
self._processed_capsules[work_order.ursula_address].add(capsule)
self._results[capsule][work_order.ursula_address] = cfrag
def update_errors(self,
work_order: "RetrievalWorkOrder",
ursula_address: ChecksumAddress,
error_message: str):
for capsule in work_order.capsules:
self._errors[capsule][ursula_address] = error_message
def is_complete(self) -> bool:
return (
# there are no more Ursulas to query
@ -121,11 +147,22 @@ class RetrievalPlan:
all(len(addresses) >= self._threshold for addresses in self._queried_addresses.values())
)
def results(self) -> List['RetrievalResult']:
# TODO (#1995): when that issue is fixed, conversion is no longer needed
return [RetrievalResult({to_checksum_address(address): cfrag
for address, cfrag in self._results[capsule].items()})
for capsule in self._capsules]
def results(self) -> Tuple[List["RetrievalResult"], List[RetrievalError]]:
results = []
errors = []
# maintain the same order with both lists
for capsule in self._capsules:
results.append(
RetrievalResult(
{
to_checksum_address(address): cfrag
for address, cfrag in self._results[capsule].items()
}
)
)
errors.append(RetrievalError(errors=self._errors[capsule]))
return results, errors
class RetrievalWorkOrder:
@ -133,9 +170,19 @@ class RetrievalWorkOrder:
A work order issued by a retrieval plan to request reencryption from an Ursula
"""
def __init__(self, ursula_address: ChecksumAddress, capsules: List[Capsule]):
def __init__(self,
ursula_address: ChecksumAddress, capsules: List[Capsule],
lingo: Optional[List[ConditionLingo]] = None
):
self.ursula_address = ursula_address
self.capsules = capsules
self.__lingo = lingo
def conditions(self, as_json=True) -> Union[str, List[ConditionLingo]]:
lingo = self.__lingo or list()
if as_json:
return json.dumps([l.to_list() if l else None for l in lingo])
return lingo
class RetrievalClient:
@ -214,7 +261,7 @@ class RetrievalClient:
self.log.warn(message)
raise RuntimeError(message) from e
except middleware.UnexpectedResponse:
raise # TODO: Handle this
raise # TODO: Handle this
try:
reencryption_response = ReencryptionResponse.from_bytes(response.content)
@ -225,36 +272,37 @@ class RetrievalClient:
ursula_verifying_key = ursula.stamp.as_umbral_pubkey()
# FIXME: Uncomment
try:
verified_cfrags = reencryption_response.verify(capsules=reencryption_request.capsules,
alice_verifying_key=alice_verifying_key,
ursula_verifying_key=ursula_verifying_key,
policy_encrypting_key=policy_encrypting_key,
bob_encrypting_key=bob_encrypting_key,
)
bob_encrypting_key=bob_encrypting_key)
except InvalidSignature as e:
self.log.warn(str(e))
self.log.warn(f"Invalid signature for ReencryptionResponse: {e}")
raise
except VerificationError:
except VerificationError as e:
# In future we may want to remember this Ursula and do something about it
self.log.warn("Failed to verify capsule frags in the ReencryptionResponse")
self.log.warn(
f"Failed to verify capsule frags in the ReencryptionResponse: {e}"
)
raise
except Exception as e:
message = f"Failed to verify the ReencryptionResponse: {e}"
message = f"Failed to verify the ReencryptionResponse ({e.__class__.__name__}): {e}"
self.log.warn(message)
raise RuntimeError(message)
return {capsule: vcfrag for capsule, vcfrag
in zip(reencryption_request.capsules, verified_cfrags)}
def retrieve_cfrags(
self,
treasure_map: TreasureMap,
retrieval_kits: Sequence[RetrievalKit],
alice_verifying_key: PublicKey, # KeyFrag signer's key
bob_encrypting_key: PublicKey, # User's public key (reencryption target)
bob_verifying_key: PublicKey,
) -> List[RetrievalResult]:
def retrieve_cfrags(self,
treasure_map: TreasureMap,
retrieval_kits: Sequence[RetrievalKit],
alice_verifying_key: PublicKey, # KeyFrag signer's key
bob_encrypting_key: PublicKey, # User's public key (reencryption target)
bob_verifying_key: PublicKey,
**context) -> Tuple[List[RetrievalResult], List[RetrievalError]]:
self._ensure_ursula_availability(treasure_map)
@ -273,12 +321,27 @@ class RetrievalClient:
continue
ursula = self._learner.known_nodes[ursula_checksum_address]
# TODO: Move to a method and handle errors?
# TODO: This serialization is rather low-level compared to the rest of this method.
# nucypher-core consumes bytes only for conditions and context.
condition_string = work_order.conditions(as_json=True) # '[[lingo], null, [lingo]]'
request_context_string = json.dumps(context)
# TODO: As this pattern swells further, it makes sense to do this in a purpose-built facility,
# such as a factory that makes helper classes and casts the appropriate types.
rust_conditions = Conditions(condition_string)
rust_context = Context(request_context_string)
reencryption_request = ReencryptionRequest(
hrac=treasure_map.hrac,
capsules=work_order.capsules,
encrypted_kfrag=treasure_map.destinations[work_order.ursula_address],
bob_verifying_key=bob_verifying_key,
publisher_verifying_key=treasure_map.publisher_verifying_key)
publisher_verifying_key=treasure_map.publisher_verifying_key,
conditions=rust_conditions,
context=rust_context
)
try:
cfrags = self._request_reencryption(ursula=ursula,
@ -287,9 +350,13 @@ class RetrievalClient:
policy_encrypting_key=treasure_map.policy_encrypting_key,
bob_encrypting_key=bob_encrypting_key)
except Exception as e:
# TODO (#2789): at this point we can separate the exceptions to "acceptable"
# (Ursula is not reachable) and "unacceptable" (Ursula provided bad results).
self.log.warn(f"Ursula {ursula} failed to reencrypt: {e}")
exception_message = f"{e.__class__.__name__}: {e}"
retrieval_plan.update_errors(
work_order, ursula_checksum_address, exception_message
)
self.log.warn(
f"Ursula {ursula} failed to reencrypt; {exception_message}"
)
continue
retrieval_plan.update(work_order, cfrags)

View File

@ -14,8 +14,7 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import json
import uuid
import weakref
from http import HTTPStatus
@ -28,11 +27,11 @@ from flask import Flask, Response, jsonify, request
from mako import exceptions as mako_exceptions
from mako.template import Template
from nucypher_core import (
ReencryptionRequest,
RevocationOrder,
MetadataRequest,
MetadataResponse,
MetadataResponsePayload,
ReencryptionRequest,
RevocationOrder,
)
from nucypher.config.constants import MAX_UPLOAD_CONTENT_LENGTH
@ -43,6 +42,13 @@ from nucypher.datastore.models import ReencryptionRequest as ReencryptionRequest
from nucypher.network.exceptions import NodeSeemsToBeDown
from nucypher.network.nodes import NodeSprout
from nucypher.network.protocols import InterfaceInfo
from nucypher.policy.conditions.base import ReencryptionCondition
from nucypher.policy.conditions.context import (
ContextVariableVerificationFailed,
InvalidContextVariableData,
RequiredContextVariable,
)
from nucypher.policy.conditions.lingo import ConditionLingo
from nucypher.utilities.logging import Logger
HERE = BASE_DIR = Path(__file__).parent
@ -168,12 +174,22 @@ def _make_rest_app(datastore: Datastore, this_node, log: Logger) -> Flask:
from nucypher.characters.lawful import Bob
# TODO: Cache & Optimize
reenc_request = ReencryptionRequest.from_bytes(request.data)
json_lingo = json.loads(str(reenc_request.conditions))
lingo = [ConditionLingo.from_list(lingo) if lingo else None for lingo in json_lingo]
context = json.loads(str(reenc_request.context)) or dict() # requester-supplied input
packets = zip(reenc_request.capsules, lingo)
# TODO: Detect if we are dealing with PRE or tDec here
# TODO: This is for PRE only, relocate HRAC to RE.context
hrac = reenc_request.hrac
# This is now either Bob or the TDec requester "Universal Bob"
bob = Bob.from_public_keys(verifying_key=reenc_request.bob_verifying_key)
log.info(f"Reencryption request from {bob} for policy {hrac}")
# TODO: Can this be integrated into reencryption conditions?
# Right off the bat, if this HRAC is already known to be revoked, reject the order.
if hrac in this_node.revoked_policies:
return Response(response=f"Policy with {hrac} has been revoked.", status=HTTPStatus.UNAUTHORIZED)
@ -188,9 +204,12 @@ def _make_rest_app(datastore: Datastore, this_node, log: Logger) -> Flask:
# Verify & Decrypt KFrag Payload
try:
verified_kfrag = this_node._decrypt_kfrag(reenc_request.encrypted_kfrag, hrac, publisher_verifying_key)
except DecryptingKeypair.DecryptionFailed:
except DecryptingKeypair.DecryptionFailed as e:
# TODO: don't we want to record suspicious activities here too?
return Response(response="EncryptedKeyFrag decryption failed.", status=HTTPStatus.FORBIDDEN)
return Response(
response=f"EncryptedKeyFrag decryption failed: {e}",
status=HTTPStatus.FORBIDDEN,
)
except InvalidSignature as e:
message = f'{bob_identity_message} Invalid signature for KeyFrag: {e}.'
log.info(message)
@ -199,20 +218,79 @@ def _make_rest_app(datastore: Datastore, this_node, log: Logger) -> Flask:
except Exception as e:
message = f'{bob_identity_message} Invalid EncryptedKeyFrag: {e}.'
log.info(message)
# TODO (#567): bucket the node as suspicious
# TODO (#567): bucket the node as suspicious.
return Response(message, status=HTTPStatus.BAD_REQUEST)
# Enforce Policy Payment
# TODO: Accept multiple payment methods
# TODO: Evaluate multiple reencryption prerequisites & enforce policy expiration
paid = this_node.payment_method.verify(payee=this_node.checksum_address, request=reenc_request)
if not paid:
message = f"{bob_identity_message} Policy {bytes(hrac)} is unpaid."
return Response(message, status=HTTPStatus.PAYMENT_REQUIRED)
# Enforce Reencryption Conditions
# TODO: back compatibility for PRE?
if not this_node.federated_only:
# TODO: Detect whether or not a provider is required by introspecting the condition instead.
context.update({'provider': this_node.application_agent.blockchain.provider})
capsules_to_process = list()
for capsule, lingo in packets:
if lingo is not None:
# TODO: Enforce policy expiration as a condition
try:
# TODO: Can conditions return a useful value?
log.info(f'Evaluating decryption condition')
lingo.eval(**context)
except ReencryptionCondition.InvalidCondition as e:
message = f"Incorrect value provided for condition: {e}"
error = (message, HTTPStatus.BAD_REQUEST)
log.info(message)
return Response(message, status=error[1])
except RequiredContextVariable as e:
message = f"Missing required inputs: {e}"
# TODO: be more specific and name the missing inputs, etc
error = (message, HTTPStatus.BAD_REQUEST)
log.info(message)
return Response(message, status=error[1])
except InvalidContextVariableData as e:
message = f"Invalid data provided for context variable: {e}"
error = (message, HTTPStatus.BAD_REQUEST)
log.info(message)
return Response(message, status=error[1])
except ContextVariableVerificationFailed as e:
message = f"Context variable data could not be verified: {e}"
error = (message, HTTPStatus.FORBIDDEN)
log.info(message)
return Response(message, status=error[1])
except ReencryptionCondition.ConditionEvaluationFailed as e:
message = f"Decryption condition not evaluated: {e}"
error = (message, HTTPStatus.BAD_REQUEST)
log.info(message)
return Response(message, status=error[1])
except lingo.Failed as e:
# TODO: Better error reporting
message = f"Decryption conditions not satisfied: {e}"
error = (message, HTTPStatus.FORBIDDEN)
log.info(message)
return Response(message, status=error[1])
except Exception as e:
# TODO: Unsure why we ended up here
message = f"Unexpected exception while evaluating " \
f"decryption condition ({e.__class__.__name__}): {e}"
error = (message, HTTPStatus.INTERNAL_SERVER_ERROR)
log.warn(message)
return Response(message, status=error[1])
capsules_to_process.append((lingo, capsule))
capsules_to_process = tuple(p[1] for p in capsules_to_process)
# FIXME: DISABLED FOR TDEC ADAPTATION
# TODO: Accept multiple payment methods?
# Subscription Manager
# paid = this_node.payment_method.verify(payee=this_node.checksum_address, request=reenc_request)
# if not paid:
# message = f"{bob_identity_message} Policy {bytes(hrac)} is unpaid."
# return Response(message, status=HTTPStatus.PAYMENT_REQUIRED)
# Re-encrypt
# TODO: return a sensible response if it fails (currently results in 500)
response = this_node._reencrypt(kfrag=verified_kfrag, capsules=reenc_request.capsules)
response = this_node._reencrypt(kfrag=verified_kfrag, capsules=capsules_to_process)
# Now, Ursula saves evidence of this workorder to her database...
# Note: we give the work order a random ID to store it under.

View File

@ -0,0 +1,25 @@
"""
This file is part of nucypher.
nucypher is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
nucypher is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import json
from pathlib import Path
STANDARD_ABIS_FILEPATH = Path(__file__).parent / 'abis.json'
with open(STANDARD_ABIS_FILEPATH, 'r') as file:
STANDARD_ABIS = json.loads(file.read())
STANDARD_ABI_CONTRACT_TYPES = set(STANDARD_ABIS)

View File

@ -0,0 +1,68 @@
import json
from marshmallow import Schema, post_dump
from typing import Union, Type, Dict
_ETH = 'eth_'
def to_camelcase(s):
parts = iter(s.split("_"))
return next(parts) + "".join(i.title() for i in parts)
class CamelCaseSchema(Schema):
"""Schema that uses camel-case for its external representation
and snake-case for its internal representation.
"""
SKIP_VALUES = tuple()
def on_bind_field(self, field_name, field_obj):
field_obj.data_key = to_camelcase(field_obj.data_key or field_name)
@post_dump
def remove_skip_values(self, data, **kwargs):
return {
key: value for key, value in data.items()
if value not in self.SKIP_VALUES
}
def _resolve_condition_lingo(json_data) -> Union[Type['Operator'], Type['ReencryptionCondition']]:
"""
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
conditions expression framework.
"""
# TODO: This is ugly but avoids circular imports :-|
from nucypher.policy.conditions.time import TimeCondition
from nucypher.policy.conditions.evm import ContractCondition
from nucypher.policy.conditions.evm import RPCCondition
from nucypher.policy.conditions.lingo import Operator
# Inspect
method = json_data.get('method')
operator = json_data.get('operator')
contract = json_data.get('contractAddress')
# Resolve
if method:
if method == TimeCondition.METHOD:
return TimeCondition
elif contract:
return ContractCondition
elif method.startswith(_ETH):
return RPCCondition
elif operator:
return Operator
else:
raise Exception(f'Cannot resolve condition lingo type from data {json_data}')
def _deserialize_condition_lingo(data: Union[str, Dict[str, str]]) -> Union['Operator', 'ReencryptionCondition']:
"""Deserialization helper for condition lingo"""
if isinstance(data, str):
data = json.loads(data)
lingo_class = _resolve_condition_lingo(json_data=data)
instance = lingo_class.from_dict(data)
return instance

View File

@ -0,0 +1,871 @@
{
"ERC20": [
{
"constant": true,
"inputs": [],
"name": "name",
"outputs": [
{
"name": "",
"type": "string"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_spender",
"type": "address"
},
{
"name": "_value",
"type": "uint256"
}
],
"name": "approve",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "totalSupply",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_from",
"type": "address"
},
{
"name": "_to",
"type": "address"
},
{
"name": "_value",
"type": "uint256"
}
],
"name": "transferFrom",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "decimals",
"outputs": [
{
"name": "",
"type": "uint8"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_owner",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"name": "balance",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "symbol",
"outputs": [
{
"name": "",
"type": "string"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_to",
"type": "address"
},
{
"name": "_value",
"type": "uint256"
}
],
"name": "transfer",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_owner",
"type": "address"
},
{
"name": "_spender",
"type": "address"
}
],
"name": "allowance",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"payable": true,
"stateMutability": "payable",
"type": "fallback"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "owner",
"type": "address"
},
{
"indexed": true,
"name": "spender",
"type": "address"
},
{
"indexed": false,
"name": "value",
"type": "uint256"
}
],
"name": "Approval",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "from",
"type": "address"
},
{
"indexed": true,
"name": "to",
"type": "address"
},
{
"indexed": false,
"name": "value",
"type": "uint256"
}
],
"name": "Transfer",
"type": "event"
}
],
"ERC721": [
{
"constant": false,
"inputs": [
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "approve",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "mint",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "safeTransferFrom",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "_data",
"type": "bytes"
}
],
"name": "safeTransferFrom",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "bool",
"name": "approved",
"type": "bool"
}
],
"name": "setApprovalForAll",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "transferFrom",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "from",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": true,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "Transfer",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "approved",
"type": "address"
},
{
"indexed": true,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "Approval",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"indexed": false,
"internalType": "bool",
"name": "approved",
"type": "bool"
}
],
"name": "ApprovalForAll",
"type": "event"
},
{
"constant": true,
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "getApproved",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"internalType": "address",
"name": "operator",
"type": "address"
}
],
"name": "isApprovedForAll",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "ownerOf",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"internalType": "bytes4",
"name": "interfaceId",
"type": "bytes4"
}
],
"name": "supportsInterface",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
}
],
"ERC1155": [
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "account",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"indexed": false,
"internalType": "bool",
"name": "approved",
"type": "bool"
}
],
"name": "ApprovalForAll",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "from",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256[]",
"name": "ids",
"type": "uint256[]"
},
{
"indexed": false,
"internalType": "uint256[]",
"name": "values",
"type": "uint256[]"
}
],
"name": "TransferBatch",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "from",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "id",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"name": "TransferSingle",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "string",
"name": "value",
"type": "string"
},
{
"indexed": true,
"internalType": "uint256",
"name": "id",
"type": "uint256"
}
],
"name": "URI",
"type": "event"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
},
{
"internalType": "uint256",
"name": "id",
"type": "uint256"
}
],
"name": "balanceOf",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address[]",
"name": "accounts",
"type": "address[]"
},
{
"internalType": "uint256[]",
"name": "ids",
"type": "uint256[]"
}
],
"name": "balanceOfBatch",
"outputs": [
{
"internalType": "uint256[]",
"name": "",
"type": "uint256[]"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
},
{
"internalType": "address",
"name": "operator",
"type": "address"
}
],
"name": "isApprovedForAll",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256[]",
"name": "ids",
"type": "uint256[]"
},
{
"internalType": "uint256[]",
"name": "amounts",
"type": "uint256[]"
},
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "safeBatchTransferFrom",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "id",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "safeTransferFrom",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"internalType": "bool",
"name": "approved",
"type": "bool"
}
],
"name": "setApprovalForAll",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes4",
"name": "interfaceId",
"type": "bytes4"
}
],
"name": "supportsInterface",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "id",
"type": "uint256"
}
],
"name": "uri",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
}
]
}

View File

@ -0,0 +1,79 @@
"""
This file is part of nucypher.
nucypher is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
nucypher is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import json
from abc import ABC, abstractmethod
from base64 import b64decode, b64encode
from typing import Any, Dict, Tuple
from marshmallow import Schema
class _Serializable:
class Schema(Schema):
field = NotImplemented
def to_json(self) -> str:
schema = self.Schema()
data = schema.dumps(self)
return data
@classmethod
def from_json(cls, data) -> '_Serializable':
data = json.loads(data)
schema = cls.Schema()
instance = schema.load(data)
return instance
def to_dict(self) -> Dict[str, str]:
schema = self.Schema()
data = schema.dump(self)
return data
@classmethod
def from_dict(cls, data) -> '_Serializable':
schema = cls.Schema()
instance = schema.load(data)
return instance
def __bytes__(self) -> bytes:
json_payload = self.to_json().encode()
b64_json_payload = b64encode(json_payload)
return b64_json_payload
@classmethod
def from_bytes(cls, data: bytes) -> '_Serializable':
json_payload = b64decode(data).decode()
instance = cls.from_json(json_payload)
return instance
class ReencryptionCondition(_Serializable, ABC):
class InvalidCondition(ValueError):
"""Invalid value for condition."""
class ConditionEvaluationFailed(Exception):
"""Could not evaluate condition for some reason."""
class Schema(Schema):
name = NotImplemented
@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

View File

@ -0,0 +1,124 @@
"""
This file is part of nucypher.
nucypher is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
nucypher is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
from typing import Any
from eip712_structs import Bytes, EIP712Struct, String, Uint
from eth_account.account import Account
from eth_account.messages import HexBytes, SignableMessage
from eth_typing import ChecksumAddress
from eth_utils import to_checksum_address
USER_ADDRESS_CONTEXT = ":userAddress"
_CONTEXT_PREFIX = ":"
_EIP712_VERSION_BYTE = b"\x01"
class RequiredContextVariable(Exception):
pass
class InvalidContextVariableData(Exception):
pass
class ContextVariableVerificationFailed(Exception):
pass
class UserAddress(EIP712Struct):
address = String()
blockNumber = Uint()
blockHash = Bytes(32)
signatureText = String()
def _recover_user_address(**context) -> ChecksumAddress:
# Expected format:
# {
# ":userAddress":
# {
# "signature": "<signature>",
# "address": "<address>",
# "typedData": "<a complicated EIP712 data structure>"
# }
# }
# setup
try:
user_address_info = context[USER_ADDRESS_CONTEXT]
signature = user_address_info["signature"]
user_address = to_checksum_address(user_address_info["address"])
eip712_message = user_address_info["typedData"]
message, domain = UserAddress.from_message(eip712_message)
signable_message = SignableMessage(
HexBytes(_EIP712_VERSION_BYTE),
header=domain.hash_struct(),
body=message.hash_struct(),
)
except Exception as e:
# data could not be processed
raise InvalidContextVariableData(
f'Invalid data provided for "{USER_ADDRESS_CONTEXT}"; {e.__class__.__name__} - {e}'
)
# actual verification
try:
address_for_signature = Account.recover_message(
signable_message=signable_message, signature=signature
)
if address_for_signature == user_address:
return user_address
except Exception as e:
# exception during verification
raise ContextVariableVerificationFailed(
f"Could not determine address of signature for '{USER_ADDRESS_CONTEXT}'; {e.__class__.__name__} - {e}"
)
# verification failed - addresses don't match
raise ContextVariableVerificationFailed(
f"Signer address for '{USER_ADDRESS_CONTEXT}' signature does not match; expected {user_address}"
)
_DIRECTIVES = {
USER_ADDRESS_CONTEXT: _recover_user_address,
}
def is_context_variable(variable) -> bool:
return isinstance(variable, str) and variable.startswith(_CONTEXT_PREFIX)
def get_context_value(context_variable: str, **context) -> Any:
try:
func = _DIRECTIVES[
context_variable
] # These are special context vars that will pre-processed by ursula
except KeyError:
# fallback for context variable without directive - assume key,value pair
# handles the case for user customized context variables
value = context.get(context_variable)
if not value:
raise RequiredContextVariable(
f'"No value provided for unrecognized context variable "{context_variable}"'
)
else:
value = func(**context) # required inputs here
return value

View File

@ -0,0 +1,273 @@
"""
This file is part of nucypher.
nucypher is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
nucypher is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import re
from typing import Any, List, Optional, Tuple, Union
from eth_typing import ChecksumAddress
from eth_utils import to_checksum_address
from marshmallow import fields, post_load
from web3 import Web3
from web3.contract import ContractFunction
from web3.providers import BaseProvider
from nucypher.blockchain.eth.clients import PUBLIC_CHAINS
from nucypher.policy.conditions import STANDARD_ABI_CONTRACT_TYPES, STANDARD_ABIS
from nucypher.policy.conditions._utils import CamelCaseSchema
from nucypher.policy.conditions.base import ReencryptionCondition
from nucypher.policy.conditions.context import get_context_value, is_context_variable
from nucypher.policy.conditions.lingo import ReturnValueTest
# TODO: Move this method to a util function
__CHAINS = {
60: 'ethereum', # TODO: make a few edits for now
131277322940537: 'testerchain', # TODO: this one can be moved to a pytest fixture / setup logic
**PUBLIC_CHAINS,
}
def _resolve_chain(chain: Union[str, int]) -> Tuple[str, int]:
"""Returns the name *and* chain ID given only a name *or* chain ID"""
for pair in __CHAINS.items():
if chain in pair:
chain_id, chain_name = pair
return chain_name, chain_id
else:
raise ReencryptionCondition.InvalidCondition(
f"{chain} is not a known blockchain."
)
def _resolve_abi(standard_contract_type: str, method: str, function_abi: List) -> List:
"""Resolves the contract an/or function ABI from a standard contract name"""
if not (function_abi or standard_contract_type):
# TODO: Is this protection needed?
raise ReencryptionCondition.InvalidCondition(
f"Ambiguous ABI - Supply either an ABI or a standard contract type ({STANDARD_ABI_CONTRACT_TYPES})."
)
if standard_contract_type:
try:
function_abi = STANDARD_ABIS[standard_contract_type]
except KeyError:
raise ReencryptionCondition.InvalidCondition(
f"Invalid standard contract type {standard_contract_type}; Must be one of {STANDARD_ABI_CONTRACT_TYPES}"
)
if not function_abi:
raise ReencryptionCondition.InvalidCondition(
f"No function ABI supplied for '{method}'"
)
# TODO: Verify that the function and ABI pair match?
# ABI(function_abi)
return function_abi
def camel_case_to_snake(data: str) -> str:
data = re.sub(r'(?<!^)(?=[A-Z])', '_', data).lower()
return data
def _resolve_any_context_variables(
parameters: List[Any], return_value_test: ReturnValueTest, **context
):
processed_parameters = []
for p in parameters:
# TODO needs additional support for ERC1155 which has lists of values
# context variables can only be strings, but other types of parameters can be passed
if is_context_variable(p):
p = get_context_value(context_variable=p, **context)
processed_parameters.append(p)
v = return_value_test.value
if is_context_variable(return_value_test.value):
v = get_context_value(context_variable=v, **context)
processed_return_value_test = ReturnValueTest(return_value_test.comparator, value=v)
return processed_parameters, processed_return_value_test
class RPCCondition(ReencryptionCondition):
ALLOWED_METHODS = (
# Contract
'balanceOf',
# RPC
'eth_getBalance',
) # TODO other allowed methods (tDEC #64)
class RPCExecutionFailed(ReencryptionCondition.ConditionEvaluationFailed):
"""Raised when an exception is raised from an RPC call."""
class Schema(CamelCaseSchema):
name = fields.Str()
chain = fields.Str()
method = fields.Str()
parameters = fields.List(fields.Field, attribute='parameters', required=False)
return_value_test = fields.Nested(ReturnValueTest.ReturnValueTestSchema())
@post_load
def make(self, data, **kwargs):
return RPCCondition(**data)
def __repr__(self) -> str:
r = f'{self.__class__.__name__}(function={self.method}, chain={self.chain_name})'
return r
def __init__(self,
chain: str,
method: str,
return_value_test: ReturnValueTest,
parameters: Optional[List[Any]] = None
):
# Validate input
# _validate_parameters(parameters=parameters)
# TODO: Additional validation (function is valid for ABI, RVT validity, standard contract name validity, etc.)
# internal
self.chain_name, self.chain_id = _resolve_chain(chain=chain)
self.method = self.validate_method(method=method)
# test
self.parameters = parameters # input
self.return_value_test = return_value_test # output
@property
def chain(self) -> str:
return self.chain_name
def validate_method(self, method):
if method not in self.ALLOWED_METHODS:
raise ReencryptionCondition.InvalidCondition(
f"'{method}' is not a permitted RPC endpoint for condition evaluation."
)
if not method.startswith('eth_'):
raise ReencryptionCondition.InvalidCondition(
f"Only eth RPC methods are accepted for condition evaluation; '{method}' is not permitted"
)
return method
def _configure_provider(self, provider: BaseProvider):
"""Binds the condition's contract function to a blockchian provider for evaluation"""
self.w3 = Web3(provider)
provider_chain = self.w3.eth.chain_id
if provider_chain != self.chain_id:
raise ReencryptionCondition.InvalidCondition(
f"This condition can only be evaluated on {self.chain_id} but the provider's "
f"connection is to chain {provider_chain}"
)
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
def verify(self, provider: BaseProvider, **context) -> Tuple[bool, Any]:
"""Performs onchain read and return value test"""
self._configure_provider(provider=provider)
parameters, return_value_test = _resolve_any_context_variables(
self.parameters, self.return_value_test, **context
)
try:
result = self._execute_call(parameters=parameters)
except Exception as e:
raise self.RPCExecutionFailed(f"Contract call '{self.method}' failed: {e}")
eval_result = return_value_test.eval(result) # test
return eval_result, result
class ContractCondition(RPCCondition):
class Schema(RPCCondition.Schema):
SKIP_VALUES = (None,)
standard_contract_type = fields.Str(required=False)
contract_address = fields.Str(required=True)
function_abi = fields.Str(required=False)
@post_load
def make(self, data, **kwargs):
return ContractCondition(**data)
def __init__(self,
contract_address: ChecksumAddress,
standard_contract_type: str = None,
function_abi: List = None,
*args, **kwargs):
# internal
super().__init__(*args, **kwargs)
self.w3 = Web3() # used to instantiate contract function without a provider
# preprocessing
contract_address = to_checksum_address(contract_address)
function_abi = _resolve_abi(
standard_contract_type=standard_contract_type,
method=self.method,
function_abi=function_abi
)
# spec
self.contract_address = contract_address
self.standard_contract_type = standard_contract_type
self.function_abi = function_abi
self.contract_function = self._get_unbound_contract_function()
def __repr__(self) -> str:
r = f'{self.__class__.__name__}(function={self.method}, ' \
f'contract={self.contract_address[:6]}..., ' \
f'chain={self.chain_name})'
return r
def validate_method(self, method):
return method
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"""
try:
contract = self.w3.eth.contract(
address=self.contract_address, abi=self.function_abi
)
contract_function = getattr(contract.functions, self.method)
return contract_function
except Exception as e:
raise ReencryptionCondition.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

View File

@ -0,0 +1,219 @@
"""
This file is part of nucypher.
nucypher is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
nucypher is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import ast
import base64
import json
import operator
from typing import Any, Dict, List, Union
from marshmallow import fields, post_load
from nucypher.policy.conditions._utils import (
CamelCaseSchema,
_deserialize_condition_lingo,
)
from nucypher.policy.conditions.base import ReencryptionCondition
from nucypher.policy.conditions.context import is_context_variable
class Operator:
OPERATORS = ('and', 'or')
def __init__(self, operator: str):
if operator not in self.OPERATORS:
raise Exception(f'{operator} is not a valid operator')
self.operator = operator
def __str__(self) -> str:
return self.operator
def to_dict(self) -> Dict[str, str]:
return {'operator': self.operator}
@classmethod
def from_dict(cls, data: Dict[str, str]) -> 'Operator':
try:
operator = data['operator']
except KeyError:
raise Exception(f'Invalid operator JSON')
instance = cls(operator=operator)
return instance
@classmethod
def from_json(cls, data) -> 'Operator':
data = json.loads(data)
instance = cls.from_dict(data)
return instance
def to_json(self) -> str:
data = self.to_dict()
data = json.dumps(data)
return data
class ReturnValueTest:
class InvalidExpression(ValueError):
pass
_COMPARATOR_FUNCTIONS = {
"==": operator.eq,
"!=": operator.ne,
">": operator.gt,
"<": operator.lt,
"<=": operator.le,
">=": operator.ge,
}
COMPARATORS = tuple(_COMPARATOR_FUNCTIONS)
class ReturnValueTestSchema(CamelCaseSchema):
comparator = fields.Str()
value = fields.Raw(allow_none=False) # any valid type (excludes None)
@post_load
def make(self, data, **kwargs):
return ReturnValueTest(**data)
def __init__(self, comparator: str, value):
if comparator not in self.COMPARATORS:
raise self.InvalidExpression(
f'"{comparator}" is not a permitted comparator.'
)
if not is_context_variable(value):
# verify that value is valid, but don't set it here so as not to change the value;
# it will be sanitized at eval time. Need to maintain serialization/deserialization
# consistency
self._sanitize_value(value)
self.comparator = comparator
self.value = value
def _sanitize_value(self, value):
try:
return ast.literal_eval(str(value))
except Exception:
raise self.InvalidExpression(f'"{value}" is not a permitted value.')
def eval(self, data) -> bool:
if is_context_variable(self.value):
# programming error if we get here
raise RuntimeError(
f"'{self.value}' is an unprocessed context variable and is not valid "
f"for condition evaluation."
)
left_operand = self._sanitize_value(data)
right_operand = self._sanitize_value(self.value)
result = self._COMPARATOR_FUNCTIONS[self.comparator](left_operand, right_operand)
return result
class ConditionLingo:
"""
A Collection of re-encryption conditions evaluated as a compound boolean expression.
This is an alternate implementation of the condition expression format used in the Lit Protocol (https://github.com/LIT-Protocol);
credit to the authors for inspiring this work.
"""
class Failed(Exception):
pass
def __init__(self, conditions: List[Union[ReencryptionCondition, Operator, Any]]):
"""
The input list must be structured:
condition
operator
condition
...
"""
self._validate(lingo=conditions)
self.conditions = conditions
@staticmethod
def _validate(lingo) -> None:
if len(lingo) % 2 == 0:
raise ValueError('conditions must be odd length, ever other element being an operator')
for index, element in enumerate(lingo):
if (not index % 2) and not (isinstance(element, ReencryptionCondition)):
raise Exception(f'{index} element must be a condition; Got {type(element)}.')
elif (index % 2) and (not isinstance(element, Operator)):
raise Exception(f'{index} element must be an operator; Got {type(element)}.')
@classmethod
def from_list(cls, payload: List[Dict[str, str]]) -> 'ConditionLingo':
conditions = [_deserialize_condition_lingo(c) for c in payload]
instance = cls(conditions=conditions)
return instance
def to_list(self): # TODO: __iter__ ?
payload = [c.to_dict() for c in self.conditions]
return payload
def to_json(self) -> str:
data = json.dumps(self.to_list())
return data
@classmethod
def from_json(cls, data: str) -> 'ConditionLingo':
data = json.loads(data)
instance = cls.from_list(payload=data)
return instance
def to_base64(self) -> bytes:
data = base64.b64encode(self.to_json().encode())
return data
@classmethod
def from_base64(cls, data: bytes) -> 'ConditionLingo':
data = base64.b64decode(data).decode()
instance = cls.from_json(data)
return instance
def __bytes__(self) -> bytes:
data = self.to_json().encode()
return data
def __eval(self, eval_string: str):
# TODO: Additional protection and/or sanitation here
result = eval(eval_string)
return result
def __process(self, *args, **kwargs):
for task in self.conditions:
if isinstance(task, ReencryptionCondition):
condition = task
result, value = condition.verify(*args, **kwargs)
yield result
elif isinstance(task, Operator):
operator = task
yield operator
else:
raise TypeError(f"Unrecognized type {type(task)} for ConditionLingo")
def eval(self, *args, **kwargs) -> bool:
data = self.__process(*args, **kwargs)
# [True, <Operator>, False] -> 'True or False'
eval_string = ' '.join(str(e) for e in data)
result = self.__eval(eval_string=eval_string)
if not result:
raise self.Failed
return True
OR = Operator('or')
AND = Operator('and')

View File

@ -0,0 +1,61 @@
"""
This file is part of nucypher.
nucypher is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
nucypher is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import time
from typing import Tuple
from marshmallow import fields, post_load
from nucypher.policy.conditions._utils import CamelCaseSchema
from nucypher.policy.conditions.base import ReencryptionCondition
from nucypher.policy.conditions.lingo import ReturnValueTest
class TimeCondition(ReencryptionCondition):
METHOD = 'timelock'
class Schema(CamelCaseSchema):
name = fields.Str()
method = fields.Str(default='timelock')
return_value_test = fields.Nested(ReturnValueTest.ReturnValueTestSchema())
@post_load
def make(self, data, **kwargs):
return TimeCondition(**data)
def __repr__(self) -> str:
r = f'{self.__class__.__name__}(timestamp={self.return_value_test.value})'
return r
def __init__(self, return_value_test: ReturnValueTest, method: str = METHOD):
if method != self.METHOD:
raise ReencryptionCondition.InvalidCondition(
f"{self.__class__.__name__} must be instantiated with the {self.METHOD} method."
)
self.return_value_test = return_value_test
@property
def method(self):
return self.METHOD
@property
def timestamp(self):
return self.return_value_test.value
def verify(self, *args, **kwargs) -> Tuple[bool, float]:
eval_time = time.time() # system clock
return self.return_value_test.eval(data=eval_time), eval_time

View File

@ -16,13 +16,14 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
from typing import Dict, Set, Union
from typing import Dict, Set
from eth_utils import to_canonical_address
from eth_typing import ChecksumAddress
from eth_utils import to_canonical_address
from nucypher_core.umbral import PublicKey, SecretKey, VerifiedCapsuleFrag
from nucypher_core import MessageKit, RetrievalKit
from nucypher_core.umbral import PublicKey, VerifiedCapsuleFrag, SecretKey
from nucypher_core import Address, MessageKit, RetrievalKit
from nucypher.policy.conditions.lingo import ConditionLingo
class PolicyMessageKit:
@ -47,7 +48,11 @@ class PolicyMessageKit:
self._result = result
def as_retrieval_kit(self) -> RetrievalKit:
return RetrievalKit(self.message_kit.capsule, self._result.canonical_addresses())
return RetrievalKit(
capsule=self.message_kit.capsule,
queried_addresses=self._result.canonical_addresses(),
conditions=self.message_kit.conditions,
)
def decrypt(self, sk: SecretKey) -> bytes:
return self.message_kit.decrypt_reencrypted(sk,
@ -63,6 +68,10 @@ class PolicyMessageKit:
result=self._result.with_result(result),
message_kit=self.message_kit)
@property
def conditions(self) -> ConditionLingo:
return self.message_kit.conditions
# TODO: a better name?
class RetrievalResult:
@ -79,7 +88,7 @@ class RetrievalResult:
def canonical_addresses(self) -> Set[bytes]:
# TODO (#1995): propagate this to use canonical addresses everywhere
return set([to_canonical_address(address) for address in self.cfrags])
return set([Address(to_canonical_address(address)) for address in self.cfrags])
def with_result(self, result: 'RetrievalResult') -> 'RetrievalResult':
"""

View File

@ -15,7 +15,6 @@
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
from abc import ABC, abstractmethod
from typing import Optional, NamedTuple, Dict
@ -28,19 +27,7 @@ from nucypher.blockchain.eth.registry import InMemoryContractRegistry, BaseContr
from nucypher.policy.policies import BlockchainPolicy, Policy
class ReencryptionPrerequisite(ABC):
"""Baseclass for reencryption preconditions relating to a policy."""
ONCHAIN = NotImplemented
NAME = NotImplemented
@abstractmethod
def verify(self, payee: ChecksumAddress, request: ReencryptionRequest) -> bool:
"""returns True if reencryption is permitted by the payee (ursula) for the given reencryption request."""
raise NotImplemented
class PaymentMethod(ReencryptionPrerequisite, ABC):
class PaymentMethod(ABC):
"""Extends ReencryptionPrerequisite to facilitate policy payment and payment verification."""
class Quote(NamedTuple):
@ -115,43 +102,6 @@ class ContractPayment(PaymentMethod, ABC):
return self.__agent # set cache
class FreeReencryptions(PaymentMethod):
"""Useful for private federations and testing."""
ONCHAIN = False
NAME = 'Free'
def verify(self, payee: ChecksumAddress, request: ReencryptionRequest) -> bool:
return True
def pay(self, policy: Policy) -> Dict:
receipt = f'Receipt for free policy {bytes(policy.hrac).hex()}.'
return dict(receipt=receipt.encode())
@property
def rate(self) -> int:
return 0
def quote(self,
shares: int,
commencement: Optional[Timestamp] = None,
expiration: Optional[Timestamp] = None,
duration: Optional[int] = None,
*args, **kwargs
) -> PaymentMethod.Quote:
return self.Quote(
value=0,
rate=0,
shares=shares,
duration=duration,
commencement=commencement,
expiration=expiration
)
def validate_price(self, *args, **kwargs) -> bool:
return True
class SubscriptionManagerPayment(ContractPayment):
"""Handle policy payment using the SubscriptionManager contract."""
@ -229,7 +179,44 @@ class SubscriptionManagerPayment(ContractPayment):
return True
class FreeReencryptions(PaymentMethod):
"""Useful for private federations and testing."""
ONCHAIN = False
NAME = 'Free'
def verify(self, payee: ChecksumAddress, request: ReencryptionRequest) -> bool:
return True
def pay(self, policy: Policy) -> Dict:
receipt = f'Receipt for free policy {bytes(policy.hrac).hex()}.'
return dict(receipt=receipt.encode())
@property
def rate(self) -> int:
return 0
def quote(self,
shares: int,
commencement: Optional[Timestamp] = None,
expiration: Optional[Timestamp] = None,
duration: Optional[int] = None,
*args, **kwargs
) -> PaymentMethod.Quote:
return self.Quote(
value=0,
rate=0,
shares=shares,
duration=duration,
commencement=commencement,
expiration=expiration
)
def validate_price(self, *args, **kwargs) -> bool:
return True
PAYMENT_METHODS = {
FreeReencryptions.NAME: FreeReencryptions,
SubscriptionManagerPayment.NAME: SubscriptionManagerPayment,
FreeReencryptions.NAME: FreeReencryptions
}

View File

@ -21,10 +21,9 @@ from typing import Sequence, Optional, Iterable, List, Dict
import maya
from eth_typing.evm import ChecksumAddress
from nucypher_core import HRAC, TreasureMap
from nucypher_core import Address, HRAC, TreasureMap
from nucypher_core.umbral import PublicKey, VerifiedKeyFrag
from nucypher.blockchain.eth.utils import calculate_period_duration
from nucypher.crypto.powers import DecryptingPower
from nucypher.network.middleware import RestMiddleware
from nucypher.policy.reservoir import (
@ -171,7 +170,7 @@ class Policy(ABC):
self._publish(ursulas=ursulas)
assigned_kfrags = {
ursula.canonical_address: (ursula.public_keys(DecryptingPower), vkfrag)
Address(ursula.canonical_address): (ursula.public_keys(DecryptingPower), vkfrag)
for ursula, vkfrag in zip(ursulas, self.kfrags)
}

View File

@ -15,10 +15,11 @@ You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
from eth_typing.evm import ChecksumAddress
from eth_utils import to_checksum_address, to_canonical_address
from nucypher_core import RevocationOrder
from nucypher_core import Address, RevocationOrder
from nucypher.crypto.signing import SignatureStamp
@ -29,7 +30,7 @@ class RevocationKit:
self.revocations = dict()
for staking_provider_address, encrypted_kfrag in treasure_map.destinations.items():
self.revocations[staking_provider_address] = RevocationOrder(signer=signer.as_umbral_signer(),
staking_provider_address=staking_provider_address,
staking_provider_address=Address(staking_provider_address),
encrypted_kfrag=encrypted_kfrag)
def __iter__(self):

View File

@ -112,7 +112,7 @@ class EtherchainGasPriceDatafeed(EthereumGasPriceDatafeed):
def _parse_gas_prices(self):
self._probe_feed()
self.gas_prices = {self.get_canonical_speed(k): int(Web3.toWei(v, 'gwei')) for k, v in self._raw_data.items()}
self.gas_prices = {self.get_canonical_speed(k): int(Web3.to_wei(v, 'gwei')) for k, v in self._raw_data.items()}
class UpvestGasPriceDatafeed(EthereumGasPriceDatafeed):
@ -130,7 +130,7 @@ class UpvestGasPriceDatafeed(EthereumGasPriceDatafeed):
def _parse_gas_prices(self):
self._probe_feed()
self.gas_prices = {self.get_canonical_speed(k): int(Web3.toWei(v, 'gwei'))
self.gas_prices = {self.get_canonical_speed(k): int(Web3.to_wei(v, 'gwei'))
for k, v in self._raw_data['estimates'].items()}
@ -152,5 +152,5 @@ class ZoltuGasPriceDatafeed(EthereumGasPriceDatafeed):
self.gas_prices = dict()
for canonical_speed_name, zoltu_speed in self._speed_names.items():
gwei_price = self._raw_data[zoltu_speed].split(" ")[0]
wei_price = int(Web3.toWei(gwei_price, 'gwei'))
wei_price = int(Web3.to_wei(gwei_price, 'gwei'))
self.gas_prices[canonical_speed_name] = wei_price

View File

@ -129,11 +129,11 @@ EXPECTED_CONFIRMATION_TIME_IN_SECONDS = { # TODO: See #2447
def construct_fixed_price_gas_strategy(gas_price, denomination: str = "wei") -> Callable:
gas_price_in_wei = Web3.toWei(gas_price, denomination)
gas_price_in_wei = Web3.to_wei(gas_price, denomination)
def _fixed_price_strategy(web3: Web3, transaction_params: TxParams = None) -> Wei:
return gas_price_in_wei
_fixed_price_strategy.name = f"{round(Web3.fromWei(gas_price_in_wei, 'gwei'))}gwei"
_fixed_price_strategy.name = f"{round(Web3.from_wei(gas_price_in_wei, 'gwei'))}gwei"
return _fixed_price_strategy

View File

@ -14,11 +14,10 @@
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
from typing import List, Optional
from typing import Dict, List, Optional
from eth_typing import ChecksumAddress
from nucypher_core import TreasureMap, RetrievalKit
from nucypher_core import RetrievalKit, TreasureMap
from nucypher_core.umbral import PublicKey
from nucypher.control.interfaces import ControlInterface, attach_schema
@ -36,12 +35,14 @@ class PorterInterface(ControlInterface):
def get_ursulas(self,
quantity: int,
exclude_ursulas: Optional[List[ChecksumAddress]] = None,
include_ursulas: Optional[List[ChecksumAddress]] = None) -> dict:
ursulas_info = self.implementer.get_ursulas(quantity=quantity,
exclude_ursulas=exclude_ursulas,
include_ursulas=include_ursulas)
include_ursulas: Optional[List[ChecksumAddress]] = None) -> Dict:
ursulas_info = self.implementer.get_ursulas(
quantity=quantity,
exclude_ursulas=exclude_ursulas,
include_ursulas=include_ursulas,
)
response_data = {"ursulas": ursulas_info}
response_data = {"ursulas": ursulas_info} # list of UrsulaInfo objects
return response_data
@attach_schema(porter_schema.AliceRevoke)
@ -59,12 +60,16 @@ class PorterInterface(ControlInterface):
alice_verifying_key: PublicKey,
bob_encrypting_key: PublicKey,
bob_verifying_key: PublicKey,
) -> dict:
retrieval_results = self.implementer.retrieve_cfrags(treasure_map=treasure_map,
retrieval_kits=retrieval_kits,
alice_verifying_key=alice_verifying_key,
bob_encrypting_key=bob_encrypting_key,
bob_verifying_key=bob_verifying_key)
results = retrieval_results # list of RetrievalResult objects
response_data = {'retrieval_results': results}
context: Optional[Dict] = None) -> Dict:
retrieval_outcomes = self.implementer.retrieve_cfrags(
treasure_map=treasure_map,
retrieval_kits=retrieval_kits,
alice_verifying_key=alice_verifying_key,
bob_encrypting_key=bob_encrypting_key,
bob_verifying_key=bob_verifying_key,
context=context,
)
response_data = {
"retrieval_results": retrieval_outcomes
} # list of RetrievalOutcome objects
return response_data

View File

@ -15,7 +15,7 @@
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
from marshmallow import fields
from marshmallow.fields import String
from nucypher_core import RetrievalKit as RetrievalKitClass
from nucypher_core.umbral import CapsuleFrag as CapsuleFragClass
@ -33,7 +33,7 @@ class RetrievalKit(Base64BytesRepresentation):
retrieval_kit_bytes = super()._deserialize(value, attr, data, **kwargs)
return RetrievalKitClass.from_bytes(retrieval_kit_bytes)
except Exception as e:
raise InvalidInputData(f"Could not convert input for {self.name} to a valid checksum address: {e}")
raise InvalidInputData(f"Could not convert input for {self.name} to a valid RetrievalKit: {e}")
class CapsuleFrag(Base64BytesRepresentation):
@ -45,9 +45,10 @@ class CapsuleFrag(Base64BytesRepresentation):
raise InvalidInputData(f"Could not parse {self.name}: {e}")
class RetrievalResultSchema(BaseSchema):
"""Schema for the result of retrieve_cfrags."""
class RetrievalOutcomeSchema(BaseSchema):
"""Schema for the result of /retrieve_cfrags endpoint."""
cfrags = fields.Dict(keys=UrsulaChecksumAddress(), values=CapsuleFrag())
errors = fields.Dict(keys=UrsulaChecksumAddress(), values=String())
# maintain field declaration ordering
class Meta:

View File

@ -159,5 +159,21 @@ class BobRetrieveCFrags(BaseSchema):
type=click.STRING,
required=True))
# optional
context = base_fields.JSON(
expected_type=dict,
required=False,
load_only=True,
click=click.option(
"--context",
"-ctx",
help="Context data for retrieval conditions",
type=click.STRING,
required=False,
),
)
# output
retrieval_results = marshmallow_fields.List(marshmallow_fields.Nested(fields.RetrievalResultSchema), dump_only=True)
retrieval_results = marshmallow_fields.List(
marshmallow_fields.Nested(fields.RetrievalOutcomeSchema), dump_only=True
)

View File

@ -17,28 +17,30 @@
from pathlib import Path
from typing import List, NamedTuple, Optional, Sequence
from typing import Dict, List, NamedTuple, Optional, Sequence
from constant_sorrow.constants import NO_BLOCKCHAIN_CONNECTION, NO_CONTROL_PROTOCOL
from eth_typing import ChecksumAddress
from eth_utils import to_checksum_address
from flask import request, Response
from nucypher_core import TreasureMap, RetrievalKit
from flask import Response, request
from nucypher_core import RetrievalKit, TreasureMap
from nucypher_core.umbral import PublicKey
from nucypher.blockchain.eth.agents import ContractAgency, PREApplicationAgent
from nucypher.blockchain.eth.interfaces import BlockchainInterfaceFactory
from nucypher.blockchain.eth.registry import BaseContractRegistry, InMemoryContractRegistry
from nucypher.blockchain.eth.registry import (
BaseContractRegistry,
InMemoryContractRegistry,
)
from nucypher.characters.lawful import Ursula
from nucypher.control.controllers import JSONRPCController, WebController
from nucypher.crypto.powers import DecryptingPower
from nucypher.network.nodes import Learner
from nucypher.network.retrieval import RetrievalClient
from nucypher.policy.kits import RetrievalResult
from nucypher.policy.reservoir import (
make_federated_staker_reservoir,
PrefetchStrategy,
make_decentralized_staking_provider_reservoir,
PrefetchStrategy
make_federated_staker_reservoir,
)
from nucypher.utilities.concurrency import WorkerPool
from nucypher.utilities.logging import Logger
@ -78,6 +80,15 @@ the Pipe for PRE Application network operations
uri: str
encrypting_key: PublicKey
class RetrievalOutcome(NamedTuple):
"""
Simple object that stores the results and errors of re-encryption operations across
one or more Ursulas.
"""
cfrags: Dict
errors: Dict
def __init__(self,
domain: str = None,
registry: BaseContractRegistry = None,
@ -164,10 +175,24 @@ the Pipe for PRE Application network operations
alice_verifying_key: PublicKey,
bob_encrypting_key: PublicKey,
bob_verifying_key: PublicKey,
) -> List[RetrievalResult]:
context: Optional[Dict] = None) -> List[RetrievalOutcome]:
client = RetrievalClient(self)
return client.retrieve_cfrags(treasure_map, retrieval_kits,
alice_verifying_key, bob_encrypting_key, bob_verifying_key)
context = context or dict() # must not be None
results, errors = client.retrieve_cfrags(
treasure_map,
retrieval_kits,
alice_verifying_key,
bob_encrypting_key,
bob_verifying_key,
**context,
)
result_outcomes = []
for result, error in zip(results, errors):
result_outcome = Porter.RetrievalOutcome(
cfrags=result.cfrags, errors=error.errors
)
result_outcomes.append(result_outcome)
return result_outcomes
def _make_reservoir(self,
quantity: int,

View File

@ -1,108 +1,110 @@
-i https://pypi.python.org/simple
aiohttp==3.8.1; python_version >= '3.6'
aiohttp==3.8.3; python_version >= '3.6'
aiosignal==1.2.0; python_version >= '3.6'
appdirs==1.4.4
async-timeout==4.0.2; python_version >= '3.6'
attrs==21.4.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
autobahn==22.3.2; python_version >= '3.7'
attrs==22.1.0; python_version >= '3.5'
autobahn==22.7.1; python_version >= '3.7'
automat==20.2.0
base58==2.1.1; python_version >= '3.5'
bitarray==1.2.2
bitarray==2.6.0
bytestring-splitter==2.4.1
cached-property==1.5.2
certifi==2021.10.8
cffi==1.15.0
charset-normalizer==2.0.12; python_version >= '3'
click==8.1.2
colorama==0.4.4
certifi==2022.9.24; python_version >= '3.6'
cffi==1.15.1
charset-normalizer==2.1.1; python_version >= '3.6'
click==8.1.3
colorama==0.4.5
constant-sorrow==0.1.0a9
constantly==15.1.0
construct==2.10.68; python_version >= '3.6'
cryptography==36.0.2
cytoolz==0.11.2; implementation_name == 'cpython'
cryptography==38.0.1
cytoolz==0.12.0; implementation_name == 'cpython'
dateparser==1.1.1; python_version >= '3.5'
ecdsa==0.18.0b2; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'
eth-abi==2.1.1; python_version >= '3.6' and python_version < '4'
eth-account==0.5.7; python_version >= '3.6' and python_version < '4'
ecdsa==0.18.0; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'
eip712-structs==1.1.0
eth-abi==3.0.1; python_version >= '3.7' and python_version < '4'
eth-account==0.7.0; python_version >= '3.6' and python_version < '4'
eth-bloom==1.0.4; python_version >= '3.6' and python_version < '4'
eth-hash[pycryptodome]==0.3.2; python_version >= '3.5' and python_version < '4'
eth-keyfile==0.5.1
eth-keys==0.3.4
eth-rlp==0.2.1; python_version >= '3.6' and python_version < '4'
eth-tester==0.6.0b6
eth-typing==2.3.0; python_version >= '3.5' and python_version < '4'
eth-utils==1.10.0
flask==2.1.1
frozenlist==1.3.0; python_version >= '3.7'
eth-hash==0.3.3; python_version >= '3.5' and python_version < '4'
eth-keyfile==0.6.0
eth-keys==0.4.0
eth-rlp==0.3.0; python_version >= '3.7' and python_version < '4'
eth-tester==0.7.0b1
eth-typing==3.2.0; python_version >= '3.6' and python_version < '4'
eth-utils==2.0.0
flask==2.2.2
frozenlist==1.3.1; python_version >= '3.7'
hendrix==3.4.0
hexbytes==0.2.2; python_version >= '3.6' and python_version < '4'
humanize==4.0.0; python_version >= '3.7'
hexbytes==0.3.0; python_version >= '3.7' and python_version < '4'
humanize==4.4.0; python_version >= '3.7'
hyperlink==21.0.0
idna==3.3; python_version >= '3'
idna==3.4; python_version >= '3.5'
incremental==21.3.0
ipfshttpclient==0.8.0a2; python_full_version >= '3.6.2' and python_full_version not in '3.7.0, 3.7.1'
itsdangerous==2.0.1
jinja2==3.0.3
jsonschema==4.4.0; python_version >= '3.7'
jsonschema==4.16.0; python_version >= '3.7'
libusb1==3.0.0
lmdb==1.3.0
lru-dict==1.1.7
mako==1.2.0
lru-dict==1.1.8
mako==1.2.3
markupsafe==2.1.1; python_version >= '3.7'
marshmallow==3.15.0
marshmallow==3.18.0
maya==0.6.1
mnemonic==0.20; python_version >= '3.5'
msgpack-python==0.5.6
msgpack==1.0.3
msgpack==1.0.4
multiaddr==0.0.9; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
multidict==6.0.2; python_version >= '3.7'
mypy-extensions==0.4.3
netaddr==0.8.0
nucypher-core==0.2.0
nucypher-core==0.4.0
packaging==21.3; python_version >= '3.6'
parsimonious==0.8.1
pendulum==2.1.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
pillow==9.1.0; python_version >= '3.7'
pillow==9.2.0; python_version >= '3.7'
protobuf==3.20.1; python_version >= '3.7'
py-ecc==5.2.0; python_version >= '3.5' and python_version < '4'
py-evm==0.5.0a3
pyasn1-modules==0.2.8
pyasn1==0.4.8
py-ecc==6.0.0; python_version >= '3.6' and python_version < '4'
py-evm==0.6.0a1
pyasn1-modules==0.3.0rc1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'
pyasn1==0.5.0rc2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'
pychalk==2.0.1
pycparser==2.21
pycryptodome==3.14.1
pycryptodome==3.15.0
pyethash==0.1.27
pynacl==1.5.0
pyopenssl==22.0.0
pyparsing==3.0.8; python_full_version >= '3.6.8'
pyopenssl==22.1.0
pyparsing==3.0.9; python_full_version >= '3.6.8'
pyrsistent==0.18.1; python_version >= '3.7'
pysha3==1.0.2
python-dateutil==2.8.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'
pytz==2022.1
pytz==2022.4
pytzdata==2020.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
qrcode[pil]==7.3.1
regex==2022.3.2; python_version >= '3.6'
requests==2.27.1
rlp==2.0.1
semantic-version==2.9.0; python_version >= '2.7'
requests==2.28.1
rlp==3.0.0
semantic-version==2.10.0; python_version >= '2.7'
service-identity==21.1.0
setuptools==62.1.0; python_version >= '3.7'
setuptools==65.4.1; python_version >= '3.7'
simple-rlp==0.1.3; python_version >= '3.7'
six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'
snaptime==0.2.4
sortedcontainers==2.4.0
tabulate==0.8.9
toolz==0.11.2; python_version >= '3.5'
trezor==0.13.0
trie==2.0.0a5; python_version >= '3.6' and python_version < '4'
twisted==22.4.0; python_full_version >= '3.6.7'
tabulate==0.8.10
toolz==0.12.0; python_version >= '3.5'
trezor==0.13.3
trie==2.0.2; python_version >= '3.6' and python_version < '4'
twisted==22.8.0; python_full_version >= '3.7.1'
txaio==22.2.1; python_version >= '3.6'
typing-extensions==3.10.0.2
typing-extensions==4.3.0; python_version >= '3.7'
tzlocal==2.1
urllib3==1.26.9; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'
urllib3==1.26.12; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4'
varint==1.0.2
watchdog==2.1.7; python_version >= '3.6'
web3==6.0.0b1
watchdog==2.1.9; python_version >= '3.6'
web3==6.0.0b6
websockets==10.3; python_version >= '3.7'
werkzeug==2.1.1; python_version >= '3.7'
yarl==1.7.2; python_version >= '3.6'
werkzeug==2.2.2; python_version >= '3.7'
yarl==1.8.1; python_version >= '3.7'
zope.interface==5.4.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'

View File

@ -0,0 +1,220 @@
"""
This file is part of nucypher.
nucypher is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
nucypher is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import json
from pathlib import Path
import pytest
from web3 import Web3
import tests.data
from nucypher.blockchain.eth.agents import (
ContractAgency,
NucypherTokenAgent,
SubscriptionManagerAgent,
)
from nucypher.blockchain.eth.signers.software import Web3Signer
from nucypher.blockchain.eth.sol.compile.compile import multiversion_compile
from nucypher.blockchain.eth.sol.compile.types import SourceBundle
from nucypher.crypto.powers import TransactingPower
from nucypher.policy.conditions.context import USER_ADDRESS_CONTEXT
from nucypher.policy.conditions.evm import ContractCondition, RPCCondition
from nucypher.policy.conditions.lingo import AND, OR, ConditionLingo, ReturnValueTest
from nucypher.policy.conditions.time import TimeCondition
VECTORS_FILE = Path(tests.__file__).parent / "data" / "test_conditions.json"
with open(VECTORS_FILE, 'r') as file:
VECTORS = json.loads(file.read())
@pytest.fixture()
def ERC1155_balance_condition_data():
data = json.dumps(VECTORS['ERC1155_balance'])
return data
@pytest.fixture()
def ERC1155_balance_condition(ERC1155_balance_condition_data):
data = ERC1155_balance_condition_data
condition = ContractCondition.from_json(data)
return condition
@pytest.fixture()
def ERC20_balance_condition_data():
data = json.dumps(VECTORS['ERC20_balance'])
return data
@pytest.fixture()
def ERC20_balance_condition(ERC20_balance_condition_data):
data = ERC20_balance_condition_data
condition = ContractCondition.from_json(data)
return condition
@pytest.fixture
def rpc_condition():
condition = RPCCondition(
method="eth_getBalance",
chain="testerchain",
return_value_test=ReturnValueTest("==", Web3.to_wei(1_000_000, "ether")),
parameters=[USER_ADDRESS_CONTEXT],
)
return condition
@pytest.fixture
def erc20_evm_condition(test_registry, agency):
token = ContractAgency.get_agent(NucypherTokenAgent, registry=test_registry)
condition = ContractCondition(
contract_address=token.contract.address,
method="balanceOf",
standard_contract_type="ERC20",
chain="testerchain",
return_value_test=ReturnValueTest("==", 0),
parameters=[USER_ADDRESS_CONTEXT],
)
return condition
@pytest.fixture
def custom_context_variable_erc20_condition(test_registry, agency):
token = ContractAgency.get_agent(NucypherTokenAgent, registry=test_registry)
condition = ContractCondition(
contract_address=token.contract.address,
method="balanceOf",
standard_contract_type="ERC20",
chain="testerchain",
return_value_test=ReturnValueTest("==", 0),
parameters=[":addressToUse"],
)
return condition
@pytest.fixture
def erc721_contract(testerchain, test_registry):
solidity_root = Path(__file__).parent / "contracts"
source_bundle = SourceBundle(base_path=solidity_root)
compiled_constracts = multiversion_compile([source_bundle], True)
testerchain._raw_contract_cache = compiled_constracts
origin, *everybody_else = testerchain.client.accounts
transacting_power = TransactingPower(
account=origin, signer=Web3Signer(testerchain.client)
)
contract, receipt = testerchain.deploy_contract(
transacting_power=transacting_power,
registry=test_registry,
contract_name="ConditionNFT",
)
# mint an NFT with tokenId = 1
tx = contract.functions.mint(origin, 1).transact({"from": origin})
testerchain.wait_for_receipt(tx)
return contract
@pytest.fixture
def erc721_evm_condition_owner(erc721_contract):
condition = ContractCondition(
contract_address=erc721_contract.address,
method="ownerOf",
standard_contract_type="ERC721",
chain="testerchain",
return_value_test=ReturnValueTest("==", ":userAddress"),
parameters=[
":tokenId",
],
)
return condition
@pytest.fixture
def erc721_evm_condition_balanceof(erc721_contract):
condition = ContractCondition(
contract_address=erc721_contract.address,
method="balanceOf",
standard_contract_type="ERC721",
chain="testerchain",
return_value_test=ReturnValueTest(">", 0),
parameters=[
":userAddress",
],
)
return condition
@pytest.fixture
def subscription_manager_is_active_policy_condition(test_registry, agency):
subscription_manager = ContractAgency.get_agent(SubscriptionManagerAgent, registry=test_registry)
condition = ContractCondition(
contract_address=subscription_manager.contract.address,
function_abi=subscription_manager.contract.abi,
method="isPolicyActive",
chain="testerchain",
return_value_test=ReturnValueTest("==", True),
parameters=[":hrac"],
)
return condition
@pytest.fixture
def subscription_manager_get_policy_zeroized_policy_struct_condition(
test_registry, agency
):
subscription_manager = ContractAgency.get_agent(
SubscriptionManagerAgent, registry=test_registry
)
condition = ContractCondition(
contract_address=subscription_manager.contract.address,
function_abi=subscription_manager.contract.abi,
method="getPolicy",
chain="testerchain",
return_value_test=ReturnValueTest("==", ":expectedPolicyStruct"),
parameters=[":hrac"],
)
return condition
@pytest.fixture
def timelock_condition():
condition = TimeCondition(
return_value_test=ReturnValueTest('>', 0)
)
return condition
@pytest.fixture()
def lingo(erc721_evm_condition_balanceof,
timelock_condition,
rpc_condition,
erc20_evm_condition):
lingo = ConditionLingo(
conditions=[
erc721_evm_condition_balanceof,
OR,
timelock_condition,
OR,
rpc_condition,
AND,
erc20_evm_condition,
]
)
return lingo

View File

@ -0,0 +1,19 @@
// contracts/MyNFT.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "zeppelin/token/ERC721/ERC721.sol";
contract ConditionNFT is ERC721 {
constructor() ERC721("ConditionsNFT", "cNFT") {
}
/**
* @dev Mints a new NFT.
* @param _to The address that will own the minted NFT.
* @param _tokenId of the NFT to be minted by the msg.sender.
*/
function mint(address _to, uint256 _tokenId) external {
super._mint(_to, _tokenId);
}
}

View File

@ -0,0 +1,521 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.7.0) (token/ERC721/ERC721.sol)
pragma solidity ^0.8.0;
import "zeppelin/token/ERC721/IERC721.sol";
import "zeppelin/token/ERC721/IERC721Receiver.sol";
import "zeppelin/token/ERC721/extensions/IERC721Metadata.sol";
import "zeppelin/utils/Address.sol";
import "zeppelin/utils/Context.sol";
import "zeppelin/utils/Strings.sol";
import "zeppelin/utils/introspection/ERC165.sol";
/**
* @dev Implementation of https://eips.ethereum.org/EIPS/eip-721[ERC721] Non-Fungible Token Standard, including
* the Metadata extension, but not including the Enumerable extension, which is available separately as
* {ERC721Enumerable}.
*/
contract ERC721 is Context, ERC165, IERC721, IERC721Metadata {
using Address for address;
using Strings for uint256;
// Token name
string private _name;
// Token symbol
string private _symbol;
// Mapping from token ID to owner address
mapping(uint256 => address) private _owners;
// Mapping owner address to token count
mapping(address => uint256) private _balances;
// Mapping from token ID to approved address
mapping(uint256 => address) private _tokenApprovals;
// Mapping from owner to operator approvals
mapping(address => mapping(address => bool)) private _operatorApprovals;
/**
* @dev Initializes the contract by setting a `name` and a `symbol` to the token collection.
*/
constructor(string memory name_, string memory symbol_) {
_name = name_;
_symbol = symbol_;
}
/**
* @dev See {IERC165-supportsInterface}.
*/
function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) {
return
interfaceId == type(IERC721).interfaceId ||
interfaceId == type(IERC721Metadata).interfaceId ||
super.supportsInterface(interfaceId);
}
/**
* @dev See {IERC721-balanceOf}.
*/
function balanceOf(address owner) public view virtual override returns (uint256) {
require(owner != address(0), "ERC721: address zero is not a valid owner");
return _balances[owner];
}
/**
* @dev See {IERC721-ownerOf}.
*/
function ownerOf(uint256 tokenId) public view virtual override returns (address) {
address owner = _ownerOf(tokenId);
require(owner != address(0), "ERC721: invalid token ID");
return owner;
}
/**
* @dev See {IERC721Metadata-name}.
*/
function name() public view virtual override returns (string memory) {
return _name;
}
/**
* @dev See {IERC721Metadata-symbol}.
*/
function symbol() public view virtual override returns (string memory) {
return _symbol;
}
/**
* @dev See {IERC721Metadata-tokenURI}.
*/
function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
_requireMinted(tokenId);
string memory baseURI = _baseURI();
return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : "";
}
/**
* @dev Base URI for computing {tokenURI}. If set, the resulting URI for each
* token will be the concatenation of the `baseURI` and the `tokenId`. Empty
* by default, can be overridden in child contracts.
*/
function _baseURI() internal view virtual returns (string memory) {
return "";
}
/**
* @dev See {IERC721-approve}.
*/
function approve(address to, uint256 tokenId) public virtual override {
address owner = ERC721.ownerOf(tokenId);
require(to != owner, "ERC721: approval to current owner");
require(
_msgSender() == owner || isApprovedForAll(owner, _msgSender()),
"ERC721: approve caller is not token owner or approved for all"
);
_approve(to, tokenId);
}
/**
* @dev See {IERC721-getApproved}.
*/
function getApproved(uint256 tokenId) public view virtual override returns (address) {
_requireMinted(tokenId);
return _tokenApprovals[tokenId];
}
/**
* @dev See {IERC721-setApprovalForAll}.
*/
function setApprovalForAll(address operator, bool approved) public virtual override {
_setApprovalForAll(_msgSender(), operator, approved);
}
/**
* @dev See {IERC721-isApprovedForAll}.
*/
function isApprovedForAll(address owner, address operator) public view virtual override returns (bool) {
return _operatorApprovals[owner][operator];
}
/**
* @dev See {IERC721-transferFrom}.
*/
function transferFrom(
address from,
address to,
uint256 tokenId
) public virtual override {
//solhint-disable-next-line max-line-length
require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner or approved");
_transfer(from, to, tokenId);
}
/**
* @dev See {IERC721-safeTransferFrom}.
*/
function safeTransferFrom(
address from,
address to,
uint256 tokenId
) public virtual override {
safeTransferFrom(from, to, tokenId, "");
}
/**
* @dev See {IERC721-safeTransferFrom}.
*/
function safeTransferFrom(
address from,
address to,
uint256 tokenId,
bytes memory data
) public virtual override {
require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner or approved");
_safeTransfer(from, to, tokenId, data);
}
/**
* @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients
* are aware of the ERC721 protocol to prevent tokens from being forever locked.
*
* `data` is additional data, it has no specified format and it is sent in call to `to`.
*
* This internal function is equivalent to {safeTransferFrom}, and can be used to e.g.
* implement alternative mechanisms to perform token transfer, such as signature-based.
*
* Requirements:
*
* - `from` cannot be the zero address.
* - `to` cannot be the zero address.
* - `tokenId` token must exist and be owned by `from`.
* - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
*
* Emits a {Transfer} event.
*/
function _safeTransfer(
address from,
address to,
uint256 tokenId,
bytes memory data
) internal virtual {
_transfer(from, to, tokenId);
require(_checkOnERC721Received(from, to, tokenId, data), "ERC721: transfer to non ERC721Receiver implementer");
}
/**
* @dev Returns the owner of the `tokenId`. Does NOT revert if token doesn't exist
*/
function _ownerOf(uint256 tokenId) internal view virtual returns (address) {
return _owners[tokenId];
}
/**
* @dev Returns whether `tokenId` exists.
*
* Tokens can be managed by their owner or approved accounts via {approve} or {setApprovalForAll}.
*
* Tokens start existing when they are minted (`_mint`),
* and stop existing when they are burned (`_burn`).
*/
function _exists(uint256 tokenId) internal view virtual returns (bool) {
return _ownerOf(tokenId) != address(0);
}
/**
* @dev Returns whether `spender` is allowed to manage `tokenId`.
*
* Requirements:
*
* - `tokenId` must exist.
*/
function _isApprovedOrOwner(address spender, uint256 tokenId) internal view virtual returns (bool) {
address owner = ERC721.ownerOf(tokenId);
return (spender == owner || isApprovedForAll(owner, spender) || getApproved(tokenId) == spender);
}
/**
* @dev Safely mints `tokenId` and transfers it to `to`.
*
* Requirements:
*
* - `tokenId` must not exist.
* - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
*
* Emits a {Transfer} event.
*/
function _safeMint(address to, uint256 tokenId) internal virtual {
_safeMint(to, tokenId, "");
}
/**
* @dev Same as {xref-ERC721-_safeMint-address-uint256-}[`_safeMint`], with an additional `data` parameter which is
* forwarded in {IERC721Receiver-onERC721Received} to contract recipients.
*/
function _safeMint(
address to,
uint256 tokenId,
bytes memory data
) internal virtual {
_mint(to, tokenId);
require(
_checkOnERC721Received(address(0), to, tokenId, data),
"ERC721: transfer to non ERC721Receiver implementer"
);
}
/**
* @dev Mints `tokenId` and transfers it to `to`.
*
* WARNING: Usage of this method is discouraged, use {_safeMint} whenever possible
*
* Requirements:
*
* - `tokenId` must not exist.
* - `to` cannot be the zero address.
*
* Emits a {Transfer} event.
*/
function _mint(address to, uint256 tokenId) internal virtual {
require(to != address(0), "ERC721: mint to the zero address");
require(!_exists(tokenId), "ERC721: token already minted");
_beforeTokenTransfer(address(0), to, tokenId);
// Check that tokenId was not minted by `_beforeTokenTransfer` hook
require(!_exists(tokenId), "ERC721: token already minted");
unchecked {
// Will not overflow unless all 2**256 token ids are minted to the same owner.
// Given that tokens are minted one by one, it is impossible in practice that
// this ever happens. Might change if we allow batch minting.
// The ERC fails to describe this case.
_balances[to] += 1;
}
_owners[tokenId] = to;
emit Transfer(address(0), to, tokenId);
_afterTokenTransfer(address(0), to, tokenId);
}
/**
* @dev Destroys `tokenId`.
* The approval is cleared when the token is burned.
* This is an internal function that does not check if the sender is authorized to operate on the token.
*
* Requirements:
*
* - `tokenId` must exist.
*
* Emits a {Transfer} event.
*/
function _burn(uint256 tokenId) internal virtual {
address owner = ERC721.ownerOf(tokenId);
_beforeTokenTransfer(owner, address(0), tokenId);
// Update ownership in case tokenId was transferred by `_beforeTokenTransfer` hook
owner = ERC721.ownerOf(tokenId);
// Clear approvals
delete _tokenApprovals[tokenId];
unchecked {
// Cannot overflow, as that would require more tokens to be burned/transferred
// out than the owner initially received through minting and transferring in.
_balances[owner] -= 1;
}
delete _owners[tokenId];
emit Transfer(owner, address(0), tokenId);
_afterTokenTransfer(owner, address(0), tokenId);
}
/**
* @dev Transfers `tokenId` from `from` to `to`.
* As opposed to {transferFrom}, this imposes no restrictions on msg.sender.
*
* Requirements:
*
* - `to` cannot be the zero address.
* - `tokenId` token must be owned by `from`.
*
* Emits a {Transfer} event.
*/
function _transfer(
address from,
address to,
uint256 tokenId
) internal virtual {
require(ERC721.ownerOf(tokenId) == from, "ERC721: transfer from incorrect owner");
require(to != address(0), "ERC721: transfer to the zero address");
_beforeTokenTransfer(from, to, tokenId);
// Check that tokenId was not transferred by `_beforeTokenTransfer` hook
require(ERC721.ownerOf(tokenId) == from, "ERC721: transfer from incorrect owner");
// Clear approvals from the previous owner
delete _tokenApprovals[tokenId];
unchecked {
// `_balances[from]` cannot overflow for the same reason as described in `_burn`:
// `from`'s balance is the number of token held, which is at least one before the current
// transfer.
// `_balances[to]` could overflow in the conditions described in `_mint`. That would require
// all 2**256 token ids to be minted, which in practice is impossible.
_balances[from] -= 1;
_balances[to] += 1;
}
_owners[tokenId] = to;
emit Transfer(from, to, tokenId);
_afterTokenTransfer(from, to, tokenId);
}
/**
* @dev Approve `to` to operate on `tokenId`
*
* Emits an {Approval} event.
*/
function _approve(address to, uint256 tokenId) internal virtual {
_tokenApprovals[tokenId] = to;
emit Approval(ERC721.ownerOf(tokenId), to, tokenId);
}
/**
* @dev Approve `operator` to operate on all of `owner` tokens
*
* Emits an {ApprovalForAll} event.
*/
function _setApprovalForAll(
address owner,
address operator,
bool approved
) internal virtual {
require(owner != operator, "ERC721: approve to caller");
_operatorApprovals[owner][operator] = approved;
emit ApprovalForAll(owner, operator, approved);
}
/**
* @dev Reverts if the `tokenId` has not been minted yet.
*/
function _requireMinted(uint256 tokenId) internal view virtual {
require(_exists(tokenId), "ERC721: invalid token ID");
}
/**
* @dev Internal function to invoke {IERC721Receiver-onERC721Received} on a target address.
* The call is not executed if the target address is not a contract.
*
* @param from address representing the previous owner of the given token ID
* @param to target address that will receive the tokens
* @param tokenId uint256 ID of the token to be transferred
* @param data bytes optional data to send along with the call
* @return bool whether the call correctly returned the expected magic value
*/
function _checkOnERC721Received(
address from,
address to,
uint256 tokenId,
bytes memory data
) private returns (bool) {
if (to.isContract()) {
try IERC721Receiver(to).onERC721Received(_msgSender(), from, tokenId, data) returns (bytes4 retval) {
return retval == IERC721Receiver.onERC721Received.selector;
} catch (bytes memory reason) {
if (reason.length == 0) {
revert("ERC721: transfer to non ERC721Receiver implementer");
} else {
/// @solidity memory-safe-assembly
assembly {
revert(add(32, reason), mload(reason))
}
}
}
} else {
return true;
}
}
/**
* @dev Hook that is called before any (single) token transfer. This includes minting and burning.
* See {_beforeConsecutiveTokenTransfer}.
*
* Calling conditions:
*
* - When `from` and `to` are both non-zero, ``from``'s `tokenId` will be
* transferred to `to`.
* - When `from` is zero, `tokenId` will be minted for `to`.
* - When `to` is zero, ``from``'s `tokenId` will be burned.
* - `from` and `to` are never both zero.
*
* To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
*/
function _beforeTokenTransfer(
address from,
address to,
uint256 tokenId
) internal virtual {}
/**
* @dev Hook that is called after any (single) transfer of tokens. This includes minting and burning.
* See {_afterConsecutiveTokenTransfer}.
*
* Calling conditions:
*
* - when `from` and `to` are both non-zero.
* - `from` and `to` are never both zero.
*
* To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
*/
function _afterTokenTransfer(
address from,
address to,
uint256 tokenId
) internal virtual {}
/**
* @dev Hook that is called before consecutive token transfers.
* Calling conditions are similar to {_beforeTokenTransfer}.
*
* The default implementation include balances updates that extensions such as {ERC721Consecutive} cannot perform
* directly.
*/
function _beforeConsecutiveTokenTransfer(
address from,
address to,
uint256, /*first*/
uint96 size
) internal virtual {
if (from != address(0)) {
_balances[from] -= size;
}
if (to != address(0)) {
_balances[to] += size;
}
}
/**
* @dev Hook that is called after consecutive token transfers.
* Calling conditions are similar to {_afterTokenTransfer}.
*/
function _afterConsecutiveTokenTransfer(
address, /*from*/
address, /*to*/
uint256, /*first*/
uint96 /*size*/
) internal virtual {}
}

View File

@ -0,0 +1,145 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.7.0) (token/ERC721/IERC721.sol)
pragma solidity ^0.8.0;
import "zeppelin/utils/introspection/IERC165.sol";
/**
* @dev Required interface of an ERC721 compliant contract.
*/
interface IERC721 is IERC165 {
/**
* @dev Emitted when `tokenId` token is transferred from `from` to `to`.
*/
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
/**
* @dev Emitted when `owner` enables `approved` to manage the `tokenId` token.
*/
event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
/**
* @dev Emitted when `owner` enables or disables (`approved`) `operator` to manage all of its assets.
*/
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
/**
* @dev Returns the number of tokens in ``owner``'s account.
*/
function balanceOf(address owner) external view returns (uint256 balance);
/**
* @dev Returns the owner of the `tokenId` token.
*
* Requirements:
*
* - `tokenId` must exist.
*/
function ownerOf(uint256 tokenId) external view returns (address owner);
/**
* @dev Safely transfers `tokenId` token from `from` to `to`.
*
* Requirements:
*
* - `from` cannot be the zero address.
* - `to` cannot be the zero address.
* - `tokenId` token must exist and be owned by `from`.
* - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.
* - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
*
* Emits a {Transfer} event.
*/
function safeTransferFrom(
address from,
address to,
uint256 tokenId,
bytes calldata data
) external;
/**
* @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients
* are aware of the ERC721 protocol to prevent tokens from being forever locked.
*
* Requirements:
*
* - `from` cannot be the zero address.
* - `to` cannot be the zero address.
* - `tokenId` token must exist and be owned by `from`.
* - If the caller is not `from`, it must have been allowed to move this token by either {approve} or {setApprovalForAll}.
* - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
*
* Emits a {Transfer} event.
*/
function safeTransferFrom(
address from,
address to,
uint256 tokenId
) external;
/**
* @dev Transfers `tokenId` token from `from` to `to`.
*
* WARNING: Note that the caller is responsible to confirm that the recipient is capable of receiving ERC721
* or else they may be permanently lost. Usage of {safeTransferFrom} prevents loss, though the caller must
* understand this adds an external call which potentially creates a reentrancy vulnerability.
*
* Requirements:
*
* - `from` cannot be the zero address.
* - `to` cannot be the zero address.
* - `tokenId` token must be owned by `from`.
* - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.
*
* Emits a {Transfer} event.
*/
function transferFrom(
address from,
address to,
uint256 tokenId
) external;
/**
* @dev Gives permission to `to` to transfer `tokenId` token to another account.
* The approval is cleared when the token is transferred.
*
* Only a single account can be approved at a time, so approving the zero address clears previous approvals.
*
* Requirements:
*
* - The caller must own the token or be an approved operator.
* - `tokenId` must exist.
*
* Emits an {Approval} event.
*/
function approve(address to, uint256 tokenId) external;
/**
* @dev Approve or remove `operator` as an operator for the caller.
* Operators can call {transferFrom} or {safeTransferFrom} for any token owned by the caller.
*
* Requirements:
*
* - The `operator` cannot be the caller.
*
* Emits an {ApprovalForAll} event.
*/
function setApprovalForAll(address operator, bool _approved) external;
/**
* @dev Returns the account approved for `tokenId` token.
*
* Requirements:
*
* - `tokenId` must exist.
*/
function getApproved(uint256 tokenId) external view returns (address operator);
/**
* @dev Returns if the `operator` is allowed to manage all of the assets of `owner`.
*
* See {setApprovalForAll}
*/
function isApprovedForAll(address owner, address operator) external view returns (bool);
}

View File

@ -0,0 +1,27 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.6.0) (token/ERC721/IERC721Receiver.sol)
pragma solidity ^0.8.0;
/**
* @title ERC721 token receiver interface
* @dev Interface for any contract that wants to support safeTransfers
* from ERC721 asset contracts.
*/
interface IERC721Receiver {
/**
* @dev Whenever an {IERC721} `tokenId` token is transferred to this contract via {IERC721-safeTransferFrom}
* by `operator` from `from`, this function is called.
*
* It must return its Solidity selector to confirm the token transfer.
* If any other value is returned or the interface is not implemented by the recipient, the transfer will be reverted.
*
* The selector can be obtained in Solidity with `IERC721Receiver.onERC721Received.selector`.
*/
function onERC721Received(
address operator,
address from,
uint256 tokenId,
bytes calldata data
) external returns (bytes4);
}

View File

@ -0,0 +1,27 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (token/ERC721/extensions/IERC721Metadata.sol)
pragma solidity ^0.8.0;
import "../IERC721.sol";
/**
* @title ERC-721 Non-Fungible Token Standard, optional metadata extension
* @dev See https://eips.ethereum.org/EIPS/eip-721
*/
interface IERC721Metadata is IERC721 {
/**
* @dev Returns the token collection name.
*/
function name() external view returns (string memory);
/**
* @dev Returns the token collection symbol.
*/
function symbol() external view returns (string memory);
/**
* @dev Returns the Uniform Resource Identifier (URI) for `tokenId` token.
*/
function tokenURI(uint256 tokenId) external view returns (string memory);
}

View File

@ -0,0 +1,244 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.7.0) (utils/Address.sol)
pragma solidity ^0.8.1;
/**
* @dev Collection of functions related to the address type
*/
library Address {
/**
* @dev Returns true if `account` is a contract.
*
* [IMPORTANT]
* ====
* It is unsafe to assume that an address for which this function returns
* false is an externally-owned account (EOA) and not a contract.
*
* Among others, `isContract` will return false for the following
* types of addresses:
*
* - an externally-owned account
* - a contract in construction
* - an address where a contract will be created
* - an address where a contract lived, but was destroyed
* ====
*
* [IMPORTANT]
* ====
* You shouldn't rely on `isContract` to protect against flash loan attacks!
*
* Preventing calls from contracts is highly discouraged. It breaks composability, breaks support for smart wallets
* like Gnosis Safe, and does not provide security since it can be circumvented by calling from a contract
* constructor.
* ====
*/
function isContract(address account) internal view returns (bool) {
// This method relies on extcodesize/address.code.length, which returns 0
// for contracts in construction, since the code is only stored at the end
// of the constructor execution.
return account.code.length > 0;
}
/**
* @dev Replacement for Solidity's `transfer`: sends `amount` wei to
* `recipient`, forwarding all available gas and reverting on errors.
*
* https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost
* of certain opcodes, possibly making contracts go over the 2300 gas limit
* imposed by `transfer`, making them unable to receive funds via
* `transfer`. {sendValue} removes this limitation.
*
* https://diligence.consensys.net/posts/2019/09/stop-using-soliditys-transfer-now/[Learn more].
*
* IMPORTANT: because control is transferred to `recipient`, care must be
* taken to not create reentrancy vulnerabilities. Consider using
* {ReentrancyGuard} or the
* https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern].
*/
function sendValue(address payable recipient, uint256 amount) internal {
require(address(this).balance >= amount, "Address: insufficient balance");
(bool success, ) = recipient.call{value: amount}("");
require(success, "Address: unable to send value, recipient may have reverted");
}
/**
* @dev Performs a Solidity function call using a low level `call`. A
* plain `call` is an unsafe replacement for a function call: use this
* function instead.
*
* If `target` reverts with a revert reason, it is bubbled up by this
* function (like regular Solidity function calls).
*
* Returns the raw returned data. To convert to the expected return value,
* use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`].
*
* Requirements:
*
* - `target` must be a contract.
* - calling `target` with `data` must not revert.
*
* _Available since v3.1._
*/
function functionCall(address target, bytes memory data) internal returns (bytes memory) {
return functionCallWithValue(target, data, 0, "Address: low-level call failed");
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with
* `errorMessage` as a fallback revert reason when `target` reverts.
*
* _Available since v3.1._
*/
function functionCall(
address target,
bytes memory data,
string memory errorMessage
) internal returns (bytes memory) {
return functionCallWithValue(target, data, 0, errorMessage);
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
* but also transferring `value` wei to `target`.
*
* Requirements:
*
* - the calling contract must have an ETH balance of at least `value`.
* - the called Solidity function must be `payable`.
*
* _Available since v3.1._
*/
function functionCallWithValue(
address target,
bytes memory data,
uint256 value
) internal returns (bytes memory) {
return functionCallWithValue(target, data, value, "Address: low-level call with value failed");
}
/**
* @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but
* with `errorMessage` as a fallback revert reason when `target` reverts.
*
* _Available since v3.1._
*/
function functionCallWithValue(
address target,
bytes memory data,
uint256 value,
string memory errorMessage
) internal returns (bytes memory) {
require(address(this).balance >= value, "Address: insufficient balance for call");
(bool success, bytes memory returndata) = target.call{value: value}(data);
return verifyCallResultFromTarget(target, success, returndata, errorMessage);
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
* but performing a static call.
*
* _Available since v3.3._
*/
function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) {
return functionStaticCall(target, data, "Address: low-level static call failed");
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`],
* but performing a static call.
*
* _Available since v3.3._
*/
function functionStaticCall(
address target,
bytes memory data,
string memory errorMessage
) internal view returns (bytes memory) {
(bool success, bytes memory returndata) = target.staticcall(data);
return verifyCallResultFromTarget(target, success, returndata, errorMessage);
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
* but performing a delegate call.
*
* _Available since v3.4._
*/
function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) {
return functionDelegateCall(target, data, "Address: low-level delegate call failed");
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`],
* but performing a delegate call.
*
* _Available since v3.4._
*/
function functionDelegateCall(
address target,
bytes memory data,
string memory errorMessage
) internal returns (bytes memory) {
(bool success, bytes memory returndata) = target.delegatecall(data);
return verifyCallResultFromTarget(target, success, returndata, errorMessage);
}
/**
* @dev Tool to verify that a low level call to smart-contract was successful, and revert (either by bubbling
* the revert reason or using the provided one) in case of unsuccessful call or if target was not a contract.
*
* _Available since v4.8._
*/
function verifyCallResultFromTarget(
address target,
bool success,
bytes memory returndata,
string memory errorMessage
) internal view returns (bytes memory) {
if (success) {
if (returndata.length == 0) {
// only check isContract if the call was successful and the return data is empty
// otherwise we already know that it was a contract
require(isContract(target), "Address: call to non-contract");
}
return returndata;
} else {
_revert(returndata, errorMessage);
}
}
/**
* @dev Tool to verify that a low level call was successful, and revert if it wasn't, either by bubbling the
* revert reason or using the provided one.
*
* _Available since v4.3._
*/
function verifyCallResult(
bool success,
bytes memory returndata,
string memory errorMessage
) internal pure returns (bytes memory) {
if (success) {
return returndata;
} else {
_revert(returndata, errorMessage);
}
}
function _revert(bytes memory returndata, string memory errorMessage) private pure {
// Look for revert reason and bubble it up if present
if (returndata.length > 0) {
// The easiest way to bubble the revert reason is using memory via assembly
/// @solidity memory-safe-assembly
assembly {
let returndata_size := mload(returndata)
revert(add(32, returndata), returndata_size)
}
} else {
revert(errorMessage);
}
}
}

View File

@ -0,0 +1,24 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (utils/Context.sol)
pragma solidity ^0.8.0;
/**
* @dev Provides information about the current execution context, including the
* sender of the transaction and its data. While these are generally available
* via msg.sender and msg.data, they should not be accessed in such a direct
* manner, since when dealing with meta-transactions the account sending and
* paying for execution may not be the actual sender (as far as an application
* is concerned).
*
* This contract is only required for intermediate, library-like contracts.
*/
abstract contract Context {
function _msgSender() internal view virtual returns (address) {
return msg.sender;
}
function _msgData() internal view virtual returns (bytes calldata) {
return msg.data;
}
}

View File

@ -0,0 +1,70 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.7.0) (utils/Strings.sol)
pragma solidity ^0.8.0;
import "zeppelin/utils/math/Math.sol";
/**
* @dev String operations.
*/
library Strings {
bytes16 private constant _SYMBOLS = "0123456789abcdef";
uint8 private constant _ADDRESS_LENGTH = 20;
/**
* @dev Converts a `uint256` to its ASCII `string` decimal representation.
*/
function toString(uint256 value) internal pure returns (string memory) {
unchecked {
uint256 length = Math.log10(value) + 1;
string memory buffer = new string(length);
uint256 ptr;
/// @solidity memory-safe-assembly
assembly {
ptr := add(buffer, add(32, length))
}
while (true) {
ptr--;
/// @solidity memory-safe-assembly
assembly {
mstore8(ptr, byte(mod(value, 10), _SYMBOLS))
}
value /= 10;
if (value == 0) break;
}
return buffer;
}
}
/**
* @dev Converts a `uint256` to its ASCII `string` hexadecimal representation.
*/
function toHexString(uint256 value) internal pure returns (string memory) {
unchecked {
return toHexString(value, Math.log256(value) + 1);
}
}
/**
* @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length.
*/
function toHexString(uint256 value, uint256 length) internal pure returns (string memory) {
bytes memory buffer = new bytes(2 * length + 2);
buffer[0] = "0";
buffer[1] = "x";
for (uint256 i = 2 * length + 1; i > 1; --i) {
buffer[i] = _SYMBOLS[value & 0xf];
value >>= 4;
}
require(value == 0, "Strings: hex length insufficient");
return string(buffer);
}
/**
* @dev Converts an `address` with fixed length of 20 bytes to its not checksummed ASCII `string` hexadecimal representation.
*/
function toHexString(address addr) internal pure returns (string memory) {
return toHexString(uint256(uint160(addr)), _ADDRESS_LENGTH);
}
}

View File

@ -0,0 +1,29 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (utils/introspection/ERC165.sol)
pragma solidity ^0.8.0;
import "zeppelin/utils/introspection/IERC165.sol";
/**
* @dev Implementation of the {IERC165} interface.
*
* Contracts that want to implement ERC165 should inherit from this contract and override {supportsInterface} to check
* for the additional interface id that will be supported. For example:
*
* ```solidity
* function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
* return interfaceId == type(MyInterface).interfaceId || super.supportsInterface(interfaceId);
* }
* ```
*
* Alternatively, {ERC165Storage} provides an easier to use but more expensive implementation.
*/
abstract contract ERC165 is IERC165 {
/**
* @dev See {IERC165-supportsInterface}.
*/
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
return interfaceId == type(IERC165).interfaceId;
}
}

View File

@ -0,0 +1,25 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (utils/introspection/IERC165.sol)
pragma solidity ^0.8.0;
/**
* @dev Interface of the ERC165 standard, as defined in the
* https://eips.ethereum.org/EIPS/eip-165[EIP].
*
* Implementers can declare support of contract interfaces, which can then be
* queried by others ({ERC165Checker}).
*
* For an implementation, see {ERC165}.
*/
interface IERC165 {
/**
* @dev Returns true if this contract implements the interface defined by
* `interfaceId`. See the corresponding
* https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section]
* to learn more about how these ids are created.
*
* This function call must use less than 30 000 gas.
*/
function supportsInterface(bytes4 interfaceId) external view returns (bool);
}

View File

@ -0,0 +1,345 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.7.0) (utils/math/Math.sol)
pragma solidity ^0.8.0;
/**
* @dev Standard math utilities missing in the Solidity language.
*/
library Math {
enum Rounding {
Down, // Toward negative infinity
Up, // Toward infinity
Zero // Toward zero
}
/**
* @dev Returns the largest of two numbers.
*/
function max(uint256 a, uint256 b) internal pure returns (uint256) {
return a > b ? a : b;
}
/**
* @dev Returns the smallest of two numbers.
*/
function min(uint256 a, uint256 b) internal pure returns (uint256) {
return a < b ? a : b;
}
/**
* @dev Returns the average of two numbers. The result is rounded towards
* zero.
*/
function average(uint256 a, uint256 b) internal pure returns (uint256) {
// (a + b) / 2 can overflow.
return (a & b) + (a ^ b) / 2;
}
/**
* @dev Returns the ceiling of the division of two numbers.
*
* This differs from standard division with `/` in that it rounds up instead
* of rounding down.
*/
function ceilDiv(uint256 a, uint256 b) internal pure returns (uint256) {
// (a + b - 1) / b can overflow on addition, so we distribute.
return a == 0 ? 0 : (a - 1) / b + 1;
}
/**
* @notice Calculates floor(x * y / denominator) with full precision. Throws if result overflows a uint256 or denominator == 0
* @dev Original credit to Remco Bloemen under MIT license (https://xn--2-umb.com/21/muldiv)
* with further edits by Uniswap Labs also under MIT license.
*/
function mulDiv(
uint256 x,
uint256 y,
uint256 denominator
) internal pure returns (uint256 result) {
unchecked {
// 512-bit multiply [prod1 prod0] = x * y. Compute the product mod 2^256 and mod 2^256 - 1, then use
// use the Chinese Remainder Theorem to reconstruct the 512 bit result. The result is stored in two 256
// variables such that product = prod1 * 2^256 + prod0.
uint256 prod0; // Least significant 256 bits of the product
uint256 prod1; // Most significant 256 bits of the product
assembly {
let mm := mulmod(x, y, not(0))
prod0 := mul(x, y)
prod1 := sub(sub(mm, prod0), lt(mm, prod0))
}
// Handle non-overflow cases, 256 by 256 division.
if (prod1 == 0) {
return prod0 / denominator;
}
// Make sure the result is less than 2^256. Also prevents denominator == 0.
require(denominator > prod1);
///////////////////////////////////////////////
// 512 by 256 division.
///////////////////////////////////////////////
// Make division exact by subtracting the remainder from [prod1 prod0].
uint256 remainder;
assembly {
// Compute remainder using mulmod.
remainder := mulmod(x, y, denominator)
// Subtract 256 bit number from 512 bit number.
prod1 := sub(prod1, gt(remainder, prod0))
prod0 := sub(prod0, remainder)
}
// Factor powers of two out of denominator and compute largest power of two divisor of denominator. Always >= 1.
// See https://cs.stackexchange.com/q/138556/92363.
// Does not overflow because the denominator cannot be zero at this stage in the function.
uint256 twos = denominator & (~denominator + 1);
assembly {
// Divide denominator by twos.
denominator := div(denominator, twos)
// Divide [prod1 prod0] by twos.
prod0 := div(prod0, twos)
// Flip twos such that it is 2^256 / twos. If twos is zero, then it becomes one.
twos := add(div(sub(0, twos), twos), 1)
}
// Shift in bits from prod1 into prod0.
prod0 |= prod1 * twos;
// Invert denominator mod 2^256. Now that denominator is an odd number, it has an inverse modulo 2^256 such
// that denominator * inv = 1 mod 2^256. Compute the inverse by starting with a seed that is correct for
// four bits. That is, denominator * inv = 1 mod 2^4.
uint256 inverse = (3 * denominator) ^ 2;
// Use the Newton-Raphson iteration to improve the precision. Thanks to Hensel's lifting lemma, this also works
// in modular arithmetic, doubling the correct bits in each step.
inverse *= 2 - denominator * inverse; // inverse mod 2^8
inverse *= 2 - denominator * inverse; // inverse mod 2^16
inverse *= 2 - denominator * inverse; // inverse mod 2^32
inverse *= 2 - denominator * inverse; // inverse mod 2^64
inverse *= 2 - denominator * inverse; // inverse mod 2^128
inverse *= 2 - denominator * inverse; // inverse mod 2^256
// Because the division is now exact we can divide by multiplying with the modular inverse of denominator.
// This will give us the correct result modulo 2^256. Since the preconditions guarantee that the outcome is
// less than 2^256, this is the final result. We don't need to compute the high bits of the result and prod1
// is no longer required.
result = prod0 * inverse;
return result;
}
}
/**
* @notice Calculates x * y / denominator with full precision, following the selected rounding direction.
*/
function mulDiv(
uint256 x,
uint256 y,
uint256 denominator,
Rounding rounding
) internal pure returns (uint256) {
uint256 result = mulDiv(x, y, denominator);
if (rounding == Rounding.Up && mulmod(x, y, denominator) > 0) {
result += 1;
}
return result;
}
/**
* @dev Returns the square root of a number. If the number is not a perfect square, the value is rounded down.
*
* Inspired by Henry S. Warren, Jr.'s "Hacker's Delight" (Chapter 11).
*/
function sqrt(uint256 a) internal pure returns (uint256) {
if (a == 0) {
return 0;
}
// For our first guess, we get the biggest power of 2 which is smaller than the square root of the target.
//
// We know that the "msb" (most significant bit) of our target number `a` is a power of 2 such that we have
// `msb(a) <= a < 2*msb(a)`. This value can be written `msb(a)=2**k` with `k=log2(a)`.
//
// This can be rewritten `2**log2(a) <= a < 2**(log2(a) + 1)`
// `sqrt(2**k) <= sqrt(a) < sqrt(2**(k+1))`
// `2**(k/2) <= sqrt(a) < 2**((k+1)/2) <= 2**(k/2 + 1)`
//
// Consequently, `2**(log2(a) / 2)` is a good first approximation of `sqrt(a)` with at least 1 correct bit.
uint256 result = 1 << (log2(a) >> 1);
// At this point `result` is an estimation with one bit of precision. We know the true value is a uint128,
// since it is the square root of a uint256. Newton's method converges quadratically (precision doubles at
// every iteration). We thus need at most 7 iteration to turn our partial result with one bit of precision
// into the expected uint128 result.
unchecked {
result = (result + a / result) >> 1;
result = (result + a / result) >> 1;
result = (result + a / result) >> 1;
result = (result + a / result) >> 1;
result = (result + a / result) >> 1;
result = (result + a / result) >> 1;
result = (result + a / result) >> 1;
return min(result, a / result);
}
}
/**
* @notice Calculates sqrt(a), following the selected rounding direction.
*/
function sqrt(uint256 a, Rounding rounding) internal pure returns (uint256) {
unchecked {
uint256 result = sqrt(a);
return result + (rounding == Rounding.Up && result * result < a ? 1 : 0);
}
}
/**
* @dev Return the log in base 2, rounded down, of a positive value.
* Returns 0 if given 0.
*/
function log2(uint256 value) internal pure returns (uint256) {
uint256 result = 0;
unchecked {
if (value >> 128 > 0) {
value >>= 128;
result += 128;
}
if (value >> 64 > 0) {
value >>= 64;
result += 64;
}
if (value >> 32 > 0) {
value >>= 32;
result += 32;
}
if (value >> 16 > 0) {
value >>= 16;
result += 16;
}
if (value >> 8 > 0) {
value >>= 8;
result += 8;
}
if (value >> 4 > 0) {
value >>= 4;
result += 4;
}
if (value >> 2 > 0) {
value >>= 2;
result += 2;
}
if (value >> 1 > 0) {
result += 1;
}
}
return result;
}
/**
* @dev Return the log in base 2, following the selected rounding direction, of a positive value.
* Returns 0 if given 0.
*/
function log2(uint256 value, Rounding rounding) internal pure returns (uint256) {
unchecked {
uint256 result = log2(value);
return result + (rounding == Rounding.Up && 1 << result < value ? 1 : 0);
}
}
/**
* @dev Return the log in base 10, rounded down, of a positive value.
* Returns 0 if given 0.
*/
function log10(uint256 value) internal pure returns (uint256) {
uint256 result = 0;
unchecked {
if (value >= 10**64) {
value /= 10**64;
result += 64;
}
if (value >= 10**32) {
value /= 10**32;
result += 32;
}
if (value >= 10**16) {
value /= 10**16;
result += 16;
}
if (value >= 10**8) {
value /= 10**8;
result += 8;
}
if (value >= 10**4) {
value /= 10**4;
result += 4;
}
if (value >= 10**2) {
value /= 10**2;
result += 2;
}
if (value >= 10**1) {
result += 1;
}
}
return result;
}
/**
* @dev Return the log in base 10, following the selected rounding direction, of a positive value.
* Returns 0 if given 0.
*/
function log10(uint256 value, Rounding rounding) internal pure returns (uint256) {
unchecked {
uint256 result = log10(value);
return result + (rounding == Rounding.Up && 10**result < value ? 1 : 0);
}
}
/**
* @dev Return the log in base 256, rounded down, of a positive value.
* Returns 0 if given 0.
*
* Adding one to the result gives the number of pairs of hex symbols needed to represent `value` as a hex string.
*/
function log256(uint256 value) internal pure returns (uint256) {
uint256 result = 0;
unchecked {
if (value >> 128 > 0) {
value >>= 128;
result += 16;
}
if (value >> 64 > 0) {
value >>= 64;
result += 8;
}
if (value >> 32 > 0) {
value >>= 32;
result += 4;
}
if (value >> 16 > 0) {
value >>= 16;
result += 2;
}
if (value >> 8 > 0) {
result += 1;
}
}
return result;
}
/**
* @dev Return the log in base 10, following the selected rounding direction, of a positive value.
* Returns 0 if given 0.
*/
function log256(uint256 value, Rounding rounding) internal pure returns (uint256) {
unchecked {
uint256 result = log256(value);
return result + (rounding == Rounding.Up && 1 << (result * 8) < value ? 1 : 0);
}
}
}

View File

@ -0,0 +1,414 @@
"""
This file is part of nucypher.
nucypher is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
nucypher is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import copy
import json
import os
from unittest import mock
import pytest
from web3 import Web3
from nucypher.policy.conditions.context import (
USER_ADDRESS_CONTEXT,
ContextVariableVerificationFailed,
InvalidContextVariableData,
RequiredContextVariable,
_recover_user_address,
)
from nucypher.policy.conditions.evm import RPCCondition, get_context_value
from nucypher.policy.conditions.lingo import ConditionLingo, ReturnValueTest
from tests.integration.characters.test_bob_handles_frags import _make_message_kits
VALID_USER_ADDRESS_CONTEXT = {
USER_ADDRESS_CONTEXT: {
"signature": "0x488a7acefdc6d098eedf73cdfd379777c0f4a4023a660d350d3bf309a51dd4251abaad9cdd11b71c400cfb4625c14ca142f72b39165bd980c8da1ea32892ff071c",
"address": "0x5ce9454909639D2D17A3F753ce7d93fa0b9aB12E",
"typedData": {
"primaryType": "Wallet",
"types": {
"EIP712Domain": [
{"name": "name", "type": "string"},
{"name": "version", "type": "string"},
{"name": "chainId", "type": "uint256"},
{"name": "salt", "type": "bytes32"},
],
"Wallet": [
{"name": "address", "type": "string"},
{"name": "blockNumber", "type": "uint256"},
{"name": "blockHash", "type": "bytes32"},
{"name": "signatureText", "type": "string"},
],
},
"domain": {
"name": "tDec",
"version": "1",
"chainId": 80001,
"salt": "0x3e6365d35fd4e53cbc00b080b0742b88f8b735352ea54c0534ed6a2e44a83ff0",
},
"message": {
"address": "0x5ce9454909639D2D17A3F753ce7d93fa0b9aB12E",
"blockNumber": 28117088,
"blockHash": "0x104dfae58be4a9b15d59ce447a565302d5658914f1093f10290cd846fbe258b7",
"signatureText": "I'm the owner of address 0x5ce9454909639D2D17A3F753ce7d93fa0b9aB12E as of block number 28117088",
},
},
}
}
def _dont_validate_user_address(context_variable: str, **context):
if context_variable == USER_ADDRESS_CONTEXT:
return context[USER_ADDRESS_CONTEXT]["address"]
return get_context_value(context_variable, **context)
def test_required_context_variable(
testerchain, custom_context_variable_erc20_condition
):
with pytest.raises(RequiredContextVariable):
custom_context_variable_erc20_condition.verify(
provider=testerchain.provider
) # no context
@pytest.mark.parametrize("expected_entry", ["address", "signature", "typedData"])
def test_user_address_context_missing_required_entries(expected_entry):
context = copy.deepcopy(VALID_USER_ADDRESS_CONTEXT)
del context[USER_ADDRESS_CONTEXT][expected_entry]
with pytest.raises(InvalidContextVariableData):
_recover_user_address(**context)
def test_user_address_context_invalid_eip712_typed_data():
# invalid typed data
context = copy.deepcopy(VALID_USER_ADDRESS_CONTEXT)
context[USER_ADDRESS_CONTEXT]["typedData"] = dict(
randomSaying="Comparison is the thief of joy." # - Theodore Roosevelt
)
with pytest.raises(InvalidContextVariableData):
_recover_user_address(**context)
def test_user_address_context_variable_verification(testerchain):
# valid user address context - signature matches address
address = _recover_user_address(**VALID_USER_ADDRESS_CONTEXT)
assert address == VALID_USER_ADDRESS_CONTEXT[USER_ADDRESS_CONTEXT]["address"]
# invalid user address context - signature does not match address
# internals are mutable - deepcopy
mismatch_with_address_context = copy.deepcopy(VALID_USER_ADDRESS_CONTEXT)
mismatch_with_address_context[USER_ADDRESS_CONTEXT][
"address"
] = testerchain.etherbase_account
with pytest.raises(ContextVariableVerificationFailed):
_recover_user_address(**mismatch_with_address_context)
# invalid user address context - signature does not match address
# internals are mutable - deepcopy
mismatch_with_address_context = copy.deepcopy(VALID_USER_ADDRESS_CONTEXT)
signature = (
"0x93252ddff5f90584b27b5eef1915b23a8b01a703be56c8bf0660647c15cb75e9"
"1983bde9877eaad11da5a3ebc9b64957f1c182536931f9844d0c600f0c41293d1b"
)
mismatch_with_address_context[USER_ADDRESS_CONTEXT]["signature"] = signature
with pytest.raises(ContextVariableVerificationFailed):
_recover_user_address(**mismatch_with_address_context)
# invalid signature
# internals are mutable - deepcopy
invalid_signature_context = copy.deepcopy(VALID_USER_ADDRESS_CONTEXT)
invalid_signature_context[USER_ADDRESS_CONTEXT][
"signature"
] = "0xdeadbeef" # invalid signature
with pytest.raises(ContextVariableVerificationFailed):
_recover_user_address(**invalid_signature_context)
@mock.patch(
"nucypher.policy.conditions.evm.get_context_value",
side_effect=_dont_validate_user_address,
)
def test_rpc_condition_evaluation(get_context_value_mock, testerchain, rpc_condition):
context = {USER_ADDRESS_CONTEXT: {"address": testerchain.unassigned_accounts[0]}}
condition_result, call_result = rpc_condition.verify(
provider=testerchain.provider, **context
)
assert condition_result is True
assert call_result == Web3.to_wei(
1_000_000, "ether"
) # same value used in rpc_condition fixture
@mock.patch(
"nucypher.policy.conditions.evm.get_context_value",
side_effect=_dont_validate_user_address,
)
def test_rpc_condition_evaluation_with_context_var_in_return_value_test(
get_context_value_mock, testerchain
):
account, *other_accounts = testerchain.client.accounts
balance = testerchain.client.get_balance(account)
# we have balance stored, use for rpc condition with context variable
rpc_condition = RPCCondition(
method="eth_getBalance",
chain="testerchain",
return_value_test=ReturnValueTest(
"==", ":balanceContextVar"
), # user-defined context var
parameters=[USER_ADDRESS_CONTEXT],
)
context = {
USER_ADDRESS_CONTEXT: {"address": account},
":balanceContextVar": balance,
}
condition_result, call_result = rpc_condition.verify(
provider=testerchain.provider, **context
)
assert condition_result is True
assert call_result == balance
# modify balance to make it false
invalid_balance = balance + 1
context[":balanceContextVar"] = invalid_balance
condition_result, call_result = rpc_condition.verify(
provider=testerchain.provider, **context
)
assert condition_result is False
assert call_result != invalid_balance
@mock.patch(
"nucypher.policy.conditions.evm.get_context_value",
side_effect=_dont_validate_user_address,
)
def test_erc20_evm_condition_evaluation(
get_context_value_mock, testerchain, erc20_evm_condition
):
context = {USER_ADDRESS_CONTEXT: {"address": testerchain.unassigned_accounts[0]}}
condition_result, call_result = erc20_evm_condition.verify(
provider=testerchain.provider, **context
)
assert condition_result is True
context[USER_ADDRESS_CONTEXT]["address"] = testerchain.etherbase_account
condition_result, call_result = erc20_evm_condition.verify(
provider=testerchain.provider, **context
)
assert condition_result is False
def test_erc20_evm_condition_evaluation_with_custom_context_variable(
testerchain, custom_context_variable_erc20_condition
):
context = {":addressToUse": testerchain.unassigned_accounts[0]}
condition_result, call_result = custom_context_variable_erc20_condition.verify(
provider=testerchain.provider, **context
)
assert condition_result is True
context[":addressToUse"] = testerchain.etherbase_account
condition_result, call_result = custom_context_variable_erc20_condition.verify(
provider=testerchain.provider, **context
)
assert condition_result is False
@mock.patch(
"nucypher.policy.conditions.evm.get_context_value",
side_effect=_dont_validate_user_address,
)
def test_erc721_evm_condition_owner_evaluation(
get_context_value_mock, testerchain, test_registry, erc721_evm_condition_owner
):
account, *other_accounts = testerchain.client.accounts
# valid owner of nft
context = {
USER_ADDRESS_CONTEXT: {"address": account},
":tokenId": 1, # valid token id
}
condition_result, call_result = erc721_evm_condition_owner.verify(
provider=testerchain.provider, **context
)
assert condition_result is True
assert call_result == account
# invalid token id
with pytest.raises(RPCCondition.RPCExecutionFailed):
context[":tokenId"] = 255
_, _ = erc721_evm_condition_owner.verify(
provider=testerchain.provider, **context
)
# invalid owner of nft
other_account = other_accounts[0]
context = {
USER_ADDRESS_CONTEXT: {"address": other_account},
":tokenId": 1, # valid token id
}
condition_result, call_result = erc721_evm_condition_owner.verify(
provider=testerchain.provider, **context
)
assert condition_result is False
assert call_result != other_account
@mock.patch(
"nucypher.policy.conditions.evm.get_context_value",
side_effect=_dont_validate_user_address,
)
def test_erc721_evm_condition_balanceof_evaluation(
get_context_value_mock, testerchain, test_registry, erc721_evm_condition_balanceof
):
account, *other_accounts = testerchain.client.accounts
context = {USER_ADDRESS_CONTEXT: {"address": account}} # owner of NFT
condition_result, call_result = erc721_evm_condition_balanceof.verify(
provider=testerchain.provider, **context
)
assert condition_result is True
# invalid owner of nft
other_account = other_accounts[0] # not an owner of NFT
context = {USER_ADDRESS_CONTEXT: {"address": other_account}}
condition_result, call_result = erc721_evm_condition_balanceof.verify(
provider=testerchain.provider, **context
)
assert not condition_result
def test_subscription_manager_is_active_policy_condition_evaluation(
testerchain,
enacted_blockchain_policy,
subscription_manager_is_active_policy_condition,
):
context = {
":hrac": bytes(enacted_blockchain_policy.hrac)
} # user-defined context var
condition_result, call_result = subscription_manager_is_active_policy_condition.verify(
provider=testerchain.provider, **context
)
assert call_result
assert condition_result is True
# non-active policy hrac
context[":hrac"] = os.urandom(16)
condition_result, call_result = subscription_manager_is_active_policy_condition.verify(
provider=testerchain.provider, **context
)
assert not call_result
assert not condition_result
def test_subscription_manager_get_policy_policy_struct_condition_evaluation(
testerchain,
enacted_blockchain_policy,
subscription_manager_get_policy_zeroized_policy_struct_condition,
):
# zeroized policy struct
zeroized_policy_struct = (
"0x0000000000000000000000000000000000000000",
0,
0,
0,
"0x0000000000000000000000000000000000000000",
)
context = {
":hrac": bytes(enacted_blockchain_policy.hrac),
":expectedPolicyStruct": zeroized_policy_struct,
} # user-defined context vars
condition_result, call_result = subscription_manager_get_policy_zeroized_policy_struct_condition.verify(
provider=testerchain.provider, **context
)
assert call_result != zeroized_policy_struct
assert not condition_result # not zeroized policy
# unknown policy hrac
context[":hrac"] = os.urandom(16)
condition_result, call_result = subscription_manager_get_policy_zeroized_policy_struct_condition.verify(
provider=testerchain.provider, **context
)
assert call_result == zeroized_policy_struct
assert condition_result is True # zeroized policy was indeed returned
def test_time_condition_evaluation(testerchain, timelock_condition):
condition_result, call_result = timelock_condition.verify(
provider=testerchain.provider
)
assert condition_result is True
def test_simple_compound_conditions_evaluation(testerchain):
# TODO Improve internals of evaluation here (natural vs recursive approach)
conditions = [
{'returnValueTest': {'value': '0', 'comparator': '>'}, 'method': 'timelock'},
{'operator': 'and'},
{'returnValueTest': {'value': '99999999999999999', 'comparator': '<'}, 'method': 'timelock'},
{'operator': 'and'},
{'returnValueTest': {'value': '0', 'comparator': '>'}, 'method': 'timelock'}
]
conditions = json.dumps(conditions)
lingo = ConditionLingo.from_json(conditions)
result = lingo.eval()
assert result is True
@mock.patch(
"nucypher.policy.conditions.evm.get_context_value",
side_effect=_dont_validate_user_address,
)
def test_onchain_conditions_lingo_evaluation(
get_context_value_mock,
testerchain,
lingo,
):
context = {USER_ADDRESS_CONTEXT: {"address": testerchain.etherbase_account}}
result = lingo.eval(provider=testerchain.provider, **context)
assert result is True
def test_single_retrieve_with_onchain_conditions(enacted_blockchain_policy, blockchain_bob, blockchain_ursulas):
blockchain_bob.start_learning_loop()
conditions = [
{'returnValueTest': {'value': '0', 'comparator': '>'}, 'method': 'timelock'},
{'operator': 'and'},
{"chain": "testerchain",
"method": "eth_getBalance",
"parameters": [
blockchain_bob.checksum_address,
"latest"
],
"returnValueTest": {
"comparator": ">=",
"value": "10000000000000"
}
}
]
messages, message_kits = _make_message_kits(enacted_blockchain_policy.public_key, conditions)
policy_info_kwargs = dict(
encrypted_treasure_map=enacted_blockchain_policy.treasure_map,
alice_verifying_key=enacted_blockchain_policy.publisher_verifying_key,
)
cleartexts = blockchain_bob.retrieve_and_decrypt(
message_kits=message_kits,
**policy_info_kwargs,
)
assert cleartexts == messages

View File

@ -22,17 +22,23 @@ from nucypher.blockchain.eth.interfaces import BlockchainDeployerInterface
from nucypher.blockchain.eth.registry import InMemoryContractRegistry
from nucypher.blockchain.eth.signers.software import Web3Signer
from nucypher.blockchain.eth.sol.compile.compile import multiversion_compile
from nucypher.blockchain.eth.sol.compile.constants import TEST_MULTIVERSION_CONTRACTS, SOLIDITY_SOURCE_ROOT
from nucypher.blockchain.eth.sol.compile.constants import (
SOLIDITY_SOURCE_ROOT,
TEST_MULTIVERSION_CONTRACTS,
)
from nucypher.blockchain.eth.sol.compile.types import SourceBundle
from nucypher.crypto.powers import TransactingPower
from tests.constants import (
DEVELOPMENT_ETH_AIRDROP_AMOUNT,
INSECURE_DEVELOPMENT_PASSWORD,
NUMBER_OF_ETH_TEST_ACCOUNTS,
NUMBER_OF_STAKING_PROVIDERS_IN_BLOCKCHAIN_TESTS,
NUMBER_OF_URSULAS_IN_BLOCKCHAIN_TESTS, INSECURE_DEVELOPMENT_PASSWORD
NUMBER_OF_URSULAS_IN_BLOCKCHAIN_TESTS,
)
# Prevents TesterBlockchain to be picked up by py.test as a test class
from tests.utils.blockchain import TesterBlockchain as _TesterBlockchain, free_gas_price_strategy
from tests.utils.blockchain import TesterBlockchain as _TesterBlockchain
from tests.utils.blockchain import free_gas_price_strategy
@pytest.fixture()
@ -53,8 +59,8 @@ def test_testerchain_creation(testerchain, another_testerchain):
assert 'tester' in chain.eth_provider_uri
# ... and that there are already some blocks mined
chain.w3.eth.web3.testing.mine(1)
assert chain.w3.eth.blockNumber > 0
chain.w3.eth.w3.testing.mine(1)
assert chain.w3.eth.block_number > 0
# Check that we have enough test accounts
assert len(chain.client.accounts) >= NUMBER_OF_ETH_TEST_ACCOUNTS

View File

@ -63,8 +63,8 @@ def test_transacting_power_sign_transaction(testerchain):
signer=Web3Signer(testerchain.client),
account=eth_address)
transaction_dict = {'nonce': testerchain.client.w3.eth.getTransactionCount(eth_address),
'gasPrice': testerchain.client.w3.eth.gasPrice,
transaction_dict = {'nonce': testerchain.client.w3.eth.get_transaction_count(eth_address),
'gasPrice': testerchain.client.w3.eth.gas_price,
'gas': 100000,
'from': eth_address,
'to': testerchain.unassigned_accounts[1],
@ -94,12 +94,12 @@ def test_transacting_power_sign_agent_transaction(testerchain, agency, test_regi
contract_function = agent.contract.functions.confirmOperatorAddress()
payload = {'chainId': int(testerchain.client.chain_id),
'nonce': testerchain.client.w3.eth.getTransactionCount(testerchain.etherbase_account),
'nonce': testerchain.client.w3.eth.get_transaction_count(testerchain.etherbase_account),
'from': testerchain.etherbase_account,
'gasPrice': testerchain.client.gas_price,
'gas': 500_000}
unsigned_transaction = contract_function.buildTransaction(payload)
unsigned_transaction = contract_function.build_transaction(payload)
# Sign with Transacting Power
transacting_power = TransactingPower(password=INSECURE_DEVELOPMENT_PASSWORD,

View File

@ -25,6 +25,7 @@ from nucypher.characters.lawful import Enrico, Ursula
from nucypher.characters.unlawful import Amonia
@pytest.mark.skip('FIXME - DISABLED FOR TDEC ADAPTATION DEVELOPMENT')
def test_try_to_post_free_service_by_hacking_enact(blockchain_ursulas,
blockchain_alice,
blockchain_bob,
@ -34,7 +35,7 @@ def test_try_to_post_free_service_by_hacking_enact(blockchain_ursulas,
This time we won't rely on the tabulation in Alice's enact() to catch the problem.
"""
amonia = Amonia.from_lawful_alice(blockchain_alice)
# Setup the policy details
# Set up the policy details
shares = 3
policy_end_datetime = maya.now() + datetime.timedelta(days=35)
label = b"another_path"

View File

@ -54,8 +54,8 @@ def test_character_transacting_power_signing(testerchain, agency, test_registry)
assert is_verified is True
# Sign Transaction
transaction_dict = {'nonce': testerchain.client.w3.eth.getTransactionCount(eth_address),
'gasPrice': testerchain.client.w3.eth.gasPrice,
transaction_dict = {'nonce': testerchain.client.w3.eth.get_transaction_count(eth_address),
'gasPrice': testerchain.client.w3.eth.gas_price,
'gas': 100000,
'from': eth_address,
'to': testerchain.unassigned_accounts[1],

View File

@ -16,10 +16,10 @@
"""
import os
import tempfile
from pathlib import Path
import pytest
import tempfile
@pytest.fixture(scope='module')
@ -31,10 +31,8 @@ def ursula(blockchain_ursulas):
@pytest.fixture(scope='module')
def client(ursula):
db_fd, db_path = tempfile.mkstemp()
ursula.rest_app.config['DATABASE'] = Path(db_path)
ursula.rest_app.config['TESTING'] = True
with ursula.rest_app.test_client() as client:
yield client
ursula.rest_app.config.update({"TESTING": True, "DATABASE": Path(db_path)})
yield ursula.rest_app.test_client()
os.close(db_fd)
ursula.rest_app.config['DATABASE'].unlink()

View File

@ -30,8 +30,12 @@ from web3 import Web3
from nucypher.cli.main import nucypher_cli
from nucypher.config.characters import AliceConfiguration, BobConfiguration
from nucypher.config.constants import NUCYPHER_ENVVAR_KEYSTORE_PASSWORD, TEMPORARY_DOMAIN, \
NUCYPHER_ENVVAR_ALICE_ETH_PASSWORD, NUCYPHER_ENVVAR_BOB_ETH_PASSWORD
from nucypher.config.constants import (
NUCYPHER_ENVVAR_KEYSTORE_PASSWORD,
TEMPORARY_DOMAIN,
NUCYPHER_ENVVAR_ALICE_ETH_PASSWORD,
NUCYPHER_ENVVAR_BOB_ETH_PASSWORD
)
from nucypher.utilities.logging import GlobalLoggerSettings
from tests.constants import INSECURE_DEVELOPMENT_PASSWORD, TEST_ETH_PROVIDER_URI
@ -329,7 +333,7 @@ def run_entire_cli_lifecycle(click_runner,
grant_args += ('--federated-only',)
else:
grant_args += ('--eth-provider', TEST_ETH_PROVIDER_URI,
'--value', Web3.toWei(9, 'gwei'))
'--value', Web3.to_wei(9, 'gwei'))
grant_result = click_runner.invoke(nucypher_cli, grant_args, catch_exceptions=False, env=envvars)
assert grant_result.exit_code == 0, (grant_result.output, grant_result.exception)

View File

@ -55,10 +55,10 @@ def mock_funded_account_password_keystore(tmp_path_factory, testerchain, thresho
json.dump(account.encrypt(password), open(path, 'x+t'))
testerchain.wait_for_receipt(
testerchain.client.w3.eth.sendTransaction({
testerchain.client.w3.eth.send_transaction({
'to': account.address,
'from': testerchain.etherbase_account,
'value': Web3.toWei('1', 'ether')}))
'value': Web3.to_wei('1', 'ether')}))
# initialize threshold stake
provider_address = testerchain.unassigned_accounts[0]
@ -151,7 +151,7 @@ def test_ursula_and_local_keystore_signer_integration(click_runner,
# ...and that transactions are signed by the keystore signer
txhash = ursula.confirm_address()
receipt = testerchain.wait_for_receipt(txhash)
transaction_data = testerchain.client.w3.eth.getTransaction(receipt['transactionHash'])
transaction_data = testerchain.client.w3.eth.get_transaction(receipt['transactionHash'])
assert transaction_data['from'] == worker_account.address
finally:
ursula.stop()

View File

@ -77,7 +77,8 @@ def test_retrieve_cfrags(blockchain_porter,
blockchain_porter_rpc_controller,
random_blockchain_policy,
blockchain_bob,
blockchain_alice):
blockchain_alice,
random_context):
method = 'retrieve_cfrags'
# Setup
@ -102,6 +103,19 @@ def test_retrieve_cfrags(blockchain_porter,
expected_results = blockchain_porter.retrieve_cfrags(**retrieve_args)
assert len(retrieval_results) == len(expected_results)
# Use context
retrieve_cfrags_params_with_context, _ = retrieval_request_setup(enacted_policy,
blockchain_bob,
blockchain_alice,
context=random_context,
encode_for_rest=True)
request_data = {'method': method, 'params': retrieve_cfrags_params_with_context}
response = blockchain_porter_rpc_controller.send(request_data)
assert response.success
retrieval_results = response.data['result']['retrieval_results']
assert retrieval_results
# Failure - use encrypted treasure map
failure_retrieve_cfrags_params = dict(retrieve_cfrags_params)
failure_retrieve_cfrags_params['treasure_map'] = b64encode(os.urandom(32)).decode()

View File

@ -18,16 +18,24 @@
import json
import os
from base64 import b64encode
from urllib.parse import urlencode
from nucypher_core import RetrievalKit
from nucypher.characters.lawful import Enrico
from nucypher.control.specifications.fields import JSON
from nucypher.crypto.powers import DecryptingPower
from nucypher.policy.kits import PolicyMessageKit, RetrievalResult
from nucypher.utilities.porter.control.specifications.fields import RetrievalResultSchema, RetrievalKit as RetrievalKitField
from nucypher.utilities.porter.control.specifications.fields import (
RetrievalKit as RetrievalKitField,
)
from nucypher.utilities.porter.control.specifications.fields import (
RetrievalOutcomeSchema,
)
from tests.utils.middleware import MockRestMiddleware
from tests.utils.policy import retrieval_request_setup, retrieval_params_decode_from_rest
from tests.utils.policy import (
retrieval_params_decode_from_rest,
retrieval_request_setup,
)
def test_get_ursulas(blockchain_porter_web_controller, blockchain_ursulas):
@ -91,7 +99,8 @@ def test_retrieve_cfrags(blockchain_porter,
blockchain_porter_web_controller,
random_blockchain_policy,
blockchain_bob,
blockchain_alice):
blockchain_alice,
random_context):
# Send bad data to assert error return
response = blockchain_porter_web_controller.post('/retrieve_cfrags', data=json.dumps({'bad': 'input'}))
assert response.status_code == 400
@ -102,11 +111,13 @@ def test_retrieve_cfrags(blockchain_porter,
enacted_policy = random_blockchain_policy.enact(network_middleware=network_middleware)
original_message = b"Those who say it can't be done are usually interrupted by others doing it." # - James Baldwin
retrieve_cfrags_params, message_kit = retrieval_request_setup(enacted_policy,
retrieve_cfrags_params, message_kits = retrieval_request_setup(enacted_policy,
blockchain_bob,
blockchain_alice,
original_message=original_message,
specific_messages=[original_message],
encode_for_rest=True)
assert len(message_kits) == 1
message_kit = message_kits[0]
#
# Success
@ -129,7 +140,7 @@ def test_retrieve_cfrags(blockchain_porter,
policy_encrypting_key=enacted_policy.public_key,
threshold=treasure_map.threshold)
assert len(retrieval_results) == 1
field = RetrievalResultSchema()
field = RetrievalOutcomeSchema()
cfrags = field.load(retrieval_results[0])['cfrags']
verified_cfrags = {}
for ursula, cfrag in cfrags.items():
@ -167,16 +178,20 @@ def test_retrieve_cfrags(blockchain_porter,
retrieval_results = response_data['result']['retrieval_results']
assert retrieval_results
assert len(retrieval_results) == 2
for i in range(0, 2):
assert len(retrieval_results[i]["cfrags"]) > 0
assert len(retrieval_results[i]["errors"]) == 0
#
# Try same retrieval (with multiple retrieval kits) using query parameters
# Use context
#
url_retrieve_params = dict(multiple_retrieval_kits_params) # use multiple kit params from above
# adjust parameter for url query parameter list format
url_retrieve_params['retrieval_kits'] = ",".join(url_retrieve_params['retrieval_kits']) # adjust for list
response = blockchain_porter_web_controller.post(f'/retrieve_cfrags'
f'?{urlencode(url_retrieve_params)}')
context_field = JSON()
multiple_retrieval_kits_params['context'] = context_field._serialize(random_context, attr=None, obj=None)
response = blockchain_porter_web_controller.post('/retrieve_cfrags', data=json.dumps(
multiple_retrieval_kits_params))
assert response.status_code == 200
response_data = json.loads(response.data)
retrieval_results = response_data['result']['retrieval_results']
assert retrieval_results

View File

@ -76,3 +76,22 @@ def test_retrieve_cfrags(blockchain_porter,
# use porter
result = blockchain_porter.retrieve_cfrags(**retrieval_args)
assert result
def test_retrieve_cfrags_with_context(blockchain_porter,
random_blockchain_policy,
blockchain_bob,
blockchain_alice,
random_context):
# Setup
network_middleware = MockRestMiddleware()
# enact new random policy since idle_blockchain_policy/enacted_blockchain_policy already modified in previous tests
enacted_policy = random_blockchain_policy.enact(network_middleware=network_middleware)
retrieval_args, _ = retrieval_request_setup(enacted_policy,
blockchain_bob,
blockchain_alice,
context=random_context)
# use porter
result = blockchain_porter.retrieve_cfrags(**retrieval_args)
assert result

View File

@ -65,7 +65,7 @@ MIN_STAKE_FOR_TESTS = NU(750_000, 'NU').to_units()
BONUS_TOKENS_FOR_TESTS = NU(150_000, 'NU').to_units()
DEVELOPMENT_ETH_AIRDROP_AMOUNT = int(Web3().toWei(100, 'ether'))
DEVELOPMENT_ETH_AIRDROP_AMOUNT = int(Web3().to_wei(100, 'ether'))
NUMBER_OF_ALLOCATIONS_IN_TESTS = 50 # TODO: Move to constants

View File

@ -89,7 +89,7 @@ def test_bond_operator(testerchain, threshold_staking, pre_application, applicat
# Staking provider bonds operator and now operator can make a confirmation
tx = pre_application.functions.bondOperator(staking_provider_3, operator1).transact({'from': owner3})
testerchain.wait_for_receipt(tx)
timestamp = testerchain.w3.eth.getBlock('latest').timestamp
timestamp = testerchain.w3.eth.get_block('latest').timestamp
assert pre_application.functions.getOperatorFromStakingProvider(staking_provider_3).call() == operator1
assert pre_application.functions.stakingProviderFromOperator(operator1).call() == staking_provider_3
assert not pre_application.functions.stakingProviderInfo(staking_provider_3).call()[CONFIRMATION_SLOT]
@ -151,7 +151,7 @@ def test_bond_operator(testerchain, threshold_staking, pre_application, applicat
testerchain.time_travel(seconds=min_operator_seconds)
tx = pre_application.functions.bondOperator(staking_provider_3, NULL_ADDRESS).transact({'from': staking_provider_3})
testerchain.wait_for_receipt(tx)
timestamp = testerchain.w3.eth.getBlock('latest').timestamp
timestamp = testerchain.w3.eth.get_block('latest').timestamp
assert pre_application.functions.getOperatorFromStakingProvider(staking_provider_3).call() == NULL_ADDRESS
assert pre_application.functions.stakingProviderFromOperator(staking_provider_3).call() == NULL_ADDRESS
assert pre_application.functions.stakingProviderFromOperator(operator1).call() == NULL_ADDRESS
@ -178,7 +178,7 @@ def test_bond_operator(testerchain, threshold_staking, pre_application, applicat
# The staking provider can bond now a new operator, without waiting additional time.
tx = pre_application.functions.bondOperator(staking_provider_3, operator2).transact({'from': staking_provider_3})
testerchain.wait_for_receipt(tx)
timestamp = testerchain.w3.eth.getBlock('latest').timestamp
timestamp = testerchain.w3.eth.get_block('latest').timestamp
assert pre_application.functions.getOperatorFromStakingProvider(staking_provider_3).call() == operator2
assert pre_application.functions.stakingProviderFromOperator(operator2).call() == staking_provider_3
assert not pre_application.functions.stakingProviderInfo(staking_provider_3).call()[CONFIRMATION_SLOT]
@ -208,7 +208,7 @@ def test_bond_operator(testerchain, threshold_staking, pre_application, applicat
# Another staker can bond a free operator
tx = pre_application.functions.bondOperator(staking_provider_4, operator1).transact({'from': staking_provider_4})
testerchain.wait_for_receipt(tx)
timestamp = testerchain.w3.eth.getBlock('latest').timestamp
timestamp = testerchain.w3.eth.get_block('latest').timestamp
assert pre_application.functions.getOperatorFromStakingProvider(staking_provider_4).call() == operator1
assert pre_application.functions.stakingProviderFromOperator(operator1).call() == staking_provider_4
assert not pre_application.functions.isOperatorConfirmed(operator1).call()
@ -242,7 +242,7 @@ def test_bond_operator(testerchain, threshold_staking, pre_application, applicat
testerchain.time_travel(seconds=min_operator_seconds)
tx = pre_application.functions.bondOperator(staking_provider_4, operator3).transact({'from': staking_provider_4})
testerchain.wait_for_receipt(tx)
timestamp = testerchain.w3.eth.getBlock('latest').timestamp
timestamp = testerchain.w3.eth.get_block('latest').timestamp
assert pre_application.functions.getOperatorFromStakingProvider(staking_provider_4).call() == operator3
assert pre_application.functions.stakingProviderFromOperator(operator3).call() == staking_provider_4
assert pre_application.functions.stakingProviderFromOperator(operator1).call() == NULL_ADDRESS
@ -289,7 +289,7 @@ def test_bond_operator(testerchain, threshold_staking, pre_application, applicat
tx = pre_application.functions.bondOperator(
staking_provider_1, staking_provider_1).transact({'from': staking_provider_1})
testerchain.wait_for_receipt(tx)
timestamp = testerchain.w3.eth.getBlock('latest').timestamp
timestamp = testerchain.w3.eth.get_block('latest').timestamp
assert pre_application.functions.getOperatorFromStakingProvider(staking_provider_1).call() == staking_provider_1
assert pre_application.functions.stakingProviderFromOperator(staking_provider_1).call() == staking_provider_1
assert pre_application.functions.getStakingProvidersLength().call() == 3

View File

@ -308,7 +308,7 @@ def test_withdraw(testerchain, token, worklock, threshold_staking, escrow):
# Set vesting for the staker
tx = threshold_staking.functions.setStakedNu(staking_provider, value // 2).transact()
testerchain.wait_for_receipt(tx)
now = testerchain.w3.eth.getBlock('latest').timestamp
now = testerchain.w3.eth.get_block('latest').timestamp
release_timestamp = now + ONE_HOUR
rate = 2 * value // ONE_HOUR
tx = escrow.functions.setupVesting([staker], [release_timestamp], [rate]).transact({'from': creator})
@ -397,7 +397,7 @@ def test_vesting(testerchain, token, worklock, escrow):
tx = worklock.functions.depositFromWorkLock(staker1, value, 0).transact()
testerchain.wait_for_receipt(tx)
now = testerchain.w3.eth.getBlock('latest').timestamp
now = testerchain.w3.eth.get_block('latest').timestamp
release_timestamp = now + ONE_HOUR
rate = 2 * value // ONE_HOUR
@ -455,7 +455,7 @@ def test_vesting(testerchain, token, worklock, escrow):
assert event_args['releaseRate'] == rate
testerchain.time_travel(seconds=40 * 60)
now = testerchain.w3.eth.getBlock('latest').timestamp
now = testerchain.w3.eth.get_block('latest').timestamp
vested = (release_timestamp - now) * rate
assert escrow.functions.getUnvestedTokens(staker1).call() == vested
@ -476,7 +476,7 @@ def test_vesting(testerchain, token, worklock, escrow):
tx = worklock.functions.depositFromWorkLock(staker4, value, 0).transact()
testerchain.wait_for_receipt(tx)
now = testerchain.w3.eth.getBlock('latest').timestamp + 1 # +1 sec because tx will be executed in new block
now = testerchain.w3.eth.get_block('latest').timestamp + 1 # +1 sec because tx will be executed in new block
release_timestamp_2 = now + ONE_HOUR
release_timestamp_3 = now + 2 * ONE_HOUR
release_timestamp_4 = now + 2 * ONE_HOUR

View File

@ -0,0 +1,119 @@
{
"SubscriptionManagerPayment": {
"contractAddress": "0xaDD9D957170dF6F33982001E4c22eCCdd5539118",
"chain": "polygon",
"method": "isValidPolicy",
"parameters": [
":hrac"
],
"returnValueTest": {
"comparator": "==",
"value": true
}
},
"ERC1155_balance": {
"contractAddress": "0xaDD9D957170dF6F33982001E4c22eCCdd5539118",
"standardContractType": "ERC1155",
"chain": "ethereum",
"method": "balanceOf",
"parameters": [
":userAddress",
9541
],
"returnValueTest": {
"comparator": ">",
"value": 0
}
},
"ERC1155_balance_batch": {
"contractAddress": "0xaDD9D957170dF6F33982001E4c22eCCdd5539118",
"standardContractType": "ERC1155",
"chain": "ethereum",
"method": "balanceOfBatch",
"parameters": [
[":userAddress",":userAddress",":userAddress",":userAddress"],
[1,2,10003,10004]
],
"returnValueTest": {
"comparator": ">",
"value": 0
}
},
"ERC721_ownership": {
"contractAddress": "0xaDD9D957170dF6F33982001E4c22eCCdd5539118",
"standardContractType": "ERC721",
"chain": "ethereum",
"method": "ownerOf",
"parameters": [
5954
],
"returnValueTest": {
"comparator": "=",
"value": ":userAddress"
}
},
"ERC721_balance": {
"contractAddress": "0xaDD9D957170dF6F33982001E4c22eCCdd5539118",
"standardContractType": "ERC721",
"chain": "ethereum",
"method": "balanceOf",
"parameters": [
":userAddress"
],
"returnValueTest": {
"comparator": ">",
"value": 0
}
},
"ERC20_balance": {
"contractAddress": "0xaDD9D957170dF6F33982001E4c22eCCdd5539118",
"standardContractType": "ERC20",
"chain": "ethereum",
"method": "balanceOf",
"parameters": [
":userAddress"
],
"returnValueTest": {
"comparator": ">",
"value": 0
}
},
"ETH_balance": {
"contractAddress": "",
"standardContractType": "",
"chain": "ethereum",
"method": "eth_getBalance",
"parameters": [
":userAddress",
"latest"
],
"returnValueTest": {
"comparator": ">=",
"value": 10000000000000
}
},
"specific_wallet_address": {
"contractAddress": "",
"standardContractType": "",
"chain": "ethereum",
"method": "",
"parameters": [
":userAddress"
],
"returnValueTest": {
"comparator": "=",
"value": "0xaDD9D957170dF6F33982001E4c22eCCdd5539118"
}
},
"timestamp": {
"contractAddress": "",
"standardContractType": "timestamp",
"chain": "ethereum",
"method": "eth_getBlockByNumber",
"parameters": ["latest"],
"returnValueTest": {
"comparator": ">=",
"value": 1234567890
}
}
}

View File

@ -36,20 +36,28 @@ from web3.types import TxReceipt
from nucypher.blockchain.economics import Economics
from nucypher.blockchain.eth.actors import Operator
from nucypher.blockchain.eth.agents import ContractAgency, NucypherTokenAgent, PREApplicationAgent
from nucypher.blockchain.eth.agents import (
ContractAgency,
NucypherTokenAgent,
PREApplicationAgent,
)
from nucypher.blockchain.eth.deployers import (
NucypherTokenDeployer,
PREApplicationDeployer,
SubscriptionManagerDeployer, NucypherTokenDeployer
SubscriptionManagerDeployer,
)
from nucypher.blockchain.eth.interfaces import BlockchainInterfaceFactory
from nucypher.blockchain.eth.registry import InMemoryContractRegistry, LocalContractRegistry
from nucypher.blockchain.eth.registry import (
InMemoryContractRegistry,
LocalContractRegistry,
)
from nucypher.blockchain.eth.signers.software import Web3Signer
from nucypher.blockchain.eth.token import NU
from nucypher.characters.lawful import Enrico
from nucypher.config.characters import (
AliceConfiguration,
BobConfiguration,
UrsulaConfiguration
UrsulaConfiguration,
)
from nucypher.config.constants import TEMPORARY_DOMAIN
from nucypher.control.emitters import StdoutEmitter
@ -57,6 +65,7 @@ from nucypher.crypto.keystore import Keystore
from nucypher.crypto.powers import TransactingPower
from nucypher.datastore import datastore
from nucypher.network.nodes import TEACHER_NODES
from nucypher.policy.conditions.context import USER_ADDRESS_CONTEXT
from nucypher.utilities.logging import GlobalLoggerSettings, Logger
from nucypher.utilities.porter.porter import Porter
from tests.constants import (
@ -65,7 +74,6 @@ from tests.constants import (
BONUS_TOKENS_FOR_TESTS,
DATETIME_FORMAT,
DEVELOPMENT_ETH_AIRDROP_AMOUNT,
DEVELOPMENT_TOKEN_AIRDROP_AMOUNT,
INSECURE_DEVELOPMENT_PASSWORD,
MIN_STAKE_FOR_TESTS,
MOCK_CUSTOM_INSTALLATION_PATH,
@ -73,8 +81,8 @@ from tests.constants import (
MOCK_POLICY_DEFAULT_THRESHOLD,
MOCK_REGISTRY_FILEPATH,
NUMBER_OF_URSULAS_IN_DEVELOPMENT_NETWORK,
TEST_ETH_PROVIDER_URI,
TEST_GAS_LIMIT,
TEST_ETH_PROVIDER_URI
)
from tests.mock.interfaces import MockBlockchain, mock_registry_source_manager
from tests.mock.performance_mocks import (
@ -86,21 +94,24 @@ from tests.mock.performance_mocks import (
mock_record_fleet_state,
mock_remember_node,
mock_rest_app_creation,
mock_verify_node
mock_verify_node,
)
from tests.utils.blockchain import TesterBlockchain, token_airdrop
from tests.utils.config import (
make_alice_test_configuration,
make_bob_test_configuration,
make_ursula_test_configuration
make_ursula_test_configuration,
)
from tests.utils.middleware import (
MockRestMiddleware,
MockRestMiddlewareForLargeFleetTests,
)
from tests.utils.middleware import MockRestMiddleware, MockRestMiddlewareForLargeFleetTests
from tests.utils.policy import generate_random_label
from tests.utils.ursula import (
MOCK_KNOWN_URSULAS_CACHE,
MOCK_URSULA_STARTING_PORT,
make_decentralized_ursulas,
make_federated_ursulas
make_federated_ursulas,
)
test_logger = Logger("test-logger")
@ -294,7 +305,7 @@ def blockchain_treasure_map(enacted_blockchain_policy, blockchain_bob):
def random_blockchain_policy(testerchain, blockchain_alice, blockchain_bob, application_economics):
random_label = generate_random_label()
seconds = 60 * 60 * 24 # TODO This needs to be better thought out...?
now = testerchain.w3.eth.getBlock('latest').timestamp
now = testerchain.w3.eth.get_block('latest').timestamp
expiration = maya.MayaDT(now).add(seconds=seconds)
shares = 3
threshold = 2
@ -544,10 +555,10 @@ def testerchain(_testerchain) -> TesterBlockchain:
if spent > 0:
tx = {'to': address, 'from': coinbase, 'value': spent}
txhash = testerchain.w3.eth.sendTransaction(tx)
txhash = testerchain.w3.eth.send_transaction(tx)
_receipt = testerchain.wait_for_receipt(txhash)
eth_amount = Web3().fromWei(spent, 'ether')
eth_amount = Web3().from_wei(spent, 'ether')
testerchain.log.info("Airdropped {} ETH {} -> {}".format(eth_amount, tx['from'], tx['to']))
BlockchainInterfaceFactory.register_interface(interface=testerchain, force=True)
@ -696,7 +707,7 @@ def blockchain_ursulas(testerchain, staking_providers, ursula_decentralized_test
@pytest.fixture(scope='module')
def policy_rate():
rate = Web3.toWei(21, 'gwei')
rate = Web3.to_wei(21, 'gwei')
return rate
@ -755,9 +766,9 @@ def software_stakeholder(testerchain, agency, stakeholder_config_file_location,
tx = {'to': address,
'from': testerchain.etherbase_account,
'value': Web3.toWei('1', 'ether')}
'value': Web3.to_wei('1', 'ether')}
txhash = testerchain.client.w3.eth.sendTransaction(tx)
txhash = testerchain.client.w3.eth.send_transaction(tx)
_receipt = testerchain.wait_for_receipt(txhash)
# Mock TransactingPower consumption (Etherbase)
@ -798,9 +809,9 @@ def manual_operator(testerchain):
tx = {'to': address,
'from': testerchain.etherbase_account,
'value': Web3.toWei('1', 'ether')}
'value': Web3.to_wei('1', 'ether')}
txhash = testerchain.client.w3.eth.sendTransaction(tx)
txhash = testerchain.client.w3.eth.send_transaction(tx)
_receipt = testerchain.wait_for_receipt(txhash)
yield address
@ -1034,3 +1045,32 @@ def basic_auth_file(temp_dir_path):
f.write("admin:$apr1$hlEpWVoI$0qjykXrvdZ0yO2TnBggQO0\n")
yield basic_auth
basic_auth.unlink()
#
# Condition Context
#
@pytest.fixture(scope='module')
def random_context():
context = {
USER_ADDRESS_CONTEXT: {
"signature": "16b15f88bbd2e0a22d1d0084b8b7080f2003ea83eab1a00f80d8c18446c9c1b6224f17aa09eaf167717ca4f355bb6dc94356e037edf3adf6735a86fc3741f5231b",
"address": "0x03e75d7DD38CCE2e20FfEE35EC914C57780A8e29",
"typedMessage": {
"domain": {
"name": "tDec",
"version": "1",
"chainId": 1,
"salt": "0xf2d857f4a3edcb9b78b4d503bfe733db1e3f6cdc2b7971ee739626c97e86a558",
},
"message": {
"address": "0x03e75d7DD38CCE2e20FfEE35EC914C57780A8e29",
"blockNumber": 15440685,
"blockHash": "0x2220da8b777767df526acffd5375ebb340fc98e53c1040b25ad1a8119829e3bd",
"signatureText": "I'm the owner of address 0x03e75d7dd38cce2e20ffee35ec914c57780a8e29 as of block number 15440685",
},
},
}
}
return context

View File

@ -15,17 +15,13 @@ You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import pytest
import pytest_twisted
from twisted.internet import threads
from nucypher_core import RetrievalKit
import json
from nucypher.characters.lawful import Enrico, Bob
from nucypher.config.constants import TEMPORARY_DOMAIN
from nucypher.network.retrieval import RetrievalClient
from nucypher_core import Address, RetrievalKit, Conditions
from tests.utils.middleware import MockRestMiddleware, NodeIsDownMiddleware
from nucypher.characters.lawful import Enrico
from tests.utils.middleware import NodeIsDownMiddleware
def _policy_info_kwargs(enacted_policy):
@ -35,14 +31,14 @@ def _policy_info_kwargs(enacted_policy):
)
def _make_message_kits(policy_pubkey):
def _make_message_kits(policy_pubkey, conditions=None):
messages = [b"plaintext1", b"plaintext2", b"plaintext3"]
message_kits = []
for message in messages:
# Using different Enricos, because why not.
enrico = Enrico(policy_encrypting_key=policy_pubkey)
message_kit = enrico.encrypt_message(message)
message_kit = enrico.encrypt_message(message, conditions=conditions)
message_kits.append(message_kit)
return messages, message_kits
@ -52,7 +48,7 @@ def test_retrieval_kit(enacted_federated_policy, federated_ursulas):
messages, message_kits = _make_message_kits(enacted_federated_policy.public_key)
capsule = message_kits[0].capsule
addresses = {ursula.canonical_address for ursula in list(federated_ursulas)[:2]}
addresses = {Address(ursula.canonical_address) for ursula in list(federated_ursulas)[:2]}
retrieval_kit = RetrievalKit(capsule, addresses)
serialized = bytes(retrieval_kit)
@ -75,6 +71,34 @@ def test_single_retrieve(enacted_federated_policy, federated_bob, federated_ursu
assert cleartexts == messages
# TODO: MOVE ME
def test_single_retrieve_with_conditions(enacted_federated_policy, federated_bob, federated_ursulas):
from nucypher_core import MessageKit
federated_bob.start_learning_loop()
conditions = [
{'returnValueTest': {'value': '0', 'comparator': '>'}, 'method': 'timelock'},
{'operator': 'and'},
{'returnValueTest': {'value': '99999999999999999', 'comparator': '<'}, 'method': 'timelock'},
]
json_conditions = json.dumps(conditions)
rust_conditions = Conditions(json_conditions)
message_kits = [
MessageKit(
enacted_federated_policy.public_key,
b'lab',
rust_conditions
)
]
cleartexts = federated_bob.retrieve_and_decrypt(
message_kits=message_kits,
**_policy_info_kwargs(enacted_federated_policy),
)
assert b'lab' in cleartexts
def test_use_external_cache(enacted_federated_policy, federated_bob, federated_ursulas):
federated_bob.start_learning_loop()

View File

@ -207,7 +207,7 @@ def test_select_client_account_with_balance_display(mock_stdin,
if show_eth:
balance = mock_testerchain.client.get_balance(account=account)
assert str(Web3.fromWei(balance, 'ether')) in captured.out
assert str(Web3.from_wei(balance, 'ether')) in captured.out
if show_staking:
if len(stake_info) == 0:

View File

@ -14,11 +14,10 @@
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
from base64 import b64decode
import pytest
from nucypher_core import HRAC, TreasureMap
from nucypher_core import Address, HRAC, TreasureMap
from nucypher.crypto.powers import DecryptingPower
@ -35,7 +34,7 @@ def random_federated_treasure_map_data(federated_alice, federated_bob, federated
label=label)
assigned_kfrags = {
ursula.canonical_address: (ursula.public_keys(DecryptingPower), vkfrag)
Address(ursula.canonical_address): (ursula.public_keys(DecryptingPower), vkfrag)
for ursula, vkfrag in zip(list(federated_ursulas)[:shares], kfrags)}
random_treasure_map = TreasureMap(signer=federated_alice.stamp.as_umbral_signer(),

View File

@ -75,7 +75,8 @@ def test_retrieve_cfrags(federated_porter,
enacted_federated_policy,
federated_bob,
federated_alice,
random_federated_treasure_map_data):
random_federated_treasure_map_data,
random_context):
method = 'retrieve_cfrags'
# Setup
@ -97,6 +98,19 @@ def test_retrieve_cfrags(federated_porter,
expected_results = federated_porter.retrieve_cfrags(**retrieve_args)
assert len(retrieval_results) == len(expected_results)
# Use context
retrieve_cfrags_params_with_context, _ = retrieval_request_setup(enacted_federated_policy,
federated_bob,
federated_alice,
context=random_context,
encode_for_rest=True)
request_data = {'method': method, 'params': retrieve_cfrags_params_with_context}
response = federated_porter_rpc_controller.send(request_data)
assert response.success
retrieval_results = response.data['result']['retrieval_results']
assert retrieval_results
# Failure - use encrypted treasure map
failure_retrieve_cfrags_params = dict(retrieve_cfrags_params)
_, random_treasure_map = random_federated_treasure_map_data

View File

@ -18,15 +18,23 @@
import json
from base64 import b64encode
from urllib.parse import urlencode
from nucypher_core import RetrievalKit
from nucypher.characters.lawful import Enrico
from nucypher.control.specifications.fields import JSON
from nucypher.crypto.powers import DecryptingPower
from nucypher.policy.kits import PolicyMessageKit, RetrievalResult
from nucypher.utilities.porter.control.specifications.fields import RetrievalResultSchema, RetrievalKit as RetrievalKitField
from tests.utils.policy import retrieval_request_setup, retrieval_params_decode_from_rest
from nucypher.utilities.porter.control.specifications.fields import (
RetrievalKit as RetrievalKitField,
)
from nucypher.utilities.porter.control.specifications.fields import (
RetrievalOutcomeSchema,
)
from tests.utils.policy import (
retrieval_params_decode_from_rest,
retrieval_request_setup,
)
def test_get_ursulas(federated_porter_web_controller, federated_ursulas):
@ -90,7 +98,8 @@ def test_retrieve_cfrags(federated_porter,
enacted_federated_policy,
federated_bob,
federated_alice,
random_federated_treasure_map_data):
random_federated_treasure_map_data,
random_context):
# Send bad data to assert error return
response = federated_porter_web_controller.post('/retrieve_cfrags', data=json.dumps({'bad': 'input'}))
assert response.status_code == 400
@ -99,11 +108,13 @@ def test_retrieve_cfrags(federated_porter,
original_message = b'The paradox of education is precisely this - that as one begins to become ' \
b'conscious one begins to examine the society in which ' \
b'he is (they are) being educated.' # - James Baldwin
retrieve_cfrags_params, message_kit = retrieval_request_setup(enacted_federated_policy,
retrieve_cfrags_params, message_kits = retrieval_request_setup(enacted_federated_policy,
federated_bob,
federated_alice,
original_message=original_message,
specific_messages=[original_message],
encode_for_rest=True)
assert len(message_kits) == 1
message_kit = message_kits[0]
#
# Success
@ -126,7 +137,7 @@ def test_retrieve_cfrags(federated_porter,
policy_encrypting_key=enacted_federated_policy.public_key,
threshold=treasure_map.threshold)
assert len(retrieval_results) == 1
field = RetrievalResultSchema()
field = RetrievalOutcomeSchema()
cfrags = field.load(retrieval_results[0])['cfrags']
verified_cfrags = {}
for ursula, cfrag in cfrags.items():
@ -168,16 +179,18 @@ def test_retrieve_cfrags(federated_porter,
retrieval_results = response_data['result']['retrieval_results']
assert retrieval_results
assert len(retrieval_results) == 4
for i in range(0, 4):
assert len(retrieval_results[i]["cfrags"]) > 0
assert len(retrieval_results[i]["errors"]) == 0
#
# Try same retrieval (with multiple retrieval kits) using query parameters
# Use context
#
url_retrieve_params = dict(multiple_retrieval_kits_params) # use multiple kit params from above
# adjust parameter for url query parameter list format
url_retrieve_params['retrieval_kits'] = ",".join(url_retrieve_params['retrieval_kits'])
response = federated_porter_web_controller.post(f'/retrieve_cfrags'
f'?{urlencode(url_retrieve_params)}')
context_field = JSON()
multiple_retrieval_kits_params['context'] = context_field._serialize(random_context, attr=None, obj=None)
response = federated_porter_web_controller.post('/retrieve_cfrags', data=json.dumps(multiple_retrieval_kits_params))
assert response.status_code == 200
response_data = json.loads(response.data)
retrieval_results = response_data['result']['retrieval_results']
assert retrieval_results

View File

@ -74,3 +74,18 @@ def test_retrieve_cfrags(federated_porter,
result = federated_porter.retrieve_cfrags(**retrieval_args)
assert result, "valid result returned"
def test_retrieve_cfrags_with_context(federated_porter,
federated_bob,
federated_alice,
enacted_federated_policy,
random_context):
# Setup
retrieval_args, _ = retrieval_request_setup(enacted_federated_policy,
federated_bob,
federated_alice,
context=random_context)
result = federated_porter.retrieve_cfrags(**retrieval_args)
assert result, "valid result returned"

View File

@ -17,15 +17,19 @@
import random
import pytest
from nucypher_core.umbral import SecretKey
from nucypher.characters.control.specifications.fields import Key
from nucypher.control.specifications.exceptions import InvalidArgumentCombo, InvalidInputData
from nucypher.utilities.porter.control.specifications.fields import UrsulaInfoSchema, RetrievalResultSchema
from nucypher.control.specifications.exceptions import (
InvalidArgumentCombo,
InvalidInputData,
)
from nucypher.utilities.porter.control.specifications.fields import (
RetrievalOutcomeSchema,
UrsulaInfoSchema,
)
from nucypher.utilities.porter.control.specifications.porter_schema import (
AliceGetUrsulas,
BobRetrieveCFrags
BobRetrieveCFrags,
)
from nucypher.utilities.porter.porter import Porter
from tests.utils.policy import retrieval_request_setup
@ -163,22 +167,47 @@ def test_alice_revoke():
def test_bob_retrieve_cfrags(federated_porter,
enacted_federated_policy,
federated_bob,
federated_alice):
federated_alice,
random_context,
get_random_checksum_address):
bob_retrieve_cfrags_schema = BobRetrieveCFrags()
# no args
with pytest.raises(InvalidInputData):
bob_retrieve_cfrags_schema.load({})
# Setup
# Setup - no context
retrieval_args, _ = retrieval_request_setup(enacted_federated_policy,
federated_bob,
federated_alice,
encode_for_rest=True)
bob_retrieve_cfrags_schema.load(retrieval_args)
# simple schema load w/ optional context
retrieval_args, _ = retrieval_request_setup(
enacted_federated_policy,
federated_bob,
federated_alice,
encode_for_rest=True,
context=random_context,
)
bob_retrieve_cfrags_schema.load(retrieval_args)
# invalid context specified
retrieval_args, _ = retrieval_request_setup(
enacted_federated_policy,
federated_bob,
federated_alice,
encode_for_rest=True,
context=[1, 2, 3], # list instead of dict
)
with pytest.raises(InvalidInputData):
# invalid context type
bob_retrieve_cfrags_schema.load(retrieval_args)
# missing required argument
updated_data = dict(retrieval_args)
updated_data.pop("context") # context is not a required param
key_to_remove = random.choice(list(updated_data.keys()))
del updated_data[key_to_remove]
with pytest.raises(InvalidInputData):
@ -186,19 +215,114 @@ def test_bob_retrieve_cfrags(federated_porter,
bob_retrieve_cfrags_schema.load(updated_data)
#
# Output i.e. dump
# Retrieval output for 1 retrieval kit
#
non_encoded_retrieval_args, _ = retrieval_request_setup(enacted_federated_policy,
federated_bob,
federated_alice,
encode_for_rest=False)
retrieval_results = federated_porter.retrieve_cfrags(**non_encoded_retrieval_args)
non_encoded_retrieval_args, _ = retrieval_request_setup(
enacted_federated_policy,
federated_bob,
federated_alice,
encode_for_rest=False,
context=random_context,
)
retrieval_outcomes = federated_porter.retrieve_cfrags(**non_encoded_retrieval_args)
expected_retrieval_results_json = []
retrieval_result_schema = RetrievalResultSchema()
retrieval_outcome_schema = RetrievalOutcomeSchema()
for result in retrieval_results:
data = retrieval_result_schema.dump(result)
assert len(retrieval_outcomes) == 1
assert len(retrieval_outcomes[0].cfrags) > 0
assert len(retrieval_outcomes[0].errors) == 0
for outcome in retrieval_outcomes:
data = retrieval_outcome_schema.dump(outcome)
expected_retrieval_results_json.append(data)
output = bob_retrieve_cfrags_schema.dump(obj={'retrieval_results': retrieval_results})
output = bob_retrieve_cfrags_schema.dump(
obj={"retrieval_results": retrieval_outcomes}
)
assert output == {"retrieval_results": expected_retrieval_results_json}
assert len(output["retrieval_results"]) == 1
assert len(output["retrieval_results"][0]["cfrags"]) > 0
assert len(output["retrieval_results"][0]["errors"]) == 0
# now include errors
errors = {
get_random_checksum_address(): "Error Message 1",
get_random_checksum_address(): "Error Message 2",
get_random_checksum_address(): "Error Message 3",
}
new_retrieval_outcome = Porter.RetrievalOutcome(
cfrags=retrieval_outcomes[0].cfrags, errors=errors
)
expected_retrieval_results_json = [
retrieval_outcome_schema.dump(new_retrieval_outcome)
]
output = bob_retrieve_cfrags_schema.dump(
obj={"retrieval_results": [new_retrieval_outcome]}
)
assert output == {"retrieval_results": expected_retrieval_results_json}
assert len(output["retrieval_results"]) == 1
assert len(output["retrieval_results"][0]["cfrags"]) > 0
assert len(output["retrieval_results"][0]["errors"]) == len(errors)
#
# Retrieval output for multiple retrieval kits
#
num_retrieval_kits = 4
non_encoded_retrieval_args, _ = retrieval_request_setup(
enacted_federated_policy,
federated_bob,
federated_alice,
encode_for_rest=False,
context=random_context,
num_random_messages=num_retrieval_kits,
)
retrieval_outcomes = federated_porter.retrieve_cfrags(**non_encoded_retrieval_args)
expected_retrieval_results_json = []
retrieval_outcome_schema = RetrievalOutcomeSchema()
assert len(retrieval_outcomes) == num_retrieval_kits
for i in range(num_retrieval_kits):
assert len(retrieval_outcomes[i].cfrags) > 0
assert len(retrieval_outcomes[i].errors) == 0
for outcome in retrieval_outcomes:
data = retrieval_outcome_schema.dump(outcome)
expected_retrieval_results_json.append(data)
output = bob_retrieve_cfrags_schema.dump(
obj={"retrieval_results": retrieval_outcomes}
)
assert output == {"retrieval_results": expected_retrieval_results_json}
# now include errors
error_message_template = "Retrieval Kit {} - Error Message {}"
new_retrieval_outcomes_with_errors = []
for i in range(num_retrieval_kits):
specific_kit_errors = dict()
for j in range(i):
# different number of errors for each kit; 1 error for kit 1, 2 errors for kit 2 etc.
specific_kit_errors[
get_random_checksum_address()
] = error_message_template.format(i, j)
new_retrieval_outcomes_with_errors.append(
Porter.RetrievalOutcome(
cfrags=retrieval_outcomes[i].cfrags, errors=specific_kit_errors
)
)
expected_retrieval_results_json = []
for outcome in new_retrieval_outcomes_with_errors:
data = retrieval_outcome_schema.dump(outcome)
expected_retrieval_results_json.append(data)
output = bob_retrieve_cfrags_schema.dump(
obj={"retrieval_results": new_retrieval_outcomes_with_errors}
)
assert output == {"retrieval_results": expected_retrieval_results_json}
assert len(output["retrieval_results"]) == num_retrieval_kits
for i in range(num_retrieval_kits):
assert len(output["retrieval_results"][i]["cfrags"]) > 0
# ensures errors are associated appropriately
kit_errors = output["retrieval_results"][i]["errors"]
assert len(kit_errors) == i
values = kit_errors.values() # ordered?
for j in range(i):
assert error_message_template.format(i, j) in values

View File

@ -202,7 +202,7 @@ def estimate_gas(analyzer: AnalyzeGas = None) -> None:
print("********* Estimating Gas *********")
def transact_and_log(label, function, transaction):
estimates = function.estimateGas(transaction)
estimates = function.estimate_gas(transaction)
transaction.update(gas=estimates)
tx = function.transact(transaction)
receipt = testerchain.wait_for_receipt(tx)
@ -288,7 +288,7 @@ def estimate_gas(analyzer: AnalyzeGas = None) -> None:
rate = 100
one_period = economics.hours_per_period * 60 * 60
value = number_of_periods * rate
current_timestamp = testerchain.w3.eth.getBlock('latest').timestamp
current_timestamp = testerchain.w3.eth.get_block('latest').timestamp
end_timestamp = current_timestamp + (number_of_periods - 1) * one_period
transact_and_log("Creating policy (1 node, 10 periods, pre-committed), first",
policy_functions.createPolicy(policy_id_1, alice1, end_timestamp, [staker1]),
@ -363,7 +363,7 @@ def estimate_gas(analyzer: AnalyzeGas = None) -> None:
policy_id_3 = os.urandom(int(POLICY_ID_LENGTH))
number_of_periods = 100
value = 3 * number_of_periods * rate
current_timestamp = testerchain.w3.eth.getBlock('latest').timestamp
current_timestamp = testerchain.w3.eth.get_block('latest').timestamp
end_timestamp = current_timestamp + (number_of_periods - 1) * one_period
transact_and_log("Creating policy (3 nodes, 100 periods, pre-committed), first",
policy_functions.createPolicy(policy_id_1, alice1, end_timestamp, [staker1, staker2, staker3]),
@ -392,13 +392,13 @@ def estimate_gas(analyzer: AnalyzeGas = None) -> None:
policy_id_3 = os.urandom(int(POLICY_ID_LENGTH))
number_of_periods = 100
value = number_of_periods * rate
current_timestamp = testerchain.w3.eth.getBlock('latest').timestamp
current_timestamp = testerchain.w3.eth.get_block('latest').timestamp
end_timestamp = current_timestamp + (number_of_periods - 1) * one_period
transact_and_log("Creating policy (1 node, 100 periods)",
policy_functions.createPolicy(policy_id_1, alice2, end_timestamp, [staker2]),
{'from': alice1, 'value': value})
testerchain.time_travel(periods=1)
current_timestamp = testerchain.w3.eth.getBlock('latest').timestamp
current_timestamp = testerchain.w3.eth.get_block('latest').timestamp
end_timestamp = current_timestamp + (number_of_periods - 1) * one_period
transact_and_log("Creating policy (1 node, 100 periods), next period",
policy_functions.createPolicy(policy_id_2, alice2, end_timestamp, [staker2]),
@ -440,7 +440,7 @@ def estimate_gas(analyzer: AnalyzeGas = None) -> None:
#
policy_id_1 = os.urandom(int(POLICY_ID_LENGTH))
policy_id_2 = os.urandom(int(POLICY_ID_LENGTH))
current_timestamp = testerchain.w3.eth.getBlock('latest').timestamp
current_timestamp = testerchain.w3.eth.get_block('latest').timestamp
end_timestamp = current_timestamp + (number_of_periods - 1) * one_period
value = 3 * number_of_periods * rate
transact_and_log("Creating 2 policies (3 nodes, 100 periods, pre-committed)",

View File

@ -16,11 +16,6 @@
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
from pathlib import Path
from nucypher.blockchain.eth.signers import Signer
from nucypher.network.nodes import TEACHER_NODES
from nucypher.policy.payment import SubscriptionManagerPayment
"""
WARNING: This script makes automatic transactions.
@ -29,22 +24,28 @@ are doing and intend to spend ETH measuring live
policy availability.
"""
import datetime
import maya
import os
import shutil
import time
from eth_typing.evm import ChecksumAddress
from pathlib import Path
from typing import Set, Optional, List, Tuple
import maya
from eth_typing.evm import ChecksumAddress
from nucypher_core.umbral import SecretKey
from web3 import Web3
from web3.types import Wei
from nucypher.blockchain.eth.signers import Signer
from nucypher.characters.lawful import Bob, Ursula, Alice
from nucypher.config.characters import AliceConfiguration
from nucypher.network.nodes import TEACHER_NODES
from nucypher.policy.payment import SubscriptionManagerPayment
from nucypher.policy.policies import Policy
from nucypher.utilities.logging import GlobalLoggerSettings
# Signer Configuration
# In order to use this script, you must configure a wallet for alice
ADDRESS_ENVVAR: str = 'NUCYPHER_GRANT_METRICS_ADDRESS'
@ -76,8 +77,9 @@ INSECURE_PASSWORD: str = "METRICS_INSECURE_DEVELOPMENT_PASSWORD"
TEMP_ALICE_DIR: Path = Path('/', 'tmp', 'grant-metrics')
# Policy Parameters
THRESHOLD: int = 2
SHARES: int = 3
THRESHOLD: int = 1
SHARES: int = 1
RATE: Wei = Web3.to_wei(50, 'gwei')
DURATION: datetime.timedelta = datetime.timedelta(days=1)
# Tuning

View File

@ -33,7 +33,7 @@ MOCK_TESTERCHAIN = MockBlockchain()
CACHED_MOCK_TESTERCHAIN = BlockchainInterfaceFactory.CachedInterface(interface=MOCK_TESTERCHAIN, emitter=None)
BlockchainInterfaceFactory._interfaces[MOCK_ETH_PROVIDER_URI] = CACHED_MOCK_TESTERCHAIN
CURRENT_BLOCK = MOCK_TESTERCHAIN.w3.eth.getBlock('latest')
CURRENT_BLOCK = MOCK_TESTERCHAIN.w3.eth.get_block('latest')
class MockContractAgent:

View File

@ -0,0 +1,150 @@
"""
This file is part of nucypher.
nucypher is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
nucypher is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import json
from pathlib import Path
import pytest
from web3 import Web3
import tests
from nucypher.policy.conditions.context import USER_ADDRESS_CONTEXT
from nucypher.policy.conditions.evm import ContractCondition, RPCCondition
from nucypher.policy.conditions.lingo import AND, OR, ConditionLingo, ReturnValueTest
from nucypher.policy.conditions.time import TimeCondition
VECTORS_FILE = Path(tests.__file__).parent / "data" / "test_conditions.json"
with open(VECTORS_FILE, 'r') as file:
VECTORS = json.loads(file.read())
# ERC1155
@pytest.fixture()
def ERC1155_balance_condition_data():
data = json.dumps(VECTORS['ERC1155_balance'])
return data
@pytest.fixture()
def ERC1155_balance_condition(ERC1155_balance_condition_data):
data = ERC1155_balance_condition_data
condition = ContractCondition.from_json(data)
return condition
# ERC20
@pytest.fixture()
def ERC20_balance_condition_data():
data = json.dumps(VECTORS['ERC20_balance'])
return data
@pytest.fixture()
def ERC20_balance_condition(ERC20_balance_condition_data):
data = ERC20_balance_condition_data
condition = ContractCondition.from_json(data)
return condition
@pytest.fixture
def rpc_condition():
condition = RPCCondition(
method="eth_getBalance",
chain="testerchain",
return_value_test=ReturnValueTest("==", Web3.to_wei(1_000_000, "ether")),
parameters=[USER_ADDRESS_CONTEXT],
)
return condition
@pytest.fixture
def erc20_evm_condition(test_registry):
condition = ContractCondition(
contract_address="0xaDD9D957170dF6F33982001E4c22eCCdd5539118",
method="balanceOf",
standard_contract_type="ERC20",
chain="testerchain",
return_value_test=ReturnValueTest("==", 0),
parameters=[
USER_ADDRESS_CONTEXT,
],
)
return condition
@pytest.fixture
def erc721_evm_condition(test_registry):
condition = ContractCondition(
contract_address="0xaDD9D957170dF6F33982001E4c22eCCdd5539118",
method="ownerOf",
standard_contract_type="ERC721",
chain="testerchain",
return_value_test=ReturnValueTest("==", ":userAddress"),
parameters=[
5954,
]
)
return condition
@pytest.fixture
def sm_condition(test_registry):
zeroized_policy_struct = (
"0x0000000000000000000000000000000000000000",
0,
0,
0,
"0x0000000000000000000000000000000000000000",
)
condition = ContractCondition(
contract_address="0xaDD9D957170dF6F33982001E4c22eCCdd5539118",
method="getPolicy",
chain="testerchain",
function_abi=ABI,
return_value_test=ReturnValueTest("!=", zeroized_policy_struct),
parameters=[
':hrac',
]
)
return condition
@pytest.fixture
def timelock_condition():
condition = TimeCondition(
return_value_test=ReturnValueTest('>', 0)
)
return condition
@pytest.fixture()
def lingo(timelock_condition, rpc_condition, erc20_evm_condition, erc721_evm_condition):
lingo = ConditionLingo(
conditions=[
erc721_evm_condition,
OR,
timelock_condition,
OR,
rpc_condition,
AND,
erc20_evm_condition,
]
)
return lingo

View File

@ -0,0 +1,35 @@
"""
This file is part of nucypher.
nucypher is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
nucypher is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
from nucypher.policy.conditions.lingo import ConditionLingo
def test_compound_condition_timelock():
conditions = [
{
"returnValueTest": {"value": 0, "comparator": ">"},
"method": "timelock"
},
{"operator": "and"},
{
"returnValueTest": {"value": 99999999999999999, "comparator": "<"},
"method": "timelock",
},
]
clingo = ConditionLingo.from_list(conditions)
assert clingo.eval()

View File

@ -0,0 +1,99 @@
"""
This file is part of nucypher.
nucypher is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
nucypher is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import pytest
from nucypher.policy.conditions.base import ReencryptionCondition
from nucypher.policy.conditions.evm import ContractCondition, RPCCondition
from nucypher.policy.conditions.lingo import ReturnValueTest
from nucypher.policy.conditions.time import TimeCondition
def test_invalid_time_condition():
with pytest.raises(ReencryptionCondition.InvalidCondition):
_ = TimeCondition(
return_value_test=ReturnValueTest('>', 0),
method="time_after_time",
)
def test_invalid_rpc_condition():
# no eth_ prefix for method
with pytest.raises(ReencryptionCondition.InvalidCondition):
_ = RPCCondition(
method="no_eth_prefix_eth_getBalance",
chain="testerchain",
return_value_test=ReturnValueTest("==", 0),
parameters=["0xaDD9D957170dF6F33982001E4c22eCCdd5539118"],
)
# non-existent method
with pytest.raises(RPCCondition.InvalidCondition):
_ = RPCCondition(
method="eth_randoMethod",
chain="testerchain",
return_value_test=ReturnValueTest("==", 0),
parameters=["0xaDD9D957170dF6F33982001E4c22eCCdd5539118"],
)
# invalid chain id
with pytest.raises(RPCCondition.InvalidCondition):
_ = RPCCondition(
method="eth_getBalance",
chain="90210", # Beverly Hills Chain :)
return_value_test=ReturnValueTest("==", 0),
parameters=["0xaDD9D957170dF6F33982001E4c22eCCdd5539118"],
)
def test_invalid_contract_condition():
# no abi or contract type
with pytest.raises(ReencryptionCondition.InvalidCondition):
_ = ContractCondition(
contract_address="0xaDD9D957170dF6F33982001E4c22eCCdd5539118",
method="getPolicy",
chain="testerchain",
return_value_test=ReturnValueTest('!=', 0),
parameters=[
':hrac',
]
)
# invalid contract type
with pytest.raises(ReencryptionCondition.InvalidCondition):
_ = ContractCondition(
contract_address="0xaDD9D957170dF6F33982001E4c22eCCdd5539118",
method="getPolicy",
chain="testerchain",
standard_contract_type="ERC90210", # Beverly Hills contract type :)
return_value_test=ReturnValueTest('!=', 0),
parameters=[
':hrac',
]
)
# invalid ABI
with pytest.raises(ReencryptionCondition.InvalidCondition):
_ = ContractCondition(
contract_address="0xaDD9D957170dF6F33982001E4c22eCCdd5539118",
method="getPolicy",
chain="testerchain",
function_abi=["rando ABI"],
return_value_test=ReturnValueTest('!=', 0),
parameters=[
':hrac',
]
)

View File

@ -0,0 +1,64 @@
"""
This file is part of nucypher.
nucypher is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
nucypher is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import json
from nucypher.policy.conditions._utils import _deserialize_condition_lingo
from nucypher.policy.conditions.evm import ContractCondition
from nucypher.policy.conditions.lingo import ConditionLingo
def test_evm_condition_json_serializers(ERC1155_balance_condition_data):
original_data = ERC1155_balance_condition_data
condition = ContractCondition.from_json(original_data)
serialized_data = condition.to_json()
# TODO functionAbi is present in serialized data
deserialized_data = json.loads(serialized_data)
deserialized_data.pop("functionAbi")
assert json.loads(original_data) == deserialized_data
def test_type_resolution_from_json(
timelock_condition, rpc_condition, erc20_evm_condition, erc721_evm_condition
):
conditions = (
timelock_condition,
rpc_condition,
erc20_evm_condition,
erc721_evm_condition,
)
for condition in conditions:
condition_json = condition.to_json()
resolved_condition = _deserialize_condition_lingo(condition_json)
assert isinstance(resolved_condition, type(condition))
def test_conditions_lingo_serialization(lingo):
json_serialized_lingo = json.dumps([l.to_dict() for l in lingo.conditions])
lingo_json = lingo.to_json()
restored_lingo = ConditionLingo.from_json(data=lingo_json)
assert lingo_json == json_serialized_lingo
restored_lingo_json = restored_lingo.to_json()
assert restored_lingo_json == json_serialized_lingo
lingo_b64 = restored_lingo.to_base64()
restored_lingo = ConditionLingo.from_base64(lingo_b64)
# after all the serialization and transformation the content must remain identical
assert restored_lingo.to_json() == lingo_json

View File

@ -0,0 +1,196 @@
"""
This file is part of nucypher.
nucypher is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
nucypher is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import os
import random
import pytest
from nucypher.policy.conditions.lingo import ReturnValueTest
def test_return_value_test_invalid_comparators():
with pytest.raises(ReturnValueTest.InvalidExpression):
_ = ReturnValueTest(comparator="eq", value=1)
with pytest.raises(ReturnValueTest.InvalidExpression):
_ = ReturnValueTest(comparator="=", value=1)
with pytest.raises(ReturnValueTest.InvalidExpression):
_ = ReturnValueTest(comparator="+", value=1)
with pytest.raises(ReturnValueTest.InvalidExpression):
_ = ReturnValueTest(comparator="*", value=1)
with pytest.raises(ReturnValueTest.InvalidExpression):
_ = ReturnValueTest(comparator="/", value=1)
with pytest.raises(ReturnValueTest.InvalidExpression):
_test = ReturnValueTest(comparator="DROP", value=1)
def test_eval_is_evil():
# TODO look into more malicious fuzzing cases
file = f"file_{os.urandom(10).hex()}"
assert not os.path.isfile(file)
with pytest.raises(ReturnValueTest.InvalidExpression):
_test = ReturnValueTest(
comparator=">",
value=f"(lambda: (42, __import__('os').system('touch {file}'))[0])()",
)
assert not os.path.isfile(file)
def test_return_value_test_with_context_variable_cant_run_eval():
# known context variable
test = ReturnValueTest(comparator="==", value=":userAddress")
with pytest.raises(RuntimeError):
test.eval("0xaDD9D957170dF6F33982001E4c22eCCdd5539118")
# fake context variable
test = ReturnValueTest(comparator="==", value=":fakeContextVar")
with pytest.raises(RuntimeError):
test.eval(0)
def test_return_value_test_integer():
# >
test = ReturnValueTest(comparator='>', value='0')
assert test.eval('1')
assert not test.eval('-1')
# mixing types can work
assert test.eval(1)
assert not test.eval(-1)
test = ReturnValueTest(comparator='>', value=0)
assert test.eval(1)
assert not test.eval(0)
assert not test.eval(-1)
# mixed types
assert test.eval("1")
assert not test.eval("0")
# >=
test = ReturnValueTest(comparator=">=", value=1)
assert test.eval(2)
assert test.eval(1)
assert not test.eval(-2)
# <
test = ReturnValueTest(comparator="<", value=2)
assert not test.eval(3)
assert not test.eval(2)
assert test.eval(-3)
# <=
test = ReturnValueTest(comparator="<=", value=-1)
assert not test.eval(3)
assert test.eval(-1)
assert test.eval(-3)
# ==
test = ReturnValueTest(comparator="==", value=4)
assert not test.eval(1)
assert test.eval(4)
assert not test.eval(-2)
# !=
test = ReturnValueTest(comparator="!=", value=20)
assert test.eval(1)
assert not test.eval(20)
assert test.eval(-2)
def test_return_value_test_string():
# string values must be quoted
with pytest.raises(ReturnValueTest.InvalidExpression):
_test = ReturnValueTest(comparator="==", value="foo")
test = ReturnValueTest(comparator='==', value='"foo"')
assert test.eval('"foo"')
assert not test.eval('"bar"')
test = ReturnValueTest(comparator="!=", value='"foo"')
assert not test.eval('"foo"')
assert test.eval('"bar"')
# mixing types works because the value is evaluated as an int, not a string
test = ReturnValueTest(
comparator="==", value="0xaDD9D957170dF6F33982001E4c22eCCdd5539118"
)
assert test.eval(992513598061863249514433594012370110655092723992)
assert test.eval("0xaDD9D957170dF6F33982001E4c22eCCdd5539118")
assert not test.eval("0xdeadbeef")
test = ReturnValueTest(
comparator="!=", value="0xaDD9D957170dF6F33982001E4c22eCCdd5539118"
)
assert not test.eval("0xaDD9D957170dF6F33982001E4c22eCCdd5539118")
assert test.eval("0xdeadbeef")
def test_return_value_test_bool():
test = ReturnValueTest(comparator="==", value=True)
assert test.eval(True)
assert not test.eval(False)
assert test.eval("True")
assert not test.eval("False")
test = ReturnValueTest(comparator="!=", value=False)
assert test.eval(True)
assert not test.eval(False)
assert test.eval("True")
assert not test.eval("False")
test = ReturnValueTest(comparator="==", value="True")
assert test.eval(True)
assert not test.eval(False)
assert test.eval("True")
assert not test.eval("False")
test = ReturnValueTest(comparator="!=", value="False")
assert test.eval(True)
assert not test.eval(False)
assert test.eval("True")
assert not test.eval("False")
@pytest.mark.parametrize(
"test_value",
[
'"foo"', # string
":userAddress", # context variable is an exception case for a string value
"0xaDD9D957170dF6F33982001E4c22eCCdd5539118", # string that is evaluated as int
125, # int
1.223, # float
os.urandom(16), # bytes
True, # bool
"True", # bool as string
(1, True, "love"), # tuple
["a", "b", "c"], # list
{"name": "John", "age": 22}, # dict
{True, False}, # set
],
)
def test_return_value_serialization(test_value):
schema = ReturnValueTest.ReturnValueTestSchema()
comparator = random.choice(ReturnValueTest.COMPARATORS)
test = ReturnValueTest(comparator=comparator, value=test_value)
reloaded = schema.load(schema.dump(test))
assert (test.comparator == reloaded.comparator) and (
test.value == reloaded.value
), f"test for '{comparator} {test_value}'"

View File

@ -39,7 +39,7 @@ def receipt():
def test_check_transaction_is_on_chain(mocker, mock_ethereum_client, receipt):
# Mocking Web3 and EthereumClient
web3_mock = mock_ethereum_client.w3
web3_mock.eth.getTransactionReceipt = mocker.Mock(return_value=receipt)
web3_mock.eth.get_transaction_receipt = mocker.Mock(return_value=receipt)
# Test with no chain reorganizations:
@ -52,7 +52,7 @@ def test_check_transaction_is_on_chain(mocker, mock_ethereum_client, receipt):
new_receipt = dict(receipt)
new_receipt.update({'blockHash': HexBytes('0xBebeCebada')})
web3_mock.eth.getTransactionReceipt = mocker.Mock(return_value=new_receipt)
web3_mock.eth.get_transaction_receipt = mocker.Mock(return_value=new_receipt)
exception = mock_ethereum_client.ChainReorganizationDetected
message = exception(receipt=receipt).message
@ -60,18 +60,19 @@ def test_check_transaction_is_on_chain(mocker, mock_ethereum_client, receipt):
_ = mock_ethereum_client.check_transaction_is_on_chain(receipt=receipt)
# Another example: there has been a chain reorganization and our beloved TX is gone for good:
web3_mock.eth.getTransactionReceipt = mocker.Mock(side_effect=TransactionNotFound)
web3_mock.eth.get_transaction_receipt = mocker.Mock(side_effect=TransactionNotFound)
with pytest.raises(exception, match=message):
_ = mock_ethereum_client.check_transaction_is_on_chain(receipt=receipt)
@pytest.mark.skip("Needs fix for web3.py v6+")
def test_block_until_enough_confirmations(mocker, mock_ethereum_client, receipt):
my_tx_hash = receipt['transactionHash']
block_number_of_my_tx = receipt['blockNumber']
# Test that web3's TimeExhausted is propagated:
web3_mock = mock_ethereum_client.w3
web3_mock.eth.waitForTransactionReceipt = mocker.Mock(side_effect=TimeExhausted)
web3_mock.eth.wait_for_transaction_receipt = mocker.Mock(side_effect=TimeExhausted)
with pytest.raises(TimeExhausted):
mock_ethereum_client.block_until_enough_confirmations(transaction_hash=my_tx_hash,
@ -80,10 +81,10 @@ def test_block_until_enough_confirmations(mocker, mock_ethereum_client, receipt)
# Test that NotEnoughConfirmations is raised when there are not enough confirmations.
# In this case, we're going to mock eth.blockNumber to be stuck
web3_mock.eth.waitForTransactionReceipt = mocker.Mock(return_value=receipt)
web3_mock.eth.getTransactionReceipt = mocker.Mock(return_value=receipt)
web3_mock.eth.wait_for_transaction_receipt = mocker.Mock(return_value=receipt)
web3_mock.eth.get_transaction_receipt = mocker.Mock(return_value=receipt)
type(web3_mock.eth).blockNumber = PropertyMock(return_value=block_number_of_my_tx) # See docs of PropertyMock
type(web3_mock.eth).block_number = PropertyMock(return_value=block_number_of_my_tx) # See docs of PropertyMock
# Additional adjustments to make the test faster
mocker.patch.object(mock_ethereum_client, '_calculate_confirmations_timeout', return_value=0.1)
@ -112,20 +113,20 @@ def test_wait_for_receipt_no_confirmations(mocker, mock_ethereum_client, receipt
# Test that web3's TimeExhausted is propagated:
web3_mock = mock_ethereum_client.w3
web3_mock.eth.waitForTransactionReceipt = mocker.Mock(side_effect=TimeExhausted)
web3_mock.eth.wait_for_transaction_receipt = mocker.Mock(side_effect=TimeExhausted)
with pytest.raises(TimeExhausted):
_ = mock_ethereum_client.wait_for_receipt(transaction_hash=my_tx_hash, timeout=1, confirmations=0)
web3_mock.eth.waitForTransactionReceipt.assert_called_once_with(transaction_hash=my_tx_hash,
timeout=1,
poll_latency=MockEthereumClient.TRANSACTION_POLLING_TIME)
web3_mock.eth.wait_for_transaction_receipt.assert_called_once_with(transaction_hash=my_tx_hash,
timeout=1,
poll_latency=MockEthereumClient.TRANSACTION_POLLING_TIME)
# Test that when web3's layer returns the receipt, we get that receipt
web3_mock.eth.waitForTransactionReceipt = mocker.Mock(return_value=receipt)
web3_mock.eth.wait_for_transaction_receipt = mocker.Mock(return_value=receipt)
returned_receipt = mock_ethereum_client.wait_for_receipt(transaction_hash=my_tx_hash, timeout=1, confirmations=0)
assert receipt == returned_receipt
web3_mock.eth.waitForTransactionReceipt.assert_called_once_with(transaction_hash=my_tx_hash,
timeout=1,
poll_latency=MockEthereumClient.TRANSACTION_POLLING_TIME)
web3_mock.eth.wait_for_transaction_receipt.assert_called_once_with(transaction_hash=my_tx_hash,
timeout=1,
poll_latency=MockEthereumClient.TRANSACTION_POLLING_TIME)
def test_wait_for_receipt_with_confirmations(mocker, mock_ethereum_client, receipt):

View File

@ -59,7 +59,7 @@ def test_use_pending_nonce_when_building_payload(mock_testerchain, mocker):
def mock_get_transaction_count(sender, block_identifier) -> int:
return transaction_count[block_identifier]
mock_testerchain.client.w3.eth.getTransactionCount = mocker.Mock(side_effect=mock_get_transaction_count)
mock_testerchain.client.w3.eth.get_transaction_count = mocker.Mock(side_effect=mock_get_transaction_count)
def simulate_successful_transaction():
transaction_count['pending'] += 1

View File

@ -14,12 +14,19 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import json
from base64 import b64encode
import pytest
from nucypher.control.specifications.exceptions import InvalidInputData
from nucypher.control.specifications.fields import PositiveInteger, StringList, String, Base64BytesRepresentation
from nucypher.control.specifications.fields import (
JSON,
Base64BytesRepresentation,
PositiveInteger,
String,
StringList,
)
def test_positive_integer_field():
@ -58,6 +65,69 @@ def test_base64_representation_field():
deserialized = field._deserialize(value=serialized, attr=None, data=None)
assert deserialized == data
with pytest.raises(InvalidInputData):
# attempt to serialize a non-serializable object
field._serialize(value=Exception("non-serializable"), attr=None, obj=None)
with pytest.raises(InvalidInputData):
# attempt to deserialize none base64 data
field._deserialize(value=b"raw bytes with non base64 chars ?&^%", attr=None, data=None)
def test_json_field():
# test data
dict_data = {
"domain": {"name": "tdec", "version": 1, "chainId": 1, "salt": "blahblahblah"},
"message": {
"address": "0x03e75d7dd38cce2e20ffee35ec914c57780a8e29",
"blockNumber": 15440685,
"blockHash": "0x2220da8b777767df526acffd5375ebb340fc98e53c1040b25ad1a8119829e3bd",
},
}
list_data = [12.5, 1.2, 4.3]
str_data = "Everything in the universe has a rhythm, everything dances." # -- Maya Angelou
num_data = 1234567890
bool_data = True
float_data = 2.35
# test serialization/deserialization of data - no expected type specified
test_data = [dict_data, list_data, str_data, num_data, bool_data, float_data]
field = JSON()
for d in test_data:
serialized = field._serialize(value=d, attr=None, obj=None)
assert serialized == json.dumps(d)
deserialized = field._deserialize(value=serialized, attr=None, data=None)
assert deserialized == d
with pytest.raises(InvalidInputData):
# attempt to serialize non-json serializable object
field._serialize(value=Exception("non-serializable"), attr=None, obj=None)
with pytest.raises(InvalidInputData):
# attempt to deserialize invalid data
field._deserialize(
value=b"raw bytes", attr=None, data=None
)
# test expected type enforcement
test_types = [type(d) for d in test_data]
for expected_type in test_types:
field = JSON(expected_type=expected_type)
for d in test_data:
if type(d) == expected_type:
# serialization/deserialization should work
serialized = field._serialize(value=d, attr=None, obj=None)
assert serialized == json.dumps(d)
deserialized = field._deserialize(value=serialized, attr=None, data=None)
assert deserialized == d
else:
# serialization/deserialization should fail
with pytest.raises(InvalidInputData):
# attempt to serialize non-json serializable object
field._serialize(d, attr=None, obj=None)
with pytest.raises(InvalidInputData):
# attempt to deserialize invalid data
field._deserialize(value=json.dumps(d), attr=None, data=None)

View File

@ -159,10 +159,10 @@ def test_etherchain():
with patch.object(feed, '_probe_feed'):
feed._raw_data = etherchain_json
assert feed.get_gas_price('safeLow') == Web3.toWei(99.0, 'gwei')
assert feed.get_gas_price('standard') == Web3.toWei(105.0, 'gwei')
assert feed.get_gas_price('fast') == Web3.toWei(108.0, 'gwei')
assert feed.get_gas_price('fastest') == Web3.toWei(119.9, 'gwei')
assert feed.get_gas_price('safeLow') == Web3.to_wei(99.0, 'gwei')
assert feed.get_gas_price('standard') == Web3.to_wei(105.0, 'gwei')
assert feed.get_gas_price('fast') == Web3.to_wei(108.0, 'gwei')
assert feed.get_gas_price('fastest') == Web3.to_wei(119.9, 'gwei')
assert feed.get_gas_price() == feed.get_gas_price('fast') # Default
parsed_gas_prices = feed.gas_prices
@ -170,7 +170,7 @@ def test_etherchain():
EtherchainGasPriceDatafeed.gas_prices = dict()
with patch.dict(EtherchainGasPriceDatafeed.gas_prices, values=parsed_gas_prices):
gas_strategy = feed.construct_gas_strategy()
assert gas_strategy("web3", "tx") == Web3.toWei(108.0, 'gwei')
assert gas_strategy("web3", "tx") == Web3.to_wei(108.0, 'gwei')
def test_upvest():
@ -181,10 +181,10 @@ def test_upvest():
with patch.object(feed, '_probe_feed'):
feed._raw_data = upvest_json
assert feed.get_gas_price('slow') == Web3.toWei(87.19, 'gwei')
assert feed.get_gas_price('medium') == Web3.toWei(91.424, 'gwei')
assert feed.get_gas_price('fast') == Web3.toWei(97.158, 'gwei')
assert feed.get_gas_price('fastest') == Web3.toWei(105.2745, 'gwei')
assert feed.get_gas_price('slow') == Web3.to_wei(87.19, 'gwei')
assert feed.get_gas_price('medium') == Web3.to_wei(91.424, 'gwei')
assert feed.get_gas_price('fast') == Web3.to_wei(97.158, 'gwei')
assert feed.get_gas_price('fastest') == Web3.to_wei(105.2745, 'gwei')
assert feed.get_gas_price() == feed.get_gas_price('fastest') # Default
parsed_gas_prices = feed.gas_prices
@ -192,7 +192,7 @@ def test_upvest():
UpvestGasPriceDatafeed.gas_prices = dict()
with patch.dict(UpvestGasPriceDatafeed.gas_prices, values=parsed_gas_prices):
gas_strategy = feed.construct_gas_strategy()
assert gas_strategy("web3", "tx") == Web3.toWei(105.2745, 'gwei')
assert gas_strategy("web3", "tx") == Web3.to_wei(105.2745, 'gwei')
def test_zoltu():
@ -203,10 +203,10 @@ def test_zoltu():
with patch.object(feed, '_probe_feed'):
feed._raw_data = zoltu_json
assert feed.get_gas_price('slow') == Web3.toWei(41, 'gwei')
assert feed.get_gas_price('medium') == Web3.toWei(58, 'gwei')
assert feed.get_gas_price('fast') == Web3.toWei(67, 'gwei')
assert feed.get_gas_price('fastest') == Web3.toWei(70, 'gwei')
assert feed.get_gas_price('slow') == Web3.to_wei(41, 'gwei')
assert feed.get_gas_price('medium') == Web3.to_wei(58, 'gwei')
assert feed.get_gas_price('fast') == Web3.to_wei(67, 'gwei')
assert feed.get_gas_price('fastest') == Web3.to_wei(70, 'gwei')
assert feed.get_gas_price() == feed.get_gas_price('fast') # Default
parsed_gas_prices = feed.gas_prices
@ -214,7 +214,7 @@ def test_zoltu():
ZoltuGasPriceDatafeed.gas_prices = dict()
with patch.dict(ZoltuGasPriceDatafeed.gas_prices, values=parsed_gas_prices):
gas_strategy = feed.construct_gas_strategy()
assert gas_strategy("web3", "tx") == Web3.toWei(67, 'gwei')
assert gas_strategy("web3", "tx") == Web3.to_wei(67, 'gwei')
def test_datafeed_median_gas_price_strategy():

View File

@ -14,11 +14,13 @@
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
from pathlib import Path
import pytest
from eth_utils import to_checksum_address
from nucypher_core import NodeMetadata, NodeMetadataPayload
from nucypher_core import Address, NodeMetadata, NodeMetadataPayload
from nucypher_core.umbral import SecretKey, Signer
from nucypher.acumen.perception import FleetSensor
@ -78,7 +80,7 @@ class Dummy: # Teacher
# A dummy signature with the recovery byte
dummy_signature = bytes(signer.sign(b'whatever')) + b'\x00'
payload = NodeMetadataPayload(staking_provider_address=self.canonical_address,
payload = NodeMetadataPayload(staking_provider_address=Address(self.canonical_address),
domain=':dummy:',
timestamp_epoch=0,
operator_signature=dummy_signature,

View File

@ -46,9 +46,9 @@ def test_fixed_price_gas_strategy():
def test_max_price_gas_strategy(mocker, monkeypatch):
gas_prices_gwei = [10, 100, 999, 1000, 1001, 1_000_000, 1_000_000_000]
gas_prices_wei = [Web3.toWei(gwei_price, 'gwei') for gwei_price in gas_prices_gwei]
gas_prices_wei = [Web3.to_wei(gwei_price, 'gwei') for gwei_price in gas_prices_gwei]
max_gas_price_gwei = 1000
max_gas_price_wei = Web3.toWei(max_gas_price_gwei, 'gwei')
max_gas_price_wei = Web3.to_wei(max_gas_price_gwei, 'gwei')
mock_gas_strategy = mocker.Mock(side_effect=itertools.cycle(gas_prices_wei))
wrapped_strategy = max_price_gas_strategy_wrapper(gas_strategy=mock_gas_strategy,

View File

@ -21,7 +21,7 @@ from eth_utils import to_canonical_address
import pytest
from nucypher_core import RetrievalKit as RetrievalKitClass, MessageKit
from nucypher_core import RetrievalKit as RetrievalKitClass, Address, MessageKit
from nucypher_core.umbral import SecretKey
from nucypher.control.specifications.exceptions import InvalidInputData
@ -106,11 +106,11 @@ def test_retrieval_kit_field(get_random_checksum_address):
encrypting_key = SecretKey.random().public_key()
capsule = MessageKit(encrypting_key, b'testing retrieval kit with 2 ursulas').capsule
ursulas = [get_random_checksum_address(), get_random_checksum_address()]
run_tests_on_kit(kit=RetrievalKitClass(capsule, {to_canonical_address(ursula) for ursula in ursulas}))
run_tests_on_kit(kit=RetrievalKitClass(capsule, {Address(to_canonical_address(ursula)) for ursula in ursulas}))
# kit with no ursulas
encrypting_key = SecretKey.random().public_key()
capsule = MessageKit(encrypting_key, b'testing retrieval kit with no ursulas').capsule
capsule = MessageKit(policy_encrypting_key=encrypting_key, plaintext=b'testing retrieval kit with no ursulas').capsule
run_tests_on_kit(kit=RetrievalKitClass(capsule, set()))
with pytest.raises(InvalidInputData):

View File

@ -68,8 +68,8 @@ class SyncedMockW3Eth:
# Support older and newer versions of web3 py in-test
version = 5
chainId = hex(5)
blockNumber = 5
chain_id = hex(5)
block_number = 5
def getBlock(self, blockNumber):
return {
@ -122,7 +122,7 @@ class SyncedMockWeb3:
return self.provider.clientVersion
@property
def isConnected(self):
def is_connected(self):
return lambda: True
@ -322,8 +322,8 @@ def test_ganache_web3_client():
def test_gas_prices(mocker, mock_ethereum_client):
web3_mock = mock_ethereum_client.w3
web3_mock.eth.generateGasPrice = mocker.Mock(side_effect=[None, GAS_PRICE_FROM_STRATEGY])
type(web3_mock.eth).gasPrice = PropertyMock(return_value=DEFAULT_GAS_PRICE) # See docs of PropertyMock
web3_mock.eth.generate_gas_price = mocker.Mock(side_effect=[None, GAS_PRICE_FROM_STRATEGY])
type(web3_mock.eth).gas_price = PropertyMock(return_value=DEFAULT_GAS_PRICE) # See docs of PropertyMock
assert mock_ethereum_client.gas_price == DEFAULT_GAS_PRICE
assert mock_ethereum_client.gas_price_for_transaction("there's no gas strategy") == DEFAULT_GAS_PRICE

View File

@ -69,7 +69,7 @@ def token_airdrop(token_agent, amount: NU, transacting_power: TransactingPower,
def free_gas_price_strategy(w3, transaction_params=None):
return 0
return None
class TesterBlockchain(BlockchainDeployerInterface):
@ -173,11 +173,11 @@ class TesterBlockchain(BlockchainDeployerInterface):
tx_hashes = list()
for address in addresses:
tx = {'to': address, 'from': coinbase, 'value': amount, 'gasPrice': self.w3.eth.generate_gas_price()}
txhash = self.w3.eth.sendTransaction(tx)
txhash = self.w3.eth.send_transaction(tx)
_receipt = self.wait_for_receipt(txhash)
tx_hashes.append(txhash)
eth_amount = Web3().fromWei(amount, 'ether')
eth_amount = Web3().from_wei(amount, 'ether')
self.log.info("Airdropped {} ETH {} -> {}".format(eth_amount, tx['from'], tx['to']))
return tx_hashes
@ -203,11 +203,11 @@ class TesterBlockchain(BlockchainDeployerInterface):
else:
raise ValueError("Specify either hours, or seconds.")
now = self.w3.eth.getBlock('latest').timestamp
now = self.w3.eth.get_block('latest').timestamp
end_timestamp = ((now+duration)//base) * base
self.w3.eth.web3.testing.timeTravel(timestamp=end_timestamp)
self.w3.eth.web3.testing.mine(1)
self.w3.eth.w3.testing.timeTravel(timestamp=end_timestamp)
self.w3.eth.w3.testing.mine(1)
delta = maya.timedelta(seconds=end_timestamp-now)
self.log.info(f"Time traveled {delta} "

View File

@ -18,14 +18,17 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
import os
import random
import string
from typing import Dict, Tuple
from typing import Dict, List, Optional, Tuple
from nucypher_core import MessageKit, RetrievalKit
from nucypher.characters.control.specifications.fields import Key, TreasureMap
from nucypher.characters.lawful import Enrico
from nucypher.control.specifications.fields import JSON
from nucypher.crypto.powers import DecryptingPower
from nucypher.utilities.porter.control.specifications.fields import RetrievalKit as RetrievalKitField
from nucypher.utilities.porter.control.specifications.fields import (
RetrievalKit as RetrievalKitField,
)
def generate_random_label() -> bytes:
@ -41,33 +44,82 @@ def generate_random_label() -> bytes:
return bytes(random_label, encoding='utf-8')
def retrieval_request_setup(enacted_policy, bob, alice, original_message: bytes = None, encode_for_rest: bool = False) -> Tuple[Dict, MessageKit]:
treasure_map = bob._decrypt_treasure_map(enacted_policy.treasure_map,
enacted_policy.publisher_verifying_key)
def retrieval_request_setup(enacted_policy,
bob,
alice,
specific_messages: Optional[List[bytes]] = None,
context: Optional[Dict] = None,
encode_for_rest: bool = False,
num_random_messages: int = None) -> Tuple[Dict, List[MessageKit]]:
"""
Creates dict of values for a retrieval request. If no specific messages or number
of random messages are provided, a single random message is encrypted.
"""
if specific_messages and num_random_messages is not None:
raise ValueError(
"Provide either original_message or num_random_messages parameter, not both."
)
if not specific_messages and num_random_messages is None:
# default to one random message
num_random_messages = 1
treasure_map = bob._decrypt_treasure_map(
enacted_policy.treasure_map, enacted_policy.publisher_verifying_key
)
# We pick up our story with Bob already having followed the treasure map above, ie:
bob.start_learning_loop()
# We can pass any number of capsules as args; here we pass just one.
enrico = Enrico(policy_encrypting_key=enacted_policy.public_key)
if not original_message:
original_message = ''.join(random.choice(string.ascii_lowercase) for i in range(20)).encode() # random message
message_kit = enrico.encrypt_message(original_message)
message_kits = []
if specific_messages:
for message in specific_messages:
message_kits.append(enrico.encrypt_message(message))
else:
for i in range(num_random_messages):
random_message = "".join(
random.choice(string.ascii_lowercase) for j in range(20)
).encode() # random message
message_kits.append(enrico.encrypt_message(random_message))
encode_bytes = (lambda field, obj: field()._serialize(value=obj, attr=None, obj=None)) if encode_for_rest else (lambda field, obj: obj)
return (dict(treasure_map=encode_bytes(TreasureMap, treasure_map),
retrieval_kits=[encode_bytes(RetrievalKitField, RetrievalKit.from_message_kit(message_kit))],
alice_verifying_key=encode_bytes(Key, alice.stamp.as_umbral_pubkey()),
bob_encrypting_key=encode_bytes(Key, bob.public_keys(DecryptingPower)),
bob_verifying_key=encode_bytes(Key, bob.stamp.as_umbral_pubkey())),
message_kit)
retrieval_params = dict(
treasure_map=encode_bytes(TreasureMap, treasure_map),
retrieval_kits=[
encode_bytes(RetrievalKitField, RetrievalKit.from_message_kit(message_kit))
for message_kit in message_kits
],
alice_verifying_key=encode_bytes(Key, alice.stamp.as_umbral_pubkey()),
bob_encrypting_key=encode_bytes(Key, bob.public_keys(DecryptingPower)),
bob_verifying_key=encode_bytes(Key, bob.stamp.as_umbral_pubkey()),
)
# context is optional
if context:
retrieval_params["context"] = encode_bytes(JSON, context)
return retrieval_params, message_kits
def retrieval_params_decode_from_rest(retrieval_params: Dict) -> Dict:
decode_bytes = (lambda field, data: field()._deserialize(value=data, attr=None, data=None))
return dict(treasure_map=decode_bytes(TreasureMap, retrieval_params['treasure_map']),
retrieval_kits=[decode_bytes(RetrievalKitField, kit) for kit in retrieval_params['retrieval_kits']],
alice_verifying_key=decode_bytes(Key, retrieval_params['alice_verifying_key']),
bob_encrypting_key=decode_bytes(Key, retrieval_params['bob_encrypting_key']),
bob_verifying_key=decode_bytes(Key, retrieval_params['bob_verifying_key']))
decode_bytes = lambda field, data: field()._deserialize(
value=data, attr=None, data=None
)
decoded_params = dict(
treasure_map=decode_bytes(TreasureMap, retrieval_params["treasure_map"]),
retrieval_kits=[
decode_bytes(RetrievalKitField, kit)
for kit in retrieval_params["retrieval_kits"]
],
alice_verifying_key=decode_bytes(Key, retrieval_params["alice_verifying_key"]),
bob_encrypting_key=decode_bytes(Key, retrieval_params["bob_encrypting_key"]),
bob_verifying_key=decode_bytes(Key, retrieval_params["bob_verifying_key"]),
)
# context is optional
if "context" in retrieval_params:
decoded_params["context"] = decode_bytes(
JSON, retrieval_params["context"]
)
return decoded_params