public transactions wip

This commit is contained in:
Sergio Chouhy 2025-08-06 20:05:04 -03:00
parent a06af6da0a
commit aba8f3549f
17 changed files with 2386 additions and 29 deletions

10
Cargo.lock generated
View File

@ -2987,6 +2987,7 @@ dependencies = [
"hex",
"k256",
"log",
"nssa",
"rand 0.8.5",
"reqwest 0.11.27",
"risc0-zkvm 2.3.1 (git+https://github.com/risc0/risc0.git?branch=release-2.3)",
@ -3069,10 +3070,19 @@ dependencies = [
name = "nssa"
version = "0.1.0"
dependencies = [
"nssa-core",
"program-methods",
"risc0-zkvm 2.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "nssa-core"
version = "0.1.0"
dependencies = [
"risc0-zkvm 2.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"serde",
]
[[package]]
name = "num-bigint"
version = "0.3.3"

View File

@ -5,4 +5,5 @@ edition = "2024"
[dependencies]
risc0-zkvm = "2.2"
nssa-core = {path="core"}
program-methods = { path = "program_methods" }

8
nssa/core/Cargo.toml Normal file
View File

@ -0,0 +1,8 @@
[package]
name = "nssa-core"
version = "0.1.0"
edition = "2024"
[dependencies]
risc0-zkvm = "2.2"
serde = { version = "1.0", default-features = false }

View File

@ -0,0 +1,3 @@
pub(crate) struct Commitment {
value: [u8; 32],
}

View File

@ -0,0 +1,37 @@
mod commitment;
mod nullifier;
pub(crate) use commitment::Commitment;
pub(crate) use nullifier::Nullifier;
use serde::{Deserialize, Serialize};
use crate::program::ProgramId;
pub type Nonce = u128;
type Data = Vec<u8>;
/// Account to be used both in public and private contexts
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Account {
pub program_owner: ProgramId,
pub balance: u128,
pub data: Data,
pub nonce: Nonce,
}
#[derive(Clone, Serialize, Deserialize)]
pub struct AccountWithMetadata {
pub account: Account,
pub is_authorized: bool,
}
impl Default for Account {
fn default() -> Self {
Self {
program_owner: [0; 8],
balance: 0,
data: vec![],
nonce: 0,
}
}
}

View File

@ -0,0 +1,3 @@
pub(crate) struct Nullifier {
value: [u8; 32]
}

2
nssa/core/src/lib.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod account;
pub mod program;

63
nssa/core/src/program.rs Normal file
View File

@ -0,0 +1,63 @@
use serde::{Deserialize, Serialize};
use crate::account::{Account, AccountWithMetadata};
pub type ProgramId = [u32; 8];
/// A trait to be implemented by inner programs.
pub trait Program {
const PROGRAM_ID: ProgramId;
const PROGRAM_ELF: &[u8];
type InstructionData: Serialize + for<'de> Deserialize<'de>;
}
/// 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_constraints(
pre_states: &[AccountWithMetadata],
post_states: &[Account],
executing_program_id: ProgramId,
) -> Result<(), ()> {
// 1. Lengths must match
if pre_states.len() != post_states.len() {
return Err(());
}
for (pre, post) in pre_states.iter().zip(post_states) {
// 2. Nonce must remain unchanged
if pre.account.nonce != post.nonce {
return Err(());
}
// 3. Ownership change only allowed from default accounts
if pre.account.program_owner != post.program_owner && pre.account != Account::default() {
return Err(());
}
// 4. Decreasing balance only allowed if owned by executing program
if post.balance < pre.account.balance && pre.account.program_owner != executing_program_id {
return Err(());
}
// 5. Data changes only allowed if owned by executing program
if pre.account.data != post.data
&& (executing_program_id != pre.account.program_owner
|| executing_program_id != post.program_owner)
{
return Err(());
}
}
// 6. 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();
if total_balance_pre_states != total_balance_post_states {
return Err(());
}
Ok(())
}

File diff suppressed because it is too large Load Diff

View File

@ -7,4 +7,5 @@ edition = "2021"
[dependencies]
risc0-zkvm = { version = "2.2.0", default-features = false, features = ['std'] }
nssa-core = {path = "../../core"}

View File

@ -0,0 +1,31 @@
use nssa_core::account::{Account, AccountWithMetadata};
use risc0_zkvm::guest::env;
/// A transfer of balance program.
/// To be used both in public and private contexts.
fn main() {
// Read input accounts.
// It is expected to receive only two accounts: [sender_account, receiver_account]
let input_accounts: Vec<AccountWithMetadata> = env::read();
let balance_to_move: u128 = env::read();
// Unpack sender and receiver
assert_eq!(input_accounts.len(), 2);
let [sender, receiver] = input_accounts
.try_into()
.unwrap_or_else(|_| panic!("Bad input"));
// Check sender has authorized this operation
assert!(sender.is_authorized);
// Check sender has enough balance
assert!(sender.account.balance >= balance_to_move);
// Create accounts post states, with updated balances
let mut sender_post = sender.account.clone();
let mut receiver_post = receiver.account.clone();
sender_post.balance -= balance_to_move;
receiver_post.balance += balance_to_move;
env::commit(&vec![sender_post, receiver_post]);
}

View File

@ -1,6 +0,0 @@
use risc0_zkvm::guest::env;
fn main() {
let a: u32 = env::read();
env::commit(&a);
}

17
nssa/src/address.rs Normal file
View File

@ -0,0 +1,17 @@
use crate::signature::PublicKey;
#[derive(Clone, Hash, PartialEq, Eq)]
pub(crate) struct Address {
pub(crate) value: [u8; 32],
}
impl Address {
pub(crate) fn new(value: [u8; 32]) -> Self {
Self { value }
}
pub(crate) fn from_public_key(public_key: &PublicKey) -> Self {
// TODO: implement
Address::new([public_key.0; 32])
}
}

View File

@ -1,14 +1,47 @@
pub fn add(left: u64, right: u64) -> u64 {
left + right
use nssa_core::{
account::{Account, AccountWithMetadata},
program::{Program, ProgramId},
};
use program_methods::{AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID};
use risc0_zkvm::{ExecutorEnv, ExecutorEnvBuilder, default_executor};
mod address;
mod public_transaction;
mod signature;
pub mod state;
struct AuthenticatedTransferProgram;
impl Program for AuthenticatedTransferProgram {
const PROGRAM_ID: ProgramId = AUTHENTICATED_TRANSFER_ID;
const PROGRAM_ELF: &[u8] = AUTHENTICATED_TRANSFER_ELF;
type InstructionData = u128;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
/// Writes inputs to `env_builder` in the order expected by the programs
fn write_inputs<P: Program>(
pre_states: &[AccountWithMetadata],
instruction_data: P::InstructionData,
env_builder: &mut ExecutorEnvBuilder,
) -> Result<(), ()> {
let pre_states = pre_states.to_vec();
env_builder.write(&pre_states).map_err(|_| ())?;
env_builder.write(&instruction_data).map_err(|_| ())?;
Ok(())
}
fn execute_public<P: Program>(
pre_states: &[AccountWithMetadata],
instruction_data: P::InstructionData,
) -> Result<Vec<Account>, ()> {
// Write inputs to the program
let mut env_builder = ExecutorEnv::builder();
write_inputs::<P>(pre_states, instruction_data, &mut env_builder)?;
let env = env_builder.build().unwrap();
// Execute the program (without proving)
let executor = default_executor();
let session_info = executor.execute(env, P::PROGRAM_ELF).map_err(|_| ())?;
// Get (inputs and) outputs
session_info.journal.decode().map_err(|_| ())
}

View File

@ -0,0 +1,85 @@
use nssa_core::{
account::{Account, Nonce},
program::ProgramId,
};
use crate::{
address::Address,
signature::{PrivateKey, PublicKey, Signature},
};
pub(crate) struct Message {
pub(crate) program_id: ProgramId,
pub(crate) addresses: Vec<Address>,
pub(crate) nonces: Vec<Nonce>,
// TODO: change to Vec<u8> for general programs
pub(crate) instruction_data: u128,
}
impl Message {
pub(crate) fn new(
program_id: ProgramId,
addresses: Vec<Address>,
nonces: Vec<Nonce>,
instruction_data: u128,
) -> Self {
Self {
program_id,
addresses,
nonces,
instruction_data,
}
}
fn to_bytes(&self) -> Vec<u8> {
//TODO: implement
vec![0, 0]
}
}
pub(crate) struct WitnessSet {
pub(crate) signatures_and_public_keys: Vec<(Signature, PublicKey)>,
}
impl WitnessSet {
pub(crate) fn for_message(message: &Message, private_keys: &[PrivateKey]) -> Self {
let message_bytes = message.to_bytes();
let signatures_and_public_keys = private_keys
.iter()
.map(|key| (Signature::new(key, &message_bytes), PublicKey::new(key)))
.collect();
Self {
signatures_and_public_keys,
}
}
}
pub(crate) struct PublicTransaction {
message: Message,
witness_set: WitnessSet,
}
impl PublicTransaction {
pub(crate) fn message(&self) -> &Message {
&self.message
}
pub(crate) fn witness_set(&self) -> &WitnessSet {
&self.witness_set
}
pub(crate) fn signer_addresses(&self) -> Vec<Address> {
self.witness_set
.signatures_and_public_keys
.iter()
.map(|(_, public_key)| Address::from_public_key(public_key))
.collect()
}
pub(crate) fn new(message: Message, witness_set: WitnessSet) -> Self {
Self {
message,
witness_set,
}
}
}

26
nssa/src/signature.rs Normal file
View File

@ -0,0 +1,26 @@
use crate::{address::Address, public_transaction::Message};
pub(crate) struct Signature;
// TODO: Dummy impl. Replace by actual private key.
pub(crate) struct PrivateKey(pub(crate) u8);
// TODO: Dummy impl. Replace by actual public key.
pub(crate) struct PublicKey(pub(crate) u8);
impl PublicKey {
pub(crate) fn new(key: &PrivateKey) -> Self {
// TODO: implement
Self(key.0)
}
}
impl Signature {
pub(crate) fn new(key: &PrivateKey, message: &[u8]) -> Self {
Self
}
pub(crate) fn is_valid_for(&self, message: &Message, public_key: &PublicKey) -> bool {
// TODO: implement
true
}
}

169
nssa/src/state.rs Normal file
View File

@ -0,0 +1,169 @@
use crate::{
AuthenticatedTransferProgram, address::Address, execute_public,
public_transaction::PublicTransaction,
};
use nssa_core::{
account::{Account, AccountWithMetadata},
program::{Program, validate_constraints},
};
use std::collections::{HashMap, HashSet};
struct V01State {
public_state: HashMap<Address, Account>,
}
impl V01State {
fn transition_from_public_transaction(&mut self, tx: PublicTransaction) -> Result<(), ()> {
let state_diff = self
.execute_and_verify_public_transaction(&tx)
.map_err(|_| ())?;
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;
}
Ok(())
}
fn get_account_by_address_mut(&mut self, address: Address) -> &mut Account {
self.public_state
.entry(address)
.or_insert_with(Account::default)
}
fn get_account_by_address(&self, address: &Address) -> Account {
self.public_state
.get(address)
.cloned()
.unwrap_or(Account::default())
}
fn execute_and_verify_public_transaction(
&mut self,
tx: &PublicTransaction,
) -> Result<HashMap<Address, Account>, ()> {
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() {
return Err(());
}
if message.nonces.len() != witness_set.signatures_and_public_keys.len() {
return Err(());
}
let mut authorized_addresses = Vec::new();
for ((signature, public_key), nonce) in witness_set
.signatures_and_public_keys
.iter()
.zip(message.nonces.iter())
{
// Check the signature is valid
if !signature.is_valid_for(message, public_key) {
return Err(());
}
// 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 {
return Err(());
}
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
if message.program_id != AuthenticatedTransferProgram::PROGRAM_ID {
return Err(());
}
// // Execute program
let post_states =
execute_public::<AuthenticatedTransferProgram>(&pre_states, message.instruction_data)
.map_err(|_| ())?;
// Verify execution corresponds to a well-behaved program.
// See the # Programs section for the definition of the `validate_constraints` method.
validate_constraints(&pre_states, &post_states, message.program_id).map_err(|_| ())?;
if (post_states.len() != message.addresses.len()) {
return Err(());
}
Ok(message
.addresses
.iter()
.cloned()
.zip(post_states.into_iter())
.collect())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
public_transaction::{self, WitnessSet},
signature::PrivateKey,
};
fn genesis_state_for_tests() -> (V01State, Address) {
let account_1 = {
let mut this = Account::default();
this.program_owner = AuthenticatedTransferProgram::PROGRAM_ID;
this.balance = 100;
this
};
let address_1 = Address::new([1; 32]);
let public_state = [(address_1.clone(), account_1)].into_iter().collect();
(V01State { public_state }, address_1)
}
fn transfer_transaction_for_tests(
from: Address,
from_key: PrivateKey,
to: Address,
balance: u128,
) -> PublicTransaction {
let addresses = vec![from, to];
let nonces = vec![0];
let program_id = AuthenticatedTransferProgram::PROGRAM_ID;
let message = public_transaction::Message::new(program_id, addresses, nonces, balance);
let witness_set = WitnessSet::for_message(&message, &[from_key]);
PublicTransaction::new(message, witness_set)
}
#[test]
fn test_1() {
let (mut genesis_state, address) = genesis_state_for_tests();
let from = address;
let from_key = PrivateKey(1);
let to = Address::new([2; 32]);
let balance_to_move = 5;
let tx = transfer_transaction_for_tests(from, from_key, to.clone(), 5);
let _ = genesis_state.transition_from_public_transaction(tx);
assert_eq!(
genesis_state.get_account_by_address(&to).balance,
balance_to_move
);
}
}