Merge branch 'main' into Pravdyvy/amm-wallet-integration

This commit is contained in:
Pravdyvy 2025-12-15 10:40:46 +02:00
commit bb6cf322a0
72 changed files with 4805 additions and 658 deletions

View File

@ -11,7 +11,9 @@ members = [
"sequencer_core",
"common",
"nssa",
"nssa/core",
"integration_tests/proc_macro_test_attribute",
"examples/program_deployment",
]
[workspace.dependencies]

View File

@ -30,16 +30,25 @@ use crate::{
pub struct SequencerClient {
pub client: reqwest::Client,
pub sequencer_addr: String,
pub basic_auth: Option<(String, Option<String>)>,
}
impl SequencerClient {
pub fn new(sequencer_addr: String) -> Result<Self> {
Self::new_with_auth(sequencer_addr, None)
}
pub fn new_with_auth(
sequencer_addr: String,
basic_auth: Option<(String, Option<String>)>,
) -> Result<Self> {
Ok(Self {
client: Client::builder()
//Add more fiedls if needed
.timeout(std::time::Duration::from_secs(60))
.build()?,
sequencer_addr,
basic_auth,
})
}
@ -51,13 +60,16 @@ impl SequencerClient {
let request =
rpc_primitives::message::Request::from_payload_version_2_0(method.to_string(), payload);
let call_builder = self.client.post(&self.sequencer_addr);
let mut call_builder = self.client.post(&self.sequencer_addr);
if let Some((username, password)) = &self.basic_auth {
call_builder = call_builder.basic_auth(username, password.as_deref());
}
let call_res = call_builder.json(&request).send().await?;
let response_vall = call_res.json::<Value>().await?;
// TODO: Actually why we need separation of `result` and `error` in rpc response?
#[derive(Debug, Clone, Deserialize)]
#[allow(dead_code)]
pub struct SequencerRpcResponse {

View File

@ -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"

View File

@ -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 projects root directory:
```bash
cargo install --path wallet --force
```
# 1. Run the sequencer
From the projects root directory, start the sequencer:
```bash
cd sequencer_runner
RUST_LOG=info cargo run $(pwd)/configs/debug
```
Keep this terminal open. Well 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 wallets 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::<Instruction>();
```
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 wallets 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<u8> = 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<u8> = 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 accounts 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, well 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, youll 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 hasnt updated its private state yet. Thats 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 accounts signing key.
- Private accounts: authorization requires that the circuit verifies knowledge of the accounts 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
```

View File

@ -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"]

View File

@ -0,0 +1,3 @@
fn main() {
risc0_build::embed_methods();
}

View File

@ -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"

View File

@ -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<u8>;
fn main() {
// Read inputs
let (
ProgramInput {
pre_states,
instruction: greeting,
},
instruction_data,
) = read_nssa_inputs::<Instruction>();
// 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]);
}

View File

@ -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<u8>;
fn main() {
// Read inputs
let (
ProgramInput {
pre_states,
instruction: greeting,
},
instruction_data,
) = read_nssa_inputs::<Instruction>();
// 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]);
}

View File

@ -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<u8>);
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<u8>) -> 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<AccountPostState> {
// Construct the post state account values
let from_data: Vec<u8> = 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::<Instruction>();
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);
}

View File

@ -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<u8> = 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],
);
}

View File

@ -0,0 +1 @@
include!(concat!(env!("OUT_DIR"), "/methods.rs"));

View File

@ -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 <account_id>
//
// 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<u8> = std::fs::read(program_path).unwrap();
let program = Program::new(bytecode).unwrap();
// Define the desired greeting in ASCII
let greeting: Vec<u8> = 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();
}

View File

@ -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 <account_id>
//
// 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<u8> = std::fs::read(program_path).unwrap();
let program = Program::new(bytecode).unwrap();
// Define the desired greeting in ASCII
let greeting: Vec<u8> = 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();
}

View File

@ -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 <account_id>
//
// 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<u8> = 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();
}

View File

@ -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 <account_id>
//
// 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<u8> = 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<u8> = 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();
}

View File

@ -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 <function> <params>
//
// 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<u8>);
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<u8> = 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();
}
};
}

View File

@ -542,5 +542,6 @@
}
}
}
]
],
"basic_auth": null
}

Binary file not shown.

View File

@ -42,7 +42,7 @@ pub const ACC_RECEIVER_PRIVATE: &str = "AKTcXgJ1xoynta1Ec7y6Jso1z1JQtHqd7aPQ1h9e
pub const TIME_TO_WAIT_FOR_BLOCK_SECONDS: u64 = 12;
pub const NSSA_PROGRAM_FOR_TEST_DATA_CHANGER: &[u8] = include_bytes!("data_changer.bin");
pub const NSSA_PROGRAM_FOR_TEST_DATA_CHANGER: &str = "data_changer.bin";
fn make_public_account_input_from_str(account_id: &str) -> String {
format!("Public/{account_id}")

View File

@ -2,6 +2,7 @@ use std::{
collections::HashMap,
path::PathBuf,
pin::Pin,
str::FromStr,
time::{Duration, Instant},
};
@ -10,7 +11,7 @@ use anyhow::Result;
use common::{PINATA_BASE58, sequencer_client::SequencerClient};
use key_protocol::key_management::key_tree::chain_index::ChainIndex;
use log::info;
use nssa::{AccountId, ProgramDeploymentTransaction, program::Program};
use nssa::{AccountId, program::Program};
use nssa_core::{NullifierPublicKey, encryption::shared_key_derivation::Secp256k1Point};
use sequencer_runner::startup_sequencer;
use tempfile::TempDir;
@ -86,9 +87,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
#[nssa_integration_test]
pub async fn test_success_move_to_another_account() {
info!("########## test_success_move_to_another_account ##########");
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Public {
cci: ChainIndex::root(),
}));
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Public { cci: None }));
let wallet_config = fetch_config().await.unwrap();
@ -292,9 +291,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
let SubcommandReturnValue::RegisterAccount {
account_id: definition_account_id,
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
NewSubcommand::Public {
cci: ChainIndex::root(),
},
NewSubcommand::Public { cci: None },
)))
.await
.unwrap()
@ -305,9 +302,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
let SubcommandReturnValue::RegisterAccount {
account_id: supply_account_id,
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
NewSubcommand::Public {
cci: ChainIndex::root(),
},
NewSubcommand::Public { cci: None },
)))
.await
.unwrap()
@ -318,9 +313,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
let SubcommandReturnValue::RegisterAccount {
account_id: recipient_account_id,
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
NewSubcommand::Public {
cci: ChainIndex::root(),
},
NewSubcommand::Public { cci: None },
)))
.await
.unwrap()
@ -356,8 +349,8 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
// The data of a token definition account has the following layout:
// [ 0x00 || name (6 bytes) || total supply (little endian 16 bytes) ]
assert_eq!(
definition_acc.data,
vec![
definition_acc.data.as_ref(),
&[
0, 65, 32, 78, 65, 77, 69, 37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]
);
@ -375,11 +368,14 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
// The data of a token definition account has the following layout:
// [ 0x01 || corresponding_token_definition_id (32 bytes) || balance (little endian 16
// bytes) ] First byte of the data equal to 1 means it's a token holding account
assert_eq!(supply_acc.data[0], 1);
assert_eq!(supply_acc.data.as_ref()[0], 1);
// Bytes from 1 to 33 represent the id of the token this account is associated with.
// In this example, this is a token account of the newly created token, so it is expected
// to be equal to the account_id of the token definition account.
assert_eq!(&supply_acc.data[1..33], definition_account_id.to_bytes());
assert_eq!(
&supply_acc.data.as_ref()[1..33],
definition_account_id.to_bytes()
);
assert_eq!(
u128::from_le_bytes(supply_acc.data[33..].try_into().unwrap()),
37
@ -441,20 +437,18 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
}
/// This test creates a new private token using the token program. After creating the token, the
/// test executes a private token transfer to a new account. All accounts are owned except
/// definition.
/// test executes a private token transfer to a new account. All accounts are private owned
/// except definition which is public.
#[nssa_integration_test]
pub async fn test_success_token_program_private_owned() {
info!("########## test_success_token_program_private_owned ##########");
pub async fn test_success_token_program_private_owned_supply() {
info!("########## test_success_token_program_private_owned_supply ##########");
let wallet_config = fetch_config().await.unwrap();
// Create new account for the token definition (public)
let SubcommandReturnValue::RegisterAccount {
account_id: definition_account_id,
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
NewSubcommand::Public {
cci: ChainIndex::root(),
},
NewSubcommand::Public { cci: None },
)))
.await
.unwrap()
@ -465,9 +459,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
let SubcommandReturnValue::RegisterAccount {
account_id: supply_account_id,
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
NewSubcommand::Private {
cci: ChainIndex::root(),
},
NewSubcommand::Private { cci: None },
)))
.await
.unwrap()
@ -478,9 +470,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
let SubcommandReturnValue::RegisterAccount {
account_id: recipient_account_id,
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
NewSubcommand::Private {
cci: ChainIndex::root(),
},
NewSubcommand::Private { cci: None },
)))
.await
.unwrap()
@ -518,8 +508,8 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
// The data of a token definition account has the following layout:
// [ 0x00 || name (6 bytes) || total supply (little endian 16 bytes) ]
assert_eq!(
definition_acc.data,
vec![
definition_acc.data.as_ref(),
&[
0, 65, 32, 78, 65, 77, 69, 37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]
);
@ -602,19 +592,104 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
assert!(verify_commitment_is_in_state(new_commitment2, &seq_client).await);
}
/// This test creates a new private token using the token program. After creating the token, the
/// test executes a private token transfer to a new account.
/// This test creates a new private token using the token program. All accounts are private
/// owned except supply which is public.
#[nssa_integration_test]
pub async fn test_success_token_program_private_claiming_path() {
info!("########## test_success_token_program_private_claiming_path ##########");
pub async fn test_success_token_program_private_owned_definition() {
info!("########## test_success_token_program_private_owned_definition ##########");
let wallet_config = fetch_config().await.unwrap();
// Create new account for the token definition (public)
// Create new account for the token definition (private)
let SubcommandReturnValue::RegisterAccount {
account_id: definition_account_id,
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
NewSubcommand::Private {
cci: Some(ChainIndex::root()),
},
)))
.await
.unwrap()
else {
panic!("invalid subcommand return value");
};
// Create new account for the token supply holder (public)
let SubcommandReturnValue::RegisterAccount {
account_id: supply_account_id,
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
NewSubcommand::Public {
cci: ChainIndex::root(),
cci: Some(ChainIndex::root()),
},
)))
.await
.unwrap()
else {
panic!("invalid subcommand return value");
};
// Create new token
let subcommand = TokenProgramAgnosticSubcommand::New {
definition_account_id: make_private_account_input_from_str(
&definition_account_id.to_string(),
),
supply_account_id: make_public_account_input_from_str(&supply_account_id.to_string()),
name: "A NAME".to_string(),
total_supply: 37,
};
wallet::cli::execute_subcommand(Command::Token(subcommand))
.await
.unwrap();
info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
let seq_client = SequencerClient::new(wallet_config.sequencer_addr.clone()).unwrap();
let wallet_config = fetch_config().await.unwrap();
let wallet_storage = WalletCore::start_from_config_update_chain(wallet_config)
.await
.unwrap();
let new_commitment1 = wallet_storage
.get_private_account_commitment(&definition_account_id)
.unwrap();
assert!(verify_commitment_is_in_state(new_commitment1, &seq_client).await);
// Check the status of the token definition account is the expected after the execution
let supply_acc = seq_client
.get_account(supply_account_id.to_string())
.await
.unwrap()
.account;
assert_eq!(supply_acc.program_owner, Program::token().id());
// The data of a token definition account has the following layout:
// [ 0x00 || name (6 bytes) || total supply (little endian 16 bytes) ]
assert_eq!(
supply_acc.data.as_ref(),
&[
1, 128, 101, 5, 31, 43, 36, 97, 108, 164, 92, 25, 157, 173, 5, 14, 194, 121, 239,
84, 19, 160, 243, 47, 193, 2, 250, 247, 232, 253, 191, 232, 173, 37, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]
);
}
/// This test creates a new private token using the token program. All accounts are private
/// owned.
#[nssa_integration_test]
pub async fn test_success_token_program_private_owned_definition_and_supply() {
info!(
"########## test_success_token_program_private_owned_definition_and_supply ##########"
);
let wallet_config = fetch_config().await.unwrap();
// Create new account for the token definition (private)
let SubcommandReturnValue::RegisterAccount {
account_id: definition_account_id,
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
NewSubcommand::Private {
cci: Some(ChainIndex::root()),
},
)))
.await
@ -627,7 +702,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
account_id: supply_account_id,
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
NewSubcommand::Private {
cci: ChainIndex::root(),
cci: Some(ChainIndex::root()),
},
)))
.await
@ -635,13 +710,105 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
else {
panic!("invalid subcommand return value");
};
// Create new token
let subcommand = TokenProgramAgnosticSubcommand::New {
definition_account_id: make_private_account_input_from_str(
&definition_account_id.to_string(),
),
supply_account_id: make_private_account_input_from_str(&supply_account_id.to_string()),
name: "A NAME".to_string(),
total_supply: 37,
};
wallet::cli::execute_subcommand(Command::Token(subcommand))
.await
.unwrap();
info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
let seq_client = SequencerClient::new(wallet_config.sequencer_addr.clone()).unwrap();
let wallet_config = fetch_config().await.unwrap();
let wallet_storage = WalletCore::start_from_config_update_chain(wallet_config)
.await
.unwrap();
let new_commitment1 = wallet_storage
.get_private_account_commitment(&definition_account_id)
.unwrap();
assert!(verify_commitment_is_in_state(new_commitment1, &seq_client).await);
let new_commitment2 = wallet_storage
.get_private_account_commitment(&supply_account_id)
.unwrap();
assert!(verify_commitment_is_in_state(new_commitment2, &seq_client).await);
let definition_acc = wallet_storage
.get_account_private(&definition_account_id)
.unwrap();
let supply_acc = wallet_storage
.get_account_private(&supply_account_id)
.unwrap();
assert_eq!(definition_acc.program_owner, Program::token().id());
// The data of a token definition account has the following layout:
// [ 0x00 || name (6 bytes) || total supply (little endian 16 bytes) ]
assert_eq!(
definition_acc.data.as_ref(),
&[
0, 65, 32, 78, 65, 77, 69, 37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]
);
assert_eq!(supply_acc.program_owner, Program::token().id());
// The data of a token definition account has the following layout:
// [ 0x00 || name (6 bytes) || total supply (little endian 16 bytes) ]
assert_eq!(
supply_acc.data.as_ref(),
&[
1, 128, 101, 5, 31, 43, 36, 97, 108, 164, 92, 25, 157, 173, 5, 14, 194, 121, 239,
84, 19, 160, 243, 47, 193, 2, 250, 247, 232, 253, 191, 232, 173, 37, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]
);
}
/// This test creates a new private token using the token program. After creating the token, the
/// test executes a private token transfer to a new account.
#[nssa_integration_test]
pub async fn test_success_token_program_private_claiming_path() {
info!("########## test_success_token_program_private_claiming_path ##########");
let wallet_config = fetch_config().await.unwrap();
// Create new account for the token definition (public)
let SubcommandReturnValue::RegisterAccount {
account_id: definition_account_id,
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
NewSubcommand::Public { cci: None },
)))
.await
.unwrap()
else {
panic!("invalid subcommand return value");
};
// Create new account for the token supply holder (private)
let SubcommandReturnValue::RegisterAccount {
account_id: supply_account_id,
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
NewSubcommand::Private { cci: None },
)))
.await
.unwrap()
else {
panic!("invalid subcommand return value");
};
// Create new account for receiving a token transaction
let SubcommandReturnValue::RegisterAccount {
account_id: recipient_account_id,
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
NewSubcommand::Private {
cci: ChainIndex::root(),
},
NewSubcommand::Private { cci: None },
)))
.await
.unwrap()
@ -679,8 +846,8 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
// The data of a token definition account has the following layout:
// [ 0x00 || name (6 bytes) || total supply (little endian 16 bytes) ]
assert_eq!(
definition_acc.data,
vec![
definition_acc.data.as_ref(),
&[
0, 65, 32, 78, 65, 77, 69, 37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]
);
@ -755,9 +922,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
let SubcommandReturnValue::RegisterAccount {
account_id: definition_account_id,
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
NewSubcommand::Public {
cci: ChainIndex::root(),
},
NewSubcommand::Public { cci: None },
)))
.await
.unwrap()
@ -768,9 +933,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
let SubcommandReturnValue::RegisterAccount {
account_id: supply_account_id,
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
NewSubcommand::Public {
cci: ChainIndex::root(),
},
NewSubcommand::Public { cci: None },
)))
.await
.unwrap()
@ -781,9 +944,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
let SubcommandReturnValue::RegisterAccount {
account_id: recipient_account_id,
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
NewSubcommand::Private {
cci: ChainIndex::root(),
},
NewSubcommand::Private { cci: None },
)))
.await
.unwrap()
@ -821,8 +982,8 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
// The data of a token definition account has the following layout:
// [ 0x00 || name (6 bytes) || total supply (little endian 16 bytes) ]
assert_eq!(
definition_acc.data,
vec![
definition_acc.data.as_ref(),
&[
0, 65, 32, 78, 65, 77, 69, 37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]
);
@ -897,9 +1058,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
let SubcommandReturnValue::RegisterAccount {
account_id: definition_account_id,
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
NewSubcommand::Public {
cci: ChainIndex::root(),
},
NewSubcommand::Public { cci: None },
)))
.await
.unwrap()
@ -910,9 +1069,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
let SubcommandReturnValue::RegisterAccount {
account_id: supply_account_id,
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
NewSubcommand::Private {
cci: ChainIndex::root(),
},
NewSubcommand::Private { cci: None },
)))
.await
.unwrap()
@ -923,9 +1080,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
let SubcommandReturnValue::RegisterAccount {
account_id: recipient_account_id,
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
NewSubcommand::Public {
cci: ChainIndex::root(),
},
NewSubcommand::Public { cci: None },
)))
.await
.unwrap()
@ -963,8 +1118,8 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
// The data of a token definition account has the following layout:
// [ 0x00 || name (6 bytes) || total supply (little endian 16 bytes) ]
assert_eq!(
definition_acc.data,
vec![
definition_acc.data.as_ref(),
&[
0, 65, 32, 78, 65, 77, 69, 37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]
);
@ -1126,9 +1281,8 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
);
let from: AccountId = ACC_SENDER_PRIVATE.parse().unwrap();
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Private {
cci: ChainIndex::root(),
}));
let command =
Command::Account(AccountSubcommand::New(NewSubcommand::Private { cci: None }));
let sub_ret = wallet::cli::execute_subcommand(command).await.unwrap();
let SubcommandReturnValue::RegisterAccount {
@ -1441,15 +1595,18 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
#[nssa_integration_test]
pub async fn test_program_deployment() {
info!("########## test program deployment ##########");
let bytecode = NSSA_PROGRAM_FOR_TEST_DATA_CHANGER.to_vec();
let message = nssa::program_deployment_transaction::Message::new(bytecode.clone());
let transaction = ProgramDeploymentTransaction::new(message);
let binary_filepath: PathBuf = NSSA_PROGRAM_FOR_TEST_DATA_CHANGER.parse().unwrap();
let command = Command::DeployProgram {
binary_filepath: binary_filepath.clone(),
};
wallet::cli::execute_subcommand(command).await.unwrap();
let wallet_config = fetch_config().await.unwrap();
let seq_client = SequencerClient::new(wallet_config.sequencer_addr.clone()).unwrap();
let _response = seq_client.send_tx_program(transaction).await.unwrap();
info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
@ -1457,13 +1614,15 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
// We pass an uninitialized account and we expect after execution to be owned by the data
// changer program (NSSA account claiming mechanism) with data equal to [0] (due to program
// logic)
//
let bytecode = std::fs::read(binary_filepath).unwrap();
let data_changer = Program::new(bytecode).unwrap();
let account_id: AccountId = "11".repeat(16).parse().unwrap();
let message = nssa::public_transaction::Message::try_new(
data_changer.id(),
vec![account_id],
vec![],
(),
vec![0],
)
.unwrap();
let witness_set = nssa::public_transaction::WitnessSet::for_message(&message, &[]);
@ -1480,7 +1639,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
.account;
assert_eq!(post_state_account.program_owner, data_changer.id());
assert_eq!(post_state_account.balance, 0);
assert_eq!(post_state_account.data, vec![0]);
assert_eq!(post_state_account.data.as_ref(), &[0]);
assert_eq!(post_state_account.nonce, 0);
info!("Success!");
@ -1489,9 +1648,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
#[nssa_integration_test]
pub async fn test_authenticated_transfer_initialize_function() {
info!("########## test initialize account for authenticated transfer ##########");
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Public {
cci: ChainIndex::root(),
}));
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Public { cci: None }));
let SubcommandReturnValue::RegisterAccount { account_id } =
wallet::cli::execute_subcommand(command).await.unwrap()
else {
@ -1589,9 +1746,7 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
let SubcommandReturnValue::RegisterAccount {
account_id: winner_account_id,
} = wallet::cli::execute_subcommand(Command::Account(AccountSubcommand::New(
NewSubcommand::Private {
cci: ChainIndex::root(),
},
NewSubcommand::Private { cci: None },
)))
.await
.unwrap()
@ -1669,6 +1824,217 @@ pub fn prepare_function_map() -> HashMap<String, TestFunction> {
info!("Success!");
}
#[nssa_integration_test]
pub async fn test_keys_restoration() {
info!("########## test_keys_restoration ##########");
let from: AccountId = ACC_SENDER_PRIVATE.parse().unwrap();
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Private {
cci: Some(ChainIndex::root()),
}));
let sub_ret = wallet::cli::execute_subcommand(command).await.unwrap();
let SubcommandReturnValue::RegisterAccount {
account_id: to_account_id1,
} = sub_ret
else {
panic!("FAILED TO REGISTER ACCOUNT");
};
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Private {
cci: Some(ChainIndex::from_str("/0").unwrap()),
}));
let sub_ret = wallet::cli::execute_subcommand(command).await.unwrap();
let SubcommandReturnValue::RegisterAccount {
account_id: to_account_id2,
} = sub_ret
else {
panic!("FAILED TO REGISTER ACCOUNT");
};
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: make_private_account_input_from_str(&from.to_string()),
to: Some(make_private_account_input_from_str(
&to_account_id1.to_string(),
)),
to_npk: None,
to_ipk: None,
amount: 100,
});
wallet::cli::execute_subcommand(command).await.unwrap();
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: make_private_account_input_from_str(&from.to_string()),
to: Some(make_private_account_input_from_str(
&to_account_id2.to_string(),
)),
to_npk: None,
to_ipk: None,
amount: 101,
});
wallet::cli::execute_subcommand(command).await.unwrap();
let from: AccountId = ACC_SENDER.parse().unwrap();
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Public {
cci: Some(ChainIndex::root()),
}));
let sub_ret = wallet::cli::execute_subcommand(command).await.unwrap();
let SubcommandReturnValue::RegisterAccount {
account_id: to_account_id3,
} = sub_ret
else {
panic!("FAILED TO REGISTER ACCOUNT");
};
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Public {
cci: Some(ChainIndex::from_str("/0").unwrap()),
}));
let sub_ret = wallet::cli::execute_subcommand(command).await.unwrap();
let SubcommandReturnValue::RegisterAccount {
account_id: to_account_id4,
} = sub_ret
else {
panic!("FAILED TO REGISTER ACCOUNT");
};
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: make_public_account_input_from_str(&from.to_string()),
to: Some(make_public_account_input_from_str(
&to_account_id3.to_string(),
)),
to_npk: None,
to_ipk: None,
amount: 102,
});
wallet::cli::execute_subcommand(command).await.unwrap();
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: make_public_account_input_from_str(&from.to_string()),
to: Some(make_public_account_input_from_str(
&to_account_id4.to_string(),
)),
to_npk: None,
to_ipk: None,
amount: 103,
});
wallet::cli::execute_subcommand(command).await.unwrap();
info!("########## PREPARATION END ##########");
wallet::cli::execute_keys_restoration("test_pass".to_string(), 10)
.await
.unwrap();
let wallet_config = fetch_config().await.unwrap();
let wallet_storage = WalletCore::start_from_config_update_chain(wallet_config.clone())
.await
.unwrap();
let acc1 = wallet_storage
.storage
.user_data
.private_key_tree
.get_node(to_account_id1)
.expect("Acc 1 should be restored");
let acc2 = wallet_storage
.storage
.user_data
.private_key_tree
.get_node(to_account_id2)
.expect("Acc 2 should be restored");
let _ = wallet_storage
.storage
.user_data
.public_key_tree
.get_node(to_account_id3)
.expect("Acc 3 should be restored");
let _ = wallet_storage
.storage
.user_data
.public_key_tree
.get_node(to_account_id4)
.expect("Acc 4 should be restored");
assert_eq!(
acc1.value.1.program_owner,
Program::authenticated_transfer_program().id()
);
assert_eq!(
acc2.value.1.program_owner,
Program::authenticated_transfer_program().id()
);
assert_eq!(acc1.value.1.balance, 100);
assert_eq!(acc2.value.1.balance, 101);
info!("########## TREE CHECKS END ##########");
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: make_private_account_input_from_str(&to_account_id1.to_string()),
to: Some(make_private_account_input_from_str(
&to_account_id2.to_string(),
)),
to_npk: None,
to_ipk: None,
amount: 10,
});
wallet::cli::execute_subcommand(command).await.unwrap();
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: make_public_account_input_from_str(&to_account_id3.to_string()),
to: Some(make_public_account_input_from_str(
&to_account_id4.to_string(),
)),
to_npk: None,
to_ipk: None,
amount: 11,
});
wallet::cli::execute_subcommand(command).await.unwrap();
let wallet_config = fetch_config().await.unwrap();
let seq_client = SequencerClient::new(wallet_config.sequencer_addr.clone()).unwrap();
let wallet_storage = WalletCore::start_from_config_update_chain(wallet_config.clone())
.await
.unwrap();
let comm1 = wallet_storage
.get_private_account_commitment(&to_account_id1)
.expect("Acc 1 commitment should exist");
let comm2 = wallet_storage
.get_private_account_commitment(&to_account_id2)
.expect("Acc 2 commitment should exist");
assert!(verify_commitment_is_in_state(comm1, &seq_client).await);
assert!(verify_commitment_is_in_state(comm2, &seq_client).await);
let acc3 = seq_client
.get_account_balance(to_account_id3.to_string())
.await
.expect("Acc 3 must be present in public state");
let acc4 = seq_client
.get_account_balance(to_account_id4.to_string())
.await
.expect("Acc 4 must be present in public state");
assert_eq!(acc3.balance, 91);
assert_eq!(acc4.balance, 114);
info!("Success!");
}
println!("{function_map:#?}");
function_map

