add pinata token example and test

This commit is contained in:
Sergio Chouhy 2025-11-28 11:10:00 -03:00
parent e61a971790
commit 1989fd25a1
5 changed files with 248 additions and 2 deletions

View File

@ -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 = []

View File

@ -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::<Instruction>();
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,
);
}

View File

@ -45,6 +45,21 @@ impl TokenDefinition {
bytes[7..].copy_from_slice(&self.total_supply.to_le_bytes());
bytes.into()
}
fn parse(data: &[u8]) -> Option<Self> {
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<Account> {
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]

View File

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

View File

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