Skip to content

Commit

Permalink
Merge branch 'main' into feat/bubble_sort
Browse files Browse the repository at this point in the history
  • Loading branch information
oceans404 authored Oct 22, 2024
2 parents 6e8add6 + 698183e commit f0e94e6
Show file tree
Hide file tree
Showing 32 changed files with 467 additions and 139 deletions.
161 changes: 161 additions & 0 deletions generate-streamlit-app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import questionary
from pathlib import Path
import os
import yaml
import subprocess
import shutil
import sys

STREAMLIT_APP_TEMPLATE = '''# This file was automatically generated by the generate-streamlit-app script.
# To run this file: from the root directory run `streamlit run streamlit_demo_apps/app_{program_name}.py`
import sys
import os
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
import streamlit_app
program_name = "{program_name}"
program_test_name = "{program_test_name}"
def main():
current_dir = os.path.dirname(os.path.abspath(__file__))
path_nada_bin = os.path.join(current_dir, "compiled_nada_programs", f"{{program_name}}.nada.bin")
path_nada_json = os.path.join(current_dir, "compiled_nada_programs", f"{{program_name}}.nada.json")
if not os.path.exists(path_nada_bin):
raise FileNotFoundError(f"Add `{{program_name}}.nada.bin` to the compiled_nada_programs folder.")
if not os.path.exists(path_nada_json):
raise FileNotFoundError(f"Run nada build --mir-json and add `{{program_name}}.nada.json` to the compiled_nada_programs folder.")
streamlit_app.main(program_test_name, path_nada_bin, path_nada_json)
if __name__ == "__main__":
main()
'''

def get_programs(directory):
return sorted([f.stem for f in Path(directory).glob('*.py') if f.is_file()])

def get_test_files(directory, program_name):
matching_files = []
for file in Path(directory).glob('*.yaml'):
try:
with open(file, 'r') as f:
test_data = yaml.safe_load(f)
if test_data and 'program' in test_data and test_data['program'] == program_name:
matching_files.append(file)
except yaml.YAMLError:
print(f"Error reading {file}. Skipping.")
except Exception as e:
print(f"Unexpected error reading {file}: {e}. Skipping.")
return matching_files

def select_program_and_test():
programs = get_programs('src')
if not programs:
print("No Python programs found in 'src' directory.")
return None, None

selected_program = questionary.select(
"Select an existing program to create a streamlit app demo:",
choices=programs
).ask()

test_files = get_test_files('tests', selected_program)
if not test_files:
print(f"No test files found for '{selected_program}' in 'tests' directory.")
return selected_program, None

selected_test = questionary.select(
"Select a test file for starting input values:",
choices=[f.name for f in test_files]
).ask()

return selected_program, selected_test

def build_nada_program(program_name):
try:
subprocess.run(['nada', 'build', program_name, '--mir-json'], check=True)
print(f"Successfully built {program_name}")
return True
except subprocess.CalledProcessError as e:
print(f"Error building {program_name}: {e}")
return False
except FileNotFoundError:
print("Error: 'nada' command not found. Make sure it's installed and in your PATH.")
return False

def copy_nada_files(program_name):
source_dir = Path('target')
dest_dir = Path('streamlit_demo_apps/compiled_nada_programs')

for ext in ['.nada.json', '.nada.bin']:
source_file = source_dir / f"{program_name}{ext}"
dest_file = dest_dir / f"{program_name}{ext}"

if source_file.exists():
shutil.copy2(source_file, dest_file)
print(f"Copied {source_file} to {dest_file}")
else:
print(f"Warning: {source_file} not found")

def create_streamlit_app(program_name, test_name):
try:
app_content = STREAMLIT_APP_TEMPLATE.format(
program_name=program_name,
program_test_name=test_name
)

app_file_path = Path('streamlit_demo_apps') / f"app_{program_name}.py"
print(f"Attempting to create file at: {app_file_path.absolute()}")

# Ensure the directory exists
app_file_path.parent.mkdir(parents=True, exist_ok=True)

with open(app_file_path, 'w') as f:
f.write(app_content)
print(f"Created Streamlit app file: {app_file_path}")

if app_file_path.exists():
print(f"Streamlit app file successfully created at {app_file_path}")
return app_file_path
else:
print(f"Error: File creation verified failed for {app_file_path}")
return None
except Exception as e:
print(f"Error creating Streamlit app file: {e}")
return None

