Skip to content

Commit

Permalink
Added staticmethod wrapper to necessary methods
Browse files Browse the repository at this point in the history
Implemented jcli wrappers for tally related decryption
Implemented higher level methods for shares generation and tally decryption
  • Loading branch information
danielsanchezq committed Oct 28, 2020
1 parent 93a57d5 commit 57d4d15
Showing 1 changed file with 170 additions and 13 deletions.
183 changes: 170 additions & 13 deletions scripts/vitlib.py
Expand Up @@ -6,8 +6,6 @@
import os
import psycopg2

from cardanolib import CardanoCLIWrapper

class VITBridge:
"""VIT tools to bridge Cardano mainnet and jormungandr"""

Expand All @@ -21,12 +19,14 @@ def __init__(self, network_magic, state_dir, db=None, dbuser="", dbhost=""):
if db:
self.db = psycopg2.connect(user=dbuser, host=dbhost, database=db)

def write_key(self, name, contents):
@staticmethod
def write_key(name, contents):
with open(name, "w") as f:
f.write(contents)
f.close()

def read_cardano_key(self, name):
@staticmethod
def read_cardano_key(name):
with open(name) as f:
data = json.load(f)["cborHex"]
return binascii.hexlify(cbor2.loads(binascii.unhexlify(data))).decode("ascii")
Expand All @@ -42,11 +42,13 @@ def get_cardano_vkey(self, skey_file):
os.unlink(vkey_file)
return vkey

def read_jcli_key(self, name):
with open(name) as f:
@staticmethod
def read_jcli_key(key_path):
with open(key_path) as f:
return f.read().rstrip()

def convert_jcli_key_to_bytes(self, key):
@staticmethod
def convert_jcli_key_to_bytes(key):
cli_args = [ "jcli", "key", "to-bytes" ]
p = subprocess.run(cli_args, capture_output=True, text=True, input=key, encoding='ascii')
if p.returncode != 0:
Expand All @@ -66,7 +68,8 @@ def jcli_sign(self, key, text):
raise Exception("Unknown error signing")
return p.stdout.rstrip()

def convert_key_to_jcli(self, key):
@staticmethod
def convert_key_to_jcli(key):
cli_args = [ "jcli", "key", "from-bytes", "--type", "ed25519" ]
p = subprocess.run(cli_args, capture_output=True, text=True, input=key, encoding='ascii')

Expand All @@ -75,24 +78,26 @@ def convert_key_to_jcli(self, key):
raise Exception("Unknown error converting from hex to bech32")
return p.stdout.rstrip()

def jcli_key_public(self, skey):
@staticmethod
def jcli_key_public(skey):
cli_args = [ "jcli", "key", "to-public" ]
p = subprocess.run(cli_args, capture_output=True, text=True, input=skey, encoding='ascii')
if p.returncode != 0:
print(p.stderr)
raise Exception("Unknown error converting to public")
return p.stdout.rstrip()


def bech32_to_hex(self, bech32_string):
@staticmethod
def bech32_to_hex(bech32_string):
cli_args = [ "bech32" ]
p = subprocess.run(cli_args, capture_output=True, text=True, input=bech32_string, encoding='ascii')
if p.returncode != 0:
print(p.stderr)
raise Exception("Unknown error converting bech32 string to hex")
return p.stdout.rstrip()

def prefix_bech32(self, prefix, key):
@staticmethod
def prefix_bech32(prefix, key):
cli_args = [ "bech32", prefix ]
p = subprocess.run(cli_args, capture_output=True, text=True, input=key, encoding="ascii")

Expand All @@ -119,6 +124,7 @@ def validate_sig(self, pub_key, sig, data):
else:
return True

