2025-08-06 20:05:04 -03:00
|
|
|
use crate::{
|
2025-08-09 20:25:58 -03:00
|
|
|
address::Address,
|
|
|
|
|
error::NssaError,
|
|
|
|
|
program::{AUTHENTICATED_TRANSFER_PROGRAM, Program},
|
2025-08-06 20:05:04 -03:00
|
|
|
public_transaction::PublicTransaction,
|
|
|
|
|
};
|
|
|
|
|
use nssa_core::{
|
|
|
|
|
account::{Account, AccountWithMetadata},
|
2025-08-09 20:16:18 -03:00
|
|
|
program::{ProgramId, validate_constraints},
|
2025-08-06 20:05:04 -03:00
|
|
|
};
|
|
|
|
|
use std::collections::{HashMap, HashSet};
|
|
|
|
|
|
2025-08-07 15:19:06 -03:00
|
|
|
pub struct V01State {
|
2025-08-06 20:05:04 -03:00
|
|
|
public_state: HashMap<Address, Account>,
|
2025-08-08 16:19:50 -03:00
|
|
|
builtin_programs: HashMap<ProgramId, Program>,
|
2025-08-06 20:05:04 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl V01State {
|
2025-08-07 15:19:06 -03:00
|
|
|
pub fn new_with_genesis_accounts(initial_data: &[([u8; 32], u128)]) -> Self {
|
|
|
|
|
let public_state = initial_data
|
2025-08-09 19:20:19 -03:00
|
|
|
.iter()
|
|
|
|
|
.copied()
|
2025-08-07 15:19:06 -03:00
|
|
|
.map(|(address_value, balance)| {
|
2025-08-09 18:40:32 -03:00
|
|
|
let account = Account {
|
2025-08-09 19:20:19 -03:00
|
|
|
balance,
|
2025-08-09 20:25:58 -03:00
|
|
|
program_owner: AUTHENTICATED_TRANSFER_PROGRAM.id(),
|
2025-08-09 18:40:32 -03:00
|
|
|
..Account::default()
|
|
|
|
|
};
|
2025-08-07 15:19:06 -03:00
|
|
|
let address = Address::new(address_value);
|
|
|
|
|
(address, account)
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
2025-08-08 16:19:50 -03:00
|
|
|
|
|
|
|
|
let builtin_programs = HashMap::from([(
|
2025-08-09 20:25:58 -03:00
|
|
|
AUTHENTICATED_TRANSFER_PROGRAM.id(),
|
2025-08-08 16:19:50 -03:00
|
|
|
AUTHENTICATED_TRANSFER_PROGRAM,
|
|
|
|
|
)]);
|
|
|
|
|
|
|
|
|
|
Self {
|
|
|
|
|
public_state,
|
|
|
|
|
builtin_programs,
|
|
|
|
|
}
|
2025-08-07 15:19:06 -03:00
|
|
|
}
|
|
|
|
|
|
2025-08-09 19:49:07 -03:00
|
|
|
pub fn transition_from_public_transaction(
|
|
|
|
|
&mut self,
|
|
|
|
|
tx: &PublicTransaction,
|
|
|
|
|
) -> Result<(), NssaError> {
|
|
|
|
|
let state_diff = self.execute_and_verify_public_transaction(tx)?;
|
2025-08-06 20:05:04 -03:00
|
|
|
|
|
|
|
|
for (address, post) in state_diff.into_iter() {
|
|
|
|
|
let current_account = self.get_account_by_address_mut(address);
|
|
|
|
|
*current_account = post;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for address in tx.signer_addresses() {
|
|
|
|
|
let current_account = self.get_account_by_address_mut(address);
|
|
|
|
|
current_account.nonce += 1;
|
|
|
|
|
}
|
2025-08-09 20:35:44 -03:00
|
|
|
|
2025-08-06 20:05:04 -03:00
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn get_account_by_address_mut(&mut self, address: Address) -> &mut Account {
|
2025-08-09 19:20:19 -03:00
|
|
|
self.public_state.entry(address).or_default()
|
2025-08-06 20:05:04 -03:00
|
|
|
}
|
|
|
|
|
|
2025-08-07 15:19:06 -03:00
|
|
|
pub fn get_account_by_address(&self, address: &Address) -> Account {
|
2025-08-06 20:05:04 -03:00
|
|
|
self.public_state
|
|
|
|
|
.get(address)
|
|
|
|
|
.cloned()
|
|
|
|
|
.unwrap_or(Account::default())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn execute_and_verify_public_transaction(
|
|
|
|
|
&mut self,
|
|
|
|
|
tx: &PublicTransaction,
|
2025-08-09 19:49:07 -03:00
|
|
|
) -> Result<HashMap<Address, Account>, NssaError> {
|
2025-08-06 20:05:04 -03:00
|
|
|
let message = tx.message();
|
|
|
|
|
let witness_set = tx.witness_set();
|
|
|
|
|
|
|
|
|
|
// All addresses must be different
|
|
|
|
|
if message.addresses.iter().collect::<HashSet<_>>().len() != message.addresses.len() {
|
2025-08-09 19:49:07 -03:00
|
|
|
return Err(NssaError::InvalidInput(
|
|
|
|
|
"Duplicate addresses found in message".into(),
|
|
|
|
|
));
|
2025-08-06 20:05:04 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if message.nonces.len() != witness_set.signatures_and_public_keys.len() {
|
2025-08-09 19:49:07 -03:00
|
|
|
return Err(NssaError::InvalidInput(
|
|
|
|
|
"Mismatch between number of nonces and signatures/public keys".into(),
|
|
|
|
|
));
|
2025-08-06 20:05:04 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut authorized_addresses = Vec::new();
|
2025-08-09 20:35:44 -03:00
|
|
|
for ((signature, public_key), nonce) in witness_set.iter_signatures().zip(&message.nonces) {
|
2025-08-06 20:05:04 -03:00
|
|
|
// Check the signature is valid
|
|
|
|
|
if !signature.is_valid_for(message, public_key) {
|
2025-08-09 19:49:07 -03:00
|
|
|
return Err(NssaError::InvalidInput(
|
|
|
|
|
"Invalid signature for given message and public key".into(),
|
|
|
|
|
));
|
2025-08-06 20:05:04 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check the nonce corresponds to the current nonce on the public state.
|
|
|
|
|
let address = Address::from_public_key(public_key);
|
|
|
|
|
let current_nonce = self.get_account_by_address(&address).nonce;
|
|
|
|
|
if current_nonce != *nonce {
|
2025-08-09 19:49:07 -03:00
|
|
|
return Err(NssaError::InvalidInput("Nonce mismatch".into()));
|
2025-08-06 20:05:04 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
authorized_addresses.push(address);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Build pre_states for execution
|
|
|
|
|
let pre_states: Vec<_> = message
|
|
|
|
|
.addresses
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|address| AccountWithMetadata {
|
|
|
|
|
account: self.get_account_by_address(address),
|
|
|
|
|
is_authorized: authorized_addresses.contains(address),
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
// Check the `program_id` corresponds to a built-in program
|
|
|
|
|
// Only allowed program so far is the authenticated transfer program
|
2025-08-08 16:19:50 -03:00
|
|
|
let Some(program) = self.builtin_programs.get(&message.program_id) else {
|
2025-08-09 19:49:07 -03:00
|
|
|
return Err(NssaError::InvalidInput("Unknown program".into()));
|
2025-08-08 16:19:50 -03:00
|
|
|
};
|
2025-08-06 20:05:04 -03:00
|
|
|
|
|
|
|
|
// // Execute program
|
2025-08-09 20:16:18 -03:00
|
|
|
let post_states = program.execute(&pre_states, message.instruction_data)?;
|
2025-08-06 20:05:04 -03:00
|
|
|
|
|
|
|
|
// Verify execution corresponds to a well-behaved program.
|
|
|
|
|
// See the # Programs section for the definition of the `validate_constraints` method.
|
2025-08-09 19:49:07 -03:00
|
|
|
if !validate_constraints(&pre_states, &post_states, message.program_id) {
|
|
|
|
|
return Err(NssaError::InvalidProgramBehavior);
|
2025-08-06 20:05:04 -03:00
|
|
|
}
|
|
|
|
|
|
2025-08-09 19:20:19 -03:00
|
|
|
Ok(message.addresses.iter().cloned().zip(post_states).collect())
|
2025-08-06 20:05:04 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
2025-08-07 09:33:54 -03:00
|
|
|
use crate::{public_transaction, signature::PrivateKey};
|
|
|
|
|
|
2025-08-09 20:35:44 -03:00
|
|
|
fn transfer_transaction(
|
2025-08-06 20:05:04 -03:00
|
|
|
from: Address,
|
|
|
|
|
from_key: PrivateKey,
|
2025-08-07 09:33:54 -03:00
|
|
|
nonce: u128,
|
2025-08-06 20:05:04 -03:00
|
|
|
to: Address,
|
|
|
|
|
balance: u128,
|
|
|
|
|
) -> PublicTransaction {
|
|
|
|
|
let addresses = vec![from, to];
|
2025-08-07 09:33:54 -03:00
|
|
|
let nonces = vec![nonce];
|
2025-08-09 20:25:58 -03:00
|
|
|
let program_id = AUTHENTICATED_TRANSFER_PROGRAM.id();
|
2025-08-06 20:05:04 -03:00
|
|
|
let message = public_transaction::Message::new(program_id, addresses, nonces, balance);
|
2025-08-09 06:36:13 -03:00
|
|
|
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&from_key]);
|
2025-08-06 20:05:04 -03:00
|
|
|
PublicTransaction::new(message, witness_set)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
2025-08-09 20:35:44 -03:00
|
|
|
fn transition_from_authenticated_transfer_program_invocation_default_account_destination() {
|
2025-08-07 15:19:06 -03:00
|
|
|
let initial_data = [([1; 32], 100)];
|
2025-08-08 16:19:50 -03:00
|
|
|
let mut state = V01State::new_with_genesis_accounts(&initial_data);
|
2025-08-09 18:37:04 -03:00
|
|
|
let from = Address::new(initial_data[0].0);
|
2025-08-06 20:05:04 -03:00
|
|
|
let from_key = PrivateKey(1);
|
|
|
|
|
let to = Address::new([2; 32]);
|
2025-08-09 20:35:44 -03:00
|
|
|
assert_eq!(state.get_account_by_address(&to), Account::default());
|
2025-08-06 20:05:04 -03:00
|
|
|
let balance_to_move = 5;
|
2025-08-09 20:35:44 -03:00
|
|
|
|
|
|
|
|
let tx = transfer_transaction(from.clone(), from_key, 0, to.clone(), balance_to_move);
|
2025-08-08 16:19:50 -03:00
|
|
|
state.transition_from_public_transaction(&tx).unwrap();
|
|
|
|
|
|
|
|
|
|
assert_eq!(state.get_account_by_address(&from).balance, 95);
|
2025-08-09 20:35:44 -03:00
|
|
|
assert_eq!(state.get_account_by_address(&to).balance, 5);
|
2025-08-08 16:19:50 -03:00
|
|
|
assert_eq!(state.get_account_by_address(&from).nonce, 1);
|
|
|
|
|
assert_eq!(state.get_account_by_address(&to).nonce, 0);
|
2025-08-07 09:33:54 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
2025-08-09 20:35:44 -03:00
|
|
|
fn transition_from_authenticated_transfer_program_invocation_insuficient_balance() {
|
|
|
|
|
let initial_data = [([1; 32], 100)];
|
|
|
|
|
let mut state = V01State::new_with_genesis_accounts(&initial_data);
|
|
|
|
|
let from = Address::new(initial_data[0].0);
|
|
|
|
|
let from_key = PrivateKey(1);
|
|
|
|
|
let to = Address::new([2; 32]);
|
|
|
|
|
let balance_to_move = 101;
|
|
|
|
|
assert!(state.get_account_by_address(&from).balance < balance_to_move);
|
|
|
|
|
|
|
|
|
|
let tx = transfer_transaction(from.clone(), from_key, 0, to.clone(), balance_to_move);
|
|
|
|
|
let result = state.transition_from_public_transaction(&tx);
|
|
|
|
|
|
|
|
|
|
assert!(matches!(result, Err(NssaError::ProgramExecutionFailed(_))));
|
|
|
|
|
assert_eq!(state.get_account_by_address(&from).balance, 100);
|
|
|
|
|
assert_eq!(state.get_account_by_address(&to).balance, 0);
|
|
|
|
|
assert_eq!(state.get_account_by_address(&from).nonce, 0);
|
|
|
|
|
assert_eq!(state.get_account_by_address(&to).nonce, 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn transition_from_authenticated_transfer_program_invocation_non_default_account_destination() {
|
2025-08-07 15:19:06 -03:00
|
|
|
let initial_data = [([1; 32], 100), ([99; 32], 200)];
|
2025-08-08 16:19:50 -03:00
|
|
|
let mut state = V01State::new_with_genesis_accounts(&initial_data);
|
2025-08-09 18:37:04 -03:00
|
|
|
let from = Address::new(initial_data[1].0);
|
2025-08-07 09:33:54 -03:00
|
|
|
let from_key = PrivateKey(99);
|
2025-08-09 18:37:04 -03:00
|
|
|
let to = Address::new(initial_data[0].0);
|
2025-08-09 20:35:44 -03:00
|
|
|
assert_ne!(state.get_account_by_address(&to), Account::default());
|
2025-08-07 09:33:54 -03:00
|
|
|
let balance_to_move = 8;
|
2025-08-09 20:35:44 -03:00
|
|
|
|
|
|
|
|
let tx = transfer_transaction(from.clone(), from_key, 0, to.clone(), balance_to_move);
|
2025-08-08 16:19:50 -03:00
|
|
|
state.transition_from_public_transaction(&tx).unwrap();
|
|
|
|
|
|
|
|
|
|
assert_eq!(state.get_account_by_address(&from).balance, 192);
|
2025-08-09 20:35:44 -03:00
|
|
|
assert_eq!(state.get_account_by_address(&to).balance, 108);
|
2025-08-08 16:19:50 -03:00
|
|
|
assert_eq!(state.get_account_by_address(&from).nonce, 1);
|
|
|
|
|
assert_eq!(state.get_account_by_address(&to).nonce, 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
2025-08-09 20:35:44 -03:00
|
|
|
fn transition_from_chained_authenticated_transfer_program_invocations() {
|
2025-08-08 16:19:50 -03:00
|
|
|
let initial_data = [([1; 32], 100)];
|
|
|
|
|
let mut state = V01State::new_with_genesis_accounts(&initial_data);
|
2025-08-09 18:37:04 -03:00
|
|
|
let address_1 = Address::new(initial_data[0].0);
|
2025-08-08 16:19:50 -03:00
|
|
|
let key_1 = PrivateKey(1);
|
|
|
|
|
let address_2 = Address::new([2; 32]);
|
|
|
|
|
let key_2 = PrivateKey(2);
|
|
|
|
|
let address_3 = Address::new([3; 32]);
|
|
|
|
|
let balance_to_move = 5;
|
2025-08-09 20:35:44 -03:00
|
|
|
|
|
|
|
|
let tx = transfer_transaction(
|
2025-08-08 16:19:50 -03:00
|
|
|
address_1.clone(),
|
|
|
|
|
key_1,
|
|
|
|
|
0,
|
|
|
|
|
address_2.clone(),
|
|
|
|
|
balance_to_move,
|
|
|
|
|
);
|
|
|
|
|
state.transition_from_public_transaction(&tx).unwrap();
|
|
|
|
|
let balance_to_move = 3;
|
2025-08-09 20:35:44 -03:00
|
|
|
let tx = transfer_transaction(
|
2025-08-08 16:19:50 -03:00
|
|
|
address_2.clone(),
|
|
|
|
|
key_2,
|
|
|
|
|
0,
|
|
|
|
|
address_3.clone(),
|
|
|
|
|
balance_to_move,
|
2025-08-07 09:33:54 -03:00
|
|
|
);
|
2025-08-08 16:19:50 -03:00
|
|
|
state.transition_from_public_transaction(&tx).unwrap();
|
|
|
|
|
|
|
|
|
|
assert_eq!(state.get_account_by_address(&address_1).balance, 95);
|
|
|
|
|
assert_eq!(state.get_account_by_address(&address_2).balance, 2);
|
|
|
|
|
assert_eq!(state.get_account_by_address(&address_3).balance, 3);
|
|
|
|
|
assert_eq!(state.get_account_by_address(&address_1).nonce, 1);
|
|
|
|
|
assert_eq!(state.get_account_by_address(&address_2).nonce, 1);
|
|
|
|
|
assert_eq!(state.get_account_by_address(&address_3).nonce, 0);
|
2025-08-06 20:05:04 -03:00
|
|
|
}
|
|
|
|
|
}
|