Skip to content

Commit f0e94e6

Browse files
authored
Merge branch 'main' into feat/bubble_sort
2 parents 6e8add6 + 698183e commit f0e94e6

32 files changed

+467
-139
lines changed

generate-streamlit-app.py

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import questionary
2+
from pathlib import Path
3+
import os
4+
import yaml
5+
import subprocess
6+
import shutil
7+
import sys
8+
9+
STREAMLIT_APP_TEMPLATE = '''# This file was automatically generated by the generate-streamlit-app script.
10+
# To run this file: from the root directory run `streamlit run streamlit_demo_apps/app_{program_name}.py`
11+
12+
import sys
13+
import os
14+
15+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
16+
import streamlit_app
17+
18+
program_name = "{program_name}"
19+
program_test_name = "{program_test_name}"
20+
21+
def main():
22+
current_dir = os.path.dirname(os.path.abspath(__file__))
23+
path_nada_bin = os.path.join(current_dir, "compiled_nada_programs", f"{{program_name}}.nada.bin")
24+
path_nada_json = os.path.join(current_dir, "compiled_nada_programs", f"{{program_name}}.nada.json")
25+
26+
if not os.path.exists(path_nada_bin):
27+
raise FileNotFoundError(f"Add `{{program_name}}.nada.bin` to the compiled_nada_programs folder.")
28+
if not os.path.exists(path_nada_json):
29+
raise FileNotFoundError(f"Run nada build --mir-json and add `{{program_name}}.nada.json` to the compiled_nada_programs folder.")
30+
31+
streamlit_app.main(program_test_name, path_nada_bin, path_nada_json)
32+
33+
if __name__ == "__main__":
34+
main()
35+
'''
36+
37+
def get_programs(directory):
38+
return sorted([f.stem for f in Path(directory).glob('*.py') if f.is_file()])
39+
40+
def get_test_files(directory, program_name):
41+
matching_files = []
42+
for file in Path(directory).glob('*.yaml'):
43+
try:
44+
with open(file, 'r') as f:
45+
test_data = yaml.safe_load(f)
46+
if test_data and 'program' in test_data and test_data['program'] == program_name:
47+
matching_files.append(file)
48+
except yaml.YAMLError:
49+
print(f"Error reading {file}. Skipping.")
50+
except Exception as e:
51+
print(f"Unexpected error reading {file}: {e}. Skipping.")
52+
return matching_files
53+
54+
def select_program_and_test():
55+
programs = get_programs('src')
56+
if not programs:
57+
print("No Python programs found in 'src' directory.")
58+
return None, None
59+
60+
selected_program = questionary.select(
61+
"Select an existing program to create a streamlit app demo:",
62+
choices=programs
63+
).ask()
64+
65+
test_files = get_test_files('tests', selected_program)
66+
if not test_files:
67+
print(f"No test files found for '{selected_program}' in 'tests' directory.")
68+
return selected_program, None
69+
70+
selected_test = questionary.select(
71+
"Select a test file for starting input values:",
72+
choices=[f.name for f in test_files]
73+
).ask()
74+
75+
return selected_program, selected_test
76+
77+
def build_nada_program(program_name):
78+
try:
79+
subprocess.run(['nada', 'build', program_name, '--mir-json'], check=True)
80+
print(f"Successfully built {program_name}")
81+
return True
82+
except subprocess.CalledProcessError as e:
83+
print(f"Error building {program_name}: {e}")
84+
return False
85+
except FileNotFoundError:
86+
print("Error: 'nada' command not found. Make sure it's installed and in your PATH.")
87+
return False
88+
89+
def copy_nada_files(program_name):
90+
source_dir = Path('target')
91+
dest_dir = Path('streamlit_demo_apps/compiled_nada_programs')
92+
93+
for ext in ['.nada.json', '.nada.bin']:
94+
source_file = source_dir / f"{program_name}{ext}"
95+
dest_file = dest_dir / f"{program_name}{ext}"
96+
97+
if source_file.exists():
98+
shutil.copy2(source_file, dest_file)
99+
print(f"Copied {source_file} to {dest_file}")
100+
else:
101+
print(f"Warning: {source_file} not found")
102+
103+
def create_streamlit_app(program_name, test_name):
104+
try:
105+
app_content = STREAMLIT_APP_TEMPLATE.format(
106+
program_name=program_name,
107+
program_test_name=test_name
108+
)
109+
110+
app_file_path = Path('streamlit_demo_apps') / f"app_{program_name}.py"
111+
print(f"Attempting to create file at: {app_file_path.absolute()}")
112+
113+
# Ensure the directory exists
114+
app_file_path.parent.mkdir(parents=True, exist_ok=True)
115+
116+
with open(app_file_path, 'w') as f:
117+
f.write(app_content)
118+
print(f"Created Streamlit app file: {app_file_path}")
119+
120+
if app_file_path.exists():
121+
print(f"Streamlit app file successfully created at {app_file_path}")
122+
return app_file_path
123+
else:
124+
print(f"Error: File creation verified failed for {app_file_path}")
125+
return None
126+
except Exception as e:
127+
print(f"Error creating Streamlit app file: {e}")
128+
return None
129+
130+
def run_streamlit_app(app_path):
131+
try:
132+
print(f"Attempting to run Streamlit app: {app_path}")
133+
subprocess.run([sys.executable, '-m', 'streamlit', 'run', str(app_path)], check=True)
134+
except subprocess.CalledProcessError as e:
135+
print(f"Error running Streamlit app: {e}")
136+
except Exception as e:
137+
print(f"Unexpected error running Streamlit app: {e}")
138+
139+
def main():
140+
program, test = select_program_and_test()
141+
142+
if program:
143+
print(f"Selected program: {program}")
144+
if test:
145+
print(f"Selected test file: {test}")
146+
147+
with open(os.path.join('tests', test), 'r') as file:
148+
test_data = yaml.safe_load(file)
149+
print("\nTest file contents:")
150+
print(yaml.dump(test_data, default_flow_style=False))
151+
152+
if build_nada_program(program):
153+
copy_nada_files(program)
154+
app_path = create_streamlit_app(program, os.path.splitext(test)[0] if test else '')
155+
if app_path:
156+
run_streamlit_app(app_path)
157+
else:
158+
print("No program selected.")
159+
160+
if __name__ == "__main__":
161+
main()

