refactor: use system faucet and vaults to supply accounts from genesis

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
Daniil Polyakov 2026-05-07 03:20:09 +03:00
parent 7f6fffe6cb
commit f3e807de3f
60 changed files with 934 additions and 703 deletions

13
Cargo.lock generated
View File

@ -3959,6 +3959,7 @@ dependencies = [
"token_core", "token_core",
"tokio", "tokio",
"url", "url",
"vault_core",
"wallet", "wallet",
"wallet-ffi", "wallet-ffi",
] ]
@ -7062,6 +7063,7 @@ dependencies = [
"serde", "serde",
"token_core", "token_core",
"token_program", "token_program",
"vault_core",
] ]
[[package]] [[package]]
@ -8416,7 +8418,6 @@ name = "sequencer_core"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"authenticated_transfer_core",
"borsh", "borsh",
"bytesize", "bytesize",
"chrono", "chrono",
@ -8440,6 +8441,7 @@ dependencies = [
"testnet_initial_state", "testnet_initial_state",
"tokio", "tokio",
"url", "url",
"vault_core",
] ]
[[package]] [[package]]
@ -10077,6 +10079,15 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]]
name = "vault_core"
version = "0.1.0"
dependencies = [
"nssa_core",
"risc0-zkvm",
"serde",
]
[[package]] [[package]]
name = "vcpkg" name = "vcpkg"
version = "0.2.15" version = "0.2.15"

View File

@ -21,6 +21,7 @@ members = [
"programs/associated_token_account/core", "programs/associated_token_account/core",
"programs/associated_token_account", "programs/associated_token_account",
"programs/authenticated_transfer/core", "programs/authenticated_transfer/core",
"programs/vault/core",
"sequencer/core", "sequencer/core",
"sequencer/service", "sequencer/service",
"sequencer/service/protocol", "sequencer/service/protocol",
@ -67,6 +68,7 @@ amm_program = { path = "programs/amm" }
ata_core = { path = "programs/associated_token_account/core" } ata_core = { path = "programs/associated_token_account/core" }
ata_program = { path = "programs/associated_token_account" } ata_program = { path = "programs/associated_token_account" }
authenticated_transfer_core = { path = "programs/authenticated_transfer/core" } authenticated_transfer_core = { path = "programs/authenticated_transfer/core" }
vault_core = { path = "programs/vault/core" }
test_program_methods = { path = "test_program_methods" } test_program_methods = { path = "test_program_methods" }
testnet_initial_state = { path = "testnet_initial_state" } testnet_initial_state = { path = "testnet_initial_state" }

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -67,7 +67,8 @@ impl NSSATransaction {
} }
/// Validates the transaction against the current state and returns the resulting diff /// Validates the transaction against the current state and returns the resulting diff
/// without applying it. Rejects transactions that modify clock system accounts. /// without applying it. Rejects transactions that modify clock system accounts and
/// rejects unsafe modifications of the system faucet account.
pub fn validate_on_state( pub fn validate_on_state(
&self, &self,
state: &V03State, state: &V03State,
@ -98,6 +99,36 @@ impl NSSATransaction {
)); ));
} }
let faucet_account_id = nssa::SYSTEM_FAUCET_ACCOUNT_ID;
if let Some(post_faucet) = public_diff.get(&faucet_account_id) {
let pre_faucet = state.get_account_by_id(faucet_account_id);
let nssa::Account {
program_owner: post_program_owner,
data: post_data,
nonce: post_nonce,
balance: post_balance,
} = post_faucet;
let nssa::Account {
program_owner: pre_program_owner,
data: pre_data,
nonce: pre_nonce,
balance: pre_balance,
} = pre_faucet;
let faucet_change_is_allowed = *post_program_owner == pre_program_owner
&& *post_data == pre_data
&& *post_nonce == pre_nonce
&& *post_balance >= pre_balance;
if !faucet_change_is_allowed {
return Err(nssa::error::NssaError::InvalidInput(
"Transaction modifies system faucet account".into(),
));
}
}
Ok(diff) Ok(diff)
} }

View File