View File

@ -8,7 +8,8 @@ use nssa::{
public_transaction as putx,
};
use nssa_core::{
MembershipProof, NullifierPublicKey, account::AccountWithMetadata,
MembershipProof, NullifierPublicKey,
account::{AccountWithMetadata, data::Data},
encryption::IncomingViewingPublicKey,
};
use sequencer_core::config::{AccountInitialData, CommitmentsInitialData, SequencerConfig};
@ -90,7 +91,7 @@ impl TpsTestManager {
balance: 100,
nonce: 0xdeadbeef,
program_owner: Program::authenticated_transfer_program().id(),
data: vec![],
data: Data::default(),
};
let initial_commitment = CommitmentsInitialData {
npk: sender_npk,
@ -129,7 +130,7 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction {
balance: 100,
nonce: 0xdeadbeef,
program_owner: program.id(),
data: vec![],
data: Data::default(),
},
true,
AccountId::from(&sender_npk),
@ -167,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(

View File

@ -16,6 +16,7 @@ bip39.workspace = true
hmac-sha512.workspace = true
thiserror.workspace = true
nssa-core = { path = "../nssa/core", features = ["host"] }
itertools.workspace = true
[dependencies.common]
path = "../common"

View File

@ -1,8 +1,9 @@
use std::{fmt::Display, str::FromStr};
use itertools::Itertools;
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Serialize, Deserialize)]
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Serialize, Deserialize, Hash)]
pub struct ChainIndex(Vec<u32>);
#[derive(thiserror::Error, Debug)]
@ -77,12 +78,82 @@ impl ChainIndex {
ChainIndex(chain)
}
pub fn previous_in_line(&self) -> Option<ChainIndex> {
let mut chain = self.0.clone();
if let Some(last_p) = chain.last_mut() {
*last_p = last_p.checked_sub(1)?;
}
Some(ChainIndex(chain))
}
pub fn parent(&self) -> Option<ChainIndex> {
if self.0.is_empty() {
None
} else {
Some(ChainIndex(self.0[..(self.0.len() - 1)].to_vec()))
}
}
pub fn nth_child(&self, child_id: u32) -> ChainIndex {
let mut chain = self.0.clone();
chain.push(child_id);
ChainIndex(chain)
}
pub fn depth(&self) -> u32 {
self.0.iter().map(|cci| cci + 1).sum()
}
fn collapse_back(&self) -> Option<Self> {
let mut res = self.parent()?;
let last_mut = res.0.last_mut()?;
*last_mut += *(self.0.last()?) + 1;
Some(res)
}
fn shuffle_iter(&self) -> impl Iterator<Item = ChainIndex> {
self.0
.iter()
.permutations(self.0.len())
.unique()
.map(|item| ChainIndex(item.into_iter().cloned().collect()))
}
pub fn chain_ids_at_depth(depth: usize) -> impl Iterator<Item = ChainIndex> {
let mut stack = vec![ChainIndex(vec![0; depth])];
let mut cumulative_stack = vec![ChainIndex(vec![0; depth])];
while let Some(id) = stack.pop() {
if let Some(collapsed_id) = id.collapse_back() {
for id in collapsed_id.shuffle_iter() {
stack.push(id.clone());
cumulative_stack.push(id);
}
}
}
cumulative_stack.into_iter().unique()
}
pub fn chain_ids_at_depth_rev(depth: usize) -> impl Iterator<Item = ChainIndex> {
let mut stack = vec![ChainIndex(vec![0; depth])];
let mut cumulative_stack = vec![ChainIndex(vec![0; depth])];
while let Some(id) = stack.pop() {
if let Some(collapsed_id) = id.collapse_back() {
for id in collapsed_id.shuffle_iter() {
stack.push(id.clone());
cumulative_stack.push(id);
}
}
}
cumulative_stack.into_iter().rev().unique()
}
}
#[cfg(test)]
@ -145,4 +216,83 @@ mod tests {
assert_eq!(string_index, "/5/7/8".to_string());
}
#[test]
fn test_prev_in_line() {
let chain_id = ChainIndex(vec![1, 7, 3]);
let prev_chain_id = chain_id.previous_in_line().unwrap();
assert_eq!(prev_chain_id, ChainIndex(vec![1, 7, 2]))
}
#[test]
fn test_prev_in_line_no_prev() {
let chain_id = ChainIndex(vec![1, 7, 0]);
let prev_chain_id = chain_id.previous_in_line();
assert_eq!(prev_chain_id, None)
}
#[test]
fn test_parent() {
let chain_id = ChainIndex(vec![1, 7, 3]);
let parent_chain_id = chain_id.parent().unwrap();
assert_eq!(parent_chain_id, ChainIndex(vec![1, 7]))
}
#[test]
fn test_parent_no_parent() {
let chain_id = ChainIndex(vec![]);
let parent_chain_id = chain_id.parent();
assert_eq!(parent_chain_id, None)
}
#[test]
fn test_parent_root() {
let chain_id = ChainIndex(vec![1]);
let parent_chain_id = chain_id.parent().unwrap();
assert_eq!(parent_chain_id, ChainIndex::root())
}
#[test]
fn test_collapse_back() {
let chain_id = ChainIndex(vec![1, 1]);
let collapsed = chain_id.collapse_back().unwrap();
assert_eq!(collapsed, ChainIndex(vec![3]))
}
#[test]
fn test_collapse_back_one() {
let chain_id = ChainIndex(vec![1]);
let collapsed = chain_id.collapse_back();
assert_eq!(collapsed, None)
}
#[test]
fn test_collapse_back_root() {
let chain_id = ChainIndex(vec![]);
let collapsed = chain_id.collapse_back();
assert_eq!(collapsed, None)
}
#[test]
fn test_shuffle() {
for id in ChainIndex::chain_ids_at_depth(5) {
println!("{id}");
}
}
}

View File

