From 4a44b12384ccf2bfc01ca08c4da2a18feb8bd1c9 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Thu, 4 Sep 2025 12:44:22 -0300 Subject: [PATCH 1/3] wip --- .../guest/src/bin/authenticated_transfer.rs | 1 + nssa/program_methods/guest/src/bin/pinata.rs | 68 +++++++++++++++++++ nssa/src/program.rs | 9 ++- nssa/src/state.rs | 36 ++++++++++ sequencer_core/src/sequencer_store/mod.rs | 6 +- sequencer_runner/src/lib.rs | 4 +- 6 files changed, 121 insertions(+), 3 deletions(-) create mode 100644 nssa/program_methods/guest/src/bin/pinata.rs diff --git a/nssa/program_methods/guest/src/bin/authenticated_transfer.rs b/nssa/program_methods/guest/src/bin/authenticated_transfer.rs index 9e7f399..89b84fd 100644 --- a/nssa/program_methods/guest/src/bin/authenticated_transfer.rs +++ b/nssa/program_methods/guest/src/bin/authenticated_transfer.rs @@ -34,3 +34,4 @@ fn main() { write_nssa_outputs(vec![sender, receiver], vec![sender_post, receiver_post]); } + diff --git a/nssa/program_methods/guest/src/bin/pinata.rs b/nssa/program_methods/guest/src/bin/pinata.rs new file mode 100644 index 0000000..43b0190 --- /dev/null +++ b/nssa/program_methods/guest/src/bin/pinata.rs @@ -0,0 +1,68 @@ +use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, ProgramInput}; +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 } + } + + fn is_nonce_valid(&self, nonce: Instruction) -> bool { + let mut bytes = [0; 32 + 16]; + bytes.copy_from_slice(&self.seed); + bytes[32..].copy_from_slice(&nonce.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.copy_from_slice(Impl::hash_bytes(&self.seed).as_bytes()); + result + } +} + +/// A pinata program +fn main() { + // Read input accounts. + // It is expected to receive only two accounts: [pinata_account, winner_account] + let ProgramInput { + pre_states, + instruction: nonce, + } = read_nssa_inputs::(); + + let [pinata, winner] = match pre_states.try_into() { + Ok(array) => array, + Err(_) => return, + }; + + let data = Challenge::new(&pinata.account.data); + + if !data.is_nonce_valid(nonce) { + return; + } + + 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(); + winner_post.balance += PRIZE; + + write_nssa_outputs(vec![pinata, winner], vec![pinata_post, winner_post]); +} diff --git a/nssa/src/program.rs b/nssa/src/program.rs index 66358e9..63b7dc4 100644 --- a/nssa/src/program.rs +++ b/nssa/src/program.rs @@ -2,7 +2,7 @@ use nssa_core::{ account::{Account, AccountWithMetadata}, program::{InstructionData, ProgramId, ProgramOutput}, }; -use program_methods::{AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID}; +use program_methods::{AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID, PINATA_ELF, PINATA_ID}; use risc0_zkvm::{ExecutorEnv, ExecutorEnvBuilder, default_executor, serde::to_vec}; use serde::Serialize; @@ -73,6 +73,13 @@ impl Program { elf: AUTHENTICATED_TRANSFER_ELF, } } + + pub fn pinata() -> Self { + Self { + id: PINATA_ID, + elf: PINATA_ELF + } + } } #[cfg(test)] diff --git a/nssa/src/state.rs b/nssa/src/state.rs index f975804..ba6c49b 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -8,6 +8,7 @@ use nssa_core::{ account::Account, program::{DEFAULT_PROGRAM_ID, ProgramId}, }; +use rand::{Rng, RngCore, rngs::OsRng}; use std::collections::{HashMap, HashSet}; pub(crate) struct CommitmentSet { @@ -85,6 +86,41 @@ impl V01State { this } + pub fn add_pinata_accounts(&mut self) { + self.insert_program(Program::pinata()); + + let mut rng = OsRng; + let mut seed = [0; 32]; + + rng.fill_bytes(&mut seed); + self.public_state.insert( + "6a79aee868a1c641ea895582af7ddd6f2da339e3091a67eddcbfdaa1b9010001" + .parse() + .unwrap(), + Account { + program_owner: Program::pinata().id(), + balance: 1500, + // Difficulty: 3 + data: std::iter::once(3).chain(seed).collect(), + nonce: 0, + }, + ); + + rng.fill_bytes(&mut seed); + self.public_state.insert( + "6a79aee868a1c641ea895582af7ddd6f2da339e3091a67eddcbfdaa1b9010002" + .parse() + .unwrap(), + Account { + program_owner: Program::pinata().id(), + balance: 1500, + // Difficulty: 4 + data: std::iter::once(4).chain(seed).collect(), + nonce: 0, + }, + ); + } + pub(crate) fn insert_program(&mut self, program: Program) { self.builtin_programs.insert(program.id(), program); } diff --git a/sequencer_core/src/sequencer_store/mod.rs b/sequencer_core/src/sequencer_store/mod.rs index 4254ed7..374e7d5 100644 --- a/sequencer_core/src/sequencer_store/mod.rs +++ b/sequencer_core/src/sequencer_store/mod.rs @@ -26,7 +26,11 @@ impl SequecerChainStore { .map(|acc_data| (acc_data.addr.parse().unwrap(), acc_data.balance)) .collect(); - let state = nssa::V01State::new_with_genesis_accounts(&init_accs); + let state = { + let mut this = nssa::V01State::new_with_genesis_accounts(&init_accs); + this.add_pinata_accounts(); + this + }; let mut data = [0; 32]; let mut prev_block_hash = [0; 32]; diff --git a/sequencer_runner/src/lib.rs b/sequencer_runner/src/lib.rs index 625350b..d7a574e 100644 --- a/sequencer_runner/src/lib.rs +++ b/sequencer_runner/src/lib.rs @@ -75,7 +75,9 @@ pub async fn main_runner() -> Result<()> { } //ToDo: Add restart on failures - let (_, _) = startup_sequencer(app_config).await?; + let (_, main_loop_handle) = startup_sequencer(app_config).await?; + + main_loop_handle.await??; Ok(()) } From a432019b23392ac27bdc2adbca73380464ce1f5c Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Thu, 4 Sep 2025 17:05:12 -0300 Subject: [PATCH 2/3] wip --- integration_tests/Cargo.toml | 1 + integration_tests/src/lib.rs | 48 ++++++++++++++++++ nssa/Cargo.toml | 3 ++ nssa/program_methods/guest/src/bin/pinata.rs | 16 +++--- nssa/src/program.rs | 9 +++- nssa/src/state.rs | 53 +++++++------------- sequencer_core/Cargo.toml | 4 ++ sequencer_core/src/sequencer_store/mod.rs | 6 ++- sequencer_runner/src/lib.rs | 4 +- wallet/src/lib.rs | 52 +++++++++++++++++-- 10 files changed, 145 insertions(+), 51 deletions(-) diff --git a/integration_tests/Cargo.toml b/integration_tests/Cargo.toml index 16e1b2e..644d2e3 100644 --- a/integration_tests/Cargo.toml +++ b/integration_tests/Cargo.toml @@ -20,6 +20,7 @@ workspace = true [dependencies.sequencer_core] path = "../sequencer_core" +features = ["testnet"] [dependencies.sequencer_runner] path = "../sequencer_runner" diff --git a/integration_tests/src/lib.rs b/integration_tests/src/lib.rs index 9b7b970..cd2260f 100644 --- a/integration_tests/src/lib.rs +++ b/integration_tests/src/lib.rs @@ -272,6 +272,50 @@ pub async fn test_success_two_transactions() { info!("Second TX Success!"); } +pub async fn test_pinata() { + let pinata_addr = "cafe".repeat(16); + let pinata_prize = 150; + let solution = 989106; + let command = Command::ClaimPinata { + pinata_addr: pinata_addr.clone(), + winner_addr: ACC_SENDER.to_string(), + solution, + }; + + let wallet_config = fetch_config().unwrap(); + + let seq_client = SequencerClient::new(wallet_config.sequencer_addr.clone()).unwrap(); + + let pinata_balance_pre = seq_client + .get_account_balance(pinata_addr.clone()) + .await + .unwrap() + .balance; + + wallet::execute_subcommand(command).await.unwrap(); + + info!("Waiting for next block creation"); + tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + + info!("Checking correct balance move"); + let pinata_balance_post = seq_client + .get_account_balance(pinata_addr.clone()) + .await + .unwrap() + .balance; + + let winner_balance_post = seq_client + .get_account_balance(ACC_SENDER.to_string()) + .await + .unwrap() + .balance; + + assert_eq!(pinata_balance_post, pinata_balance_pre - pinata_prize); + assert_eq!(winner_balance_post, 10000 + pinata_prize); + + info!("Success!"); +} + macro_rules! test_cleanup_wrap { ($home_dir:ident, $test_func:ident) => {{ let res = pre_test($home_dir.clone()).await.unwrap(); @@ -307,11 +351,15 @@ pub async fn main_tests_runner() -> Result<()> { "test_success_two_transactions" => { test_cleanup_wrap!(home_dir, test_success_two_transactions); } + "test_pinata" => { + test_cleanup_wrap!(home_dir, test_pinata); + } "all" => { test_cleanup_wrap!(home_dir, test_success_move_to_another_account); test_cleanup_wrap!(home_dir, test_success); test_cleanup_wrap!(home_dir, test_failure); test_cleanup_wrap!(home_dir, test_success_two_transactions); + test_cleanup_wrap!(home_dir, test_pinata); } _ => { anyhow::bail!("Unknown test name"); diff --git a/nssa/Cargo.toml b/nssa/Cargo.toml index 3163902..25907a1 100644 --- a/nssa/Cargo.toml +++ b/nssa/Cargo.toml @@ -18,3 +18,6 @@ hex = "0.4.3" [dev-dependencies] test-program-methods = { path = "test_program_methods" } hex-literal = "1.0.0" + +[features] +default = [] diff --git a/nssa/program_methods/guest/src/bin/pinata.rs b/nssa/program_methods/guest/src/bin/pinata.rs index 43b0190..fbea167 100644 --- a/nssa/program_methods/guest/src/bin/pinata.rs +++ b/nssa/program_methods/guest/src/bin/pinata.rs @@ -1,4 +1,4 @@ -use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, ProgramInput}; +use nssa_core::program::{ProgramInput, read_nssa_inputs, write_nssa_outputs}; use risc0_zkvm::sha::{Impl, Sha256}; const PRIZE: u128 = 150; @@ -21,10 +21,12 @@ impl Challenge { Self { difficulty, seed } } - fn is_nonce_valid(&self, nonce: Instruction) -> bool { + // 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.copy_from_slice(&self.seed); - bytes[32..].copy_from_slice(&nonce.to_le_bytes()); + 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) @@ -33,7 +35,7 @@ impl Challenge { fn next_data(self) -> [u8; 33] { let mut result = [0; 33]; result[0] = self.difficulty; - result.copy_from_slice(Impl::hash_bytes(&self.seed).as_bytes()); + result[1..].copy_from_slice(Impl::hash_bytes(&self.seed).as_bytes()); result } } @@ -44,7 +46,7 @@ fn main() { // It is expected to receive only two accounts: [pinata_account, winner_account] let ProgramInput { pre_states, - instruction: nonce, + instruction: solution, } = read_nssa_inputs::(); let [pinata, winner] = match pre_states.try_into() { @@ -54,7 +56,7 @@ fn main() { let data = Challenge::new(&pinata.account.data); - if !data.is_nonce_valid(nonce) { + if !data.validate_solution(solution) { return; } diff --git a/nssa/src/program.rs b/nssa/src/program.rs index 63b7dc4..4828246 100644 --- a/nssa/src/program.rs +++ b/nssa/src/program.rs @@ -2,7 +2,9 @@ use nssa_core::{ account::{Account, AccountWithMetadata}, program::{InstructionData, ProgramId, ProgramOutput}, }; -use program_methods::{AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID, PINATA_ELF, PINATA_ID}; +use program_methods::{ + AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID, PINATA_ELF, PINATA_ID, +}; use risc0_zkvm::{ExecutorEnv, ExecutorEnvBuilder, default_executor, serde::to_vec}; use serde::Serialize; @@ -73,11 +75,14 @@ impl Program { elf: AUTHENTICATED_TRANSFER_ELF, } } +} +// TODO: This is for testnet only, consider refactoring to have this not compiled for mainnet +impl Program { pub fn pinata() -> Self { Self { id: PINATA_ID, - elf: PINATA_ELF + elf: PINATA_ELF, } } } diff --git a/nssa/src/state.rs b/nssa/src/state.rs index ba6c49b..ef90f37 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -86,41 +86,6 @@ impl V01State { this } - pub fn add_pinata_accounts(&mut self) { - self.insert_program(Program::pinata()); - - let mut rng = OsRng; - let mut seed = [0; 32]; - - rng.fill_bytes(&mut seed); - self.public_state.insert( - "6a79aee868a1c641ea895582af7ddd6f2da339e3091a67eddcbfdaa1b9010001" - .parse() - .unwrap(), - Account { - program_owner: Program::pinata().id(), - balance: 1500, - // Difficulty: 3 - data: std::iter::once(3).chain(seed).collect(), - nonce: 0, - }, - ); - - rng.fill_bytes(&mut seed); - self.public_state.insert( - "6a79aee868a1c641ea895582af7ddd6f2da339e3091a67eddcbfdaa1b9010002" - .parse() - .unwrap(), - Account { - program_owner: Program::pinata().id(), - balance: 1500, - // Difficulty: 4 - data: std::iter::once(4).chain(seed).collect(), - nonce: 0, - }, - ); - } - pub(crate) fn insert_program(&mut self, program: Program) { self.builtin_programs.insert(program.id(), program); } @@ -242,6 +207,24 @@ impl V01State { } } +// TODO: This is for testnet only, consider refactoring to have this not compiled for mainnet +impl V01State { + pub fn add_pinata_program(&mut self, address: Address) { + self.insert_program(Program::pinata()); + + self.public_state.insert( + address, + Account { + program_owner: Program::pinata().id(), + balance: 1500, + // Difficulty: 3 + data: vec![3; 33], + nonce: 0, + }, + ); + } +} + #[cfg(test)] pub mod tests { diff --git a/sequencer_core/Cargo.toml b/sequencer_core/Cargo.toml index 1c56f11..aca92ff 100644 --- a/sequencer_core/Cargo.toml +++ b/sequencer_core/Cargo.toml @@ -22,3 +22,7 @@ path = "../common" [dependencies.nssa] path = "../nssa" + +[features] +default = [] +testnet = [] diff --git a/sequencer_core/src/sequencer_store/mod.rs b/sequencer_core/src/sequencer_store/mod.rs index f9c86cd..8d8ef23 100644 --- a/sequencer_core/src/sequencer_store/mod.rs +++ b/sequencer_core/src/sequencer_store/mod.rs @@ -26,9 +26,13 @@ impl SequecerChainStore { .map(|acc_data| (acc_data.addr.parse().unwrap(), acc_data.balance)) .collect(); + #[cfg(not(feature = "testnet"))] + let state = nssa::V01State::new_with_genesis_accounts(&init_accs); + + #[cfg(feature = "testnet")] let state = { let mut this = nssa::V01State::new_with_genesis_accounts(&init_accs); - this.add_pinata_accounts(); + this.add_pinata_program("cafe".repeat(16).parse().unwrap()); this }; diff --git a/sequencer_runner/src/lib.rs b/sequencer_runner/src/lib.rs index 068d3d7..b682a18 100644 --- a/sequencer_runner/src/lib.rs +++ b/sequencer_runner/src/lib.rs @@ -77,9 +77,7 @@ pub async fn main_runner() -> Result<()> { } //ToDo: Add restart on failures - let (_, main_loop_handle) = startup_sequencer(app_config).await?; - - main_loop_handle.await??; + let (_, _) = startup_sequencer(app_config).await?; Ok(()) } diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 3814925..171dda9 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -84,6 +84,24 @@ impl WalletCore { None } + pub async fn claim_pinata( + &self, + pinata_addr: Address, + winner_addr: Address, + solution: u128, + ) -> Result { + let addresses = vec![pinata_addr, winner_addr]; + let program_id = nssa::program::Program::pinata().id(); + let message = + nssa::public_transaction::Message::try_new(program_id, addresses, 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(tx).await?) + } + pub async fn send_public_native_token_transfer( &self, from: Address, @@ -191,6 +209,20 @@ pub enum Command { #[arg(short, long)] addr: String, }, + + // TODO: This is for testnet only, consider refactoring to have this not compiled for mainnet + // Claim piñata prize + ClaimPinata { + ///pinata_addr - valid 32 byte hex string + #[arg(long)] + pinata_addr: String, + ///winner_addr - valid 32 byte hex string + #[arg(long)] + winner_addr: String, + ///solution - solution to pinata challenge + #[arg(long)] + solution: u128, + }, } ///To execute commands, env var NSSA_WALLET_HOME_DIR must be set into directory with config @@ -228,7 +260,7 @@ pub async fn execute_subcommand(command: Command) -> Result<()> { let key = wallet_core.storage.user_data.get_account_signing_key(&addr); - info!("Generated new account with addr {addr:#?}"); + println!("Generated new account with addr {addr}"); info!("With key {key:#?}"); } Command::FetchTx { tx_hash } => { @@ -243,13 +275,27 @@ pub async fn execute_subcommand(command: Command) -> Result<()> { let addr = Address::from_str(&addr)?; let balance = wallet_core.get_account_balance(addr).await?; - info!("Accounts {addr:#?} balance is {balance}"); + println!("Accounts {addr} balance is {balance}"); } Command::GetAccountNonce { addr } => { let addr = Address::from_str(&addr)?; let nonce = wallet_core.get_accounts_nonces(vec![addr]).await?[0]; - info!("Accounts {addr:#?} nonce is {nonce}"); + println!("Accounts {addr} nonce is {nonce}"); + } + Command::ClaimPinata { + pinata_addr, + winner_addr, + solution, + } => { + let res = wallet_core + .claim_pinata( + pinata_addr.parse().unwrap(), + winner_addr.parse().unwrap(), + solution, + ) + .await?; + info!("Results of tx send is {res:#?}"); } } From e2d596be2061ddd863b40bd3fb4eb81a5adfec13 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Fri, 5 Sep 2025 23:45:44 -0300 Subject: [PATCH 3/3] clippy --- nssa/src/program.rs | 2 +- nssa/src/state.rs | 3 +-- wallet/src/lib.rs | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/nssa/src/program.rs b/nssa/src/program.rs index 4828246..9577411 100644 --- a/nssa/src/program.rs +++ b/nssa/src/program.rs @@ -77,7 +77,7 @@ impl Program { } } -// TODO: This is for testnet only, consider refactoring to have this not compiled for mainnet +// TODO: Testnet only. Refactor to prevent compilation on mainnet. impl Program { pub fn pinata() -> Self { Self { diff --git a/nssa/src/state.rs b/nssa/src/state.rs index ef90f37..3fe0b93 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -8,7 +8,6 @@ use nssa_core::{ account::Account, program::{DEFAULT_PROGRAM_ID, ProgramId}, }; -use rand::{Rng, RngCore, rngs::OsRng}; use std::collections::{HashMap, HashSet}; pub(crate) struct CommitmentSet { @@ -207,7 +206,7 @@ impl V01State { } } -// TODO: This is for testnet only, consider refactoring to have this not compiled for mainnet +// TODO: Testnet only. Refactor to prevent compilation on mainnet. impl V01State { pub fn add_pinata_program(&mut self, address: Address) { self.insert_program(Program::pinata()); diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 171dda9..0ea3892 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -210,7 +210,7 @@ pub enum Command { addr: String, }, - // TODO: This is for testnet only, consider refactoring to have this not compiled for mainnet + // TODO: Testnet only. Refactor to prevent compilation on mainnet. // Claim piñata prize ClaimPinata { ///pinata_addr - valid 32 byte hex string