diff --git a/Cargo.toml b/Cargo.toml index a54b91a..dd24d98 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,9 @@ members = [ "sequencer_core", "common", "nssa", + "nssa/core", "integration_tests/proc_macro_test_attribute", + "examples/program_deployment", ] [workspace.dependencies] diff --git a/examples/program_deployment/Cargo.toml b/examples/program_deployment/Cargo.toml new file mode 100644 index 0000000..21d4fc8 --- /dev/null +++ b/examples/program_deployment/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "program_deployment" +version = "0.1.0" +edition = "2024" + +[dependencies] +tokio = { workspace = true, features = ["macros"] } +wallet = { path = "../../wallet" } +nssa-core = { path = "../../nssa/core" } +nssa = { path = "../../nssa" } +key_protocol = { path = "../../key_protocol/" } +clap = "4.5.53" +serde = "1.0.228" diff --git a/examples/program_deployment/README.md b/examples/program_deployment/README.md new file mode 100644 index 0000000..1bc7ed7 --- /dev/null +++ b/examples/program_deployment/README.md @@ -0,0 +1,571 @@ +# Program deployment tutorial + +This guide walks you through running the sequencer, compiling example programs, deploying a Hello World program, and interacting with accounts. + +You'll find: +- Programs: example NSSA programs under `methods/guest/src/bin`. +- Runners: scripts to create and submit transactions to invoke these programs publicly and privately under `src/bin`. + +# 0. Install the wallet +From the project’s root directory: +```bash +cargo install --path wallet --force +``` + +# 1. Run the sequencer +From the project’s root directory, start the sequencer: +```bash +cd sequencer_runner +RUST_LOG=info cargo run $(pwd)/configs/debug +``` +Keep this terminal open. We’ll use it only to observe the node logs. + +> [!NOTE] +> If you have already ran this before you'll see a `rocksdb` directory with stored blocks. Be sure to remove that directory to follow this tutorial. + + +## Checking and setting up the wallet +For sanity let's check that the wallet can connect to it. + +```bash +wallet check-health +``` + +If this is your first time, the wallet will ask for a password. This is used as seed to deterministically generate all account keys (public and private). +For this tutorial, use: `program-tutorial` + +You should see `✅All looks good!` if everything went well. + +# 2. Compile the example programs +In a second terminal, from the `lssa` root directory, compile the example Risc0 programs: +```bash +cargo risczero build --manifest-path examples/program_deployment/methods/guest/Cargo.toml +``` +The compiled `.bin` files will appear under: +``` +examples/program_deployment/methods/guest/target/riscv32im-risc0-zkvm-elf/docker/ +``` +For convenience, export this path: +```bash +export EXAMPLE_PROGRAMS_BUILD_DIR=$(pwd)/examples/program_deployment/methods/guest/target/riscv32im-risc0-zkvm-elf/docker +``` + +> [!IMPORTANT] +> **All remaining commands must be run from the `examples/program_deployment` directory.** + +# 3. Hello world example + +The Hello world program reads an arbitrary sequence of bytes from its instruction and appends them to the data field of the input account. +Execution succeeds only if the account is: + +- Uninitialized, or +- Already owned by this program + +If uninitialized, the program will claim the account and emit the updated state. + +## Navigate to the example directory +All remaining commands must be run from: +```bash +cd examples/program_deployment +``` + +## Deploy the Program + +Use the wallet’s built-in program deployment command: +```bash +wallet deploy-program $EXAMPLE_PROGRAMS_BUILD_DIR/hello_world.bin +``` + +# 4. Public execution of the Hello world example + +## Create a Public Account + +Generate a new public account: +```bash +wallet account new public +``` + +You'll see an output similar to: +```bash +Generated new account with account_id Public/BzdBoL4JRa5M873cuWb9rbYgASr1pXyaAZ1YW9ertWH9 at path /0 +``` +The relevant part is the account id `BzdBoL4JRa5M873cuWb9rbYgASr1pXyaAZ1YW9ertWH9` + +## Check the account state +New accounts are always Uninitialized. Verify: +```bash +wallet account get --account-id Public/BzdBoL4JRa5M873cuWb9rbYgASr1pXyaAZ1YW9ertWH9 +``` +Expected output: +``` +Account is Uninitialized +``` +The `Public/` prefix tells the wallet to query the public state. + +## Execute the Hello world program +Run the example: +```bash +cargo run --bin run_hello_world \ + $EXAMPLE_PROGRAMS_BUILD_DIR/hello_world.bin \ + BzdBoL4JRa5M873cuWb9rbYgASr1pXyaAZ1YW9ertWH9 +``` +> [!NOTE] +> - Passing the `.bin` lets the script compute the program ID and build the transaction. +> - Because this program executes publicly, the node performs the execution. +> - The program will claim the account and write data into it. + +Monitor the sequencer terminal to confirm execution. + +## Inspect the updated account +After the transaction is processed, check the new state: +```bash +wallet account get --account-id Public/BzdBoL4JRa5M873cuWb9rbYgASr1pXyaAZ1YW9ertWH9 +``` +Example output: +```json +{ + "balance": 0, + "program_owner_b64": "o6C6/bbjDmN9VUC51McBpPrta8lxrx2X0iHExhX0yNU=", + "data_b64": "SG9sYSBtdW5kbyE=", + "nonce": 0 +} +``` +The `data_b64` field contains de data in Base64. +Decode it: +```bash +echo -n SG9sYSBtdW5kbyE= | base64 -d +``` +You should see `Hola mundo!`. + +# 5. Understanding the code in `hello_world.rs`. +The Hello world example demonstrates the minimal structure of an NSSA program. +Its purpose is very simple: append the instruction bytes to the data field of a single account. + +### What this program does in a nutshell +1. Reads the program inputs + - The list of pre-state accounts (`pre_states`) + - The instruction bytes (`instruction`) + - The raw instruction data (used again when writing outputs) +2. Checks that there is exactly one input account: this example operates on a single account, so it expects `pre_states` to contain exactly one entry. +3. Builds the post-state: It clones the input account and appends the instruction bytes to its data field. +4. Handles account claiming logic: If the account is uninitialized (i.e. not yet claimed by any program), its program_owner will equal `DEFAULT_PROGRAM_ID`. In that case, the program issues a claim request, meaning: "This program now owns this account." +5. Outputs the proposed state transition: `write_nssa_outputs` emits: + - The original instruction data + - The original pre-states + - The new post-states + +## Code walkthrough +1. Reading inputs: +```rust +let (ProgramInput { pre_states, instruction: greeting }, instruction_data) + = read_nssa_inputs::(); +``` +2. Extracting the single account: +```rust +let [pre_state] = pre_states + .try_into() + .unwrap_or_else(|_| panic!("Input pre states should consist of a single account")); +``` +3. Constructing the updated account post state +```rust +let mut this = pre_state.account.clone(); +let mut bytes = this.data.into_inner(); +bytes.extend_from_slice(&greeting); +this.data = bytes.try_into().expect("Data should fit within the allowed limits"); +``` +4. Instantiating the `AccountPostState` with a claiming request only if the account pre state is uninitialized: +```rust +let post_state = if post_account.program_owner == DEFAULT_PROGRAM_ID { + AccountPostState::new_claimed(post_account) +} else { + AccountPostState::new(post_account) +}; +``` +5. Emmiting the output +```rust +write_nssa_outputs(instruction_data, vec![pre_state], vec![post_state]); +``` + +# 6. Understanding the runner script `run_hello_world.rs` +The `run_hello_world.rs` example demonstrates how to construct and submit a public transaction that executes the `hello_world` program. Below is a breakdown of what the file does and how the pieces fit together. + +### 1. Wallet initialization +```rust +let wallet_config = fetch_config().await.unwrap(); +let wallet_core = WalletCore::start_from_config_update_chain(wallet_config) + .await + .unwrap(); +``` +The example loads the wallet configuration and initializes `WalletCore`. +This gives access to: +- the sequencer client, +- the wallet’s account storage. + +### 2. Parsing inputs +```rust +let program_path = std::env::args_os().nth(1).unwrap().into_string().unwrap(); +let account_id: AccountId = std::env::args_os().nth(2).unwrap().into_string().unwrap().parse().unwrap(); +``` +The program expects two arguments: +- Path to the guest binary +- AccountId of the public account to operate on + +This is the account that the program will claim and write data into. + +### 3. Loading the program bytecode +```rust +let bytecode: Vec = std::fs::read(program_path).unwrap(); +let program = Program::new(bytecode).unwrap(); +``` +The Risc0 ELF is read from disk and wrapped in a Program object, which can be used to compute the program ID. The ID is used by the node to identify which program is invoked by the transaction. + + +### 4. Preparing the instruction data +```rust +let greeting: Vec = vec![72,111,108,97,32,109,117,110,100,111,33]; +``` +The example hardcodes the ASCII bytes for `Hola mundo!`. These bytes are passed to the program as its “instruction,” which the Hello World program simply appends to the account’s data field. + +### 5. Creating the public transaction + +```rust +let nonces = vec![]; +let signing_keys = []; +let message = Message::try_new(program.id(), vec![account_id], nonces, greeting).unwrap(); +let witness_set = WitnessSet::for_message(&message, &signing_keys); +let tx = PublicTransaction::new(message, witness_set); +``` + +A public transaction consists of: +- a `Message` +- a corresponding `WitnessSet` + +For this simple example, no signing or nonces are required. The transaction includes only the program ID, the target account, and the instruction bytes. The Hello World program allows this because it does not explicitly require authorization. In the next example, we’ll see how authorization requirements are enforced and how to construct a transaction that includes signatures and nonces. + +### 6. Submitting the transaction +```rust +let response = wallet_core.sequencer_client.send_tx_public(tx).await.unwrap(); +``` +The transaction is sent to the sequencer, which processes it and updates the public state accordingly. + +Once executed, you’ll be able to query the updated account to see the newly written "Hola mundo!" data. + +# 7. Private execution of the Hello world example + +This section is very similar to the previous case: + +## Create a private account + +Generate a new private account: +```bash +wallet account new private +``` + +You'll see an output similar to: +```bash +Generated new account with account_id Private/7EDHyxejuynBpmbLuiEym9HMUyCYxZDuF8X3B89ADeMr at path /0 +``` +The relevant part for this tutorial is the account id `7EDHyxejuynBpmbLuiEym9HMUyCYxZDuF8X3B89ADeMr` + +You can check it's uninitialized with + +```bash +wallet account get --account-id Private/7EDHyxejuynBpmbLuiEym9HMUyCYxZDuF8X3B89ADeMr +``` + +## Privately executing the Hello world program + +### Execute the Hello world program +Run the example: +```bash +cargo run --bin run_hello_world_private \ + $EXAMPLE_PROGRAMS_BUILD_DIR/hello_world.bin \ + 7EDHyxejuynBpmbLuiEym9HMUyCYxZDuF8X3B89ADeMr +``` +> [!NOTE] +> - This command may take a few minutes to complete. A ZK proof of the Hello world program execution and the privacy preserving circuit are being generated. Depending on the machine this can take from 30 seconds to 4 minutes. +> - We are passing the same `hello_world.bin` binary as in the previous case with public executions. This is because the program is the same, it is the privacy context of the input account that's different. +> - Because this program executes privately, the local machine runs the program and generate the proof of execution. +> - The program will claim the private account and write data into it. + +### Syncing the new private account values +The `run_hello_world` script submitted a transaction and it was (hopefully) accepted by the node. On chain there is now a commitment to the new private account values, and the account data is stored encrypted. However, the local client hasn’t updated its private state yet. That’s why, if you try to get the private account values now, it still reads the old values from local storage instead. + +```bash +wallet account get --account-id Private/7EDHyxejuynBpmbLuiEym9HMUyCYxZDuF8X3B89ADeMr +``` + +This will still show `Account is Uninitialized`. To see the new values locally, you need to run the wallet sync command. Once the client syncs, the local store will reflect the updated account data. + +To sync private accounts run: +```bash +wallet account sync-private +``` +> [!NOTE] +> - This queries the node for transactions and goes throught the encrypted accounts. Whenever a new value is found for one of the owned private accounts, the local storage is updated. + +After this completes, running +```bash +wallet account get --account-id Private/7EDHyxejuynBpmbLuiEym9HMUyCYxZDuF8X3B89ADeMr +``` +should show something similar to +```json +{ + "balance":0, + "program_owner_b64":"dWgtNRixwjC0C8aA0NL0Iuss3Q26Dw6ECk7bzExW4bI=", + "data_b64":"SG9sYSBtdW5kbyE=", + "nonce":236788677072686551559312843688143377080 +} +``` + +## The `run_hello_world_private.rs` runner +This example extends the public `run_hello_world.rs` flow by constructing a privacy-preserving transaction instead of a public one. +Both runners load a guest program, prepare a transaction, and submit it. But the private version handles encrypted account data, nullifiers, ephemeral keys, and zk proofs. + +Unlike the public version, `run_hello_world_private.rs` must: +- prepare the private account pre-state (nullifier keys, membership proof, encrypted values) +- derive a shared secret to encrypt the post-state +- compute the correct visibility mask (initialized vs. uninitialized private account) +- execute the guest program inside the zkVM and produce a proof +- build a PrivacyPreservingTransaction composed of: +- a Message encoding commitments + encrypted post-state +- a WitnessSet embedding the zk proof + +Luckily all that complexity is hidden behind the `wallet_core.send_privacy_preserving_tx` function: +```rust + let accounts = vec![PrivacyPreservingAccount::PrivateOwned(account_id)]; + + // Construct and submit the privacy-preserving transaction + wallet_core + .send_privacy_preserving_tx( + accounts, + &Program::serialize_instruction(greeting).unwrap(), + &program, + ) + .await + .unwrap(); +``` +Check the `run_hello_world_private.rs` file to see how it is used. + +# 8. Account authorization mechanism +The Hello world example does not enforce any authorization on the input account. This means any user can execute it on any account, regardless of ownership. +NSSA provides a mechanism for programs to enforce proper authorization before an execution can succeed. The meaning of authorization differs between public and private accounts: +- Public accounts: authorization requires that the transaction is signed with the account’s signing key. +- Private accounts: authorization requires that the circuit verifies knowledge of the account’s nullifier secret key. + +From the program development perspective it is very simple: input accounts come with a flag indicating whether they has been properly authorized. And so, the only difference between the program `hello_world.rs` and `hello_world_with_authorization.rs` is in the lines + +```rust + // #### Difference with `hello_world` example here: + // Fail if the input account is not authorized + // The `is_authorized` field will be correctly populated or verified by the system if + // authorization is provided. + if !pre_state.is_authorized { + panic!("Missing required authorization"); + } + // #### +``` + +Which just checks the `is_authorized` flag and fails if it is set to false. + +# 9. Public execution of the Hello world with authorization example +The workflow to execute it publicly is very similar: + +### Deploy the program +```bash +wallet deploy-program $EXAMPLE_PROGRAMS_BUILD_DIR/hello_world_with_authorization.bin +``` + +### Create a new public account +Our previous public account is already claimed by the simple Hello world program. So we need a new one to work with this other version of the hello program +```bash +wallet account new public +``` + +Outupt: +``` +Generated new account with account_id Public/9Ppqqf8NeCX58pnr8ZqKoHvSoYGqH79dSikZAtLxKgXE at path /1 +``` + +### Run the program + +```bash +cargo run --bin run_hello_world_with_authorization \ + $EXAMPLE_PROGRAMS_BUILD_DIR/hello_world_with_authorization.bin \ + 9Ppqqf8NeCX58pnr8ZqKoHvSoYGqH79dSikZAtLxKgXE +``` + +# 10. Understanding `run_hello_world_with_authorization.rs` +From the runner script perspective, the only difference is that the signing keys are passed to the `WitnessSet` constructor for it to sign it. You can see this in the following parts of the code: + +1. Loading the sigining keys from the wallet storage +```rust + // Load signing keys to provide authorization + let signing_key = wallet_core + .storage + .user_data + .get_pub_account_signing_key(&account_id) + .expect("Input account should be a self owned public account"); +``` +2. Fetching the current public nonce. +```rust + // Construct the public transaction + // Query the current nonce from the node + let nonces = wallet_core + .get_accounts_nonces(vec![account_id]) + .await + .expect("Node should be reachable to query account data"); +``` +2. Instantiate the witness set using the signing keys +```rust + let signing_keys = [signing_key]; + let message = Message::try_new(program.id(), vec![account_id], nonces, greeting).unwrap(); + // Pass the signing key to sign the message. This will be used by the node + // to flag the pre_state as `is_authorized` when executing the program + let witness_set = WitnessSet::for_message(&message, &signing_keys); +``` + +## Seeing the mechanism in action +If everything went well you won't notice any difference with the first Hello world, because the runner takes care of signing the transaction to provide authorization and the program just succeeds. +Try using the `run_hello_world.rs` runner with the `hello_world_with_authorization.bin` program. This will fail because the runner will submit the transaction without the corresponding signature. +```bash +cargo run --bin run_hello_world \ + $EXAMPLE_PROGRAMS_BUILD_DIR/hello_world_with_authorization.bin \ + 9Ppqqf8NeCX58pnr8ZqKoHvSoYGqH79dSikZAtLxKgXE +``` + +You should see something like the following **on the node logs**. +```bash +[2025-12-11T13:43:22Z WARN sequencer_core] Error at transition ProgramExecutionFailed( + "Guest panicked: Missing required authorization", + ) +``` + +# 11. Public and private account interaction example +Previous examples only operated on public or private accounts independently. Those minimal programs were useful to introduce basic concepts, but they couldn't demonstrate how different types of accounts interact within a single program invocation. +The "Hello world with move function" introduces two operations that require one or two input accounts: +- `write`: appends arbitrary bytes to a single account. This is what we already had. +- `move_data`: reads all bytes from one account, clears it, and appends those bytes to another account. +Because these operations may involve multiple accounts, we'll see how public and private accounts can participate together in one execution. It highlights how ownership checks work, when an account needs to be claimed, and how multiple post-states are emitted when several accounts are modified. + +> [!NOTE] +> The program logic is completely agnostic to whether input accounts are public or private. It always executes the same way. +> See `methods/guest/src/bin/hello_world_with_move_function.rs`. The program just reads the instruction bytes and updates the accounts state. +> All privacy handling happens on the runner side. When constructing the transaction, the runner decides which accounts are public or private and prepares the appropriate proofs. The program itself can't differentiate between privacy modes. + +Let's start by deploying the program +```bash +wallet deploy-program $EXAMPLE_PROGRAMS_BUILD_DIR/hello_world_with_move_function.bin +``` + +Let's also create a new public account +```bash +wallet account new public +``` + +Output: +``` +Generated new account with account_id Public/95iNQMbmxMRY6jULiHYkCzCkYKPEuysvBh5kEHayDxLs at path /0/0 +``` + +Let's execute the write function + +```bash +cargo run --bin run_hello_world_with_move_function \ + $EXAMPLE_PROGRAMS_BUILD_DIR/hello_world_with_move_function.bin \ + write-public 95iNQMbmxMRY6jULiHYkCzCkYKPEuysvBh5kEHayDxLs mundo! +``` + +Let's crate a new private account. + +```bash +wallet account new private +``` + +Output: +``` +Generated new account with account_id Private/8vzkK7vsdrS2gdPhLk72La8X4FJkgJ5kJLUBRbEVkReU at path /1 +``` + +Let's execute the write function + +```bash +cargo run --bin run_hello_world_with_move_function \ + $EXAMPLE_PROGRAMS_BUILD_DIR/hello_world_with_move_function.bin \ + write-private 8vzkK7vsdrS2gdPhLk72La8X4FJkgJ5kJLUBRbEVkReU Hola +``` + +To check the values of the accounts are as expected run: +```bash +wallet account get --account-id Public/95iNQMbmxMRY6jULiHYkCzCkYKPEuysvBh5kEHayDxLs +``` +and + +```bash +wallet account sync-private +wallet account get --account-id Private/8vzkK7vsdrS2gdPhLk72La8X4FJkgJ5kJLUBRbEVkReU +``` + +and check the (base64 encoded) data values are `mundo!` and `Hola` respectively. + +Now we can execute the move function to clear the data on the public account and move it to the private account. + +```bash +cargo run --bin run_hello_world_with_move_function \ + $EXAMPLE_PROGRAMS_BUILD_DIR/hello_world_with_move_function.bin \ + move-data-public-to-private 95iNQMbmxMRY6jULiHYkCzCkYKPEuysvBh5kEHayDxLs 8vzkK7vsdrS2gdPhLk72La8X4FJkgJ5kJLUBRbEVkReU +``` + +After succeeding, re run the get and sync commands and check that the public account has empty data and the private account data is `Holamundo!`. + +# 12. Program composition: tail calls +Programs can chain calls to other programs when they return. This is the tail call or chained call mechanism. It is used by programs that depend on other programs. + +The examples include a `guest/src/bin/simple_tail_call.rs` program that shows how to trigger this mechanism. It internally calls the first Hello World program with a fixed greeting: `Hello from tail call`. + +> [!NOTE] +> This program hardcodes the ID of the Hello World program. If something fails, check that this ID matches the one produced when building the Hello World program. You can see it in the output of `cargo risczero build` from the earlier sections of this tutorial. If it differs, update the ID in `simple_tail_call.rs` and build again. + +As before, let's start by deploying the program + +```bash +wallet deploy-program $EXAMPLE_PROGRAMS_BUILD_DIR/simple_tail_call.bin +``` + +We'll use the first public account of this tutorial. The one with account id `BzdBoL4JRa5M873cuWb9rbYgASr1pXyaAZ1YW9ertWH9`. This account is already owned by the Hello world program and its data reads `Hola mundo!`. + +Let's run the tail call program + +```bash +cargo run --bin run_hello_world_through_tail_call \ + $EXAMPLE_PROGRAMS_BUILD_DIR/simple_tail_call.bin \ + BzdBoL4JRa5M873cuWb9rbYgASr1pXyaAZ1YW9ertWH9 +``` + +Once the transaction is processed, query the account values with: + +```bash +wallet account get --account-id Public/BzdBoL4JRa5M873cuWb9rbYgASr1pXyaAZ1YW9ertWH9 +``` + +You should se an output similar to + +```json +{ + "balance":0, + "program_owner_b64":"fpnW4tFY9N6llZcBHaXRwu7xe+7WZnZX9RWzhwNbk1o=", + "data_b64":"SG9sYSBtdW5kbyFIZWxsbyBmcm9tIHRhaWwgY2FsbA==", + "nonce":0 +} +``` + +Decoding the (base64 encoded) data +```bash +echo -n SG9sYSBtdW5kbyFIZWxsbyBmcm9tIHRhaWwgY2FsbA== | base64 -d +``` + +Output: +``` +Hola mundo!Hello from tail call +``` + diff --git a/examples/program_deployment/methods/Cargo.toml b/examples/program_deployment/methods/Cargo.toml new file mode 100644 index 0000000..0317d2b --- /dev/null +++ b/examples/program_deployment/methods/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "test-program-methods" +version = "0.1.0" +edition = "2024" + +[build-dependencies] +risc0-build = { version = "3.0.3" } + +[package.metadata.risc0] +methods = ["guest"] diff --git a/examples/program_deployment/methods/build.rs b/examples/program_deployment/methods/build.rs new file mode 100644 index 0000000..08a8a4e --- /dev/null +++ b/examples/program_deployment/methods/build.rs @@ -0,0 +1,3 @@ +fn main() { + risc0_build::embed_methods(); +} diff --git a/examples/program_deployment/methods/guest/Cargo.toml b/examples/program_deployment/methods/guest/Cargo.toml new file mode 100644 index 0000000..8e2a199 --- /dev/null +++ b/examples/program_deployment/methods/guest/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "programs" +version = "0.1.0" +edition = "2024" + +[workspace] + +[dependencies] +risc0-zkvm = { version = "3.0.3", features = ['std'] } +nssa-core = { path = "../../../../nssa/core" } +serde = { version = "1.0.219", default-features = false } +hex = "0.4.3" +bytemuck = "1.24.0" diff --git a/examples/program_deployment/methods/guest/src/bin/hello_world.rs b/examples/program_deployment/methods/guest/src/bin/hello_world.rs new file mode 100644 index 0000000..3391eb5 --- /dev/null +++ b/examples/program_deployment/methods/guest/src/bin/hello_world.rs @@ -0,0 +1,60 @@ +use nssa_core::program::{ + AccountPostState, DEFAULT_PROGRAM_ID, ProgramInput, read_nssa_inputs, write_nssa_outputs, +}; + +// Hello-world example program. +// +// This program reads an arbitrary sequence of bytes as its instruction +// and appends those bytes to the `data` field of the single input account. +// +// Execution succeeds only if the input account is either: +// - uninitialized, or +// - already owned by this program. +// +// In case the input account is uninitialized, the program claims it. +// +// The updated account is emitted as the sole post-state. + +type Instruction = Vec; + +fn main() { + // Read inputs + let ( + ProgramInput { + pre_states, + instruction: greeting, + }, + instruction_data, + ) = read_nssa_inputs::(); + + // Unpack the input account pre state + let [pre_state] = pre_states + .try_into() + .unwrap_or_else(|_| panic!("Input pre states should consist of a single account")); + + // Construct the post state account values + let post_account = { + let mut this = pre_state.account.clone(); + let mut bytes = this.data.into_inner(); + bytes.extend_from_slice(&greeting); + this.data = bytes + .try_into() + .expect("Data should fit within the allowed limits"); + this + }; + + // Wrap the post state account values inside a `AccountPostState` instance. + // This is used to forward the account claiming request if any + let post_state = if post_account.program_owner == DEFAULT_PROGRAM_ID { + // This produces a claim request + AccountPostState::new_claimed(post_account) + } else { + // This doesn't produce a claim request + AccountPostState::new(post_account) + }; + + // The output is a proposed state difference. It will only succeed if the pre states coincide + // with the previous values of the accounts, and the transition to the post states conforms + // with the NSSA program rules. + write_nssa_outputs(instruction_data, vec![pre_state], vec![post_state]); +} diff --git a/examples/program_deployment/methods/guest/src/bin/hello_world_with_authorization.rs b/examples/program_deployment/methods/guest/src/bin/hello_world_with_authorization.rs new file mode 100644 index 0000000..043da1b --- /dev/null +++ b/examples/program_deployment/methods/guest/src/bin/hello_world_with_authorization.rs @@ -0,0 +1,69 @@ +use nssa_core::program::{ + AccountPostState, DEFAULT_PROGRAM_ID, ProgramInput, read_nssa_inputs, write_nssa_outputs, +}; + +// Hello-world with authorization example program. +// +// This program reads an arbitrary sequence of bytes as its instruction +// and appends those bytes to the `data` field of the single input account. +// +// Execution succeeds only if the input account **is authorized** and is either: +// - uninitialized, or +// - already owned by this program. +// +// In case the input account is uninitialized, the program claims it. +// +// The updated account is emitted as the sole post-state. + +type Instruction = Vec; + +fn main() { + // Read inputs + let ( + ProgramInput { + pre_states, + instruction: greeting, + }, + instruction_data, + ) = read_nssa_inputs::(); + + // Unpack the input account pre state + let [pre_state] = pre_states + .try_into() + .unwrap_or_else(|_| panic!("Input pre states should consist of a single account")); + + // #### Difference with `hello_world` example here: + // Fail if the input account is not authorized + // The `is_authorized` field will be correctly populated or verified by the system if + // authorization is provided. + if !pre_state.is_authorized { + panic!("Missing required authorization"); + } + // #### + + // Construct the post state account values + let post_account = { + let mut this = pre_state.account.clone(); + let mut bytes = this.data.into_inner(); + bytes.extend_from_slice(&greeting); + this.data = bytes + .try_into() + .expect("Data should fit within the allowed limits"); + this + }; + + // Wrap the post state account values inside a `AccountPostState` instance. + // This is used to forward the account claiming request if any + let post_state = if post_account.program_owner == DEFAULT_PROGRAM_ID { + // This produces a claim request + AccountPostState::new_claimed(post_account) + } else { + // This doesn't produce a claim request + AccountPostState::new(post_account) + }; + + // The output is a proposed state difference. It will only succeed if the pre states coincide + // with the previous values of the accounts, and the transition to the post states conforms + // with the NSSA program rules. + write_nssa_outputs(instruction_data, vec![pre_state], vec![post_state]); +} diff --git a/examples/program_deployment/methods/guest/src/bin/hello_world_with_move_function.rs b/examples/program_deployment/methods/guest/src/bin/hello_world_with_move_function.rs new file mode 100644 index 0000000..af0d4bf --- /dev/null +++ b/examples/program_deployment/methods/guest/src/bin/hello_world_with_move_function.rs @@ -0,0 +1,101 @@ +use nssa_core::{ + account::{Account, AccountWithMetadata}, + program::{ + AccountPostState, DEFAULT_PROGRAM_ID, ProgramInput, read_nssa_inputs, write_nssa_outputs, + }, +}; + +// Hello-world with write + move_data example program. +// +// This program reads an instruction of the form `(function_id, data)` and +// dispatches to either: +// +// - `write`: appends `data` to the `data` field of a single input account. +// - `move_data`: moves all bytes from one account to another. The source account is cleared and the +// destination account receives the appended bytes. +// +// Execution succeeds only if: +// - the accounts involved are either uninitialized, or +// - already owned by this program. +// +// In case an input account is uninitialized, the program will claim it when +// producing the post-state. + +type Instruction = (u8, Vec); +const WRITE_FUNCTION_ID: u8 = 0; +const MOVE_DATA_FUNCTION_ID: u8 = 1; + +fn build_post_state(post_account: Account) -> AccountPostState { + if post_account.program_owner == DEFAULT_PROGRAM_ID { + // This produces a claim request + AccountPostState::new_claimed(post_account) + } else { + // This doesn't produce a claim request + AccountPostState::new(post_account) + } +} + +fn write(pre_state: AccountWithMetadata, greeting: Vec) -> AccountPostState { + // Construct the post state account values + let post_account = { + let mut this = pre_state.account.clone(); + let mut bytes = this.data.into_inner(); + bytes.extend_from_slice(&greeting); + this.data = bytes + .try_into() + .expect("Data should fit within the allowed limits"); + this + }; + + build_post_state(post_account) +} + +fn move_data( + from_pre: &AccountWithMetadata, + to_pre: &AccountWithMetadata, +) -> Vec { + // Construct the post state account values + let from_data: Vec = from_pre.account.data.clone().into(); + + let from_post = { + let mut this = from_pre.account.clone(); + this.data = Default::default(); + build_post_state(this) + }; + + let to_post = { + let mut this = to_pre.account.clone(); + let mut bytes = this.data.into_inner(); + bytes.extend_from_slice(&from_data); + this.data = bytes + .try_into() + .expect("Data should fit within the allowed limits"); + build_post_state(this) + }; + + vec![from_post, to_post] +} + +fn main() { + // Read input accounts. + let ( + ProgramInput { + pre_states, + instruction: (function_id, data), + }, + instruction_words, + ) = read_nssa_inputs::(); + + let post_states = match (pre_states.as_slice(), function_id, data.len()) { + ([account_pre], WRITE_FUNCTION_ID, _) => { + let post = write(account_pre.clone(), data); + vec![post] + } + ([account_from_pre, account_to_pre], MOVE_DATA_FUNCTION_ID, 0) => { + move_data(account_from_pre, account_to_pre) + } + _ => panic!("invalid params"), + }; + + write_nssa_outputs(instruction_words, pre_states, post_states); +} diff --git a/examples/program_deployment/methods/guest/src/bin/simple_tail_call.rs b/examples/program_deployment/methods/guest/src/bin/simple_tail_call.rs new file mode 100644 index 0000000..d2bb58c --- /dev/null +++ b/examples/program_deployment/methods/guest/src/bin/simple_tail_call.rs @@ -0,0 +1,64 @@ +use nssa_core::program::{ + AccountPostState, ChainedCall, ProgramId, ProgramInput, read_nssa_inputs, + write_nssa_outputs_with_chained_call, +}; + +// Tail Call example program. +// +// This program shows how to chain execution to another program using `ChainedCall`. +// It reads a single account, emits it unchanged, and then triggers a tail call +// to the Hello World program with a fixed greeting. + + +/// This needs to be set to the ID of the Hello world program. +/// To get the ID run **from the root directoy of the repository**: +/// `cargo risczero build --manifest-path examples/program_deployment/methods/guest/Cargo.toml` +/// This compiles the programs and outputs the IDs in hex that can be used to copy here. +const HELLO_WORLD_PROGRAM_ID_HEX: &str = + "7e99d6e2d158f4dea59597011da5d1c2eef17beed6667657f515b387035b935a"; + +fn hello_world_program_id() -> ProgramId { + let hello_world_program_id_bytes: [u8; 32] = hex::decode(HELLO_WORLD_PROGRAM_ID_HEX) + .unwrap() + .try_into() + .unwrap(); + bytemuck::cast(hello_world_program_id_bytes) +} + +fn main() { + // Read inputs + let ( + ProgramInput { + pre_states, + instruction: _, + }, + instruction_data, + ) = read_nssa_inputs::<()>(); + + // Unpack the input account pre state + let [pre_state] = pre_states + .clone() + .try_into() + .unwrap_or_else(|_| panic!("Input pre states should consist of a single account")); + + // Create the (unchanged) post state + let post_state = AccountPostState::new(pre_state.account.clone()); + + // Create the chained call + let chained_call_greeting: Vec = b"Hello from tail call".to_vec(); + let chained_call_instruction_data = risc0_zkvm::serde::to_vec(&chained_call_greeting).unwrap(); + let chained_call = ChainedCall { + program_id: hello_world_program_id(), + instruction_data: chained_call_instruction_data, + pre_states, + pda_seeds: vec![], + }; + + // Write the outputs + write_nssa_outputs_with_chained_call( + instruction_data, + vec![pre_state], + vec![post_state], + vec![chained_call], + ); +} diff --git a/examples/program_deployment/methods/src/lib.rs b/examples/program_deployment/methods/src/lib.rs new file mode 100644 index 0000000..1bdb308 --- /dev/null +++ b/examples/program_deployment/methods/src/lib.rs @@ -0,0 +1 @@ +include!(concat!(env!("OUT_DIR"), "/methods.rs")); diff --git a/examples/program_deployment/src/bin/run_hello_world.rs b/examples/program_deployment/src/bin/run_hello_world.rs new file mode 100644 index 0000000..a7dc0fc --- /dev/null +++ b/examples/program_deployment/src/bin/run_hello_world.rs @@ -0,0 +1,67 @@ +use nssa::{ + AccountId, PublicTransaction, + program::Program, + public_transaction::{Message, WitnessSet}, +}; +use wallet::{WalletCore, helperfunctions::fetch_config}; + +// Before running this example, compile the `hello_world.rs` guest program with: +// +// cargo risczero build --manifest-path examples/program_deployment/methods/guest/Cargo.toml +// +// Note: you must run the above command from the root of the `lssa` repository. +// Note: The compiled binary file is stored in +// methods/guest/target/riscv32im-risc0-zkvm-elf/docker/hello_world.bin +// +// +// Usage: +// cargo run --bin run_hello_world /path/to/guest/binary +// +// Example: +// cargo run --bin run_hello_world \ +// methods/guest/target/riscv32im-risc0-zkvm-elf/docker/hello_world.bin \ +// Ds8q5PjLcKwwV97Zi7duhRVF9uwA2PuYMoLL7FwCzsXE + +#[tokio::main] +async fn main() { + // Load wallet config and storage + let wallet_config = fetch_config().await.unwrap(); + let wallet_core = WalletCore::start_from_config_update_chain(wallet_config) + .await + .unwrap(); + + // Parse arguments + // First argument is the path to the program binary + let program_path = std::env::args_os().nth(1).unwrap().into_string().unwrap(); + // Second argument is the account_id + let account_id: AccountId = std::env::args_os() + .nth(2) + .unwrap() + .into_string() + .unwrap() + .parse() + .unwrap(); + + // Load the program + let bytecode: Vec = std::fs::read(program_path).unwrap(); + let program = Program::new(bytecode).unwrap(); + + // Define the desired greeting in ASCII + let greeting: Vec = vec![72, 111, 108, 97, 32, 109, 117, 110, 100, 111, 33]; + + // Construct the public transaction + // No nonces nor signing keys are needed for this example. Check out the + // `run_hello_world_with_authorization` on how to use them. + let nonces = vec![]; + let signing_keys = []; + let message = Message::try_new(program.id(), vec![account_id], nonces, greeting).unwrap(); + let witness_set = WitnessSet::for_message(&message, &signing_keys); + let tx = PublicTransaction::new(message, witness_set); + + // Submit the transaction + let _response = wallet_core + .sequencer_client + .send_tx_public(tx) + .await + .unwrap(); +} diff --git a/examples/program_deployment/src/bin/run_hello_world_private.rs b/examples/program_deployment/src/bin/run_hello_world_private.rs new file mode 100644 index 0000000..be4280b --- /dev/null +++ b/examples/program_deployment/src/bin/run_hello_world_private.rs @@ -0,0 +1,61 @@ +use nssa::{AccountId, program::Program}; +use wallet::{PrivacyPreservingAccount, WalletCore, helperfunctions::fetch_config}; + +// Before running this example, compile the `hello_world.rs` guest program with: +// +// cargo risczero build --manifest-path examples/program_deployment/methods/guest/Cargo.toml +// +// Note: you must run the above command from the root of the `lssa` repository. +// Note: The compiled binary file is stored in +// methods/guest/target/riscv32im-risc0-zkvm-elf/docker/hello_world.bin +// +// +// Usage: +// cargo run --bin run_hello_world_private /path/to/guest/binary +// +// Note: the provided account_id needs to be of a private self owned account +// +// Example: +// cargo run --bin run_hello_world_private \ +// methods/guest/target/riscv32im-risc0-zkvm-elf/docker/hello_world.bin \ +// Ds8q5PjLcKwwV97Zi7duhRVF9uwA2PuYMoLL7FwCzsXE + +#[tokio::main] +async fn main() { + // Load wallet config and storage + let wallet_config = fetch_config().await.unwrap(); + let wallet_core = WalletCore::start_from_config_update_chain(wallet_config) + .await + .unwrap(); + + // Parse arguments + // First argument is the path to the program binary + let program_path = std::env::args_os().nth(1).unwrap().into_string().unwrap(); + // Second argument is the account_id + let account_id: AccountId = std::env::args_os() + .nth(2) + .unwrap() + .into_string() + .unwrap() + .parse() + .unwrap(); + + // Load the program + let bytecode: Vec = std::fs::read(program_path).unwrap(); + let program = Program::new(bytecode).unwrap(); + + // Define the desired greeting in ASCII + let greeting: Vec = vec![72, 111, 108, 97, 32, 109, 117, 110, 100, 111, 33]; + + let accounts = vec![PrivacyPreservingAccount::PrivateOwned(account_id)]; + + // Construct and submit the privacy-preserving transaction + wallet_core + .send_privacy_preserving_tx( + accounts, + &Program::serialize_instruction(greeting).unwrap(), + &program, + ) + .await + .unwrap(); +} diff --git a/examples/program_deployment/src/bin/run_hello_world_through_tail_call.rs b/examples/program_deployment/src/bin/run_hello_world_through_tail_call.rs new file mode 100644 index 0000000..d7c91f8 --- /dev/null +++ b/examples/program_deployment/src/bin/run_hello_world_through_tail_call.rs @@ -0,0 +1,63 @@ +use nssa::{ + AccountId, PublicTransaction, + program::Program, + public_transaction::{Message, WitnessSet}, +}; +use wallet::{WalletCore, helperfunctions::fetch_config}; + +// Before running this example, compile the `simple_tail_call.rs` guest program with: +// +// cargo risczero build --manifest-path examples/program_deployment/methods/guest/Cargo.toml +// +// Note: you must run the above command from the root of the `lssa` repository. +// Note: The compiled binary file is stored in +// methods/guest/target/riscv32im-risc0-zkvm-elf/docker/simple_tail_call.bin +// +// +// Usage: +// cargo run --bin run_hello_world_through_tail_call /path/to/guest/binary +// +// Example: +// cargo run --bin run_hello_world_through_tail_call \ +// methods/guest/target/riscv32im-risc0-zkvm-elf/docker/simple_tail_call.bin \ +// Ds8q5PjLcKwwV97Zi7duhRVF9uwA2PuYMoLL7FwCzsXE + +#[tokio::main] +async fn main() { + // Load wallet config and storage + let wallet_config = fetch_config().await.unwrap(); + let wallet_core = WalletCore::start_from_config_update_chain(wallet_config) + .await + .unwrap(); + + // Parse arguments + // First argument is the path to the program binary + let program_path = std::env::args_os().nth(1).unwrap().into_string().unwrap(); + // Second argument is the account_id + let account_id: AccountId = std::env::args_os() + .nth(2) + .unwrap() + .into_string() + .unwrap() + .parse() + .unwrap(); + + // Load the program + let bytecode: Vec = std::fs::read(program_path).unwrap(); + let program = Program::new(bytecode).unwrap(); + + let instruction_data = (); + let nonces = vec![]; + let signing_keys = []; + let message = + Message::try_new(program.id(), vec![account_id], nonces, instruction_data).unwrap(); + let witness_set = WitnessSet::for_message(&message, &signing_keys); + let tx = PublicTransaction::new(message, witness_set); + + // Submit the transaction + let _response = wallet_core + .sequencer_client + .send_tx_public(tx) + .await + .unwrap(); +} diff --git a/examples/program_deployment/src/bin/run_hello_world_with_authorization.rs b/examples/program_deployment/src/bin/run_hello_world_with_authorization.rs new file mode 100644 index 0000000..21740ae --- /dev/null +++ b/examples/program_deployment/src/bin/run_hello_world_with_authorization.rs @@ -0,0 +1,80 @@ +use nssa::{ + AccountId, PublicTransaction, + program::Program, + public_transaction::{Message, WitnessSet}, +}; +use wallet::{WalletCore, helperfunctions::fetch_config}; + +// Before running this example, compile the `hello_world_with_authorization.rs` guest program with: +// +// cargo risczero build --manifest-path examples/program_deployment/methods/guest/Cargo.toml +// +// Note: you must run the above command from the root of the `lssa` repository. +// Note: The compiled binary file is stored in +// methods/guest/target/riscv32im-risc0-zkvm-elf/docker/hello_world_with_authorization.bin +// +// +// Usage: +// ./run_hello_world_with_authorization /path/to/guest/binary +// +// Note: the provided account_id needs to be of a public self owned account +// +// Example: +// cargo run --bin run_hello_world_with_authorization \ +// methods/guest/target/riscv32im-risc0-zkvm-elf/docker/hello_world_with_authorization.bin \ +// Ds8q5PjLcKwwV97Zi7duhRVF9uwA2PuYMoLL7FwCzsXE + +#[tokio::main] +async fn main() { + // Load wallet config and storage + let wallet_config = fetch_config().await.unwrap(); + let wallet_core = WalletCore::start_from_config_update_chain(wallet_config) + .await + .unwrap(); + + // Parse arguments + // First argument is the path to the program binary + let program_path = std::env::args_os().nth(1).unwrap().into_string().unwrap(); + // Second argument is the account_id + let account_id: AccountId = std::env::args_os() + .nth(2) + .unwrap() + .into_string() + .unwrap() + .parse() + .unwrap(); + + // Load the program + let bytecode: Vec = std::fs::read(program_path).unwrap(); + let program = Program::new(bytecode).unwrap(); + + // Load signing keys to provide authorization + let signing_key = wallet_core + .storage + .user_data + .get_pub_account_signing_key(&account_id) + .expect("Input account should be a self owned public account"); + + // Define the desired greeting in ASCII + let greeting: Vec = vec![72, 111, 108, 97, 32, 109, 117, 110, 100, 111, 33]; + + // Construct the public transaction + // Query the current nonce from the node + let nonces = wallet_core + .get_accounts_nonces(vec![account_id]) + .await + .expect("Node should be reachable to query account data"); + let signing_keys = [signing_key]; + let message = Message::try_new(program.id(), vec![account_id], nonces, greeting).unwrap(); + // Pass the signing key to sign the message. This will be used by the node + // to flag the pre_state as `is_authorized` when executing the program + let witness_set = WitnessSet::for_message(&message, &signing_keys); + let tx = PublicTransaction::new(message, witness_set); + + // Submit the transaction + let _response = wallet_core + .sequencer_client + .send_tx_public(tx) + .await + .unwrap(); +} diff --git a/examples/program_deployment/src/bin/run_hello_world_with_move_function.rs b/examples/program_deployment/src/bin/run_hello_world_with_move_function.rs new file mode 100644 index 0000000..77c2597 --- /dev/null +++ b/examples/program_deployment/src/bin/run_hello_world_with_move_function.rs @@ -0,0 +1,155 @@ +use clap::{Parser, Subcommand}; +use nssa::{PublicTransaction, program::Program, public_transaction}; +use wallet::{PrivacyPreservingAccount, WalletCore, helperfunctions::fetch_config}; + +// Before running this example, compile the `hello_world_with_move_function.rs` guest program with: +// +// cargo risczero build --manifest-path examples/program_deployment/methods/guest/Cargo.toml +// +// Note: you must run the above command from the root of the `lssa` repository. +// Note: The compiled binary file is stored in +// methods/guest/target/riscv32im-risc0-zkvm-elf/docker/hello_world_with_move_function.bin +// +// +// Usage: +// cargo run --bin run_hello_world_with_move_function /path/to/guest/binary +// +// Example: +// cargo run --bin run_hello_world_with_move_function \ +// methods/guest/target/riscv32im-risc0-zkvm-elf/docker/hello_world_with_move_function.bin \ +// write-public Ds8q5PjLcKwwV97Zi7duhRVF9uwA2PuYMoLL7FwCzsXE Hola + +type Instruction = (u8, Vec); +const WRITE_FUNCTION_ID: u8 = 0; +const MOVE_DATA_FUNCTION_ID: u8 = 1; + +#[derive(Parser, Debug)] +struct Cli { + /// Path to program binary + program_path: String, + + #[command(subcommand)] + command: Command, +} + +#[derive(Subcommand, Debug)] +enum Command { + /// Write instruction into one account + WritePublic { + account_id: String, + greeting: String, + }, + WritePrivate { + account_id: String, + greeting: String, + }, + /// Move data between two accounts + MoveDataPublicToPublic { + from: String, + to: String, + }, + MoveDataPublicToPrivate { + from: String, + to: String, + }, +} + +#[tokio::main] +async fn main() { + let cli = Cli::parse(); + + // Load the program + let bytecode: Vec = std::fs::read(cli.program_path).unwrap(); + let program = Program::new(bytecode).unwrap(); + + // Load wallet config and storage + let wallet_config = fetch_config().await.unwrap(); + let wallet_core = WalletCore::start_from_config_update_chain(wallet_config) + .await + .unwrap(); + + match cli.command { + Command::WritePublic { + account_id, + greeting, + } => { + let instruction: Instruction = (WRITE_FUNCTION_ID, greeting.into_bytes()); + let account_id = account_id.parse().unwrap(); + let nonces = vec![]; + let message = public_transaction::Message::try_new( + program.id(), + vec![account_id], + nonces, + instruction, + ) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + let tx = PublicTransaction::new(message, witness_set); + + // Submit the transaction + let _response = wallet_core + .sequencer_client + .send_tx_public(tx) + .await + .unwrap(); + } + Command::WritePrivate { + account_id, + greeting, + } => { + let instruction: Instruction = (WRITE_FUNCTION_ID, greeting.into_bytes()); + let account_id = account_id.parse().unwrap(); + let accounts = vec![PrivacyPreservingAccount::PrivateOwned(account_id)]; + + wallet_core + .send_privacy_preserving_tx( + accounts, + &Program::serialize_instruction(instruction).unwrap(), + &program, + ) + .await + .unwrap(); + } + Command::MoveDataPublicToPublic { from, to } => { + let instruction: Instruction = (MOVE_DATA_FUNCTION_ID, vec![]); + let from = from.parse().unwrap(); + let to = to.parse().unwrap(); + let nonces = vec![]; + let message = public_transaction::Message::try_new( + program.id(), + vec![from, to], + nonces, + instruction, + ) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + let tx = PublicTransaction::new(message, witness_set); + + // Submit the transaction + let _response = wallet_core + .sequencer_client + .send_tx_public(tx) + .await + .unwrap(); + } + Command::MoveDataPublicToPrivate { from, to } => { + let instruction: Instruction = (MOVE_DATA_FUNCTION_ID, vec![]); + let from = from.parse().unwrap(); + let to = to.parse().unwrap(); + + let accounts = vec![ + PrivacyPreservingAccount::Public(from), + PrivacyPreservingAccount::PrivateOwned(to), + ]; + + wallet_core + .send_privacy_preserving_tx( + accounts, + &Program::serialize_instruction(instruction).unwrap(), + &program, + ) + .await + .unwrap(); + } + }; +} diff --git a/integration_tests/configs/debug/wallet/wallet_config.json b/integration_tests/configs/debug/wallet/wallet_config.json index ac4bae8..ad7b279 100644 --- a/integration_tests/configs/debug/wallet/wallet_config.json +++ b/integration_tests/configs/debug/wallet/wallet_config.json @@ -542,5 +542,6 @@ } } } - ] + ], + "basic_auth": null } \ No newline at end of file diff --git a/integration_tests/data_changer.bin b/integration_tests/data_changer.bin index 3d062c3..eb28a62 100644 Binary files a/integration_tests/data_changer.bin and b/integration_tests/data_changer.bin differ diff --git a/integration_tests/src/tps_test_utils.rs b/integration_tests/src/tps_test_utils.rs index 29462f6..6f597e2 100644 --- a/integration_tests/src/tps_test_utils.rs +++ b/integration_tests/src/tps_test_utils.rs @@ -168,7 +168,7 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction { (recipient_npk.clone(), recipient_ss), ], &[(sender_nsk, proof)], - &program, + &program.into(), ) .unwrap(); let message = pptx::message::Message::try_from_circuit_output( diff --git a/nssa/core/src/account.rs b/nssa/core/src/account.rs index 89bec37..c152581 100644 --- a/nssa/core/src/account.rs +++ b/nssa/core/src/account.rs @@ -25,8 +25,8 @@ pub struct Account { pub nonce: Nonce, } -#[derive(Serialize, Deserialize, Clone)] -#[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))] +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)] +#[cfg_attr(any(feature = "host", test), derive(Debug))] pub struct AccountWithMetadata { pub account: Account, pub is_authorized: bool, diff --git a/nssa/core/src/circuit_io.rs b/nssa/core/src/circuit_io.rs index 5bf620e..848fe3e 100644 --- a/nssa/core/src/circuit_io.rs +++ b/nssa/core/src/circuit_io.rs @@ -10,7 +10,7 @@ use crate::{ #[derive(Serialize, Deserialize)] pub struct PrivacyPreservingCircuitInput { - pub program_output: ProgramOutput, + pub program_outputs: Vec, pub visibility_mask: Vec, pub private_account_nonces: Vec, pub private_account_keys: Vec<(NullifierPublicKey, SharedSecretKey)>, diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index 8f49724..26ee8de 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -1,3 +1,5 @@ +use std::collections::HashSet; + use risc0_zkvm::{DeserializeOwned, guest::env, serde::Deserializer}; use serde::{Deserialize, Serialize}; @@ -8,6 +10,7 @@ use crate::account::{Account, AccountWithMetadata}; pub type ProgramId = [u32; 8]; pub type InstructionData = Vec; pub const DEFAULT_PROGRAM_ID: ProgramId = [0; 8]; +pub const MAX_NUMBER_CHAINED_CALLS: usize = 10; pub struct ProgramInput { pub pre_states: Vec, @@ -54,7 +57,9 @@ impl From<(&ProgramId, &PdaSeed)> for AccountId { #[derive(Serialize, Deserialize, Clone)] #[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))] pub struct ChainedCall { + /// The program ID of the program to execute pub program_id: ProgramId, + /// The instruction data to pass pub instruction_data: InstructionData, pub pre_states: Vec, pub pda_seeds: Vec, @@ -111,26 +116,34 @@ impl AccountPostState { #[derive(Serialize, Deserialize, Clone)] #[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))] pub struct ProgramOutput { + /// The instruction data the program received to produce this output + pub instruction_data: InstructionData, + /// The account pre states the program received to produce this output pub pre_states: Vec, pub post_states: Vec, pub chained_calls: Vec, } -pub fn read_nssa_inputs() -> ProgramInput { +pub fn read_nssa_inputs() -> (ProgramInput, InstructionData) { let pre_states: Vec = env::read(); let instruction_words: InstructionData = env::read(); let instruction = T::deserialize(&mut Deserializer::new(instruction_words.as_ref())).unwrap(); - ProgramInput { - pre_states, - instruction, - } + ( + ProgramInput { + pre_states, + instruction, + }, + instruction_words, + ) } pub fn write_nssa_outputs( + instruction_data: InstructionData, pre_states: Vec, post_states: Vec, ) { let output = ProgramOutput { + instruction_data, pre_states, post_states, chained_calls: Vec::new(), @@ -139,11 +152,13 @@ pub fn write_nssa_outputs( } pub fn write_nssa_outputs_with_chained_call( + instruction_data: InstructionData, pre_states: Vec, post_states: Vec, chained_calls: Vec, ) { let output = ProgramOutput { + instruction_data, pre_states, post_states, chained_calls, @@ -162,32 +177,37 @@ pub fn validate_execution( post_states: &[AccountPostState], executing_program_id: ProgramId, ) -> bool { - // 1. Lengths must match + // 1. Check account ids are all different + if !validate_uniqueness_of_account_ids(pre_states) { + return false; + } + + // 2. Lengths must match if pre_states.len() != post_states.len() { return false; } for (pre, post) in pre_states.iter().zip(post_states) { - // 2. Nonce must remain unchanged + // 3. Nonce must remain unchanged if pre.account.nonce != post.account.nonce { return false; } - // 3. Program ownership changes are not allowed + // 4. Program ownership changes are not allowed if pre.account.program_owner != post.account.program_owner { return false; } let account_program_owner = pre.account.program_owner; - // 4. Decreasing balance only allowed if owned by executing program + // 5. Decreasing balance only allowed if owned by executing program if post.account.balance < pre.account.balance && account_program_owner != executing_program_id { return false; } - // 5. Data changes only allowed if owned by executing program or if account pre state has + // 6. Data changes only allowed if owned by executing program or if account pre state has // default values if pre.account.data != post.account.data && pre.account != Account::default() @@ -196,14 +216,14 @@ pub fn validate_execution( return false; } - // 6. If a post state has default program owner, the pre state must have been a default + // 7. If a post state has default program owner, the pre state must have been a default // account if post.account.program_owner == DEFAULT_PROGRAM_ID && pre.account != Account::default() { return false; } } - // 7. Total balance is preserved + // 8. Total balance is preserved let Some(total_balance_pre_states) = WrappedBalanceSum::from_balances(pre_states.iter().map(|pre| pre.account.balance)) @@ -224,6 +244,17 @@ pub fn validate_execution( true } +fn validate_uniqueness_of_account_ids(pre_states: &[AccountWithMetadata]) -> bool { + let number_of_accounts = pre_states.len(); + let number_of_account_ids = pre_states + .iter() + .map(|account| &account.account_id) + .collect::>() + .len(); + + number_of_accounts == number_of_account_ids +} + /// Representation of a number as `lo + hi * 2^128`. #[derive(PartialEq, Eq)] struct WrappedBalanceSum { diff --git a/nssa/program_methods/guest/src/bin/authenticated_transfer.rs b/nssa/program_methods/guest/src/bin/authenticated_transfer.rs index 50afa50..fe02d06 100644 --- a/nssa/program_methods/guest/src/bin/authenticated_transfer.rs +++ b/nssa/program_methods/guest/src/bin/authenticated_transfer.rs @@ -6,34 +6,37 @@ use nssa_core::{ }; /// Initializes a default account under the ownership of this program. -fn initialize_account(pre_state: AccountWithMetadata) { +fn initialize_account(pre_state: AccountWithMetadata) -> AccountPostState { let account_to_claim = AccountPostState::new_claimed(pre_state.account.clone()); let is_authorized = pre_state.is_authorized; // Continue only if the account to claim has default values if account_to_claim.account() != &Account::default() { - return; + panic!("Account must be uninitialized"); } // Continue only if the owner authorized this operation if !is_authorized { - return; + panic!("Invalid input"); } - // Noop will result in account being claimed for this program - write_nssa_outputs(vec![pre_state], vec![account_to_claim]); + account_to_claim } /// Transfers `balance_to_move` native balance from `sender` to `recipient`. -fn transfer(sender: AccountWithMetadata, recipient: AccountWithMetadata, balance_to_move: u128) { +fn transfer( + sender: AccountWithMetadata, + recipient: AccountWithMetadata, + balance_to_move: u128, +) -> Vec { // Continue only if the sender has authorized this operation if !sender.is_authorized { - return; + panic!("Invalid input"); } // Continue only if the sender has enough balance if sender.account.balance < balance_to_move { - return; + panic!("Invalid input"); } // Create accounts post states, with updated balances @@ -57,23 +60,31 @@ fn transfer(sender: AccountWithMetadata, recipient: AccountWithMetadata, balance } }; - write_nssa_outputs(vec![sender, recipient], vec![sender_post, recipient_post]); + vec![sender_post, recipient_post] } /// A transfer of balance program. /// To be used both in public and private contexts. fn main() { // Read input accounts. - let ProgramInput { - pre_states, - instruction: balance_to_move, - } = read_nssa_inputs(); + let ( + ProgramInput { + pre_states, + instruction: balance_to_move, + }, + instruction_words, + ) = read_nssa_inputs(); - match (pre_states.as_slice(), balance_to_move) { - ([account_to_claim], 0) => initialize_account(account_to_claim.clone()), + let post_states = match (pre_states.as_slice(), balance_to_move) { + ([account_to_claim], 0) => { + let post = initialize_account(account_to_claim.clone()); + vec![post] + } ([sender, recipient], balance_to_move) => { transfer(sender.clone(), recipient.clone(), balance_to_move) } _ => panic!("invalid params"), - } + }; + + write_nssa_outputs(instruction_words, pre_states, post_states); } diff --git a/nssa/program_methods/guest/src/bin/pinata.rs b/nssa/program_methods/guest/src/bin/pinata.rs index 1c880e2..a0c46a1 100644 --- a/nssa/program_methods/guest/src/bin/pinata.rs +++ b/nssa/program_methods/guest/src/bin/pinata.rs @@ -44,10 +44,13 @@ impl Challenge { fn main() { // Read input accounts. // It is expected to receive only two accounts: [pinata_account, winner_account] - let ProgramInput { - pre_states, - instruction: solution, - } = read_nssa_inputs::(); + let ( + ProgramInput { + pre_states, + instruction: solution, + }, + instruction_words, + ) = read_nssa_inputs::(); let [pinata, winner] = match pre_states.try_into() { Ok(array) => array, @@ -71,6 +74,7 @@ fn main() { winner_post.balance += PRIZE; write_nssa_outputs( + instruction_words, vec![pinata, winner], vec![ AccountPostState::new(pinata_post), diff --git a/nssa/program_methods/guest/src/bin/pinata_token.rs b/nssa/program_methods/guest/src/bin/pinata_token.rs index 3810485..f988be9 100644 --- a/nssa/program_methods/guest/src/bin/pinata_token.rs +++ b/nssa/program_methods/guest/src/bin/pinata_token.rs @@ -54,10 +54,13 @@ fn main() { // Read input accounts. // It is expected to receive three accounts: [pinata_definition, pinata_token_holding, // winner_token_holding] - let ProgramInput { - pre_states, - instruction: solution, - } = read_nssa_inputs::(); + let ( + ProgramInput { + pre_states, + instruction: solution, + }, + instruction_words, + ) = read_nssa_inputs::(); let [ pinata_definition, @@ -98,6 +101,7 @@ fn main() { }]; write_nssa_outputs_with_chained_call( + instruction_words, vec![ pinata_definition, pinata_token_holding, diff --git a/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs b/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs index ac4e212..29162db 100644 --- a/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs +++ b/nssa/program_methods/guest/src/bin/privacy_preserving_circuit.rs @@ -1,4 +1,4 @@ -use std::collections::HashSet; +use std::collections::HashMap; use nssa_core::{ Commitment, CommitmentSetDigest, DUMMY_COMMITMENT_HASH, EncryptionScheme, Nullifier, @@ -6,43 +6,114 @@ use nssa_core::{ account::{Account, AccountId, AccountWithMetadata}, compute_digest_for_path, encryption::Ciphertext, - program::{DEFAULT_PROGRAM_ID, ProgramOutput, validate_execution}, + program::{DEFAULT_PROGRAM_ID, MAX_NUMBER_CHAINED_CALLS, validate_execution}, }; use risc0_zkvm::{guest::env, serde::to_vec}; fn main() { let PrivacyPreservingCircuitInput { - program_output, + program_outputs, visibility_mask, private_account_nonces, private_account_keys, private_account_auth, - program_id, + mut program_id, } = env::read(); - // Check that `program_output` is consistent with the execution of the corresponding program. - env::verify(program_id, &to_vec(&program_output).unwrap()).unwrap(); + let mut pre_states: Vec = Vec::new(); + let mut state_diff: HashMap = HashMap::new(); - let ProgramOutput { - pre_states, - post_states, - chained_calls, - } = program_output; - - // TODO: implement chained calls for privacy preserving transactions - if !chained_calls.is_empty() { - panic!("Privacy preserving transactions do not support yet chained calls.") + let num_calls = program_outputs.len(); + if num_calls > MAX_NUMBER_CHAINED_CALLS { + panic!("Max chained calls depth is exceeded"); } - // Check that there are no repeated account ids - if !validate_uniqueness_of_account_ids(&pre_states) { - panic!("Repeated account ids found") + let Some(last_program_call) = program_outputs.last() else { + panic!("Program outputs is empty") + }; + + if !last_program_call.chained_calls.is_empty() { + panic!("Call stack is incomplete"); } - // Check that the program is well behaved. - // See the # Programs section for the definition of the `validate_execution` method. - if !validate_execution(&pre_states, &post_states, program_id) { - panic!("Bad behaved program"); + for window in program_outputs.windows(2) { + let caller = &window[0]; + let callee = &window[1]; + + if caller.chained_calls.len() > 1 { + panic!("Privacy Multi-chained calls are not supported yet"); + } + + // TODO: Modify when multi-chain calls are supported in the circuit + let Some(caller_chained_call) = &caller.chained_calls.first() else { + panic!("Expected chained call"); + }; + + // Check that instruction data in caller is the instruction data in callee + if caller_chained_call.instruction_data != callee.instruction_data { + panic!("Invalid instruction data"); + } + + // Check that account pre_states in caller are the ones in calle + if caller_chained_call.pre_states != callee.pre_states { + panic!("Invalid pre states"); + } + } + + for (i, program_output) in program_outputs.iter().enumerate() { + let mut program_output = program_output.clone(); + + // Check that `program_output` is consistent with the execution of the corresponding program. + let program_output_words = + &to_vec(&program_output).expect("program_output must be serializable"); + env::verify(program_id, program_output_words) + .expect("program output must match the program's execution"); + + // Check that the program is well behaved. + // See the # Programs section for the definition of the `validate_execution` method. + if !validate_execution( + &program_output.pre_states, + &program_output.post_states, + program_id, + ) { + panic!("Bad behaved program"); + } + + // The invoked program claims the accounts with default program id. + for post in program_output + .post_states + .iter_mut() + .filter(|post| post.requires_claim()) + { + // The invoked program can only claim accounts with default program id. + if post.account().program_owner == DEFAULT_PROGRAM_ID { + post.account_mut().program_owner = program_id; + } else { + panic!("Cannot claim an initialized account") + } + } + + for (pre, post) in program_output + .pre_states + .iter() + .zip(&program_output.post_states) + { + if let Some(account_pre) = state_diff.get(&pre.account_id) { + if account_pre != &pre.account { + panic!("Invalid input"); + } + } else { + pre_states.push(pre.clone()); + } + state_diff.insert(pre.account_id.clone(), post.account().clone()); + } + + // TODO: Modify when multi-chain calls are supported in the circuit + if let Some(next_chained_call) = &program_output.chained_calls.first() { + program_id = next_chained_call.program_id; + } else if i != program_outputs.len() - 1 { + panic!("Inner call without a chained call found") + }; } let n_accounts = pre_states.len(); @@ -69,10 +140,8 @@ fn main() { // Public account public_pre_states.push(pre_states[i].clone()); - let mut post = post_states[i].account().clone(); - if pre_states[i].is_authorized { - post.nonce += 1; - } + let mut post = state_diff.get(&pre_states[i].account_id).unwrap().clone(); + if post.program_owner == DEFAULT_PROGRAM_ID { // Claim account post.program_owner = program_id; @@ -125,7 +194,8 @@ fn main() { } // Update post-state with new nonce - let mut post_with_updated_values = post_states[i].account().clone(); + let mut post_with_updated_values = + state_diff.get(&pre_states[i].account_id).unwrap().clone(); post_with_updated_values.nonce = *new_nonce; if post_with_updated_values.program_owner == DEFAULT_PROGRAM_ID { @@ -174,14 +244,3 @@ fn main() { env::commit(&output); } - -fn validate_uniqueness_of_account_ids(pre_states: &[AccountWithMetadata]) -> bool { - let number_of_accounts = pre_states.len(); - let number_of_account_ids = pre_states - .iter() - .map(|account| account.account_id.clone()) - .collect::>() - .len(); - - number_of_accounts == number_of_account_ids -} diff --git a/nssa/program_methods/guest/src/bin/token.rs b/nssa/program_methods/guest/src/bin/token.rs index 4e9336b..60da9a2 100644 --- a/nssa/program_methods/guest/src/bin/token.rs +++ b/nssa/program_methods/guest/src/bin/token.rs @@ -1,24 +1,27 @@ use nssa_core::{ - account::{Account, AccountId, AccountWithMetadata, Data, data::DATA_MAX_LENGTH_IN_BYTES}, + account::{Account, AccountId, AccountWithMetadata, Data}, program::{ AccountPostState, DEFAULT_PROGRAM_ID, ProgramInput, read_nssa_inputs, write_nssa_outputs, }, }; // The token program has three functions: -// 1. New token definition. Arguments to this function are: -// * Two **default** accounts: [definition_account, holding_account]. The first default account -// will be initialized with the token definition account values. The second account will be -// initialized to a token holding account for the new token, holding the entire total supply. -// * An instruction data of 23-bytes, indicating the total supply and the token name, with the -// following layout: [0x00 || total_supply (little-endian 16 bytes) || name (6 bytes)] The -// name cannot be equal to [0x00, 0x00, 0x00, 0x00, 0x00, 0x00] -// 2. Token transfer Arguments to this function are: +// 1. New token definition. +// Arguments to this function are: +// * Two **default** accounts: [definition_account, holding_account]. +// The first default account will be initialized with the token definition account values. The second account will +// be initialized to a token holding account for the new token, holding the entire total supply. +// * An instruction data of 23-bytes, indicating the total supply and the token name, with +// the following layout: +// [0x00 || total_supply (little-endian 16 bytes) || name (6 bytes)] +// The name cannot be equal to [0x00, 0x00, 0x00, 0x00, 0x00, 0x00] +// 2. Token transfer +// Arguments to this function are: // * Two accounts: [sender_account, recipient_account]. -// * An instruction data byte string of length 23, indicating the total supply with the -// following layout [0x01 || amount (little-endian 16 bytes) || 0x00 || 0x00 || 0x00 || 0x00 -// || 0x00 || 0x00]. -// 3. Initialize account with zero balance Arguments to this function are: +// * An instruction data byte string of length 23, indicating the total supply with the following layout +// [0x01 || amount (little-endian 16 bytes) || 0x00 || 0x00 || 0x00 || 0x00 || 0x00 || 0x00]. +// 3. Initialize account with zero balance +// Arguments to this function are: // * Two accounts: [definition_account, account_to_initialize]. // * An dummy byte string of length 23, with the following layout // [0x02 || 0x00 || 0x00 || 0x00 || ... || 0x00 || 0x00]. @@ -37,11 +40,9 @@ use nssa_core::{ const TOKEN_DEFINITION_TYPE: u8 = 0; const TOKEN_DEFINITION_DATA_SIZE: usize = 23; -const _: () = assert!(TOKEN_DEFINITION_DATA_SIZE <= DATA_MAX_LENGTH_IN_BYTES); const TOKEN_HOLDING_TYPE: u8 = 1; const TOKEN_HOLDING_DATA_SIZE: usize = 49; -const _: () = assert!(TOKEN_HOLDING_DATA_SIZE <= DATA_MAX_LENGTH_IN_BYTES); struct TokenDefinition { account_type: u8, @@ -381,10 +382,13 @@ fn mint_additional_supply( type Instruction = [u8; 23]; fn main() { - let ProgramInput { - pre_states, - instruction, - } = read_nssa_inputs::(); + let ( + ProgramInput { + pre_states, + instruction, + }, + instruction_words, + ) = read_nssa_inputs::(); let post_states = match instruction[0] { 0 => { @@ -455,7 +459,7 @@ fn main() { _ => panic!("Invalid instruction"), }; - write_nssa_outputs(pre_states, post_states); + write_nssa_outputs(instruction_words, pre_states, post_states); } #[cfg(test)] diff --git a/nssa/src/lib.rs b/nssa/src/lib.rs index b698ae3..e7182c9 100644 --- a/nssa/src/lib.rs +++ b/nssa/src/lib.rs @@ -17,7 +17,12 @@ pub mod public_transaction; mod signature; mod state; -pub use nssa_core::account::{Account, AccountId}; +pub use nssa_core::{ + SharedSecretKey, + account::{Account, AccountId}, + encryption::EphemeralPublicKey, + program::ProgramId, +}; pub use privacy_preserving_transaction::{ PrivacyPreservingTransaction, circuit::execute_and_prove, }; diff --git a/nssa/src/privacy_preserving_transaction/circuit.rs b/nssa/src/privacy_preserving_transaction/circuit.rs index 4ef02b3..95933a3 100644 --- a/nssa/src/privacy_preserving_transaction/circuit.rs +++ b/nssa/src/privacy_preserving_transaction/circuit.rs @@ -1,9 +1,11 @@ +use std::collections::HashMap; + use borsh::{BorshDeserialize, BorshSerialize}; use nssa_core::{ MembershipProof, NullifierPublicKey, NullifierSecretKey, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, SharedSecretKey, account::AccountWithMetadata, - program::{InstructionData, ProgramOutput}, + program::{InstructionData, ProgramId, ProgramOutput}, }; use risc0_zkvm::{ExecutorEnv, InnerReceipt, Receipt, default_prover}; @@ -11,12 +13,35 @@ use crate::{ error::NssaError, program::Program, program_methods::{PRIVACY_PRESERVING_CIRCUIT_ELF, PRIVACY_PRESERVING_CIRCUIT_ID}, + state::MAX_NUMBER_CHAINED_CALLS, }; /// Proof of the privacy preserving execution circuit #[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] pub struct Proof(pub(crate) Vec); +#[derive(Clone)] +pub struct ProgramWithDependencies { + pub program: Program, + // TODO: avoid having a copy of the bytecode of each dependency. + pub dependencies: HashMap, +} + +impl ProgramWithDependencies { + pub fn new(program: Program, dependencies: HashMap) -> Self { + Self { + program, + dependencies, + } + } +} + +impl From for ProgramWithDependencies { + fn from(program: Program) -> Self { + ProgramWithDependencies::new(program, HashMap::new()) + } +} + /// Generates a proof of the execution of a NSSA program inside the privacy preserving execution /// circuit pub fn execute_and_prove( @@ -26,27 +51,64 @@ pub fn execute_and_prove( private_account_nonces: &[u128], private_account_keys: &[(NullifierPublicKey, SharedSecretKey)], private_account_auth: &[(NullifierSecretKey, MembershipProof)], - program: &Program, + program_with_dependencies: &ProgramWithDependencies, ) -> Result<(PrivacyPreservingCircuitOutput, Proof), NssaError> { - let inner_receipt = execute_and_prove_program(program, pre_states, instruction_data)?; + let mut program = &program_with_dependencies.program; + let dependencies = &program_with_dependencies.dependencies; + let mut instruction_data = instruction_data.clone(); + let mut pre_states = pre_states.to_vec(); + let mut env_builder = ExecutorEnv::builder(); + let mut program_outputs = Vec::new(); - let program_output: ProgramOutput = inner_receipt - .journal - .decode() - .map_err(|e| NssaError::ProgramOutputDeserializationError(e.to_string()))?; + for _i in 0..MAX_NUMBER_CHAINED_CALLS { + let inner_receipt = execute_and_prove_program(program, &pre_states, &instruction_data)?; + + let program_output: ProgramOutput = inner_receipt + .journal + .decode() + .map_err(|e| NssaError::ProgramOutputDeserializationError(e.to_string()))?; + + // TODO: remove clone + program_outputs.push(program_output.clone()); + + // Prove circuit. + env_builder.add_assumption(inner_receipt); + + // TODO: Remove when multi-chain calls are supported in the circuit + assert!(program_output.chained_calls.len() <= 1); + // TODO: Modify when multi-chain calls are supported in the circuit + if let Some(next_call) = program_output.chained_calls.first() { + program = dependencies + .get(&next_call.program_id) + .ok_or(NssaError::InvalidProgramBehavior)?; + instruction_data = next_call.instruction_data.clone(); + // Build post states with metadata for next call + let mut post_states_with_metadata = Vec::new(); + for (pre, post) in program_output + .pre_states + .iter() + .zip(program_output.post_states) + { + let mut post_with_metadata = pre.clone(); + post_with_metadata.account = post.account().clone(); + post_states_with_metadata.push(post_with_metadata); + } + + pre_states = next_call.pre_states.clone(); + } else { + break; + } + } let circuit_input = PrivacyPreservingCircuitInput { - program_output, + program_outputs, visibility_mask: visibility_mask.to_vec(), private_account_nonces: private_account_nonces.to_vec(), private_account_keys: private_account_keys.to_vec(), private_account_auth: private_account_auth.to_vec(), - program_id: program.id(), + program_id: program_with_dependencies.program.id(), }; - // Prove circuit. - let mut env_builder = ExecutorEnv::builder(); - env_builder.add_assumption(inner_receipt); env_builder.write(&circuit_input).unwrap(); let env = env_builder.build().unwrap(); let prover = default_prover(); @@ -133,7 +195,7 @@ mod tests { let expected_sender_post = Account { program_owner: program.id(), balance: 100 - balance_to_move, - nonce: 1, + nonce: 0, data: Data::default(), }; @@ -156,7 +218,7 @@ mod tests { &[0xdeadbeef], &[(recipient_keys.npk(), shared_secret.clone())], &[], - &Program::authenticated_transfer_program(), + &Program::authenticated_transfer_program().into(), ) .unwrap(); @@ -257,7 +319,7 @@ mod tests { sender_keys.nsk, commitment_set.get_proof_for(&commitment_sender).unwrap(), )], - &program, + &program.into(), ) .unwrap(); diff --git a/nssa/src/privacy_preserving_transaction/mod.rs b/nssa/src/privacy_preserving_transaction/mod.rs index c74c077..48d8818 100644 --- a/nssa/src/privacy_preserving_transaction/mod.rs +++ b/nssa/src/privacy_preserving_transaction/mod.rs @@ -4,4 +4,6 @@ pub mod witness_set; pub mod circuit; +pub use message::Message; pub use transaction::PrivacyPreservingTransaction; +pub use witness_set::WitnessSet; diff --git a/nssa/src/program.rs b/nssa/src/program.rs index 89c3ed3..1865248 100644 --- a/nssa/src/program.rs +++ b/nssa/src/program.rs @@ -14,7 +14,7 @@ use crate::{ /// TODO: Make this variable when fees are implemented const MAX_NUM_CYCLES_PUBLIC_EXECUTION: u64 = 1024 * 1024 * 32; // 32M cycles -#[derive(Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct Program { id: ProgramId, elf: Vec, diff --git a/nssa/src/state.rs b/nssa/src/state.rs index 72efbd2..86df3a5 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -154,6 +154,12 @@ impl V02State { *current_account = post; } + // 5. Increment nonces for public signers + for account_id in tx.signer_account_ids() { + let current_account = self.get_account_by_id_mut(account_id); + current_account.nonce += 1; + } + Ok(()) } @@ -272,7 +278,10 @@ pub mod tests { error::NssaError, execute_and_prove, privacy_preserving_transaction::{ - PrivacyPreservingTransaction, circuit, message::Message, witness_set::WitnessSet, + PrivacyPreservingTransaction, + circuit::{self, ProgramWithDependencies}, + message::Message, + witness_set::WitnessSet, }, program::Program, public_transaction, @@ -859,7 +868,7 @@ pub mod tests { &[0xdeadbeef], &[(recipient_keys.npk(), shared_secret)], &[], - &Program::authenticated_transfer_program(), + &Program::authenticated_transfer_program().into(), ) .unwrap(); @@ -911,7 +920,7 @@ pub mod tests { sender_keys.nsk, state.get_proof_for_commitment(&sender_commitment).unwrap(), )], - &program, + &program.into(), ) .unwrap(); @@ -963,7 +972,7 @@ pub mod tests { sender_keys.nsk, state.get_proof_for_commitment(&sender_commitment).unwrap(), )], - &program, + &program.into(), ) .unwrap(); @@ -1176,7 +1185,7 @@ pub mod tests { &[], &[], &[], - &program, + &program.into(), ); assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); @@ -1202,7 +1211,7 @@ pub mod tests { &[], &[], &[], - &program, + &program.into(), ); assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); @@ -1228,7 +1237,7 @@ pub mod tests { &[], &[], &[], - &program, + &program.into(), ); assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); @@ -1254,7 +1263,7 @@ pub mod tests { &[], &[], &[], - &program, + &program.into(), ); assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); @@ -1282,7 +1291,7 @@ pub mod tests { &[], &[], &[], - &program, + &program.to_owned().into(), ); assert!(matches!(result, Err(NssaError::ProgramProveFailed(_)))); @@ -1308,7 +1317,7 @@ pub mod tests { &[], &[], &[], - &program, + &program.into(), ); assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); @@ -1343,7 +1352,7 @@ pub mod tests { &[], &[], &[], - &program, + &program.into(), ); assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); @@ -1369,7 +1378,7 @@ pub mod tests { &[], &[], &[], - &program, + &program.into(), ); assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); @@ -1404,7 +1413,7 @@ pub mod tests { &[], &[], &[], - &program, + &program.into(), ); assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); @@ -1441,7 +1450,7 @@ pub mod tests { &[], &[], &[], - &program, + &program.into(), ); assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); @@ -1482,7 +1491,7 @@ pub mod tests { ), ], &[(sender_keys.nsk, (0, vec![]))], - &program, + &program.into(), ); assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); @@ -1516,7 +1525,7 @@ pub mod tests { &[0xdeadbeef1, 0xdeadbeef2], &private_account_keys, &[(sender_keys.nsk, (0, vec![]))], - &program, + &program.into(), ); assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); @@ -1557,7 +1566,7 @@ pub mod tests { ), ], &private_account_auth, - &program, + &program.into(), ); assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); @@ -1605,7 +1614,7 @@ pub mod tests { &[0xdeadbeef1, 0xdeadbeef2], &private_account_keys, &private_account_auth, - &program, + &program.into(), ); assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); @@ -1651,7 +1660,7 @@ pub mod tests { ), ], &[(sender_keys.nsk, (0, vec![]))], - &program, + &program.into(), ); assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); @@ -1698,7 +1707,7 @@ pub mod tests { ), ], &[(sender_keys.nsk, (0, vec![]))], - &program, + &program.into(), ); assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); @@ -1744,7 +1753,7 @@ pub mod tests { ), ], &[(sender_keys.nsk, (0, vec![]))], - &program, + &program.into(), ); assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); @@ -1790,7 +1799,7 @@ pub mod tests { ), ], &[(sender_keys.nsk, (0, vec![]))], - &program, + &program.into(), ); assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); @@ -1834,7 +1843,7 @@ pub mod tests { ), ], &[(sender_keys.nsk, (0, vec![]))], - &program, + &program.into(), ); assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); @@ -1863,7 +1872,7 @@ pub mod tests { &[], &[], &[], - &program, + &program.into(), ); assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); @@ -1905,7 +1914,7 @@ pub mod tests { ), ], &[(sender_keys.nsk, (0, vec![]))], - &program, + &program.into(), ); assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); @@ -1951,7 +1960,7 @@ pub mod tests { &[0xdeadbeef1, 0xdeadbeef2], &private_account_keys, &[(sender_keys.nsk, (0, vec![]))], - &program, + &program.into(), ); assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); @@ -1997,7 +2006,7 @@ pub mod tests { ), ], &private_account_auth, - &program, + &program.into(), ); assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); @@ -2088,7 +2097,7 @@ pub mod tests { (sender_keys.npk(), shared_secret), ], &private_account_auth, - &program, + &program.into(), ); assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); @@ -2131,7 +2140,7 @@ pub mod tests { } #[test] - fn test_chained_call_succeeds() { + fn test_public_chained_call() { let program = Program::chain_caller(); let key = PrivateKey::try_new([1; 32]).unwrap(); let from = AccountId::from(&PublicKey::new_from_private_key(&key)); @@ -2310,6 +2319,128 @@ pub mod tests { assert_eq!(to_post, expected_to_post); } + #[test] + fn test_private_chained_call() { + // Arrange + let chain_caller = Program::chain_caller(); + let auth_transfers = Program::authenticated_transfer_program(); + let from_keys = test_private_account_keys_1(); + let to_keys = test_private_account_keys_2(); + let initial_balance = 100; + let from_account = AccountWithMetadata::new( + Account { + program_owner: auth_transfers.id(), + balance: initial_balance, + ..Account::default() + }, + true, + &from_keys.npk(), + ); + let to_account = AccountWithMetadata::new( + Account { + program_owner: auth_transfers.id(), + ..Account::default() + }, + true, + &to_keys.npk(), + ); + + let from_commitment = Commitment::new(&from_keys.npk(), &from_account.account); + let to_commitment = Commitment::new(&to_keys.npk(), &to_account.account); + let mut state = V02State::new_with_genesis_accounts( + &[], + &[from_commitment.clone(), to_commitment.clone()], + ) + .with_test_programs(); + let amount: u128 = 37; + let instruction: (u128, ProgramId, u32, Option) = ( + amount, + Program::authenticated_transfer_program().id(), + 1, + None, + ); + + let from_esk = [3; 32]; + let from_ss = SharedSecretKey::new(&from_esk, &from_keys.ivk()); + let from_epk = EphemeralPublicKey::from_scalar(from_esk); + + let to_esk = [3; 32]; + let to_ss = SharedSecretKey::new(&to_esk, &to_keys.ivk()); + let to_epk = EphemeralPublicKey::from_scalar(to_esk); + + let mut dependencies = HashMap::new(); + + dependencies.insert(auth_transfers.id(), auth_transfers); + let program_with_deps = ProgramWithDependencies::new(chain_caller, dependencies); + + let from_new_nonce = 0xdeadbeef1; + let to_new_nonce = 0xdeadbeef2; + + let from_expected_post = Account { + balance: initial_balance - amount, + nonce: from_new_nonce, + ..from_account.account.clone() + }; + let from_expected_commitment = Commitment::new(&from_keys.npk(), &from_expected_post); + + let to_expected_post = Account { + balance: amount, + nonce: to_new_nonce, + ..to_account.account.clone() + }; + let to_expected_commitment = Commitment::new(&to_keys.npk(), &to_expected_post); + + // Act + let (output, proof) = execute_and_prove( + &[to_account, from_account], + &Program::serialize_instruction(instruction).unwrap(), + &[1, 1], + &[from_new_nonce, to_new_nonce], + &[(from_keys.npk(), to_ss), (to_keys.npk(), from_ss)], + &[ + ( + from_keys.nsk, + state.get_proof_for_commitment(&from_commitment).unwrap(), + ), + ( + to_keys.nsk, + state.get_proof_for_commitment(&to_commitment).unwrap(), + ), + ], + &program_with_deps, + ) + .unwrap(); + + let message = Message::try_from_circuit_output( + vec![], + vec![], + vec![ + (to_keys.npk(), to_keys.ivk(), to_epk), + (from_keys.npk(), from_keys.ivk(), from_epk), + ], + output, + ) + .unwrap(); + let witness_set = WitnessSet::for_message(&message, proof, &[]); + let transaction = PrivacyPreservingTransaction::new(message, witness_set); + + state + .transition_from_privacy_preserving_transaction(&transaction) + .unwrap(); + + // Assert + assert!( + state + .get_proof_for_commitment(&from_expected_commitment) + .is_some() + ); + assert!( + state + .get_proof_for_commitment(&to_expected_commitment) + .is_some() + ); + } + #[test] fn test_pda_mechanism_with_pinata_token_program() { let pinata_token = Program::pinata_token(); diff --git a/nssa/test_program_methods/guest/src/bin/burner.rs b/nssa/test_program_methods/guest/src/bin/burner.rs index 01b46b2..5deef7c 100644 --- a/nssa/test_program_methods/guest/src/bin/burner.rs +++ b/nssa/test_program_methods/guest/src/bin/burner.rs @@ -3,10 +3,13 @@ use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write type Instruction = u128; fn main() { - let ProgramInput { - pre_states, - instruction: balance_to_burn, - } = read_nssa_inputs::(); + let ( + ProgramInput { + pre_states, + instruction: balance_to_burn, + }, + instruction_words, + ) = read_nssa_inputs::(); let [pre] = match pre_states.try_into() { Ok(array) => array, @@ -17,5 +20,5 @@ fn main() { let mut account_post = account_pre.clone(); account_post.balance -= balance_to_burn; - write_nssa_outputs(vec![pre], vec![AccountPostState::new(account_post)]); + write_nssa_outputs(instruction_words, vec![pre], vec![AccountPostState::new(account_post)]); } diff --git a/nssa/test_program_methods/guest/src/bin/chain_caller.rs b/nssa/test_program_methods/guest/src/bin/chain_caller.rs index ee01ffa..f2d3cb6 100644 --- a/nssa/test_program_methods/guest/src/bin/chain_caller.rs +++ b/nssa/test_program_methods/guest/src/bin/chain_caller.rs @@ -10,10 +10,13 @@ type Instruction = (u128, ProgramId, u32, Option); /// It permutes the order of the input accounts on the subsequent call /// The `ProgramId` in the instruction must be the program_id of the authenticated transfers program fn main() { - let ProgramInput { - pre_states, + let ( + ProgramInput { + pre_states, instruction: (balance, auth_transfer_id, num_chain_calls, pda_seed), - } = read_nssa_inputs::(); + }, + instruction_words + ) = read_nssa_inputs::(); let [recipient_pre, sender_pre] = match pre_states.try_into() { Ok(array) => array, @@ -44,6 +47,7 @@ fn main() { } write_nssa_outputs_with_chained_call( + instruction_words, vec![sender_pre.clone(), recipient_pre.clone()], vec![ AccountPostState::new(sender_pre.account), diff --git a/nssa/test_program_methods/guest/src/bin/claimer.rs b/nssa/test_program_methods/guest/src/bin/claimer.rs index 7687e5a..8687704 100644 --- a/nssa/test_program_methods/guest/src/bin/claimer.rs +++ b/nssa/test_program_methods/guest/src/bin/claimer.rs @@ -3,10 +3,13 @@ use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write type Instruction = (); fn main() { - let ProgramInput { - pre_states, - instruction: _, - } = read_nssa_inputs::(); + let ( + ProgramInput { + pre_states, + instruction: _, + }, + instruction_words, + ) = read_nssa_inputs::(); let [pre] = match pre_states.try_into() { Ok(array) => array, @@ -15,5 +18,5 @@ fn main() { let account_post = AccountPostState::new_claimed(pre.account.clone()); - write_nssa_outputs(vec![pre], vec![account_post]); + write_nssa_outputs(instruction_words, vec![pre], vec![account_post]); } diff --git a/nssa/test_program_methods/guest/src/bin/data_changer.rs b/nssa/test_program_methods/guest/src/bin/data_changer.rs index b590886..9154440 100644 --- a/nssa/test_program_methods/guest/src/bin/data_changer.rs +++ b/nssa/test_program_methods/guest/src/bin/data_changer.rs @@ -4,7 +4,7 @@ type Instruction = Vec; /// A program that modifies the account data by setting bytes sent in instruction. fn main() { - let ProgramInput { pre_states, instruction: data } = read_nssa_inputs::(); + let (ProgramInput { pre_states, instruction: data }, instruction_words) = read_nssa_inputs::(); let [pre] = match pre_states.try_into() { Ok(array) => array, @@ -15,5 +15,9 @@ fn main() { let mut account_post = account_pre.clone(); account_post.data = data.try_into().expect("provided data should fit into data limit"); - write_nssa_outputs(vec![pre], vec![AccountPostState::new_claimed(account_post)]); + write_nssa_outputs( + instruction_words, + vec![pre], + vec![AccountPostState::new_claimed(account_post)], + ); } diff --git a/nssa/test_program_methods/guest/src/bin/extra_output.rs b/nssa/test_program_methods/guest/src/bin/extra_output.rs index 7137262..4950f14 100644 --- a/nssa/test_program_methods/guest/src/bin/extra_output.rs +++ b/nssa/test_program_methods/guest/src/bin/extra_output.rs @@ -6,7 +6,7 @@ use nssa_core::{ type Instruction = (); fn main() { - let ProgramInput { pre_states, .. } = read_nssa_inputs::(); + let (ProgramInput { pre_states, .. }, instruction_words) = read_nssa_inputs::(); let [pre] = match pre_states.try_into() { Ok(array) => array, @@ -16,6 +16,7 @@ fn main() { let account_pre = pre.account.clone(); write_nssa_outputs( + instruction_words, vec![pre], vec![ AccountPostState::new(account_pre), diff --git a/nssa/test_program_methods/guest/src/bin/minter.rs b/nssa/test_program_methods/guest/src/bin/minter.rs index 5f69772..51baa5e 100644 --- a/nssa/test_program_methods/guest/src/bin/minter.rs +++ b/nssa/test_program_methods/guest/src/bin/minter.rs @@ -3,7 +3,7 @@ use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, AccountPostState, type Instruction = (); fn main() { - let ProgramInput { pre_states, .. } = read_nssa_inputs::(); + let (ProgramInput { pre_states, .. }, instruction_words) = read_nssa_inputs::(); let [pre] = match pre_states.try_into() { Ok(array) => array, @@ -14,5 +14,5 @@ fn main() { let mut account_post = account_pre.clone(); account_post.balance += 1; - write_nssa_outputs(vec![pre], vec![AccountPostState::new(account_post)]); + write_nssa_outputs(instruction_words, vec![pre], vec![AccountPostState::new(account_post)]); } diff --git a/nssa/test_program_methods/guest/src/bin/missing_output.rs b/nssa/test_program_methods/guest/src/bin/missing_output.rs index f7d78be..7b910c6 100644 --- a/nssa/test_program_methods/guest/src/bin/missing_output.rs +++ b/nssa/test_program_methods/guest/src/bin/missing_output.rs @@ -3,7 +3,7 @@ use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write type Instruction = (); fn main() { - let ProgramInput { pre_states, .. } = read_nssa_inputs::(); + let (ProgramInput { pre_states, .. }, instruction_words) = read_nssa_inputs::(); let [pre1, pre2] = match pre_states.try_into() { Ok(array) => array, @@ -12,5 +12,9 @@ fn main() { let account_pre1 = pre1.account.clone(); - write_nssa_outputs(vec![pre1, pre2], vec![AccountPostState::new(account_pre1)]); + write_nssa_outputs( + instruction_words, + vec![pre1, pre2], + vec![AccountPostState::new(account_pre1)], + ); } diff --git a/nssa/test_program_methods/guest/src/bin/modified_transfer.rs b/nssa/test_program_methods/guest/src/bin/modified_transfer.rs index 0f85e53..dd93e83 100644 --- a/nssa/test_program_methods/guest/src/bin/modified_transfer.rs +++ b/nssa/test_program_methods/guest/src/bin/modified_transfer.rs @@ -5,32 +5,32 @@ use nssa_core::{ /// Initializes a default account under the ownership of this program. /// This is achieved by a noop. -fn initialize_account(pre_state: AccountWithMetadata) { +fn initialize_account(pre_state: AccountWithMetadata) -> AccountPostState { let account_to_claim = pre_state.account.clone(); let is_authorized = pre_state.is_authorized; // Continue only if the account to claim has default values if account_to_claim != Account::default() { - return; + panic!("Account is already initialized"); } // Continue only if the owner authorized this operation if !is_authorized { - return; + panic!("Missing required authorization"); } - // Noop will result in account being claimed for this program - write_nssa_outputs( - vec![pre_state], - vec![AccountPostState::new(account_to_claim)], - ); + AccountPostState::new(account_to_claim) } /// Transfers `balance_to_move` native balance from `sender` to `recipient`. -fn transfer(sender: AccountWithMetadata, recipient: AccountWithMetadata, balance_to_move: u128) { +fn transfer( + sender: AccountWithMetadata, + recipient: AccountWithMetadata, + balance_to_move: u128, +) -> Vec { // Continue only if the sender has authorized this operation if !sender.is_authorized { - return; + panic!("Missing required authorization"); } // This segment is a safe protection from authenticated transfer program @@ -50,29 +50,33 @@ fn transfer(sender: AccountWithMetadata, recipient: AccountWithMetadata, balance sender_post.balance -= balance_to_move + malicious_offset; recipient_post.balance += balance_to_move + malicious_offset; - write_nssa_outputs( - vec![sender, recipient], - vec![ - AccountPostState::new(sender_post), - AccountPostState::new(recipient_post), - ], - ); + vec![ + AccountPostState::new(sender_post), + AccountPostState::new(recipient_post), + ] } /// A transfer of balance program. /// To be used both in public and private contexts. fn main() { // Read input accounts. - let ProgramInput { - pre_states, - instruction: balance_to_move, - } = read_nssa_inputs(); + let ( + ProgramInput { + pre_states, + instruction: balance_to_move, + }, + instruction_data, + ) = read_nssa_inputs(); - match (pre_states.as_slice(), balance_to_move) { - ([account_to_claim], 0) => initialize_account(account_to_claim.clone()), + let post_states = match (pre_states.as_slice(), balance_to_move) { + ([account_to_claim], 0) => { + let post = initialize_account(account_to_claim.clone()); + vec![post] + } ([sender, recipient], balance_to_move) => { transfer(sender.clone(), recipient.clone(), balance_to_move) } _ => panic!("invalid params"), - } + }; + write_nssa_outputs(instruction_data, pre_states, post_states); } diff --git a/nssa/test_program_methods/guest/src/bin/nonce_changer.rs b/nssa/test_program_methods/guest/src/bin/nonce_changer.rs index fc24572..4ca6c73 100644 --- a/nssa/test_program_methods/guest/src/bin/nonce_changer.rs +++ b/nssa/test_program_methods/guest/src/bin/nonce_changer.rs @@ -3,7 +3,7 @@ use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, AccountPostState, type Instruction = (); fn main() { - let ProgramInput { pre_states, .. } = read_nssa_inputs::(); + let (ProgramInput { pre_states, .. } , instruction_words) = read_nssa_inputs::(); let [pre] = match pre_states.try_into() { Ok(array) => array, @@ -14,5 +14,5 @@ fn main() { let mut account_post = account_pre.clone(); account_post.nonce += 1; - write_nssa_outputs(vec![pre], vec![AccountPostState::new(account_post)]); + write_nssa_outputs(instruction_words ,vec![pre], vec![AccountPostState::new(account_post)]); } diff --git a/nssa/test_program_methods/guest/src/bin/program_owner_changer.rs b/nssa/test_program_methods/guest/src/bin/program_owner_changer.rs index 2fa5400..2b212c1 100644 --- a/nssa/test_program_methods/guest/src/bin/program_owner_changer.rs +++ b/nssa/test_program_methods/guest/src/bin/program_owner_changer.rs @@ -3,7 +3,7 @@ use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, AccountPostState, type Instruction = (); fn main() { - let ProgramInput { pre_states, .. } = read_nssa_inputs::(); + let (ProgramInput { pre_states, .. }, instruction_words) = read_nssa_inputs::(); let [pre] = match pre_states.try_into() { Ok(array) => array, @@ -14,5 +14,5 @@ fn main() { let mut account_post = account_pre.clone(); account_post.program_owner = [0, 1, 2, 3, 4, 5, 6, 7]; - write_nssa_outputs(vec![pre], vec![AccountPostState::new(account_post)]); + write_nssa_outputs(instruction_words, vec![pre], vec![AccountPostState::new(account_post)]); } diff --git a/nssa/test_program_methods/guest/src/bin/simple_balance_transfer.rs b/nssa/test_program_methods/guest/src/bin/simple_balance_transfer.rs index be56e16..e1dbc1b 100644 --- a/nssa/test_program_methods/guest/src/bin/simple_balance_transfer.rs +++ b/nssa/test_program_methods/guest/src/bin/simple_balance_transfer.rs @@ -3,10 +3,13 @@ use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write type Instruction = u128; fn main() { - let ProgramInput { - pre_states, - instruction: balance, - } = read_nssa_inputs::(); + let ( + ProgramInput { + pre_states, + instruction: balance, + }, + instruction_words, + ) = read_nssa_inputs::(); let [sender_pre, receiver_pre] = match pre_states.try_into() { Ok(array) => array, @@ -19,6 +22,7 @@ fn main() { receiver_post.balance += balance; write_nssa_outputs( + instruction_words, vec![sender_pre, receiver_pre], vec![ AccountPostState::new(sender_post), diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 78cd00a..6f8e29a 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -293,7 +293,7 @@ impl WalletCore { .map(|keys| (keys.npk.clone(), keys.ssk.clone())) .collect::>(), &acc_manager.private_account_auth(), - program, + &program.to_owned().into(), ) .unwrap(); diff --git a/wallet/src/pinata_interactions.rs b/wallet/src/pinata_interactions.rs new file mode 100644 index 0000000..65a67b7 --- /dev/null +++ b/wallet/src/pinata_interactions.rs @@ -0,0 +1,161 @@ +use common::{error::ExecutionFailureKind, sequencer_client::json::SendTxResponse}; +use key_protocol::key_management::ephemeral_key_holder::EphemeralKeyHolder; +use nssa::{AccountId, privacy_preserving_transaction::circuit}; +use nssa_core::{MembershipProof, SharedSecretKey, account::AccountWithMetadata}; + +use crate::{ + WalletCore, helperfunctions::produce_random_nonces, transaction_utils::AccountPreparedData, +}; + +impl WalletCore { + pub async fn claim_pinata( + &self, + pinata_account_id: AccountId, + winner_account_id: AccountId, + solution: u128, + ) -> Result { + let account_ids = vec![pinata_account_id, winner_account_id]; + let program_id = nssa::program::Program::pinata().id(); + let message = + nssa::public_transaction::Message::try_new(program_id, account_ids, vec![], solution) + .unwrap(); + + let witness_set = nssa::public_transaction::WitnessSet::for_message(&message, &[]); + let tx = nssa::PublicTransaction::new(message, witness_set); + + Ok(self.sequencer_client.send_tx_public(tx).await?) + } + + pub async fn claim_pinata_private_owned_account_already_initialized( + &self, + pinata_account_id: AccountId, + winner_account_id: AccountId, + solution: u128, + winner_proof: MembershipProof, + ) -> Result<(SendTxResponse, [SharedSecretKey; 1]), ExecutionFailureKind> { + let AccountPreparedData { + nsk: winner_nsk, + npk: winner_npk, + ipk: winner_ipk, + auth_acc: winner_pre, + proof: _, + } = self + .private_acc_preparation(winner_account_id, true, false) + .await?; + + let pinata_acc = self.get_account_public(pinata_account_id).await.unwrap(); + + let program = nssa::program::Program::pinata(); + + let pinata_pre = AccountWithMetadata::new(pinata_acc.clone(), false, pinata_account_id); + + let eph_holder_winner = EphemeralKeyHolder::new(&winner_npk); + let shared_secret_winner = eph_holder_winner.calculate_shared_secret_sender(&winner_ipk); + + let (output, proof) = circuit::execute_and_prove( + &[pinata_pre, winner_pre], + &nssa::program::Program::serialize_instruction(solution).unwrap(), + &[0, 1], + &produce_random_nonces(1), + &[(winner_npk.clone(), shared_secret_winner.clone())], + &[(winner_nsk.unwrap(), winner_proof)], + &program.into(), + ) + .unwrap(); + + let message = + nssa::privacy_preserving_transaction::message::Message::try_from_circuit_output( + vec![pinata_account_id], + vec![], + vec![( + winner_npk.clone(), + winner_ipk.clone(), + eph_holder_winner.generate_ephemeral_public_key(), + )], + output, + ) + .unwrap(); + + let witness_set = + nssa::privacy_preserving_transaction::witness_set::WitnessSet::for_message( + &message, + proof, + &[], + ); + let tx = nssa::privacy_preserving_transaction::PrivacyPreservingTransaction::new( + message, + witness_set, + ); + + Ok(( + self.sequencer_client.send_tx_private(tx).await?, + [shared_secret_winner], + )) + } + + pub async fn claim_pinata_private_owned_account_not_initialized( + &self, + pinata_account_id: AccountId, + winner_account_id: AccountId, + solution: u128, + ) -> Result<(SendTxResponse, [SharedSecretKey; 1]), ExecutionFailureKind> { + let AccountPreparedData { + nsk: _, + npk: winner_npk, + ipk: winner_ipk, + auth_acc: winner_pre, + proof: _, + } = self + .private_acc_preparation(winner_account_id, false, false) + .await?; + + let pinata_acc = self.get_account_public(pinata_account_id).await.unwrap(); + + let program = nssa::program::Program::pinata(); + + let pinata_pre = AccountWithMetadata::new(pinata_acc.clone(), false, pinata_account_id); + + let eph_holder_winner = EphemeralKeyHolder::new(&winner_npk); + let shared_secret_winner = eph_holder_winner.calculate_shared_secret_sender(&winner_ipk); + + let (output, proof) = circuit::execute_and_prove( + &[pinata_pre, winner_pre], + &nssa::program::Program::serialize_instruction(solution).unwrap(), + &[0, 2], + &produce_random_nonces(1), + &[(winner_npk.clone(), shared_secret_winner.clone())], + &[], + &program.into(), + ) + .unwrap(); + + let message = + nssa::privacy_preserving_transaction::message::Message::try_from_circuit_output( + vec![pinata_account_id], + vec![], + vec![( + winner_npk.clone(), + winner_ipk.clone(), + eph_holder_winner.generate_ephemeral_public_key(), + )], + output, + ) + .unwrap(); + + let witness_set = + nssa::privacy_preserving_transaction::witness_set::WitnessSet::for_message( + &message, + proof, + &[], + ); + let tx = nssa::privacy_preserving_transaction::PrivacyPreservingTransaction::new( + message, + witness_set, + ); + + Ok(( + self.sequencer_client.send_tx_private(tx).await?, + [shared_secret_winner], + )) + } +} diff --git a/wallet/src/privacy_preserving_tx.rs b/wallet/src/privacy_preserving_tx.rs index e8e14d9..e79bbac 100644 --- a/wallet/src/privacy_preserving_tx.rs +++ b/wallet/src/privacy_preserving_tx.rs @@ -61,7 +61,7 @@ impl AccountManager { } PrivacyPreservingAccount::PrivateOwned(account_id) => { let pre = private_acc_preparation(wallet, account_id).await?; - let mask = if pre.auth_acc.is_authorized { 1 } else { 2 }; + let mask = if pre.pre_state.is_authorized { 1 } else { 2 }; (State::Private(pre), mask) } @@ -72,7 +72,7 @@ impl AccountManager { nsk: None, npk, ipk, - auth_acc, + pre_state: auth_acc, proof: None, }; @@ -95,7 +95,7 @@ impl AccountManager { .iter() .map(|state| match state { State::Public { account, .. } => account.clone(), - State::Private(pre) => pre.auth_acc.clone(), + State::Private(pre) => pre.pre_state.clone(), }) .collect() } @@ -168,7 +168,7 @@ struct AccountPreparedData { nsk: Option, npk: NullifierPublicKey, ipk: IncomingViewingPublicKey, - auth_acc: AccountWithMetadata, + pre_state: AccountWithMetadata, proof: Option, } @@ -206,7 +206,7 @@ async fn private_acc_preparation( nsk, npk: from_npk, ipk: from_ipk, - auth_acc: sender_pre, + pre_state: sender_pre, proof, }) } diff --git a/wallet/src/transaction_utils.rs b/wallet/src/transaction_utils.rs new file mode 100644 index 0000000..123e49d --- /dev/null +++ b/wallet/src/transaction_utils.rs @@ -0,0 +1,592 @@ +use common::{error::ExecutionFailureKind, sequencer_client::json::SendTxResponse}; +use key_protocol::key_management::ephemeral_key_holder::EphemeralKeyHolder; +use nssa::{ + Account, AccountId, PrivacyPreservingTransaction, + privacy_preserving_transaction::{circuit, message::Message, witness_set::WitnessSet}, + program::Program, +}; +use nssa_core::{ + Commitment, MembershipProof, NullifierPublicKey, NullifierSecretKey, SharedSecretKey, + account::AccountWithMetadata, encryption::IncomingViewingPublicKey, program::InstructionData, +}; + +use crate::{WalletCore, helperfunctions::produce_random_nonces}; + +pub(crate) struct AccountPreparedData { + pub nsk: Option, + pub npk: NullifierPublicKey, + pub ipk: IncomingViewingPublicKey, + pub auth_acc: AccountWithMetadata, + pub proof: Option, +} + +impl WalletCore { + pub(crate) async fn private_acc_preparation( + &self, + account_id: AccountId, + is_authorized: bool, + needs_proof: bool, + ) -> Result { + let Some((from_keys, from_acc)) = self + .storage + .user_data + .get_private_account(&account_id) + .cloned() + else { + return Err(ExecutionFailureKind::KeyNotFoundError); + }; + + let mut nsk = None; + let mut proof = None; + + let from_npk = from_keys.nullifer_public_key; + let from_ipk = from_keys.incoming_viewing_public_key; + + let sender_commitment = Commitment::new(&from_npk, &from_acc); + + let sender_pre = AccountWithMetadata::new(from_acc.clone(), is_authorized, &from_npk); + + if is_authorized { + nsk = Some(from_keys.private_key_holder.nullifier_secret_key); + } + + if needs_proof { + proof = self + .sequencer_client + .get_proof_for_commitment(sender_commitment) + .await + .unwrap(); + } + + Ok(AccountPreparedData { + nsk, + npk: from_npk, + ipk: from_ipk, + auth_acc: sender_pre, + proof, + }) + } + + pub(crate) async fn private_tx_two_accs_all_init( + &self, + from: AccountId, + to: AccountId, + instruction_data: InstructionData, + tx_pre_check: impl FnOnce(&Account, &Account) -> Result<(), ExecutionFailureKind>, + program: Program, + to_proof: MembershipProof, + ) -> Result<(SendTxResponse, [SharedSecretKey; 2]), ExecutionFailureKind> { + let AccountPreparedData { + nsk: from_nsk, + npk: from_npk, + ipk: from_ipk, + auth_acc: sender_pre, + proof: from_proof, + } = self.private_acc_preparation(from, true, true).await?; + + let AccountPreparedData { + nsk: to_nsk, + npk: to_npk, + ipk: to_ipk, + auth_acc: recipient_pre, + proof: _, + } = self.private_acc_preparation(to, true, false).await?; + + tx_pre_check(&sender_pre.account, &recipient_pre.account)?; + + let eph_holder_from = EphemeralKeyHolder::new(&from_npk); + let shared_secret_from = eph_holder_from.calculate_shared_secret_sender(&from_ipk); + + let eph_holder_to = EphemeralKeyHolder::new(&to_npk); + let shared_secret_to = eph_holder_to.calculate_shared_secret_sender(&to_ipk); + + let (output, proof) = circuit::execute_and_prove( + &[sender_pre, recipient_pre], + &instruction_data, + &[1, 1], + &produce_random_nonces(2), + &[ + (from_npk.clone(), shared_secret_from.clone()), + (to_npk.clone(), shared_secret_to.clone()), + ], + &[ + (from_nsk.unwrap(), from_proof.unwrap()), + (to_nsk.unwrap(), to_proof), + ], + &program.into(), + ) + .unwrap(); + + let message = Message::try_from_circuit_output( + vec![], + vec![], + vec![ + ( + from_npk.clone(), + from_ipk.clone(), + eph_holder_from.generate_ephemeral_public_key(), + ), + ( + to_npk.clone(), + to_ipk.clone(), + eph_holder_to.generate_ephemeral_public_key(), + ), + ], + output, + ) + .unwrap(); + + let witness_set = WitnessSet::for_message(&message, proof, &[]); + let tx = PrivacyPreservingTransaction::new(message, witness_set); + + Ok(( + self.sequencer_client.send_tx_private(tx).await?, + [shared_secret_from, shared_secret_to], + )) + } + + pub(crate) async fn private_tx_two_accs_receiver_uninit( + &self, + from: AccountId, + to: AccountId, + instruction_data: InstructionData, + tx_pre_check: impl FnOnce(&Account, &Account) -> Result<(), ExecutionFailureKind>, + program: Program, + ) -> Result<(SendTxResponse, [SharedSecretKey; 2]), ExecutionFailureKind> { + let AccountPreparedData { + nsk: from_nsk, + npk: from_npk, + ipk: from_ipk, + auth_acc: sender_pre, + proof: from_proof, + } = self.private_acc_preparation(from, true, true).await?; + + let AccountPreparedData { + nsk: _, + npk: to_npk, + ipk: to_ipk, + auth_acc: recipient_pre, + proof: _, + } = self.private_acc_preparation(to, false, false).await?; + + tx_pre_check(&sender_pre.account, &recipient_pre.account)?; + + let eph_holder_from = EphemeralKeyHolder::new(&from_npk); + let shared_secret_from = eph_holder_from.calculate_shared_secret_sender(&from_ipk); + + let eph_holder_to = EphemeralKeyHolder::new(&to_npk); + let shared_secret_to = eph_holder_to.calculate_shared_secret_sender(&to_ipk); + + let (output, proof) = circuit::execute_and_prove( + &[sender_pre, recipient_pre], + &instruction_data, + &[1, 2], + &produce_random_nonces(2), + &[ + (from_npk.clone(), shared_secret_from.clone()), + (to_npk.clone(), shared_secret_to.clone()), + ], + &[(from_nsk.unwrap(), from_proof.unwrap())], + &program.into(), + ) + .unwrap(); + + let message = Message::try_from_circuit_output( + vec![], + vec![], + vec![ + ( + from_npk.clone(), + from_ipk.clone(), + eph_holder_from.generate_ephemeral_public_key(), + ), + ( + to_npk.clone(), + to_ipk.clone(), + eph_holder_to.generate_ephemeral_public_key(), + ), + ], + output, + ) + .unwrap(); + + let witness_set = WitnessSet::for_message(&message, proof, &[]); + let tx = PrivacyPreservingTransaction::new(message, witness_set); + + Ok(( + self.sequencer_client.send_tx_private(tx).await?, + [shared_secret_from, shared_secret_to], + )) + } + + pub(crate) async fn private_tx_two_accs_receiver_outer( + &self, + from: AccountId, + to_npk: NullifierPublicKey, + to_ipk: IncomingViewingPublicKey, + instruction_data: InstructionData, + tx_pre_check: impl FnOnce(&Account, &Account) -> Result<(), ExecutionFailureKind>, + program: Program, + ) -> Result<(SendTxResponse, [SharedSecretKey; 2]), ExecutionFailureKind> { + let AccountPreparedData { + nsk: from_nsk, + npk: from_npk, + ipk: from_ipk, + auth_acc: sender_pre, + proof: from_proof, + } = self.private_acc_preparation(from, true, true).await?; + + let to_acc = nssa_core::account::Account::default(); + + tx_pre_check(&sender_pre.account, &to_acc)?; + + let recipient_pre = AccountWithMetadata::new(to_acc.clone(), false, &to_npk); + + let eph_holder = EphemeralKeyHolder::new(&to_npk); + + let shared_secret_from = eph_holder.calculate_shared_secret_sender(&from_ipk); + let shared_secret_to = eph_holder.calculate_shared_secret_sender(&to_ipk); + + let (output, proof) = circuit::execute_and_prove( + &[sender_pre, recipient_pre], + &instruction_data, + &[1, 2], + &produce_random_nonces(2), + &[ + (from_npk.clone(), shared_secret_from.clone()), + (to_npk.clone(), shared_secret_to.clone()), + ], + &[(from_nsk.unwrap(), from_proof.unwrap())], + &program.into(), + ) + .unwrap(); + + let message = Message::try_from_circuit_output( + vec![], + vec![], + vec![ + ( + from_npk.clone(), + from_ipk.clone(), + eph_holder.generate_ephemeral_public_key(), + ), + ( + to_npk.clone(), + to_ipk.clone(), + eph_holder.generate_ephemeral_public_key(), + ), + ], + output, + ) + .unwrap(); + + let witness_set = WitnessSet::for_message(&message, proof, &[]); + + let tx = PrivacyPreservingTransaction::new(message, witness_set); + + Ok(( + self.sequencer_client.send_tx_private(tx).await?, + [shared_secret_from, shared_secret_to], + )) + } + + pub(crate) async fn deshielded_tx_two_accs( + &self, + from: AccountId, + to: AccountId, + instruction_data: InstructionData, + tx_pre_check: impl FnOnce(&Account, &Account) -> Result<(), ExecutionFailureKind>, + program: Program, + ) -> Result<(SendTxResponse, [nssa_core::SharedSecretKey; 1]), ExecutionFailureKind> { + let AccountPreparedData { + nsk: from_nsk, + npk: from_npk, + ipk: from_ipk, + auth_acc: sender_pre, + proof: from_proof, + } = self.private_acc_preparation(from, true, true).await?; + + let Ok(to_acc) = self.get_account_public(to).await else { + return Err(ExecutionFailureKind::KeyNotFoundError); + }; + + tx_pre_check(&sender_pre.account, &to_acc)?; + + let recipient_pre = AccountWithMetadata::new(to_acc.clone(), false, to); + + let eph_holder = EphemeralKeyHolder::new(&from_npk); + let shared_secret = eph_holder.calculate_shared_secret_sender(&from_ipk); + + let (output, proof) = circuit::execute_and_prove( + &[sender_pre, recipient_pre], + &instruction_data, + &[1, 0], + &produce_random_nonces(1), + &[(from_npk.clone(), shared_secret.clone())], + &[(from_nsk.unwrap(), from_proof.unwrap())], + &program.into(), + ) + .unwrap(); + + let message = Message::try_from_circuit_output( + vec![to], + vec![], + vec![( + from_npk.clone(), + from_ipk.clone(), + eph_holder.generate_ephemeral_public_key(), + )], + output, + ) + .unwrap(); + + let witness_set = WitnessSet::for_message(&message, proof, &[]); + + let tx = PrivacyPreservingTransaction::new(message, witness_set); + + Ok(( + self.sequencer_client.send_tx_private(tx).await?, + [shared_secret], + )) + } + + pub(crate) async fn shielded_two_accs_all_init( + &self, + from: AccountId, + to: AccountId, + instruction_data: InstructionData, + tx_pre_check: impl FnOnce(&Account, &Account) -> Result<(), ExecutionFailureKind>, + program: Program, + to_proof: MembershipProof, + ) -> Result<(SendTxResponse, [SharedSecretKey; 1]), ExecutionFailureKind> { + let Ok(from_acc) = self.get_account_public(from).await else { + return Err(ExecutionFailureKind::KeyNotFoundError); + }; + + let AccountPreparedData { + nsk: to_nsk, + npk: to_npk, + ipk: to_ipk, + auth_acc: recipient_pre, + proof: _, + } = self.private_acc_preparation(to, true, false).await?; + + tx_pre_check(&from_acc, &recipient_pre.account)?; + + let sender_pre = AccountWithMetadata::new(from_acc.clone(), true, from); + + let eph_holder = EphemeralKeyHolder::new(&to_npk); + let shared_secret = eph_holder.calculate_shared_secret_sender(&to_ipk); + + let (output, proof) = circuit::execute_and_prove( + &[sender_pre, recipient_pre], + &instruction_data, + &[0, 1], + &produce_random_nonces(1), + &[(to_npk.clone(), shared_secret.clone())], + &[(to_nsk.unwrap(), to_proof)], + &program.into(), + ) + .unwrap(); + + let message = Message::try_from_circuit_output( + vec![from], + vec![from_acc.nonce], + vec![( + to_npk.clone(), + to_ipk.clone(), + eph_holder.generate_ephemeral_public_key(), + )], + output, + ) + .unwrap(); + + let signing_key = self.storage.user_data.get_pub_account_signing_key(&from); + + let Some(signing_key) = signing_key else { + return Err(ExecutionFailureKind::KeyNotFoundError); + }; + + let witness_set = WitnessSet::for_message(&message, proof, &[signing_key]); + + let tx = PrivacyPreservingTransaction::new(message, witness_set); + + Ok(( + self.sequencer_client.send_tx_private(tx).await?, + [shared_secret], + )) + } + + pub(crate) async fn shielded_two_accs_receiver_uninit( + &self, + from: AccountId, + to: AccountId, + instruction_data: InstructionData, + tx_pre_check: impl FnOnce(&Account, &Account) -> Result<(), ExecutionFailureKind>, + program: Program, + ) -> Result<(SendTxResponse, [SharedSecretKey; 1]), ExecutionFailureKind> { + let Ok(from_acc) = self.get_account_public(from).await else { + return Err(ExecutionFailureKind::KeyNotFoundError); + }; + + let AccountPreparedData { + nsk: _, + npk: to_npk, + ipk: to_ipk, + auth_acc: recipient_pre, + proof: _, + } = self.private_acc_preparation(to, false, false).await?; + + tx_pre_check(&from_acc, &recipient_pre.account)?; + + let sender_pre = AccountWithMetadata::new(from_acc.clone(), true, from); + + let eph_holder = EphemeralKeyHolder::new(&to_npk); + let shared_secret = eph_holder.calculate_shared_secret_sender(&to_ipk); + + let (output, proof) = circuit::execute_and_prove( + &[sender_pre, recipient_pre], + &instruction_data, + &[0, 2], + &produce_random_nonces(1), + &[(to_npk.clone(), shared_secret.clone())], + &[], + &program.into(), + ) + .unwrap(); + + let message = Message::try_from_circuit_output( + vec![from], + vec![from_acc.nonce], + vec![( + to_npk.clone(), + to_ipk.clone(), + eph_holder.generate_ephemeral_public_key(), + )], + output, + ) + .unwrap(); + + let signing_key = self.storage.user_data.get_pub_account_signing_key(&from); + + let Some(signing_key) = signing_key else { + return Err(ExecutionFailureKind::KeyNotFoundError); + }; + + let witness_set = WitnessSet::for_message(&message, proof, &[signing_key]); + + let tx = PrivacyPreservingTransaction::new(message, witness_set); + + Ok(( + self.sequencer_client.send_tx_private(tx).await?, + [shared_secret], + )) + } + + pub(crate) async fn shielded_two_accs_receiver_outer( + &self, + from: AccountId, + to_npk: NullifierPublicKey, + to_ipk: IncomingViewingPublicKey, + instruction_data: InstructionData, + tx_pre_check: impl FnOnce(&Account, &Account) -> Result<(), ExecutionFailureKind>, + program: Program, + ) -> Result { + let Ok(from_acc) = self.get_account_public(from).await else { + return Err(ExecutionFailureKind::KeyNotFoundError); + }; + + let to_acc = Account::default(); + + tx_pre_check(&from_acc, &to_acc)?; + + let sender_pre = AccountWithMetadata::new(from_acc.clone(), true, from); + let recipient_pre = AccountWithMetadata::new(to_acc.clone(), false, &to_npk); + + let eph_holder = EphemeralKeyHolder::new(&to_npk); + let shared_secret = eph_holder.calculate_shared_secret_sender(&to_ipk); + + let (output, proof) = circuit::execute_and_prove( + &[sender_pre, recipient_pre], + &instruction_data, + &[0, 2], + &produce_random_nonces(1), + &[(to_npk.clone(), shared_secret.clone())], + &[], + &program.into(), + ) + .unwrap(); + + let message = Message::try_from_circuit_output( + vec![from], + vec![from_acc.nonce], + vec![( + to_npk.clone(), + to_ipk.clone(), + eph_holder.generate_ephemeral_public_key(), + )], + output, + ) + .unwrap(); + + let signing_key = self.storage.user_data.get_pub_account_signing_key(&from); + + let Some(signing_key) = signing_key else { + return Err(ExecutionFailureKind::KeyNotFoundError); + }; + + let witness_set = WitnessSet::for_message(&message, proof, &[signing_key]); + let tx = PrivacyPreservingTransaction::new(message, witness_set); + + Ok(self.sequencer_client.send_tx_private(tx).await?) + } + + pub async fn register_account_under_authenticated_transfers_programs_private( + &self, + from: AccountId, + ) -> Result<(SendTxResponse, [SharedSecretKey; 1]), ExecutionFailureKind> { + let AccountPreparedData { + nsk: _, + npk: from_npk, + ipk: from_ipk, + auth_acc: sender_pre, + proof: _, + } = self.private_acc_preparation(from, false, false).await?; + + let eph_holder_from = EphemeralKeyHolder::new(&from_npk); + let shared_secret_from = eph_holder_from.calculate_shared_secret_sender(&from_ipk); + + let instruction: u128 = 0; + + let (output, proof) = circuit::execute_and_prove( + &[sender_pre], + &Program::serialize_instruction(instruction).unwrap(), + &[2], + &produce_random_nonces(1), + &[(from_npk.clone(), shared_secret_from.clone())], + &[], + &Program::authenticated_transfer_program().into(), + ) + .unwrap(); + + let message = Message::try_from_circuit_output( + vec![], + vec![], + vec![( + from_npk.clone(), + from_ipk.clone(), + eph_holder_from.generate_ephemeral_public_key(), + )], + output, + ) + .unwrap(); + + let witness_set = WitnessSet::for_message(&message, proof, &[]); + let tx = PrivacyPreservingTransaction::new(message, witness_set); + + Ok(( + self.sequencer_client.send_tx_private(tx).await?, + [shared_secret_from], + )) + } +}