building a cryptographic hash function from scratch in rust (part 1)
I built a minimal cryptographic hash function from scratch in Rust using sponge construction and ARX operations — not to replace SHA-256, but to understand what's inside one.

Building a Cryptographic Hash Function from Scratch in Rust
"Anyone can create an algorithm they themselves cannot break." — Bruce Schneier
That quote should humble anyone who touches cryptography. I kept it pinned in my head the entire time I built TorqueHash — a minimal, from-scratch cryptographic hash function written in Rust. This isn't meant to replace SHA-256. Nobody should use it in production. But building it peeled back the magic of cryptography and gave me a direct, hands-on understanding of how data is mathematically scrambled into a fixed-length fingerprint.
It also gave me an excuse to get my hands dirty in Rust, coming from a Python background. Exploring Rust is fun, humbling, and occasionally infuriating — which is exactly how you know you're learning something real.
The full source code is on GitHub: github.com/hfmuzb/torque_hash
Why Rust?
You could write a hash function in Python. But the moment you try to rotate bits inside a 64-bit integer, you realize Python doesn't really have 64-bit integers. Python integers have arbitrary precision — they grow as large as they need to. That's wonderful for general programming, but it's a problem when the entire security model of your hash depends on overflow behavior at exactly 2^64.
In Rust, a u64 is a u64. It is exactly 64 bits wide, it lives in a register, and when it overflows, wrapping_add() wraps it back around modulo 2^64 — deterministically, every time. There's no silent promotion to a bigger type. No surprise. No ambiguity.
This matters because cryptographic primitives are built on precise bit manipulation: shifts, rotations, XORs, modular addition. These operations need to behave identically on every machine, every run. Rust gives you that guarantee at the language level while also preventing the memory bugs that plague C implementations.
In short: Rust lets you think at the register level without shooting yourself in the foot. For building low-level primitives, that's exactly the tradeoff you want.
The Architecture: Sponges and Grids
TorqueHash uses a sponge construction — the same fundamental architecture behind SHA-3 (Keccak), the algorithm that secures Ethereum and a large part of the Web3 ecosystem. If you understand how a sponge works, you have a direct mental model for how blockchains hash their data.
The idea is simple. Imagine a sponge with two zones:
- Rate (256 bits) — this is the part you interact with. You pour data in here, and you squeeze output out from here.
- Capacity (256 bits) — this is the hidden security margin. Data never enters or leaves through the capacity. It exists solely to make the internal state larger than the output, which is what makes the hash resistant to collisions.
Together, they form a 512-bit internal state, represented as 8 x 64-bit integers:
pub struct TorqueHash {
state: [u64; 8], // 512-bit sponge state
buffer: [u8; 32], // 32-byte rate buffer
buffer_len: usize, // how full the buffer is
}
The lifecycle has three phases:
- Initialize — fill the state with "nothing-up-my-sleeve" constants (fractional parts of prime square roots — the same idea SHA-256 uses, so there's no hidden mathematical backdoor).
- Absorb — feed data into the rate portion, 32 bytes at a time. After each block, scramble the entire state.
- Squeeze — pad the final block, scramble one last time, and read 32 bytes out of the rate as the digest.
The key insight: every block of input gets mixed into the state and then permuted. By the time you squeeze the output, every input bit has influenced every state bit through dozens of rounds of mixing. That's where the real engine comes in.
The Engine: ARX Operations
At the heart of TorqueHash is a permutation function built entirely from three operations — collectively known as ARX:
- Addition —
wrapping_add()provides non-linearity. Unlike XOR, addition causes carries that propagate unpredictably across bits. - Rotation —
rotate_left()shifts bits and wraps them around, spreading local changes across the full 64-bit word. - XOR — combines values without losing information, and it's its own inverse.
These three operations are cheap (single CPU instructions each), constant-time (no secret-dependent branching), and together they form the basis of many real-world ciphers (ChaCha20, BLAKE3, Salsa20).
Here's the actual permutation from TorqueHash:
fn permute(&mut self) {
const ROUNDS: usize = 24;
for _ in 0..ROUNDS {
for i in 0..4 {
let j = i + 4;
// Addition: modulo 2^64, creates non-linearity
self.state[i] = self.state[i].wrapping_add(self.state[j]);
// Rotation + XOR: diffuse bits across the word
self.state[j] = self.state[j].rotate_left(21) ^ self.state[i];
self.state[i] = self.state[i].rotate_left(13) ^ self.state[(i + 1) % 8];
}
}
}
Each round mixes the upper half of the state (state[4..8]) into the lower half (state[0..4]), then cross-links neighbors via state[(i + 1) % 8]. After 24 rounds, a single changed bit in the input has cascaded into every word of the state. That's 24 x 4 x 3 = 288 ARX operations per block — enough mixing to thoroughly scramble 512 bits.
The rotation amounts (21 and 13) were chosen to avoid alignment with byte or word boundaries, ensuring bits don't settle back into their original positions during repeated rounds.
The Proof: The Avalanche Effect
A cryptographic hash must exhibit the avalanche effect: changing a single bit of input should flip roughly 50% of the output bits. This is what separates a hash from a checksum.
Here's the actual output from TorqueHash:
=== The Avalanche Effect Test ===
Input 1: 'hello world'
Hash 1: a688809d741d93faa3bd5c540515e8508bf6b5ad7c51e9ceab3b73e97e2b53d9
Input 2: 'hello world!'
Hash 2: 2cee41d8bdc1a06e78c8055fd026fd3db516447f14ac27e7d72d1c560e3e769a
One exclamation mark. That's the only difference in the input. But the two hex strings share almost nothing in common. The unit tests confirm it — at least 16 out of 32 bytes differ every time:
running 5 tests
test tests::test_avalanche_effect ... ok
test tests::test_determinism ... ok
test tests::test_incremental_update ... ok
test tests::test_output_length ... ok
test tests::test_empty_input ... ok
test result: ok. 5 passed; 0 failed; 0 ignored
This is the moment the algorithm stopped being an exercise and started feeling like a real hash function.
Conclusion
Building TorqueHash taught me more about cryptography and systems programming than any textbook could. I now have an intuitive understanding of why sponge constructions work, how ARX operations create diffusion, and why Rust's strict type system is a gift when you need every bit to behave predictably.
The entire implementation is 117 lines of Rust with zero dependencies. No external crates. No unsafe blocks. Just the standard library and bitwise math.
Is TorqueHash secure? Almost certainly not — it hasn't undergone professional cryptanalysis, and Schneier's quote at the top of this post is a constant reminder of that. But that was never the point. The point was to open the black box, understand the machinery, and come out the other side a better engineer.
The full source code is available on GitHub: github.com/hfmuzb/torque_hash
If you've ever been curious about what happens inside a hash function, I'd encourage you to build one yourself. You'll never look at a hex string the same way again.