diff --git a/Cargo.lock b/Cargo.lock index 845324d0..1cc1072c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2794,6 +2794,14 @@ dependencies = [ "typenum", ] +[[package]] +name = "genesis_supply_account_core" +version = "0.1.0" +dependencies = [ + "nssa_core", + "serde", +] + [[package]] name = "getrandom" version = "0.2.17" @@ -3611,7 +3619,6 @@ dependencies = [ "serde_json", "tempfile", "testcontainers", - "testnet_initial_state", "token_core", "tokio", "url", @@ -3998,7 +4005,6 @@ dependencies = [ "hmac-sha512", "itertools 0.14.0", "k256", - "log", "nssa", "nssa_core", "rand 0.8.5", @@ -6097,6 +6103,7 @@ dependencies = [ "ata_core", "ata_program", "clock_core", + "genesis_supply_account_core", "nssa_core", "risc0-zkvm", "serde", @@ -7347,8 +7354,10 @@ dependencies = [ "chrono", "common", "futures", + "genesis_supply_account_core", "humantime-serde", "jsonrpsee", + "key_protocol", "log", "logos-blockchain-core", "logos-blockchain-key-management-system-service", diff --git a/Cargo.toml b/Cargo.toml index 78db576d..5aac10b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ members = [ "programs/token", "programs/associated_token_account/core", "programs/associated_token_account", + "programs/genesis_supply_account/core", "sequencer/core", "sequencer/service", "sequencer/service/protocol", @@ -64,6 +65,7 @@ amm_core = { path = "programs/amm/core" } amm_program = { path = "programs/amm" } ata_core = { path = "programs/associated_token_account/core" } ata_program = { path = "programs/associated_token_account" } +genesis_supply_account_core = { path = "programs/genesis_supply_account/core" } test_program_methods = { path = "test_program_methods" } bedrock_client = { path = "bedrock_client" } testnet_initial_state = { path = "testnet_initial_state" } diff --git a/artifacts/program_methods/amm.bin b/artifacts/program_methods/amm.bin index 29792d9f..4c5a9bb2 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 94e7f824..6dfa1ba5 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 1541ad33..8c0e7b20 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 732e6868..f382ca08 100644 Binary files a/artifacts/program_methods/clock.bin and b/artifacts/program_methods/clock.bin differ diff --git a/artifacts/program_methods/genesis_supply_account.bin b/artifacts/program_methods/genesis_supply_account.bin new file mode 100644 index 00000000..c377a1e6 Binary files /dev/null and b/artifacts/program_methods/genesis_supply_account.bin differ diff --git a/artifacts/program_methods/genesis_supply_private_account.bin b/artifacts/program_methods/genesis_supply_private_account.bin new file mode 100644 index 00000000..9d6aa313 Binary files /dev/null and b/artifacts/program_methods/genesis_supply_private_account.bin differ diff --git a/artifacts/program_methods/pinata.bin b/artifacts/program_methods/pinata.bin index 2e55c190..fe8cc0cd 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 1091ff9f..ee9c2329 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 7506b118..7d57202f 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 b917e392..69f8c3ee 100644 Binary files a/artifacts/program_methods/token.bin and b/artifacts/program_methods/token.bin differ diff --git a/artifacts/test_program_methods/burner.bin b/artifacts/test_program_methods/burner.bin index c8675c9d..92ec00ae 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 e8a89862..2913cf0b 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 223862a3..0f91bfd8 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 64faf397..7c4460ba 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 6789fdc1..656c33fe 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 bf588bc2..09d42cce 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 9688783e..4f1e6393 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 080160bb..34eae04b 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 ee3706ad..50e9d2f7 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 4b93c2f3..37f9aad8 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 b449018c..ba101957 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 bbfa9f9d..82434f47 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 d0a37e64..e14eb44c 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 8f61b773..0c9f1d7c 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 9470b6a0..7b212fea 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 87e1eb13..5c997dea 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 30f82b6a..935952ab 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/pinata_cooldown.bin b/artifacts/test_program_methods/pinata_cooldown.bin index 9ad5a3c8..1599e6d1 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/program_owner_changer.bin b/artifacts/test_program_methods/program_owner_changer.bin index 7e24861f..4b97a87a 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 65c574c4..6246809d 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 70be21fe..10c381c3 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/validity_window.bin b/artifacts/test_program_methods/validity_window.bin index 0acfd77c..3c3d6666 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 3dde4687..54d02a9f 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..4389072f 100644 --- a/common/src/transaction.rs +++ b/common/src/transaction.rs @@ -76,7 +76,7 @@ impl NSSATransaction { ) -> Result { let diff = match self { Self::Public(tx) => { - ValidatedStateDiff::from_public_transaction(tx, state, block_id, timestamp) + ValidatedStateDiff::from_public_transaction(tx, state, block_id, timestamp, false) } Self::PrivacyPreserving(tx) => ValidatedStateDiff::from_privacy_preserving_transaction( tx, state, block_id, timestamp, diff --git a/configs/docker-all-in-one/sequencer_config.json b/configs/docker-all-in-one/sequencer_config.json index d7fd3490..36eee2bd 100644 --- a/configs/docker-all-in-one/sequencer_config.json +++ b/configs/docker-all-in-one/sequencer_config.json @@ -16,117 +16,29 @@ "node_url": "http://logos-blockchain-node-0:18080" }, "indexer_rpc_url": "ws://indexer_service:8779", - "initial_accounts": [ + "genesis": [ { - "account_id": "6iArKUXxhUJqS7kCaPNhwMWt3ro71PDyBj7jwAyE2VQV", - "balance": 10000 - }, - { - "account_id": "7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo", - "balance": 20000 - } - ], - "initial_commitments": [ - { - "npk":[ - 177, - 64, - 1, - 11, - 87, - 38, - 254, - 159, - 231, - 165, - 1, - 94, - 64, - 137, - 243, - 76, - 249, - 101, - 251, - 129, - 33, - 101, - 189, - 30, - 42, - 11, - 191, - 34, - 103, - 186, - 227, - 230 - ] , - "account": { - "program_owner": [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - "balance": 10000, - "data": [], - "nonce": 0 + "supply_public_account": { + "account_id": "6iArKUXxhUJqS7kCaPNhwMWt3ro71PDyBj7jwAyE2VQV", + "balance": 10000 } }, { - "npk": [ - 32, - 67, - 72, - 164, - 106, - 53, - 66, - 239, - 141, - 15, - 52, - 230, - 136, - 177, - 2, - 236, - 207, - 243, - 134, - 135, - 210, - 143, - 87, - 232, - 215, - 128, - 194, - 120, - 113, - 224, - 4, - 165 - ], - "account": { - "program_owner": [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - "balance": 20000, - "data": [], - "nonce": 0 + "supply_public_account": { + "account_id": "7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo", + "balance": 20000 + } + }, + { + "supply_private_account": { + "npk": [177,64,1,11,87,38,254,159,231,165,1,94,64,137,243,76,249,101,251,129,33,101,189,30,42,11,191,34,103,186,227,230], + "balance": 10000 + } + }, + { + "supply_private_account": { + "npk": [32,67,72,164,106,53,66,239,141,15,52,230,136,177,2,236,207,243,134,135,210,143,87,232,215,128,194,120,113,224,4,165], + "balance": 20000 } } ], diff --git a/integration_tests/Cargo.toml b/integration_tests/Cargo.toml index cb5277d2..8e070784 100644 --- a/integration_tests/Cargo.toml +++ b/integration_tests/Cargo.toml @@ -22,10 +22,8 @@ ata_core.workspace = true indexer_service_rpc.workspace = true sequencer_service_rpc = { workspace = true, features = ["client"] } wallet-ffi.workspace = true -testnet_initial_state.workspace = true url.workspace = true - anyhow.workspace = true env_logger.workspace = true log.workspace = true diff --git a/integration_tests/src/config.rs b/integration_tests/src/config.rs index ec58b17f..0e531d85 100644 --- a/integration_tests/src/config.rs +++ b/integration_tests/src/config.rs @@ -6,8 +6,7 @@ use indexer_service::{BackoffConfig, ChannelId, ClientConfig, IndexerConfig}; use key_protocol::key_management::KeyChain; use nssa::{Account, AccountId, PrivateKey, PublicKey}; use nssa_core::{account::Data, program::DEFAULT_PROGRAM_ID}; -use sequencer_core::config::{BedrockConfig, SequencerConfig}; -use testnet_initial_state::{PrivateAccountPublicInitialData, PublicAccountPublicInitialData}; +use sequencer_core::config::{BedrockConfig, GenesisTransaction, SequencerConfig}; use url::Url; use wallet::config::WalletConfig; @@ -100,27 +99,24 @@ impl InitialData { } } - fn sequencer_initial_public_accounts(&self) -> Vec { + fn sequencer_genesis(&self) -> Vec { self.public_accounts .iter() .map(|(priv_key, balance)| { let pub_key = PublicKey::new_from_private_key(priv_key); let account_id = AccountId::from(&pub_key); - PublicAccountPublicInitialData { + GenesisTransaction::SupplyPublicAccount { account_id, balance: *balance, } }) - .collect() - } - - fn sequencer_initial_private_accounts(&self) -> Vec { - self.private_accounts - .iter() - .map(|(key_chain, account)| PrivateAccountPublicInitialData { - npk: key_chain.nullifier_public_key.clone(), - account: account.clone(), - }) + .chain(self.private_accounts.iter().map(|(key_chain, account)| { + GenesisTransaction::SupplyPrivateAccount { + npk: key_chain.nullifier_public_key.clone(), + vpk: key_chain.viewing_public_key.clone(), + balance: account.balance, + } + })) .collect() } } @@ -180,8 +176,7 @@ pub fn sequencer_config( mempool_max_size, block_create_timeout, retry_pending_blocks_timeout: Duration::from_secs(5), - initial_public_accounts: Some(initial_data.sequencer_initial_public_accounts()), - initial_private_accounts: Some(initial_data.sequencer_initial_private_accounts()), + genesis: initial_data.sequencer_genesis(), signing_key: [37; 32], bedrock_config: BedrockConfig { backoff: BackoffConfig { diff --git a/integration_tests/tests/keys.rs b/integration_tests/tests/keys.rs index d81512aa..71ac70a2 100644 --- a/integration_tests/tests/keys.rs +++ b/integration_tests/tests/keys.rs @@ -13,7 +13,8 @@ use integration_tests::{ }; use key_protocol::key_management::{KeyChain, key_tree::chain_index::ChainIndex}; use log::info; -use nssa::{AccountId, program::Program}; +use nssa::{AccountId, Data, program::Program}; +use nssa_core::account::Nonce; use sequencer_service_rpc::RpcClient as _; use tokio::test; use wallet::{ @@ -349,8 +350,8 @@ async fn import_private_account() -> Result<()> { let account = nssa::Account { program_owner: Program::authenticated_transfer_program().id(), balance: 777, - data: Default::default(), - nonce: Default::default(), + data: Data::default(), + nonce: Nonce::default(), }; let key_chain_json = serde_json::to_string(&key_chain) @@ -394,8 +395,8 @@ async fn import_private_account_second_time_overrides_account_data() -> Result<( let initial_account = nssa::Account { program_owner: Program::authenticated_transfer_program().id(), balance: 100, - data: Default::default(), - nonce: Default::default(), + data: Data::default(), + nonce: Nonce::default(), }; // First import @@ -411,8 +412,8 @@ async fn import_private_account_second_time_overrides_account_data() -> Result<( let updated_account = nssa::Account { program_owner: Program::authenticated_transfer_program().id(), balance: 999, - data: Default::default(), - nonce: Default::default(), + data: Data::default(), + nonce: Nonce::default(), }; // Second import with different account data (same key chain) diff --git a/key_protocol/Cargo.toml b/key_protocol/Cargo.toml index 392b4cd7..f62f4c50 100644 --- a/key_protocol/Cargo.toml +++ b/key_protocol/Cargo.toml @@ -18,7 +18,6 @@ common.workspace = true anyhow.workspace = true serde.workspace = true -log.workspace = true k256.workspace = true sha2.workspace = true rand.workspace = true diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index bf1ef74f..8e93303b 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -517,10 +517,13 @@ pub fn read_nssa_inputs() -> (ProgramInput, InstructionD /// - `pre_states`: The list of input accounts, each annotated with authorization metadata. /// - `post_states`: The list of resulting accounts after executing the program logic. /// - `executing_program_id`: The identifier of the program that was executed. +/// - `is_genesis`: When `true`, skips the total balance conservation check (rule 8), allowing +/// balance minting. Must only be `true` for genesis supply transactions. pub fn validate_execution( pre_states: &[AccountWithMetadata], post_states: &[AccountPostState], executing_program_id: ProgramId, + is_genesis: bool, ) -> Result<(), ExecutionValidationError> { // 1. Check account ids are all different if !validate_uniqueness_of_account_ids(pre_states) { @@ -588,25 +591,26 @@ pub fn validate_execution( } } - // 8. Total balance is preserved + // 8. Total balance is preserved (skipped for genesis supply transactions) + if !is_genesis { + let Some(total_balance_pre_states) = + WrappedBalanceSum::from_balances(pre_states.iter().map(|pre| pre.account.balance)) + else { + return Err(ExecutionValidationError::BalanceSumOverflow); + }; - let Some(total_balance_pre_states) = - WrappedBalanceSum::from_balances(pre_states.iter().map(|pre| pre.account.balance)) - else { - return Err(ExecutionValidationError::BalanceSumOverflow); - }; + let Some(total_balance_post_states) = + WrappedBalanceSum::from_balances(post_states.iter().map(|post| post.account.balance)) + else { + return Err(ExecutionValidationError::BalanceSumOverflow); + }; - let Some(total_balance_post_states) = - WrappedBalanceSum::from_balances(post_states.iter().map(|post| post.account.balance)) - else { - return Err(ExecutionValidationError::BalanceSumOverflow); - }; - - if total_balance_pre_states != total_balance_post_states { - return Err(ExecutionValidationError::MismatchedTotalBalance { - total_balance_pre_states, - total_balance_post_states, - }); + if total_balance_pre_states != total_balance_post_states { + return Err(ExecutionValidationError::MismatchedTotalBalance { + total_balance_pre_states, + total_balance_post_states, + }); + } } Ok(()) diff --git a/nssa/src/program.rs b/nssa/src/program.rs index 698032e2..bb779cd7 100644 --- a/nssa/src/program.rs +++ b/nssa/src/program.rs @@ -10,8 +10,9 @@ use crate::{ error::NssaError, 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, + AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID, CLOCK_ELF, CLOCK_ID, + GENESIS_SUPPLY_ACCOUNT_ELF, GENESIS_SUPPLY_ACCOUNT_ID, PINATA_ELF, PINATA_ID, TOKEN_ELF, + TOKEN_ID, }, }; @@ -148,6 +149,14 @@ impl Program { elf: ASSOCIATED_TOKEN_ACCOUNT_ELF.to_vec(), } } + + #[must_use] + pub fn genesis_supply_account() -> Self { + Self { + id: GENESIS_SUPPLY_ACCOUNT_ID, + elf: GENESIS_SUPPLY_ACCOUNT_ELF.to_vec(), + } + } } // TODO: Testnet only. Refactor to prevent compilation on mainnet. diff --git a/nssa/src/public_transaction/transaction.rs b/nssa/src/public_transaction/transaction.rs index 8ab79535..ec3d3c93 100644 --- a/nssa/src/public_transaction/transaction.rs +++ b/nssa/src/public_transaction/transaction.rs @@ -177,7 +177,7 @@ pub mod tests { let witness_set = WitnessSet::for_message(&message, &[&key1, &key1]); let tx = PublicTransaction::new(message, witness_set); - let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0); + let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0, false); assert!(matches!(result, Err(NssaError::InvalidInput(_)))); } @@ -197,7 +197,7 @@ pub mod tests { let witness_set = WitnessSet::for_message(&message, &[&key1, &key2]); let tx = PublicTransaction::new(message, witness_set); - let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0); + let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0, false); assert!(matches!(result, Err(NssaError::InvalidInput(_)))); } @@ -218,7 +218,7 @@ pub mod tests { let mut witness_set = WitnessSet::for_message(&message, &[&key1, &key2]); witness_set.signatures_and_public_keys[0].0 = Signature::new_for_tests([1; 64]); let tx = PublicTransaction::new(message, witness_set); - let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0); + let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0, false); assert!(matches!(result, Err(NssaError::InvalidInput(_)))); } @@ -238,7 +238,7 @@ pub mod tests { let witness_set = WitnessSet::for_message(&message, &[&key1, &key2]); let tx = PublicTransaction::new(message, witness_set); - let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0); + let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0, false); assert!(matches!(result, Err(NssaError::InvalidInput(_)))); } @@ -254,7 +254,7 @@ pub mod tests { let witness_set = WitnessSet::for_message(&message, &[&key1, &key2]); let tx = PublicTransaction::new(message, witness_set); - let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0); + let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0, false); assert!(matches!(result, Err(NssaError::InvalidInput(_)))); } } diff --git a/nssa/src/state.rs b/nssa/src/state.rs index 5aa54d1c..f9ea05a0 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -176,6 +176,8 @@ impl V03State { this.insert_program(Program::clock()); this.insert_clock_accounts(genesis_timestamp); + this.insert_program(Program::genesis_supply_account()); + this.insert_program(Program::authenticated_transfer_program()); this.insert_program(Program::token()); this.insert_program(Program::amm()); @@ -243,7 +245,8 @@ impl V03State { block_id: BlockId, timestamp: Timestamp, ) -> Result<(), NssaError> { - let diff = ValidatedStateDiff::from_public_transaction(tx, self, block_id, timestamp)?; + let diff = + ValidatedStateDiff::from_public_transaction(tx, self, block_id, timestamp, false)?; self.apply_state_diff(diff); Ok(()) } diff --git a/nssa/src/validated_state_diff.rs b/nssa/src/validated_state_diff.rs index b4c62a3e..d4273a07 100644 --- a/nssa/src/validated_state_diff.rs +++ b/nssa/src/validated_state_diff.rs @@ -44,6 +44,7 @@ impl ValidatedStateDiff { state: &V03State, block_id: BlockId, timestamp: Timestamp, + is_genesis: bool, ) -> Result { let message = tx.message(); let witness_set = tx.witness_set(); @@ -189,6 +190,7 @@ impl ValidatedStateDiff { &program_output.pre_states, &program_output.post_states, chained_call.program_id, + is_genesis, ) .map_err(InvalidProgramBehaviorError::ExecutionValidationFailed)?; @@ -215,9 +217,10 @@ impl ValidatedStateDiff { match claim { Claim::Authorized => { - // The program can only claim accounts that were authorized by the signer. + // The program can only claim accounts that were authorized by the signer if + // it's not genesis. ensure!( - is_authorized(&account_id), + is_authorized(&account_id) || is_genesis, InvalidProgramBehaviorError::ClaimedUnauthorizedAccount { account_id } ); } diff --git a/program_methods/guest/Cargo.toml b/program_methods/guest/Cargo.toml index dc2077b7..6f6c61bd 100644 --- a/program_methods/guest/Cargo.toml +++ b/program_methods/guest/Cargo.toml @@ -16,5 +16,6 @@ amm_core.workspace = true amm_program.workspace = true ata_core.workspace = true ata_program.workspace = true +genesis_supply_account_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 32b69c3a..669462b7 100644 --- a/program_methods/guest/src/bin/authenticated_transfer.rs +++ b/program_methods/guest/src/bin/authenticated_transfer.rs @@ -8,7 +8,6 @@ use nssa_core::{ /// Initializes a default account under the ownership of this program. fn initialize_account(pre_state: AccountWithMetadata) -> AccountPostState { let account_to_claim = AccountPostState::new_claimed(pre_state.account, Claim::Authorized); - let is_authorized = pre_state.is_authorized; // Continue only if the account to claim has default values assert!( @@ -16,9 +15,6 @@ fn initialize_account(pre_state: AccountWithMetadata) -> AccountPostState { "Account must be uninitialized" ); - // Continue only if the owner authorized this operation - assert!(is_authorized, "Account must be authorized"); - account_to_claim } diff --git a/program_methods/guest/src/bin/genesis_supply_account.rs b/program_methods/guest/src/bin/genesis_supply_account.rs new file mode 100644 index 00000000..05bd202c --- /dev/null +++ b/program_methods/guest/src/bin/genesis_supply_account.rs @@ -0,0 +1,129 @@ +//! Genesis Supply Account Program. +//! +//! A genesis-only program that supplies initial balance to an account. +//! Uses the "initiate → callback" chained-call pattern to: +//! 1. Initialize the target account via `authenticated_transfer` +//! 2. Add the supplied balance in a self-callback +//! +//! This program verifies that the clock's `block_id` is 0, ensuring it can only +//! execute at genesis. The balance increase violates normal balance conservation, +//! so it requires `is_genesis: true` in the validation pipeline. +//! +//! # Accounts +//! +//! - `Initiate`: `[target_account, clock_account]` +//! - `Callback`: `[target_account, clock_account]` + +use clock_core::{CLOCK_01_PROGRAM_ACCOUNT_ID, ClockAccountData}; +use genesis_supply_account_core::Instruction; +use nssa_core::program::{ + AccountPostState, ChainedCall, ProgramInput, ProgramOutput, read_nssa_inputs, +}; + +fn main() { + let ( + ProgramInput { + self_program_id, + caller_program_id, + pre_states, + instruction, + }, + instruction_words, + ) = read_nssa_inputs::(); + + match instruction { + Instruction::Initiate { + balance, + authenticated_transfer_id, + } => { + let Ok([target_pre, clock_pre]) = <[_; 2]>::try_from(pre_states) else { + panic!("Initiate requires exactly 2 accounts: target, clock"); + }; + + assert_eq!( + clock_pre.account_id, CLOCK_01_PROGRAM_ACCOUNT_ID, + "Second account must be the clock account" + ); + + let clock_data = + ClockAccountData::from_bytes(&clock_pre.account.data.clone().into_inner()); + assert_eq!( + clock_data.block_id, 0, + "Genesis supply can only execute at genesis (block_id must be 0)" + ); + + // Compute the expected post-state of the target after authenticated_transfer + // initializes it. authenticated_transfer claims the account (sets + // program_owner = auth_transfer_id), leaving balance at 0. + let mut target_after_init = target_pre.clone(); + target_after_init.account.program_owner = authenticated_transfer_id; + + // Chained call 1: authenticated_transfer(0) — initializes (claims) the target account. + let init_instruction = + risc0_zkvm::serde::to_vec(&0_u128).expect("init instruction serialization"); + let call_1 = ChainedCall { + program_id: authenticated_transfer_id, + pre_states: vec![target_pre.clone()], + instruction_data: init_instruction, + pda_seeds: vec![], + }; + + // Chained call 2: self-callback to add the supplied balance. + let callback_instruction = + risc0_zkvm::serde::to_vec(&Instruction::Callback { balance }) + .expect("callback instruction serialization"); + let call_2 = ChainedCall { + program_id: self_program_id, + pre_states: vec![target_after_init, clock_pre.clone()], + instruction_data: callback_instruction, + pda_seeds: vec![], + }; + + ProgramOutput::new( + self_program_id, + caller_program_id, + instruction_words, + vec![target_pre.clone(), clock_pre.clone()], + // post_states match pre_states — mutations happen in chained calls + vec![ + AccountPostState::new(target_pre.account), + AccountPostState::new(clock_pre.account), + ], + ) + .with_chained_calls(vec![call_1, call_2]) + .write(); + } + + Instruction::Callback { balance } => { + // Access control: must be called from this program itself. + assert_eq!( + caller_program_id, + Some(self_program_id), + "Callback can only be invoked as a chained call from genesis_supply_account" + ); + + let Ok([target_pre, clock_pre]) = <[_; 2]>::try_from(pre_states) else { + panic!("Callback requires exactly 2 accounts: target, clock"); + }; + + // Add the supplied balance to the target account. + let mut target_post_account = target_pre.account.clone(); + target_post_account.balance = target_post_account + .balance + .checked_add(balance) + .expect("target balance overflow"); + + let target_post = AccountPostState::new(target_post_account); + let clock_post = AccountPostState::new(clock_pre.account.clone()); + + ProgramOutput::new( + self_program_id, + caller_program_id, + instruction_words, + vec![target_pre, clock_pre], + vec![target_post, clock_post], + ) + .write(); + } + } +} diff --git a/program_methods/guest/src/bin/privacy_preserving_circuit.rs b/program_methods/guest/src/bin/privacy_preserving_circuit.rs index 16ba91ff..1b844b8c 100644 --- a/program_methods/guest/src/bin/privacy_preserving_circuit.rs +++ b/program_methods/guest/src/bin/privacy_preserving_circuit.rs @@ -3,6 +3,7 @@ use std::{ convert::Infallible, }; +use clock_core::{CLOCK_PROGRAM_ACCOUNT_IDS, ClockAccountData}; use nssa_core::{ Commitment, CommitmentSetDigest, DUMMY_COMMITMENT_HASH, EncryptionScheme, MembershipProof, Nullifier, NullifierPublicKey, NullifierSecretKey, PrivacyPreservingCircuitInput, @@ -125,10 +126,22 @@ impl ExecutionState { // Check that the program is well behaved. // See the # Programs section for the definition of the `validate_execution` method. + // + // TODO: This looks hacky as hell. + // Derive is_genesis: true if any pre_state is a clock account at block 0. + // This allows the genesis supply account program to mint balance at genesis. + // Security: program execution is verified via env::verify(), and the genesis supply + // program itself panics if clock.block_id != 0, so is_genesis cannot be forged. + let is_genesis = program_output.pre_states.iter().any(|pre| { + CLOCK_PROGRAM_ACCOUNT_IDS.contains(&pre.account_id) + && ClockAccountData::from_bytes(&pre.account.data.clone().into_inner()).block_id + == 0 + }); let validated_execution = validate_execution( &program_output.pre_states, &program_output.post_states, chained_call.program_id, + is_genesis, ); if let Err(err) = validated_execution { panic!( @@ -151,6 +164,7 @@ impl ExecutionState { &authorized_pdas, program_output.pre_states, program_output.post_states, + is_genesis, ); chain_calls_counter = chain_calls_counter.checked_add(1).expect( "Chain calls counter should not overflow as it checked before incrementing", @@ -194,6 +208,7 @@ impl ExecutionState { authorized_pdas: &HashSet, pre_states: Vec, post_states: Vec, + is_genesis: bool, ) { for (pre, mut post) in pre_states.into_iter().zip(post_states) { let pre_account_id = pre.account_id; @@ -264,7 +279,7 @@ impl ExecutionState { // Note: no need to check authorized pdas because we have already // checked consistency of authorization above. assert!( - pre_is_authorized, + pre_is_authorized || is_genesis, "Cannot claim unauthorized account {pre_account_id}" ); } diff --git a/programs/genesis_supply_account/core/Cargo.toml b/programs/genesis_supply_account/core/Cargo.toml new file mode 100644 index 00000000..442778e2 --- /dev/null +++ b/programs/genesis_supply_account/core/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "genesis_supply_account_core" +version = "0.1.0" +edition = "2024" +license = { workspace = true } + +[lints] +workspace = true + +[dependencies] +nssa_core.workspace = true +serde.workspace = true diff --git a/programs/genesis_supply_account/core/src/lib.rs b/programs/genesis_supply_account/core/src/lib.rs new file mode 100644 index 00000000..2c8aede5 --- /dev/null +++ b/programs/genesis_supply_account/core/src/lib.rs @@ -0,0 +1,25 @@ +//! Core data structures for the Genesis Supply Account Program. + +use nssa_core::program::ProgramId; +use serde::{Deserialize, Serialize}; + +/// Instruction type for the Genesis Supply Account program. +#[derive(Serialize, Deserialize)] +pub enum Instruction { + /// External entrypoint: initialize the target account then supply it with `balance`. + /// + /// Required accounts: `[target_account, clock_account]`. + /// + /// Emits 2 chained calls: + /// 1. `authenticated_transfer(0)` on `[target_account]` — claims the account + /// 2. `Callback { balance }` on `[target_after_init, clock_account]` — adds balance + Initiate { + balance: u128, + authenticated_transfer_id: ProgramId, + }, + /// Internal: add `balance` to the already-initialized target account. + /// + /// Access control: only callable as a chained call from this program itself + /// (enforced via `caller_program_id == Some(self_program_id)`). + Callback { balance: u128 }, +} diff --git a/sequencer/core/Cargo.toml b/sequencer/core/Cargo.toml index efd0e359..4fcb3420 100644 --- a/sequencer/core/Cargo.toml +++ b/sequencer/core/Cargo.toml @@ -15,6 +15,8 @@ storage.workspace = true mempool.workspace = true bedrock_client.workspace = true testnet_initial_state.workspace = true +genesis_supply_account_core.workspace = true +key_protocol.workspace = true anyhow.workspace = true serde.workspace = true diff --git a/sequencer/core/src/block_store.rs b/sequencer/core/src/block_store.rs index 7e47005d..1ef0b04c 100644 --- a/sequencer/core/src/block_store.rs +++ b/sequencer/core/src/block_store.rs @@ -6,6 +6,7 @@ use common::{ block::{Block, BlockMeta, MantleMsgId}, transaction::NSSATransaction, }; +use log::info; use nssa::V03State; use storage::{error::DbError, sequencer::RocksDBIO}; @@ -18,21 +19,48 @@ pub struct SequencerStore { } impl SequencerStore { + /// Open existing database at the given location. Fails if no database is found. + pub fn open_db(location: &Path, signing_key: nssa::PrivateKey) -> Result { + let dbio = RocksDBIO::open(location)?; + let genesis_id = dbio.get_meta_first_block_in_db()?; + let last_id = dbio.latest_block_meta()?.id; + + info!("Preparing block cache"); + let mut tx_hash_to_block_map = HashMap::new(); + for i in genesis_id..=last_id { + let block = dbio + .get_block(i)? + .expect("Block should be present in the database"); + + tx_hash_to_block_map.extend(block_to_transactions_map(&block)); + } + info!( + "Block cache prepared. Total blocks in cache: {}", + tx_hash_to_block_map.len() + ); + + Ok(Self { + dbio, + tx_hash_to_block_map, + genesis_id, + signing_key, + }) + } + /// Starting database at the start of new chain. /// Creates files if necessary. /// /// ATTENTION: Will overwrite genesis block. - pub fn open_db_with_genesis( + pub fn create_db_with_genesis( location: &Path, genesis_block: &Block, genesis_msg_id: MantleMsgId, + genesis_state: &V03State, signing_key: nssa::PrivateKey, ) -> Result { - let tx_hash_to_block_map = block_to_transactions_map(genesis_block); - - let dbio = RocksDBIO::open_or_create(location, genesis_block, genesis_msg_id)?; - + let dbio = RocksDBIO::create(location, genesis_block, genesis_msg_id, genesis_state)?; let genesis_id = dbio.get_meta_first_block_in_db()?; + let tx_hash_to_block_map = block_to_transactions_map(genesis_block); Ok(Self { dbio, @@ -100,8 +128,8 @@ impl SequencerStore { Ok(()) } - pub fn get_nssa_state(&self) -> Option { - self.dbio.get_nssa_state().ok() + pub fn get_nssa_state(&self) -> Result { + self.dbio.get_nssa_state().map_err(Into::into) } } @@ -139,9 +167,14 @@ mod tests { let genesis_block = genesis_block_hashable_data.into_pending_block(&signing_key, [0; 32]); // Start an empty node store - let mut node_store = - SequencerStore::open_db_with_genesis(path, &genesis_block, [0; 32], signing_key) - .unwrap(); + let mut node_store = SequencerStore::create_db_with_genesis( + path, + &genesis_block, + [0; 32], + &testnet_initial_state::initial_state(), + signing_key, + ) + .unwrap(); let tx = common::test_utils::produce_dummy_empty_transaction(); let block = common::test_utils::produce_dummy_block(1, None, vec![tx.clone()]); @@ -174,9 +207,14 @@ mod tests { let genesis_block = genesis_block_hashable_data.into_pending_block(&signing_key, [0; 32]); let genesis_hash = genesis_block.header.hash; - let node_store = - SequencerStore::open_db_with_genesis(path, &genesis_block, [0; 32], signing_key) - .unwrap(); + let node_store = SequencerStore::create_db_with_genesis( + path, + &genesis_block, + [0; 32], + &testnet_initial_state::initial_state(), + signing_key, + ) + .unwrap(); // Verify that initially the latest block hash equals genesis hash let latest_meta = node_store.latest_block_meta().unwrap(); @@ -199,9 +237,14 @@ mod tests { }; let genesis_block = genesis_block_hashable_data.into_pending_block(&signing_key, [0; 32]); - let mut node_store = - SequencerStore::open_db_with_genesis(path, &genesis_block, [0; 32], signing_key) - .unwrap(); + let mut node_store = SequencerStore::create_db_with_genesis( + path, + &genesis_block, + [0; 32], + &testnet_initial_state::initial_state(), + signing_key, + ) + .unwrap(); // Add a new block let tx = common::test_utils::produce_dummy_empty_transaction(); @@ -235,9 +278,14 @@ mod tests { }; let genesis_block = genesis_block_hashable_data.into_pending_block(&signing_key, [0; 32]); - let mut node_store = - SequencerStore::open_db_with_genesis(path, &genesis_block, [0; 32], signing_key) - .unwrap(); + let mut node_store = SequencerStore::create_db_with_genesis( + path, + &genesis_block, + [0; 32], + &testnet_initial_state::initial_state(), + signing_key, + ) + .unwrap(); // Add a new block with Pending status let tx = common::test_utils::produce_dummy_empty_transaction(); @@ -264,4 +312,49 @@ mod tests { common::block::BedrockStatus::Finalized )); } + + #[test] + fn open_existing_db_caches_transactions() { + let temp_dir = tempdir().unwrap(); + let path = temp_dir.path(); + + let signing_key = sequencer_sign_key_for_testing(); + + let genesis_block_hashable_data = HashableBlockData { + block_id: 0, + prev_block_hash: HashType([0; 32]), + timestamp: 0, + transactions: vec![], + }; + + let genesis_block = genesis_block_hashable_data.into_pending_block(&signing_key, [0; 32]); + let tx = common::test_utils::produce_dummy_empty_transaction(); + { + // Create a scope to drop the first store after creating the db + let mut node_store = SequencerStore::create_db_with_genesis( + path, + &genesis_block, + [0; 32], + &testnet_initial_state::initial_state(), + signing_key.clone(), + ) + .unwrap(); + + // Add a new block + let block = common::test_utils::produce_dummy_block(1, None, vec![tx.clone()]); + node_store + .update( + &block, + [1; 32], + &V03State::new_with_genesis_accounts(&[], vec![], 0), + ) + .unwrap(); + } + + // Re-open the store and verify that the transaction is still retrievable (which means it + // was cached correctly) + let node_store = SequencerStore::open_db(path, signing_key).unwrap(); + let retrieved_tx = node_store.get_transaction_by_hash(tx.hash()); + assert_eq!(Some(tx), retrieved_tx); + } } diff --git a/sequencer/core/src/config.rs b/sequencer/core/src/config.rs index fa4a2fa7..6f950006 100644 --- a/sequencer/core/src/config.rs +++ b/sequencer/core/src/config.rs @@ -11,10 +11,26 @@ use bytesize::ByteSize; use common::config::BasicAuth; use humantime_serde; use logos_blockchain_core::mantle::ops::channel::ChannelId; +use nssa::AccountId; +use nssa_core::{NullifierPublicKey, encryption::ViewingPublicKey}; use serde::{Deserialize, Serialize}; -use testnet_initial_state::{PrivateAccountPublicInitialData, PublicAccountPublicInitialData}; 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 { + account_id: AccountId, + balance: u128, + }, + SupplyPrivateAccount { + npk: NullifierPublicKey, + vpk: ViewingPublicKey, + balance: u128, + }, +} + // TODO: Provide default values #[derive(Clone, Serialize, Deserialize)] pub struct SequencerConfig { @@ -44,10 +60,9 @@ pub struct SequencerConfig { pub bedrock_config: BedrockConfig, /// Indexer RPC URL. pub indexer_rpc_url: Url, - #[serde(skip_serializing_if = "Option::is_none")] - pub initial_public_accounts: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub initial_private_accounts: Option>, + /// Genesis configuration. + #[serde(default)] + pub genesis: Vec, } #[derive(Clone, Serialize, Deserialize)] diff --git a/sequencer/core/src/lib.rs b/sequencer/core/src/lib.rs index cbf8e910..4e8c33b0 100644 --- a/sequencer/core/src/lib.rs +++ b/sequencer/core/src/lib.rs @@ -1,23 +1,25 @@ -use std::{path::Path, time::Instant}; +use std::{collections::HashMap, path::Path, time::Instant}; use anyhow::{Context as _, Result, anyhow}; use bedrock_client::SignedMantleTx; -#[cfg(feature = "testnet")] -use common::PINATA_BASE58; use common::{ HashType, block::{BedrockStatus, Block, HashableBlockData}, transaction::{NSSATransaction, clock_invocation}, }; -use config::SequencerConfig; +use config::{GenesisTransaction, 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::V03State; +use nssa::{ + Account, AccountId, PrivacyPreservingTransaction, PublicTransaction, ValidatedStateDiff, + privacy_preserving_transaction::circuit::ProgramWithDependencies, program::Program, + public_transaction::Message, +}; +use nssa_core::account::AccountWithMetadata; pub use storage::error::DbError; -use testnet_initial_state::initial_state; use crate::{ block_settlement_client::{BlockSettlementClient, BlockSettlementClientTrait, MsgId}, @@ -55,16 +57,8 @@ impl SequencerCore (Self, MemPoolHandle) { - let hashable_data = HashableBlockData { - block_id: config.genesis_id, - transactions: vec![], - prev_block_hash: HashType([0; 32]), - timestamp: 0, - }; - let signing_key = nssa::PrivateKey::try_new(config.signing_key).unwrap(); - let genesis_parent_msg_id = [0; 32]; - let genesis_block = hashable_data.into_pending_block(&signing_key, genesis_parent_msg_id); + let db_path = config.home.join("rocksdb"); let bedrock_signing_key = load_or_create_signing_key(&config.home.join("bedrock_signing_key")) @@ -77,79 +71,52 @@ impl SequencerCore, - > = config.initial_private_accounts.clone().map(|accounts| { - accounts - .iter() - .map(|init_comm_data| { - let npk = &init_comm_data.npk; - - let mut acc = init_comm_data.account.clone(); - - acc.program_owner = - nssa::program::Program::authenticated_transfer_program().id(); - - ( - nssa_core::Commitment::new(npk, &acc), - nssa_core::Nullifier::for_account_initialization(npk), - ) - }) - .collect() - }); - - let init_accs: Option> = config - .initial_public_accounts - .clone() - .map(|initial_accounts| { - initial_accounts - .iter() - .map(|acc_data| (acc_data.account_id, acc_data.balance)) - .collect() - }); - - // If initial commitments or accounts are present in config, need to construct state - // from them - if initial_private_accounts.is_some() || init_accs.is_some() { - V03State::new_with_genesis_accounts( - &init_accs.unwrap_or_default(), - initial_private_accounts.unwrap_or_default(), - genesis_block.header.timestamp, - ) - } else { - initial_state() - } - }; - - #[cfg(feature = "testnet")] - state.add_pinata_program(PINATA_BASE58.parse().unwrap()); - let (mempool, mempool_handle) = MemPool::new(config.mempool_max_size); let sequencer_core = Self { @@ -362,6 +329,134 @@ impl SequencerCore (nssa::V03State, Vec) { + #[cfg(not(feature = "testnet"))] + let mut state = testnet_initial_state::initial_state(); + + #[cfg(feature = "testnet")] + let mut state = testnet_initial_state::initial_state_testnet(); + + let genesis_txs = config + .genesis + .iter() + .map(|genesis_tx| { + let (tx, diff) = match genesis_tx { + GenesisTransaction::SupplyPublicAccount { + account_id, + balance, + } => build_public_genesis_transaction(config, &state, account_id, *balance), + GenesisTransaction::SupplyPrivateAccount { npk, vpk, balance } => { + build_private_genesis_transaction( + config, + &state, + npk.clone(), + vpk.clone(), + *balance, + ) + } + }; + state.apply_state_diff(diff); + tx + }) + .collect(); + + (state, genesis_txs) +} + +fn build_public_genesis_transaction( + config: &SequencerConfig, + state: &nssa::V03State, + account_id: &AccountId, + balance: u128, +) -> (NSSATransaction, ValidatedStateDiff) { + let authenticated_transfer_id = Program::authenticated_transfer_program().id(); + let genesis_supply_id = nssa::program::Program::genesis_supply_account().id(); + + let message = Message::try_new( + genesis_supply_id, + vec![*account_id, nssa::CLOCK_01_PROGRAM_ACCOUNT_ID], + vec![], + genesis_supply_account_core::Instruction::Initiate { + balance, + authenticated_transfer_id, + }, + ) + .expect("Failed to serialize genesis supply instruction"); + let witness_set = nssa::public_transaction::WitnessSet::for_message(&message, &[]); + + let tx = PublicTransaction::new(message, witness_set); + let diff = ValidatedStateDiff::from_public_transaction(&tx, state, config.genesis_id, 0, true) + .expect("Failed to execute genesis supply public transaction"); + + (tx.into(), diff) +} + +fn build_private_genesis_transaction( + config: &SequencerConfig, + state: &nssa::V03State, + npk: nssa_core::NullifierPublicKey, + vpk: nssa_core::encryption::ViewingPublicKey, + balance: u128, +) -> (NSSATransaction, ValidatedStateDiff) { + let authenticated_transfer = Program::authenticated_transfer_program(); + let account_id = AccountId::from(&npk); + let clock_account = state.get_account_by_id(nssa::CLOCK_01_PROGRAM_ACCOUNT_ID); + let pre_states = vec![ + AccountWithMetadata::new(Account::default(), false, account_id), + AccountWithMetadata::new(clock_account, false, nssa::CLOCK_01_PROGRAM_ACCOUNT_ID), + ]; + let instruction = nssa::program::Program::serialize_instruction( + genesis_supply_account_core::Instruction::Initiate { + balance, + authenticated_transfer_id: authenticated_transfer.id(), + }, + ) + .expect("Failed to serialize genesis supply private account instruction"); + + let eph_holder = + key_protocol::key_management::ephemeral_key_holder::EphemeralKeyHolder::new(&npk); + let ssk = eph_holder.calculate_shared_secret_sender(&vpk); + let epk = eph_holder.generate_ephemeral_public_key(); + + let genesis_supply_account = Program::genesis_supply_account(); + let (output, proof) = nssa::privacy_preserving_transaction::circuit::execute_and_prove( + pre_states, + instruction, + vec![2, 0], + vec![(npk.clone(), ssk)], + vec![], + vec![None], + &ProgramWithDependencies::new( + genesis_supply_account.clone(), + HashMap::from([ + (authenticated_transfer.id(), authenticated_transfer), + (genesis_supply_account.id(), genesis_supply_account), + ]), + ), + ) + .expect("Failed to execute and prove genesis supply private account transaction"); + + let message = nssa::privacy_preserving_transaction::Message::try_from_circuit_output( + vec![nssa::CLOCK_01_PROGRAM_ACCOUNT_ID], + vec![], + vec![(npk, vpk, epk)], + output, + ) + .expect("Failed to serialize genesis supply private account instruction"); + + let witness_set = + nssa::privacy_preserving_transaction::WitnessSet::for_message(&message, proof, &[]); + + let tx = PrivacyPreservingTransaction::new(message, witness_set); + let diff = + ValidatedStateDiff::from_privacy_preserving_transaction(&tx, state, config.genesis_id, 0) + .expect("Failed to validate genesis supply private account transaction"); + (tx.into(), diff) +} + /// Load signing key from file or generate a new one if it doesn't exist. fn load_or_create_signing_key(path: &Path) -> Result { if path.exists() { @@ -401,7 +496,7 @@ mod tests { use testnet_initial_state::{initial_accounts, initial_pub_accounts_private_keys}; use crate::{ - config::{BedrockConfig, SequencerConfig}, + config::{BedrockConfig, GenesisTransaction, SequencerConfig}, mock::SequencerCoreWithMockClients, }; @@ -429,8 +524,7 @@ mod tests { }, retry_pending_blocks_timeout: Duration::from_mins(4), indexer_rpc_url: "ws://localhost:8779".parse().unwrap(), - initial_public_accounts: None, - initial_private_accounts: None, + genesis: vec![], } } @@ -1079,24 +1173,21 @@ mod tests { account::AccountWithMetadata, encryption::{EphemeralPublicKey, EphemeralSecretKey, ViewingPublicKey}, }; - use testnet_initial_state::PrivateAccountPublicInitialData; let nsk: nssa_core::NullifierSecretKey = [7; 32]; let npk = nssa_core::NullifierPublicKey::from(&nsk); let vsk: EphemeralSecretKey = [8; 32]; let vpk = ViewingPublicKey::from_scalar(vsk); - let genesis_account = Account { - program_owner: Program::authenticated_transfer_program().id(), - ..Account::default() - }; - // Start a sequencer from config with a preconfigured private genesis account let mut config = setup_sequencer_config(); - config.initial_private_accounts = Some(vec![PrivateAccountPublicInitialData { - npk: npk.clone(), - account: genesis_account, - }]); + config + .genesis + .push(GenesisTransaction::SupplyPrivateAccount { + npk: npk.clone(), + vpk: vpk.clone(), + balance: 10, + }); let (mut sequencer, _mempool_handle) = SequencerCoreWithMockClients::start_from_config(config).await; diff --git a/storage/src/sequencer/mod.rs b/storage/src/sequencer/mod.rs index 508f6c29..d6b72fda 100644 --- a/storage/src/sequencer/mod.rs +++ b/storage/src/sequencer/mod.rs @@ -40,36 +40,26 @@ impl DBIO for RocksDBIO { } impl RocksDBIO { - pub fn open_or_create( + pub fn open(path: &Path) -> DbResult { + let db_opts = Options::default(); + Self::open_inner(path, &db_opts) + } + + pub fn create( path: &Path, genesis_block: &Block, genesis_msg_id: MantleMsgId, + genesis_state: &V03State, ) -> DbResult { - let mut cf_opts = Options::default(); - cf_opts.set_max_write_buffer_number(16); - // ToDo: Add more column families for different data - let cfb = ColumnFamilyDescriptor::new(CF_BLOCK_NAME, cf_opts.clone()); - let cfmeta = ColumnFamilyDescriptor::new(CF_META_NAME, cf_opts.clone()); - let cfstate = ColumnFamilyDescriptor::new(CF_NSSA_STATE_NAME, cf_opts.clone()); - let mut db_opts = Options::default(); db_opts.create_missing_column_families(true); db_opts.create_if_missing(true); - let db = DBWithThreadMode::::open_cf_descriptors( - &db_opts, - path, - vec![cfb, cfmeta, cfstate], - ) - .map_err(|err| DbError::RocksDbError { - error: err, - additional_info: Some("Failed to open or create DB".to_owned()), - })?; - - let dbio = Self { db }; + let dbio = Self::open_inner(path, &db_opts)?; let is_start_set = dbio.get_meta_is_first_block_set()?; if !is_start_set { let block_id = genesis_block.header.block_id; + // TODO: Shouldn't this be atomic (batched)? dbio.put_meta_first_block_in_db(genesis_block, genesis_msg_id)?; dbio.put_meta_is_first_block_set()?; dbio.put_meta_last_block_in_db(block_id)?; @@ -79,11 +69,35 @@ impl RocksDBIO { hash: genesis_block.header.hash, msg_id: genesis_msg_id, })?; + dbio.put_nssa_state_in_db(genesis_state)?; } Ok(dbio) } + fn open_inner(path: &Path, db_opts: &Options) -> DbResult { + let mut cf_opts = Options::default(); + cf_opts.set_max_write_buffer_number(16); + + // ToDo: Add more column families for different data + let cfb = ColumnFamilyDescriptor::new(CF_BLOCK_NAME, cf_opts.clone()); + let cfmeta = ColumnFamilyDescriptor::new(CF_META_NAME, cf_opts.clone()); + let cfstate = ColumnFamilyDescriptor::new(CF_NSSA_STATE_NAME, cf_opts.clone()); + + let db = DBWithThreadMode::::open_cf_descriptors( + db_opts, + path, + vec![cfb, cfmeta, cfstate], + ) + .map_err(|err| DbError::RocksDbError { + error: err, + additional_info: Some("Failed to open or create DB".to_owned()), + })?; + + let dbio = Self { db }; + Ok(dbio) + } + pub fn destroy(path: &Path) -> DbResult<()> { let mut cf_opts = Options::default(); cf_opts.set_max_write_buffer_number(16); @@ -133,7 +147,15 @@ impl RocksDBIO { Ok(self.get_opt::(())?.is_some()) } - pub fn put_nssa_state_in_db(&self, state: &V03State, batch: &mut WriteBatch) -> DbResult<()> { + pub fn put_nssa_state_in_db(&self, state: &V03State) -> DbResult<()> { + self.put(&NSSAStateCellRef(state), ()) + } + + pub fn put_nssa_state_in_db_batch( + &self, + state: &V03State, + batch: &mut WriteBatch, + ) -> DbResult<()> { self.put_batch(&NSSAStateCellRef(state), (), batch) } @@ -338,7 +360,7 @@ impl RocksDBIO { let block_id = block.header.block_id; let mut batch = WriteBatch::default(); self.put_block(block, msg_id, false, &mut batch)?; - self.put_nssa_state_in_db(state, &mut batch)?; + self.put_nssa_state_in_db_batch(state, &mut batch)?; self.db.write(batch).map_err(|rerr| { DbError::rocksdb_cast_message( rerr,