From 1989fd25a178eca653992ef3c47e1541eb490378 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Fri, 28 Nov 2025 11:10:00 -0300 Subject: [PATCH] add pinata token example and test --- nssa/core/Cargo.toml | 2 +- .../guest/src/bin/pinata_token.rs | 103 ++++++++++++++++++ nssa/program_methods/guest/src/bin/token.rs | 49 ++++++++- nssa/src/program.rs | 5 + nssa/src/state.rs | 91 ++++++++++++++++ 5 files changed, 248 insertions(+), 2 deletions(-) create mode 100644 nssa/program_methods/guest/src/bin/pinata_token.rs diff --git a/nssa/core/Cargo.toml b/nssa/core/Cargo.toml index 67f40b2..0e16a3f 100644 --- a/nssa/core/Cargo.toml +++ b/nssa/core/Cargo.toml @@ -12,7 +12,7 @@ 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.workspace = true +borsh = "1.5.7" [features] default = [] diff --git a/nssa/program_methods/guest/src/bin/pinata_token.rs b/nssa/program_methods/guest/src/bin/pinata_token.rs new file mode 100644 index 0000000..ab04237 --- /dev/null +++ b/nssa/program_methods/guest/src/bin/pinata_token.rs @@ -0,0 +1,103 @@ +use nssa_core::program::{ + ChainedCall, PdaSeed, ProgramInput, read_nssa_inputs, write_nssa_outputs_with_chained_call, +}; +use risc0_zkvm::serde::to_vec; +use risc0_zkvm::sha::{Impl, Sha256}; + +const PRIZE: u128 = 150; + +type Instruction = u128; + +struct Challenge { + difficulty: u8, + seed: [u8; 32], +} + +impl Challenge { + fn new(bytes: &[u8]) -> Self { + assert_eq!(bytes.len(), 33); + let difficulty = bytes[0]; + assert!(difficulty <= 32); + + let mut seed = [0; 32]; + seed.copy_from_slice(&bytes[1..]); + Self { difficulty, seed } + } + + // Checks if the leftmost `self.difficulty` number of bytes of SHA256(self.data || solution) are + // zero. + fn validate_solution(&self, solution: Instruction) -> bool { + let mut bytes = [0; 32 + 16]; + bytes[..32].copy_from_slice(&self.seed); + bytes[32..].copy_from_slice(&solution.to_le_bytes()); + let digest: [u8; 32] = Impl::hash_bytes(&bytes).as_bytes().try_into().unwrap(); + let difficulty = self.difficulty as usize; + digest[..difficulty].iter().all(|&b| b == 0) + } + + fn next_data(self) -> [u8; 33] { + let mut result = [0; 33]; + result[0] = self.difficulty; + result[1..].copy_from_slice(Impl::hash_bytes(&self.seed).as_bytes()); + result + } +} + +/// 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::(); + + let [ + pinata_definition, + pinata_token_holding, + winner_token_holding, + ] = match pre_states.try_into() { + Ok(array) => array, + Err(_) => return, + }; + + let data = Challenge::new(&pinata_definition.account.data); + + if !data.validate_solution(solution) { + return; + } + + 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(); + + let mut instruction_data: [u8; 23] = [0; 23]; + instruction_data[0] = 1; + instruction_data[1..17].copy_from_slice(&PRIZE.to_le_bytes()); + + // Flip authorization to true for chained call + let mut pinata_token_holding_for_chain_call = pinata_token_holding.clone(); + pinata_token_holding_for_chain_call.is_authorized = true; + + 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()], + pda_seeds: vec![PdaSeed::new([0; 32])], + }]; + + write_nssa_outputs_with_chained_call( + vec![ + pinata_definition, + pinata_token_holding, + winner_token_holding, + ], + vec![ + pinata_definition_post, + pinata_token_holding_post, + winner_token_holding_post, + ], + chained_calls, + ); +} diff --git a/nssa/program_methods/guest/src/bin/token.rs b/nssa/program_methods/guest/src/bin/token.rs index e5680be..59aae6a 100644 --- a/nssa/program_methods/guest/src/bin/token.rs +++ b/nssa/program_methods/guest/src/bin/token.rs @@ -45,6 +45,21 @@ impl TokenDefinition { bytes[7..].copy_from_slice(&self.total_supply.to_le_bytes()); bytes.into() } + + fn parse(data: &[u8]) -> Option { + if data.len() != TOKEN_DEFINITION_DATA_SIZE || data[0] != TOKEN_DEFINITION_TYPE { + None + } else { + let account_type = data[0]; + let name = data[1..7].try_into().unwrap(); + let total_supply = u128::from_le_bytes(data[7..].try_into().unwrap()); + Some(Self { + account_type, + name, + total_supply, + }) + } + } } impl TokenHolding { @@ -196,15 +211,47 @@ fn main() { let post_states = transfer(&pre_states, balance_to_move); write_nssa_outputs(pre_states, post_states); } + 2 => { + // Initialize account + assert_eq!(instruction[1..], [0; 22]); + let post_states = initialize(&pre_states); + write_nssa_outputs(pre_states, post_states); + } _ => panic!("Invalid instruction"), }; } +fn initialize(pre_states: &[AccountWithMetadata]) -> Vec { + if pre_states.len() != 2 { + panic!("Invalid number of accounts"); + } + + let definition = &pre_states[0]; + let account_to_initialize = &pre_states[1]; + + if account_to_initialize.account != Account::default() { + panic!("Only uninitialized accounts can be initialized"); + } + + // TODO: We should check that this is an account owned by the token program. + // This check can't be done here since the ID of the program is known only after compiling it + // Check definition account is valid + let _definition_values = + TokenDefinition::parse(&definition.account.data).expect("Definition account must be valid"); + let holding_for_definition = TokenHolding::new(&definition.account_id); + + let definition_post = definition.account.clone(); + let mut account_to_initialize_post = account_to_initialize.account.clone(); + account_to_initialize_post.data = holding_for_definition.into_data(); + + vec![definition_post, account_to_initialize_post] +} + #[cfg(test)] mod tests { use nssa_core::account::{Account, AccountId, AccountWithMetadata}; - use crate::{new_definition, transfer, TOKEN_HOLDING_DATA_SIZE, TOKEN_HOLDING_TYPE}; + use crate::{TOKEN_HOLDING_DATA_SIZE, TOKEN_HOLDING_TYPE, new_definition, transfer}; #[should_panic(expected = "Invalid number of input accounts")] #[test] diff --git a/nssa/src/program.rs b/nssa/src/program.rs index d3f28b5..4d2232d 100644 --- a/nssa/src/program.rs +++ b/nssa/src/program.rs @@ -104,6 +104,11 @@ impl Program { // `program_methods` Self::new(PINATA_ELF.to_vec()).unwrap() } + + pub fn pinata_token() -> Self { + use crate::program_methods::PINATA_TOKEN_ELF; + Self::new(PINATA_TOKEN_ELF.to_vec()).unwrap() + } } #[cfg(test)] diff --git a/nssa/src/state.rs b/nssa/src/state.rs index 79541a3..78b8b7b 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -239,6 +239,20 @@ impl V02State { }, ); } + + pub fn add_pinata_token_program(&mut self, account_id: AccountId) { + self.insert_program(Program::pinata_token()); + + self.public_state.insert( + account_id, + Account { + program_owner: Program::pinata_token().id(), + // Difficulty: 3 + data: vec![3; 33], + ..Account::default() + }, + ); + } } #[cfg(test)] @@ -2210,4 +2224,81 @@ pub mod tests { assert_eq!(from_post.balance, initial_balance - amount); assert_eq!(to_post, expected_to_post); } + + #[test] + fn test_pda_mechanism_with_pinata_token_program() { + let pinata_token = Program::pinata_token(); + let token = Program::token(); + + let pinata_definition_id = AccountId::new([1; 32]); + let pinata_token_definition_id = AccountId::new([2; 32]); + // Total supply of pinata token will be in an account under a PDA. + let pinata_token_holding_id = AccountId::from((&pinata_token.id(), &PdaSeed::new([0; 32]))); + let winner_token_holding_id = AccountId::new([3; 32]); + + let mut expected_winner_account_data = [0; 49]; + expected_winner_account_data[0] = 1; + expected_winner_account_data[1..33].copy_from_slice(pinata_token_definition_id.value()); + 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(), + ..Account::default() + }; + + let mut state = V02State::new_with_genesis_accounts(&[], &[]); + state.add_pinata_token_program(pinata_definition_id); + + // Execution of the token program to create new token for the pinata token + // definition and supply accounts + let total_supply: u128 = 10_000_000; + // instruction: [0x00 || total_supply (little-endian 16 bytes) || name (6 bytes)] + let mut instruction: [u8; 23] = [0; 23]; + instruction[1..17].copy_from_slice(&total_supply.to_le_bytes()); + instruction[17..].copy_from_slice(b"PINATA"); + let message = public_transaction::Message::try_new( + token.id(), + vec![pinata_token_definition_id, pinata_token_holding_id], + vec![], + instruction, + ) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + let tx = PublicTransaction::new(message, witness_set); + state.transition_from_public_transaction(&tx).unwrap(); + + // Execution of the token program transfer just to initialize the winner token account + let mut instruction: [u8; 23] = [0; 23]; + instruction[0] = 2; + let message = public_transaction::Message::try_new( + token.id(), + vec![pinata_token_definition_id, winner_token_holding_id], + vec![], + instruction, + ) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + let tx = PublicTransaction::new(message, witness_set); + state.transition_from_public_transaction(&tx).unwrap(); + + // Submit a solution to the pinata program to claim the prize + let solution: u128 = 989106; + let message = public_transaction::Message::try_new( + pinata_token.id(), + vec![ + pinata_definition_id, + pinata_token_holding_id, + winner_token_holding_id, + ], + vec![], + solution, + ) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + let tx = PublicTransaction::new(message, witness_set); + state.transition_from_public_transaction(&tx).unwrap(); + + let winner_token_holding_post = state.get_account_by_id(&winner_token_holding_id); + assert_eq!(winner_token_holding_post, expected_winner_token_holding_post); + } }