From 89c1a97798d665ba79b0d0259aa22f9f825bbb01 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Fri, 12 Dec 2025 13:00:16 -0300 Subject: [PATCH] add example and instructions for pda --- examples/program_deployment/README.md | 68 +++++++++++++++++ .../methods/guest/src/bin/simple_tail_call.rs | 3 +- .../guest/src/bin/tail_call_with_pda.rs | 76 +++++++++++++++++++ ...uthorization_through_tail_call_with_pda.rs | 62 +++++++++++++++ nssa/core/src/program.rs | 2 +- 5 files changed, 208 insertions(+), 3 deletions(-) create mode 100644 examples/program_deployment/methods/guest/src/bin/tail_call_with_pda.rs create mode 100644 examples/program_deployment/src/bin/run_hello_world_with_authorization_through_tail_call_with_pda.rs diff --git a/examples/program_deployment/README.md b/examples/program_deployment/README.md index 1bc7ed7..6325d2d 100644 --- a/examples/program_deployment/README.md +++ b/examples/program_deployment/README.md @@ -569,3 +569,71 @@ Output: Hola mundo!Hello from tail call ``` +# 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 5d8e221..cc7f429 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 @@ -9,9 +9,8 @@ use nssa_core::program::{ // It reads a single account, emits it unchanged, and then triggers a tail call // to the Hello World program with a fixed greeting. - 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_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/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) } }