From e72f24655443116847ccdee46853dd6d61abc71d Mon Sep 17 00:00:00 2001 From: Leila Wang Date: Fri, 2 Jan 2026 15:42:34 +0000 Subject: [PATCH] Preserve empty items when merging checkpoints. --- .../consecutive_checkpoint_rollups_tests.nr | 104 ++++++ .../src/checkpoint_merge/tests/mod.nr | 2 - .../utils/merge_checkpoint_rollups.nr | 28 +- .../crates/types/src/utils/arrays.nr | 2 + .../src/utils/arrays/copy_items_into_array.nr | 322 ++++++++++++++++++ 5 files changed, 451 insertions(+), 7 deletions(-) create mode 100644 noir-projects/noir-protocol-circuits/crates/types/src/utils/arrays/copy_items_into_array.nr diff --git a/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/checkpoint_merge/tests/consecutive_checkpoint_rollups_tests.nr b/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/checkpoint_merge/tests/consecutive_checkpoint_rollups_tests.nr index f0bf64380bc0..a1f49017c09d 100644 --- a/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/checkpoint_merge/tests/consecutive_checkpoint_rollups_tests.nr +++ b/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/checkpoint_merge/tests/consecutive_checkpoint_rollups_tests.nr @@ -1,4 +1,8 @@ use super::TestBuilder; +use types::{ + abis::fee_recipient::FeeRecipient, address::EthAddress, + constants::CHECKPOINT_MERGE_ROLLUP_VK_INDEX, tests::utils::pad_end, traits::Empty, +}; #[test] fn correct_default_inputs() { @@ -7,6 +11,102 @@ fn correct_default_inputs() { builder.assert_expected_public_inputs(pi); } +// --- fees --- + +fn mock_fee(recipient: Field, value: Field) -> FeeRecipient { + FeeRecipient { recipient: EthAddress::from_field(recipient), value } +} + +#[test] +fn accumulated_fees_from_both_side() { + let mut builder = TestBuilder::new( + CHECKPOINT_MERGE_ROLLUP_VK_INDEX, + 2, + CHECKPOINT_MERGE_ROLLUP_VK_INDEX, + 2, + ); + + let fees = [mock_fee(11, 23), mock_fee(456, 7), mock_fee(8, 90), mock_fee(1234, 56789)]; + + builder.left_rollup.fees[0] = fees[0]; + builder.left_rollup.fees[1] = fees[1]; + builder.right_rollup.fees[0] = fees[2]; + builder.right_rollup.fees[1] = fees[3]; + + let pi = builder.execute(); + builder.assert_expected_public_inputs(pi); + + assert_eq(pi.fees, pad_end(fees)); +} + +#[test] +fn preserve_empty_fees_from_left() { + let mut builder = TestBuilder::new( + CHECKPOINT_MERGE_ROLLUP_VK_INDEX, + 2, + CHECKPOINT_MERGE_ROLLUP_VK_INDEX, + 2, + ); + + let fees = [mock_fee(0, 0), mock_fee(11, 23), mock_fee(456, 7), mock_fee(8, 90)]; + + builder.left_rollup.fees[0] = FeeRecipient::empty(); + builder.left_rollup.fees[1] = fees[1]; + builder.right_rollup.fees[0] = fees[2]; + builder.right_rollup.fees[1] = fees[3]; + + let pi = builder.execute(); + builder.assert_expected_public_inputs(pi); + + assert_eq(pi.fees, pad_end(fees)); +} + +#[test] +fn preserve_empty_fees_from_right() { + let mut builder = TestBuilder::new( + CHECKPOINT_MERGE_ROLLUP_VK_INDEX, + 2, + CHECKPOINT_MERGE_ROLLUP_VK_INDEX, + 2, + ); + + let fees = [mock_fee(11, 23), mock_fee(456, 7), mock_fee(0, 0), mock_fee(8, 90)]; + + builder.left_rollup.fees[0] = fees[0]; + builder.left_rollup.fees[1] = fees[1]; + builder.right_rollup.fees[0] = FeeRecipient::empty(); + builder.right_rollup.fees[1] = fees[3]; + + let pi = builder.execute(); + builder.assert_expected_public_inputs(pi); + + assert_eq(pi.fees, pad_end(fees)); +} + +#[test] +fn preserve_empty_fees_from_both_sides() { + let mut builder = TestBuilder::new( + CHECKPOINT_MERGE_ROLLUP_VK_INDEX, + 2, + CHECKPOINT_MERGE_ROLLUP_VK_INDEX, + 2, + ); + + let fees = [mock_fee(11, 23), mock_fee(0, 0), mock_fee(0, 0), mock_fee(456, 7)]; + + builder.left_rollup.fees[0] = fees[0]; + builder.left_rollup.fees[1] = FeeRecipient::empty(); + builder.right_rollup.fees[0] = FeeRecipient::empty(); + builder.right_rollup.fees[1] = fees[3]; + + let pi = builder.execute(); + builder.assert_expected_public_inputs(pi); + + assert_eq(pi.fees, pad_end(fees)); +} + +// -- constants --- + #[test(should_fail_with = "Mismatched constants in checkpoint rollups")] fn mismatch_chain_id() { let mut builder = TestBuilder::default(); @@ -69,6 +169,8 @@ fn mismatch_prover_id() { builder.execute_and_fail(); } +// --- archives --- + #[test(should_fail_with = "Mismatched archives: expected right.previous_archive to match left.new_archive")] fn non_consecutive_archive_roots() { let mut builder = TestBuilder::default(); @@ -89,6 +191,8 @@ fn non_consecutive_archive_next_available_leaf_indices() { builder.execute_and_fail(); } +// --- blob accumulators --- + #[test(should_fail_with = "Mismatched blob accumulators: expected right.start_blob_accumulator to match left.end_blob_accumulator")] fn non_consecutive_blob_accumulators() { let mut builder = TestBuilder::default(); diff --git a/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/checkpoint_merge/tests/mod.nr b/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/checkpoint_merge/tests/mod.nr index d0d23bc66b97..7be11623191b 100644 --- a/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/checkpoint_merge/tests/mod.nr +++ b/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/checkpoint_merge/tests/mod.nr @@ -109,14 +109,12 @@ impl TestBuilder { expected_header_hashes[i] = left.checkpoint_header_hashes[i]; expected_fees[i] = left.fees[i]; assert(left.checkpoint_header_hashes[i] != 0); - assert(!left.fees[i].is_empty()); } let offset = self.num_left_checkpoints as u32; for i in 0..self.num_right_checkpoints as u32 { expected_header_hashes[i + offset] = right.checkpoint_header_hashes[i]; expected_fees[i + offset] = right.fees[i]; assert(right.checkpoint_header_hashes[i] != 0); - assert(!right.fees[i].is_empty()); } assert_eq(pi.checkpoint_header_hashes, expected_header_hashes); assert_eq(pi.fees, expected_fees); diff --git a/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/checkpoint_merge/utils/merge_checkpoint_rollups.nr b/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/checkpoint_merge/utils/merge_checkpoint_rollups.nr index eecf93ce00e6..40e5013ea456 100644 --- a/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/checkpoint_merge/utils/merge_checkpoint_rollups.nr +++ b/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/checkpoint_merge/utils/merge_checkpoint_rollups.nr @@ -1,25 +1,43 @@ use crate::abis::CheckpointRollupPublicInputs; -use types::{constants::AZTEC_MAX_EPOCH_DURATION, utils::arrays::array_merge}; +use types::{constants::AZTEC_MAX_EPOCH_DURATION, utils::arrays::copy_items_into_array}; pub fn merge_checkpoint_rollups( left: CheckpointRollupPublicInputs, right: CheckpointRollupPublicInputs, ) -> CheckpointRollupPublicInputs { + let num_left_checkpoints = left.num_checkpoints() as u32; + let num_right_checkpoints = right.num_checkpoints() as u32; + // Make sure that the total number of checkpoints does not exceed the maximum allowed in an epoch, preventing the // merged arrays (`checkpoint_header_hashes`, `fees`) from being truncated. - let num_checkpoints = left.num_checkpoints() + right.num_checkpoints(); + let num_checkpoints = num_left_checkpoints + num_right_checkpoints; assert( - num_checkpoints <= AZTEC_MAX_EPOCH_DURATION as u16, + num_checkpoints <= AZTEC_MAX_EPOCH_DURATION, "total number of checkpoints exceeds max allowed in an epoch", ); - let checkpoint_header_hashes = array_merge( + // We use `copy_items_into_array` below because we know exactly how many items should be taken from each side when + // merging checkpoints. + // It is especially critical for `fees` to copy the exact amount of items from both checkpoints. Because the L1 + // contracts will process fees by index, assuming each entry corresponds to the checkpoint at the same position. + // However, the existence of zero fees or recipients is not prohibited by the circuit. Therefore, preserving empty + // values is important. + // Using this helper (instead of helper like `array_merge`) ensures those empty entries are copied over. + + let checkpoint_header_hashes = copy_items_into_array( left.checkpoint_header_hashes, + num_left_checkpoints, right.checkpoint_header_hashes, + num_right_checkpoints, ); // We can't merge fees for the same recipient since the l1 contract iterates per checkpoint. - let fees = array_merge(left.fees, right.fees); + let fees = copy_items_into_array( + left.fees, + num_left_checkpoints, + right.fees, + num_right_checkpoints, + ); CheckpointRollupPublicInputs { constants: left.constants, diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/utils/arrays.nr b/noir-projects/noir-protocol-circuits/crates/types/src/utils/arrays.nr index 03c9d72e5373..f0b08279203c 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/utils/arrays.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/utils/arrays.nr @@ -1,9 +1,11 @@ pub(crate) mod assert_trailing_zeros; +pub(crate) mod copy_items_into_array; pub(crate) mod find_index; pub(crate) mod get_sorted_tuples; // Re-exports. pub use assert_trailing_zeros::assert_trailing_zeros; +pub use copy_items_into_array::copy_items_into_array; pub use find_index::{find_first_index, find_last_index}; pub use get_sorted_tuples::{get_sorted_tuples, SortedTuple}; diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/utils/arrays/copy_items_into_array.nr b/noir-projects/noir-protocol-circuits/crates/types/src/utils/arrays/copy_items_into_array.nr new file mode 100644 index 000000000000..0e7977f2edff --- /dev/null +++ b/noir-projects/noir-protocol-circuits/crates/types/src/utils/arrays/copy_items_into_array.nr @@ -0,0 +1,322 @@ +/// Copies up to `num_items_to_copy` items from `array2` into `array1`, starting at `start_index`, and returns a new +/// array with the modifications. +/// +/// - Fails if writing `array2[i]` into `array1[start_index + i]` would exceed the bounds of `array1`. +/// - Only the first `num_items_to_copy` elements of `array2` are copied. +/// - At most `array2.len()` items from `array2` will be copied. +/// +/// Note: This function may silently stop copying from `array2` once `array2` ends. It's the caller's responsibility to +/// ensure that `num_items_to_copy` is not greater than `array2.len()` if this is desired. +pub fn copy_items_into_array( + array1: [T; N], + start_index: u32, + array2: [T; M], + num_items_to_copy: u32, +) -> [T; N] +where + T: Eq, +{ + // Safety: result is constrained below by `validate_copy_items_into_result`. + let result = + unsafe { copy_items_into_array_helper(array1, start_index, array2, num_items_to_copy) }; + + validate_copy_items_into_result(array1, start_index, array2, num_items_to_copy, result); + + result +} + +/// Validates that `result` is the correct output of copying `num_items_to_copy` items from `array2` +/// into `array1` starting at `start_index`. +/// +/// This function verifies that: +/// - Elements before `start_index` in `result` match `array1` +/// - Elements from `start_index` to `start_index + num_items_to_copy - 1` match the corresponding elements from `array2` +/// - Elements from `start_index + num_items_to_copy` onwards match `array1` +fn validate_copy_items_into_result( + array1: [T; N], + start_index: u32, + array2: [T; M], + num_items_to_copy: u32, + result: [T; N], +) +where + T: Eq, +{ + let end_index = start_index + num_items_to_copy; + // Tracks whether we've reached or passed start_index (false -> true transition at i == start_index) + let mut is_gte_start = false; + // Tracks whether we're still before end_index (true -> false transition at i == end_index) + let mut is_lt_end = true; + for i in 0..N { + is_gte_start |= i == start_index; + is_lt_end &= i != end_index; + // In the copy region (start_index <= i < end_index): expect array2 value + // Outside the copy region: expect original array1 value + let expected = if is_gte_start & is_lt_end { + array2[i - start_index] + } else { + array1[i] + }; + assert_eq( + result[i], + expected, + "Mismatch expected and actual values: copy_items_into_array", + ); + } +} + +/// Unconstrained helper that performs the actual array copy operation. +/// This runs outside the constraint code for efficiency, with the result validated by `validate_copy_items_into_result`. +unconstrained fn copy_items_into_array_helper( + array1: [T; N], + start_index: u32, + array2: [T; M], + num_items_to_copy: u32, +) -> [T; N] { + let mut result: [T; N] = array1; + for i in 0..N { + if i < num_items_to_copy { + result[i + start_index] = array2[i]; + } + } + result +} + +mod tests { + use super::{copy_items_into_array, validate_copy_items_into_result}; + + // --- validate_copy_items_into_result --- + // Tests for validate_copy_items_into_result to verify that tampered results are rejected. + // These tests ensure the validation function correctly catches any modifications to the result array. + + #[test(should_fail_with = "Mismatch expected and actual values: copy_items_into_array")] + fn validate_rejects_tampered_element_before_copy_region() { + let array1 = [1, 2, 3, 4, 5]; + let array2 = [10, 20, 30, 40, 50, 60]; + // Correct result for copying 3 items starting at index 1 would be [1, 10, 20, 30, 5] + // Tamper with element at index 0 (before the copy region) + let tampered_result = [999, 10, 20, 30, 5]; + validate_copy_items_into_result(array1, 1, array2, 3, tampered_result); + } + + #[test(should_fail_with = "Mismatch expected and actual values: copy_items_into_array")] + fn validate_rejects_tampered_element_at_start_boundary() { + let array1 = [1, 2, 3, 4, 5]; + let array2 = [10, 20, 30, 40, 50, 60]; + // Correct result for copying 3 items starting at index 1 would be [1, 10, 20, 30, 5] + // Tamper with element at index 1 (the start of the copy region) + let tampered_result = [1, 999, 20, 30, 5]; + validate_copy_items_into_result(array1, 1, array2, 3, tampered_result); + } + + #[test(should_fail_with = "Mismatch expected and actual values: copy_items_into_array")] + fn validate_rejects_tampered_element_in_copy_region() { + let array1 = [1, 2, 3, 4, 5]; + let array2 = [10, 20, 30, 40, 50, 60]; + // Correct result for copying 3 items starting at index 1 would be [1, 10, 20, 30, 5] + // Tamper with element at index 2 (middle of the copy region) + let tampered_result = [1, 10, 999, 30, 5]; + validate_copy_items_into_result(array1, 1, array2, 3, tampered_result); + } + + #[test(should_fail_with = "Mismatch expected and actual values: copy_items_into_array")] + fn validate_rejects_tampered_element_at_end_boundary() { + let array1 = [1, 2, 3, 4, 5]; + let array2 = [10, 20, 30, 40, 50, 60]; + // Correct result for copying 3 items starting at index 1 would be [1, 10, 20, 30, 5] + // Tamper with element at index 3 (the last element of the copy region) + let tampered_result = [1, 10, 20, 999, 5]; + validate_copy_items_into_result(array1, 1, array2, 3, tampered_result); + } + + #[test(should_fail_with = "Mismatch expected and actual values: copy_items_into_array")] + fn validate_rejects_tampered_element_after_copy_region() { + let array1 = [1, 2, 3, 4, 5]; + let array2 = [10, 20, 30, 40, 50, 60]; + // Correct result for copying 3 items starting at index 1 would be [1, 10, 20, 30, 5] + // Tamper with element at index 4 (after the copy region) + let tampered_result = [1, 10, 20, 30, 999]; + validate_copy_items_into_result(array1, 1, array2, 3, tampered_result); + } + + #[test(should_fail_with = "Mismatch expected and actual values: copy_items_into_array")] + fn validate_rejects_tampered_element_when_copying_from_start() { + let array1 = [1, 2, 3, 4, 5]; + let array2 = [10, 20, 30, 40, 50, 60]; + // Correct result for copying 3 items starting at index 0 would be [10, 20, 30, 4, 5] + // Tamper with the first element (in the copy region) + let tampered_result = [999, 20, 30, 4, 5]; + validate_copy_items_into_result(array1, 0, array2, 3, tampered_result); + } + + #[test(should_fail_with = "Mismatch expected and actual values: copy_items_into_array")] + fn validate_rejects_tampered_element_when_copying_to_end() { + let array1 = [1, 2, 3, 4, 5]; + let array2 = [10, 20, 30, 40, 50, 60]; + // Correct result for copying 3 items starting at index 2 would be [1, 2, 10, 20, 30] + // Tamper with the last element (in the copy region) + let tampered_result = [1, 2, 10, 20, 999]; + validate_copy_items_into_result(array1, 2, array2, 3, tampered_result); + } + + #[test(should_fail_with = "Mismatch expected and actual values: copy_items_into_array")] + fn validate_rejects_completely_wrong_result() { + let array1 = [1, 2, 3, 4, 5]; + let array2 = [10, 20, 30, 40, 50, 60]; + // Correct result for copying 3 items starting at index 1 would be [1, 10, 20, 30, 5] + // Provide a completely incorrect result + let tampered_result = [999, 999, 999, 999, 999]; + validate_copy_items_into_result(array1, 1, array2, 3, tampered_result); + } + + #[test(should_fail_with = "Mismatch expected and actual values: copy_items_into_array")] + fn validate_rejects_swapped_values_in_copy_region() { + let array1 = [1, 2, 3, 4, 5]; + let array2 = [10, 20, 30, 40, 50, 60]; + // Correct result for copying 3 items starting at index 1 would be [1, 10, 20, 30, 5] + // Swap values within the copy region + let tampered_result = [1, 20, 10, 30, 5]; + validate_copy_items_into_result(array1, 1, array2, 3, tampered_result); + } + + #[test(should_fail_with = "Mismatch expected and actual values: copy_items_into_array")] + fn validate_rejects_when_copy_zero_but_result_differs() { + let array1 = [1, 2, 3, 4, 5]; + let array2 = [10, 20, 30, 40, 50, 60]; + // When copying 0 items, the result should be identical to array1 + let tampered_result = [1, 2, 999, 4, 5]; + validate_copy_items_into_result(array1, 0, array2, 0, tampered_result); + } + + // --- copy_items_into_array --- + + #[test] + fn copy_to_fill_from_start_to_middle() { + let array1 = [1, 2, 3, 4, 5]; + let array2 = [10, 20, 30, 40, 50, 60]; + // Copy 3 items from array2, starting at index 0 in array1. + let result = copy_items_into_array(array1, 0, array2, 3); + assert_eq(result, [10, 20, 30, 4, 5]); + } + + #[test] + fn copy_to_fill_from_start_to_end() { + let array1 = [1, 2, 3, 4, 5]; + let array2 = [10, 20, 30, 40, 50, 60]; + // Copy 5 items from array2, starting at index 0 in array1. + let result = copy_items_into_array(array1, 0, array2, 5); + assert_eq(result, [10, 20, 30, 40, 50]); + } + + #[test] + fn copy_to_fill_in_middle() { + let array1 = [1, 2, 3, 4, 5]; + let array2 = [10, 20, 30, 40, 50, 60]; + // Copy 3 items from array2, starting at index 1 in array1. + let result = copy_items_into_array(array1, 1, array2, 3); + assert_eq(result, [1, 10, 20, 30, 5]); + } + + #[test] + fn copy_to_fill_from_middle_to_end() { + let array1 = [1, 2, 3, 4, 5]; + let array2 = [10, 20, 30, 40, 50, 60]; + // Copy 3 items from array2, starting at index 2 in array1. + let result = copy_items_into_array(array1, 2, array2, 3); + assert_eq(result, [1, 2, 10, 20, 30]); + } + + #[test] + fn copy_to_overfill_from_start() { + let array1 = [1, 2, 3, 4, 5]; + let array2 = [10, 20, 30, 40, 50, 60]; + // Copy 6 items from array2. Only 5 items from array2 are actually copied. + assert_eq(copy_items_into_array(array1, 0, array2, 6), [10, 20, 30, 40, 50]); + } + + #[test(should_fail_with = "Index out of bounds")] + fn copy_to_overfill_from_middle() { + let array1 = [1, 2, 3, 4, 5]; + let array2 = [10, 20, 30, 40, 50, 60]; + // Copy 3 items from array2, starting at index 3 in array1. + let _ = copy_items_into_array(array1, 3, array2, 3); + } + + #[test] + fn validate_result_copy_to_overfill_from_middle() { + let array1 = [1, 2, 3, 4, 5]; + let array2 = [10, 20, 30, 40, 50, 60]; + // Same as the above test, but avoid calling the unconstrained function and have the result validated directly. + // Copy 3 items from array2, starting at index 3 in array1. + // At most 2 items from array2 are copied. + let result = [1, 2, 3, 10, 20]; + validate_copy_items_into_result(array1, 3, array2, 3, result); + } + + #[test(should_fail_with = "Index out of bounds")] + fn copy_to_fill_from_out_of_bounds() { + let array1 = [1, 2, 3, 4, 5]; + let array2 = [10, 20, 30, 40, 50, 60]; + // Copy 3 items from array2, starting at index beyond the size of array1. + let _ = copy_items_into_array(array1, array1.len(), array2, 3); + } + + #[test] + fn validate_result_copy_to_fill_from_out_of_bounds() { + let array1 = [1, 2, 3, 4, 5]; + let array2 = [10, 20, 30, 40, 50, 60]; + // Same as the above test, but avoid calling the unconstrained function and have the result validated directly. + // Copy 3 items from array2, starting at index beyond the size of array1. + // Nothing from array2 is copied. + let result = [1, 2, 3, 4, 5]; + validate_copy_items_into_result(array1, array1.len(), array2, 3, result); + } + + #[test] + fn copy_nothing() { + let array1 = [1, 2, 3, 4, 5]; + let array2 = [10, 20, 30, 40, 50, 60]; + // Copy 0 items from array2. + let result = copy_items_into_array(array1, 0, array2, 0); + assert_eq(result, [1, 2, 3, 4, 5]); + } + + #[test] + fn copy_nothing_to_fill_from_out_of_bounds() { + let array1 = [1, 2, 3, 4, 5]; + let array2 = [10, 20, 30, 40, 50, 60]; + // Copy 0 items from array2, starting at index beyond the size of array1. + let result = copy_items_into_array(array1, array1.len(), array2, 0); + // It didn't fail. Nothing was copied. + assert_eq(result, [1, 2, 3, 4, 5]); + } + + #[test] + fn copy_more_than_array2_length() { + let array1 = [1, 2, 3, 4, 5]; + let array2 = [10, 20, 30, 40, 50]; + // Copy more items than array2 has. + let result = copy_items_into_array(array1, 0, array2, array2.len() + 1); + // It didn't fail. All items in array2 were copied. + assert_eq(result, [10, 20, 30, 40, 50]); + } + + #[test(should_fail_with = "Index out of bounds")] + fn copy_more_than_array2_length_smaller_than_array1_length() { + let array1 = [1, 2, 3, 4, 5]; + let array2 = [10, 20, 30]; + // Similar to the above test, copying more items than array2 has. + // But this time, the length of array2 is smaller than the length of array1. + let _ = copy_items_into_array(array1, 0, array2, array2.len() + 1); + } + + #[test(should_fail_with = "Index out of bounds")] + fn validate_result_copy_more_than_array2_length_smaller_than_array1_length() { + let array1 = [1, 2, 3, 4, 5]; + let array2 = [10, 20, 30]; + // Same as the above test, but avoid calling the unconstrained function and have the result validated directly. + // The index out of bound error is still thrown from validating the result. + let result = [10, 20, 30, 4, 5]; + validate_copy_items_into_result(array1, 0, array2, array2.len() + 1, result); + } +}