lssa/nssa/core/src/program.rs

334 lines
10 KiB
Rust

use std::collections::HashSet;
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];
pub type InstructionData = Vec<u32>;
pub const DEFAULT_PROGRAM_ID: ProgramId = [0; 8];
pub const MAX_NUMBER_CHAINED_CALLS: usize = 10;
pub struct ProgramInput<T> {
pub pre_states: Vec<AccountWithMetadata>,
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 {
/// The program ID of the program to execute
pub program_id: ProgramId,
/// The instruction data to pass
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 {
/// The instruction data the program received to produce this output
pub instruction_data: InstructionData,
/// The account pre states the program received to produce this output
pub pre_states: Vec<AccountWithMetadata>,
pub post_states: Vec<AccountPostState>,
pub chained_calls: Vec<ChainedCall>,
}
pub fn read_nssa_inputs<T: DeserializeOwned>() -> (ProgramInput<T>, InstructionData) {
let pre_states: Vec<AccountWithMetadata> = env::read();
let instruction_words: InstructionData = env::read();
let instruction = T::deserialize(&mut Deserializer::new(instruction_words.as_ref())).unwrap();
(
ProgramInput {
pre_states,
instruction,
},
instruction_words,
)
}
pub fn write_nssa_outputs(
instruction_data: InstructionData,
pre_states: Vec<AccountWithMetadata>,
post_states: Vec<AccountPostState>,
) {
let output = ProgramOutput {
instruction_data,
pre_states,
post_states,
chained_calls: Vec::new(),
};
env::commit(&output);
}
pub fn write_nssa_outputs_with_chained_call(
instruction_data: InstructionData,
pre_states: Vec<AccountWithMetadata>,
post_states: Vec<AccountPostState>,
chained_calls: Vec<ChainedCall>,
) {
let output = ProgramOutput {
instruction_data,
pre_states,
post_states,
chained_calls,
};
env::commit(&output);
}
/// Validates well-behaved program execution
///
/// # Parameters
/// - `pre_states`: The list of input accounts, each annotated with authorization metadata.
/// - `post_states`: The list of resulting accounts after executing the program logic.
/// - `executing_program_id`: The identifier of the program that was executed.
pub fn validate_execution(
pre_states: &[AccountWithMetadata],
post_states: &[AccountPostState],
executing_program_id: ProgramId,
) -> bool {
// 1. Check account ids are all different
if !validate_uniqueness_of_account_ids(pre_states) {
return false;
}
// 2. Lengths must match
if pre_states.len() != post_states.len() {
return false;
}
for (pre, post) in pre_states.iter().zip(post_states) {
// 3. Nonce must remain unchanged
if pre.account.nonce != post.account.nonce {
return false;
}
// 4. Program ownership changes are not allowed
if pre.account.program_owner != post.account.program_owner {
return false;
}
let account_program_owner = pre.account.program_owner;
// 5. Decreasing balance only allowed if owned by executing program
if post.account.balance < pre.account.balance
&& account_program_owner != executing_program_id
{
return false;
}
// 6. Data changes only allowed if owned by executing program or if account pre state has
// default values
if pre.account.data != post.account.data
&& pre.account != Account::default()
&& account_program_owner != executing_program_id
{
return false;
}
// 7. If a post state has default program owner, the pre state must have been a default
// account
if post.account.program_owner == DEFAULT_PROGRAM_ID && pre.account != Account::default() {
return false;
}
}
// 8. Total balance is preserved
let Some(total_balance_pre_states) =
WrappedBalanceSum::from_balances(pre_states.iter().map(|pre| pre.account.balance))
else {
return false;
};
let Some(total_balance_post_states) =
WrappedBalanceSum::from_balances(post_states.iter().map(|post| post.account.balance))
else {
return false;
};
if total_balance_pre_states != total_balance_post_states {
return false;
}
true
}
fn validate_uniqueness_of_account_ids(pre_states: &[AccountWithMetadata]) -> bool {
let number_of_accounts = pre_states.len();
let number_of_account_ids = pre_states
.iter()
.map(|account| &account.account_id)
.collect::<HashSet<_>>()
.len();
number_of_accounts == number_of_account_ids
}
/// Representation of a number as `lo + hi * 2^128`.
#[derive(PartialEq, Eq)]
struct WrappedBalanceSum {
lo: u128,
hi: u128,
}
impl WrappedBalanceSum {
/// Constructs a [`WrappedBalanceSum`] from an iterator of balances.
///
/// Returns [`None`] if balance sum overflows `lo + hi * 2^128` representation, which is not
/// expected in practical scenarios.
fn from_balances(balances: impl Iterator<Item = u128>) -> Option<Self> {
let mut wrapped = WrappedBalanceSum { lo: 0, hi: 0 };
for balance in balances {
let (new_sum, did_overflow) = wrapped.lo.overflowing_add(balance);
if did_overflow {
wrapped.hi = wrapped.hi.checked_add(1)?;
}
wrapped.lo = new_sum;
}
Some(wrapped)
}
}
#[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].try_into().unwrap(),
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].try_into().unwrap(),
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].try_into().unwrap(),
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);
}
}