From 6765c8b9b7f883562c25a4f98259739f9d1519cf Mon Sep 17 00:00:00 2001 From: ananas Date: Sat, 25 Oct 2025 14:44:12 +0100 Subject: [PATCH 1/9] chore: registry program throw on zero network fee --- programs/registry/src/lib.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/programs/registry/src/lib.rs b/programs/registry/src/lib.rs index 2f5dccd7d0..1095fa381c 100644 --- a/programs/registry/src/lib.rs +++ b/programs/registry/src/lib.rs @@ -500,10 +500,8 @@ pub mod light_registry { msg!("Forester pubkey must not be defined for trees serviced by light foresters."); return err!(RegistryError::ForesterDefined); } - } else if params.forester.is_none() { - msg!("Forester pubkey required for trees without a network fee."); - msg!("Trees without a network fee will not be serviced by light foresters."); - return err!(RegistryError::ForesterUndefined); + } else { + return err!(RegistryError::InvalidNetworkFee); } check_cpi_context( ctx.accounts.cpi_context_account.to_account_info(), @@ -580,10 +578,8 @@ pub mod light_registry { msg!("Forester pubkey must not be defined for trees serviced by light foresters."); return err!(RegistryError::ForesterDefined); } - } else if params.forester.is_none() { - msg!("Forester pubkey required for trees without a network fee."); - msg!("Trees without a network fee will not be serviced by light foresters."); - return err!(RegistryError::ForesterUndefined); + } else { + return err!(RegistryError::InvalidNetworkFee); } process_initialize_batched_address_merkle_tree(&ctx, bump, params.try_to_vec()?) } From 69a0e06f4e417c7493b68867493d4a9c9f9271cb Mon Sep 17 00:00:00 2001 From: ananas Date: Sat, 25 Oct 2025 15:09:03 +0100 Subject: [PATCH 2/9] chore: set batched address tree default fee to 10k --- program-libs/batched-merkle-tree/src/initialize_address_tree.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/program-libs/batched-merkle-tree/src/initialize_address_tree.rs b/program-libs/batched-merkle-tree/src/initialize_address_tree.rs index 974b71e97c..497e86dad4 100644 --- a/program-libs/batched-merkle-tree/src/initialize_address_tree.rs +++ b/program-libs/batched-merkle-tree/src/initialize_address_tree.rs @@ -44,7 +44,7 @@ impl Default for InitAddressTreeAccountsInstructionData { height: 40, root_history_capacity: DEFAULT_BATCH_ROOT_HISTORY_LEN, bloom_filter_capacity: DEFAULT_BATCH_SIZE * 8, - network_fee: Some(5000), + network_fee: Some(10000), rollover_threshold: Some(95), close_threshold: None, } From 942ac0ea862b6435f902996e24d46d979b38e335 Mon Sep 17 00:00:00 2001 From: ananas Date: Sat, 25 Oct 2025 15:11:02 +0100 Subject: [PATCH 3/9] chore: limit batched tree creations to light security group --- .../initialize_batched_address_merkle_tree.rs | 11 +++++++++++ .../initialize_batched_state_merkle_tree.rs | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/programs/account-compression/src/instructions/initialize_batched_address_merkle_tree.rs b/programs/account-compression/src/instructions/initialize_batched_address_merkle_tree.rs index 1a51e43343..88304e4017 100644 --- a/programs/account-compression/src/instructions/initialize_batched_address_merkle_tree.rs +++ b/programs/account-compression/src/instructions/initialize_batched_address_merkle_tree.rs @@ -46,6 +46,17 @@ pub fn process_initialize_batched_address_merkle_tree<'info>( if params != InitAddressTreeAccountsInstructionData::default() { return err!(AccountCompressionErrorCode::UnsupportedParameters); } + // TODO: test that only security group 24rt4RgeyjUCWGS2eF7L7gyNMuz6JWdqYpAvb1KRoHxs can create batched state trees + use crate::errors::AccountCompressionErrorCode; + if let Some(registered_program_pda) = ctx.accounts.registered_program_pda.as_ref() { + if registered_program_pda.group_authority_pda + != pubkey!("24rt4RgeyjUCWGS2eF7L7gyNMuz6JWdqYpAvb1KRoHxs") + { + return err!(AccountCompressionErrorCode::UnsupportedParameters); + } + } else { + return err!(AccountCompressionErrorCode::UnsupportedParameters); + } } // Check signer. diff --git a/programs/account-compression/src/instructions/initialize_batched_state_merkle_tree.rs b/programs/account-compression/src/instructions/initialize_batched_state_merkle_tree.rs index eccea83a51..9ad0d5deb0 100644 --- a/programs/account-compression/src/instructions/initialize_batched_state_merkle_tree.rs +++ b/programs/account-compression/src/instructions/initialize_batched_state_merkle_tree.rs @@ -39,9 +39,20 @@ pub fn process_initialize_batched_state_merkle_tree<'info>( validate_batched_tree_params(params); #[cfg(not(feature = "test"))] { + // TODO: test that only security group 24rt4RgeyjUCWGS2eF7L7gyNMuz6JWdqYpAvb1KRoHxs can create batched state trees + use crate::errors::AccountCompressionErrorCode; if params != InitStateTreeAccountsInstructionData::default() { return err!(AccountCompressionErrorCode::UnsupportedParameters); } + if let Some(registered_program_pda) = ctx.accounts.registered_program_pda.as_ref() { + if registered_program_pda.group_authority_pda + != pubkey!("24rt4RgeyjUCWGS2eF7L7gyNMuz6JWdqYpAvb1KRoHxs") + { + return err!(AccountCompressionErrorCode::UnsupportedParameters); + } + } else { + return err!(AccountCompressionErrorCode::UnsupportedParameters); + } } let owner = match ctx.accounts.registered_program_pda.as_ref() { From ae55d8385d13f7ee79e38814cdcd0c7ceff8ebba Mon Sep 17 00:00:00 2001 From: ananas Date: Sat, 25 Oct 2025 15:14:00 +0100 Subject: [PATCH 4/9] chore: disable program owned trees --- programs/registry/src/errors.rs | 1 + programs/registry/src/lib.rs | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/programs/registry/src/errors.rs b/programs/registry/src/errors.rs index 966b8f1050..165ef00f6f 100644 --- a/programs/registry/src/errors.rs +++ b/programs/registry/src/errors.rs @@ -30,4 +30,5 @@ pub enum RegistryError { ForesterDefined, #[msg("Insufficient funds in pool")] InsufficientFunds, + ProgramOwnerDefined, } diff --git a/programs/registry/src/lib.rs b/programs/registry/src/lib.rs index 1095fa381c..463017b1f8 100644 --- a/programs/registry/src/lib.rs +++ b/programs/registry/src/lib.rs @@ -291,6 +291,11 @@ pub mod light_registry { merkle_tree_config: AddressMerkleTreeConfig, queue_config: AddressQueueConfig, ) -> Result<()> { + // Program owned trees are disabled + if program_owner.is_some() { + msg!("Program owner must not be defined."); + return err!(RegistryError::ProgramOwnerDefined); + } // The network fee must be either zero or the same as the protocol config. // Only trees with a network fee will be serviced by light foresters. if let Some(network_fee) = merkle_tree_config.network_fee { @@ -329,6 +334,11 @@ pub mod light_registry { merkle_tree_config: StateMerkleTreeConfig, queue_config: NullifierQueueConfig, ) -> Result<()> { + // Program owned trees are disabled + if program_owner.is_some() { + msg!("Program owner must not be defined."); + return err!(RegistryError::ProgramOwnerDefined); + } // The network fee must be either zero or the same as the protocol config. // Only trees with a network fee will be serviced by light foresters. if let Some(network_fee) = merkle_tree_config.network_fee { @@ -492,6 +502,11 @@ pub mod light_registry { params: Vec, ) -> Result<()> { let params = InitStateTreeAccountsInstructionData::try_from_slice(¶ms)?; + // Program owned trees are disabled + if params.program_owner.is_some() { + msg!("Program owner must not be defined."); + return err!(RegistryError::ProgramOwnerDefined); + } if let Some(network_fee) = params.network_fee { if network_fee != ctx.accounts.protocol_config_pda.config.network_fee { return err!(RegistryError::InvalidNetworkFee); @@ -570,6 +585,11 @@ pub mod light_registry { params: Vec, ) -> Result<()> { let params = InitAddressTreeAccountsInstructionData::try_from_slice(¶ms)?; + // Program owned trees are disabled + if params.program_owner.is_some() { + msg!("Program owner must not be defined."); + return err!(RegistryError::ProgramOwnerDefined); + } if let Some(network_fee) = params.network_fee { if network_fee != ctx.accounts.protocol_config_pda.config.network_fee { return err!(RegistryError::InvalidNetworkFee); From ae542d03c65fed3073d0e60ae1f849be29125c02 Mon Sep 17 00:00:00 2001 From: ananas Date: Sat, 25 Oct 2025 22:35:26 +0100 Subject: [PATCH 5/9] chore: cleanup features, add tests for mainnet tree config and features --- .../src/initialize_address_tree.rs | 6 +- .../tests/address_merkle_tree_tests.rs | 4 +- .../tests/batched_merkle_tree_test.rs | 118 ++++++++++- program-tests/registry-test/tests/tests.rs | 193 +++++++++--------- program-tests/system-cpi-test/tests/test.rs | 36 ++-- .../tests/test_program_owned_trees.rs | 4 +- .../tests/invoke_cpi_with_read_only.rs | 2 +- program-tests/system-test/tests/test.rs | 23 ++- program-tests/utils/src/e2e_test_env.rs | 54 +++-- program-tests/utils/src/lib.rs | 4 + programs/account-compression/Cargo.toml | 3 +- ...nitialize_address_merkle_tree_and_queue.rs | 2 +- .../initialize_batched_address_merkle_tree.rs | 9 +- .../initialize_batched_state_merkle_tree.rs | 6 +- programs/account-compression/src/lib.rs | 13 +- programs/package.json | 2 +- .../rollover_batched_address_tree.rs | 2 +- programs/registry/src/lib.rs | 33 +-- .../registry/src/protocol_config/state.rs | 6 +- programs/system/Cargo.toml | 4 +- .../src/accounts/remaining_account_checks.rs | 13 +- programs/system/src/context.rs | 60 ++++-- programs/system/src/processor/cpi.rs | 1 - .../src/processor/create_address_cpi_data.rs | 13 +- .../src/processor/create_inputs_cpi_data.rs | 12 +- .../src/processor/create_outputs_cpi_data.rs | 2 +- sdk-libs/client/src/fee.rs | 29 ++- .../program-test/src/accounts/initialize.rs | 32 +-- .../program-test/src/indexer/test_indexer.rs | 19 +- .../src/program_test/light_program_test.rs | 54 +++-- .../program-test/src/utils/load_accounts.rs | 55 +++++ 31 files changed, 558 insertions(+), 256 deletions(-) diff --git a/program-libs/batched-merkle-tree/src/initialize_address_tree.rs b/program-libs/batched-merkle-tree/src/initialize_address_tree.rs index 497e86dad4..8524cbd459 100644 --- a/program-libs/batched-merkle-tree/src/initialize_address_tree.rs +++ b/program-libs/batched-merkle-tree/src/initialize_address_tree.rs @@ -202,7 +202,7 @@ pub mod test_utils { height: 40, root_history_capacity: DEFAULT_BATCH_ROOT_HISTORY_LEN, bloom_filter_capacity: 20_000 * 8, - network_fee: Some(5000), + network_fee: Some(10000), rollover_threshold: Some(95), close_threshold: None, } @@ -219,7 +219,7 @@ pub mod test_utils { height: 40, root_history_capacity: DEFAULT_BATCH_ROOT_HISTORY_LEN, bloom_filter_capacity: 20_000 * 8, - network_fee: Some(5000), + network_fee: Some(10000), rollover_threshold: Some(95), close_threshold: None, } @@ -235,7 +235,7 @@ pub mod test_utils { height: 40, root_history_capacity: DEFAULT_BATCH_ROOT_HISTORY_LEN, bloom_filter_capacity: ADDRESS_BLOOM_FILTER_CAPACITY, - network_fee: Some(5000), + network_fee: Some(10000), rollover_threshold: Some(95), close_threshold: None, } diff --git a/program-tests/account-compression-test/tests/address_merkle_tree_tests.rs b/program-tests/account-compression-test/tests/address_merkle_tree_tests.rs index 1a67f70052..59ec911de7 100644 --- a/program-tests/account-compression-test/tests/address_merkle_tree_tests.rs +++ b/program-tests/account-compression-test/tests/address_merkle_tree_tests.rs @@ -149,7 +149,7 @@ async fn test_address_queue_and_tree_functional_custom() { roots_size, canopy_depth: ADDRESS_MERKLE_TREE_CANOPY_DEPTH, address_changelog_size, - network_fee: Some(5000), + network_fee: Some(10000), rollover_threshold: Some(95), close_threshold: None, }, @@ -1143,7 +1143,7 @@ async fn update_address_merkle_tree_wrap_around_custom() { roots_size, canopy_depth: ADDRESS_MERKLE_TREE_CANOPY_DEPTH, address_changelog_size, - network_fee: Some(5000), + network_fee: Some(10000), rollover_threshold: Some(95), close_threshold: None, }, diff --git a/program-tests/account-compression-test/tests/batched_merkle_tree_test.rs b/program-tests/account-compression-test/tests/batched_merkle_tree_test.rs index 530c4a8324..38bae5f372 100644 --- a/program-tests/account-compression-test/tests/batched_merkle_tree_test.rs +++ b/program-tests/account-compression-test/tests/batched_merkle_tree_test.rs @@ -1,5 +1,3 @@ -#![cfg(feature = "test-sbf")] - use account_compression::{errors::AccountCompressionErrorCode, ID}; use anchor_lang::{ error::ErrorCode, prelude::AccountMeta, AnchorSerialize, InstructionData, ToAccountMetas, @@ -67,6 +65,122 @@ pub enum TestMode { InvalidRegisteredProgram, } +#[ignore = "only execute with program compiled non test"] +#[tokio::test] +async fn test_batch_state_merkle_tree_failing() { + let config = ProgramTestConfig { + skip_protocol_init: true, + ..Default::default() + }; + let mut context = LightProgramTest::new(config).await.unwrap(); + + let merkle_tree_keypair = Keypair::new(); + let merkle_tree_pubkey = merkle_tree_keypair.pubkey(); + let nullifier_queue_keypair = Keypair::new(); + let output_queue_pubkey = nullifier_queue_keypair.pubkey(); + + let payer_pubkey = context.get_payer().pubkey(); + let payer = context.get_payer().insecure_clone(); + let params = InitStateTreeAccountsInstructionData::test_default(); + let queue_account_size = get_output_queue_account_size( + params.output_queue_batch_size, + params.output_queue_zkp_batch_size, + ); + let mt_account_size = get_merkle_tree_account_size( + params.input_queue_batch_size, + params.bloom_filter_capacity, + params.input_queue_zkp_batch_size, + params.root_history_capacity, + params.height, + ); + let queue_rent = context + .get_minimum_balance_for_rent_exemption(queue_account_size) + .await + .unwrap(); + let mt_rent = context + .get_minimum_balance_for_rent_exemption(mt_account_size) + .await + .unwrap(); + + // 1. Functional initialize a batched Merkle tree and output queue + { + let create_queue_account_ix = create_account_instruction( + &payer_pubkey, + queue_account_size, + queue_rent, + &ID, + Some(&nullifier_queue_keypair), + ); + + let create_mt_account_ix = create_account_instruction( + &payer_pubkey, + mt_account_size, + mt_rent, + &ID, + Some(&merkle_tree_keypair), + ); + + let instruction = account_compression::instruction::InitializeBatchedStateMerkleTree { + bytes: params.try_to_vec().unwrap(), + }; + let accounts = account_compression::accounts::InitializeBatchedStateMerkleTreeAndQueue { + authority: context.get_payer().pubkey(), + merkle_tree: merkle_tree_pubkey, + queue: output_queue_pubkey, + registered_program_pda: None, + }; + + let instruction = Instruction { + program_id: ID, + accounts: accounts.to_account_metas(Some(true)), + data: instruction.data(), + }; + let result = context + .create_and_send_transaction( + &[create_queue_account_ix, create_mt_account_ix, instruction], + &payer_pubkey, + &[&payer, &nullifier_queue_keypair, &merkle_tree_keypair], + ) + .await; + println!("result {:?}", result); + // Incorrect security group + assert_rpc_error( + result, + 2, + AccountCompressionErrorCode::UnsupportedParameters.into(), + ) + .unwrap(); + } +} + +#[ignore = "only execute with program compiled non test"] +#[tokio::test] +async fn test_init_batch_address_merkle_trees_failing() { + let config = ProgramTestConfig { + skip_protocol_init: true, + with_prover: false, + skip_startup_logs: false, + no_logs: false, + ..Default::default() + }; + let mut context = LightProgramTest::new(config).await.unwrap(); + context.config.no_logs = false; + let params = InitAddressTreeAccountsInstructionData::test_default(); + + let merkle_tree_keypair = Keypair::new(); + + let result = + perform_init_batch_address_merkle_tree(&mut context, ¶ms, &merkle_tree_keypair).await; + println!("result {:?}", result); + // Incorrect security group + assert_rpc_error( + result, + 1, + AccountCompressionErrorCode::UnsupportedParameters.into(), + ) + .unwrap(); +} + /// 1. init accounts - Functional: initialize a batched Merkle tree and output queue /// 2. append leaves - Failing: Invalid signe /// 3. append leaves - Functional insert 10 leaves into output queue diff --git a/program-tests/registry-test/tests/tests.rs b/program-tests/registry-test/tests/tests.rs index 01f2b3b19f..b8b9cb3b59 100644 --- a/program-tests/registry-test/tests/tests.rs +++ b/program-tests/registry-test/tests/tests.rs @@ -1,5 +1,3 @@ -#![cfg(feature = "test-sbf")] - use std::collections::HashSet; use account_compression::{ @@ -33,7 +31,6 @@ use light_program_test::{ deregister_program_with_registry_program, register_program_with_registry_program, }, state_tree::create_state_merkle_tree_and_queue_account, - state_tree_v2::create_batched_state_merkle_tree, test_accounts::{TestAccounts, NOOP_PROGRAM_ID}, test_keypairs::{GROUP_PDA_SEED_TEST_KEYPAIR, OLD_REGISTRY_ID_TEST_KEYPAIR}, }, @@ -108,7 +105,7 @@ fn test_protocol_config_active_phase_continuity() { cpi_context_size: 20488, finalize_counter_limit: 100, place_holder: Pubkey::default(), - place_holder_a: 0, + address_network_fee: 10000, place_holder_b: 0, place_holder_c: 0, place_holder_d: 0, @@ -127,7 +124,7 @@ fn test_protocol_config_active_phase_continuity() { cpi_context_size: 20488, finalize_counter_limit: 100, place_holder: Pubkey::default(), - place_holder_a: 0, + address_network_fee: 10000, place_holder_b: 0, place_holder_c: 0, place_holder_d: 0, @@ -490,41 +487,67 @@ async fn test_initialize_protocol_config() { &mut rpc, &merkle_tree_keypair, &queue_keypair, - None, + Some(Pubkey::new_unique()), Some(Pubkey::new_unique()), &AddressMerkleTreeConfig { network_fee: None, ..Default::default() }, - &AddressQueueConfig::default(), + &AddressQueueConfig { + network_fee: None, + ..Default::default() + }, 0, ) .await .unwrap(); } - // initialize a Merkle tree with network fee = 5000 (default) + // Deprecated should fail + // initialize a Merkle tree without a forester { let merkle_tree_keypair = Keypair::new(); let queue_keypair = Keypair::new(); - create_address_merkle_tree_and_queue_account_with_assert( + let result = create_address_merkle_tree_and_queue_account_with_assert( &payer, true, &mut rpc, &merkle_tree_keypair, &queue_keypair, + Some(Pubkey::new_unique()), None, + &AddressMerkleTreeConfig { + network_fee: None, + ..Default::default() + }, + &AddressQueueConfig::default(), + 0, + ) + .await; + assert_rpc_error(result, 3, RegistryError::ForesterUndefined.into()).unwrap(); + } + // initialize a Merkle tree without a Program owner + { + let merkle_tree_keypair = Keypair::new(); + let queue_keypair = Keypair::new(); + let result = create_address_merkle_tree_and_queue_account_with_assert( + &payer, + true, + &mut rpc, + &merkle_tree_keypair, + &queue_keypair, None, + Some(Pubkey::new_unique()), &AddressMerkleTreeConfig { - network_fee: Some(5000), + network_fee: None, ..Default::default() }, &AddressQueueConfig::default(), 0, ) - .await - .unwrap(); + .await; + assert_rpc_error(result, 3, RegistryError::ForesterUndefined.into()).unwrap(); } - // FAIL: initialize a Merkle tree with network fee != 0 || 5000 + // FAIL: initialize a Merkle tree with network fee != 0 { let merkle_tree_keypair = Keypair::new(); let queue_keypair = Keypair::new(); @@ -534,10 +557,10 @@ async fn test_initialize_protocol_config() { &mut rpc, &merkle_tree_keypair, &queue_keypair, - None, - None, + Some(Pubkey::new_unique()), + Some(Pubkey::new_unique()), &AddressMerkleTreeConfig { - network_fee: Some(5001), + network_fee: Some(10000), ..Default::default() }, &AddressQueueConfig::default(), @@ -1645,7 +1668,7 @@ async fn test_migrate_state() { // 6. Failing - invoke account compression program migrate state without registered program PDA { let instruction = account_compression::instruction::MigrateState { - input: instruction_params.inputs, + _input: instruction_params.inputs, }; let accounts = account_compression::accounts::MigrateState { authority: payer.pubkey(), @@ -1776,94 +1799,20 @@ async fn test_rollover_batch_state_tree() { params.rollover_threshold = Some(0); params.forester = Some(custom_forester.pubkey().into()); params.network_fee = None; - let is_light_forester = false; let mut tree_params = InitAddressTreeAccountsInstructionData::test_default(); tree_params.rollover_threshold = Some(0); let mut config = ProgramTestConfig::default_with_batched_trees(false); config.v2_state_tree_config = Some(params); config.v2_address_tree_config = Some(tree_params); - let mut rpc = LightProgramTest::new(config).await.unwrap(); - let test_accounts = rpc.test_accounts().clone(); - airdrop_lamports(&mut rpc, &custom_forester.pubkey(), 10_000_000_000) - .await - .unwrap(); - let payer = rpc.get_payer().insecure_clone(); - let mut test_indexer: TestIndexer = TestIndexer::init_from_acounts( - &test_accounts.protocol.forester.insecure_clone(), - &test_accounts, - 50, - ) - .await; - light_test_utils::system_program::compress_sol_test( - &mut rpc, - &mut test_indexer, - &payer, - &[], - false, - 1_000_000, - &test_accounts.v2_state_trees[0].output_queue, - None, - ) - .await - .unwrap(); - let new_merkle_tree_keypair = Keypair::new(); - let new_nullifier_queue_keypair = Keypair::new(); - let new_cpi_context = Keypair::new(); - - // 3. functional without network fee and forester + let result = LightProgramTest::new(config).await; - perform_rollover_batch_state_merkle_tree( - &mut rpc, - &custom_forester, - custom_forester.pubkey(), - test_accounts.v2_state_trees[0].merkle_tree, - test_accounts.v2_state_trees[0].output_queue, - &new_merkle_tree_keypair, - &new_nullifier_queue_keypair, - &new_cpi_context, - 0, - is_light_forester, + assert_rpc_error( + result, + 3, + light_registry::errors::RegistryError::InvalidNetworkFee.into(), ) - .await .unwrap(); - let new_cpi_ctx_account = rpc - .get_account(new_cpi_context.pubkey()) - .await - .unwrap() - .unwrap(); - assert_perform_state_mt_roll_over( - &mut rpc, - test_accounts.protocol.group_pda, - test_accounts.v2_state_trees[0].merkle_tree, - new_merkle_tree_keypair.pubkey(), - test_accounts.v2_state_trees[0].output_queue, - new_nullifier_queue_keypair.pubkey(), - params, - new_cpi_ctx_account.lamports, - ) - .await; - // 4. failing with custom forester and non-zero network fee - { - let mut params = InitStateTreeAccountsInstructionData::test_default(); - params.rollover_threshold = Some(0); - params.forester = Some(custom_forester.pubkey().into()); - params.network_fee = Some(1); - let new_merkle_tree_keypair = Keypair::new(); - let new_nullifier_queue_keypair = Keypair::new(); - let new_cpi_context = Keypair::new(); - let result = create_batched_state_merkle_tree( - &test_accounts.protocol.governance_authority, - true, - &mut rpc, - &new_merkle_tree_keypair, - &new_nullifier_queue_keypair, - &new_cpi_context, - params, - ) - .await; - assert_rpc_error(result, 3, RegistryError::InvalidNetworkFee.into()).unwrap(); - } } } @@ -2130,3 +2079,55 @@ async fn test_rollover_batch_address_tree() { .await .unwrap(); } + +#[ignore = "requires account compression program without test features"] +#[tokio::test] +async fn test_v2_tree_mainnet_init() { + let mut config = ProgramTestConfig::default_test_forester(true); + config.v2_state_tree_config = Some(InitStateTreeAccountsInstructionData::default()); + config.v2_address_tree_config = Some(InitAddressTreeAccountsInstructionData::default()); + config.additional_programs = Some(vec![( + "create_address_test_program", + CREATE_ADDRESS_TEST_PROGRAM_ID, + )]); + LightProgramTest::new(config).await.unwrap(); +} + +#[ignore = "requires account compression program without test features"] +#[tokio::test] +async fn test_v2_state_tree_mainnet_init_fail() { + let mut config = ProgramTestConfig::default_test_forester(true); + config.v2_state_tree_config = Some(InitStateTreeAccountsInstructionData::test_default()); + config.v1_state_tree_config = StateMerkleTreeConfig::default(); + config.v2_address_tree_config = Some(InitAddressTreeAccountsInstructionData::default()); + config.additional_programs = Some(vec![( + "create_address_test_program", + CREATE_ADDRESS_TEST_PROGRAM_ID, + )]); + let result = LightProgramTest::new(config).await; + assert_rpc_error( + result, + 3, + AccountCompressionErrorCode::UnsupportedParameters.into(), + ) + .unwrap(); +} + +#[ignore = "requires account compression program without test features"] +#[tokio::test] +async fn test_v2_address_tree_mainnet_init_fail() { + let mut config = ProgramTestConfig::default_test_forester(true); + config.v2_state_tree_config = Some(InitStateTreeAccountsInstructionData::default()); + config.v2_address_tree_config = Some(InitAddressTreeAccountsInstructionData::test_default()); + config.additional_programs = Some(vec![( + "create_address_test_program", + CREATE_ADDRESS_TEST_PROGRAM_ID, + )]); + let result = LightProgramTest::new(config).await; + assert_rpc_error( + result, + 1, + AccountCompressionErrorCode::UnsupportedParameters.into(), + ) + .unwrap(); +} diff --git a/program-tests/system-cpi-test/tests/test.rs b/program-tests/system-cpi-test/tests/test.rs index 443d55fdc9..9f4623ed02 100644 --- a/program-tests/system-cpi-test/tests/test.rs +++ b/program-tests/system-cpi-test/tests/test.rs @@ -1,5 +1,3 @@ -#![cfg(feature = "test-sbf")] - use anchor_lang::{AnchorDeserialize, AnchorSerialize}; use light_account_checks::error::AccountError; use light_batched_merkle_tree::initialize_state_tree::InitStateTreeAccountsInstructionData; @@ -986,7 +984,7 @@ async fn only_test_create_pda() { &program_owned_merkle_tree_keypair, &program_owned_queue_keypair, &program_owned_cpi_context_keypair, - Some(light_compressed_token::ID), + None, // Program owned trees are deprecated None, TreeType::StateV1, ) @@ -1469,7 +1467,7 @@ async fn test_create_pda_in_program_owned_merkle_trees() { &program_owned_state_merkle_tree_keypair, &program_owned_state_queue_keypair, &program_owned_cpi_context_keypair, - Some(light_compressed_token::ID), + None, None, TreeType::StateV1, ) @@ -1484,20 +1482,20 @@ async fn test_create_pda_in_program_owned_merkle_trees() { cpi_context: program_owned_cpi_context_keypair.pubkey(), tree_type: TreeType::StateV1, }]; - - perform_create_pda_failing( - &mut rpc, - &mut test_indexer, - &env_with_program_owned_state_merkle_tree, - &payer, - [3u8; 32], - &[4u8; 31], - &ID, - CreatePdaMode::ProgramIsSigner, - SystemProgramError::InvalidMerkleTreeOwner.into(), - ) - .await - .unwrap(); + // Program owned state trees are deprecated. + // perform_create_pda_failing( + // &mut rpc, + // &mut test_indexer, + // &env_with_program_owned_state_merkle_tree, + // &payer, + // [3u8; 32], + // &[4u8; 31], + // &ID, + // CreatePdaMode::ProgramIsSigner, + // SystemProgramError::InvalidMerkleTreeOwner.into(), + // ) + // .await + // .unwrap(); // Functional test ---------------------------------------------- let program_owned_state_merkle_tree_keypair = Keypair::new(); @@ -1510,7 +1508,7 @@ async fn test_create_pda_in_program_owned_merkle_trees() { &program_owned_state_merkle_tree_keypair, &program_owned_state_queue_keypair, &program_owned_cpi_context_keypair, - Some(ID), + None, None, TreeType::StateV1, ) diff --git a/program-tests/system-cpi-test/tests/test_program_owned_trees.rs b/program-tests/system-cpi-test/tests/test_program_owned_trees.rs index d2273a204a..9451028c41 100644 --- a/program-tests/system-cpi-test/tests/test_program_owned_trees.rs +++ b/program-tests/system-cpi-test/tests/test_program_owned_trees.rs @@ -47,6 +47,7 @@ use system_cpi_test::sdk::{ create_initialize_merkle_tree_instruction, }; +#[ignore = "program owned state trees are deprecated"] #[serial] #[tokio::test] async fn test_program_owned_merkle_tree() { @@ -106,7 +107,8 @@ async fn test_program_owned_merkle_tree() { &[&payer], Some(TransactionParams { num_new_addresses: 0, - num_input_compressed_accounts: 0, + v1_input_compressed_accounts: 0u8, + v2_input_compressed_accounts: false, num_output_compressed_accounts: 1, compress: 0, fee_config: FeeConfig::default(), diff --git a/program-tests/system-cpi-v2-test/tests/invoke_cpi_with_read_only.rs b/program-tests/system-cpi-v2-test/tests/invoke_cpi_with_read_only.rs index 00aad1a13c..d9af38e708 100644 --- a/program-tests/system-cpi-v2-test/tests/invoke_cpi_with_read_only.rs +++ b/program-tests/system-cpi-v2-test/tests/invoke_cpi_with_read_only.rs @@ -2938,7 +2938,7 @@ async fn test_duplicate_account_in_inputs_and_read_only() { } pub mod local_sdk { - use std::collections::HashMap; + use std::{collections::HashMap, println}; use anchor_lang::{prelude::AccountMeta, AnchorSerialize}; use solana_sdk::pubkey::Pubkey; diff --git a/program-tests/system-test/tests/test.rs b/program-tests/system-test/tests/test.rs index 78ba3f5f08..d30c0d89de 100644 --- a/program-tests/system-test/tests/test.rs +++ b/program-tests/system-test/tests/test.rs @@ -923,7 +923,8 @@ async fn invoke_test() { &payer_pubkey, &[&payer], Some(TransactionParams { - num_input_compressed_accounts: 0, + v1_input_compressed_accounts: 0u8, + v2_input_compressed_accounts: false, num_output_compressed_accounts: 1, num_new_addresses: 0, compress: 0, @@ -1063,7 +1064,8 @@ async fn invoke_test() { &payer_pubkey, &[&payer], Some(TransactionParams { - num_input_compressed_accounts: 1, + v1_input_compressed_accounts: 1, + v2_input_compressed_accounts: false, num_output_compressed_accounts: 1, num_new_addresses: 0, compress: 0, @@ -1706,11 +1708,12 @@ async fn regenerate_accounts() { pubkeys.push(("cpi_context", tree.cpi_context)); } - // Add all v1 address trees - for tree in &env.v1_address_trees { - pubkeys.push(("address_merkle_tree", tree.merkle_tree)); - pubkeys.push(("address_merkle_tree_queue", tree.queue)); - } + // V1 address trees are deprecated - do not regenerate + // They are loaded from existing JSON files in devenv mode + // for tree in &env.v1_address_trees { + // pubkeys.push(("address_merkle_tree", tree.merkle_tree)); + // pubkeys.push(("address_merkle_tree_queue", tree.queue)); + // } // Add all v2 state trees for tree in &env.v2_state_trees { @@ -2010,7 +2013,8 @@ async fn batch_invoke_test() { &payer_pubkey, &[&payer], Some(TransactionParams { - num_input_compressed_accounts: 1, + v1_input_compressed_accounts: 1, + v2_input_compressed_accounts: true, num_output_compressed_accounts: 1, num_new_addresses: 0, compress: 0, @@ -2629,7 +2633,8 @@ pub async fn create_output_accounts( &payer.pubkey(), &[payer], Some(TransactionParams { - num_input_compressed_accounts: 0, + v1_input_compressed_accounts: 0u8, + v2_input_compressed_accounts: is_batched, num_output_compressed_accounts: num_accounts as u8, num_new_addresses: 0, compress: 0, diff --git a/program-tests/utils/src/e2e_test_env.rs b/program-tests/utils/src/e2e_test_env.rs index f9c5a8a42a..06387999e2 100644 --- a/program-tests/utils/src/e2e_test_env.rs +++ b/program-tests/utils/src/e2e_test_env.rs @@ -1314,7 +1314,7 @@ where canopy_depth: 10, address_changelog_size: self.rng.gen_range(1..5000), rollover_threshold, - network_fee: Some(5000), + network_fee: Some(10000), close_threshold: None, // TODO: double check that close threshold cannot be set }, @@ -1543,9 +1543,15 @@ where }; let recipients = vec![*to]; let transaction_params = if self.keypair_action_config.fee_assert { + let (inputs, is_v2) = if bundle.tree_type == TreeType::StateV2 { + (0u8, true) + } else { + (input_compressed_accounts.len() as u8, false) + }; Some(TransactionParams { num_new_addresses: 0, - num_input_compressed_accounts: input_compressed_accounts.len() as u8, + v1_input_compressed_accounts: inputs, + v2_input_compressed_accounts: is_v2, num_output_compressed_accounts: 1u8, compress: 0, fee_config: FeeConfig { @@ -1596,7 +1602,8 @@ where let transaction_parameters = if self.keypair_action_config.fee_assert { Some(TransactionParams { num_new_addresses: 0, - num_input_compressed_accounts: input_compressed_accounts.len() as u8, + v1_input_compressed_accounts: input_compressed_accounts.len() as u8, + v2_input_compressed_accounts: false, num_output_compressed_accounts: num_output_merkle_trees as u8, compress: 0, fee_config: FeeConfig::default(), @@ -1637,7 +1644,8 @@ where let transaction_paramets = if self.keypair_action_config.fee_assert { Some(TransactionParams { num_new_addresses: 0, - num_input_compressed_accounts: input_compressed_accounts.len() as u8, + v1_input_compressed_accounts: input_compressed_accounts.len() as u8, + v2_input_compressed_accounts: false, num_output_compressed_accounts: 1u8, compress: 0, fee_config: FeeConfig::default(), @@ -1694,9 +1702,15 @@ where }; let transaction_parameters = if self.keypair_action_config.fee_assert { + let (inputs, is_v2) = if bundle.tree_type == TreeType::StateV2 { + (0u8, true) + } else { + (input_compressed_accounts.len() as u8, false) + }; Some(TransactionParams { num_new_addresses: 0, - num_input_compressed_accounts: input_compressed_accounts.len() as u8, + v1_input_compressed_accounts: inputs, + v2_input_compressed_accounts: is_v2, num_output_compressed_accounts: 1u8, compress: amount as i64, fee_config: FeeConfig { @@ -1739,7 +1753,8 @@ where let transaction_parameters = if self.keypair_action_config.fee_assert { Some(TransactionParams { num_new_addresses: 0, - num_input_compressed_accounts: input_compressed_accounts.len() as u8, + v1_input_compressed_accounts: input_compressed_accounts.len() as u8, + v2_input_compressed_accounts: false, num_output_compressed_accounts: 1u8, compress: amount as i64, fee_config: FeeConfig::default(), @@ -1827,7 +1842,8 @@ where let transaction_parameters = if self.keypair_action_config.fee_assert { Some(TransactionParams { num_new_addresses: num_addresses as u8, - num_input_compressed_accounts: 0u8, + v1_input_compressed_accounts: 0u8, + v2_input_compressed_accounts: false, num_output_compressed_accounts: num_addresses as u8, compress: 0, fee_config: FeeConfig::default(), @@ -1906,7 +1922,8 @@ where let transaction_paramets = if self.keypair_action_config.fee_assert { Some(TransactionParams { num_new_addresses: 0u8, - num_input_compressed_accounts: token_accounts.len() as u8, + v1_input_compressed_accounts: token_accounts.len() as u8, + v2_input_compressed_accounts: false, num_output_compressed_accounts: output_merkle_tree_pubkeys.len() as u8, compress: 0, fee_config: FeeConfig::default(), @@ -1965,7 +1982,8 @@ where let transaction_paramets = if self.keypair_action_config.fee_assert { Some(TransactionParams { num_new_addresses: 0u8, - num_input_compressed_accounts: token_accounts.len() as u8, + v1_input_compressed_accounts: token_accounts.len() as u8, + v2_input_compressed_accounts: false, num_output_compressed_accounts, compress: 0, fee_config: FeeConfig::default(), @@ -2018,7 +2036,8 @@ where let transaction_paramets = if self.keypair_action_config.fee_assert { Some(TransactionParams { num_new_addresses: 0u8, - num_input_compressed_accounts: token_accounts.len() as u8, + v1_input_compressed_accounts: token_accounts.len() as u8, + v2_input_compressed_accounts: false, num_output_compressed_accounts, compress: 0, fee_config: FeeConfig::default(), @@ -2067,7 +2086,8 @@ where let transaction_paramets = if self.keypair_action_config.fee_assert { Some(TransactionParams { num_new_addresses: 0u8, - num_input_compressed_accounts: token_accounts.len() as u8, + v1_input_compressed_accounts: token_accounts.len() as u8, + v2_input_compressed_accounts: false, num_output_compressed_accounts, compress: 0, fee_config: FeeConfig::default(), @@ -2117,7 +2137,8 @@ where let transaction_parameters = if self.keypair_action_config.fee_assert { Some(TransactionParams { num_new_addresses: 0u8, - num_input_compressed_accounts: token_accounts.len() as u8, + v1_input_compressed_accounts: token_accounts.len() as u8, + v2_input_compressed_accounts: false, num_output_compressed_accounts: token_accounts.len() as u8, compress: 0, fee_config: FeeConfig::default(), @@ -2155,7 +2176,8 @@ where let transaction_paramets = if self.keypair_action_config.fee_assert { Some(TransactionParams { num_new_addresses: 0u8, - num_input_compressed_accounts: token_accounts.len() as u8, + v1_input_compressed_accounts: token_accounts.len() as u8, + v2_input_compressed_accounts: false, num_output_compressed_accounts: token_accounts.len() as u8, compress: 0, fee_config: FeeConfig::default(), @@ -2214,7 +2236,8 @@ where let transaction_paramets = if self.keypair_action_config.fee_assert { Some(TransactionParams { num_new_addresses: 0u8, - num_input_compressed_accounts: 0u8, + v1_input_compressed_accounts: 0u8, + v2_input_compressed_accounts: false, num_output_compressed_accounts: 1u8, compress: 0, // sol amount this is a spl compress test fee_config: FeeConfig::default(), @@ -2292,7 +2315,8 @@ where let transaction_paramets = if self.keypair_action_config.fee_assert { Some(TransactionParams { num_new_addresses: 0u8, - num_input_compressed_accounts: token_accounts.len() as u8, + v1_input_compressed_accounts: token_accounts.len() as u8, + v2_input_compressed_accounts: false, num_output_compressed_accounts: 1u8, compress: 0, fee_config: FeeConfig::default(), diff --git a/program-tests/utils/src/lib.rs b/program-tests/utils/src/lib.rs index 1cc47620cc..fe0ed981b4 100644 --- a/program-tests/utils/src/lib.rs +++ b/program-tests/utils/src/lib.rs @@ -101,6 +101,10 @@ pub async fn create_address_merkle_tree_and_queue_account_with_assert( ) .await; + #[allow(clippy::question_mark)] + if result.is_err() { + return result; + } // To initialize the indexed tree we do 4 operations: // 1. insert 0 append 0 and update 0 // 2. insert 1 append BN254_FIELD_SIZE -1 and update 0 diff --git a/programs/account-compression/Cargo.toml b/programs/account-compression/Cargo.toml index cbe50c45ab..1c11e080f6 100644 --- a/programs/account-compression/Cargo.toml +++ b/programs/account-compression/Cargo.toml @@ -18,10 +18,11 @@ no-log-ix-name = [] cpi = ["no-entrypoint"] custom-heap = ["light-heap"] mem-profiling = [] -default = ["test"] +default = [] test-sbf = [] bench-sbf = ["custom-heap"] test = [] +migrate-state = [] [dependencies] diff --git a/programs/account-compression/src/instructions/initialize_address_merkle_tree_and_queue.rs b/programs/account-compression/src/instructions/initialize_address_merkle_tree_and_queue.rs index 69d08a27a9..024a4d908b 100644 --- a/programs/account-compression/src/instructions/initialize_address_merkle_tree_and_queue.rs +++ b/programs/account-compression/src/instructions/initialize_address_merkle_tree_and_queue.rs @@ -41,7 +41,7 @@ impl Default for AddressMerkleTreeConfig { roots_size: ADDRESS_MERKLE_TREE_ROOTS, canopy_depth: ADDRESS_MERKLE_TREE_CANOPY_DEPTH, address_changelog_size: ADDRESS_MERKLE_TREE_INDEXED_CHANGELOG, - network_fee: Some(5000), + network_fee: Some(10000), rollover_threshold: Some(95), close_threshold: None, } diff --git a/programs/account-compression/src/instructions/initialize_batched_address_merkle_tree.rs b/programs/account-compression/src/instructions/initialize_batched_address_merkle_tree.rs index 88304e4017..116b55c06f 100644 --- a/programs/account-compression/src/instructions/initialize_batched_address_merkle_tree.rs +++ b/programs/account-compression/src/instructions/initialize_batched_address_merkle_tree.rs @@ -1,8 +1,7 @@ use anchor_lang::prelude::*; use light_batched_merkle_tree::{ initialize_address_tree::{ - init_batched_address_merkle_tree_from_account_info, validate_batched_address_tree_params, - InitAddressTreeAccountsInstructionData, + init_batched_address_merkle_tree_from_account_info, InitAddressTreeAccountsInstructionData, }, merkle_tree::BatchedMerkleTreeAccount, }; @@ -40,14 +39,16 @@ pub fn process_initialize_batched_address_merkle_tree<'info>( params: InitAddressTreeAccountsInstructionData, ) -> Result<()> { #[cfg(feature = "test")] - validate_batched_address_tree_params(params); + light_batched_merkle_tree::initialize_address_tree::validate_batched_address_tree_params( + params, + ); #[cfg(not(feature = "test"))] { + use crate::errors::AccountCompressionErrorCode; if params != InitAddressTreeAccountsInstructionData::default() { return err!(AccountCompressionErrorCode::UnsupportedParameters); } // TODO: test that only security group 24rt4RgeyjUCWGS2eF7L7gyNMuz6JWdqYpAvb1KRoHxs can create batched state trees - use crate::errors::AccountCompressionErrorCode; if let Some(registered_program_pda) = ctx.accounts.registered_program_pda.as_ref() { if registered_program_pda.group_authority_pda != pubkey!("24rt4RgeyjUCWGS2eF7L7gyNMuz6JWdqYpAvb1KRoHxs") diff --git a/programs/account-compression/src/instructions/initialize_batched_state_merkle_tree.rs b/programs/account-compression/src/instructions/initialize_batched_state_merkle_tree.rs index 9ad0d5deb0..072ebac408 100644 --- a/programs/account-compression/src/instructions/initialize_batched_state_merkle_tree.rs +++ b/programs/account-compression/src/instructions/initialize_batched_state_merkle_tree.rs @@ -1,7 +1,6 @@ use anchor_lang::{prelude::*, solana_program::program_error::ProgramError}; use light_batched_merkle_tree::initialize_state_tree::{ - init_batched_state_merkle_tree_from_account_info, validate_batched_tree_params, - InitStateTreeAccountsInstructionData, + init_batched_state_merkle_tree_from_account_info, InitStateTreeAccountsInstructionData, }; use super::RegisteredProgram; @@ -36,10 +35,9 @@ pub fn process_initialize_batched_state_merkle_tree<'info>( params: InitStateTreeAccountsInstructionData, ) -> Result<()> { #[cfg(feature = "test")] - validate_batched_tree_params(params); + light_batched_merkle_tree::initialize_state_tree::validate_batched_tree_params(params); #[cfg(not(feature = "test"))] { - // TODO: test that only security group 24rt4RgeyjUCWGS2eF7L7gyNMuz6JWdqYpAvb1KRoHxs can create batched state trees use crate::errors::AccountCompressionErrorCode; if params != InitStateTreeAccountsInstructionData::default() { return err!(AccountCompressionErrorCode::UnsupportedParameters); diff --git a/programs/account-compression/src/lib.rs b/programs/account-compression/src/lib.rs index f19d7c8245..1a50e7a2a9 100644 --- a/programs/account-compression/src/lib.rs +++ b/programs/account-compression/src/lib.rs @@ -289,9 +289,16 @@ pub mod account_compression { /// Migrate state from a v1 state Merkle tree /// to a v2 state Merkle tree. pub fn migrate_state<'a, 'b, 'c: 'info, 'info>( - ctx: Context<'a, 'b, 'c, 'info, MigrateState<'info>>, - input: MigrateLeafParams, + _ctx: Context<'a, 'b, 'c, 'info, MigrateState<'info>>, + _input: MigrateLeafParams, ) -> Result<()> { - process_migrate_state(&ctx, input) + #[cfg(feature = "migrate-state")] + { + process_migrate_state(&_ctx, _input) + } + #[cfg(not(feature = "migrate-state"))] + { + unimplemented!("migrate_state is disabled") + } } } diff --git a/programs/package.json b/programs/package.json index 79d1e557df..244d22318d 100644 --- a/programs/package.json +++ b/programs/package.json @@ -3,7 +3,7 @@ "version": "0.3.0", "license": "Apache-2.0", "scripts": { - "build": "cd system/ && cargo build-sbf && cd .. && cd account-compression/ && cargo build-sbf && cd .. && cd registry/ && cargo build-sbf && cd .. && cd compressed-token/program && cargo build-sbf && cd ../../..", + "build": "cd system/ && cargo build-sbf && cd .. && cd account-compression/ && cargo build-sbf --features 'test, migrate-state' && cd .. && cd registry/ && cargo build-sbf && cd .. && cd compressed-token/program && cargo build-sbf && cd ../../..", "build-compressed-token-small": "cd compressed-token && cargo build-sbf --features cpi-without-program-ids && cd ..", "build-system": "anchor build --program-name light_system_program -- --features idl-build custom-heap", "build-compressed-token": "anchor build --program-name light_compressed_token -- --features idl-build custom-heap", diff --git a/programs/registry/src/account_compression_cpi/rollover_batched_address_tree.rs b/programs/registry/src/account_compression_cpi/rollover_batched_address_tree.rs index 3d4a747894..f30f5dec6b 100644 --- a/programs/registry/src/account_compression_cpi/rollover_batched_address_tree.rs +++ b/programs/registry/src/account_compression_cpi/rollover_batched_address_tree.rs @@ -49,6 +49,6 @@ pub fn process_rollover_batched_address_merkle_tree( account_compression::cpi::rollover_batched_address_merkle_tree( cpi_ctx, - if_equals_zero_u64(ctx.accounts.protocol_config_pda.config.network_fee), + if_equals_zero_u64(ctx.accounts.protocol_config_pda.config.address_network_fee), ) } diff --git a/programs/registry/src/lib.rs b/programs/registry/src/lib.rs index 463017b1f8..d9740a32d5 100644 --- a/programs/registry/src/lib.rs +++ b/programs/registry/src/lib.rs @@ -291,22 +291,20 @@ pub mod light_registry { merkle_tree_config: AddressMerkleTreeConfig, queue_config: AddressQueueConfig, ) -> Result<()> { - // Program owned trees are disabled - if program_owner.is_some() { - msg!("Program owner must not be defined."); - return err!(RegistryError::ProgramOwnerDefined); + // Address V1 trees are deprecated. + // Disable creation of forested address V1 trees. + // All address V1 trees must be program owned. + // All address V1 trees must not have fees. + if program_owner.is_none() { + msg!("Program owner must be defined."); + return err!(RegistryError::ForesterUndefined); + } + if merkle_tree_config.network_fee.is_some() { + msg!("Network fee must be None."); + return err!(RegistryError::InvalidNetworkFee); } - // The network fee must be either zero or the same as the protocol config. // Only trees with a network fee will be serviced by light foresters. - if let Some(network_fee) = merkle_tree_config.network_fee { - if network_fee != ctx.accounts.protocol_config_pda.config.network_fee { - return err!(RegistryError::InvalidNetworkFee); - } - if forester.is_some() { - msg!("Forester pubkey must not be defined for trees serviced by light foresters."); - return err!(RegistryError::ForesterDefined); - } - } else if forester.is_none() { + if forester.is_none() { msg!("Forester pubkey required for trees without a network fee."); msg!("Trees without a network fee will not be serviced by light foresters."); return err!(RegistryError::ForesterUndefined); @@ -591,7 +589,12 @@ pub mod light_registry { return err!(RegistryError::ProgramOwnerDefined); } if let Some(network_fee) = params.network_fee { - if network_fee != ctx.accounts.protocol_config_pda.config.network_fee { + if network_fee != ctx.accounts.protocol_config_pda.config.address_network_fee { + msg!( + "ctx.accounts.protocol_config_pda.config.address_network_fee {:?}", + ctx.accounts.protocol_config_pda.config.address_network_fee + ); + msg!("network_fee {:?}", network_fee); return err!(RegistryError::InvalidNetworkFee); } if params.forester.is_some() { diff --git a/programs/registry/src/protocol_config/state.rs b/programs/registry/src/protocol_config/state.rs index 3c65b897ca..04ade39f1d 100644 --- a/programs/registry/src/protocol_config/state.rs +++ b/programs/registry/src/protocol_config/state.rs @@ -39,7 +39,7 @@ pub struct ProtocolConfig { pub finalize_counter_limit: u64, /// Placeholder for future protocol updates. pub place_holder: Pubkey, - pub place_holder_a: u64, + pub address_network_fee: u64, pub place_holder_b: u64, pub place_holder_c: u64, pub place_holder_d: u64, @@ -60,7 +60,7 @@ impl Default for ProtocolConfig { cpi_context_size: 20 * 1024 + 8, finalize_counter_limit: 100, place_holder: Pubkey::default(), - place_holder_a: 0, + address_network_fee: 10000, place_holder_b: 0, place_holder_c: 0, place_holder_d: 0, @@ -83,7 +83,7 @@ impl ProtocolConfig { cpi_context_size: 20 * 1024 + 8, finalize_counter_limit: 100, place_holder: Pubkey::default(), - place_holder_a: 0, + address_network_fee: 10000, place_holder_b: 0, place_holder_c: 0, place_holder_d: 0, diff --git a/programs/system/Cargo.toml b/programs/system/Cargo.toml index 9817ee655f..9f4924e945 100644 --- a/programs/system/Cargo.toml +++ b/programs/system/Cargo.toml @@ -22,11 +22,9 @@ mem-profiling = [] # 3. Recompile program WITHOUT reinit feature # 4. Redeploy program to remove migration code reinit = [] -default = ["debug", "readonly", "reinit"] +default = ["reinit"] test-sbf = [] -debug = [] readonly = [] -deactivate-cpi-context = [] profile-program = ["light-program-profiler/profile-program"] profile-heap = ["light-program-profiler/profile-heap", "dep:light-heap"] custom-heap = [] diff --git a/programs/system/src/accounts/remaining_account_checks.rs b/programs/system/src/accounts/remaining_account_checks.rs index 7373683a35..e5d1b1165c 100644 --- a/programs/system/src/accounts/remaining_account_checks.rs +++ b/programs/system/src/accounts/remaining_account_checks.rs @@ -125,12 +125,12 @@ pub(crate) fn try_from_account_info<'a, 'info: 'a>( let merkle_tree = bytemuck::from_bytes::( &data[8..StateMerkleTreeAccount::LEN], ); - context.set_network_fee(merkle_tree.metadata.rollover_metadata.network_fee, index); context.set_legacy_merkle_context( index, MerkleTreeContext { rollover_fee: merkle_tree.metadata.rollover_metadata.rollover_fee, hashed_pubkey: hash_to_bn254_field_size_be(account_info.key().as_slice()), + network_fee: merkle_tree.metadata.rollover_metadata.network_fee, }, ); @@ -164,8 +164,14 @@ pub(crate) fn try_from_account_info<'a, 'info: 'a>( let merkle_tree = bytemuck::from_bytes::( &data[8..AddressMerkleTreeAccount::LEN], ); - - context.set_address_fee(merkle_tree.metadata.rollover_metadata.network_fee, index); + context.set_legacy_merkle_context( + index, + MerkleTreeContext { + rollover_fee: merkle_tree.metadata.rollover_metadata.rollover_fee, + hashed_pubkey: [0u8; 32], // not used for address trees + network_fee: merkle_tree.metadata.rollover_metadata.network_fee, + }, + ); merkle_tree.metadata.access_metadata.program_owner }; let mut merkle_tree = account_info @@ -197,6 +203,7 @@ pub(crate) fn try_from_account_info<'a, 'info: 'a>( MerkleTreeContext { rollover_fee: queue.metadata.rollover_metadata.rollover_fee, hashed_pubkey: [0u8; 32], // not used for address trees + network_fee: queue.metadata.rollover_metadata.network_fee, }, ); diff --git a/programs/system/src/context.rs b/programs/system/src/context.rs index 2b581f20d0..8cb2586fab 100644 --- a/programs/system/src/context.rs +++ b/programs/system/src/context.rs @@ -9,7 +9,10 @@ use light_compressed_account::{ }, }; use light_program_profiler::profile; -use pinocchio::{account_info::AccountInfo, instruction::AccountMeta, pubkey::Pubkey}; +use pinocchio::{ + account_info::AccountInfo, instruction::AccountMeta, program_error::ProgramError, + pubkey::Pubkey, +}; use solana_msg::msg; use crate::{ @@ -28,7 +31,6 @@ pub struct SystemContext<'info> { pub addresses: Vec>, // Index of account and fee to be paid. pub rollover_fee_payments: Vec<(u8, u64)>, - pub address_fee_is_set: bool, pub network_fee_is_set: bool, pub legacy_merkle_context: Vec<(u8, MerkleTreeContext)>, pub invoking_program_id: Option, @@ -38,6 +40,7 @@ pub struct SystemContext<'info> { pub struct MerkleTreeContext { pub rollover_fee: u64, pub hashed_pubkey: [u8; 32], + pub network_fee: u64, } impl SystemContext<'_> { @@ -53,21 +56,42 @@ impl SystemContext<'_> { } #[profile] - pub fn set_address_fee(&mut self, fee: u64, index: u8) { - if !self.address_fee_is_set { - self.address_fee_is_set = true; - self.rollover_fee_payments.push((index, fee)); - } + pub fn set_address_fee(&mut self, fee: u64, index: u8) -> Result<()> { + let payment = self.rollover_fee_payments.iter_mut().find(|a| a.0 == index); + match payment { + Some(payment) => { + payment.1 = payment + .1 + .checked_add(fee) + .ok_or(ProgramError::ArithmeticOverflow)?; + } + None => self.rollover_fee_payments.push((index, fee)), + }; + Ok(()) } - #[profile] - pub fn set_network_fee(&mut self, fee: u64, index: u8) { + pub fn set_network_fee_v2(&mut self, fee: u64, index: u8) { if !self.network_fee_is_set { self.network_fee_is_set = true; self.rollover_fee_payments.push((index, fee)); } } + #[profile] + pub fn set_network_fee_v1(&mut self, fee: u64, index: u8) -> Result<()> { + let payment = self.rollover_fee_payments.iter_mut().find(|a| a.0 == index); + match payment { + Some(payment) => { + payment.1 = payment + .1 + .checked_add(fee) + .ok_or(ProgramError::ArithmeticOverflow)?; + } + None => self.rollover_fee_payments.push((index, fee)), + }; + Ok(()) + } + pub fn get_or_hash_pubkey(&mut self, pubkey: Pubkey) -> [u8; 32] { let hashed_pubkey = self .hashed_pubkeys @@ -139,14 +163,18 @@ impl<'info> SystemContext<'info> { } /// Network fee distribution: - /// - if any account is created or modified -> transfer network fee (5000 lamports) - /// (Previously we didn't charge for appends now we have to since values go into a queue.) - /// - if an address is created -> transfer an additional network fee (5000 lamports) + /// - V1 state trees: charge per input (5000 lamports × num_inputs) + /// - V2 batched state trees: charge once per tree if inputs > 0 (5000 lamports) + /// - Address creation: charge per address (10000 lamports × num_addresses) + /// + /// Examples (V1 state trees): + /// 1. create account with 1 address, 0 inputs: network fee 10,000 lamports + /// 2. token transfer (1 input, 1 output): network fee 5,000 lamports + /// 3. transfer with 2 V1 inputs, 1 address: network fee 20,000 lamports (2×5k + 1×10k) /// - /// Examples: - /// 1. create account with address network fee 10,000 lamports - /// 2. token transfer network fee 5,000 lamports - /// 3. mint token network fee 5,000 lamports + /// Examples (V2 batched state trees): + /// 1. token transfer (1 input, 1 output): network fee 5,000 lamports (once per tree) + /// 2. transfer with 2 V2 inputs, 1 address: network fee 15,000 lamports (5k + 1×10k) /// Transfers rollover and network fees. pub fn transfer_fees(&self, accounts: &[AccountInfo], fee_payer: &AccountInfo) -> Result<()> { for (i, fee) in self.rollover_fee_payments.iter() { diff --git a/programs/system/src/processor/cpi.rs b/programs/system/src/processor/cpi.rs index e7a35652d7..263c0168ba 100644 --- a/programs/system/src/processor/cpi.rs +++ b/programs/system/src/processor/cpi.rs @@ -70,7 +70,6 @@ pub fn create_cpi_data_and_context<'info, A: InvokeAccounts<'info> + SignerAccou hashed_pubkeys: Vec::with_capacity(hashed_pubkeys_capacity), addresses: Vec::with_capacity((num_nullifiers + num_new_addresses) as usize), rollover_fee_payments: Vec::new(), - address_fee_is_set: false, network_fee_is_set: false, legacy_merkle_context: Vec::new(), invoking_program_id, diff --git a/programs/system/src/processor/create_address_cpi_data.rs b/programs/system/src/processor/create_address_cpi_data.rs index 5774677164..201a5a5de9 100644 --- a/programs/system/src/processor/create_address_cpi_data.rs +++ b/programs/system/src/processor/create_address_cpi_data.rs @@ -40,6 +40,17 @@ pub fn derive_new_addresses<'info, 'a, 'b: 'a, const ADDRESS_ASSIGNMENT: bool>( "V1 address tree", )?; + let mut network_fee = context + .get_legacy_merkle_context( + new_address_params.address_merkle_tree_account_index(), + ) + .unwrap() + .network_fee; + if network_fee != 0 { + network_fee += 5000; + } + context.set_address_fee(network_fee, new_address_params.address_queue_index())?; + ( derive_address_legacy(pubkey, &new_address_params.seed()) .map_err(ProgramError::from)?, @@ -67,7 +78,7 @@ pub fn derive_new_addresses<'info, 'a, 'b: 'a, const ADDRESS_ASSIGNMENT: bool>( context.set_address_fee( tree.metadata.rollover_metadata.network_fee, new_address_params.address_merkle_tree_account_index(), - ); + )?; cpi_ix_data.insert_address_sequence_number( &mut seq_index, diff --git a/programs/system/src/processor/create_inputs_cpi_data.rs b/programs/system/src/processor/create_inputs_cpi_data.rs index 1c5d683532..8fe0b29de6 100644 --- a/programs/system/src/processor/create_inputs_cpi_data.rs +++ b/programs/system/src/processor/create_inputs_cpi_data.rs @@ -59,7 +59,7 @@ pub fn create_inputs_cpi_data<'a, 'info, T: InstructionData<'a>>( .ok_or(SystemProgramError::InputMerkleTreeIndexOutOfBounds)? { AcpAccount::BatchedStateTree(tree) => { - context.set_network_fee( + context.set_network_fee_v2( tree.metadata.rollover_metadata.network_fee, current_mt_index, ); @@ -76,10 +76,12 @@ pub fn create_inputs_cpi_data<'a, 'info, T: InstructionData<'a>>( } AcpAccount::StateTree(_) => { is_batched = false; - context - .get_legacy_merkle_context(current_mt_index) - .unwrap() - .hashed_pubkey + let legacy_context = + context.get_legacy_merkle_context(current_mt_index).unwrap(); + let network_fee = legacy_context.network_fee; + let hashed_pubkey = legacy_context.hashed_pubkey; + context.set_network_fee_v1(network_fee, current_mt_index)?; + hashed_pubkey } _ => { msg!(format!("create_inputs_cpi_data {} ", current_mt_index).as_str()); diff --git a/programs/system/src/processor/create_outputs_cpi_data.rs b/programs/system/src/processor/create_outputs_cpi_data.rs index 84fc7a3b88..72a656c75f 100644 --- a/programs/system/src/processor/create_outputs_cpi_data.rs +++ b/programs/system/src/processor/create_outputs_cpi_data.rs @@ -72,7 +72,7 @@ pub fn create_outputs_cpi_data<'a, 'info, T: InstructionData<'a>>( .ok_or(SystemProgramError::OutputMerkleTreeIndexOutOfBounds)? { AcpAccount::OutputQueue(output_queue) => { - context.set_network_fee( + context.set_network_fee_v2( output_queue.metadata.rollover_metadata.network_fee, current_index as u8, ); diff --git a/sdk-libs/client/src/fee.rs b/sdk-libs/client/src/fee.rs index f0bbc5e1c1..15d1587a98 100644 --- a/sdk-libs/client/src/fee.rs +++ b/sdk-libs/client/src/fee.rs @@ -12,6 +12,7 @@ pub struct FeeConfig { // pub address_tree_configs: Vec, pub network_fee: u64, pub address_network_fee: u64, + pub batch_address_network_fee: u64, pub solana_network_fee: i64, } @@ -25,7 +26,8 @@ impl Default for FeeConfig { // state_tree_configs: vec![StateMerkleTreeConfig::default()], // address_tree_configs: vec![AddressMerkleTreeConfig::default()], network_fee: 5000, - address_network_fee: 5000, + address_network_fee: 10000, + batch_address_network_fee: 10_000, solana_network_fee: 5000, } } @@ -38,7 +40,8 @@ impl FeeConfig { state_merkle_tree_rollover: 1, address_queue_rollover: 392, // not batched network_fee: 5000, - address_network_fee: 5000, + address_network_fee: 10000, + batch_address_network_fee: 10_000, solana_network_fee: 5000, } } @@ -46,7 +49,8 @@ impl FeeConfig { #[derive(Debug, Clone, PartialEq)] pub struct TransactionParams { - pub num_input_compressed_accounts: u8, + pub v1_input_compressed_accounts: u8, + pub v2_input_compressed_accounts: bool, pub num_output_compressed_accounts: u8, pub num_new_addresses: u8, pub compress: i64, @@ -65,15 +69,20 @@ pub async fn assert_transaction_params( deduped_signers.dedup(); let post_balance = rpc.get_account(*payer).await?.unwrap().lamports; - // a network_fee is charged if there are input compressed accounts or new addresses + // Network fee is charged per input and per address let mut network_fee: i64 = 0; - if transaction_params.num_input_compressed_accounts != 0 - || transaction_params.num_output_compressed_accounts != 0 - { + + // Charge per input compressed account + if transaction_params.v1_input_compressed_accounts != 0 { + network_fee += transaction_params.fee_config.network_fee as i64 + * transaction_params.v1_input_compressed_accounts as i64; + } else if transaction_params.v2_input_compressed_accounts { network_fee += transaction_params.fee_config.network_fee as i64; } + // Charge per address created if transaction_params.num_new_addresses != 0 { - network_fee += transaction_params.fee_config.address_network_fee as i64; + network_fee += transaction_params.fee_config.address_network_fee as i64 + * transaction_params.num_new_addresses as i64; } let expected_post_balance = pre_balance as i64 - i64::from(transaction_params.num_new_addresses) @@ -103,6 +112,10 @@ pub async fn assert_transaction_params( ); println!("network_fee: {}", network_fee); println!("num signers {}", deduped_signers.len()); + println!( + "transaction_params.fee_config {:?}", + transaction_params.fee_config + ); return Err(RpcError::CustomError("Transaction fee error.".to_string())); } } diff --git a/sdk-libs/program-test/src/accounts/initialize.rs b/sdk-libs/program-test/src/accounts/initialize.rs index 443c0cfbea..37700d2393 100644 --- a/sdk-libs/program-test/src/accounts/initialize.rs +++ b/sdk-libs/program-test/src/accounts/initialize.rs @@ -26,7 +26,6 @@ use super::{ use crate::accounts::register_program::register_program_with_registry_program; use crate::{ accounts::{ - address_tree::create_address_merkle_tree_and_queue_account, state_tree::create_state_merkle_tree_and_queue_account, test_accounts::{ProtocolAccounts, StateMerkleTreeAccountsV2, TestAccounts}, test_keypairs::*, @@ -50,8 +49,6 @@ pub async fn initialize_accounts( skip_second_v1_tree, v1_state_tree_config, v1_nullifier_queue_config, - v1_address_tree_config, - v1_address_queue_config, .. } = config; let _v2_address_tree_config = v2_address_tree_config; @@ -151,19 +148,22 @@ pub async fn initialize_accounts( .await?; } - create_address_merkle_tree_and_queue_account( - &keypairs.governance_authority, - true, - context, - &keypairs.address_merkle_tree, - &keypairs.address_merkle_tree_queue, - None, - None, - v1_address_tree_config, - v1_address_queue_config, - 0, - ) - .await?; + // V1 address trees are now loaded from JSON files instead of created fresh + // to avoid registry validation issues with deprecated V1 address trees. + // See light_program_test.rs for the JSON loading logic. + // create_address_merkle_tree_and_queue_account( + // &keypairs.governance_authority, + // true, + // context, + // &keypairs.address_merkle_tree, + // &keypairs.address_merkle_tree_queue, + // None, + // None, + // v1_address_tree_config, + // v1_address_queue_config, + // 0, + // ) + // .await?; } #[cfg(feature = "v2")] if let Some(v2_state_tree_config) = _v2_state_tree_config { diff --git a/sdk-libs/program-test/src/indexer/test_indexer.rs b/sdk-libs/program-test/src/indexer/test_indexer.rs index 985c9f2e79..0a91049ba3 100644 --- a/sdk-libs/program-test/src/indexer/test_indexer.rs +++ b/sdk-libs/program-test/src/indexer/test_indexer.rs @@ -1387,6 +1387,17 @@ impl TestIndexer { queue_keypair: &Keypair, owning_program_id: Option, ) -> Result { + use crate::accounts::test_keypairs::FORESTER_TEST_KEYPAIR; + + let config = if owning_program_id.is_some() { + // We only allow program owned address trees with custom fees. + AddressMerkleTreeConfig { + network_fee: None, + ..AddressMerkleTreeConfig::default() + } + } else { + AddressMerkleTreeConfig::default() + }; create_address_merkle_tree_and_queue_account( &self.payer, true, @@ -1394,8 +1405,12 @@ impl TestIndexer { merkle_tree_keypair, queue_keypair, owning_program_id, - None, - &AddressMerkleTreeConfig::default(), + Some( + Keypair::try_from(FORESTER_TEST_KEYPAIR.as_slice()) + .unwrap() + .pubkey(), + ), // std forester, we now need to set it. + &config, &AddressQueueConfig::default(), 0, ) diff --git a/sdk-libs/program-test/src/program_test/light_program_test.rs b/sdk-libs/program-test/src/program_test/light_program_test.rs index b3ccd94f89..86fe5e824d 100644 --- a/sdk-libs/program-test/src/program_test/light_program_test.rs +++ b/sdk-libs/program-test/src/program_test/light_program_test.rs @@ -1,7 +1,7 @@ use std::fmt::{self, Debug, Formatter}; #[cfg(feature = "devenv")] -use account_compression::{AddressMerkleTreeAccount, QueueAccount}; +use account_compression::QueueAccount; use light_client::{ indexer::{AddressMerkleTreeAccounts, StateMerkleTreeAccounts}, rpc::{merkle_tree::MerkleTreeExt, RpcError}, @@ -109,29 +109,45 @@ impl LightProgramTest { let test_accounts = context.test_accounts.clone(); context.add_indexer(&test_accounts, batch_size).await?; - // ensure that address tree pubkey is amt1Ayt45jfbdw5YSo7iz6WZxUmnZsQTYXy82hVwyC2 + // Load V1 address tree accounts from JSON files { + use crate::utils::load_accounts::load_account_from_dir; + + if context.test_accounts.v1_address_trees.len() != 1 { + return Err(RpcError::CustomError(format!( + "Expected exactly 1 V1 address tree, found {}. V1 address trees are deprecated and only one is supported.", + context.test_accounts.v1_address_trees.len() + ))); + } + let address_mt = context.test_accounts.v1_address_trees[0].merkle_tree; let address_queue_pubkey = context.test_accounts.v1_address_trees[0].queue; - let mut account = context - .context - .get_account(&keypairs.address_merkle_tree.pubkey()) - .unwrap(); - let merkle_tree_account = bytemuck::from_bytes_mut::( - &mut account.data_as_mut_slice()[8..AddressMerkleTreeAccount::LEN], - ); - merkle_tree_account.metadata.associated_queue = address_queue_pubkey.into(); - context.set_account(address_mt, account); - let mut account = context + let tree_account = + load_account_from_dir(&address_mt, Some("address_merkle_tree"))?; + context + .context + .set_account(address_mt, tree_account) + .map_err(|e| { + RpcError::CustomError(format!( + "Failed to set V1 address tree account: {}", + e + )) + })?; + + let queue_account = load_account_from_dir( + &address_queue_pubkey, + Some("address_merkle_tree_queue"), + )?; + context .context - .get_account(&keypairs.address_merkle_tree_queue.pubkey()) - .unwrap(); - let queue_account = bytemuck::from_bytes_mut::( - &mut account.data_as_mut_slice()[8..QueueAccount::LEN], - ); - queue_account.metadata.associated_merkle_tree = address_mt.into(); - context.set_account(address_queue_pubkey, account); + .set_account(address_queue_pubkey, queue_account) + .map_err(|e| { + RpcError::CustomError(format!( + "Failed to set V1 address queue account: {}", + e + )) + })?; } } // Copy v1 state merkle tree accounts to devnet pubkeys diff --git a/sdk-libs/program-test/src/utils/load_accounts.rs b/sdk-libs/program-test/src/utils/load_accounts.rs index e6dbe4d92d..e6ad41837e 100644 --- a/sdk-libs/program-test/src/utils/load_accounts.rs +++ b/sdk-libs/program-test/src/utils/load_accounts.rs @@ -138,3 +138,58 @@ pub fn load_all_accounts_from_dir() -> Result, RpcError Ok(accounts) } + +/// Load a specific account by pubkey from the accounts directory +/// Optionally provide a prefix for the filename (e.g. "address_merkle_tree") +pub fn load_account_from_dir(pubkey: &Pubkey, prefix: Option<&str>) -> Result { + let accounts_dir = find_accounts_dir().ok_or_else(|| { + RpcError::CustomError( + "Failed to find accounts directory. Make sure light CLI is installed.".to_string(), + ) + })?; + + let filename = if let Some(prefix) = prefix { + format!("{}_{}.json", prefix, pubkey) + } else { + format!("{}.json", pubkey) + }; + let path = accounts_dir.join(&filename); + + let contents = fs::read_to_string(&path).map_err(|e| { + RpcError::CustomError(format!("Failed to read account file {:?}: {}", path, e)) + })?; + + let account_data: AccountData = serde_json::from_str(&contents).map_err(|e| { + RpcError::CustomError(format!( + "Failed to parse account JSON from {:?}: {}", + path, e + )) + })?; + + let owner = account_data + .account + .owner + .parse::() + .map_err(|e| RpcError::CustomError(format!("Invalid owner pubkey: {}", e)))?; + + // Decode base64 data + let data = if account_data.account.data.1 == "base64" { + use base64::{engine::general_purpose, Engine as _}; + general_purpose::STANDARD + .decode(&account_data.account.data.0) + .map_err(|e| RpcError::CustomError(format!("Failed to decode base64 data: {}", e)))? + } else { + return Err(RpcError::CustomError(format!( + "Unsupported encoding: {}", + account_data.account.data.1 + ))); + }; + + Ok(Account { + lamports: account_data.account.lamports, + data, + owner, + executable: account_data.account.executable, + rent_epoch: account_data.account.rent_epoch, + }) +} From 0656cb86f198f2f2a82f400565db90d5428c89b9 Mon Sep 17 00:00:00 2001 From: ananas Date: Sat, 25 Oct 2025 23:39:22 +0100 Subject: [PATCH 6/9] cleanup --- js/stateless.js/src/constants.ts | 2 +- js/stateless.js/tests/e2e/compress.test.ts | 21 +++++++++++++------ js/stateless.js/tests/e2e/test-rpc.test.ts | 5 ++++- .../initialize_batched_address_merkle_tree.rs | 1 - programs/registry/src/lib.rs | 6 +++--- programs/system/src/context.rs | 8 ++++--- 6 files changed, 28 insertions(+), 15 deletions(-) diff --git a/js/stateless.js/src/constants.ts b/js/stateless.js/src/constants.ts index 0b1d1ae301..3bac7ec7e3 100644 --- a/js/stateless.js/src/constants.ts +++ b/js/stateless.js/src/constants.ts @@ -358,4 +358,4 @@ export const STATE_MERKLE_TREE_NETWORK_FEE = new BN(5000); /** * Is charged if the transaction creates at least one address. */ -export const ADDRESS_TREE_NETWORK_FEE = new BN(5000); +export const ADDRESS_TREE_NETWORK_FEE = new BN(10000); diff --git a/js/stateless.js/tests/e2e/compress.test.ts b/js/stateless.js/tests/e2e/compress.test.ts index 843db5492c..2a2227c5c5 100644 --- a/js/stateless.js/tests/e2e/compress.test.ts +++ b/js/stateless.js/tests/e2e/compress.test.ts @@ -5,6 +5,7 @@ import { ADDRESS_QUEUE_ROLLOVER_FEE, STATE_MERKLE_TREE_ROLLOVER_FEE, ADDRESS_TREE_NETWORK_FEE, + featureFlags, } from '../../src/constants'; import { newAccountWithLamports } from '../../src/test-helpers/test-utils'; import { Rpc } from '../../src/rpc'; @@ -44,11 +45,18 @@ function txFees( : bn(0); /// Fee if the tx nullifies at least one input account - const networkInFee = - tx.in || tx.out ? STATE_MERKLE_TREE_NETWORK_FEE : bn(0); - - /// Fee if the tx creates at least one address - const networkAddressFee = tx.addr ? ADDRESS_TREE_NETWORK_FEE : bn(0); + const networkInFee = tx.in + ? featureFlags.isV2() + ? STATE_MERKLE_TREE_NETWORK_FEE + : STATE_MERKLE_TREE_NETWORK_FEE.mul(bn(tx.in)) + : tx.out && featureFlags.isV2() + ? STATE_MERKLE_TREE_NETWORK_FEE + : bn(0); + + /// Network fee charged per address created + const networkAddressFee = tx.addr + ? ADDRESS_TREE_NETWORK_FEE.mul(bn(tx.addr)) + : bn(0); totalFee = totalFee.add( solanaBaseFee @@ -230,9 +238,10 @@ describe('compress', () => { ); const postCreateAccountBalance = await rpc.getBalance(payer.publicKey); + let expectedTxFees = txFees([{ in: 1, out: 2, addr: 1 }]); assert.equal( postCreateAccountBalance, - postCompressBalance - txFees([{ in: 1, out: 2, addr: 1 }]), + postCompressBalance - expectedTxFees, ); }); diff --git a/js/stateless.js/tests/e2e/test-rpc.test.ts b/js/stateless.js/tests/e2e/test-rpc.test.ts index 09dfdd58c3..6a5b137299 100644 --- a/js/stateless.js/tests/e2e/test-rpc.test.ts +++ b/js/stateless.js/tests/e2e/test-rpc.test.ts @@ -60,12 +60,15 @@ describe('test-rpc', () => { assert.equal(compressedTestAccount.data?.data, null); postCompressBalance = await rpc.getBalance(payer.publicKey); + let expectedFee = featureFlags.isV2() + ? STATE_MERKLE_TREE_NETWORK_FEE.toNumber() + : 0; assert.equal( postCompressBalance, preCompressBalance - compressLamportsAmount - 5000 - - 5000 - + expectedFee - STATE_MERKLE_TREE_ROLLOVER_FEE.toNumber(), ); }); diff --git a/programs/account-compression/src/instructions/initialize_batched_address_merkle_tree.rs b/programs/account-compression/src/instructions/initialize_batched_address_merkle_tree.rs index 116b55c06f..4ad9e445ee 100644 --- a/programs/account-compression/src/instructions/initialize_batched_address_merkle_tree.rs +++ b/programs/account-compression/src/instructions/initialize_batched_address_merkle_tree.rs @@ -48,7 +48,6 @@ pub fn process_initialize_batched_address_merkle_tree<'info>( if params != InitAddressTreeAccountsInstructionData::default() { return err!(AccountCompressionErrorCode::UnsupportedParameters); } - // TODO: test that only security group 24rt4RgeyjUCWGS2eF7L7gyNMuz6JWdqYpAvb1KRoHxs can create batched state trees if let Some(registered_program_pda) = ctx.accounts.registered_program_pda.as_ref() { if registered_program_pda.group_authority_pda != pubkey!("24rt4RgeyjUCWGS2eF7L7gyNMuz6JWdqYpAvb1KRoHxs") diff --git a/programs/registry/src/lib.rs b/programs/registry/src/lib.rs index d9740a32d5..9ce7632d8a 100644 --- a/programs/registry/src/lib.rs +++ b/programs/registry/src/lib.rs @@ -292,9 +292,9 @@ pub mod light_registry { queue_config: AddressQueueConfig, ) -> Result<()> { // Address V1 trees are deprecated. - // Disable creation of forested address V1 trees. - // All address V1 trees must be program owned. - // All address V1 trees must not have fees. + // Creation of forested address V1 trees is disabled. + // New address V1 trees must be program owned. + // New address V1 trees must not have fees. if program_owner.is_none() { msg!("Program owner must be defined."); return err!(RegistryError::ForesterUndefined); diff --git a/programs/system/src/context.rs b/programs/system/src/context.rs index 8cb2586fab..4e37e2efd4 100644 --- a/programs/system/src/context.rs +++ b/programs/system/src/context.rs @@ -164,7 +164,7 @@ impl<'info> SystemContext<'info> { /// Network fee distribution: /// - V1 state trees: charge per input (5000 lamports × num_inputs) - /// - V2 batched state trees: charge once per tree if inputs > 0 (5000 lamports) + /// - V2 batched state trees: charge once per tree if inputs > 0 OR outputs > 0 (5000 lamports) /// - Address creation: charge per address (10000 lamports × num_addresses) /// /// Examples (V1 state trees): @@ -173,8 +173,10 @@ impl<'info> SystemContext<'info> { /// 3. transfer with 2 V1 inputs, 1 address: network fee 20,000 lamports (2×5k + 1×10k) /// /// Examples (V2 batched state trees): - /// 1. token transfer (1 input, 1 output): network fee 5,000 lamports (once per tree) - /// 2. transfer with 2 V2 inputs, 1 address: network fee 15,000 lamports (5k + 1×10k) + /// 1. token transfer (1 input, 0 output): network fee 5,000 lamports (once per tree) + /// 2. token transfer (0 input, 1 output): network fee 5,000 lamports (once per tree) + /// 3. token transfer (1 input, 1 output): network fee 5,000 lamports (once per tree) + /// 4. transfer with 2 V2 inputs, 1 address: network fee 15,000 lamports (5k + 1×10k) /// Transfers rollover and network fees. pub fn transfer_fees(&self, accounts: &[AccountInfo], fee_payer: &AccountInfo) -> Result<()> { for (i, fee) in self.rollover_fee_payments.iter() { From 894f472fbc2d8446a4a744066b0944d586395b32 Mon Sep 17 00:00:00 2001 From: ananas Date: Sun, 26 Oct 2025 00:15:00 +0100 Subject: [PATCH 7/9] chore: fix nits --- programs/registry/src/errors.rs | 1 + programs/registry/src/lib.rs | 14 +++++++------- programs/system/src/errors.rs | 3 +++ .../src/processor/create_address_cpi_data.rs | 4 ++-- .../system/src/processor/create_inputs_cpi_data.rs | 5 +++-- .../src/processor/create_outputs_cpi_data.rs | 2 +- sdk-libs/client/src/fee.rs | 3 --- 7 files changed, 17 insertions(+), 15 deletions(-) diff --git a/programs/registry/src/errors.rs b/programs/registry/src/errors.rs index 165ef00f6f..4cf821386d 100644 --- a/programs/registry/src/errors.rs +++ b/programs/registry/src/errors.rs @@ -31,4 +31,5 @@ pub enum RegistryError { #[msg("Insufficient funds in pool")] InsufficientFunds, ProgramOwnerDefined, + ProgramOwnerUndefined, } diff --git a/programs/registry/src/lib.rs b/programs/registry/src/lib.rs index 9ce7632d8a..8d30c65d6d 100644 --- a/programs/registry/src/lib.rs +++ b/programs/registry/src/lib.rs @@ -292,21 +292,21 @@ pub mod light_registry { queue_config: AddressQueueConfig, ) -> Result<()> { // Address V1 trees are deprecated. - // Creation of forested address V1 trees is disabled. - // New address V1 trees must be program owned. - // New address V1 trees must not have fees. + // Light foresters (fee-based) are disabled for address V1 trees. + // New address V1 trees must be program owned with a designated forester. + // New address V1 trees must not have network fees. if program_owner.is_none() { msg!("Program owner must be defined."); - return err!(RegistryError::ForesterUndefined); + return err!(RegistryError::ProgramOwnerUndefined); } if merkle_tree_config.network_fee.is_some() { msg!("Network fee must be None."); return err!(RegistryError::InvalidNetworkFee); } - // Only trees with a network fee will be serviced by light foresters. + // A designated program-owned forester is required for address V1 trees. + // Light foresters (fee-based) will not service address V1 trees. if forester.is_none() { - msg!("Forester pubkey required for trees without a network fee."); - msg!("Trees without a network fee will not be serviced by light foresters."); + msg!("Forester pubkey required for program-owned trees."); return err!(RegistryError::ForesterUndefined); } // Unused parameter diff --git a/programs/system/src/errors.rs b/programs/system/src/errors.rs index b69ae8af1c..4f53447bfe 100644 --- a/programs/system/src/errors.rs +++ b/programs/system/src/errors.rs @@ -140,6 +140,8 @@ pub enum SystemProgramError { PackedAccountIndexOutOfBounds, #[error("Unimplemented.")] Unimplemented, + #[error("Missing legacy Merkle tree context")] + MissingLegacyMerkleContext, #[error("Batched Merkle tree error {0}")] BatchedMerkleTreeError(#[from] BatchedMerkleTreeError), #[error("Concurrent Merkle tree error {0}")] @@ -223,6 +225,7 @@ impl From for u32 { SystemProgramError::Unimplemented => 6063, SystemProgramError::CpiContextDeactivated => 6064, SystemProgramError::InputMerkleTreeIndexOutOfBounds => 6065, + SystemProgramError::MissingLegacyMerkleContext => 6066, SystemProgramError::BatchedMerkleTreeError(e) => e.into(), SystemProgramError::IndexedMerkleTreeError(e) => e.into(), SystemProgramError::ConcurrentMerkleTreeError(e) => e.into(), diff --git a/programs/system/src/processor/create_address_cpi_data.rs b/programs/system/src/processor/create_address_cpi_data.rs index 201a5a5de9..c852897875 100644 --- a/programs/system/src/processor/create_address_cpi_data.rs +++ b/programs/system/src/processor/create_address_cpi_data.rs @@ -44,7 +44,7 @@ pub fn derive_new_addresses<'info, 'a, 'b: 'a, const ADDRESS_ASSIGNMENT: bool>( .get_legacy_merkle_context( new_address_params.address_merkle_tree_account_index(), ) - .unwrap() + .ok_or(SystemProgramError::MissingLegacyMerkleContext)? .network_fee; if network_fee != 0 { network_fee += 5000; @@ -56,7 +56,7 @@ pub fn derive_new_addresses<'info, 'a, 'b: 'a, const ADDRESS_ASSIGNMENT: bool>( .map_err(ProgramError::from)?, context .get_legacy_merkle_context(new_address_params.address_queue_index()) - .unwrap() + .ok_or(SystemProgramError::MissingLegacyMerkleContext)? .rollover_fee, ) } diff --git a/programs/system/src/processor/create_inputs_cpi_data.rs b/programs/system/src/processor/create_inputs_cpi_data.rs index 8fe0b29de6..44ed4dad11 100644 --- a/programs/system/src/processor/create_inputs_cpi_data.rs +++ b/programs/system/src/processor/create_inputs_cpi_data.rs @@ -76,8 +76,9 @@ pub fn create_inputs_cpi_data<'a, 'info, T: InstructionData<'a>>( } AcpAccount::StateTree(_) => { is_batched = false; - let legacy_context = - context.get_legacy_merkle_context(current_mt_index).unwrap(); + let legacy_context = context + .get_legacy_merkle_context(current_mt_index) + .ok_or(SystemProgramError::MissingLegacyMerkleContext)?; let network_fee = legacy_context.network_fee; let hashed_pubkey = legacy_context.hashed_pubkey; context.set_network_fee_v1(network_fee, current_mt_index)?; diff --git a/programs/system/src/processor/create_outputs_cpi_data.rs b/programs/system/src/processor/create_outputs_cpi_data.rs index 72a656c75f..eb06460432 100644 --- a/programs/system/src/processor/create_outputs_cpi_data.rs +++ b/programs/system/src/processor/create_outputs_cpi_data.rs @@ -100,7 +100,7 @@ pub fn create_outputs_cpi_data<'a, 'info, T: InstructionData<'a>>( }; let merkle_context = context .get_legacy_merkle_context(current_index as u8) - .unwrap(); + .ok_or(SystemProgramError::MissingLegacyMerkleContext)?; hashed_merkle_tree = merkle_context.hashed_pubkey; rollover_fee = merkle_context.rollover_fee; mt_next_index = tree.next_index() as u32; diff --git a/sdk-libs/client/src/fee.rs b/sdk-libs/client/src/fee.rs index 15d1587a98..7cd6dbbbb5 100644 --- a/sdk-libs/client/src/fee.rs +++ b/sdk-libs/client/src/fee.rs @@ -12,7 +12,6 @@ pub struct FeeConfig { // pub address_tree_configs: Vec, pub network_fee: u64, pub address_network_fee: u64, - pub batch_address_network_fee: u64, pub solana_network_fee: i64, } @@ -27,7 +26,6 @@ impl Default for FeeConfig { // address_tree_configs: vec![AddressMerkleTreeConfig::default()], network_fee: 5000, address_network_fee: 10000, - batch_address_network_fee: 10_000, solana_network_fee: 5000, } } @@ -41,7 +39,6 @@ impl FeeConfig { address_queue_rollover: 392, // not batched network_fee: 5000, address_network_fee: 10000, - batch_address_network_fee: 10_000, solana_network_fee: 5000, } } From 8633b7adf474c429a33eb59aa9891fdd1eac33ab Mon Sep 17 00:00:00 2001 From: ananas Date: Sun, 26 Oct 2025 01:13:39 +0100 Subject: [PATCH 8/9] fix test --- program-tests/registry-test/tests/tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/program-tests/registry-test/tests/tests.rs b/program-tests/registry-test/tests/tests.rs index b8b9cb3b59..1f8cf2d793 100644 --- a/program-tests/registry-test/tests/tests.rs +++ b/program-tests/registry-test/tests/tests.rs @@ -545,7 +545,7 @@ async fn test_initialize_protocol_config() { 0, ) .await; - assert_rpc_error(result, 3, RegistryError::ForesterUndefined.into()).unwrap(); + assert_rpc_error(result, 3, RegistryError::ProgramOwnerUndefined.into()).unwrap(); } // FAIL: initialize a Merkle tree with network fee != 0 { From f6c990e6b33b40f0321c2c9716d4783b9e639688 Mon Sep 17 00:00:00 2001 From: ananas Date: Sun, 26 Oct 2025 17:22:54 +0000 Subject: [PATCH 9/9] fix: impl feedback --- js/stateless.js/src/constants.ts | 9 +++++-- js/stateless.js/tests/e2e/compress.test.ts | 12 ++++++--- .../tests/address_merkle_tree_tests.rs | 4 +-- program-tests/utils/src/e2e_test_env.rs | 2 +- ...nitialize_address_merkle_tree_and_queue.rs | 2 +- programs/system/src/context.rs | 26 ++++++++----------- .../src/processor/create_address_cpi_data.rs | 5 +--- sdk-libs/client/src/fee.rs | 2 +- 8 files changed, 33 insertions(+), 29 deletions(-) diff --git a/js/stateless.js/src/constants.ts b/js/stateless.js/src/constants.ts index 3bac7ec7e3..6f358007d2 100644 --- a/js/stateless.js/src/constants.ts +++ b/js/stateless.js/src/constants.ts @@ -356,6 +356,11 @@ export const ADDRESS_QUEUE_ROLLOVER_FEE = featureFlags.isV2() export const STATE_MERKLE_TREE_NETWORK_FEE = new BN(5000); /** - * Is charged if the transaction creates at least one address. + * Is charged per address the transaction creates. */ -export const ADDRESS_TREE_NETWORK_FEE = new BN(10000); +export const ADDRESS_TREE_NETWORK_FEE_V1 = new BN(5000); + +/** + * Is charged per address the transaction creates. + */ +export const ADDRESS_TREE_NETWORK_FEE_V2 = new BN(10000); diff --git a/js/stateless.js/tests/e2e/compress.test.ts b/js/stateless.js/tests/e2e/compress.test.ts index 2a2227c5c5..2180325662 100644 --- a/js/stateless.js/tests/e2e/compress.test.ts +++ b/js/stateless.js/tests/e2e/compress.test.ts @@ -4,7 +4,8 @@ import { STATE_MERKLE_TREE_NETWORK_FEE, ADDRESS_QUEUE_ROLLOVER_FEE, STATE_MERKLE_TREE_ROLLOVER_FEE, - ADDRESS_TREE_NETWORK_FEE, + ADDRESS_TREE_NETWORK_FEE_V1, + ADDRESS_TREE_NETWORK_FEE_V2, featureFlags, } from '../../src/constants'; import { newAccountWithLamports } from '../../src/test-helpers/test-utils'; @@ -55,9 +56,14 @@ function txFees( /// Network fee charged per address created const networkAddressFee = tx.addr - ? ADDRESS_TREE_NETWORK_FEE.mul(bn(tx.addr)) + ? ADDRESS_TREE_NETWORK_FEE_V1.mul(bn(tx.addr)) : bn(0); - + // TODO: adapt once we use v2 address trees in tests. + // tx.addr + // ? featureFlags.isV2() + // ? ADDRESS_TREE_NETWORK_FEE_V2.mul(bn(tx.addr)) + // : ADDRESS_TREE_NETWORK_FEE_V1.mul(bn(tx.addr)) + // : bn(0); totalFee = totalFee.add( solanaBaseFee .add(stateOutFee) diff --git a/program-tests/account-compression-test/tests/address_merkle_tree_tests.rs b/program-tests/account-compression-test/tests/address_merkle_tree_tests.rs index 59ec911de7..1a67f70052 100644 --- a/program-tests/account-compression-test/tests/address_merkle_tree_tests.rs +++ b/program-tests/account-compression-test/tests/address_merkle_tree_tests.rs @@ -149,7 +149,7 @@ async fn test_address_queue_and_tree_functional_custom() { roots_size, canopy_depth: ADDRESS_MERKLE_TREE_CANOPY_DEPTH, address_changelog_size, - network_fee: Some(10000), + network_fee: Some(5000), rollover_threshold: Some(95), close_threshold: None, }, @@ -1143,7 +1143,7 @@ async fn update_address_merkle_tree_wrap_around_custom() { roots_size, canopy_depth: ADDRESS_MERKLE_TREE_CANOPY_DEPTH, address_changelog_size, - network_fee: Some(10000), + network_fee: Some(5000), rollover_threshold: Some(95), close_threshold: None, }, diff --git a/program-tests/utils/src/e2e_test_env.rs b/program-tests/utils/src/e2e_test_env.rs index 06387999e2..c912e7e2aa 100644 --- a/program-tests/utils/src/e2e_test_env.rs +++ b/program-tests/utils/src/e2e_test_env.rs @@ -1314,7 +1314,7 @@ where canopy_depth: 10, address_changelog_size: self.rng.gen_range(1..5000), rollover_threshold, - network_fee: Some(10000), + network_fee: Some(5000), close_threshold: None, // TODO: double check that close threshold cannot be set }, diff --git a/programs/account-compression/src/instructions/initialize_address_merkle_tree_and_queue.rs b/programs/account-compression/src/instructions/initialize_address_merkle_tree_and_queue.rs index 024a4d908b..69d08a27a9 100644 --- a/programs/account-compression/src/instructions/initialize_address_merkle_tree_and_queue.rs +++ b/programs/account-compression/src/instructions/initialize_address_merkle_tree_and_queue.rs @@ -41,7 +41,7 @@ impl Default for AddressMerkleTreeConfig { roots_size: ADDRESS_MERKLE_TREE_ROOTS, canopy_depth: ADDRESS_MERKLE_TREE_CANOPY_DEPTH, address_changelog_size: ADDRESS_MERKLE_TREE_INDEXED_CHANGELOG, - network_fee: Some(10000), + network_fee: Some(5000), rollover_threshold: Some(95), close_threshold: None, } diff --git a/programs/system/src/context.rs b/programs/system/src/context.rs index 4e37e2efd4..431bb8d8fa 100644 --- a/programs/system/src/context.rs +++ b/programs/system/src/context.rs @@ -57,6 +57,16 @@ impl SystemContext<'_> { #[profile] pub fn set_address_fee(&mut self, fee: u64, index: u8) -> Result<()> { + self.set_additive_fee(fee, index) + } + + #[profile] + pub fn set_network_fee_v1(&mut self, fee: u64, index: u8) -> Result<()> { + self.set_additive_fee(fee, index) + } + + #[inline(always)] + fn set_additive_fee(&mut self, fee: u64, index: u8) -> Result<()> { let payment = self.rollover_fee_payments.iter_mut().find(|a| a.0 == index); match payment { Some(payment) => { @@ -69,6 +79,7 @@ impl SystemContext<'_> { }; Ok(()) } + #[profile] pub fn set_network_fee_v2(&mut self, fee: u64, index: u8) { if !self.network_fee_is_set { @@ -77,21 +88,6 @@ impl SystemContext<'_> { } } - #[profile] - pub fn set_network_fee_v1(&mut self, fee: u64, index: u8) -> Result<()> { - let payment = self.rollover_fee_payments.iter_mut().find(|a| a.0 == index); - match payment { - Some(payment) => { - payment.1 = payment - .1 - .checked_add(fee) - .ok_or(ProgramError::ArithmeticOverflow)?; - } - None => self.rollover_fee_payments.push((index, fee)), - }; - Ok(()) - } - pub fn get_or_hash_pubkey(&mut self, pubkey: Pubkey) -> [u8; 32] { let hashed_pubkey = self .hashed_pubkeys diff --git a/programs/system/src/processor/create_address_cpi_data.rs b/programs/system/src/processor/create_address_cpi_data.rs index c852897875..7decd5da0a 100644 --- a/programs/system/src/processor/create_address_cpi_data.rs +++ b/programs/system/src/processor/create_address_cpi_data.rs @@ -40,15 +40,12 @@ pub fn derive_new_addresses<'info, 'a, 'b: 'a, const ADDRESS_ASSIGNMENT: bool>( "V1 address tree", )?; - let mut network_fee = context + let network_fee = context .get_legacy_merkle_context( new_address_params.address_merkle_tree_account_index(), ) .ok_or(SystemProgramError::MissingLegacyMerkleContext)? .network_fee; - if network_fee != 0 { - network_fee += 5000; - } context.set_address_fee(network_fee, new_address_params.address_queue_index())?; ( diff --git a/sdk-libs/client/src/fee.rs b/sdk-libs/client/src/fee.rs index 7cd6dbbbb5..8d7b18c88f 100644 --- a/sdk-libs/client/src/fee.rs +++ b/sdk-libs/client/src/fee.rs @@ -25,7 +25,7 @@ impl Default for FeeConfig { // state_tree_configs: vec![StateMerkleTreeConfig::default()], // address_tree_configs: vec![AddressMerkleTreeConfig::default()], network_fee: 5000, - address_network_fee: 10000, + address_network_fee: 5000, solana_network_fee: 5000, } }