@ -1,5 +1,10 @@
use std::collections::{BTreeMap, HashMap};
use std::{
collections::{BTreeMap, HashMap},
sync::Arc,
};
use anyhow::Result;
use common::sequencer_client::SequencerClient;
use serde::{Deserialize, Serialize};
use crate::key_management::{
@ -15,6 +20,8 @@ pub mod keys_private;
pub mod keys_public;
pub mod traits;
pub const DEPTH_SOFT_CAP: u32 = 20;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct KeyTree<N: KeyNode> {
pub key_map: BTreeMap<ChainIndex, N>,
@ -96,21 +103,53 @@ impl<N: KeyNode> KeyTree<N> {
}
}
pub fn generate_new_node(&mut self, parent_cci: ChainIndex) -> Option<nssa::AccountId> {
let father_keys = self.key_map.get(&parent_cci)?;
pub fn generate_new_node(
&mut self,
parent_cci: &ChainIndex,
) -> Option<(nssa::AccountId, ChainIndex)> {
let parent_keys = self.key_map.get(parent_cci)?;
let next_child_id = self
.find_next_last_child_of_id(&parent_cci)
.find_next_last_child_of_id(parent_cci)
.expect("Can be None only if parent is not present");
let next_cci = parent_cci.nth_child(next_child_id);
let child_keys = father_keys.nth_child(next_child_id);
let child_keys = parent_keys.nth_child(next_child_id);
let account_id = child_keys.account_id();
self.key_map.insert(next_cci.clone(), child_keys);
self.account_id_map.insert(account_id, next_cci);
self.account_id_map.insert(account_id, next_cci.clone());
Some(account_id)
Some((account_id, next_cci))
}
fn find_next_slot_layered(&self) -> ChainIndex {
let mut depth = 1;
'outer: loop {
for chain_id in ChainIndex::chain_ids_at_depth_rev(depth) {
if !self.key_map.contains_key(&chain_id) {
break 'outer chain_id;
}
}
depth += 1;
}
}
pub fn fill_node(&mut self, chain_index: &ChainIndex) -> Option<(nssa::AccountId, ChainIndex)> {
let parent_keys = self.key_map.get(&chain_index.parent()?)?;
let child_id = *chain_index.chain().last()?;
let child_keys = parent_keys.nth_child(child_id);
let account_id = child_keys.account_id();
self.key_map.insert(chain_index.clone(), child_keys);
self.account_id_map.insert(account_id, chain_index.clone());
Some((account_id, chain_index.clone()))
}
pub fn generate_new_node_layered(&mut self) -> Option<(nssa::AccountId, ChainIndex)> {
self.fill_node(&self.find_next_slot_layered())
}
pub fn get_node(&self, account_id: nssa::AccountId) -> Option<&N> {
@ -129,11 +168,164 @@ impl<N: KeyNode> KeyTree<N> {
self.account_id_map.insert(account_id, chain_index.clone());
self.key_map.insert(chain_index, node);
}
pub fn remove(&mut self, addr: nssa::AccountId) -> Option<N> {
let chain_index = self.account_id_map.remove(&addr).unwrap();
self.key_map.remove(&chain_index)
}
/// Populates tree with children.
///
/// For given `depth` adds children to a tree such that their `ChainIndex::depth(&self) <
/// depth`.
///
/// Tree must be empty before start
pub fn generate_tree_for_depth(&mut self, depth: u32) {
let mut id_stack = vec![ChainIndex::root()];
while let Some(curr_id) = id_stack.pop() {
let mut next_id = curr_id.nth_child(0);
while (next_id.depth()) < depth {
self.generate_new_node(&curr_id);
id_stack.push(next_id.clone());
next_id = next_id.next_in_line();
}
}
}
}
impl KeyTree<ChildKeysPrivate> {
/// Cleanup of all non-initialized accounts in a private tree
///
/// For given `depth` checks children to a tree such that their `ChainIndex::depth(&self) <
/// depth`.
///
/// If account is default, removes them.
///
/// Chain must be parsed for accounts beforehand
///
/// Fast, leaves gaps between accounts
pub fn cleanup_tree_remove_uninit_for_depth(&mut self, depth: u32) {
let mut id_stack = vec![ChainIndex::root()];
while let Some(curr_id) = id_stack.pop() {
if let Some(node) = self.key_map.get(&curr_id)
&& node.value.1 == nssa::Account::default()
&& curr_id != ChainIndex::root()
{
let addr = node.account_id();
self.remove(addr);
}
let mut next_id = curr_id.nth_child(0);
while (next_id.depth()) < depth {
id_stack.push(next_id.clone());
next_id = next_id.next_in_line();
}
}
}
/// Cleanup of non-initialized accounts in a private tree
///
/// If account is default, removes them, stops at first non-default account.
///
/// Walks through tree in lairs of same depth using `ChainIndex::chain_ids_at_depth()`
///
/// Chain must be parsed for accounts beforehand
///
/// Slow, maintains tree consistency.
pub fn cleanup_tree_remove_uninit_layered(&mut self, depth: u32) {
'outer: for i in (1..(depth as usize)).rev() {
println!("Cleanup of tree at depth {i}");
for id in ChainIndex::chain_ids_at_depth(i) {
if let Some(node) = self.key_map.get(&id) {
if node.value.1 == nssa::Account::default() {
let addr = node.account_id();
self.remove(addr);
} else {
break 'outer;
}
}
}
}
}
}
impl KeyTree<ChildKeysPublic> {
/// Cleanup of all non-initialized accounts in a public tree
///
/// For given `depth` checks children to a tree such that their `ChainIndex::depth(&self) <
/// depth`.
///
/// If account is default, removes them.
///
/// Fast, leaves gaps between accounts
pub async fn cleanup_tree_remove_ininit_for_depth(
&mut self,
depth: u32,
client: Arc<SequencerClient>,
) -> Result<()> {
let mut id_stack = vec![ChainIndex::root()];
while let Some(curr_id) = id_stack.pop() {
if let Some(node) = self.key_map.get(&curr_id) {
let address = node.account_id();
let node_acc = client.get_account(address.to_string()).await?.account;
if node_acc == nssa::Account::default() && curr_id != ChainIndex::root() {
self.remove(address);
}
}
let mut next_id = curr_id.nth_child(0);
while (next_id.depth()) < depth {
id_stack.push(next_id.clone());
next_id = next_id.next_in_line();
}
}
Ok(())
}
/// Cleanup of non-initialized accounts in a public tree
///
/// If account is default, removes them, stops at first non-default account.
///
/// Walks through tree in lairs of same depth using `ChainIndex::chain_ids_at_depth()`
///
/// Slow, maintains tree consistency.
pub async fn cleanup_tree_remove_uninit_layered(
&mut self,
depth: u32,
client: Arc<SequencerClient>,
) -> Result<()> {
'outer: for i in (1..(depth as usize)).rev() {
println!("Cleanup of tree at depth {i}");
for id in ChainIndex::chain_ids_at_depth(i) {
if let Some(node) = self.key_map.get(&id) {
let address = node.account_id();
let node_acc = client.get_account(address.to_string()).await?.account;
if node_acc == nssa::Account::default() {
let addr = node.account_id();
self.remove(addr);
} else {
break 'outer;
}
}
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use std::{collections::HashSet, str::FromStr};
use nssa::AccountId;
@ -162,7 +354,7 @@ mod tests {
fn test_small_key_tree() {
let seed_holder = seed_holder_for_tests();
let mut tree = KeyTreePublic::new(&seed_holder);
let mut tree = KeyTreePrivate::new(&seed_holder);
let next_last_child_for_parent_id = tree
.find_next_last_child_of_id(&ChainIndex::root())
@ -170,7 +362,7 @@ mod tests {
assert_eq!(next_last_child_for_parent_id, 0);
tree.generate_new_node(ChainIndex::root()).unwrap();
tree.generate_new_node(&ChainIndex::root()).unwrap();
assert!(
tree.key_map
@ -183,12 +375,12 @@ mod tests {
assert_eq!(next_last_child_for_parent_id, 1);
tree.generate_new_node(ChainIndex::root()).unwrap();
tree.generate_new_node(ChainIndex::root()).unwrap();
tree.generate_new_node(ChainIndex::root()).unwrap();
tree.generate_new_node(ChainIndex::root()).unwrap();
tree.generate_new_node(ChainIndex::root()).unwrap();
tree.generate_new_node(ChainIndex::root()).unwrap();
tree.generate_new_node(&ChainIndex::root()).unwrap();
tree.generate_new_node(&ChainIndex::root()).unwrap();
tree.generate_new_node(&ChainIndex::root()).unwrap();
tree.generate_new_node(&ChainIndex::root()).unwrap();
tree.generate_new_node(&ChainIndex::root()).unwrap();
tree.generate_new_node(&ChainIndex::root()).unwrap();
let next_last_child_for_parent_id = tree
.find_next_last_child_of_id(&ChainIndex::root())
@ -201,7 +393,7 @@ mod tests {
fn test_key_tree_can_not_make_child_keys() {
let seed_holder = seed_holder_for_tests();
let mut tree = KeyTreePublic::new(&seed_holder);
let mut tree = KeyTreePrivate::new(&seed_holder);
let next_last_child_for_parent_id = tree
.find_next_last_child_of_id(&ChainIndex::root())
@ -209,7 +401,7 @@ mod tests {
assert_eq!(next_last_child_for_parent_id, 0);
tree.generate_new_node(ChainIndex::root()).unwrap();
tree.generate_new_node(&ChainIndex::root()).unwrap();
assert!(
tree.key_map
@ -222,7 +414,7 @@ mod tests {
assert_eq!(next_last_child_for_parent_id, 1);
let key_opt = tree.generate_new_node(ChainIndex::from_str("/3").unwrap());
let key_opt = tree.generate_new_node(&ChainIndex::from_str("/3").unwrap());
assert_eq!(key_opt, None);
}
@ -239,7 +431,7 @@ mod tests {
assert_eq!(next_last_child_for_parent_id, 0);
tree.generate_new_node(ChainIndex::root()).unwrap();
tree.generate_new_node(&ChainIndex::root()).unwrap();
assert!(
tree.key_map
@ -252,7 +444,7 @@ mod tests {
assert_eq!(next_last_child_for_parent_id, 1);
tree.generate_new_node(ChainIndex::root()).unwrap();
tree.generate_new_node(&ChainIndex::root()).unwrap();
assert!(
tree.key_map
@ -265,7 +457,7 @@ mod tests {
assert_eq!(next_last_child_for_parent_id, 2);
tree.generate_new_node(ChainIndex::from_str("/0").unwrap())
tree.generate_new_node(&ChainIndex::from_str("/0").unwrap())
.unwrap();
let next_last_child_for_parent_id = tree
@ -279,7 +471,7 @@ mod tests {
.contains_key(&ChainIndex::from_str("/0/0").unwrap())
);
tree.generate_new_node(ChainIndex::from_str("/0").unwrap())
tree.generate_new_node(&ChainIndex::from_str("/0").unwrap())
.unwrap();
let next_last_child_for_parent_id = tree
@ -293,7 +485,7 @@ mod tests {
.contains_key(&ChainIndex::from_str("/0/1").unwrap())
);
tree.generate_new_node(ChainIndex::from_str("/0").unwrap())
tree.generate_new_node(&ChainIndex::from_str("/0").unwrap())
.unwrap();
let next_last_child_for_parent_id = tree
@ -307,7 +499,7 @@ mod tests {
.contains_key(&ChainIndex::from_str("/0/2").unwrap())
);
tree.generate_new_node(ChainIndex::from_str("/0/1").unwrap())
tree.generate_new_node(&ChainIndex::from_str("/0/1").unwrap())
.unwrap();
assert!(
@ -321,4 +513,94 @@ mod tests {
assert_eq!(next_last_child_for_parent_id, 1);
}
#[test]
fn test_tree_balancing_automatic() {
let seed_holder = seed_holder_for_tests();
let mut tree = KeyTreePublic::new(&seed_holder);
for _ in 0..100 {
tree.generate_new_node_layered().unwrap();
}
let next_slot = tree.find_next_slot_layered();
assert_eq!(next_slot, ChainIndex::from_str("/0/0/2/1").unwrap());
}
#[test]
fn test_cleanup() {
let seed_holder = seed_holder_for_tests();
let mut tree = KeyTreePrivate::new(&seed_holder);
tree.generate_tree_for_depth(10);
let acc = tree
.key_map
.get_mut(&ChainIndex::from_str("/1").unwrap())
.unwrap();
acc.value.1.balance = 2;
let acc = tree
.key_map
.get_mut(&ChainIndex::from_str("/2").unwrap())
.unwrap();
acc.value.1.balance = 3;
let acc = tree
.key_map
.get_mut(&ChainIndex::from_str("/0/1").unwrap())
.unwrap();
acc.value.1.balance = 5;
let acc = tree
.key_map
.get_mut(&ChainIndex::from_str("/1/0").unwrap())
.unwrap();
acc.value.1.balance = 6;
tree.cleanup_tree_remove_uninit_layered(10);
let mut key_set_res = HashSet::new();
key_set_res.insert("/0".to_string());
key_set_res.insert("/1".to_string());
key_set_res.insert("/2".to_string());
key_set_res.insert("/".to_string());
key_set_res.insert("/0/0".to_string());
key_set_res.insert("/0/1".to_string());
key_set_res.insert("/1/0".to_string());
let mut key_set = HashSet::new();
for key in tree.key_map.keys() {
key_set.insert(key.to_string());
}
assert_eq!(key_set, key_set_res);
let acc = tree
.key_map
.get(&ChainIndex::from_str("/1").unwrap())
.unwrap();
assert_eq!(acc.value.1.balance, 2);
let acc = tree
.key_map
.get(&ChainIndex::from_str("/2").unwrap())
.unwrap();
assert_eq!(acc.value.1.balance, 3);
let acc = tree
.key_map
.get(&ChainIndex::from_str("/0/1").unwrap())
.unwrap();
assert_eq!(acc.value.1.balance, 5);
let acc = tree
.key_map
.get(&ChainIndex::from_str("/1/0").unwrap())
.unwrap();
assert_eq!(acc.value.1.balance, 6);
}
}

View File

@ -89,9 +89,18 @@ impl NSSAUserData {
/// Returns the account_id of new account
pub fn generate_new_public_transaction_private_key(
&mut self,
parent_cci: ChainIndex,
) -> nssa::AccountId {
self.public_key_tree.generate_new_node(parent_cci).unwrap()
parent_cci: Option<ChainIndex>,
) -> (nssa::AccountId, ChainIndex) {
match parent_cci {
Some(parent_cci) => self
.public_key_tree
.generate_new_node(&parent_cci)
.expect("Parent must be present in a tree"),
None => self
.public_key_tree
.generate_new_node_layered()
.expect("Search for new node slot failed"),
}
}
/// Returns the signing key for public transaction signatures
@ -113,9 +122,18 @@ impl NSSAUserData {
/// Returns the account_id of new account
pub fn generate_new_privacy_preserving_transaction_key_chain(
&mut self,
parent_cci: ChainIndex,
) -> nssa::AccountId {
self.private_key_tree.generate_new_node(parent_cci).unwrap()
parent_cci: Option<ChainIndex>,
) -> (nssa::AccountId, ChainIndex) {
match parent_cci {
Some(parent_cci) => self
.private_key_tree
.generate_new_node(&parent_cci)
.expect("Parent must be present in a tree"),
None => self
.private_key_tree
.generate_new_node_layered()
.expect("Search for new node slot failed"),
}
}
/// Returns the signing key for public transaction signatures
@ -169,16 +187,8 @@ mod tests {
fn test_new_account() {
let mut user_data = NSSAUserData::default();
let account_id_pub =
user_data.generate_new_public_transaction_private_key(ChainIndex::root());
let account_id_private =
user_data.generate_new_privacy_preserving_transaction_key_chain(ChainIndex::root());
let is_private_key_generated = user_data
.get_pub_account_signing_key(&account_id_pub)
.is_some();
assert!(is_private_key_generated);
let (account_id_private, _) = user_data
.generate_new_privacy_preserving_transaction_key_chain(Some(ChainIndex::root()));
let is_key_chain_generated = user_data.get_private_account(&account_id_private).is_some();

View File

@ -6,14 +6,17 @@ edition = "2024"
[dependencies]
risc0-zkvm = { version = "3.0.3", features = ['std'] }
serde = { version = "1.0", default-features = false }
thiserror = { version = "2.0.12", optional = true }
bytemuck = "1.13"
thiserror = { version = "2.0.12" }
bytemuck = { version = "1.13" }
chacha20 = { version = "0.9", default-features = false }
k256 = { version = "0.13.3", optional = true }
base58 = { version = "0.2.0", optional = true }
anyhow = { version = "1.0.98", optional = true }
borsh = "1.5.7"
[dev-dependencies]
serde_json = "1.0.81"
[features]
default = []
host = ["thiserror", "k256", "base58", "anyhow"]
host = ["dep:k256", "dep:base58", "dep:anyhow"]

View File

@ -4,12 +4,14 @@ use std::{fmt::Display, str::FromStr};
#[cfg(feature = "host")]
use base58::{FromBase58, ToBase58};
use borsh::{BorshDeserialize, BorshSerialize};
pub use data::Data;
use serde::{Deserialize, Serialize};
use crate::program::ProgramId;
pub mod data;
pub type Nonce = u128;
pub type Data = Vec<u8>;
/// Account to be used both in public and private contexts
#[derive(
@ -23,8 +25,8 @@ pub struct Account {
pub nonce: Nonce,
}
#[derive(Serialize, Deserialize, Clone, Default, PartialEq)]
#[cfg_attr(any(feature = "host", test), derive(Debug, 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,
@ -41,11 +43,10 @@ impl AccountWithMetadata {
}
}
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Hash, BorshSerialize, BorshDeserialize, Default)]
#[cfg_attr(
any(feature = "host", test),
derive(Debug, Copy, PartialOrd, Ord)
#[derive(
Serialize, Deserialize, Clone, PartialEq, Eq, Hash, BorshSerialize, BorshDeserialize, Default,
)]
#[cfg_attr(any(feature = "host", test), derive(Debug, Copy, PartialOrd, Ord))]
pub struct AccountId {
value: [u8; 32],
}
@ -138,7 +139,10 @@ mod tests {
let account = Account {
program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
balance: 1337,
data: b"testing_account_with_metadata_constructor".to_vec(),
data: b"testing_account_with_metadata_constructor"
.to_vec()
.try_into()
.unwrap(),
nonce: 0xdeadbeef,
};
let fingerprint = AccountId::new([8; 32]);
@ -179,7 +183,7 @@ mod tests {
#[test]
fn default_account_id() {
let default_account_id = AccountId::default();
let expected_account_id = AccountId::new([0;32]);
let expected_account_id = AccountId::new([0; 32]);
assert!(default_account_id == expected_account_id);
}
}

View File

@ -0,0 +1,174 @@
use std::ops::Deref;
use borsh::{BorshDeserialize, BorshSerialize};
use serde::{Deserialize, Serialize};
pub const DATA_MAX_LENGTH_IN_BYTES: usize = 100 * 1024; // 100 KiB
#[derive(Default, Clone, PartialEq, Eq, Serialize, BorshSerialize)]
#[cfg_attr(any(feature = "host", test), derive(Debug))]
pub struct Data(Vec<u8>);
impl Data {
pub fn into_inner(self) -> Vec<u8> {
self.0
}
#[cfg(feature = "host")]
pub fn from_cursor(
cursor: &mut std::io::Cursor<&[u8]>,
) -> Result<Self, crate::error::NssaCoreError> {
use std::io::Read as _;
let mut u32_bytes = [0u8; 4];
cursor.read_exact(&mut u32_bytes)?;
let data_length = u32::from_le_bytes(u32_bytes);
if data_length as usize > DATA_MAX_LENGTH_IN_BYTES {
return Err(
std::io::Error::new(std::io::ErrorKind::InvalidData, DataTooBigError).into(),
);
}
let mut data = vec![0; data_length as usize];
cursor.read_exact(&mut data)?;
Ok(Self(data))
}
}
#[derive(Debug, thiserror::Error, Clone, Copy, PartialEq, Eq)]
#[error("data length exceeds maximum allowed length of {DATA_MAX_LENGTH_IN_BYTES} bytes")]
pub struct DataTooBigError;
impl From<Data> for Vec<u8> {
fn from(data: Data) -> Self {
data.0
}
}
impl TryFrom<Vec<u8>> for Data {
type Error = DataTooBigError;
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
if value.len() > DATA_MAX_LENGTH_IN_BYTES {
Err(DataTooBigError)
} else {
Ok(Self(value))
}
}
}
impl Deref for Data {
type Target = [u8];
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl AsRef<[u8]> for Data {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
impl<'de> Deserialize<'de> for Data {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
/// Data deserialization visitor.
///
/// Compared to a simple deserialization into a `Vec<u8>`, this visitor enforces
/// early length check defined by [`DATA_MAX_LENGTH_IN_BYTES`].
struct DataVisitor;
impl<'de> serde::de::Visitor<'de> for DataVisitor {
type Value = Data;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
formatter,
"a byte array with length not exceeding {} bytes",
DATA_MAX_LENGTH_IN_BYTES
)
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
let mut vec =
Vec::with_capacity(seq.size_hint().unwrap_or(0).min(DATA_MAX_LENGTH_IN_BYTES));
while let Some(value) = seq.next_element()? {
if vec.len() >= DATA_MAX_LENGTH_IN_BYTES {
return Err(serde::de::Error::custom(DataTooBigError));
}
vec.push(value);
}
Ok(Data(vec))
}
}
deserializer.deserialize_seq(DataVisitor)
}
}
impl BorshDeserialize for Data {
fn deserialize_reader<R: std::io::Read>(reader: &mut R) -> std::io::Result<Self> {
// Implementation adapted from `impl BorshDeserialize for Vec<T>`
let len = u32::deserialize_reader(reader)?;
match len {
0 => Ok(Self::default()),
len if len as usize > DATA_MAX_LENGTH_IN_BYTES => Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
DataTooBigError,
)),
len => {
let vec_bytes = u8::vec_from_reader(len, reader)?
.expect("can't be None in current borsh crate implementation");
Ok(Self(vec_bytes))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_data_max_length_allowed() {
let max_vec = vec![0u8; DATA_MAX_LENGTH_IN_BYTES];
let result = Data::try_from(max_vec);
assert!(result.is_ok());
}
#[test]
fn test_data_too_big_error() {
let big_vec = vec![0u8; DATA_MAX_LENGTH_IN_BYTES + 1];
let result = Data::try_from(big_vec);
assert!(matches!(result, Err(DataTooBigError)));
}
#[test]
fn test_borsh_deserialize_exceeding_limit_error() {
let too_big_data = vec![0u8; DATA_MAX_LENGTH_IN_BYTES + 1];
let mut serialized = Vec::new();
<_ as BorshSerialize>::serialize(&too_big_data, &mut serialized).unwrap();
let result = <Data as BorshDeserialize>::deserialize(&mut serialized.as_ref());
assert!(result.is_err());
}
#[test]
fn test_json_deserialize_exceeding_limit_error() {
let data = vec![0u8; DATA_MAX_LENGTH_IN_BYTES + 1];
let json = serde_json::to_string(&data).unwrap();
let result: Result<Data, _> = serde_json::from_str(&json);
assert!(result.is_err());
}
}

View File

@ -10,7 +10,7 @@ use crate::{
#[derive(Serialize, Deserialize)]
pub struct PrivacyPreservingCircuitInput {
pub program_output: ProgramOutput,
pub program_outputs: Vec<ProgramOutput>,
pub visibility_mask: Vec<u8>,
pub private_account_nonces: Vec<Nonce>,
pub private_account_keys: Vec<(NullifierPublicKey, SharedSecretKey)>,
@ -54,7 +54,7 @@ mod tests {
Account {
program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
balance: 12345678901234567890,
data: b"test data".to_vec(),
data: b"test data".to_vec().try_into().unwrap(),
nonce: 18446744073709551614,
},
true,
@ -64,7 +64,7 @@ mod tests {
Account {
program_owner: [9, 9, 9, 8, 8, 8, 7, 7],
balance: 123123123456456567112,
data: b"test data".to_vec(),
data: b"test data".to_vec().try_into().unwrap(),
nonce: 9999999999999999999999,
},
false,
@ -74,7 +74,7 @@ mod tests {
public_post_states: vec![Account {
program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
balance: 100,
data: b"post state data".to_vec(),
data: b"post state data".to_vec().try_into().unwrap(),
nonce: 18446744073709551615,
}],
ciphertexts: vec![Ciphertext(vec![255, 255, 1, 1, 2, 2])],

View File

@ -26,12 +26,14 @@ impl Account {
bytes.extend_from_slice(&self.nonce.to_le_bytes());
let data_length: u32 = self.data.len() as u32;
bytes.extend_from_slice(&data_length.to_le_bytes());
bytes.extend_from_slice(self.data.as_slice());
bytes.extend_from_slice(self.data.as_ref());
bytes
}
#[cfg(feature = "host")]
pub fn from_cursor(cursor: &mut Cursor<&[u8]>) -> Result<Self, NssaCoreError> {
use crate::account::data::Data;
let mut u32_bytes = [0u8; 4];
let mut u128_bytes = [0u8; 16];
@ -51,10 +53,7 @@ impl Account {
let nonce = u128::from_le_bytes(u128_bytes);
// data
cursor.read_exact(&mut u32_bytes)?;
let data_length = u32::from_le_bytes(u32_bytes);
let mut data = vec![0; data_length as usize];
cursor.read_exact(&mut data)?;
let data = Data::from_cursor(cursor)?;
Ok(Self {
program_owner,
@ -149,7 +148,7 @@ mod tests {
program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
balance: 123456789012345678901234567890123456,
nonce: 42,
data: b"hola mundo".to_vec(),
data: b"hola mundo".to_vec().try_into().unwrap(),
};
// program owner || balance || nonce || data_len || data
@ -210,7 +209,7 @@ mod tests {
program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
balance: 123456789012345678901234567890123456,
nonce: 42,
data: b"hola mundo".to_vec(),
data: b"hola mundo".to_vec().try_into().unwrap(),
};
let bytes = account.to_bytes();
let mut cursor = Cursor::new(bytes.as_ref());

View File

@ -1,13 +1,14 @@
use std::collections::HashSet;
use risc0_zkvm::{DeserializeOwned, guest::env, serde::Deserializer};
use serde::{Deserialize, Serialize};
//#[cfg(feature = "host")]
use crate::account::AccountId;
use crate::account::{Account, AccountWithMetadata};
use crate::account::{Account, AccountId, AccountWithMetadata};
pub type ProgramId = [u32; 8];
pub type InstructionData = Vec<u32>;
pub const DEFAULT_PROGRAM_ID: ProgramId = [0; 8];
pub const MAX_NUMBER_CHAINED_CALLS: usize = 10;
pub struct ProgramInput<T> {
pub pre_states: Vec<AccountWithMetadata>,
@ -29,7 +30,6 @@ impl PdaSeed {
}
}
//#[cfg(feature = "host")]
impl From<(&ProgramId, &PdaSeed)> for AccountId {
fn from(value: (&ProgramId, &PdaSeed)) -> Self {
use risc0_zkvm::sha::{Impl, Sha256};
@ -54,7 +54,9 @@ impl From<(&ProgramId, &PdaSeed)> for AccountId {
#[derive(Serialize, Deserialize, Clone, PartialEq)]
#[cfg_attr(any(feature = "host", test), derive(Debug, 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<AccountWithMetadata>,
pub pda_seeds: Vec<PdaSeed>,
@ -111,26 +113,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<AccountWithMetadata>,
pub post_states: Vec<AccountPostState>,
pub chained_calls: Vec<ChainedCall>,
}
pub fn read_nssa_inputs<T: DeserializeOwned>() -> ProgramInput<T> {
pub fn read_nssa_inputs<T: DeserializeOwned>() -> (ProgramInput<T>, InstructionData) {
let pre_states: Vec<AccountWithMetadata> = 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<AccountWithMetadata>,
post_states: Vec<AccountPostState>,
) {
let output = ProgramOutput {
instruction_data,
pre_states,
post_states,
chained_calls: Vec::new(),
@ -139,11 +149,13 @@ pub fn write_nssa_outputs(
}
pub fn write_nssa_outputs_with_chained_call(
instruction_data: InstructionData,
pre_states: Vec<AccountWithMetadata>,
post_states: Vec<AccountPostState>,
chained_calls: Vec<ChainedCall>,
) {
let output = ProgramOutput {
instruction_data,
pre_states,
post_states,
chained_calls,
@ -162,32 +174,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,16 +213,27 @@ 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
let total_balance_pre_states: u128 = pre_states.iter().map(|pre| pre.account.balance).sum();
let total_balance_post_states: u128 = post_states.iter().map(|post| post.account.balance).sum();
// 8. Total balance is preserved
let Some(total_balance_pre_states) =
WrappedBalanceSum::from_balances(pre_states.iter().map(|pre| pre.account.balance))
else {
return false;
};
let Some(total_balance_post_states) =
WrappedBalanceSum::from_balances(post_states.iter().map(|post| post.account.balance))
else {
return false;
};
if total_balance_pre_states != total_balance_post_states {
return false;
}
@ -213,6 +241,44 @@ 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::<HashSet<_>>()
.len();
number_of_accounts == number_of_account_ids
}
/// Representation of a number as `lo + hi * 2^128`.
#[derive(PartialEq, Eq)]
struct WrappedBalanceSum {
lo: u128,
hi: u128,
}
impl WrappedBalanceSum {
/// Constructs a [`WrappedBalanceSum`] from an iterator of balances.
///
/// Returns [`None`] if balance sum overflows `lo + hi * 2^128` representation, which is not
/// expected in practical scenarios.
fn from_balances(balances: impl Iterator<Item = u128>) -> Option<Self> {
let mut wrapped = WrappedBalanceSum { lo: 0, hi: 0 };
for balance in balances {
let (new_sum, did_overflow) = wrapped.lo.overflowing_add(balance);
if did_overflow {
wrapped.hi = wrapped.hi.checked_add(1)?;
}
wrapped.lo = new_sum;
}
Some(wrapped)
}
}
#[cfg(test)]
mod tests {
use super::*;
@ -222,7 +288,7 @@ mod tests {
let account = Account {
program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
balance: 1337,
data: vec![0xde, 0xad, 0xbe, 0xef],
data: vec![0xde, 0xad, 0xbe, 0xef].try_into().unwrap(),
nonce: 10,
};
@ -237,7 +303,7 @@ mod tests {
let account = Account {
program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
balance: 1337,
data: vec![0xde, 0xad, 0xbe, 0xef],
data: vec![0xde, 0xad, 0xbe, 0xef].try_into().unwrap(),
nonce: 10,
};
@ -252,7 +318,7 @@ mod tests {
let mut account = Account {
program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
balance: 1337,
data: vec![0xde, 0xad, 0xbe, 0xef],
data: vec![0xde, 0xad, 0xbe, 0xef].try_into().unwrap(),
nonce: 10,
};

View File

@ -1579,6 +1579,7 @@ dependencies = [
"chacha20",
"risc0-zkvm",
"serde",
"thiserror",
]
[[package]]

View File

@ -209,17 +209,17 @@ impl TokenHolding {
bytes[0] = self.account_type;
bytes[1..33].copy_from_slice(&self.definition_id.to_bytes());
bytes[33..].copy_from_slice(&self.balance.to_le_bytes());
bytes.into()
bytes.to_vec().try_into().expect("Data too big")
}
}
type Instruction = Vec<u8>;
fn main() {
let ProgramInput {
let ( ProgramInput {
pre_states,
instruction,
} = read_nssa_inputs::<Instruction>();
}, instruction_data) = read_nssa_inputs::<Instruction>();
let (post_states, chained_calls) = match instruction[0] {
0 => {
@ -266,7 +266,7 @@ fn main() {
_ => panic!("Invalid instruction"),
};
write_nssa_outputs_with_chained_call(pre_states, post_states, chained_calls);
write_nssa_outputs_with_chained_call(instruction_data, pre_states, post_states, chained_calls);
}
@ -434,7 +434,7 @@ fn new_definition (
active: true,
};
pool_post.data = pool_post_definition.into_data();
pool_post.data = pool_post_definition.into_data().try_into().expect("Data too big");
let pool_post: AccountPostState =
if pool.account == Account::default() { AccountPostState::new_claimed(pool_post.clone()) }
else { AccountPostState::new(pool_post.clone()) };
@ -590,7 +590,7 @@ fn swap(
active: true,
};
pool_post.data = pool_post_definition.into_data();
pool_post.data = pool_post_definition.into_data().try_into().expect("Data too big");
let post_states = vec![
AccountPostState::new(pool_post.clone()),
@ -769,7 +769,7 @@ fn add_liquidity(pre_states: &[AccountWithMetadata],
active: true,
};
pool_post.data = pool_post_definition.into_data();
pool_post.data = pool_post_definition.into_data().try_into().expect("Data too big");
let mut chained_call = Vec::new();
// Chain call for Token A (UserHoldingA -> Vault_A)
@ -925,7 +925,7 @@ fn remove_liquidity(pre_states: &[AccountWithMetadata],
active,
};
pool_post.data = pool_post_definition.into_data();
pool_post.data = pool_post_definition.into_data().try_into().expect("Data too big");
let mut chained_calls = Vec::new();

View File

@ -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<AccountPostState> {
// 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);
}

View File

@ -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::<Instruction>();
let (
ProgramInput {
pre_states,
instruction: solution,
},
instruction_words,
) = read_nssa_inputs::<Instruction>();
let [pinata, winner] = match pre_states.try_into() {
Ok(array) => array,
@ -63,10 +66,15 @@ fn main() {
let mut pinata_post = pinata.account.clone();
let mut winner_post = winner.account.clone();
pinata_post.balance -= PRIZE;
pinata_post.data = data.next_data().to_vec();
pinata_post.data = data
.next_data()
.to_vec()
.try_into()
.expect("33 bytes should fit into Data");
winner_post.balance += PRIZE;
write_nssa_outputs(
instruction_words,
vec![pinata, winner],
vec![
AccountPostState::new(pinata_post),

View File

@ -1,8 +1,14 @@
use nssa_core::program::{
read_nssa_inputs, write_nssa_outputs_with_chained_call, AccountPostState, ChainedCall, PdaSeed, ProgramInput
use nssa_core::{
account::Data,
program::{
AccountPostState, ChainedCall, PdaSeed, ProgramInput, read_nssa_inputs,
write_nssa_outputs_with_chained_call,
},
};
use risc0_zkvm::{
serde::to_vec,
sha::{Impl, Sha256},
};
use risc0_zkvm::serde::to_vec;
use risc0_zkvm::sha::{Impl, Sha256};
const PRIZE: u128 = 150;
@ -35,22 +41,26 @@ impl Challenge {
digest[..difficulty].iter().all(|&b| b == 0)
}
fn next_data(self) -> [u8; 33] {
fn next_data(self) -> Data {
let mut result = [0; 33];
result[0] = self.difficulty;
result[1..].copy_from_slice(Impl::hash_bytes(&self.seed).as_bytes());
result
result.to_vec().try_into().expect("should fit")
}
}
/// A pinata program
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::<Instruction>();
// It is expected to receive three accounts: [pinata_definition, pinata_token_holding,
// winner_token_holding]
let (
ProgramInput {
pre_states,
instruction: solution,
},
instruction_words,
) = read_nssa_inputs::<Instruction>();
let [
pinata_definition,
@ -70,7 +80,7 @@ fn main() {
let mut pinata_definition_post = pinata_definition.account.clone();
let pinata_token_holding_post = pinata_token_holding.account.clone();
let winner_token_holding_post = winner_token_holding.account.clone();
pinata_definition_post.data = data.next_data().to_vec();
pinata_definition_post.data = data.next_data();
let mut instruction_data: [u8; 23] = [0; 23];
instruction_data[0] = 1;
@ -83,11 +93,15 @@ fn main() {
let chained_calls = vec![ChainedCall {
program_id: pinata_token_holding_post.program_owner,
instruction_data: to_vec(&instruction_data).unwrap(),
pre_states: vec![pinata_token_holding_for_chain_call, winner_token_holding.clone()],
pre_states: vec![
pinata_token_holding_for_chain_call,
winner_token_holding.clone(),
],
pda_seeds: vec![PdaSeed::new([0; 32])],
}];
write_nssa_outputs_with_chained_call(
instruction_words,
vec![
pinata_definition,
pinata_token_holding,

View File

@ -1,49 +1,119 @@
use std::collections::HashSet;
use risc0_zkvm::{guest::env, serde::to_vec};
use std::collections::HashMap;
use nssa_core::{
Commitment, CommitmentSetDigest, DUMMY_COMMITMENT_HASH, EncryptionScheme,
Nullifier, NullifierPublicKey, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput,
Commitment, CommitmentSetDigest, DUMMY_COMMITMENT_HASH, EncryptionScheme, Nullifier,
NullifierPublicKey, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput,
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<AccountWithMetadata> = Vec::new();
let mut state_diff: HashMap<AccountId, Account> = 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();
@ -70,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;
@ -126,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 {
@ -175,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::<HashSet<_>>()
.len();
number_of_accounts == number_of_account_ids
}

View File

@ -25,14 +25,16 @@ use nssa_core::{
// * Two accounts: [definition_account, account_to_initialize].
// * An dummy byte string of length 23, with the following layout
// [0x02 || 0x00 || 0x00 || 0x00 || ... || 0x00 || 0x00].
// 4. Burn tokens from a Toking Holding account (thus lowering total supply)
// 4. Burn tokens from a Token Holding account (thus lowering total supply)
// Arguments to this function are:
// * Two accounts: [definition_account, holding_account].
// * Authorization required: holding_account
// * An instruction data byte string of length 23, indicating the balance to burn with the folloiwng layout
// [0x03 || amount (little-endian 16 bytes) || 0x00 || 0x00 || 0x00 || 0x00 || 0x00 || 0x00].
// 5. Mint additional supply of tokens tokens to a Toking Holding account (thus increasing total supply)
// 5. Mint additional supply of tokens tokens to a Token Holding account (thus increasing total supply)
// Arguments to this function are:
// * Two accounts: [definition_account, holding_account].
// * Authorization required: definition_account
// * An instruction data byte string of length 23, indicating the balance to mint with the folloiwng layout
// [0x04 || amount (little-endian 16 bytes) || 0x00 || 0x00 || 0x00 || 0x00 || 0x00 || 0x00].
@ -55,12 +57,15 @@ struct TokenHolding {
}
impl TokenDefinition {
fn into_data(self) -> Vec<u8> {
fn into_data(self) -> Data {
let mut bytes = [0; TOKEN_DEFINITION_DATA_SIZE];
bytes[0] = self.account_type;
bytes[1..7].copy_from_slice(&self.name);
bytes[7..].copy_from_slice(&self.total_supply.to_le_bytes());
bytes.into()
bytes
.to_vec()
.try_into()
.expect("23 bytes should fit into Data")
}
fn parse(data: &[u8]) -> Option<Self> {
@ -120,7 +125,10 @@ impl TokenHolding {
bytes[0] = self.account_type;
bytes[1..33].copy_from_slice(&self.definition_id.to_bytes());
bytes[33..].copy_from_slice(&self.balance.to_le_bytes());
bytes.into()
bytes
.to_vec()
.try_into()
.expect("33 bytes should fit into Data")
}
}
@ -155,7 +163,7 @@ fn transfer(pre_states: &[AccountWithMetadata], balance_to_move: u128) -> Vec<Ac
recipient_holding.balance = recipient_holding
.balance
.checked_add(balance_to_move)
.expect("Recipient balance overflow.");
.expect("Recipient balance overflow");
let sender_post = {
let mut this = sender.account.clone();
@ -282,13 +290,19 @@ fn burn(pre_states: &[AccountWithMetadata], balance_to_burn: u128) -> Vec<Accoun
post_user_holding.data = TokenHolding::into_data(TokenHolding {
account_type: user_values.account_type,
definition_id: user_values.definition_id,
balance: user_values.balance - balance_to_burn,
balance: user_values
.balance
.checked_sub(balance_to_burn)
.expect("Checked above"),
});
post_definition.data = TokenDefinition::into_data(TokenDefinition {
account_type: definition_values.account_type,
name: definition_values.name,
total_supply: definition_values.total_supply - balance_to_burn,
total_supply: definition_values
.total_supply
.checked_sub(balance_to_burn)
.expect("Total supply underflow"),
});
vec![
@ -315,9 +329,6 @@ fn mint_additional_supply(
let definition_values =
TokenDefinition::parse(&definition.account.data).expect("Definition account must be valid");
//TODO: add overflow protection
// TokenDefinition.supply_limit + amount_to_mint
let token_holding_values: TokenHolding = if token_holding.account == Account::default() {
TokenHolding::new(&definition.account_id)
} else {
@ -331,13 +342,21 @@ fn mint_additional_supply(
let token_holding_post_data = TokenHolding {
account_type: token_holding_values.account_type,
definition_id: token_holding_values.definition_id,
balance: token_holding_values.balance + amount_to_mint,
balance: token_holding_values
.balance
.checked_add(amount_to_mint)
.expect("New balance overflow"),
};
let post_total_supply = definition_values
.total_supply
.checked_add(amount_to_mint)
.expect("Total supply overflow");
let post_definition_data = TokenDefinition {
account_type: definition_values.account_type,
name: definition_values.name,
total_supply: definition_values.total_supply + amount_to_mint,
total_supply: post_total_supply,
};
let post_definition = {
@ -363,10 +382,13 @@ fn mint_additional_supply(
type Instruction = [u8; 23];
fn main() {
let ProgramInput {
pre_states,
instruction,
} = read_nssa_inputs::<Instruction>();
let (
ProgramInput {
pre_states,
instruction,
},
instruction_words,
) = read_nssa_inputs::<Instruction>();
let post_states = match instruction[0] {
0 => {
@ -437,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)]
@ -549,15 +571,15 @@ mod tests {
let post_states = new_definition(&pre_states, [0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe], 10);
let [definition_account, holding_account] = post_states.try_into().ok().unwrap();
assert_eq!(
definition_account.account().data,
vec![
definition_account.account().data.as_ref(),
&[
0, 0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0
]
);
assert_eq!(
holding_account.account().data,
vec![
holding_account.account().data.as_ref(),
&[
1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22,
23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0
@ -607,7 +629,9 @@ mod tests {
AccountWithMetadata {
account: Account {
// First byte should be `TOKEN_HOLDING_TYPE` for token holding accounts
data: vec![invalid_type; TOKEN_HOLDING_DATA_SIZE],
data: vec![invalid_type; TOKEN_HOLDING_DATA_SIZE]
.try_into()
.unwrap(),
..Account::default()
},
is_authorized: true,
@ -629,7 +653,7 @@ mod tests {
AccountWithMetadata {
account: Account {
// Data must be of exact length `TOKEN_HOLDING_DATA_SIZE`
data: vec![1; TOKEN_HOLDING_DATA_SIZE - 1],
data: vec![1; TOKEN_HOLDING_DATA_SIZE - 1].try_into().unwrap(),
..Account::default()
},
is_authorized: true,
@ -651,7 +675,7 @@ mod tests {
AccountWithMetadata {
account: Account {
// Data must be of exact length `TOKEN_HOLDING_DATA_SIZE`
data: vec![1; TOKEN_HOLDING_DATA_SIZE + 1],
data: vec![1; TOKEN_HOLDING_DATA_SIZE + 1].try_into().unwrap(),
..Account::default()
},
is_authorized: true,
@ -672,7 +696,7 @@ mod tests {
let pre_states = vec![
AccountWithMetadata {
account: Account {
data: vec![1; TOKEN_HOLDING_DATA_SIZE],
data: vec![1; TOKEN_HOLDING_DATA_SIZE].try_into().unwrap(),
..Account::default()
},
is_authorized: true,
@ -680,10 +704,12 @@ mod tests {
},
AccountWithMetadata {
account: Account {
data: vec![1]
data: [1]
.into_iter()
.chain(vec![2; TOKEN_HOLDING_DATA_SIZE - 1])
.collect(),
.collect::<Vec<_>>()
.try_into()
.unwrap(),
..Account::default()
},
is_authorized: true,
@ -700,10 +726,12 @@ mod tests {
AccountWithMetadata {
account: Account {
// Account with balance 37
data: vec![1; TOKEN_HOLDING_DATA_SIZE - 16]
data: [1; TOKEN_HOLDING_DATA_SIZE - 16]
.into_iter()
.chain(u128::to_le_bytes(37))
.collect(),
.collect::<Vec<_>>()
.try_into()
.unwrap(),
..Account::default()
},
is_authorized: true,
@ -711,7 +739,7 @@ mod tests {
},
AccountWithMetadata {
account: Account {
data: vec![1; TOKEN_HOLDING_DATA_SIZE],
data: vec![1; TOKEN_HOLDING_DATA_SIZE].try_into().unwrap(),
..Account::default()
},
is_authorized: true,
@ -729,10 +757,12 @@ mod tests {
AccountWithMetadata {
account: Account {
// Account with balance 37
data: vec![1; TOKEN_HOLDING_DATA_SIZE - 16]
data: [1; TOKEN_HOLDING_DATA_SIZE - 16]
.into_iter()
.chain(u128::to_le_bytes(37))
.collect(),
.collect::<Vec<_>>()
.try_into()
.unwrap(),
..Account::default()
},
is_authorized: false,
@ -740,7 +770,7 @@ mod tests {
},
AccountWithMetadata {
account: Account {
data: vec![1; TOKEN_HOLDING_DATA_SIZE],
data: vec![1; TOKEN_HOLDING_DATA_SIZE].try_into().unwrap(),
..Account::default()
},
is_authorized: true,
@ -756,10 +786,12 @@ mod tests {
AccountWithMetadata {
account: Account {
// Account with balance 37
data: vec![1; TOKEN_HOLDING_DATA_SIZE - 16]
data: [1; TOKEN_HOLDING_DATA_SIZE - 16]
.into_iter()
.chain(u128::to_le_bytes(37))
.collect(),
.collect::<Vec<_>>()
.try_into()
.unwrap(),
..Account::default()
},
is_authorized: true,
@ -768,10 +800,12 @@ mod tests {
AccountWithMetadata {
account: Account {
// Account with balance 255
data: vec![1; TOKEN_HOLDING_DATA_SIZE - 16]
data: [1; TOKEN_HOLDING_DATA_SIZE - 16]
.into_iter()
.chain(u128::to_le_bytes(255))
.collect(),
.collect::<Vec<_>>()
.try_into()
.unwrap(),
..Account::default()
},
is_authorized: true,
@ -781,15 +815,15 @@ mod tests {
let post_states = transfer(&pre_states, 11);
let [sender_post, recipient_post] = post_states.try_into().ok().unwrap();
assert_eq!(
sender_post.account().data,
vec![
sender_post.account().data.as_ref(),
[
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]
);
assert_eq!(
recipient_post.account().data,
vec![
recipient_post.account().data.as_ref(),
[
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 10, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]
@ -805,7 +839,9 @@ mod tests {
data: [0; TOKEN_DEFINITION_DATA_SIZE - 16]
.into_iter()
.chain(u128::to_le_bytes(1000))
.collect(),
.collect::<Vec<_>>()
.try_into()
.unwrap(),
..Account::default()
},
is_authorized: false,
@ -819,10 +855,13 @@ mod tests {
];
let post_states = initialize_account(&pre_states);
let [definition, holding] = post_states.try_into().ok().unwrap();
assert_eq!(definition.account().data, pre_states[0].account.data);
assert_eq!(
holding.account().data,
vec![
definition.account().data.as_ref(),
pre_states[0].account.data.as_ref()
);
assert_eq!(
holding.account().data.as_ref(),
[
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]
@ -839,6 +878,7 @@ mod tests {
MintSuccess,
InitSupplyMint,
HoldingBalanceMint,
MintOverflow,
}
enum AccountsEnum {
@ -847,12 +887,14 @@ mod tests {
HoldingDiffDef,
HoldingSameDefAuth,
HoldingSameDefNotAuth,
HoldingSameDefNotAuthOverflow,
DefinitionAccountPostBurn,
HoldingAccountPostBurn,
Uninit,
InitMint,
DefinitionAccountMint,
HoldingSameDefMint,
HoldingSameDefAuthLargeBalance,
}
enum IdEnum {
@ -933,6 +975,20 @@ mod tests {
is_authorized: false,
account_id: helper_id_constructor(IdEnum::HoldingId),
},
AccountsEnum::HoldingSameDefNotAuthOverflow => AccountWithMetadata {
account: Account {
program_owner: [5u32; 8],
balance: 0u128,
data: TokenHolding::into_data(TokenHolding {
account_type: TOKEN_HOLDING_TYPE,
definition_id: helper_id_constructor(IdEnum::PoolDefinitionId),
balance: helper_balance_constructor(BalanceEnum::InitSupply),
}),
nonce: 0,
},
is_authorized: false,
account_id: helper_id_constructor(IdEnum::HoldingId),
},
AccountsEnum::DefinitionAccountPostBurn => AccountWithMetadata {
account: Account {
program_owner: [5u32; 8],
@ -1008,6 +1064,20 @@ mod tests {
is_authorized: true,
account_id: helper_id_constructor(IdEnum::PoolDefinitionId),
},
AccountsEnum::HoldingSameDefAuthLargeBalance => AccountWithMetadata {
account: Account {
program_owner: [5u32; 8],
balance: 0u128,
data: TokenHolding::into_data(TokenHolding {
account_type: TOKEN_HOLDING_TYPE,
definition_id: helper_id_constructor(IdEnum::PoolDefinitionId),
balance: helper_balance_constructor(BalanceEnum::MintOverflow),
}),
nonce: 0,
},
is_authorized: true,
account_id: helper_id_constructor(IdEnum::PoolDefinitionId),
},
_ => panic!("Invalid selection"),
}
}
@ -1023,6 +1093,7 @@ mod tests {
BalanceEnum::MintSuccess => 50_000,
BalanceEnum::InitSupplyMint => 150_000,
BalanceEnum::HoldingBalanceMint => 51_000,
BalanceEnum::MintOverflow => (2 as u128).pow(128) - 40_000,
_ => panic!("Invalid selection"),
}
}
@ -1086,6 +1157,19 @@ mod tests {
);
}
#[test]
#[should_panic(expected = "Total supply underflow")]
fn test_burn_total_supply_underflow() {
let pre_states = vec![
helper_account_constructor(AccountsEnum::DefinitionAccountAuth),
helper_account_constructor(AccountsEnum::HoldingSameDefAuthLargeBalance),
];
let _post_states = burn(
&pre_states,
helper_balance_constructor(BalanceEnum::MintOverflow),
);
}
#[test]
fn test_burn_success() {
let pre_states = vec![
@ -1208,4 +1292,30 @@ mod tests {
);
assert!(holding_post.requires_claim() == true);
}
#[test]
#[should_panic(expected = "Total supply overflow")]
fn test_mint_total_supply_overflow() {
let pre_states = vec![
helper_account_constructor(AccountsEnum::DefinitionAccountAuth),
helper_account_constructor(AccountsEnum::HoldingSameDefNotAuth),
];
let _post_states = mint_additional_supply(
&pre_states,
helper_balance_constructor(BalanceEnum::MintOverflow),
);
}
#[test]
#[should_panic(expected = "New balance overflow")]
fn test_mint_holding_account_overflow() {
let pre_states = vec![
helper_account_constructor(AccountsEnum::DefinitionAccountAuth),
helper_account_constructor(AccountsEnum::HoldingSameDefNotAuthOverflow),
];
let _post_states = mint_additional_supply(
&pre_states,
helper_balance_constructor(BalanceEnum::MintOverflow),
);
}
}

View File

@ -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,
};

View File

@ -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<u8>);
#[derive(Clone)]
pub struct ProgramWithDependencies {
pub program: Program,
// TODO: avoid having a copy of the bytecode of each dependency.
pub dependencies: HashMap<ProgramId, Program>,
}
impl ProgramWithDependencies {
pub fn new(program: Program, dependencies: HashMap<ProgramId, Program>) -> Self {
Self {
program,
dependencies,
}
}
}
impl From<Program> 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();
@ -95,7 +157,7 @@ impl Proof {
mod tests {
use nssa_core::{
Commitment, DUMMY_COMMITMENT_HASH, EncryptionScheme, Nullifier,
account::{Account, AccountId, AccountWithMetadata},
account::{Account, AccountId, AccountWithMetadata, data::Data},
};
use super::*;
@ -133,15 +195,15 @@ mod tests {
let expected_sender_post = Account {
program_owner: program.id(),
balance: 100 - balance_to_move,
nonce: 1,
data: vec![],
nonce: 0,
data: Data::default(),
};
let expected_recipient_post = Account {
program_owner: program.id(),
balance: balance_to_move,
nonce: 0xdeadbeef,
data: vec![],
data: Data::default(),
};
let expected_sender_pre = sender.clone();
@ -156,7 +218,7 @@ mod tests {
&[0xdeadbeef],
&[(recipient_keys.npk(), shared_secret.clone())],
&[],
&Program::authenticated_transfer_program(),
&Program::authenticated_transfer_program().into(),
)
.unwrap();
@ -191,7 +253,7 @@ mod tests {
balance: 100,
nonce: 0xdeadbeef,
program_owner: program.id(),
data: vec![],
data: Data::default(),
},
true,
AccountId::from(&sender_keys.npk()),
@ -257,7 +319,7 @@ mod tests {
sender_keys.nsk,
commitment_set.get_proof_for(&commitment_sender).unwrap(),
)],
&program,
&program.into(),
)
.unwrap();

View File

@ -4,4 +4,6 @@ pub mod witness_set;
pub mod circuit;
pub use message::Message;
pub use transaction::PrivacyPreservingTransaction;
pub use witness_set::WitnessSet;

View File

@ -7,14 +7,14 @@ use serde::Serialize;
use crate::{
error::NssaError,
program_methods::{AUTHENTICATED_TRANSFER_ELF, PINATA_ELF, TOKEN_ELF, AMM_ELF},
program_methods::{AMM_ELF, AUTHENTICATED_TRANSFER_ELF, PINATA_ELF, TOKEN_ELF},
};
/// Maximum number of cycles for a public execution.
/// 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<u8>,
@ -227,6 +227,13 @@ mod tests {
elf: CLAIMER_ELF.to_vec(),
}
}
pub fn modified_transfer_program() -> Self {
use test_program_methods::MODIFIED_TRANSFER_ELF;
// This unwrap won't panic since the `MODIFIED_TRANSFER_ELF` comes from risc0 build of
// `program_methods`
Self::new(MODIFIED_TRANSFER_ELF.to_vec()).unwrap()
}
}
#[test]

View File

@ -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(())
}
@ -234,7 +240,7 @@ impl V02State {
program_owner: Program::pinata().id(),
balance: 1500,
// Difficulty: 3
data: vec![3; 33],
data: vec![3; 33].try_into().expect("should fit"),
nonce: 0,
},
);
@ -248,7 +254,7 @@ impl V02State {
Account {
program_owner: Program::pinata_token().id(),
// Difficulty: 3
data: vec![3; 33],
data: vec![3; 33].try_into().expect("should fit"),
..Account::default()
},
);
@ -258,12 +264,11 @@ impl V02State {
#[cfg(test)]
pub mod tests {
use serde::Serialize;
use std::collections::HashMap;
use nssa_core::{
Commitment, Nullifier, NullifierPublicKey, NullifierSecretKey, SharedSecretKey,
account::{Account, AccountId, AccountWithMetadata, Data, Nonce},
account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data},
encryption::{EphemeralPublicKey, IncomingViewingPublicKey, Scalar},
program::{PdaSeed, ProgramId},
};
@ -273,10 +278,13 @@ 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,
program_methods, public_transaction,
public_transaction,
signature::PrivateKey,
state::MAX_NUMBER_CHAINED_CALLS,
};
@ -507,7 +515,7 @@ pub mod tests {
..Account::default()
};
let account_with_default_values_except_data = Account {
data: vec![0xca, 0xfe],
data: vec![0xca, 0xfe].try_into().unwrap(),
..Account::default()
};
self.force_insert_account(
@ -732,7 +740,8 @@ pub mod tests {
program_id
);
let message =
public_transaction::Message::try_new(program_id, vec![account_id], vec![], ()).unwrap();
public_transaction::Message::try_new(program_id, vec![account_id], vec![], vec![0])
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
let tx = PublicTransaction::new(message, witness_set);
@ -860,7 +869,7 @@ pub mod tests {
&[0xdeadbeef],
&[(recipient_keys.npk(), shared_secret)],
&[],
&Program::authenticated_transfer_program(),
&Program::authenticated_transfer_program().into(),
)
.unwrap();
@ -912,7 +921,7 @@ pub mod tests {
sender_keys.nsk,
state.get_proof_for_commitment(&sender_commitment).unwrap(),
)],
&program,
&program.into(),
)
.unwrap();
@ -964,7 +973,7 @@ pub mod tests {
sender_keys.nsk,
state.get_proof_for_commitment(&sender_commitment).unwrap(),
)],
&program,
&program.into(),
)
.unwrap();
@ -1029,7 +1038,7 @@ pub mod tests {
program_owner: Program::authenticated_transfer_program().id(),
balance: 100,
nonce: 0xdeadbeef,
data: vec![],
data: Data::default(),
};
let recipient_keys = test_private_account_keys_2();
@ -1053,7 +1062,7 @@ pub mod tests {
program_owner: Program::authenticated_transfer_program().id(),
nonce: 0xcafecafe,
balance: sender_private_account.balance - balance_to_move,
data: vec![],
data: Data::default(),
},
);
@ -1095,7 +1104,7 @@ pub mod tests {
program_owner: Program::authenticated_transfer_program().id(),
balance: 100,
nonce: 0xdeadbeef,
data: vec![],
data: Data::default(),
};
let recipient_keys = test_public_account_keys_1();
let recipient_initial_balance = 400;
@ -1128,7 +1137,7 @@ pub mod tests {
program_owner: Program::authenticated_transfer_program().id(),
nonce: 0xcafecafe,
balance: sender_private_account.balance - balance_to_move,
data: vec![],
data: Data::default(),
},
);
@ -1177,7 +1186,7 @@ pub mod tests {
&[],
&[],
&[],
&program,
&program.into(),
);
assert!(matches!(result, Err(NssaError::CircuitProvingError(_))));
@ -1203,7 +1212,7 @@ pub mod tests {
&[],
&[],
&[],
&program,
&program.into(),
);
assert!(matches!(result, Err(NssaError::CircuitProvingError(_))));
@ -1229,7 +1238,7 @@ pub mod tests {
&[],
&[],
&[],
&program,
&program.into(),
);
assert!(matches!(result, Err(NssaError::CircuitProvingError(_))));
@ -1250,17 +1259,45 @@ pub mod tests {
let result = execute_and_prove(
&[public_account],
&Program::serialize_instruction(()).unwrap(),
&Program::serialize_instruction(vec![0]).unwrap(),
&[0],
&[],
&[],
&[],
&program,
&program.into(),
);
assert!(matches!(result, Err(NssaError::CircuitProvingError(_))));
}
#[test]
fn test_data_changer_program_should_fail_for_too_large_data_in_privacy_preserving_circuit() {
let program = Program::data_changer();
let public_account = AccountWithMetadata::new(
Account {
program_owner: program.id(),
balance: 0,
..Account::default()
},
true,
AccountId::new([0; 32]),
);
let large_data: Vec<u8> = vec![0; nssa_core::account::data::DATA_MAX_LENGTH_IN_BYTES + 1];
let result = execute_and_prove(
&[public_account],
&Program::serialize_instruction(large_data).unwrap(),
&[0],
&[],
&[],
&[],
&program.to_owned().into(),
);
assert!(matches!(result, Err(NssaError::ProgramProveFailed(_))));
}
#[test]
fn test_extra_output_program_should_fail_in_privacy_preserving_circuit() {
let program = Program::extra_output_program();
@ -1281,7 +1318,7 @@ pub mod tests {
&[],
&[],
&[],
&program,
&program.into(),
);
assert!(matches!(result, Err(NssaError::CircuitProvingError(_))));
@ -1316,7 +1353,7 @@ pub mod tests {
&[],
&[],
&[],
&program,
&program.into(),
);
assert!(matches!(result, Err(NssaError::CircuitProvingError(_))));
@ -1342,7 +1379,7 @@ pub mod tests {
&[],
&[],
&[],
&program,
&program.into(),
);
assert!(matches!(result, Err(NssaError::CircuitProvingError(_))));
@ -1377,7 +1414,7 @@ pub mod tests {
&[],
&[],
&[],
&program,
&program.into(),
);
assert!(matches!(result, Err(NssaError::CircuitProvingError(_))));
@ -1414,7 +1451,7 @@ pub mod tests {
&[],
&[],
&[],
&program,
&program.into(),
);
assert!(matches!(result, Err(NssaError::CircuitProvingError(_))));
@ -1455,7 +1492,7 @@ pub mod tests {
),
],
&[(sender_keys.nsk, (0, vec![]))],
&program,
&program.into(),
);
assert!(matches!(result, Err(NssaError::CircuitProvingError(_))));
@ -1489,7 +1526,7 @@ pub mod tests {
&[0xdeadbeef1, 0xdeadbeef2],
&private_account_keys,
&[(sender_keys.nsk, (0, vec![]))],
&program,
&program.into(),
);
assert!(matches!(result, Err(NssaError::CircuitProvingError(_))));
@ -1530,7 +1567,7 @@ pub mod tests {
),
],
&private_account_auth,
&program,
&program.into(),
);
assert!(matches!(result, Err(NssaError::CircuitProvingError(_))));
@ -1578,7 +1615,7 @@ pub mod tests {
&[0xdeadbeef1, 0xdeadbeef2],
&private_account_keys,
&private_account_auth,
&program,
&program.into(),
);
assert!(matches!(result, Err(NssaError::CircuitProvingError(_))));
@ -1624,7 +1661,7 @@ pub mod tests {
),
],
&[(sender_keys.nsk, (0, vec![]))],
&program,
&program.into(),
);
assert!(matches!(result, Err(NssaError::CircuitProvingError(_))));
@ -1671,7 +1708,7 @@ pub mod tests {
),
],
&[(sender_keys.nsk, (0, vec![]))],
&program,
&program.into(),
);
assert!(matches!(result, Err(NssaError::CircuitProvingError(_))));
@ -1694,7 +1731,7 @@ pub mod tests {
let private_account_2 = AccountWithMetadata::new(
Account {
// Non default data
data: b"hola mundo".to_vec(),
data: b"hola mundo".to_vec().try_into().unwrap(),
..Account::default()
},
false,
@ -1717,7 +1754,7 @@ pub mod tests {
),
],
&[(sender_keys.nsk, (0, vec![]))],
&program,
&program.into(),
);
assert!(matches!(result, Err(NssaError::CircuitProvingError(_))));
@ -1763,7 +1800,7 @@ pub mod tests {
),
],
&[(sender_keys.nsk, (0, vec![]))],
&program,
&program.into(),
);
assert!(matches!(result, Err(NssaError::CircuitProvingError(_))));
@ -1807,7 +1844,7 @@ pub mod tests {
),
],
&[(sender_keys.nsk, (0, vec![]))],
&program,
&program.into(),
);
assert!(matches!(result, Err(NssaError::CircuitProvingError(_))));
@ -1836,7 +1873,7 @@ pub mod tests {
&[],
&[],
&[],
&program,
&program.into(),
);
assert!(matches!(result, Err(NssaError::CircuitProvingError(_))));
@ -1878,7 +1915,7 @@ pub mod tests {
),
],
&[(sender_keys.nsk, (0, vec![]))],
&program,
&program.into(),
);
assert!(matches!(result, Err(NssaError::CircuitProvingError(_))));
@ -1924,7 +1961,7 @@ pub mod tests {
&[0xdeadbeef1, 0xdeadbeef2],
&private_account_keys,
&[(sender_keys.nsk, (0, vec![]))],
&program,
&program.into(),
);
assert!(matches!(result, Err(NssaError::CircuitProvingError(_))));
@ -1970,7 +2007,7 @@ pub mod tests {
),
],
&private_account_auth,
&program,
&program.into(),
);
assert!(matches!(result, Err(NssaError::CircuitProvingError(_))));
@ -1983,7 +2020,7 @@ pub mod tests {
program_owner: Program::authenticated_transfer_program().id(),
balance: 100,
nonce: 0xdeadbeef,
data: vec![],
data: Data::default(),
};
let recipient_keys = test_private_account_keys_2();
@ -2009,7 +2046,7 @@ pub mod tests {
program_owner: Program::authenticated_transfer_program().id(),
balance: 100 - balance_to_move,
nonce: 0xcafecafe,
data: vec![],
data: Data::default(),
};
let tx = private_balance_transfer_for_tests(
@ -2061,7 +2098,7 @@ pub mod tests {
(sender_keys.npk(), shared_secret),
],
&private_account_auth,
&program,
&program.into(),
);
assert!(matches!(result, Err(NssaError::CircuitProvingError(_))));
@ -2104,7 +2141,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));
@ -2185,11 +2222,11 @@ pub mod tests {
));
}
//TODO: repeated code needs to be cleaned up
//from token.rs (also repeated in amm.rs)
const TOKEN_DEFINITION_TYPE: u8 = 0;
// TODO: repeated code needs to be cleaned up
// from token.rs (also repeated in amm.rs)
const TOKEN_DEFINITION_DATA_SIZE: usize = 23;
#[allow(unused)]
const TOKEN_HOLDING_TYPE: u8 = 1;
const TOKEN_HOLDING_DATA_SIZE: usize = 49;
@ -2216,39 +2253,16 @@ pub mod tests {
}
impl TokenHolding {
fn new(definition_id: &AccountId) -> Self {
Self {
account_type: TOKEN_HOLDING_TYPE,
definition_id: definition_id.clone(),
balance: 0,
}
}
fn parse(data: &[u8]) -> Option<Self> {
if data.len() != TOKEN_HOLDING_DATA_SIZE || data[0] != TOKEN_HOLDING_TYPE {
None
} else {
let account_type = data[0];
let definition_id = AccountId::new(data[1..33].try_into().unwrap());
let balance = u128::from_le_bytes(data[33..].try_into().unwrap());
Some(Self {
definition_id,
balance,
account_type,
})
}
}
fn into_data(self) -> Data {
let mut bytes = [0; TOKEN_HOLDING_DATA_SIZE];
bytes[0] = self.account_type;
bytes[1..33].copy_from_slice(&self.definition_id.to_bytes());
bytes[33..].copy_from_slice(&self.balance.to_le_bytes());
bytes.into()
bytes.to_vec().try_into().expect("Data too big")
}
}
//TODO repeated code should ultimately be removed;
// TODO repeated code should ultimately be removed;
fn compute_pool_pda(
amm_program_id: ProgramId,
definition_token_a_id: AccountId,
@ -2372,52 +2386,6 @@ pub mod tests {
bytes[224] = self.active as u8;
bytes.into()
}
fn parse(data: &[u8]) -> Option<Self> {
if data.len() != POOL_DEFINITION_DATA_SIZE {
None
} else {
let definition_token_a_id = AccountId::new(data[0..32].try_into().expect("Parse data: The AMM program must be provided a valid AccountId for Token A definition"));
let definition_token_b_id = AccountId::new(data[32..64].try_into().expect("Parse data: The AMM program must be provided a valid AccountId for Vault B definition"));
let vault_a_addr = AccountId::new(data[64..96].try_into().expect(
"Parse data: The AMM program must be provided a valid AccountId for Vault A",
));
let vault_b_addr = AccountId::new(data[96..128].try_into().expect(
"Parse data: The AMM program must be provided a valid AccountId for Vault B",
));
let liquidity_pool_id = AccountId::new(data[128..160].try_into().expect("Parse data: The AMM program must be provided a valid AccountId for Token liquidity pool definition"));
let liquidity_pool_supply = u128::from_le_bytes(data[160..176].try_into().expect(
"Parse data: The AMM program must be provided a valid u128 for liquidity cap",
));
let reserve_a = u128::from_le_bytes(data[176..192].try_into().expect("Parse data: The AMM program must be provided a valid u128 for reserve A balance"));
let reserve_b = u128::from_le_bytes(data[192..208].try_into().expect("Parse data: The AMM program must be provided a valid u128 for reserve B balance"));
let fees =
u128::from_le_bytes(data[208..224].try_into().expect(
"Parse data: The AMM program must be provided a valid u128 for fees",
));
let active = match data[224] {
0 => false,
1 => true,
_ => panic!(
"Parse data: The AMM program must be provided a valid bool for active"
),
};
Some(Self {
definition_token_a_id,
definition_token_b_id,
vault_a_addr,
vault_b_addr,
liquidity_pool_id,
liquidity_pool_supply,
reserve_a,
reserve_b,
fees,
active,
})
}
}
}
enum AccountsEnum {
@ -2567,7 +2535,6 @@ pub mod tests {
BalancesEnum::TokenLPSupplyRemove => 4_000,
BalancesEnum::UserTokenAHoldingNewDef => 5_000,
BalancesEnum::UserTokenBHoldingNewDef => 7_500,
_ => panic!("Invalid selection"),
}
}
@ -2582,7 +2549,6 @@ pub mod tests {
PrivateKeysEnum::UserTokenLPKey => {
PrivateKey::try_new([33; 32]).expect("Keys constructor expects valid private key")
}
_ => panic!("Invalid selection"),
}
}
@ -2618,7 +2584,6 @@ pub mod tests {
IdEnum::UserTokenLPId => AccountId::from(&PublicKey::new_from_private_key(
&helper_private_keys_constructor(PrivateKeysEnum::UserTokenLPKey),
)),
_ => panic!("Invalid selection"),
}
}
@ -2660,7 +2625,9 @@ pub mod tests {
reserve_b: helper_balances_constructor(BalancesEnum::VaultBBalanceInit),
fees: 0u128,
active: true,
}),
})
.try_into()
.expect("Data too big"),
nonce: 0,
},
AccountsEnum::TokenADefinitionAcc => Account {
@ -2670,7 +2637,9 @@ pub mod tests {
account_type: 0u8,
name: [1u8; 6],
total_supply: helper_balances_constructor(BalancesEnum::TokenASupply),
}),
})
.try_into()
.expect("Data too big"),
nonce: 0,
},
AccountsEnum::TokenBDefinitionAcc => Account {
@ -2680,7 +2649,9 @@ pub mod tests {
account_type: 0u8,
name: [1u8; 6],
total_supply: helper_balances_constructor(BalancesEnum::TokenBSupply),
}),
})
.try_into()
.expect("Data too big"),
nonce: 0,
},
AccountsEnum::TokenLPDefinitionAcc => Account {
@ -2690,7 +2661,9 @@ pub mod tests {
account_type: 0u8,
name: [1u8; 6],
total_supply: helper_balances_constructor(BalancesEnum::TokenLPSupply),
}),
})
.try_into()
.expect("Data too big"),
nonce: 0,
},
AccountsEnum::VaultAInit => Account {
@ -2759,7 +2732,9 @@ pub mod tests {
reserve_b: helper_balances_constructor(BalancesEnum::VaultBBalanceSwap1),
fees: 0u128,
active: true,
}),
})
.try_into()
.expect("Data too big"),
nonce: 0,
},
AccountsEnum::UserTokenAHoldingSwap1 => Account {
@ -2818,7 +2793,9 @@ pub mod tests {
reserve_b: helper_balances_constructor(BalancesEnum::VaultBBalanceSwap2),
fees: 0u128,
active: true,
}),
})
.try_into()
.expect("Data too big"),
nonce: 0,
},
AccountsEnum::UserTokenAHoldingSwap2 => Account {
@ -2877,7 +2854,9 @@ pub mod tests {
reserve_b: helper_balances_constructor(BalancesEnum::VaultBBalanceAdd),
fees: 0u128,
active: true,
}),
})
.try_into()
.expect("Data too big"),
nonce: 0,
},
AccountsEnum::UserTokenAHoldingAdd => Account {
@ -2917,7 +2896,9 @@ pub mod tests {
account_type: 0u8,
name: [1u8; 6],
total_supply: helper_balances_constructor(BalancesEnum::TokenLPSupplyAdd),
}),
})
.try_into()
.expect("Data too big"),
nonce: 0,
},
AccountsEnum::VaultARemove => Account {
@ -2956,7 +2937,9 @@ pub mod tests {
reserve_b: helper_balances_constructor(BalancesEnum::VaultBBalanceRemove),
fees: 0u128,
active: true,
}),
})
.try_into()
.expect("Data too big"),
nonce: 0,
},
AccountsEnum::UserTokenAHoldingRemove => Account {
@ -2996,7 +2979,9 @@ pub mod tests {
account_type: 0u8,
name: [1u8; 6],
total_supply: helper_balances_constructor(BalancesEnum::TokenLPSupplyRemove),
}),
})
.try_into()
.expect("Data too big"),
nonce: 0,
},
AccountsEnum::TokenLPDefinitionInitInactive => Account {
@ -3006,7 +2991,9 @@ pub mod tests {
account_type: 0u8,
name: [1u8; 6],
total_supply: 0,
}),
})
.try_into()
.expect("Data too big"),
nonce: 0,
},
AccountsEnum::VaultAInitInactive => Account {
@ -3043,7 +3030,9 @@ pub mod tests {
reserve_b: 0,
fees: 0u128,
active: false,
}),
})
.try_into()
.expect("Data too big"),
nonce: 0,
},
AccountsEnum::UserTokenAHoldingNewInit => Account {
@ -3083,7 +3072,9 @@ pub mod tests {
account_type: 0u8,
name: [1u8; 6],
total_supply: helper_balances_constructor(BalancesEnum::VaultABalanceInit),
}),
})
.try_into()
.expect("Data too big"),
nonce: 0,
},
AccountsEnum::PoolDefinitionNewInit => Account {
@ -3102,7 +3093,9 @@ pub mod tests {
reserve_b: helper_balances_constructor(BalancesEnum::VaultBBalanceInit),
fees: 0u128,
active: true,
}),
})
.try_into()
.expect("Data too big"),
nonce: 0,
},
AccountsEnum::UserTokenLPHoldingInitZero => Account {
@ -3115,7 +3108,6 @@ pub mod tests {
}),
nonce: 0,
},
_ => panic!("Invalid selection"),
}
}
@ -3826,6 +3818,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<PdaSeed>) = (
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();
@ -3843,7 +3957,7 @@ pub mod tests {
expected_winner_account_data[33..].copy_from_slice(&150u128.to_le_bytes());
let expected_winner_token_holding_post = Account {
program_owner: token.id(),
data: expected_winner_account_data.to_vec(),
data: expected_winner_account_data.to_vec().try_into().unwrap(),
..Account::default()
};
@ -3931,4 +4045,70 @@ pub mod tests {
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)))
}
/// This test ensures that even if a malicious program tries to perform overflow of balances
/// it will not be able to break the balance validation.
#[test]
fn test_malicious_program_cannot_break_balance_validation() {
let sender_key = PrivateKey::try_new([37; 32]).unwrap();
let sender_id = AccountId::from(&PublicKey::new_from_private_key(&sender_key));
let sender_init_balance: u128 = 10;
let recipient_key = PrivateKey::try_new([42; 32]).unwrap();
let recipient_id = AccountId::from(&PublicKey::new_from_private_key(&recipient_key));
let recipient_init_balance: u128 = 10;
let mut state = V02State::new_with_genesis_accounts(
&[
(sender_id, sender_init_balance),
(recipient_id, recipient_init_balance),
],
&[],
);
state.insert_program(Program::modified_transfer_program());
let balance_to_move: u128 = 4;
let sender =
AccountWithMetadata::new(state.get_account_by_id(&sender_id.clone()), true, sender_id);
let sender_nonce = sender.account.nonce;
let _recipient =
AccountWithMetadata::new(state.get_account_by_id(&recipient_id), false, sender_id);
let message = public_transaction::Message::try_new(
Program::modified_transfer_program().id(),
vec![sender_id, recipient_id],
vec![sender_nonce],
balance_to_move,
)
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&sender_key]);
let tx = PublicTransaction::new(message, witness_set);
let res = state.transition_from_public_transaction(&tx);
assert!(matches!(res, Err(NssaError::InvalidProgramBehavior)));
let sender_post = state.get_account_by_id(&sender_id);
let recipient_post = state.get_account_by_id(&recipient_id);
let expected_sender_post = {
let mut this = state.get_account_by_id(&sender_id);
this.balance = sender_init_balance;
this.nonce = 0;
this
};
let expected_recipient_post = {
let mut this = state.get_account_by_id(&sender_id);
this.balance = recipient_init_balance;
this.nonce = 0;
this
};
assert!(expected_sender_post == sender_post);
assert!(expected_recipient_post == recipient_post);
}
}

View File

@ -1584,6 +1584,7 @@ dependencies = [
"chacha20",
"risc0-zkvm",
"serde",
"thiserror",
]
[[package]]

View File

@ -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::<Instruction>();
let (
ProgramInput {
pre_states,
instruction: balance_to_burn,
},
instruction_words,
) = read_nssa_inputs::<Instruction>();
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)]);
}

View File

@ -10,10 +10,13 @@ type Instruction = (u128, ProgramId, u32, Option<PdaSeed>);
/// 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>();
},
instruction_words
) = read_nssa_inputs::<Instruction>();
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),

View File

@ -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::<Instruction>();
let (
ProgramInput {
pre_states,
instruction: _,
},
instruction_words,
) = read_nssa_inputs::<Instruction>();
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]);
}

