diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 5ba78d64b4..0756aa52f3 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -37,7 +37,7 @@ jobs: - name: Run linters run: | set -euxo pipefail - RUSTFMT_NIGHTLY_TOOLCHAIN=nightly-2025-10-26 + RUSTFMT_NIGHTLY_TOOLCHAIN=nightly rustup install "$RUSTFMT_NIGHTLY_TOOLCHAIN" rustup target add x86_64-unknown-linux-gnu --toolchain "$RUSTFMT_NIGHTLY_TOOLCHAIN" rustup component add --toolchain "$RUSTFMT_NIGHTLY_TOOLCHAIN" rustfmt diff --git a/forester/src/forester_status.rs b/forester/src/forester_status.rs index 80c4539075..d8f958134d 100644 --- a/forester/src/forester_status.rs +++ b/forester/src/forester_status.rs @@ -670,20 +670,22 @@ fn parse_tree_status( let fullness = next_index as f64 / capacity as f64 * 100.0; let (queue_len, queue_cap) = queue_account - .map(|acc| { - unsafe { parse_hash_set_from_bytes::(&acc.data) } - .ok() - .map(|hs| { + .map( + |acc| match unsafe { parse_hash_set_from_bytes::(&acc.data) } { + Ok(hs) => { let len = hs .iter() .filter(|(_, cell)| cell.sequence_number.is_none()) .count() as u64; let cap = hs.get_capacity() as u64; - (len, cap) - }) - .unwrap_or((0, 0)) - }) - .map(|(l, c)| (Some(l), Some(c))) + (Some(len), Some(cap)) + } + Err(error) => { + warn!(?error, "Failed to parse StateV1 queue hash set"); + (None, None) + } + }, + ) .unwrap_or((None, None)); ( @@ -725,20 +727,22 @@ fn parse_tree_status( let fullness = next_index as f64 / capacity as f64 * 100.0; let (queue_len, queue_cap) = queue_account - .map(|acc| { - unsafe { parse_hash_set_from_bytes::(&acc.data) } - .ok() - .map(|hs| { + .map( + |acc| match unsafe { parse_hash_set_from_bytes::(&acc.data) } { + Ok(hs) => { let len = hs .iter() .filter(|(_, cell)| cell.sequence_number.is_none()) .count() as u64; let cap = hs.get_capacity() as u64; - (len, cap) - }) - .unwrap_or((0, 0)) - }) - .map(|(l, c)| (Some(l), Some(c))) + (Some(len), Some(cap)) + } + Err(error) => { + warn!(?error, "Failed to parse AddressV1 queue hash set"); + (None, None) + } + }, + ) .unwrap_or((None, None)); ( diff --git a/forester/src/processor/v2/helpers.rs b/forester/src/processor/v2/helpers.rs index 66b574dc4e..a2a9ad7506 100644 --- a/forester/src/processor/v2/helpers.rs +++ b/forester/src/processor/v2/helpers.rs @@ -519,7 +519,47 @@ impl StreamingAddressQueue { let addresses = data.addresses[start..actual_end].to_vec(); if addresses.is_empty() { - return Err(anyhow!("Empty batch at start={}", start)); + return Ok(None); + } + let expected_len = addresses.len(); + let Some(low_element_values) = data + .low_element_values + .get(start..end) + .map(|slice| slice.to_vec()) + else { + return Ok(None); + }; + let Some(low_element_next_values) = data + .low_element_next_values + .get(start..end) + .map(|slice| slice.to_vec()) + else { + return Ok(None); + }; + let Some(low_element_indices) = data + .low_element_indices + .get(start..end) + .map(|slice| slice.to_vec()) + else { + return Ok(None); + }; + let Some(low_element_next_indices) = data + .low_element_next_indices + .get(start..end) + .map(|slice| slice.to_vec()) + else { + return Ok(None); + }; + if [ + low_element_values.len(), + low_element_next_values.len(), + low_element_indices.len(), + low_element_next_indices.len(), + ] + .iter() + .any(|&len| len != expected_len) + { + return Ok(None); } let leaves_hashchain = match data.leaves_hash_chains.get(hashchain_idx).copied() { diff --git a/forester/src/processor/v2/processor.rs b/forester/src/processor/v2/processor.rs index 3de6dea860..3484a8318d 100644 --- a/forester/src/processor/v2/processor.rs +++ b/forester/src/processor/v2/processor.rs @@ -118,6 +118,14 @@ where self.proof_cache = Some(cache); } + fn ensure_worker_pool(&mut self) -> crate::Result<()> { + if self.worker_pool.is_none() { + let job_tx = spawn_proof_workers(&self.context.prover_config)?; + self.worker_pool = Some(WorkerPool { job_tx }); + } + Ok(()) + } + pub async fn process(&mut self) -> std::result::Result { let queue_size = self.zkp_batch_size * self.context.max_batches_per_tree as u64; self.process_queue_update(queue_size).await @@ -131,10 +139,7 @@ where return Ok(ProcessingResult::default()); } - if self.worker_pool.is_none() { - let job_tx = spawn_proof_workers(&self.context.prover_config); - self.worker_pool = Some(WorkerPool { job_tx }); - } + self.ensure_worker_pool()?; if let Some(cached) = self.cached_state.take() { let actual_available = self @@ -531,10 +536,7 @@ where let max_batches = ((queue_size / self.zkp_batch_size) as usize).min(self.context.max_batches_per_tree); - if self.worker_pool.is_none() { - let job_tx = spawn_proof_workers(&self.context.prover_config); - self.worker_pool = Some(WorkerPool { job_tx }); - } + self.ensure_worker_pool()?; let queue_data = match self .strategy @@ -560,10 +562,7 @@ where let max_batches = max_batches.min(self.context.max_batches_per_tree); - if self.worker_pool.is_none() { - let job_tx = spawn_proof_workers(&self.context.prover_config); - self.worker_pool = Some(WorkerPool { job_tx }); - } + self.ensure_worker_pool()?; let queue_data = match self .strategy diff --git a/forester/src/processor/v2/proof_worker.rs b/forester/src/processor/v2/proof_worker.rs index b7afeacf0b..603fa3f19b 100644 --- a/forester/src/processor/v2/proof_worker.rs +++ b/forester/src/processor/v2/proof_worker.rs @@ -132,27 +132,27 @@ struct ProofClients { } impl ProofClients { - fn new(config: &ProverConfig) -> Self { - Self { + fn new(config: &ProverConfig) -> crate::Result { + Ok(Self { append_client: ProofClient::with_config( config.append_url.clone(), config.polling_interval, config.max_wait_time, config.api_key.clone(), - ), + )?, nullify_client: ProofClient::with_config( config.update_url.clone(), config.polling_interval, config.max_wait_time, config.api_key.clone(), - ), + )?, address_append_client: ProofClient::with_config( config.address_append_url.clone(), config.polling_interval, config.max_wait_time, config.api_key.clone(), - ), - } + )?, + }) } fn get_client(&self, input: &ProofInput) -> &ProofClient { @@ -164,11 +164,13 @@ impl ProofClients { } } -pub fn spawn_proof_workers(config: &ProverConfig) -> async_channel::Sender { +pub fn spawn_proof_workers( + config: &ProverConfig, +) -> crate::Result> { let (job_tx, job_rx) = async_channel::bounded::(256); - let clients = Arc::new(ProofClients::new(config)); + let clients = Arc::new(ProofClients::new(config)?); tokio::spawn(async move { run_proof_pipeline(job_rx, clients).await }); - job_tx + Ok(job_tx) } async fn run_proof_pipeline( diff --git a/js/token-interface/src/instructions/mint-to-compressed.ts b/js/token-interface/src/instructions/mint-to-compressed.ts index d31a91b900..410505ba7a 100644 --- a/js/token-interface/src/instructions/mint-to-compressed.ts +++ b/js/token-interface/src/instructions/mint-to-compressed.ts @@ -1,7 +1,4 @@ -import { - SystemProgram, - TransactionInstruction, -} from '@solana/web3.js'; +import { SystemProgram, TransactionInstruction } from '@solana/web3.js'; import { Buffer } from 'buffer'; import { LIGHT_TOKEN_PROGRAM_ID, @@ -125,9 +122,17 @@ export function createMintToCompressedInstruction({ isSigner: false, isWritable: false, }, - { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, + { + pubkey: SystemProgram.programId, + isSigner: false, + isWritable: false, + }, { pubkey: outputQueue, isSigner: false, isWritable: true }, - { pubkey: merkleContext.treeInfo.tree, isSigner: false, isWritable: true }, + { + pubkey: merkleContext.treeInfo.tree, + isSigner: false, + isWritable: true, + }, { pubkey: merkleContext.treeInfo.queue, isSigner: false, diff --git a/js/token-interface/tests/e2e/ata-read.test.ts b/js/token-interface/tests/e2e/ata-read.test.ts index 8159e81738..2c709f9f3e 100644 --- a/js/token-interface/tests/e2e/ata-read.test.ts +++ b/js/token-interface/tests/e2e/ata-read.test.ts @@ -1,6 +1,10 @@ import { describe, expect, it } from 'vitest'; import { newAccountWithLamports } from '@lightprotocol/stateless.js'; -import { createAtaInstructions, getAta, getAssociatedTokenAddress } from '../../src'; +import { + createAtaInstructions, + getAta, + getAssociatedTokenAddress, +} from '../../src'; import { createMintFixture, sendInstructions } from './helpers'; describe('ata creation and reads', () => { diff --git a/js/token-interface/tests/e2e/mint-to-compressed.test.ts b/js/token-interface/tests/e2e/mint-to-compressed.test.ts index 0bf4e08abe..59e040c79e 100644 --- a/js/token-interface/tests/e2e/mint-to-compressed.test.ts +++ b/js/token-interface/tests/e2e/mint-to-compressed.test.ts @@ -52,7 +52,12 @@ describe('mint-to-compressed instruction', () => { [COMPRESSED_MINT_SEED, mintSigner.publicKey.toBuffer()], LIGHT_TOKEN_PROGRAM_ID, ); - const mintInfo = await getMint(rpc, mint, undefined, LIGHT_TOKEN_PROGRAM_ID); + const mintInfo = await getMint( + rpc, + mint, + undefined, + LIGHT_TOKEN_PROGRAM_ID, + ); if (!mintInfo.merkleContext || !mintInfo.mintContext) { throw new Error('Light mint context missing.'); } @@ -103,7 +108,12 @@ describe('mint-to-compressed instruction', () => { recipientB.publicKey, { mint }, ); - const mintAfter = await getMint(rpc, mint, undefined, LIGHT_TOKEN_PROGRAM_ID); + const mintAfter = await getMint( + rpc, + mint, + undefined, + LIGHT_TOKEN_PROGRAM_ID, + ); const amountA = aAccounts.items.reduce( (sum, account) => sum + BigInt(account.parsed.amount.toString()), diff --git a/js/token-interface/tests/unit/instruction-builders.test.ts b/js/token-interface/tests/unit/instruction-builders.test.ts index b70bf20790..ba36c7e433 100644 --- a/js/token-interface/tests/unit/instruction-builders.test.ts +++ b/js/token-interface/tests/unit/instruction-builders.test.ts @@ -670,7 +670,9 @@ describe('instruction builders', () => { mintSigner: Keypair.generate().publicKey.toBytes(), bump: 255, }, - recipients: [{ recipient: Keypair.generate().publicKey, amount: 42n }], + recipients: [ + { recipient: Keypair.generate().publicKey, amount: 42n }, + ], }); expect(instruction.programId.equals(LIGHT_TOKEN_PROGRAM_ID)).toBe(true); diff --git a/program-tests/utils/src/actions/legacy/instructions/transfer2.rs b/program-tests/utils/src/actions/legacy/instructions/transfer2.rs index 1ff92eeda9..b006824302 100644 --- a/program-tests/utils/src/actions/legacy/instructions/transfer2.rs +++ b/program-tests/utils/src/actions/legacy/instructions/transfer2.rs @@ -169,13 +169,23 @@ pub async fn create_generic_transfer2_instruction( payer: Pubkey, should_filter_zero_outputs: bool, ) -> Result { - // // Get a single shared output queue for ALL compress/compress-and-close operations - // // This prevents reordering issues caused by the sort_by_key at the end - // let shared_output_queue = rpc - // .get_random_state_tree_info() - // .unwrap() - // .get_output_pubkey() - // .unwrap(); + // Transfer2 supports a single output queue per instruction. Legacy helpers accept + // per-action queues, but normalize them down to one shared queue for the IX. + let mut explicit_output_queue = None; + for action in &actions { + let candidate = match action { + Transfer2InstructionType::Compress(input) => Some(input.output_queue), + Transfer2InstructionType::CompressAndClose(input) => Some(input.output_queue), + Transfer2InstructionType::Decompress(_) + | Transfer2InstructionType::Transfer(_) + | Transfer2InstructionType::Approve(_) => None, + }; + if let Some(candidate) = candidate { + if explicit_output_queue.is_none() { + explicit_output_queue = Some(candidate); + } + } + } let mut hashes = Vec::new(); actions.iter().for_each(|account| match account { @@ -210,24 +220,16 @@ pub async fn create_generic_transfer2_instruction( .value; let mut packed_tree_accounts = PackedAccounts::default(); - // tree infos must be packed before packing the token input accounts - let packed_tree_infos = rpc_proof_result.pack_tree_infos(&mut packed_tree_accounts); + // Pack only input state tree infos. Grouped transfer2 proofs can span multiple output trees. + let packed_tree_infos = rpc_proof_result.pack_state_tree_infos(&mut packed_tree_accounts); - // We use a single shared output queue for all compress/compress-and-close operations to avoid ordering failures. - let shared_output_queue = if packed_tree_infos.address_trees.is_empty() { - let shared_output_queue = rpc - .get_random_state_tree_info() + let shared_output_queue = explicit_output_queue.unwrap_or_else(|| { + rpc.get_random_state_tree_info() .unwrap() .get_output_pubkey() - .unwrap(); - packed_tree_accounts.insert_or_get(shared_output_queue) - } else { - packed_tree_infos - .state_trees - .as_ref() .unwrap() - .output_tree_index - }; + }); + let shared_output_queue = packed_tree_accounts.insert_or_get(shared_output_queue); let mut inputs_offset = 0; let mut in_lamports = Vec::new(); @@ -242,14 +244,7 @@ pub async fn create_generic_transfer2_instruction( if let Some(ref input_token_account) = input.compressed_token_account { let token_data = input_token_account .iter() - .zip( - packed_tree_infos - .state_trees - .as_ref() - .unwrap() - .packed_tree_infos[inputs_offset..] - .iter(), - ) + .zip(packed_tree_infos[inputs_offset..].iter()) .map(|(account, rpc_account)| { if input.to != account.token.owner { return Err(TokenSdkError::InvalidCompressInputOwner); @@ -391,14 +386,7 @@ pub async fn create_generic_transfer2_instruction( let token_data = input .compressed_token_account .iter() - .zip( - packed_tree_infos - .state_trees - .as_ref() - .unwrap() - .packed_tree_infos[inputs_offset..] - .iter(), - ) + .zip(packed_tree_infos[inputs_offset..].iter()) .map(|(account, rpc_account)| { pack_input_token_account( account, @@ -460,14 +448,7 @@ pub async fn create_generic_transfer2_instruction( let token_data = input .compressed_token_account .iter() - .zip( - packed_tree_infos - .state_trees - .as_ref() - .unwrap() - .packed_tree_infos[inputs_offset..] - .iter(), - ) + .zip(packed_tree_infos[inputs_offset..].iter()) .map(|(account, rpc_account)| { pack_input_token_account( account, @@ -542,14 +523,7 @@ pub async fn create_generic_transfer2_instruction( let token_data = input .compressed_token_account .iter() - .zip( - packed_tree_infos - .state_trees - .as_ref() - .unwrap() - .packed_tree_infos[inputs_offset..] - .iter(), - ) + .zip(packed_tree_infos[inputs_offset..].iter()) .map(|(account, rpc_account)| { pack_input_token_account( account, diff --git a/program-tests/utils/src/e2e_test_env.rs b/program-tests/utils/src/e2e_test_env.rs index edb94fdf48..482eb5eaaa 100644 --- a/program-tests/utils/src/e2e_test_env.rs +++ b/program-tests/utils/src/e2e_test_env.rs @@ -835,7 +835,7 @@ where .map_err(|error| RpcError::CustomError(error.to_string())) .unwrap(); let (proof_a, proof_b, proof_c) = - proof_from_json_struct(proof_json); + proof_from_json_struct(proof_json).unwrap(); let (proof_a, proof_b, proof_c) = compress_proof(&proof_a, &proof_b, &proof_c); let instruction_data = InstructionDataBatchNullifyInputs { diff --git a/prover/client/src/errors.rs b/prover/client/src/errors.rs index 859cae32b8..e095bf3579 100644 --- a/prover/client/src/errors.rs +++ b/prover/client/src/errors.rs @@ -39,6 +39,7 @@ pub enum ProverClientError { #[error("Integer conversion failed: {0}")] IntegerConversion(String), + #[error("Hashchain mismatch: computed {computed:?} != expected {expected:?} (batch_size={batch_size}, next_index={next_index})")] HashchainMismatch { computed: [u8; 32], diff --git a/prover/client/src/proof.rs b/prover/client/src/proof.rs index cc66f3aed6..da9b913830 100644 --- a/prover/client/src/proof.rs +++ b/prover/client/src/proof.rs @@ -12,6 +12,9 @@ use solana_bn254::compression::prelude::{ alt_bn128_g2_decompress_be, convert_endianness, }; +pub type CompressedProofBytes = ([u8; 32], [u8; 64], [u8; 32]); +pub type UncompressedProofBytes = ([u8; 64], [u8; 128], [u8; 64]); + #[derive(Debug, Clone, Copy)] pub struct ProofCompressed { pub a: [u8; 32], @@ -66,16 +69,27 @@ pub fn deserialize_gnark_proof_json(json_data: &str) -> serde_json::Result [u8; 32] { - let trimmed_str = hex_str.trim_start_matches("0x"); - let big_int = num_bigint::BigInt::from_str_radix(trimmed_str, 16).unwrap(); - let big_int_bytes = big_int.to_bytes_be().1; - if big_int_bytes.len() < 32 { +pub fn deserialize_hex_string_to_be_bytes(hex_str: &str) -> Result<[u8; 32], ProverClientError> { + let trimmed_str = hex_str + .strip_prefix("0x") + .or_else(|| hex_str.strip_prefix("0X")) + .unwrap_or(hex_str); + let big_uint = num_bigint::BigUint::from_str_radix(trimmed_str, 16) + .map_err(|error| ProverClientError::InvalidHexString(format!("{hex_str}: {error}")))?; + let big_uint_bytes = big_uint.to_bytes_be(); + if big_uint_bytes.len() > 32 { + return Err(ProverClientError::InvalidHexString(format!( + "{hex_str}: exceeds 32 bytes" + ))); + } + if big_uint_bytes.len() < 32 { let mut result = [0u8; 32]; - result[32 - big_int_bytes.len()..].copy_from_slice(&big_int_bytes); - result + result[32 - big_uint_bytes.len()..].copy_from_slice(&big_uint_bytes); + Ok(result) } else { - big_int_bytes.try_into().unwrap() + big_uint_bytes.try_into().map_err(|_| { + ProverClientError::InvalidHexString(format!("{hex_str}: invalid 32-byte encoding")) + }) } } @@ -90,40 +104,72 @@ pub fn compress_proof( (proof_a, proof_b, proof_c) } -pub fn proof_from_json_struct(json: GnarkProofJson) -> ([u8; 64], [u8; 128], [u8; 64]) { - let proof_a_x = deserialize_hex_string_to_be_bytes(&json.ar[0]); - let proof_a_y = deserialize_hex_string_to_be_bytes(&json.ar[1]); - let proof_a: [u8; 64] = [proof_a_x, proof_a_y].concat().try_into().unwrap(); - let proof_a = negate_g1(&proof_a); - let proof_b_x_0 = deserialize_hex_string_to_be_bytes(&json.bs[0][0]); - let proof_b_x_1 = deserialize_hex_string_to_be_bytes(&json.bs[0][1]); - let proof_b_y_0 = deserialize_hex_string_to_be_bytes(&json.bs[1][0]); - let proof_b_y_1 = deserialize_hex_string_to_be_bytes(&json.bs[1][1]); +pub fn proof_from_json_struct( + json: GnarkProofJson, +) -> Result { + let proof_a_x = deserialize_hex_string_to_be_bytes(json.ar.first().ok_or_else(|| { + ProverClientError::InvalidProofData("missing proof A x coordinate".to_string()) + })?)?; + let proof_a_y = deserialize_hex_string_to_be_bytes(json.ar.get(1).ok_or_else(|| { + ProverClientError::InvalidProofData("missing proof A y coordinate".to_string()) + })?)?; + let proof_a: [u8; 64] = [proof_a_x, proof_a_y] + .concat() + .try_into() + .map_err(|_| ProverClientError::InvalidProofData("invalid proof A length".to_string()))?; + let proof_a = negate_g1(&proof_a)?; + let proof_b_x_0 = deserialize_hex_string_to_be_bytes( + json.bs.first().and_then(|row| row.first()).ok_or_else(|| { + ProverClientError::InvalidProofData("missing proof B x0 coordinate".to_string()) + })?, + )?; + let proof_b_x_1 = deserialize_hex_string_to_be_bytes( + json.bs.first().and_then(|row| row.get(1)).ok_or_else(|| { + ProverClientError::InvalidProofData("missing proof B x1 coordinate".to_string()) + })?, + )?; + let proof_b_y_0 = deserialize_hex_string_to_be_bytes( + json.bs.get(1).and_then(|row| row.first()).ok_or_else(|| { + ProverClientError::InvalidProofData("missing proof B y0 coordinate".to_string()) + })?, + )?; + let proof_b_y_1 = + deserialize_hex_string_to_be_bytes(json.bs.get(1).and_then(|row| row.get(1)).ok_or_else( + || ProverClientError::InvalidProofData("missing proof B y1 coordinate".to_string()), + )?)?; let proof_b: [u8; 128] = [proof_b_x_0, proof_b_x_1, proof_b_y_0, proof_b_y_1] .concat() .try_into() - .unwrap(); + .map_err(|_| ProverClientError::InvalidProofData("invalid proof B length".to_string()))?; - let proof_c_x = deserialize_hex_string_to_be_bytes(&json.krs[0]); - let proof_c_y = deserialize_hex_string_to_be_bytes(&json.krs[1]); - let proof_c: [u8; 64] = [proof_c_x, proof_c_y].concat().try_into().unwrap(); - (proof_a, proof_b, proof_c) + let proof_c_x = deserialize_hex_string_to_be_bytes(json.krs.first().ok_or_else(|| { + ProverClientError::InvalidProofData("missing proof C x coordinate".to_string()) + })?)?; + let proof_c_y = deserialize_hex_string_to_be_bytes(json.krs.get(1).ok_or_else(|| { + ProverClientError::InvalidProofData("missing proof C y coordinate".to_string()) + })?)?; + let proof_c: [u8; 64] = [proof_c_x, proof_c_y] + .concat() + .try_into() + .map_err(|_| ProverClientError::InvalidProofData("invalid proof C length".to_string()))?; + Ok((proof_a, proof_b, proof_c)) } -pub fn negate_g1(g1_be: &[u8; 64]) -> [u8; 64] { +pub fn negate_g1(g1_be: &[u8; 64]) -> Result<[u8; 64], ProverClientError> { let g1_le = convert_endianness::<32, 64>(g1_be); - let g1: G1 = G1::deserialize_with_mode(g1_le.as_slice(), Compress::No, Validate::No).unwrap(); + let g1: G1 = G1::deserialize_with_mode(g1_le.as_slice(), Compress::No, Validate::Yes) + .map_err(|error| ProverClientError::InvalidProofData(error.to_string()))?; let g1_neg = g1.neg(); let mut g1_neg_be = [0u8; 64]; g1_neg .x .serialize_with_mode(&mut g1_neg_be[..32], Compress::No) - .unwrap(); + .map_err(|error| ProverClientError::InvalidProofData(error.to_string()))?; g1_neg .y .serialize_with_mode(&mut g1_neg_be[32..], Compress::No) - .unwrap(); + .map_err(|error| ProverClientError::InvalidProofData(error.to_string()))?; let g1_neg_be: [u8; 64] = convert_endianness::<32, 64>(&g1_neg_be); - g1_neg_be + Ok(g1_neg_be) } diff --git a/prover/client/src/proof_client.rs b/prover/client/src/proof_client.rs index 859ea4917f..a4f0e27aba 100644 --- a/prover/client/src/proof_client.rs +++ b/prover/client/src/proof_client.rs @@ -84,21 +84,21 @@ impl ProofClient { polling_interval: Duration, max_wait_time: Duration, api_key: Option, - ) -> Self { + ) -> Result { let initial_poll_delay = if api_key.is_some() { Duration::from_millis(INITIAL_POLL_DELAY_LARGE_CIRCUIT_MS) } else { Duration::from_millis(INITIAL_POLL_DELAY_SMALL_CIRCUIT_MS) }; - Self { + Ok(Self { client: build_http_client(), server_address, polling_interval, max_wait_time, api_key, initial_poll_delay, - } + }) } #[allow(unused)] @@ -654,7 +654,7 @@ impl ProofClient { ProverClientError::ProverServerError(format!("Failed to deserialize proof JSON: {}", e)) })?; - let (proof_a, proof_b, proof_c) = proof_from_json_struct(proof_json); + let (proof_a, proof_b, proof_c) = proof_from_json_struct(proof_json)?; let (proof_a, proof_b, proof_c) = compress_proof(&proof_a, &proof_b, &proof_c); Ok(ProofResult { diff --git a/prover/client/src/proof_types/batch_address_append/proof_inputs.rs b/prover/client/src/proof_types/batch_address_append/proof_inputs.rs index fdc50621cc..477ee73140 100644 --- a/prover/client/src/proof_types/batch_address_append/proof_inputs.rs +++ b/prover/client/src/proof_types/batch_address_append/proof_inputs.rs @@ -256,9 +256,14 @@ pub fn get_batch_address_append_circuit_inputs( next_index ); - let mut patcher = ChangelogProofPatcher::new::(changelog); + let mut staged_changelog = changelog.clone(); + let mut staged_indexed_changelog = indexed_changelog.clone(); + let mut staged_sparse_merkle_tree = sparse_merkle_tree.clone(); + let initial_changelog_len = staged_changelog.len(); - let is_first_batch = indexed_changelog.is_empty(); + let mut patcher = ChangelogProofPatcher::new::(&staged_changelog); + + let is_first_batch = staged_indexed_changelog.is_empty(); let mut expected_root_for_low = current_root; for i in 0..zkp_batch_size { @@ -294,7 +299,7 @@ pub fn get_batch_address_append_circuit_inputs( patch_indexed_changelogs( 0, &mut changelog_index, - indexed_changelog, + &mut staged_indexed_changelog, &mut low_element, &mut new_element, &mut low_element_next_value, @@ -386,7 +391,7 @@ pub fn get_batch_address_append_circuit_inputs( new_low_element.index, )?; - patcher.push_changelog_entry::(changelog, changelog_entry); + patcher.push_changelog_entry::(&mut staged_changelog, changelog_entry); low_element_circuit_merkle_proofs.push( merkle_proof .iter() @@ -399,10 +404,10 @@ pub fn get_batch_address_append_circuit_inputs( let low_element_changelog_entry = IndexedChangelogEntry { element: new_low_element_raw, proof: low_element_changelog_proof, - changelog_index: indexed_changelog.len(), //change_log_index, + changelog_index: staged_indexed_changelog.len(), }; - indexed_changelog.push(low_element_changelog_entry); + staged_indexed_changelog.push(low_element_changelog_entry); { let new_element_next_value = low_element_next_value; @@ -412,10 +417,10 @@ pub fn get_batch_address_append_circuit_inputs( ProverClientError::GenericError(format!("Failed to hash new element: {}", e)) })?; - let sparse_root_before = sparse_merkle_tree.root(); - let sparse_next_idx_before = sparse_merkle_tree.get_next_index(); + let sparse_root_before = staged_sparse_merkle_tree.root(); + let sparse_next_idx_before = staged_sparse_merkle_tree.get_next_index(); - let mut merkle_proof_array = sparse_merkle_tree.append(new_element_leaf_hash); + let mut merkle_proof_array = staged_sparse_merkle_tree.append(new_element_leaf_hash); let current_index = next_index + i; @@ -427,7 +432,7 @@ pub fn get_batch_address_append_circuit_inputs( current_index, )?; - if i == 0 && changelog.len() == 1 { + if i == 0 && staged_changelog.len() == initial_changelog_len + 1 { if sparse_next_idx_before != current_index { return Err(ProverClientError::GenericError(format!( "sparse index mismatch: sparse tree next_index={} but expected current_index={}", @@ -486,7 +491,7 @@ pub fn get_batch_address_append_circuit_inputs( new_root = updated_root; - patcher.push_changelog_entry::(changelog, changelog_entry); + patcher.push_changelog_entry::(&mut staged_changelog, changelog_entry); new_element_circuit_merkle_proofs.push( merkle_proof_array .iter() @@ -504,9 +509,9 @@ pub fn get_batch_address_append_circuit_inputs( let new_element_changelog_entry = IndexedChangelogEntry { element: new_element_raw, proof: merkle_proof_array, - changelog_index: indexed_changelog.len(), + changelog_index: staged_indexed_changelog.len(), }; - indexed_changelog.push(new_element_changelog_entry); + staged_indexed_changelog.push(new_element_changelog_entry); } } @@ -542,18 +547,18 @@ pub fn get_batch_address_append_circuit_inputs( patcher.hits, patcher.misses, patcher.overwrites, - changelog.len(), - indexed_changelog.len() + staged_changelog.len(), + staged_indexed_changelog.len() ); - if patcher.hits == 0 && !changelog.is_empty() { + if patcher.hits == 0 && !staged_changelog.is_empty() { tracing::warn!( "Address proof patcher had 0 cache hits despite non-empty changelog (changelog_len={}, indexed_changelog_len={})", - changelog.len(), - indexed_changelog.len() + staged_changelog.len(), + staged_indexed_changelog.len() ); } - Ok(BatchAddressAppendInputs { + let inputs = BatchAddressAppendInputs { batch_size: patched_low_element_values.len(), hashchain_hash: BigUint::from_bytes_be(&leaves_hashchain), low_element_values: patched_low_element_values @@ -573,7 +578,7 @@ pub fn get_batch_address_append_circuit_inputs( .map(|v| BigUint::from_bytes_be(v)) .collect(), low_element_proofs: low_element_circuit_merkle_proofs, - new_element_values: new_element_values[0..] + new_element_values: new_element_values .iter() .map(|v| BigUint::from_bytes_be(v)) .collect(), @@ -583,5 +588,11 @@ pub fn get_batch_address_append_circuit_inputs( public_input_hash: BigUint::from_bytes_be(&public_input_hash), start_index: next_index, tree_height: HEIGHT, - }) + }; + + *changelog = staged_changelog; + *indexed_changelog = staged_indexed_changelog; + *sparse_merkle_tree = staged_sparse_merkle_tree; + + Ok(inputs) } diff --git a/prover/client/src/proof_types/batch_append/proof_inputs.rs b/prover/client/src/proof_types/batch_append/proof_inputs.rs index 41a6dcfcd6..186086a6a5 100644 --- a/prover/client/src/proof_types/batch_append/proof_inputs.rs +++ b/prover/client/src/proof_types/batch_append/proof_inputs.rs @@ -128,9 +128,20 @@ pub fn get_batch_append_inputs( batch_size: u32, previous_changelogs: &[ChangelogEntry], ) -> Result<(BatchAppendsCircuitInputs, Vec>), ProverClientError> { + let batch_size_usize = batch_size as usize; + if old_leaves.len() != batch_size_usize + || leaves.len() != batch_size_usize + || merkle_proofs.len() != batch_size_usize + { + return Err(ProverClientError::InvalidProofData(format!( + "batch append input length mismatch: old_leaves={}, leaves={}, merkle_proofs={}, expected batch_size={}", + old_leaves.len(), leaves.len(), merkle_proofs.len(), batch_size + ))); + } + let mut new_root = [0u8; 32]; let mut changelog: Vec> = Vec::new(); - let mut circuit_merkle_proofs = Vec::with_capacity(batch_size as usize); + let mut circuit_merkle_proofs = Vec::with_capacity(batch_size_usize); for (i, (old_leaf, (new_leaf, mut merkle_proof))) in old_leaves .iter() diff --git a/prover/client/src/proof_types/batch_update/proof_inputs.rs b/prover/client/src/proof_types/batch_update/proof_inputs.rs index 2ada02b92b..f5467184aa 100644 --- a/prover/client/src/proof_types/batch_update/proof_inputs.rs +++ b/prover/client/src/proof_types/batch_update/proof_inputs.rs @@ -31,8 +31,12 @@ pub struct BatchUpdateCircuitInputs { } impl BatchUpdateCircuitInputs { - pub fn public_inputs_arr(&self) -> [u8; 32] { - bigint_to_u8_32(&self.public_input_hash).unwrap() + pub fn public_inputs_arr(&self) -> Result<[u8; 32], ProverClientError> { + bigint_to_u8_32(&self.public_input_hash).map_err(|error| { + ProverClientError::GenericError(format!( + "failed to serialize batch update public input: {error}" + )) + }) } pub fn new( @@ -112,9 +116,17 @@ impl BatchUpdateCircuitInputs { pub struct BatchUpdateInputs<'a>(pub &'a [BatchUpdateCircuitInputs]); impl BatchUpdateInputs<'_> { - pub fn public_inputs(&self) -> Vec<[u8; 32]> { - // Concatenate all public inputs into a single flat vector - vec![self.0[0].public_inputs_arr()] + pub fn public_inputs(&self) -> Result, ProverClientError> { + if self.0.is_empty() { + return Err(ProverClientError::GenericError( + "batch update inputs cannot be empty".to_string(), + )); + } + + self.0 + .iter() + .map(BatchUpdateCircuitInputs::public_inputs_arr) + .collect() } } diff --git a/scripts/lint.sh b/scripts/lint.sh index 8c988572ef..b440380898 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -3,7 +3,7 @@ set -e # Keep formatter deterministic across local + CI. -RUSTFMT_NIGHTLY_TOOLCHAIN="${RUSTFMT_NIGHTLY_TOOLCHAIN:-nightly-2025-10-26}" +RUSTFMT_NIGHTLY_TOOLCHAIN="${RUSTFMT_NIGHTLY_TOOLCHAIN:-nightly}" # JS linting (use subshells to avoid directory issues) (cd js/stateless.js && pnpm prettier --write . && pnpm lint) diff --git a/sdk-libs/client/src/indexer/types/proof.rs b/sdk-libs/client/src/indexer/types/proof.rs index 0b45e00986..75291e8cff 100644 --- a/sdk-libs/client/src/indexer/types/proof.rs +++ b/sdk-libs/client/src/indexer/types/proof.rs @@ -189,41 +189,57 @@ pub struct PackedTreeInfos { } impl ValidityProofWithContext { - pub fn pack_tree_infos(&self, packed_accounts: &mut PackedAccounts) -> PackedTreeInfos { - let mut packed_tree_infos = Vec::new(); - let mut address_trees = Vec::new(); - let mut output_tree_index = None; - for account in self.accounts.iter() { - // Pack TreeInfo - let merkle_tree_pubkey_index = packed_accounts.insert_or_get(account.tree_info.tree); - let queue_pubkey_index = packed_accounts.insert_or_get(account.tree_info.queue); - let tree_info_packed = PackedStateTreeInfo { - root_index: account.root_index.root_index, - merkle_tree_pubkey_index, - queue_pubkey_index, + pub fn pack_state_tree_infos( + &self, + packed_accounts: &mut PackedAccounts, + ) -> Vec { + self.accounts + .iter() + .map(|account| PackedStateTreeInfo { + root_index: account.root_index.root_index().unwrap_or_default(), + merkle_tree_pubkey_index: packed_accounts.insert_or_get(account.tree_info.tree), + queue_pubkey_index: packed_accounts.insert_or_get(account.tree_info.queue), leaf_index: account.leaf_index as u32, prove_by_index: account.root_index.proof_by_index(), - }; - packed_tree_infos.push(tree_info_packed); + }) + .collect() + } + pub fn pack_tree_infos( + &self, + packed_accounts: &mut PackedAccounts, + ) -> Result { + let packed_tree_infos = self.pack_state_tree_infos(packed_accounts); + let mut address_trees = Vec::new(); + let mut output_tree_index = None; + for account in self.accounts.iter() { // If a next Merkle tree exists the Merkle tree is full -> use the next Merkle tree for new state. // Else use the current Merkle tree for new state. if let Some(next) = account.tree_info.next_tree_info { // SAFETY: account will always have a state Merkle tree context. // pack_output_tree_index only panics on an address Merkle tree context. - let index = next.pack_output_tree_index(packed_accounts).unwrap(); - if output_tree_index.is_none() { - output_tree_index = Some(index); + let index = next.pack_output_tree_index(packed_accounts)?; + match output_tree_index { + Some(existing) if existing != index => { + return Err(IndexerError::InvalidParameters(format!( + "mixed output tree indices in state proof: {existing} != {index}" + ))); + } + Some(_) => {} + None => output_tree_index = Some(index), } } else { // SAFETY: account will always have a state Merkle tree context. // pack_output_tree_index only panics on an address Merkle tree context. - let index = account - .tree_info - .pack_output_tree_index(packed_accounts) - .unwrap(); - if output_tree_index.is_none() { - output_tree_index = Some(index); + let index = account.tree_info.pack_output_tree_index(packed_accounts)?; + match output_tree_index { + Some(existing) if existing != index => { + return Err(IndexerError::InvalidParameters(format!( + "mixed output tree indices in state proof: {existing} != {index}" + ))); + } + Some(_) => {} + None => output_tree_index = Some(index), } } } @@ -244,13 +260,17 @@ impl ValidityProofWithContext { } else { Some(PackedStateTreeInfos { packed_tree_infos, - output_tree_index: output_tree_index.unwrap(), + output_tree_index: output_tree_index.ok_or_else(|| { + IndexerError::InvalidParameters( + "missing output tree index for non-empty state proof".to_string(), + ) + })?, }) }; - PackedTreeInfos { + Ok(PackedTreeInfos { state_trees: packed_tree_infos, address_trees, - } + }) } pub fn from_api_model( @@ -365,3 +385,133 @@ impl ValidityProofWithContext { }) } } + +#[cfg(test)] +mod tests { + use light_compressed_account::TreeType; + + use super::*; + use crate::indexer::NextTreeInfo; + + /// Helper to build an `AccountProofInputs` with a given tree and queue pubkey. + fn account_with_tree(tree: Pubkey, queue: Pubkey) -> AccountProofInputs { + AccountProofInputs { + hash: [0u8; 32], + root: [0u8; 32], + root_index: RootIndex::new_some(0), + leaf_index: 0, + tree_info: TreeInfo { + tree, + queue, + cpi_context: None, + next_tree_info: None, + tree_type: TreeType::StateV1, + }, + } + } + + #[test] + fn test_pack_tree_infos_mixed_output_tree_indices_error() { + let tree_a = Pubkey::new_unique(); + let tree_b = Pubkey::new_unique(); + let queue_a = Pubkey::new_unique(); + let queue_b = Pubkey::new_unique(); + + let proof_ctx = ValidityProofWithContext { + proof: ValidityProof::default(), + accounts: vec![ + account_with_tree(tree_a, queue_a), + account_with_tree(tree_b, queue_b), + ], + addresses: vec![], + }; + + let mut packed = PackedAccounts::default(); + let result = proof_ctx.pack_tree_infos(&mut packed); + + assert!(result.is_err(), "expected error for mixed output trees"); + let err = result.unwrap_err(); + match &err { + IndexerError::InvalidParameters(msg) => { + assert!( + msg.contains("mixed output tree indices"), + "unexpected error message: {msg}" + ); + } + other => panic!("expected InvalidParameters, got: {other:?}"), + } + } + + #[test] + fn test_pack_tree_infos_mixed_next_tree_indices_error() { + // Both accounts use the same input tree but have different *next* trees. + let input_tree = Pubkey::new_unique(); + let input_queue = Pubkey::new_unique(); + let next_tree_a = Pubkey::new_unique(); + let next_queue_a = Pubkey::new_unique(); + let next_tree_b = Pubkey::new_unique(); + let next_queue_b = Pubkey::new_unique(); + + let mut acc1 = account_with_tree(input_tree, input_queue); + acc1.tree_info.next_tree_info = Some(NextTreeInfo { + tree: next_tree_a, + queue: next_queue_a, + cpi_context: None, + tree_type: TreeType::StateV1, + }); + + let mut acc2 = account_with_tree(input_tree, input_queue); + acc2.tree_info.next_tree_info = Some(NextTreeInfo { + tree: next_tree_b, + queue: next_queue_b, + cpi_context: None, + tree_type: TreeType::StateV1, + }); + + let proof_ctx = ValidityProofWithContext { + proof: ValidityProof::default(), + accounts: vec![acc1, acc2], + addresses: vec![], + }; + + let mut packed = PackedAccounts::default(); + let result = proof_ctx.pack_tree_infos(&mut packed); + + assert!( + result.is_err(), + "expected error for mixed next-tree output indices" + ); + let err = result.unwrap_err(); + match &err { + IndexerError::InvalidParameters(msg) => { + assert!( + msg.contains("mixed output tree indices"), + "unexpected error message: {msg}" + ); + } + other => panic!("expected InvalidParameters, got: {other:?}"), + } + } + + #[test] + fn test_pack_tree_infos_same_output_tree_ok() { + let tree = Pubkey::new_unique(); + let queue = Pubkey::new_unique(); + + let proof_ctx = ValidityProofWithContext { + proof: ValidityProof::default(), + accounts: vec![ + account_with_tree(tree, queue), + account_with_tree(tree, queue), + ], + addresses: vec![], + }; + + let mut packed = PackedAccounts::default(); + let result = proof_ctx.pack_tree_infos(&mut packed); + assert!( + result.is_ok(), + "same output trees should succeed: {result:?}" + ); + } +} diff --git a/sdk-libs/client/src/indexer/types/queue.rs b/sdk-libs/client/src/indexer/types/queue.rs index f64e200477..940ba4fa9e 100644 --- a/sdk-libs/client/src/indexer/types/queue.rs +++ b/sdk-libs/client/src/indexer/types/queue.rs @@ -88,7 +88,7 @@ impl AddressQueueData { address_range: std::ops::Range, ) -> Result, IndexerError> { self.validate_proof_height::()?; - let available = self.proof_count(); + let available = self.proof_count()?; if address_range.start > address_range.end { return Err(IndexerError::InvalidParameters(format!( "invalid address proof range {}..{}", @@ -115,6 +115,7 @@ impl AddressQueueData { pub fn reconstruct_all_proofs( &self, ) -> Result, IndexerError> { + self.validate_proof_height::()?; self.reconstruct_proofs::(0..self.addresses.len()) } @@ -126,8 +127,15 @@ impl AddressQueueData { lookup } - fn proof_count(&self) -> usize { - self.addresses.len().min(self.low_element_indices.len()) + fn proof_count(&self) -> Result { + let addr_len = self.addresses.len(); + let idx_len = self.low_element_indices.len(); + if addr_len != idx_len { + return Err(IndexerError::InvalidParameters(format!( + "address queue length mismatch: addresses.len()={addr_len} != low_element_indices.len()={idx_len}" + ))); + } + Ok(addr_len) } fn reconstruct_proof_with_lookup( @@ -135,6 +143,7 @@ impl AddressQueueData { address_idx: usize, node_lookup: &HashMap, ) -> Result<[[u8; 32]; HEIGHT], IndexerError> { + self.validate_proof_height::()?; let leaf_index = *self.low_element_indices.get(address_idx).ok_or_else(|| { IndexerError::MissingResult { context: "reconstruct_proof".to_string(), diff --git a/sdk-libs/client/src/interface/initialize_config.rs b/sdk-libs/client/src/interface/initialize_config.rs index 7b5919cdb1..9fbeacfe89 100644 --- a/sdk-libs/client/src/interface/initialize_config.rs +++ b/sdk-libs/client/src/interface/initialize_config.rs @@ -7,6 +7,8 @@ use borsh::{BorshDeserialize as AnchorDeserialize, BorshSerialize as AnchorSeria use solana_instruction::{AccountMeta, Instruction}; use solana_pubkey::Pubkey; +use crate::interface::instructions::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR; + /// Default address tree v2 pubkey. pub const ADDRESS_TREE_V2: Pubkey = solana_pubkey::pubkey!("amt2kaJA14v3urZbZvnc5v2np8jqvc4Z8zDep5wbtzx"); @@ -115,16 +117,14 @@ impl InitializeRentFreeConfig { address_space: self.address_space, }; - // Anchor discriminator for "initialize_compression_config" - // SHA256("global:initialize_compression_config")[..8] - const DISCRIMINATOR: [u8; 8] = [133, 228, 12, 169, 56, 76, 222, 61]; - let serialized_data = instruction_data .try_to_vec() .expect("Failed to serialize instruction data"); - let mut data = Vec::with_capacity(DISCRIMINATOR.len() + serialized_data.len()); - data.extend_from_slice(&DISCRIMINATOR); + let mut data = Vec::with_capacity( + INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR.len() + serialized_data.len(), + ); + data.extend_from_slice(&INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR); data.extend_from_slice(&serialized_data); let instruction = Instruction { diff --git a/sdk-libs/client/src/interface/instructions.rs b/sdk-libs/client/src/interface/instructions.rs index e80e7b72c1..bb4056ceae 100644 --- a/sdk-libs/client/src/interface/instructions.rs +++ b/sdk-libs/client/src/interface/instructions.rs @@ -234,12 +234,7 @@ where let output_queue = get_output_queue(&cold_accounts[0].0.tree_info); let output_state_tree_index = remaining_accounts.insert_or_get(output_queue); - let packed_tree_infos = proof.pack_tree_infos(&mut remaining_accounts); - let tree_infos = &packed_tree_infos - .state_trees - .as_ref() - .ok_or("missing state_trees in packed_tree_infos")? - .packed_tree_infos; + let tree_infos = proof.pack_state_tree_infos(&mut remaining_accounts); let mut accounts = program_account_metas.to_vec(); let mut typed_accounts = Vec::with_capacity(cold_accounts.len()); @@ -313,14 +308,9 @@ pub fn build_compress_accounts_idempotent( let output_queue = get_output_queue(&proof.accounts[0].tree_info); let output_state_tree_index = remaining_accounts.insert_or_get(output_queue); - let packed_tree_infos = proof.pack_tree_infos(&mut remaining_accounts); - let tree_infos = packed_tree_infos - .state_trees - .as_ref() - .ok_or("missing state_trees in packed_tree_infos")?; + let tree_infos = proof.pack_state_tree_infos(&mut remaining_accounts); let cold_metas: Vec<_> = tree_infos - .packed_tree_infos .iter() .map(|tree_info| CompressedAccountMetaNoLamportsNoAddress { tree_info: *tree_info, diff --git a/sdk-libs/client/src/interface/load_accounts.rs b/sdk-libs/client/src/interface/load_accounts.rs index 0d564734a2..01d91364c1 100644 --- a/sdk-libs/client/src/interface/load_accounts.rs +++ b/sdk-libs/client/src/interface/load_accounts.rs @@ -220,7 +220,7 @@ fn group_pda_specs<'a, V>( specs: &[&'a PdaSpec], max_per_group: usize, ) -> Vec>> { - assert!(max_per_group > 0, "max_per_group must be non-zero"); + debug_assert!(max_per_group > 0, "max_per_group must be non-zero"); if specs.is_empty() { return Vec::new(); } @@ -424,11 +424,7 @@ fn build_transfer2( fee_payer: Pubkey, ) -> Result { let mut packed = PackedAccounts::default(); - let packed_trees = proof.pack_tree_infos(&mut packed); - let tree_infos = packed_trees - .state_trees - .as_ref() - .ok_or_else(|| LoadAccountsError::BuildInstruction("no state trees".into()))?; + let tree_infos = proof.pack_state_tree_infos(&mut packed); let mut token_accounts = Vec::with_capacity(contexts.len()); let mut tlv_data: Vec> = Vec::with_capacity(contexts.len()); @@ -436,12 +432,12 @@ fn build_transfer2( for (i, ctx) in contexts.iter().enumerate() { let token = &ctx.compressed.token; - let tree = tree_infos.packed_tree_infos.get(i).ok_or( - LoadAccountsError::TreeInfoIndexOutOfBounds { + let tree = tree_infos + .get(i) + .ok_or(LoadAccountsError::TreeInfoIndexOutOfBounds { index: i, - len: tree_infos.packed_tree_infos.len(), - }, - )?; + len: tree_infos.len(), + })?; let owner_idx = packed.insert_or_get_config(ctx.wallet_owner, true, false); let ata_idx = packed.insert_or_get(derive_token_ata(&ctx.wallet_owner, &ctx.mint)); diff --git a/sdk-libs/client/src/interface/pack.rs b/sdk-libs/client/src/interface/pack.rs index 804a48751d..97505adabe 100644 --- a/sdk-libs/client/src/interface/pack.rs +++ b/sdk-libs/client/src/interface/pack.rs @@ -12,6 +12,9 @@ use crate::indexer::{TreeInfo, ValidityProofWithContext}; pub enum PackError { #[error("Failed to add system accounts: {0}")] SystemAccounts(#[from] light_sdk::error::LightSdkError), + + #[error("Failed to pack tree infos: {0}")] + Indexer(#[from] crate::indexer::IndexerError), } /// Packed state tree infos from validity proof. @@ -87,7 +90,7 @@ fn pack_proof_internal( // For mint creation: pack address tree first (index 1), then state tree. let (client_packed_tree_infos, state_tree_index) = if include_state_tree { // Pack tree infos first to ensure address tree is at index 1 - let tree_infos = proof.pack_tree_infos(&mut packed); + let tree_infos = proof.pack_tree_infos(&mut packed)?; // Then add state tree (will be after address tree) let state_tree = output_tree @@ -99,7 +102,7 @@ fn pack_proof_internal( (tree_infos, Some(state_idx)) } else { - let tree_infos = proof.pack_tree_infos(&mut packed); + let tree_infos = proof.pack_tree_infos(&mut packed)?; (tree_infos, None) }; let (remaining_accounts, system_offset, _) = packed.to_account_metas(); diff --git a/sdk-libs/client/src/local_test_validator.rs b/sdk-libs/client/src/local_test_validator.rs index 36ed7c04b3..b27daa6a25 100644 --- a/sdk-libs/client/src/local_test_validator.rs +++ b/sdk-libs/client/src/local_test_validator.rs @@ -1,6 +1,7 @@ -use std::process::{Command, Stdio}; +use std::process::Stdio; use light_prover_client::helpers::get_project_root; +use tokio::process::Command; /// Configuration for an upgradeable program to deploy to the validator. #[derive(Debug, Clone)] @@ -57,25 +58,25 @@ impl Default for LightValidatorConfig { pub async fn spawn_validator(config: LightValidatorConfig) { if let Some(project_root) = get_project_root() { - let path = "cli/test_bin/run test-validator"; - let mut path = format!("{}/{}", project_root.trim(), path); + let command = "cli/test_bin/run test-validator"; + let mut command = format!("{}/{}", project_root.trim(), command); if !config.enable_indexer { - path.push_str(" --skip-indexer"); + command.push_str(" --skip-indexer"); } if let Some(limit_ledger_size) = config.limit_ledger_size { - path.push_str(&format!(" --limit-ledger-size {}", limit_ledger_size)); + command.push_str(&format!(" --limit-ledger-size {}", limit_ledger_size)); } for sbf_program in config.sbf_programs.iter() { - path.push_str(&format!( + command.push_str(&format!( " --sbf-program {} {}", sbf_program.0, sbf_program.1 )); } for upgradeable_program in config.upgradeable_programs.iter() { - path.push_str(&format!( + command.push_str(&format!( " --upgradeable-program {} {} {}", upgradeable_program.program_id, upgradeable_program.program_path, @@ -84,18 +85,18 @@ pub async fn spawn_validator(config: LightValidatorConfig) { } if !config.enable_prover { - path.push_str(" --skip-prover"); + command.push_str(" --skip-prover"); } if config.use_surfpool { - path.push_str(" --use-surfpool"); + command.push_str(" --use-surfpool"); } for arg in config.validator_args.iter() { - path.push_str(&format!(" {}", arg)); + command.push_str(&format!(" {}", arg)); } - println!("Starting validator with command: {}", path); + println!("Starting validator with command: {}", command); if config.use_surfpool { // The CLI starts surfpool, prover, and photon, then exits once all @@ -103,24 +104,25 @@ pub async fn spawn_validator(config: LightValidatorConfig) { // is up before the test proceeds. let mut child = Command::new("sh") .arg("-c") - .arg(path) + .arg(command) .stdin(Stdio::null()) .stdout(Stdio::inherit()) .stderr(Stdio::inherit()) .spawn() .expect("Failed to start server process"); - let status = child.wait().expect("Failed to wait for CLI process"); + let status = child.wait().await.expect("Failed to wait for CLI process"); assert!(status.success(), "CLI exited with error: {}", status); } else { - let child = Command::new("sh") + let _child = Command::new("sh") .arg("-c") - .arg(path) + .arg(command) .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null()) .spawn() .expect("Failed to start server process"); - std::mem::drop(child); + // Intentionally detaching the spawned child; the caller only waits + // for the validator services to become available. tokio::time::sleep(tokio::time::Duration::from_secs(config.wait_time)).await; } } diff --git a/sdk-libs/client/src/utils.rs b/sdk-libs/client/src/utils.rs index b8f2e05ecb..0055f8dbea 100644 --- a/sdk-libs/client/src/utils.rs +++ b/sdk-libs/client/src/utils.rs @@ -15,8 +15,11 @@ pub fn find_light_bin() -> Option { if !output.status.success() { return None; } - // Convert the output into a string (removing any trailing newline) - let light_path = String::from_utf8_lossy(&output.stdout).trim().to_string(); + let light_path = std::str::from_utf8(&output.stdout) + .ok()? + .trim_end_matches("\r\n") + .trim_end_matches('\n') + .to_string(); // Get the parent directory of the 'light' binary let mut light_bin_path = PathBuf::from(light_path); light_bin_path.pop(); // Remove the 'light' binary itself @@ -30,16 +33,16 @@ pub fn find_light_bin() -> Option { #[cfg(feature = "devenv")] { println!("Use only in light protocol monorepo. Using 'git rev-parse --show-toplevel' to find the location of 'light' binary"); - 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(); + let output = std::process::Command::new("git") + .arg("rev-parse") + .arg("--show-toplevel") + .output() + .expect("Failed to get top-level directory"); + let light_protocol_toplevel = std::str::from_utf8(&output.stdout) + .ok()? + .trim_end_matches("\r\n") + .trim_end_matches('\n') + .to_string(); let light_path = PathBuf::from(format!("{}/target/deploy/", light_protocol_toplevel)); Some(light_path) } diff --git a/sdk-libs/program-test/src/indexer/test_indexer.rs b/sdk-libs/program-test/src/indexer/test_indexer.rs index 584a684a59..d1b46c9148 100644 --- a/sdk-libs/program-test/src/indexer/test_indexer.rs +++ b/sdk-libs/program-test/src/indexer/test_indexer.rs @@ -95,6 +95,20 @@ use crate::accounts::{ }; use crate::indexer::TestIndexerExtensions; +fn build_compressed_proof(body: &str) -> Result { + let proof_json = deserialize_gnark_proof_json(body) + .map_err(|error| IndexerError::CustomError(error.to_string()))?; + let (proof_a, proof_b, proof_c) = proof_from_json_struct(proof_json) + .map_err(|error| IndexerError::CustomError(error.to_string()))?; + let (proof_a, proof_b, proof_c) = compress_proof(&proof_a, &proof_b, &proof_c); + + Ok(CompressedProof { + a: proof_a, + b: proof_b, + c: proof_c, + }) +} + #[derive(Debug)] pub struct TestIndexer { pub state_merkle_trees: Vec, @@ -1696,11 +1710,13 @@ impl TestIndexer { DEFAULT_BATCH_ROOT_HISTORY_LEN, )); - (FeeConfig::test_batched().state_merkle_tree_rollover as i64,merkle_tree, Some(params.output_queue_batch_size as usize)) + (FeeConfig::test_batched().state_merkle_tree_rollover as i64, merkle_tree, Some(params.output_queue_batch_size as usize)) } #[cfg(not(feature = "devenv"))] - panic!("Batched state merkle trees require the 'devenv' feature to be enabled") + { + panic!("Batched state merkle trees require the 'devenv' feature to be enabled") + } } _ => panic!( "add_state_merkle_tree: tree_type not supported, {}. tree_type: 1 concurrent, 2 batched", @@ -2585,20 +2601,10 @@ impl TestIndexer { })?; if status.is_success() { - let proof_json = deserialize_gnark_proof_json(&body) - .map_err(|error| IndexerError::CustomError(error.to_string()))?; - let (proof_a, proof_b, proof_c) = proof_from_json_struct(proof_json); - let (proof_a, proof_b, proof_c) = - compress_proof(&proof_a, &proof_b, &proof_c); return Ok(ValidityProofWithContext { accounts: account_proof_inputs, addresses: address_proof_inputs, - proof: CompressedProof { - a: proof_a, - b: proof_b, - c: proof_c, - } - .into(), + proof: build_compressed_proof(&body)?.into(), }); } diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/integration_tests.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/integration_tests.rs index 9b40b900e5..2c3e82972a 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/integration_tests.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/integration_tests.rs @@ -3863,7 +3863,7 @@ async fn test_d9_edge_many_literals() { #[tokio::test] async fn test_d9_edge_mixed() { use csdk_anchor_full_derived_test::d9_seeds::{ - edge_cases::{AB, SEED_123, _UNDERSCORE_CONST}, + edge_cases::{_UNDERSCORE_CONST, AB, SEED_123}, D9EdgeMixedParams, }; 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 154f4e2045..e36b1ef30c 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 @@ -127,7 +127,7 @@ async fn create_compressed_account( ) .await? .value; - let packed_accounts = rpc_result.pack_tree_infos(&mut remaining_accounts); + let packed_accounts = rpc_result.pack_tree_infos(&mut remaining_accounts).unwrap(); let output_tree_index = rpc .get_random_state_tree_info() @@ -178,6 +178,7 @@ async fn read_sha256_light_system_cpi( let packed_tree_accounts = rpc_result .pack_tree_infos(&mut remaining_accounts) + .unwrap() .state_trees .unwrap(); @@ -231,6 +232,7 @@ async fn read_sha256_lowlevel( let packed_tree_accounts = rpc_result .pack_tree_infos(&mut remaining_accounts) + .unwrap() .state_trees .unwrap(); @@ -289,7 +291,7 @@ async fn create_compressed_account_poseidon( ) .await? .value; - let packed_accounts = rpc_result.pack_tree_infos(&mut remaining_accounts); + let packed_accounts = rpc_result.pack_tree_infos(&mut remaining_accounts).unwrap(); let output_tree_index = rpc .get_random_state_tree_info() @@ -340,6 +342,7 @@ async fn read_poseidon_light_system_cpi( let packed_tree_accounts = rpc_result .pack_tree_infos(&mut remaining_accounts) + .unwrap() .state_trees .unwrap(); @@ -393,6 +396,7 @@ async fn read_poseidon_lowlevel( let packed_tree_accounts = rpc_result .pack_tree_infos(&mut remaining_accounts) + .unwrap() .state_trees .unwrap(); 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 e19d0742de..a5a4655db2 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 @@ -171,7 +171,7 @@ async fn create_compressed_account( ) .await? .value; - let packed_accounts = rpc_result.pack_tree_infos(&mut remaining_accounts); + let packed_accounts = rpc_result.pack_tree_infos(&mut remaining_accounts).unwrap(); let output_tree_index = rpc .get_random_state_tree_info() @@ -223,6 +223,7 @@ async fn update_compressed_account( let packed_tree_accounts = rpc_result .pack_tree_infos(&mut remaining_accounts) + .unwrap() .state_trees .unwrap(); @@ -277,6 +278,7 @@ async fn close_compressed_account( let packed_tree_accounts = rpc_result .pack_tree_infos(&mut remaining_accounts) + .unwrap() .state_trees .unwrap(); @@ -340,6 +342,7 @@ async fn reinit_closed_account( let packed_tree_accounts = rpc_result .pack_tree_infos(&mut remaining_accounts) + .unwrap() .state_trees .unwrap(); @@ -388,6 +391,7 @@ async fn close_compressed_account_permanent( let packed_tree_accounts = rpc_result .pack_tree_infos(&mut remaining_accounts) + .unwrap() .state_trees .unwrap(); diff --git a/sdk-tests/sdk-native-test/tests/test.rs b/sdk-tests/sdk-native-test/tests/test.rs index 30d792487f..ac519f0e57 100644 --- a/sdk-tests/sdk-native-test/tests/test.rs +++ b/sdk-tests/sdk-native-test/tests/test.rs @@ -103,7 +103,10 @@ pub async fn create_pda( .value; let output_merkle_tree_index = accounts.insert_or_get(*merkle_tree_pubkey); - let packed_address_tree_info = rpc_result.pack_tree_infos(&mut accounts).address_trees[0]; + let packed_address_tree_info = rpc_result + .pack_tree_infos(&mut accounts) + .unwrap() + .address_trees[0]; let (accounts, system_accounts_offset, tree_accounts_offset) = accounts.to_account_metas(); let instruction_data = CreatePdaInstructionData { @@ -147,6 +150,7 @@ pub async fn update_pda( let packed_accounts = rpc_result .pack_tree_infos(&mut accounts) + .unwrap() .state_trees .unwrap(); diff --git a/sdk-tests/sdk-pinocchio-v1-test/tests/test.rs b/sdk-tests/sdk-pinocchio-v1-test/tests/test.rs index 0ae7f5c029..17d2c07b1e 100644 --- a/sdk-tests/sdk-pinocchio-v1-test/tests/test.rs +++ b/sdk-tests/sdk-pinocchio-v1-test/tests/test.rs @@ -101,7 +101,10 @@ pub async fn create_pda( .value; let output_merkle_tree_index = accounts.insert_or_get(*merkle_tree_pubkey); - let packed_address_tree_info = rpc_result.pack_tree_infos(&mut accounts).address_trees[0]; + let packed_address_tree_info = rpc_result + .pack_tree_infos(&mut accounts) + .unwrap() + .address_trees[0]; let (accounts, system_accounts_offset, tree_accounts_offset) = accounts.to_account_metas(); let instruction_data = CreatePdaInstructionData { proof: rpc_result.proof, @@ -145,6 +148,7 @@ pub async fn update_pda( let packed_accounts = rpc_result .pack_tree_infos(&mut accounts) + .unwrap() .state_trees .unwrap(); diff --git a/sdk-tests/sdk-pinocchio-v2-test/tests/test.rs b/sdk-tests/sdk-pinocchio-v2-test/tests/test.rs index 59a0562c63..510c98b2b5 100644 --- a/sdk-tests/sdk-pinocchio-v2-test/tests/test.rs +++ b/sdk-tests/sdk-pinocchio-v2-test/tests/test.rs @@ -111,7 +111,8 @@ pub async fn create_pda( .value; let output_merkle_tree_index = accounts.insert_or_get(*merkle_tree_pubkey); - let packed_address_tree_info = rpc_result.pack_tree_infos(&mut accounts).address_trees[0]; + let packed_tree_infos = rpc_result.pack_tree_infos(&mut accounts)?; + let packed_address_tree_info = packed_tree_infos.address_trees[0]; let (accounts, system_accounts_offset, tree_accounts_offset) = accounts.to_account_metas(); let instruction_data = CreatePdaInstructionData { proof: rpc_result.proof, @@ -154,7 +155,7 @@ pub async fn update_pda( .value; let packed_accounts = rpc_result - .pack_tree_infos(&mut accounts) + .pack_tree_infos(&mut accounts)? .state_trees .unwrap(); diff --git a/sdk-tests/sdk-token-test/tests/ctoken_pda.rs b/sdk-tests/sdk-token-test/tests/ctoken_pda.rs index 8e2b595285..e17308bd62 100644 --- a/sdk-tests/sdk-token-test/tests/ctoken_pda.rs +++ b/sdk-tests/sdk-token-test/tests/ctoken_pda.rs @@ -156,7 +156,7 @@ pub async fn create_mint( let config = SystemAccountMetaConfig::new_with_cpi_context(ID, tree_info.cpi_context.unwrap()); packed_accounts.add_system_accounts_v2(config).unwrap(); // packed_accounts.insert_or_get(tree_info.get_output_pubkey()?); - rpc_result.pack_tree_infos(&mut packed_accounts); + rpc_result.pack_tree_infos(&mut packed_accounts).unwrap(); // Create PDA parameters let pda_amount = 100u64; diff --git a/sdk-tests/sdk-token-test/tests/decompress_full_cpi.rs b/sdk-tests/sdk-token-test/tests/decompress_full_cpi.rs index 5f096af560..bd511f2b2b 100644 --- a/sdk-tests/sdk-token-test/tests/decompress_full_cpi.rs +++ b/sdk-tests/sdk-token-test/tests/decompress_full_cpi.rs @@ -213,7 +213,7 @@ async fn test_decompress_full_cpi() { .unwrap() .value; - let packed_tree_info = rpc_result.pack_tree_infos(&mut remaining_accounts); + let packed_tree_infos = rpc_result.pack_state_tree_infos(&mut remaining_accounts); let config = DecompressFullAccounts::new(None); remaining_accounts .add_custom_system_accounts(config) @@ -236,12 +236,7 @@ async fn test_decompress_full_cpi() { let indices: Vec<_> = token_data .iter() .zip( - packed_tree_info - .state_trees - .as_ref() - .unwrap() - .packed_tree_infos - .iter(), + packed_tree_infos.iter(), ) .zip(ctx.destination_accounts.iter()) .zip(versions.iter()) @@ -370,7 +365,7 @@ async fn test_decompress_full_cpi_with_context() { .value; // Add tree accounts first, then custom system accounts (no CPI context since params is None) - let packed_tree_info = rpc_result.pack_tree_infos(&mut remaining_accounts); + let packed_tree_infos = rpc_result.pack_state_tree_infos(&mut remaining_accounts); let config = DecompressFullAccounts::new(None); remaining_accounts .add_custom_system_accounts(config) @@ -393,12 +388,7 @@ async fn test_decompress_full_cpi_with_context() { let indices: Vec<_> = token_data .iter() .zip( - packed_tree_info - .state_trees - .as_ref() - .unwrap() - .packed_tree_infos - .iter(), + packed_tree_infos.iter(), ) .zip(ctx.destination_accounts.iter()) .zip(versions.iter()) diff --git a/sdk-tests/sdk-token-test/tests/pda_ctoken.rs b/sdk-tests/sdk-token-test/tests/pda_ctoken.rs index 91e0f2db9e..130c1caa0d 100644 --- a/sdk-tests/sdk-token-test/tests/pda_ctoken.rs +++ b/sdk-tests/sdk-token-test/tests/pda_ctoken.rs @@ -214,7 +214,7 @@ pub async fn create_mint( let mut packed_accounts = PackedAccounts::default(); let config = SystemAccountMetaConfig::new_with_cpi_context(ID, tree_info.cpi_context.unwrap()); packed_accounts.add_system_accounts_v2(config).unwrap(); - rpc_result.pack_tree_infos(&mut packed_accounts); + rpc_result.pack_tree_infos(&mut packed_accounts).unwrap(); // Create PDA parameters let pda_amount = 100u64; diff --git a/sdk-tests/sdk-token-test/tests/test.rs b/sdk-tests/sdk-token-test/tests/test.rs index 3c6941881d..9bebbcf897 100644 --- a/sdk-tests/sdk-token-test/tests/test.rs +++ b/sdk-tests/sdk-token-test/tests/test.rs @@ -367,7 +367,7 @@ async fn transfer_compressed_tokens( .await? .value; - let packed_tree_info = rpc_result.pack_tree_infos(&mut remaining_accounts); + let packed_tree_info = rpc_result.pack_tree_infos(&mut remaining_accounts).unwrap(); let output_tree_index = packed_tree_info .state_trees .as_ref() @@ -433,7 +433,7 @@ async fn decompress_compressed_tokens( .await? .value; - let packed_tree_info = rpc_result.pack_tree_infos(&mut remaining_accounts); + let packed_tree_info = rpc_result.pack_tree_infos(&mut remaining_accounts).unwrap(); let output_tree_index = packed_tree_info .state_trees .as_ref() diff --git a/sdk-tests/sdk-token-test/tests/test_4_invocations.rs b/sdk-tests/sdk-token-test/tests/test_4_invocations.rs index 9e70170056..3c0bce8241 100644 --- a/sdk-tests/sdk-token-test/tests/test_4_invocations.rs +++ b/sdk-tests/sdk-token-test/tests/test_4_invocations.rs @@ -22,6 +22,21 @@ use solana_sdk::{ signature::{Keypair, Signature, Signer}, }; +fn pack_selected_output_tree_index( + tree_info: light_client::indexer::TreeInfo, + remaining_accounts: &mut PackedAccounts, +) -> Result> { + tree_info + .next_tree_info + .map(|next| next.pack_output_tree_index(remaining_accounts)) + .unwrap_or_else(|| tree_info.pack_output_tree_index(remaining_accounts)) + .map_err(|error| { + Box::new(RpcError::CustomError(format!( + "Failed to pack output tree index: {error}" + ))) + }) +} + #[ignore = "fix cpi context usage"] #[tokio::test] async fn test_4_invocations() { @@ -389,7 +404,7 @@ async fn create_compressed_escrow_pda( .await? .value; - let packed_tree_info = rpc_result.pack_tree_infos(&mut remaining_accounts); + let packed_tree_info = rpc_result.pack_tree_infos(&mut remaining_accounts).unwrap(); let new_address_params = packed_tree_info.address_trees[0] .into_new_address_params_assigned_packed(address_seed, Some(0)); @@ -495,29 +510,18 @@ async fn test_four_invokes_instruction( ) .await? .value; - // We need to pack the tree after the cpi context. - remaining_accounts.insert_or_get(rpc_result.accounts[0].tree_info.tree); - - let packed_tree_info = rpc_result.pack_tree_infos(&mut remaining_accounts); - let output_tree_index = packed_tree_info - .state_trees - .as_ref() - .unwrap() - .output_tree_index; + let output_tree_index = pack_selected_output_tree_index( + mint2_token_account.account.tree_info, + &mut remaining_accounts, + ) + .map_err(|error| *error)?; + let packed_tree_infos = rpc_result.pack_state_tree_infos(&mut remaining_accounts); // Create token metas from compressed accounts - each uses its respective tree info index // Index 0: escrow PDA, Index 1: mint2 token account, Index 2: mint3 token account - let mint2_tree_info = packed_tree_info - .state_trees - .as_ref() - .unwrap() - .packed_tree_infos[1]; + let mint2_tree_info = packed_tree_infos[1]; - let mint3_tree_info = packed_tree_info - .state_trees - .as_ref() - .unwrap() - .packed_tree_infos[2]; + let mint3_tree_info = packed_tree_infos[2]; // Create FourInvokesParams let four_invokes_params = sdk_token_test::FourInvokesParams { @@ -557,11 +561,7 @@ async fn test_four_invokes_instruction( }; // Create PdaParams - escrow PDA uses tree info index 0 - let escrow_tree_info = packed_tree_info - .state_trees - .as_ref() - .unwrap() - .packed_tree_infos[0]; + let escrow_tree_info = packed_tree_infos[0]; let pda_params = sdk_token_test::PdaParams { account_meta: light_sdk::instruction::account_meta::CompressedAccountMeta { diff --git a/sdk-tests/sdk-token-test/tests/test_4_transfer2.rs b/sdk-tests/sdk-token-test/tests/test_4_transfer2.rs index d7ef38a08c..1e17b61ce1 100644 --- a/sdk-tests/sdk-token-test/tests/test_4_transfer2.rs +++ b/sdk-tests/sdk-token-test/tests/test_4_transfer2.rs @@ -27,6 +27,20 @@ use solana_sdk::{ signature::{Keypair, Signer}, }; +#[allow(clippy::result_large_err)] +fn pack_selected_output_tree_index( + tree_info: light_client::indexer::TreeInfo, + remaining_accounts: &mut PackedAccounts, +) -> Result { + tree_info + .next_tree_info + .map(|next| next.pack_output_tree_index(remaining_accounts)) + .unwrap_or_else(|| tree_info.pack_output_tree_index(remaining_accounts)) + .map_err(|error| { + RpcError::CustomError(format!("Failed to pack output tree index: {error}")) + }) +} + #[tokio::test] async fn test_4_transfer2() { // Initialize the test environment @@ -339,7 +353,7 @@ async fn create_compressed_escrow_pda( .await? .value; - let packed_tree_info = rpc_result.pack_tree_infos(&mut remaining_accounts); + let packed_tree_info = rpc_result.pack_tree_infos(&mut remaining_accounts).unwrap(); let new_address_params = packed_tree_info.address_trees[0] .into_new_address_params_assigned_packed(address_seed, Some(0)); @@ -435,29 +449,17 @@ async fn test_four_transfer2_instruction( ) .await? .value; - // We need to pack the tree after the cpi context. - remaining_accounts.insert_or_get(rpc_result.accounts[0].tree_info.tree); - - let packed_tree_info = rpc_result.pack_tree_infos(&mut remaining_accounts); - let output_tree_index = packed_tree_info - .state_trees - .as_ref() - .unwrap() - .output_tree_index; + let output_tree_index = pack_selected_output_tree_index( + mint2_token_account.account.tree_info, + &mut remaining_accounts, + )?; + let packed_tree_infos = rpc_result.pack_state_tree_infos(&mut remaining_accounts); // Create token metas from compressed accounts - each uses its respective tree info index // Index 0: escrow PDA, Index 1: mint2 token account, Index 2: mint3 token account - let mint2_tree_info = packed_tree_info - .state_trees - .as_ref() - .unwrap() - .packed_tree_infos[1]; + let mint2_tree_info = packed_tree_infos[1]; - let mint3_tree_info = packed_tree_info - .state_trees - .as_ref() - .unwrap() - .packed_tree_infos[2]; + let mint3_tree_info = packed_tree_infos[2]; // Create FourTransfer2Params let four_transfer2_params = sdk_token_test::process_four_transfer2::FourTransfer2Params { @@ -491,11 +493,7 @@ async fn test_four_transfer2_instruction( }; // Create PdaParams - escrow PDA uses tree info index 0 - let escrow_tree_info = packed_tree_info - .state_trees - .as_ref() - .unwrap() - .packed_tree_infos[0]; + let escrow_tree_info = packed_tree_infos[0]; let pda_params = sdk_token_test::PdaParams { account_meta: light_sdk::instruction::account_meta::CompressedAccountMeta { diff --git a/sdk-tests/sdk-token-test/tests/test_deposit.rs b/sdk-tests/sdk-token-test/tests/test_deposit.rs index 9ebcbd8549..7353b08d92 100644 --- a/sdk-tests/sdk-token-test/tests/test_deposit.rs +++ b/sdk-tests/sdk-token-test/tests/test_deposit.rs @@ -23,6 +23,39 @@ use solana_sdk::{ signature::{Keypair, Signature, Signer}, }; +#[allow(clippy::result_large_err)] +fn pack_selected_output_tree_context( + tree_info: light_client::indexer::TreeInfo, + remaining_accounts: &mut PackedAccounts, +) -> Result<(u8, u8, u8), RpcError> { + let (tree, queue, output_state_tree_index) = if let Some(next) = tree_info.next_tree_info { + ( + next.tree, + next.queue, + next.pack_output_tree_index(remaining_accounts) + .map_err(|error| { + RpcError::CustomError(format!("Failed to pack output tree index: {error}")) + })?, + ) + } else { + ( + tree_info.tree, + tree_info.queue, + tree_info + .pack_output_tree_index(remaining_accounts) + .map_err(|error| { + RpcError::CustomError(format!("Failed to pack output tree index: {error}")) + })?, + ) + }; + + Ok(( + remaining_accounts.insert_or_get(tree), + remaining_accounts.insert_or_get(queue), + output_state_tree_index, + )) +} + #[ignore = "fix cpi context usage"] #[tokio::test] async fn test_deposit_compressed_account() { @@ -206,7 +239,7 @@ async fn create_deposit_compressed_account( ) .await? .value; - let packed_accounts = rpc_result.pack_tree_infos(&mut remaining_accounts); + let packed_accounts = rpc_result.pack_tree_infos(&mut remaining_accounts).unwrap(); println!("packed_accounts {:?}", packed_accounts.state_trees); // Create token meta from compressed account @@ -302,9 +335,14 @@ async fn update_deposit_compressed_account( "rpc_result.accounts[0].tree_info.queue {:?}", rpc_result.accounts[0].tree_info.queue.to_bytes() ); - // We need to pack the tree after the cpi context. - let index = remaining_accounts.insert_or_get(rpc_result.accounts[0].tree_info.tree); - println!("index {}", index); + let (output_tree_index, output_tree_queue_index, output_state_tree_index) = + pack_selected_output_tree_context( + rpc_result.accounts[0].tree_info, + &mut remaining_accounts, + )?; + println!("output_tree_index {}", output_tree_index); + println!("output_tree_queue_index {}", output_tree_queue_index); + println!("output_state_tree_index {}", output_state_tree_index); // Get mint from the compressed token account let mint = deposit_ctoken_account.token.mint; println!( @@ -318,15 +356,11 @@ async fn update_deposit_compressed_account( // Get validity proof for the compressed token account and new address println!("rpc_result {:?}", rpc_result); - let packed_accounts = rpc_result.pack_tree_infos(&mut remaining_accounts); - println!("packed_accounts {:?}", packed_accounts.state_trees); + let packed_tree_infos = rpc_result.pack_state_tree_infos(&mut remaining_accounts); + println!("packed_tree_infos {:?}", packed_tree_infos); // TODO: investigate why packed_tree_infos seem to be out of order // Create token meta from compressed account - let tree_info = packed_accounts - .state_trees - .as_ref() - .unwrap() - .packed_tree_infos[1]; + let tree_info = packed_tree_infos[1]; let depositing_token_metas = vec![TokenAccountMeta { amount: deposit_ctoken_account.token.amount, delegate_index: None, @@ -335,11 +369,7 @@ async fn update_deposit_compressed_account( tlv: None, }]; println!("depositing_token_metas {:?}", depositing_token_metas); - let tree_info = packed_accounts - .state_trees - .as_ref() - .unwrap() - .packed_tree_infos[2]; + let tree_info = packed_tree_infos[2]; let escrowed_token_meta = TokenAccountMeta { amount: escrow_ctoken_account.token.amount, delegate_index: None, @@ -354,19 +384,11 @@ async fn update_deposit_compressed_account( let system_accounts_start_offset = system_accounts_start_offset as u8; println!("remaining_accounts {:?}", remaining_accounts); - let tree_info = packed_accounts - .state_trees - .as_ref() - .unwrap() - .packed_tree_infos[0]; + let tree_info = packed_tree_infos[0]; let account_meta = CompressedAccountMeta { tree_info, address: escrow_pda.address.unwrap(), - output_state_tree_index: packed_accounts - .state_trees - .as_ref() - .unwrap() - .output_tree_index, + output_state_tree_index, }; let instruction = Instruction { @@ -381,14 +403,8 @@ async fn update_deposit_compressed_account( .concat(), data: sdk_token_test::instruction::UpdateDeposit { proof: rpc_result.proof, - output_tree_index: packed_accounts - .state_trees - .as_ref() - .unwrap() - .packed_tree_infos[0] - .merkle_tree_pubkey_index, - output_tree_queue_index: packed_accounts.state_trees.unwrap().packed_tree_infos[0] - .queue_pubkey_index, + output_tree_index, + output_tree_queue_index, system_accounts_start_offset, token_params: sdk_token_test::TokenParams { deposit_amount: amount, diff --git a/sdk-tests/sdk-v1-native-test/tests/test.rs b/sdk-tests/sdk-v1-native-test/tests/test.rs index a93beab599..2e10e61e14 100644 --- a/sdk-tests/sdk-v1-native-test/tests/test.rs +++ b/sdk-tests/sdk-v1-native-test/tests/test.rs @@ -94,7 +94,8 @@ pub async fn create_pda( .value; let output_merkle_tree_index = accounts.insert_or_get(*merkle_tree_pubkey); - let packed_address_tree_info = rpc_result.pack_tree_infos(&mut accounts).address_trees[0]; + let packed_tree_infos = rpc_result.pack_tree_infos(&mut accounts)?; + let packed_address_tree_info = packed_tree_infos.address_trees[0]; let (accounts, system_accounts_offset, tree_accounts_offset) = accounts.to_account_metas(); let instruction_data = CreatePdaInstructionData { @@ -137,7 +138,7 @@ pub async fn update_pda( .value; let packed_accounts = rpc_result - .pack_tree_infos(&mut accounts) + .pack_tree_infos(&mut accounts)? .state_trees .unwrap();