diff --git a/Cargo.lock b/Cargo.lock index 64248df7..2f5e766b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3955,10 +3955,10 @@ dependencies = [ "serde_json", "tempfile", "testcontainers", - "testnet_initial_state", "token_core", "tokio", "url", + "vault_core", "wallet", "wallet-ffi", ] @@ -7062,6 +7062,7 @@ dependencies = [ "serde", "token_core", "token_program", + "vault_core", ] [[package]] @@ -8416,7 +8417,6 @@ name = "sequencer_core" version = "0.1.0" dependencies = [ "anyhow", - "authenticated_transfer_core", "borsh", "bytesize", "chrono", @@ -8440,6 +8440,7 @@ dependencies = [ "testnet_initial_state", "tokio", "url", + "vault_core", ] [[package]] @@ -10077,6 +10078,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" diff --git a/Cargo.toml b/Cargo.toml index be6c2583..974f73ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" } diff --git a/artifacts/program_methods/amm.bin b/artifacts/program_methods/amm.bin index 9b9bbf9a..06d149eb 100644 Binary files a/artifacts/program_methods/amm.bin and b/artifacts/program_methods/amm.bin differ diff --git a/artifacts/program_methods/associated_token_account.bin b/artifacts/program_methods/associated_token_account.bin index 55e7c94d..1598a7fd 100644 Binary files a/artifacts/program_methods/associated_token_account.bin and b/artifacts/program_methods/associated_token_account.bin differ diff --git a/artifacts/program_methods/authenticated_transfer.bin b/artifacts/program_methods/authenticated_transfer.bin index 48c065ba..3191ac44 100644 Binary files a/artifacts/program_methods/authenticated_transfer.bin and b/artifacts/program_methods/authenticated_transfer.bin differ diff --git a/artifacts/program_methods/clock.bin b/artifacts/program_methods/clock.bin index 27096692..b477d939 100644 Binary files a/artifacts/program_methods/clock.bin and b/artifacts/program_methods/clock.bin differ diff --git a/artifacts/program_methods/pinata.bin b/artifacts/program_methods/pinata.bin index ef1edefe..87943ad0 100644 Binary files a/artifacts/program_methods/pinata.bin and b/artifacts/program_methods/pinata.bin differ diff --git a/artifacts/program_methods/pinata_token.bin b/artifacts/program_methods/pinata_token.bin index 5c65366f..b44b4395 100644 Binary files a/artifacts/program_methods/pinata_token.bin and b/artifacts/program_methods/pinata_token.bin differ diff --git a/artifacts/program_methods/privacy_preserving_circuit.bin b/artifacts/program_methods/privacy_preserving_circuit.bin index e6772240..580489f7 100644 Binary files a/artifacts/program_methods/privacy_preserving_circuit.bin and b/artifacts/program_methods/privacy_preserving_circuit.bin differ diff --git a/artifacts/program_methods/token.bin b/artifacts/program_methods/token.bin index 89015992..f9682d3a 100644 Binary files a/artifacts/program_methods/token.bin and b/artifacts/program_methods/token.bin differ diff --git a/artifacts/program_methods/vault.bin b/artifacts/program_methods/vault.bin new file mode 100644 index 00000000..3f50fa9f Binary files /dev/null and b/artifacts/program_methods/vault.bin differ diff --git a/artifacts/test_program_methods/auth_asserting_noop.bin b/artifacts/test_program_methods/auth_asserting_noop.bin index eee2f222..d6034541 100644 Binary files a/artifacts/test_program_methods/auth_asserting_noop.bin and b/artifacts/test_program_methods/auth_asserting_noop.bin differ diff --git a/artifacts/test_program_methods/auth_transfer_proxy.bin b/artifacts/test_program_methods/auth_transfer_proxy.bin index 76c4b953..f1796ce8 100644 Binary files a/artifacts/test_program_methods/auth_transfer_proxy.bin and b/artifacts/test_program_methods/auth_transfer_proxy.bin differ diff --git a/artifacts/test_program_methods/burner.bin b/artifacts/test_program_methods/burner.bin index 75b9ca0e..3873ab01 100644 Binary files a/artifacts/test_program_methods/burner.bin and b/artifacts/test_program_methods/burner.bin differ diff --git a/artifacts/test_program_methods/chain_caller.bin b/artifacts/test_program_methods/chain_caller.bin index bf944906..56766a3c 100644 Binary files a/artifacts/test_program_methods/chain_caller.bin and b/artifacts/test_program_methods/chain_caller.bin differ diff --git a/artifacts/test_program_methods/changer_claimer.bin b/artifacts/test_program_methods/changer_claimer.bin index 970d9cf4..f392842c 100644 Binary files a/artifacts/test_program_methods/changer_claimer.bin and b/artifacts/test_program_methods/changer_claimer.bin differ diff --git a/artifacts/test_program_methods/claimer.bin b/artifacts/test_program_methods/claimer.bin index be5cc6b1..21a5c887 100644 Binary files a/artifacts/test_program_methods/claimer.bin and b/artifacts/test_program_methods/claimer.bin differ diff --git a/artifacts/test_program_methods/clock_chain_caller.bin b/artifacts/test_program_methods/clock_chain_caller.bin index 589131f7..e4e2f867 100644 Binary files a/artifacts/test_program_methods/clock_chain_caller.bin and b/artifacts/test_program_methods/clock_chain_caller.bin differ diff --git a/artifacts/test_program_methods/data_changer.bin b/artifacts/test_program_methods/data_changer.bin index acaf3233..e7fce46b 100644 Binary files a/artifacts/test_program_methods/data_changer.bin and b/artifacts/test_program_methods/data_changer.bin differ diff --git a/artifacts/test_program_methods/extra_output.bin b/artifacts/test_program_methods/extra_output.bin index 8915c64b..e1e91acb 100644 Binary files a/artifacts/test_program_methods/extra_output.bin and b/artifacts/test_program_methods/extra_output.bin differ diff --git a/artifacts/test_program_methods/flash_swap_callback.bin b/artifacts/test_program_methods/flash_swap_callback.bin index 91be3cfb..3ba46235 100644 Binary files a/artifacts/test_program_methods/flash_swap_callback.bin and b/artifacts/test_program_methods/flash_swap_callback.bin differ diff --git a/artifacts/test_program_methods/flash_swap_initiator.bin b/artifacts/test_program_methods/flash_swap_initiator.bin index 4280c7e8..06a09dff 100644 Binary files a/artifacts/test_program_methods/flash_swap_initiator.bin and b/artifacts/test_program_methods/flash_swap_initiator.bin differ diff --git a/artifacts/test_program_methods/malicious_authorization_changer.bin b/artifacts/test_program_methods/malicious_authorization_changer.bin index 03669255..d0be0447 100644 Binary files a/artifacts/test_program_methods/malicious_authorization_changer.bin and b/artifacts/test_program_methods/malicious_authorization_changer.bin differ diff --git a/artifacts/test_program_methods/malicious_caller_program_id.bin b/artifacts/test_program_methods/malicious_caller_program_id.bin index f02d1655..751ed897 100644 Binary files a/artifacts/test_program_methods/malicious_caller_program_id.bin and b/artifacts/test_program_methods/malicious_caller_program_id.bin differ diff --git a/artifacts/test_program_methods/malicious_self_program_id.bin b/artifacts/test_program_methods/malicious_self_program_id.bin index d57676ab..fabc9b44 100644 Binary files a/artifacts/test_program_methods/malicious_self_program_id.bin and b/artifacts/test_program_methods/malicious_self_program_id.bin differ diff --git a/artifacts/test_program_methods/minter.bin b/artifacts/test_program_methods/minter.bin index 21d1a84a..d6ca6b99 100644 Binary files a/artifacts/test_program_methods/minter.bin and b/artifacts/test_program_methods/minter.bin differ diff --git a/artifacts/test_program_methods/missing_output.bin b/artifacts/test_program_methods/missing_output.bin index 5fc04f94..1c9f6914 100644 Binary files a/artifacts/test_program_methods/missing_output.bin and b/artifacts/test_program_methods/missing_output.bin differ diff --git a/artifacts/test_program_methods/modified_transfer.bin b/artifacts/test_program_methods/modified_transfer.bin index 94fb2835..a8a87da8 100644 Binary files a/artifacts/test_program_methods/modified_transfer.bin and b/artifacts/test_program_methods/modified_transfer.bin differ diff --git a/artifacts/test_program_methods/nonce_changer.bin b/artifacts/test_program_methods/nonce_changer.bin index 48582a69..e5659b80 100644 Binary files a/artifacts/test_program_methods/nonce_changer.bin and b/artifacts/test_program_methods/nonce_changer.bin differ diff --git a/artifacts/test_program_methods/noop.bin b/artifacts/test_program_methods/noop.bin index 82c85571..3622ab12 100644 Binary files a/artifacts/test_program_methods/noop.bin and b/artifacts/test_program_methods/noop.bin differ diff --git a/artifacts/test_program_methods/pda_claimer.bin b/artifacts/test_program_methods/pda_claimer.bin index e2d2da70..011561d6 100644 Binary files a/artifacts/test_program_methods/pda_claimer.bin and b/artifacts/test_program_methods/pda_claimer.bin differ diff --git a/artifacts/test_program_methods/pda_fund_spend_proxy.bin b/artifacts/test_program_methods/pda_fund_spend_proxy.bin index e377e6bf..677d054b 100644 Binary files a/artifacts/test_program_methods/pda_fund_spend_proxy.bin and b/artifacts/test_program_methods/pda_fund_spend_proxy.bin differ diff --git a/artifacts/test_program_methods/pinata_cooldown.bin b/artifacts/test_program_methods/pinata_cooldown.bin index b2d1d68c..8e3b97c5 100644 Binary files a/artifacts/test_program_methods/pinata_cooldown.bin and b/artifacts/test_program_methods/pinata_cooldown.bin differ diff --git a/artifacts/test_program_methods/private_pda_delegator.bin b/artifacts/test_program_methods/private_pda_delegator.bin index 3e0a1025..e8caa0ad 100644 Binary files a/artifacts/test_program_methods/private_pda_delegator.bin and b/artifacts/test_program_methods/private_pda_delegator.bin differ diff --git a/artifacts/test_program_methods/program_owner_changer.bin b/artifacts/test_program_methods/program_owner_changer.bin index 8568b9b1..4a47211b 100644 Binary files a/artifacts/test_program_methods/program_owner_changer.bin and b/artifacts/test_program_methods/program_owner_changer.bin differ diff --git a/artifacts/test_program_methods/simple_balance_transfer.bin b/artifacts/test_program_methods/simple_balance_transfer.bin index 7c7fb0c9..5df52e38 100644 Binary files a/artifacts/test_program_methods/simple_balance_transfer.bin and b/artifacts/test_program_methods/simple_balance_transfer.bin differ diff --git a/artifacts/test_program_methods/time_locked_transfer.bin b/artifacts/test_program_methods/time_locked_transfer.bin index 4af11d59..937bd468 100644 Binary files a/artifacts/test_program_methods/time_locked_transfer.bin and b/artifacts/test_program_methods/time_locked_transfer.bin differ diff --git a/artifacts/test_program_methods/two_pda_claimer.bin b/artifacts/test_program_methods/two_pda_claimer.bin index 49b55dc4..fb8b9bf3 100644 Binary files a/artifacts/test_program_methods/two_pda_claimer.bin and b/artifacts/test_program_methods/two_pda_claimer.bin differ diff --git a/artifacts/test_program_methods/validity_window.bin b/artifacts/test_program_methods/validity_window.bin index d4ed3e9d..902f149b 100644 Binary files a/artifacts/test_program_methods/validity_window.bin and b/artifacts/test_program_methods/validity_window.bin differ diff --git a/artifacts/test_program_methods/validity_window_chain_caller.bin b/artifacts/test_program_methods/validity_window_chain_caller.bin index 2531a59d..05815525 100644 Binary files a/artifacts/test_program_methods/validity_window_chain_caller.bin and b/artifacts/test_program_methods/validity_window_chain_caller.bin differ diff --git a/common/src/transaction.rs b/common/src/transaction.rs index 7ce0e76f..f5ea6488 100644 --- a/common/src/transaction.rs +++ b/common/src/transaction.rs @@ -67,7 +67,11 @@ 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. + /// + /// This check is required for all user transactions. Only sequencer transaction may bypass this + /// check. pub fn validate_on_state( &self, state: &V03State, @@ -98,6 +102,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) } diff --git a/configs/docker-all-in-one/sequencer_config.json b/configs/docker-all-in-one/sequencer_config.json index 36eee2bd..7c0a4426 100644 --- a/configs/docker-all-in-one/sequencer_config.json +++ b/configs/docker-all-in-one/sequencer_config.json @@ -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 } diff --git a/indexer/core/src/block_store.rs b/indexer/core/src/block_store.rs index 3b593071..b66b778f 100644 --- a/indexer/core/src/block_store.rs +++ b/indexer/core/src/block_store.rs @@ -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); } } diff --git a/integration_tests/Cargo.toml b/integration_tests/Cargo.toml index 50596f37..288137ea 100644 --- a/integration_tests/Cargo.toml +++ b/integration_tests/Cargo.toml @@ -20,12 +20,12 @@ 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"] } wallet-ffi.workspace = true indexer_ffi.workspace = true -testnet_initial_state.workspace = true indexer_service_protocol.workspace = true url.workspace = true diff --git a/integration_tests/src/config.rs b/integration_tests/src/config.rs index 9c0db5df..e225e080 100644 --- a/integration_tests/src/config.rs +++ b/integration_tests/src/config.rs @@ -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 sequencer_core::config::{BedrockConfig, GenesisTransaction, SequencerConfig}; +use nssa_core::Identifier; +use sequencer_core::config::{BedrockConfig, GenesisAction, 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 { @@ -50,7 +66,7 @@ pub fn sequencer_config( partial: SequencerPartialConfig, home: PathBuf, bedrock_addr: SocketAddr, - genesis_transactions: Vec, + genesis_transactions: Vec, ) -> Result { let SequencerPartialConfig { max_num_tx_in_block, @@ -80,43 +96,57 @@ 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 { - 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 { + 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 { + 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); + GenesisAction::SupplyAccount { + account_id, + balance: *balance, + } + }); + + let private_genesis = private_accounts + .iter() + .map(|account| GenesisAction::SupplyAccount { + account_id: account.account_id(), + balance: account.balance, + }); + + public_genesis.chain(private_genesis).collect() +} + pub fn wallet_config(sequencer_addr: SocketAddr) -> Result { Ok(WalletConfig { sequencer_addr: addr_to_url(UrlProtocol::Http, sequencer_addr) diff --git a/integration_tests/src/lib.rs b/integration_tests/src/lib.rs index 7dd950eb..3662e006 100644 --- a/integration_tests/src/lib.rs +++ b/integration_tests/src/lib.rs @@ -9,7 +9,7 @@ use indexer_service::IndexerHandle; use log::{debug, error}; use nssa::{AccountId, PrivacyPreservingTransaction}; use nssa_core::Commitment; -use sequencer_core::config::GenesisTransaction; +use sequencer_core::config::GenesisAction; use sequencer_service::SequencerHandle; use sequencer_service_rpc::{RpcClient as _, SequencerClient, SequencerClientBuilder}; use tempfile::TempDir; @@ -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, }, }; @@ -216,7 +216,7 @@ impl Drop for TestContext { } pub struct TestContextBuilder { - genesis_transactions: Option>, + genesis_transactions: Option>, sequencer_partial_config: Option, enable_indexer: bool, } @@ -226,12 +226,12 @@ impl TestContextBuilder { Self { genesis_transactions: None, sequencer_partial_config: None, - enable_indexer: false, + enable_indexer: true, } } #[must_use] - pub fn with_genesis(mut self, genesis_transactions: Vec) -> Self { + pub fn with_genesis(mut self, genesis_transactions: Vec) -> Self { self.genesis_transactions = Some(genesis_transactions); self } @@ -291,19 +291,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")?; diff --git a/integration_tests/src/setup.rs b/integration_tests/src/setup.rs index 55c24e92..c43590d0 100644 --- a/integration_tests/src/setup.rs +++ b/integration_tests/src/setup.rs @@ -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 sequencer_service::{GenesisTransaction, SequencerHandle}; +use nssa::{AccountId, PrivateKey, PublicKey, PublicTransaction, program::Program}; +use sequencer_service::{GenesisAction, 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)> { @@ -115,7 +109,7 @@ pub async fn setup_indexer(bedrock_addr: SocketAddr) -> Result<(IndexerHandle, T pub async fn setup_sequencer( partial: config::SequencerPartialConfig, bedrock_addr: SocketAddr, - genesis_transactions: Vec, + genesis_transactions: Vec, ) -> Result<(SequencerHandle, TempDir)> { let temp_sequencer_dir = tempfile::tempdir().context("Failed to create temp dir for sequencer home")?; @@ -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(()) +} diff --git a/integration_tests/tests/auth_transfer/public.rs b/integration_tests/tests/auth_transfer/public.rs index 2b6ec130..80c9d575 100644 --- a/integration_tests/tests/auth_transfer/public.rs +++ b/integration_tests/tests/auth_transfer/public.rs @@ -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(()) +} diff --git a/integration_tests/tests/indexer_ffi.rs b/integration_tests/tests/indexer_ffi.rs index c1523061..178b2640 100644 --- a/integration_tests/tests/indexer_ffi.rs +++ b/integration_tests/tests/indexer_ffi.rs @@ -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::::from_none(); let limit = 100; diff --git a/integration_tests/tests/tps.rs b/integration_tests/tests/tps.rs index 030417b8..22550bb0 100644 --- a/integration_tests/tests/tps.rs +++ b/integration_tests/tests/tps.rs @@ -28,7 +28,7 @@ use nssa_core::{ account::{AccountWithMetadata, Nonce, data::Data}, encryption::ViewingPublicKey, }; -use sequencer_core::config::GenesisTransaction; +use sequencer_core::config::GenesisAction; use sequencer_service_rpc::RpcClient as _; use tokio::test; @@ -94,10 +94,10 @@ impl TpsTestManager { /// Generates a sequencer configuration with initial balance in a number of public accounts. /// The transactions generated with the function `build_public_txs` will be valid in a node /// started with the config from this method. - fn generate_genesis(&self) -> Vec { + fn generate_genesis(&self) -> Vec { self.public_keypairs .iter() - .map(|(_, account_id)| GenesisTransaction::SupplyPublicAccount { + .map(|(_, account_id)| GenesisAction::SupplyAccount { account_id: *account_id, balance: 10, }) diff --git a/nssa/core/src/lib.rs b/nssa/core/src/lib.rs index 466e1f5d..c5210d0f 100644 --- a/nssa/core/src/lib.rs +++ b/nssa/core/src/lib.rs @@ -26,6 +26,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. diff --git a/nssa/src/lib.rs b/nssa/src/lib.rs index 0ad9b143..f8b9cd0f 100644 --- a/nssa/src/lib.rs +++ b/nssa/src/lib.rs @@ -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, diff --git a/nssa/src/privacy_preserving_transaction/circuit.rs b/nssa/src/privacy_preserving_transaction/circuit.rs index e4940347..915c8d3e 100644 --- a/nssa/src/privacy_preserving_transaction/circuit.rs +++ b/nssa/src/privacy_preserving_transaction/circuit.rs @@ -597,7 +597,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], @@ -631,7 +635,8 @@ mod tests { let (output, _) = execute_and_prove( vec![pre], - Program::serialize_instruction(0_u128).unwrap(), + Program::serialize_instruction(authenticated_transfer_core::Instruction::Initialize) + .unwrap(), vec![InputAccountIdentity::PrivateAuthorizedInit { ssk, nsk: keys.nsk, @@ -670,7 +675,10 @@ mod tests { let (output, _) = execute_and_prove( vec![sender, recipient], - Program::serialize_instruction(1_u128).unwrap(), + Program::serialize_instruction(authenticated_transfer_core::Instruction::Transfer { + amount: 1, + }) + .unwrap(), vec![ InputAccountIdentity::Public, InputAccountIdentity::PrivateUnauthorized { @@ -712,7 +720,10 @@ mod tests { let (output, _) = execute_and_prove( vec![sender, recipient], - Program::serialize_instruction(1_u128).unwrap(), + Program::serialize_instruction(authenticated_transfer_core::Instruction::Transfer { + amount: 1, + }) + .unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { ssk, diff --git a/nssa/src/program.rs b/nssa/src/program.rs index 059aa5ca..07a85469 100644 --- a/nssa/src/program.rs +++ b/nssa/src/program.rs @@ -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, }, }; @@ -492,12 +500,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); } @@ -512,6 +523,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(); diff --git a/nssa/src/public_transaction/execution.rs b/nssa/src/public_transaction/execution.rs deleted file mode 100644 index a5dc1cd9..00000000 --- a/nssa/src/public_transaction/execution.rs +++ /dev/null @@ -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, - caller_program_id: Option, - chained_call: &ChainedCall, - program_output: &ProgramOutput, - ) -> Result<(), NssaError>; - - fn validate_post_execution( - &mut self, - state_diff: &HashMap, - ) -> Result<(), NssaError>; -} - -pub fn execute( - mut validator: impl Validator, - tx: &PublicTransaction, - state: &V03State, -) -> Result, 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 = 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) -} diff --git a/nssa/src/public_transaction/mod.rs b/nssa/src/public_transaction/mod.rs index 108dbf6e..1af61e10 100644 --- a/nssa/src/public_transaction/mod.rs +++ b/nssa/src/public_transaction/mod.rs @@ -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; diff --git a/nssa/src/state.rs b/nssa/src/state.rs index dc473a26..a2c7cbea 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -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 }; diff --git a/nssa/src/validated_state_diff.rs b/nssa/src/validated_state_diff.rs index f7044b51..068dc32c 100644 --- a/nssa/src/validated_state_diff.rs +++ b/nssa/src/validated_state_diff.rs @@ -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 { - 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::>().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 = 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 { - 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::>().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, - caller_program_id: Option, - 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, - ) -> 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, - _caller_program_id: Option, - _chained_call: &ChainedCall, - _program_output: &ProgramOutput, - ) -> Result<(), NssaError> { - Ok(()) - } - - fn validate_post_execution( - &mut self, - _state_diff: &HashMap, - ) -> Result<(), NssaError> { - Ok(()) - } -} - fn check_privacy_preserving_circuit_proof_is_valid( proof: &Proof, public_pre_states: &[AccountWithMetadata], diff --git a/program_methods/guest/Cargo.toml b/program_methods/guest/Cargo.toml index e0ef5e75..98938d1f 100644 --- a/program_methods/guest/Cargo.toml +++ b/program_methods/guest/Cargo.toml @@ -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 } diff --git a/program_methods/guest/src/bin/authenticated_transfer.rs b/program_methods/guest/src/bin/authenticated_transfer.rs index 525fcf2b..bdb91ac5 100644 --- a/program_methods/guest/src/bin/authenticated_transfer.rs +++ b/program_methods/guest/src/bin/authenticated_transfer.rs @@ -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 { // 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 { - 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( diff --git a/program_methods/guest/src/bin/vault.rs b/program_methods/guest/src/bin/vault.rs new file mode 100644 index 00000000..c691e8f6 --- /dev/null +++ b/program_methods/guest/src/bin/vault.rs @@ -0,0 +1,94 @@ +//! Vault program which allows users to create vault accounts and transfer funds to them. +//! Funds can later be claimed from the vault accounts by their owners. +//! +//! The program is designed to be used in conjunction with the authenticated transfer program, which +//! performs the actual transfer of funds from the vault accounts. + +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 { + 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::(); + + let pre_states_clone = pre_states.clone(); + let post_states = unchanged_post_states(&pre_states_clone); + + let chained_calls = match instruction { + Instruction::Transfer { + recipient_id, + amount, + } => { + let [sender, recipient_vault] = pre_states + .try_into() + .expect("Transfer requires exactly 3 accounts"); + + let seed = vault_core::compute_vault_seed(recipient_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"); + + assert!( + owner.is_authorized, + "Owner must be authorized to claim from the vault" + ); + + 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(); +} diff --git a/programs/authenticated_transfer/core/src/lib.rs b/programs/authenticated_transfer/core/src/lib.rs index ed1121f8..14edac5e 100644 --- a/programs/authenticated_transfer/core/src/lib.rs +++ b/programs/authenticated_transfer/core/src/lib.rs @@ -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 }, } diff --git a/programs/vault/core/Cargo.toml b/programs/vault/core/Cargo.toml new file mode 100644 index 00000000..fd3cdf96 --- /dev/null +++ b/programs/vault/core/Cargo.toml @@ -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 diff --git a/programs/vault/core/src/lib.rs b/programs/vault/core/src/lib.rs new file mode 100644 index 00000000..8937e087 --- /dev/null +++ b/programs/vault/core/src/lib.rs @@ -0,0 +1,53 @@ +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 { + /// Transfers native tokens from sender to recipient's vault. + /// + /// Required accounts (3): + /// - Sender account + /// - Recipient account + /// - Recipient vault PDA account + Transfer { + recipient_id: AccountId, + 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) +} diff --git a/sequencer/core/Cargo.toml b/sequencer/core/Cargo.toml index ef9db98f..da88d481 100644 --- a/sequencer/core/Cargo.toml +++ b/sequencer/core/Cargo.toml @@ -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 diff --git a/sequencer/core/src/config.rs b/sequencer/core/src/config.rs index bdac163b..20d27566 100644 --- a/sequencer/core/src/config.rs +++ b/sequencer/core/src/config.rs @@ -17,8 +17,8 @@ use url::Url; /// A transaction to be applied at genesis to supply initial balances. #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] -pub enum GenesisTransaction { - SupplyPublicAccount { +pub enum GenesisAction { + SupplyAccount { account_id: AccountId, balance: u128, }, @@ -51,7 +51,7 @@ pub struct SequencerConfig { pub bedrock_config: BedrockConfig, /// Genesis configuration. #[serde(default)] - pub genesis: Vec, + pub genesis: Vec, } #[derive(Clone, Serialize, Deserialize)] diff --git a/sequencer/core/src/lib.rs b/sequencer/core/src/lib.rs index 1c35aa8c..db8a6894 100644 --- a/sequencer/core/src/lib.rs +++ b/sequencer/core/src/lib.rs @@ -6,15 +6,13 @@ use common::{ block::{BedrockStatus, Block, HashableBlockData}, transaction::{NSSATransaction, clock_invocation}, }; -use config::{GenesisTransaction, SequencerConfig}; +use config::{GenesisAction, SequencerConfig}; use log::{error, info, warn}; use logos_blockchain_key_management_system_service::keys::{ED25519_SECRET_KEY_SIZE, Ed25519Key}; 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,47 @@ fn build_genesis_state(config: &SequencerConfig) -> (nssa::V03State, Vec 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, recipient_vault_id], vec![], - authenticated_transfer_core::Instruction::Mint { amount: balance }, + vault_core::Instruction::Transfer { + recipient_id: *account_id, + 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. diff --git a/storage/src/indexer/mod.rs b/storage/src/indexer/mod.rs index 4cc63c89..97be70e5 100644 --- a/storage/src/indexer/mod.rs +++ b/storage/src/indexer/mod.rs @@ -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() diff --git a/test_program_methods/guest/src/bin/auth_transfer_proxy.rs b/test_program_methods/guest/src/bin/auth_transfer_proxy.rs index 17316f16..b3590074 100644 --- a/test_program_methods/guest/src/bin/auth_transfer_proxy.rs +++ b/test_program_methods/guest/src/bin/auth_transfer_proxy.rs @@ -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, diff --git a/test_program_methods/guest/src/bin/pda_fund_spend_proxy.rs b/test_program_methods/guest/src/bin/pda_fund_spend_proxy.rs index c02261f9..567f9af1 100644 --- a/test_program_methods/guest/src/bin/pda_fund_spend_proxy.rs +++ b/test_program_methods/guest/src/bin/pda_fund_spend_proxy.rs @@ -53,7 +53,8 @@ fn main() { let chained_call = ChainedCall { program_id: auth_transfer_id, - instruction_data: to_vec(&amount).unwrap(), + instruction_data: to_vec(&authenticated_transfer_core::Instruction::Transfer { amount }) + .unwrap(), pre_states: chained_pre_states, pda_seeds: vec![seed], }; diff --git a/wallet/src/storage/key_chain.rs b/wallet/src/storage/key_chain.rs index 72097a01..e00dee8d 100644 --- a/wallet/src/storage/key_chain.rs +++ b/wallet/src/storage/key_chain.rs @@ -330,7 +330,14 @@ impl UserKeyChain { kind: PrivateAccountKind, account: nssa_core::account::Account, ) -> Result<()> { - // First try to update imported account + // Try to find in shared accounts + if let Some(entry) = self.shared_private_accounts.get_mut(&account_id) { + debug!("Updating shared private account {account_id}"); + entry.account = account; + return Ok(()); + } + + // Then try to update imported account for (key, data) in &mut self.imported_private_accounts { for (kind, imported_account) in &mut data.accounts { let expected_id = @@ -540,7 +547,7 @@ impl UserKeyChain { PersistentAccountDataPrivate { account_id: *account_id, chain_index: key.clone(), - data: data.clone(), + data: data.clone().into(), }, ))); } @@ -621,7 +628,7 @@ impl UserKeyChain { _ => unreachable!(), }); let mut private_key_tree = KeyTreePrivate::new_from_root(match private_root { - PersistentAccountData::Private(data) => data.data, + PersistentAccountData::Private(data) => data.data.into(), _ => unreachable!(), }); @@ -631,7 +638,7 @@ impl UserKeyChain { public_key_tree.insert(data.account_id, data.chain_index, data.data); } PersistentAccountData::Private(data) => { - private_key_tree.insert(data.account_id, data.chain_index, data.data); + private_key_tree.insert(data.account_id, data.chain_index, data.data.into()); } PersistentAccountData::ImportedPublic(data) => { imported_public_accounts.insert(data.account_id, data.pub_sign_key); diff --git a/wallet/src/storage/persistent.rs b/wallet/src/storage/persistent.rs index b3835827..0c82ceaa 100644 --- a/wallet/src/storage/persistent.rs +++ b/wallet/src/storage/persistent.rs @@ -52,5 +52,39 @@ pub struct PersistentAccountDataPublic { pub struct PersistentAccountDataPrivate { pub account_id: nssa::AccountId, pub chain_index: ChainIndex, - pub data: ChildKeysPrivate, + pub data: ChildKeysPrivatePersistent, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ChildKeysPrivatePersistent { + pub value: ( + key_protocol::key_management::KeyChain, + Vec<(nssa_core::PrivateAccountKind, nssa::Account)>, + ), + pub ccc: [u8; 32], + pub cci: Option, +} + +impl From for ChildKeysPrivatePersistent { + fn from(value: ChildKeysPrivate) -> Self { + let ChildKeysPrivate { value, ccc, cci } = value; + + Self { + value: (value.0, Vec::from_iter(value.1)), + ccc, + cci, + } + } +} + +impl From for ChildKeysPrivate { + fn from(value: ChildKeysPrivatePersistent) -> Self { + let ChildKeysPrivatePersistent { value, ccc, cci } = value; + + Self { + value: (value.0, BTreeMap::from_iter(value.1)), + ccc, + cci, + } + } }