Merge pull request #3123 from derekpierre/e2e-dkg

E2EE Threshold Decryption
pull/3133/head
Derek Pierre 2023-05-23 16:01:07 -04:00 committed by GitHub
commit 9305d4d76c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 1216 additions and 887 deletions

View File

@ -11,7 +11,7 @@ python_version = "3"
constant-sorrow = ">=0.1.0a9"
bytestring-splitter = ">=2.4.0"
hendrix = ">=4.0"
nucypher-core = ">=0.7.0"
nucypher-core = ">=0.8.0"
# Cryptography
cryptography = ">=3.2"
ferveo = ">=0.1.11"
@ -30,7 +30,7 @@ requests = "*"
# Third-Party Ethereum
eip712-structs = "*"
eth-tester = "*" # providers.py still uses this
py-evm = "==0.6.1a2" # ape -> evm-trace needs this version
py-evm = "*"
web3 = ">=6.0.0"
watchdog = "<3.0.0" # needed for eth-ape to be happy
@ -53,8 +53,9 @@ pytest-cov = "*"
pytest-mock = "*"
pytest-timeout = "*"
# Tools
eth-ape = ">=0.6.3,<0.7.0"
ape-solidity = ">=0.6.0,<0.7.0"
eth-ape = "*"
# TODO eventually change to official release, issue #3131, once fix is available
ape-solidity = ">=0.6.5"
hypothesis = "*"
pre-commit = "2.12.1"
coverage = "<=6.5.0"

1316
Pipfile.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,8 @@
-i https://pypi.python.org/simple
aiohttp==3.8.2
aiosignal==1.3.1 ; python_version >= '3.7'
ape-solidity==0.6.3
ape-solidity==0.6.5
appnope==0.1.3 ; sys_platform == 'darwin'
asttokens==2.2.1
async-timeout==4.0.2 ; python_version >= '3.6'
attrs==23.1.0 ; python_version >= '3.7'
@ -9,7 +10,7 @@ backcall==0.2.0
base58==1.0.3
bitarray==2.7.3
cached-property==1.5.2
certifi==2022.12.7 ; python_version >= '3.6'
certifi==2023.5.7 ; python_version >= '3.6'
cffi==1.15.1
cfgv==3.3.1 ; python_full_version >= '3.6.1'
charset-normalizer==2.1.1 ; python_full_version >= '3.6.0'
@ -25,41 +26,42 @@ distlib==0.3.6
eip712==0.2.1 ; python_version >= '3.8' and python_version < '4'
eth-abi==4.0.0 ; python_version >= '3.7' and python_version < '4'
eth-account==0.8.0 ; python_version >= '3.6' and python_version < '4'
eth-ape==0.6.8
eth-ape==0.6.9
eth-bloom==2.0.0 ; python_version >= '3.7' and python_version < '4'
eth-hash==0.5.1 ; python_version >= '3.7' and python_version < '4'
eth-keyfile==0.6.1
eth-keys==0.4.0
eth-rlp==0.3.0 ; python_version >= '3.7' and python_version < '4'
eth-tester==0.8.0b3
eth-tester==0.9.0b1
eth-typing==3.3.0 ; python_full_version >= '3.7.2' and python_version < '4'
eth-utils==2.1.0
ethpm-types==0.4.5 ; python_version >= '3.8' and python_version < '4'
evm-trace==0.1.0a18 ; python_version >= '3.8' and python_version < '4'
ethpm-types==0.5.1 ; python_version >= '3.8' and python_version < '4'
evm-trace==0.1.0a20 ; python_version >= '3.8' and python_version < '4'
exceptiongroup==1.1.1 ; python_version < '3.11'
executing==1.2.0
filelock==3.12.0 ; python_version >= '3.7'
frozenlist==1.3.3 ; python_version >= '3.7'
greenlet==2.0.2 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
hexbytes==0.3.0 ; python_version >= '3.7' and python_version < '4'
hypothesis==6.75.1
identify==2.5.23 ; python_version >= '3.7'
hypothesis==6.75.3
identify==2.5.24 ; python_version >= '3.7'
idna==3.4 ; python_version >= '3.5'
ijson==3.2.0.post0
importlib-metadata==6.6.0 ; python_version < '3.10'
importlib-resources==5.12.0 ; python_version < '3.9'
iniconfig==2.0.0 ; python_version >= '3.7'
ipython==8.12.1 ; python_version >= '3.8'
ipython==8.12.2 ; python_version >= '3.8'
jedi==0.18.2 ; python_version >= '3.6'
jsonschema==4.18.0a6 ; python_version >= '3.8'
jsonschema-specifications==2023.3.6 ; python_version >= '3.8'
jsonschema==4.18.0a7 ; python_version >= '3.8'
jsonschema-specifications==2023.5.1 ; python_version >= '3.8'
lazyasd==0.1.4
lru-dict==1.1.8
matplotlib-inline==0.1.6 ; python_version >= '3.5'
morphys==1.0
msgspec==0.14.2 ; python_version >= '3.8'
msgspec==0.15.1 ; python_version >= '3.8'
multidict==5.2.0 ; python_version >= '3.6'
mypy-extensions==0.4.4 ; python_version >= '2.7'
nodeenv==1.7.0 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'
nodeenv==1.8.0 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'
numpy==1.24.3 ; python_version < '3.10'
packaging==23.1 ; python_version >= '3.7'
pandas==1.5.3 ; python_version >= '3.8'
@ -68,29 +70,29 @@ parso==0.8.3 ; python_version >= '3.6'
pexpect==4.8.0 ; sys_platform != 'win32'
pickleshare==0.7.5
pkgutil-resolve-name==1.3.10 ; python_version < '3.9'
platformdirs==3.5.0 ; python_version >= '3.7'
platformdirs==3.5.1 ; python_version >= '3.7'
pluggy==1.0.0 ; python_version >= '3.6'
pre-commit==3.3.1
pre-commit==3.3.2
prompt-toolkit==3.0.38 ; python_full_version >= '3.7.0'
protobuf==4.23.0rc2 ; python_version >= '3.7'
protobuf==4.23.1 ; python_version >= '3.7'
ptyprocess==0.7.0
pure-eval==0.2.2
py==1.11.0 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
py-cid==0.3.0
py-ecc==6.0.0 ; python_version >= '3.6' and python_version < '4'
py-evm==0.6.1a2
py-evm==0.7.0a2
py-geth==3.12.0 ; python_version >= '3'
py-multibase==1.0.3
py-multicodec==0.2.1
py-multihash==0.2.3
py-solc-x==1.1.1 ; python_version >= '3.6' and python_version < '4'
pycparser==2.21
pycryptodome==3.17
pydantic==1.10.7 ; python_version >= '3.7'
pycryptodome==3.18.0
pydantic==1.10.8 ; python_version >= '3.7'
pyethash==0.1.27
pygithub==1.58.1 ; python_version >= '3.7'
pygithub==1.58.2 ; python_version >= '3.7'
pygments==2.15.1 ; python_version >= '3.7'
pyjwt[crypto]==2.6.0 ; python_version >= '3.7'
pyjwt[crypto]==2.7.0 ; python_version >= '3.7'
pynacl==1.5.0
pysha3==1.0.2
pytest==6.2.5
@ -102,17 +104,17 @@ python-baseconv==1.2.2
python-dateutil==2.8.2 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'
pytz==2023.3
pyyaml==6.0 ; python_version >= '3.6'
referencing==0.28.0 ; python_version >= '3.8'
regex==2023.5.4 ; python_version >= '3.6'
requests==2.29.0
referencing==0.28.3 ; python_version >= '3.8'
regex==2023.5.5 ; python_version >= '3.6'
requests==2.31.0
rich==12.6.0 ; python_full_version >= '3.6.3' and python_full_version < '4.0.0'
rlp==3.0.0
rpds-py==0.7.1 ; python_version >= '3.8'
semantic-version==2.10.0 ; python_version >= '2.7'
setuptools==67.7.2 ; python_version >= '3.7'
setuptools==67.8.0 ; python_version >= '3.7'
six==1.16.0 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'
sortedcontainers==2.4.0
sqlalchemy==2.0.12 ; python_version >= '3.7'
sqlalchemy==2.0.15 ; python_version >= '3.7'
stack-data==0.6.2
toml==0.10.2 ; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'
tomli==2.0.1
@ -120,14 +122,14 @@ toolz==0.12.0 ; python_version >= '3.5'
tqdm==4.65.0 ; python_version >= '3.7'
traitlets==5.9.0 ; python_version >= '3.7'
trie==2.1.0 ; python_version >= '3.7' and python_version < '4'
typing-extensions==4.5.0 ; python_version >= '3.7'
urllib3==1.26.15 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'
typing-extensions==4.6.0 ; python_version >= '3.7'
urllib3==2.0.2 ; python_version >= '3.7'
varint==1.0.2
virtualenv==20.23.0 ; python_version >= '3.7'
watchdog==2.3.1
wcwidth==0.2.6
web3==6.2.0
websockets==11.0.2 ; python_version >= '3.7'
web3==6.4.0
websockets==11.0.3 ; python_version >= '3.7'
wrapt==1.15.0 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
yarl==1.9.2 ; python_version >= '3.7'
zipp==3.15.0 ; python_version >= '3.7'

