Skip to content

Commit 0c5a9dc

Browse files
authored
Chore/Improve Rationals Development Experience (#14)
* feat: Improved Devex for Rationals and made small adjustments to types.py * docs: Improved examples and commenting * feat: Added script that permits using nada-algebra with pre-release versions of Nillion * afix: Added corrections to code to improve consistency * fix: updated readme.md for rationals example
1 parent 9d7bc9f commit 0c5a9dc

31 files changed

+1628
-665
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ Nada-Algebra is a Python library designed for algebraic operations on NumPy-like
88

99
## Features
1010

11+
### Use Numpy Array Features
1112
- **Dot Product**: Compute the dot product between two NadaArray objects.
1213
- **Element-wise Operations**: Perform element-wise addition, subtraction, multiplication, and division with broadcasting support.
1314
- **Stacking**: Horizontally and vertically stack arrays.
14-
- **Custom Function Application**: Apply custom functions to each element of the array.
15+
### Use Decimal Numbers in Nada
16+
- **Rational Number Support**: Our implementation of `Rational` and `SecretRational` allows the use of simplified implementations of decimal numbers on top of Nillion.
1517

1618
## Installation
1719
### Using pip

examples/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,6 @@
33
The following are the currently available examples:
44

55
- [Dot Product](./dot_product)
6-
- [Matrix Multiplication](./matrix_multiplication)
6+
- [Matrix Multiplication](./matrix_multiplication)
7+
- [Broadcasting](./broadcasting/)
8+
- [Rational Numbers](./rational_numbers/)
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# Import necessary libraries and modules
2+
import asyncio
3+
import py_nillion_client as nillion
4+
import os
5+
import sys
6+
import pytest
7+
import numpy as np
8+
import time
9+
from dotenv import load_dotenv
10+
11+
# Add the parent directory to the system path to import modules from it
12+
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
13+
14+
# Import helper functions for creating nillion client and getting keys
15+
from dot_product.network.helpers.nillion_client_helper import create_nillion_client
16+
from dot_product.network.helpers.nillion_keypath_helper import (
17+
getUserKeyFromFile,
18+
getNodeKeyFromFile,
19+
)
20+
import nada_algebra.client as na_client
21+
22+
# Load environment variables from a .env file
23+
load_dotenv()
24+
from dot_product.config.parameters import DIM
25+
26+
27+
# Main asynchronous function to coordinate the process
28+
async def main():
29+
print(f"USING: {DIM}")
30+
cluster_id = os.getenv("NILLION_CLUSTER_ID")
31+
userkey = getUserKeyFromFile(os.getenv("NILLION_USERKEY_PATH_PARTY_1"))
32+
nodekey = getNodeKeyFromFile(os.getenv("NILLION_NODEKEY_PATH_PARTY_1"))
33+
client = create_nillion_client(userkey, nodekey)
34+
party_id = client.party_id
35+
user_id = client.user_id
36+
party_names = na_client.parties(3)
37+
program_name = "main"
38+
program_mir_path = f"./target/{program_name}.nada.bin"
39+
40+
# Store the program
41+
action_id = await client.store_program(cluster_id, program_name, program_mir_path)
42+
program_id = f"{user_id}/{program_name}"
43+
print("Stored program. action_id:", action_id)
44+
print("Stored program_id:", program_id)
45+
46+
# Create and store secrets for two parties
47+
A = np.ones([DIM, DIM])
48+
stored_secret = nillion.Secrets(na_client.array(A, "A"))
49+
secret_bindings = nillion.ProgramBindings(program_id)
50+
secret_bindings.add_input_party(party_names[0], party_id)
51+
52+
# Store the secret for the specified party
53+
A_store_id = await client.store_secrets(
54+
cluster_id, secret_bindings, stored_secret, None
55+
)
56+
57+
B = np.ones([DIM, DIM])
58+
stored_secret = nillion.Secrets(na_client.array(B, "B"))
59+
60+
secret_bindings = nillion.ProgramBindings(program_id)
61+
secret_bindings.add_input_party(party_names[1], party_id)
62+
63+
# Store the secret for the specified party
64+
B_store_id = await client.store_secrets(
65+
cluster_id, secret_bindings, stored_secret, None
66+
)
67+
68+
# Set up the compute bindings for the parties
69+
compute_bindings = nillion.ProgramBindings(program_id)
70+
[
71+
compute_bindings.add_input_party(party_name, party_id)
72+
for party_name in party_names[:-1]
73+
]
74+
compute_bindings.add_output_party(party_names[-1], party_id)
75+
76+
print(f"Computing using program {program_id}")
77+
print(f"Use secret store_id: {A_store_id}, {B_store_id}")
78+
79+
computation_time_secrets = nillion.Secrets({"my_int2": nillion.SecretInteger(10)})
80+
81+
# Perform the computation and return the result
82+
compute_id = await client.compute(
83+
cluster_id,
84+
compute_bindings,
85+
[A_store_id, B_store_id],
86+
computation_time_secrets,
87+
nillion.PublicVariables({}),
88+
)
89+
90+
# Monitor and print the computation result
91+
print(f"The computation was sent to the network. compute_id: {compute_id}")
92+
while True:
93+
compute_event = await client.next_compute_event()
94+
if isinstance(compute_event, nillion.ComputeFinishedEvent):
95+
print(f"✅ Compute complete for compute_id {compute_event.uuid}")
96+
print(f"🖥️ The result is {compute_event.result.value}")
97+
return compute_event.result.value
98+
return result
99+
100+
101+
# Run the main function if the script is executed directly
102+
if __name__ == "__main__":
103+
asyncio.run(main())
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import os
2+
import py_nillion_client as nillion
3+
from helpers.nillion_payments_helper import create_payments_config
4+
5+
6+
def create_nillion_client(userkey, nodekey):
7+
bootnodes = [os.getenv("NILLION_BOOTNODE_MULTIADDRESS")]
8+
payments_config = create_payments_config()
9+
10+
return nillion.NillionClient(
11+
nodekey, bootnodes, nillion.ConnectionMode.relay(), userkey, payments_config
12+
)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import os
2+
import py_nillion_client as nillion
3+
4+
5+
def getUserKeyFromFile(userkey_filepath):
6+
return nillion.UserKey.from_file(userkey_filepath)
7+
8+
9+
def getNodeKeyFromFile(nodekey_filepath):
10+
return nillion.NodeKey.from_file(nodekey_filepath)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import os
2+
import py_nillion_client as nillion
3+
4+
5+
def create_payments_config():
6+
return nillion.PaymentsConfig(
7+
os.getenv("NILLION_BLOCKCHAIN_RPC_ENDPOINT"),
8+
os.getenv("NILLION_WALLET_PRIVATE_KEY"),
9+
int(os.getenv("NILLION_CHAIN_ID")),
10+
os.getenv("NILLION_PAYMENTS_SC_ADDRESS"),
11+
os.getenv("NILLION_BLINDING_FACTORS_MANAGER_SC_ADDRESS"),
12+
)

examples/rational_numbers/README.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Rational Numbers Tutorial
2+
3+
This tutorial shows how to use Nada Algebra Rational datatypes to work with fixed-point numbers in Nada.
4+
5+
## Notions
6+
7+
This tutorial uses fixed point numbers as it is the only available way to use Fixed-Point numbers in Nada. The representation of a fixed point number uses integer places to represent decimal digits. Thus, every number is multiplied by a scaling factor, that we refer to as `SCALE` ($\Delta = 2^{16}$) or `LOG_SCALE` in its logarithmic notation ($log_2\Delta = 16$). In a nutshell, this means we will use 16 bits to represent decimals.
8+
9+
If we want to input a variable `a = float(3.2)`, we need to first encode it. For that we will define a new variable `a'` which is going to be the scaled version. In this case, the scaling factor (to simplify) is going to by 3 bits so, $log_2\Delta = 3$ and $\Delta = 2^3 = 8$. With the following formula, we compute the encoded value:
10+
11+
$$ a' = round(a * \Delta) = round(a * 2^{log_2\Delta}) = 3.2 \cdot 2^3 = 3.2 \cdot 8 = 26 $$
12+
13+
Thus, in order to introduce a value with 3 bits of precision, we would be inputing 26 instead of 3.2.
14+
15+
16+
17+
## Example
18+
19+
```python
20+
from nada_dsl import *
21+
import nada_algebra as na
22+
23+
24+
def nada_main():
25+
# We define the number of parties
26+
parties = na.parties(3)
27+
28+
# We use na.SecretRational to create a secret rational number for party 0
29+
a = na.SecretRational("my_input_0", parties[0])
30+
31+
# We use na.SecretRational to create a secret rational number for party 1
32+
b = na.SecretRational("my_input_1", parties[1])
33+
34+
# This is a compile time rational number
35+
c = na.Rational(1.2)
36+
37+
# The formula below does operations on rational numbers and returns a rational number
38+
# It's easy to see that (a + b - c) is both on numerator and denominator, so the end result is b
39+
out_0 = ((a + b - c) * b) / (a + b - c)
40+
41+
return [
42+
Output(out_0.value, "my_output_0", parties[2]),
43+
]
44+
45+
46+
```
47+
48+
0. We import Nada algebra using `import nada_algebra as na`.
49+
1. We create an array of parties, with our wrapper using `parties = na.parties(3)` which creates an array of parties named: `Party0`, `Party1` and `Party2`.
50+
2. We create our secret floating point variable `a` as `SecretRational("my_input_0", parties[0])` meaning the variable belongs to `Party0` and the name of the variable is `my_input_0`.
51+
3. We create our secret floating point variable `b` as `SecretRational("my_input_1", parties[1])` meaning the variable belongs to `Party1` and the name of the variable is `my_input_1`.
52+
4. Then, we operate normally with this variables, and Nada Algebra will ensure they maintain the consistency of the decimal places.
53+
5. Finally, we produce the outputs of the array like: `Output(out_0.value, "my_output_0", parties[2]),` establishing that the output party will be `Party2`and the name of the output variable will be `my_output`. Not the difference between Nada Algebra and classic Nada where we add `out_0`**`.value`**.
54+
55+
# How to Run the Tutorial
56+
57+
1. Start by compiling the Nada program using the command:
58+
```
59+
nada build
60+
```
61+
62+
2. Next, ensure that the program functions correctly by testing it with:
63+
```
64+
nada test
65+
```
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
name = "rational_numbers"
2+
version = "0.1.0"
3+
authors = [""]
4+
5+
[[programs]]
6+
path = "src/rational_numbers.py"
7+
prime_size = 128
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# Import necessary libraries and modules
2+
import asyncio
3+
import py_nillion_client as nillion
4+
import os
5+
import sys
6+
import pytest
7+
import numpy as np
8+
import time
9+
from dotenv import load_dotenv
10+
11+
# Add the parent directory to the system path to import modules from it
12+
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
13+
14+
# Import helper functions for creating nillion client and getting keys
15+
from dot_product.network.helpers.nillion_client_helper import create_nillion_client
16+
from dot_product.network.helpers.nillion_keypath_helper import (
17+
getUserKeyFromFile,
18+
getNodeKeyFromFile,
19+
)
20+
import nada_algebra.client as na_client
21+
22+
# Load environment variables from a .env file
23+
load_dotenv()
24+
from dot_product.config.parameters import DIM
25+
26+
27+
# Main asynchronous function to coordinate the process
28+
async def main():
29+
print(f"USING: {DIM}")
30+
cluster_id = os.getenv("NILLION_CLUSTER_ID")
31+
userkey = getUserKeyFromFile(os.getenv("NILLION_USERKEY_PATH_PARTY_1"))
32+
nodekey = getNodeKeyFromFile(os.getenv("NILLION_NODEKEY_PATH_PARTY_1"))
33+
client = create_nillion_client(userkey, nodekey)
34+
party_id = client.party_id
35+
user_id = client.user_id
36+
party_names = na_client.parties(3)
37+
program_name = "main"
38+
program_mir_path = f"./target/{program_name}.nada.bin"
39+
40+
# Store the program
41+
action_id = await client.store_program(cluster_id, program_name, program_mir_path)
42+
program_id = f"{user_id}/{program_name}"
43+
print("Stored program. action_id:", action_id)
44+
print("Stored program_id:", program_id)
45+
46+
# Create and store secrets for two parties
47+
stored_secret = nillion.Secrets({"my_input_0": na_client.SecretRational(3.2)})
48+
secret_bindings = nillion.ProgramBindings(program_id)
49+
secret_bindings.add_input_party(party_names[0], party_id)
50+
51+
# Store the secret for the specified party
52+
A_store_id = await client.store_secrets(
53+
cluster_id, secret_bindings, stored_secret, None
54+
)
55+
56+
stored_secret = nillion.Secrets({"my_input_1": na_client.SecretRational(2.3)})
57+
secret_bindings = nillion.ProgramBindings(program_id)
58+
secret_bindings.add_input_party(party_names[1], party_id)
59+
60+
# Store the secret for the specified party
61+
B_store_id = await client.store_secrets(
62+
cluster_id, secret_bindings, stored_secret, None
63+
)
64+
65+
# Set up the compute bindings for the parties
66+
compute_bindings = nillion.ProgramBindings(program_id)
67+
[
68+
compute_bindings.add_input_party(party_name, party_id)
69+
for party_name in party_names[:-1]
70+
]
71+
compute_bindings.add_output_party(party_names[-1], party_id)
72+
73+
print(f"Computing using program {program_id}")
74+
print(f"Use secret store_id: {A_store_id}, {B_store_id}")
75+
76+
computation_time_secrets = nillion.Secrets({"my_int2": nillion.SecretInteger(10)})
77+
78+
# Perform the computation and return the result
79+
compute_id = await client.compute(
80+
cluster_id,
81+
compute_bindings,
82+
[A_store_id, B_store_id],
83+
computation_time_secrets,
84+
nillion.PublicVariables({}),
85+
)
86+
87+
# Monitor and print the computation result
88+
print(f"The computation was sent to the network. compute_id: {compute_id}")
89+
while True:
90+
compute_event = await client.next_compute_event()
91+
if isinstance(compute_event, nillion.ComputeFinishedEvent):
92+
print(f"✅ Compute complete for compute_id {compute_event.uuid}")
93+
print(f"🖥️ The result is {compute_event.result.value}")
94+
return compute_event.result.value
95+
return result
96+
97+
98+
# Run the main function if the script is executed directly
99+
if __name__ == "__main__":
100+
asyncio.run(main())
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import os
2+
import py_nillion_client as nillion
3+
from helpers.nillion_payments_helper import create_payments_config
4+
5+
6+
def create_nillion_client(userkey, nodekey):
7+
bootnodes = [os.getenv("NILLION_BOOTNODE_MULTIADDRESS")]
8+
payments_config = create_payments_config()
9+
10+
return nillion.NillionClient(
11+
nodekey, bootnodes, nillion.ConnectionMode.relay(), userkey, payments_config
12+
)

0 commit comments

Comments
 (0)