Compute
Perform single or multi party computation using secrets stored in the network.
๐ Single Party Examples
๐ Multi Party Examples
๐ Millionaires Problem
Single Party Computeโ
Single party compute involves only one Party that provides inputs and receives outputs of a program. Single party compute examples are available in the Python Starter Repo core_concept_single_party_compute folder.
Example: addition_simple.pyโ
The addition_simple example is a single party compute example that adds two secret integers. In the client code, the first secret integer is stored in the network ahead of time, and the second secret integer is provided at computation time.
- Client code
- Nada program
PrivateKey,
)
from dotenv import load_dotenv
home = os.getenv("HOME")
load_dotenv(f"{home}/.config/nillion/nillion-devnet.env")
# 1 Party running simple addition on 1 stored secret and 1 compute time secret
async def main():
# Use the devnet configuration generated by `nillion-devnet`
network = Network.from_config("devnet")
# Create payments config and set up Nillion wallet with a private key to pay for operations
nilchain_key: str = os.getenv("NILLION_NILCHAIN_PRIVATE_KEY_0") # type: ignore
payer = NilChainPayer(
network,
wallet_private_key=NilChainPrivateKey(bytes.fromhex(nilchain_key)),
gas_limit=10000000,
)
# Use a random key to identify ourselves
signing_key = PrivateKey()
client = await VmClient.create(signing_key, network, payer)
party_name = "Party1"
program_name = "addition_simple"
program_mir_path = f"../nada_programs/target/{program_name}.nada.bin"
# Adding funds to the client balance so the upcoming operations can be paid for
funds_amount = 3000000
print(f"๐ฐ Adding some funds to the client balance: {funds_amount} uNIL")
await client.add_funds(funds_amount)
##### STORE PROGRAM
print("-----STORE PROGRAM")
# Store program
program_mir = open(program_mir_path, "rb").read()
program_id = await client.store_program(program_name, program_mir).invoke()
# Print details about stored program
print(f"Stored program_id: {program_id}")
##### STORE SECRETS
print("-----STORE SECRETS")
# Create a secret
values = {
"my_int1": SecretInteger(500),
}
# Create a permissions object to attach to the stored secret
permissions = Permissions.defaults_for_user(client.user_id).allow_compute(
client.user_id, program_id
)
# Store a secret, passing in the receipt that shows proof of payment
values_id = await client.store_values(
values, ttl_days=5, permissions=permissions
).invoke()
# Print details about stored program
print(f"Stored secret values_id: {values_id}")
##### COMPUTE
print("-----COMPUTE")
# Bind the parties in the computation to the client to set input and output parties
input_bindings = [InputPartyBinding(party_name, client.user_id)]
output_bindings = [OutputPartyBinding(party_name, [client.user_id])]
# Create a computation time secret to use
compute_time_values = {"my_int2": SecretInteger(10)}
# Compute, passing in the compute time values as well as the previously uploaded value.
print(f"Invoking computation using program_id: {program_id} and secret values_id: {values_id}")
compute_id = await client.compute(
program_id,
input_bindings,
output_bindings,
values=compute_time_values,
value_ids=[values_id],
).invoke()
# Print compute result
print(f"The computation was sent to the network. compute_id: {compute_id}")
result = await client.retrieve_compute_results(compute_id).invoke()
print(f"โ
Compute complete for compute_id {compute_id}")
print(f"๐ฅ๏ธ The result is {result}")
balance = await client.balance()
print(f"๐ฐ Final client balance: {balance.balance} Credits")
client.close()
return result
if __name__ == "__main__":
asyncio.run(main())
@pytest.mark.asyncio
async def test_main():
result = await main()
assert result == {"my_output": SecretInteger(510)}
from nada_dsl import *
def nada_main():
party1 = Party(name="Party1")
my_int1 = SecretInteger(Input(name="my_int1", party=party1))
my_int2 = SecretInteger(Input(name="my_int2", party=party1))
new_int = my_int1 + my_int2
return [Output(new_int, "my_output", party1)]
Multi Party Computeโ
Multi party compute involves more than one Party. These Parties collaborate to provide secret inputs and one Party receives outputs of the program.
The core_concept_multi_party_compute folder has a 3-step multi party compute example involving multiple parties providing secret inputs for computation of a program. The first party stores a secret, then N other parties store permissioned secrets giving the first party compute access. The first party computes with all secrets.
- README
- Config file
- Step 1
- Step 2
- Step 3
# Multi Party Example
This is an example using the Nillion Python Client to store a program, and run multi party compute, computation involving secret inputs from multiple parties.
## Run the example
1. Ensure the `nillion-devnet` is running (this will write the local devnet configs to your machine, which will be picked up by the examples in this repo) or ensure you are reading the real network environments into the examples.
2. Ensure the multi party program you want to run (defined in `config.py`) is compiled, by running `nada build` in the `nada_programs` directory.
3. Run the scripts in number order, starting with `01_store_program.py`, check the ouputs of each script to get the
inputs to the next.
import hashlib
# replace this with your name
CONFIG_PROGRAM_NAME = "addition_simple_multi_party"
# 1st party
CONFIG_PARTY_1 = {
"private_key": hashlib.sha256(b"party_1_seed").digest(),
"party_name": "Party1",
"secrets": {
"my_int1": 1,
},
}
# N other parties
CONFIG_N_PARTIES = [
{
"private_key": hashlib.sha256(b"party_2_seed").digest(),
"party_name": "Party2",
"secret_name": "my_int2",
"secret_value": 5,
},
{
"private_key": hashlib.sha256(b"party_3_seed").digest(),
"party_name": "Party3",
"secret_name": "my_int3",
"secret_value": 2,
},
]
Step 1: 1st Party Stores a Secretโ
404: Not Found
Step 2: N other parties store a permissioned secretโ
"--program_id",
required=True,
type=str,
help="The id of the program that will run on the uploaded secrets",
)
args = parser.parse_args(args)
# Use the devnet configuration generated by `nillion-devnet`
network = Network.from_config("devnet")
# Create payments config and set up Nillion wallet with a private key to pay for operations
nilchain_key: str = os.getenv("NILLION_NILCHAIN_PRIVATE_KEY_0") # type: ignore
payer = NilChainPayer(
network,
wallet_private_key=NilChainPrivateKey(bytes.fromhex(nilchain_key)),
gas_limit=10000000,
)
uploader_user_id = UserId.parse(args.user_id_1)
# start a list of values ids to keep track of stored secrets
values_ids = []
user_ids = []
for party_info in CONFIG_N_PARTIES:
signing_key = PrivateKey(party_info["private_key"])
client = await VmClient.create(signing_key, network, payer)
party_name = party_info["party_name"]
secret_name = party_info["secret_name"]
secret_value = party_info["secret_value"]
# Create a secret for the current party
values = {secret_name: SecretInteger(secret_value)}
# Create permissions object
permissions = Permissions.defaults_for_user(client.user_id).allow_compute(
uploader_user_id, args.program_id
)
# Store the permissioned secret
values_id = await client.store_values(
values=values, ttl_days=5, permissions=permissions
).invoke()
values_ids.append(values_id)
user_ids.append(client.user_id)
print(
f"\n๐N Party {party_name} stored {secret_name}: {secret_value} at store id: {values_id} using user id {client.user_id}"
)
print(
f"\n๐Compute permission on the secret granted to user_id: {args.user_id_1}\n--------------------------------"
)
user_ids_to_values_ids = " ".join(
[f"{party_id}:{store_id}" for party_id, store_id in zip(user_ids, values_ids)]
)
print(
"\n๐โฌ๏ธ Copy and run the following command to run multi party computation using the secrets"
)
print(
f"\npython3 03_multi_party_compute.py --user_ids_to_values_ids {user_ids_to_values_ids} --program_id {args.program_id}"
)
return [user_ids_to_values_ids, args.program_id]
if __name__ == "__main__":
asyncio.run(main())
@pytest.mark.asyncio
async def test_main():
Step 3: The 1st Party computes with all secretsโ
)
args = parser.parse_args(args)
# Use the devnet configuration generated by `nillion-devnet`
network = Network.from_config("devnet")
# Create payments config and set up Nillion wallet with a private key to pay for operations
nilchain_key: str = os.getenv("NILLION_NILCHAIN_PRIVATE_KEY_0") # type: ignore
payer = NilChainPayer(
network,
wallet_private_key=NilChainPrivateKey(bytes.fromhex(nilchain_key)),
gas_limit=10000000,
)
# Use the same identity we used originally to upload the program
signing_key = PrivateKey(CONFIG_PARTY_1["private_key"])
client = await VmClient.create(signing_key, network, payer)
print(f"Computing using program {args.program_id}")
# Construct the input party bindings.
input_bindings = []
values_ids = []
for i, pair in enumerate(args.user_ids_to_values_ids):
user_id, values_id = pair.split(":")
party_name = CONFIG_N_PARTIES[i]["party_name"]
input_bindings.append(InputPartyBinding(party_name, UserId.parse(user_id)))
values_ids.append(UUID(values_id))
# Bind ourselves as an input party since we're also providing some inputs
input_bindings.append(
InputPartyBinding(CONFIG_PARTY_1["party_name"], client.user_id)
)
# We are the only output party
output_bindings = [
OutputPartyBinding(CONFIG_PARTY_1["party_name"], [client.user_id])
]
# The values that this party provides
values = {
key: SecretInteger(value) for key, value in CONFIG_PARTY_1["secrets"].items()
}
compute_id = await client.compute(
program_id=args.program_id,
input_bindings=input_bindings,
output_bindings=output_bindings,
values=values,
value_ids=values_ids,
).invoke()
print(f"The computation was sent to the network. compute_id: {compute_id}")
# Wait for execution to end
result = await client.retrieve_compute_results(compute_id).invoke()
print(f"โ
Compute complete for compute_id {compute_id}")
print(f"๐ฅ๏ธ The result is {result}")
print(f"๐ฅ๏ธ The result is {result}")