@staticmethod
def generate_meta_data(self, stake, vote, sig):
meta = { "1": {
"purpose": "voting_registration",
Expand All @@ -144,7 +150,6 @@ def get_stake_hash(self, stake_vkey):
raise Exception("Unknown error generating stake address")
return p.stdout.rstrip()


def fetch_voting_keys(self):
cursor = self.db.cursor()
# TODO: maybe add psycopg2.extra for parsing the json
Expand All @@ -169,3 +174,155 @@ def get_stake(self, stake_hash):
if row[0]:
return int(row[0].to_integral_value())
return 0

@staticmethod
def jcli_generate_share(encrypted_tally_path, decryption_key_path):
cli_args = [
"jcli", "vote", "tally", "generate-decryption-share",
"--encrypted-tally", encrypted_tally_path,
"--decryption-key", decryption_key_path
]
try:
result = subprocess.check_output(cli_args)
return json.loads(result)
except subprocess.CalledProcessError as e:
print(f"Error executing process, exit code {e.returncode}:\n{e.output}")

@staticmethod
def jcli_decrypt_tally(encrypted_tally_path, shares_path, threshold, max_votes, table_size, output_format="json"):
cli_args = [
"jcli", "vote", "tally", "tally-decrypt-with-all-shares",
"--encrypted-tally", encrypted_tally_path,
"--shares", shares_path,
"--threshold", threshold,
"--max-votes", max_votes,
"--table-size", table_size,
"--output-format", output_format
]
try:
result = subprocess.check_output(cli_args)
if output_format.lower() == "json":
return json.loads(result)
return result
except subprocess.CalledProcessError as e:
print(f"Error executing process, exit code {e.returncode}:\n{e.output}")

@staticmethod
def generate_committee_member_shares(rest_api_url, decryption_key_path, output_file="./proposals.shares"):
# some imports needed just for this method
import requests

full_url = f"{rest_api_url}/v0/vote/active/plans"
try:
active_vote_plans = requests.get(full_url).json()
except ValueError:
raise Exception(f"Couldn't get a proper json reply from {rest_api_url}")

# Active voteplans dict would look like:
# {
# id: Hash,
# payload: PauloadType,
# vote_start: BlockDate,
# vote_end: BlcokDate,
# committee_end: BlockDate,
# committee_member_keys: [MemberPublicKey]
# proposals: [
# index: int,
# proposal_id: Hash,
# options: [u8],
# tally: Tally(Public or Private),
# votes_cast: int,
# ]
# }
proposals = active_vote_plans["proposals"]
for proposal in proposals:
try:
encrypted_tally = proposal["tally"]["private"]["encrypted"]["encrypted_tally"]
except KeyError:
raise Exception(f"Tally data wasn't expected:\n{proposal}")
f, tmp_tally_path = tempfile.mkstemp()
# result is of format:
# {
# state: base64,
# share: base64
# }
result = VITBridge.jcli_generate_share(tmp_tally_path, decryption_key_path)
proposal["shares"] = result["share"]


with open(output_file, "w") as f:
json.dump(f, proposals, indent=4)
print(f"Shares file processed properly at: {output_file}")

@staticmethod
def merge_generated_shares(*share_files_paths, output_file="aggregated_shares.shares"):
from functools import reduce

def load_data(path):
with open(path) as f:
return json.load(f)

def merge_two_shares_data(data1, data2):
data1["shares"].append(data2.pop())
return data1

shares_data = (load_data(p) for p in share_files_paths)
full_data = reduce(merge_two_shares_data, shares_data)
with open(output_file, "w") as f:
json.dump(f, full_data)
print(f"Data succesfully aggregated at: {output_file}")

@staticmethod
def tally_with_shares(aggregated_data_shares_file, output_file="decrypted_tally"):

def write_shares(f, shares):
for s in shares:
f.writeline(s)

def write_tally(f, tally):
f.write(tally)

with open(aggregated_data_shares_file) as f:
try:
aggregated_data = json.load(f)
except ValueError:
raise Exception(f"Error loading data from file: {aggregated_data_shares_file}")

for data in aggregated_data:
try:
encrypted_tally = data["tally"]["private"]["encrypted"]["encrypted_tally"]
shares = data["shares"]
except KeyError:
raise Exception(f"Tally data wasn't expected:\n{data}")

_, tmp_tally_file = tempfile.mkstemp()
_, tmp_shares_file = tempfile.mkstemp()

with open(tmp_tally_file, "w") as tally_f, open(tmp_shares_file, "w") as shares_f:
write_tally(tally_f, encrypted_tally)
write_shares(shares_f, shares)


threshold = len(shares)
max_votes = data["votes_cast"]
options = len(data["options"])
table_size = max_votes // options

result = VITBridge.jcli_decrypt_tally(
tmp_tally_file, tmp_shares_file, threshold, max_votes, table_size
)

data["tally"]["tally_result"] = result

with open(output_file, "w") as f:
json.dump(f, aggregated_data)

print("Tally successfully decrypted")








0 comments on commit 57d4d15

Please sign in to comment.