From 4de6e77038607b8c9c401d40a23f29e637f6466b Mon Sep 17 00:00:00 2001 From: Mariusz Zak Date: Thu, 11 Dec 2025 10:25:35 +0100 Subject: [PATCH 1/2] feat(solana): validate chunk_count in assemble instructions --- .../assemble_and_submit_misbehaviour.rs | 6 + .../assemble_and_submit_misbehaviour/tests.rs | 116 +++++++++++ .../assemble_and_update_client.rs | 5 + .../assemble_and_update_client/tests.rs | 189 ++++++++++++++++++ 4 files changed, 316 insertions(+) diff --git a/programs/solana/programs/ics07-tendermint/src/instructions/assemble_and_submit_misbehaviour.rs b/programs/solana/programs/ics07-tendermint/src/instructions/assemble_and_submit_misbehaviour.rs index 999267615..0072046ce 100644 --- a/programs/solana/programs/ics07-tendermint/src/instructions/assemble_and_submit_misbehaviour.rs +++ b/programs/solana/programs/ics07-tendermint/src/instructions/assemble_and_submit_misbehaviour.rs @@ -71,6 +71,12 @@ pub fn assemble_and_submit_misbehaviour<'info>( ); let chunk_count = chunk_count as usize; + + require!( + chunk_count > 0 && chunk_count <= ctx.remaining_accounts.len(), + ErrorCode::InvalidChunkCount + ); + let submitter = ctx.accounts.submitter.key(); let misbehaviour_bytes = assemble_chunks(&ctx, submitter, chunk_count)?; diff --git a/programs/solana/programs/ics07-tendermint/src/instructions/assemble_and_submit_misbehaviour/tests.rs b/programs/solana/programs/ics07-tendermint/src/instructions/assemble_and_submit_misbehaviour/tests.rs index ef4bff9d8..080bd6385 100644 --- a/programs/solana/programs/ics07-tendermint/src/instructions/assemble_and_submit_misbehaviour/tests.rs +++ b/programs/solana/programs/ics07-tendermint/src/instructions/assemble_and_submit_misbehaviour/tests.rs @@ -382,6 +382,122 @@ fn test_assemble_and_submit_misbehaviour_client_already_frozen() { mollusk.process_and_validate_instruction(&instruction, &test_accounts.accounts, &checks); } +/// Test that `chunk_count` exceeding `remaining_accounts` fails with `InvalidChunkCount` +#[test] +fn test_assemble_and_submit_misbehaviour_chunk_count_exceeds_remaining_accounts() { + let chain_id = "test-chain"; + let height_1 = 90; + let height_2 = 95; + let submitter = Pubkey::new_unique(); + + let misbehaviour_bytes = create_mock_misbehaviour_bytes(100, 100, true); + + // Setup test accounts with chunks + let test_accounts = setup_test_accounts(TestSetupConfig { + chain_id, + height_1, + height_2, + submitter, + client_frozen: false, + with_valid_consensus_states: true, + with_chunks: true, + misbehaviour_bytes: &misbehaviour_bytes, + }); + + // Remember how many chunk PDAs we actually have + let actual_chunk_count = test_accounts.chunk_pdas.len(); + + // Create instruction with inflated chunk_count + let (access_manager_pda, _) = + solana_ibc_types::access_manager::AccessManager::pda(access_manager::ID); + + let instruction_data = crate::instruction::AssembleAndSubmitMisbehaviour { + chunk_count: (actual_chunk_count + 5) as u8, // Claim more chunks than provided + trusted_height_1: height_1, + trusted_height_2: height_2, + }; + + let mut account_metas = vec![ + AccountMeta::new(test_accounts.client_state_pda, false), + AccountMeta::new_readonly(test_accounts.app_state_pda, false), + AccountMeta::new_readonly(access_manager_pda, false), + AccountMeta::new_readonly(test_accounts.trusted_consensus_state_1_pda, false), + AccountMeta::new_readonly(test_accounts.trusted_consensus_state_2_pda, false), + AccountMeta::new(test_accounts.submitter, true), + AccountMeta::new_readonly(anchor_lang::solana_program::sysvar::instructions::ID, false), + ]; + + // Only add the actual chunk accounts (fewer than claimed) + for chunk_pda in &test_accounts.chunk_pdas { + account_metas.push(AccountMeta::new(*chunk_pda, false)); + } + + let instruction = Instruction { + program_id: crate::ID, + accounts: account_metas, + data: instruction_data.data(), + }; + + let mollusk = Mollusk::new(&crate::ID, PROGRAM_BINARY_PATH); + let checks = vec![Check::err( + anchor_lang::error::Error::from(ErrorCode::InvalidChunkCount).into(), + )]; + mollusk.process_and_validate_instruction(&instruction, &test_accounts.accounts, &checks); +} + +/// Test that `chunk_count` = 0 fails with `InvalidChunkCount` +#[test] +fn test_assemble_and_submit_misbehaviour_zero_chunk_count_rejected() { + let chain_id = "test-chain"; + let height_1 = 90; + let height_2 = 95; + let submitter = Pubkey::new_unique(); + + // Setup without chunks + let test_accounts = setup_test_accounts(TestSetupConfig { + chain_id, + height_1, + height_2, + submitter, + client_frozen: false, + with_valid_consensus_states: true, + with_chunks: false, + misbehaviour_bytes: &[], + }); + + let (access_manager_pda, _) = + solana_ibc_types::access_manager::AccessManager::pda(access_manager::ID); + + // Create instruction with chunk_count = 0 + let instruction_data = crate::instruction::AssembleAndSubmitMisbehaviour { + chunk_count: 0, // Zero chunks - invalid + trusted_height_1: height_1, + trusted_height_2: height_2, + }; + + let account_metas = vec![ + AccountMeta::new(test_accounts.client_state_pda, false), + AccountMeta::new_readonly(test_accounts.app_state_pda, false), + AccountMeta::new_readonly(access_manager_pda, false), + AccountMeta::new_readonly(test_accounts.trusted_consensus_state_1_pda, false), + AccountMeta::new_readonly(test_accounts.trusted_consensus_state_2_pda, false), + AccountMeta::new(test_accounts.submitter, true), + AccountMeta::new_readonly(anchor_lang::solana_program::sysvar::instructions::ID, false), + ]; + + let instruction = Instruction { + program_id: crate::ID, + accounts: account_metas, + data: instruction_data.data(), + }; + + let mollusk = Mollusk::new(&crate::ID, PROGRAM_BINARY_PATH); + let checks = vec![Check::err( + anchor_lang::error::Error::from(ErrorCode::InvalidChunkCount).into(), + )]; + mollusk.process_and_validate_instruction(&instruction, &test_accounts.accounts, &checks); +} + #[test] fn test_assemble_and_submit_misbehaviour_wrong_chunk_pda() { let chain_id = "test-chain"; diff --git a/programs/solana/programs/ics07-tendermint/src/instructions/assemble_and_update_client.rs b/programs/solana/programs/ics07-tendermint/src/instructions/assemble_and_update_client.rs index 09654f1f5..02b5d1773 100644 --- a/programs/solana/programs/ics07-tendermint/src/instructions/assemble_and_update_client.rs +++ b/programs/solana/programs/ics07-tendermint/src/instructions/assemble_and_update_client.rs @@ -87,6 +87,11 @@ pub fn assemble_and_update_client<'info>( let chunk_count = chunk_count as usize; + require!( + chunk_count > 0 && chunk_count <= ctx.remaining_accounts.len(), + ErrorCode::InvalidChunkCount + ); + let header_bytes = assemble_chunks(&ctx, target_height, chunk_count)?; let result = process_header_update( diff --git a/programs/solana/programs/ics07-tendermint/src/instructions/assemble_and_update_client/tests.rs b/programs/solana/programs/ics07-tendermint/src/instructions/assemble_and_update_client/tests.rs index 49b81b4d6..3a988f488 100644 --- a/programs/solana/programs/ics07-tendermint/src/instructions/assemble_and_update_client/tests.rs +++ b/programs/solana/programs/ics07-tendermint/src/instructions/assemble_and_update_client/tests.rs @@ -1547,6 +1547,195 @@ fn test_assemble_updates_latest_height() { } } +/// Test that `chunk_count` exceeding `remaining_accounts` fails with `InvalidChunkCount` +#[test] +fn test_assemble_chunk_count_exceeds_remaining_accounts() { + let mollusk = setup_mollusk(); + + let chain_id = "test-chain"; + let target_height = 100u64; + let trusted_height = 90u64; + let submitter = Pubkey::new_unique(); + + // Create only 2 chunk accounts + let (_, chunks) = create_test_header_and_chunks(2); + + let client_state_pda = derive_client_state_pda(); + let (consensus_state_pda, _) = Pubkey::find_program_address( + &[ + crate::state::ConsensusStateStore::SEED, + &target_height.to_le_bytes(), + ], + &crate::ID, + ); + + // Only create PDAs for 2 chunks + let chunk_pdas = get_chunk_pdas(&submitter, target_height, 2); + + let (access_manager_pda, _) = + solana_ibc_types::access_manager::AccessManager::pda(access_manager::ID); + let (app_state_pda, app_state_account) = create_app_state_account(access_manager::ID); + + let trusted_consensus_pda = derive_consensus_state_pda(trusted_height); + + // Create instruction claiming 5 chunks but only provide 2 accounts + let mut account_metas = vec![ + AccountMeta::new(client_state_pda, false), + AccountMeta::new_readonly(app_state_pda, false), + AccountMeta::new_readonly(access_manager_pda, false), + AccountMeta::new_readonly(trusted_consensus_pda, false), + AccountMeta::new(consensus_state_pda, false), + AccountMeta::new(submitter, true), + AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(anchor_lang::solana_program::sysvar::instructions::ID, false), + ]; + + // Only add 2 chunk accounts to remaining_accounts + for chunk_pda in &chunk_pdas { + account_metas.push(AccountMeta::new(*chunk_pda, false)); + } + + // But claim chunk_count = 5 (more than provided) + let instruction = Instruction { + program_id: crate::ID, + accounts: account_metas, + data: crate::instruction::AssembleAndUpdateClient { + target_height, + chunk_count: 5, // Claiming 5 chunks but only 2 provided! + trusted_height, + } + .data(), + }; + + let (_, access_manager_account) = + crate::test_helpers::access_control::create_access_manager_account( + submitter, + vec![submitter], + ); + + let mut accounts = vec![ + (app_state_pda, app_state_account), + (access_manager_pda, access_manager_account), + (client_state_pda, create_client_state_account(chain_id, trusted_height)), + ( + trusted_consensus_pda, + create_consensus_state_account([0; 32], [0; 32], 0), + ), + (consensus_state_pda, Account::default()), + (submitter, create_submitter_account(10_000_000_000)), + keyed_account_for_system_program(), + ]; + + // Add only 2 chunk accounts + for (i, chunk_pda) in chunk_pdas.iter().enumerate() { + accounts.push((*chunk_pda, create_chunk_account(chunks[i].clone()))); + } + + // Add Clock sysvar + accounts.push(create_clock_account(0)); + + // Add instructions sysvar + accounts.push(( + anchor_lang::solana_program::sysvar::instructions::ID, + crate::test_helpers::create_instructions_sysvar_account(), + )); + + let result = mollusk.process_instruction(&instruction, &accounts); + + // Should fail with InvalidChunkCount because chunk_count (5) > remaining_accounts.len() (2) + assert_error_code( + result, + ErrorCode::InvalidChunkCount, + "chunk_count exceeds remaining_accounts", + ); +} + +/// Test that `chunk_count` = 0 fails with `InvalidChunkCount` +#[test] +fn test_assemble_zero_chunk_count_rejected() { + let mollusk = setup_mollusk(); + + let chain_id = "test-chain"; + let target_height = 100u64; + let trusted_height = 90u64; + let submitter = Pubkey::new_unique(); + + let client_state_pda = derive_client_state_pda(); + let (consensus_state_pda, _) = Pubkey::find_program_address( + &[ + crate::state::ConsensusStateStore::SEED, + &target_height.to_le_bytes(), + ], + &crate::ID, + ); + + let (access_manager_pda, _) = + solana_ibc_types::access_manager::AccessManager::pda(access_manager::ID); + let (app_state_pda, app_state_account) = create_app_state_account(access_manager::ID); + + let trusted_consensus_pda = derive_consensus_state_pda(trusted_height); + + // Create instruction with chunk_count = 0 + let account_metas = vec![ + AccountMeta::new(client_state_pda, false), + AccountMeta::new_readonly(app_state_pda, false), + AccountMeta::new_readonly(access_manager_pda, false), + AccountMeta::new_readonly(trusted_consensus_pda, false), + AccountMeta::new(consensus_state_pda, false), + AccountMeta::new(submitter, true), + AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(anchor_lang::solana_program::sysvar::instructions::ID, false), + ]; + + let instruction = Instruction { + program_id: crate::ID, + accounts: account_metas, + data: crate::instruction::AssembleAndUpdateClient { + target_height, + chunk_count: 0, // Zero chunks - invalid + trusted_height, + } + .data(), + }; + + let (_, access_manager_account) = + crate::test_helpers::access_control::create_access_manager_account( + submitter, + vec![submitter], + ); + + let mut accounts = vec![ + (app_state_pda, app_state_account), + (access_manager_pda, access_manager_account), + (client_state_pda, create_client_state_account(chain_id, trusted_height)), + ( + trusted_consensus_pda, + create_consensus_state_account([0; 32], [0; 32], 0), + ), + (consensus_state_pda, Account::default()), + (submitter, create_submitter_account(10_000_000_000)), + keyed_account_for_system_program(), + ]; + + // Add Clock sysvar + accounts.push(create_clock_account(0)); + + // Add instructions sysvar + accounts.push(( + anchor_lang::solana_program::sysvar::instructions::ID, + crate::test_helpers::create_instructions_sysvar_account(), + )); + + let result = mollusk.process_instruction(&instruction, &accounts); + + // Should fail immediately with InvalidChunkCount because chunk_count must be > 0 + assert_error_code( + result, + ErrorCode::InvalidChunkCount, + "zero chunks rejected", + ); +} + /// Test that header with invalid cryptographic proof fails during `update_client` /// and triggers `UpdateClientFailed` error #[test] From d04a21fcc028c6cf27284f37776d68b4b17a9520 Mon Sep 17 00:00:00 2001 From: Mariusz Zak Date: Wed, 25 Feb 2026 13:47:31 +0100 Subject: [PATCH 2/2] cargo fmt --- .../assemble_and_update_client/tests.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/programs/solana/programs/ics07-tendermint/src/instructions/assemble_and_update_client/tests.rs b/programs/solana/programs/ics07-tendermint/src/instructions/assemble_and_update_client/tests.rs index 3a988f488..011977f85 100644 --- a/programs/solana/programs/ics07-tendermint/src/instructions/assemble_and_update_client/tests.rs +++ b/programs/solana/programs/ics07-tendermint/src/instructions/assemble_and_update_client/tests.rs @@ -1616,7 +1616,10 @@ fn test_assemble_chunk_count_exceeds_remaining_accounts() { let mut accounts = vec![ (app_state_pda, app_state_account), (access_manager_pda, access_manager_account), - (client_state_pda, create_client_state_account(chain_id, trusted_height)), + ( + client_state_pda, + create_client_state_account(chain_id, trusted_height), + ), ( trusted_consensus_pda, create_consensus_state_account([0; 32], [0; 32], 0), @@ -1707,7 +1710,10 @@ fn test_assemble_zero_chunk_count_rejected() { let mut accounts = vec![ (app_state_pda, app_state_account), (access_manager_pda, access_manager_account), - (client_state_pda, create_client_state_account(chain_id, trusted_height)), + ( + client_state_pda, + create_client_state_account(chain_id, trusted_height), + ), ( trusted_consensus_pda, create_consensus_state_account([0; 32], [0; 32], 0), @@ -1729,11 +1735,7 @@ fn test_assemble_zero_chunk_count_rejected() { let result = mollusk.process_instruction(&instruction, &accounts); // Should fail immediately with InvalidChunkCount because chunk_count must be > 0 - assert_error_code( - result, - ErrorCode::InvalidChunkCount, - "zero chunks rejected", - ); + assert_error_code(result, ErrorCode::InvalidChunkCount, "zero chunks rejected"); } /// Test that header with invalid cryptographic proof fails during `update_client`