nada-project.toml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,4 +247,14 @@ prime_size = 128
247247
[[programs]]
248248
path = "src/bubble_sort.py"
249249
name = "bubble_sort"
250-
prime_size = 128
250+
prime_size = 128
251+
252+
[[programs]]
253+
path = "src/auction.py"
254+
name = "auction"
255+
prime_size = 128
256+
257+
[[programs]]
258+
path = "src/auction_can_tie.py"
259+
name = "auction_can_tie"
260+
prime_size = 128

nillion_client_script.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ async def store_inputs_and_run_blind_computation(
182182
blind_computation_results = compute_event.result.value
183183
return {
184184
'user_id': user_id,
185+
'user_key': userkey,
185186
'program_id': program_id,
186187
'store_ids': store_ids,
187188
'output': blind_computation_results,

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ nada-test
55
# streamlit demo dependencies
66
pyyaml
77
streamlit
8+
questionary
89

910
# nillion_client_script dependencies for streamlit demo
1011
py-nillion-client

src/auction.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from nada_dsl import *
2+
3+
def nada_main():
4+
# Define parties
5+
auctioneer = Party(name="auctioneer")
6+
num_bidders = 5
7+
bidders = [Party(name=f"bidder_{i}") for i in range(num_bidders)]
8+
9+
# Collect bids from each party
10+
bids = [SecretInteger(Input(name=f"bid_{i}", party=bidders[i])) for i in range(num_bidders)]
11+
12+
# Initialize variables to track the highest bid and the winning party index
13+
highest_bid = bids[0]
14+
winning_index = Integer(0)
15+
16+
# Compare bids to find the highest
17+
for i in range(1, num_bidders):
18+
is_higher = bids[i] > highest_bid
19+
highest_bid = is_higher.if_else(bids[i], highest_bid)
20+
winning_index = is_higher.if_else(Integer(i), winning_index)
21+
22+
# Output the highest bid and the winning party index
23+
return [
24+
Output(highest_bid, "highest_bid", auctioneer),
25+
Output(winning_index, "winning_index", auctioneer)
26+
]

src/auction_can_tie.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from nada_dsl import *
2+
3+
def nada_main():
4+
# Define the auctioneer as the party responsible for overseeing the auction results
5+
auctioneer = Party(name="auctioneer")
6+
7+
# Create a list of 5 bidders participating in the auction, 'bidder_0' to 'bidder_4'
8+
num_bidders = 5
9+
bidders = [Party(name=f"bidder_{i}") for i in range(num_bidders)]
10+
11+
# Collect bids from each bidder, where each bid is a SecretInteger input unique to the respective bidder
12+
bids = [SecretInteger(Input(name=f"bid_{i}", party=bidders[i])) for i in range(num_bidders)]
13+
14+
# Determine the highest bid among all the bidders
15+
# Start by initializing the highest bid to the first bidder's bid
16+
highest_bid = bids[0]
17+
# Iterate through the remaining bids to update the highest bid if a higher one is found
18+
for i in range(1, num_bidders):
19+
is_higher = bids[i] > highest_bid
20+
highest_bid = is_higher.if_else(bids[i], highest_bid)
21+
22+
# Create a list of outputs for each bidder, indicating if their bid matches the highest bid
23+
# Each output is a flag (1 if the bid matches the highest bid, 0 otherwise), visible to the auctioneer
24+
flag_highest_bid = [
25+
Output((bids[i] == highest_bid).if_else(Integer(1), Integer(0)), f"bidder_{i}_flag_highest_bid", auctioneer)
26+
for i in range(num_bidders)
27+
]
28+
29+
# Return the highest bid and a list of flags indicating which bidders flag the highest bid
30+
return [Output(highest_bid, "highest_bid", auctioneer)] + flag_highest_bid

streamlit_app.py

Lines changed: 46 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,39 @@ def parse_nada_json(json_data):
104104

105105
return input_info, output_info
106106

107+
import streamlit as st
108+
109+
def create_party_inputs(input_info, input_values):
110+
party_names = sorted(set(info['party'] for info in input_info.values()))
111+
updated_input_values = input_values.copy()
112+
113+
if len(party_names) > 1:
114+
# Create two columns if there's more than one party
115+
columns = st.columns(2)
116+
else:
117+
# Create a single column if there's only one party
118+
columns = [st.columns(1)[0]]
119+
120+
# Distribute parties between the columns
121+
for i, party_name in enumerate(party_names):
122+
with columns[i % len(columns)]:
123+
st.subheader(f"{party_name}'s inputs")
124+
for input_name, value in input_values.items():
125+
if input_info[input_name]['party'] == party_name:
126+
input_type = input_info[input_name]['type']
127+
if input_type == 'SecretBoolean':
128+
updated_input_values[input_name] = st.checkbox(
129+
label=f"{input_type}: {input_name}",
130+
value=bool(value)
131+
)
132+
else:
133+
updated_input_values[input_name] = st.number_input(
134+
label=f"{input_type}: {input_name}",
135+
value=value
136+
)
137+
138+
return updated_input_values
139+
107140
def main(nada_test_file_name=None, path_nada_bin=None, path_nada_json=None):
108141
# pass test name in via the command line
109142
if nada_test_file_name is None:
@@ -153,35 +186,19 @@ def main(nada_test_file_name=None, path_nada_bin=None, path_nada_json=None):
153186

154187
# Display the program code
155188
st.subheader(f"{program_name}.py")
156-
st.code(program_code, language='python')
189+
with st.expander(f"Nada Program: {program_name}"):
190+
st.code(program_code, language='python')
157191

158192
# Display inputs grouped by party, alphabetized
159-
updated_input_values = {}
160-
# Get unique party names and sort them alphabetically
161-
party_names = sorted(set(info['party'] for info in input_info.values()))
162-
163-
for party_name in party_names:
164-
st.subheader(f"{party_name}'s Inputs")
165-
for input_name, value in input_values.items():
166-
if input_info[input_name]['party'] == party_name:
167-
input_type = input_info[input_name]['type']
168-
if input_type == 'SecretBoolean':
169-
updated_input_values[input_name] = st.checkbox(
170-
label=f"{input_type}: {input_name}",
171-
value=bool(value)
172-
)
173-
else:
174-
updated_input_values[input_name] = st.number_input(
175-
label=f"{input_type}: {input_name}",
176-
value=value
177-
)
193+
updated_input_values = create_party_inputs(input_info, input_values)
178194

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

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

183199
# Button to store inputs with a loading screen
184200
if st.button('Run blind computation'):
201+
st.divider()
185202
# Conditional spinner text
186203
if should_store_inputs:
187204
spinner_text = "Storing the Nada program, storing inputs, and running blind computation on the Nillion Network Testnet..."
@@ -203,13 +220,12 @@ def main(nada_test_file_name=None, path_nada_bin=None, path_nada_json=None):
203220
# Call the async store_inputs_and_run_blind_computation function and wait for it to complete
204221
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))
205222

206-
st.divider()
207-
208223
st.subheader("Nada Program Result")
209224

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

214230
st.text('Nilchain Nillion Address')
215231
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']})")
@@ -219,8 +235,12 @@ def main(nada_test_file_name=None, path_nada_bin=None, path_nada_json=None):
219235
st.caption('The Store IDs are the unique identifiers used to reference input values you stored in the Nillion Network on the PetNet.')
220236
st.code(result_message['store_ids'], language='json')
221237

222-
st.text('PetNet User ID')
223-
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.")
238+
st.text('User Key')
239+
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.")
240+
st.code(result_message['user_key'], language='json')
241+
242+
st.text('User ID')
243+
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.")
224244
st.code(result_message['user_id'], language='json')
225245

226246
st.text('Program ID')

0 commit comments

Comments
 (0)