diff --git a/examples/program_deployment/README.md b/examples/program_deployment/README.md index 1bc7ed7..14a2bab 100644 --- a/examples/program_deployment/README.md +++ b/examples/program_deployment/README.md @@ -340,7 +340,7 @@ Luckily all that complexity is hidden behind the `wallet_core.send_privacy_prese .send_privacy_preserving_tx( accounts, &Program::serialize_instruction(greeting).unwrap(), - &program, + &program.into(), ) .await .unwrap(); @@ -568,4 +568,94 @@ Output: ``` Hola mundo!Hello from tail call ``` +## Private tail-calls +There's support for tail calls in privacy preserving executions too. The `run_hello_world_through_tail_call_private.rs` runner walks you through the process of invoking such an execution. +The only difference is that, since the execution is local, the runner will need both programs: the `simple_tail_call` and it's dependency `hello_world`. + +Let's use our existing private account with id `8vzkK7vsdrS2gdPhLk72La8X4FJkgJ5kJLUBRbEVkReU`. This one is already owned by the `hello_world` program. + +You can test the privacy tail calls with +```bash +cargo run --bin run_hello_world_through_tail_call_private \ + $EXAMPLE_PROGRAMS_BUILD_DIR/simple_tail_call.bin \ + $EXAMPLE_PROGRAMS_BUILD_DIR/hello_world.bin \ + 8vzkK7vsdrS2gdPhLk72La8X4FJkgJ5kJLUBRbEVkReU +``` + +>[!NOTE] +> The above command may take longer than the previous privacy executions because needs to generate proofs of execution of both the `simple_tail_call` and the `hello_world` programs. + +Once finished run the following to see the changes +```bash +wallet account sync-private +wallet account get --account-id Private/8vzkK7vsdrS2gdPhLk72La8X4FJkgJ5kJLUBRbEVkReU +``` + +# 13. Program derived accounts: authorizing accounts through tail calls + +## Digression: account authority vs account program ownership + +In NSSA there are two distinct concepts that control who can modify an account: +**Program Ownership:** Each account has a field: `program_owner: ProgramId`. +This indicates which program is allowed to update the account’s state during execution. +- If a program is the program_owner of an account, it can freely mutate its fields. +- If the account is uninitialized (`program_owner = DEFAULT_PROGRAM_ID`), a program may claim it and become its owner. +- If a program is not the owner and the account is not claimable, any attempt to modify it will cause the transition to fail. +Program ownership is about mutation rights during program execution. + +**Account authority**: Independent from program ownership, each account also has an authority. The entity that is allowed to set: `is_authorized = true`. This flag indicates that the account has been authorized for use in a transaction. +Who can act as authority? +- User-defined accounts: The user is the authority. They can mark an account as authorized by: + - Signing the transaction (public accounts) + - Providing a valid nullifiers secret key ownership proof (private accounts) +- Program derived accounts: Programs are automatically the authority of a dedicated namespace of public accounts. + +Each program owns a non-overlapping space of 2^256 **public** account IDs. They do not overlap with: +- User accounts (public or private) +- Other program’s PDAs + +> [!NOTE] +> Currently PDAs are restricted to the public state. + +A program can be the authority of an account owned by another program, which is the most common case. +During a chained call, a program can mark its PDA accounts as `is_authorized=true` without requiring any user signatures or nullifier secret keys. This enables programs to safely authorize accounts during program composition. Importantly, these flags can only be set to true for PDA accounts through an execution of the program that is their authority. No user and no other program can execute any transition that requires authorization of PDA accounts belonging to a different program. + +## Running the example +This tutorial includes an example of PDA usage in `methods/guest/src/bin/tail_call_with_pda.rs.`. That program’s sole purpose is to forward one of its own PDA accounts, an account for which it is the authority, to the "Hello World with authorization" program via a chained call. The Hello World program will then claim the account and become its program owner, but the `tail_call_with_pda` program remains the authority. This means it is still the only entity capable of marking that account as `is_authorized=true`. + +Deploy the program: +```bash +wallet deploy-program $EXAMPLE_PROGRAMS_BUILD_DIR/tail_call_with_pda.bin +``` + +There is no need to create a new account for this example, because we simply use one of the PDA accounts belonging to the `tail_call_with_pda` program. + +Execute the program +```bash +cargo run --bin run_hello_world_with_authorization_through_tail_call_with_pda $EXAMPLE_PROGRAMS_BUILD_DIR/tail_call_with_pda.bin +``` + +You'll see an output like the following: + +```bash +The program derived account ID is: 3tfTPPuxj3eSE1cLVuNBEk8eSHzpnYS1oqEdeH3Nfsks +``` + +Then check the status of that account + +```bash +wallet account get --account-id Public/3tfTPPuxj3eSE1cLVuNBEk8eSHzpnYS1oqEdeH3Nfsks +``` + +Output: +```bash +{ + "balance":0, + "program_owner_b64":"HZXHYRaKf6YusVo8x00/B15uyY5sGsJb1bzH4KlCY5g=", + "data_b64": "SGVsbG8gZnJvbSB0YWlsIGNhbGwgd2l0aCBQcm9ncmFtIERlcml2ZWQgQWNjb3VudCBJRA==", + "nonce":0" +} +``` + + diff --git a/examples/program_deployment/methods/guest/src/bin/simple_tail_call.rs b/examples/program_deployment/methods/guest/src/bin/simple_tail_call.rs index d2bb58c..d0dac57 100644 --- a/examples/program_deployment/methods/guest/src/bin/simple_tail_call.rs +++ b/examples/program_deployment/methods/guest/src/bin/simple_tail_call.rs @@ -15,7 +15,7 @@ use nssa_core::program::{ /// `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"; + "e9dfc5a5d03c9afa732adae6e0edfce4bbb44c7a2afb9f148f4309917eb2de6f"; fn hello_world_program_id() -> ProgramId { let hello_world_program_id_bytes: [u8; 32] = hex::decode(HELLO_WORLD_PROGRAM_ID_HEX) diff --git a/examples/program_deployment/methods/guest/src/bin/tail_call_with_pda.rs b/examples/program_deployment/methods/guest/src/bin/tail_call_with_pda.rs new file mode 100644 index 0000000..684fa1e --- /dev/null +++ b/examples/program_deployment/methods/guest/src/bin/tail_call_with_pda.rs @@ -0,0 +1,76 @@ +use nssa_core::program::{ + AccountPostState, ChainedCall, PdaSeed, ProgramId, ProgramInput, read_nssa_inputs, + write_nssa_outputs_with_chained_call, +}; + +// Tail Call with PDA example program. +// +// Demonstrates how to chain execution to another program using `ChainedCall` +// while authorizing program-derived accounts. +// +// Expects a single input account whose Account ID is derived from this +// program’s ID and the fixed PDA seed below (as defined by the +// `>` implementation). +// +// Emits this account unchanged, then performs a tail call to the +// Hello-World-with-Authorization program with a fixed greeting. The same +// account is passed along but marked with `is_authorized = true`. + +const HELLO_WORLD_WITH_AUTHORIZATION_PROGRAM_ID_HEX: &str = + "1d95c761168a7fa62eb15a3cc74d3f075e6ec98e6c1ac25bd5bcc7e0a9426398"; +const PDA_SEED: PdaSeed = PdaSeed::new([37; 32]); + +fn hello_world_program_id() -> ProgramId { + let hello_world_program_id_bytes: [u8; 32] = + hex::decode(HELLO_WORLD_WITH_AUTHORIZATION_PROGRAM_ID_HEX) + .unwrap() + .try_into() + .unwrap(); + bytemuck::cast(hello_world_program_id_bytes) +} + +fn main() { + // Read inputs + let ( + ProgramInput { + pre_states, + instruction: _, + }, + instruction_data, + ) = read_nssa_inputs::<()>(); + + // Unpack the input account pre state + let [pre_state] = pre_states + .clone() + .try_into() + .unwrap_or_else(|_| panic!("Input pre states should consist of a single account")); + + // Create the (unchanged) post state + let post_state = AccountPostState::new(pre_state.account.clone()); + + // Create the chained call + let chained_call_greeting: Vec = + b"Hello from tail call with Program Derived Account ID".to_vec(); + let chained_call_instruction_data = risc0_zkvm::serde::to_vec(&chained_call_greeting).unwrap(); + + // Flip the `is_authorized` flag to true + let pre_state_for_chained_call = { + let mut this = pre_state.clone(); + this.is_authorized = true; + this + }; + let chained_call = ChainedCall { + program_id: hello_world_program_id(), + instruction_data: chained_call_instruction_data, + pre_states: vec![pre_state_for_chained_call], + pda_seeds: vec![PDA_SEED], + }; + + // Write the outputs + write_nssa_outputs_with_chained_call( + instruction_data, + vec![pre_state], + vec![post_state], + vec![chained_call], + ); +} diff --git a/examples/program_deployment/src/bin/run_hello_world_private.rs b/examples/program_deployment/src/bin/run_hello_world_private.rs index be4280b..dcbe59a 100644 --- a/examples/program_deployment/src/bin/run_hello_world_private.rs +++ b/examples/program_deployment/src/bin/run_hello_world_private.rs @@ -54,7 +54,7 @@ async fn main() { .send_privacy_preserving_tx( accounts, &Program::serialize_instruction(greeting).unwrap(), - &program, + &program.into(), ) .await .unwrap(); diff --git a/examples/program_deployment/src/bin/run_hello_world_through_tail_call_private.rs b/examples/program_deployment/src/bin/run_hello_world_through_tail_call_private.rs new file mode 100644 index 0000000..5a014f2 --- /dev/null +++ b/examples/program_deployment/src/bin/run_hello_world_through_tail_call_private.rs @@ -0,0 +1,69 @@ +use std::collections::HashMap; + +use nssa::{ + AccountId, ProgramId, privacy_preserving_transaction::circuit::ProgramWithDependencies, + program::Program, +}; +use wallet::{PrivacyPreservingAccount, 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_private /path/to/guest/binary +// +// Example: +// cargo run --bin run_hello_world_through_tail_call \ +// methods/guest/target/riscv32im-risc0-zkvm-elf/docker/simple_tail_call.bin \ +// Ds8q5PjLcKwwV97Zi7duhRVF9uwA2PuYMoLL7FwCzsXE + +#[tokio::main] +async fn main() { + // Load wallet config and storage + let wallet_config = fetch_config().await.unwrap(); + let wallet_core = WalletCore::start_from_config_update_chain(wallet_config) + .await + .unwrap(); + + // Parse arguments + // First argument is the path to the simple_tail_call program binary + let simple_tail_call_path = std::env::args_os().nth(1).unwrap().into_string().unwrap(); + // Second argument is the path to the hello_world program binary + let hello_world_path = std::env::args_os().nth(2).unwrap().into_string().unwrap(); + // Third argument is the account_id + let account_id: AccountId = std::env::args_os() + .nth(3) + .unwrap() + .into_string() + .unwrap() + .parse() + .unwrap(); + + // Load the program and its dependencies (the hellow world program) + let simple_tail_call_bytecode: Vec = std::fs::read(simple_tail_call_path).unwrap(); + let simple_tail_call = Program::new(simple_tail_call_bytecode).unwrap(); + let hello_world_bytecode: Vec = std::fs::read(hello_world_path).unwrap(); + let hello_world = Program::new(hello_world_bytecode).unwrap(); + let dependencies: HashMap = + [(hello_world.id(), hello_world)].into_iter().collect(); + let program_with_dependencies = ProgramWithDependencies::new(simple_tail_call, dependencies); + + let accounts = vec![PrivacyPreservingAccount::PrivateOwned(account_id)]; + + // Construct and submit the privacy-preserving transaction + let instruction = (); + wallet_core + .send_privacy_preserving_tx( + accounts, + &Program::serialize_instruction(instruction).unwrap(), + &program_with_dependencies, + ) + .await + .unwrap(); +} diff --git a/examples/program_deployment/src/bin/run_hello_world_with_authorization_through_tail_call_with_pda.rs b/examples/program_deployment/src/bin/run_hello_world_with_authorization_through_tail_call_with_pda.rs new file mode 100644 index 0000000..6c673d8 --- /dev/null +++ b/examples/program_deployment/src/bin/run_hello_world_with_authorization_through_tail_call_with_pda.rs @@ -0,0 +1,62 @@ +use nssa::{ + AccountId, PublicTransaction, + program::Program, + public_transaction::{Message, WitnessSet}, +}; +use nssa_core::program::PdaSeed; +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_with_authorization_through_tail_call_with_pda +// /path/to/guest/binary +// +// Example: +// cargo run --bin run_hello_world_with_authorization_through_tail_call_with_pda \ +// methods/guest/target/riscv32im-risc0-zkvm-elf/docker/tail_call_with_pda.bin + +const PDA_SEED: PdaSeed = PdaSeed::new([37; 32]); + +#[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(); + + // Load the program + let bytecode: Vec = std::fs::read(program_path).unwrap(); + let program = Program::new(bytecode).unwrap(); + + // Compute the PDA to pass it as input account to the public execution + let pda = AccountId::from((&program.id(), &PDA_SEED)); + let account_ids = vec![pda]; + let instruction_data = (); + let nonces = vec![]; + let signing_keys = []; + let message = Message::try_new(program.id(), account_ids, 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(); + + println!("The program derived account id is: {pda}"); +} diff --git a/examples/program_deployment/src/bin/run_hello_world_with_move_function.rs b/examples/program_deployment/src/bin/run_hello_world_with_move_function.rs index 77c2597..4307315 100644 --- a/examples/program_deployment/src/bin/run_hello_world_with_move_function.rs +++ b/examples/program_deployment/src/bin/run_hello_world_with_move_function.rs @@ -105,7 +105,7 @@ async fn main() { .send_privacy_preserving_tx( accounts, &Program::serialize_instruction(instruction).unwrap(), - &program, + &program.into(), ) .await .unwrap(); @@ -146,7 +146,7 @@ async fn main() { .send_privacy_preserving_tx( accounts, &Program::serialize_instruction(instruction).unwrap(), - &program, + &program.into(), ) .await .unwrap(); diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index 26ee8de..51ac487 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -27,7 +27,7 @@ pub struct ProgramInput { pub struct PdaSeed([u8; 32]); impl PdaSeed { - pub fn new(value: [u8; 32]) -> Self { + pub const fn new(value: [u8; 32]) -> Self { Self(value) } } diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 237b2f2..7cab837 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -14,7 +14,9 @@ use key_protocol::key_management::key_tree::{chain_index::ChainIndex, traits::Ke use log::info; use nssa::{ Account, AccountId, PrivacyPreservingTransaction, - privacy_preserving_transaction::message::EncryptedAccountData, program::Program, + privacy_preserving_transaction::{ + circuit::ProgramWithDependencies, message::EncryptedAccountData, + }, }; use nssa_core::{Commitment, MembershipProof, SharedSecretKey, program::InstructionData}; pub use privacy_preserving_tx::PrivacyPreservingAccount; @@ -247,7 +249,7 @@ impl WalletCore { &self, accounts: Vec, instruction_data: &InstructionData, - program: &Program, + program: &ProgramWithDependencies, ) -> Result<(SendTxResponse, Vec), ExecutionFailureKind> { self.send_privacy_preserving_tx_with_pre_check(accounts, instruction_data, program, |_| { Ok(()) @@ -259,7 +261,7 @@ impl WalletCore { &self, accounts: Vec, instruction_data: &InstructionData, - program: &Program, + program: &ProgramWithDependencies, tx_pre_check: impl FnOnce(&[&Account]) -> Result<(), ExecutionFailureKind>, ) -> Result<(SendTxResponse, Vec), ExecutionFailureKind> { let acc_manager = privacy_preserving_tx::AccountManager::new(self, accounts).await?; @@ -284,7 +286,7 @@ impl WalletCore { .collect::>(), &acc_manager.private_account_auth(), &acc_manager.private_account_membership_proofs(), - &program.to_owned().into(), + &program.to_owned(), ) .unwrap(); diff --git a/wallet/src/program_facades/native_token_transfer/deshielded.rs b/wallet/src/program_facades/native_token_transfer/deshielded.rs index 35a13ba..df604c6 100644 --- a/wallet/src/program_facades/native_token_transfer/deshielded.rs +++ b/wallet/src/program_facades/native_token_transfer/deshielded.rs @@ -20,7 +20,7 @@ impl NativeTokenTransfer<'_> { PrivacyPreservingAccount::Public(to), ], &instruction_data, - &program, + &program.into(), tx_pre_check, ) .await diff --git a/wallet/src/program_facades/native_token_transfer/private.rs b/wallet/src/program_facades/native_token_transfer/private.rs index 320027b..0baeeac 100644 --- a/wallet/src/program_facades/native_token_transfer/private.rs +++ b/wallet/src/program_facades/native_token_transfer/private.rs @@ -18,7 +18,7 @@ impl NativeTokenTransfer<'_> { .send_privacy_preserving_tx_with_pre_check( vec![PrivacyPreservingAccount::PrivateOwned(from)], &Program::serialize_instruction(instruction).unwrap(), - &Program::authenticated_transfer_program(), + &Program::authenticated_transfer_program().into(), |_| Ok(()), ) .await @@ -48,7 +48,7 @@ impl NativeTokenTransfer<'_> { }, ], &instruction_data, - &program, + &program.into(), tx_pre_check, ) .await @@ -75,7 +75,7 @@ impl NativeTokenTransfer<'_> { PrivacyPreservingAccount::PrivateOwned(to), ], &instruction_data, - &program, + &program.into(), tx_pre_check, ) .await diff --git a/wallet/src/program_facades/native_token_transfer/shielded.rs b/wallet/src/program_facades/native_token_transfer/shielded.rs index 0802d6e..6abd2d2 100644 --- a/wallet/src/program_facades/native_token_transfer/shielded.rs +++ b/wallet/src/program_facades/native_token_transfer/shielded.rs @@ -21,7 +21,7 @@ impl NativeTokenTransfer<'_> { PrivacyPreservingAccount::PrivateOwned(to), ], &instruction_data, - &program, + &program.into(), tx_pre_check, ) .await @@ -53,7 +53,7 @@ impl NativeTokenTransfer<'_> { }, ], &instruction_data, - &program, + &program.into(), tx_pre_check, ) .await diff --git a/wallet/src/program_facades/pinata.rs b/wallet/src/program_facades/pinata.rs index 41e7510..fdd5d70 100644 --- a/wallet/src/program_facades/pinata.rs +++ b/wallet/src/program_facades/pinata.rs @@ -38,7 +38,7 @@ impl Pinata<'_> { PrivacyPreservingAccount::PrivateOwned(winner_account_id), ], &nssa::program::Program::serialize_instruction(solution).unwrap(), - &nssa::program::Program::pinata(), + &nssa::program::Program::pinata().into(), ) .await .map(|(resp, secrets)| { diff --git a/wallet/src/program_facades/token.rs b/wallet/src/program_facades/token.rs index 4ec9c12..e7bdca9 100644 --- a/wallet/src/program_facades/token.rs +++ b/wallet/src/program_facades/token.rs @@ -54,7 +54,7 @@ impl Token<'_> { PrivacyPreservingAccount::PrivateOwned(supply_account_id), ], &instruction_data, - &program, + &program.into(), ) .await .map(|(resp, secrets)| { @@ -82,7 +82,7 @@ impl Token<'_> { PrivacyPreservingAccount::Public(supply_account_id), ], &instruction_data, - &program, + &program.into(), ) .await .map(|(resp, secrets)| { @@ -110,7 +110,7 @@ impl Token<'_> { PrivacyPreservingAccount::PrivateOwned(supply_account_id), ], &instruction_data, - &program, + &program.into(), ) .await .map(|(resp, secrets)| { @@ -176,7 +176,7 @@ impl Token<'_> { PrivacyPreservingAccount::PrivateOwned(recipient_account_id), ], &instruction_data, - &program, + &program.into(), ) .await .map(|(resp, secrets)| { @@ -206,7 +206,7 @@ impl Token<'_> { }, ], &instruction_data, - &program, + &program.into(), ) .await .map(|(resp, secrets)| { @@ -232,7 +232,7 @@ impl Token<'_> { PrivacyPreservingAccount::Public(recipient_account_id), ], &instruction_data, - &program, + &program.into(), ) .await .map(|(resp, secrets)| { @@ -259,7 +259,7 @@ impl Token<'_> { PrivacyPreservingAccount::PrivateOwned(recipient_account_id), ], &instruction_data, - &program, + &program.into(), ) .await .map(|(resp, secrets)| { @@ -290,7 +290,7 @@ impl Token<'_> { }, ], &instruction_data, - &program, + &program.into(), ) .await .map(|(resp, secrets)| {