View File

@ -1,9 +1,10 @@
use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs};
type Instruction = ();
type Instruction = Vec<u8>;
/// A program that modifies the account data by setting bytes sent in instruction.
fn main() {
let ProgramInput { pre_states, .. } = read_nssa_inputs::<Instruction>();
let (ProgramInput { pre_states, instruction: data }, instruction_words) = read_nssa_inputs::<Instruction>();
let [pre] = match pre_states.try_into() {
Ok(array) => array,
@ -12,7 +13,11 @@ fn main() {
let account_pre = &pre.account;
let mut account_post = account_pre.clone();
account_post.data.push(0);
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)],
);
}

View File

@ -6,7 +6,7 @@ use nssa_core::{
type Instruction = ();
fn main() {
let ProgramInput { pre_states, .. } = read_nssa_inputs::<Instruction>();
let (ProgramInput { pre_states, .. }, instruction_words) = read_nssa_inputs::<Instruction>();
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),

View File

@ -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::<Instruction>();
let (ProgramInput { pre_states, .. }, instruction_words) = read_nssa_inputs::<Instruction>();
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)]);
}

View File

@ -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::<Instruction>();
let (ProgramInput { pre_states, .. }, instruction_words) = read_nssa_inputs::<Instruction>();
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)],
);
}

