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. Each wrapper
9//! hands the `&[&[u8]]` straight to the syscall (its in-memory shape is
10//! the `(ptr, len)` array the runtime reads), writes the digest into an
11//! uninitialized output buffer, and returns without inspecting a result
12//! code: the Agave hash syscalls return zero or abort the transaction
13//! (compute exhaustion, an unmapped slice), so there is no error value to
14//! branch on. Off-chain, `sha256` runs the const implementation in
15//! [`crate::sha256`]; the other digests return all zeros, as documented on
16//! each function.
17
18use crate::error::ProgramError;
19#[cfg(target_os = "solana")]
20use core::mem::MaybeUninit;
21
22/// SHA-256 hash output: 32 bytes.
23pub type Sha256Hash = [u8; 32];
24
25/// Keccak-256 hash output: 32 bytes.
26pub type Keccak256Hash = [u8; 32];
27
28/// BLAKE3 hash output: 32 bytes.
29pub type Blake3Hash = [u8; 32];
30
31/// SHA-512 hash output: 64 bytes.
32#[cfg(feature = "sha512-syscall")]
33pub type Sha512Hash = [u8; 64];
34
35/// Maximum number of byte slices one hash syscall accepts: the runtime's
36/// `sha256_max_slices` (Agave `execution_budget.rs`), which applies to
37/// every hash syscall. Beyond it the syscall aborts the transaction, so the
38/// wrappers refuse first with `InvalidArgument`. (An earlier release capped
39/// this at 16 and called that the runtime limit; it was not.)
40pub const MAX_HASH_SEGMENTS: usize = 20_000;
41
42#[cfg(target_os = "solana")]
43macro_rules! hash_syscall {
44    ($syscall:ident, $inputs:expr, $out:expr) => {{
45        // The syscall reads `inputs.len()` (ptr, len) pairs of 8-byte
46        // words, exactly the in-memory shape of a `&[&[u8]]` on the SBF
47        // target, so the slice is handed over directly instead of being
48        // repacked through a staging buffer.
49        const _: () = assert!(core::mem::size_of::<&[u8]>() == 16);
50        // SAFETY: `$inputs` is `$inputs.len()` slice descriptors the
51        // runtime translates; `$out` is a writable buffer of the digest's
52        // exact length that the syscall fills completely before returning
53        // (it returns zero or aborts the transaction, never a partial
54        // write with an error code).
55        unsafe {
56            crate::syscalls::$syscall(
57                $inputs.as_ptr() as *const u8,
58                $inputs.len() as u64,
59                $out.as_mut_ptr() as *mut u8,
60            );
61        }
62    }};
63}
64
65/// Compute SHA-256 over one or more byte slices.
66///
67/// The Solana `sol_sha256` syscall accepts a vector of (ptr, len) pairs,
68/// so multi-part hashing is done in a single syscall without concatenation.
69/// Off-chain this runs the const SHA-256 in [`crate::sha256`], so host
70/// tests see the real digest.
71///
72/// # Example
73///
74/// ```ignore
75/// let hash = sha256(&[b"hello", b" world"])?;
76/// ```
77#[inline]
78pub fn sha256(inputs: &[&[u8]]) -> Result<Sha256Hash, ProgramError> {
79    if inputs.len() > MAX_HASH_SEGMENTS {
80        return Err(ProgramError::InvalidArgument);
81    }
82    #[cfg(target_os = "solana")]
83    {
84        let mut result = MaybeUninit::<Sha256Hash>::uninit();
85        hash_syscall!(sol_sha256, inputs, result);
86        // SAFETY: the syscall wrote all 32 bytes (see `hash_syscall!`).
87        Ok(unsafe { result.assume_init() })
88    }
89    #[cfg(not(target_os = "solana"))]
90    {
91        let mut hasher = crate::sha256::ConstSha256::new();
92        let mut i = 0;
93        while i < inputs.len() {
94            hasher = hasher.update(inputs[i]);
95            i += 1;
96        }
97        Ok(hasher.finalize())
98    }
99}
100
101/// Compute SHA-256 over a single byte slice.
102#[inline]
103pub fn sha256_single(input: &[u8]) -> Result<Sha256Hash, ProgramError> {
104    sha256(&[input])
105}
106
107/// Compute Keccak-256 over one or more byte slices.
108///
109/// Same multi-part API as `sha256`. Keccak-256 is the hash function used
110/// by Ethereum's `keccak256()` and by Solana's secp256k1 precompile.
111/// Off-chain there is no keccak implementation in this crate, so the
112/// result is all zeros; host tests that need the real digest should use a
113/// software implementation.
114#[inline]
115pub fn keccak256(inputs: &[&[u8]]) -> Result<Keccak256Hash, ProgramError> {
116    if inputs.len() > MAX_HASH_SEGMENTS {
117        return Err(ProgramError::InvalidArgument);
118    }
119    #[cfg(target_os = "solana")]
120    {
121        let mut result = MaybeUninit::<Keccak256Hash>::uninit();
122        hash_syscall!(sol_keccak256, inputs, result);
123        // SAFETY: the syscall wrote all 32 bytes (see `hash_syscall!`).
124        Ok(unsafe { result.assume_init() })
125    }
126    #[cfg(not(target_os = "solana"))]
127    {
128        let _ = inputs;
129        Ok([0u8; 32])
130    }
131}
132
133/// Compute Keccak-256 over a single byte slice.
134#[inline]
135pub fn keccak256_single(input: &[u8]) -> Result<Keccak256Hash, ProgramError> {
136    keccak256(&[input])
137}
138
139/// Compute BLAKE3 over one or more byte slices.
140///
141/// Off-chain there is no BLAKE3 implementation in this crate, so the
142/// result is all zeros.
143#[inline]
144pub fn blake3(inputs: &[&[u8]]) -> Result<Blake3Hash, ProgramError> {
145    if inputs.len() > MAX_HASH_SEGMENTS {
146        return Err(ProgramError::InvalidArgument);
147    }
148    #[cfg(target_os = "solana")]
149    {
150        let mut result = MaybeUninit::<Blake3Hash>::uninit();
151        hash_syscall!(sol_blake3, inputs, result);
152        // SAFETY: the syscall wrote all 32 bytes (see `hash_syscall!`).
153        Ok(unsafe { result.assume_init() })
154    }
155    #[cfg(not(target_os = "solana"))]
156    {
157        let _ = inputs;
158        Ok([0u8; 32])
159    }
160}
161
162/// Compute BLAKE3 over a single byte slice.
163#[inline]
164pub fn blake3_single(input: &[u8]) -> Result<Blake3Hash, ProgramError> {
165    blake3(&[input])
166}
167
168/// Compute SHA-512 over one or more byte slices through the `sol_sha512`
169/// syscall (feature gate `s512oDwgx8hjMnaQjXfqqrZroVj4HvC6TkN3iSSWXCh`,
170/// `enable_sha512_syscall`).
171///
172/// Bound only under the `sha512-syscall` cargo feature, because a program
173/// that references the symbol fails to load on a cluster where the gate is
174/// inactive (`Unresolved symbol`), and on 2026-09-27 the gate was active on
175/// devnet and testnet but absent on mainnet-beta. Query the target cluster
176/// (`hopper feature-gate`) before enabling it for a deployment. Off-chain
177/// the result is all zeros.
178#[cfg(feature = "sha512-syscall")]
179#[inline]
180pub fn sha512(inputs: &[&[u8]]) -> Result<Sha512Hash, ProgramError> {
181    if inputs.len() > MAX_HASH_SEGMENTS {
182        return Err(ProgramError::InvalidArgument);
183    }
184    #[cfg(target_os = "solana")]
185    {
186        let mut result = MaybeUninit::<Sha512Hash>::uninit();
187        hash_syscall!(sol_sha512, inputs, result);
188        // SAFETY: the syscall wrote all 64 bytes (see `hash_syscall!`).
189        Ok(unsafe { result.assume_init() })
190    }
191    #[cfg(not(target_os = "solana"))]
192    {
193        let _ = inputs;
194        Ok([0u8; 64])
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201
202    const EMPTY: &[u8] = b"";
203
204    #[test]
205    fn sha256_off_chain_is_the_const_digest() {
206        assert_eq!(
207            sha256(&[b"abc"]).unwrap(),
208            crate::sha256::sha256(b"abc"),
209            "single segment"
210        );
211        assert_eq!(
212            sha256(&[b"global:", b"initialize"]).unwrap(),
213            crate::sha256::sha256(b"global:initialize"),
214            "segments hash as one stream"
215        );
216        assert_eq!(sha256_single(b"").unwrap(), crate::sha256::sha256(b""));
217    }
218
219    static AT_LIMIT: [&[u8]; MAX_HASH_SEGMENTS] = [EMPTY; MAX_HASH_SEGMENTS];
220    static PAST_LIMIT: [&[u8]; MAX_HASH_SEGMENTS + 1] = [EMPTY; MAX_HASH_SEGMENTS + 1];
221
222    #[test]
223    fn wrappers_accept_the_runtime_slice_limit() {
224        assert_eq!(sha256(&AT_LIMIT[..]), Ok(crate::sha256::sha256(b"")));
225        assert_eq!(keccak256(&AT_LIMIT[..]), Ok([0; 32]));
226        assert_eq!(blake3(&AT_LIMIT[..]), Ok([0; 32]));
227    }
228
229    #[test]
230    fn wrappers_refuse_beyond_the_runtime_slice_limit() {
231        assert_eq!(sha256(&PAST_LIMIT[..]), Err(ProgramError::InvalidArgument));
232        assert_eq!(
233            keccak256(&PAST_LIMIT[..]),
234            Err(ProgramError::InvalidArgument)
235        );
236        assert_eq!(blake3(&PAST_LIMIT[..]), Err(ProgramError::InvalidArgument));
237    }
238}