Merge pull request #111 from vacp2p/schouhy/add-pinata-program

Add piñata program
This commit is contained in:
Sergio Chouhy 2025-09-24 09:44:53 -03:00 committed by GitHub
commit ae0c589d1e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 214 additions and 15 deletions

View File

@ -20,6 +20,7 @@ workspace = true
[dependencies.sequencer_core]
path = "../sequencer_core"
features = ["testnet"]
[dependencies.sequencer_runner]
path = "../sequencer_runner"

View File

@ -273,21 +273,10 @@ pub async fn test_success_two_transactions() {
info!("Second TX Success!");
}
pub async fn test_get_account_wallet_command() {
let command = Command::GetAccount {
addr: ACC_SENDER.to_string(),
};
pub async fn test_get_account() {
let wallet_config = fetch_config().unwrap();
let seq_client = SequencerClient::new(wallet_config.sequencer_addr.clone()).unwrap();
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 account");
let account = seq_client
.get_account(ACC_SENDER.to_string())
.await
@ -303,6 +292,50 @@ pub async fn test_get_account_wallet_command() {
assert_eq!(account.nonce, 0);
}
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();
@ -336,17 +369,21 @@ pub async fn main_tests_runner() -> Result<()> {
test_cleanup_wrap!(home_dir, test_failure);
}
"test_get_account_wallet_command" => {
test_cleanup_wrap!(home_dir, test_get_account_wallet_command);
test_cleanup_wrap!(home_dir, test_get_account);
}
"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_get_account_wallet_command);
test_cleanup_wrap!(home_dir, test_pinata);
test_cleanup_wrap!(home_dir, test_get_account);
}
_ => {
anyhow::bail!("Unknown test name");

View File

@ -19,3 +19,6 @@ k256 = "0.13.3"
[dev-dependencies]
test-program-methods = { path = "test_program_methods" }
hex-literal = "1.0.0"
[features]
default = []

View File

@ -34,3 +34,4 @@ fn main() {
write_nssa_outputs(vec![sender, receiver], vec![sender_post, receiver_post]);
}

View File

@ -0,0 +1,70 @@
use nssa_core::program::{ProgramInput, read_nssa_inputs, write_nssa_outputs};
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 only two accounts: [pinata_account, winner_account]
let ProgramInput {
pre_states,
instruction: solution,
} = read_nssa_inputs::<Instruction>();
let [pinata, winner] = match pre_states.try_into() {
Ok(array) => array,
Err(_) => return,
};
let data = Challenge::new(&pinata.account.data);
if !data.validate_solution(solution) {
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]);
}

View File

@ -2,7 +2,9 @@ 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;
@ -75,6 +77,16 @@ impl Program {
}
}
// TODO: Testnet only. Refactor to prevent compilation on mainnet.
impl Program {
pub fn pinata() -> Self {
Self {
id: PINATA_ID,
elf: PINATA_ELF,
}
}
}
#[cfg(test)]
mod tests {
use nssa_core::account::{Account, AccountWithMetadata};

View File

@ -205,6 +205,24 @@ impl V01State {
}
}
// 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());
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 {

View File

@ -23,3 +23,7 @@ path = "../common"
[dependencies.nssa]
path = "../nssa"
[features]
default = []
testnet = []

View File

@ -27,8 +27,16 @@ 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_program("cafe".repeat(16).parse().unwrap());
this
};
let mut data = [0; 32];
let mut prev_block_hash = [0; 32];

View File

@ -87,6 +87,24 @@ impl WalletCore {
None
}
pub async fn claim_pinata(
&self,
pinata_addr: Address,
winner_addr: Address,
solution: u128,
) -> Result<SendTxResponse, ExecutionFailureKind> {
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_public(tx).await?)
}
pub async fn send_public_native_token_transfer(
&self,
from: Address,
@ -201,6 +219,19 @@ pub enum Command {
#[arg(short, long)]
addr: String,
},
// TODO: Testnet only. Refactor to prevent compilation on 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
@ -264,6 +295,20 @@ pub async fn execute_subcommand(command: Command) -> Result<()> {
let account: HumanReadableAccount = wallet_core.get_account(addr).await?.into();
println!("{}", serde_json::to_string(&account).unwrap());
}
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:#?}");
}
}
Ok(())