Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ jobs:
aligned-sized light-hasher light-compressed-account light-account-checks \
light-verifier light-merkle-tree-metadata light-zero-copy light-hash-set
test_cmd: |
cargo test -p light-macros
cargo test -p aligned-sized
cargo test -p light-hasher --all-features
cargo test -p light-compressed-account --all-features
Expand Down
6 changes: 4 additions & 2 deletions .github/workflows/sdk-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,12 @@ jobs:
- program: sdk-anchor-test-program
sub-tests: '["cargo-test-sbf -p sdk-anchor-test", "cargo-test-sbf -p sdk-pinocchio-test"]'
- program: sdk-libs
packages: light-macros light-sdk light-program-test light-client light-batched-merkle-tree
packages: light-sdk-macros light-sdk light-program-test light-client light-batched-merkle-tree
test_cmd: |
cargo test -p light-macros
cargo test -p light-sdk-macros
cargo test -p light-sdk-macros --all-features
cargo test -p light-sdk
cargo test -p light-sdk --all-features
cargo test -p light-program-test
cargo test -p light-client
cargo test -p client-test
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions program-libs/hasher/src/keccak.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ use crate::{
pub struct Keccak;

impl Hasher for Keccak {
const ID: u8 = 2;

fn hash(val: &[u8]) -> Result<Hash, HasherError> {
Self::hashv(&[val])
}
Expand Down
1 change: 1 addition & 0 deletions program-libs/hasher/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ pub const HASH_BYTES: usize = 32;
pub type Hash = [u8; HASH_BYTES];

pub trait Hasher {
const ID: u8;
fn hash(val: &[u8]) -> Result<Hash, HasherError>;
fn hashv(vals: &[&[u8]]) -> Result<Hash, HasherError>;
fn zero_bytes() -> ZeroBytes;
Expand Down
2 changes: 2 additions & 0 deletions program-libs/hasher/src/poseidon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ impl From<PoseidonSyscallError> for u64 {
pub struct Poseidon;

impl Hasher for Poseidon {
const ID: u8 = 0;

fn hash(val: &[u8]) -> Result<Hash, HasherError> {
Self::hashv(&[val])
}
Expand Down
12 changes: 12 additions & 0 deletions program-libs/hasher/src/sha256.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,22 @@ use crate::{
Hash, Hasher,
};

/// Compile-time assertion trait that ensures a generic Hasher type is SHA256.
/// Used by LightHasherSha macro to enforce SHA256-only implementation at compile time.
pub trait RequireSha256: Hasher {
const ASSERT: () = assert!(
Self::ID == 1,
"DataHasher for LightHasherSha only works with SHA256 (ID=1). Example: your_struct.hash::<Sha256>()?"
);
}

impl<T: Hasher> RequireSha256 for T {}

Comment on lines +8 to +18
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

❓ Verification inconclusive

Compile-time gating via RequireSha256 is brittle; blanket impl negates the constraint.

With impl<T: Hasher> RequireSha256 for T {}, any Hasher satisfies the bound unless the associated const is actually referenced. The assert! in an associated const may also depend on MSRV/const‑panic semantics and won’t fire unless evaluated. Prefer a simple marker trait implemented only for Sha256, or a const‑generic assertion pattern.

Minimal, robust fix:

-pub trait RequireSha256: Hasher {
-    const ASSERT: () = assert!(
-        Self::ID == 1,
-        "DataHasher for LightHasherSha only works with SHA256 (ID=1). Example: your_struct.hash::<Sha256>()?"
-    );
-}
-
-impl<T: Hasher> RequireSha256 for T {}
+pub trait RequireSha256: Hasher {}
+impl RequireSha256 for Sha256 {}

If you need a generic proof, use a const‑generic helper type instead of assert! in a const.


Replace brittle RequireSha256 implementation with a marker trait
In program-libs/hasher/src/sha256.rs remove the const ASSERT and blanket impl, and instead use:

pub trait RequireSha256: Hasher {}
impl RequireSha256 for Sha256 {}
🤖 Prompt for AI Agents
In program-libs/hasher/src/sha256.rs around lines 8-18, replace the current
RequireSha256 trait that uses a const ASSERT plus a blanket impl (which is
brittle) with a simple marker trait and a concrete impl: remove the const ASSERT
and the impl<T: Hasher> RequireSha256 for T {}, declare pub trait RequireSha256:
Hasher {} and add impl RequireSha256 for Sha256 {} (ensure Sha256 is
imported/visible in this module and update any call sites expecting the old
ASSERT behavior).

#[derive(Clone, Copy)] // To allow using with zero copy Solana accounts.
pub struct Sha256;

impl Hasher for Sha256 {
const ID: u8 = 1;
fn hash(val: &[u8]) -> Result<Hash, HasherError> {
Self::hashv(&[val])
}
Expand Down
5 changes: 5 additions & 0 deletions sdk-libs/macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ repository = "https://github.com/Lightprotocol/light-protocol"
license = "Apache-2.0"
edition = "2021"


[features]
anchor-discriminator = []

[dependencies]
proc-macro2 = { workspace = true }
quote = { workspace = true }
Expand All @@ -22,6 +26,7 @@ prettyplease = "0.2.29"
solana-pubkey = { workspace = true, features = ["borsh"] }
borsh = { workspace = true }
light-macros = { workspace = true }
light-account-checks = { workspace = true }

[lib]
proc-macro = true
16 changes: 12 additions & 4 deletions sdk-libs/macros/src/discriminator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,17 @@ use syn::{ItemStruct, Result};

pub(crate) fn discriminator(input: ItemStruct) -> Result<TokenStream> {
let account_name = &input.ident;
// When anchor-discriminator-compat feature is enabled, use "account:" prefix like Anchor does
#[cfg(feature = "anchor-discriminator")]
let hash_input = format!("account:{}", account_name);

#[cfg(not(feature = "anchor-discriminator"))]
let hash_input = account_name.to_string();

let (impl_gen, type_gen, where_clause) = input.generics.split_for_impl();

let mut discriminator = [0u8; 8];
discriminator.copy_from_slice(&Sha256::hash(account_name.to_string().as_bytes()).unwrap()[..8]);
discriminator.copy_from_slice(&Sha256::hash(hash_input.as_bytes()).unwrap()[..8]);
let discriminator: proc_macro2::TokenStream = format!("{discriminator:?}").parse().unwrap();

Ok(quote! {
Expand All @@ -26,12 +32,14 @@ pub(crate) fn discriminator(input: ItemStruct) -> Result<TokenStream> {

#[cfg(test)]
mod tests {
use syn::parse_quote;

use super::*;

#[cfg(not(feature = "anchor-discriminator"))]
#[test]
fn test_discriminator() {
use syn::parse_quote;

use super::*;

let input: ItemStruct = parse_quote! {
struct MyAccount {
a: u32,
Expand Down
53 changes: 50 additions & 3 deletions sdk-libs/macros/src/hasher/data_hasher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,14 @@ pub(crate) fn generate_data_hasher_impl(
slices[num_flattned_fields] = element.as_slice();
}

H::hashv(slices.as_slice())
let mut result = H::hashv(slices.as_slice())?;

// Apply field size truncation for non-Poseidon hashers
if H::ID != ::light_hasher::Poseidon::ID {
result[0] = 0;
}

Ok(result)
}
Comment on lines +40 to 48
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Potentially incorrect slices population before hashing.

hashv(slices.as_slice()) assumes slices are fully populated. In the surrounding code, slices are filled via a loop using a constant index (num_flattned_fields) that never increments, so only index 0 is set repeatedly and others remain empty. This will corrupt inputs for flattened structs.

Suggested fix outside this hunk: populate slices using enumerate and remove the unused counter.

for (i, element) in field_array.iter().enumerate() {
    slices[i] = element.as_slice();
}

Also consider renaming num_flattned_fields -> num_flattened_fields (and removing it if unused).

Search for the current population pattern to patch all occurrences:


🏁 Script executed:

#!/bin/bash
rg -n 'for element in field_array\.iter\(\) \{[^}]*slices\[[^\]]+\]\s*=\s*element\.as_slice\(\);[^}]*\}' sdk-libs/macros/src/hasher

Length of output: 0


🏁 Script executed:

#!/bin/bash
sed -n '1,200p' sdk-libs/macros/src/hasher/data_hasher.rs

Length of output: 4363


Use enumerate() to properly populate slices and correct the index variable
In sdk-libs/macros/src/hasher/data_hasher.rs the loop

let mut num_flattned_fields = 0;
for element in field_array.iter() {
    slices[num_flattned_fields] = element.as_slice();
}

never increments the counter, so only slices[0] is set and the rest remain empty. Replace with:

for (i, element) in field_array.iter().enumerate() {
    slices[i] = element.as_slice();
}

and rename num_flattned_fieldsnum_flattened_fields (or remove it if unused).

🤖 Prompt for AI Agents
In sdk-libs/macros/src/hasher/data_hasher.rs around lines 40 to 48, the loop
that fills the slices array never increments its index variable so only
slices[0] is set; replace the loop with an enumerate-based iterator (for (i,
element) in field_array.iter().enumerate() { slices[i] = element.as_slice(); })
to correctly populate each slot, and rename num_flattned_fields to
num_flattened_fields (or remove that variable entirely if it's no longer used).

}
}
Expand All @@ -59,13 +66,53 @@ pub(crate) fn generate_data_hasher_impl(
println!("DataHasher::hash inputs {:?}", debug_prints);
}
}
H::hashv(&[
let mut result = H::hashv(&[
#(#data_hasher_assignments.as_slice(),)*
])
])?;

// Apply field size truncation for non-Poseidon hashers
if H::ID != ::light_hasher::Poseidon::ID {
result[0] = 0;
}

Ok(result)
}
}
}
};

Ok(hasher_impl)
}

/// SHA256-specific DataHasher implementation that serializes the whole struct
pub(crate) fn generate_data_hasher_impl_sha(
struct_name: &syn::Ident,
generics: &syn::Generics,
) -> Result<TokenStream> {
let (impl_gen, type_gen, where_clause) = generics.split_for_impl();

let hasher_impl = quote! {
impl #impl_gen ::light_hasher::DataHasher for #struct_name #type_gen #where_clause {
fn hash<H>(&self) -> ::std::result::Result<[u8; 32], ::light_hasher::HasherError>
where
H: ::light_hasher::Hasher
{
use ::light_hasher::Hasher;
use borsh::BorshSerialize;

// Compile-time assertion that H must be SHA256 (ID = 1)
use ::light_hasher::sha256::RequireSha256;
let _ = <H as RequireSha256>::ASSERT;

// For SHA256, we serialize the whole struct and hash it in one go
let serialized = self.try_to_vec().map_err(|_| ::light_hasher::HasherError::BorshError)?;
let mut result = H::hash(&serialized)?;
// Truncate sha256 to 31 be bytes less than 254 bits bn254 field size.
result[0] = 0;
Ok(result)
}
}
};

Ok(hasher_impl)
}
30 changes: 30 additions & 0 deletions sdk-libs/macros/src/hasher/input_validator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,36 @@ pub(crate) fn validate_input(input: &ItemStruct) -> Result<()> {
Ok(())
}

/// SHA256-specific validation - much more relaxed constraints
pub(crate) fn validate_input_sha(input: &ItemStruct) -> Result<()> {
// Check that we have a struct with named fields
match &input.fields {
Fields::Named(_) => (),
_ => {
return Err(Error::new_spanned(
input,
"Only structs with named fields are supported",
))
}
};

// For SHA256, we don't limit field count or require specific attributes
// Just ensure flatten is not used (not implemented for SHA256 path)
let flatten_field_exists = input
.fields
.iter()
.any(|field| get_field_attribute(field) == FieldAttribute::Flatten);

if flatten_field_exists {
return Err(Error::new_spanned(
input,
"Flatten attribute is not supported in SHA256 hasher.",
));
}

Ok(())
}

/// Gets the primary attribute for a field (only one attribute can be active)
pub(crate) fn get_field_attribute(field: &Field) -> FieldAttribute {
if field.attrs.iter().any(|attr| attr.path().is_ident("hash")) {
Expand Down
Loading
Loading