@@ -1285,3 +1285,218 @@ fn test_wrap_program_ids_account_compression_insufficient_accounts() {
12851285 "AccountCompression with insufficient accounts should be Unknown"
12861286 ) ;
12871287}
1288+
1289+ // ==========================================================================
1290+ // Regression test: mixed batch / legacy input accounts in one transaction
1291+ // ==========================================================================
1292+
1293+ /// Regression test for OOB panic in create_nullifier_queue_indices.
1294+ ///
1295+ /// Transaction 3ybts1eFSC7QN6aU4ao6NJCgn7xTbtBVyzeLDZJf9eVN93vHZWupX4TXqHHgV18xf17eit7Uw5T135uabnpToKK4
1296+ /// at slot 407265372 triggered "index out of bounds: len is 3 but index is 3"
1297+ /// because the system instruction had 4 input accounts mixing batch and
1298+ /// legacy/concurrent trees: [batchA, legacy, batchB, batchA].
1299+ ///
1300+ /// The InsertIntoQueues instruction also had 4 nullifiers. After filtering out
1301+ /// the legacy nullifier, batch_input_accounts.len() == 3. The old code used the
1302+ /// raw loop index i from input_compressed_accounts (4 elements) to write into
1303+ /// nullifier_queue_indices (len 3), causing the OOB on i==3.
1304+ ///
1305+ /// The fix walks input_compressed_accounts in order and uses a compact
1306+ /// batch_idx counter that only advances when a batch tree is found.
1307+ #[ test]
1308+ fn test_mixed_batch_legacy_nullifier_queue_indices_no_oob ( ) {
1309+ use light_compressed_account:: {
1310+ compressed_account:: {
1311+ CompressedAccount , PackedCompressedAccountWithMerkleContext , PackedMerkleContext ,
1312+ } ,
1313+ constants:: LIGHT_SYSTEM_PROGRAM_ID ,
1314+ discriminators:: DISCRIMINATOR_INVOKE ,
1315+ instruction_data:: {
1316+ data:: InstructionDataInvoke ,
1317+ insert_into_queues:: {
1318+ InsertIntoQueuesInstructionDataMut , InsertNullifierInput ,
1319+ MerkleTreeSequenceNumber as IxSeqNum ,
1320+ } ,
1321+ } ,
1322+ } ;
1323+ use light_event:: parse:: event_from_light_transaction;
1324+
1325+ let tree_a = Pubkey :: new_from_array ( [ 1u8 ; 32 ] ) ;
1326+ let legacy_tree = Pubkey :: new_from_array ( [ 2u8 ; 32 ] ) ;
1327+ let tree_b = Pubkey :: new_from_array ( [ 3u8 ; 32 ] ) ;
1328+
1329+ // --- Build the LightSystem instruction ---
1330+ // 4 input accounts: batchA (index 0), legacy (index 1), batchB (index 2), batchA (index 0)
1331+ let system_invoke_data = InstructionDataInvoke {
1332+ input_compressed_accounts_with_merkle_context : vec ! [
1333+ PackedCompressedAccountWithMerkleContext {
1334+ compressed_account: CompressedAccount :: default ( ) ,
1335+ merkle_context: PackedMerkleContext {
1336+ merkle_tree_pubkey_index: 0 , // treeA
1337+ queue_pubkey_index: 0 ,
1338+ leaf_index: 100 ,
1339+ prove_by_index: false ,
1340+ } ,
1341+ root_index: 0 ,
1342+ read_only: false ,
1343+ } ,
1344+ PackedCompressedAccountWithMerkleContext {
1345+ compressed_account: CompressedAccount :: default ( ) ,
1346+ merkle_context: PackedMerkleContext {
1347+ merkle_tree_pubkey_index: 1 , // legacyTree
1348+ queue_pubkey_index: 1 ,
1349+ leaf_index: 200 ,
1350+ prove_by_index: false ,
1351+ } ,
1352+ root_index: 0 ,
1353+ read_only: false ,
1354+ } ,
1355+ PackedCompressedAccountWithMerkleContext {
1356+ compressed_account: CompressedAccount :: default ( ) ,
1357+ merkle_context: PackedMerkleContext {
1358+ merkle_tree_pubkey_index: 2 , // treeB
1359+ queue_pubkey_index: 2 ,
1360+ leaf_index: 300 ,
1361+ prove_by_index: false ,
1362+ } ,
1363+ root_index: 0 ,
1364+ read_only: false ,
1365+ } ,
1366+ PackedCompressedAccountWithMerkleContext {
1367+ compressed_account: CompressedAccount :: default ( ) ,
1368+ merkle_context: PackedMerkleContext {
1369+ merkle_tree_pubkey_index: 0 , // treeA again
1370+ queue_pubkey_index: 0 ,
1371+ leaf_index: 400 ,
1372+ prove_by_index: false ,
1373+ } ,
1374+ root_index: 0 ,
1375+ read_only: false ,
1376+ } ,
1377+ ] ,
1378+ ..InstructionDataInvoke :: default ( )
1379+ } ;
1380+ // Format: [discriminator: 8][Anchor prefix: 4][borsh InstructionDataInvoke]
1381+ let mut system_ix_data = Vec :: new ( ) ;
1382+ system_ix_data. extend_from_slice ( & DISCRIMINATOR_INVOKE ) ;
1383+ system_ix_data. extend_from_slice ( & [ 0u8 ; 4 ] ) ;
1384+ system_ix_data. extend ( system_invoke_data. try_to_vec ( ) . unwrap ( ) ) ;
1385+
1386+ // First 9 are system accounts; accounts[9..] are the tree accounts referenced
1387+ // by merkle_tree_pubkey_index in each input compressed account.
1388+ let mut system_accounts = vec ! [ Pubkey :: default ( ) ; 9 ] ;
1389+ system_accounts. push ( tree_a) ; // index 0
1390+ system_accounts. push ( legacy_tree) ; // index 1
1391+ system_accounts. push ( tree_b) ; // index 2
1392+
1393+ // --- Solana system instruction (required for the CPI pattern match) ---
1394+ let solana_system_ix_data = vec ! [ 0u8 ; 12 ] ;
1395+ let solana_system_accounts: Vec < Pubkey > = vec ! [ ] ;
1396+
1397+ // --- Build the AccountCompression (InsertIntoQueues) instruction ---
1398+ // 4 nullifiers matching the 4 system inputs: batchA, legacy, batchB, batchA.
1399+ // 2 input sequence numbers: treeA seq=6, treeB seq=3.
1400+ let size = InsertIntoQueuesInstructionDataMut :: required_size_for_capacity (
1401+ 0 , // leaves
1402+ 4 , // nullifiers
1403+ 0 , // addresses
1404+ 0 , // output trees
1405+ 2 , // input trees (treeA, treeB)
1406+ 0 , // address trees
1407+ ) ;
1408+ let mut insert_queue_buf = vec ! [ 0u8 ; size] ;
1409+ {
1410+ let ( mut data_mut, _) =
1411+ InsertIntoQueuesInstructionDataMut :: new_at ( & mut insert_queue_buf, 0 , 4 , 0 , 0 , 2 , 0 )
1412+ . unwrap ( ) ;
1413+
1414+ data_mut. tx_hash = [ 42u8 ; 32 ] ;
1415+
1416+ // nullifiers: tree_index is an index into ac_accounts[2..] = [treeA, legacyTree, treeB]
1417+ data_mut. nullifiers [ 0 ] = InsertNullifierInput {
1418+ account_hash : [ 11u8 ; 32 ] ,
1419+ leaf_index : 100u32 . into ( ) ,
1420+ prove_by_index : 1 ,
1421+ tree_index : 0 , // treeA
1422+ queue_index : 0 ,
1423+ } ;
1424+ data_mut. nullifiers [ 1 ] = InsertNullifierInput {
1425+ account_hash : [ 22u8 ; 32 ] ,
1426+ leaf_index : 200u32 . into ( ) ,
1427+ prove_by_index : 0 ,
1428+ tree_index : 1 , // legacyTree — no sequence number entry
1429+ queue_index : 1 ,
1430+ } ;
1431+ data_mut. nullifiers [ 2 ] = InsertNullifierInput {
1432+ account_hash : [ 33u8 ; 32 ] ,
1433+ leaf_index : 300u32 . into ( ) ,
1434+ prove_by_index : 1 ,
1435+ tree_index : 2 , // treeB
1436+ queue_index : 2 ,
1437+ } ;
1438+ data_mut. nullifiers [ 3 ] = InsertNullifierInput {
1439+ account_hash : [ 44u8 ; 32 ] ,
1440+ leaf_index : 400u32 . into ( ) ,
1441+ prove_by_index : 1 ,
1442+ tree_index : 0 , // treeA again
1443+ queue_index : 0 ,
1444+ } ;
1445+
1446+ data_mut. input_sequence_numbers [ 0 ] = IxSeqNum {
1447+ tree_pubkey : tree_a,
1448+ queue_pubkey : Pubkey :: default ( ) ,
1449+ tree_type : 3u64 . into ( ) , // StateV2
1450+ seq : 6u64 . into ( ) ,
1451+ } ;
1452+ data_mut. input_sequence_numbers [ 1 ] = IxSeqNum {
1453+ tree_pubkey : tree_b,
1454+ queue_pubkey : Pubkey :: default ( ) ,
1455+ tree_type : 3u64 . into ( ) , // StateV2
1456+ seq : 3u64 . into ( ) ,
1457+ } ;
1458+ }
1459+
1460+ // Format: [discriminator: 8][prefix: 4][zero-copy data][empty cpi_context_outputs: 4]
1461+ let mut ac_ix_data = Vec :: new ( ) ;
1462+ ac_ix_data. extend_from_slice ( & DISCRIMINATOR_INSERT_INTO_QUEUES ) ;
1463+ ac_ix_data. extend_from_slice ( & [ 0u8 ; 4 ] ) ;
1464+ ac_ix_data. extend_from_slice ( & insert_queue_buf) ;
1465+ ac_ix_data. extend_from_slice ( & [ 0u8 ; 4 ] ) ; // borsh-encoded empty Vec (u32 len = 0)
1466+
1467+ // accounts[0] = signer, accounts[1] = REGISTERED_PROGRAM_PDA, accounts[2..] = trees
1468+ let ac_accounts = vec ! [
1469+ Pubkey :: default ( ) ,
1470+ Pubkey :: from( REGISTERED_PROGRAM_PDA ) ,
1471+ tree_a,
1472+ legacy_tree,
1473+ tree_b,
1474+ ] ;
1475+
1476+ // --- Assemble and invoke ---
1477+ let program_ids = vec ! [
1478+ Pubkey :: new_from_array( LIGHT_SYSTEM_PROGRAM_ID ) ,
1479+ Pubkey :: default ( ) , // SolanaSystem
1480+ Pubkey :: new_from_array( ACCOUNT_COMPRESSION_PROGRAM_ID ) ,
1481+ ] ;
1482+ let instructions = vec ! [ system_ix_data, solana_system_ix_data, ac_ix_data] ;
1483+ let accounts = vec ! [ system_accounts, solana_system_accounts, ac_accounts] ;
1484+
1485+ // Before the fix this panicked: "index out of bounds: len is 3 but index is 3"
1486+ let result = event_from_light_transaction ( & program_ids, & instructions, accounts) ;
1487+ let events = result
1488+ . expect ( "should parse without error" )
1489+ . expect ( "should find events" ) ;
1490+ assert_eq ! ( events. len( ) , 1 ) ;
1491+
1492+ let event = & events[ 0 ] ;
1493+ // 3 batch inputs: batchA, batchB, batchA (legacy is filtered out)
1494+ assert_eq ! ( event. batch_input_accounts. len( ) , 3 ) ;
1495+ // nullifier_queue_indices: batchA->seq=6, batchB->seq=3, batchA->seq=7 (incremented)
1496+ let queue_indices: Vec < u64 > = event
1497+ . batch_input_accounts
1498+ . iter ( )
1499+ . map ( |c| c. nullifier_queue_index )
1500+ . collect ( ) ;
1501+ assert_eq ! ( queue_indices, vec![ 6 , 3 , 7 ] ) ;
1502+ }
0 commit comments