@ -18,13 +18,13 @@
"indexer_rpc_url": "ws://indexer_service:8779", "indexer_rpc_url": "ws://indexer_service:8779",
"genesis": [ "genesis": [
{ {
"supply_public_account": { "supply_account": {
"account_id": "6iArKUXxhUJqS7kCaPNhwMWt3ro71PDyBj7jwAyE2VQV", "account_id": "6iArKUXxhUJqS7kCaPNhwMWt3ro71PDyBj7jwAyE2VQV",
"balance": 10000 "balance": 10000
} }
}, },
{ {
"supply_public_account": { "supply_account": {
"account_id": "7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo", "account_id": "7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo",
"balance": 20000 "balance": 20000
} }

View File

@ -8,7 +8,7 @@ use common::{
use log::info; use log::info;
use logos_blockchain_core::{header::HeaderId, mantle::ops::channel::MsgId}; use logos_blockchain_core::{header::HeaderId, mantle::ops::channel::MsgId};
use logos_blockchain_zone_sdk::Slot; use logos_blockchain_zone_sdk::Slot;
use nssa::{Account, AccountId, V03State, ValidatedStateDiff}; use nssa::{Account, AccountId, V03State};
use nssa_core::BlockId; use nssa_core::BlockId;
use storage::indexer::RocksDBIO; use storage::indexer::RocksDBIO;
use tokio::sync::RwLock; use tokio::sync::RwLock;
@ -160,12 +160,13 @@ impl IndexerStore {
anyhow::bail!("Genesis block should contain only public transactions") anyhow::bail!("Genesis block should contain only public transactions")
} }
}; };
let state_diff = ValidatedStateDiff::from_public_genesis_transaction( state_guard
genesis_tx, .transition_from_public_transaction(
&state_guard, genesis_tx,
) block.header.block_id,
.context("Failed to create state diff from genesis transaction")?; block.header.timestamp,
state_guard.apply_state_diff(state_diff); )
.context("Failed to execute genesis public transaction")?;
} else { } else {
transaction transaction
.clone() .clone()
@ -202,39 +203,11 @@ impl IndexerStore {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use common::{HashType, block::HashableBlockData}; use common::{HashType, block::HashableBlockData};
use nssa::{AccountId, CLOCK_01_PROGRAM_ACCOUNT_ID, PublicKey, PublicTransaction};
use tempfile::tempdir; use tempfile::tempdir;
use testnet_initial_state::initial_pub_accounts_private_keys;
use super::*; use super::*;
fn acc1_sign_key() -> nssa::PrivateKey {
nssa::PrivateKey::try_new([1; 32]).unwrap()
}
fn acc2_sign_key() -> nssa::PrivateKey {
nssa::PrivateKey::try_new([2; 32]).unwrap()
}
fn acc1() -> AccountId {
AccountId::from(&PublicKey::new_from_private_key(&acc1_sign_key()))
}
fn acc2() -> AccountId {
AccountId::from(&PublicKey::new_from_private_key(&acc2_sign_key()))
}
fn genesis_mint_tx(account: AccountId, balance: u128) -> NSSATransaction {
let message = nssa::public_transaction::Message::try_new(
nssa::program::Program::authenticated_transfer_program().id(),
vec![account, CLOCK_01_PROGRAM_ACCOUNT_ID],
vec![],
authenticated_transfer_core::Instruction::Mint { amount: balance },
)
.unwrap();
let witness_set = nssa::public_transaction::WitnessSet::for_message(&message, &[]);
PublicTransaction::new(message, witness_set).into()
}
#[test] #[test]
fn correct_startup() { fn correct_startup() {
let home = tempdir().unwrap(); let home = tempdir().unwrap();
@ -252,19 +225,18 @@ mod tests {
let storage = IndexerStore::open_db(home.as_ref()).unwrap(); let storage = IndexerStore::open_db(home.as_ref()).unwrap();
let from = acc1(); let initial_accounts = initial_pub_accounts_private_keys();
let to = acc2(); let from = initial_accounts[0].account_id;
let sign_key = acc1_sign_key(); let to = initial_accounts[1].account_id;
let sign_key = initial_accounts[0].pub_sign_key.clone();
// Submit genesis block // Submit genesis block
let clock_tx = NSSATransaction::Public(clock_invocation(0)); let clock_tx = NSSATransaction::Public(clock_invocation(0));
let supply_from_tx = genesis_mint_tx(from, 10000);
let supply_to_tx = genesis_mint_tx(to, 20000);
let genesis_block_data = HashableBlockData { let genesis_block_data = HashableBlockData {
block_id: 1, block_id: 1,
prev_block_hash: HashType::default(), prev_block_hash: HashType::default(),
timestamp: 0, timestamp: 0,
transactions: vec![supply_from_tx, supply_to_tx, clock_tx], transactions: vec![clock_tx],
}; };
let genesis_block = genesis_block_data.into_pending_block( let genesis_block = genesis_block_data.into_pending_block(
&common::test_utils::sequencer_sign_key_for_testing(), &common::test_utils::sequencer_sign_key_for_testing(),
@ -276,30 +248,29 @@ mod tests {
.await .await
.unwrap(); .unwrap();
for i in 2..10 { for i in 0..10 {
let tx = common::test_utils::create_transaction_native_token_transfer( let tx = common::test_utils::create_transaction_native_token_transfer(
from, from, i, to, 10, &sign_key,
i - 2,
to,
10,
&sign_key,
); );
let block_id = u64::try_from(i).unwrap(); let block_id = u64::try_from(i + 1).unwrap();
let next_block = common::test_utils::produce_dummy_block(block_id, prev_hash, vec![tx]); let next_block = common::test_utils::produce_dummy_block(block_id, prev_hash, vec![tx]);
prev_hash = Some(next_block.header.hash); prev_hash = Some(next_block.header.hash);
storage storage
.put_block(next_block, HeaderId::from([u8::try_from(i).unwrap(); 32])) .put_block(
next_block,
HeaderId::from([u8::try_from(i + 1).unwrap(); 32]),
)
.await .await
.unwrap(); .unwrap();
} }
let acc1_val = storage.account_current_state(&acc1()).await.unwrap(); let acc1_val = storage.account_current_state(&from).await.unwrap();
let acc2_val = storage.account_current_state(&acc2()).await.unwrap(); let acc2_val = storage.account_current_state(&to).await.unwrap();
assert_eq!(acc1_val.balance, 9920); assert_eq!(acc1_val.balance, 9900);
assert_eq!(acc2_val.balance, 20080); assert_eq!(acc2_val.balance, 20100);
} }
#[tokio::test] #[tokio::test]
@ -310,45 +281,45 @@ mod tests {
let mut prev_hash = None; let mut prev_hash = None;
let from = acc1(); let initial_accounts = initial_pub_accounts_private_keys();
let to = acc2(); let from = initial_accounts[0].account_id;
let sign_key = acc1_sign_key(); let to = initial_accounts[1].account_id;
let sign_key = initial_accounts[0].pub_sign_key.clone();
for i in 2..10 { for i in 0..10 {
let tx = common::test_utils::create_transaction_native_token_transfer( let tx = common::test_utils::create_transaction_native_token_transfer(
from, from, i, to, 10, &sign_key,
i - 2,
to,
10,
&sign_key,
); );
let block_id = u64::try_from(i).unwrap(); let block_id = u64::try_from(i + 1).unwrap();
let next_block = common::test_utils::produce_dummy_block(block_id, prev_hash, vec![tx]); let next_block = common::test_utils::produce_dummy_block(block_id, prev_hash, vec![tx]);
prev_hash = Some(next_block.header.hash); prev_hash = Some(next_block.header.hash);
storage storage
.put_block(next_block, HeaderId::from([u8::try_from(i).unwrap(); 32])) .put_block(
next_block,
HeaderId::from([u8::try_from(i + 1).unwrap(); 32]),
)
.await .await
.unwrap(); .unwrap();
} }
// Genesis block: no transfers applied yet. // Genesis block: no transfers applied yet.
let acc1_at_1 = storage.account_state_at_block(&acc1(), 1).unwrap(); let acc1_at_1 = storage.account_state_at_block(&from, 1).unwrap();
let acc2_at_1 = storage.account_state_at_block(&acc2(), 1).unwrap(); let acc2_at_1 = storage.account_state_at_block(&to, 1).unwrap();
assert_eq!(acc1_at_1.balance, 10000); assert_eq!(acc1_at_1.balance, 9990);
assert_eq!(acc2_at_1.balance, 20000); assert_eq!(acc2_at_1.balance, 20010);
// After block 5: 4 transfers of 10 applied (one each in blocks 2..=5). // After block 5: 4 transfers of 10 applied (one each in blocks 2..=5).
let acc1_at_5 = storage.account_state_at_block(&acc1(), 5).unwrap(); let acc1_at_5 = storage.account_state_at_block(&from, 5).unwrap();
let acc2_at_5 = storage.account_state_at_block(&acc2(), 5).unwrap(); let acc2_at_5 = storage.account_state_at_block(&to, 5).unwrap();
assert_eq!(acc1_at_5.balance, 9960); assert_eq!(acc1_at_5.balance, 9950);
assert_eq!(acc2_at_5.balance, 20040); assert_eq!(acc2_at_5.balance, 20050);
// After final block 9: 8 transfers applied; should match current state. // After final block 9: 8 transfers applied; should match current state.
let acc1_at_9 = storage.account_state_at_block(&acc1(), 9).unwrap(); let acc1_at_9 = storage.account_state_at_block(&from, 9).unwrap();
let acc2_at_9 = storage.account_state_at_block(&acc2(), 9).unwrap(); let acc2_at_9 = storage.account_state_at_block(&to, 9).unwrap();
assert_eq!(acc1_at_9.balance, 9920); assert_eq!(acc1_at_9.balance, 9910);
assert_eq!(acc2_at_9.balance, 20080); assert_eq!(acc2_at_9.balance, 20090);
} }
} }

View File

@ -20,6 +20,7 @@ indexer_service.workspace = true
serde_json.workspace = true serde_json.workspace = true
token_core.workspace = true token_core.workspace = true
ata_core.workspace = true ata_core.workspace = true
vault_core.workspace = true
indexer_service_rpc = { workspace = true, features = ["client"] } indexer_service_rpc = { workspace = true, features = ["client"] }
sequencer_service_rpc = { workspace = true, features = ["client"] } sequencer_service_rpc = { workspace = true, features = ["client"] }
jsonrpsee = { workspace = true, features = ["ws-client"] } jsonrpsee = { workspace = true, features = ["ws-client"] }

View File

@ -3,14 +3,30 @@ use std::{net::SocketAddr, path::PathBuf, time::Duration};
use anyhow::{Context as _, Result}; use anyhow::{Context as _, Result};
use bytesize::ByteSize; use bytesize::ByteSize;
use indexer_service::{ChannelId, ClientConfig, IndexerConfig}; use indexer_service::{ChannelId, ClientConfig, IndexerConfig};
use key_protocol::key_management::KeyChain;
use nssa::{AccountId, PrivateKey, PublicKey}; use nssa::{AccountId, PrivateKey, PublicKey};
use nssa_core::Identifier;
use sequencer_core::config::{BedrockConfig, GenesisTransaction, SequencerConfig}; use sequencer_core::config::{BedrockConfig, GenesisTransaction, SequencerConfig};
use url::Url; use url::Url;
use wallet::config::WalletConfig; use wallet::config::WalletConfig;
pub const INITIAL_PUBLIC_BALANCES_FOR_WALLET: [u128; 2] = [20_000, 40_000]; pub const INITIAL_PUBLIC_BALANCES_FOR_WALLET: [u128; 2] = [10_000, 20_000];
pub const INITIAL_PRIVATE_BALANCES_FOR_WALLET: [u128; 2] = [10_000, 20_000]; pub const INITIAL_PRIVATE_BALANCES_FOR_WALLET: [u128; 2] = [10_000, 20_000];
#[derive(Clone)]
pub struct InitialPrivateAccountForWallet {
pub key_chain: KeyChain,
pub identifier: Identifier,
pub balance: u128,
}
impl InitialPrivateAccountForWallet {
#[must_use]
pub fn account_id(&self) -> AccountId {
AccountId::from((&self.key_chain.nullifier_public_key, self.identifier))
}
}
/// Sequencer config options available for custom changes in integration tests. /// Sequencer config options available for custom changes in integration tests.
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub struct SequencerPartialConfig { pub struct SequencerPartialConfig {
@ -80,43 +96,58 @@ pub fn sequencer_config(
#[must_use] #[must_use]
pub fn default_public_accounts_for_wallet() -> Vec<(PrivateKey, u128)> { pub fn default_public_accounts_for_wallet() -> Vec<(PrivateKey, u128)> {
let mut first_private_key = PrivateKey::new_os_random(); let mut private_keys = vec![PrivateKey::new_os_random(), PrivateKey::new_os_random()];
let first_public_key = PublicKey::new_from_private_key(&first_private_key); private_keys.sort_unstable_by_key(|private_key| {
let mut first_account_id = AccountId::from(&first_public_key); AccountId::from(&PublicKey::new_from_private_key(private_key))
});
let mut second_private_key = PrivateKey::new_os_random(); private_keys
let second_public_key = PublicKey::new_from_private_key(&second_private_key); .into_iter()
let mut second_account_id = AccountId::from(&second_public_key); .zip(INITIAL_PUBLIC_BALANCES_FOR_WALLET)
.collect()
// Keep account ordering deterministic for tests that index into account lists.
if first_account_id > second_account_id {
std::mem::swap(&mut first_private_key, &mut second_private_key);
std::mem::swap(&mut first_account_id, &mut second_account_id);
}
vec![
(first_private_key, INITIAL_PUBLIC_BALANCES_FOR_WALLET[0]),
(second_private_key, INITIAL_PUBLIC_BALANCES_FOR_WALLET[1]),
]
} }
#[must_use] #[must_use]
pub fn genesis_from_public_accounts( pub fn default_private_accounts_for_wallet() -> Vec<InitialPrivateAccountForWallet> {
public_accounts: &[(PrivateKey, u128)], let mut key_chains = vec![KeyChain::new_os_random(), KeyChain::new_os_random()];
) -> Vec<GenesisTransaction> { key_chains.sort_unstable();
public_accounts
.iter() key_chains
.map(|(private_key, balance)| { .into_iter()
let public_key = PublicKey::new_from_private_key(private_key); .zip(INITIAL_PRIVATE_BALANCES_FOR_WALLET)
let account_id = AccountId::from(&public_key); .map(|(key_chain, balance)| InitialPrivateAccountForWallet {
GenesisTransaction::SupplyPublicAccount { key_chain,
account_id, identifier: 0,
balance: *balance, balance,
}
}) })
.collect() .collect()
} }
#[must_use]
pub fn genesis_from_accounts(
public_accounts: &[(PrivateKey, u128)],
private_accounts: &[InitialPrivateAccountForWallet],
) -> Vec<GenesisTransaction> {
let public_genesis = public_accounts.iter().map(|(private_key, balance)| {
let public_key = PublicKey::new_from_private_key(private_key);
let account_id = AccountId::from(&public_key);
GenesisTransaction::SupplyAccount {
account_id,
balance: *balance,
}
});
let private_genesis =
private_accounts
.iter()
.map(|account| GenesisTransaction::SupplyAccount {
account_id: account.account_id(),
balance: account.balance,
});
public_genesis.chain(private_genesis).collect()
}
pub fn wallet_config(sequencer_addr: SocketAddr) -> Result<WalletConfig> { pub fn wallet_config(sequencer_addr: SocketAddr) -> Result<WalletConfig> {
Ok(WalletConfig { Ok(WalletConfig {
sequencer_addr: addr_to_url(UrlProtocol::Http, sequencer_addr) sequencer_addr: addr_to_url(UrlProtocol::Http, sequencer_addr)

View File

@ -20,7 +20,7 @@ use crate::{
indexer_client::IndexerClient, indexer_client::IndexerClient,
setup::{ setup::{
setup_bedrock_node, setup_indexer, setup_private_accounts_with_initial_supply, setup_bedrock_node, setup_indexer, setup_private_accounts_with_initial_supply,
setup_sequencer, setup_wallet, setup_public_accounts_with_initial_supply, setup_sequencer, setup_wallet,
}, },
}; };
@ -225,7 +225,7 @@ impl TestContextBuilder {
Self { Self {
genesis_transactions: None, genesis_transactions: None,
sequencer_partial_config: None, sequencer_partial_config: None,
enable_indexer: false, enable_indexer: true,
} }
} }
@ -290,19 +290,29 @@ impl TestContextBuilder {
}; };
let initial_public_accounts = config::default_public_accounts_for_wallet(); let initial_public_accounts = config::default_public_accounts_for_wallet();
let initial_private_accounts = config::default_private_accounts_for_wallet();
let (sequencer_handle, temp_sequencer_dir) = setup_sequencer( let (sequencer_handle, temp_sequencer_dir) = setup_sequencer(
sequencer_partial_config.unwrap_or_default(), sequencer_partial_config.unwrap_or_default(),
bedrock_addr, bedrock_addr,
genesis_transactions genesis_transactions.unwrap_or_else(|| {
.unwrap_or_else(|| config::genesis_from_public_accounts(&initial_public_accounts)), config::genesis_from_accounts(&initial_public_accounts, &initial_private_accounts)
}),
) )
.await .await
.context("Failed to setup Sequencer")?; .context("Failed to setup Sequencer")?;
let (mut wallet, temp_wallet_dir, wallet_password) = let (mut wallet, temp_wallet_dir, wallet_password) = setup_wallet(
setup_wallet(sequencer_handle.addr(), &initial_public_accounts) sequencer_handle.addr(),
.context("Failed to setup wallet")?; &initial_public_accounts,
setup_private_accounts_with_initial_supply(&mut wallet) &initial_private_accounts,
)
.context("Failed to setup wallet")?;
setup_public_accounts_with_initial_supply(&wallet, &initial_public_accounts)
.await
.context("Failed to initialize public accounts in wallet")?;
setup_private_accounts_with_initial_supply(&mut wallet, &initial_private_accounts)
.await .await
.context("Failed to initialize private accounts in wallet")?; .context("Failed to initialize private accounts in wallet")?;

View File

@ -1,27 +1,21 @@
use std::{net::SocketAddr, path::PathBuf}; use std::{collections::HashMap, net::SocketAddr, path::PathBuf};
use anyhow::{Context as _, Result, bail}; use anyhow::{Context as _, Result, bail};
use common::transaction::NSSATransaction;
use indexer_service::IndexerHandle; use indexer_service::IndexerHandle;
use log::{debug, warn}; use log::{debug, warn};
use nssa::PrivateKey; use nssa::{AccountId, PrivateKey, PublicKey, PublicTransaction, program::Program};
use sequencer_service::{GenesisTransaction, SequencerHandle}; use sequencer_service::{GenesisTransaction, SequencerHandle};
use sequencer_service_rpc::RpcClient as _;
use tempfile::TempDir; use tempfile::TempDir;
use testcontainers::compose::DockerCompose; use testcontainers::compose::DockerCompose;
use wallet::{ use wallet::{
WalletCore, AccDecodeData::Decode, PrivacyPreservingAccount, WalletCore, config::WalletConfigOverrides,
cli::{
Command, SubcommandReturnValue,
account::{AccountSubcommand, NewSubcommand},
execute_subcommand,
programs::native_token_transfer::AuthTransferSubcommand,
},
config::WalletConfigOverrides,
}; };
use crate::{ use crate::{
BEDROCK_SERVICE_PORT, BEDROCK_SERVICE_WITH_OPEN_PORT, BEDROCK_SERVICE_PORT, BEDROCK_SERVICE_WITH_OPEN_PORT,
config::{self, INITIAL_PRIVATE_BALANCES_FOR_WALLET}, config::{self, InitialPrivateAccountForWallet},
private_mention, public_mention,
}; };
pub async fn setup_bedrock_node() -> Result<(DockerCompose, SocketAddr)> { pub async fn setup_bedrock_node() -> Result<(DockerCompose, SocketAddr)> {
@ -141,6 +135,7 @@ pub async fn setup_sequencer(
pub fn setup_wallet( pub fn setup_wallet(
sequencer_addr: SocketAddr, sequencer_addr: SocketAddr,
initial_public_accounts: &[(PrivateKey, u128)], initial_public_accounts: &[(PrivateKey, u128)],
initial_private_accounts: &[InitialPrivateAccountForWallet],
) -> Result<(WalletCore, TempDir, String)> { ) -> Result<(WalletCore, TempDir, String)> {
let config = config::wallet_config(sequencer_addr).context("Failed to create Wallet config")?; let config = config::wallet_config(sequencer_addr).context("Failed to create Wallet config")?;
let config_serialized = let config_serialized =
@ -172,6 +167,18 @@ pub fn setup_wallet(
.add_imported_public_account(private_key.clone()); .add_imported_public_account(private_key.clone());
} }
for private_account in initial_private_accounts {
wallet
.storage_mut()
.key_chain_mut()
.add_imported_private_account(
private_account.key_chain.clone(),
None,
private_account.identifier,
nssa::Account::default(),
);
}
wallet wallet
.store_persistent_data() .store_persistent_data()
.context("Failed to store wallet persistent data")?; .context("Failed to store wallet persistent data")?;
@ -179,72 +186,142 @@ pub fn setup_wallet(
Ok((wallet, temp_wallet_dir, wallet_password)) Ok((wallet, temp_wallet_dir, wallet_password))
} }
pub async fn setup_private_accounts_with_initial_supply(wallet: &mut WalletCore) -> Result<()> { pub async fn setup_public_accounts_with_initial_supply(
for _ in INITIAL_PRIVATE_BALANCES_FOR_WALLET { wallet: &WalletCore,
let result = execute_subcommand( initial_public_accounts: &[(PrivateKey, u128)],
) -> Result<()> {
for (private_key, amount) in initial_public_accounts {
claim_funds_from_vault(
wallet, wallet,
Command::Account(AccountSubcommand::New(NewSubcommand::Private { AccountId::from(&PublicKey::new_from_private_key(private_key)),
cci: None, *amount,
label: None,
})),
) )
.await .await
.context("Failed to create a private account")?; .context("Failed to claim funds from vault into public account")?;
let SubcommandReturnValue::RegisterAccount { account_id: _ } = result else {
bail!("Expected RegisterAccount return value when creating private account");
};
}
let public_account_ids: Vec<_> = wallet
.storage()
.key_chain()
.public_account_ids()
.map(|(account_id, _idx)| account_id)
.collect();
if public_account_ids.len() < INITIAL_PRIVATE_BALANCES_FOR_WALLET.len() {
bail!(
"Expected at least {} public accounts in wallet storage, found {}",
INITIAL_PRIVATE_BALANCES_FOR_WALLET.len(),
public_account_ids.len()
);
}
let private_account_ids: Vec<_> = wallet
.storage()
.key_chain()
.private_account_ids()
.map(|(account_id, _idx)| account_id)
.collect();
for ((from, to), amount) in public_account_ids
.into_iter()
.zip(private_account_ids.into_iter())
.zip(INITIAL_PRIVATE_BALANCES_FOR_WALLET)
{
let result = execute_subcommand(
wallet,
Command::AuthTransfer(AuthTransferSubcommand::Send {
from: public_mention(from),
to: Some(private_mention(to)),
to_npk: None,
to_vpk: None,
to_identifier: None,
amount,
}),
)
.await
.context("Failed to perform initial shielded transfer to private account")?;
if !matches!(
result,
SubcommandReturnValue::PrivacyPreservingTransfer { .. }
) {
bail!(
"Expected PrivacyPreservingTransfer return value when shielding initial private funds"
);
}
} }
Ok(()) Ok(())
} }
pub async fn setup_private_accounts_with_initial_supply(
wallet: &mut WalletCore,
initial_private_accounts: &[InitialPrivateAccountForWallet],
) -> Result<()> {
for private_account in initial_private_accounts {
claim_funds_from_vault_to_private(
wallet,
private_account.account_id(),
private_account.balance,
)
.await
.context("Failed to claim funds from vault into private account")?;
}
Ok(())
}
async fn claim_funds_from_vault(
wallet: &WalletCore,
owner_id: AccountId,
amount: u128,
) -> Result<()> {
let vault_program_id = Program::vault().id();
let owner_vault_id = vault_core::compute_vault_account_id(vault_program_id, owner_id);
let nonces = wallet
.get_accounts_nonces(vec![owner_id])
.await
.context("Failed to fetch owner nonce")?;
let signing_key = wallet
.storage()
.key_chain()
.pub_account_signing_key(owner_id)
.with_context(|| format!("Missing signing key for public account {owner_id}"))?;
let message = nssa::public_transaction::Message::try_new(
vault_program_id,
vec![owner_id, owner_vault_id],
nonces,
vault_core::Instruction::Claim { amount },
)
.context("Failed to build vault claim message")?;
let witness_set = nssa::public_transaction::WitnessSet::for_message(&message, &[signing_key]);
let tx = PublicTransaction::new(message, witness_set);
let tx_hash = wallet
.sequencer_client
.send_transaction(NSSATransaction::Public(tx))
.await
.context("Failed to submit vault claim transaction")?;
wallet
.poll_native_token_transfer(tx_hash)
.await
.context("Failed to confirm vault claim transaction")?;
Ok(())
}
async fn claim_funds_from_vault_to_private(
wallet: &mut WalletCore,
owner_id: AccountId,
amount: u128,
) -> Result<()> {
let Some(_) = wallet.storage().key_chain().private_account(owner_id) else {
bail!("Missing private account in wallet key chain for account {owner_id}");
};
let vault_program = Program::vault();
let vault_program_id = vault_program.id();
let owner_vault_id = vault_core::compute_vault_account_id(vault_program_id, owner_id);
let instruction_data =
Program::serialize_instruction(vault_core::Instruction::Claim { amount })
.context("Failed to serialize vault private claim instruction")?;
let program_with_dependencies =
nssa::privacy_preserving_transaction::circuit::ProgramWithDependencies::new(
vault_program,
HashMap::from([(
Program::authenticated_transfer_program().id(),
Program::authenticated_transfer_program(),
)]),
);
let (tx_hash, mut secrets) = wallet
.send_privacy_preserving_tx(
vec![
PrivacyPreservingAccount::PrivateOwned(owner_id),
PrivacyPreservingAccount::Public(owner_vault_id),
],
instruction_data,
&program_with_dependencies,
)
.await
.context("Failed to submit private vault claim transaction")?;
let secret = secrets
.pop()
.context("Expected one private output secret for vault claim")?;
let transfer_tx = wallet
.poll_native_token_transfer(tx_hash)
.await
.context("Failed to confirm private vault claim transaction")?;
let NSSATransaction::PrivacyPreserving(tx) = transfer_tx else {
bail!("Expected privacy preserving transaction result for private vault claim");
};
wallet
.decode_insert_privacy_preserving_transaction_results(&tx, &[Decode(secret, owner_id)])
.context("Failed to decode private vault claim transaction")?;
wallet
.store_persistent_data()
.context("Failed to store wallet data after private vault claim")?;
Ok(())
}

View File

@ -1,9 +1,10 @@
use std::time::Duration; use std::time::Duration;
use anyhow::Result; use anyhow::Result;
use common::transaction::NSSATransaction;
use integration_tests::{TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, public_mention}; use integration_tests::{TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, public_mention};
use log::info; use log::info;
use nssa::program::Program; use nssa::{SYSTEM_FAUCET_ACCOUNT_ID, program::Program, public_transaction};
use sequencer_service_rpc::RpcClient as _; use sequencer_service_rpc::RpcClient as _;
use tokio::test; use tokio::test;
use wallet::{ use wallet::{
@ -344,3 +345,90 @@ async fn successful_transfer_using_to_label() -> Result<()> {
Ok(()) Ok(())
} }
#[test]
async fn cannot_transfer_funds_from_system_faucet_account() -> Result<()> {
let ctx = TestContext::new().await?;
let recipient = ctx.existing_public_accounts()[0];
let recipient_balance_before = ctx
.sequencer_client()
.get_account_balance(recipient)
.await?;
let faucet_balance_before = ctx
.sequencer_client()
.get_account_balance(SYSTEM_FAUCET_ACCOUNT_ID)
.await?;
let amount = 1_u128;
let message = public_transaction::Message::try_new(
Program::authenticated_transfer_program().id(),
vec![SYSTEM_FAUCET_ACCOUNT_ID, recipient],
vec![],
authenticated_transfer_core::Instruction::Transfer { amount },
)?;
let tx = nssa::PublicTransaction::new(
message,
nssa::public_transaction::WitnessSet::from_raw_parts(vec![]),
);
let tx_hash = ctx
.sequencer_client()
.send_transaction(NSSATransaction::Public(tx))
.await?;
info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
let recipient_balance_after = ctx
.sequencer_client()
.get_account_balance(recipient)
.await?;
let faucet_balance_after = ctx
.sequencer_client()
.get_account_balance(SYSTEM_FAUCET_ACCOUNT_ID)
.await?;
let tx_on_chain = ctx.sequencer_client().get_transaction(tx_hash).await?;
assert_eq!(recipient_balance_after, recipient_balance_before);
assert_eq!(faucet_balance_after, faucet_balance_before);
assert!(tx_on_chain.is_none());
Ok(())
}
#[test]
async fn can_transfer_funds_to_system_faucet_account() -> Result<()> {
let mut ctx = TestContext::new().await?;
let sender = ctx.existing_public_accounts()[0];
let sender_balance_before = ctx.sequencer_client().get_account_balance(sender).await?;
let faucet_balance_before = ctx
.sequencer_client()
.get_account_balance(SYSTEM_FAUCET_ACCOUNT_ID)
.await?;
let amount = 100_u128;
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: public_mention(sender),
to: Some(public_mention(SYSTEM_FAUCET_ACCOUNT_ID)),
to_npk: None,
to_vpk: None,
to_identifier: Some(0),
amount,
});
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
let sender_balance_after = ctx.sequencer_client().get_account_balance(sender).await?;
let faucet_balance_after = ctx
.sequencer_client()
.get_account_balance(SYSTEM_FAUCET_ACCOUNT_ID)
.await?;
assert_eq!(sender_balance_after, sender_balance_before - amount);
assert_eq!(faucet_balance_after, faucet_balance_before + amount);
Ok(())
}

View File

@ -125,9 +125,9 @@ fn indexer_test_run_ffi() -> Result<()> {
let last_block_indexer_ffi = unsafe { *last_block_indexer_ffi_res.value }; let last_block_indexer_ffi = unsafe { *last_block_indexer_ffi_res.value };
info!("Last block on ind ffi now is {last_block_indexer_ffi}"); info!("Last block on indexer FFI now is {last_block_indexer_ffi}");
assert!(last_block_indexer_ffi > 1); assert!(last_block_indexer_ffi > 0);
Ok(()) Ok(())
} }
@ -149,9 +149,9 @@ fn indexer_ffi_block_batching() -> Result<()> {
let last_block_indexer = unsafe { *last_block_indexer_ffi_res.value }; let last_block_indexer = unsafe { *last_block_indexer_ffi_res.value };
info!("Last block on ind now is {last_block_indexer}"); info!("Last block on indexer FFI now is {last_block_indexer}");
assert!(last_block_indexer > 1); assert!(last_block_indexer > 0);
let before_ffi = FfiOption::<u64>::from_none(); let before_ffi = FfiOption::<u64>::from_none();
let limit = 100; let limit = 100;

View File

@ -97,7 +97,7 @@ impl TpsTestManager {
fn generate_genesis(&self) -> Vec<GenesisTransaction> { fn generate_genesis(&self) -> Vec<GenesisTransaction> {
self.public_keypairs self.public_keypairs
.iter() .iter()
.map(|(_, account_id)| GenesisTransaction::SupplyPublicAccount { .map(|(_, account_id)| GenesisTransaction::SupplyAccount {
account_id: *account_id, account_id: *account_id,
balance: 10, balance: 10,
}) })

View File

@ -25,6 +25,8 @@ pub mod program;
pub mod error; pub mod error;
pub const GENESIS_BLOCK_ID: BlockId = 1; pub const GENESIS_BLOCK_ID: BlockId = 1;
pub const SYSTEM_FAUCET_ACCOUNT_ID: account::AccountId =
account::AccountId::new(*b"/LEZ/SystemFaucetAccount/0000000");
pub type BlockId = u64; pub type BlockId = u64;
/// Unix timestamp in milliseconds. /// Unix timestamp in milliseconds.

View File

@ -4,7 +4,7 @@
)] )]
pub use nssa_core::{ pub use nssa_core::{
GENESIS_BLOCK_ID, SharedSecretKey, GENESIS_BLOCK_ID, SYSTEM_FAUCET_ACCOUNT_ID, SharedSecretKey,
account::{Account, AccountId, Data}, account::{Account, AccountId, Data},
encryption::EphemeralPublicKey, encryption::EphemeralPublicKey,
program::ProgramId, program::ProgramId,

View File

@ -543,7 +543,11 @@ mod tests {
let recipient = AccountWithMetadata::new(Account::default(), false, shared_account_id); let recipient = AccountWithMetadata::new(Account::default(), false, shared_account_id);
let balance_to_move: u128 = 100; let balance_to_move: u128 = 100;
let instruction = Program::serialize_instruction(balance_to_move).unwrap(); let instruction =
Program::serialize_instruction(authenticated_transfer_core::Instruction::Transfer {
amount: balance_to_move,
})
.unwrap();
let result = execute_and_prove( let result = execute_and_prove(
vec![sender, recipient], vec![sender, recipient],

View File

@ -11,7 +11,7 @@ use crate::{
program_methods::{ program_methods::{
AMM_ELF, AMM_ID, ASSOCIATED_TOKEN_ACCOUNT_ELF, ASSOCIATED_TOKEN_ACCOUNT_ID, AMM_ELF, AMM_ID, ASSOCIATED_TOKEN_ACCOUNT_ELF, ASSOCIATED_TOKEN_ACCOUNT_ID,
AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID, CLOCK_ELF, CLOCK_ID, PINATA_ELF, AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID, CLOCK_ELF, CLOCK_ID, PINATA_ELF,
PINATA_ID, TOKEN_ELF, TOKEN_ID, PINATA_ID, TOKEN_ELF, TOKEN_ID, VAULT_ELF, VAULT_ID,
}, },
}; };
@ -148,6 +148,14 @@ impl Program {
elf: ASSOCIATED_TOKEN_ACCOUNT_ELF.to_vec(), elf: ASSOCIATED_TOKEN_ACCOUNT_ELF.to_vec(),
} }
} }
#[must_use]
pub fn vault() -> Self {
Self {
id: VAULT_ID,
elf: VAULT_ELF.to_vec(),
}
}
} }
// TODO: Testnet only. Refactor to prevent compilation on mainnet. // TODO: Testnet only. Refactor to prevent compilation on mainnet.
@ -179,7 +187,7 @@ mod tests {
program_methods::{ program_methods::{
AMM_ELF, AMM_ID, ASSOCIATED_TOKEN_ACCOUNT_ELF, ASSOCIATED_TOKEN_ACCOUNT_ID, AMM_ELF, AMM_ID, ASSOCIATED_TOKEN_ACCOUNT_ELF, ASSOCIATED_TOKEN_ACCOUNT_ID,
AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID, CLOCK_ELF, CLOCK_ID, PINATA_ELF, AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID, CLOCK_ELF, CLOCK_ID, PINATA_ELF,
PINATA_ID, PINATA_TOKEN_ELF, PINATA_TOKEN_ID, TOKEN_ELF, TOKEN_ID, PINATA_ID, PINATA_TOKEN_ELF, PINATA_TOKEN_ID, TOKEN_ELF, TOKEN_ID, VAULT_ELF, VAULT_ID,
}, },
}; };
@ -482,12 +490,15 @@ mod tests {
fn builtin_programs() { fn builtin_programs() {
let auth_transfer_program = Program::authenticated_transfer_program(); let auth_transfer_program = Program::authenticated_transfer_program();
let token_program = Program::token(); let token_program = Program::token();
let vault_program = Program::vault();
let pinata_program = Program::pinata(); let pinata_program = Program::pinata();
assert_eq!(auth_transfer_program.id, AUTHENTICATED_TRANSFER_ID); assert_eq!(auth_transfer_program.id, AUTHENTICATED_TRANSFER_ID);
assert_eq!(auth_transfer_program.elf, AUTHENTICATED_TRANSFER_ELF); assert_eq!(auth_transfer_program.elf, AUTHENTICATED_TRANSFER_ELF);
assert_eq!(token_program.id, TOKEN_ID); assert_eq!(token_program.id, TOKEN_ID);
assert_eq!(token_program.elf, TOKEN_ELF); assert_eq!(token_program.elf, TOKEN_ELF);
assert_eq!(vault_program.id, VAULT_ID);
assert_eq!(vault_program.elf, VAULT_ELF);
assert_eq!(pinata_program.id, PINATA_ID); assert_eq!(pinata_program.id, PINATA_ID);
assert_eq!(pinata_program.elf, PINATA_ELF); assert_eq!(pinata_program.elf, PINATA_ELF);
} }
@ -502,6 +513,7 @@ mod tests {
(PINATA_ELF, PINATA_ID), (PINATA_ELF, PINATA_ID),
(PINATA_TOKEN_ELF, PINATA_TOKEN_ID), (PINATA_TOKEN_ELF, PINATA_TOKEN_ID),
(TOKEN_ELF, TOKEN_ID), (TOKEN_ELF, TOKEN_ID),
(VAULT_ELF, VAULT_ID),
]; ];
for (elf, expected_id) in cases { for (elf, expected_id) in cases {
let program = Program::new(elf.to_vec()).unwrap(); let program = Program::new(elf.to_vec()).unwrap();

View File

@ -1,118 +0,0 @@
use std::collections::{HashMap, VecDeque};
use log::debug;
use nssa_core::{
account::{Account, AccountId, AccountWithMetadata},
program::{ChainedCall, ProgramId, ProgramOutput},
};
use crate::{PublicTransaction, V03State, error::NssaError};
pub trait Validator {
fn validate_pre_execution(&mut self) -> Result<(), NssaError>;
fn on_chained_call(&mut self) -> Result<(), NssaError>;
fn validate_output(
&mut self,
state_diff: &HashMap<AccountId, Account>,
caller_program_id: Option<ProgramId>,
chained_call: &ChainedCall,
program_output: &ProgramOutput,
) -> Result<(), NssaError>;
fn validate_post_execution(
&mut self,
state_diff: &HashMap<AccountId, Account>,
) -> Result<(), NssaError>;
}
pub fn execute(
mut validator: impl Validator,
tx: &PublicTransaction,
state: &V03State,
) -> Result<HashMap<AccountId, Account>, NssaError> {
validator.validate_pre_execution()?;
let message = tx.message();
let signer_account_ids = tx.signer_account_ids();
// Build pre_states for execution
let input_pre_states: Vec<_> = message
.account_ids
.iter()
.map(|account_id| {
AccountWithMetadata::new(
state.get_account_by_id(*account_id),
signer_account_ids.contains(account_id),
*account_id,
)
})
.collect();
let mut state_diff: HashMap<AccountId, Account> = HashMap::new();
let initial_call = ChainedCall {
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, None)]);
while let Some((chained_call, caller_program_id)) = chained_calls.pop_front() {
validator.on_chained_call()?;
// Check that the `program_id` corresponds to a deployed program
let Some(program) = state.programs().get(&chained_call.program_id) else {
return Err(NssaError::InvalidInput("Unknown program".into()));
};
debug!(
"Program {:?} pre_states: {:?}, instruction_data: {:?}",
chained_call.program_id, chained_call.pre_states, chained_call.instruction_data
);
let mut program_output = program.execute(
caller_program_id,
&chained_call.pre_states,
&chained_call.instruction_data,
)?;
debug!(
"Program {:?} output: {:?}",
chained_call.program_id, program_output
);
validator.validate_output(
&state_diff,
caller_program_id,
&chained_call,
&program_output,
)?;
for post in program_output
.post_states
.iter_mut()
.filter(|post| post.required_claim().is_some())
{
post.account_mut().program_owner = chained_call.program_id;
}
// Update the state diff
for (pre, post) in program_output
.pre_states
.iter()
.zip(program_output.post_states.iter())
{
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, Some(chained_call.program_id)));
}
}
validator.validate_post_execution(&state_diff)?;
Ok(state_diff)
}

View File

@ -1,9 +1,7 @@
pub use execution::{Validator, execute};
pub use message::Message; pub use message::Message;
pub use transaction::PublicTransaction; pub use transaction::PublicTransaction;
pub use witness_set::WitnessSet; pub use witness_set::WitnessSet;
mod execution;
mod message; mod message;
mod transaction; mod transaction;
mod witness_set; mod witness_set;

View File

@ -8,7 +8,7 @@ pub use clock_core::{
}; };
use nssa_core::{ use nssa_core::{
BlockId, Commitment, CommitmentSetDigest, DUMMY_COMMITMENT, MembershipProof, Nullifier, BlockId, Commitment, CommitmentSetDigest, DUMMY_COMMITMENT, MembershipProof, Nullifier,
Timestamp, SYSTEM_FAUCET_ACCOUNT_ID, Timestamp,
account::{Account, AccountId, Nonce}, account::{Account, AccountId, Nonce},
program::ProgramId, program::ProgramId,
}; };
@ -124,8 +124,12 @@ pub struct V03State {
impl Default for V03State { impl Default for V03State {
fn default() -> Self { fn default() -> Self {
let faucet_account = system_faucet_account();
let mut public_state = HashMap::new();
public_state.insert(SYSTEM_FAUCET_ACCOUNT_ID, faucet_account);
Self { Self {
public_state: HashMap::new(), public_state,
private_state: (CommitmentSet::with_capacity(32), NullifierSet::new()), private_state: (CommitmentSet::with_capacity(32), NullifierSet::new()),
programs: HashMap::new(), programs: HashMap::new(),
} }
@ -145,7 +149,7 @@ impl V03State {
genesis_timestamp: nssa_core::Timestamp, genesis_timestamp: nssa_core::Timestamp,
) -> Self { ) -> Self {
let authenticated_transfer_program = Program::authenticated_transfer_program(); let authenticated_transfer_program = Program::authenticated_transfer_program();
let public_state = initial_data let mut public_state: HashMap<_, _> = initial_data
.iter() .iter()
.copied() .copied()
.map(|(account_id, balance)| { .map(|(account_id, balance)| {
@ -157,6 +161,8 @@ impl V03State {
(account_id, account) (account_id, account)
}) })
.collect(); .collect();
let faucet_account = system_faucet_account();
public_state.insert(SYSTEM_FAUCET_ACCOUNT_ID, faucet_account);
let mut commitment_set = CommitmentSet::with_capacity(32); let mut commitment_set = CommitmentSet::with_capacity(32);
commitment_set.extend(&[DUMMY_COMMITMENT]); commitment_set.extend(&[DUMMY_COMMITMENT]);
@ -180,6 +186,7 @@ impl V03State {
this.insert_program(Program::token()); this.insert_program(Program::token());
this.insert_program(Program::amm()); this.insert_program(Program::amm());
this.insert_program(Program::ata()); this.insert_program(Program::ata());
this.insert_program(Program::vault());
this this
} }
@ -366,6 +373,14 @@ impl V03State {
} }
} }
fn system_faucet_account() -> Account {
Account {
program_owner: Program::authenticated_transfer_program().id(),
balance: u128::MAX,
..Account::default()
}
}
#[cfg(test)] #[cfg(test)]
pub mod tests { pub mod tests {
#![expect( #![expect(
@ -389,7 +404,7 @@ pub mod tests {
}; };
use crate::{ use crate::{
PublicKey, PublicTransaction, V03State, PublicKey, PublicTransaction, SYSTEM_FAUCET_ACCOUNT_ID, V03State,
error::{InvalidProgramBehaviorError, NssaError}, error::{InvalidProgramBehaviorError, NssaError},
execute_and_prove, execute_and_prove,
privacy_preserving_transaction::{ privacy_preserving_transaction::{
@ -403,7 +418,7 @@ pub mod tests {
signature::PrivateKey, signature::PrivateKey,
state::{ state::{
CLOCK_01_PROGRAM_ACCOUNT_ID, CLOCK_10_PROGRAM_ACCOUNT_ID, CLOCK_50_PROGRAM_ACCOUNT_ID, CLOCK_01_PROGRAM_ACCOUNT_ID, CLOCK_10_PROGRAM_ACCOUNT_ID, CLOCK_50_PROGRAM_ACCOUNT_ID,
CLOCK_PROGRAM_ACCOUNT_IDS, MAX_NUMBER_CHAINED_CALLS, CLOCK_PROGRAM_ACCOUNT_IDS, MAX_NUMBER_CHAINED_CALLS, system_faucet_account,
}, },
}; };
@ -597,6 +612,7 @@ pub mod tests {
..Account::default() ..Account::default()
}, },
); );
this.insert(SYSTEM_FAUCET_ACCOUNT_ID, system_faucet_account());
for account_id in CLOCK_PROGRAM_ACCOUNT_IDS { for account_id in CLOCK_PROGRAM_ACCOUNT_IDS {
this.insert( this.insert(
account_id, account_id,
@ -619,6 +635,7 @@ pub mod tests {
this.insert(Program::token().id(), Program::token()); this.insert(Program::token().id(), Program::token());
this.insert(Program::amm().id(), Program::amm()); this.insert(Program::amm().id(), Program::amm());
this.insert(Program::ata().id(), Program::ata()); this.insert(Program::ata().id(), Program::ata());
this.insert(Program::vault().id(), Program::vault());
this this
}; };

View File

@ -1,14 +1,14 @@
use std::{ use std::{
collections::{HashMap, HashSet}, collections::{HashMap, HashSet, VecDeque},
hash::Hash, hash::Hash,
}; };
use log::debug;
use nssa_core::{ use nssa_core::{
BlockId, Commitment, Nullifier, PrivacyPreservingCircuitOutput, Timestamp, BlockId, Commitment, Nullifier, PrivacyPreservingCircuitOutput, Timestamp,
account::{Account, AccountId, AccountWithMetadata}, account::{Account, AccountId, AccountWithMetadata},
program::{ program::{
ChainedCall, Claim, DEFAULT_PROGRAM_ID, ProgramId, ProgramOutput, ChainedCall, Claim, DEFAULT_PROGRAM_ID, compute_public_authorized_pdas, validate_execution,
compute_public_authorized_pdas, validate_execution,
}, },
}; };
@ -45,11 +45,237 @@ impl ValidatedStateDiff {
block_id: BlockId, block_id: BlockId,
timestamp: Timestamp, timestamp: Timestamp,
) -> Result<Self, NssaError> { ) -> Result<Self, NssaError> {
let validator = PublicTransactionValidator::new(tx, state, block_id, timestamp); let message = tx.message();
let state_diff = crate::public_transaction::execute(validator, tx, state)?; let witness_set = tx.witness_set();
// All account_ids must be different
ensure!(
message.account_ids.iter().collect::<HashSet<_>>().len() == message.account_ids.len(),
NssaError::InvalidInput("Duplicate account_ids found in message".into(),)
);
// Check exactly one nonce is provided for each signature
ensure!(
message.nonces.len() == witness_set.signatures_and_public_keys.len(),
NssaError::InvalidInput(
"Mismatch between number of nonces and signatures/public keys".into(),
)
);
// Check the signatures are valid
ensure!(
witness_set.is_valid_for(message),
NssaError::InvalidInput("Invalid signature for given message and public key".into())
);
let signer_account_ids = tx.signer_account_ids();
// Check nonces corresponds to the current nonces on the public state.
for (account_id, nonce) in signer_account_ids.iter().zip(&message.nonces) {
let current_nonce = state.get_account_by_id(*account_id).nonce;
ensure!(
current_nonce == *nonce,
NssaError::InvalidInput("Nonce mismatch".into())
);
}
// Build pre_states for execution
let input_pre_states: Vec<_> = message
.account_ids
.iter()
.map(|account_id| {
AccountWithMetadata::new(
state.get_account_by_id(*account_id),
signer_account_ids.contains(account_id),
*account_id,
)
})
.collect();
let mut state_diff: HashMap<AccountId, Account> = HashMap::new();
let initial_call = ChainedCall {
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, None)]);
let mut chain_calls_counter = 0;
while let Some((chained_call, caller_program_id)) = chained_calls.pop_front() {
ensure!(
chain_calls_counter <= MAX_NUMBER_CHAINED_CALLS,
NssaError::MaxChainedCallsDepthExceeded
);
// Check that the `program_id` corresponds to a deployed program
let Some(program) = state.programs().get(&chained_call.program_id) else {
return Err(NssaError::InvalidInput("Unknown program".into()));
};
debug!(
"Program {:?} pre_states: {:?}, instruction_data: {:?}",
chained_call.program_id, chained_call.pre_states, chained_call.instruction_data
);
let mut program_output = program.execute(
caller_program_id,
&chained_call.pre_states,
&chained_call.instruction_data,
)?;
debug!(
"Program {:?} output: {:?}",
chained_call.program_id, program_output
);
let authorized_pdas =
compute_public_authorized_pdas(caller_program_id, &chained_call.pda_seeds);
let is_authorized = |account_id: &AccountId| {
signer_account_ids.contains(account_id) || authorized_pdas.contains(account_id)
};
for pre in &program_output.pre_states {
let account_id = pre.account_id;
// Check that the program output pre_states coincide with the values in the public
// state or with any modifications to those values during the chain of calls.
let expected_pre = state_diff
.get(&account_id)
.cloned()
.unwrap_or_else(|| state.get_account_by_id(account_id));
ensure!(
pre.account == expected_pre,
InvalidProgramBehaviorError::InconsistentAccountPreState {
account_id,
expected: Box::new(expected_pre),
actual: Box::new(pre.account.clone())
}
);
// Check that authorization flags are consistent with the provided ones or
// authorized by program through the PDA mechanism
let expected_is_authorized = is_authorized(&account_id);
ensure!(
pre.is_authorized == expected_is_authorized,
InvalidProgramBehaviorError::InconsistentAccountAuthorization {
account_id,
expected_authorization: expected_is_authorized,
actual_authorization: pre.is_authorized
}
);
}
// Verify that the program output's self_program_id matches the expected program ID.
ensure!(
program_output.self_program_id == chained_call.program_id,
InvalidProgramBehaviorError::MismatchedProgramId {
expected: chained_call.program_id,
actual: program_output.self_program_id
}
);
// Verify that the program output's caller_program_id matches the actual caller.
ensure!(
program_output.caller_program_id == caller_program_id,
InvalidProgramBehaviorError::MismatchedCallerProgramId {
expected: caller_program_id,
actual: program_output.caller_program_id,
}
);
// Verify execution corresponds to a well-behaved program.
// See the # Programs section for the definition of the `validate_execution` method.
validate_execution(
&program_output.pre_states,
&program_output.post_states,
chained_call.program_id,
)
.map_err(InvalidProgramBehaviorError::ExecutionValidationFailed)?;
// Verify validity window
ensure!(
program_output.block_validity_window.is_valid_for(block_id)
&& program_output
.timestamp_validity_window
.is_valid_for(timestamp),
NssaError::OutOfValidityWindow
);
for (i, post) in program_output.post_states.iter_mut().enumerate() {
let Some(claim) = post.required_claim() else {
continue;
};
let account_id = program_output.pre_states[i].account_id;
// The invoked program can only claim accounts with default program id.
ensure!(
post.account().program_owner == DEFAULT_PROGRAM_ID,
InvalidProgramBehaviorError::ClaimedNonDefaultAccount { account_id }
);
match claim {
Claim::Authorized => {
// The program can only claim accounts that were authorized by the signer.
ensure!(
is_authorized(&account_id),
InvalidProgramBehaviorError::ClaimedUnauthorizedAccount { account_id }
);
}
Claim::Pda(seed) => {
// The program can only claim accounts that correspond to the PDAs it is
// authorized to claim. The public-execution path only sees public
// accounts, so the public-PDA derivation is the correct formula here.
let pda = AccountId::for_public_pda(&chained_call.program_id, &seed);
ensure!(
account_id == pda,
InvalidProgramBehaviorError::MismatchedPdaClaim {
expected: pda,
actual: account_id
}
);
}
}
post.account_mut().program_owner = chained_call.program_id;
}
// Update the state diff
for (pre, post) in program_output
.pre_states
.iter()
.zip(program_output.post_states.iter())
{
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, Some(chained_call.program_id)));
}
chain_calls_counter = chain_calls_counter
.checked_add(1)
.expect("we check the max depth at the beginning of the loop");
}
// Check that all modified uninitialized accounts where claimed
for (account_id, post) in state_diff.iter().filter_map(|(account_id, post)| {
let pre = state.get_account_by_id(*account_id);
if pre.program_owner != DEFAULT_PROGRAM_ID {
return None;
}
if pre == *post {
return None;
}
Some((*account_id, post))
}) {
ensure!(
post.program_owner != DEFAULT_PROGRAM_ID,
InvalidProgramBehaviorError::DefaultAccountModifiedWithoutClaim { account_id }
);
}
Ok(Self(StateDiff { Ok(Self(StateDiff {
signer_account_ids: tx.signer_account_ids(), signer_account_ids,
public_diff: state_diff, public_diff: state_diff,
new_commitments: vec![], new_commitments: vec![],
new_nullifiers: vec![], new_nullifiers: vec![],
@ -190,22 +416,6 @@ impl ValidatedStateDiff {
})) }))
} }
pub fn from_public_genesis_transaction(
tx: &PublicTransaction,
state: &V03State,
) -> Result<Self, NssaError> {
let validator = GenesisPublicTransactionValidator;
let state_diff = crate::public_transaction::execute(validator, tx, state)?;
Ok(Self(StateDiff {
signer_account_ids: tx.signer_account_ids(),
public_diff: state_diff,
new_commitments: vec![],
new_nullifiers: vec![],
program: None,
}))
}
/// Returns the public account changes produced by this transaction. /// Returns the public account changes produced by this transaction.
/// ///
/// Used by callers (e.g. the sequencer) to inspect the diff before committing it, for example /// Used by callers (e.g. the sequencer) to inspect the diff before committing it, for example
@ -220,256 +430,6 @@ impl ValidatedStateDiff {
} }
} }
pub struct PublicTransactionValidator<'tx, 'state> {
tx: &'tx PublicTransaction,
state: &'state V03State,
block_id: BlockId,
timestamp: Timestamp,
chain_calls_counter: usize,
}
impl<'tx, 'state> PublicTransactionValidator<'tx, 'state> {
pub const fn new(
tx: &'tx PublicTransaction,
state: &'state V03State,
block_id: BlockId,
timestamp: Timestamp,
) -> Self {
Self {
tx,
state,
block_id,
timestamp,
chain_calls_counter: 0,
}
}
}
impl crate::public_transaction::Validator for PublicTransactionValidator<'_, '_> {
fn validate_pre_execution(&mut self) -> Result<(), NssaError> {
let message = self.tx.message();
let witness_set = self.tx.witness_set();
// All account_ids must be different
ensure!(
message.account_ids.iter().collect::<HashSet<_>>().len() == message.account_ids.len(),
NssaError::InvalidInput("Duplicate account_ids found in message".into(),)
);
// Check exactly one nonce is provided for each signature
ensure!(
message.nonces.len() == witness_set.signatures_and_public_keys.len(),
NssaError::InvalidInput(
"Mismatch between number of nonces and signatures/public keys".into(),
)
);
// Check the signatures are valid
ensure!(
witness_set.is_valid_for(message),
NssaError::InvalidInput("Invalid signature for given message and public key".into())
);
let signer_account_ids = self.tx.signer_account_ids();
// Check nonces corresponds to the current nonces on the public state.
for (account_id, nonce) in signer_account_ids.iter().zip(&message.nonces) {
let current_nonce = self.state.get_account_by_id(*account_id).nonce;
ensure!(
current_nonce == *nonce,
NssaError::InvalidInput("Nonce mismatch".into())
);
}
Ok(())
}
fn on_chained_call(&mut self) -> Result<(), NssaError> {
self.chain_calls_counter = self
.chain_calls_counter
.checked_add(1)
.ok_or(NssaError::MaxChainedCallsDepthExceeded)?;
ensure!(
self.chain_calls_counter <= MAX_NUMBER_CHAINED_CALLS,
NssaError::MaxChainedCallsDepthExceeded
);
Ok(())
}
fn validate_output(
&mut self,
state_diff: &HashMap<AccountId, Account>,
caller_program_id: Option<ProgramId>,
chained_call: &ChainedCall,
program_output: &ProgramOutput,
) -> Result<(), NssaError> {
let authorized_pdas =
compute_public_authorized_pdas(caller_program_id, &chained_call.pda_seeds);
let is_authorized = |account_id: &AccountId| {
self.tx.signer_account_ids().contains(account_id)
|| authorized_pdas.contains(account_id)
};
for pre in &program_output.pre_states {
let account_id = pre.account_id;
// Check that the program output pre_states coincide with the values in the public
// state or with any modifications to those values during the chain of calls.
let expected_pre = state_diff
.get(&account_id)
.cloned()
.unwrap_or_else(|| self.state.get_account_by_id(account_id));
ensure!(
pre.account == expected_pre,
InvalidProgramBehaviorError::InconsistentAccountPreState {
account_id,
expected: Box::new(expected_pre),
actual: Box::new(pre.account.clone())
}
);
// Check that authorization flags are consistent with the provided ones or
// authorized by program through the PDA mechanism
let expected_is_authorized = is_authorized(&account_id);
ensure!(
pre.is_authorized == expected_is_authorized,
InvalidProgramBehaviorError::InconsistentAccountAuthorization {
account_id,
expected_authorization: expected_is_authorized,
actual_authorization: pre.is_authorized
}
);
}
// Verify that the program output's self_program_id matches the expected program ID.
ensure!(
program_output.self_program_id == chained_call.program_id,
InvalidProgramBehaviorError::MismatchedProgramId {
expected: chained_call.program_id,
actual: program_output.self_program_id
}
);
// Verify that the program output's caller_program_id matches the actual caller.
ensure!(
program_output.caller_program_id == caller_program_id,
InvalidProgramBehaviorError::MismatchedCallerProgramId {
expected: caller_program_id,
actual: program_output.caller_program_id,
}
);
// Verify execution corresponds to a well-behaved program.
// See the # Programs section for the definition of the `validate_execution` method.
validate_execution(
&program_output.pre_states,
&program_output.post_states,
chained_call.program_id,
)
.map_err(InvalidProgramBehaviorError::ExecutionValidationFailed)?;
// Verify validity window
ensure!(
program_output
.block_validity_window
.is_valid_for(self.block_id)
&& program_output
.timestamp_validity_window
.is_valid_for(self.timestamp),
NssaError::OutOfValidityWindow
);
for (i, post) in program_output.post_states.iter().enumerate() {
let Some(claim) = post.required_claim() else {
continue;
};
let account_id = program_output.pre_states[i].account_id;
// The invoked program can only claim accounts with default program id.
ensure!(
post.account().program_owner == DEFAULT_PROGRAM_ID,
InvalidProgramBehaviorError::ClaimedNonDefaultAccount { account_id }
);
match claim {
Claim::Authorized => {
// The program can only claim accounts that were authorized by the signer.
ensure!(
is_authorized(&account_id),
InvalidProgramBehaviorError::ClaimedUnauthorizedAccount { account_id }
);
}
Claim::Pda(seed) => {
// The program can only claim accounts that correspond to the PDAs it is
// authorized to claim.
let pda = AccountId::for_public_pda(&chained_call.program_id, &seed);
ensure!(
account_id == pda,
InvalidProgramBehaviorError::MismatchedPdaClaim {
expected: pda,
actual: account_id
}
);
}
}
}
Ok(())
}
fn validate_post_execution(
&mut self,
state_diff: &HashMap<AccountId, Account>,
) -> Result<(), NssaError> {
// Check that all modified uninitialized accounts where claimed
for (account_id, post) in state_diff.iter().filter_map(|(account_id, post)| {
let pre = self.state.get_account_by_id(*account_id);
if pre.program_owner != DEFAULT_PROGRAM_ID {
return None;
}
if pre == *post {
return None;
}
Some((*account_id, post))
}) {
ensure!(
post.program_owner != DEFAULT_PROGRAM_ID,
InvalidProgramBehaviorError::DefaultAccountModifiedWithoutClaim { account_id }
);
}
Ok(())
}
}
pub struct GenesisPublicTransactionValidator;
impl crate::public_transaction::Validator for GenesisPublicTransactionValidator {
fn validate_pre_execution(&mut self) -> Result<(), NssaError> {
Ok(())
}
fn on_chained_call(&mut self) -> Result<(), NssaError> {
Ok(())
}
fn validate_output(
&mut self,
_state_diff: &HashMap<AccountId, Account>,
_caller_program_id: Option<ProgramId>,
_chained_call: &ChainedCall,
_program_output: &ProgramOutput,
) -> Result<(), NssaError> {
Ok(())
}
fn validate_post_execution(
&mut self,
_state_diff: &HashMap<AccountId, Account>,
) -> Result<(), NssaError> {
Ok(())
}
}
fn check_privacy_preserving_circuit_proof_is_valid( fn check_privacy_preserving_circuit_proof_is_valid(
proof: &Proof, proof: &Proof,
public_pre_states: &[AccountWithMetadata], public_pre_states: &[AccountWithMetadata],

View File

@ -17,5 +17,6 @@ amm_core.workspace = true
amm_program.workspace = true amm_program.workspace = true
ata_core.workspace = true ata_core.workspace = true
ata_program.workspace = true ata_program.workspace = true
vault_core.workspace = true
risc0-zkvm.workspace = true risc0-zkvm.workspace = true
serde = { workspace = true, default-features = false } serde = { workspace = true, default-features = false }

View File

@ -1,6 +1,6 @@
use authenticated_transfer_core::Instruction; use authenticated_transfer_core::Instruction;
use clock_core::{CLOCK_01_PROGRAM_ACCOUNT_ID, ClockAccountData};
use nssa_core::{ use nssa_core::{
SYSTEM_FAUCET_ACCOUNT_ID,
account::{Account, AccountWithMetadata}, account::{Account, AccountWithMetadata},
program::{ program::{
AccountPostState, Claim, DEFAULT_PROGRAM_ID, ProgramInput, ProgramOutput, read_nssa_inputs, AccountPostState, Claim, DEFAULT_PROGRAM_ID, ProgramInput, ProgramOutput, read_nssa_inputs,
@ -27,7 +27,12 @@ fn transfer(
balance_to_move: u128, balance_to_move: u128,
) -> Vec<AccountPostState> { ) -> Vec<AccountPostState> {
// Continue only if the sender has authorized this operation // Continue only if the sender has authorized this operation
assert!(sender.is_authorized, "Sender must be authorized"); // or it's the system faucet account which is allowed without authorization as it may be used
// only by sequencer.
assert!(
sender.is_authorized || sender.account_id == SYSTEM_FAUCET_ACCOUNT_ID,
"Sender must be authorized"
);
// Create accounts post states, with updated balances // Create accounts post states, with updated balances
let sender_post = { let sender_post = {
@ -59,39 +64,6 @@ fn transfer(
vec![sender_post, recipient_post] vec![sender_post, recipient_post]
} }
/// Mints `balance` into a new account at genesis (`block_id` == 0).
///
/// Claims the target account and sets its balance in a single operation.
fn mint(
target: AccountWithMetadata,
clock: AccountWithMetadata,
balance: u128,
) -> Vec<AccountPostState> {
assert_eq!(
clock.account_id, CLOCK_01_PROGRAM_ACCOUNT_ID,
"Second account must be the clock account"
);
let clock_data = ClockAccountData::from_bytes(&clock.account.data.clone().into_inner());
assert_eq!(
clock_data.block_id, 0,
"Mint can only execute at genesis (block_id must be 0)"
);
assert!(
target.account == Account::default(),
"Target account must be uninitialized"
);
let mut target_post_account = target.account;
target_post_account.balance = balance;
let target_post = AccountPostState::new_claimed(target_post_account, Claim::Authorized);
let clock_post = AccountPostState::new(clock.account);
vec![target_post, clock_post]
}
/// A transfer of balance program. /// A transfer of balance program.
/// To be used both in public and private contexts. /// To be used both in public and private contexts.
fn main() { fn main() {
@ -119,11 +91,6 @@ fn main() {
.expect("Transfer requires exactly 2 accounts"); .expect("Transfer requires exactly 2 accounts");
transfer(sender, recipient, balance_to_move) transfer(sender, recipient, balance_to_move)
} }
Instruction::Mint { amount: balance } => {
let [target, clock] = <[_; 2]>::try_from(pre_states.clone())
.expect("Mint requires exactly 2 accounts: target, clock");
mint(target, clock, balance)
}
}; };
ProgramOutput::new( ProgramOutput::new(

View File

@ -0,0 +1,103 @@
use authenticated_transfer_core::Instruction as AuthTransferInstruction;
use nssa_core::program::{
AccountPostState, ChainedCall, ProgramInput, ProgramOutput, read_nssa_inputs,
};
use vault_core::Instruction;
fn unchanged_post_states(
pre_states: &[nssa_core::account::AccountWithMetadata],
) -> Vec<AccountPostState> {
pre_states
.iter()
.map(|pre_state| AccountPostState::new(pre_state.account.clone()))
.collect()
}
fn main() {
let (
ProgramInput {
self_program_id,
caller_program_id,
pre_states,
instruction,
},
instruction_words,
) = read_nssa_inputs::<Instruction>();
let pre_states_clone = pre_states.clone();
let post_states = unchanged_post_states(&pre_states_clone);
let chained_calls = match instruction {
Instruction::Initialize => {
let [owner, owner_vault] = pre_states
.try_into()
.expect("Initialize requires exactly 2 accounts");
let seed = vault_core::compute_vault_seed(owner.account_id);
assert!(
owner.account.program_owner != nssa_core::program::DEFAULT_PROGRAM_ID,
"Owner account must be initialized"
);
let mut owner_vault_for_callee = owner_vault;
owner_vault_for_callee.is_authorized = true;
vec![
ChainedCall::new(
owner.account.program_owner,
vec![owner_vault_for_callee],
&AuthTransferInstruction::Initialize,
)
.with_pda_seeds(vec![seed]),
]
}
Instruction::Transfer { amount } => {
let [sender, recipient, recipient_vault] = pre_states
.try_into()
.expect("Transfer requires exactly 3 accounts");
let seed = vault_core::compute_vault_seed(recipient.account_id);
let mut recipient_vault_for_callee = recipient_vault;
recipient_vault_for_callee.is_authorized = true;
vec![
ChainedCall::new(
sender.account.program_owner,
vec![sender, recipient_vault_for_callee],
&AuthTransferInstruction::Transfer { amount },
)
.with_pda_seeds(vec![seed]),
]
}
Instruction::Claim { amount } => {
let [owner, owner_vault] = pre_states
.try_into()
.expect("Claim requires exactly 2 accounts");
let seed = vault_core::compute_vault_seed(owner.account_id);
let mut owner_vault_for_callee = owner_vault;
owner_vault_for_callee.is_authorized = true;
vec![
ChainedCall::new(
owner_vault_for_callee.account.program_owner,
vec![owner_vault_for_callee, owner],
&AuthTransferInstruction::Transfer { amount },
)
.with_pda_seeds(vec![seed]),
]
}
};
ProgramOutput::new(
self_program_id,
caller_program_id,
instruction_words,
pre_states_clone,
post_states,
)
.with_chained_calls(chained_calls)
.write();
}

View File

@ -14,16 +14,4 @@ pub enum Instruction {
/// ///
/// Required accounts: `[account_to_initialize]`. /// Required accounts: `[account_to_initialize]`.
Initialize, Initialize,
/// Mint `amount` into a new account at genesis (`block_id` == 0).
///
/// Claims the target account (sets `program_owner` to `authenticated_transfer` program id)
/// and sets its balance in a single operation.
///
/// Required accounts: `[target_account, clock_account]`.
///
/// Panics if:
/// - `target_account` is not in the default (uninitialized) state
/// - clock's `block_id` is not 0
Mint { amount: u128 },
} }

View File

@ -0,0 +1,13 @@
[package]
name = "vault_core"
version = "0.1.0"
edition = "2024"
license = { workspace = true }
[lints]
workspace = true
[dependencies]
nssa_core.workspace = true
serde = { workspace = true, default-features = false }
risc0-zkvm.workspace = true

View File

@ -0,0 +1,57 @@
pub use nssa_core::program::PdaSeed;
use nssa_core::{account::AccountId, program::ProgramId};
use serde::{Deserialize, Serialize};
const VAULT_SEED_DOMAIN_SEPARATOR: &[u8] = b"/LEZ/v0.3/VaultSeed/00000000000/";
const _: () = assert!(
VAULT_SEED_DOMAIN_SEPARATOR.len() == 32,
"Domain separator must be exactly 32 bytes long"
);
#[derive(Serialize, Deserialize)]
pub enum Instruction {
/// Initializes a vault account for owner.
///
/// Required accounts (2):
/// - Owner account
/// - Owner vault PDA account
Initialize,
/// Transfers native tokens from sender to recipient's vault.
///
/// Required accounts (3):
/// - Sender account
/// - Recipient account
/// - Recipient vault PDA account
Transfer { amount: u128 },
/// Claims native tokens from owner's vault into owner's account.
///
/// Required accounts (2):
/// - Owner account
/// - Owner vault PDA account
Claim { amount: u128 },
}
#[must_use]
pub fn compute_vault_seed(owner_id: AccountId) -> PdaSeed {
use risc0_zkvm::sha::{Impl, Sha256 as _};
let mut bytes = [0_u8; 64];
bytes[..32].copy_from_slice(VAULT_SEED_DOMAIN_SEPARATOR);
bytes[32..64].copy_from_slice(&owner_id.to_bytes());
PdaSeed::new(
Impl::hash_bytes(&bytes)
.as_bytes()
.try_into()
.expect("Hash output must be exactly 32 bytes long"),
)
}
#[must_use]
pub fn compute_vault_account_id(vault_program_id: ProgramId, owner_id: AccountId) -> AccountId {
let seed = compute_vault_seed(owner_id);
AccountId::for_public_pda(&vault_program_id, &seed)
}

View File

@ -15,7 +15,7 @@ storage.workspace = true
mempool.workspace = true mempool.workspace = true
logos-blockchain-zone-sdk.workspace = true logos-blockchain-zone-sdk.workspace = true
testnet_initial_state.workspace = true testnet_initial_state.workspace = true
authenticated_transfer_core.workspace = true vault_core.workspace = true
anyhow.workspace = true anyhow.workspace = true
serde.workspace = true serde.workspace = true

View File

@ -18,7 +18,7 @@ use url::Url;
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum GenesisTransaction { pub enum GenesisTransaction {
SupplyPublicAccount { SupplyAccount {
account_id: AccountId, account_id: AccountId,
balance: u128, balance: u128,
}, },

View File

@ -12,9 +12,7 @@ use logos_blockchain_key_management_system_service::keys::{ED25519_SECRET_KEY_SI
use mempool::{MemPool, MemPoolHandle}; use mempool::{MemPool, MemPoolHandle};
#[cfg(feature = "mock")] #[cfg(feature = "mock")]
pub use mock::SequencerCoreWithMockClients; pub use mock::SequencerCoreWithMockClients;
use nssa::{ use nssa::{AccountId, PublicTransaction, program::Program, public_transaction::Message};
AccountId, PublicTransaction, ValidatedStateDiff, program::Program, public_transaction::Message,
};
use nssa_core::GENESIS_BLOCK_ID; use nssa_core::GENESIS_BLOCK_ID;
pub use storage::error::DbError; pub use storage::error::DbError;
@ -363,49 +361,48 @@ fn build_genesis_state(config: &SequencerConfig) -> (nssa::V03State, Vec<NSSATra
#[cfg(feature = "testnet")] #[cfg(feature = "testnet")]
let mut state = testnet_initial_state::initial_state_testnet(); let mut state = testnet_initial_state::initial_state_testnet();
let mut genesis_txs = Vec::new(); let genesis_txs = config
.genesis
for genesis_tx in &config.genesis { .iter()
let (tx, diff) = match genesis_tx { .map(|genesis_tx| match genesis_tx {
GenesisTransaction::SupplyPublicAccount { GenesisTransaction::SupplyAccount {
account_id, account_id,
balance, balance,
} => build_supply_public_account_genesis_transaction(&state, account_id, *balance), } => build_supply_account_genesis_transaction(account_id, *balance),
}; })
state.apply_state_diff(diff); .chain(std::iter::once(clock_invocation(0)))
genesis_txs.push(tx); .inspect(|tx| {
} state
.transition_from_public_transaction(tx, GENESIS_BLOCK_ID, 0)
let clock_tx = clock_invocation(0); .expect("Failed to execute genesis transaction");
let diff = ValidatedStateDiff::from_public_transaction(&clock_tx, &state, GENESIS_BLOCK_ID, 0) })
.expect("Failed to execute clock transaction for genesis block"); .map(NSSATransaction::Public)
state.apply_state_diff(diff); .collect();
genesis_txs.push(clock_tx.into());
(state, genesis_txs) (state, genesis_txs)
} }
fn build_supply_public_account_genesis_transaction( fn build_supply_account_genesis_transaction(
state: &nssa::V03State,
account_id: &AccountId, account_id: &AccountId,
balance: u128, balance: u128,
) -> (NSSATransaction, ValidatedStateDiff) { ) -> PublicTransaction {
let authenticated_transfer_id = Program::authenticated_transfer_program().id(); let vault_program_id = Program::vault().id();
let recipient_vault_id = vault_core::compute_vault_account_id(vault_program_id, *account_id);
let message = Message::try_new( let message = Message::try_new(
authenticated_transfer_id, vault_program_id,
vec![*account_id, nssa::CLOCK_01_PROGRAM_ACCOUNT_ID], vec![
nssa::SYSTEM_FAUCET_ACCOUNT_ID,
*account_id,
recipient_vault_id,
],
vec![], vec![],
authenticated_transfer_core::Instruction::Mint { amount: balance }, vault_core::Instruction::Transfer { amount: balance },
) )
.expect("Failed to serialize genesis mint instruction"); .expect("Failed to serialize genesis transfer instruction");
let witness_set = nssa::public_transaction::WitnessSet::for_message(&message, &[]); let witness_set = nssa::public_transaction::WitnessSet::from_raw_parts(vec![]);
let tx = PublicTransaction::new(message, witness_set); PublicTransaction::new(message, witness_set)
let diff = ValidatedStateDiff::from_public_genesis_transaction(&tx, state)
.expect("Failed to execute genesis mint public transaction");
(tx.into(), diff)
} }
/// Load signing key from file or generate a new one if it doesn't exist. /// Load signing key from file or generate a new one if it doesn't exist.

View File

@ -4,7 +4,7 @@ use common::{
block::Block, block::Block,
transaction::{NSSATransaction, clock_invocation}, transaction::{NSSATransaction, clock_invocation},
}; };
use nssa::{GENESIS_BLOCK_ID, V03State, ValidatedStateDiff}; use nssa::{GENESIS_BLOCK_ID, V03State};
use rocksdb::{ use rocksdb::{
BoundColumnFamily, ColumnFamilyDescriptor, DBWithThreadMode, MultiThreaded, Options, BoundColumnFamily, ColumnFamilyDescriptor, DBWithThreadMode, MultiThreaded, Options,
}; };
@ -189,16 +189,17 @@ impl RocksDBIO {
)); ));
} }
}; };
let state_diff = ValidatedStateDiff::from_public_genesis_transaction( breakpoint
&genesis_tx, .transition_from_public_transaction(
&breakpoint, &genesis_tx,
) block.header.block_id,
.map_err(|err| { block.header.timestamp,
DbError::db_interaction_error(format!( )
"Failed to create state diff from genesis transaction with err {err:?}" .map_err(|err| {
)) DbError::db_interaction_error(format!(
})?; "genesis transaction execution failed with err {err:?}"
breakpoint.apply_state_diff(state_diff); ))
})?;
} else { } else {
transaction transaction
.transaction_stateless_check() .transaction_stateless_check()

View File

@ -56,9 +56,12 @@ fn main() {
// private PDA (seed, npk) binding when pda_seeds match the private PDA derivation. // private PDA (seed, npk) binding when pda_seeds match the private PDA derivation.
let mut auth_pda_pre = pda_pre; let mut auth_pda_pre = pda_pre;
auth_pda_pre.is_authorized = true; auth_pda_pre.is_authorized = true;
let auth_call = let auth_call = ChainedCall::new(
ChainedCall::new(auth_transfer_id, vec![auth_pda_pre, recipient_pre], &amount) auth_transfer_id,
.with_pda_seeds(vec![pda_seed]); vec![auth_pda_pre, recipient_pre],
&authenticated_transfer_core::Instruction::Transfer { amount },
)
.with_pda_seeds(vec![pda_seed]);
ProgramOutput::new( ProgramOutput::new(
self_program_id, self_program_id,
@ -81,8 +84,12 @@ fn main() {
// to authorize the PDA. authenticated_transfer will claim it with Claim::Authorized. // to authorize the PDA. authenticated_transfer will claim it with Claim::Authorized.
let mut auth_pda_pre = pda_pre; let mut auth_pda_pre = pda_pre;
auth_pda_pre.is_authorized = true; auth_pda_pre.is_authorized = true;
let auth_call = ChainedCall::new(auth_transfer_id, vec![auth_pda_pre], &0_u128) let auth_call = ChainedCall::new(
.with_pda_seeds(vec![pda_seed]); auth_transfer_id,
vec![auth_pda_pre],
&authenticated_transfer_core::Instruction::Initialize,
)
.with_pda_seeds(vec![pda_seed]);
ProgramOutput::new( ProgramOutput::new(
self_program_id, self_program_id,