View File

@ -1,7 +1,7 @@
-i https://pypi.python.org/simple
alabaster==0.7.13 ; python_version >= '3.6'
babel==2.12.1 ; python_version >= '3.7'
certifi==2022.12.7 ; python_version >= '3.6'
certifi==2023.5.7 ; python_version >= '3.6'
charset-normalizer==2.1.1
click==8.1.3 ; python_version >= '3.7'
click-default-group==1.2.2
@ -14,11 +14,11 @@ markupsafe==2.1.2 ; python_version >= '3.7'
packaging==23.1 ; python_version >= '3.7'
pygments==2.15.1 ; python_version >= '3.7'
pytz==2023.3 ; python_version < '3.9'
requests==2.29.0 ; python_version >= '3.7'
setuptools==67.7.2 ; python_version >= '3.7'
requests==2.31.0 ; python_version >= '3.7'
setuptools==67.8.0 ; python_version >= '3.7'
snowballstemmer==2.2.0
sphinx==3.0.1
sphinx-rtd-theme==1.2.0
sphinx-rtd-theme==1.2.1
sphinxcontrib-applehelp==1.0.4 ; python_version >= '3.8'
sphinxcontrib-devhelp==1.0.2 ; python_version >= '3.5'
sphinxcontrib-htmlhelp==2.0.1 ; python_version >= '3.8'
@ -28,4 +28,4 @@ sphinxcontrib-qthelp==1.0.3 ; python_version >= '3.5'
sphinxcontrib-serializinghtml==1.1.5 ; python_version >= '3.5'
tomli==2.0.1 ; python_version < '3.11'
towncrier==22.12.0
urllib3==1.26.15 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'
urllib3==2.0.2 ; python_version >= '3.7'

View File

@ -0,0 +1 @@
End-to-end encryption for CBD decryption requests.

View File

@ -4,8 +4,15 @@ from typing import List, Optional, Tuple, Union
import maya
from eth_typing import ChecksumAddress
from ferveo_py import AggregatedTranscript, Ciphertext, PublicKey, Validator
from ferveo_py import AggregatedTranscript, Ciphertext
from ferveo_py import DkgPublicKey as FerveoPublicKey
from ferveo_py import Validator
from hexbytes import HexBytes
from nucypher_core import (
EncryptedThresholdDecryptionRequest,
ThresholdDecryptionRequest,
)
from nucypher_core.umbral import PublicKey
from web3 import Web3
from web3.types import TxReceipt
@ -27,7 +34,12 @@ from nucypher.blockchain.eth.token import NU
from nucypher.blockchain.eth.trackers.dkg import ActiveRitualTracker
from nucypher.blockchain.eth.trackers.pre import WorkTracker
from nucypher.crypto.ferveo.dkg import DecryptionShareSimple, FerveoVariant, Transcript
from nucypher.crypto.powers import CryptoPower, RitualisticPower, TransactingPower
from nucypher.crypto.powers import (
CryptoPower,
RitualisticPower,
ThresholdRequestDecryptingPower,
TransactingPower,
)
from nucypher.datastore.dkg import DKGStorage
from nucypher.network.trackers import OperatorBondedTracker
from nucypher.policy.conditions.lingo import ConditionLingo
@ -293,9 +305,18 @@ class Ritualist(BaseActor):
contract=self.coordinator_agent.contract
)
self.publish_finalization = publish_finalization # publish the DKG final key if True
self.dkg_storage = DKGStorage() # TODO: #3052 stores locally generated public DKG artifacts
self.ritual_power = crypto_power.power_ups(RitualisticPower) # ferveo material contained within
self.publish_finalization = (
publish_finalization # publish the DKG final key if True
)
self.dkg_storage = (
DKGStorage()
) # TODO: #3052 stores locally generated public DKG artifacts
self.ritual_power = crypto_power.power_ups(
RitualisticPower
) # ferveo material contained within
self.threshold_request_power = crypto_power.power_ups(
ThresholdRequestDecryptingPower
) # used for secure decryption request channel
def get_ritual(self, ritual_id: int) -> CoordinatorAgent.Ritual:
try:
@ -353,7 +374,7 @@ class Ritualist(BaseActor):
# look up the node index for this node on the blockchain
receipt = self.coordinator_agent.post_transcript(
ritual_id=ritual_id,
transcript=bytes(transcript),
transcript=transcript,
transacting_power=self.transacting_power
)
return receipt
@ -362,14 +383,18 @@ class Ritualist(BaseActor):
self,
ritual_id: int,
aggregated_transcript: AggregatedTranscript,
public_key: PublicKey,
public_key: FerveoPublicKey,
) -> TxReceipt:
"""Publish an aggregated transcript to publicly available storage."""
# look up the node index for this node on the blockchain
request_encrypting_key = self.threshold_request_power.get_pubkey_from_ritual_id(
ritual_id
)
receipt = self.coordinator_agent.post_aggregation(
ritual_id=ritual_id,
aggregated_transcript=bytes(aggregated_transcript),
aggregated_transcript=aggregated_transcript,
public_key=public_key,
request_encrypting_key=request_encrypting_key,
transacting_power=self.transacting_power
)
return receipt
@ -553,6 +578,13 @@ class Ritualist(BaseActor):
return decryption_share
def decrypt_threshold_decryption_request(
self, encrypted_request: EncryptedThresholdDecryptionRequest
) -> Tuple[ThresholdDecryptionRequest, PublicKey]:
return self.threshold_request_power.decrypt_encrypted_request(
encrypted_request=encrypted_request
)
class PolicyAuthor(NucypherTokenActor):
"""Alice base class for blockchain operations, mocking up new policies!"""

View File

