Nada Numpy
nada-numpy is an opinionated clone of Numpy, seamlessly integrated with the Nada DSL. It allows for fast and efficient manipulation of array structures using Nada DSL, while extending functionality to include interfacing with decimal numbers.
One key difference between Numpy and Nada Numpy is that Nada Numpy enforces strong typing. Unlike Numpy, which dynamically changes types, Nada Numpy requires a strongly-typed interface, working with root families (Integer
, UnsignedInteger
, Rational
, and SecretBoolean
).
In this tutorial, we’ll explore the capabilities of Nada Numpy together. Before we start, make sure you have the Nillion SDK installed.
Installation
First things first, let’s get Nada Numpy installed on your system. It’s as easy as running:
pip install --upgrade nada-numpy
Writing a Nada-Numpy program
Nada Numpy aligns with the structure of the Nada DSL. A typical Nada Numpy program comprises the following sections:
- Import Section
- Party Declaration Section
- Input Declaration Section
- Computation Section
- Output Section
To get started, we can initialize our project structure with the following command:
nada init hello-nada-numpy
This sets up everything we need. Now, let’s dive into the code! We’ll create a program that adds two arrays, similar to the Nada DSL starter which adds two variables. Here’s the complete code to put in src/main.py
:
from nada_dsl import *
import nada_numpy as na
def nada_main():
parties = na.parties(3)
a = na.array([3], parties[0], "A", SecretInteger)
b = na.array([3], parties[1], "B", SecretInteger)
result = a + b
return result.output(parties[2], "my_output")
Let’s break this down step-by-step.
1. Party Declaration Section
Start by importing the necessary modules. We need Nada DSL and Nada Numpy, so we import them like this:
from nada_dsl import *
import nada_numpy as na
Next, define the main function for our program:
def nada_main():
In this section, we’ll declare the parties involved in our computation. Parties represent the different participants in our Nada program. Nada Numpy makes it easy to create multiple parties with the na.parties
function:
parties = na.parties(3)
Instead of having to define each party manually, we use Nada Numpy functionality. In this case with na.parties(3)
we'll be creating a list of three parties named: Party0
, Party1
and Party2
.
2. Input Declaration Section
Now, let’s define our inputs. We’ll create two NadaArrays
, each with three elements. The na.array
function allows us to define an array with arbitrary dimensions and types.
a = na.array([3], parties[0], "A", SecretInteger)
This line creates an array a with 3 elements, owned by Party0
, named “A”
, and of type SecretInteger
.
Similarly, we define the second array b:
b = na.array([3], parties[1], "B", SecretInteger)
This array is also 3 elements long, owned by Party1
, named “B”
, and of type SecretInteger
.
Under the hood, NadaArray creates variables for each element of the array. For example, a
will have variables "A_0"
, "A_1"
, and "A_2"
, while b
will have "B_0"
, "B_1"
, and "B_2"
.
3. Computation Phase
With our inputs ready, we can perform computations. Adding the two arrays is straightforward. In the Numpy fashion, we'll be operating all the variables consecutively with the +
operator.
res = a + b
This line operates each entry of the array linearly. If our input arrays were: a = [1, 2, 3]
and b = [4, 5, 6]
we would be summing each entry of array a
with the corresponding entry of array b
. That is: res[0] = a[0] + b[0]
, res[1] = a[1] + b[1]
and res[2] = a[2] + b[2]
; and obtaining res = [5, 7, 9]
.
You can experiment with different operations (+, -, *, /) and array dimensions to see what Nada Numpy can do!
4. Output Section
Finally, we need to produce the output. Since NadaArray
is not a base Nada type, we use the .output
method to generate the output. This method takes the output party and the output variable name as arguments:
return res.output(parties[2], "my_output")
In this case, we'll be invoking res.output(parties[2], "my_output")
establishing that the output party will be Party2
and the name of the output variable will be "my_output"
.
With everything in place, we can build and test our program:
nada build
Using Nada Numpy with Nillion Network
After completing the program writing, we can upload it and interact with it in the network with the ease provided by Nada Numpy. For that, we use the Python Nillion Client. We can use the same complete code as for other examples. The only difference is how Nada Numpy allows to easily include arrays in our uploads to the Nillion Network with the Nada Numpy client. We add the link to the complete code.
First, import the necessary modules:
import numpy as np
import nada_numpy.client as na_client
Then, we can use it to introduce arrays with a similar syntax that will take care of the array formatting na_client.array
. The first element will be the array with the secret values that we want to upload. The second value is the name, which shall match with the initial name set on the Nada Program.
A = np.ones((3,)) # Sample numpy array with ones [1, 1, 1]
stored_secret = nillion.Secrets(na_client.array(A, "A"))
And that’s it! You’ve successfully created, built, and integrated a Nada Numpy program with the Nillion Network.
For more examples, please visit our Github Repository Examples.