Merge pull request #226 from logos-blockchain/main

main into burn
This commit is contained in:
jonesmarvin8 2025-12-07 20:27:27 -05:00 committed by GitHub
commit c6e68ff9b1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 759 additions and 123 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

@ -1,6 +1,8 @@
use risc0_zkvm::{DeserializeOwned, guest::env, serde::Deserializer};
use serde::{Deserialize, Serialize};
#[cfg(feature = "host")]
use crate::account::AccountId;
use crate::account::{Account, AccountWithMetadata};
pub type ProgramId = [u32; 8];
@ -12,19 +14,105 @@ pub struct ProgramInput<T> {
pub instruction: T,
}
/// A 32-byte seed used to compute a *Program-Derived AccountId* (PDA).
///
/// Each program can derive up to `2^256` unique account IDs by choosing different
/// seeds. PDAs allow programs to control namespaced account identifiers without
/// collisions between programs.
#[derive(Serialize, Deserialize, Clone)]
#[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))]
pub struct PdaSeed([u8; 32]);
impl PdaSeed {
pub fn new(value: [u8; 32]) -> Self {
Self(value)
}
}
#[cfg(feature = "host")]
impl From<(&ProgramId, &PdaSeed)> for AccountId {
fn from(value: (&ProgramId, &PdaSeed)) -> Self {
use risc0_zkvm::sha::{Impl, Sha256};
const PROGRAM_DERIVED_ACCOUNT_ID_PREFIX: &[u8; 32] =
b"/NSSA/v0.2/AccountId/PDA/\x00\x00\x00\x00\x00\x00\x00";
let mut bytes = [0; 96];
bytes[0..32].copy_from_slice(PROGRAM_DERIVED_ACCOUNT_ID_PREFIX);
let program_id_bytes: &[u8] =
bytemuck::try_cast_slice(value.0).expect("ProgramId should be castable to &[u8]");
bytes[32..64].copy_from_slice(program_id_bytes);
bytes[64..].copy_from_slice(&value.1.0);
AccountId::new(
Impl::hash_bytes(&bytes)
.as_bytes()
.try_into()
.expect("Hash output must be exactly 32 bytes long"),
)
}
}
#[derive(Serialize, Deserialize, Clone)]
#[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))]
pub struct ChainedCall {
pub program_id: ProgramId,
pub instruction_data: InstructionData,
pub pre_states: Vec<AccountWithMetadata>,
pub pda_seeds: Vec<PdaSeed>,
}
/// Represents the final state of an `Account` after a program execution.
/// A post state may optionally request that the executing program
/// becomes the owner of the account (a “claim”). This is used to signal
/// that the program intends to take ownership of the account.
#[derive(Serialize, Deserialize, Clone)]
#[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))]
pub struct AccountPostState {
account: Account,
claim: bool,
}
impl AccountPostState {
/// Creates a post state without a claim request.
/// The executing program is not requesting ownership of the account.
pub fn new(account: Account) -> Self {
Self {
account,
claim: false,
}
}
/// Creates a post state that requests ownership of the account.
/// This indicates that the executing program intends to claim the
/// account as its own and is allowed to mutate it.
pub fn new_claimed(account: Account) -> Self {
Self {
account,
claim: true,
}
}
/// Returns `true` if this post state requests that the account
/// be claimed (owned) by the executing program.
pub fn requires_claim(&self) -> bool {
self.claim
}
/// Returns the underlying account
pub fn account(&self) -> &Account {
&self.account
}
/// Returns the underlying account
pub fn account_mut(&mut self) -> &mut Account {
&mut self.account
}
}
#[derive(Serialize, Deserialize, Clone)]
#[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))]
pub struct ProgramOutput {
pub pre_states: Vec<AccountWithMetadata>,
pub post_states: Vec<Account>,
pub post_states: Vec<AccountPostState>,
pub chained_calls: Vec<ChainedCall>,
}
@ -38,7 +126,10 @@ pub fn read_nssa_inputs<T: DeserializeOwned>() -> ProgramInput<T> {
}
}
pub fn write_nssa_outputs(pre_states: Vec<AccountWithMetadata>, post_states: Vec<Account>) {
pub fn write_nssa_outputs(
pre_states: Vec<AccountWithMetadata>,
post_states: Vec<AccountPostState>,
) {
let output = ProgramOutput {
pre_states,
post_states,
@ -49,7 +140,7 @@ pub fn write_nssa_outputs(pre_states: Vec<AccountWithMetadata>, post_states: Vec
pub fn write_nssa_outputs_with_chained_call(
pre_states: Vec<AccountWithMetadata>,
post_states: Vec<Account>,
post_states: Vec<AccountPostState>,
chained_calls: Vec<ChainedCall>,
) {
let output = ProgramOutput {
@ -68,7 +159,7 @@ pub fn write_nssa_outputs_with_chained_call(
/// - `executing_program_id`: The identifier of the program that was executed.
pub fn validate_execution(
pre_states: &[AccountWithMetadata],
post_states: &[Account],
post_states: &[AccountPostState],
executing_program_id: ProgramId,
) -> bool {
// 1. Lengths must match
@ -78,25 +169,27 @@ pub fn validate_execution(
for (pre, post) in pre_states.iter().zip(post_states) {
// 2. Nonce must remain unchanged
if pre.account.nonce != post.nonce {
if pre.account.nonce != post.account.nonce {
return false;
}
// 3. Program ownership changes are not allowed
if pre.account.program_owner != post.program_owner {
if pre.account.program_owner != post.account.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 && account_program_owner != executing_program_id {
if post.account.balance < pre.account.balance
&& account_program_owner != executing_program_id
{
return false;
}
// 5. Data changes only allowed if owned by executing program or if account pre state has
// default values
if pre.account.data != post.data
if pre.account.data != post.account.data
&& pre.account != Account::default()
&& account_program_owner != executing_program_id
{
@ -105,17 +198,67 @@ pub fn validate_execution(
// 6. If a post state has default program owner, the pre state must have been a default
// account
if post.program_owner == DEFAULT_PROGRAM_ID && pre.account != Account::default() {
if post.account.program_owner == DEFAULT_PROGRAM_ID && pre.account != Account::default() {
return false;
}
}
// 7. Total balance is preserved
let total_balance_pre_states: u128 = pre_states.iter().map(|pre| pre.account.balance).sum();
let total_balance_post_states: u128 = post_states.iter().map(|post| post.balance).sum();
let total_balance_post_states: u128 = post_states.iter().map(|post| post.account.balance).sum();
if total_balance_pre_states != total_balance_post_states {
return false;
}
true
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_post_state_new_with_claim_constructor() {
let account = Account {
program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
balance: 1337,
data: vec![0xde, 0xad, 0xbe, 0xef],
nonce: 10,
};
let account_post_state = AccountPostState::new_claimed(account.clone());
assert_eq!(account, account_post_state.account);
assert!(account_post_state.requires_claim());
}
#[test]
fn test_post_state_new_without_claim_constructor() {
let account = Account {
program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
balance: 1337,
data: vec![0xde, 0xad, 0xbe, 0xef],
nonce: 10,
};
let account_post_state = AccountPostState::new(account.clone());
assert_eq!(account, account_post_state.account);
assert!(!account_post_state.requires_claim());
}
#[test]
fn test_post_state_account_getter() {
let mut account = Account {
program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
balance: 1337,
data: vec![0xde, 0xad, 0xbe, 0xef],
nonce: 10,
};
let mut account_post_state = AccountPostState::new(account.clone());
assert_eq!(account_post_state.account(), &account);
assert_eq!(account_post_state.account_mut(), &mut account);
}
}

View File

@ -1,16 +1,17 @@
use nssa_core::{
account::{Account, AccountWithMetadata},
program::{ProgramInput, read_nssa_inputs, write_nssa_outputs},
program::{
AccountPostState, DEFAULT_PROGRAM_ID, ProgramInput, read_nssa_inputs, write_nssa_outputs,
},
};
/// Initializes a default account under the ownership of this program.
/// This is achieved by a noop.
fn initialize_account(pre_state: AccountWithMetadata) {
let account_to_claim = pre_state.account.clone();
let account_to_claim = AccountPostState::new_claimed(pre_state.account.clone());
let is_authorized = pre_state.is_authorized;
// Continue only if the account to claim has default values
if account_to_claim != Account::default() {
if account_to_claim.account() != &Account::default() {
return;
}
@ -36,10 +37,25 @@ fn transfer(sender: AccountWithMetadata, recipient: AccountWithMetadata, balance
}
// Create accounts post states, with updated balances
let mut sender_post = sender.account.clone();
let mut recipient_post = recipient.account.clone();
sender_post.balance -= balance_to_move;
recipient_post.balance += balance_to_move;
let sender_post = {
// Modify sender's balance
let mut sender_post_account = sender.account.clone();
sender_post_account.balance -= balance_to_move;
AccountPostState::new(sender_post_account)
};
let recipient_post = {
// Modify recipient's balance
let mut recipient_post_account = recipient.account.clone();
recipient_post_account.balance += balance_to_move;
// Claim recipient account if it has default program owner
if recipient_post_account.program_owner == DEFAULT_PROGRAM_ID {
AccountPostState::new_claimed(recipient_post_account)
} else {
AccountPostState::new(recipient_post_account)
}
};
write_nssa_outputs(vec![sender, recipient], vec![sender_post, recipient_post]);
}

View File

@ -1,4 +1,4 @@
use nssa_core::program::{ProgramInput, read_nssa_inputs, write_nssa_outputs};
use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs};
use risc0_zkvm::sha::{Impl, Sha256};
const PRIZE: u128 = 150;
@ -66,5 +66,11 @@ fn main() {
pinata_post.data = data.next_data().to_vec();
winner_post.balance += PRIZE;
write_nssa_outputs(vec![pinata, winner], vec![pinata_post, winner_post]);
write_nssa_outputs(
vec![pinata, winner],
vec![
AccountPostState::new(pinata_post),
AccountPostState::new(winner_post),
],
);
}

View File

@ -0,0 +1,103 @@
use nssa_core::program::{
read_nssa_inputs, write_nssa_outputs_with_chained_call, AccountPostState, ChainedCall, PdaSeed, ProgramInput
};
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![
AccountPostState::new(pinata_definition_post),
AccountPostState::new(pinata_token_holding_post),
AccountPostState::new(winner_token_holding_post),
],
chained_calls,
);
}

View File

@ -70,7 +70,7 @@ fn main() {
// Public account
public_pre_states.push(pre_states[i].clone());
let mut post = post_states[i].clone();
let mut post = post_states[i].account().clone();
if pre_states[i].is_authorized {
post.nonce += 1;
}
@ -126,7 +126,7 @@ fn main() {
}
// Update post-state with new nonce
let mut post_with_updated_values = post_states[i].clone();
let mut post_with_updated_values = post_states[i].account().clone();
post_with_updated_values.nonce = *new_nonce;
if post_with_updated_values.program_owner == DEFAULT_PROGRAM_ID {

View File

@ -1,6 +1,8 @@
use nssa_core::{
account::{Account, AccountId, AccountWithMetadata, Data},
program::{ProgramInput, read_nssa_inputs, write_nssa_outputs},
program::{
AccountPostState, DEFAULT_PROGRAM_ID, ProgramInput, read_nssa_inputs, write_nssa_outputs,
},
};
// The token program has three functions:
@ -92,25 +94,25 @@ impl TokenHolding {
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()
.expect("Defintion ID must be 32 bytes long"),
);
let balance = u128::from_le_bytes(
data[33..]
.try_into()
.expect("balance must be 16 bytes little-endian"),
);
Some(Self {
definition_id,
balance,
account_type,
})
return None;
}
let account_type = data[0];
let definition_id = AccountId::new(
data[1..33]
.try_into()
.expect("Defintion ID must be 32 bytes long"),
);
let balance = u128::from_le_bytes(
data[33..]
.try_into()
.expect("balance must be 16 bytes little-endian"),
);
Some(Self {
definition_id,
balance,
account_type,
})
}
fn into_data(self) -> Data {
@ -122,7 +124,7 @@ impl TokenHolding {
}
}
fn transfer(pre_states: &[AccountWithMetadata], balance_to_move: u128) -> Vec<Account> {
fn transfer(pre_states: &[AccountWithMetadata], balance_to_move: u128) -> Vec<AccountPostState> {
if pre_states.len() != 2 {
panic!("Invalid number of input accounts");
}
@ -158,12 +160,19 @@ fn transfer(pre_states: &[AccountWithMetadata], balance_to_move: u128) -> Vec<Ac
let sender_post = {
let mut this = sender.account.clone();
this.data = sender_holding.into_data();
this
AccountPostState::new(this)
};
let recipient_post = {
let mut this = recipient.account.clone();
this.data = recipient_holding.into_data();
this
// Claim the recipient account if it has default program owner
if this.program_owner == DEFAULT_PROGRAM_ID {
AccountPostState::new_claimed(this)
} else {
AccountPostState::new(this)
}
};
vec![sender_post, recipient_post]
@ -173,7 +182,7 @@ fn new_definition(
pre_states: &[AccountWithMetadata],
name: [u8; 6],
total_supply: u128,
) -> Vec<Account> {
) -> Vec<AccountPostState> {
if pre_states.len() != 2 {
panic!("Invalid number of input accounts");
}
@ -206,10 +215,13 @@ fn new_definition(
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]
vec![
AccountPostState::new_claimed(definition_target_account_post),
AccountPostState::new_claimed(holding_target_account_post),
]
}
fn initialize_account(pre_states: &[AccountWithMetadata]) -> Vec<Account> {
fn initialize_account(pre_states: &[AccountWithMetadata]) -> Vec<AccountPostState> {
if pre_states.len() != 2 {
panic!("Invalid number of accounts");
}
@ -221,7 +233,7 @@ fn initialize_account(pre_states: &[AccountWithMetadata]) -> Vec<Account> {
panic!("Only uninitialized accounts can be initialized");
}
// TODO: We should check that this is an account owned by the token program.
// TODO: #212 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
@ -230,10 +242,13 @@ fn initialize_account(pre_states: &[AccountWithMetadata]) -> Vec<Account> {
let holding_values = 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_values.into_data();
let mut account_to_initialize = account_to_initialize.account.clone();
account_to_initialize.data = holding_values.into_data();
vec![definition_post, account_to_initialize_post]
vec![
AccountPostState::new(definition_post),
AccountPostState::new_claimed(account_to_initialize),
]
}
fn burn(pre_states: &[AccountWithMetadata], balance_to_burn: u128) -> Vec<Account> {
@ -344,7 +359,7 @@ fn main() {
instruction,
} = read_nssa_inputs::<Instruction>();
let (pre_states, post_states) = match instruction[0] {
let post_states = match instruction[0] {
0 => {
// Parse instruction
let total_supply = u128::from_le_bytes(
@ -358,8 +373,7 @@ fn main() {
assert_ne!(name, [0; 6]);
// Execute
let post_states = new_definition(&pre_states, name, total_supply);
(pre_states, post_states)
new_definition(&pre_states, name, total_supply)
}
1 => {
// Parse instruction
@ -374,14 +388,14 @@ fn main() {
assert_eq!(name, [0; 6]);
// Execute
let post_states = transfer(&pre_states, balance_to_move);
(pre_states, post_states)
transfer(&pre_states, balance_to_move)
}
2 => {
// Initialize account
assert_eq!(instruction[1..], [0; 22]);
let post_states = initialize_account(&pre_states);
(pre_states, post_states)
if instruction[1..] != [0; 22] {
panic!("Invalid instruction for initialize account");
}
initialize_account(&pre_states)
}
3 => {
let balance_to_burn = u128::from_le_bytes(
@ -528,14 +542,14 @@ mod tests {
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,
definition_account.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,
holding_account.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,
@ -760,14 +774,14 @@ mod tests {
let post_states = transfer(&pre_states, 11);
let [sender_post, recipient_post] = post_states.try_into().ok().unwrap();
assert_eq!(
sender_post.data,
sender_post.account().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,
recipient_post.account().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
@ -781,7 +795,7 @@ mod tests {
AccountWithMetadata {
account: Account {
// Definition ID with
data: vec![0; TOKEN_DEFINITION_DATA_SIZE - 16]
data: [0; TOKEN_DEFINITION_DATA_SIZE - 16]
.into_iter()
.chain(u128::to_le_bytes(1000))
.collect(),
@ -798,9 +812,9 @@ mod tests {
];
let post_states = initialize_account(&pre_states);
let [definition, holding] = post_states.try_into().ok().unwrap();
assert_eq!(definition.data, pre_states[0].account.data);
assert_eq!(definition.account().data, pre_states[0].account.data);
assert_eq!(
holding.data,
holding.account().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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0

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()).expect("Piñata program must be a valid R0BF file")
}
}
#[cfg(test)]
@ -207,6 +212,15 @@ mod tests {
elf: CHAIN_CALLER_ELF.to_vec(),
}
}
pub fn claimer() -> Self {
use test_program_methods::{CLAIMER_ELF, CLAIMER_ID};
Program {
id: CLAIMER_ID,
elf: CLAIMER_ELF.to_vec(),
}
}
}
#[test]
@ -239,8 +253,8 @@ mod tests {
let [sender_post, recipient_post] = program_output.post_states.try_into().unwrap();
assert_eq!(sender_post, expected_sender_post);
assert_eq!(recipient_post, expected_recipient_post);
assert_eq!(sender_post.account(), &expected_sender_post);
assert_eq!(recipient_post.account(), &expected_recipient_post);
}
#[test]

View File

@ -3,7 +3,7 @@ use std::collections::{HashMap, HashSet, VecDeque};
use borsh::{BorshDeserialize, BorshSerialize};
use nssa_core::{
account::{Account, AccountId, AccountWithMetadata},
program::{ChainedCall, DEFAULT_PROGRAM_ID, validate_execution},
program::{ChainedCall, DEFAULT_PROGRAM_ID, PdaSeed, ProgramId, validate_execution},
};
use sha2::{Digest, digest::FixedOutput};
@ -107,12 +107,13 @@ impl PublicTransaction {
program_id: message.program_id,
instruction_data: message.instruction_data.clone(),
pre_states: input_pre_states,
pda_seeds: vec![],
};
let mut chained_calls = VecDeque::from_iter([initial_call]);
let mut chained_calls = VecDeque::from_iter([(initial_call, None)]);
let mut chain_calls_counter = 0;
while let Some(chained_call) = chained_calls.pop_front() {
while let Some((chained_call, caller_program_id)) = chained_calls.pop_front() {
if chain_calls_counter > MAX_NUMBER_CHAINED_CALLS {
return Err(NssaError::MaxChainedCallsDepthExceeded);
}
@ -125,6 +126,9 @@ impl PublicTransaction {
let mut program_output =
program.execute(&chained_call.pre_states, &chained_call.instruction_data)?;
let authorized_pdas =
self.compute_authorized_pdas(&caller_program_id, &chained_call.pda_seeds);
for pre in &program_output.pre_states {
let account_id = pre.account_id;
// Check that the program output pre_states coinicide with the values in the public
@ -137,8 +141,11 @@ impl PublicTransaction {
return Err(NssaError::InvalidProgramBehavior);
}
// Check that authorization flags are consistent with the provided ones
if pre.is_authorized && !signer_account_ids.contains(&account_id) {
// Check that authorization flags are consistent with the provided ones or
// authorized by program through the PDA mechanism
let is_authorized = signer_account_ids.contains(&account_id)
|| authorized_pdas.contains(&account_id);
if pre.is_authorized != is_authorized {
return Err(NssaError::InvalidProgramBehavior);
}
}
@ -153,10 +160,16 @@ impl PublicTransaction {
return Err(NssaError::InvalidProgramBehavior);
}
// The invoked program claims the accounts with default program id.
for post in program_output.post_states.iter_mut() {
if post.program_owner == DEFAULT_PROGRAM_ID {
post.program_owner = chained_call.program_id;
for post in program_output
.post_states
.iter_mut()
.filter(|post| post.requires_claim())
{
// The invoked program can only claim accounts with default program id.
if post.account().program_owner == DEFAULT_PROGRAM_ID {
post.account_mut().program_owner = chained_call.program_id;
} else {
return Err(NssaError::InvalidProgramBehavior);
}
}
@ -166,11 +179,11 @@ impl PublicTransaction {
.iter()
.zip(program_output.post_states.iter())
{
state_diff.insert(pre.account_id, post.clone());
state_diff.insert(pre.account_id, post.account().clone());
}
for new_call in program_output.chained_calls.into_iter().rev() {
chained_calls.push_front(new_call);
chained_calls.push_front((new_call, Some(chained_call.program_id)));
}
chain_calls_counter += 1;
@ -178,6 +191,21 @@ impl PublicTransaction {
Ok(state_diff)
}
fn compute_authorized_pdas(
&self,
caller_program_id: &Option<ProgramId>,
pda_seeds: &[PdaSeed],
) -> HashSet<AccountId> {
if let Some(caller_program_id) = caller_program_id {
pda_seeds
.iter()
.map(|pda_seed| AccountId::from((caller_program_id, pda_seed)))
.collect()
} else {
HashSet::new()
}
}
}
#[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)]
@ -250,7 +264,7 @@ pub mod tests {
Commitment, Nullifier, NullifierPublicKey, NullifierSecretKey, SharedSecretKey,
account::{Account, AccountId, AccountWithMetadata, Nonce},
encryption::{EphemeralPublicKey, IncomingViewingPublicKey, Scalar},
program::ProgramId,
program::{PdaSeed, ProgramId},
};
use crate::{
@ -477,6 +491,7 @@ pub mod tests {
self.insert_program(Program::minter());
self.insert_program(Program::burner());
self.insert_program(Program::chain_caller());
self.insert_program(Program::claimer());
self
}
@ -2092,14 +2107,18 @@ pub mod tests {
let key = PrivateKey::try_new([1; 32]).unwrap();
let from = AccountId::from(&PublicKey::new_from_private_key(&key));
let to = AccountId::new([2; 32]);
let initial_balance = 100;
let initial_balance = 1000;
let initial_data = [(from, initial_balance), (to, 0)];
let mut state =
V02State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs();
let from_key = key;
let amount: u128 = 0;
let instruction: (u128, ProgramId, u32) =
(amount, Program::authenticated_transfer_program().id(), 2);
let amount: u128 = 37;
let instruction: (u128, ProgramId, u32, Option<PdaSeed>) = (
amount,
Program::authenticated_transfer_program().id(),
2,
None,
);
let expected_to_post = Account {
program_owner: Program::authenticated_transfer_program().id(),
@ -2139,10 +2158,11 @@ pub mod tests {
V02State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs();
let from_key = key;
let amount: u128 = 0;
let instruction: (u128, ProgramId, u32) = (
let instruction: (u128, ProgramId, u32, Option<PdaSeed>) = (
amount,
Program::authenticated_transfer_program().id(),
MAX_NUMBER_CHAINED_CALLS as u32 + 1,
None,
);
let message = public_transaction::Message::try_new(
@ -2162,4 +2182,208 @@ pub mod tests {
Err(NssaError::MaxChainedCallsDepthExceeded)
));
}
#[test]
fn test_execution_that_requires_authentication_of_a_program_derived_account_id_succeeds() {
let chain_caller = Program::chain_caller();
let pda_seed = PdaSeed::new([37; 32]);
let from = AccountId::from((&chain_caller.id(), &pda_seed));
let to = AccountId::new([2; 32]);
let initial_balance = 1000;
let initial_data = [(from, initial_balance), (to, 0)];
let mut state =
V02State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs();
let amount: u128 = 58;
let instruction: (u128, ProgramId, u32, Option<PdaSeed>) = (
amount,
Program::authenticated_transfer_program().id(),
1,
Some(pda_seed),
);
let expected_to_post = Account {
program_owner: Program::authenticated_transfer_program().id(),
balance: amount, // The `chain_caller` chains the program twice
..Account::default()
};
let message = public_transaction::Message::try_new(
chain_caller.id(),
vec![to, from], // The chain_caller program permutes the account order in the chain
// call
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();
let from_post = state.get_account_by_id(&from);
let to_post = state.get_account_by_id(&to);
assert_eq!(from_post.balance, initial_balance - amount);
assert_eq!(to_post, expected_to_post);
}
#[test]
fn test_claiming_mechanism_within_chain_call() {
// This test calls the authenticated transfer program through the chain_caller program.
// The transfer is made from an initialized sender to an uninitialized recipient. And
// it is expected that the recipient account is claimed by the authenticated transfer
// program and not the chained_caller program.
let chain_caller = Program::chain_caller();
let auth_transfer = Program::authenticated_transfer_program();
let key = PrivateKey::try_new([1; 32]).unwrap();
let account_id = AccountId::from(&PublicKey::new_from_private_key(&key));
let initial_balance = 100;
let initial_data = [(account_id, initial_balance)];
let mut state =
V02State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs();
let from = account_id;
let from_key = key;
let to = AccountId::new([2; 32]);
let amount: u128 = 37;
// Check the recipient is an uninitialized account
assert_eq!(state.get_account_by_id(&to), Account::default());
let expected_to_post = Account {
// The expected program owner is the authenticated transfer program
program_owner: auth_transfer.id(),
balance: amount,
..Account::default()
};
// The transaction executes the chain_caller program, which internally calls the
// authenticated_transfer program
let instruction: (u128, ProgramId, u32, Option<PdaSeed>) = (
amount,
Program::authenticated_transfer_program().id(),
1,
None,
);
let message = public_transaction::Message::try_new(
chain_caller.id(),
vec![to, from], // The chain_caller program permutes the account order in the chain
// call
vec![0],
instruction,
)
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&from_key]);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx).unwrap();
let from_post = state.get_account_by_id(&from);
let to_post = state.get_account_by_id(&to);
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
);
}
#[test]
fn test_claiming_mechanism_cannot_claim_initialied_accounts() {
let claimer = Program::claimer();
let mut state = V02State::new_with_genesis_accounts(&[], &[]).with_test_programs();
let account_id = AccountId::new([2; 32]);
// Insert an account with non-default program owner
state.force_insert_account(
account_id,
Account {
program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
..Account::default()
},
);
let message =
public_transaction::Message::try_new(claimer.id(), vec![account_id], vec![], ())
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
let tx = PublicTransaction::new(message, witness_set);
let result = state.transition_from_public_transaction(&tx);
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)))
}
}

View File

@ -1,4 +1,4 @@
use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, ProgramInput};
use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs};
type Instruction = u128;
@ -17,5 +17,5 @@ fn main() {
let mut account_post = account_pre.clone();
account_post.balance -= balance_to_burn;
write_nssa_outputs(vec![pre], vec![account_post]);
write_nssa_outputs(vec![pre], vec![AccountPostState::new(account_post)]);
}

View File

@ -1,43 +1,54 @@
use nssa_core::program::{
ChainedCall, ProgramId, ProgramInput, read_nssa_inputs, write_nssa_outputs_with_chained_call,
AccountPostState, ChainedCall, PdaSeed, ProgramId, ProgramInput, read_nssa_inputs,
write_nssa_outputs_with_chained_call,
};
use risc0_zkvm::serde::to_vec;
type Instruction = (u128, ProgramId, u32);
type Instruction = (u128, ProgramId, u32, Option<PdaSeed>);
/// A program that calls another program `num_chain_calls` times.
/// It permutes the order of the input accounts on the subsequent call
/// The `ProgramId` in the instruction must be the program_id of the authenticated transfers program
fn main() {
let ProgramInput {
pre_states,
instruction: (balance, program_id, num_chain_calls),
instruction: (balance, auth_transfer_id, num_chain_calls, pda_seed),
} = read_nssa_inputs::<Instruction>();
let [sender_pre, receiver_pre] = match pre_states.try_into() {
let [recipient_pre, sender_pre] = match pre_states.try_into() {
Ok(array) => array,
Err(_) => return,
};
let instruction_data = to_vec(&balance).unwrap();
let mut chained_call = vec![
ChainedCall {
program_id,
instruction_data: instruction_data.clone(),
pre_states: vec![receiver_pre.clone(), sender_pre.clone()], // <- Account order permutation here
};
num_chain_calls as usize - 1
];
let mut running_recipient_pre = recipient_pre.clone();
let mut running_sender_pre = sender_pre.clone();
chained_call.push(ChainedCall {
program_id,
instruction_data,
pre_states: vec![receiver_pre.clone(), sender_pre.clone()], // <- Account order permutation here
});
if pda_seed.is_some() {
running_sender_pre.is_authorized = true;
}
let mut chained_calls = Vec::new();
for _i in 0..num_chain_calls {
let new_chained_call = ChainedCall {
program_id: auth_transfer_id,
instruction_data: instruction_data.clone(),
pre_states: vec![running_sender_pre.clone(), running_recipient_pre.clone()], // <- Account order permutation here
pda_seeds: pda_seed.iter().cloned().collect(),
};
chained_calls.push(new_chained_call);
running_sender_pre.account.balance -= balance;
running_recipient_pre.account.balance += balance;
}
write_nssa_outputs_with_chained_call(
vec![sender_pre.clone(), receiver_pre.clone()],
vec![sender_pre.account, receiver_pre.account],
chained_call,
vec![sender_pre.clone(), recipient_pre.clone()],
vec![
AccountPostState::new(sender_pre.account),
AccountPostState::new(recipient_pre.account),
],
chained_calls,
);
}

View File

@ -0,0 +1,19 @@
use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs};
type Instruction = ();
fn main() {
let ProgramInput {
pre_states,
instruction: _,
} = read_nssa_inputs::<Instruction>();
let [pre] = match pre_states.try_into() {
Ok(array) => array,
Err(_) => return,
};
let account_post = AccountPostState::new_claimed(pre.account.clone());
write_nssa_outputs(vec![pre], vec![account_post]);
}

View File

@ -1,4 +1,4 @@
use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, ProgramInput};
use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs};
type Instruction = ();
@ -14,5 +14,5 @@ fn main() {
let mut account_post = account_pre.clone();
account_post.data.push(0);
write_nssa_outputs(vec![pre], vec![account_post]);
write_nssa_outputs(vec![pre], vec![AccountPostState::new_claimed(account_post)]);
}

View File

@ -1,6 +1,6 @@
use nssa_core::{
account::Account,
program::{read_nssa_inputs, write_nssa_outputs, ProgramInput},
program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs},
};
type Instruction = ();
@ -15,5 +15,11 @@ fn main() {
let account_pre = pre.account.clone();
write_nssa_outputs(vec![pre], vec![account_pre, Account::default()]);
write_nssa_outputs(
vec![pre],
vec![
AccountPostState::new(account_pre),
AccountPostState::new(Account::default()),
],
);
}

View File

@ -1,4 +1,4 @@
use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, ProgramInput};
use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, AccountPostState, ProgramInput};
type Instruction = ();
@ -14,5 +14,5 @@ fn main() {
let mut account_post = account_pre.clone();
account_post.balance += 1;
write_nssa_outputs(vec![pre], vec![account_post]);
write_nssa_outputs(vec![pre], vec![AccountPostState::new(account_post)]);
}

View File

@ -1,4 +1,4 @@
use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, ProgramInput};
use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs};
type Instruction = ();
@ -12,5 +12,5 @@ fn main() {
let account_pre1 = pre1.account.clone();
write_nssa_outputs(vec![pre1, pre2], vec![account_pre1]);
write_nssa_outputs(vec![pre1, pre2], vec![AccountPostState::new(account_pre1)]);
}

View File

@ -1,4 +1,4 @@
use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, ProgramInput};
use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, AccountPostState, ProgramInput};
type Instruction = ();
@ -14,5 +14,5 @@ fn main() {
let mut account_post = account_pre.clone();
account_post.nonce += 1;
write_nssa_outputs(vec![pre], vec![account_post]);
write_nssa_outputs(vec![pre], vec![AccountPostState::new(account_post)]);
}

View File

@ -1,4 +1,4 @@
use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, ProgramInput};
use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, AccountPostState, ProgramInput};
type Instruction = ();
@ -14,5 +14,5 @@ fn main() {
let mut account_post = account_pre.clone();
account_post.program_owner = [0, 1, 2, 3, 4, 5, 6, 7];
write_nssa_outputs(vec![pre], vec![account_post]);
write_nssa_outputs(vec![pre], vec![AccountPostState::new(account_post)]);
}

View File

@ -1,4 +1,4 @@
use nssa_core::program::{read_nssa_inputs, write_nssa_outputs, ProgramInput};
use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs};
type Instruction = u128;
@ -20,6 +20,9 @@ fn main() {
write_nssa_outputs(
vec![sender_pre, receiver_pre],
vec![sender_post, receiver_post],
vec![
AccountPostState::new(sender_post),
AccountPostState::new(receiver_post),
],
);
}

View File

@ -176,7 +176,12 @@ impl From<TokenDefinition> for TokedDefinitionAccountView {
fn from(value: TokenDefinition) -> Self {
Self {
account_type: "Token definition".to_string(),
name: hex::encode(value.name),
name: {
// Assuming, that name does not have UTF-8 NULL and all zeroes are padding.
let name_trimmed: Vec<_> =
value.name.into_iter().take_while(|ch| *ch != 0).collect();
String::from_utf8(name_trimmed).unwrap_or(hex::encode(value.name))
},
total_supply: value.total_supply,
}
}
@ -334,3 +339,47 @@ impl WalletSubcommand for AccountSubcommand {
}
}
}
#[cfg(test)]
mod tests {
use crate::cli::account::{TokedDefinitionAccountView, TokenDefinition};
#[test]
fn test_invalid_utf_8_name_of_token() {
let token_def = TokenDefinition {
account_type: 1,
name: [137, 12, 14, 3, 5, 4],
total_supply: 100,
};
let token_def_view: TokedDefinitionAccountView = token_def.into();
assert_eq!(token_def_view.name, "890c0e030504");
}
#[test]
fn test_valid_utf_8_name_of_token_all_bytes() {
let token_def = TokenDefinition {
account_type: 1,
name: [240, 159, 146, 150, 66, 66],
total_supply: 100,
};
let token_def_view: TokedDefinitionAccountView = token_def.into();
assert_eq!(token_def_view.name, "💖BB");
}
#[test]
fn test_valid_utf_8_name_of_token_less_bytes() {
let token_def = TokenDefinition {
account_type: 1,
name: [78, 65, 77, 69, 0, 0],
total_supply: 100,
};
let token_def_view: TokedDefinitionAccountView = token_def.into();
assert_eq!(token_def_view.name, "NAME");
}
}