From b9e0ff230f4b988518d8ad01b7f907569b339bfb Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Fri, 12 Sep 2025 15:18:25 -0300 Subject: [PATCH] add token program --- nssa/core/src/account.rs | 4 +- nssa/core/src/encoding.rs | 7 + nssa/program_methods/guest/Cargo.lock | 1 + nssa/program_methods/guest/Cargo.toml | 1 + nssa/program_methods/guest/src/bin/token.rs | 166 ++++++++++++++++++++ nssa/src/program.rs | 9 +- nssa/src/state.rs | 1 + 7 files changed, 186 insertions(+), 3 deletions(-) create mode 100644 nssa/program_methods/guest/src/bin/token.rs diff --git a/nssa/core/src/account.rs b/nssa/core/src/account.rs index fdd51e1..597f558 100644 --- a/nssa/core/src/account.rs +++ b/nssa/core/src/account.rs @@ -2,7 +2,7 @@ use crate::program::ProgramId; use serde::{Deserialize, Serialize}; pub type Nonce = u128; -type Data = Vec; +pub type Data = Vec; /// Account to be used both in public and private contexts #[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq)] @@ -18,7 +18,7 @@ pub struct Account { /// is public, or a `NullifierPublicKey` in case the account is private. #[derive(Serialize, Deserialize, Clone, PartialEq, Eq)] #[cfg_attr(any(feature = "host", test), derive(Debug))] -pub struct AccountId([u8; 32]); +pub struct AccountId(pub(super) [u8; 32]); impl AccountId { pub fn new(value: [u8; 32]) -> Self { Self(value) diff --git a/nssa/core/src/encoding.rs b/nssa/core/src/encoding.rs index dd586de..d7fc8b8 100644 --- a/nssa/core/src/encoding.rs +++ b/nssa/core/src/encoding.rs @@ -7,6 +7,7 @@ use std::io::Read; use crate::account::Account; +use crate::account::AccountId; #[cfg(feature = "host")] use crate::encryption::shared_key_derivation::Secp256k1Point; @@ -137,6 +138,12 @@ impl Secp256k1Point { } } +impl AccountId { + pub fn to_bytes(&self) -> [u8; 32] { + self.0 + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/nssa/program_methods/guest/Cargo.lock b/nssa/program_methods/guest/Cargo.lock index 18285e9..3330858 100644 --- a/nssa/program_methods/guest/Cargo.lock +++ b/nssa/program_methods/guest/Cargo.lock @@ -1817,6 +1817,7 @@ version = "0.1.0" dependencies = [ "nssa-core", "risc0-zkvm", + "serde", ] [[package]] diff --git a/nssa/program_methods/guest/Cargo.toml b/nssa/program_methods/guest/Cargo.toml index da4dbe8..59dac61 100644 --- a/nssa/program_methods/guest/Cargo.toml +++ b/nssa/program_methods/guest/Cargo.toml @@ -8,3 +8,4 @@ edition = "2024" [dependencies] risc0-zkvm = { version = "3.0.3", default-features = false, features = ['std'] } nssa-core = { path = "../../core" } +serde = { version = "1.0.219", default-features = false } diff --git a/nssa/program_methods/guest/src/bin/token.rs b/nssa/program_methods/guest/src/bin/token.rs new file mode 100644 index 0000000..caa7692 --- /dev/null +++ b/nssa/program_methods/guest/src/bin/token.rs @@ -0,0 +1,166 @@ +use nssa_core::{ + account::{Account, AccountId, AccountWithMetadata, Data}, + program::{ProgramInput, read_nssa_inputs, write_nssa_outputs}, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +enum Instruction { + Transfer(u128), + NewDefinition([u8; 6], u128), +} + +const TOKEN_DEFINITION_TYPE: u8 = 0; +const TOKEN_DEFINITION_SIZE: usize = 23; + +const TOKEN_HOLDING_TYPE: u8 = 1; +const TOKEN_HOLDING_SIZE: usize = 49; + +struct TokenDefinition { + account_type: u8, + name: [u8; 6], + total_supply: u128, +} + +struct TokenHolding { + account_type: u8, + definition_id: AccountId, + balance: u128, +} + +impl TokenDefinition { + fn into_data(self) -> Vec { + let mut bytes = [0; TOKEN_DEFINITION_SIZE]; + bytes[0] = self.account_type; + bytes[1..7].copy_from_slice(&self.name); + bytes[7..].copy_from_slice(&self.total_supply.to_le_bytes()); + bytes.into() + } +} + +impl TokenHolding { + fn new(definition_id: &AccountId) -> Self { + Self { + account_type: TOKEN_HOLDING_TYPE, + definition_id: definition_id.clone(), + balance: 0, + } + } + + fn parse(data: &[u8]) -> Option { + if data.len() != TOKEN_HOLDING_SIZE && data[0] != TOKEN_HOLDING_TYPE { + None + } else { + let account_type = data[0]; + let definition_id = AccountId::new(data[33..65].try_into().unwrap()); + let balance = u128::from_le_bytes(data[65..].try_into().unwrap()); + Some(Self { + definition_id, + balance, + account_type, + }) + } + } + + fn into_data(self) -> Data { + let mut bytes = [0; TOKEN_HOLDING_SIZE]; + bytes[0] = self.account_type; + bytes[1..33].copy_from_slice(&self.definition_id.to_bytes()); + bytes[33..].copy_from_slice(&self.balance.to_le_bytes()); + bytes.into() + } +} + +fn transfer(pre_states: Vec, balance_to_move: u128) { + let [sender, recipient] = match pre_states.try_into() { + Ok(array) => array, + Err(_) => return, + }; + + let mut sender_holding = TokenHolding::parse(&sender.account.data).unwrap(); + let mut recipient_holding = if recipient.account == Account::default() { + TokenHolding::new(&sender_holding.definition_id) + } else { + TokenHolding::parse(&recipient.account.data).unwrap() + }; + + if sender_holding.definition_id != recipient_holding.definition_id { + panic!("Sender and recipient definition id mismatch"); + } + + if sender_holding.balance < balance_to_move { + panic!("Insufficient balance"); + } + + if !sender.is_authorized { + panic!("Sender authorization is missing"); + } + + sender_holding.balance -= balance_to_move; + recipient_holding.balance += balance_to_move; + + let sender_post = { + let mut this = sender.account.clone(); + this.data = sender_holding.into_data(); + this + }; + let recipient_post = { + let mut this = recipient.account.clone(); + this.data = recipient_holding.into_data(); + this + }; + + write_nssa_outputs(vec![sender, recipient], vec![sender_post, recipient_post]); +} + +fn new_definition(pre_states: Vec, name: [u8; 6], total_supply: u128) { + let [definition_target_account, holding_target_account] = match pre_states.try_into() { + Ok(array) => array, + Err(_) => return, + }; + + if definition_target_account.account != Account::default() { + panic!("Definition target account must have default values."); + } + + if holding_target_account.account != Account::default() { + panic!("Holding target account must have default values."); + } + + let token_definition = TokenDefinition { + account_type: TOKEN_DEFINITION_TYPE, + name, + total_supply, + }; + + let token_holding = TokenHolding { + account_type: TOKEN_HOLDING_TYPE, + definition_id: definition_target_account.account_id.clone(), + balance: total_supply, + }; + + let mut definition_target_account_post = definition_target_account.account.clone(); + definition_target_account_post.data = token_definition.into_data(); + + let mut holding_target_account_post = holding_target_account.account.clone(); + holding_target_account_post.data = token_holding.into_data(); + + write_nssa_outputs( + vec![definition_target_account, holding_target_account], + vec![definition_target_account_post, holding_target_account_post], + ); +} + +fn main() { + let ProgramInput { + pre_states, + instruction, + } = read_nssa_inputs::(); + + match instruction { + Instruction::Transfer(balance_to_move) => transfer(pre_states, balance_to_move), + Instruction::NewDefinition(name, total_supply) => { + new_definition(pre_states, name, total_supply) + } + }; +} diff --git a/nssa/src/program.rs b/nssa/src/program.rs index 1096df8..9c34e1d 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, TOKEN_ELF, TOKEN_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 token() -> Self { + Self { + id: TOKEN_ID, + elf: TOKEN_ELF, + } + } } #[cfg(test)] diff --git a/nssa/src/state.rs b/nssa/src/state.rs index 4b8b25b..8f48888 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -86,6 +86,7 @@ impl V01State { }; this.insert_program(Program::authenticated_transfer_program()); + this.insert_program(Program::token()); this }