Skip to main content

hopper_native/
hash.rs

1//! Cryptographic hash functions via Solana syscalls.
2//!
3//! No existing Solana framework wraps `sol_sha256` or `sol_keccak256`
4//! with ergonomic APIs at the raw substrate level. Programs that need
5//! hashing either pull in heavy crates or write unsafe syscall glue
6//! every time.
7//!
8//! Hopper wraps these syscalls with safe, zero-alloc APIs.
9
10use crate::error::ProgramError;
11
12/// SHA-256 hash output: 32 bytes.
13pub type Sha256Hash = [u8; 32];
14
15/// Keccak-256 hash output: 32 bytes.
16pub type Keccak256Hash = [u8; 32];
17
18/// BLAKE3 hash output: 32 bytes.
19pub type Blake3Hash = [u8; 32];
20
21/// Maximum number of byte slices accepted by Solana hash syscalls.
22pub const MAX_HASH_SEGMENTS: usize = 16;
23
24/// Compute SHA-256 over one or more byte slices.
25///
26/// The Solana `sol_sha256` syscall accepts a vector of (ptr, len) pairs,
27/// so multi-part hashing is done in a single syscall without concatenation.
28///
29/// # Example
30///
31/// ```ignore
32/// let hash = sha256(&[b"hello", b" world"])?;
33/// ```
34#[inline]
35#[allow(unused_mut)]
36pub fn sha256(inputs: &[&[u8]]) -> Result<Sha256Hash, ProgramError> {
37    if inputs.len() > MAX_HASH_SEGMENTS {
38        return Err(ProgramError::InvalidArgument);
39    }
40
41    let mut result = [0u8; 32];
42
43    #[cfg(target_os = "solana")]
44    {
45        // The syscall reads `inputs.len()` (ptr, len) pairs of 8-byte words,
46        // exactly the in-memory shape of a `&[&[u8]]` on the SBF target, so
47        // the slice is handed over directly instead of being repacked
48        // through a zero-filled staging buffer (the same fix as
49        // `pda::create_program_address`).
50        const _: () = assert!(core::mem::size_of::<&[u8]>() == 16);
51        // SAFETY: This block is part of Hopper's reviewed zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
52        let rc = unsafe {
53            crate::syscalls::sol_sha256(
54                inputs.as_ptr() as *const u8,
55                inputs.len() as u64,
56                result.as_mut_ptr(),
57            )
58        };
59        if rc != 0 {
60            return Err(ProgramError::InvalidArgument);
61        }
62    }
63    #[cfg(not(target_os = "solana"))]
64    {
65        let _ = inputs;
66        // Off-chain: return zeroed hash (tests should use a software
67        // implementation if they need real hashes).
68    }
69
70    Ok(result)
71}
72
73/// Compute SHA-256 over a single byte slice.
74#[inline]
75pub fn sha256_single(input: &[u8]) -> Result<Sha256Hash, ProgramError> {
76    sha256(&[input])
77}
78
79/// Compute Keccak-256 over one or more byte slices.
80///
81/// Same multi-part API as `sha256`. Keccak-256 is the hash function used
82/// by Ethereum's `keccak256()` and by Solana's secp256k1 precompile.
83#[inline]
84#[allow(unused_mut)]
85pub fn keccak256(inputs: &[&[u8]]) -> Result<Keccak256Hash, ProgramError> {
86    if inputs.len() > MAX_HASH_SEGMENTS {
87        return Err(ProgramError::InvalidArgument);
88    }
89
90    let mut result = [0u8; 32];
91
92    #[cfg(target_os = "solana")]
93    {
94        // Direct slice pass; see `sha256` above.
95        const _: () = assert!(core::mem::size_of::<&[u8]>() == 16);
96        // SAFETY: This block is part of Hopper's reviewed zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
97        let rc = unsafe {
98            crate::syscalls::sol_keccak256(
99                inputs.as_ptr() as *const u8,
100                inputs.len() as u64,
101                result.as_mut_ptr(),
102            )
103        };
104        if rc != 0 {
105            return Err(ProgramError::InvalidArgument);
106        }
107    }
108    #[cfg(not(target_os = "solana"))]
109    {
110        let _ = inputs;
111    }
112
113    Ok(result)
114}
115
116/// Compute Keccak-256 over a single byte slice.
117#[inline]
118pub fn keccak256_single(input: &[u8]) -> Result<Keccak256Hash, ProgramError> {
119    keccak256(&[input])
120}
121
122/// Compute BLAKE3 over one or more byte slices.
123#[inline]
124#[allow(unused_mut)]
125pub fn blake3(inputs: &[&[u8]]) -> Result<Blake3Hash, ProgramError> {
126    if inputs.len() > MAX_HASH_SEGMENTS {
127        return Err(ProgramError::InvalidArgument);
128    }
129
130    let mut result = [0u8; 32];
131
132    #[cfg(target_os = "solana")]
133    {
134        // Direct slice pass; see `sha256` above.
135        const _: () = assert!(core::mem::size_of::<&[u8]>() == 16);
136        // SAFETY: `inputs` is `inputs.len()` slice descriptors and `result`
137        // is a 32-byte writable hash output buffer.
138        let rc = unsafe {
139            crate::syscalls::sol_blake3(
140                inputs.as_ptr() as *const u8,
141                inputs.len() as u64,
142                result.as_mut_ptr(),
143            )
144        };
145        if rc != 0 {
146            return Err(ProgramError::InvalidArgument);
147        }
148    }
149    #[cfg(not(target_os = "solana"))]
150    {
151        let _ = inputs;
152    }
153
154    Ok(result)
155}
156
157/// Compute BLAKE3 over a single byte slice.
158#[inline]
159pub fn blake3_single(input: &[u8]) -> Result<Blake3Hash, ProgramError> {
160    blake3(&[input])
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    const EMPTY: &[u8] = b"";
168
169    #[test]
170    fn sha256_accepts_sixteen_segments() {
171        let inputs = [EMPTY; MAX_HASH_SEGMENTS];
172        assert_eq!(sha256(&inputs), Ok([0; 32]));
173    }
174
175    #[test]
176    fn keccak256_accepts_sixteen_segments() {
177        let inputs = [EMPTY; MAX_HASH_SEGMENTS];
178        assert_eq!(keccak256(&inputs), Ok([0; 32]));
179    }
180
181    #[test]
182    fn blake3_accepts_sixteen_segments() {
183        let inputs = [EMPTY; MAX_HASH_SEGMENTS];
184        assert_eq!(blake3(&inputs), Ok([0; 32]));
185    }
186
187    #[test]
188    fn sha256_rejects_more_than_sixteen_segments() {
189        let inputs = [EMPTY; MAX_HASH_SEGMENTS + 1];
190        assert_eq!(sha256(&inputs), Err(ProgramError::InvalidArgument));
191    }
192
193    #[test]
194    fn keccak256_rejects_more_than_sixteen_segments() {
195        let inputs = [EMPTY; MAX_HASH_SEGMENTS + 1];
196        assert_eq!(keccak256(&inputs), Err(ProgramError::InvalidArgument));
197    }
198
199    #[test]
200    fn blake3_rejects_more_than_sixteen_segments() {
201        let inputs = [EMPTY; MAX_HASH_SEGMENTS + 1];
202        assert_eq!(blake3(&inputs), Err(ProgramError::InvalidArgument));
203    }
204}