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

View File

@ -21,6 +21,7 @@ members = [
"programs/associated_token_account/core",
"programs/associated_token_account",
"programs/authenticated_transfer/core",
"programs/vault/core",
"sequencer/core",
"sequencer/service",
"sequencer/service/protocol",
@ -67,6 +68,7 @@ amm_program = { path = "programs/amm" }
ata_core = { path = "programs/associated_token_account/core" }
ata_program = { path = "programs/associated_token_account" }
authenticated_transfer_core = { path = "programs/authenticated_transfer/core" }
vault_core = { path = "programs/vault/core" }
test_program_methods = { path = "test_program_methods" }
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
/// 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(
&self,
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)
}

View File

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

View File

@ -8,7 +8,7 @@ use common::{
use log::info;
use logos_blockchain_core::{header::HeaderId, mantle::ops::channel::MsgId};
use logos_blockchain_zone_sdk::Slot;
use nssa::{Account, AccountId, V03State, ValidatedStateDiff};
use nssa::{Account, AccountId, V03State};
use nssa_core::BlockId;
use storage::indexer::RocksDBIO;
use tokio::sync::RwLock;
@ -160,12 +160,13 @@ impl IndexerStore {
anyhow::bail!("Genesis block should contain only public transactions")
}
};
let state_diff = ValidatedStateDiff::from_public_genesis_transaction(
genesis_tx,
&state_guard,
)
.context("Failed to create state diff from genesis transaction")?;
state_guard.apply_state_diff(state_diff);
state_guard
.transition_from_public_transaction(
genesis_tx,
block.header.block_id,
block.header.timestamp,
)
.context("Failed to execute genesis public transaction")?;
} else {
transaction
.clone()
@ -202,39 +203,11 @@ impl IndexerStore {
#[cfg(test)]
mod tests {
use common::{HashType, block::HashableBlockData};
use nssa::{AccountId, CLOCK_01_PROGRAM_ACCOUNT_ID, PublicKey, PublicTransaction};
use tempfile::tempdir;
use testnet_initial_state::initial_pub_accounts_private_keys;
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]
fn correct_startup() {
let home = tempdir().unwrap();
@ -252,19 +225,18 @@ mod tests {
let storage = IndexerStore::open_db(home.as_ref()).unwrap();
let from = acc1();
let to = acc2();
let sign_key = acc1_sign_key();
let initial_accounts = initial_pub_accounts_private_keys();
let from = initial_accounts[0].account_id;
let to = initial_accounts[1].account_id;
let sign_key = initial_accounts[0].pub_sign_key.clone();
// Submit genesis block
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 {
block_id: 1,
prev_block_hash: HashType::default(),
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(
&common::test_utils::sequencer_sign_key_for_testing(),
@ -276,30 +248,29 @@ mod tests {
.await
.unwrap();
for i in 2..10 {
for i in 0..10 {
let tx = common::test_utils::create_transaction_native_token_transfer(
from,
i - 2,
to,
10,
&sign_key,
from, i, 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]);
prev_hash = Some(next_block.header.hash);
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
.unwrap();
}
let acc1_val = storage.account_current_state(&acc1()).await.unwrap();
let acc2_val = storage.account_current_state(&acc2()).await.unwrap();
let acc1_val = storage.account_current_state(&from).await.unwrap();
let acc2_val = storage.account_current_state(&to).await.unwrap();
assert_eq!(acc1_val.balance, 9920);
assert_eq!(acc2_val.balance, 20080);
assert_eq!(acc1_val.balance, 9900);
assert_eq!(acc2_val.balance, 20100);
}
#[tokio::test]
@ -310,45 +281,45 @@ mod tests {
let mut prev_hash = None;
let from = acc1();
let to = acc2();
let sign_key = acc1_sign_key();
let initial_accounts = initial_pub_accounts_private_keys();
let from = initial_accounts[0].account_id;
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(
from,
i - 2,
to,
10,
&sign_key,
from, i, 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]);
prev_hash = Some(next_block.header.hash);
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
.unwrap();
}
// Genesis block: no transfers applied yet.
let acc1_at_1 = storage.account_state_at_block(&acc1(), 1).unwrap();
let acc2_at_1 = storage.account_state_at_block(&acc2(), 1).unwrap();
assert_eq!(acc1_at_1.balance, 10000);
assert_eq!(acc2_at_1.balance, 20000);
let acc1_at_1 = storage.account_state_at_block(&from, 1).unwrap();
let acc2_at_1 = storage.account_state_at_block(&to, 1).unwrap();
assert_eq!(acc1_at_1.balance, 9990);
assert_eq!(acc2_at_1.balance, 20010);
// 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 acc2_at_5 = storage.account_state_at_block(&acc2(), 5).unwrap();
assert_eq!(acc1_at_5.balance, 9960);
assert_eq!(acc2_at_5.balance, 20040);
let acc1_at_5 = storage.account_state_at_block(&from, 5).unwrap();
let acc2_at_5 = storage.account_state_at_block(&to, 5).unwrap();
assert_eq!(acc1_at_5.balance, 9950);
assert_eq!(acc2_at_5.balance, 20050);
// After final block 9: 8 transfers applied; should match current state.
let acc1_at_9 = storage.account_state_at_block(&acc1(), 9).unwrap();
let acc2_at_9 = storage.account_state_at_block(&acc2(), 9).unwrap();
assert_eq!(acc1_at_9.balance, 9920);
assert_eq!(acc2_at_9.balance, 20080);
let acc1_at_9 = storage.account_state_at_block(&from, 9).unwrap();
let acc2_at_9 = storage.account_state_at_block(&to, 9).unwrap();
assert_eq!(acc1_at_9.balance, 9910);
assert_eq!(acc2_at_9.balance, 20090);
}
}

View File

@ -20,6 +20,7 @@ indexer_service.workspace = true
serde_json.workspace = true
token_core.workspace = true
ata_core.workspace = true
vault_core.workspace = true
indexer_service_rpc = { workspace = true, features = ["client"] }
sequencer_service_rpc = { workspace = true, features = ["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 bytesize::ByteSize;
use indexer_service::{ChannelId, ClientConfig, IndexerConfig};
use key_protocol::key_management::KeyChain;
use nssa::{AccountId, PrivateKey, PublicKey};
use nssa_core::Identifier;
use sequencer_core::config::{BedrockConfig, GenesisTransaction, SequencerConfig};
use url::Url;
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];
#[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.
#[derive(Debug, Clone, Copy)]
pub struct SequencerPartialConfig {
@ -80,43 +96,58 @@ pub fn sequencer_config(
#[must_use]
pub fn default_public_accounts_for_wallet() -> Vec<(PrivateKey, u128)> {
let mut first_private_key = PrivateKey::new_os_random();
let first_public_key = PublicKey::new_from_private_key(&first_private_key);
let mut first_account_id = AccountId::from(&first_public_key);
let mut private_keys = vec![PrivateKey::new_os_random(), PrivateKey::new_os_random()];
private_keys.sort_unstable_by_key(|private_key| {
AccountId::from(&PublicKey::new_from_private_key(private_key))
});
let mut second_private_key = PrivateKey::new_os_random();
let second_public_key = PublicKey::new_from_private_key(&second_private_key);
let mut second_account_id = AccountId::from(&second_public_key);
// 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]),
]
private_keys
.into_iter()
.zip(INITIAL_PUBLIC_BALANCES_FOR_WALLET)
.collect()
}
#[must_use]
pub fn genesis_from_public_accounts(
public_accounts: &[(PrivateKey, u128)],
) -> Vec<GenesisTransaction> {
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::SupplyPublicAccount {
account_id,
balance: *balance,
}
pub fn default_private_accounts_for_wallet() -> Vec<InitialPrivateAccountForWallet> {
let mut key_chains = vec![KeyChain::new_os_random(), KeyChain::new_os_random()];
key_chains.sort_unstable();
key_chains
.into_iter()
.zip(INITIAL_PRIVATE_BALANCES_FOR_WALLET)
.map(|(key_chain, balance)| InitialPrivateAccountForWallet {
key_chain,
identifier: 0,
balance,
})
.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> {
Ok(WalletConfig {
sequencer_addr: addr_to_url(UrlProtocol::Http, sequencer_addr)

View File

@ -20,7 +20,7 @@ use crate::{
indexer_client::IndexerClient,
setup::{
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 {
genesis_transactions: 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_private_accounts = config::default_private_accounts_for_wallet();
let (sequencer_handle, temp_sequencer_dir) = setup_sequencer(
sequencer_partial_config.unwrap_or_default(),
bedrock_addr,
genesis_transactions
.unwrap_or_else(|| config::genesis_from_public_accounts(&initial_public_accounts)),
genesis_transactions.unwrap_or_else(|| {
config::genesis_from_accounts(&initial_public_accounts, &initial_private_accounts)
}),
)
.await
.context("Failed to setup Sequencer")?;
let (mut wallet, temp_wallet_dir, wallet_password) =
setup_wallet(sequencer_handle.addr(), &initial_public_accounts)
.context("Failed to setup wallet")?;
setup_private_accounts_with_initial_supply(&mut wallet)
let (mut wallet, temp_wallet_dir, wallet_password) = setup_wallet(
sequencer_handle.addr(),
&initial_public_accounts,
&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
.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 common::transaction::NSSATransaction;
use indexer_service::IndexerHandle;
use log::{debug, warn};
use nssa::PrivateKey;
use nssa::{AccountId, PrivateKey, PublicKey, PublicTransaction, program::Program};
use sequencer_service::{GenesisTransaction, SequencerHandle};
use sequencer_service_rpc::RpcClient as _;
use tempfile::TempDir;
use testcontainers::compose::DockerCompose;
use wallet::{
WalletCore,
cli::{
Command, SubcommandReturnValue,
account::{AccountSubcommand, NewSubcommand},
execute_subcommand,
programs::native_token_transfer::AuthTransferSubcommand,
},
config::WalletConfigOverrides,
AccDecodeData::Decode, PrivacyPreservingAccount, WalletCore, config::WalletConfigOverrides,
};
use crate::{
BEDROCK_SERVICE_PORT, BEDROCK_SERVICE_WITH_OPEN_PORT,
config::{self, INITIAL_PRIVATE_BALANCES_FOR_WALLET},
private_mention, public_mention,
config::{self, InitialPrivateAccountForWallet},
};
pub async fn setup_bedrock_node() -> Result<(DockerCompose, SocketAddr)> {
@ -141,6 +135,7 @@ pub async fn setup_sequencer(
pub fn setup_wallet(
sequencer_addr: SocketAddr,
initial_public_accounts: &[(PrivateKey, u128)],
initial_private_accounts: &[InitialPrivateAccountForWallet],
) -> Result<(WalletCore, TempDir, String)> {
let config = config::wallet_config(sequencer_addr).context("Failed to create Wallet config")?;
let config_serialized =
@ -172,6 +167,18 @@ pub fn setup_wallet(
.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
.store_persistent_data()
.context("Failed to store wallet persistent data")?;
@ -179,72 +186,142 @@ pub fn setup_wallet(
Ok((wallet, temp_wallet_dir, wallet_password))
}
pub async fn setup_private_accounts_with_initial_supply(wallet: &mut WalletCore) -> Result<()> {
for _ in INITIAL_PRIVATE_BALANCES_FOR_WALLET {
let result = execute_subcommand(
pub async fn setup_public_accounts_with_initial_supply(
wallet: &WalletCore,
initial_public_accounts: &[(PrivateKey, u128)],
) -> Result<()> {
for (private_key, amount) in initial_public_accounts {
claim_funds_from_vault(
wallet,
Command::Account(AccountSubcommand::New(NewSubcommand::Private {
cci: None,
label: None,
})),
AccountId::from(&PublicKey::new_from_private_key(private_key)),
*amount,
)
.await
.context("Failed to create a private 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"
);
}
.context("Failed to claim funds from vault into public account")?;
}
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 anyhow::Result;
use common::transaction::NSSATransaction;
use integration_tests::{TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, public_mention};
use log::info;
use nssa::program::Program;
use nssa::{SYSTEM_FAUCET_ACCOUNT_ID, program::Program, public_transaction};
use sequencer_service_rpc::RpcClient as _;
use tokio::test;
use wallet::{
@ -344,3 +345,90 @@ async fn successful_transfer_using_to_label() -> Result<()> {
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 };
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(())
}
@ -149,9 +149,9 @@ fn indexer_ffi_block_batching() -> Result<()> {
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 limit = 100;

View File

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

View File

@ -25,6 +25,8 @@ pub mod program;
pub mod error;
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;
/// Unix timestamp in milliseconds.

View File

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

View File

@ -543,7 +543,11 @@ mod tests {
let recipient = AccountWithMetadata::new(Account::default(), false, shared_account_id);
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(
vec![sender, recipient],

View File

@ -11,7 +11,7 @@ use crate::{
program_methods::{
AMM_ELF, AMM_ID, ASSOCIATED_TOKEN_ACCOUNT_ELF, ASSOCIATED_TOKEN_ACCOUNT_ID,
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(),
}
}
#[must_use]
pub fn vault() -> Self {
Self {
id: VAULT_ID,
elf: VAULT_ELF.to_vec(),
}
}
}
// TODO: Testnet only. Refactor to prevent compilation on mainnet.
@ -179,7 +187,7 @@ mod tests {
program_methods::{
AMM_ELF, AMM_ID, ASSOCIATED_TOKEN_ACCOUNT_ELF, ASSOCIATED_TOKEN_ACCOUNT_ID,
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() {
let auth_transfer_program = Program::authenticated_transfer_program();
let token_program = Program::token();
let vault_program = Program::vault();
let pinata_program = Program::pinata();
assert_eq!(auth_transfer_program.id, AUTHENTICATED_TRANSFER_ID);
assert_eq!(auth_transfer_program.elf, AUTHENTICATED_TRANSFER_ELF);
assert_eq!(token_program.id, TOKEN_ID);
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.elf, PINATA_ELF);
}
@ -502,6 +513,7 @@ mod tests {
(PINATA_ELF, PINATA_ID),
(PINATA_TOKEN_ELF, PINATA_TOKEN_ID),
(TOKEN_ELF, TOKEN_ID),
(VAULT_ELF, VAULT_ID),
];
for (elf, expected_id) in cases {
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 transaction::PublicTransaction;
pub use witness_set::WitnessSet;
mod execution;
mod message;
mod transaction;
mod witness_set;

View File

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

View File

@ -1,14 +1,14 @@
use std::{
collections::{HashMap, HashSet},
collections::{HashMap, HashSet, VecDeque},
hash::Hash,
};
use log::debug;
use nssa_core::{
BlockId, Commitment, Nullifier, PrivacyPreservingCircuitOutput, Timestamp,
account::{Account, AccountId, AccountWithMetadata},
program::{
ChainedCall, Claim, DEFAULT_PROGRAM_ID, ProgramId, ProgramOutput,
compute_public_authorized_pdas, validate_execution,
ChainedCall, Claim, DEFAULT_PROGRAM_ID, compute_public_authorized_pdas, validate_execution,
},
};
@ -45,11 +45,237 @@ impl ValidatedStateDiff {
block_id: BlockId,
timestamp: Timestamp,
) -> Result<Self, NssaError> {
let validator = PublicTransactionValidator::new(tx, state, block_id, timestamp);
let state_diff = crate::public_transaction::execute(validator, tx, state)?;
let message = tx.message();
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 {
signer_account_ids: tx.signer_account_ids(),
signer_account_ids,
public_diff: state_diff,
new_commitments: 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.
///
/// 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(
proof: &Proof,
public_pre_states: &[AccountWithMetadata],

View File

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

View File

@ -1,6 +1,6 @@
use authenticated_transfer_core::Instruction;
use clock_core::{CLOCK_01_PROGRAM_ACCOUNT_ID, ClockAccountData};
use nssa_core::{
SYSTEM_FAUCET_ACCOUNT_ID,
account::{Account, AccountWithMetadata},
program::{
AccountPostState, Claim, DEFAULT_PROGRAM_ID, ProgramInput, ProgramOutput, read_nssa_inputs,
@ -27,7 +27,12 @@ fn transfer(
balance_to_move: u128,
) -> Vec<AccountPostState> {
// 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
let sender_post = {
@ -59,39 +64,6 @@ fn transfer(
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.
/// To be used both in public and private contexts.
fn main() {
@ -119,11 +91,6 @@ fn main() {
.expect("Transfer requires exactly 2 accounts");
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(

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]`.
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
logos-blockchain-zone-sdk.workspace = true
testnet_initial_state.workspace = true
authenticated_transfer_core.workspace = true
vault_core.workspace = true
anyhow.workspace = true
serde.workspace = true

View File

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

View File

@ -12,9 +12,7 @@ use logos_blockchain_key_management_system_service::keys::{ED25519_SECRET_KEY_SI
use mempool::{MemPool, MemPoolHandle};
#[cfg(feature = "mock")]
pub use mock::SequencerCoreWithMockClients;
use nssa::{
AccountId, PublicTransaction, ValidatedStateDiff, program::Program, public_transaction::Message,
};
use nssa::{AccountId, PublicTransaction, program::Program, public_transaction::Message};
use nssa_core::GENESIS_BLOCK_ID;
pub use storage::error::DbError;
@ -363,49 +361,48 @@ fn build_genesis_state(config: &SequencerConfig) -> (nssa::V03State, Vec<NSSATra
#[cfg(feature = "testnet")]
let mut state = testnet_initial_state::initial_state_testnet();
let mut genesis_txs = Vec::new();
for genesis_tx in &config.genesis {
let (tx, diff) = match genesis_tx {
GenesisTransaction::SupplyPublicAccount {
let genesis_txs = config
.genesis
.iter()
.map(|genesis_tx| match genesis_tx {
GenesisTransaction::SupplyAccount {
account_id,
balance,
} => build_supply_public_account_genesis_transaction(&state, account_id, *balance),
};
state.apply_state_diff(diff);
genesis_txs.push(tx);
}
let clock_tx = clock_invocation(0);
let diff = ValidatedStateDiff::from_public_transaction(&clock_tx, &state, GENESIS_BLOCK_ID, 0)
.expect("Failed to execute clock transaction for genesis block");
state.apply_state_diff(diff);
genesis_txs.push(clock_tx.into());
} => build_supply_account_genesis_transaction(account_id, *balance),
})
.chain(std::iter::once(clock_invocation(0)))
.inspect(|tx| {
state
.transition_from_public_transaction(tx, GENESIS_BLOCK_ID, 0)
.expect("Failed to execute genesis transaction");
})
.map(NSSATransaction::Public)
.collect();
(state, genesis_txs)
}
fn build_supply_public_account_genesis_transaction(
state: &nssa::V03State,
fn build_supply_account_genesis_transaction(
account_id: &AccountId,
balance: u128,
) -> (NSSATransaction, ValidatedStateDiff) {
let authenticated_transfer_id = Program::authenticated_transfer_program().id();
) -> PublicTransaction {
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(
authenticated_transfer_id,
vec![*account_id, nssa::CLOCK_01_PROGRAM_ACCOUNT_ID],
vault_program_id,
vec![
nssa::SYSTEM_FAUCET_ACCOUNT_ID,
*account_id,
recipient_vault_id,
],
vec![],
authenticated_transfer_core::Instruction::Mint { amount: balance },
vault_core::Instruction::Transfer { amount: balance },
)
.expect("Failed to serialize genesis mint instruction");
let witness_set = nssa::public_transaction::WitnessSet::for_message(&message, &[]);
.expect("Failed to serialize genesis transfer instruction");
let witness_set = nssa::public_transaction::WitnessSet::from_raw_parts(vec![]);
let tx = 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)
PublicTransaction::new(message, witness_set)
}
/// 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,
transaction::{NSSATransaction, clock_invocation},
};
use nssa::{GENESIS_BLOCK_ID, V03State, ValidatedStateDiff};
use nssa::{GENESIS_BLOCK_ID, V03State};
use rocksdb::{
BoundColumnFamily, ColumnFamilyDescriptor, DBWithThreadMode, MultiThreaded, Options,
};
@ -189,16 +189,17 @@ impl RocksDBIO {
));
}
};
let state_diff = ValidatedStateDiff::from_public_genesis_transaction(
&genesis_tx,
&breakpoint,
)
.map_err(|err| {
DbError::db_interaction_error(format!(
"Failed to create state diff from genesis transaction with err {err:?}"
))
})?;
breakpoint.apply_state_diff(state_diff);
breakpoint
.transition_from_public_transaction(
&genesis_tx,
block.header.block_id,
block.header.timestamp,
)
.map_err(|err| {
DbError::db_interaction_error(format!(
"genesis transaction execution failed with err {err:?}"
))
})?;
} else {
transaction
.transaction_stateless_check()

View File

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