nucypher/scripts/nucypher_dkg.py

443 lines
14 KiB
Python

import random
import time
import click
import maya
from nucypher_core.ferveo import DkgPublicKey
from web3 import Web3
from nucypher.blockchain.eth import domains
from nucypher.blockchain.eth.agents import (
ContractAgency,
CoordinatorAgent,
TACoChildApplicationAgent,
)
from nucypher.blockchain.eth.models import Coordinator
from nucypher.blockchain.eth.registry import ContractRegistry
from nucypher.blockchain.eth.signers import InMemorySigner, Signer
from nucypher.characters.lawful import Bob, Enrico
from nucypher.crypto.powers import TransactingPower
from nucypher.policy.conditions.lingo import ConditionLingo, ConditionType
from nucypher.utilities.emitters import StdoutEmitter
from nucypher.utilities.logging import GlobalLoggerSettings
from tests.constants import DEFAULT_TEST_ENRICO_PRIVATE_KEY, GLOBAL_ALLOW_LIST
GlobalLoggerSettings.start_console_logging()
emitter = StdoutEmitter(verbosity=2)
def get_transacting_power(signer: Signer):
account_address = signer.accounts[0]
emitter.echo(
f"Using {account_address} for initiation/authorization for DKG Ritual",
color="green",
)
password = click.prompt(
"Enter your keystore password", confirmation_prompt=False, hide_input=True
)
signer.unlock_account(account=account_address, password=password)
transacting_power = TransactingPower(signer=signer, account=account_address)
return transacting_power
@click.command()
@click.option(
"--domain",
"domain",
help="TACo Domain",
type=click.Choice([str(domains.TAPIR), str(domains.LYNX)]),
default=str(domains.LYNX),
)
@click.option(
"--eth-endpoint",
"eth_endpoint",
help="ETH staking network provider URI",
type=click.STRING,
required=True,
)
@click.option(
"--polygon-endpoint",
"polygon_endpoint",
help="Polygon network provider URI",
type=click.STRING,
required=True,
)
@click.option(
"--ritual-id",
"ritual_id",
"-r",
help="Ritual ID; defaults to -1 to initiate a new ritual",
type=click.INT,
default=-1,
required=True,
)
@click.option(
"--signer",
"signer_uri",
"-S",
help="Signer URI for initiating a new ritual with Coordinator contract",
default=None,
type=click.STRING,
)
@click.option(
"--dkg-size",
"dkg_size",
"-n",
help="Number of nodes to participate in ritual",
type=click.INT,
default=0,
)
@click.option(
"--num-rituals",
"num_rituals",
help="The number of rituals to initiate",
type=click.INT,
default=1,
)
@click.option(
"--ritual-duration",
"ritual_duration",
help="The duration (in seconds) for initialized ritual(s)",
type=click.INT,
default=60 * 60 * 24, # 24 hours
)
@click.option(
"--use-random-enrico",
"use_random_enrico",
help="Use a random Enrico signing account vs using a known default Enrico signing account (default)",
is_flag=True,
default=False,
)
@click.option(
"--access-controller",
"-a",
help=f"'{GLOBAL_ALLOW_LIST}' or 'OpenAccessAuthorizer' contract",
type=click.Choice([GLOBAL_ALLOW_LIST, "OpenAccessAuthorizer"]),
required=False,
)
def nucypher_dkg(
domain,
eth_endpoint,
polygon_endpoint,
ritual_id,
signer_uri,
dkg_size,
num_rituals,
ritual_duration,
use_random_enrico,
access_controller,
):
if ritual_id < 0:
# if creating ritual(s)
if signer_uri is None:
raise click.BadOptionUsage(
option_name="--signer",
message=click.style(
"--signer must be provided to create new ritual",
fg="red",
),
)
if access_controller is None:
raise click.BadOptionUsage(
option_name="--access-controller",
message=click.style(
"--access-controller must be provided to create new ritual",
fg="red",
),
)
if num_rituals < 1:
raise click.BadOptionUsage(
option_name="--num-rituals",
message=click.style("Number of rituals must be >= 1", fg="red"),
)
if ritual_id >= 0:
# if re-using existing ritual
if access_controller:
raise click.BadOptionUsage(
option_name="--access-controller",
message=click.style(
"--access-controller not needed since it is obtained from the Coordinator",
fg="red",
),
)
if num_rituals != 1:
raise click.BadOptionUsage(
option_name="--ritual-id, --num-rituals",
message=click.style(
"--ritual-id and --num-rituals cannot be used together", fg="red"
),
)
if dkg_size != 0:
raise click.BadOptionUsage(
option_name="--ritual-id, --dkg-size",
message=click.style(
"--ritual-id and --dkg-size cannot be used together", fg="red"
),
)
domain = domains.get_domain(domain)
registry = ContractRegistry.from_latest_publication(domain=domain)
coordinator_agent = ContractAgency.get_agent(
agent_class=CoordinatorAgent,
registry=registry,
blockchain_endpoint=polygon_endpoint,
) # type: CoordinatorAgent
child_application_agent = ContractAgency.get_agent(
agent_class=TACoChildApplicationAgent,
registry=registry,
blockchain_endpoint=polygon_endpoint,
) # type: TACoChildApplicationAgent
#
# Get deployer account
#
signer = None
transacting_power = None
# Get AccessController contract
blockchain = coordinator_agent.blockchain
#
# Initiate Ritual(s)
#
if ritual_id < 0:
# Obtain transacting power
signer = Signer.from_signer_uri(uri=signer_uri)
account_address = signer.accounts[0]
transacting_power = get_transacting_power(signer=signer)
emitter.echo("--------- Initiating Ritual ---------", color="yellow")
emitter.echo(
f"Commencing DKG Ritual(s) on {domain.polygon_chain.name} using {account_address}",
color="green",
)
access_controller_contract = blockchain.get_contract_by_name(
registry=registry, contract_name=access_controller
)
initiated_rituals = []
for i in range(num_rituals):
# find staking addresses
(
_,
staking_providers_dict,
) = child_application_agent.get_all_active_staking_providers()
staking_providers = list(staking_providers_dict.keys())
if domain == domains.LYNX:
# Ignore delinquent lynx providers
exclude_staking_providers = [
"0x24dbb0BEE134C3773D2C1791d65d99e307Fe86CF",
"0xE4c8d3bcf8C87D73CE38Ab2DC288d309072ee4E7",
]
for provider in exclude_staking_providers:
try:
staking_providers.remove(provider)
except ValueError:
pass
# sample then sort
dkg_staking_providers = random.sample(staking_providers, dkg_size)
dkg_staking_providers.sort()
emitter.echo(f"Using staking providers for DKG: {dkg_staking_providers}")
receipt = coordinator_agent.initiate_ritual(
providers=dkg_staking_providers,
authority=account_address,
duration=ritual_duration,
access_controller=access_controller_contract.address,
transacting_power=transacting_power,
)
start_ritual_event = (
coordinator_agent.contract.events.StartRitual().process_receipt(receipt)
)
ritual_id = start_ritual_event[0]["args"]["ritualId"]
ritual_status = coordinator_agent.get_ritual_status(ritual_id)
assert (
ritual_status != Coordinator.RitualStatus.NON_INITIATED
), "ritual successfully initiated"
initiated_rituals.append(ritual_id)
emitter.echo(
f"DKG Ritual #{ritual_id} initiated; tx: {Web3.to_hex(receipt['transactionHash'])}",
color="green",
)
#
# Wait for Ritual(s) to complete
# TODO perhaps reuse EventActuator here
#
completed_rituals = dict()
start_time = maya.now()
while True:
for initiated_ritual in initiated_rituals:
if initiated_ritual in completed_rituals:
# already completed
continue
ritual_status = coordinator_agent.get_ritual_status(initiated_ritual)
if ritual_status == Coordinator.RitualStatus.ACTIVE:
# success
emitter.echo(
f"DKG Ritual #{initiated_ritual} completed after {(maya.now() - start_time).seconds}s",
color="green",
)
completed_rituals[initiated_ritual] = ritual_status
elif (
# failure
ritual_status == Coordinator.RitualStatus.DKG_TIMEOUT
or ritual_status == Coordinator.RitualStatus.DKG_INVALID
):
emitter.error(
f"Ritual #{initiated_ritual} failed with status '{ritual_status}'"
)
completed_rituals[initiated_ritual] = ritual_status
if len(completed_rituals) >= num_rituals:
break
emitter.echo(
f"Waiting for Ritual(s) to complete ({len(completed_rituals)} / {num_rituals} completed); {(maya.now() - start_time).seconds}s elapsed thus far"
)
time.sleep(15)
emitter.echo("\n--------- Ritual(s) Summary ---------")
# sort by ritual id, print results, stop script
for r_id in sorted(completed_rituals.keys()):
ritual_status = completed_rituals[r_id]
if ritual_status == Coordinator.RitualStatus.ACTIVE:
message = f"✓ Ritual #{r_id} successfully created"
color = "green"
else:
message = f"x Ritual #{r_id} failed with status {ritual_status}"
color = "red"
emitter.echo(message, color=color)
return
else:
emitter.echo(f"Reusing existing DKG Ritual #{ritual_id}", color="green")
ritual = coordinator_agent.get_ritual(ritual_id) # ensure ritual can be found
#
# Encrypt some data
#
emitter.echo("--------- Data Encryption ---------")
PLAINTEXT = """
Those who mistake the unessential to be essential and the essential to be unessential,
dwelling in wrong thoughts, never arrive at the essential.
Those who know the essential to be essential and the unessential to be unessential,
dwelling in right thoughts, do arrive at the essential.
"""
# -- Dhammapada
CONDITIONS = {
"version": ConditionLingo.VERSION,
"condition": {
"conditionType": ConditionType.TIME.value,
"returnValueTest": {"value": 0, "comparator": ">"},
"method": "blocktime",
"chain": child_application_agent.blockchain.client.chain_id,
},
}
encrypting_key = DkgPublicKey.from_bytes(bytes(ritual.public_key))
private_key = None
if not use_random_enrico:
# use known enrico address
print("Using default Enrico signing account")
private_key = DEFAULT_TEST_ENRICO_PRIVATE_KEY
enrico_signer = InMemorySigner(private_key)
enrico_account = enrico_signer.accounts[0]
emitter.echo(f"Using account {enrico_account} to sign data")
enrico = Enrico(encrypting_key=encrypting_key, signer=enrico_signer)
threshold_message_kit = enrico.encrypt_for_dkg(
plaintext=PLAINTEXT.encode(), conditions=CONDITIONS
)
emitter.echo("-- Data encrypted --", color="green")
allow_list = blockchain.get_contract_by_name(
registry=registry, contract_name=GLOBAL_ALLOW_LIST
)
if ritual.access_controller == allow_list.address:
#
# Authorize Enrico to use the ritual
#
is_enrico_already_authorized = allow_list.functions.isAddressAuthorized(
ritual_id, enrico_account
).call()
if is_enrico_already_authorized:
emitter.echo(
f"Enrico {enrico_account} already authorized for DKG Ritual #{ritual_id}",
color="green",
)
elif click.confirm(f"Do you want to authorize Enrico ('{enrico_account}')?"):
# Obtain transacting power
if not signer_uri:
emitter.echo(
"--signer must be provided to initiate rituals", color="red"
)
return click.Abort()
if not signer:
signer = Signer.from_signer_uri(uri=signer_uri)
if not transacting_power:
transacting_power = get_transacting_power(signer)
# Authorize Enrico
contract_function = allow_list.functions.authorize(
ritual_id, [enrico_account]
)
blockchain.send_transaction(
contract_function=contract_function, transacting_power=transacting_power
)
emitter.echo(
f"Enrico {enrico_account} authorized to use DKG Ritual #{ritual_id}",
color="green",
)
else:
emitter.echo(
f"Enrico {enrico_account} may/may not be authorized to use DKG Ritual #{ritual_id} - decryption may fail",
color="yellow",
)
else:
emitter.echo(
f"No authorization checked/performed - Enrico {enrico_account} may/may not be authorized to use DKG Ritual #{ritual_id} - decryption may fail",
color="yellow",
)
#
# Get Data Decrypted
#
emitter.echo("--------- Threshold Decryption ---------")
bob = Bob(
domain=domain,
eth_endpoint=eth_endpoint,
polygon_endpoint=polygon_endpoint,
registry=registry,
)
bob.start_learning_loop(now=True)
cleartext = bob.threshold_decrypt(
threshold_message_kit=threshold_message_kit,
)
emitter.echo(f"\n-- Data decrypted -- \n{bytes(cleartext).decode()}", color="green")
assert bytes(cleartext).decode() == PLAINTEXT
if __name__ == "__main__":
nucypher_dkg()