From 8994051e558c76c6804c55d1563867aafacfe197 Mon Sep 17 00:00:00 2001 From: Sergey Timoshin Date: Wed, 18 Mar 2026 16:54:07 +0000 Subject: [PATCH] fix: index-out-of-bounds event parser panic + regression test --- Cargo.lock | 1 + external/photon | 2 +- sdk-libs/event/Cargo.toml | 1 + sdk-libs/event/src/lib.rs | 2 + sdk-libs/event/src/parse.rs | 206 ++++++++++---------------- sdk-libs/event/src/regression_test.rs | 110 ++++++++++++++ 6 files changed, 194 insertions(+), 128 deletions(-) create mode 100644 sdk-libs/event/src/regression_test.rs diff --git a/Cargo.lock b/Cargo.lock index 9e293fd2b8..f8734a680d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3978,6 +3978,7 @@ name = "light-event" version = "0.23.0" dependencies = [ "borsh 0.10.4", + "bs58", "light-compressed-account", "light-hasher", "light-token-interface", diff --git a/external/photon b/external/photon index 7a649f9c45..8a0bbce6a9 160000 --- a/external/photon +++ b/external/photon @@ -1 +1 @@ -Subproject commit 7a649f9c45a138ef47b090445163abe84775145c +Subproject commit 8a0bbce6a9250e2cc41e50d10efa9256a180db58 diff --git a/sdk-libs/event/Cargo.toml b/sdk-libs/event/Cargo.toml index dbdfdf6ef4..1894f290de 100644 --- a/sdk-libs/event/Cargo.toml +++ b/sdk-libs/event/Cargo.toml @@ -15,4 +15,5 @@ light-zero-copy = { workspace = true } thiserror = { workspace = true } [dev-dependencies] +bs58 = { workspace = true } rand = { workspace = true } diff --git a/sdk-libs/event/src/lib.rs b/sdk-libs/event/src/lib.rs index 6a78baffe8..1955c3911d 100644 --- a/sdk-libs/event/src/lib.rs +++ b/sdk-libs/event/src/lib.rs @@ -11,3 +11,5 @@ pub mod error; pub mod event; pub mod parse; +#[cfg(test)] +mod regression_test; diff --git a/sdk-libs/event/src/parse.rs b/sdk-libs/event/src/parse.rs index f034eaebef..46d5e9bf82 100644 --- a/sdk-libs/event/src/parse.rs +++ b/sdk-libs/event/src/parse.rs @@ -639,16 +639,37 @@ fn create_batched_transaction_event( tx_hash: associated_instructions .insert_into_queues_instruction .tx_hash, - new_addresses: associated_instructions - .insert_into_queues_instruction - .addresses - .iter() - .map(|x| NewAddress { - address: x.address, - mt_pubkey: associated_instructions.accounts[x.tree_index as usize], - queue_index: u64::MAX, - }) - .collect::>(), + new_addresses: { + let mut addr_seq_map: Vec<_> = associated_instructions + .insert_into_queues_instruction + .address_sequence_numbers + .iter() + .map(|s| (s.tree_pubkey, u64::from(s.seq))) + .collect(); + + associated_instructions + .insert_into_queues_instruction + .addresses + .iter() + .map(|x| { + let tree_pubkey = associated_instructions.accounts[x.tree_index as usize]; + let queue_index = addr_seq_map + .iter_mut() + .find(|(pk, _)| *pk == tree_pubkey) + .map(|(_, seq)| { + let idx = *seq; + *seq += 1; + idx + }) + .unwrap_or(u64::MAX); + NewAddress { + address: x.address, + mt_pubkey: tree_pubkey, + queue_index, + } + }) + .collect::>() + }, address_sequence_numbers: associated_instructions .insert_into_queues_instruction .address_sequence_numbers @@ -656,127 +677,58 @@ fn create_batched_transaction_event( .map(From::from) .filter(|x: &MerkleTreeSequenceNumber| !(*x).is_empty()) .collect::>(), - batch_input_accounts: associated_instructions - .insert_into_queues_instruction - .nullifiers - .iter() - .filter(|x| { - input_sequence_numbers.iter().any(|y| { - y.tree_pubkey == associated_instructions.accounts[x.tree_index as usize] + batch_input_accounts: { + // Build a map from tree_pubkey -> incrementing sequence number for queue index assignment. + let mut seq_map: Vec<_> = associated_instructions + .insert_into_queues_instruction + .input_sequence_numbers + .iter() + .map(|s| (s.tree_pubkey, u64::from(s.seq))) + .collect(); + + associated_instructions + .insert_into_queues_instruction + .nullifiers + .iter() + .filter(|x| { + input_sequence_numbers.iter().any(|y| { + y.tree_pubkey == associated_instructions.accounts[x.tree_index as usize] + }) }) - }) - .map(|n| { - Ok(BatchNullifyContext { - tx_hash: associated_instructions - .insert_into_queues_instruction - .tx_hash, - account_hash: n.account_hash, - nullifier: { - // The nullifier is computed inside the account compression program. - // -> it is not part of the cpi system to account compression program that we index. - // -> we need to compute the nullifier here. - create_nullifier( - &n.account_hash, - n.leaf_index.into(), - &associated_instructions - .insert_into_queues_instruction - .tx_hash, - )? - }, - nullifier_queue_index: u64::MAX, + .map(|n| { + let tree_pubkey = associated_instructions.accounts[n.tree_index as usize]; + // Find the matching sequence entry and consume the next index. + let queue_index = seq_map + .iter_mut() + .find(|(pk, _)| *pk == tree_pubkey) + .map(|(_, seq)| { + let idx = *seq; + *seq += 1; + idx + }) + .unwrap_or(u64::MAX); + + Ok(BatchNullifyContext { + tx_hash: associated_instructions + .insert_into_queues_instruction + .tx_hash, + account_hash: n.account_hash, + nullifier: { + create_nullifier( + &n.account_hash, + n.leaf_index.into(), + &associated_instructions + .insert_into_queues_instruction + .tx_hash, + )? + }, + nullifier_queue_index: queue_index, + }) }) - }) - .collect::, ParseIndexerEventError>>()?, + .collect::, ParseIndexerEventError>>()? + }, input_sequence_numbers, }; - let nullifier_queue_indices = create_nullifier_queue_indices( - associated_instructions, - batched_transaction_event.batch_input_accounts.len(), - ); - - batched_transaction_event - .batch_input_accounts - .iter_mut() - .zip(nullifier_queue_indices.iter()) - .for_each(|(context, index)| { - context.nullifier_queue_index = *index; - }); - - let address_queue_indices = create_address_queue_indices( - associated_instructions, - batched_transaction_event.new_addresses.len(), - ); - - batched_transaction_event - .new_addresses - .iter_mut() - .zip(address_queue_indices.iter()) - .for_each(|(context, index)| { - context.queue_index = *index; - }); - Ok(batched_transaction_event) } - -fn create_nullifier_queue_indices( - associated_instructions: &AssociatedInstructions, - len: usize, -) -> Vec { - let input_merkle_tree_pubkeys = associated_instructions - .executing_system_instruction - .input_compressed_accounts - .iter() - .map(|x| { - associated_instructions - .executing_system_instruction - .accounts[x.merkle_context.merkle_tree_pubkey_index as usize] - }) - .collect::>(); - let mut nullifier_queue_indices = vec![u64::MAX; len]; - let mut internal_input_sequence_numbers = associated_instructions - .insert_into_queues_instruction - .input_sequence_numbers - .to_vec(); - // For every sequence number: - // 1. Find every input compressed account - // 2. assign sequence number as nullifier queue index - // 3. increment the sequence number - internal_input_sequence_numbers.iter_mut().for_each(|seq| { - for (i, merkle_tree_pubkey) in input_merkle_tree_pubkeys.iter().enumerate() { - if *merkle_tree_pubkey == seq.tree_pubkey { - nullifier_queue_indices[i] = seq.seq.into(); - seq.seq += 1; - } - } - }); - nullifier_queue_indices -} - -fn create_address_queue_indices( - associated_instructions: &AssociatedInstructions, - len: usize, -) -> Vec { - let address_merkle_tree_pubkeys = associated_instructions - .insert_into_queues_instruction - .addresses - .iter() - .map(|x| associated_instructions.accounts[x.tree_index as usize]) - .collect::>(); - let mut address_queue_indices = vec![u64::MAX; len]; - let mut internal_address_sequence_numbers = associated_instructions - .insert_into_queues_instruction - .address_sequence_numbers - .to_vec(); - internal_address_sequence_numbers - .iter_mut() - .for_each(|seq| { - for (i, merkle_tree_pubkey) in address_merkle_tree_pubkeys.iter().enumerate() { - if *merkle_tree_pubkey == seq.tree_pubkey { - address_queue_indices[i] = seq.seq.into(); - seq.seq += 1; - } - } - }); - address_queue_indices -} diff --git a/sdk-libs/event/src/regression_test.rs b/sdk-libs/event/src/regression_test.rs new file mode 100644 index 0000000000..9c46046c11 --- /dev/null +++ b/sdk-libs/event/src/regression_test.rs @@ -0,0 +1,110 @@ +/// Regression test for index-out-of-bounds panic in create_nullifier_queue_indices. +/// Transaction: 3ybts1eFSC7QN6aU4ao6NJCgn7xTbtBVyzeLDZJf9eVN93vHZWupX4TXqHHgV18xf17eit7Uw5T135uabnpToKK4 +/// Slot: 407265372 (mainnet) +/// This transaction crashed photon's indexer because `len` passed to +/// `create_nullifier_queue_indices` didn't match the number of input accounts. +#[cfg(test)] +mod tests { + use light_compressed_account::Pubkey; + + use crate::parse::event_from_light_transaction; + + fn pubkey(s: &str) -> Pubkey { + let bytes: [u8; 32] = bs58::decode(s).into_vec().unwrap().try_into().unwrap(); + Pubkey::from(bytes) + } + + fn ix_data(s: &str) -> Vec { + bs58::decode(s).into_vec().unwrap() + } + + // Account addresses used in the transaction + const USER: &str = "33X2Tg3gdxTwouaVSxpcNwVHJt2ZYxo3Hm7UjH2i8M3r"; + const REGISTERED_PDA: &str = "35hkDgaAKwMCaxRz2ocSZ6NaUrtKkyNqU6c4RV3tYJRh"; + const NOOP: &str = "noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV"; + const CPI_CONTEXT: &str = "HwXnGK3tPkkVY6P439H2p68AxpeuWXd5PcrAxFpbmfbA"; + const ACCOUNT_COMPRESSION: &str = "compr6CUsB5m2jS4Y3831ztGSTnDpnKJTKS95d64XVq"; + const SOL_POOL: &str = "CHK57ywWSDncAoRu1F8QgwYJeXuAJyyBYT4LixLXvMZ1"; + const SYSTEM_PROGRAM: &str = "11111111111111111111111111111111"; + const BMT3_TREE: &str = "bmt3ccLd4bqSVZVeCJnH1F6C8jNygAhaDfxDwePyyGb"; + const OQ3_QUEUE: &str = "oq3AxjekBWgo64gpauB6QtuZNesuv19xrhaC1ZM1THQ"; + const SMT5_TREE: &str = "smt5uPaQT9n6b1qAkgyonmzRxtuazA53Rddwntqistc"; + const NFQ5_QUEUE: &str = "nfq5b5xEguPtdD6uPetZduyrB5EUqad7gcUE46rALau"; + const BMT2_TREE: &str = "bmt2UxoBxB9xWev4BkLvkGdapsz6sZGkzViPNph7VFi"; + const OQ2_QUEUE: &str = "oq2UkeMsJLfXt2QHzim242SUi3nvjJs8Pn7Eac9H9vg"; + + // Program IDs + const LIGHT_SYSTEM: &str = "SySTEM1eSU2p4BGQfQpimFEWWSC1XDFeun3Nqzz3rT7"; + const COMPUTE_BUDGET: &str = "ComputeBudget111111111111111111111111111111"; + + #[test] + fn test_mainnet_tx_407265372_no_panic() { + // Tx 3ybts1eFSC7QN...ToKK4, slot 407265372 + // Before fix: panicked with "index out of bounds: the len is 3 but the index is 3" + let program_ids = vec![ + pubkey(COMPUTE_BUDGET), // SetComputeUnitLimit + pubkey(COMPUTE_BUDGET), // SetComputeUnitPrice + pubkey(LIGHT_SYSTEM), // Light system invoke + pubkey(SYSTEM_PROGRAM), // SOL transfer (inner) + pubkey(SYSTEM_PROGRAM), // SOL transfer (inner) + pubkey(SYSTEM_PROGRAM), // SOL transfer (inner) + pubkey(ACCOUNT_COMPRESSION), // InsertIntoQueues (inner) + ]; + + let instructions: Vec> = vec![ + ix_data("K1FDJ7"), + ix_data("3cDeqiGMb6md"), + ix_data("7Xu3JKNhcxBjvH52amHsaGu55uKzfsGvVjkBKAcEAAByDYGHt2TQQRq8aam17wkuH3Vtu2xuLyh8nZRxaqTEZPKM88CTs2e9MMiHW1ZA2NmFwbtgeHLFSRvW2DCayZMqHZWGEPjKwXnEJjFfKCTiJDXLeHbqirZeH4M3rYpeudpPnbNH9F9vLchjWs73hKJ9aSLVJKnJNXyr6ZW4hZd8YKVk3jaS11oW2ndPQT8CzYAF79wu8uishgqpLN42RwytWTDpMNUq7mKRFj1LkKKnpv8ya9WRxDCCKHfp1zn8sc1YviTcMyFsDRBvnE7kibyhcvd6hY9PvojPZNWABNDHxMGZoUL8xoUNRiD8Fxk7DyWQBvtqwyoPSjgFmKA97yEp4Kvj8btYDP24t51GYYXZyKjfFHnShcmdoKxuGohShW1UdjAhSWMVySZ92KRXjVJm6uv7CD5uXRy5Kuqco9ZHwASTv6HE1fQCEWKDdvq8Nx8SBMZF9jPM8JKJEarj"), + ix_data("3Bxs4HHMpGEM2775"), + ix_data("3Bxs4PckVVt51W8w"), + ix_data("3Bxs4PckVVt51W8w"), + ix_data("42NS6uhgPkAU4qDJGz54pXVoPKYL4VENq5jdLryg8pPRKsdthWiNYkaBQEimb4SSscjPZ2uYSXD7TjANLcaUdRMjh7Hid94o5GpGTxM3Pg2ALYdg8Qps6w2Sn6FXc1cp2vWVaXFQicExxLSTUNSSZwKH2M2XiqDxZBSekyELNcXkJCji9heVWqiB48zJX1YDBMYKLgXu3MoFvUgGjpYRteuuw44rBYUSfrs5tNh5CdfMtNkUJVCEvr5LSWeRUYwwXT8shx53iYb186vE3Gm2qY1Up7PfHdqGH1KZmzNz6ZjU2oC2r6zUHxoAA4v7HhMiC2cgwFXMrVGnw2nfKunjEP7Xm2Q62G4uJHGH3aMucTrSKCiwc55czqV9RaUDZUrvtfbLUjwG7XcPwwaY9JusFs21sZNveGE9xm1groM6uGn8ERCc6oBtFhouRKpfQiGoWxKeSrS6K5KWEq5aJ7XsZcXkNSdNGsGtgGu4nDXDtGhbamhXUtVmXcEfMMsMfoSzm1Cj1HCP89thHHC6P52Wert8XAfeei8X8bfwRHw6SzVFTBKkP7W8vjE2PgjwD5rVprBxS5owL4HPEnuTdSoawLA5JEqucpqgvXv7qihuJZ5aEQ8q2JhayJx3hqDriN6g1Vc2br8MtGRPXuwQYAd84jJoS6puMoanPnyFccv35jaxkEwUi5vY8J88ejut9W4uP7JVBivLBXYgDyLteffxA5a6rhJtFZ"), + ]; + + let accounts: Vec> = vec![ + vec![], // ComputeBudget + vec![], // ComputeBudget + // Light system invoke: user, registered PDA, noop, CPI context, account compression, + // SOL pool, system program, V2 trees (bmt3, oq3), V1 trees (smt5, nfq5), V2 trees (bmt2, oq2) + vec![ + pubkey(USER), + pubkey(USER), + pubkey(REGISTERED_PDA), + pubkey(NOOP), + pubkey(CPI_CONTEXT), + pubkey(ACCOUNT_COMPRESSION), + pubkey(SOL_POOL), + pubkey(USER), + pubkey(SYSTEM_PROGRAM), + pubkey(BMT3_TREE), + pubkey(OQ3_QUEUE), + pubkey(SMT5_TREE), + pubkey(NFQ5_QUEUE), + pubkey(BMT2_TREE), + pubkey(OQ2_QUEUE), + ], + // SOL transfers (inner) + vec![pubkey(SOL_POOL), pubkey(USER)], + vec![pubkey(USER), pubkey(BMT3_TREE)], + vec![pubkey(USER), pubkey(SMT5_TREE)], + // InsertIntoQueues (inner): CPI context, registered PDA, queues and trees + vec![ + pubkey(CPI_CONTEXT), + pubkey(REGISTERED_PDA), + pubkey(OQ3_QUEUE), + pubkey(BMT3_TREE), + pubkey(NFQ5_QUEUE), + pubkey(SMT5_TREE), + pubkey(OQ2_QUEUE), + pubkey(BMT2_TREE), + ], + ]; + + let result = event_from_light_transaction(&program_ids, &instructions, accounts); + assert!( + result.is_ok(), + "event_from_light_transaction failed: {:?}", + result.err() + ); + } +}