@ -10,7 +10,8 @@ from constant_sorrow.constants import CONTRACT_ATTRIBUTE # type: ignore
from constant_sorrow.constants import CONTRACT_CALL, TRANSACTION
from eth_typing.evm import ChecksumAddress
from eth_utils.address import to_checksum_address
from ferveo_py.ferveo_py import DkgPublicKey
from ferveo_py.ferveo_py import AggregatedTranscript, DkgPublicKey, Transcript
from nucypher_core.umbral import PublicKey
from web3.contract.contract import Contract, ContractFunction
from web3.types import Timestamp, TxParams, TxReceipt, Wei
@ -555,6 +556,7 @@ class CoordinatorAgent(EthereumContractAgent):
provider: ChecksumAddress
aggregated: bool = False
transcript: bytes = bytes()
requestEncryptingKey: bytes = bytes()
class G1Point(NamedTuple):
"""Coordinator contract representation of DkgPublicKey."""
@ -610,6 +612,16 @@ class CoordinatorAgent(EthereumContractAgent):
def shares(self) -> int:
return len(self.providers)
@property
def request_encrypting_keys(self):
request_encrypting_keys = {}
for p in self.participants:
request_encrypting_keys[p.provider] = PublicKey.from_compressed_bytes(
p.requestEncryptingKey
)
return request_encrypting_keys
@contract_api(CONTRACT_CALL)
def get_timeout(self) -> int:
return self.contract.functions.timeout().call()
@ -648,7 +660,10 @@ class CoordinatorAgent(EthereumContractAgent):
participants = list()
for r in result:
participant = self.Ritual.Participant(
provider=ChecksumAddress(r[0]), aggregated=r[1], transcript=bytes(r[2])
provider=ChecksumAddress(r[0]),
aggregated=r[1],
transcript=bytes(r[2]),
requestEncryptingKey=bytes(r[3]),
)
participants.append(participant)
return participants
@ -669,6 +684,7 @@ class CoordinatorAgent(EthereumContractAgent):
provider=ChecksumAddress(result[0]),
aggregated=result[1],
transcript=bytes(result[2]),
requestEncryptingKey=bytes(result[3]),
)
return participant
@ -688,12 +704,11 @@ class CoordinatorAgent(EthereumContractAgent):
def post_transcript(
self,
ritual_id: int,
transcript: bytes,
transcript: Transcript,
transacting_power: TransactingPower,
) -> TxReceipt:
contract_function: ContractFunction = self.contract.functions.postTranscript(
ritualId=ritual_id,
transcript=transcript
ritualId=ritual_id, transcript=bytes(transcript)
)
receipt = self.blockchain.send_transaction(contract_function=contract_function,
transacting_power=transacting_power)
@ -703,14 +718,16 @@ class CoordinatorAgent(EthereumContractAgent):
def post_aggregation(
self,
ritual_id: int,
aggregated_transcript: bytes,
aggregated_transcript: AggregatedTranscript,
public_key: DkgPublicKey,
request_encrypting_key: PublicKey,
transacting_power: TransactingPower,
) -> TxReceipt:
contract_function: ContractFunction = self.contract.functions.postAggregation(
ritualId=ritual_id,
aggregatedTranscript=aggregated_transcript,
aggregatedTranscript=bytes(aggregated_transcript),
publicKey=self.Ritual.G1Point.from_dkg_public_key(public_key),
requestEncryptingKey=request_encrypting_key.to_compressed_bytes(),
)
receipt = self.blockchain.send_transaction(
contract_function=contract_function,

View File

@ -1,6 +1,3 @@
from eth_tester import EthereumTester, PyEVMBackend
from eth_tester.backends.mock.main import MockBackend
from typing import Union

View File

@ -53,12 +53,12 @@ from nucypher_core import (
NodeMetadataPayload,
ReencryptionResponse,
ThresholdDecryptionRequest,
ThresholdDecryptionResponse,
TreasureMap,
)
from nucypher_core.umbral import (
PublicKey,
RecoverableSignature,
SecretKey,
VerifiedKeyFrag,
reencrypt,
)
@ -94,6 +94,7 @@ from nucypher.crypto.powers import (
PowerUpError,
RitualisticPower,
SigningPower,
ThresholdRequestDecryptingPower,
TLSHostingPower,
TransactingPower,
)
@ -574,7 +575,7 @@ class Bob(Character):
if context:
context = Context(json.dumps(context))
decryption_request = ThresholdDecryptionRequest(
id=ritual_id,
ritual_id=ritual_id,
variant=int(variant.value),
ciphertext=bytes(ciphertext),
conditions=conditions,
@ -582,22 +583,37 @@ class Bob(Character):
)
return decryption_request
def get_decryption_shares_using_existing_decryption_request(self,
decryption_request: ThresholdDecryptionRequest,
variant: FerveoVariant,
cohort: List["Ursula"],
threshold: int,
):
def get_decryption_shares_using_existing_decryption_request(
self,
decryption_request: ThresholdDecryptionRequest,
request_encrypting_keys: Dict[ChecksumAddress, PublicKey],
variant: FerveoVariant,
cohort: List["Ursula"],
threshold: int,
) -> Dict[
ChecksumAddress, Union[DecryptionShareSimple, DecryptionSharePrecomputed]
]:
if variant == FerveoVariant.PRECOMPUTED:
share_type = DecryptionSharePrecomputed
elif variant == FerveoVariant.SIMPLE:
share_type = DecryptionShareSimple
# use ephemeral key for request
# TODO don't use Umbral in the long-run
response_sk = SecretKey.random()
response_encrypting_key = response_sk.public_key()
decryption_request_mapping = {}
for ursula in cohort:
ursula_checksum_address = to_checksum_address(ursula.checksum_address)
request_encrypting_key = request_encrypting_keys[ursula_checksum_address]
encrypted_decryption_request = decryption_request.encrypt(
request_encrypting_key=request_encrypting_key,
response_encrypting_key=response_encrypting_key,
)
decryption_request_mapping[
to_checksum_address(ursula.checksum_address)
] = bytes(decryption_request)
ursula_checksum_address
] = encrypted_decryption_request
decryption_client = ThresholdDecryptionClient(learner=self)
successes, failures = decryption_client.gather_encrypted_decryption_shares(
@ -605,12 +621,12 @@ class Bob(Character):
)
if len(successes) < threshold:
raise Ursula.NotEnoughUrsulas(f"Not enough Ursulas to decrypt")
raise Ursula.NotEnoughUrsulas(f"Not enough Ursulas to decrypt: {failures}")
self.log.debug(f"Got enough shares to decrypt.")
gathered_shares = {}
for provider_address, response_bytes in successes.items():
decryption_response = ThresholdDecryptionResponse.from_bytes(response_bytes)
for provider_address, encrypted_decryption_response in successes.items():
decryption_response = encrypted_decryption_response.decrypt(sk=response_sk)
decryption_share = share_type.from_bytes(
decryption_response.decryption_share
)
@ -618,37 +634,40 @@ class Bob(Character):
return gathered_shares
def gather_decryption_shares(
self,
ritual_id: int,
cohort: List["Ursula"],
ciphertext: Ciphertext,
lingo: LingoList,
threshold: int,
variant: FerveoVariant,
context: Optional[dict] = None,
self,
ritual_id: int,
cohort: List["Ursula"],
ciphertext: Ciphertext,
lingo: LingoList,
threshold: int,
variant: FerveoVariant,
request_encrypting_keys: Dict[ChecksumAddress, PublicKey],
context: Optional[dict] = None,
) -> Dict[
ChecksumAddress, Union[DecryptionShareSimple, DecryptionSharePrecomputed]
]:
decryption_request = self.make_decryption_request(
ritual_id=ritual_id,
ciphertext=ciphertext,
lingo=lingo,
variant=variant,
context=context,
)
return self.get_decryption_shares_using_existing_decryption_request(
decryption_request, request_encrypting_keys, variant, cohort, threshold
)
decryption_request = self.make_decryption_request(ritual_id=ritual_id,
ciphertext=ciphertext,
lingo=lingo,
variant=variant,
context=context)
return self.get_decryption_shares_using_existing_decryption_request(decryption_request, variant, cohort,
threshold)
def threshold_decrypt(self,
ritual_id: int,
ciphertext: Ciphertext,
conditions: LingoList,
context: Optional[dict] = None,
params: Optional[DkgPublicParameters] = None,
ursulas: Optional[List['Ursula']] = None,
variant: str = 'simple',
peering_timeout: int = 60,
) -> bytes:
def threshold_decrypt(
self,
ritual_id: int,
ciphertext: Ciphertext,
conditions: LingoList,
context: Optional[dict] = None,
params: Optional[DkgPublicParameters] = None,
ursulas: Optional[List["Ursula"]] = None,
variant: str = "simple",
peering_timeout: int = 60,
) -> bytes:
# blockchain reads: get the DKG parameters and the cohort.
coordinator_agent = ContractAgency.get_agent(CoordinatorAgent, registry=self.registry)
ritual = coordinator_agent.get_ritual(ritual_id, with_participants=True)
@ -660,19 +679,25 @@ class Bob(Character):
ursulas = self.resolve_cohort(ritual=ritual, timeout=peering_timeout)
else:
for ursula in ursulas:
if ursula.staking_provider_address not in ritual.participants:
raise ValueError(f"{ursula} is not part of the cohort")
if ursula.staking_provider_address not in ritual.providers:
raise ValueError(
f"{ursula} ({ursula.staking_provider_address}) is not part of the cohort"
)
self.remember_node(ursula)
try:
variant = FerveoVariant(getattr(FerveoVariant, variant.upper()).value)
except AttributeError:
raise ValueError(f"Invalid variant: {variant}; Options are: {list(v.name.lower() for v in list(FerveoVariant))}")
raise ValueError(
f"Invalid variant: {variant}; Options are: {list(v.name.lower() for v in list(FerveoVariant))}"
)
threshold = (
(ritual.shares // 2) + 1
if variant == FerveoVariant.SIMPLE
else ritual.shares
) # TODO: #3095 get this from the ritual / put it on-chain?
request_encrypting_keys = ritual.request_encrypting_keys
decryption_shares = self.gather_decryption_shares(
ritual_id=ritual_id,
cohort=ursulas,
@ -681,6 +706,7 @@ class Bob(Character):
lingo=conditions,
threshold=threshold,
variant=variant,
request_encrypting_keys=request_encrypting_keys,
)
if not params:
@ -746,6 +772,7 @@ class Ursula(Teacher, Character, Operator, Ritualist):
SigningPower,
DecryptingPower,
RitualisticPower,
ThresholdRequestDecryptingPower,
# TLSHostingPower # Still considered a default for Ursula, but needs the host context
]
@ -1392,7 +1419,7 @@ class Enrico:
ciphertext = self.encrypt_for_dkg(plaintext=plaintext,
conditions=conditions)
tdr = ThresholdDecryptionRequest(
id=ritual_id,
ritual_id=ritual_id,
ciphertext=bytes(ciphertext),
conditions=Conditions(json.dumps(conditions)),
context=context,

View File

@ -105,7 +105,7 @@ class DecryptingKeypair(Keypair):
class RitualisticKeypair(Keypair):
"""A keypair for Ferveo"""
"""A keypair for Ferveo DKG"""
_private_key_source = ferveo_py.Keypair.random
_public_key_method = "public_key"

View File

@ -1,47 +1,50 @@
from json import JSONDecodeError
from os.path import abspath
import click
import json
import os
import stat
import string
import time
from json import JSONDecodeError
from os.path import abspath
from pathlib import Path
from secrets import token_bytes
from typing import Callable, ClassVar, Dict, List, Optional, Tuple, Union
import click
from constant_sorrow.constants import KEYSTORE_LOCKED
from ferveo_py import ferveo_py
from mnemonic.mnemonic import Mnemonic
from nucypher_core.umbral import SecretKeyFactory
from pathlib import Path
from secrets import token_bytes
from typing import Callable, ClassVar, Dict, List, Union, Optional, Tuple
from nucypher.config.constants import DEFAULT_CONFIG_ROOT
from nucypher.crypto.keypairs import HostingKeypair, RitualisticKeypair
from nucypher.crypto.passwords import (
SecretBoxAuthenticationError,
derive_key_material_from_password,
secret_box_decrypt,
secret_box_encrypt,
derive_key_material_from_password,
SecretBoxAuthenticationError
)
from nucypher.crypto.powers import (
CryptoPowerUp,
DecryptingPower,
DelegatingPower,
DerivedKeyBasedPower,
KeyPairBasedPower,
RitualisticPower,
SigningPower,
CryptoPowerUp,
DelegatingPower,
TLSHostingPower, RitualisticPower,
ThresholdRequestDecryptingPower,
TLSHostingPower,
)
from nucypher.crypto.tls import generate_self_signed_certificate
from nucypher.utilities.emitters import StdoutEmitter
# HKDF
__INFO_BASE = b'NuCypher/'
_SIGNING_INFO = __INFO_BASE + b'signing'
_DECRYPTING_INFO = __INFO_BASE + b'decrypting'
_DELEGATING_INFO = __INFO_BASE + b'delegating'
_RITUALISTIC_INFO = __INFO_BASE + b'ritualistic'
_TLS_INFO = __INFO_BASE + b'tls'
__INFO_BASE = b"NuCypher/"
_SIGNING_INFO = __INFO_BASE + b"signing"
_DECRYPTING_INFO = __INFO_BASE + b"decrypting"
_DELEGATING_INFO = __INFO_BASE + b"delegating"
_RITUALISTIC_INFO = __INFO_BASE + b"ritualistic"
_THRESHOLD_REQUEST_DECRYPTING_INFO = __INFO_BASE + b"threshoold_request_decrypting"
_TLS_INFO = __INFO_BASE + b"tls"
# Wrapping key
_SALT_SIZE = 32
@ -224,11 +227,14 @@ class Keystore:
_SUFFIX = 'priv'
# Powers derivation
__HKDF_INFO = {SigningPower: _SIGNING_INFO,
DecryptingPower: _DECRYPTING_INFO,
DelegatingPower: _DELEGATING_INFO,
TLSHostingPower: _TLS_INFO,
RitualisticPower: _RITUALISTIC_INFO}
__HKDF_INFO = {
SigningPower: _SIGNING_INFO,
DecryptingPower: _DECRYPTING_INFO,
DelegatingPower: _DELEGATING_INFO,
TLSHostingPower: _TLS_INFO,
RitualisticPower: _RITUALISTIC_INFO,
ThresholdRequestDecryptingPower: _THRESHOLD_REQUEST_DECRYPTING_INFO,
}
class Exists(FileExistsError):
pass

View File

@ -12,6 +12,10 @@ from ferveo_py import (
Validator,
)
from hexbytes import HexBytes
from nucypher_core import (
EncryptedThresholdDecryptionRequest,
ThresholdDecryptionRequest,
)
from nucypher_core.umbral import PublicKey, SecretKey, SecretKeyFactory, generate_kfrags
from nucypher.blockchain.eth.decorators import validate_checksum_address
@ -47,6 +51,10 @@ class NoRitualisticPower(PowerUpError):
pass
class NoThresholdRequestDecryptingPower(PowerUpError):
pass
class CryptoPower(object):
def __init__(self, power_ups: list = None) -> None:
self.__power_ups = {} # type: dict
@ -318,6 +326,35 @@ class DerivedKeyBasedPower(CryptoPowerUp):
"""
class ThresholdRequestDecryptingPower(DerivedKeyBasedPower):
class ThresholdRequestDecryptionFailed(Exception):
"""Raised when decryption of the request fails."""
def __init__(self, secret_key_factory: Optional[SecretKeyFactory] = None):
if not secret_key_factory:
secret_key_factory = SecretKeyFactory.random()
self.__secret_key_factory = secret_key_factory
def _get_privkey_from_ritual_id(self, ritual_id: int):
return self.__secret_key_factory.make_key(bytes(ritual_id))
def get_pubkey_from_ritual_id(self, ritual_id: int) -> PublicKey:
return self._get_privkey_from_ritual_id(ritual_id).public_key()
def decrypt_encrypted_request(
self, encrypted_request: EncryptedThresholdDecryptionRequest
) -> Tuple[ThresholdDecryptionRequest, PublicKey]:
try:
priv_key = self._get_privkey_from_ritual_id(encrypted_request.ritual_id)
e2e_request = encrypted_request.decrypt(sk=priv_key)
return (
e2e_request.decryption_request,
e2e_request.response_encrypting_key,
)
except Exception as e:
raise self.ThresholdRequestDecryptionFailed from e
class DelegatingPower(DerivedKeyBasedPower):
def __init__(self, secret_key_factory: Optional[SecretKeyFactory] = None):

View File

@ -1,16 +1,20 @@
from typing import Dict, List, Tuple
from eth_typing import ChecksumAddress
from nucypher_core import (
EncryptedThresholdDecryptionRequest,
EncryptedThresholdDecryptionResponse,
)
from nucypher.network.client import ThresholdAccessControlClient
from nucypher.utilities.concurrency import BatchValueFactory, WorkerPool
class ThresholdDecryptionClient(ThresholdAccessControlClient):
class DecryptionRequestFailed(Exception):
class ThresholdDecryptionRequestFailed(Exception):
"""Raised when a decryption request returns a non-zero status code."""
class DecryptionRequestFactory(BatchValueFactory):
class ThresholdDecryptionRequestFactory(BatchValueFactory):
def __init__(self, ursula_to_contact: List[ChecksumAddress], threshold: int):
# TODO should we batch the ursulas to contact i.e. pass `batch_size` parameter
super().__init__(values=ursula_to_contact, required_successes=threshold)
@ -20,17 +24,22 @@ class ThresholdDecryptionClient(ThresholdAccessControlClient):
def gather_encrypted_decryption_shares(
self,
encrypted_requests: Dict[ChecksumAddress, bytes],
encrypted_requests: Dict[ChecksumAddress, EncryptedThresholdDecryptionRequest],
threshold: int,
timeout: float = 10,
) -> Tuple[Dict[ChecksumAddress, bytes], Dict[ChecksumAddress, str]]:
) -> Tuple[
Dict[ChecksumAddress, EncryptedThresholdDecryptionResponse],
Dict[ChecksumAddress, str],
]:
self._ensure_ursula_availability(
ursulas=list(encrypted_requests.keys()),
threshold=threshold,
timeout=timeout,
)
def worker(ursula_address: ChecksumAddress) -> bytes:
def worker(
ursula_address: ChecksumAddress,
) -> EncryptedThresholdDecryptionResponse:
encrypted_request = encrypted_requests[ursula_address]
try:
@ -38,23 +47,24 @@ class ThresholdDecryptionClient(ThresholdAccessControlClient):
node_or_sprout.mature()
response = (
self._learner.network_middleware.get_encrypted_decryption_share(
node_or_sprout, encrypted_request
node_or_sprout, bytes(encrypted_request)
)
)
if response.status_code == 200:
return EncryptedThresholdDecryptionResponse.from_bytes(
response.content
)
except Exception as e:
self.log.warn(f"Node {ursula_address} raised {e}")
raise
else:
if response.status_code != 200:
message = f"Node {ursula_address} returned {response.status_code} - {response.content}."
self.log.warn(message)
raise self.DecryptionRequestFailed(message)
return response.content
message = f"Node {ursula_address} returned {response.status_code} - {response.content}."
self.log.warn(message)
raise self.ThresholdDecryptionRequestFailed(message)
worker_pool = WorkerPool(
worker=worker,
value_factory=self.DecryptionRequestFactory(
value_factory=self.ThresholdDecryptionRequestFactory(
ursula_to_contact=list(encrypted_requests.keys()), threshold=threshold
),
target_successes=threshold,

View File

@ -1,12 +1,11 @@
import ferveo_py
import time
from collections import defaultdict, deque
from contextlib import suppress
from pathlib import Path
from queue import Queue
from typing import Callable, List, Optional, Set, Tuple, Union
import ferveo_py
import maya
import requests
from constant_sorrow.constants import (
@ -37,7 +36,8 @@ from nucypher.crypto.powers import (
CryptoPower,
DecryptingPower,
NoSigningPower,
SigningPower, RitualisticPower,
RitualisticPower,
SigningPower,
)
from nucypher.crypto.signing import InvalidSignature, SignatureStamp
from nucypher.network.exceptions import NodeSeemsToBeDown

View File

@ -10,6 +10,7 @@ from flask import Flask, Response, jsonify, request
from mako import exceptions as mako_exceptions
from mako.template import Template
from nucypher_core import (
EncryptedThresholdDecryptionRequest,
MetadataRequest,
MetadataResponse,
MetadataResponsePayload,
@ -145,9 +146,19 @@ def _make_rest_app(this_node, log: Logger) -> Flask:
def threshold_decrypt():
# Deserialize and instantiate ThresholdDecryptionRequest from the request data
decryption_request = ThresholdDecryptionRequest.from_bytes(request.data)
encrypted_decryption_request = EncryptedThresholdDecryptionRequest.from_bytes(
request.data
)
(
decryption_request,
response_encrypting_key,
) = this_node.decrypt_threshold_decryption_request(
encrypted_request=encrypted_decryption_request
)
log.info(f"Threshold decryption request for ritual ID #{decryption_request.id}")
log.info(
f"Threshold decryption request for ritual ID #{decryption_request.ritual_id}"
)
# Deserialize and instantiate ConditionLingo from the request data
conditions_data = str(decryption_request.conditions) # nucypher_core.Conditions -> str
@ -169,17 +180,22 @@ def _make_rest_app(this_node, log: Logger) -> Flask:
# TODO: #3052 consider using the DKGStorage cache instead of the coordinator agent
# dkg_public_key = this_node.dkg_storage.get_public_key(decryption_request.ritual_id)
ritual = this_node.coordinator_agent.get_ritual(decryption_request.id, with_participants=True)
ritual = this_node.coordinator_agent.get_ritual(
decryption_request.ritual_id, with_participants=True
)
participants = [p.provider for p in ritual.participants]
# enforces that the node is part of the ritual
if this_node.checksum_address not in participants:
return Response(f'Node not part of ritual {decryption_request.id}', status=HTTPStatus.FORBIDDEN)
return Response(
f"Node not part of ritual {decryption_request.ritual_id}",
status=HTTPStatus.FORBIDDEN,
)
# derive the decryption share
ciphertext = Ciphertext.from_bytes(decryption_request.ciphertext)
decryption_share = this_node.derive_decryption_share(
ritual_id=decryption_request.id,
ritual_id=decryption_request.ritual_id,
ciphertext=ciphertext,
conditions=decryption_request.conditions,
variant=FerveoVariant(decryption_request.variant),
@ -189,7 +205,11 @@ def _make_rest_app(this_node, log: Logger) -> Flask:
# TODO: #3079 #3081 encrypt the response with the requester's public key
# TODO: #3098 nucypher-core#49 Use DecryptionShare type
response = ThresholdDecryptionResponse(decryption_share=bytes(decryption_share))
return Response(bytes(response), headers={'Content-Type': 'application/octet-stream'})
encrypted_response = response.encrypt(encrypting_key=response_encrypting_key)
return Response(
bytes(encrypted_response),
headers={"Content-Type": "application/octet-stream"},
)
@rest_app.route('/reencrypt', methods=["POST"])
def reencrypt():

View File

@ -10,7 +10,7 @@ backports.zoneinfo==0.2.1 ; python_version >= '3.7' and python_version < '3.9'
bitarray==2.7.3
bytestring-splitter==2.4.1
cached-property==1.5.2
certifi==2022.12.7 ; python_version >= '3.6'
certifi==2023.5.7 ; python_version >= '3.6'
cffi==1.15.1
charset-normalizer==2.1.1 ; python_full_version >= '3.6.0'
click==8.1.3
@ -28,10 +28,10 @@ eth-hash==0.5.1 ; python_version >= '3.7' and python_version < '4'
eth-keyfile==0.6.1
eth-keys==0.4.0
eth-rlp==0.3.0 ; python_version >= '3.7' and python_version < '4'
eth-tester==0.8.0b3
eth-tester==0.9.0b1
eth-typing==3.3.0 ; python_version < '4' and python_full_version >= '3.7.2'
eth-utils==2.1.0
ferveo==0.1.11
ferveo==0.1.13
flask==2.2.5
frozenlist==1.3.3 ; python_version >= '3.7'
hendrix==4.0.0
@ -44,8 +44,8 @@ importlib-resources==5.12.0 ; python_version < '3.9'
incremental==22.10.0
itsdangerous==2.1.2 ; python_version >= '3.7'
jinja2==3.0.3
jsonschema==4.18.0a6 ; python_version >= '3.8'
jsonschema-specifications==2023.3.6 ; python_version >= '3.8'
jsonschema==4.18.0a7 ; python_version >= '3.8'
jsonschema-specifications==2023.5.1 ; python_version >= '3.8'
lru-dict==1.1.8
mako==1.2.4
markupsafe==2.1.2 ; python_version >= '3.7'
@ -56,33 +56,33 @@ msgpack==1.0.5
msgpack-python==0.5.6
multidict==5.2.0 ; python_version >= '3.6'
mypy-extensions==0.4.4 ; python_version >= '2.7'
nucypher-core==0.7.0
nucypher-core==0.8.0
packaging==23.1 ; python_version >= '3.7'
parsimonious==0.9.0
pendulum==3.0.0a1 ; python_version >= '3.7' and python_version < '4.0'
pkgutil-resolve-name==1.3.10 ; python_version < '3.9'
protobuf==4.23.0rc2 ; python_version >= '3.7'
protobuf==4.23.1 ; python_version >= '3.7'
py-ecc==6.0.0 ; python_version >= '3.6' and python_version < '4'
py-evm==0.6.1a2
py-evm==0.7.0a2
pyasn1==0.5.0 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'
pyasn1-modules==0.3.0 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'
pychalk==2.0.1
pycparser==2.21
pycryptodome==3.17
pycryptodome==3.18.0
pyethash==0.1.27
pynacl==1.5.0
pyopenssl==23.1.1
pysha3==1.0.2
python-dateutil==2.8.2 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
pytz==2023.3
referencing==0.28.0 ; python_version >= '3.8'
regex==2023.5.4 ; python_version >= '3.6'
requests==2.29.0
referencing==0.28.3 ; python_version >= '3.8'
regex==2023.5.5 ; python_version >= '3.6'
requests==2.31.0
rlp==3.0.0
rpds-py==0.7.1 ; python_version >= '3.8'
semantic-version==2.10.0 ; python_version >= '2.7'
service-identity==21.1.0
setuptools==67.7.2 ; python_version >= '3.7'
setuptools==67.8.0 ; python_version >= '3.7'
six==1.16.0 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
snaptime==0.2.4
sortedcontainers==2.4.0
@ -92,14 +92,14 @@ toolz==0.12.0 ; python_version >= '3.5'
trie==2.1.0 ; python_version >= '3.7' and python_version < '4'
twisted==22.10.0 ; python_full_version >= '3.7.1'
txaio==23.1.1 ; python_version >= '3.7'
typing-extensions==4.5.0 ; python_version >= '3.7'
typing-extensions==4.6.0 ; python_version >= '3.7'
tzdata==2023.3 ; python_version >= '2'
tzlocal==5.0b2 ; python_version >= '3.7'
urllib3==1.26.15 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'
tzlocal==5.0.1 ; python_version >= '3.7'
urllib3==2.0.2 ; python_version >= '3.7'
watchdog==2.3.1
web3==6.2.0
websockets==11.0.2 ; python_version >= '3.7'
werkzeug==2.3.3 ; python_version >= '3.8'
web3==6.4.0
websockets==11.0.3 ; python_version >= '3.7'
werkzeug==2.3.4 ; python_version >= '3.8'
yarl==1.9.2 ; python_version >= '3.7'
zipp==3.15.0 ; python_version >= '3.7'
zope.interface==6.1a2 ; python_version >= '3.7'

View File

@ -2,6 +2,7 @@ import os
import pytest
from eth_utils import keccak
from nucypher_core.umbral import SecretKey
from nucypher.blockchain.eth.agents import (
ContractAgency,
@ -25,11 +26,6 @@ def transcripts():
return [os.urandom(32), os.urandom(32)]
@pytest.fixture(scope='module')
def aggregated_transcript():
return os.urandom(32)
@pytest.fixture(scope="module")
def cohort(testerchain, staking_providers):
deployer, cohort_provider_1, cohort_provider_2, *everybody_else = staking_providers
@ -126,16 +122,20 @@ def test_post_transcript(agent, transcripts, transacting_powers):
def test_post_aggregation(
agent, aggregated_transcript, dkg_public_key, transacting_powers
agent, aggregated_transcript, dkg_public_key, transacting_powers, cohort
):
ritual_id = agent.number_of_rituals() - 1
request_encrypting_keys = {}
for i, transacting_power in enumerate(transacting_powers):
request_encrypting_key = SecretKey.random().public_key()
receipt = agent.post_aggregation(
ritual_id=ritual_id,
aggregated_transcript=aggregated_transcript,
public_key=dkg_public_key,
request_encrypting_key=request_encrypting_key,
transacting_power=transacting_power,
)
request_encrypting_keys[cohort[i]] = request_encrypting_key
assert receipt["status"] == 1
post_aggregation_events = (
@ -145,11 +145,19 @@ def test_post_aggregation(
event = post_aggregation_events[0]
assert event["args"]["ritualId"] == ritual_id
assert event["args"]["aggregatedTranscriptDigest"] == keccak(
aggregated_transcript
bytes(aggregated_transcript)
)
participants = agent.get_participants(ritual_id)
assert all([p.aggregated for p in participants])
for p in participants:
assert p.aggregated
assert (
p.requestEncryptingKey
== request_encrypting_keys[p.provider].to_compressed_bytes()
)
ritual = agent.get_ritual(ritual_id)
assert ritual.request_encrypting_keys == request_encrypting_keys
assert agent.get_ritual_status(ritual_id=ritual_id) == agent.Ritual.Status.FINALIZED

View File

@ -12,7 +12,8 @@ dependencies:
version: 4.8.1
solidity:
version: 0.8.17
version: 0.8.20
evm_version: paris
import_remapping:
- "@openzeppelin/contracts=openzeppelin/v4.8.1"
@ -33,8 +34,6 @@ deployments:
ritual_timeout: 3600
max_dkg_size: 8
test:
mnemonic: test test test test test test test test test test test junk
number_of_accounts: 30

View File

@ -156,6 +156,3 @@ RPC_SUCCESSFUL_RESPONSE = {
"id": 1,
"result": "Geth/v1.9.20-stable-979fc968/linux-amd64/go1.15"
}
FAKE_TRANSCRIPT = b'\x98\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\xae\xdb_-\xeaj\x9bz\xdd\xd6\x98\xf8\xf91A\xc1\x8f;\x13@\x89\xcb\xcf>\x86\xc4T\xfb\x0c\x1ety\x8b\xd8mSkk\xbb\xcaU\xe5]v}E\xfa\xbc\xae\xb6\xa1\xf4e\x19\x86\xf2L\xcaZj\x03]h:\xbfP\x03Q\x8c\x95e\xe0c\xaa\xc2\xb4\xbby}\xecW%\xdet\xc8\xfc\xe7ky\xe5\xf6\xe9\xf5\x05\xe5\xdf\x81\x9bx\x18\xa4\x15\x85\xdeA9\x9f\x99\xceQ\xb0\xd0&\x9a\xa7\xaed&\x99\xdc\xa7\xfeLM\x01\x02\x87\xc8\x14$\x89"kA\x0b\x91\t\x1e\x1c/f\x00N,\x88\x01\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\xab\x0f\tFA\xdcB\xd4\xb3\x08\xd7IVkmw6za\xb6)\x13\x014]f.\xa1\xcd\xe27\xee\xc0\x95\xf6\xa4\x12\xa9\x19\x94\xed\x05\xffF\x81\xb2\xb2\xcb\x06\xaf-\xe4\xb5\x98\xbd\x81\x0f\xb8\xb7\xa1<\xf6/\xe5\xa4\x11\x83}\xfaH\x15\x80h\n\xe7\xc6\xc2\xb3\xd5{dH\xeb\x1e]v\xb4\x88v\x88\xb7N1\xff\x80\xd0\x88\x04.\x00\x82K\x1e\x96\xa0\xbd}X\xbb{?6\xeb\xe7\rg\x03\xeeG\x01\x10^\xee\x9cH\x94[\x9d8s\xa3\xb6\x8f\xfc\xf1\xdf\x01m\xf9\x08_N\xb5-\x16O\x89n\x95\xf3\x8b[\x1f&Yk?*\x07\x8fQ\x98\x85\xd5\xc1YL\xe0CB\xb2"!\x8d,\x90Q7\xca\x9c\x0e\xb2\x7f\xb0\xe1\xc8\xdd\xe7\xe1\xe4\x14\xb3\xa6\xb4\x8e\x8b\xed\xacM\xc3\x9d\xc4|U\x93k\x17\xac\x14\x86\x16\xd7\xebk\xbd{\xad}\x87\x13Y\x83\x9d\x88\x1e\x1b4\xa7r\xa6\x80\xbf\xf0\x15\x99\x11Q\xdb\xeb\xdf\x15ns\xc6\x85\xb3\x1d\xf5j\xc5\x87`=OD\x86\x86\x08\x8d\xb6\x0b\xec\x1d\x15\xc9\x93\x9a\xed\xa3\xe2\x96\xa4\xa2b\xa6\xa5h\xb0\xbb4\xb3\x0c\xa5\xdcu\x1f{\xb9\xaf\xd0W\xe1\xa3&\xa8\xb5\xea\xe5c\xfd\xc7?\xbdLg\xb3\xae\xb9\xb8*\xfc\xd5\xa6\xeeI\x15v\xdc\xa2`1VZ\xb5\x1c_`\x86\xbe{\xef\xae\t\xf2\xa9N\x00\x9a\xa1F\x84\xb2\xe3\xbc\xfa\xf7I\xee\xe8[~\x99;i\xfc%\xa8\x80\x80\x8e%\'\x9c+\x9c\xa9\x13R!\x80w\xc0\xda[\x84\xf6X\xfe\xc2\xe3\x0f\x94-\xbb`\x00\x00\x00\x00\x00\x00\x00\x93\xff\x1e\x1b\x15;e\xfe}\x83v K\xf9\r\xc9\xad\x9d\xddN\xcd\xcaWq\xfa\x8e\x98sn\x9b~t\x01 =p\xe5\xb1\x7f"!\xb4\xb9\xc9W\x90\x86\x80\x17\nm\xa0\x8dD\xb5\xaf\xfc\xa5\xf5%V]\xb9\x89a@\xe5\x0c@#%x\xecW\xed\xb0a\x98\x1a!C\x80B@{\xf0\xffJ{\xa3\xeayDP\'u'

View File

@ -6,15 +6,16 @@ import tempfile
from datetime import timedelta
from functools import partial
from pathlib import Path
from typing import Tuple
import maya
import pytest
from click.testing import CliRunner
from eth_account import Account
from eth_utils import to_checksum_address
from ferveo_py.ferveo_py import DkgPublicKey
from ferveo_py.ferveo_py import AggregatedTranscript, DkgPublicKey, DkgPublicParameters
from ferveo_py.ferveo_py import Keypair as FerveoKeyPair
from ferveo_py.ferveo_py import Validator
from ferveo_py.ferveo_py import Transcript, Validator
from twisted.internet.task import Clock
from web3 import Web3
@ -380,7 +381,7 @@ def log_in_and_out_of_test(request):
test_logger.info(f"Finalized {module_name}.py::{test_name}")
@pytest.fixture(scope='module')
@pytest.fixture(scope="session")
def get_random_checksum_address():
def _get_random_checksum_address():
canonical_address = os.urandom(20)
@ -699,8 +700,10 @@ def ursulas(testerchain, staking_providers, ursula_test_config):
_ursulas.clear()
@pytest.fixture(scope="module")
def dkg_public_key(get_random_checksum_address) -> DkgPublicKey:
@pytest.fixture(scope="session")
def dkg_public_key_data(
get_random_checksum_address,
) -> Tuple[AggregatedTranscript, DkgPublicKey, DkgPublicParameters]:
ritual_id = 0
num_shares = 4
threshold = 3
@ -726,7 +729,7 @@ def dkg_public_key(get_random_checksum_address) -> DkgPublicKey:
)
transcripts.append(transcript)
_, public_key, _ = dkg.aggregate_transcripts(
aggregate_transcript, public_key, params = dkg.aggregate_transcripts(
ritual_id=ritual_id,
me=validators[0],
shares=num_shares,
@ -734,4 +737,16 @@ def dkg_public_key(get_random_checksum_address) -> DkgPublicKey:
transcripts=list(zip(validators, transcripts)),
)
return public_key
return aggregate_transcript, public_key, params
@pytest.fixture(scope="session")
def dkg_public_key(dkg_public_key_data) -> DkgPublicKey:
_, dkg_public_key, _ = dkg_public_key_data
return dkg_public_key
@pytest.fixture(scope="session")
def aggregated_transcript(dkg_public_key_data) -> AggregatedTranscript:
aggregated_transcript, _, _ = dkg_public_key_data
return aggregated_transcript

View File

@ -1,23 +1,26 @@
import json
from unittest.mock import ANY
import pytest
from cryptography.hazmat.primitives.serialization import Encoding
from flask import Flask
from nucypher_core import Conditions, ThresholdDecryptionRequest
from nucypher_core.umbral import SecretKey, Signer
from nucypher.blockchain.eth.signers.software import Web3Signer
from nucypher.characters.lawful import Alice, Bob, Ursula
from nucypher.characters.lawful import Alice, Bob, Enrico, Ursula
from nucypher.config.constants import TEMPORARY_DOMAIN
from nucypher.crypto.keystore import Keystore
from nucypher.crypto.powers import DecryptingPower, DelegatingPower, TLSHostingPower
from nucypher.crypto.powers import (
DecryptingPower,
DelegatingPower,
ThresholdRequestDecryptingPower,
TLSHostingPower,
)
from nucypher.network.server import ProxyRESTServer
from nucypher.policy.payment import SubscriptionManagerPayment
from nucypher.utilities.networking import LOOPBACK_ADDRESS
from tests.constants import (
INSECURE_DEVELOPMENT_PASSWORD,
MOCK_ETH_PROVIDER_URI,
MOCK_IP_ADDRESS,
)
from tests.constants import INSECURE_DEVELOPMENT_PASSWORD, MOCK_ETH_PROVIDER_URI
from tests.utils.matchers import IsType
@ -131,3 +134,76 @@ def test_tls_hosting_certificate_remains_the_same(temp_dir_path, mocker):
rest_app=IsType(Flask),
hosting_power=tls_hosting_power)
recreated_ursula.disenchant()
def test_ritualist(temp_dir_path, testerchain, dkg_public_key):
keystore = Keystore.generate(
password=INSECURE_DEVELOPMENT_PASSWORD, keystore_dir=temp_dir_path
)
keystore.unlock(password=INSECURE_DEVELOPMENT_PASSWORD)
payment_method = SubscriptionManagerPayment(
eth_provider=MOCK_ETH_PROVIDER_URI, network=TEMPORARY_DOMAIN
)
ursula = Ursula(
start_learning_now=False,
keystore=keystore,
rest_host=LOOPBACK_ADDRESS,
rest_port=12345,
domain=TEMPORARY_DOMAIN,
payment_method=payment_method,
operator_address=testerchain.ursulas_accounts[0],
signer=Web3Signer(testerchain.client),
eth_provider_uri=MOCK_ETH_PROVIDER_URI,
)
ritual_id = 23
# Use actual decryption request
plaintext = b"Records break when you don't" # Jordan branch ad tagline
CONDITIONS = [
{"returnValueTest": {"value": "0", "comparator": ">"}, "method": "timelock"}
]
# encrypt
enrico = Enrico(encrypting_key=dkg_public_key)
ciphertext = enrico.encrypt_for_dkg(plaintext=plaintext, conditions=CONDITIONS)
decryption_request = ThresholdDecryptionRequest(
ritual_id=ritual_id,
variant=0,
ciphertext=bytes(ciphertext),
conditions=Conditions(json.dumps(CONDITIONS)),
)
request_encrypting_key = ursula.threshold_request_power.get_pubkey_from_ritual_id(
ritual_id=ritual_id
)
response_encrypting_key = SecretKey.random().public_key()
encrypted_decryption_request = decryption_request.encrypt(
request_encrypting_key=request_encrypting_key,
response_encrypting_key=response_encrypting_key,
)
# successful decryption
(
decrypted_decryption_request,
decrypted_response_encrypting_key,
) = ursula.threshold_request_power.decrypt_encrypted_request(
encrypted_decryption_request
)
assert bytes(decrypted_decryption_request) == bytes(decryption_request)
assert (
decrypted_response_encrypting_key.to_compressed_bytes()
== response_encrypting_key.to_compressed_bytes()
)
# failed decryption - incorrect encrypting key used
invalid_encrypted_decryption_request = decryption_request.encrypt(
request_encrypting_key=SecretKey.random().public_key(),
response_encrypting_key=response_encrypting_key,
)
with pytest.raises(
ThresholdRequestDecryptingPower.ThresholdRequestDecryptionFailed
):
ursula.threshold_request_power.decrypt_encrypted_request(
invalid_encrypted_decryption_request
)

View File

@ -1,10 +1,11 @@
import time
from enum import Enum
from typing import Dict, List, Union
from typing import Dict, List
from eth_typing import ChecksumAddress
from eth_utils import keccak
from ferveo_py.ferveo_py import DkgPublicKey
from ferveo_py.ferveo_py import AggregatedTranscript, DkgPublicKey, Transcript
from nucypher_core.umbral import PublicKey
from web3.types import TxReceipt
from nucypher.blockchain.eth.agents import CoordinatorAgent
@ -80,10 +81,10 @@ class MockCoordinatorAgent(MockContractAgent):
return self.blockchain.FAKE_RECEIPT
def post_transcript(
self,
ritual_id: int,
transcript: bytes,
transacting_power: TransactingPower
self,
ritual_id: int,
transcript: Transcript,
transacting_power: TransactingPower,
) -> TxReceipt:
ritual = self.rituals[ritual_id]
operator_address = transacting_power.account
@ -93,7 +94,7 @@ class MockCoordinatorAgent(MockContractAgent):
or transacting_power.account
)
participant = self.get_participant_from_provider(ritual_id, provider)
participant.transcript = transcript
participant.transcript = bytes(transcript)
ritual.total_transcripts += 1
if ritual.total_transcripts == ritual.dkg_size:
ritual.status = self.RitualStatus.AWAITING_AGGREGATIONS
@ -109,8 +110,9 @@ class MockCoordinatorAgent(MockContractAgent):
def post_aggregation(
self,
ritual_id: int,
aggregated_transcript: bytes,
aggregated_transcript: AggregatedTranscript,
public_key: DkgPublicKey,
request_encrypting_key: PublicKey,
transacting_power: TransactingPower,
) -> TxReceipt:
ritual = self.rituals[ritual_id]
@ -122,14 +124,15 @@ class MockCoordinatorAgent(MockContractAgent):
)
participant = self.get_participant_from_provider(ritual_id, provider)
participant.aggregated = True
participant.requestEncryptingKey = request_encrypting_key.to_compressed_bytes()
g1_point = self.Ritual.G1Point.from_dkg_public_key(public_key)
if len(ritual.aggregated_transcript) == 0:
ritual.aggregated_transcript = aggregated_transcript
ritual.aggregated_transcript = bytes(aggregated_transcript)
ritual.public_key = g1_point
elif bytes(ritual.public_key) != bytes(g1_point) or keccak(
ritual.aggregated_transcript
) != keccak(aggregated_transcript):
) != keccak(bytes(aggregated_transcript)):
ritual.aggregation_mismatch = True
# don't increment aggregations
# TODO Emit EndRitual here?

View File

@ -1,10 +1,13 @@
import pytest
from ferveo_py.ferveo_py import Keypair as FerveoKeyPair
from ferveo_py.ferveo_py import Validator
from nucypher.blockchain.economics import EconomicsFactory
from nucypher.blockchain.eth.actors import Operator
from nucypher.blockchain.eth.agents import ContractAgency
from nucypher.blockchain.eth.interfaces import BlockchainInterfaceFactory
from nucypher.blockchain.eth.registry import InMemoryContractRegistry
from nucypher.crypto.ferveo import dkg
from nucypher.crypto.powers import TransactingPower
from nucypher.network.nodes import Teacher
from tests.mock.interfaces import MockBlockchain, MockEthereumClient
@ -88,3 +91,30 @@ def mock_substantiate_stamp(module_mocker, monkeymodule):
module_mocker.patch.object(Ursula, "_substantiate_stamp", autospec=True)
module_mocker.patch.object(Ursula, "operator_signature", fake_signature)
module_mocker.patch.object(Teacher, "validate_operator")
@pytest.fixture(scope="session")
def random_transcript(get_random_checksum_address):
ritual_id = 0
num_shares = 4
threshold = 3
validators = []
for i in range(0, num_shares):
validators.append(
Validator(
address=get_random_checksum_address(),
public_key=FerveoKeyPair.random().public_key(),
)
)
validators.sort(key=lambda x: x.address) # must be sorte
transcript = dkg.generate_transcript(
ritual_id=ritual_id,
me=validators[0],
shares=num_shares,
threshold=threshold,
nodes=validators,
)
return transcript

View File

@ -7,25 +7,27 @@ import pytest
from constant_sorrow.constants import KEYSTORE_LOCKED
from cryptography.hazmat.primitives._serialization import Encoding
from mnemonic.mnemonic import Mnemonic
from nucypher_core.umbral import SecretKeyFactory
from nucypher_core.umbral import SecretKey, SecretKeyFactory
from nucypher.crypto.constants import UMBRAL_SECRET_KEY_SIZE
from nucypher.crypto.keystore import (
Keystore,
InvalidPassword,
validate_keystore_filename,
_MNEMONIC_LANGUAGE,
_DELEGATING_INFO,
)
from nucypher.crypto.keystore import (
_MNEMONIC_LANGUAGE,
InvalidPassword,
Keystore,
_assemble_keystore,
_serialize_keystore,
_deserialize_keystore,
_read_keystore,
_serialize_keystore,
_write_keystore,
_read_keystore
validate_keystore_filename,
)
from nucypher.crypto.powers import (
DecryptingPower,
DelegatingPower,
SigningPower,
ThresholdRequestDecryptingPower,
TLSHostingPower,
)
from nucypher.crypto.powers import DecryptingPower, SigningPower, DelegatingPower, TLSHostingPower
from nucypher.utilities.networking import LOOPBACK_ADDRESS
from tests.constants import INSECURE_DEVELOPMENT_PASSWORD
@ -311,3 +313,35 @@ def test_derive_hosting_power(tmpdir):
assert hosting_power.keypair.certificate.public_bytes(encoding=Encoding.PEM)
rederived_hosting_power = keystore.derive_crypto_power(power_class=TLSHostingPower, host=LOOPBACK_ADDRESS)
assert hosting_power.public_key().public_numbers() == rederived_hosting_power.public_key().public_numbers()
def test_derive_threshold_request_decrypting_power(tmpdir):
keystore = Keystore.generate(INSECURE_DEVELOPMENT_PASSWORD, keystore_dir=tmpdir)
keystore.unlock(password=INSECURE_DEVELOPMENT_PASSWORD)
threshold_request_decrypting_power = keystore.derive_crypto_power(
power_class=ThresholdRequestDecryptingPower
)
ritual_id = 23
request_encrypting_key = (
threshold_request_decrypting_power.get_pubkey_from_ritual_id(
ritual_id=ritual_id
)
)
other_request_encrypting_key = (
threshold_request_decrypting_power.get_pubkey_from_ritual_id(
ritual_id=ritual_id
)
)
assert (
request_encrypting_key.to_compressed_bytes()
== other_request_encrypting_key.to_compressed_bytes()
)
different_ritual_request_encrypting_key = (
threshold_request_decrypting_power.get_pubkey_from_ritual_id(ritual_id=0)
)
assert (
request_encrypting_key.to_compressed_bytes
!= different_ritual_request_encrypting_key.to_compressed_bytes()
)

View File

@ -1,11 +1,10 @@
import os
from collections import OrderedDict
from unittest.mock import Mock
import pytest
from eth_account import Account
from nucypher_core.umbral import SecretKey
from tests.constants import FAKE_TRANSCRIPT
from tests.mock.coordinator import MockCoordinatorAgent
from tests.mock.interfaces import MockBlockchain
@ -59,7 +58,9 @@ def test_mock_coordinator_initiation(mocker, nodes_transacting_powers, coordinat
assert set(signal_data["participants"]) == nodes_transacting_powers.keys()
def test_mock_coordinator_round_1(nodes_transacting_powers, coordinator):
def test_mock_coordinator_round_1(
nodes_transacting_powers, coordinator, random_transcript
):
ritual = coordinator.rituals[0]
assert (
coordinator.get_ritual_status(0)
@ -70,7 +71,7 @@ def test_mock_coordinator_round_1(nodes_transacting_powers, coordinator):
assert p.transcript == bytes()
for index, node_address in enumerate(nodes_transacting_powers):
transcript = FAKE_TRANSCRIPT
transcript = random_transcript
coordinator.post_transcript(
ritual_id=0,
@ -79,7 +80,7 @@ def test_mock_coordinator_round_1(nodes_transacting_powers, coordinator):
)
performance = ritual.participants[index]
assert performance.transcript == transcript
assert performance.transcript == bytes(transcript)
if index == len(nodes_transacting_powers) - 1:
assert len(coordinator.EVENTS) == 2
@ -91,7 +92,11 @@ def test_mock_coordinator_round_1(nodes_transacting_powers, coordinator):
def test_mock_coordinator_round_2(
nodes_transacting_powers, coordinator, dkg_public_key
nodes_transacting_powers,
coordinator,
aggregated_transcript,
dkg_public_key,
random_transcript,
):
ritual = coordinator.rituals[0]
assert (
@ -100,26 +105,33 @@ def test_mock_coordinator_round_2(
)
for p in ritual.participants:
assert p.transcript == FAKE_TRANSCRIPT
assert p.transcript == bytes(random_transcript)
aggregated_transcript = os.urandom(len(FAKE_TRANSCRIPT))
request_encrypting_keys = []
for index, node_address in enumerate(nodes_transacting_powers):
request_encrypting_key = SecretKey.random().public_key()
coordinator.post_aggregation(
ritual_id=0,
aggregated_transcript=aggregated_transcript,
public_key=dkg_public_key,
request_encrypting_key=request_encrypting_key,
transacting_power=nodes_transacting_powers[node_address]
)
request_encrypting_keys.append(request_encrypting_key)
if index == len(nodes_transacting_powers) - 1:
assert len(coordinator.EVENTS) == 2
assert ritual.aggregated_transcript == aggregated_transcript
assert ritual.aggregated_transcript == bytes(aggregated_transcript)
assert bytes(ritual.public_key) == bytes(dkg_public_key)
for p in ritual.participants:
for index, p in enumerate(ritual.participants):
# unchanged
assert p.transcript == FAKE_TRANSCRIPT
assert p.transcript != aggregated_transcript
assert p.transcript == bytes(random_transcript)
assert p.transcript != bytes(aggregated_transcript)
assert (
p.requestEncryptingKey
== request_encrypting_keys[index].to_compressed_bytes()
)
assert len(coordinator.EVENTS) == 2 # no additional event emitted here?
assert (

View File

@ -3,7 +3,6 @@ import pytest
from nucypher.blockchain.eth.agents import CoordinatorAgent
from nucypher.blockchain.eth.signers.software import Web3Signer
from nucypher.crypto.powers import TransactingPower
from tests.constants import FAKE_TRANSCRIPT
from tests.mock.coordinator import MockCoordinatorAgent
@ -84,10 +83,12 @@ def test_perform_round_1(ursula, random_address, cohort, agent):
)
def test_perform_round_2(ursula, cohort, transacting_power, agent, mocker):
def test_perform_round_2(
ursula, cohort, transacting_power, agent, mocker, random_transcript
):
participants = [
CoordinatorAgent.Ritual.Participant(
provider=c, aggregated=False, transcript=FAKE_TRANSCRIPT
provider=c, aggregated=False, transcript=bytes(random_transcript)
)
for c in cohort
]