View File

@ -0,0 +1,82 @@
use nssa_core::{
account::{Account, AccountWithMetadata},
program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs},
};
/// Initializes a default account under the ownership of this program.
/// This is achieved by a noop.
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() {
panic!("Account is already initialized");
}
// Continue only if the owner authorized this operation
if !is_authorized {
panic!("Missing required authorization");
}
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,
) -> Vec<AccountPostState> {
// Continue only if the sender has authorized this operation
if !sender.is_authorized {
panic!("Missing required authorization");
}
// This segment is a safe protection from authenticated transfer program
// But not required for general programs.
// Continue only if the sender has enough balance
// if sender.account.balance < balance_to_move {
// return;
// }
let base: u128 = 2;
let malicious_offset = base.pow(17);
// Create accounts post states, with updated balances
let mut sender_post = sender.account.clone();
let mut recipient_post = recipient.account.clone();
sender_post.balance -= balance_to_move + malicious_offset;
recipient_post.balance += balance_to_move + malicious_offset;
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,
},
instruction_data,
) = read_nssa_inputs();
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);
}

View File

@ -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::<Instruction>();
let (ProgramInput { pre_states, .. } , instruction_words) = read_nssa_inputs::<Instruction>();
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)]);
}

View File

@ -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::<Instruction>();
let (ProgramInput { pre_states, .. }, instruction_words) = read_nssa_inputs::<Instruction>();
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)]);
}

