Merge pull request #116 from vacp2p/schouhy/add-token-program

Add token program
This commit is contained in:
Sergio Chouhy 2025-09-29 21:31:02 -03:00 committed by GitHub
commit fea132ef24
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 876 additions and 22 deletions

5
ci_scripts/test-ubuntu.sh Normal file → Executable file
View File

@ -5,10 +5,15 @@ curl -L https://risczero.com/install | bash
source env.sh
RISC0_DEV_MODE=1 cargo test --release
cd integration_tests
export NSSA_WALLET_HOME_DIR=$(pwd)/configs/debug/wallet/
export RUST_LOG=info
cargo run $(pwd)/configs/debug all
echo "Try test valid proof at least once"
cargo run $(pwd)/configs/debug test_success_private_transfer_to_another_owned_account
echo "Continuing in dev mode"
RISC0_DEV_MODE=1 cargo run $(pwd)/configs/debug all
cd ..
cd nssa/program_methods/guest && cargo test --release

View File

@ -8,6 +8,7 @@ anyhow.workspace = true
env_logger.workspace = true
log.workspace = true
actix.workspace = true
bytemuck = "1.23.2"
actix-web.workspace = true
tokio.workspace = true

View File

@ -13,6 +13,7 @@ use tempfile::TempDir;
use tokio::task::JoinHandle;
use wallet::{
Command, SubcommandReturnValue, WalletCore,
config::PersistentAccountData,
helperfunctions::{fetch_config, fetch_persistent_accounts, produce_account_addr_from_hex},
};
@ -303,6 +304,152 @@ pub async fn test_get_account() {
assert_eq!(account.nonce, 0);
}
/// This test creates a new token using the token program. After creating the token, the test executes a
/// token transfer to a new account.
pub async fn test_success_token_program() {
let wallet_config = fetch_config().unwrap();
// Create new account for the token definition
wallet::execute_subcommand(Command::RegisterAccountPublic {})
.await
.unwrap();
// Create new account for the token supply holder
wallet::execute_subcommand(Command::RegisterAccountPublic {})
.await
.unwrap();
// Create new account for receiving a token transaction
wallet::execute_subcommand(Command::RegisterAccountPublic {})
.await
.unwrap();
let persistent_accounts = fetch_persistent_accounts().unwrap();
let mut new_persistent_accounts_addr = Vec::new();
for per_acc in persistent_accounts {
match per_acc {
PersistentAccountData::Public(per_acc) => {
if (per_acc.address.to_string() != ACC_RECEIVER)
&& (per_acc.address.to_string() != ACC_SENDER)
{
new_persistent_accounts_addr.push(per_acc.address);
}
}
_ => continue,
}
}
let [definition_addr, supply_addr, recipient_addr] = new_persistent_accounts_addr
.try_into()
.expect("Failed to produce new account, not present in persistent accounts");
// Create new token
let command = Command::CreateNewToken {
definition_addr: definition_addr.to_string(),
supply_addr: supply_addr.to_string(),
name: "A NAME".to_string(),
total_supply: 37,
};
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;
let seq_client = SequencerClient::new(wallet_config.sequencer_addr.clone()).unwrap();
// Check the status of the token definition account is the expected after the execution
let definition_acc = seq_client
.get_account(definition_addr.to_string())
.await
.unwrap()
.account;
assert_eq!(definition_acc.program_owner, Program::token().id());
// The data of a token definition account has the following layout:
// [ 0x00 || name (6 bytes) || total supply (little endian 16 bytes) ]
assert_eq!(
definition_acc.data,
vec![
0, 65, 32, 78, 65, 77, 69, 37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]
);
// Check the status of the token holding account with the total supply is the expected after the execution
let supply_acc = seq_client
.get_account(supply_addr.to_string())
.await
.unwrap()
.account;
// The account must be owned by the token program
assert_eq!(supply_acc.program_owner, Program::token().id());
// The data of a token definition account has the following layout:
// [ 0x01 || corresponding_token_definition_id (32 bytes) || balance (little endian 16 bytes) ]
// First byte of the data equal to 1 means it's a token holding account
assert_eq!(supply_acc.data[0], 1);
// Bytes from 1 to 33 represent the id of the token this account is associated with.
// In this example, this is a token account of the newly created token, so it is expected
// to be equal to the address of the token definition account.
assert_eq!(
&supply_acc.data[1..33],
nssa::AccountId::from(&definition_addr).to_bytes()
);
assert_eq!(
u128::from_le_bytes(supply_acc.data[33..].try_into().unwrap()),
37
);
// Transfer 7 tokens from `supply_acc` to the account at address `recipient_addr`
let command = Command::TransferToken {
sender_addr: supply_addr.to_string(),
recipient_addr: recipient_addr.to_string(),
balance_to_move: 7,
};
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;
// Check the status of the account at `supply_addr` is the expected after the execution
let supply_acc = seq_client
.get_account(supply_addr.to_string())
.await
.unwrap()
.account;
// The account must be owned by the token program
assert_eq!(supply_acc.program_owner, Program::token().id());
// First byte equal to 1 means it's a token holding account
assert_eq!(supply_acc.data[0], 1);
// Bytes from 1 to 33 represent the id of the token this account is associated with.
assert_eq!(
&supply_acc.data[1..33],
nssa::AccountId::from(&definition_addr).to_bytes()
);
assert_eq!(
u128::from_le_bytes(supply_acc.data[33..].try_into().unwrap()),
30
);
// Check the status of the account at `recipient_addr` is the expected after the execution
let recipient_acc = seq_client
.get_account(recipient_addr.to_string())
.await
.unwrap()
.account;
// The account must be owned by the token program
assert_eq!(recipient_acc.program_owner, Program::token().id());
// First byte equal to 1 means it's a token holding account
assert_eq!(recipient_acc.data[0], 1);
// Bytes from 1 to 33 represent the id of the token this account is associated with.
assert_eq!(
&recipient_acc.data[1..33],
nssa::AccountId::from(&definition_addr).to_bytes()
);
assert_eq!(
u128::from_le_bytes(recipient_acc.data[33..].try_into().unwrap()),
7
);
}
pub async fn test_success_private_transfer_to_another_owned_account() {
info!("test_success_private_transfer_to_another_owned_account");
let command = Command::SendNativeTokenTransferPrivate {
@ -846,6 +993,9 @@ pub async fn main_tests_runner() -> Result<()> {
} = args;
match test_name.as_str() {
"test_success_token_program" => {
test_cleanup_wrap!(home_dir, test_success_token_program);
}
"test_success_move_to_another_account" => {
test_cleanup_wrap!(home_dir, test_success_move_to_another_account);
}
@ -855,7 +1005,7 @@ pub async fn main_tests_runner() -> Result<()> {
"test_failure" => {
test_cleanup_wrap!(home_dir, test_failure);
}
"test_get_account_wallet_command" => {
"test_get_account" => {
test_cleanup_wrap!(home_dir, test_get_account);
}
"test_success_two_transactions" => {
@ -911,6 +1061,7 @@ pub async fn main_tests_runner() -> Result<()> {
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_success_token_program);
test_cleanup_wrap!(
home_dir,
test_success_private_transfer_to_another_owned_account

View File

@ -2,7 +2,7 @@ use crate::program::ProgramId;
use serde::{Deserialize, Serialize};
pub type Nonce = u128;
type Data = Vec<u8>;
pub type Data = Vec<u8>;
/// 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)

View File

@ -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::*;

View File

@ -59,20 +59,23 @@ pub fn validate_execution(
return false;
}
// 3. Ownership change only allowed from default accounts
if pre.account.program_owner != post.program_owner && pre.account != Account::default() {
// 3. Program ownership changes are not allowed
if pre.account.program_owner != post.program_owner {
return false;
}
let account_program_owner = pre.account.program_owner;
// 4. Decreasing balance only allowed if owned by executing program
if post.balance < pre.account.balance && pre.account.program_owner != executing_program_id {
if post.balance < pre.account.balance && account_program_owner != executing_program_id {
return false;
}
// 5. Data changes only allowed if owned by executing program
// 5. Data changes only allowed if owned by executing program or if account pre state has
// default values
if pre.account.data != post.data
&& (executing_program_id != pre.account.program_owner
|| executing_program_id != post.program_owner)
&& pre.account != Account::default()
&& account_program_owner != executing_program_id
{
return false;
}

View File

@ -8,3 +8,4 @@ edition = "2024"
[dependencies]
risc0-zkvm = { version = "3.0.3", features = ['std'] }
nssa-core = { path = "../../core" }
serde = { version = "1.0.219", default-features = false }

View File

@ -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};
/// A transfer of balance program.
/// To be used both in public and private contexts.

View File

@ -1,7 +1,12 @@
use risc0_zkvm::{guest::env, serde::to_vec};
use nssa_core::{
account::{Account, AccountWithMetadata, AccountId}, compute_digest_for_path, encryption::Ciphertext, program::{validate_execution, ProgramOutput, DEFAULT_PROGRAM_ID}, Commitment, CommitmentSetDigest, EncryptionScheme, Nullifier, NullifierPublicKey, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput
Commitment, CommitmentSetDigest, EncryptionScheme, Nullifier, NullifierPublicKey,
PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput,
account::{Account, AccountId, AccountWithMetadata},
compute_digest_for_path,
encryption::Ciphertext,
program::{DEFAULT_PROGRAM_ID, ProgramOutput, validate_execution},
};
fn main() {

View File

@ -0,0 +1,554 @@
use nssa_core::{
account::{Account, AccountId, AccountWithMetadata, Data},
program::{ProgramInput, read_nssa_inputs, write_nssa_outputs},
};
// The token program has two functions:
// 1. New token definition.
// Arguments to this function are:
// * Two **default** accounts: [definition_account, holding_account].
// The first default account will be initialized with the token definition account values. The second account will
// be initialized to a token holding account for the new token, holding the entire total supply.
// * An instruction data of 23-bytes, indicating the total supply and the token name, with
// the following layout:
// [0x00 || total_supply (little-endian 16 bytes) || name (6 bytes)]
// The name cannot be equal to [0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
// 2. Token transfer
// Arguments to this function are:
// * Two accounts: [sender_account, recipient_account].
// * An instruction data byte string of length 23, indicating the total supply with the following layout
// [0x01 || amount (little-endian 16 bytes) || 0x00 || 0x00 || 0x00 || 0x00 || 0x00 || 0x00].
const TOKEN_DEFINITION_TYPE: u8 = 0;
const TOKEN_DEFINITION_DATA_SIZE: usize = 23;
const TOKEN_HOLDING_TYPE: u8 = 1;
const TOKEN_HOLDING_DATA_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<u8> {
let mut bytes = [0; TOKEN_DEFINITION_DATA_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<Self> {
if data.len() != TOKEN_HOLDING_DATA_SIZE || data[0] != TOKEN_HOLDING_TYPE {
None
} else {
let account_type = data[0];
let definition_id = AccountId::new(data[1..33].try_into().unwrap());
let balance = u128::from_le_bytes(data[33..].try_into().unwrap());
Some(Self {
definition_id,
balance,
account_type,
})
}
}
fn into_data(self) -> Data {
let mut bytes = [0; TOKEN_HOLDING_DATA_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: &[AccountWithMetadata], balance_to_move: u128) -> Vec<Account> {
if pre_states.len() != 2 {
panic!("Invalid number of input accounts");
}
let sender = &pre_states[0];
let recipient = &pre_states[1];
let mut sender_holding =
TokenHolding::parse(&sender.account.data).expect("Invalid sender data");
let mut recipient_holding = if recipient.account == Account::default() {
TokenHolding::new(&sender_holding.definition_id)
} else {
TokenHolding::parse(&recipient.account.data).expect("Invalid recipient data")
};
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 = recipient_holding
.balance
.checked_add(balance_to_move)
.expect("Recipient balance overflow.");
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
};
vec![sender_post, recipient_post]
}
fn new_definition(
pre_states: &[AccountWithMetadata],
name: [u8; 6],
total_supply: u128,
) -> Vec<Account> {
if pre_states.len() != 2 {
panic!("Invalid number of input accounts");
}
let definition_target_account = &pre_states[0];
let holding_target_account = &pre_states[1];
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();
vec![definition_target_account_post, holding_target_account_post]
}
type Instruction = [u8; 23];
fn main() {
let ProgramInput {
pre_states,
instruction,
} = read_nssa_inputs::<Instruction>();
match instruction[0] {
0 => {
// Parse instruction
let total_supply = u128::from_le_bytes(instruction[1..17].try_into().unwrap());
let name: [u8; 6] = instruction[17..].try_into().unwrap();
assert_ne!(name, [0; 6]);
// Execute
let post_states = new_definition(&pre_states, name, total_supply);
write_nssa_outputs(pre_states, post_states);
}
1 => {
// Parse instruction
let balance_to_move = u128::from_le_bytes(instruction[1..17].try_into().unwrap());
let name: [u8; 6] = instruction[17..].try_into().unwrap();
assert_eq!(name, [0; 6]);
// Execute
let post_states = transfer(&pre_states, balance_to_move);
write_nssa_outputs(pre_states, post_states);
}
_ => panic!("Invalid instruction"),
};
}
#[cfg(test)]
mod tests {
use nssa_core::account::{Account, AccountId, AccountWithMetadata};
use crate::{new_definition, transfer, TOKEN_HOLDING_DATA_SIZE, TOKEN_HOLDING_TYPE};
#[should_panic(expected = "Invalid number of input accounts")]
#[test]
fn test_call_new_definition_with_invalid_number_of_accounts_1() {
let pre_states = vec![AccountWithMetadata {
account: Account::default(),
is_authorized: true,
account_id: AccountId::new([1; 32]),
}];
let _post_states = new_definition(&pre_states, [0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe], 10);
}
#[should_panic(expected = "Invalid number of input accounts")]
#[test]
fn test_call_new_definition_with_invalid_number_of_accounts_2() {
let pre_states = vec![
AccountWithMetadata {
account: Account::default(),
is_authorized: true,
account_id: AccountId::new([1; 32]),
},
AccountWithMetadata {
account: Account::default(),
is_authorized: true,
account_id: AccountId::new([2; 32]),
},
AccountWithMetadata {
account: Account::default(),
is_authorized: true,
account_id: AccountId::new([3; 32]),
},
];
let _post_states = new_definition(&pre_states, [0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe], 10);
}
#[should_panic(expected = "Definition target account must have default values")]
#[test]
fn test_new_definition_non_default_first_account_should_fail() {
let pre_states = vec![
AccountWithMetadata {
account: Account {
program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
..Account::default()
},
is_authorized: true,
account_id: AccountId::new([1; 32]),
},
AccountWithMetadata {
account: Account::default(),
is_authorized: true,
account_id: AccountId::new([2; 32]),
},
];
let _post_states = new_definition(&pre_states, [0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe], 10);
}
#[should_panic(expected = "Holding target account must have default values")]
#[test]
fn test_new_definition_non_default_second_account_should_fail() {
let pre_states = vec![
AccountWithMetadata {
account: Account::default(),
is_authorized: true,
account_id: AccountId::new([1; 32]),
},
AccountWithMetadata {
account: Account {
program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
..Account::default()
},
is_authorized: true,
account_id: AccountId::new([2; 32]),
},
];
let _post_states = new_definition(&pre_states, [0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe], 10);
}
#[test]
fn test_new_definition_with_valid_inputs_succeeds() {
let pre_states = vec![
AccountWithMetadata {
account: Account::default(),
is_authorized: false,
account_id: AccountId::new([
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22,
23, 24, 25, 26, 27, 28, 29, 30, 31, 32,
]),
},
AccountWithMetadata {
account: Account {
..Account::default()
},
is_authorized: false,
account_id: AccountId::new([2; 32]),
},
];
let post_states = new_definition(&pre_states, [0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe], 10);
let [definition_account, holding_account] = post_states.try_into().ok().unwrap();
assert_eq!(
definition_account.data,
vec![
0, 0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0
]
);
assert_eq!(
holding_account.data,
vec![
1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22,
23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0
]
);
}
#[should_panic(expected = "Invalid number of input accounts")]
#[test]
fn test_call_transfer_with_invalid_number_of_accounts_1() {
let pre_states = vec![AccountWithMetadata {
account: Account::default(),
is_authorized: true,
account_id: AccountId::new([1; 32]),
}];
let _post_states = transfer(&pre_states, 10);
}
#[should_panic(expected = "Invalid number of input accounts")]
#[test]
fn test_call_transfer_with_invalid_number_of_accounts_2() {
let pre_states = vec![
AccountWithMetadata {
account: Account::default(),
is_authorized: true,
account_id: AccountId::new([1; 32]),
},
AccountWithMetadata {
account: Account::default(),
is_authorized: true,
account_id: AccountId::new([2; 32]),
},
AccountWithMetadata {
account: Account::default(),
is_authorized: true,
account_id: AccountId::new([3; 32]),
},
];
let _post_states = transfer(&pre_states, 10);
}
#[should_panic(expected = "Invalid sender data")]
#[test]
fn test_transfer_invalid_instruction_type_should_fail() {
let invalid_type = TOKEN_HOLDING_TYPE ^ 1;
let pre_states = vec![
AccountWithMetadata {
account: Account {
// First byte should be `TOKEN_HOLDING_TYPE` for token holding accounts
data: vec![invalid_type; TOKEN_HOLDING_DATA_SIZE],
..Account::default()
},
is_authorized: true,
account_id: AccountId::new([1; 32]),
},
AccountWithMetadata {
account: Account::default(),
is_authorized: true,
account_id: AccountId::new([2; 32]),
},
];
let _post_states = transfer(&pre_states, 10);
}
#[should_panic(expected = "Invalid sender data")]
#[test]
fn test_transfer_invalid_data_size_should_fail_1() {
let pre_states = vec![
AccountWithMetadata {
account: Account {
// Data must be of exact length `TOKEN_HOLDING_DATA_SIZE`
data: vec![1; TOKEN_HOLDING_DATA_SIZE - 1],
..Account::default()
},
is_authorized: true,
account_id: AccountId::new([1; 32]),
},
AccountWithMetadata {
account: Account::default(),
is_authorized: true,
account_id: AccountId::new([2; 32]),
},
];
let _post_states = transfer(&pre_states, 10);
}
#[should_panic(expected = "Invalid sender data")]
#[test]
fn test_transfer_invalid_data_size_should_fail_2() {
let pre_states = vec![
AccountWithMetadata {
account: Account {
// Data must be of exact length `TOKEN_HOLDING_DATA_SIZE`
data: vec![1; TOKEN_HOLDING_DATA_SIZE + 1],
..Account::default()
},
is_authorized: true,
account_id: AccountId::new([1; 32]),
},
AccountWithMetadata {
account: Account::default(),
is_authorized: true,
account_id: AccountId::new([2; 32]),
},
];
let _post_states = transfer(&pre_states, 10);
}
#[should_panic(expected = "Sender and recipient definition id mismatch")]
#[test]
fn test_transfer_with_different_definition_ids_should_fail() {
let pre_states = vec![
AccountWithMetadata {
account: Account {
data: vec![1; TOKEN_HOLDING_DATA_SIZE],
..Account::default()
},
is_authorized: true,
account_id: AccountId::new([1; 32]),
},
AccountWithMetadata {
account: Account {
data: vec![1]
.into_iter()
.chain(vec![2; TOKEN_HOLDING_DATA_SIZE - 1])
.collect(),
..Account::default()
},
is_authorized: true,
account_id: AccountId::new([2; 32]),
},
];
let _post_states = transfer(&pre_states, 10);
}
#[should_panic(expected = "Insufficient balance")]
#[test]
fn test_transfer_with_insufficient_balance_should_fail() {
let pre_states = vec![
AccountWithMetadata {
account: Account {
// Account with balance 37
data: vec![1; TOKEN_HOLDING_DATA_SIZE - 16]
.into_iter()
.chain(u128::to_le_bytes(37))
.collect(),
..Account::default()
},
is_authorized: true,
account_id: AccountId::new([1; 32]),
},
AccountWithMetadata {
account: Account {
data: vec![1; TOKEN_HOLDING_DATA_SIZE],
..Account::default()
},
is_authorized: true,
account_id: AccountId::new([2; 32]),
},
];
// Attempt to transfer 38 tokens
let _post_states = transfer(&pre_states, 38);
}
#[should_panic(expected = "Sender authorization is missing")]
#[test]
fn test_transfer_without_sender_authorization_should_fail() {
let pre_states = vec![
AccountWithMetadata {
account: Account {
// Account with balance 37
data: vec![1; TOKEN_HOLDING_DATA_SIZE - 16]
.into_iter()
.chain(u128::to_le_bytes(37))
.collect(),
..Account::default()
},
is_authorized: false,
account_id: AccountId::new([1; 32]),
},
AccountWithMetadata {
account: Account {
data: vec![1; TOKEN_HOLDING_DATA_SIZE],
..Account::default()
},
is_authorized: true,
account_id: AccountId::new([2; 32]),
},
];
let _post_states = transfer(&pre_states, 37);
}
#[test]
fn test_transfer_with_valid_inputs_succeeds() {
let pre_states = vec![
AccountWithMetadata {
account: Account {
// Account with balance 37
data: vec![1; TOKEN_HOLDING_DATA_SIZE - 16]
.into_iter()
.chain(u128::to_le_bytes(37))
.collect(),
..Account::default()
},
is_authorized: true,
account_id: AccountId::new([1; 32]),
},
AccountWithMetadata {
account: Account {
// Account with balance 255
data: vec![1; TOKEN_HOLDING_DATA_SIZE - 16]
.into_iter()
.chain(u128::to_le_bytes(255))
.collect(),
..Account::default()
},
is_authorized: true,
account_id: AccountId::new([2; 32]),
},
];
let post_states = transfer(&pre_states, 11);
let [sender_post, recipient_post] = post_states.try_into().ok().unwrap();
assert_eq!(
sender_post.data,
vec![
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]
);
assert_eq!(
recipient_post.data,
vec![
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 10, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]
);
}
}

View File

@ -9,7 +9,7 @@ mod signature;
mod state;
pub use address::Address;
pub use nssa_core::account::Account;
pub use nssa_core::account::{Account, AccountId};
pub use privacy_preserving_transaction::{
PrivacyPreservingTransaction, circuit::execute_and_prove,
};

View File

@ -3,7 +3,8 @@ use nssa_core::{
program::{InstructionData, ProgramId, ProgramOutput},
};
use program_methods::{
AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID, PINATA_ELF, PINATA_ID,
AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID, PINATA_ELF, PINATA_ID, TOKEN_ELF,
TOKEN_ID,
};
use risc0_zkvm::{ExecutorEnv, ExecutorEnvBuilder, default_executor, serde::to_vec};
use serde::Serialize;
@ -75,6 +76,13 @@ impl Program {
elf: AUTHENTICATED_TRANSFER_ELF,
}
}
pub fn token() -> Self {
Self {
id: TOKEN_ID,
elf: TOKEN_ELF,
}
}
}
// TODO: Testnet only. Refactor to prevent compilation on mainnet.

View File

@ -92,6 +92,7 @@ impl V01State {
};
this.insert_program(Program::authenticated_transfer_program());
this.insert_program(Program::token());
this
}
@ -275,14 +276,14 @@ pub mod tests {
let addr1 = Address::from(&PublicKey::new_from_private_key(&key1));
let addr2 = Address::from(&PublicKey::new_from_private_key(&key2));
let initial_data = [(addr1, 100u128), (addr2, 151u128)];
let program = Program::authenticated_transfer_program();
let authenticated_transfers_program = Program::authenticated_transfer_program();
let expected_public_state = {
let mut this = HashMap::new();
this.insert(
addr1,
Account {
balance: 100,
program_owner: program.id(),
program_owner: authenticated_transfers_program.id(),
..Account::default()
},
);
@ -290,7 +291,7 @@ pub mod tests {
addr2,
Account {
balance: 151,
program_owner: program.id(),
program_owner: authenticated_transfers_program.id(),
..Account::default()
},
);
@ -298,7 +299,11 @@ pub mod tests {
};
let expected_builtin_programs = {
let mut this = HashMap::new();
this.insert(program.id(), program);
this.insert(
authenticated_transfers_program.id(),
authenticated_transfers_program,
);
this.insert(Program::token().id(), Program::token());
this
};
@ -683,13 +688,13 @@ pub mod tests {
#[test]
fn test_program_should_fail_if_modifies_data_of_non_owned_account() {
let initial_data = [];
let mut state =
V01State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs();
let address = Address::new([1; 32]);
let mut state = V01State::new_with_genesis_accounts(&initial_data, &[])
.with_test_programs()
.with_non_default_accounts_but_default_program_owners();
let address = Address::new([255; 32]);
let program_id = Program::data_changer().id();
// Consider the extreme case where the target account is the default account
assert_eq!(state.get_account_by_address(&address), Account::default());
assert_ne!(state.get_account_by_address(&address), Account::default());
assert_ne!(
state.get_account_by_address(&address).program_owner,
program_id

View File

@ -104,6 +104,63 @@ impl WalletCore {
Ok(self.sequencer_client.send_tx_public(tx).await?)
}
pub async fn send_new_token_definition(
&self,
definition_address: Address,
supply_address: Address,
name: [u8; 6],
total_supply: u128,
) -> Result<SendTxResponse, ExecutionFailureKind> {
let addresses = vec![definition_address, supply_address];
let program_id = nssa::program::Program::token().id();
// Instruction must be: [0x00 || total_supply (little-endian 16 bytes) || name (6 bytes)]
let mut instruction = [0; 23];
instruction[1..17].copy_from_slice(&total_supply.to_le_bytes());
instruction[17..].copy_from_slice(&name);
let message =
nssa::public_transaction::Message::try_new(program_id, addresses, vec![], instruction)
.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_transfer_token_transaction(
&self,
sender_address: Address,
recipient_address: Address,
amount: u128,
) -> Result<SendTxResponse, ExecutionFailureKind> {
let addresses = vec![sender_address, recipient_address];
let program_id = nssa::program::Program::token().id();
// Instruction must be: [0x01 || amount (little-endian 16 bytes) || 0x00 || 0x00 || 0x00 || 0x00 || 0x00 || 0x00].
let mut instruction = [0; 23];
instruction[0] = 0x01;
instruction[1..17].copy_from_slice(&amount.to_le_bytes());
let Ok(nonces) = self.get_accounts_nonces(vec![sender_address]).await else {
return Err(ExecutionFailureKind::SequencerError);
};
let message =
nssa::public_transaction::Message::try_new(program_id, addresses, nonces, instruction)
.unwrap();
let Some(signing_key) = self
.storage
.user_data
.get_pub_account_signing_key(&sender_address)
else {
return Err(ExecutionFailureKind::KeyNotFoundError);
};
let witness_set =
nssa::public_transaction::WitnessSet::for_message(&message, &[signing_key]);
let tx = nssa::PublicTransaction::new(message, witness_set);
Ok(self.sequencer_client.send_tx_public(tx).await?)
}
///Get account balance
pub async fn get_account_balance(&self, acc: Address) -> Result<u128> {
Ok(self
@ -268,6 +325,26 @@ pub enum Command {
#[arg(short, long)]
addr: String,
},
//Create a new token using the token program
CreateNewToken {
#[arg(short, long)]
definition_addr: String,
#[arg(short, long)]
supply_addr: String,
#[arg(short, long)]
name: String,
#[arg(short, long)]
total_supply: u128,
},
//Transfer tokens using the token program
TransferToken {
#[arg(short, long)]
sender_addr: String,
#[arg(short, long)]
recipient_addr: String,
#[arg(short, long)]
balance_to_move: u128,
},
// TODO: Testnet only. Refactor to prevent compilation on mainnet.
// Claim piñata prize
ClaimPinata {
@ -678,6 +755,43 @@ pub async fn execute_subcommand(command: Command) -> Result<SubcommandReturnValu
SubcommandReturnValue::Empty
}
Command::CreateNewToken {
definition_addr,
supply_addr,
name,
total_supply,
} => {
let name = name.as_bytes();
if name.len() > 6 {
// TODO: return error
panic!();
}
let mut name_bytes = [0; 6];
name_bytes[..name.len()].copy_from_slice(name);
wallet_core
.send_new_token_definition(
definition_addr.parse().unwrap(),
supply_addr.parse().unwrap(),
name_bytes,
total_supply,
)
.await?;
SubcommandReturnValue::Empty
}
Command::TransferToken {
sender_addr,
recipient_addr,
balance_to_move,
} => {
wallet_core
.send_transfer_token_transaction(
sender_addr.parse().unwrap(),
recipient_addr.parse().unwrap(),
balance_to_move,
)
.await?;
SubcommandReturnValue::Empty
}
Command::ClaimPinata {
pinata_addr,
winner_addr,