def run_streamlit_app(app_path):
try:
print(f"Attempting to run Streamlit app: {app_path}")
subprocess.run([sys.executable, '-m', 'streamlit', 'run', str(app_path)], check=True)
except subprocess.CalledProcessError as e:
print(f"Error running Streamlit app: {e}")
except Exception as e:
print(f"Unexpected error running Streamlit app: {e}")

def main():
program, test = select_program_and_test()

if program:
print(f"Selected program: {program}")
if test:
print(f"Selected test file: {test}")

with open(os.path.join('tests', test), 'r') as file:
test_data = yaml.safe_load(file)
print("\nTest file contents:")
print(yaml.dump(test_data, default_flow_style=False))

if build_nada_program(program):
copy_nada_files(program)
app_path = create_streamlit_app(program, os.path.splitext(test)[0] if test else '')
if app_path:
run_streamlit_app(app_path)
else:
print("No program selected.")

if __name__ == "__main__":
main()
12 changes: 11 additions & 1 deletion nada-project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -247,4 +247,14 @@ prime_size = 128
[[programs]]
path = "src/bubble_sort.py"
name = "bubble_sort"
prime_size = 128
prime_size = 128

[[programs]]
path = "src/auction.py"
name = "auction"
prime_size = 128

[[programs]]
path = "src/auction_can_tie.py"
name = "auction_can_tie"
prime_size = 128
1 change: 1 addition & 0 deletions nillion_client_script.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ async def store_inputs_and_run_blind_computation(
blind_computation_results = compute_event.result.value
return {
'user_id': user_id,
'user_key': userkey,
'program_id': program_id,
'store_ids': store_ids,
'output': blind_computation_results,
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ nada-test
# streamlit demo dependencies
pyyaml
streamlit
questionary

# nillion_client_script dependencies for streamlit demo
py-nillion-client
Expand Down
26 changes: 26 additions & 0 deletions src/auction.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from nada_dsl import *

def nada_main():
# Define parties
auctioneer = Party(name="auctioneer")
num_bidders = 5
bidders = [Party(name=f"bidder_{i}") for i in range(num_bidders)]

# Collect bids from each party
bids = [SecretInteger(Input(name=f"bid_{i}", party=bidders[i])) for i in range(num_bidders)]

# Initialize variables to track the highest bid and the winning party index
highest_bid = bids[0]
winning_index = Integer(0)

# Compare bids to find the highest
for i in range(1, num_bidders):
is_higher = bids[i] > highest_bid
highest_bid = is_higher.if_else(bids[i], highest_bid)
winning_index = is_higher.if_else(Integer(i), winning_index)

# Output the highest bid and the winning party index
return [
Output(highest_bid, "highest_bid", auctioneer),
Output(winning_index, "winning_index", auctioneer)
]
30 changes: 30 additions & 0 deletions src/auction_can_tie.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from nada_dsl import *

def nada_main():
# Define the auctioneer as the party responsible for overseeing the auction results
auctioneer = Party(name="auctioneer")

# Create a list of 5 bidders participating in the auction, 'bidder_0' to 'bidder_4'
num_bidders = 5
bidders = [Party(name=f"bidder_{i}") for i in range(num_bidders)]

# Collect bids from each bidder, where each bid is a SecretInteger input unique to the respective bidder
bids = [SecretInteger(Input(name=f"bid_{i}", party=bidders[i])) for i in range(num_bidders)]

# Determine the highest bid among all the bidders
# Start by initializing the highest bid to the first bidder's bid
highest_bid = bids[0]
# Iterate through the remaining bids to update the highest bid if a higher one is found
for i in range(1, num_bidders):
is_higher = bids[i] > highest_bid
highest_bid = is_higher.if_else(bids[i], highest_bid)

# Create a list of outputs for each bidder, indicating if their bid matches the highest bid
# Each output is a flag (1 if the bid matches the highest bid, 0 otherwise), visible to the auctioneer
flag_highest_bid = [
Output((bids[i] == highest_bid).if_else(Integer(1), Integer(0)), f"bidder_{i}_flag_highest_bid", auctioneer)
for i in range(num_bidders)
]

# Return the highest bid and a list of flags indicating which bidders flag the highest bid
return [Output(highest_bid, "highest_bid", auctioneer)] + flag_highest_bid
72 changes: 46 additions & 26 deletions streamlit_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,39 @@ def parse_nada_json(json_data):

return input_info, output_info

import streamlit as st

def create_party_inputs(input_info, input_values):
party_names = sorted(set(info['party'] for info in input_info.values()))
updated_input_values = input_values.copy()

if len(party_names) > 1:
# Create two columns if there's more than one party
columns = st.columns(2)
else:
# Create a single column if there's only one party
columns = [st.columns(1)[0]]

# Distribute parties between the columns
for i, party_name in enumerate(party_names):
with columns[i % len(columns)]:
st.subheader(f"{party_name}'s inputs")
for input_name, value in input_values.items():
if input_info[input_name]['party'] == party_name:
input_type = input_info[input_name]['type']
if input_type == 'SecretBoolean':
updated_input_values[input_name] = st.checkbox(
label=f"{input_type}: {input_name}",
value=bool(value)
)
else:
updated_input_values[input_name] = st.number_input(
label=f"{input_type}: {input_name}",
value=value
)

return updated_input_values

def main(nada_test_file_name=None, path_nada_bin=None, path_nada_json=None):
# pass test name in via the command line
if nada_test_file_name is None:
Expand Down Expand Up @@ -153,35 +186,19 @@ def main(nada_test_file_name=None, path_nada_bin=None, path_nada_json=None):

# Display the program code
st.subheader(f"{program_name}.py")
st.code(program_code, language='python')
with st.expander(f"Nada Program: {program_name}"):
st.code(program_code, language='python')

# Display inputs grouped by party, alphabetized
updated_input_values = {}
# Get unique party names and sort them alphabetically
party_names = sorted(set(info['party'] for info in input_info.values()))

for party_name in party_names:
st.subheader(f"{party_name}'s Inputs")
for input_name, value in input_values.items():
if input_info[input_name]['party'] == party_name:
input_type = input_info[input_name]['type']
if input_type == 'SecretBoolean':
updated_input_values[input_name] = st.checkbox(
label=f"{input_type}: {input_name}",
value=bool(value)
)
else:
updated_input_values[input_name] = st.number_input(
label=f"{input_type}: {input_name}",
value=value
)
updated_input_values = create_party_inputs(input_info, input_values)

output_parties = list(set(output['party'] for output in output_info.values()))

should_store_inputs = st.checkbox("Store secret inputs before running blind computation", value=False)

# Button to store inputs with a loading screen
if st.button('Run blind computation'):
st.divider()
# Conditional spinner text
if should_store_inputs:
spinner_text = "Storing the Nada program, storing inputs, and running blind computation on the Nillion Network Testnet..."
Expand All @@ -203,13 +220,12 @@ def main(nada_test_file_name=None, path_nada_bin=None, path_nada_json=None):
# Call the async store_inputs_and_run_blind_computation function and wait for it to complete
result_message = asyncio.run(store_inputs_and_run_blind_computation(input_data, program_name, output_parties, nilchain_private_key, path_nada_bin, cluster_id_from_streamlit_config, grpc_endpoint_from_streamlit_config, chain_id_from_streamlit_config, bootnodes, should_store_inputs))

st.divider()

st.subheader("Nada Program Result")

st.text('Output(s)')
# st.text('Output(s)')
st.success('Output(s)', icon="🖥️")
st.caption(f"The Nada program returned one or more outputs to designated output parties - {output_parties}")
st.success(result_message['output'], icon="🖥️")
st.json(result_message['output'])

st.text('Nilchain Nillion Address')
st.caption(f"Blind computation ran on the Nillion PetNet and operations were paid for on the Nilchain Testnet. Check out the Nilchain transactions that paid for each PetNet operation (store program, store secrets, compute) on the [Nillion Testnet Explorer](https://testnet.nillion.explorers.guru/account/{result_message['nillion_address']})")
Expand All @@ -219,8 +235,12 @@ def main(nada_test_file_name=None, path_nada_bin=None, path_nada_json=None):
st.caption('The Store IDs are the unique identifiers used to reference input values you stored in the Nillion Network on the PetNet.')
st.code(result_message['store_ids'], language='json')

st.text('PetNet User ID')
st.caption(f"The User ID is derived from your PetNet user public/private key pair and serves as your public user identifier on the Nillion Network. The user key is randomized every time you run this demo, so the User ID is also randomized.")
st.text('User Key')
st.caption(f"The user key is a private key derived from a PetNet user public/private key pair. It is randomized every time you run this page for the sake of the demo, ensuring that the key is different for each session.")
st.code(result_message['user_key'], language='json')

st.text('User ID')
st.caption(f"The user id is derived from your PetNet user key and serves as your public user identifier on the Nillion Network. Since the user key is randomized with each run of the demo, the user id is also randomized accordingly.")
st.code(result_message['user_id'], language='json')

st.text('Program ID')
Expand Down
Loading

0 comments on commit f0e94e6

Please sign in to comment.