From adc8d1c28d03aeda4125264b065b7e4759b87839 Mon Sep 17 00:00:00 2001 From: Michal Rostecki Date: Wed, 30 Jul 2025 18:06:22 +0200 Subject: [PATCH] fix: Enforce input length to match the modulus To avoid misunderstandings related to padding in Poseidon hash inputs, enforce them to have size 32, which is the number of bytes in `Fr` modulus. --- .../batched-merkle-tree/tests/merkle_tree.rs | 7 ++-- .../src/compressed_account.rs | 4 +-- program-libs/hasher/src/poseidon.rs | 13 ++++---- program-libs/hasher/src/to_byte_array.rs | 2 ++ program-libs/indexed-merkle-tree/src/array.rs | 9 +++--- .../indexed-merkle-tree/tests/tests.rs | 12 +++---- .../src/create_pda.rs | 4 ++- program-tests/merkle-tree/tests/indexed.rs | 12 ++++--- .../system-cpi-test/src/create_pda.rs | 5 ++- .../utils/src/mock_batched_forester.rs | 6 ++-- .../utils/src/test_batch_forester.rs | 7 ++-- .../proof_types/batch_update/proof_inputs.rs | 6 ++-- prover/client/tests/batch_update.rs | 8 +++-- sdk-libs/macros/tests/hasher.rs | 32 +++++++++---------- 14 files changed, 71 insertions(+), 56 deletions(-) diff --git a/program-libs/batched-merkle-tree/tests/merkle_tree.rs b/program-libs/batched-merkle-tree/tests/merkle_tree.rs index 47a77ff87a..f19779f5c2 100644 --- a/program-libs/batched-merkle-tree/tests/merkle_tree.rs +++ b/program-libs/batched-merkle-tree/tests/merkle_tree.rs @@ -36,7 +36,7 @@ use light_compressed_account::{ hash_chain::create_hash_chain_from_slice, instruction_data::compressed_proof::CompressedProof, pubkey::Pubkey, }; -use light_hasher::{Hasher, Poseidon}; +use light_hasher::{to_byte_array::ToByteArray, Hasher, Poseidon}; use light_merkle_tree_reference::MerkleTree; use light_prover_client::prover::{spawn_prover, ProverConfig}; use light_test_utils::mock_batched_forester::{ @@ -62,9 +62,8 @@ pub fn assert_nullifier_queue_insert( ) -> Result<(), BatchedMerkleTreeError> { let mut leaf_hash_chain_insert_values = vec![]; for (insert_value, leaf_index) in bloom_filter_insert_values.iter().zip(leaf_indices.iter()) { - let nullifier = - Poseidon::hashv(&[insert_value.as_slice(), &leaf_index.to_be_bytes(), &tx_hash]) - .unwrap(); + let leaf_index = leaf_index.to_byte_array().unwrap(); + let nullifier = Poseidon::hashv(&[insert_value.as_slice(), &leaf_index, &tx_hash]).unwrap(); leaf_hash_chain_insert_values.push(nullifier); } assert_input_queue_insert( diff --git a/program-libs/compressed-account/src/compressed_account.rs b/program-libs/compressed-account/src/compressed_account.rs index 8e2cfc13c5..763c26af02 100644 --- a/program-libs/compressed-account/src/compressed_account.rs +++ b/program-libs/compressed-account/src/compressed_account.rs @@ -378,7 +378,7 @@ impl ZCompressedAccount<'_> { #[cfg(not(feature = "pinocchio"))] #[cfg(test)] mod tests { - use light_hasher::Poseidon; + use light_hasher::{to_byte_array::ToByteArray, Poseidon}; use light_zero_copy::traits::ZeroCopyAt; use num_bigint::BigUint; use rand::Rng; @@ -750,7 +750,7 @@ mod tests { Some(CompressedAccountData { discriminator: rng.gen(), data: Vec::new(), // not used in hash - data_hash: Poseidon::hash(rng.gen::().to_be_bytes().as_slice()) + data_hash: Poseidon::hash(&rng.gen::().to_byte_array().unwrap()) .unwrap(), }) } else { diff --git a/program-libs/hasher/src/poseidon.rs b/program-libs/hasher/src/poseidon.rs index 0cd6c670da..baa0d442f0 100644 --- a/program-libs/hasher/src/poseidon.rs +++ b/program-libs/hasher/src/poseidon.rs @@ -83,6 +83,12 @@ impl Hasher for Poseidon { } fn hashv(vals: &[&[u8]]) -> Result { + for val in vals { + if val.len() != 32 { + return Err(HasherError::InvalidInputLength(32, val.len())); + } + } + // Perform the calculation inline, calling this from within a program is // not supported. #[cfg(not(target_os = "solana"))] @@ -99,13 +105,6 @@ impl Hasher for Poseidon { #[cfg(target_os = "solana")] { use crate::HASH_BYTES; - // TODO: reenable once LightHasher refactor is merged - // solana_program::msg!("remove len check onchain."); - // for val in vals { - // if val.len() != 32 { - // return Err(HasherError::InvalidInputLength(val.len())); - // } - // } let mut hash_result = [0; HASH_BYTES]; let result = unsafe { crate::syscalls::sol_poseidon( diff --git a/program-libs/hasher/src/to_byte_array.rs b/program-libs/hasher/src/to_byte_array.rs index ac56df5d2d..21ce3d5819 100644 --- a/program-libs/hasher/src/to_byte_array.rs +++ b/program-libs/hasher/src/to_byte_array.rs @@ -67,6 +67,8 @@ impl_to_byte_array_for_integer_type!(i32); impl_to_byte_array_for_integer_type!(u32); impl_to_byte_array_for_integer_type!(i64); impl_to_byte_array_for_integer_type!(u64); +impl_to_byte_array_for_integer_type!(isize); +impl_to_byte_array_for_integer_type!(usize); impl_to_byte_array_for_integer_type!(i128); impl_to_byte_array_for_integer_type!(u128); diff --git a/program-libs/indexed-merkle-tree/src/array.rs b/program-libs/indexed-merkle-tree/src/array.rs index 387a2ae6ca..f1cca388db 100644 --- a/program-libs/indexed-merkle-tree/src/array.rs +++ b/program-libs/indexed-merkle-tree/src/array.rs @@ -463,6 +463,7 @@ where #[cfg(test)] mod test { use light_concurrent_merkle_tree::light_hasher::Poseidon; + use light_hasher::to_byte_array::ToByteArray; use num_bigint::{RandBigInt, ToBigUint}; use rand::thread_rng; @@ -561,7 +562,7 @@ mod test { bigint_to_be_bytes_array::<32>(&nullifier1) .unwrap() .as_ref(), - 0_usize.to_be_bytes().as_ref(), + 0_usize.to_byte_array().unwrap().as_ref(), bigint_to_be_bytes_array::<32>(&(0.to_biguint().unwrap())) .unwrap() .as_ref(), @@ -631,7 +632,7 @@ mod test { bigint_to_be_bytes_array::<32>(&nullifier2) .unwrap() .as_ref(), - 1_usize.to_be_bytes().as_ref(), + 1_usize.to_byte_array().unwrap().as_ref(), bigint_to_be_bytes_array::<32>(&(30.to_biguint().unwrap())) .unwrap() .as_ref(), @@ -711,7 +712,7 @@ mod test { bigint_to_be_bytes_array::<32>(&nullifier3) .unwrap() .as_ref(), - 1_usize.to_be_bytes().as_ref(), + 1_usize.to_byte_array().unwrap().as_ref(), bigint_to_be_bytes_array::<32>(&(30.to_biguint().unwrap())) .unwrap() .as_ref(), @@ -806,7 +807,7 @@ mod test { bigint_to_be_bytes_array::<32>(&nullifier4) .unwrap() .as_ref(), - 0_usize.to_be_bytes().as_ref(), + 0_usize.to_byte_array().unwrap().as_ref(), bigint_to_be_bytes_array::<32>(&(0.to_biguint().unwrap())) .unwrap() .as_ref(), diff --git a/program-libs/indexed-merkle-tree/tests/tests.rs b/program-libs/indexed-merkle-tree/tests/tests.rs index cf18deee24..d2586597bf 100644 --- a/program-libs/indexed-merkle-tree/tests/tests.rs +++ b/program-libs/indexed-merkle-tree/tests/tests.rs @@ -639,18 +639,18 @@ pub fn functional_non_inclusion_test() { assert_eq!( leaf_0, Poseidon::hashv(&[ - &0_u32.to_biguint().unwrap().to_bytes_be(), - &1_u32.to_biguint().unwrap().to_bytes_be(), - &30_u32.to_biguint().unwrap().to_bytes_be() + &bigint_to_be_bytes_array::<32>(&0_u32.to_biguint().unwrap()).unwrap(), + &bigint_to_be_bytes_array::<32>(&1_u32.to_biguint().unwrap()).unwrap(), + &bigint_to_be_bytes_array::<32>(&30_u32.to_biguint().unwrap()).unwrap() ]) .unwrap() ); assert_eq!( leaf_1, Poseidon::hashv(&[ - &30_u32.to_biguint().unwrap().to_bytes_be(), - &0_u32.to_biguint().unwrap().to_bytes_be(), - &0_u32.to_biguint().unwrap().to_bytes_be() + &bigint_to_be_bytes_array::<32>(&30_u32.to_biguint().unwrap()).unwrap(), + &bigint_to_be_bytes_array::<32>(&0_u32.to_biguint().unwrap()).unwrap(), + &bigint_to_be_bytes_array::<32>(&0_u32.to_biguint().unwrap()).unwrap() ]) .unwrap() ); diff --git a/program-tests/create-address-test-program/src/create_pda.rs b/program-tests/create-address-test-program/src/create_pda.rs index eceefaceeb..f29460fdbb 100644 --- a/program-tests/create-address-test-program/src/create_pda.rs +++ b/program-tests/create-address-test-program/src/create_pda.rs @@ -133,8 +133,10 @@ pub struct RegisteredUser { impl light_hasher::DataHasher for RegisteredUser { fn hash(&self) -> std::result::Result<[u8; 32], HasherError> { let truncated_user_pubkey = hash_to_bn254_field_size_be(&self.user_pubkey.to_bytes()); + let mut data = [0u8; 32]; + data[1..].copy_from_slice(&self.data); - H::hashv(&[truncated_user_pubkey.as_slice(), self.data.as_slice()]) + H::hashv(&[truncated_user_pubkey.as_slice(), data.as_slice()]) } } diff --git a/program-tests/merkle-tree/tests/indexed.rs b/program-tests/merkle-tree/tests/indexed.rs index 1c57bf78aa..8a45a96c4b 100644 --- a/program-tests/merkle-tree/tests/indexed.rs +++ b/program-tests/merkle-tree/tests/indexed.rs @@ -1,4 +1,6 @@ -use light_hasher::{bigint::bigint_to_be_bytes_array, Hasher, Poseidon}; +use light_hasher::{ + bigint::bigint_to_be_bytes_array, to_byte_array::ToByteArray, Hasher, Poseidon, +}; use light_merkle_tree_reference::indexed::IndexedMerkleTree; use num_bigint::ToBigUint; @@ -38,16 +40,16 @@ pub fn functional_non_inclusion_test() { assert_eq!( leaf_0, Poseidon::hashv(&[ - &0_u32.to_biguint().unwrap().to_bytes_be(), - &30_u32.to_biguint().unwrap().to_bytes_be() + &0_u32.to_byte_array().unwrap(), + &30_u32.to_byte_array().unwrap(), ]) .unwrap() ); assert_eq!( leaf_1, Poseidon::hashv(&[ - &30_u32.to_biguint().unwrap().to_bytes_be(), - &0_u32.to_biguint().unwrap().to_bytes_be() + &30_u32.to_byte_array().unwrap(), + &0_u32.to_byte_array().unwrap(), ]) .unwrap() ); diff --git a/program-tests/system-cpi-test/src/create_pda.rs b/program-tests/system-cpi-test/src/create_pda.rs index 5cf14b98ac..545cb1b086 100644 --- a/program-tests/system-cpi-test/src/create_pda.rs +++ b/program-tests/system-cpi-test/src/create_pda.rs @@ -543,7 +543,10 @@ pub struct RegisteredUser { impl light_hasher::DataHasher for RegisteredUser { fn hash(&self) -> std::result::Result<[u8; 32], HasherError> { let truncated_user_pubkey = hash_to_bn254_field_size_be(&self.user_pubkey.to_bytes()); - H::hashv(&[truncated_user_pubkey.as_slice(), self.data.as_slice()]) + let mut data = [0u8; 32]; + data[1..].copy_from_slice(&self.data); + + H::hashv(&[truncated_user_pubkey.as_slice(), data.as_slice()]) } } diff --git a/program-tests/utils/src/mock_batched_forester.rs b/program-tests/utils/src/mock_batched_forester.rs index 990f912d6c..dd42319571 100644 --- a/program-tests/utils/src/mock_batched_forester.rs +++ b/program-tests/utils/src/mock_batched_forester.rs @@ -1,7 +1,9 @@ use light_compressed_account::{ hash_chain::create_hash_chain_from_slice, instruction_data::compressed_proof::CompressedProof, }; -use light_hasher::{bigint::bigint_to_be_bytes_array, Hasher, Poseidon}; +use light_hasher::{ + bigint::bigint_to_be_bytes_array, to_byte_array::ToByteArray, Hasher, Poseidon, +}; use light_merkle_tree_reference::{indexed::IndexedMerkleTree, MerkleTree}; use light_prover_client::{ errors::ProverClientError, @@ -186,7 +188,7 @@ impl MockBatchedForester { .iter() .find(|tx_event| tx_event.inputs.contains(leaf)) .expect("No event for leaf found."); - let index_bytes = index.to_be_bytes(); + let index_bytes = index.to_byte_array().unwrap(); let nullifier = Poseidon::hashv(&[leaf, &index_bytes, &event.tx_hash]).unwrap(); tx_hashes.push(event.tx_hash); nullifiers.push(nullifier); diff --git a/program-tests/utils/src/test_batch_forester.rs b/program-tests/utils/src/test_batch_forester.rs index 44122fefd6..781ce143b7 100644 --- a/program-tests/utils/src/test_batch_forester.rs +++ b/program-tests/utils/src/test_batch_forester.rs @@ -25,7 +25,9 @@ use light_compressed_account::{ hash_chain::create_hash_chain_from_slice, instruction_data::compressed_proof::CompressedProof, QueueType, }; -use light_hasher::{bigint::bigint_to_be_bytes_array, Poseidon}; +use light_hasher::{ + bigint::bigint_to_be_bytes_array, to_byte_array::ToByteArray, Hasher, Poseidon, +}; use light_prover_client::{ proof_client::ProofClient, proof_types::{ @@ -269,8 +271,7 @@ pub async fn get_batched_nullify_ix_data( let proof = bundle.merkle_tree.get_proof_of_leaf(index, true).unwrap(); merkle_proofs.push(proof.to_vec()); bundle.input_leaf_indices.remove(0); - let index_bytes = index.to_be_bytes(); - use light_hasher::Hasher; + let index_bytes = index.to_byte_array().unwrap(); let nullifier = Poseidon::hashv(&[&leaf, &index_bytes, &leaf_info.tx_hash]).unwrap(); tx_hashes.push(leaf_info.tx_hash); 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 d376fd0da2..0502b362dd 100644 --- a/prover/client/src/proof_types/batch_update/proof_inputs.rs +++ b/prover/client/src/proof_types/batch_update/proof_inputs.rs @@ -1,4 +1,6 @@ -use light_hasher::{hash_chain::create_hash_chain_from_array, Hasher, Poseidon}; +use light_hasher::{ + hash_chain::create_hash_chain_from_array, to_byte_array::ToByteArray, Hasher, Poseidon, +}; use light_sparse_merkle_tree::changelog::ChangelogEntry; use num_bigint::{BigInt, Sign}; @@ -91,7 +93,7 @@ pub fn get_batch_update_inputs( let merkle_proof_array = merkle_proof.try_into().unwrap(); // Use the adjusted index bytes for computing the nullifier. - let index_bytes = (*index).to_be_bytes(); + let index_bytes = index.to_byte_array().unwrap(); let nullifier = Poseidon::hashv(&[leaf, &index_bytes, &tx_hashes[i]]).unwrap(); let (root, changelog_entry) = compute_root_from_merkle_proof(nullifier, &merkle_proof_array, *index); diff --git a/prover/client/tests/batch_update.rs b/prover/client/tests/batch_update.rs index 90efc5132a..6ea9c879b2 100644 --- a/prover/client/tests/batch_update.rs +++ b/prover/client/tests/batch_update.rs @@ -1,4 +1,6 @@ -use light_hasher::{hash_chain::create_hash_chain_from_slice, Hasher, Poseidon}; +use light_hasher::{ + hash_chain::create_hash_chain_from_slice, to_byte_array::ToByteArray, Hasher, Poseidon, +}; use light_merkle_tree_reference::MerkleTree; use light_prover_client::{ constants::{DEFAULT_BATCH_STATE_TREE_HEIGHT, PROVE_PATH, SERVER_ADDRESS}, @@ -31,9 +33,9 @@ async fn prove_batch_update() { old_leaves.push(leaf); merkle_tree.append(&leaf).unwrap(); + let index_bytes = (i as usize).to_byte_array().unwrap(); #[allow(clippy::unnecessary_cast)] - let nullifier = - Poseidon::hashv(&[&leaf, &(i as usize).to_be_bytes(), &tx_hash]).unwrap(); + let nullifier = Poseidon::hashv(&[&leaf, &index_bytes, &tx_hash]).unwrap(); nullifiers.push(nullifier); } diff --git a/sdk-libs/macros/tests/hasher.rs b/sdk-libs/macros/tests/hasher.rs index 4b58c57ab7..5117dbb033 100644 --- a/sdk-libs/macros/tests/hasher.rs +++ b/sdk-libs/macros/tests/hasher.rs @@ -141,8 +141,8 @@ mod basic_hashing { let account = create_account(Some(42)); let manual_nested_bytes: Vec> = vec![ - nested_struct.a.to_be_bytes().to_vec(), - nested_struct.b.to_be_bytes().to_vec(), + nested_struct.a.to_byte_array().unwrap().to_vec(), + nested_struct.b.to_byte_array().unwrap().to_vec(), light_compressed_account::hash_to_bn254_field_size_be( nested_struct.c.try_to_vec().unwrap().as_slice(), ) @@ -163,8 +163,8 @@ mod basic_hashing { assert_eq!(nested_hash_result, manual_nested_hash); let manual_account_bytes: Vec> = vec![ - vec![u8::from(account.a)], - account.b.to_be_bytes().to_vec(), + account.a.to_byte_array().unwrap().to_vec(), + account.b.to_byte_array().unwrap().to_vec(), account.c.hash::().unwrap().to_vec(), light_compressed_account::hash_to_bn254_field_size_be(&account.d).to_vec(), { @@ -495,18 +495,18 @@ fn test_poseidon_width_limits() { assert!(max_fields.hash::().is_ok()); let expected_hash = Poseidon::hashv(&[ - 1u64.to_be_bytes().as_ref(), - 2u64.to_be_bytes().as_ref(), - 3u64.to_be_bytes().as_ref(), - 4u64.to_be_bytes().as_ref(), - 5u64.to_be_bytes().as_ref(), - 6u64.to_be_bytes().as_ref(), - 7u64.to_be_bytes().as_ref(), - 8u64.to_be_bytes().as_ref(), - 9u64.to_be_bytes().as_ref(), - 10u64.to_be_bytes().as_ref(), - 11u64.to_be_bytes().as_ref(), - 12u64.to_be_bytes().as_ref(), + 1u64.to_byte_array().unwrap().as_ref(), + 2u64.to_byte_array().unwrap().as_ref(), + 3u64.to_byte_array().unwrap().as_ref(), + 4u64.to_byte_array().unwrap().as_ref(), + 5u64.to_byte_array().unwrap().as_ref(), + 6u64.to_byte_array().unwrap().as_ref(), + 7u64.to_byte_array().unwrap().as_ref(), + 8u64.to_byte_array().unwrap().as_ref(), + 9u64.to_byte_array().unwrap().as_ref(), + 10u64.to_byte_array().unwrap().as_ref(), + 11u64.to_byte_array().unwrap().as_ref(), + 12u64.to_byte_array().unwrap().as_ref(), ]) .unwrap(); assert_eq!(max_fields.hash::().unwrap(), expected_hash);