From 4cca6dbed6815424a2e23bf6bed24e9c283a1bb0 Mon Sep 17 00:00:00 2001 From: "Kieran R. Prasch" Date: Wed, 23 Feb 2022 13:06:46 -0800 Subject: [PATCH 01/19] Ensure eth provider and registry are passed correctly to seperate L1/L2. --- nucypher/blockchain/eth/actors.py | 8 ++++++-- nucypher/blockchain/eth/agents.py | 14 ++++++++------ nucypher/characters/lawful.py | 4 +++- nucypher/policy/payment.py | 17 ++++++++++++----- 4 files changed, 29 insertions(+), 14 deletions(-) diff --git a/nucypher/blockchain/eth/actors.py b/nucypher/blockchain/eth/actors.py index 72c050cff..1bbb1a13b 100644 --- a/nucypher/blockchain/eth/actors.py +++ b/nucypher/blockchain/eth/actors.py @@ -383,9 +383,13 @@ class Operator(BaseActor): class BlockchainPolicyAuthor(NucypherTokenActor): """Alice base class for blockchain operations, mocking up new policies!""" - def __init__(self, *args, **kwargs): + def __init__(self, eth_provider_uri, *args, **kwargs): super().__init__(*args, **kwargs) - self.application_agent = ContractAgency.get_agent(PREApplicationAgent, registry=self.registry) + self.application_agent = ContractAgency.get_agent( + PREApplicationAgent, + registry=self.registry, + eth_provider_uri=eth_provider_uri + ) def create_policy(self, *args, **kwargs): """Hence the name, a BlockchainPolicyAuthor can create a BlockchainPolicy with themself as the author.""" diff --git a/nucypher/blockchain/eth/agents.py b/nucypher/blockchain/eth/agents.py index 790b7844b..de3cf567e 100644 --- a/nucypher/blockchain/eth/agents.py +++ b/nucypher/blockchain/eth/agents.py @@ -92,7 +92,7 @@ class EthereumContractAgent: contract_version: Optional[str] = None): self.log = Logger(self.__class__.__name__) - self.registry_str = str(registry) + self.registry = registry self.blockchain = BlockchainInterfaceFactory.get_or_create_interface(eth_provider_uri=eth_provider_uri) @@ -111,15 +111,17 @@ class EthereumContractAgent: transaction_gas = EthereumContractAgent.DEFAULT_TRANSACTION_GAS_LIMITS['default'] self.transaction_gas = transaction_gas - self.log.info("Initialized new {} for {} with {} and {}".format(self.__class__.__name__, - self.contract.address, - self.blockchain.eth_provider_uri, - self.registry_str)) + self.log.info("Initialized new {} for {} with {} and {}".format( + self.__class__.__name__, + self.contract.address, + self.blockchain.eth_provider_uri, + str(self.registry) + )) def __repr__(self) -> str: class_name = self.__class__.__name__ r = "{}(registry={}, contract={})" - return r.format(class_name, self.registry_str, self.contract_name) + return r.format(class_name, str(self.registry), self.contract_name) def __eq__(self, other: Any) -> bool: return bool(self.contract.address == other.contract.address) diff --git a/nucypher/characters/lawful.py b/nucypher/characters/lawful.py index 565c91655..ca5b772bb 100644 --- a/nucypher/characters/lawful.py +++ b/nucypher/characters/lawful.py @@ -169,7 +169,8 @@ class Alice(Character, BlockchainPolicyAuthor): BlockchainPolicyAuthor.__init__(self, domain=self.domain, transacting_power=self.transacting_power, - registry=self.registry) + registry=self.registry, + eth_provider_uri=eth_provider_uri) self.log = Logger(self.__class__.__name__) if is_me: @@ -572,6 +573,7 @@ class Bob(Character): if not publisher_verifying_key: publisher_verifying_key = alice_verifying_key + publisher_verifying_key = PublicKey.from_bytes(bytes(publisher_verifying_key)) # A small optimization to avoid multiple treasure map decryptions. map_hash = hash(bytes(encrypted_treasure_map)) diff --git a/nucypher/policy/payment.py b/nucypher/policy/payment.py index 7e489f8d4..320cc20a0 100644 --- a/nucypher/policy/payment.py +++ b/nucypher/policy/payment.py @@ -15,15 +15,16 @@ along with nucypher. If not, see . """ + from abc import ABC, abstractmethod from typing import Optional, NamedTuple, Dict import maya from nucypher_core import ReencryptionRequest -from web3.types import Wei, ChecksumAddress, Timestamp, TxReceipt +from web3.types import Wei, Timestamp, TxReceipt, ChecksumAddress from nucypher.blockchain.eth.agents import SubscriptionManagerAgent -from nucypher.blockchain.eth.registry import InMemoryContractRegistry +from nucypher.blockchain.eth.registry import InMemoryContractRegistry, BaseContractRegistry from nucypher.policy.policies import BlockchainPolicy, Policy @@ -91,11 +92,17 @@ class ContractPayment(PaymentMethod, ABC): rate: Wei value: Wei - def __init__(self, provider: str, network: str, *args, **kwargs): + def __init__(self, + eth_provider: str, + network: str, + registry: Optional[BaseContractRegistry] = None, + *args, **kwargs): super().__init__(*args, **kwargs) - self.provider = provider + self.provider = eth_provider self.network = network - self.registry = InMemoryContractRegistry.from_latest_publication(network=network) + if not registry: + registry = InMemoryContractRegistry.from_latest_publication(network=network) + self.registry = registry self.__agent = None # delay blockchain/registry reads until later @property From d206cbd8729bd8f2c6886a2a13cc38d75234bb5e Mon Sep 17 00:00:00 2001 From: "Kieran R. Prasch" Date: Wed, 23 Feb 2022 13:10:23 -0800 Subject: [PATCH 02/19] temporary inclusion of latest SubscriptionManager contract source. --- .../source/contracts/SubscriptionManager.sol | 47 ++++++++++++++----- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/nucypher/blockchain/eth/sol/source/contracts/SubscriptionManager.sol b/nucypher/blockchain/eth/sol/source/contracts/SubscriptionManager.sol index d37db011b..c83ee1efc 100644 --- a/nucypher/blockchain/eth/sol/source/contracts/SubscriptionManager.sol +++ b/nucypher/blockchain/eth/sol/source/contracts/SubscriptionManager.sol @@ -2,9 +2,15 @@ pragma solidity ^0.8.0; -import "../zeppelin/proxy/Initializable.sol"; +import "@openzeppelin-upgradeable/contracts/access/AccessControlUpgradeable.sol"; +import "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol"; -contract SubscriptionManager is Initializable { +contract SubscriptionManager is Initializable, AccessControlUpgradeable { + + bytes32 public constant SET_RATE_ROLE = + keccak256("Power to set the fee rate"); + bytes32 public constant WITHDRAW_ROLE = + keccak256("Power to withdraw funds from SubscriptionManager"); // The layout of policy struct is optimized, so sponsor, timestamps and size // fit in a single 256-word. @@ -13,6 +19,7 @@ contract SubscriptionManager is Initializable { uint32 startTimestamp; uint32 endTimestamp; uint16 size; // also known as `N` + // There's still 2 bytes available here address owner; } @@ -22,12 +29,13 @@ contract SubscriptionManager is Initializable { address indexed owner, uint16 size, uint32 startTimestamp, - uint32 endTimestamp + uint32 endTimestamp, + uint256 cost ); event FeeRateUpdated(uint256 oldFeeRate, uint256 newFeeRate); - // Per-second service fee rate + // Per-second, per-node service fee rate uint256 public feeRate; // Mapping that stores policy structs, keyed by policy ID @@ -35,6 +43,20 @@ contract SubscriptionManager is Initializable { function initialize(uint256 _feeRate) public initializer { _setFeeRate(_feeRate); + _setupRole(SET_RATE_ROLE, msg.sender); + _setupRole(WITHDRAW_ROLE, msg.sender); + _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); + } + + function getPolicyCost( + uint16 _size, + uint32 _startTimestamp, + uint32 _endTimestamp + ) public view returns (uint256){ + uint32 duration = _endTimestamp - _startTimestamp; + require(duration > 0, "Invalid timestamps"); + require(_size > 0, "Invalid policy size"); + return feeRate * _size * duration; } function createPolicy( @@ -47,14 +69,12 @@ contract SubscriptionManager is Initializable { external payable { require( - _startTimestamp < _endTimestamp && - block.timestamp < _endTimestamp, + _startTimestamp < _endTimestamp && block.timestamp < _endTimestamp, "Invalid timestamps" ); - uint32 duration = _endTimestamp - _startTimestamp; require( - duration > 0 && _size > 0 && - msg.value == feeRate * _size * uint32(duration) + msg.value == getPolicyCost(_size, _startTimestamp, _endTimestamp), + "Invalid policy cost" ); _createPolicy(_policyId, _policyOwner, _size, _startTimestamp, _endTimestamp); @@ -98,7 +118,8 @@ contract SubscriptionManager is Initializable { _policyOwner == address(0) ? msg.sender : _policyOwner, _size, _startTimestamp, - _endTimestamp + _endTimestamp, + msg.value ); } @@ -116,11 +137,11 @@ contract SubscriptionManager is Initializable { emit FeeRateUpdated(oldFee, newFee); } - function setFeeRate(uint256 _rate_per_second) external { - _setFeeRate(_rate_per_second); + function setFeeRate(uint256 _ratePerSecond) onlyRole(SET_RATE_ROLE) external { + _setFeeRate(_ratePerSecond); } - function sweep(address payable recipient) external { + function sweep(address payable recipient) onlyRole(WITHDRAW_ROLE) external { uint256 balance = address(this).balance; (bool sent, ) = recipient.call{value: balance}(""); require(sent, "Failed transfer"); From 1e6c50e2cf86931ab7cc36cd0736d833f31922b3 Mon Sep 17 00:00:00 2001 From: "Kieran R. Prasch" Date: Wed, 23 Feb 2022 13:11:54 -0800 Subject: [PATCH 03/19] modifies finnegans wake demo to work with local registries, handpicked ursulas, and polygon payments. --- .../finnegans-wake-demo-testnet-l2.py | 48 ++++++++++++++++--- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/examples/finnegans_wake_demo/finnegans-wake-demo-testnet-l2.py b/examples/finnegans_wake_demo/finnegans-wake-demo-testnet-l2.py index 7dbfb6191..588bd6f13 100644 --- a/examples/finnegans_wake_demo/finnegans-wake-demo-testnet-l2.py +++ b/examples/finnegans_wake_demo/finnegans-wake-demo-testnet-l2.py @@ -24,10 +24,12 @@ import os from pathlib import Path from web3.main import Web3 +from nucypher.blockchain.eth.registry import LocalContractRegistry from nucypher.blockchain.eth.signers.base import Signer from nucypher.characters.lawful import Alice, Bob, Ursula from nucypher.characters.lawful import Enrico as Enrico from nucypher.crypto.powers import SigningPower, DecryptingPower +from nucypher.policy.payment import SubscriptionManagerPayment from nucypher.utilities.ethereum import connect_web3_provider from nucypher.utilities.logging import GlobalLoggerSettings @@ -62,16 +64,26 @@ except KeyError: # NuCypher Network # #################### -L1_TESTNET = 'lynx' +L1_TESTNET = 'ibex' L2_TESTNET = 'mumbai' # TODO: Needs name different than the network name +base_path = Path('/home/k/Git/nucypher/nucypher/blockchain/eth/contract_registry') + +# Staking local registry (L1) +application_registry_path = base_path / Path(f'{L1_TESTNET}/contract_registry.json') +application_registry = LocalContractRegistry(filepath=application_registry_path) + +# Policy/Payment local registry (L2 or Sidechain) +policy_registry_path = base_path / Path(f'{L2_TESTNET}/contract_registry.json') +policy_registry = LocalContractRegistry(filepath=policy_registry_path) + ##################### # Bob the BUIDLer ## ##################### # Then, there was bob. Bob learns about the # rest of the network from the seednode. -bob = Bob(domain=L1_TESTNET) +bob = Bob(domain=L1_TESTNET, registry=application_registry) # Bob puts his public keys somewhere alice can find them. verifying_key = bob.public_keys(SigningPower) @@ -92,14 +104,29 @@ wallet = Signer.from_signer_uri(SIGNER_URI) password = os.environ.get('DEMO_ALICE_PASSWORD') or getpass(f"Enter password to unlock {ALICE_ADDRESS[:8]}: ") wallet.unlock_account(account=ALICE_ADDRESS, password=password) + +handpicked_ursulas = { + Ursula.from_teacher_uri( + teacher_uri=f"https://{L1_TESTNET}.nucypher.network", + min_stake=0, + federated_only=False + ), +} + +payment_method = SubscriptionManagerPayment( + network='mumbai', + registry=policy_registry, + eth_provider=L2_PROVIDER +) + # This is Alice. alice = Alice( checksum_address=ALICE_ADDRESS, + registry=application_registry, signer=wallet, domain=L1_TESTNET, - payment_network=L2_TESTNET, - payment_provider=L2_PROVIDER, eth_provider_uri=L1_PROVIDER, + payment_method=payment_method, ) # Alice puts her public key somewhere for Bob to find later... @@ -120,11 +147,18 @@ remote_bob = Bob.from_public_keys(encrypting_key=encrypting_key, verifying_key=v # In this example bob will be granted access for 1 day, # trusting 2 of 3 nodes paying each of them 50 gwei per period. expiration = maya.now() + datetime.timedelta(days=1) -threshold, shares = 2, 3 -price = alice.payment_method.quote(expiration=expiration, shares=shares).value +threshold, shares = 1, 1 +price = alice.payment_method.quote(expiration=expiration.epoch, shares=shares).value # Alice grants access to Bob... -policy = alice.grant(remote_bob, label, threshold=threshold, shares=shares, value=price, expiration=expiration) +policy = alice.grant( + remote_bob, label, + threshold=threshold, + shares=shares, + value=price, + expiration=expiration, + ursulas=handpicked_ursulas +) # ...and then disappears from the internet. # From 06ed69990e7145c178a927c45fc9042f1766428b Mon Sep 17 00:00:00 2001 From: "Kieran R. Prasch" Date: Wed, 23 Feb 2022 20:59:52 -0800 Subject: [PATCH 04/19] CLI and config support for policy/payment registry independently from pre application. --- nucypher/cli/commands/ursula.py | 19 ++++++++++++++++--- nucypher/cli/options.py | 1 + nucypher/config/base.py | 25 ++++++++++++++++++++++--- nucypher/network/server.py | 2 +- 4 files changed, 40 insertions(+), 7 deletions(-) diff --git a/nucypher/cli/commands/ursula.py b/nucypher/cli/commands/ursula.py index bcd741df4..ce364b2d9 100644 --- a/nucypher/cli/commands/ursula.py +++ b/nucypher/cli/commands/ursula.py @@ -14,6 +14,8 @@ You should have received a copy of the GNU Affero General Public License along with nucypher. If not, see . """ + + from pathlib import Path import click @@ -54,7 +56,11 @@ from nucypher.cli.options import ( option_teacher_uri, option_lonely, option_max_gas_price, - option_key_material, option_payment_provider, option_payment_method, option_payment_network + option_key_material, + option_payment_provider, + option_payment_method, + option_payment_network, + option_policy_registry_filepath ) from nucypher.cli.painting.help import paint_new_installation_help from nucypher.cli.types import EIP55_CHECKSUM_ADDRESS, NETWORK_PORT, OPERATOR_IP @@ -80,6 +86,7 @@ class UrsulaConfigOptions: db_filepath: Path, network: str, registry_filepath: Path, + policy_registry_filepath: Path, dev: bool, poa: bool, light: bool, @@ -94,9 +101,9 @@ class UrsulaConfigOptions: ): if federated_only: - if registry_filepath: + if registry_filepath or policy_registry_filepath: raise click.BadOptionUsage(option_name="--registry-filepath", - message=f"--registry-filepath cannot be used in federated mode.") + message=f"--registry-filepath and --policy-registry-filepath cannot be used in federated mode.") self.eth_provider_uri = eth_provider_uri self.signer_uri = signer_uri @@ -107,6 +114,7 @@ class UrsulaConfigOptions: self.db_filepath = db_filepath self.domain = network self.registry_filepath = registry_filepath + self.policy_registry_filepath = policy_registry_filepath self.dev = dev self.poa = poa self.light = light @@ -127,6 +135,7 @@ class UrsulaConfigOptions: poa=self.poa, light=self.light, registry_filepath=self.registry_filepath, + policy_registry_filepath=self.policy_registry_filepath, eth_provider_uri=self.eth_provider_uri, signer_uri=self.signer_uri, gas_strategy=self.gas_strategy, @@ -152,6 +161,7 @@ class UrsulaConfigOptions: filepath=config_file, domain=self.domain, registry_filepath=self.registry_filepath, + policy_registry_filepath=self.policy_registry_filepath, eth_provider_uri=self.eth_provider_uri, signer_uri=self.signer_uri, gas_strategy=self.gas_strategy, @@ -200,6 +210,7 @@ class UrsulaConfigOptions: federated_only=self.federated_only, operator_address=self.operator_address, registry_filepath=self.registry_filepath, + policy_registry_filepath=self.policy_registry_filepath, eth_provider_uri=self.eth_provider_uri, signer_uri=self.signer_uri, gas_strategy=self.gas_strategy, @@ -220,6 +231,7 @@ class UrsulaConfigOptions: federated_only=self.federated_only, operator_address=self.operator_address, registry_filepath=self.registry_filepath, + policy_registry_filepath=self.policy_registry_filepath, eth_provider_uri=self.eth_provider_uri, signer_uri=self.signer_uri, gas_strategy=self.gas_strategy, @@ -250,6 +262,7 @@ group_config_options = group_options( db_filepath=option_db_filepath, network=option_network(), registry_filepath=option_registry_filepath, + policy_registry_filepath=option_policy_registry_filepath, poa=option_poa, light=option_light, dev=option_dev, diff --git a/nucypher/cli/options.py b/nucypher/cli/options.py index 9eaa0d52f..1511db439 100644 --- a/nucypher/cli/options.py +++ b/nucypher/cli/options.py @@ -63,6 +63,7 @@ option_payment_network = click.option('--payment-network', help="Payment network option_payment_method = click.option('--payment-method', help="Payment method name", type=PAYMENT_METHOD_CHOICES, required=False) option_poa = click.option('--poa/--disable-poa', help="Inject POA middleware", is_flag=True, default=None) option_registry_filepath = click.option('--registry-filepath', help="Custom contract registry filepath", type=EXISTING_READABLE_FILE) +option_policy_registry_filepath = click.option('--policy-registry-filepath', help="Custom contract registry filepath for policies", type=EXISTING_READABLE_FILE) option_shares = click.option('--shares', '-n', help="N-Total shares", type=click.INT) option_signer_uri = click.option('--signer', 'signer_uri', '-S', default=None, type=str) option_staking_provider = click.option('--staking-provider', help="Staking provider ethereum address", type=EIP55_CHECKSUM_ADDRESS, required=True) diff --git a/nucypher/config/base.py b/nucypher/config/base.py index 7fb00201c..edd20f37b 100644 --- a/nucypher/config/base.py +++ b/nucypher/config/base.py @@ -408,9 +408,11 @@ class CharacterConfiguration(BaseConfiguration): payment_provider: str = None, payment_network: str = None, - # Registry + # Registries registry: BaseContractRegistry = None, registry_filepath: Optional[Path] = None, + policy_registry: BaseContractRegistry = None, + policy_registry_filepath: Optional[Path] = None, # Deployed Operators worker_data: dict = None @@ -444,6 +446,9 @@ class CharacterConfiguration(BaseConfiguration): self.registry = registry or NO_BLOCKCHAIN_CONNECTION.bool_value(False) self.registry_filepath = registry_filepath or UNINITIALIZED_CONFIGURATION + self.policy_registry = policy_registry or NO_BLOCKCHAIN_CONNECTION.bool_value(False) + self.policy_registry_filepath = policy_registry_filepath or UNINITIALIZED_CONFIGURATION + # Blockchain self.poa = poa self.is_light = light @@ -495,6 +500,7 @@ class CharacterConfiguration(BaseConfiguration): self.is_light = False self.eth_provider_uri = None self.registry_filepath = None + self.policy_registry_filepath = None self.gas_strategy = None self.max_gas_price = None @@ -533,7 +539,10 @@ class CharacterConfiguration(BaseConfiguration): self.testnet = self.domain != NetworksInventory.MAINNET self.signer = Signer.from_signer_uri(self.signer_uri, testnet=self.testnet) - # Onchain Payments + # + # Onchain Payments & Policies + # + # TODO: Enforce this for Ursula/Alice but not Bob? # if not payment_provider: # raise self.ConfigurationError("payment provider is required.") @@ -541,6 +550,15 @@ class CharacterConfiguration(BaseConfiguration): self.payment_network = payment_network or self.DEFAULT_PAYMENT_NETWORK self.payment_provider = payment_provider or (self.eth_provider_uri or None) # default to L1 payments + # TODO: Dedupe + if not self.policy_registry: + if not self.policy_registry_filepath: + self.log.info(f"Fetching latest policy registry from source.") + self.policy_registry = InMemoryContractRegistry.from_latest_publication(network=self.payment_network) + else: + self.policy_registry = LocalContractRegistry(filepath=self.policy_registry_filepath) + self.log.info(f"Using local policy registry ({self.policy_registry}).") + if dev_mode: self.__temp_dir = UNINITIALIZED_CONFIGURATION self._setup_node_storage() @@ -869,7 +887,8 @@ class CharacterConfiguration(BaseConfiguration): if payment_class.ONCHAIN: # on-chain payment strategies require a blockchain connection payment_strategy = payment_class(network=self.payment_network, - provider=self.payment_provider) + eth_provider=self.payment_provider, + registry=self.policy_registry) else: payment_strategy = payment_class() return payment_strategy diff --git a/nucypher/network/server.py b/nucypher/network/server.py index 3fd794b9b..7b1a5a02e 100644 --- a/nucypher/network/server.py +++ b/nucypher/network/server.py @@ -209,7 +209,7 @@ def _make_rest_app(datastore: Datastore, this_node, log: Logger) -> Flask: # 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 {hrac} is unpaid." + message = f"{bob_identity_message} Policy {bytes(hrac)} is unpaid." return Response(message, status=HTTPStatus.PAYMENT_REQUIRED) # Re-encrypt From 37980a269e2f47777e4c215a1372227422ed7dec Mon Sep 17 00:00:00 2001 From: Kieran Prasch Date: Wed, 2 Mar 2022 13:23:35 -0800 Subject: [PATCH 05/19] Removes temporary workarounds. --- .../finnegans-wake-demo-federated.py | 2 +- .../finnegans-wake-demo-testnet-l2.py | 47 ++++++------------- .../finnegans-wake-demo-testnet.py | 2 +- 3 files changed, 16 insertions(+), 35 deletions(-) diff --git a/examples/finnegans_wake_demo/finnegans-wake-demo-federated.py b/examples/finnegans_wake_demo/finnegans-wake-demo-federated.py index 0cb5117d1..466bb0c0b 100644 --- a/examples/finnegans_wake_demo/finnegans-wake-demo-federated.py +++ b/examples/finnegans_wake_demo/finnegans-wake-demo-federated.py @@ -36,7 +36,7 @@ from nucypher.utilities.logging import GlobalLoggerSettings BOOK_PATH = Path(os.getenv('FINNEGANS_WAKE_PATH') or 'finnegans-wake-excerpt.txt') # Twisted Logger -GlobalLoggerSettings.set_log_level(log_level_name='debug') +GlobalLoggerSettings.set_log_level(log_level_name='info') GlobalLoggerSettings.start_console_logging() # if your ursulas are NOT running on your current host, diff --git a/examples/finnegans_wake_demo/finnegans-wake-demo-testnet-l2.py b/examples/finnegans_wake_demo/finnegans-wake-demo-testnet-l2.py index 588bd6f13..239f4df4f 100644 --- a/examples/finnegans_wake_demo/finnegans-wake-demo-testnet-l2.py +++ b/examples/finnegans_wake_demo/finnegans-wake-demo-testnet-l2.py @@ -16,29 +16,26 @@ """ -from getpass import getpass - import datetime -import maya import os +from getpass import getpass from pathlib import Path -from web3.main import Web3 -from nucypher.blockchain.eth.registry import LocalContractRegistry +import maya + from nucypher.blockchain.eth.signers.base import Signer -from nucypher.characters.lawful import Alice, Bob, Ursula +from nucypher.characters.lawful import Alice, Bob from nucypher.characters.lawful import Enrico as Enrico from nucypher.crypto.powers import SigningPower, DecryptingPower from nucypher.policy.payment import SubscriptionManagerPayment from nucypher.utilities.ethereum import connect_web3_provider from nucypher.utilities.logging import GlobalLoggerSettings - ###################### # Boring setup stuff # ###################### -GlobalLoggerSettings.set_log_level(log_level_name='debug') +GlobalLoggerSettings.set_log_level(log_level_name='info') GlobalLoggerSettings.start_console_logging() BOOK_PATH = Path('finnegans-wake-excerpt.txt') @@ -67,15 +64,6 @@ except KeyError: L1_TESTNET = 'ibex' L2_TESTNET = 'mumbai' # TODO: Needs name different than the network name -base_path = Path('/home/k/Git/nucypher/nucypher/blockchain/eth/contract_registry') - -# Staking local registry (L1) -application_registry_path = base_path / Path(f'{L1_TESTNET}/contract_registry.json') -application_registry = LocalContractRegistry(filepath=application_registry_path) - -# Policy/Payment local registry (L2 or Sidechain) -policy_registry_path = base_path / Path(f'{L2_TESTNET}/contract_registry.json') -policy_registry = LocalContractRegistry(filepath=policy_registry_path) ##################### # Bob the BUIDLer ## @@ -83,7 +71,7 @@ policy_registry = LocalContractRegistry(filepath=policy_registry_path) # Then, there was bob. Bob learns about the # rest of the network from the seednode. -bob = Bob(domain=L1_TESTNET, registry=application_registry) +bob = Bob(domain=L1_TESTNET) # Bob puts his public keys somewhere alice can find them. verifying_key = bob.public_keys(SigningPower) @@ -105,32 +93,22 @@ password = os.environ.get('DEMO_ALICE_PASSWORD') or getpass(f"Enter password to wallet.unlock_account(account=ALICE_ADDRESS, password=password) -handpicked_ursulas = { - Ursula.from_teacher_uri( - teacher_uri=f"https://{L1_TESTNET}.nucypher.network", - min_stake=0, - federated_only=False - ), -} - payment_method = SubscriptionManagerPayment( network='mumbai', - registry=policy_registry, eth_provider=L2_PROVIDER ) # This is Alice. alice = Alice( checksum_address=ALICE_ADDRESS, - registry=application_registry, signer=wallet, domain=L1_TESTNET, eth_provider_uri=L1_PROVIDER, - payment_method=payment_method, + payment_method=payment_method ) # Alice puts her public key somewhere for Bob to find later... -alice_verifying_key = bytes(alice.stamp) +alice_verifying_key = alice.stamp.as_umbral_pubkey() # Alice can get the policy's public key even before creating the policy. label = b"secret/files/42" @@ -141,13 +119,16 @@ policy_public_key = alice.get_policy_encrypting_key_from_label(label) # can be shared with any Bob that Alice grants access. # Alice already knows Bob's public keys from a side-channel. -remote_bob = Bob.from_public_keys(encrypting_key=encrypting_key, verifying_key=verifying_key) +remote_bob = Bob.from_public_keys( + encrypting_key=encrypting_key, + verifying_key=verifying_key, +) # These are the policy details for bob. # In this example bob will be granted access for 1 day, # trusting 2 of 3 nodes paying each of them 50 gwei per period. expiration = maya.now() + datetime.timedelta(days=1) -threshold, shares = 1, 1 +threshold, shares = 1, 2 price = alice.payment_method.quote(expiration=expiration.epoch, shares=shares).value # Alice grants access to Bob... @@ -157,7 +138,6 @@ policy = alice.grant( shares=shares, value=price, expiration=expiration, - ursulas=handpicked_ursulas ) # ...and then disappears from the internet. @@ -203,5 +183,6 @@ for counter, plaintext in enumerate(finnegans_wake): # We show that indeed this is the passage originally encrypted by Enrico. assert plaintext == cleartexts[0] + print(cleartexts) bob.disenchant() diff --git a/examples/finnegans_wake_demo/finnegans-wake-demo-testnet.py b/examples/finnegans_wake_demo/finnegans-wake-demo-testnet.py index fde0f998f..78a013fa6 100644 --- a/examples/finnegans_wake_demo/finnegans-wake-demo-testnet.py +++ b/examples/finnegans_wake_demo/finnegans-wake-demo-testnet.py @@ -36,7 +36,7 @@ from nucypher.utilities.logging import GlobalLoggerSettings # Boring setup stuff # ###################### -GlobalLoggerSettings.set_log_level(log_level_name='debug') +GlobalLoggerSettings.set_log_level(log_level_name='info') GlobalLoggerSettings.start_console_logging() BOOK_PATH = Path('finnegans-wake-excerpt.txt') From ae79dfc6fb0bc21bb5c5d3916a515ea4e75bdd92 Mon Sep 17 00:00:00 2001 From: Kieran Prasch Date: Fri, 4 Mar 2022 14:52:37 -0800 Subject: [PATCH 06/19] Handle SSL certificates inside middleware. --- nucypher/characters/lawful.py | 31 ++--- nucypher/cli/utils.py | 2 +- nucypher/config/storages.py | 2 +- nucypher/network/exceptions.py | 12 +- nucypher/network/middleware.py | 108 +++++++++++++----- nucypher/network/nodes.py | 44 +++---- nucypher/network/server.py | 12 +- nucypher/utilities/networking.py | 5 +- .../test_ursula_prepares_to_act_as_worker.py | 8 +- .../learning/test_fault_tolerance.py | 6 +- 10 files changed, 129 insertions(+), 101 deletions(-) diff --git a/nucypher/characters/lawful.py b/nucypher/characters/lawful.py index ca5b772bb..3cf5c4ff7 100644 --- a/nucypher/characters/lawful.py +++ b/nucypher/characters/lawful.py @@ -84,7 +84,7 @@ from nucypher.crypto.powers import ( TLSHostingPower, ) from nucypher.network.exceptions import NodeSeemsToBeDown -from nucypher.network.middleware import RestMiddleware +from nucypher.network.middleware import RestMiddleware, fetch_ssl_certificate from nucypher.network.nodes import NodeSprout, TEACHER_NODES, Teacher from nucypher.network.protocols import parse_node_uri from nucypher.network.retrieval import RetrievalClient @@ -1077,15 +1077,9 @@ class Ursula(Teacher, Character, Operator): def from_rest_url(cls, network_middleware: RestMiddleware, host: str, - port: int, - certificate_filepath, - *args, **kwargs - ): - response_data = network_middleware.client.node_information(host, port, - certificate_filepath=certificate_filepath) - + port: int): + response_data = network_middleware.client.node_information(host, port) stranger_ursula_from_public_keys = cls.from_metadata_bytes(response_data) - return stranger_ursula_from_public_keys @classmethod @@ -1136,7 +1130,7 @@ class Ursula(Teacher, Character, Operator): except NodeSeemsToBeDown as e: log = Logger(cls.__name__) log.warn( - "Can't connect to seed node (attempt {}). Will retry in {} seconds.".format(attempt, interval)) + "Can't connect to peer (attempt {}). Will retry in {} seconds.".format(attempt, interval)) time.sleep(interval) return __attempt(attempt=attempt + 1) else: @@ -1151,8 +1145,6 @@ class Ursula(Teacher, Character, Operator): minimum_stake: int = 0, registry: BaseContractRegistry = None, network_middleware: RestMiddleware = None, - *args, - **kwargs ) -> Union['Ursula', 'NodeSprout']: if network_middleware is None: @@ -1163,25 +1155,18 @@ class Ursula(Teacher, Character, Operator): # Fetch the hosts TLS certificate and read the common name try: - certificate = network_middleware.get_certificate(host=host, port=port) + certificate = fetch_ssl_certificate(host=host, port=port) except NodeSeemsToBeDown as e: - e.args += (f"While trying to load seednode {seed_uri}",) - e.crash_right_now = True + e.args += (f"While trying to contact {seed_uri}",) + e.crash_right_now = False raise real_host = certificate.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value - # Create a temporary certificate storage area - temp_node_storage = ForgetfulNodeStorage(federated_only=federated_only) - temp_certificate_filepath = temp_node_storage.store_node_certificate(certificate=certificate, port=port) - # Load the host as a potential seed node potential_seed_node = cls.from_rest_url( host=real_host, port=port, network_middleware=network_middleware, - certificate_filepath=temp_certificate_filepath, - *args, - **kwargs ) # Check the node's stake (optional) @@ -1191,8 +1176,6 @@ class Ursula(Teacher, Character, Operator): if seednode_stake < minimum_stake: raise Learner.NotATeacher(f"{staking_provider_address} is staking less than the specified minimum stake value ({minimum_stake}).") - # OK - everyone get out - temp_node_storage.forget() return potential_seed_node @classmethod diff --git a/nucypher/cli/utils.py b/nucypher/cli/utils.py index 75fa7c01d..2a87866d4 100644 --- a/nucypher/cli/utils.py +++ b/nucypher/cli/utils.py @@ -123,7 +123,7 @@ def make_cli_character(character_config, if character_config.federated_only: emitter.message(FEDERATED_WARNING, color='yellow') - emitter.message(f"Loaded {CHARACTER.__class__.__name__} {CHARACTER.checksum_address} ({CHARACTER.domain})", color='green') + emitter.message(f"Loaded {CHARACTER.__class__.__name__} ({CHARACTER.domain})", color='green') return CHARACTER diff --git a/nucypher/config/storages.py b/nucypher/config/storages.py index e477b2a92..9a057a1fc 100644 --- a/nucypher/config/storages.py +++ b/nucypher/config/storages.py @@ -49,7 +49,7 @@ class NodeStorage(ABC): pass def __init__(self, - federated_only: bool, # TODO# 466 + federated_only: bool = False, # TODO# 466 character_class=None, registry: BaseContractRegistry = None, ) -> None: diff --git a/nucypher/network/exceptions.py b/nucypher/network/exceptions.py index e6e861ada..eb832881f 100644 --- a/nucypher/network/exceptions.py +++ b/nucypher/network/exceptions.py @@ -18,8 +18,10 @@ import requests import socket -NodeSeemsToBeDown = (requests.exceptions.ConnectionError, - requests.exceptions.ReadTimeout, - requests.exceptions.ConnectTimeout, - socket.gaierror, - ConnectionRefusedError) +NodeSeemsToBeDown = ( + requests.exceptions.ConnectionError, + requests.exceptions.ReadTimeout, + requests.exceptions.ConnectTimeout, + socket.gaierror, + ConnectionRefusedError +) diff --git a/nucypher/network/middleware.py b/nucypher/network/middleware.py index 9aa06ff8d..173d43a0d 100644 --- a/nucypher/network/middleware.py +++ b/nucypher/network/middleware.py @@ -16,31 +16,72 @@ along with nucypher. If not, see . """ -from http import HTTPStatus import socket import ssl import time +from http import HTTPStatus +from typing import Optional from typing import Sequence + import requests - -from nucypher_core import MetadataRequest, FleetStateChecksum, NodeMetadata - -from constant_sorrow.constants import CERTIFICATE_NOT_SAVED, EXEMPT_FROM_VERIFICATION +from constant_sorrow.constants import EXEMPT_FROM_VERIFICATION from cryptography import x509 from cryptography.hazmat.backends import default_backend +from cryptography.x509 import Certificate +from nucypher_core import MetadataRequest, FleetStateChecksum, NodeMetadata +from nucypher.blockchain.eth.registry import BaseContractRegistry +from nucypher.config.storages import ForgetfulNodeStorage +from nucypher.network.exceptions import NodeSeemsToBeDown from nucypher.utilities.logging import Logger +SSL_LOGGER = Logger('ssl-middleware') EXEMPT_FROM_VERIFICATION.bool_value(False) +def fetch_ssl_certificate(host: str, + port: int, + timeout: int = 3, + retry_attempts: int = 3, + retry_rate: int = 2, + current_attempt: int = 0 + ) -> Certificate: + + socket.setdefaulttimeout(timeout) # Set Socket Timeout + + try: + SSL_LOGGER.debug(f"Fetching {host}:{port} TLS certificate") + certificate = ssl.get_server_certificate(addr=(host, port)) + + except socket.timeout: + if current_attempt == retry_attempts: + message = f"No Response from {host}:{port} after {retry_attempts} attempts" + SSL_LOGGER.info(message) + raise ConnectionRefusedError("No response from {}:{}".format(host, port)) + SSL_LOGGER.debug(f"No Response from {host}:{port}. Retrying in {retry_rate} seconds...") + time.sleep(retry_rate) + return fetch_ssl_certificate(host, port, timeout, retry_attempts, retry_rate, current_attempt + 1) + + except OSError: + raise # TODO: #1835 + + certificate = x509.load_pem_x509_certificate(certificate.encode(), backend=default_backend()) + return certificate + + class NucypherMiddlewareClient: library = requests timeout = 1.2 - def __init__(self, registry=None, eth_provider_uri: str = None, *args, **kwargs): + def __init__(self, + registry: Optional['BaseContractRegistry'] = None, + eth_provider_uri: Optional[str] = None, + storage: Optional['NodeStorage'] = None, + *args, **kwargs): + self.registry = registry self.eth_provider_uri = eth_provider_uri + self.storage = storage or ForgetfulNodeStorage() # for certificate storage @staticmethod def response_cleaner(response): @@ -63,15 +104,10 @@ class NucypherMiddlewareClient: if node: if any((host, port)): raise ValueError("Don't pass host and port if you are passing the node.") - host = node.rest_url() - certificate_filepath = node.certificate_filepath - elif all((host, port)): - host = f"{host}:{port}" - certificate_filepath = CERTIFICATE_NOT_SAVED - else: + host, port = node.rest_interface + elif not (host and port): raise ValueError("You need to pass either the node or a host and port.") - - return host, certificate_filepath, self.library + return host, port, self.library def invoke_method(self, method, url, *args, **kwargs): self.clean_params(kwargs) @@ -86,13 +122,12 @@ class NucypherMiddlewareClient: No cleaning needed. """ - def node_information(self, host, port, certificate_filepath=None): + def node_information(self, host, port): # The only time a node is exempt from verification - when we are first getting its info. response = self.get(node_or_sprout=EXEMPT_FROM_VERIFICATION, host=host, port=port, path="public_information", - timeout=2, - certificate_filepath=certificate_filepath) + timeout=2) return response.content def __getattr__(self, method_name): @@ -104,38 +139,47 @@ class NucypherMiddlewareClient: node_or_sprout=None, host=None, port=None, - certificate_filepath=None, *args, **kwargs): - host, node_certificate_filepath, http_client = self.verify_and_parse_node_or_host_and_port(node_or_sprout, host, port) - - if certificate_filepath: - filepaths_are_different = node_certificate_filepath != certificate_filepath - node_has_a_cert = node_certificate_filepath is not CERTIFICATE_NOT_SAVED - if node_has_a_cert and filepaths_are_different: - raise ValueError("Don't try to pass a node with a certificate_filepath while also passing a" - " different certificate_filepath. What do you even expect?") - else: - certificate_filepath = node_certificate_filepath + # Get interface + host, port, http_client = self.verify_and_parse_node_or_host_and_port(node_or_sprout, host, port) + endpoint = f"https://{host}:{port}/{path}" method = getattr(http_client, method_name) - url = f"https://{host}/{path}" - response = self.invoke_method(method, url, verify=certificate_filepath, *args, **kwargs) + # Fetch SSL certificate + try: + certificate = fetch_ssl_certificate(host=host, port=port) + certificate_filepath = self.storage.store_node_certificate(certificate=certificate, port=port) + + # Handle no response + except NodeSeemsToBeDown as e: + raise RestMiddleware.Unreachable(message=f'Node {node_or_sprout} {host}:{port} is unreachable: {e}') + + # Send request + response = self.invoke_method(method, endpoint, verify=certificate_filepath, *args, **kwargs) + + # Handle response cleaned_response = self.response_cleaner(response) if cleaned_response.status_code >= 300: + if cleaned_response.status_code == HTTPStatus.BAD_REQUEST: raise RestMiddleware.BadRequest(reason=cleaned_response.json) + elif cleaned_response.status_code == HTTPStatus.NOT_FOUND: m = f"While trying to {method_name} {args} ({kwargs}), server 404'd. Response: {cleaned_response.content}" raise RestMiddleware.NotFound(m) + elif cleaned_response.status_code == HTTPStatus.PAYMENT_REQUIRED: # TODO: Use this as a hook to prompt Bob's payment for policy sponsorship # https://getyarn.io/yarn-clip/ce0d37ba-4984-4210-9a40-c9c9859a3164 raise RestMiddleware.PaymentRequired(cleaned_response.content) + elif cleaned_response.status_code == HTTPStatus.FORBIDDEN: raise RestMiddleware.Unauthorized(cleaned_response.content) + else: raise RestMiddleware.UnexpectedResponse(cleaned_response.content, status=cleaned_response.status_code) + return cleaned_response return method_wrapper @@ -152,6 +196,10 @@ class RestMiddleware: _client_class = NucypherMiddlewareClient + class Unreachable(Exception): + def __init__(self, message, *args, **kwargs): + super().__init__(message, *args, **kwargs) + class UnexpectedResponse(Exception): def __init__(self, message, status, *args, **kwargs): super().__init__(message, *args, **kwargs) diff --git a/nucypher/network/nodes.py b/nucypher/network/nodes.py index 8fc1d37c1..1cfd0d914 100644 --- a/nucypher/network/nodes.py +++ b/nucypher/network/nodes.py @@ -24,7 +24,6 @@ from typing import Callable, List, Optional, Set, Tuple, Union import maya import requests -from constant_sorrow import constant_or_bytes from constant_sorrow.constants import ( CERTIFICATE_NOT_SAVED, FLEET_STATES_MATCH, @@ -32,19 +31,18 @@ from constant_sorrow.constants import ( NO_STORAGE_AVAILABLE, RELAX, ) -from cryptography.x509 import Certificate, load_der_x509_certificate from cryptography.hazmat.backends import default_backend +from cryptography.x509 import Certificate +from cryptography.x509 import load_der_x509_certificate from eth_utils import to_checksum_address +from nucypher_core import NodeMetadata, MetadataResponse, MetadataResponsePayload +from nucypher_core.umbral import Signature from requests.exceptions import SSLError from twisted.internet import reactor, task from twisted.internet.defer import Deferred -from nucypher_core import NodeMetadata, MetadataResponse, MetadataResponsePayload -from nucypher_core.umbral import Signature - from nucypher.acumen.nicknames import Nickname from nucypher.acumen.perception import FleetSensor -from nucypher.blockchain.economics import EconomicsFactory from nucypher.blockchain.eth.agents import ContractAgency, PREApplicationAgent from nucypher.blockchain.eth.constants import NULL_ADDRESS from nucypher.blockchain.eth.networks import NetworksInventory @@ -58,7 +56,6 @@ from nucypher.crypto.powers import ( SigningPower, ) from nucypher.crypto.signing import SignatureStamp, InvalidSignature -from nucypher.crypto.utils import recover_address_eip_191, verify_eip_191 from nucypher.network.exceptions import NodeSeemsToBeDown from nucypher.network.middleware import RestMiddleware from nucypher.network.protocols import SuspiciousActivity, InterfaceInfo @@ -475,7 +472,7 @@ class Learner: # TODO: Bucket this node as having bad TLS info - maybe it's an update that hasn't fully propagated? 567 return False - except NodeSeemsToBeDown: + except RestMiddleware.Unreachable: self.log.info("No Response while trying to verify node {}|{}".format(node.rest_interface, node)) # TODO: Bucket this node as "ghost" or something: somebody else knows about it, but we can't get to it. 567 return False @@ -805,15 +802,15 @@ class Learner: # These except clauses apply to the current_teacher itself, not the learned-about nodes. except NodeSeemsToBeDown as e: unresponsive_nodes.add(current_teacher) - self.log.info( - f"Teacher {str(current_teacher)} is perhaps down:{e}.") # FIXME: This was printing the node bytestring. Is this really necessary? #1712 + raise + self.log.info(f"Teacher {str(current_teacher)} is perhaps down:{e}.") # FIXME: This was printing the node bytestring. Is this really necessary? #1712 return except current_teacher.InvalidNode as e: # Ugh. The teacher is invalid. Rough. # TODO: Bucket separately and report. unresponsive_nodes.add(current_teacher) # This does nothing. self.known_nodes.mark_as(current_teacher.InvalidNode, current_teacher) - self.log.warn(f"Teacher {str(current_teacher)} is invalid (hex={bytes(current_teacher.metadata()).hex()}):{e}.") + self.log.warn(f"Teacher {str(current_teacher)} is invalid: {e}.") # TODO (#567): bucket the node as suspicious return except RuntimeError as e: @@ -918,8 +915,8 @@ class Learner: # except sprout.Invalidsprout: # self.log.warn(sprout.invalid_metadata_message.format(sprout)) - except InvalidNodeCertificate as e: - message = f"Discovered sprout with invalid node certificate: {sprout}. Full error: {e.__str__()} " + except NodeSeemsToBeDown as e: + message = f"Node is unreachable: {sprout}. Full error: {e.__str__()} " self.log.warn(message) except SuspiciousActivity: @@ -973,7 +970,7 @@ class Teacher: # Assume unverified self.verified_stamp = False - self.verified_worker = False + self.verified_operator = False self.verified_metadata = False self.verified_node = False @@ -1054,7 +1051,7 @@ class Teacher: is_staking = application_agent.is_authorized(staking_provider=self.checksum_address) # checksum address here is staking provider return is_staking - def validate_worker(self, registry: BaseContractRegistry = None, eth_provider_uri: Optional[str] = None) -> None: + def validate_operator(self, registry: BaseContractRegistry = None, eth_provider_uri: Optional[str] = None) -> None: # Federated if self.federated_only: @@ -1062,29 +1059,33 @@ class Teacher: "but is OK to use in federated mode if you " \ "have reason to believe it is trustworthy." raise self.WrongMode(message) + elif not registry: + self.log.info('No registry provided for staking verification.') # Decentralized else: # Try to derive the worker address if it hasn't been derived yet. try: - operator_address = self.operator_address + _operator_address = self.operator_address except Exception as e: raise self.InvalidOperatorSignature(str(e)) from e # On-chain staking check, if registry is present if registry: + if not self._operator_is_bonded(registry=registry): # <-- Blockchain CALL message = f"Operator {self.operator_address} is not bonded to staking provider {self.checksum_address}" self.log.debug(message) raise self.UnbondedOperator(message) if self._staking_provider_is_really_staking(registry=registry, eth_provider_uri=eth_provider_uri): # <-- Blockchain CALL - self.verified_worker = True + self.log.info(f'Verified operator {self}') + self.verified_operator = True else: raise self.NotStaking(f"{self.checksum_address} is not staking") - self.verified_stamp = True + self.verified_stamp = True # TODO: Why is this here? def validate_metadata_signature(self) -> bool: """Checks that the interface info is valid for this node's canonical address.""" @@ -1107,7 +1108,7 @@ class Teacher: # Offline check of valid stamp signature by worker try: - self.validate_worker(registry=registry, eth_provider_uri=eth_provider_uri) + self.validate_operator(registry=registry, eth_provider_uri=eth_provider_uri) except self.WrongMode: if bool(registry): raise @@ -1137,7 +1138,7 @@ class Teacher: self.verified_metadata = False self.verified_node = False self.verified_stamp = False - self.verified_worker = False + self.verified_operator = False if self.verified_node: return True @@ -1156,8 +1157,7 @@ class Teacher: certificate_filepath = self.certificate_filepath response_data = network_middleware_client.node_information(host=self.rest_interface.host, - port=self.rest_interface.port, - certificate_filepath=certificate_filepath) + port=self.rest_interface.port) try: sprout = self.from_metadata_bytes(response_data) diff --git a/nucypher/network/server.py b/nucypher/network/server.py index 7b1a5a02e..f8b4eb192 100644 --- a/nucypher/network/server.py +++ b/nucypher/network/server.py @@ -16,9 +16,9 @@ along with nucypher. If not, see . """ -from http import HTTPStatus import uuid import weakref +from http import HTTPStatus from pathlib import Path from typing import Tuple @@ -27,14 +27,13 @@ from constant_sorrow.constants import RELAX 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, - ) +) from nucypher.config.constants import MAX_UPLOAD_CONTENT_LENGTH from nucypher.crypto.keypairs import DecryptingKeypair @@ -42,8 +41,8 @@ from nucypher.crypto.signing import InvalidSignature from nucypher.datastore.datastore import Datastore from nucypher.datastore.models import ReencryptionRequest as ReencryptionRequestModel from nucypher.network.exceptions import NodeSeemsToBeDown -from nucypher.network.protocols import InterfaceInfo from nucypher.network.nodes import NodeSprout +from nucypher.network.protocols import InterfaceInfo from nucypher.utilities.logging import Logger HERE = BASE_DIR = Path(__file__).parent @@ -105,7 +104,6 @@ def _make_rest_app(datastore: Datastore, this_node, log: Logger) -> Flask: # TODO: Avoid circular imports :-( from nucypher.characters.lawful import Alice, Bob, Ursula - from nucypher.policy.policies import Policy _alice_class = Alice _bob_class = Bob @@ -255,13 +253,9 @@ def _make_rest_app(datastore: Datastore, this_node, log: Logger) -> Flask: # Make a Sandwich try: - # Fetch and store initiator's teacher certificate. - certificate = this_node.network_middleware.get_certificate(host=initiator_address, port=initiator_port) - certificate_filepath = this_node.node_storage.store_node_certificate(certificate=certificate) requesting_ursula_metadata = this_node.network_middleware.client.node_information( host=initiator_address, port=initiator_port, - certificate_filepath=certificate_filepath ) except NodeSeemsToBeDown: return Response({'error': 'Unreachable node'}, status=HTTPStatus.BAD_REQUEST) # ... toasted diff --git a/nucypher/utilities/networking.py b/nucypher/utilities/networking.py index 0352bab77..a6ec3385a 100644 --- a/nucypher/utilities/networking.py +++ b/nucypher/utilities/networking.py @@ -17,11 +17,12 @@ import random -import requests from ipaddress import ip_address -from requests.exceptions import RequestException, HTTPError from typing import Union, Optional +import requests +from requests.exceptions import RequestException, HTTPError + from nucypher.acumen.perception import FleetSensor from nucypher.blockchain.eth.registry import BaseContractRegistry from nucypher.config.storages import LocalFileBasedNodeStorage diff --git a/tests/acceptance/characters/test_ursula_prepares_to_act_as_worker.py b/tests/acceptance/characters/test_ursula_prepares_to_act_as_worker.py index 2b4466995..812efd247 100644 --- a/tests/acceptance/characters/test_ursula_prepares_to_act_as_worker.py +++ b/tests/acceptance/characters/test_ursula_prepares_to_act_as_worker.py @@ -36,8 +36,8 @@ def test_stakers_bond_to_ursulas(testerchain, test_registry, staking_providers, assert len(ursulas) == len(staking_providers) for ursula in ursulas: - ursula.validate_worker(registry=test_registry) - assert ursula.verified_worker + ursula.validate_operator(registry=test_registry) + assert ursula.verified_operator def test_blockchain_ursula_substantiates_stamp(blockchain_ursulas): @@ -55,7 +55,7 @@ def test_blockchain_ursula_verifies_stamp(blockchain_ursulas): # This Ursula does not yet have a verified stamp first_ursula.verified_stamp = False - first_ursula.validate_worker() + first_ursula.validate_operator() # ...but now it's verified. assert first_ursula.verified_stamp @@ -138,7 +138,7 @@ def test_vladimir_invalidity_without_stake(testerchain, blockchain_ursulas, bloc # But the actual handshake proves him wrong. message = "Wallet address swapped out. It appears that someone is trying to defraud this node." with pytest.raises(vladimir.InvalidNode, match=message): - vladimir.verify_node(blockchain_alice.network_middleware.client, certificate_filepath="doesn't matter") + vladimir.verify_node(blockchain_alice.network_middleware.client) # TODO: Change name of this file, extract this test diff --git a/tests/acceptance/learning/test_fault_tolerance.py b/tests/acceptance/learning/test_fault_tolerance.py index 90301cc54..e04276902 100644 --- a/tests/acceptance/learning/test_fault_tolerance.py +++ b/tests/acceptance/learning/test_fault_tolerance.py @@ -134,7 +134,7 @@ def test_invalid_operators_tolerance(testerchain, testerchain.time_travel(periods=1) # The worker is valid and can be verified (even with the force option) - worker.verify_node(force=True, network_middleware=MockRestMiddleware(), certificate_filepath="quietorl") + worker.verify_node(force=True, network_middleware=MockRestMiddleware()) # In particular, we know that it's bonded to a staker who is really staking. assert worker._operator_is_bonded(registry=test_registry) assert worker._staking_provider_is_really_staking(registry=test_registry) @@ -159,11 +159,11 @@ def test_invalid_operators_tolerance(testerchain, assert 0 == staking_agent.owned_tokens(idle_staker.checksum_address) # ... but the worker node still is "verified" (since we're not forcing on-chain verification) - worker.verify_node(network_middleware=MockRestMiddleware(), certificate_filepath="quietorl") + worker.verify_node(network_middleware=MockRestMiddleware()) # If we force, on-chain verification, the worker is of course not verified with pytest.raises(worker.NotStaking): - worker.verify_node(force=True, network_middleware=MockRestMiddleware(), certificate_filepath="quietorl") + worker.verify_node(force=True, network_middleware=MockRestMiddleware()) # Let's learn from this invalid node lonely_blockchain_learner._current_teacher_node = worker From f0d07d0297375172ed0162deb4c5644f7c681d2f Mon Sep 17 00:00:00 2001 From: Kieran Prasch Date: Fri, 4 Mar 2022 14:53:13 -0800 Subject: [PATCH 07/19] Clear stored peer metadata every startup; Always use temporary storage areas. --- nucypher/config/base.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/nucypher/config/base.py b/nucypher/config/base.py index edd20f37b..36d167487 100644 --- a/nucypher/config/base.py +++ b/nucypher/config/base.py @@ -636,12 +636,21 @@ class CharacterConfiguration(BaseConfiguration): return self.__dev_mode def _setup_node_storage(self, node_storage=None) -> None: - if self.dev_mode: - node_storage = ForgetfulNodeStorage(registry=self.registry, federated_only=self.federated_only) - elif not node_storage: - node_storage = LocalFileBasedNodeStorage(registry=self.registry, - config_root=self.config_root, - federated_only=self.federated_only) + # TODO: Disables node metadata persistence.. + # if self.dev_mode: + # node_storage = ForgetfulNodeStorage(registry=self.registry, federated_only=self.federated_only) + + # TODO: Forcibly clears the filesystem of any stored node metadata and certificates... + local_node_storage = LocalFileBasedNodeStorage( + registry=self.registry, + config_root=self.config_root, + federated_only=self.federated_only + ) + local_node_storage.clear() + self.log.info(f'Cleared peer metadata from {local_node_storage.root_dir}') + + # TODO: Always sets up nodes for in-memory node metadata storage + node_storage = ForgetfulNodeStorage(registry=self.registry, federated_only=self.federated_only) self.node_storage = node_storage def forget_nodes(self) -> None: From e082f12eac4dc62ff13ae41000883f18cc1c8a34 Mon Sep 17 00:00:00 2001 From: "Kieran R. Prasch" Date: Tue, 22 Mar 2022 13:05:46 -0700 Subject: [PATCH 08/19] revert subscription manager to simpler times --- .../source/contracts/SubscriptionManager.sol | 47 +++++-------------- 1 file changed, 13 insertions(+), 34 deletions(-) diff --git a/nucypher/blockchain/eth/sol/source/contracts/SubscriptionManager.sol b/nucypher/blockchain/eth/sol/source/contracts/SubscriptionManager.sol index c83ee1efc..d37db011b 100644 --- a/nucypher/blockchain/eth/sol/source/contracts/SubscriptionManager.sol +++ b/nucypher/blockchain/eth/sol/source/contracts/SubscriptionManager.sol @@ -2,15 +2,9 @@ pragma solidity ^0.8.0; -import "@openzeppelin-upgradeable/contracts/access/AccessControlUpgradeable.sol"; -import "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol"; +import "../zeppelin/proxy/Initializable.sol"; -contract SubscriptionManager is Initializable, AccessControlUpgradeable { - - bytes32 public constant SET_RATE_ROLE = - keccak256("Power to set the fee rate"); - bytes32 public constant WITHDRAW_ROLE = - keccak256("Power to withdraw funds from SubscriptionManager"); +contract SubscriptionManager is Initializable { // The layout of policy struct is optimized, so sponsor, timestamps and size // fit in a single 256-word. @@ -19,7 +13,6 @@ contract SubscriptionManager is Initializable, AccessControlUpgradeable { uint32 startTimestamp; uint32 endTimestamp; uint16 size; // also known as `N` - // There's still 2 bytes available here address owner; } @@ -29,13 +22,12 @@ contract SubscriptionManager is Initializable, AccessControlUpgradeable { address indexed owner, uint16 size, uint32 startTimestamp, - uint32 endTimestamp, - uint256 cost + uint32 endTimestamp ); event FeeRateUpdated(uint256 oldFeeRate, uint256 newFeeRate); - // Per-second, per-node service fee rate + // Per-second service fee rate uint256 public feeRate; // Mapping that stores policy structs, keyed by policy ID @@ -43,20 +35,6 @@ contract SubscriptionManager is Initializable, AccessControlUpgradeable { function initialize(uint256 _feeRate) public initializer { _setFeeRate(_feeRate); - _setupRole(SET_RATE_ROLE, msg.sender); - _setupRole(WITHDRAW_ROLE, msg.sender); - _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); - } - - function getPolicyCost( - uint16 _size, - uint32 _startTimestamp, - uint32 _endTimestamp - ) public view returns (uint256){ - uint32 duration = _endTimestamp - _startTimestamp; - require(duration > 0, "Invalid timestamps"); - require(_size > 0, "Invalid policy size"); - return feeRate * _size * duration; } function createPolicy( @@ -69,12 +47,14 @@ contract SubscriptionManager is Initializable, AccessControlUpgradeable { external payable { require( - _startTimestamp < _endTimestamp && block.timestamp < _endTimestamp, + _startTimestamp < _endTimestamp && + block.timestamp < _endTimestamp, "Invalid timestamps" ); + uint32 duration = _endTimestamp - _startTimestamp; require( - msg.value == getPolicyCost(_size, _startTimestamp, _endTimestamp), - "Invalid policy cost" + duration > 0 && _size > 0 && + msg.value == feeRate * _size * uint32(duration) ); _createPolicy(_policyId, _policyOwner, _size, _startTimestamp, _endTimestamp); @@ -118,8 +98,7 @@ contract SubscriptionManager is Initializable, AccessControlUpgradeable { _policyOwner == address(0) ? msg.sender : _policyOwner, _size, _startTimestamp, - _endTimestamp, - msg.value + _endTimestamp ); } @@ -137,11 +116,11 @@ contract SubscriptionManager is Initializable, AccessControlUpgradeable { emit FeeRateUpdated(oldFee, newFee); } - function setFeeRate(uint256 _ratePerSecond) onlyRole(SET_RATE_ROLE) external { - _setFeeRate(_ratePerSecond); + function setFeeRate(uint256 _rate_per_second) external { + _setFeeRate(_rate_per_second); } - function sweep(address payable recipient) onlyRole(WITHDRAW_ROLE) external { + function sweep(address payable recipient) external { uint256 balance = address(this).balance; (bool sent, ) = recipient.call{value: balance}(""); require(sent, "Failed transfer"); From 99ac4693c23ee1861e887fa81a1053b4a44cd5df Mon Sep 17 00:00:00 2001 From: "Kieran R. Prasch" Date: Tue, 22 Mar 2022 13:06:54 -0700 Subject: [PATCH 09/19] Handle remote certificate mocks and storage in middleware. --- nucypher/characters/lawful.py | 17 +--- nucypher/network/middleware.py | 100 ++++++++--------------- tests/unit/test_external_ip_utilities.py | 19 +++-- tests/utils/middleware.py | 23 ++---- 4 files changed, 62 insertions(+), 97 deletions(-) diff --git a/nucypher/characters/lawful.py b/nucypher/characters/lawful.py index 3cf5c4ff7..18f3f150c 100644 --- a/nucypher/characters/lawful.py +++ b/nucypher/characters/lawful.py @@ -71,7 +71,7 @@ from nucypher.characters.banners import ALICE_BANNER, BOB_BANNER, ENRICO_BANNER, from nucypher.characters.base import Character, Learner from nucypher.characters.control.interfaces import AliceInterface, BobInterface, EnricoInterface from nucypher.cli.processes import UrsulaCommandProtocol -from nucypher.config.storages import ForgetfulNodeStorage, NodeStorage +from nucypher.config.storages import NodeStorage from nucypher.control.controllers import WebController from nucypher.control.emitters import StdoutEmitter from nucypher.crypto.keypairs import HostingKeypair @@ -84,7 +84,7 @@ from nucypher.crypto.powers import ( TLSHostingPower, ) from nucypher.network.exceptions import NodeSeemsToBeDown -from nucypher.network.middleware import RestMiddleware, fetch_ssl_certificate +from nucypher.network.middleware import RestMiddleware from nucypher.network.nodes import NodeSprout, TEACHER_NODES, Teacher from nucypher.network.protocols import parse_node_uri from nucypher.network.retrieval import RetrievalClient @@ -717,7 +717,7 @@ class Ursula(Teacher, Character, Operator): known_nodes: Iterable[Teacher] = None, **character_kwargs - ) -> None: + ): Character.__init__(self, is_me=is_me, @@ -1153,18 +1153,9 @@ class Ursula(Teacher, Character, Operator): # Parse node URI host, port, staking_provider_address = parse_node_uri(seed_uri) - # Fetch the hosts TLS certificate and read the common name - try: - certificate = fetch_ssl_certificate(host=host, port=port) - except NodeSeemsToBeDown as e: - e.args += (f"While trying to contact {seed_uri}",) - e.crash_right_now = False - raise - real_host = certificate.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value - # Load the host as a potential seed node potential_seed_node = cls.from_rest_url( - host=real_host, + host=host, port=port, network_middleware=network_middleware, ) diff --git a/nucypher/network/middleware.py b/nucypher/network/middleware.py index 173d43a0d..738c80640 100644 --- a/nucypher/network/middleware.py +++ b/nucypher/network/middleware.py @@ -20,7 +20,8 @@ import socket import ssl import time from http import HTTPStatus -from typing import Optional +from pathlib import Path +from typing import Optional, Tuple from typing import Sequence import requests @@ -39,36 +40,6 @@ SSL_LOGGER = Logger('ssl-middleware') EXEMPT_FROM_VERIFICATION.bool_value(False) -def fetch_ssl_certificate(host: str, - port: int, - timeout: int = 3, - retry_attempts: int = 3, - retry_rate: int = 2, - current_attempt: int = 0 - ) -> Certificate: - - socket.setdefaulttimeout(timeout) # Set Socket Timeout - - try: - SSL_LOGGER.debug(f"Fetching {host}:{port} TLS certificate") - certificate = ssl.get_server_certificate(addr=(host, port)) - - except socket.timeout: - if current_attempt == retry_attempts: - message = f"No Response from {host}:{port} after {retry_attempts} attempts" - SSL_LOGGER.info(message) - raise ConnectionRefusedError("No response from {}:{}".format(host, port)) - SSL_LOGGER.debug(f"No Response from {host}:{port}. Retrying in {retry_rate} seconds...") - time.sleep(retry_rate) - return fetch_ssl_certificate(host, port, timeout, retry_attempts, retry_rate, current_attempt + 1) - - except OSError: - raise # TODO: #1835 - - certificate = x509.load_pem_x509_certificate(certificate.encode(), backend=default_backend()) - return certificate - - class NucypherMiddlewareClient: library = requests timeout = 1.2 @@ -83,6 +54,37 @@ class NucypherMiddlewareClient: self.eth_provider_uri = eth_provider_uri self.storage = storage or ForgetfulNodeStorage() # for certificate storage + def get_certificate(self, + host, + port, + timeout=3, + retry_attempts: int = 3, + retry_rate: int = 2, + current_attempt: int = 0): + + socket.setdefaulttimeout(timeout) # Set Socket Timeout + + try: + SSL_LOGGER.info(f"Fetching {host}:{port} TLS certificate") + certificate_pem = ssl.get_server_certificate(addr=(host, port)) + certificate = ssl.PEM_cert_to_DER_cert(certificate_pem) + + except socket.timeout: + if current_attempt == retry_attempts: + message = f"No Response from {host}:{port} after {retry_attempts} attempts" + self.log.info(message) + raise ConnectionRefusedError("No response from {}:{}".format(host, port)) + self.log.info(f"No Response from {host}:{port}. Retrying in {retry_rate} seconds...") + time.sleep(retry_rate) + return self.get_certificate(host, port, timeout, retry_attempts, retry_rate, current_attempt + 1) + + except OSError: + raise # TODO: #1835 + + certificate = x509.load_der_x509_certificate(certificate, backend=default_backend()) + filepath = self.storage.store_node_certificate(certificate=certificate, port=port) + return certificate, filepath + @staticmethod def response_cleaner(response): return response @@ -104,7 +106,7 @@ class NucypherMiddlewareClient: if node: if any((host, port)): raise ValueError("Don't pass host and port if you are passing the node.") - host, port = node.rest_interface + host, port = node.rest_interface.host, node.rest_interface.port elif not (host and port): raise ValueError("You need to pass either the node or a host and port.") return host, port, self.library @@ -148,15 +150,12 @@ class NucypherMiddlewareClient: # Fetch SSL certificate try: - certificate = fetch_ssl_certificate(host=host, port=port) - certificate_filepath = self.storage.store_node_certificate(certificate=certificate, port=port) - - # Handle no response + certificate, filepath = self.get_certificate(host=host, port=port) except NodeSeemsToBeDown as e: raise RestMiddleware.Unreachable(message=f'Node {node_or_sprout} {host}:{port} is unreachable: {e}') # Send request - response = self.invoke_method(method, endpoint, verify=certificate_filepath, *args, **kwargs) + response = self.invoke_method(method, endpoint, verify=filepath, *args, **kwargs) # Handle response cleaned_response = self.response_cleaner(response) @@ -227,33 +226,6 @@ class RestMiddleware: def __init__(self, registry=None, eth_provider_uri: str = None): self.client = self._client_class(registry=registry, eth_provider_uri=eth_provider_uri) - def get_certificate(self, host, port, timeout=3, retry_attempts: int = 3, retry_rate: int = 2, - current_attempt: int = 0): - - socket.setdefaulttimeout(timeout) # Set Socket Timeout - - try: - self.log.info(f"Fetching seednode {host}:{port} TLS certificate") - seednode_certificate_pem = ssl.get_server_certificate(addr=(host, port)) - seednode_certificate = ssl.PEM_cert_to_DER_cert(seednode_certificate_pem) - - except socket.timeout: - if current_attempt == retry_attempts: - message = f"No Response from seednode {host}:{port} after {retry_attempts} attempts" - self.log.info(message) - raise ConnectionRefusedError("No response from {}:{}".format(host, port)) - self.log.info(f"No Response from seednode {host}:{port}. Retrying in {retry_rate} seconds...") - time.sleep(retry_rate) - return self.get_certificate(host, port, timeout, retry_attempts, retry_rate, current_attempt + 1) - - except OSError: - raise # TODO: #1835 - - else: - certificate = x509.load_der_x509_certificate(seednode_certificate, - backend=default_backend()) - return certificate - def request_revocation(self, ursula, revocation): # TODO: Implement offchain revocation #2787 response = self.client.post( diff --git a/tests/unit/test_external_ip_utilities.py b/tests/unit/test_external_ip_utilities.py index d60ef79f4..ca12cd8f2 100644 --- a/tests/unit/test_external_ip_utilities.py +++ b/tests/unit/test_external_ip_utilities.py @@ -14,26 +14,27 @@ You should have received a copy of the GNU Affero General Public License along with nucypher. If not, see . """ +from pathlib import Path -from eth_utils import to_checksum_address import pytest - +from eth_utils import to_checksum_address from nucypher_core import NodeMetadata, NodeMetadataPayload from nucypher_core.umbral import SecretKey, Signer from nucypher.acumen.perception import FleetSensor -from nucypher.blockchain.eth.constants import LENGTH_ECDSA_SIGNATURE_WITH_RECOVERY from nucypher.characters.lawful import Ursula +from nucypher.crypto.tls import generate_self_signed_certificate from nucypher.network.exceptions import NodeSeemsToBeDown from nucypher.network.middleware import NucypherMiddlewareClient from nucypher.network.nodes import TEACHER_NODES +from nucypher.network.protocols import InterfaceInfo from nucypher.utilities.networking import ( determine_external_ip_address, get_external_ip_from_centralized_source, get_external_ip_from_default_teacher, get_external_ip_from_known_nodes, CENTRALIZED_IP_ORACLE_URL, - UnknownIPAddress + UnknownIPAddress, LOOPBACK_ADDRESS ) from tests.constants import MOCK_IP_ADDRESS @@ -66,6 +67,10 @@ class Dummy: # Teacher def rest_url(self): return MOCK_IP_ADDRESS + @property + def rest_interface(self): + return InterfaceInfo(host=MOCK_IP_ADDRESS, port=1111) + def metadata(self): signer = Signer(SecretKey.random()) @@ -95,6 +100,8 @@ def mock_requests(mocker): @pytest.fixture(autouse=True) def mock_client(mocker): + cert, pk = generate_self_signed_certificate(host=LOOPBACK_ADDRESS) + mocker.patch.object(NucypherMiddlewareClient, 'get_certificate', return_value=(cert, Path())) yield mocker.patch.object(NucypherMiddlewareClient, 'invoke_method', return_value=Dummy.GoodResponse) @@ -169,7 +176,7 @@ def test_get_external_ip_from_known_nodes_client(mocker, mock_client): function, endpoint = mock_client.call_args[0] assert function.__name__ == 'get' - assert endpoint == f'https://{teacher_uri}/ping' + # assert endpoint == f'https://{teacher_uri}/ping' def test_get_external_ip_default_teacher_unreachable(mocker): @@ -195,7 +202,7 @@ def test_get_external_ip_from_default_teacher(mocker, mock_client, mock_requests mock_client.assert_called_once() function, endpoint = mock_client.call_args[0] assert function.__name__ == 'get' - assert endpoint == f'https://{teacher_uri}/ping' + # assert endpoint == f'https://{teacher_uri}/ping' def test_get_external_ip_default_unknown_network(): diff --git a/tests/utils/middleware.py b/tests/utils/middleware.py index 5934f1b5e..86419306d 100644 --- a/tests/utils/middleware.py +++ b/tests/utils/middleware.py @@ -14,19 +14,16 @@ 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 . """ -import time import random -import os +import socket +import time +from pathlib import Path import requests -import socket -from constant_sorrow.constants import CERTIFICATE_NOT_SAVED from flask import Response - from nucypher_core import MetadataRequest, FleetStateChecksum from nucypher.characters.lawful import Ursula -from nucypher.config.constants import TEMPORARY_DOMAIN from nucypher.network.middleware import NucypherMiddlewareClient, RestMiddleware from tests.utils.ursula import MOCK_KNOWN_URSULAS_CACHE @@ -76,9 +73,8 @@ class _TestMiddlewareClient(NucypherMiddlewareClient): mock_client = self._get_mock_client_by_port(port) else: raise ValueError("You need to pass either the node or a host and port.") - - # We don't use certs in mock-style tests anyway. - return node.rest_url(), CERTIFICATE_NOT_SAVED, mock_client + host, port = node.rest_interface.host, node.rest_interface.port + return host, port, mock_client def invoke_method(self, method, url, *args, **kwargs): _cert_location = kwargs.pop("verify") # TODO: Is this something that can be meaningfully tested? @@ -89,6 +85,10 @@ class _TestMiddlewareClient(NucypherMiddlewareClient): def clean_params(self, request_kwargs): request_kwargs["query_string"] = request_kwargs.pop("params", {}) + def get_certificate(self, port, *args, **kwargs): + ursula = self._get_ursula_by_port(port) + return ursula.certificate, Path() + class MockRestMiddleware(RestMiddleware): _ursulas = None @@ -98,11 +98,6 @@ class MockRestMiddleware(RestMiddleware): class NotEnoughMockUrsulas(Ursula.NotEnoughUrsulas): pass - def get_certificate(self, host, port, timeout=3, retry_attempts: int = 3, retry_rate: int = 2, - current_attempt: int = 0): - ursula = self.client._get_ursula_by_port(port) - return ursula.certificate - class MockRestMiddlewareForLargeFleetTests(MockRestMiddleware): """ From aad511ba30a7481be080879377d393041bf002a1 Mon Sep 17 00:00:00 2001 From: "Kieran R. Prasch" Date: Tue, 22 Mar 2022 13:08:13 -0700 Subject: [PATCH 10/19] test adjustments to handle policy registry, in-memory node storage and polygon mocks. --- nucypher/config/base.py | 30 ++++++++++--------- nucypher/network/middleware.py | 2 +- nucypher/network/nodes.py | 7 +++-- .../blockchain/agents/test_contract_agency.py | 4 +-- .../characters/test_decentralized_grant.py | 2 +- tests/acceptance/cli/test_alice.py | 4 ++- tests/acceptance/cli/test_bob.py | 4 ++- tests/acceptance/cli/test_cli_config.py | 4 ++- .../cli/test_mixed_configurations.py | 9 +++++- .../cli/ursula/test_federated_ursula.py | 4 ++- .../cli/actions/test_auth_actions.py | 7 ++++- .../config/test_character_configuration.py | 14 +++++++-- .../control/test_character_fields.py | 6 ++-- tests/utils/config.py | 8 +++-- 14 files changed, 69 insertions(+), 36 deletions(-) diff --git a/nucypher/config/base.py b/nucypher/config/base.py index 36d167487..b6f305174 100644 --- a/nucypher/config/base.py +++ b/nucypher/config/base.py @@ -543,21 +543,23 @@ class CharacterConfiguration(BaseConfiguration): # Onchain Payments & Policies # - # TODO: Enforce this for Ursula/Alice but not Bob? - # if not payment_provider: - # raise self.ConfigurationError("payment provider is required.") - self.payment_method = payment_method or self.DEFAULT_PAYMENT_METHOD - self.payment_network = payment_network or self.DEFAULT_PAYMENT_NETWORK - self.payment_provider = payment_provider or (self.eth_provider_uri or None) # default to L1 payments + # FIXME: Enforce this for Ursula/Alice but not Bob? + from nucypher.config.characters import BobConfiguration + if not isinstance(self, BobConfiguration): + # if not payment_provider: + # raise self.ConfigurationError("payment provider is required.") + self.payment_method = payment_method or self.DEFAULT_PAYMENT_METHOD + self.payment_network = payment_network or self.DEFAULT_PAYMENT_NETWORK + self.payment_provider = payment_provider or (self.eth_provider_uri or None) # default to L1 payments - # TODO: Dedupe - if not self.policy_registry: - if not self.policy_registry_filepath: - self.log.info(f"Fetching latest policy registry from source.") - self.policy_registry = InMemoryContractRegistry.from_latest_publication(network=self.payment_network) - else: - self.policy_registry = LocalContractRegistry(filepath=self.policy_registry_filepath) - self.log.info(f"Using local policy registry ({self.policy_registry}).") + # TODO: Dedupe + if not self.policy_registry: + if not self.policy_registry_filepath: + self.log.info(f"Fetching latest policy registry from source.") + self.policy_registry = InMemoryContractRegistry.from_latest_publication(network=self.payment_network) + else: + self.policy_registry = LocalContractRegistry(filepath=self.policy_registry_filepath) + self.log.info(f"Using local policy registry ({self.policy_registry}).") if dev_mode: self.__temp_dir = UNINITIALIZED_CONFIGURATION diff --git a/nucypher/network/middleware.py b/nucypher/network/middleware.py index 738c80640..717d76361 100644 --- a/nucypher/network/middleware.py +++ b/nucypher/network/middleware.py @@ -74,7 +74,7 @@ class NucypherMiddlewareClient: message = f"No Response from {host}:{port} after {retry_attempts} attempts" self.log.info(message) raise ConnectionRefusedError("No response from {}:{}".format(host, port)) - self.log.info(f"No Response from {host}:{port}. Retrying in {retry_rate} seconds...") + SSL_LOGGER.info(f"No Response from {host}:{port}. Retrying in {retry_rate} seconds...") time.sleep(retry_rate) return self.get_certificate(host, port, timeout, retry_attempts, retry_rate, current_attempt + 1) diff --git a/nucypher/network/nodes.py b/nucypher/network/nodes.py index 1cfd0d914..8a6e26a04 100644 --- a/nucypher/network/nodes.py +++ b/nucypher/network/nodes.py @@ -1059,17 +1059,17 @@ class Teacher: "but is OK to use in federated mode if you " \ "have reason to believe it is trustworthy." raise self.WrongMode(message) - elif not registry: - self.log.info('No registry provided for staking verification.') # Decentralized else: # Try to derive the worker address if it hasn't been derived yet. try: + # TODO: This is overtly implicit _operator_address = self.operator_address except Exception as e: raise self.InvalidOperatorSignature(str(e)) from e + self.verified_stamp = True # TODO: Does this belong here? # On-chain staking check, if registry is present if registry: @@ -1085,7 +1085,8 @@ class Teacher: else: raise self.NotStaking(f"{self.checksum_address} is not staking") - self.verified_stamp = True # TODO: Why is this here? + else: + self.log.info('No registry provided for staking verification.') def validate_metadata_signature(self) -> bool: """Checks that the interface info is valid for this node's canonical address.""" diff --git a/tests/acceptance/blockchain/agents/test_contract_agency.py b/tests/acceptance/blockchain/agents/test_contract_agency.py index b4c37d890..8bab62af2 100644 --- a/tests/acceptance/blockchain/agents/test_contract_agency.py +++ b/tests/acceptance/blockchain/agents/test_contract_agency.py @@ -22,10 +22,10 @@ def test_get_agent_with_different_registries(application_economics, agency, test # Get agents using same registry instance application_agent_1 = ContractAgency.get_agent(PREApplicationAgent, registry=test_registry) application_agent_2 = ContractAgency.get_agent(PREApplicationAgent, registry=test_registry) - assert application_agent_2.registry_str == application_agent_1.registry_str == str(test_registry) + assert application_agent_2.registry == application_agent_1.registry == test_registry assert application_agent_2 is application_agent_1 # Same content but different classes of registries application_agent_2 = ContractAgency.get_agent(PREApplicationAgent, registry=agency_local_registry) - assert application_agent_2.registry_str == str(test_registry) + assert application_agent_2.registry == test_registry assert application_agent_2 is application_agent_1 diff --git a/tests/acceptance/characters/test_decentralized_grant.py b/tests/acceptance/characters/test_decentralized_grant.py index 88b42afe6..d497e41c1 100644 --- a/tests/acceptance/characters/test_decentralized_grant.py +++ b/tests/acceptance/characters/test_decentralized_grant.py @@ -47,7 +47,7 @@ def check(policy, bob, ursulas): def test_decentralized_grant_subscription_manager(blockchain_alice, blockchain_bob, blockchain_ursulas): - payment_method = SubscriptionManagerPayment(provider=TEST_ETH_PROVIDER_URI, network=TEMPORARY_DOMAIN) + payment_method = SubscriptionManagerPayment(eth_provider=TEST_ETH_PROVIDER_URI, network=TEMPORARY_DOMAIN) blockchain_alice.payment_method = payment_method policy = blockchain_alice.grant(bob=blockchain_bob, label=os.urandom(16), diff --git a/tests/acceptance/cli/test_alice.py b/tests/acceptance/cli/test_alice.py index 03cadf609..737de5e33 100644 --- a/tests/acceptance/cli/test_alice.py +++ b/tests/acceptance/cli/test_alice.py @@ -115,7 +115,9 @@ def test_initialize_alice_with_custom_configuration_root(custom_filepath, click_ # Files and Directories assert custom_filepath.is_dir(), 'Configuration file does not exist' assert (custom_filepath / 'keystore').is_dir(), 'Keystore does not exist' - assert (custom_filepath / 'known_nodes').is_dir(), 'known_nodes directory does not exist' + + # TODO: Only using in-memory node storage for now + # assert (custom_filepath / 'known_nodes').is_dir(), 'known_nodes directory does not exist' custom_config_filepath = custom_filepath / AliceConfiguration.generate_filename() assert custom_config_filepath.is_file(), 'Configuration file does not exist' diff --git a/tests/acceptance/cli/test_bob.py b/tests/acceptance/cli/test_bob.py index 56b1af71e..665e6efb9 100644 --- a/tests/acceptance/cli/test_bob.py +++ b/tests/acceptance/cli/test_bob.py @@ -64,7 +64,9 @@ def test_initialize_bob_with_custom_configuration_root(click_runner, custom_file # Files and Directories assert custom_filepath.is_dir(), 'Configuration file does not exist' assert (custom_filepath / 'keystore').is_dir(), 'Keystore does not exist' - assert (custom_filepath / 'known_nodes').is_dir(), 'known_nodes directory does not exist' + + # TODO: Only using in-memory node storage for now + # assert (custom_filepath / 'known_nodes').is_dir(), 'known_nodes directory does not exist' custom_config_filepath = custom_filepath / BobConfiguration.generate_filename() assert custom_config_filepath.is_file(), 'Configuration file does not exist' diff --git a/tests/acceptance/cli/test_cli_config.py b/tests/acceptance/cli/test_cli_config.py index e7456f495..42408708e 100644 --- a/tests/acceptance/cli/test_cli_config.py +++ b/tests/acceptance/cli/test_cli_config.py @@ -67,7 +67,9 @@ def test_initialize_via_cli(config_class, custom_filepath: Path, click_runner, m # Files and Directories assert custom_filepath.is_dir(), 'Configuration file does not exist' assert (custom_filepath / 'keystore').is_dir(), 'Keystore does not exist' - assert (custom_filepath / 'known_nodes').is_dir(), 'known_nodes directory does not exist' + + # TODO: Only using in-memory node storage for now + # assert (custom_filepath / 'known_nodes').is_dir(), 'known_nodes directory does not exist' @pytest.mark.parametrize('config_class', CONFIG_CLASSES) diff --git a/tests/acceptance/cli/test_mixed_configurations.py b/tests/acceptance/cli/test_mixed_configurations.py index 097972bd2..a5a871006 100644 --- a/tests/acceptance/cli/test_mixed_configurations.py +++ b/tests/acceptance/cli/test_mixed_configurations.py @@ -284,7 +284,14 @@ def test_corrupted_configuration(click_runner, # Ensure configuration creation top_level_config_root = [f.name for f in custom_filepath.iterdir()] assert default_filename in top_level_config_root, "JSON configuration file was not created" - for field in ['known_nodes', 'keystore', default_filename]: + + expected_fields = [ + # TODO: Only using in-memory node storage for now + # 'known_nodes', + 'keystore', + default_filename + ] + for field in expected_fields: assert field in top_level_config_root # "Corrupt" the configuration by removing the contract registry diff --git a/tests/acceptance/cli/ursula/test_federated_ursula.py b/tests/acceptance/cli/ursula/test_federated_ursula.py index 3d4fd05a0..05f432db4 100644 --- a/tests/acceptance/cli/ursula/test_federated_ursula.py +++ b/tests/acceptance/cli/ursula/test_federated_ursula.py @@ -84,7 +84,9 @@ def test_initialize_custom_configuration_root(click_runner, custom_filepath: Pat # Files and Directories assert custom_filepath.is_dir(), 'Configuration file does not exist' assert (custom_filepath / 'keystore').is_dir(), 'KEYSTORE does not exist' - assert (custom_filepath / 'known_nodes').is_dir(), 'known_nodes directory does not exist' + + # TODO: Only using in-memory node storage for now + # assert (custom_filepath / 'known_nodes').is_dir(), 'known_nodes directory does not exist' custom_config_filepath = custom_filepath / UrsulaConfiguration.generate_filename() assert custom_config_filepath.is_file(), 'Configuration file does not exist' diff --git a/tests/integration/cli/actions/test_auth_actions.py b/tests/integration/cli/actions/test_auth_actions.py index 64bbb124f..f070e955b 100644 --- a/tests/integration/cli/actions/test_auth_actions.py +++ b/tests/integration/cli/actions/test_auth_actions.py @@ -104,7 +104,12 @@ def test_get_nucypher_password(mock_stdin, mock_account, confirm, capsys): assert prompt in captured.out -def test_unlock_nucypher_keystore_invalid_password(mocker, test_emitter, alice_blockchain_test_config, capsys, tmpdir): +def test_unlock_nucypher_keystore_invalid_password(mocker, + test_emitter, + alice_blockchain_test_config, + capsys, + tmpdir, + test_registry_source_manager): # Setup mocker.patch.object(passwords, 'secret_box_decrypt', side_effect=SecretBoxAuthenticationError) diff --git a/tests/integration/config/test_character_configuration.py b/tests/integration/config/test_character_configuration.py index efb35f416..d7ebe1776 100644 --- a/tests/integration/config/test_character_configuration.py +++ b/tests/integration/config/test_character_configuration.py @@ -95,9 +95,12 @@ def test_federated_development_character_configurations(character, configuration alice.disenchant() -# TODO: This test is unnecessarily slow due to the blockchain configurations. Perhaps we should mock them -- See #2230 @pytest.mark.parametrize('configuration_class', all_configurations) -def test_default_character_configuration_preservation(configuration_class, testerchain, test_registry_source_manager, tmpdir): +def test_default_character_configuration_preservation(configuration_class, + mock_testerchain, + test_registry_source_manager, + tmpdir, + test_registry): configuration_class.DEFAULT_CONFIG_ROOT = Path('/tmp') fake_address = '0xdeadbeef' @@ -121,10 +124,15 @@ def test_default_character_configuration_preservation(configuration_class, teste domain=network, rest_host=MOCK_IP_ADDRESS, payment_provider=MOCK_ETH_PROVIDER_URI, + policy_registry=test_registry, + payment_network=TEMPORARY_DOMAIN, keystore=keystore) else: - character_config = configuration_class(checksum_address=fake_address, domain=network) + character_config = configuration_class(checksum_address=fake_address, + domain=network, + payment_network=TEMPORARY_DOMAIN, + policy_registry=test_registry) generated_filepath = character_config.generate_filepath() assert generated_filepath == expected_filepath diff --git a/tests/unit/characters/control/test_character_fields.py b/tests/unit/characters/control/test_character_fields.py index d98e786b0..b0588007d 100644 --- a/tests/unit/characters/control/test_character_fields.py +++ b/tests/unit/characters/control/test_character_fields.py @@ -15,15 +15,14 @@ You should have received a copy of the GNU Affero General Public License along with nucypher. If not, see . """ import datetime -from base64 import b64encode, b64decode +from base64 import b64encode import maya import pytest - from nucypher_core import ( MessageKit as MessageKitClass, EncryptedTreasureMap as EncryptedTreasureMapClass) -from nucypher_core.umbral import SecretKey, Signer +from nucypher_core.umbral import SecretKey from nucypher.characters.control.specifications.fields import ( DateTime, @@ -48,6 +47,7 @@ from nucypher.control.specifications.exceptions import InvalidInputData # assert deserialized == data + def test_file(tmpdir): text = b"I never saw a wild thing sorry for itself. A small bird will drop frozen dead from a bough without " \ b"ever having felt sorry for itself." # -- D.H. Lawrence diff --git a/tests/utils/config.py b/tests/utils/config.py index 4d61580d2..84c935faa 100644 --- a/tests/utils/config.py +++ b/tests/utils/config.py @@ -74,7 +74,8 @@ def make_ursula_test_configuration(rest_port: int = MOCK_URSULA_STARTING_PORT, ursula_config = UrsulaConfiguration(**test_params, rest_port=rest_port, payment_provider=payment_provider, - payment_network=payment_network) + payment_network=payment_network, + policy_registry=test_params['registry']) return ursula_config @@ -86,11 +87,12 @@ def make_alice_test_configuration(payment_provider: str = None, payment_network = TEMPORARY_DOMAIN if not federated else None config = AliceConfiguration(**test_params, payment_provider=payment_provider, - payment_network=payment_network) + payment_network=payment_network, + policy_registry=test_params['registry']) return config def make_bob_test_configuration(**assemble_kwargs) -> BobConfiguration: test_params = assemble(**assemble_kwargs) - config = BobConfiguration(**test_params) + config = BobConfiguration(**test_params, policy_registry=test_params['registry']) return config From 47e205e3d6fc3c4ec56dfad726f9b486fbcde438 Mon Sep 17 00:00:00 2001 From: "Kieran R. Prasch" Date: Tue, 22 Mar 2022 15:33:43 -0700 Subject: [PATCH 11/19] bump probation --- nucypher/config/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nucypher/config/constants.py b/nucypher/config/constants.py index 05a066c79..efeb017ad 100644 --- a/nucypher/config/constants.py +++ b/nucypher/config/constants.py @@ -75,4 +75,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-3-17T23:59:59.0Z') +END_OF_POLICIES_PROBATIONARY_PERIOD = MayaDT.from_iso8601('2022-6-16T23:59:59.0Z') From a1c8dea4b475db1629c56d5d34c810b7672c5c36 Mon Sep 17 00:00:00 2001 From: "Kieran R. Prasch" Date: Tue, 22 Mar 2022 15:39:54 -0700 Subject: [PATCH 12/19] newsfragment for PR #2873 --- newsfragments/2873.feature.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 newsfragments/2873.feature.rst diff --git a/newsfragments/2873.feature.rst b/newsfragments/2873.feature.rst new file mode 100644 index 000000000..e4df35ee9 --- /dev/null +++ b/newsfragments/2873.feature.rst @@ -0,0 +1,3 @@ +- Full support of policy payments sumitted to polygon in demos and top-level APIs. +- Improved certificate handling for network requests. +- Prioritizes in-memory node storage for all node runtimes. From fc943eff926ee819276b0e7cd60bb166a84d5dc6 Mon Sep 17 00:00:00 2001 From: "Kieran R. Prasch" Date: Tue, 22 Mar 2022 15:43:40 -0700 Subject: [PATCH 13/19] RFCs for PR #2873 --- nucypher/network/nodes.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/nucypher/network/nodes.py b/nucypher/network/nodes.py index 8a6e26a04..4f987c813 100644 --- a/nucypher/network/nodes.py +++ b/nucypher/network/nodes.py @@ -802,8 +802,7 @@ class Learner: # These except clauses apply to the current_teacher itself, not the learned-about nodes. except NodeSeemsToBeDown as e: unresponsive_nodes.add(current_teacher) - raise - self.log.info(f"Teacher {str(current_teacher)} is perhaps down:{e}.") # FIXME: This was printing the node bytestring. Is this really necessary? #1712 + self.log.info(f"Teacher {current_teacher.seed_node_metadata(as_teacher_uri=True)} is perhaps down:{e}.") return except current_teacher.InvalidNode as e: # Ugh. The teacher is invalid. Rough. From cb49fd4c034bc7a10ac0b91ab742de812897ad08 Mon Sep 17 00:00:00 2001 From: "Kieran R. Prasch" Date: Tue, 22 Mar 2022 17:08:09 -0700 Subject: [PATCH 14/19] Restores seednode host/ip resolution via certificates. --- nucypher/characters/lawful.py | 11 ++++++++++- nucypher/network/middleware.py | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/nucypher/characters/lawful.py b/nucypher/characters/lawful.py index 18f3f150c..5dbc76037 100644 --- a/nucypher/characters/lawful.py +++ b/nucypher/characters/lawful.py @@ -1153,9 +1153,18 @@ class Ursula(Teacher, Character, Operator): # Parse node URI host, port, staking_provider_address = parse_node_uri(seed_uri) + # Fetch the hosts TLS certificate and read the common name + try: + certificate, _filepath = network_middleware.client.get_certificate(host=host, port=port) + except NodeSeemsToBeDown as e: + e.args += (f"While trying to load seednode {seed_uri}",) + e.crash_right_now = True + raise + real_host = certificate.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value + # Load the host as a potential seed node potential_seed_node = cls.from_rest_url( - host=host, + host=real_host, port=port, network_middleware=network_middleware, ) diff --git a/nucypher/network/middleware.py b/nucypher/network/middleware.py index 717d76361..6a2792290 100644 --- a/nucypher/network/middleware.py +++ b/nucypher/network/middleware.py @@ -72,7 +72,7 @@ class NucypherMiddlewareClient: except socket.timeout: if current_attempt == retry_attempts: message = f"No Response from {host}:{port} after {retry_attempts} attempts" - self.log.info(message) + SSL_LOGGER.info(message) raise ConnectionRefusedError("No response from {}:{}".format(host, port)) SSL_LOGGER.info(f"No Response from {host}:{port}. Retrying in {retry_rate} seconds...") time.sleep(retry_rate) From 03f598f987d48ad53b7661ddd422f46de400e65a Mon Sep 17 00:00:00 2001 From: Damon Ciarelli Date: Wed, 23 Mar 2022 11:24:37 -0700 Subject: [PATCH 15/19] update porter Dockerfile so it builds --- deploy/docker/porter/Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/deploy/docker/porter/Dockerfile b/deploy/docker/porter/Dockerfile index 477ff7354..369a07a42 100644 --- a/deploy/docker/porter/Dockerfile +++ b/deploy/docker/porter/Dockerfile @@ -1,8 +1,8 @@ -FROM python:3.8.7-slim +FROM python:3.8.12-slim # Update -RUN apt update -y && apt upgrade -y -RUN apt install patch gcc libffi-dev wget git -y +RUN apt-get update -y && apt upgrade -y +RUN apt-get install patch gcc libffi-dev wget git -y WORKDIR /code COPY . /code From 5f6bee537c6a80f84f6ba0e9330bd8a99d0338c3 Mon Sep 17 00:00:00 2001 From: derekpierre Date: Wed, 23 Mar 2022 11:24:38 -0400 Subject: [PATCH 16/19] Enforce consistency of IP address and port when testing external ip utilities. --- tests/unit/test_external_ip_utilities.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/tests/unit/test_external_ip_utilities.py b/tests/unit/test_external_ip_utilities.py index ca12cd8f2..be2e99830 100644 --- a/tests/unit/test_external_ip_utilities.py +++ b/tests/unit/test_external_ip_utilities.py @@ -34,11 +34,12 @@ from nucypher.utilities.networking import ( get_external_ip_from_default_teacher, get_external_ip_from_known_nodes, CENTRALIZED_IP_ORACLE_URL, - UnknownIPAddress, LOOPBACK_ADDRESS + UnknownIPAddress ) from tests.constants import MOCK_IP_ADDRESS MOCK_NETWORK = 'holodeck' +MOCK_PORT = 1111 class Dummy: # Teacher @@ -69,7 +70,7 @@ class Dummy: # Teacher @property def rest_interface(self): - return InterfaceInfo(host=MOCK_IP_ADDRESS, port=1111) + return InterfaceInfo(host=MOCK_IP_ADDRESS, port=MOCK_PORT) def metadata(self): signer = Signer(SecretKey.random()) @@ -84,8 +85,8 @@ class Dummy: # Teacher verifying_key=signer.verifying_key(), encrypting_key=SecretKey.random().public_key(), certificate_der=b'not a certificate', - host='127.0.0.1', - port=1111, + host=MOCK_IP_ADDRESS, + port=MOCK_PORT, ) return NodeMetadata(signer=signer, payload=payload) @@ -100,14 +101,14 @@ def mock_requests(mocker): @pytest.fixture(autouse=True) def mock_client(mocker): - cert, pk = generate_self_signed_certificate(host=LOOPBACK_ADDRESS) + cert, pk = generate_self_signed_certificate(host=MOCK_IP_ADDRESS) mocker.patch.object(NucypherMiddlewareClient, 'get_certificate', return_value=(cert, Path())) yield mocker.patch.object(NucypherMiddlewareClient, 'invoke_method', return_value=Dummy.GoodResponse) @pytest.fixture(autouse=True) def mock_default_teachers(mocker): - teachers = {MOCK_NETWORK: (MOCK_IP_ADDRESS, )} + teachers = {MOCK_NETWORK: (f"{MOCK_IP_ADDRESS}:{MOCK_PORT}", )} mocker.patch.dict(TEACHER_NODES, teachers, clear=True) @@ -176,7 +177,7 @@ def test_get_external_ip_from_known_nodes_client(mocker, mock_client): function, endpoint = mock_client.call_args[0] assert function.__name__ == 'get' - # assert endpoint == f'https://{teacher_uri}/ping' + assert endpoint == f'https://{teacher_uri}/ping' def test_get_external_ip_default_teacher_unreachable(mocker): @@ -202,7 +203,7 @@ def test_get_external_ip_from_default_teacher(mocker, mock_client, mock_requests mock_client.assert_called_once() function, endpoint = mock_client.call_args[0] assert function.__name__ == 'get' - # assert endpoint == f'https://{teacher_uri}/ping' + assert endpoint == f'https://{teacher_uri}/ping' def test_get_external_ip_default_unknown_network(): From 68748af1a298a1d7a18c7996d2e909fbda0a9334 Mon Sep 17 00:00:00 2001 From: DAMON CIARELLI Date: Thu, 24 Mar 2022 11:35:30 -0700 Subject: [PATCH 17/19] NUCYPHER_OPERATOR_ETH_PASSWORD --- nucypher/config/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nucypher/config/constants.py b/nucypher/config/constants.py index efeb017ad..4dbe0f16c 100644 --- a/nucypher/config/constants.py +++ b/nucypher/config/constants.py @@ -29,7 +29,7 @@ import nucypher # Environment variables NUCYPHER_ENVVAR_KEYSTORE_PASSWORD = "NUCYPHER_KEYSTORE_PASSWORD" NUCYPHER_ENVVAR_OPERATOR_ADDRESS = "NUCYPHER_OPERATOR_ADDRESS" -NUCYPHER_ENVVAR_OPERATOR_ETH_PASSWORD = "NUCYPHER_WORKER_ETH_PASSWORD" +NUCYPHER_ENVVAR_OPERATOR_ETH_PASSWORD = "NUCYPHER_OPERATOR_ETH_PASSWORD" NUCYPHER_ENVVAR_STAKING_PROVIDER_ETH_PASSWORD = "NUCYPHER_STAKING_PROVIDER_ETH_PASSWORD" NUCYPHER_ENVVAR_ALICE_ETH_PASSWORD = "NUCYPHER_ALICE_ETH_PASSWORD" NUCYPHER_ENVVAR_BOB_ETH_PASSWORD = "NUCYPHER_BOB_ETH_PASSWORD" From 18d24d1fe2f73cab7413665096dd66d08c1ddc0e Mon Sep 17 00:00:00 2001 From: Kieran Prasch Date: Thu, 24 Mar 2022 13:17:34 -0700 Subject: [PATCH 18/19] Responds to RFCs for PR #2873 --- examples/finnegans_wake_demo/finnegans-wake-demo-testnet-l2.py | 2 +- nucypher/blockchain/eth/actors.py | 2 +- nucypher/network/nodes.py | 2 +- tests/acceptance/cli/test_alice.py | 1 + tests/acceptance/cli/test_bob.py | 1 + tests/acceptance/cli/test_cli_config.py | 2 +- tests/acceptance/cli/ursula/test_federated_ursula.py | 1 + tests/utils/config.py | 2 +- tests/utils/middleware.py | 2 ++ 9 files changed, 10 insertions(+), 5 deletions(-) diff --git a/examples/finnegans_wake_demo/finnegans-wake-demo-testnet-l2.py b/examples/finnegans_wake_demo/finnegans-wake-demo-testnet-l2.py index 239f4df4f..ee9ddd741 100644 --- a/examples/finnegans_wake_demo/finnegans-wake-demo-testnet-l2.py +++ b/examples/finnegans_wake_demo/finnegans-wake-demo-testnet-l2.py @@ -128,7 +128,7 @@ remote_bob = Bob.from_public_keys( # In this example bob will be granted access for 1 day, # trusting 2 of 3 nodes paying each of them 50 gwei per period. expiration = maya.now() + datetime.timedelta(days=1) -threshold, shares = 1, 2 +threshold, shares = 2, 3 price = alice.payment_method.quote(expiration=expiration.epoch, shares=shares).value # Alice grants access to Bob... diff --git a/nucypher/blockchain/eth/actors.py b/nucypher/blockchain/eth/actors.py index 1bbb1a13b..4a22f6356 100644 --- a/nucypher/blockchain/eth/actors.py +++ b/nucypher/blockchain/eth/actors.py @@ -383,7 +383,7 @@ class Operator(BaseActor): class BlockchainPolicyAuthor(NucypherTokenActor): """Alice base class for blockchain operations, mocking up new policies!""" - def __init__(self, eth_provider_uri, *args, **kwargs): + def __init__(self, eth_provider_uri: str, *args, **kwargs): super().__init__(*args, **kwargs) self.application_agent = ContractAgency.get_agent( PREApplicationAgent, diff --git a/nucypher/network/nodes.py b/nucypher/network/nodes.py index 4f987c813..72a1992fe 100644 --- a/nucypher/network/nodes.py +++ b/nucypher/network/nodes.py @@ -802,7 +802,7 @@ class Learner: # These except clauses apply to the current_teacher itself, not the learned-about nodes. except NodeSeemsToBeDown as e: unresponsive_nodes.add(current_teacher) - self.log.info(f"Teacher {current_teacher.seed_node_metadata(as_teacher_uri=True)} is perhaps down:{e}.") + self.log.info(f"Teacher {current_teacher.seed_node_metadata(as_teacher_uri=True)} is unreachable: {e}.") return except current_teacher.InvalidNode as e: # Ugh. The teacher is invalid. Rough. diff --git a/tests/acceptance/cli/test_alice.py b/tests/acceptance/cli/test_alice.py index 737de5e33..cd9a0a660 100644 --- a/tests/acceptance/cli/test_alice.py +++ b/tests/acceptance/cli/test_alice.py @@ -118,6 +118,7 @@ def test_initialize_alice_with_custom_configuration_root(custom_filepath, click_ # TODO: Only using in-memory node storage for now # assert (custom_filepath / 'known_nodes').is_dir(), 'known_nodes directory does not exist' + assert not (custom_filepath / 'known_nodes').is_dir(), 'known_nodes directory does not exist' custom_config_filepath = custom_filepath / AliceConfiguration.generate_filename() assert custom_config_filepath.is_file(), 'Configuration file does not exist' diff --git a/tests/acceptance/cli/test_bob.py b/tests/acceptance/cli/test_bob.py index 665e6efb9..63b1ef572 100644 --- a/tests/acceptance/cli/test_bob.py +++ b/tests/acceptance/cli/test_bob.py @@ -67,6 +67,7 @@ def test_initialize_bob_with_custom_configuration_root(click_runner, custom_file # TODO: Only using in-memory node storage for now # assert (custom_filepath / 'known_nodes').is_dir(), 'known_nodes directory does not exist' + assert not (custom_filepath / 'known_nodes').is_dir(), 'known_nodes directory does not exist' custom_config_filepath = custom_filepath / BobConfiguration.generate_filename() assert custom_config_filepath.is_file(), 'Configuration file does not exist' diff --git a/tests/acceptance/cli/test_cli_config.py b/tests/acceptance/cli/test_cli_config.py index 42408708e..2b8221c98 100644 --- a/tests/acceptance/cli/test_cli_config.py +++ b/tests/acceptance/cli/test_cli_config.py @@ -70,7 +70,7 @@ def test_initialize_via_cli(config_class, custom_filepath: Path, click_runner, m # TODO: Only using in-memory node storage for now # assert (custom_filepath / 'known_nodes').is_dir(), 'known_nodes directory does not exist' - + assert not (custom_filepath / 'known_nodes').is_dir(), 'known_nodes directory does not exist' @pytest.mark.parametrize('config_class', CONFIG_CLASSES) def test_reconfigure_via_cli(click_runner, custom_filepath: Path, config_class, monkeypatch, test_registry, test_registry_source_manager): diff --git a/tests/acceptance/cli/ursula/test_federated_ursula.py b/tests/acceptance/cli/ursula/test_federated_ursula.py index 05f432db4..455c32924 100644 --- a/tests/acceptance/cli/ursula/test_federated_ursula.py +++ b/tests/acceptance/cli/ursula/test_federated_ursula.py @@ -87,6 +87,7 @@ def test_initialize_custom_configuration_root(click_runner, custom_filepath: Pat # TODO: Only using in-memory node storage for now # assert (custom_filepath / 'known_nodes').is_dir(), 'known_nodes directory does not exist' + assert not (custom_filepath / 'known_nodes').is_dir(), 'known_nodes directory does not exist' custom_config_filepath = custom_filepath / UrsulaConfiguration.generate_filename() assert custom_config_filepath.is_file(), 'Configuration file does not exist' diff --git a/tests/utils/config.py b/tests/utils/config.py index 84c935faa..77846bd82 100644 --- a/tests/utils/config.py +++ b/tests/utils/config.py @@ -94,5 +94,5 @@ def make_alice_test_configuration(payment_provider: str = None, def make_bob_test_configuration(**assemble_kwargs) -> BobConfiguration: test_params = assemble(**assemble_kwargs) - config = BobConfiguration(**test_params, policy_registry=test_params['registry']) + config = BobConfiguration(**test_params) return config diff --git a/tests/utils/middleware.py b/tests/utils/middleware.py index 86419306d..a32c67a8c 100644 --- a/tests/utils/middleware.py +++ b/tests/utils/middleware.py @@ -14,6 +14,8 @@ 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 . """ + + import random import socket import time From fc30a73b3380894fa0f0cfe46301f9ca5b25690e Mon Sep 17 00:00:00 2001 From: Kieran Prasch Date: Thu, 24 Mar 2022 13:21:18 -0700 Subject: [PATCH 19/19] Additional newsfragment for policy probationary period extension. --- newsfragments/2873.misc.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 newsfragments/2873.misc.rst diff --git a/newsfragments/2873.misc.rst b/newsfragments/2873.misc.rst new file mode 100644 index 000000000..f62e8403a --- /dev/null +++ b/newsfragments/2873.misc.rst @@ -0,0 +1,2 @@ +Extend policy probationary period to 2022-6-16T23:59:59.0Z. +