From 31c76f75a9df469736ef34d886353a510127302a Mon Sep 17 00:00:00 2001 From: ananas Date: Thu, 9 Oct 2025 00:33:50 +0100 Subject: [PATCH 1/3] refactor: light program test make anchor programs optional deps --- Cargo.lock | 1 + sdk-libs/client/src/indexer/photon_indexer.rs | 3 +- sdk-libs/program-test/Cargo.toml | 13 +- sdk-libs/program-test/src/accounts/mod.rs | 12 +- .../src/accounts/test_accounts.rs | 89 +++- .../program-test/src/indexer/address_tree.rs | 6 +- .../program-test/src/indexer/extensions.rs | 1 + .../program-test/src/indexer/state_tree.rs | 2 +- .../program-test/src/indexer/test_indexer.rs | 81 ++-- .../program-test/src/program_test/config.rs | 41 +- .../src/program_test/light_program_test.rs | 402 ++++++++++-------- .../program-test/src/utils/create_account.rs | 2 +- .../program-test/src/utils/load_accounts.rs | 140 ++++++ sdk-libs/program-test/src/utils/mod.rs | 2 + .../src/utils/setup_light_programs.rs | 33 +- 15 files changed, 582 insertions(+), 246 deletions(-) create mode 100644 sdk-libs/program-test/src/utils/load_accounts.rs diff --git a/Cargo.lock b/Cargo.lock index 7f9d40cf26..da3c1f9183 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3699,6 +3699,7 @@ dependencies = [ "account-compression", "anchor-lang", "async-trait", + "base64 0.22.1", "borsh 0.10.4", "bs58", "bytemuck", diff --git a/sdk-libs/client/src/indexer/photon_indexer.rs b/sdk-libs/client/src/indexer/photon_indexer.rs index ece18fd969..0d886dab5f 100644 --- a/sdk-libs/client/src/indexer/photon_indexer.rs +++ b/sdk-libs/client/src/indexer/photon_indexer.rs @@ -15,7 +15,7 @@ use super::{ CompressedAccount, CompressedTokenAccount, OwnerBalance, QueueElementsResult, SignatureWithMetadata, TokenBalance, }, - BatchAddressUpdateIndexerResponse, MerkleProofWithContext, + BatchAddressUpdateIndexerResponse, }; use crate::indexer::{ base58::Base58Conversions, @@ -1590,6 +1590,7 @@ impl Indexer for PhotonIndexer { unimplemented!("get_queue_elements"); #[cfg(feature = "v2")] { + use super::MerkleProofWithContext; let pubkey = _pubkey; let queue_type = _queue_type; let limit = _num_elements; diff --git a/sdk-libs/program-test/Cargo.toml b/sdk-libs/program-test/Cargo.toml index ecff35b0fc..0654e51991 100644 --- a/sdk-libs/program-test/Cargo.toml +++ b/sdk-libs/program-test/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" [features] default = [] -devenv = ["v2", "light-client/devenv", "light-prover-client/devenv"] +devenv = ["v2", "light-client/devenv", "light-prover-client/devenv", "dep:account-compression", "dep:light-compressed-token", "dep:light-registry", "dep:light-batched-merkle-tree", "dep:light-concurrent-merkle-tree"] v2 = ["light-client/v2"] [dependencies] @@ -16,19 +16,19 @@ light-indexed-merkle-tree = { workspace = true, features = ["solana"] } light-indexed-array = { workspace = true } light-merkle-tree-reference = { workspace = true } light-merkle-tree-metadata = { workspace = true, features = ["anchor"] } -light-concurrent-merkle-tree = { workspace = true } +light-concurrent-merkle-tree = { workspace = true, optional = true } light-hasher = { workspace = true } light-compressed-account = { workspace = true, features = ["anchor"] } -light-batched-merkle-tree = { workspace = true, features = ["test-only"] } +light-batched-merkle-tree = { workspace = true, features = ["test-only"], optional = true } # unreleased light-client = { workspace = true, features = ["program-test"] } light-prover-client = { workspace = true } litesvm = { workspace = true } -light-registry = { workspace = true, features = ["cpi"] } -light-compressed-token = { workspace = true, features = ["cpi"] } -account-compression = { workspace = true, features = ["cpi"] } +light-registry = { workspace = true, features = ["cpi"], optional = true } +light-compressed-token = { workspace = true, features = ["cpi"], optional = true } +account-compression = { workspace = true, features = ["cpi"], optional = true } photon-api = { workspace = true } log = { workspace = true } @@ -58,3 +58,4 @@ bs58 = { workspace = true } light-sdk-types = { workspace = true } tabled = { workspace = true } chrono = "0.4" +base64 = "0.22" diff --git a/sdk-libs/program-test/src/accounts/mod.rs b/sdk-libs/program-test/src/accounts/mod.rs index aeeb01ca5e..408fdd4ae1 100644 --- a/sdk-libs/program-test/src/accounts/mod.rs +++ b/sdk-libs/program-test/src/accounts/mod.rs @@ -1,13 +1,15 @@ -#[cfg(feature = "v2")] +#[cfg(feature = "devenv")] +pub mod address_tree; +#[cfg(feature = "devenv")] pub mod address_tree_v2; +#[cfg(feature = "devenv")] pub mod initialize; #[cfg(feature = "devenv")] pub mod register_program; pub mod registered_program_accounts; -#[cfg(feature = "v2")] -pub mod state_tree_v2; - -pub mod address_tree; +#[cfg(feature = "devenv")] pub mod state_tree; +#[cfg(feature = "devenv")] +pub mod state_tree_v2; pub mod test_accounts; pub mod test_keypairs; diff --git a/sdk-libs/program-test/src/accounts/test_accounts.rs b/sdk-libs/program-test/src/accounts/test_accounts.rs index f91555c5b6..fad7ee8da2 100644 --- a/sdk-libs/program-test/src/accounts/test_accounts.rs +++ b/sdk-libs/program-test/src/accounts/test_accounts.rs @@ -1,17 +1,18 @@ use light_client::indexer::{AddressMerkleTreeAccounts, StateMerkleTreeAccounts, TreeInfo}; use light_compressed_account::TreeType; +#[cfg(feature = "devenv")] use light_registry::{ account_compression_cpi::sdk::get_registered_program_pda, sdk::create_register_program_instruction, utils::{get_forester_pda, get_protocol_config_pda_address}, }; -use solana_sdk::{ - pubkey, - pubkey::Pubkey, - signature::{Keypair, Signer}, -}; +#[cfg(feature = "devenv")] +use solana_sdk::signature::Signer; +use solana_sdk::{pubkey, pubkey::Pubkey, signature::Keypair}; -use super::{initialize::*, test_keypairs::*}; +#[cfg(feature = "devenv")] +use super::initialize::*; +use super::test_keypairs::*; pub const NOOP_PROGRAM_ID: Pubkey = pubkey!("noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV"); @@ -62,10 +63,10 @@ impl TestAccounts { governance_authority_pda: Pubkey::default(), group_pda: Pubkey::default(), forester: Keypair::from_bytes(&FORESTER_TEST_KEYPAIR).unwrap(), - registered_program_pda: get_registered_program_pda(&Pubkey::from( - light_sdk::constants::LIGHT_SYSTEM_PROGRAM_ID, - )), - registered_registry_program_pda: get_registered_program_pda(&light_registry::ID), + registered_program_pda: pubkey!("35hkDgaAKwMCaxRz2ocSZ6NaUrtKkyNqU6c4RV3tYJRh"), + registered_registry_program_pda: pubkey!( + "DumMsyvkaGJG4QnQ1BhTgvoRMXsgGxfpKDUCr22Xqu4w" + ), registered_forester_pda: Pubkey::default(), }, v1_state_trees: vec![ @@ -120,31 +121,73 @@ impl TestAccounts { } pub fn get_program_test_test_accounts() -> TestAccounts { - let group_seed_keypair = Keypair::from_bytes(&GROUP_PDA_SEED_TEST_KEYPAIR).unwrap(); - let group_pda = get_group_pda(group_seed_keypair.pubkey()); - - let payer = Keypair::from_bytes(&PAYER_KEYPAIR).unwrap(); - let protocol_config_pda = get_protocol_config_pda_address(); - let (_, registered_program_pda) = create_register_program_instruction( - payer.pubkey(), + #[cfg(feature = "devenv")] + let ( + group_pda, protocol_config_pda, + registered_program_pda, + registered_registry_program_pda, + registered_forester_pda, + ) = { + let group_seed_keypair = Keypair::from_bytes(&GROUP_PDA_SEED_TEST_KEYPAIR).unwrap(); + let group_pda = get_group_pda(group_seed_keypair.pubkey()); + let payer = Keypair::from_bytes(&PAYER_KEYPAIR).unwrap(); + let protocol_config_pda = get_protocol_config_pda_address(); + let (_, registered_program_pda) = create_register_program_instruction( + payer.pubkey(), + protocol_config_pda, + group_pda, + Pubkey::from(light_sdk::constants::LIGHT_SYSTEM_PROGRAM_ID), + ); + let registered_registry_program_pda = + get_registered_program_pda(&pubkey!("Lighton6oQpVkeewmo2mcPTQQp7kYHr4fWpAgJyEmDX")); + let forester = Keypair::from_bytes(&FORESTER_TEST_KEYPAIR).unwrap(); + let registered_forester_pda = get_forester_pda(&forester.pubkey()).0; + ( + group_pda, + protocol_config_pda.0, + registered_program_pda, + registered_registry_program_pda, + registered_forester_pda, + ) + }; + + #[cfg(not(feature = "devenv"))] + let ( group_pda, - Pubkey::from(light_sdk::constants::LIGHT_SYSTEM_PROGRAM_ID), - ); + protocol_config_pda, + registered_program_pda, + registered_registry_program_pda, + registered_forester_pda, + ) = { + // Hardcoded PDAs for non-devenv mode (these match the devenv calculations) + let group_pda = pubkey!("Fomh1YizJdDfqvMJhC42cLNdcJM8NMM2NfxgZVEh3rkC"); + let protocol_config_pda = pubkey!("CuEtcKkkbTn6qy2qxqDswq5U2ADsqoipYDAYfRvxPjcp"); + let registered_program_pda = pubkey!("35hkDgaAKwMCaxRz2ocSZ6NaUrtKkyNqU6c4RV3tYJRh"); + let registered_registry_program_pda = + pubkey!("DumMsyvkaGJG4QnQ1BhTgvoRMXsgGxfpKDUCr22Xqu4w"); + let registered_forester_pda = pubkey!("3FBt1BPQHCQkS8k3wrUXMfB6JBhtMhEqQXueHRw2ojZV"); + ( + group_pda, + protocol_config_pda, + registered_program_pda, + registered_registry_program_pda, + registered_forester_pda, + ) + }; - let registered_registry_program_pda = get_registered_program_pda(&light_registry::ID); + let payer = Keypair::from_bytes(&PAYER_KEYPAIR).unwrap(); let forester = Keypair::from_bytes(&FORESTER_TEST_KEYPAIR).unwrap(); - let forester_pubkey = forester.pubkey(); TestAccounts { protocol: ProtocolAccounts { governance_authority: payer, - governance_authority_pda: protocol_config_pda.0, + governance_authority_pda: protocol_config_pda, group_pda, forester, registered_program_pda, registered_registry_program_pda, - registered_forester_pda: get_forester_pda(&forester_pubkey).0, + registered_forester_pda, }, v1_state_trees: vec![ StateMerkleTreeAccounts { diff --git a/sdk-libs/program-test/src/indexer/address_tree.rs b/sdk-libs/program-test/src/indexer/address_tree.rs index 194748ae90..4e12fbfcd0 100644 --- a/sdk-libs/program-test/src/indexer/address_tree.rs +++ b/sdk-libs/program-test/src/indexer/address_tree.rs @@ -1,11 +1,12 @@ use std::fmt::Debug; +#[cfg(feature = "devenv")] use light_batched_merkle_tree::constants::DEFAULT_BATCH_ROOT_HISTORY_LEN; use light_client::{ fee::FeeConfig, indexer::{AddressMerkleTreeAccounts, IndexerError}, }; -use light_concurrent_merkle_tree::light_hasher::Poseidon; +use light_hasher::Poseidon; use light_indexed_merkle_tree::{ array::{IndexedArray, IndexedElement, IndexedElementBundle}, reference::IndexedMerkleTree, @@ -15,6 +16,9 @@ use light_sdk::constants::STATE_MERKLE_TREE_ROOTS; use num_bigint::{BigInt, BigUint}; use num_traits::ops::bytes::FromBytes; +#[cfg(not(feature = "devenv"))] +use super::test_indexer::DEFAULT_BATCH_ROOT_HISTORY_LEN; + #[derive(Debug, Clone)] pub enum IndexedMerkleTreeVersion { V1(Box>), diff --git a/sdk-libs/program-test/src/indexer/extensions.rs b/sdk-libs/program-test/src/indexer/extensions.rs index 88a2da8127..1c5b2434eb 100644 --- a/sdk-libs/program-test/src/indexer/extensions.rs +++ b/sdk-libs/program-test/src/indexer/extensions.rs @@ -72,6 +72,7 @@ pub trait TestIndexerExtensions { fn get_proof_by_index(&mut self, merkle_tree_pubkey: Pubkey, index: u64) -> MerkleProof; + #[cfg(feature = "devenv")] async fn finalize_batched_address_tree_update( &mut self, merkle_tree_pubkey: Pubkey, diff --git a/sdk-libs/program-test/src/indexer/state_tree.rs b/sdk-libs/program-test/src/indexer/state_tree.rs index ce2d30751c..832fb962ed 100644 --- a/sdk-libs/program-test/src/indexer/state_tree.rs +++ b/sdk-libs/program-test/src/indexer/state_tree.rs @@ -2,7 +2,7 @@ use std::fmt::Debug; use light_client::indexer::{IndexerError, StateMerkleTreeAccounts}; use light_compressed_account::TreeType; -use light_concurrent_merkle_tree::light_hasher::Poseidon; +use light_hasher::Poseidon; use light_merkle_tree_reference::MerkleTree; #[derive(Debug, Clone)] diff --git a/sdk-libs/program-test/src/indexer/test_indexer.rs b/sdk-libs/program-test/src/indexer/test_indexer.rs index 5d724ac4d4..5c2d125237 100644 --- a/sdk-libs/program-test/src/indexer/test_indexer.rs +++ b/sdk-libs/program-test/src/indexer/test_indexer.rs @@ -1,10 +1,24 @@ use std::{fmt::Debug, time::Duration}; +#[cfg(feature = "devenv")] use account_compression::{ AddressMerkleTreeConfig, AddressQueueConfig, NullifierQueueConfig, StateMerkleTreeConfig, }; + +use crate::accounts::test_accounts::TestAccounts; +// Constants from account_compression and light_batched_merkle_tree for non-devenv mode +pub(crate) const STATE_MERKLE_TREE_HEIGHT: u64 = 26; +pub(crate) const STATE_MERKLE_TREE_CANOPY_DEPTH: u64 = 10; +pub(crate) const STATE_MERKLE_TREE_ROOTS: u64 = 2400; +#[cfg(not(feature = "devenv"))] +pub(crate) const DEFAULT_BATCH_STATE_TREE_HEIGHT: usize = 32; +#[cfg(not(feature = "devenv"))] +pub(crate) const DEFAULT_BATCH_ADDRESS_TREE_HEIGHT: usize = 40; +#[cfg(not(feature = "devenv"))] +pub(crate) const DEFAULT_BATCH_ROOT_HISTORY_LEN: usize = 200; use async_trait::async_trait; use borsh::BorshDeserialize; +#[cfg(feature = "devenv")] use light_batched_merkle_tree::{ constants::{ DEFAULT_BATCH_ADDRESS_TREE_HEIGHT, DEFAULT_BATCH_ROOT_HISTORY_LEN, @@ -12,6 +26,10 @@ use light_batched_merkle_tree::{ }, merkle_tree::BatchedMerkleTreeAccount, }; +#[cfg(feature = "v2")] +use light_client::indexer::MerkleProofWithContext; +#[cfg(feature = "devenv")] +use light_client::rpc::{Rpc, RpcError}; use light_client::{ fee::FeeConfig, indexer::{ @@ -19,12 +37,10 @@ use light_client::{ AddressWithTree, BatchAddressUpdateIndexerResponse, CompressedAccount, CompressedTokenAccount, Context, GetCompressedAccountsByOwnerConfig, GetCompressedTokenAccountsByOwnerOrDelegateOptions, Indexer, IndexerError, - IndexerRpcConfig, Items, ItemsWithCursor, MerkleProof, MerkleProofWithContext, - NewAddressProofWithContext, OwnerBalance, PaginatedOptions, QueueElementsResult, Response, - RetryConfig, RootIndex, SignatureWithMetadata, StateMerkleTreeAccounts, TokenBalance, - ValidityProofWithContext, + IndexerRpcConfig, Items, ItemsWithCursor, MerkleProof, NewAddressProofWithContext, + OwnerBalance, PaginatedOptions, QueueElementsResult, Response, RetryConfig, RootIndex, + SignatureWithMetadata, StateMerkleTreeAccounts, TokenBalance, ValidityProofWithContext, }, - rpc::{Rpc, RpcError}, }; use light_compressed_account::{ compressed_account::{CompressedAccountWithMerkleContext, MerkleContext}, @@ -74,22 +90,20 @@ use solana_sdk::{ signature::{Keypair, Signer}, }; +#[cfg(feature = "devenv")] +use super::address_tree::IndexedMerkleTreeVersion; use super::{ - address_tree::{AddressMerkleTreeBundle, IndexedMerkleTreeVersion}, + address_tree::AddressMerkleTreeBundle, state_tree::{LeafIndexInfo, StateMerkleTreeBundle}, }; #[cfg(feature = "devenv")] use crate::accounts::{ + address_tree::create_address_merkle_tree_and_queue_account, address_tree_v2::create_batch_address_merkle_tree, + state_tree::create_state_merkle_tree_and_queue_account, state_tree_v2::create_batched_state_merkle_tree, }; -use crate::{ - accounts::{ - address_tree::create_address_merkle_tree_and_queue_account, - state_tree::create_state_merkle_tree_and_queue_account, test_accounts::TestAccounts, - }, - indexer::TestIndexerExtensions, -}; +use crate::indexer::TestIndexerExtensions; #[derive(Debug)] pub struct TestIndexer { @@ -460,6 +474,8 @@ impl Indexer for TestIndexer { let account_data = account.value.ok_or(IndexerError::AccountNotFound)?; state_merkle_tree_pubkeys.push(account_data.tree_info.tree); } + println!("state_merkle_tree_pubkeys {:?}", state_merkle_tree_pubkeys); + println!("hashes {:?}", hashes); let mut proof_inputs = vec![]; let mut indices_to_remove = Vec::new(); @@ -481,7 +497,14 @@ impl Indexer for TestIndexer { .output_queue_elements .iter() .find(|(hash, _)| hash == compressed_account); + println!("queue_element {:?}", queue_element); + if let Some((_, index)) = queue_element { + println!("index {:?}", index); + println!( + "accounts.output_queue_batch_size {:?}", + accounts.output_queue_batch_size + ); if accounts.output_queue_batch_size.is_some() && accounts.leaf_index_in_queue_range(*index as usize)? { @@ -1209,6 +1232,7 @@ impl TestIndexerExtensions for TestIndexer { } } + #[cfg(feature = "devenv")] async fn finalize_batched_address_tree_update( &mut self, merkle_tree_pubkey: Pubkey, @@ -1315,11 +1339,10 @@ impl TestIndexer { ) } else { let merkle_tree = Box::new(MerkleTree::::new_with_history( - account_compression::utils::constants::STATE_MERKLE_TREE_HEIGHT as usize, - account_compression::utils::constants::STATE_MERKLE_TREE_CANOPY_DEPTH - as usize, + STATE_MERKLE_TREE_HEIGHT as usize, + STATE_MERKLE_TREE_CANOPY_DEPTH as usize, 0, - account_compression::utils::constants::STATE_MERKLE_TREE_ROOTS as usize, + STATE_MERKLE_TREE_ROOTS as usize, )); (TreeType::StateV1, merkle_tree, None) }; @@ -1365,7 +1388,7 @@ impl TestIndexer { AddressMerkleTreeBundle::new_v1(address_merkle_tree_accounts) } } - + #[cfg(feature = "devenv")] async fn add_address_merkle_tree_v1( &mut self, rpc: &mut R, @@ -1429,6 +1452,7 @@ impl TestIndexer { Ok(accounts) } + #[cfg(feature = "devenv")] pub async fn add_address_merkle_tree( &mut self, rpc: &mut R, @@ -1465,6 +1489,7 @@ impl TestIndexer { } #[allow(clippy::too_many_arguments)] + #[cfg(feature = "devenv")] pub async fn add_state_merkle_tree( &mut self, rpc: &mut R, @@ -1493,10 +1518,10 @@ impl TestIndexer { .await .unwrap(); let merkle_tree = Box::new(MerkleTree::::new_with_history( - account_compression::utils::constants::STATE_MERKLE_TREE_HEIGHT as usize, - account_compression::utils::constants::STATE_MERKLE_TREE_CANOPY_DEPTH as usize, + STATE_MERKLE_TREE_HEIGHT as usize, + STATE_MERKLE_TREE_CANOPY_DEPTH as usize, 0, - account_compression::utils::constants::STATE_MERKLE_TREE_ROOTS as usize, + STATE_MERKLE_TREE_ROOTS as usize, )); (FeeConfig::default().state_merkle_tree_rollover as i64,merkle_tree, None) @@ -1643,11 +1668,17 @@ impl TestIndexer { match compressed_account.compressed_account.data.as_ref() { Some(data) => { // Check for both V1 and V2 token account discriminators - let is_v1_token = data.discriminator == light_compressed_token::constants::TOKEN_COMPRESSED_ACCOUNT_DISCRIMINATOR; // [2, 0, 0, 0, 0, 0, 0, 0] + const TOKEN_COMPRESSED_ACCOUNT_DISCRIMINATOR: [u8; 8] = + [2, 0, 0, 0, 0, 0, 0, 0]; + let is_v1_token = data.discriminator == TOKEN_COMPRESSED_ACCOUNT_DISCRIMINATOR; let is_v2_token = data.discriminator == [0, 0, 0, 0, 0, 0, 0, 3]; // V2 discriminator + use solana_sdk::pubkey; + const LIGHT_COMPRESSED_TOKEN_ID: solana_sdk::pubkey::Pubkey = + pubkey!("cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m"); + if compressed_account.compressed_account.owner - == light_compressed_token::ID.to_bytes() + == LIGHT_COMPRESSED_TOKEN_ID.to_bytes() && (is_v1_token || is_v2_token) { if let Ok(token_data) = TokenData::deserialize(&mut data.data.as_slice()) { @@ -1920,6 +1951,7 @@ impl TestIndexer { x.accounts.merkle_tree == pubkey || x.accounts.nullifier_queue == pubkey }) .unwrap(); + println!("accounts {:?}", bundle.accounts); let merkle_tree = &bundle.merkle_tree; queues.push(bundle.accounts.nullifier_queue); cpi_contextes.push(bundle.accounts.cpi_context); @@ -1981,8 +2013,7 @@ impl TestIndexer { )), None, ) - } else if height == account_compression::utils::constants::STATE_MERKLE_TREE_HEIGHT as usize - { + } else if height == STATE_MERKLE_TREE_HEIGHT as usize { let inclusion_proof_inputs = InclusionProofInputsLegacy(inclusion_proofs.as_slice()); ( None, diff --git a/sdk-libs/program-test/src/program_test/config.rs b/sdk-libs/program-test/src/program_test/config.rs index aec0faf78c..65a95e7161 100644 --- a/sdk-libs/program-test/src/program_test/config.rs +++ b/sdk-libs/program-test/src/program_test/config.rs @@ -1,11 +1,14 @@ +#[cfg(feature = "devenv")] use account_compression::{ AddressMerkleTreeConfig, AddressQueueConfig, NullifierQueueConfig, StateMerkleTreeConfig, }; +#[cfg(feature = "devenv")] use light_batched_merkle_tree::{ initialize_address_tree::InitAddressTreeAccountsInstructionData, initialize_state_tree::InitStateTreeAccountsInstructionData, }; use light_prover_client::prover::ProverConfig; +#[cfg(feature = "devenv")] use light_registry::protocol_config::state::ProtocolConfig; use solana_sdk::pubkey::Pubkey; @@ -15,18 +18,29 @@ use crate::logging::EnhancedLoggingConfig; #[derive(Debug, Clone)] pub struct ProgramTestConfig { pub additional_programs: Option>, + #[cfg(feature = "devenv")] pub protocol_config: ProtocolConfig, pub with_prover: bool, pub prover_config: Option, + #[cfg(feature = "devenv")] pub skip_register_programs: bool, + #[cfg(feature = "devenv")] pub skip_v1_trees: bool, + #[cfg(feature = "devenv")] pub skip_second_v1_tree: bool, + #[cfg(feature = "devenv")] pub v1_state_tree_config: StateMerkleTreeConfig, + #[cfg(feature = "devenv")] pub v1_nullifier_queue_config: NullifierQueueConfig, + #[cfg(feature = "devenv")] pub v1_address_tree_config: AddressMerkleTreeConfig, + #[cfg(feature = "devenv")] pub v1_address_queue_config: AddressQueueConfig, + #[cfg(feature = "devenv")] pub v2_state_tree_config: Option, + #[cfg(feature = "devenv")] pub v2_address_tree_config: Option, + #[cfg(feature = "devenv")] pub skip_protocol_init: bool, /// Log failed transactions pub log_failed_tx: bool, @@ -57,13 +71,19 @@ impl ProgramTestConfig { with_prover: bool, additional_programs: Option>, ) -> Self { - let mut res = Self::default_with_batched_trees(with_prover); - res.additional_programs = additional_programs; - - res + Self { + additional_programs, + prover_config: Some(ProverConfig::default()), + with_prover, + #[cfg(feature = "devenv")] + v2_state_tree_config: Some(InitStateTreeAccountsInstructionData::test_default()), + #[cfg(feature = "devenv")] + v2_address_tree_config: Some(InitAddressTreeAccountsInstructionData::test_default()), + ..Default::default() + } } - #[cfg(feature = "v2")] + #[cfg(feature = "devenv")] pub fn default_with_batched_trees(with_prover: bool) -> Self { Self { additional_programs: None, @@ -104,6 +124,7 @@ impl Default for ProgramTestConfig { fn default() -> Self { Self { additional_programs: None, + #[cfg(feature = "devenv")] protocol_config: ProtocolConfig { // Init with an active epoch which doesn't end active_phase_length: 1_000_000_000, @@ -114,15 +135,25 @@ impl Default for ProgramTestConfig { }, with_prover: true, prover_config: None, + #[cfg(feature = "devenv")] skip_second_v1_tree: false, + #[cfg(feature = "devenv")] skip_register_programs: false, + #[cfg(feature = "devenv")] v1_state_tree_config: StateMerkleTreeConfig::default(), + #[cfg(feature = "devenv")] v1_address_tree_config: AddressMerkleTreeConfig::default(), + #[cfg(feature = "devenv")] v1_address_queue_config: AddressQueueConfig::default(), + #[cfg(feature = "devenv")] v1_nullifier_queue_config: NullifierQueueConfig::default(), + #[cfg(feature = "devenv")] v2_state_tree_config: None, + #[cfg(feature = "devenv")] v2_address_tree_config: None, + #[cfg(feature = "devenv")] skip_protocol_init: false, + #[cfg(feature = "devenv")] skip_v1_trees: false, log_failed_tx: true, no_logs: false, 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 044671c2f8..6661d370b4 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,22 +1,26 @@ use std::fmt::{self, Debug, Formatter}; +#[cfg(feature = "devenv")] use account_compression::{AddressMerkleTreeAccount, QueueAccount}; use light_client::{ indexer::{AddressMerkleTreeAccounts, StateMerkleTreeAccounts}, rpc::{merkle_tree::MerkleTreeExt, RpcError}, }; +#[cfg(feature = "devenv")] use light_compressed_account::hash_to_bn254_field_size_be; use light_prover_client::prover::{spawn_prover, ProverConfig}; use litesvm::LiteSVM; +#[cfg(feature = "devenv")] use solana_account::WritableAccount; use solana_sdk::signature::{Keypair, Signer}; +#[cfg(feature = "devenv")] +use crate::accounts::initialize::initialize_accounts; +#[cfg(feature = "devenv")] +use crate::program_test::TestRpc; use crate::{ - accounts::{ - initialize::initialize_accounts, test_accounts::TestAccounts, test_keypairs::TestKeypairs, - }, + accounts::{test_accounts::TestAccounts, test_keypairs::TestKeypairs}, indexer::TestIndexer, - program_test::TestRpc, utils::setup_light_programs::setup_light_programs, ProgramTestConfig, }; @@ -73,187 +77,243 @@ impl LightProgramTest { .airdrop(&keypairs.forester.pubkey(), 10_000_000_000) .expect("forester airdrop failed."); - if !config.skip_protocol_init { - let restore_logs = context.config.no_logs; - if context.config.skip_startup_logs { - context.config.no_logs = true; - } - initialize_accounts(&mut context, &config, &keypairs).await?; - if context.config.skip_startup_logs { - context.config.no_logs = restore_logs; - } - let batch_size = config - .v2_state_tree_config - .as_ref() - .map(|config| config.output_queue_batch_size as usize); - let test_accounts = context.test_accounts.clone(); - context.add_indexer(&test_accounts, batch_size).await?; + #[cfg(feature = "devenv")] + { + if !config.skip_protocol_init { + let restore_logs = context.config.no_logs; + if context.config.skip_startup_logs { + context.config.no_logs = true; + } + initialize_accounts(&mut context, &config, &keypairs).await?; + if context.config.skip_startup_logs { + context.config.no_logs = restore_logs; + } + let batch_size = config + .v2_state_tree_config + .as_ref() + .map(|config| config.output_queue_batch_size as usize); + let test_accounts = context.test_accounts.clone(); + context.add_indexer(&test_accounts, batch_size).await?; - // ensure that address tree pubkey is amt1Ayt45jfbdw5YSo7iz6WZxUmnZsQTYXy82hVwyC2 + // ensure that address tree pubkey is amt1Ayt45jfbdw5YSo7iz6WZxUmnZsQTYXy82hVwyC2 + { + 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 + .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); + } + } + // Copy v1 state merkle tree accounts to devnet pubkeys { - 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 + let tree_account = context + .context + .get_account(&keypairs.state_merkle_tree.pubkey()); + let queue_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 + .get_account(&keypairs.nullifier_queue.pubkey()); + let cpi_account = 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); + .get_account(&keypairs.cpi_context_account.pubkey()); + + if let (Some(tree_acc), Some(queue_acc), Some(cpi_acc)) = + (tree_account, queue_account, cpi_account) + { + for i in 0..context.test_accounts.v1_state_trees.len() { + let state_mt = context.test_accounts.v1_state_trees[i].merkle_tree; + let nullifier_queue_pubkey = + context.test_accounts.v1_state_trees[i].nullifier_queue; + let cpi_context_pubkey = + context.test_accounts.v1_state_trees[i].cpi_context; + + // Update tree account with correct associated queue + let mut tree_account_data = tree_acc.clone(); + { + let merkle_tree_account = bytemuck::from_bytes_mut::< + account_compression::StateMerkleTreeAccount, + >( + &mut tree_account_data.data_as_mut_slice() + [8..account_compression::StateMerkleTreeAccount::LEN], + ); + merkle_tree_account.metadata.associated_queue = + nullifier_queue_pubkey.into(); + } + context.set_account(state_mt, tree_account_data); + + // Update queue account with correct associated merkle tree + let mut queue_account_data = queue_acc.clone(); + { + let queue_account = bytemuck::from_bytes_mut::( + &mut queue_account_data.data_as_mut_slice()[8..QueueAccount::LEN], + ); + queue_account.metadata.associated_merkle_tree = state_mt.into(); + } + context.set_account(nullifier_queue_pubkey, queue_account_data); + + // Update CPI context account with correct associated merkle tree and queue + let mut cpi_account_data = cpi_acc.clone(); + { + let associated_merkle_tree_offset = 8 + 32; // discriminator + fee_payer + let associated_queue_offset = 8 + 32 + 32; // discriminator + fee_payer + associated_merkle_tree + cpi_account_data.data_as_mut_slice() + [associated_merkle_tree_offset..associated_merkle_tree_offset + 32] + .copy_from_slice(&state_mt.to_bytes()); + cpi_account_data.data_as_mut_slice() + [associated_queue_offset..associated_queue_offset + 32] + .copy_from_slice(&nullifier_queue_pubkey.to_bytes()); + } + context.set_account(cpi_context_pubkey, cpi_account_data); + } + } } - } - // Copy v1 state merkle tree accounts to devnet pubkeys - { - let tree_account = context - .context - .get_account(&keypairs.state_merkle_tree.pubkey()); - let queue_account = context - .context - .get_account(&keypairs.nullifier_queue.pubkey()); - let cpi_account = context - .context - .get_account(&keypairs.cpi_context_account.pubkey()); - - if let (Some(tree_acc), Some(queue_acc), Some(cpi_acc)) = - (tree_account, queue_account, cpi_account) { - for i in 0..context.test_accounts.v1_state_trees.len() { - let state_mt = context.test_accounts.v1_state_trees[i].merkle_tree; - let nullifier_queue_pubkey = - context.test_accounts.v1_state_trees[i].nullifier_queue; - let cpi_context_pubkey = context.test_accounts.v1_state_trees[i].cpi_context; - - // Update tree account with correct associated queue - let mut tree_account_data = tree_acc.clone(); - { - let merkle_tree_account = bytemuck::from_bytes_mut::< - account_compression::StateMerkleTreeAccount, - >( - &mut tree_account_data.data_as_mut_slice() - [8..account_compression::StateMerkleTreeAccount::LEN], - ); - merkle_tree_account.metadata.associated_queue = - nullifier_queue_pubkey.into(); - } - context.set_account(state_mt, tree_account_data); - - // Update queue account with correct associated merkle tree - let mut queue_account_data = queue_acc.clone(); - { - let queue_account = bytemuck::from_bytes_mut::( - &mut queue_account_data.data_as_mut_slice()[8..QueueAccount::LEN], - ); - queue_account.metadata.associated_merkle_tree = state_mt.into(); - } - context.set_account(nullifier_queue_pubkey, queue_account_data); - - // Update CPI context account with correct associated merkle tree and queue - let mut cpi_account_data = cpi_acc.clone(); - { - let associated_merkle_tree_offset = 8 + 32; // discriminator + fee_payer - let associated_queue_offset = 8 + 32 + 32; // discriminator + fee_payer + associated_merkle_tree - cpi_account_data.data_as_mut_slice() - [associated_merkle_tree_offset..associated_merkle_tree_offset + 32] - .copy_from_slice(&state_mt.to_bytes()); - cpi_account_data.data_as_mut_slice() - [associated_queue_offset..associated_queue_offset + 32] - .copy_from_slice(&nullifier_queue_pubkey.to_bytes()); - } - context.set_account(cpi_context_pubkey, cpi_account_data); + let address_mt = context.test_accounts.v2_address_trees[0]; + let account = context + .context + .get_account(&keypairs.batch_address_merkle_tree.pubkey()); + if let Some(account) = account { + context.set_account(address_mt, account); } } - } - { - let address_mt = context.test_accounts.v2_address_trees[0]; - let account = context - .context - .get_account(&keypairs.batch_address_merkle_tree.pubkey()); - if let Some(account) = account { - context.set_account(address_mt, account); + // Copy batched state merkle tree accounts to devnet pubkeys + { + let tree_account = context + .context + .get_account(&keypairs.batched_state_merkle_tree.pubkey()); + let queue_account = context + .context + .get_account(&keypairs.batched_output_queue.pubkey()); + let cpi_account = context + .context + .get_account(&keypairs.batched_cpi_context.pubkey()); + + if let (Some(tree_acc), Some(queue_acc), Some(cpi_acc)) = + (tree_account, queue_account, cpi_account) + { + use light_batched_merkle_tree::{ + merkle_tree::BatchedMerkleTreeAccount, queue::BatchedQueueAccount, + }; + + for i in 0..context.test_accounts.v2_state_trees.len() { + let merkle_tree_pubkey = + context.test_accounts.v2_state_trees[i].merkle_tree; + let output_queue_pubkey = + context.test_accounts.v2_state_trees[i].output_queue; + let cpi_context_pubkey = + context.test_accounts.v2_state_trees[i].cpi_context; + + // Update tree account with correct associated queue and hashed pubkey + let mut tree_account_data = tree_acc.clone(); + { + let mut tree = BatchedMerkleTreeAccount::state_from_bytes( + tree_account_data.data_as_mut_slice(), + &merkle_tree_pubkey.into(), + ) + .unwrap(); + let metadata = tree.get_metadata_mut(); + metadata.metadata.associated_queue = output_queue_pubkey.into(); + metadata.hashed_pubkey = + hash_to_bn254_field_size_be(&merkle_tree_pubkey.to_bytes()); + } + context.set_account(merkle_tree_pubkey, tree_account_data); + + // Update queue account with correct associated merkle tree and hashed pubkeys + let mut queue_account_data = queue_acc.clone(); + { + let mut queue = BatchedQueueAccount::output_from_bytes( + queue_account_data.data_as_mut_slice(), + ) + .unwrap(); + let metadata = queue.get_metadata_mut(); + metadata.metadata.associated_merkle_tree = merkle_tree_pubkey.into(); + metadata.hashed_merkle_tree_pubkey = + hash_to_bn254_field_size_be(&merkle_tree_pubkey.to_bytes()); + metadata.hashed_queue_pubkey = + hash_to_bn254_field_size_be(&output_queue_pubkey.to_bytes()); + } + context.set_account(output_queue_pubkey, queue_account_data); + + // Update CPI context account with correct associated merkle tree and queue + let mut cpi_account_data = cpi_acc.clone(); + { + let associated_merkle_tree_offset = 8 + 32; // discriminator + fee_payer + let associated_queue_offset = 8 + 32 + 32; // discriminator + fee_payer + associated_merkle_tree + cpi_account_data.data_as_mut_slice() + [associated_merkle_tree_offset..associated_merkle_tree_offset + 32] + .copy_from_slice(&merkle_tree_pubkey.to_bytes()); + cpi_account_data.data_as_mut_slice() + [associated_queue_offset..associated_queue_offset + 32] + .copy_from_slice(&output_queue_pubkey.to_bytes()); + } + context.set_account(cpi_context_pubkey, cpi_account_data); + } + } } } - // Copy batched state merkle tree accounts to devnet pubkeys + + #[cfg(not(feature = "devenv"))] { - let tree_account = context - .context - .get_account(&keypairs.batched_state_merkle_tree.pubkey()); - let queue_account = context - .context - .get_account(&keypairs.batched_output_queue.pubkey()); - let cpi_account = context - .context - .get_account(&keypairs.batched_cpi_context.pubkey()); - - if let (Some(tree_acc), Some(queue_acc), Some(cpi_acc)) = - (tree_account, queue_account, cpi_account) - { - use light_batched_merkle_tree::{ - merkle_tree::BatchedMerkleTreeAccount, queue::BatchedQueueAccount, - }; - - for i in 0..context.test_accounts.v2_state_trees.len() { - let merkle_tree_pubkey = context.test_accounts.v2_state_trees[i].merkle_tree; - let output_queue_pubkey = context.test_accounts.v2_state_trees[i].output_queue; - let cpi_context_pubkey = context.test_accounts.v2_state_trees[i].cpi_context; - - // Update tree account with correct associated queue and hashed pubkey - let mut tree_account_data = tree_acc.clone(); - { - let mut tree = BatchedMerkleTreeAccount::state_from_bytes( - tree_account_data.data_as_mut_slice(), - &merkle_tree_pubkey.into(), - ) - .unwrap(); - let metadata = tree.get_metadata_mut(); - metadata.metadata.associated_queue = output_queue_pubkey.into(); - metadata.hashed_pubkey = - hash_to_bn254_field_size_be(&merkle_tree_pubkey.to_bytes()); - } - context.set_account(merkle_tree_pubkey, tree_account_data); - - // Update queue account with correct associated merkle tree and hashed pubkeys - let mut queue_account_data = queue_acc.clone(); - { - let mut queue = BatchedQueueAccount::output_from_bytes( - queue_account_data.data_as_mut_slice(), - ) - .unwrap(); - let metadata = queue.get_metadata_mut(); - metadata.metadata.associated_merkle_tree = merkle_tree_pubkey.into(); - metadata.hashed_merkle_tree_pubkey = - hash_to_bn254_field_size_be(&merkle_tree_pubkey.to_bytes()); - metadata.hashed_queue_pubkey = - hash_to_bn254_field_size_be(&output_queue_pubkey.to_bytes()); - } - context.set_account(output_queue_pubkey, queue_account_data); - - // Update CPI context account with correct associated merkle tree and queue - let mut cpi_account_data = cpi_acc.clone(); - { - let associated_merkle_tree_offset = 8 + 32; // discriminator + fee_payer - let associated_queue_offset = 8 + 32 + 32; // discriminator + fee_payer + associated_merkle_tree - cpi_account_data.data_as_mut_slice() - [associated_merkle_tree_offset..associated_merkle_tree_offset + 32] - .copy_from_slice(&merkle_tree_pubkey.to_bytes()); - cpi_account_data.data_as_mut_slice() - [associated_queue_offset..associated_queue_offset + 32] - .copy_from_slice(&output_queue_pubkey.to_bytes()); + // Load all accounts from JSON directory + use crate::utils::load_accounts::load_all_accounts_from_dir; + + let accounts = load_all_accounts_from_dir()?; + + // Extract and verify batch_size from all V2 state tree output queues + // BatchedQueueMetadata layout: discriminator (8) + QueueMetadata (224) + QueueBatches.num_batches (8) + QueueBatches.batch_size (8) + const BATCH_SIZE_OFFSET: usize = 240; + let mut batch_sizes = Vec::new(); + + for v2_tree in &context.test_accounts.v2_state_trees { + if let Some(queue_account) = accounts.get(&v2_tree.output_queue) { + if queue_account.data.len() >= BATCH_SIZE_OFFSET + 8 { + let bytes: [u8; 8] = queue_account.data + [BATCH_SIZE_OFFSET..BATCH_SIZE_OFFSET + 8] + .try_into() + .map_err(|_| { + RpcError::CustomError("Failed to read batch_size bytes".to_string()) + })?; + batch_sizes.push(u64::from_le_bytes(bytes) as usize); } - context.set_account(cpi_context_pubkey, cpi_account_data); } } + + // Verify all batch sizes are the same + if !batch_sizes.is_empty() && !batch_sizes.windows(2).all(|w| w[0] == w[1]) { + return Err(RpcError::CustomError(format!( + "Inconsistent batch_sizes found across output queues: {:?}", + batch_sizes + ))); + } + + let batch_size = batch_sizes.first().copied().unwrap_or(0); + + for (pubkey, account) in accounts { + context.context.set_account(pubkey, account).map_err(|e| { + RpcError::CustomError(format!("Failed to set account {}: {}", pubkey, e)) + })?; + } + + // Initialize indexer with extracted batch size + let test_accounts = context.test_accounts.clone(); + context + .add_indexer(&test_accounts, Some(batch_size)) + .await?; } // reset tx counter after program setup. diff --git a/sdk-libs/program-test/src/utils/create_account.rs b/sdk-libs/program-test/src/utils/create_account.rs index 55cecf673e..27011bc369 100644 --- a/sdk-libs/program-test/src/utils/create_account.rs +++ b/sdk-libs/program-test/src/utils/create_account.rs @@ -1,5 +1,5 @@ -use account_compression::processor::initialize_address_merkle_tree::Pubkey; use anchor_lang::solana_program::{instruction::Instruction, system_instruction}; +use solana_pubkey::Pubkey; use solana_sdk::signature::{Keypair, Signer}; pub fn create_account_instruction( diff --git a/sdk-libs/program-test/src/utils/load_accounts.rs b/sdk-libs/program-test/src/utils/load_accounts.rs new file mode 100644 index 0000000000..e6dbe4d92d --- /dev/null +++ b/sdk-libs/program-test/src/utils/load_accounts.rs @@ -0,0 +1,140 @@ +use std::{collections::HashMap, fs, path::PathBuf}; + +use light_client::rpc::RpcError; +use serde::{Deserialize, Serialize}; +use solana_sdk::{account::Account, pubkey::Pubkey}; + +#[derive(Debug, Serialize, Deserialize)] +struct AccountData { + pubkey: String, + account: AccountInfo, +} + +#[derive(Debug, Serialize, Deserialize)] +struct AccountInfo { + lamports: u64, + data: (String, String), // (data, encoding) where encoding is typically "base64" + owner: String, + executable: bool, + #[serde(rename = "rentEpoch")] + rent_epoch: u64, +} + +pub fn find_accounts_dir() -> Option { + #[cfg(not(feature = "devenv"))] + { + use std::process::Command; + let output = Command::new("which") + .arg("light") + .output() + .expect("Failed to execute 'which light'"); + + if !output.status.success() { + return None; + } + + let light_path = String::from_utf8_lossy(&output.stdout).trim().to_string(); + let mut light_bin_path = PathBuf::from(light_path); + light_bin_path.pop(); + + let accounts_dir = + light_bin_path.join("../lib/node_modules/@lightprotocol/zk-compression-cli/accounts"); + + Some(accounts_dir.canonicalize().unwrap_or(accounts_dir)) + } + #[cfg(feature = "devenv")] + { + println!("Use only in light protocol monorepo. Using 'git rev-parse --show-toplevel' to find the accounts directory"); + let light_protocol_toplevel = String::from_utf8_lossy( + &std::process::Command::new("git") + .arg("rev-parse") + .arg("--show-toplevel") + .output() + .expect("Failed to get top-level directory") + .stdout, + ) + .trim() + .to_string(); + + // In devenv mode, we don't load accounts from directory + // This path won't be used as we initialize accounts directly + let accounts_path = PathBuf::from(format!("{}/cli/accounts/", light_protocol_toplevel)); + Some(accounts_path) + } +} + +/// Load all accounts from the accounts directory +/// Returns a HashMap of Pubkey -> Account +pub fn load_all_accounts_from_dir() -> Result, RpcError> { + 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 mut accounts = HashMap::new(); + + let entries = fs::read_dir(&accounts_dir).map_err(|e| { + RpcError::CustomError(format!( + "Failed to read accounts directory at {:?}: {}", + accounts_dir, e + )) + })?; + + for entry in entries { + let entry = entry + .map_err(|e| RpcError::CustomError(format!("Failed to read directory entry: {}", e)))?; + let path = entry.path(); + + if path.extension().and_then(|s| s.to_str()) == Some("json") { + let contents = fs::read_to_string(&path).map_err(|e| { + RpcError::CustomError(format!("Failed to read 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 pubkey = account_data + .pubkey + .parse::() + .map_err(|e| RpcError::CustomError(format!("Invalid pubkey: {}", 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 + ))); + }; + + let account = Account { + lamports: account_data.account.lamports, + data, + owner, + executable: account_data.account.executable, + rent_epoch: account_data.account.rent_epoch, + }; + + accounts.insert(pubkey, account); + } + } + + Ok(accounts) +} diff --git a/sdk-libs/program-test/src/utils/mod.rs b/sdk-libs/program-test/src/utils/mod.rs index e1b9d7be63..ccd3e02457 100644 --- a/sdk-libs/program-test/src/utils/mod.rs +++ b/sdk-libs/program-test/src/utils/mod.rs @@ -1,6 +1,8 @@ pub mod assert; pub mod create_account; pub mod find_light_bin; +pub mod load_accounts; +#[cfg(feature = "devenv")] pub mod register_test_forester; pub mod setup_light_programs; pub mod tree_accounts; diff --git a/sdk-libs/program-test/src/utils/setup_light_programs.rs b/sdk-libs/program-test/src/utils/setup_light_programs.rs index f5d67323d7..eaddcc43d0 100644 --- a/sdk-libs/program-test/src/utils/setup_light_programs.rs +++ b/sdk-libs/program-test/src/utils/setup_light_programs.rs @@ -1,9 +1,11 @@ use light_client::rpc::RpcError; use light_compressed_account::constants::REGISTERED_PROGRAM_PDA; +#[cfg(feature = "devenv")] use light_registry::account_compression_cpi::sdk::get_registered_program_pda; use litesvm::LiteSVM; use solana_compute_budget::compute_budget::ComputeBudget; use solana_pubkey::Pubkey; +use solana_sdk::pubkey; use crate::{ accounts::{ @@ -16,6 +18,11 @@ use crate::{ utils::find_light_bin::find_light_bin, }; +// Program IDs as Pubkeys +const ACCOUNT_COMPRESSION_ID: Pubkey = pubkey!("compr6CUsB5m2jS4Y3831ztGSTnDpnKJTKS95d64XVq"); +const LIGHT_REGISTRY_ID: Pubkey = pubkey!("Lighton6oQpVkeewmo2mcPTQQp7kYHr4fWpAgJyEmDX"); +const LIGHT_COMPRESSED_TOKEN_ID: Pubkey = pubkey!("cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m"); + /// Creates ProgramTestContext with light protocol and additional programs. /// /// Programs: @@ -46,19 +53,19 @@ pub fn setup_light_programs( )))?; let path = format!("{}/light_registry.so", light_bin_path); program_test - .add_program_from_file(light_registry::ID, path.clone()) + .add_program_from_file(LIGHT_REGISTRY_ID, path.clone()) .inspect_err(|_| { println!("Program light_registry bin not found in {}", path); })?; let path = format!("{}/account_compression.so", light_bin_path); program_test - .add_program_from_file(account_compression::ID, path.clone()) + .add_program_from_file(ACCOUNT_COMPRESSION_ID, path.clone()) .inspect_err(|_| { println!("Program account_compression bin not found in {}", path); })?; let path = format!("{}/light_compressed_token.so", light_bin_path); program_test - .add_program_from_file(light_compressed_token::ID, path.clone()) + .add_program_from_file(LIGHT_COMPRESSED_TOKEN_ID, path.clone()) .inspect_err(|_| { println!("Program light_compressed_token bin not found in {}", path); })?; @@ -89,11 +96,23 @@ pub fn setup_light_programs( RpcError::CustomError(format!("Setting registered program account failed {}", e)) })?; let registered_program = registered_program_test_account_registry_program(); - program_test - .set_account( - get_registered_program_pda(&light_registry::ID), - registered_program, + + #[cfg(feature = "devenv")] + let registry_pda = get_registered_program_pda(&LIGHT_REGISTRY_ID); + + #[cfg(not(feature = "devenv"))] + let registry_pda = { + // Compute the PDA manually in non-devenv mode + // This is the registered program PDA for light_registry + Pubkey::find_program_address( + &[b"registered_program", LIGHT_REGISTRY_ID.as_ref()], + &ACCOUNT_COMPRESSION_ID, ) + .0 + }; + + program_test + .set_account(registry_pda, registered_program) .map_err(|e| { RpcError::CustomError(format!("Setting registered program account failed {}", e)) })?; From 9f25af8ca41e1f350f1d2a9b6ce592e72b3b3e41 Mon Sep 17 00:00:00 2001 From: ananas Date: Thu, 9 Oct 2025 01:57:12 +0100 Subject: [PATCH 2/3] feat: LightAccount read only support --- program-tests/utils/src/conversions.rs | 1 - .../program-test/src/indexer/test_indexer.rs | 27 +- .../sdk-types/src/instruction/account_meta.rs | 5 +- sdk-libs/sdk/src/account.rs | 302 +++++++++++-- sdk-libs/sdk/src/cpi/instruction.rs | 7 + sdk-libs/sdk/src/cpi/invoke.rs | 8 + sdk-libs/sdk/src/cpi/v1/invoke.rs | 6 + sdk-libs/sdk/src/cpi/v2/invoke.rs | 38 ++ sdk-libs/sdk/src/error.rs | 14 + .../programs/sdk-anchor-test/src/lib.rs | 95 ++++ .../programs/sdk-anchor-test/src/read_only.rs | 152 +++++++ .../sdk-anchor-test/tests/read_only.rs | 426 ++++++++++++++++++ 12 files changed, 1031 insertions(+), 50 deletions(-) create mode 100644 sdk-tests/sdk-anchor-test/programs/sdk-anchor-test/src/read_only.rs create mode 100644 sdk-tests/sdk-anchor-test/programs/sdk-anchor-test/tests/read_only.rs diff --git a/program-tests/utils/src/conversions.rs b/program-tests/utils/src/conversions.rs index 2891fd6713..4104774adc 100644 --- a/program-tests/utils/src/conversions.rs +++ b/program-tests/utils/src/conversions.rs @@ -13,7 +13,6 @@ use light_sdk::{self as sdk}; // prove_by_index: sdk_merkle_context.prove_by_index, // } // } - // pub fn program_to_sdk_merkle_context( // program_merkle_context: ProgramMerkleContext, // ) -> sdk::merkle_context::MerkleContext { diff --git a/sdk-libs/program-test/src/indexer/test_indexer.rs b/sdk-libs/program-test/src/indexer/test_indexer.rs index 5c2d125237..17e89a6418 100644 --- a/sdk-libs/program-test/src/indexer/test_indexer.rs +++ b/sdk-libs/program-test/src/indexer/test_indexer.rs @@ -10,22 +10,13 @@ use crate::accounts::test_accounts::TestAccounts; pub(crate) const STATE_MERKLE_TREE_HEIGHT: u64 = 26; pub(crate) const STATE_MERKLE_TREE_CANOPY_DEPTH: u64 = 10; pub(crate) const STATE_MERKLE_TREE_ROOTS: u64 = 2400; -#[cfg(not(feature = "devenv"))] pub(crate) const DEFAULT_BATCH_STATE_TREE_HEIGHT: usize = 32; -#[cfg(not(feature = "devenv"))] pub(crate) const DEFAULT_BATCH_ADDRESS_TREE_HEIGHT: usize = 40; -#[cfg(not(feature = "devenv"))] pub(crate) const DEFAULT_BATCH_ROOT_HISTORY_LEN: usize = 200; use async_trait::async_trait; use borsh::BorshDeserialize; #[cfg(feature = "devenv")] -use light_batched_merkle_tree::{ - constants::{ - DEFAULT_BATCH_ADDRESS_TREE_HEIGHT, DEFAULT_BATCH_ROOT_HISTORY_LEN, - DEFAULT_BATCH_STATE_TREE_HEIGHT, - }, - merkle_tree::BatchedMerkleTreeAccount, -}; +use light_batched_merkle_tree::merkle_tree::BatchedMerkleTreeAccount; #[cfg(feature = "v2")] use light_client::indexer::MerkleProofWithContext; #[cfg(feature = "devenv")] @@ -1327,10 +1318,10 @@ impl TestIndexer { let (tree_type, merkle_tree, output_queue_batch_size) = if state_merkle_tree_account.tree_type == TreeType::StateV2 { let merkle_tree = Box::new(MerkleTree::::new_with_history( - DEFAULT_BATCH_STATE_TREE_HEIGHT as usize, + DEFAULT_BATCH_STATE_TREE_HEIGHT, 0, 0, - DEFAULT_BATCH_ROOT_HISTORY_LEN as usize, + DEFAULT_BATCH_ROOT_HISTORY_LEN, )); ( TreeType::StateV2, @@ -1541,10 +1532,10 @@ impl TestIndexer { params, ).await.unwrap(); let merkle_tree = Box::new(MerkleTree::::new_with_history( - DEFAULT_BATCH_STATE_TREE_HEIGHT as usize, + DEFAULT_BATCH_STATE_TREE_HEIGHT, 0, 0, - DEFAULT_BATCH_ROOT_HISTORY_LEN as usize, + DEFAULT_BATCH_ROOT_HISTORY_LEN, )); (FeeConfig::test_batched().state_merkle_tree_rollover as i64,merkle_tree, Some(params.output_queue_batch_size as usize)) @@ -2002,9 +1993,7 @@ impl TestIndexer { }); } - let (batch_inclusion_proof_inputs, legacy) = if height - == DEFAULT_BATCH_STATE_TREE_HEIGHT as usize - { + let (batch_inclusion_proof_inputs, legacy) = if height == DEFAULT_BATCH_STATE_TREE_HEIGHT { let inclusion_proof_inputs = InclusionProofInputs::new(inclusion_proofs.as_slice()).unwrap(); ( @@ -2263,8 +2252,8 @@ impl TestIndexer { CombinedJsonStruct { circuit_type: ProofType::Combined.to_string(), - state_tree_height: DEFAULT_BATCH_STATE_TREE_HEIGHT, - address_tree_height: DEFAULT_BATCH_ADDRESS_TREE_HEIGHT, + state_tree_height: DEFAULT_BATCH_STATE_TREE_HEIGHT as u32, + address_tree_height: DEFAULT_BATCH_ADDRESS_TREE_HEIGHT as u32, public_input_hash: big_int_to_string(&public_input_hash), inclusion: inclusion_payload.unwrap().inputs, non_inclusion: non_inclusion_payload.inputs, diff --git a/sdk-libs/sdk-types/src/instruction/account_meta.rs b/sdk-libs/sdk-types/src/instruction/account_meta.rs index 81855db72c..74e277792d 100644 --- a/sdk-libs/sdk-types/src/instruction/account_meta.rs +++ b/sdk-libs/sdk-types/src/instruction/account_meta.rs @@ -167,16 +167,17 @@ impl CompressedAccountMetaTrait for CompressedAccountMetaWithLamports { Some(self.output_state_tree_index) } } +pub type CompressedAccountMetaBurn = CompressedAccountMetaReadOnly; #[derive(Default, Debug, Clone, Copy, PartialEq, AnchorSerialize, AnchorDeserialize)] -pub struct CompressedAccountMetaBurn { +pub struct CompressedAccountMetaReadOnly { /// State Merkle tree context. pub tree_info: PackedStateTreeInfo, /// Address. pub address: [u8; 32], } -impl CompressedAccountMetaTrait for CompressedAccountMetaBurn { +impl CompressedAccountMetaTrait for CompressedAccountMetaReadOnly { fn get_tree_info(&self) -> &PackedStateTreeInfo { &self.tree_info } diff --git a/sdk-libs/sdk/src/account.rs b/sdk-libs/sdk/src/account.rs index 1083ddcca8..dbad5e1c57 100644 --- a/sdk-libs/sdk/src/account.rs +++ b/sdk-libs/sdk/src/account.rs @@ -119,10 +119,7 @@ //! ``` // TODO: add example for manual hashing -use std::{ - marker::PhantomData, - ops::{Deref, DerefMut}, -}; +use std::marker::PhantomData; use light_compressed_account::{ compressed_account::PackedMerkleContext, @@ -191,6 +188,8 @@ pub mod __internal { data::OutputCompressedAccountWithPackedContext, with_readonly::InAccount, }; use light_sdk_types::instruction::account_meta::CompressedAccountMetaBurn; + #[cfg(feature = "v2")] + use light_sdk_types::instruction::account_meta::CompressedAccountMetaReadOnly; use solana_program_error::ProgramError; use super::*; @@ -207,9 +206,41 @@ pub mod __internal { pub account: A, account_info: CompressedAccountInfo, should_remove_data: bool, + /// If set, this account is read-only and this contains the precomputed account hash. + pub read_only_account_hash: Option<[u8; 32]>, _hasher: PhantomData, } + impl< + 'a, + H: Hasher, + A: AnchorSerialize + AnchorDeserialize + LightDiscriminator + Default, + const HASH_FLAT: bool, + > core::ops::Deref for LightAccountInner<'a, H, A, HASH_FLAT> + { + type Target = A; + + fn deref(&self) -> &Self::Target { + &self.account + } + } + + impl< + 'a, + H: Hasher, + A: AnchorSerialize + AnchorDeserialize + LightDiscriminator + Default, + const HASH_FLAT: bool, + > core::ops::DerefMut for LightAccountInner<'a, H, A, HASH_FLAT> + { + fn deref_mut(&mut self) -> &mut Self::Target { + assert!( + self.read_only_account_hash.is_none(), + "Cannot mutate read-only account" + ); + &mut self.account + } + } + impl< 'a, H: Hasher, @@ -236,6 +267,7 @@ pub mod __internal { output: Some(output_account_info), }, should_remove_data: false, + read_only_account_hash: None, _hasher: PhantomData, } } @@ -331,6 +363,7 @@ pub mod __internal { output: Some(output_account_info), }, should_remove_data: false, + read_only_account_hash: None, _hasher: PhantomData, }) } @@ -376,6 +409,7 @@ pub mod __internal { output: Some(output_account_info), }, should_remove_data: false, + read_only_account_hash: None, _hasher: PhantomData, }) } @@ -428,11 +462,101 @@ pub mod __internal { output: None, }, should_remove_data: false, + read_only_account_hash: None, + _hasher: PhantomData, + }) + } + + /// Creates a read-only compressed account for validation without state updates. + /// Read-only accounts are used to prove that an account exists in a specific state + /// without modifying it (v2 only). + /// + /// # Arguments + /// * `owner` - The program that owns this compressed account + /// * `input_account_meta` - Metadata about the existing compressed account + /// * `input_account` - The account data to validate + /// * `packed_account_pubkeys` - Slice of packed pubkeys from CPI accounts (packed accounts after system accounts) + /// + /// # Note + /// Data hashing is consistent with the hasher type (H): SHA256 for `sha::LightAccount`, + /// Poseidon for `LightAccount`. The same hasher is used for both the data hash and account hash. + #[cfg(feature = "v2")] + pub fn new_read_only( + owner: &'a Pubkey, + input_account_meta: &CompressedAccountMetaReadOnly, + input_account: A, + packed_account_pubkeys: &[Pubkey], + ) -> Result { + // Hash account data once and reuse + let input_data_hash = input_account + .hash::() + .map_err(LightSdkError::from) + .map_err(ProgramError::from)?; + let tree_info = input_account_meta.get_tree_info(); + + let input_account_info = InAccountInfo { + data_hash: input_data_hash, + lamports: 0, // read-only accounts don't track lamports + merkle_context: PackedMerkleContext { + merkle_tree_pubkey_index: tree_info.merkle_tree_pubkey_index, + queue_pubkey_index: tree_info.queue_pubkey_index, + leaf_index: tree_info.leaf_index, + prove_by_index: tree_info.prove_by_index, + }, + root_index: input_account_meta.get_root_index().unwrap_or_default(), + discriminator: A::LIGHT_DISCRIMINATOR, + }; + + // Compute account hash for read-only account + let account_hash = { + use light_compressed_account::compressed_account::{ + CompressedAccount, CompressedAccountData, + }; + + let compressed_account = CompressedAccount { + address: Some(input_account_meta.address), + owner: owner.to_bytes().into(), + data: Some(CompressedAccountData { + data: vec![], // not used for hash computation + data_hash: input_data_hash, // Reuse already computed hash + discriminator: A::LIGHT_DISCRIMINATOR, + }), + lamports: 0, + }; + + // Get merkle tree pubkey from packed pubkeys slice + let merkle_tree_pubkey = packed_account_pubkeys + .get(tree_info.merkle_tree_pubkey_index as usize) + .ok_or(LightSdkError::InvalidMerkleTreeIndex) + .map_err(ProgramError::from)? + .to_bytes() + .into(); + + compressed_account + .hash(&merkle_tree_pubkey, &tree_info.leaf_index, true) + .map_err(LightSdkError::from) + .map_err(ProgramError::from)? + }; + + Ok(Self { + owner, + account: input_account, + account_info: CompressedAccountInfo { + address: Some(input_account_meta.address), + input: Some(input_account_info), + output: None, + }, + should_remove_data: false, + read_only_account_hash: Some(account_hash), _hasher: PhantomData, }) } pub fn to_account_info(mut self) -> Result { + if self.read_only_account_hash.is_some() { + return Err(LightSdkError::ReadOnlyAccountCannotUseToAccountInfo.into()); + } + if let Some(output) = self.account_info.output.as_mut() { if self.should_remove_data { // Data should be empty to close account. @@ -456,6 +580,30 @@ pub mod __internal { } Ok(self.account_info) } + + #[cfg(feature = "v2")] + pub fn to_packed_read_only_account( + self, + ) -> Result< + light_compressed_account::compressed_account::PackedReadOnlyCompressedAccount, + ProgramError, + > { + let account_hash = self + .read_only_account_hash + .ok_or(LightSdkError::NotReadOnlyAccount)?; + + let input_account = self + .account_info + .input + .ok_or(ProgramError::InvalidAccountData)?; + + use light_compressed_account::compressed_account::PackedReadOnlyCompressedAccount; + Ok(PackedReadOnlyCompressedAccount { + root_index: input_account.root_index, + merkle_context: input_account.merkle_context, + account_hash, + }) + } pub fn to_in_account(&self) -> Option { self.account_info .input @@ -563,6 +711,7 @@ pub mod __internal { output: Some(output_account_info), }, should_remove_data: false, + read_only_account_hash: None, _hasher: PhantomData, }) } @@ -649,6 +798,7 @@ pub mod __internal { output: Some(output_account_info), }, should_remove_data: false, + read_only_account_hash: None, _hasher: PhantomData, }) } @@ -708,11 +858,105 @@ pub mod __internal { output: None, }, should_remove_data: false, + read_only_account_hash: None, + _hasher: PhantomData, + }) + } + + /// Creates a read-only compressed account for validation without state updates. + /// Read-only accounts are used to prove that an account exists in a specific state + /// without modifying it (v2 only). + /// + /// # Arguments + /// * `owner` - The program that owns this compressed account + /// * `input_account_meta` - Metadata about the existing compressed account + /// * `input_account` - The account data to validate + /// * `packed_account_pubkeys` - Slice of packed pubkeys from CPI accounts (packed accounts after system accounts) + /// + /// # Note + /// Uses SHA256 flat hashing with borsh serialization (HASH_FLAT = true). + #[cfg(feature = "v2")] + pub fn new_read_only( + owner: &'a Pubkey, + input_account_meta: &CompressedAccountMetaReadOnly, + input_account: A, + packed_account_pubkeys: &[Pubkey], + ) -> Result { + // Hash account data once and reuse (SHA256 flat: borsh serialize then hash) + let data = input_account + .try_to_vec() + .map_err(|_| LightSdkError::Borsh) + .map_err(ProgramError::from)?; + let mut input_data_hash = H::hash(data.as_slice()) + .map_err(LightSdkError::from) + .map_err(ProgramError::from)?; + input_data_hash[0] = 0; + + let tree_info = input_account_meta.get_tree_info(); + + let input_account_info = InAccountInfo { + data_hash: input_data_hash, + lamports: 0, // read-only accounts don't track lamports + merkle_context: PackedMerkleContext { + merkle_tree_pubkey_index: tree_info.merkle_tree_pubkey_index, + queue_pubkey_index: tree_info.queue_pubkey_index, + leaf_index: tree_info.leaf_index, + prove_by_index: tree_info.prove_by_index, + }, + root_index: input_account_meta.get_root_index().unwrap_or_default(), + discriminator: A::LIGHT_DISCRIMINATOR, + }; + + // Compute account hash for read-only account + let account_hash = { + use light_compressed_account::compressed_account::{ + CompressedAccount, CompressedAccountData, + }; + + let compressed_account = CompressedAccount { + address: Some(input_account_meta.address), + owner: owner.to_bytes().into(), + data: Some(CompressedAccountData { + data: vec![], // not used for hash computation + data_hash: input_data_hash, // Reuse already computed hash + discriminator: A::LIGHT_DISCRIMINATOR, + }), + lamports: 0, + }; + + // Get merkle tree pubkey from packed pubkeys slice + let merkle_tree_pubkey = packed_account_pubkeys + .get(tree_info.merkle_tree_pubkey_index as usize) + .ok_or(LightSdkError::InvalidMerkleTreeIndex) + .map_err(ProgramError::from)? + .to_bytes() + .into(); + + compressed_account + .hash(&merkle_tree_pubkey, &tree_info.leaf_index, true) + .map_err(LightSdkError::from) + .map_err(ProgramError::from)? + }; + + Ok(Self { + owner, + account: input_account, + account_info: CompressedAccountInfo { + address: Some(input_account_meta.address), + input: Some(input_account_info), + output: None, + }, + should_remove_data: false, + read_only_account_hash: Some(account_hash), _hasher: PhantomData, }) } pub fn to_account_info(mut self) -> Result { + if self.read_only_account_hash.is_some() { + return Err(LightSdkError::ReadOnlyAccountCannotUseToAccountInfo.into()); + } + if let Some(output) = self.account_info.output.as_mut() { if self.should_remove_data { // Data should be empty to close account. @@ -735,12 +979,38 @@ pub mod __internal { } Ok(self.account_info) } + + #[cfg(feature = "v2")] + pub fn to_packed_read_only_account( + self, + ) -> Result< + light_compressed_account::compressed_account::PackedReadOnlyCompressedAccount, + ProgramError, + > { + let account_hash = self + .read_only_account_hash + .ok_or(LightSdkError::NotReadOnlyAccount)?; + + let input_account = self + .account_info + .input + .ok_or(ProgramError::InvalidAccountData)?; + + use light_compressed_account::compressed_account::PackedReadOnlyCompressedAccount; + Ok(PackedReadOnlyCompressedAccount { + root_index: input_account.root_index, + merkle_context: input_account.merkle_context, + account_hash, + }) + } + pub fn to_in_account(&self) -> Option { self.account_info .input .as_ref() .map(|input| input.into_in_account(self.account_info.address)) } + pub fn to_output_compressed_account_with_packed_context( &self, owner: Option, @@ -786,28 +1056,4 @@ pub mod __internal { } } } - - impl< - H: Hasher, - A: AnchorSerialize + AnchorDeserialize + LightDiscriminator + Default, - const HASH_FLAT: bool, - > Deref for LightAccountInner<'_, H, A, HASH_FLAT> - { - type Target = A; - - fn deref(&self) -> &Self::Target { - &self.account - } - } - - impl< - H: Hasher, - A: AnchorSerialize + AnchorDeserialize + LightDiscriminator + Default, - const HASH_FLAT: bool, - > DerefMut for LightAccountInner<'_, H, A, HASH_FLAT> - { - fn deref_mut(&mut self) -> &mut ::Target { - &mut self.account - } - } } diff --git a/sdk-libs/sdk/src/cpi/instruction.rs b/sdk-libs/sdk/src/cpi/instruction.rs index be4856932f..afb46815d3 100644 --- a/sdk-libs/sdk/src/cpi/instruction.rs +++ b/sdk-libs/sdk/src/cpi/instruction.rs @@ -92,4 +92,11 @@ pub trait LightCpiInstruction: Sized { fn get_cpi_context( &self, ) -> &light_compressed_account::instruction_data::cpi_context::CompressedCpiContext; + + /// Returns whether this instruction has any read-only accounts. + /// + /// # Availability + /// Only available with the `cpi-context` feature enabled. + #[cfg(feature = "cpi-context")] + fn has_read_only_accounts(&self) -> bool; } diff --git a/sdk-libs/sdk/src/cpi/invoke.rs b/sdk-libs/sdk/src/cpi/invoke.rs index 0d97a78a21..91b0bf479d 100644 --- a/sdk-libs/sdk/src/cpi/invoke.rs +++ b/sdk-libs/sdk/src/cpi/invoke.rs @@ -136,6 +136,14 @@ fn inner_invoke_write_to_cpi_context_typed<'info, T>( where T: LightInstructionData + LightCpiInstruction, { + // Check if read-only accounts are present + if instruction_data.has_read_only_accounts() { + solana_msg::msg!( + "Read-only accounts are not supported in write_to_cpi_context operations. Use invoke_execute_cpi_context() instead." + ); + return Err(LightSdkError::ReadOnlyAccountsNotSupportedInCpiContext.into()); + } + // Serialize instruction data with discriminator let data = instruction_data .data() diff --git a/sdk-libs/sdk/src/cpi/v1/invoke.rs b/sdk-libs/sdk/src/cpi/v1/invoke.rs index dc13bd258b..ee16ad021b 100644 --- a/sdk-libs/sdk/src/cpi/v1/invoke.rs +++ b/sdk-libs/sdk/src/cpi/v1/invoke.rs @@ -347,6 +347,12 @@ impl LightCpiInstruction for LightSystemProgramCpi { .as_ref() .unwrap_or(&DEFAULT) } + + #[cfg(feature = "cpi-context")] + fn has_read_only_accounts(&self) -> bool { + // V1 doesn't support read-only accounts + false + } } // Manual BorshSerialize implementation that only serializes instruction_data diff --git a/sdk-libs/sdk/src/cpi/v2/invoke.rs b/sdk-libs/sdk/src/cpi/v2/invoke.rs index 9ef8b3ddfd..60841eecdd 100644 --- a/sdk-libs/sdk/src/cpi/v2/invoke.rs +++ b/sdk-libs/sdk/src/cpi/v2/invoke.rs @@ -44,6 +44,13 @@ impl LightCpiInstruction for InstructionDataInvokeCpiWithReadOnly { where A: AnchorSerialize + AnchorDeserialize + LightDiscriminator + Default, { + // Check if this is a read-only account + if account.read_only_account_hash.is_some() { + let read_only_account = account.to_packed_read_only_account()?; + self.read_only_accounts.push(read_only_account); + return Ok(self); + } + // Convert LightAccount to instruction data format let account_info = account.to_account_info()?; @@ -94,6 +101,13 @@ impl LightCpiInstruction for InstructionDataInvokeCpiWithReadOnly { where A: AnchorSerialize + AnchorDeserialize + DataHasher + LightDiscriminator + Default, { + // Check if this is a read-only account + if account.read_only_account_hash.is_some() { + let read_only_account = account.to_packed_read_only_account()?; + self.read_only_accounts.push(read_only_account); + return Ok(self); + } + // Convert LightAccount to instruction data format let account_info = account.to_account_info()?; @@ -169,6 +183,11 @@ impl LightCpiInstruction for InstructionDataInvokeCpiWithReadOnly { fn get_bump(&self) -> u8 { self.bump } + + #[cfg(feature = "cpi-context")] + fn has_read_only_accounts(&self) -> bool { + !self.read_only_accounts.is_empty() + } } impl LightCpiInstruction for InstructionDataInvokeCpiWithAccountInfo { @@ -186,6 +205,13 @@ impl LightCpiInstruction for InstructionDataInvokeCpiWithAccountInfo { where A: AnchorSerialize + AnchorDeserialize + LightDiscriminator + Default, { + // Check if this is a read-only account + if account.read_only_account_hash.is_some() { + let read_only_account = account.to_packed_read_only_account()?; + self.read_only_accounts.push(read_only_account); + return Ok(self); + } + // Convert LightAccount to instruction data format let account_info = account.to_account_info()?; self.account_infos.push(account_info); @@ -199,6 +225,13 @@ impl LightCpiInstruction for InstructionDataInvokeCpiWithAccountInfo { where A: AnchorSerialize + AnchorDeserialize + LightDiscriminator + DataHasher + Default, { + // Check if this is a read-only account + if account.read_only_account_hash.is_some() { + let read_only_account = account.to_packed_read_only_account()?; + self.read_only_accounts.push(read_only_account); + return Ok(self); + } + // Convert LightAccount to instruction data format let account_info = account.to_account_info()?; self.account_infos.push(account_info); @@ -237,4 +270,9 @@ impl LightCpiInstruction for InstructionDataInvokeCpiWithAccountInfo { fn get_bump(&self) -> u8 { self.bump } + + #[cfg(feature = "cpi-context")] + fn has_read_only_accounts(&self) -> bool { + !self.read_only_accounts.is_empty() + } } diff --git a/sdk-libs/sdk/src/error.rs b/sdk-libs/sdk/src/error.rs index 33e876f1cd..f8d183a56d 100644 --- a/sdk-libs/sdk/src/error.rs +++ b/sdk-libs/sdk/src/error.rs @@ -81,6 +81,16 @@ pub enum LightSdkError { ExpectedNoData, #[error("CPI context must be added before any other accounts (next_index must be 0)")] CpiContextOrderingViolation, + #[error("Invalid merkle tree index in CPI accounts")] + InvalidMerkleTreeIndex, + #[error( + "Read-only account cannot use to_account_info(), use to_packed_read_only_account() instead" + )] + ReadOnlyAccountCannotUseToAccountInfo, + #[error("Account is not read-only, cannot use to_packed_read_only_account()")] + NotReadOnlyAccount, + #[error("Read-only accounts are not supported in write_to_cpi_context operations")] + ReadOnlyAccountsNotSupportedInCpiContext, #[error(transparent)] AccountError(#[from] AccountError), #[error(transparent)] @@ -170,6 +180,10 @@ impl From for u32 { LightSdkError::InvalidCpiAccountsOffset => 16034, LightSdkError::ExpectedNoData => 16035, LightSdkError::CpiContextOrderingViolation => 16036, + LightSdkError::InvalidMerkleTreeIndex => 16037, + LightSdkError::ReadOnlyAccountCannotUseToAccountInfo => 16038, + LightSdkError::NotReadOnlyAccount => 16039, + LightSdkError::ReadOnlyAccountsNotSupportedInCpiContext => 16040, LightSdkError::AccountError(e) => e.into(), LightSdkError::Hasher(e) => e.into(), LightSdkError::ZeroCopy(e) => e.into(), diff --git a/sdk-tests/sdk-anchor-test/programs/sdk-anchor-test/src/lib.rs b/sdk-tests/sdk-anchor-test/programs/sdk-anchor-test/src/lib.rs index 12da4847da..febaaba1d9 100644 --- a/sdk-tests/sdk-anchor-test/programs/sdk-anchor-test/src/lib.rs +++ b/sdk-tests/sdk-anchor-test/programs/sdk-anchor-test/src/lib.rs @@ -1,6 +1,8 @@ #![allow(unexpected_cfgs)] #![allow(deprecated)] +mod read_only; + use anchor_lang::{prelude::*, Discriminator}; use light_sdk::{ // anchor test test poseidon LightAccount, native tests sha256 LightAccount @@ -178,6 +180,49 @@ pub mod sdk_anchor_test { Ok(()) } + /// Create compressed account with Poseidon hashing + pub fn create_compressed_account_poseidon<'info>( + ctx: Context<'_, '_, '_, 'info, WithNestedData<'info>>, + proof: ValidityProof, + address_tree_info: PackedAddressTreeInfo, + output_tree_index: u8, + name: String, + ) -> Result<()> { + let light_cpi_accounts = CpiAccounts::new( + ctx.accounts.signer.as_ref(), + ctx.remaining_accounts, + crate::LIGHT_CPI_SIGNER, + ); + + let (address, address_seed) = derive_address( + &[b"compressed", name.as_bytes()], + &address_tree_info + .get_tree_pubkey(&light_cpi_accounts) + .map_err(|_| ErrorCode::AccountNotEnoughKeys)?, + &crate::ID, + ); + let new_address_params = + address_tree_info.into_new_address_params_assigned_packed(address_seed, Some(0)); + + let mut my_compressed_account = light_sdk::account::poseidon::LightAccount::< + '_, + MyCompressedAccount, + >::new_init( + &crate::ID, Some(address), output_tree_index + ); + + my_compressed_account.name = name; + my_compressed_account.nested = NestedData::default(); + + InstructionDataInvokeCpiWithReadOnly::new_cpi(LIGHT_CPI_SIGNER, proof) + .mode_v1() + .with_light_account_poseidon(my_compressed_account)? + .with_new_addresses(&[new_address_params]) + .invoke(light_cpi_accounts)?; + + Ok(()) + } + // V2 Instructions pub fn create_compressed_account_v2<'info>( ctx: Context<'_, '_, '_, 'info, WithNestedData<'info>>, @@ -276,6 +321,56 @@ pub mod sdk_anchor_test { Ok(()) } + + /// Test read-only account with SHA256 hasher using LightSystemProgramCpi + pub fn read_sha256_light_system_cpi<'info>( + ctx: Context<'_, '_, '_, 'info, UpdateNestedData<'info>>, + proof: ValidityProof, + my_compressed_account: MyCompressedAccount, + account_meta: CompressedAccountMetaBurn, + ) -> Result<()> { + read_only::process_read_sha256_light_system_cpi( + ctx, + proof, + my_compressed_account, + account_meta, + ) + } + + /// Test read-only account with Poseidon hasher using LightSystemProgramCpi + pub fn read_poseidon_light_system_cpi<'info>( + ctx: Context<'_, '_, '_, 'info, UpdateNestedData<'info>>, + proof: ValidityProof, + my_compressed_account: MyCompressedAccount, + account_meta: CompressedAccountMetaBurn, + ) -> Result<()> { + read_only::process_read_poseidon_light_system_cpi( + ctx, + proof, + my_compressed_account, + account_meta, + ) + } + + /// Test read-only account with SHA256 hasher using InstructionDataInvokeCpiWithReadOnly + pub fn read_sha256_lowlevel<'info>( + ctx: Context<'_, '_, '_, 'info, UpdateNestedData<'info>>, + proof: ValidityProof, + my_compressed_account: MyCompressedAccount, + account_meta: CompressedAccountMetaBurn, + ) -> Result<()> { + read_only::process_read_sha256_lowlevel(ctx, proof, my_compressed_account, account_meta) + } + + /// Test read-only account with Poseidon hasher using InstructionDataInvokeCpiWithReadOnly + pub fn read_poseidon_lowlevel<'info>( + ctx: Context<'_, '_, '_, 'info, UpdateNestedData<'info>>, + proof: ValidityProof, + my_compressed_account: MyCompressedAccount, + account_meta: CompressedAccountMetaBurn, + ) -> Result<()> { + read_only::process_read_poseidon_lowlevel(ctx, proof, my_compressed_account, account_meta) + } } #[event] diff --git a/sdk-tests/sdk-anchor-test/programs/sdk-anchor-test/src/read_only.rs b/sdk-tests/sdk-anchor-test/programs/sdk-anchor-test/src/read_only.rs new file mode 100644 index 0000000000..e6d25cda59 --- /dev/null +++ b/sdk-tests/sdk-anchor-test/programs/sdk-anchor-test/src/read_only.rs @@ -0,0 +1,152 @@ +use anchor_lang::prelude::*; +use light_sdk::{ + account::LightAccount, + cpi::{ + v1::CpiAccounts, + v2::{lowlevel::InstructionDataInvokeCpiWithReadOnly, LightSystemProgramCpi}, + InvokeLightSystemProgram, LightCpiInstruction, + }, + instruction::{account_meta::CompressedAccountMetaBurn, ValidityProof}, +}; + +use crate::{MyCompressedAccount, UpdateNestedData, LIGHT_CPI_SIGNER}; + +#[error_code] +pub enum ReadOnlyError { + #[msg("Invalid account")] + InvalidAccount, +} + +/// Test read-only account validation with SHA256 hasher using LightSystemProgramCpi (v2) +pub fn process_read_sha256_light_system_cpi<'info>( + ctx: Context<'_, '_, '_, 'info, UpdateNestedData<'info>>, + proof: ValidityProof, + my_compressed_account: MyCompressedAccount, + account_meta: CompressedAccountMetaBurn, +) -> Result<()> { + let light_cpi_accounts = CpiAccounts::new( + ctx.accounts.signer.as_ref(), + ctx.remaining_accounts, + crate::LIGHT_CPI_SIGNER, + ); + + // Create read-only account with SHA256 hasher + let tree_pubkeys = light_cpi_accounts + .tree_pubkeys() + .map_err(|_| error!(ReadOnlyError::InvalidAccount))?; + + let read_only_account = LightAccount::<'_, MyCompressedAccount>::new_read_only( + &crate::ID, + &account_meta, + my_compressed_account, + tree_pubkeys.as_slice(), + )?; + + LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, proof) + .mode_v1() + .with_light_account(read_only_account)? + .invoke(light_cpi_accounts)?; + + Ok(()) +} + +/// Test read-only account validation with Poseidon hasher using LightSystemProgramCpi (v2) +pub fn process_read_poseidon_light_system_cpi<'info>( + ctx: Context<'_, '_, '_, 'info, UpdateNestedData<'info>>, + proof: ValidityProof, + my_compressed_account: MyCompressedAccount, + account_meta: CompressedAccountMetaBurn, +) -> Result<()> { + let light_cpi_accounts = CpiAccounts::new( + ctx.accounts.signer.as_ref(), + ctx.remaining_accounts, + crate::LIGHT_CPI_SIGNER, + ); + + // Create read-only account with Poseidon hasher + let tree_pubkeys = light_cpi_accounts + .tree_pubkeys() + .map_err(|_| error!(ReadOnlyError::InvalidAccount))?; + + let read_only_account = + light_sdk::account::poseidon::LightAccount::<'_, MyCompressedAccount>::new_read_only( + &crate::ID, + &account_meta, + my_compressed_account, + tree_pubkeys.as_slice(), + )?; + + LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, proof) + .mode_v1() + .with_light_account_poseidon(read_only_account)? + .invoke(light_cpi_accounts)?; + + Ok(()) +} + +/// Test read-only account with SHA256 hasher using InstructionDataInvokeCpiWithReadOnly (v2) +pub fn process_read_sha256_lowlevel<'info>( + ctx: Context<'_, '_, '_, 'info, UpdateNestedData<'info>>, + proof: ValidityProof, + my_compressed_account: MyCompressedAccount, + account_meta: CompressedAccountMetaBurn, +) -> Result<()> { + let light_cpi_accounts = CpiAccounts::new( + ctx.accounts.signer.as_ref(), + ctx.remaining_accounts, + crate::LIGHT_CPI_SIGNER, + ); + + // Create read-only account with SHA256 hasher + let tree_pubkeys = light_cpi_accounts + .tree_pubkeys() + .map_err(|_| error!(ReadOnlyError::InvalidAccount))?; + + let read_only_account = LightAccount::<'_, MyCompressedAccount>::new_read_only( + &crate::ID, + &account_meta, + my_compressed_account, + tree_pubkeys.as_slice(), + )?; + + InstructionDataInvokeCpiWithReadOnly::new_cpi(LIGHT_CPI_SIGNER, proof) + .mode_v1() + .with_light_account(read_only_account)? + .invoke(light_cpi_accounts)?; + + Ok(()) +} + +/// Test read-only account with Poseidon hasher using InstructionDataInvokeCpiWithReadOnly (v2) +pub fn process_read_poseidon_lowlevel<'info>( + ctx: Context<'_, '_, '_, 'info, UpdateNestedData<'info>>, + proof: ValidityProof, + my_compressed_account: MyCompressedAccount, + account_meta: CompressedAccountMetaBurn, +) -> Result<()> { + let light_cpi_accounts = CpiAccounts::new( + ctx.accounts.signer.as_ref(), + ctx.remaining_accounts, + crate::LIGHT_CPI_SIGNER, + ); + + // Create read-only account with Poseidon hasher + let tree_pubkeys = light_cpi_accounts + .tree_pubkeys() + .map_err(|_| error!(ReadOnlyError::InvalidAccount))?; + + let read_only_account = + light_sdk::account::poseidon::LightAccount::<'_, MyCompressedAccount>::new_read_only( + &crate::ID, + &account_meta, + my_compressed_account, + tree_pubkeys.as_slice(), + )?; + + InstructionDataInvokeCpiWithReadOnly::new_cpi(LIGHT_CPI_SIGNER, proof) + .mode_v1() + .with_light_account_poseidon(read_only_account)? + .invoke(light_cpi_accounts)?; + + Ok(()) +} diff --git a/sdk-tests/sdk-anchor-test/programs/sdk-anchor-test/tests/read_only.rs b/sdk-tests/sdk-anchor-test/programs/sdk-anchor-test/tests/read_only.rs new file mode 100644 index 0000000000..7ed9bccff7 --- /dev/null +++ b/sdk-tests/sdk-anchor-test/programs/sdk-anchor-test/tests/read_only.rs @@ -0,0 +1,426 @@ +#![cfg(feature = "test-sbf")] + +use anchor_lang::AnchorDeserialize; +use light_client::indexer::CompressedAccount; +use light_program_test::{ + program_test::LightProgramTest, AddressWithTree, Indexer, ProgramTestConfig, +}; +use light_sdk::{ + address::v1::derive_address, + instruction::{ + account_meta::CompressedAccountMetaBurn, PackedAccounts, SystemAccountMetaConfig, + }, +}; +use light_test_utils::{Rpc, RpcError}; +use sdk_anchor_test::MyCompressedAccount; +use solana_sdk::{ + instruction::{AccountMeta, Instruction}, + signature::{Keypair, Signature, Signer}, +}; + +#[tokio::test] +async fn test_read_sha256() { + let config = + ProgramTestConfig::new_v2(true, Some(vec![("sdk_anchor_test", sdk_anchor_test::ID)])); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + let address_tree_info = rpc.get_address_tree_v1(); + + let (address, _) = derive_address( + &[b"compressed", b"readonly_sha_test".as_slice()], + &address_tree_info.tree, + &sdk_anchor_test::ID, + ); + + // Create a compressed account to test read-only on + create_compressed_account("readonly_sha_test".to_string(), &mut rpc, &payer, &address) + .await + .unwrap(); + + // Get the created account + let compressed_account = rpc + .get_compressed_account(address, None) + .await + .unwrap() + .value + .unwrap(); + + // Test read_sha256_light_system_cpi + read_sha256_light_system_cpi(&mut rpc, &payer, compressed_account.clone()) + .await + .unwrap(); + + // Test read_sha256_lowlevel + read_sha256_lowlevel(&mut rpc, &payer, compressed_account) + .await + .unwrap(); +} + +#[tokio::test] +async fn test_read_poseidon() { + let config = + ProgramTestConfig::new_v2(true, Some(vec![("sdk_anchor_test", sdk_anchor_test::ID)])); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + let address_tree_info = rpc.get_address_tree_v1(); + + let (address, _) = derive_address( + &[b"compressed", b"readonly_poseidon_test".as_slice()], + &address_tree_info.tree, + &sdk_anchor_test::ID, + ); + + // Create a compressed account with Poseidon hashing to test read-only on + create_compressed_account_poseidon( + "readonly_poseidon_test".to_string(), + &mut rpc, + &payer, + &address, + ) + .await + .unwrap(); + + // Get the created account + let compressed_account = rpc + .get_compressed_account(address, None) + .await + .unwrap() + .value + .unwrap(); + + // Test read_poseidon_light_system_cpi + read_poseidon_light_system_cpi(&mut rpc, &payer, compressed_account.clone()) + .await + .unwrap(); + + // Test read_poseidon_lowlevel + read_poseidon_lowlevel(&mut rpc, &payer, compressed_account) + .await + .unwrap(); +} + +async fn create_compressed_account( + name: String, + rpc: &mut LightProgramTest, + payer: &Keypair, + address: &[u8; 32], +) -> Result { + let config = SystemAccountMetaConfig::new(sdk_anchor_test::ID); + let mut remaining_accounts = PackedAccounts::default(); + remaining_accounts.add_system_accounts(config).unwrap(); + + let address_merkle_tree_info = rpc.get_address_tree_v1(); + + let rpc_result = rpc + .get_validity_proof( + vec![], + vec![AddressWithTree { + address: *address, + tree: address_merkle_tree_info.tree, + }], + None, + ) + .await? + .value; + let packed_accounts = rpc_result.pack_tree_infos(&mut remaining_accounts); + + let output_tree_index = rpc + .get_random_state_tree_info() + .unwrap() + .pack_output_tree_index(&mut remaining_accounts) + .unwrap(); + + let (remaining_accounts, _, _) = remaining_accounts.to_account_metas(); + + let instruction = Instruction { + program_id: sdk_anchor_test::ID, + accounts: [ + vec![AccountMeta::new(payer.pubkey(), true)], + remaining_accounts, + ] + .concat(), + data: { + use anchor_lang::InstructionData; + sdk_anchor_test::instruction::CreateCompressedAccount { + proof: rpc_result.proof, + address_tree_info: packed_accounts.address_trees[0], + output_tree_index, + name, + } + .data() + }, + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .await +} + +async fn read_sha256_light_system_cpi( + rpc: &mut LightProgramTest, + payer: &Keypair, + mut compressed_account: CompressedAccount, +) -> Result { + let mut remaining_accounts = PackedAccounts::default(); + + let config = SystemAccountMetaConfig::new(sdk_anchor_test::ID); + remaining_accounts.add_system_accounts(config).unwrap(); + let hash = compressed_account.hash; + + let rpc_result = rpc + .get_validity_proof(vec![hash], vec![], None) + .await? + .value; + + let packed_tree_accounts = rpc_result + .pack_tree_infos(&mut remaining_accounts) + .state_trees + .unwrap(); + + let (remaining_accounts, _, _) = remaining_accounts.to_account_metas(); + + let my_compressed_account = MyCompressedAccount::deserialize( + &mut compressed_account.data.as_mut().unwrap().data.as_slice(), + ) + .unwrap(); + + let instruction = Instruction { + program_id: sdk_anchor_test::ID, + accounts: [ + vec![AccountMeta::new(payer.pubkey(), true)], + remaining_accounts, + ] + .concat(), + data: { + use anchor_lang::InstructionData; + sdk_anchor_test::instruction::ReadSha256LightSystemCpi { + proof: rpc_result.proof, + my_compressed_account, + account_meta: CompressedAccountMetaBurn { + tree_info: packed_tree_accounts.packed_tree_infos[0], + address: compressed_account.address.unwrap(), + }, + } + .data() + }, + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .await +} + +async fn read_sha256_lowlevel( + rpc: &mut LightProgramTest, + payer: &Keypair, + mut compressed_account: CompressedAccount, +) -> Result { + let mut remaining_accounts = PackedAccounts::default(); + + let config = SystemAccountMetaConfig::new(sdk_anchor_test::ID); + remaining_accounts.add_system_accounts(config).unwrap(); + let hash = compressed_account.hash; + + let rpc_result = rpc + .get_validity_proof(vec![hash], vec![], None) + .await? + .value; + + let packed_tree_accounts = rpc_result + .pack_tree_infos(&mut remaining_accounts) + .state_trees + .unwrap(); + + let (remaining_accounts, _, _) = remaining_accounts.to_account_metas(); + + let my_compressed_account = MyCompressedAccount::deserialize( + &mut compressed_account.data.as_mut().unwrap().data.as_slice(), + ) + .unwrap(); + + let instruction = Instruction { + program_id: sdk_anchor_test::ID, + accounts: [ + vec![AccountMeta::new(payer.pubkey(), true)], + remaining_accounts, + ] + .concat(), + data: { + use anchor_lang::InstructionData; + sdk_anchor_test::instruction::ReadSha256Lowlevel { + proof: rpc_result.proof, + my_compressed_account, + account_meta: CompressedAccountMetaBurn { + tree_info: packed_tree_accounts.packed_tree_infos[0], + address: compressed_account.address.unwrap(), + }, + } + .data() + }, + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .await +} + +async fn create_compressed_account_poseidon( + name: String, + rpc: &mut LightProgramTest, + payer: &Keypair, + address: &[u8; 32], +) -> Result { + let config = SystemAccountMetaConfig::new(sdk_anchor_test::ID); + let mut remaining_accounts = PackedAccounts::default(); + remaining_accounts.add_system_accounts(config).unwrap(); + + let address_merkle_tree_info = rpc.get_address_tree_v1(); + + let rpc_result = rpc + .get_validity_proof( + vec![], + vec![AddressWithTree { + address: *address, + tree: address_merkle_tree_info.tree, + }], + None, + ) + .await? + .value; + let packed_accounts = rpc_result.pack_tree_infos(&mut remaining_accounts); + + let output_tree_index = rpc + .get_random_state_tree_info() + .unwrap() + .pack_output_tree_index(&mut remaining_accounts) + .unwrap(); + + let (remaining_accounts, _, _) = remaining_accounts.to_account_metas(); + + let instruction = Instruction { + program_id: sdk_anchor_test::ID, + accounts: [ + vec![AccountMeta::new(payer.pubkey(), true)], + remaining_accounts, + ] + .concat(), + data: { + use anchor_lang::InstructionData; + sdk_anchor_test::instruction::CreateCompressedAccountPoseidon { + proof: rpc_result.proof, + address_tree_info: packed_accounts.address_trees[0], + output_tree_index, + name, + } + .data() + }, + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .await +} + +async fn read_poseidon_light_system_cpi( + rpc: &mut LightProgramTest, + payer: &Keypair, + mut compressed_account: CompressedAccount, +) -> Result { + let mut remaining_accounts = PackedAccounts::default(); + + let config = SystemAccountMetaConfig::new(sdk_anchor_test::ID); + remaining_accounts.add_system_accounts(config).unwrap(); + let hash = compressed_account.hash; + + let rpc_result = rpc + .get_validity_proof(vec![hash], vec![], None) + .await? + .value; + + let packed_tree_accounts = rpc_result + .pack_tree_infos(&mut remaining_accounts) + .state_trees + .unwrap(); + + let (remaining_accounts, _, _) = remaining_accounts.to_account_metas(); + + let my_compressed_account = MyCompressedAccount::deserialize( + &mut compressed_account.data.as_mut().unwrap().data.as_slice(), + ) + .unwrap(); + + let instruction = Instruction { + program_id: sdk_anchor_test::ID, + accounts: [ + vec![AccountMeta::new(payer.pubkey(), true)], + remaining_accounts, + ] + .concat(), + data: { + use anchor_lang::InstructionData; + sdk_anchor_test::instruction::ReadPoseidonLightSystemCpi { + proof: rpc_result.proof, + my_compressed_account, + account_meta: CompressedAccountMetaBurn { + tree_info: packed_tree_accounts.packed_tree_infos[0], + address: compressed_account.address.unwrap(), + }, + } + .data() + }, + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .await +} + +async fn read_poseidon_lowlevel( + rpc: &mut LightProgramTest, + payer: &Keypair, + mut compressed_account: CompressedAccount, +) -> Result { + let mut remaining_accounts = PackedAccounts::default(); + + let config = SystemAccountMetaConfig::new(sdk_anchor_test::ID); + remaining_accounts.add_system_accounts(config).unwrap(); + let hash = compressed_account.hash; + + let rpc_result = rpc + .get_validity_proof(vec![hash], vec![], None) + .await? + .value; + + let packed_tree_accounts = rpc_result + .pack_tree_infos(&mut remaining_accounts) + .state_trees + .unwrap(); + + let (remaining_accounts, _, _) = remaining_accounts.to_account_metas(); + + let my_compressed_account = MyCompressedAccount::deserialize( + &mut compressed_account.data.as_mut().unwrap().data.as_slice(), + ) + .unwrap(); + + let instruction = Instruction { + program_id: sdk_anchor_test::ID, + accounts: [ + vec![AccountMeta::new(payer.pubkey(), true)], + remaining_accounts, + ] + .concat(), + data: { + use anchor_lang::InstructionData; + sdk_anchor_test::instruction::ReadPoseidonLowlevel { + proof: rpc_result.proof, + my_compressed_account, + account_meta: CompressedAccountMetaBurn { + tree_info: packed_tree_accounts.packed_tree_infos[0], + address: compressed_account.address.unwrap(), + }, + } + .data() + }, + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .await +} From b9d2471be30e7f4da5c80ba5d317cfc634745c59 Mon Sep 17 00:00:00 2001 From: ananas Date: Thu, 9 Oct 2025 02:57:10 +0100 Subject: [PATCH 3/3] fix: add test serial --- Cargo.lock | 1 + sdk-tests/sdk-anchor-test/programs/sdk-anchor-test/Cargo.toml | 1 + .../programs/sdk-anchor-test/tests/read_only.rs | 3 +++ .../sdk-anchor-test/programs/sdk-anchor-test/tests/test.rs | 2 ++ 4 files changed, 7 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index da3c1f9183..02dd0a583e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5554,6 +5554,7 @@ dependencies = [ "light-sdk", "light-sdk-types", "light-test-utils", + "serial_test", "solana-sdk", "tokio", ] diff --git a/sdk-tests/sdk-anchor-test/programs/sdk-anchor-test/Cargo.toml b/sdk-tests/sdk-anchor-test/programs/sdk-anchor-test/Cargo.toml index 2a9192b9fd..82448e4f90 100644 --- a/sdk-tests/sdk-anchor-test/programs/sdk-anchor-test/Cargo.toml +++ b/sdk-tests/sdk-anchor-test/programs/sdk-anchor-test/Cargo.toml @@ -26,6 +26,7 @@ light-hasher = { workspace = true, features = ["solana"] } anchor-lang = { workspace = true } light-sdk = { workspace = true, features = ["anchor", "v2"] } light-sdk-types = { workspace = true } +serial_test = { workspace = true } [target.'cfg(not(target_os = "solana"))'.dependencies] solana-sdk = { workspace = true } diff --git a/sdk-tests/sdk-anchor-test/programs/sdk-anchor-test/tests/read_only.rs b/sdk-tests/sdk-anchor-test/programs/sdk-anchor-test/tests/read_only.rs index 7ed9bccff7..154f4e2045 100644 --- a/sdk-tests/sdk-anchor-test/programs/sdk-anchor-test/tests/read_only.rs +++ b/sdk-tests/sdk-anchor-test/programs/sdk-anchor-test/tests/read_only.rs @@ -13,11 +13,13 @@ use light_sdk::{ }; use light_test_utils::{Rpc, RpcError}; use sdk_anchor_test::MyCompressedAccount; +use serial_test::serial; use solana_sdk::{ instruction::{AccountMeta, Instruction}, signature::{Keypair, Signature, Signer}, }; +#[serial] #[tokio::test] async fn test_read_sha256() { let config = @@ -57,6 +59,7 @@ async fn test_read_sha256() { .unwrap(); } +#[serial] #[tokio::test] async fn test_read_poseidon() { let config = diff --git a/sdk-tests/sdk-anchor-test/programs/sdk-anchor-test/tests/test.rs b/sdk-tests/sdk-anchor-test/programs/sdk-anchor-test/tests/test.rs index 4968606618..e19d0742de 100644 --- a/sdk-tests/sdk-anchor-test/programs/sdk-anchor-test/tests/test.rs +++ b/sdk-tests/sdk-anchor-test/programs/sdk-anchor-test/tests/test.rs @@ -13,11 +13,13 @@ use light_sdk::{ }; use light_test_utils::{Rpc, RpcError}; use sdk_anchor_test::{MyCompressedAccount, NestedData}; +use serial_test::serial; use solana_sdk::{ instruction::{AccountMeta, Instruction}, signature::{Keypair, Signature, Signer}, }; +#[serial] #[tokio::test] async fn test_anchor_sdk_test() { let config =