Skip to content

chore: hasher and light hasher macro support for sha256 #1892

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
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
1 change: 1 addition & 0 deletions program-libs/hasher/src/sha256.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::{
pub struct Sha256;

impl Hasher for Sha256 {
const ID: u8 = 1;
fn hash(val: &[u8]) -> Result<Hash, HasherError> {
Self::hashv(&[val])
}
Expand Down
54 changes: 54 additions & 0 deletions sdk-libs/macros/src/discriminator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ use quote::quote;
use syn::{ItemStruct, Result};

pub(crate) fn discriminator(input: ItemStruct) -> Result<TokenStream> {
discriminator_with_hasher(input, false)
}

pub(crate) fn discriminator_sha(input: ItemStruct) -> Result<TokenStream> {
discriminator_with_hasher(input, true)
}

fn discriminator_with_hasher(input: ItemStruct, is_sha: bool) -> Result<TokenStream> {
let account_name = &input.ident;

let (impl_gen, type_gen, where_clause) = input.generics.split_for_impl();
Expand All @@ -12,6 +20,10 @@ pub(crate) fn discriminator(input: ItemStruct) -> Result<TokenStream> {
discriminator.copy_from_slice(&Sha256::hash(account_name.to_string().as_bytes()).unwrap()[..8]);
let discriminator: proc_macro2::TokenStream = format!("{discriminator:?}").parse().unwrap();

// For SHA256 variant, we could add specific logic here if needed
// Currently both variants work the same way since discriminator is just based on struct name
let _variant_marker = if is_sha { "sha256" } else { "poseidon" };

Ok(quote! {
impl #impl_gen LightDiscriminator for #account_name #type_gen #where_clause {
const LIGHT_DISCRIMINATOR: [u8; 8] = #discriminator;
Expand Down Expand Up @@ -47,4 +59,46 @@ mod tests {
assert!(output.contains("impl LightDiscriminator for MyAccount"));
assert!(output.contains("[181 , 255 , 112 , 42 , 17 , 188 , 66 , 199]"));
}

#[test]
fn test_discriminator_sha() {
let input: ItemStruct = parse_quote! {
struct MyAccount {
a: u32,
b: i32,
c: u64,
d: i64,
}
};

let output = discriminator_sha(input).unwrap();
let output = output.to_string();

assert!(output.contains("impl LightDiscriminator for MyAccount"));
assert!(output.contains("[181 , 255 , 112 , 42 , 17 , 188 , 66 , 199]"));
}

#[test]
fn test_discriminator_sha_large_struct() {
// Test that SHA256 discriminator can handle large structs (that would fail with regular hasher)
let input: ItemStruct = parse_quote! {
struct LargeAccount {
pub field1: u64, pub field2: u64, pub field3: u64, pub field4: u64,
pub field5: u64, pub field6: u64, pub field7: u64, pub field8: u64,
pub field9: u64, pub field10: u64, pub field11: u64, pub field12: u64,
pub field13: u64, pub field14: u64, pub field15: u64,
pub owner: solana_program::pubkey::Pubkey,
pub authority: solana_program::pubkey::Pubkey,
}
};

let result = discriminator_sha(input);
assert!(
result.is_ok(),
"SHA256 discriminator should handle large structs"
);

let output = result.unwrap().to_string();
assert!(output.contains("impl LightDiscriminator for LargeAccount"));
}
}
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 != 0 {
result[0] = 0;
}

Ok(result)
}
}
}
Expand All @@ -59,10 +66,50 @@ 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 != 0 {
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;

// 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 field size for non-Poseidon hashers
if H::ID != 0 {
result[0] = 0;
}

Ok(result)
}
}
};
Expand Down
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