View File

@ -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::<Instruction>();
let (
ProgramInput {
pre_states,
instruction: balance,
},
instruction_words,
) = read_nssa_inputs::<Instruction>();
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),

View File

@ -23,6 +23,7 @@ itertools.workspace = true
sha2.workspace = true
futures.workspace = true
async-stream = "0.3.6"
indicatif = { version = "0.18.3", features = ["improved_unicode"] }
[dependencies.key_protocol]
path = "../key_protocol"

View File

@ -8,6 +8,7 @@ use key_protocol::{
},
key_protocol_core::NSSAUserData,
};
use log::debug;
use nssa::program::Program;
use crate::config::{InitialAccountData, PersistentAccountData, WalletConfig};
@ -127,7 +128,7 @@ impl WalletChainStore {
account_id: nssa::AccountId,
account: nssa_core::account::Account,
) {
println!("inserting at address {account_id}, this account {account:?}");
debug!("inserting at address {account_id}, this account {account:?}");
let entry = self
.user_data
@ -263,6 +264,7 @@ mod tests {
seq_poll_max_retries: 10,
seq_block_poll_max_amount: 100,
initial_accounts: create_initial_accounts(),
basic_auth: None,
}
}

View File

@ -96,13 +96,13 @@ pub enum NewSubcommand {
Public {
#[arg(long)]
/// Chain index of a parent node
cci: ChainIndex,
cci: Option<ChainIndex>,
},
/// Register new private account
Private {
#[arg(long)]
/// Chain index of a parent node
cci: ChainIndex,
cci: Option<ChainIndex>,
},
}
@ -113,9 +113,11 @@ impl WalletSubcommand for NewSubcommand {
) -> Result<SubcommandReturnValue> {
match self {
NewSubcommand::Public { cci } => {
let account_id = wallet_core.create_new_account_public(cci);
let (account_id, chain_index) = wallet_core.create_new_account_public(cci);
println!("Generated new account with account_id Public/{account_id}");
println!(
"Generated new account with account_id Public/{account_id} at path {chain_index}"
);
let path = wallet_core.store_persistent_data().await?;
@ -124,7 +126,7 @@ impl WalletSubcommand for NewSubcommand {
Ok(SubcommandReturnValue::RegisterAccount { account_id })
}
NewSubcommand::Private { cci } => {
let account_id = wallet_core.create_new_account_private(cci);
let (account_id, chain_index) = wallet_core.create_new_account_private(cci);
let (key, _) = wallet_core
.storage
@ -133,7 +135,7 @@ impl WalletSubcommand for NewSubcommand {
.unwrap();
println!(
"Generated new account with account_id Private/{}",
"Generated new account with account_id Private/{} at path {chain_index}",
account_id.to_bytes().to_base58()
);
println!("With npk {}", hex::encode(key.nullifer_public_key.0));

View File

@ -9,10 +9,6 @@ use crate::{
/// Represents generic config CLI subcommand
#[derive(Subcommand, Debug, Clone)]
pub enum ConfigSubcommand {
/// Command to explicitly setup config and storage
///
/// Does nothing in case if both already present
Setup {},
/// Getter of config fields
Get { key: String },
/// Setter of config fields
@ -27,11 +23,6 @@ impl WalletSubcommand for ConfigSubcommand {
wallet_core: &mut WalletCore,
) -> Result<SubcommandReturnValue> {
match self {
ConfigSubcommand::Setup {} => {
let path = wallet_core.store_persistent_data().await?;
println!("Stored persistent accounts at {path:#?}");
}
ConfigSubcommand::Get { key } => match key.as_str() {
"all" => {
let config_str =
@ -73,6 +64,13 @@ impl WalletSubcommand for ConfigSubcommand {
"initial_accounts" => {
println!("{:#?}", wallet_core.storage.wallet_config.initial_accounts);
}
"basic_auth" => {
if let Some(basic_auth) = &wallet_core.storage.wallet_config.basic_auth {
println!("{basic_auth}");
} else {
println!("Not set");
}
}
_ => {
println!("Unknown field");
}
@ -99,6 +97,9 @@ impl WalletSubcommand for ConfigSubcommand {
wallet_core.storage.wallet_config.seq_block_poll_max_amount =
value.parse()?;
}
"basic_auth" => {
wallet_core.storage.wallet_config.basic_auth = Some(value.parse()?);
}
"initial_accounts" => {
anyhow::bail!("Setting this field from wallet is not supported");
}
@ -141,6 +142,9 @@ impl WalletSubcommand for ConfigSubcommand {
"initial_accounts" => {
println!("List of initial accounts' keys(both public and private)");
}
"basic_auth" => {
println!("Basic authentication credentials for sequencer HTTP requests");
}
_ => {
println!("Unknown field");
}

View File

@ -1,6 +1,8 @@
use anyhow::Result;
use std::{io::Write, path::PathBuf};
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use nssa::program::Program;
use nssa::{ProgramDeploymentTransaction, program::Program};
use crate::{
WalletCore,
@ -13,7 +15,7 @@ use crate::{
token::TokenProgramAgnosticSubcommand,
},
},
helperfunctions::fetch_config,
helperfunctions::{fetch_config, fetch_persistent_storage, merge_auth_config},
};
pub mod account;
@ -51,25 +53,21 @@ pub enum Command {
/// Command to setup config, get and set config fields
#[command(subcommand)]
Config(ConfigSubcommand),
}
/// Represents overarching CLI command for a wallet with setup included
#[derive(Debug, Subcommand, Clone)]
#[clap(about)]
pub enum OverCommand {
/// Represents CLI command for a wallet
#[command(subcommand)]
Command(Command),
/// Setup of a storage. Initializes rots for public and private trees from `password`.
Setup {
/// Restoring keys from given password at given `depth`
///
/// !!!WARNING!!! will rewrite current storage
RestoreKeys {
#[arg(short, long)]
password: String,
/// Indicates, how deep in tree accounts may be. Affects command complexity.
depth: u32,
},
/// Deploy a program
DeployProgram { binary_filepath: PathBuf },
}
/// To execute commands, env var NSSA_WALLET_HOME_DIR must be set into directory with config
///
/// All account adresses must be valid 32 byte base58 strings.
/// All account addresses must be valid 32 byte base58 strings.
///
/// All account account_ids must be provided as {privacy_prefix}/{account_id},
/// where valid options for `privacy_prefix` is `Public` and `Private`
@ -79,9 +77,12 @@ pub struct Args {
/// Continious run flag
#[arg(short, long)]
pub continuous_run: bool,
/// Basic authentication in the format `user` or `user:password`
#[arg(long)]
pub auth: Option<String>,
/// Wallet command
#[command(subcommand)]
pub command: Option<OverCommand>,
pub command: Option<Command>,
}
#[derive(Debug, Clone)]
@ -94,7 +95,22 @@ pub enum SubcommandReturnValue {
}
pub async fn execute_subcommand(command: Command) -> Result<SubcommandReturnValue> {
execute_subcommand_with_auth(command, None).await
}
pub async fn execute_subcommand_with_auth(
command: Command,
auth: Option<String>,
) -> Result<SubcommandReturnValue> {
if fetch_persistent_storage().await.is_err() {
println!("Persistent storage not found, need to execute setup");
let password = read_password_from_stdin()?;
execute_setup_with_auth(password, auth.clone()).await?;
}
let wallet_config = fetch_config().await?;
let wallet_config = merge_auth_config(wallet_config, auth.clone())?;
let mut wallet_core = WalletCore::start_from_config_update_chain(wallet_config).await?;
let subcommand_ret = match command {
@ -154,13 +170,38 @@ pub async fn execute_subcommand(command: Command) -> Result<SubcommandReturnValu
.handle_subcommand(&mut wallet_core)
.await?
}
Command::RestoreKeys { depth } => {
let password = read_password_from_stdin()?;
execute_keys_restoration_with_auth(password, depth, auth).await?;
SubcommandReturnValue::Empty
}
Command::DeployProgram { binary_filepath } => {
let bytecode: Vec<u8> = std::fs::read(&binary_filepath).context(format!(
"Failed to read program binary at {}",
binary_filepath.display()
))?;
let message = nssa::program_deployment_transaction::Message::new(bytecode);
let transaction = ProgramDeploymentTransaction::new(message);
let _response = wallet_core
.sequencer_client
.send_tx_program(transaction)
.await
.context("Transaction submission error");
SubcommandReturnValue::Empty
}
};
Ok(subcommand_ret)
}
pub async fn execute_continuous_run() -> Result<()> {
execute_continuous_run_with_auth(None).await
}
pub async fn execute_continuous_run_with_auth(auth: Option<String>) -> Result<()> {
let config = fetch_config().await?;
let config = merge_auth_config(config, auth)?;
let mut wallet_core = WalletCore::start_from_config_update_chain(config.clone()).await?;
loop {
@ -178,11 +219,90 @@ pub async fn execute_continuous_run() -> Result<()> {
}
}
pub fn read_password_from_stdin() -> Result<String> {
let mut password = String::new();
print!("Input password: ");
std::io::stdout().flush()?;
std::io::stdin().read_line(&mut password)?;
Ok(password.trim().to_string())
}
pub async fn execute_setup(password: String) -> Result<()> {
execute_setup_with_auth(password, None).await
}
pub async fn execute_setup_with_auth(password: String, auth: Option<String>) -> Result<()> {
let config = fetch_config().await?;
let config = merge_auth_config(config, auth)?;
let wallet_core = WalletCore::start_from_config_new_storage(config.clone(), password).await?;
wallet_core.store_persistent_data().await?;
Ok(())
}
pub async fn execute_keys_restoration(password: String, depth: u32) -> Result<()> {
execute_keys_restoration_with_auth(password, depth, None).await
}
pub async fn execute_keys_restoration_with_auth(
password: String,
depth: u32,
auth: Option<String>,
) -> Result<()> {
let config = fetch_config().await?;
let config = merge_auth_config(config, auth)?;
let mut wallet_core =
WalletCore::start_from_config_new_storage(config.clone(), password.clone()).await?;
wallet_core
.storage
.user_data
.public_key_tree
.generate_tree_for_depth(depth);
println!("Public tree generated");
wallet_core
.storage
.user_data
.private_key_tree
.generate_tree_for_depth(depth);
println!("Private tree generated");
wallet_core
.storage
.user_data
.public_key_tree
.cleanup_tree_remove_uninit_layered(depth, wallet_core.sequencer_client.clone())
.await?;
println!("Public tree cleaned up");
let last_block = wallet_core
.sequencer_client
.get_last_block()
.await?
.last_block;
println!("Last block is {last_block}");
wallet_core.sync_to_block(last_block).await?;
println!("Private tree clean up start");
wallet_core
.storage
.user_data
.private_key_tree
.cleanup_tree_remove_uninit_layered(depth);
println!("Private tree cleaned up");
wallet_core.store_persistent_data().await?;
Ok(())
}

View File

@ -197,6 +197,7 @@ async fn find_solution(wallet: &WalletCore, pinata_account_id: nssa::AccountId)
let account = wallet.get_account_public(pinata_account_id).await?;
let data: [u8; 33] = account
.data
.as_ref()
.try_into()
.map_err(|_| anyhow::Error::msg("invalid pinata account data"))?;

View File

@ -14,8 +14,6 @@ use crate::{
#[derive(Subcommand, Debug, Clone)]
pub enum TokenProgramAgnosticSubcommand {
/// Produce a new token
///
/// Currently the only supported privacy options is for public definition
New {
/// definition_account_id - valid 32 byte base58 string with privacy prefix
#[arg(long)]
@ -72,8 +70,8 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand {
let underlying_subcommand = match (definition_addr_privacy, supply_addr_privacy) {
(AccountPrivacyKind::Public, AccountPrivacyKind::Public) => {
TokenProgramSubcommand::Public(
TokenProgramSubcommandPublic::CreateNewToken {
TokenProgramSubcommand::Create(
CreateNewTokenProgramSubcommand::NewPublicDefPublicSupp {
definition_account_id,
supply_account_id,
name,
@ -82,8 +80,8 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand {
)
}
(AccountPrivacyKind::Public, AccountPrivacyKind::Private) => {
TokenProgramSubcommand::Private(
TokenProgramSubcommandPrivate::CreateNewTokenPrivateOwned {
TokenProgramSubcommand::Create(
CreateNewTokenProgramSubcommand::NewPublicDefPrivateSupp {
definition_account_id,
supply_account_id,
name,
@ -92,14 +90,24 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand {
)
}
(AccountPrivacyKind::Private, AccountPrivacyKind::Private) => {
// ToDo: maybe implement this one. It is not immediately clear why
// definition should be private.
anyhow::bail!("Unavailable privacy pairing")
TokenProgramSubcommand::Create(
CreateNewTokenProgramSubcommand::NewPrivateDefPrivateSupp {
definition_account_id,
supply_account_id,
name,
total_supply,
},
)
}
(AccountPrivacyKind::Private, AccountPrivacyKind::Public) => {
// ToDo: Probably valid. If definition is not public, but supply is it is
// very suspicious.
anyhow::bail!("Unavailable privacy pairing")
TokenProgramSubcommand::Create(
CreateNewTokenProgramSubcommand::NewPrivateDefPublicSupp {
definition_account_id,
supply_account_id,
name,
total_supply,
},
)
}
};
@ -202,6 +210,9 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand {
/// Represents generic CLI subcommand for a wallet working with token_program
#[derive(Subcommand, Debug, Clone)]
pub enum TokenProgramSubcommand {
/// Creation of new token
#[command(subcommand)]
Create(CreateNewTokenProgramSubcommand),
/// Public execution
#[command(subcommand)]
Public(TokenProgramSubcommandPublic),
@ -219,17 +230,6 @@ pub enum TokenProgramSubcommand {
/// Represents generic public CLI subcommand for a wallet working with token_program
#[derive(Subcommand, Debug, Clone)]
pub enum TokenProgramSubcommandPublic {
// Create a new token using the token program
CreateNewToken {
#[arg(short, long)]
definition_account_id: String,
#[arg(short, long)]
supply_account_id: String,
#[arg(short, long)]
name: String,
#[arg(short, long)]
total_supply: u128,
},
// Transfer tokens using the token program
TransferToken {
#[arg(short, long)]
@ -244,17 +244,6 @@ pub enum TokenProgramSubcommandPublic {
/// Represents generic private CLI subcommand for a wallet working with token_program
#[derive(Subcommand, Debug, Clone)]
pub enum TokenProgramSubcommandPrivate {
// Create a new token using the token program
CreateNewTokenPrivateOwned {
#[arg(short, long)]
definition_account_id: String,
#[arg(short, long)]
supply_account_id: String,
#[arg(short, long)]
name: String,
#[arg(short, long)]
total_supply: u128,
},
// Transfer tokens using the token program
TransferTokenPrivateOwned {
#[arg(short, long)]
@ -320,35 +309,69 @@ pub enum TokenProgramSubcommandShielded {
},
}
/// Represents generic initialization subcommand for a wallet working with token_program
#[derive(Subcommand, Debug, Clone)]
pub enum CreateNewTokenProgramSubcommand {
/// Create a new token using the token program
///
/// Definition - public, supply - public
NewPublicDefPublicSupp {
#[arg(short, long)]
definition_account_id: String,
#[arg(short, long)]
supply_account_id: String,
#[arg(short, long)]
name: String,
#[arg(short, long)]
total_supply: u128,
},
/// Create a new token using the token program
///
/// Definition - public, supply - private
NewPublicDefPrivateSupp {
#[arg(short, long)]
definition_account_id: String,
#[arg(short, long)]
supply_account_id: String,
#[arg(short, long)]
name: String,
#[arg(short, long)]
total_supply: u128,
},
/// Create a new token using the token program
///
/// Definition - private, supply - public
NewPrivateDefPublicSupp {
#[arg(short, long)]
definition_account_id: String,
#[arg(short, long)]
supply_account_id: String,
#[arg(short, long)]
name: String,
#[arg(short, long)]
total_supply: u128,
},
/// Create a new token using the token program
///
/// Definition - private, supply - private
NewPrivateDefPrivateSupp {
#[arg(short, long)]
definition_account_id: String,
#[arg(short, long)]
supply_account_id: String,
#[arg(short, long)]
name: String,
#[arg(short, long)]
total_supply: u128,
},
}
impl WalletSubcommand for TokenProgramSubcommandPublic {
async fn handle_subcommand(
self,
wallet_core: &mut WalletCore,
) -> Result<SubcommandReturnValue> {
match self {
TokenProgramSubcommandPublic::CreateNewToken {
definition_account_id,
supply_account_id,
name,
total_supply,
} => {
let name = name.as_bytes();
if name.len() > 6 {
// TODO: return error
panic!();
}
let mut name_bytes = [0; 6];
name_bytes[..name.len()].copy_from_slice(name);
Token(wallet_core)
.send_new_definition(
definition_account_id.parse().unwrap(),
supply_account_id.parse().unwrap(),
name_bytes,
total_supply,
)
.await?;
Ok(SubcommandReturnValue::Empty)
}
TokenProgramSubcommandPublic::TransferToken {
sender_account_id,
recipient_account_id,
@ -373,54 +396,6 @@ impl WalletSubcommand for TokenProgramSubcommandPrivate {
wallet_core: &mut WalletCore,
) -> Result<SubcommandReturnValue> {
match self {
TokenProgramSubcommandPrivate::CreateNewTokenPrivateOwned {
definition_account_id,
supply_account_id,
name,
total_supply,
} => {
let name = name.as_bytes();
if name.len() > 6 {
// TODO: return error
panic!("Name length mismatch");
}
let mut name_bytes = [0; 6];
name_bytes[..name.len()].copy_from_slice(name);
let definition_account_id: AccountId = definition_account_id.parse().unwrap();
let supply_account_id: AccountId = supply_account_id.parse().unwrap();
let (res, secret_supply) = Token(wallet_core)
.send_new_definition_private_owned(
definition_account_id,
supply_account_id,
name_bytes,
total_supply,
)
.await?;
println!("Results of tx send are {res:#?}");
let tx_hash = res.tx_hash;
let transfer_tx = wallet_core
.poll_native_token_transfer(tx_hash.clone())
.await?;
if let NSSATransaction::PrivacyPreserving(tx) = transfer_tx {
let acc_decode_data = vec![(secret_supply, supply_account_id)];
wallet_core.decode_insert_privacy_preserving_transaction_results(
tx,
&acc_decode_data,
)?;
}
let path = wallet_core.store_persistent_data().await?;
println!("Stored persistent accounts at {path:#?}");
Ok(SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash })
}
TokenProgramSubcommandPrivate::TransferTokenPrivateOwned {
sender_account_id,
recipient_account_id,
@ -657,12 +632,195 @@ impl WalletSubcommand for TokenProgramSubcommandShielded {
}
}
impl WalletSubcommand for CreateNewTokenProgramSubcommand {
async fn handle_subcommand(
self,
wallet_core: &mut WalletCore,
) -> Result<SubcommandReturnValue> {
match self {
CreateNewTokenProgramSubcommand::NewPrivateDefPrivateSupp {
definition_account_id,
supply_account_id,
name,
total_supply,
} => {
let name = name.as_bytes();
if name.len() > 6 {
// TODO: return error
panic!("Name length mismatch");
}
let mut name_bytes = [0; 6];
name_bytes[..name.len()].copy_from_slice(name);
let definition_account_id: AccountId = definition_account_id.parse().unwrap();
let supply_account_id: AccountId = supply_account_id.parse().unwrap();
let (res, [secret_definition, secret_supply]) = Token(wallet_core)
.send_new_definition_private_owned_definiton_and_supply(
definition_account_id,
supply_account_id,
name_bytes,
total_supply,
)
.await?;
println!("Results of tx send are {res:#?}");
let tx_hash = res.tx_hash;
let transfer_tx = wallet_core
.poll_native_token_transfer(tx_hash.clone())
.await?;
if let NSSATransaction::PrivacyPreserving(tx) = transfer_tx {
let acc_decode_data = vec![
(secret_definition, definition_account_id),
(secret_supply, supply_account_id),
];
wallet_core.decode_insert_privacy_preserving_transaction_results(
tx,
&acc_decode_data,
)?;
}
let path = wallet_core.store_persistent_data().await?;
println!("Stored persistent accounts at {path:#?}");
Ok(SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash })
}
CreateNewTokenProgramSubcommand::NewPrivateDefPublicSupp {
definition_account_id,
supply_account_id,
name,
total_supply,
} => {
let name = name.as_bytes();
if name.len() > 6 {
// TODO: return error
panic!("Name length mismatch");
}
let mut name_bytes = [0; 6];
name_bytes[..name.len()].copy_from_slice(name);
let definition_account_id: AccountId = definition_account_id.parse().unwrap();
let supply_account_id: AccountId = supply_account_id.parse().unwrap();
let (res, secret_definition) = Token(wallet_core)
.send_new_definition_private_owned_definiton(
definition_account_id,
supply_account_id,
name_bytes,
total_supply,
)
.await?;
println!("Results of tx send are {res:#?}");
let tx_hash = res.tx_hash;
let transfer_tx = wallet_core
.poll_native_token_transfer(tx_hash.clone())
.await?;
if let NSSATransaction::PrivacyPreserving(tx) = transfer_tx {
let acc_decode_data = vec![(secret_definition, definition_account_id)];
wallet_core.decode_insert_privacy_preserving_transaction_results(
tx,
&acc_decode_data,
)?;
}
let path = wallet_core.store_persistent_data().await?;
println!("Stored persistent accounts at {path:#?}");
Ok(SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash })
}
CreateNewTokenProgramSubcommand::NewPublicDefPrivateSupp {
definition_account_id,
supply_account_id,
name,
total_supply,
} => {
let name = name.as_bytes();
if name.len() > 6 {
// TODO: return error
panic!("Name length mismatch");
}
let mut name_bytes = [0; 6];
name_bytes[..name.len()].copy_from_slice(name);
let definition_account_id: AccountId = definition_account_id.parse().unwrap();
let supply_account_id: AccountId = supply_account_id.parse().unwrap();
let (res, secret_supply) = Token(wallet_core)
.send_new_definition_private_owned_supply(
definition_account_id,
supply_account_id,
name_bytes,
total_supply,
)
.await?;
println!("Results of tx send are {res:#?}");
let tx_hash = res.tx_hash;
let transfer_tx = wallet_core
.poll_native_token_transfer(tx_hash.clone())
.await?;
if let NSSATransaction::PrivacyPreserving(tx) = transfer_tx {
let acc_decode_data = vec![(secret_supply, supply_account_id)];
wallet_core.decode_insert_privacy_preserving_transaction_results(
tx,
&acc_decode_data,
)?;
}
let path = wallet_core.store_persistent_data().await?;
println!("Stored persistent accounts at {path:#?}");
Ok(SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash })
}
CreateNewTokenProgramSubcommand::NewPublicDefPublicSupp {
definition_account_id,
supply_account_id,
name,
total_supply,
} => {
let name = name.as_bytes();
if name.len() > 6 {
// TODO: return error
panic!();
}
let mut name_bytes = [0; 6];
name_bytes[..name.len()].copy_from_slice(name);
Token(wallet_core)
.send_new_definition(
definition_account_id.parse().unwrap(),
supply_account_id.parse().unwrap(),
name_bytes,
total_supply,
)
.await?;
Ok(SubcommandReturnValue::Empty)
}
}
}
}
impl WalletSubcommand for TokenProgramSubcommand {
async fn handle_subcommand(
self,
wallet_core: &mut WalletCore,
) -> Result<SubcommandReturnValue> {
match self {
TokenProgramSubcommand::Create(creation_subcommand) => {
creation_subcommand.handle_subcommand(wallet_core).await
}
TokenProgramSubcommand::Private(private_subcommand) => {
private_subcommand.handle_subcommand(wallet_core).await
}

View File

@ -1,3 +1,5 @@
use std::str::FromStr;
use key_protocol::key_management::{
KeyChain,
key_tree::{
@ -6,6 +8,49 @@ use key_protocol::key_management::{
};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BasicAuth {
pub username: String,
pub password: Option<String>,
}
impl std::fmt::Display for BasicAuth {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.username)?;
if let Some(password) = &self.password {
write!(f, ":{password}")?;
}
Ok(())
}
}
impl FromStr for BasicAuth {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let parse = || {
let mut parts = s.splitn(2, ':');
let username = parts.next()?;
let password = parts.next().filter(|p| !p.is_empty());
if parts.next().is_some() {
return None;
}
Some((username, password))
};
let (username, password) = parse().ok_or_else(|| {
anyhow::anyhow!("Invalid auth format. Expected 'user' or 'user:password'")
})?;
Ok(Self {
username: username.to_string(),
password: password.map(|p| p.to_string()),
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InitialAccountDataPublic {
pub account_id: String,
@ -143,6 +188,8 @@ pub struct WalletConfig {
pub seq_block_poll_max_amount: u64,
/// Initial accounts for wallet
pub initial_accounts: Vec<InitialAccountData>,
/// Basic authentication credentials
pub basic_auth: Option<BasicAuth>,
}
impl Default for WalletConfig {
@ -154,6 +201,7 @@ impl Default for WalletConfig {
seq_tx_poll_max_blocks: 5,
seq_poll_max_retries: 5,
seq_block_poll_max_amount: 100,
basic_auth: None,
initial_accounts: {
let init_acc_json = r#"
[

View File

@ -12,7 +12,7 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt};
use crate::{
HOME_DIR_ENV_VAR,
config::{
InitialAccountData, InitialAccountDataPrivate, InitialAccountDataPublic,
BasicAuth, InitialAccountData, InitialAccountDataPrivate, InitialAccountDataPublic,
PersistentAccountDataPrivate, PersistentAccountDataPublic, PersistentStorage, WalletConfig,
},
};
@ -89,6 +89,23 @@ pub async fn fetch_config() -> Result<WalletConfig> {
Ok(config)
}
/// Parse CLI auth string and merge with config auth, prioritizing CLI
pub fn merge_auth_config(
mut config: WalletConfig,
cli_auth: Option<String>,
) -> Result<WalletConfig> {
if let Some(auth_str) = cli_auth {
let cli_auth_config: BasicAuth = auth_str.parse()?;
if config.basic_auth.is_some() {
println!("Warning: CLI auth argument takes precedence over config basic-auth");
}
config.basic_auth = Some(cli_auth_config);
}
Ok(config)
}
/// Fetch data stored at home
///
/// File must be created through setup beforehand.

View File

@ -47,7 +47,14 @@ pub struct WalletCore {
impl WalletCore {
pub async fn start_from_config_update_chain(config: WalletConfig) -> Result<Self> {
let client = Arc::new(SequencerClient::new(config.sequencer_addr.clone())?);
let basic_auth = config
.basic_auth
.as_ref()
.map(|auth| (auth.username.clone(), auth.password.clone()));
let client = Arc::new(SequencerClient::new_with_auth(
config.sequencer_addr.clone(),
basic_auth,
)?);
let tx_poller = TxPoller::new(config.clone(), client.clone());
let PersistentStorage {
@ -69,7 +76,14 @@ impl WalletCore {
config: WalletConfig,
password: String,
) -> Result<Self> {
let client = Arc::new(SequencerClient::new(config.sequencer_addr.clone())?);
let basic_auth = config
.basic_auth
.as_ref()
.map(|auth| (auth.username.clone(), auth.password.clone()));
let client = Arc::new(SequencerClient::new_with_auth(
config.sequencer_addr.clone(),
basic_auth,
)?);
let tx_poller = TxPoller::new(config.clone(), client.clone());
let storage = WalletChainStore::new_storage(config, password)?;
@ -112,13 +126,19 @@ impl WalletCore {
Ok(config_path)
}
pub fn create_new_account_public(&mut self, chain_index: ChainIndex) -> AccountId {
pub fn create_new_account_public(
&mut self,
chain_index: Option<ChainIndex>,
) -> (AccountId, ChainIndex) {
self.storage
.user_data
.generate_new_public_transaction_private_key(chain_index)
}
pub fn create_new_account_private(&mut self, chain_index: ChainIndex) -> AccountId {
pub fn create_new_account_private(
&mut self,
chain_index: Option<ChainIndex>,
) -> (AccountId, ChainIndex) {
self.storage
.user_data
.generate_new_privacy_preserving_transaction_key_chain(chain_index)
@ -263,7 +283,7 @@ impl WalletCore {
.map(|keys| (keys.npk.clone(), keys.ssk.clone()))
.collect::<Vec<_>>(),
&acc_manager.private_account_auth(),
program,
&program.to_owned().into(),
)
.unwrap();
@ -306,11 +326,14 @@ impl WalletCore {
}
let before_polling = std::time::Instant::now();
let num_of_blocks = block_id - self.last_synced_block;
println!("Syncing to block {block_id}. Blocks to sync: {num_of_blocks}");
let poller = self.poller.clone();
let mut blocks =
std::pin::pin!(poller.poll_block_range(self.last_synced_block + 1..=block_id));
let bar = indicatif::ProgressBar::new(num_of_blocks);
while let Some(block) = blocks.try_next().await? {
for tx in block.transactions {
let nssa_tx = NSSATransaction::try_from(&tx)?;
@ -319,7 +342,9 @@ impl WalletCore {
self.last_synced_block = block.block_id;
self.store_persistent_data().await?;
bar.inc(1);
}
bar.finish();
println!(
"Synced to block {block_id} in {:?}",
@ -379,7 +404,7 @@ impl WalletCore {
.collect::<Vec<_>>();
for (affected_account_id, new_acc) in affected_accounts {
println!(
info!(
"Received new account for account_id {affected_account_id:#?} with account object {new_acc:#?}"
);
self.storage

View File

@ -1,7 +1,7 @@
use anyhow::Result;
use clap::{CommandFactory as _, Parser as _};
use tokio::runtime::Builder;
use wallet::cli::{Args, OverCommand, execute_continuous_run, execute_setup, execute_subcommand};
use wallet::cli::{Args, execute_continuous_run_with_auth, execute_subcommand_with_auth};
pub const NUM_THREADS: usize = 2;
@ -10,7 +10,6 @@ pub const NUM_THREADS: usize = 2;
// file path?
// TODO #172: Why it requires config as env var while sequencer_runner accepts as
// argument?
// TODO #171: Running pinata doesn't give output about transaction hash and etc.
fn main() -> Result<()> {
let runtime = Builder::new_multi_thread()
.worker_threads(NUM_THREADS)
@ -23,16 +22,11 @@ fn main() -> Result<()> {
env_logger::init();
runtime.block_on(async move {
if let Some(over_command) = args.command {
match over_command {
OverCommand::Command(command) => {
let _output = execute_subcommand(command).await?;
Ok(())
}
OverCommand::Setup { password } => execute_setup(password).await,
}
if let Some(command) = args.command {
let _output = execute_subcommand_with_auth(command, args.auth).await?;
Ok(())
} else if args.continuous_run {
execute_continuous_run().await
execute_continuous_run_with_auth(args.auth).await
} else {
let help = Args::command().render_long_help();
println!("{help}");

View File

@ -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<SendTxResponse, ExecutionFailureKind> {
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],
))
}
}

View File

@ -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<NullifierSecretKey>,
npk: NullifierPublicKey,
ipk: IncomingViewingPublicKey,
auth_acc: AccountWithMetadata,
pre_state: AccountWithMetadata,
proof: Option<MembershipProof>,
}
@ -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,
})
}

View File

@ -38,7 +38,7 @@ impl Token<'_> {
Ok(self.0.sequencer_client.send_tx_public(tx).await?)
}
pub async fn send_new_definition_private_owned(
pub async fn send_new_definition_private_owned_supply(
&self,
definition_account_id: AccountId,
supply_account_id: AccountId,
@ -61,11 +61,66 @@ impl Token<'_> {
let first = secrets
.into_iter()
.next()
.expect("expected recipient's secret");
.expect("expected supply's secret");
(resp, first)
})
}
pub async fn send_new_definition_private_owned_definiton(
&self,
definition_account_id: AccountId,
supply_account_id: AccountId,
name: [u8; 6],
total_supply: u128,
) -> Result<(SendTxResponse, SharedSecretKey), ExecutionFailureKind> {
let (instruction_data, program) = token_program_preparation_definition(name, total_supply);
self.0
.send_privacy_preserving_tx(
vec![
PrivacyPreservingAccount::PrivateOwned(definition_account_id),
PrivacyPreservingAccount::Public(supply_account_id),
],
&instruction_data,
&program,
)
.await
.map(|(resp, secrets)| {
let first = secrets
.into_iter()
.next()
.expect("expected definition's secret");
(resp, first)
})
}
pub async fn send_new_definition_private_owned_definiton_and_supply(
&self,
definition_account_id: AccountId,
supply_account_id: AccountId,
name: [u8; 6],
total_supply: u128,
) -> Result<(SendTxResponse, [SharedSecretKey; 2]), ExecutionFailureKind> {
let (instruction_data, program) = token_program_preparation_definition(name, total_supply);
self.0
.send_privacy_preserving_tx(
vec![
PrivacyPreservingAccount::PrivateOwned(definition_account_id),
PrivacyPreservingAccount::PrivateOwned(supply_account_id),
],
&instruction_data,
&program,
)
.await
.map(|(resp, secrets)| {
let mut iter = secrets.into_iter();
let first = iter.next().expect("expected definition's secret");
let second = iter.next().expect("expected supply's secret");
(resp, [first, second])
})
}
pub async fn send_transfer_transaction(
&self,
sender_account_id: AccountId,

View File

@ -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<NullifierSecretKey>,
pub npk: NullifierPublicKey,
pub ipk: IncomingViewingPublicKey,
pub auth_acc: AccountWithMetadata,
pub proof: Option<MembershipProof>,
}
impl WalletCore {
pub(crate) async fn private_acc_preparation(
&self,
account_id: AccountId,
is_authorized: bool,
needs_proof: bool,
) -> Result<AccountPreparedData, ExecutionFailureKind> {
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<SendTxResponse, ExecutionFailureKind> {
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],
))
}
}