use std::time::Instant;
use handwired_rs::dsl::Params;
use handwired_rs::{BatchExecutor, handwired};
type Quad = [u32; 4];
const K0: u32 = 0x5A827999u32;
const K1: u32 = 0x6ED9EBA1u32;
const K2: u32 = 0x8F1BBCDCu32;
const K3: u32 = 0xCA62C1D6u32;
const DEFAULT_STATE: [u32; 5] = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0];
#[handwired]
fn sha1_first_add(e: u32, w0: Quad) -> Quad {
[e.wrapping_add(w0[0]), w0[1], w0[2], w0[3]]
}
#[handwired]
fn sha1msg1(a: Quad, b: Quad) -> Quad {
[a[0] ^ a[2], a[1] ^ a[3], a[2] ^ b[0], a[3] ^ b[1]]
}
#[handwired]
fn sha1msg2(a: Quad, b: Quad) -> Quad {
let w16 = (a[0] ^ b[1]).rotate_left(1);
let w17 = (a[1] ^ b[2]).rotate_left(1);
let w18 = (a[2] ^ b[3]).rotate_left(1);
let w19 = (a[3] ^ w16).rotate_left(1);
[w16, w17, w18, w19]
}
#[handwired]
fn schedule(v0: Quad, v1: Quad, v2: Quad, v3: Quad) -> Quad {
let first = sha1msg1(v0, v1);
sha1msg2([first[0] ^ v2[0], first[1] ^ v2[1], first[2] ^ v2[2], first[3] ^ v2[3]], v3)
}
#[handwired]
fn sha1_first_half(abcd: Quad, msg: Quad) -> Quad {
sha1_first_add(abcd[0].rotate_left(30), msg)
}
#[handwired]
fn choose(a: u32, b: u32, c: u32) -> u32 {
c ^ (a & (b ^ c))
}
#[handwired]
fn parity(a: u32, b: u32, c: u32) -> u32 {
a ^ b ^ c
}
#[handwired]
fn majority(a: u32, b: u32, c: u32) -> u32 {
(a & b) ^ (a & c) ^ (b & c)
}
#[handwired]
fn add_key(work: Quad, k: u32) -> Quad {
[work[0].wrapping_add(k), work[1].wrapping_add(k), work[2].wrapping_add(k), work[3].wrapping_add(k)]
}
#[handwired]
fn sha1rnds4c(abcd: Quad, msg: Quad) -> Quad {
let (mut a, mut b, mut c, mut d) = (abcd[0], abcd[1], abcd[2], abcd[3]);
let (t, u, v, w) = (msg[0], msg[1], msg[2], msg[3]);
let mut e = 0u32;
e = e.wrapping_add(a.rotate_left(5)).wrapping_add(choose(b, c, d)).wrapping_add(t);
b = b.rotate_left(30);
d = d.wrapping_add(e.rotate_left(5)).wrapping_add(choose(a, b, c)).wrapping_add(u);
a = a.rotate_left(30);
c = c.wrapping_add(d.rotate_left(5)).wrapping_add(choose(e, a, b)).wrapping_add(v);
e = e.rotate_left(30);
b = b.wrapping_add(c.rotate_left(5)).wrapping_add(choose(d, e, a)).wrapping_add(w);
d = d.rotate_left(30);
[b, c, d, e]
}
#[handwired]
fn sha1rnds4p(abcd: Quad, msg: Quad) -> Quad {
let (mut a, mut b, mut c, mut d) = (abcd[0], abcd[1], abcd[2], abcd[3]);
let (t, u, v, w) = (msg[0], msg[1], msg[2], msg[3]);
let mut e = 0u32;
e = e.wrapping_add(a.rotate_left(5)).wrapping_add(parity(b, c, d)).wrapping_add(t);
b = b.rotate_left(30);
d = d.wrapping_add(e.rotate_left(5)).wrapping_add(parity(a, b, c)).wrapping_add(u);
a = a.rotate_left(30);
c = c.wrapping_add(d.rotate_left(5)).wrapping_add(parity(e, a, b)).wrapping_add(v);
e = e.rotate_left(30);
b = b.wrapping_add(c.rotate_left(5)).wrapping_add(parity(d, e, a)).wrapping_add(w);
d = d.rotate_left(30);
[b, c, d, e]
}
#[handwired]
fn sha1rnds4m(abcd: Quad, msg: Quad) -> Quad {
let (mut a, mut b, mut c, mut d) = (abcd[0], abcd[1], abcd[2], abcd[3]);
let (t, u, v, w) = (msg[0], msg[1], msg[2], msg[3]);
let mut e = 0u32;
e = e.wrapping_add(a.rotate_left(5)).wrapping_add(majority(b, c, d)).wrapping_add(t);
b = b.rotate_left(30);
d = d.wrapping_add(e.rotate_left(5)).wrapping_add(majority(a, b, c)).wrapping_add(u);
a = a.rotate_left(30);
c = c.wrapping_add(d.rotate_left(5)).wrapping_add(majority(e, a, b)).wrapping_add(v);
e = e.rotate_left(30);
b = b.wrapping_add(c.rotate_left(5)).wrapping_add(majority(d, e, a)).wrapping_add(w);
d = d.rotate_left(30);
[b, c, d, e]
}
#[handwired]
fn word_from_bytes(bytes: [u8; 4]) -> u32 {
(bytes[0] as u32) << 24 | (bytes[1] as u32) << 16 | (bytes[2] as u32) << 8 | bytes[3] as u32
}
#[handwired]
fn sha1_block(state: [u8; 20], block: [u8; 64]) -> [u8; 20] {
let mut words = [0u32; 16];
for i in 0..16 {
words[i] = word_from_bytes([block[4 * i], block[4 * i + 1], block[4 * i + 2], block[4 * i + 3]]);
}
let mut s = [0u32; 5];
for i in 0..5 {
s[i] = word_from_bytes([state[4 * i], state[4 * i + 1], state[4 * i + 2], state[4 * i + 3]]);
}
let mut h0 = [s[0], s[1], s[2], s[3]];
let mut w0 = [words[0], words[1], words[2], words[3]];
let mut h1 = sha1rnds4c(h0, add_key(sha1_first_add(s[4], w0), K0));
let mut w1 = [words[4], words[5], words[6], words[7]];
h0 = sha1rnds4c(h1, add_key(sha1_first_half(h0, w1), K0));
let mut w2 = [words[8], words[9], words[10], words[11]];
h1 = sha1rnds4c(h0, add_key(sha1_first_half(h1, w2), K0));
let mut w3 = [words[12], words[13], words[14], words[15]];
h0 = sha1rnds4c(h1, add_key(sha1_first_half(h0, w3), K0));
let mut w4 = schedule(w0, w1, w2, w3);
h1 = sha1rnds4c(h0, add_key(sha1_first_half(h1, w4), K0));
w0 = schedule(w1, w2, w3, w4);
h0 = sha1rnds4p(h1, add_key(sha1_first_half(h0, w0), K1));
w1 = schedule(w2, w3, w4, w0);
h1 = sha1rnds4p(h0, add_key(sha1_first_half(h1, w1), K1));
w2 = schedule(w3, w4, w0, w1);
h0 = sha1rnds4p(h1, add_key(sha1_first_half(h0, w2), K1));
w3 = schedule(w4, w0, w1, w2);
h1 = sha1rnds4p(h0, add_key(sha1_first_half(h1, w3), K1));
w4 = schedule(w0, w1, w2, w3);
h0 = sha1rnds4p(h1, add_key(sha1_first_half(h0, w4), K1));
w0 = schedule(w1, w2, w3, w4);
h1 = sha1rnds4m(h0, add_key(sha1_first_half(h1, w0), K2));
w1 = schedule(w2, w3, w4, w0);
h0 = sha1rnds4m(h1, add_key(sha1_first_half(h0, w1), K2));
w2 = schedule(w3, w4, w0, w1);
h1 = sha1rnds4m(h0, add_key(sha1_first_half(h1, w2), K2));
w3 = schedule(w4, w0, w1, w2);
h0 = sha1rnds4m(h1, add_key(sha1_first_half(h0, w3), K2));
w4 = schedule(w0, w1, w2, w3);
h1 = sha1rnds4m(h0, add_key(sha1_first_half(h1, w4), K2));
w0 = schedule(w1, w2, w3, w4);
h0 = sha1rnds4p(h1, add_key(sha1_first_half(h0, w0), K3));
w1 = schedule(w2, w3, w4, w0);
h1 = sha1rnds4p(h0, add_key(sha1_first_half(h1, w1), K3));
w2 = schedule(w3, w4, w0, w1);
h0 = sha1rnds4p(h1, add_key(sha1_first_half(h0, w2), K3));
w3 = schedule(w4, w0, w1, w2);
h1 = sha1rnds4p(h0, add_key(sha1_first_half(h1, w3), K3));
w4 = schedule(w0, w1, w2, w3);
h0 = sha1rnds4p(h1, add_key(sha1_first_half(h0, w4), K3));
let e = h1[0].rotate_left(30);
let next = [
s[0].wrapping_add(h0[0]),
s[1].wrapping_add(h0[1]),
s[2].wrapping_add(h0[2]),
s[3].wrapping_add(h0[3]),
s[4].wrapping_add(e),
];
let mut result = [0u8; 20];
for i in 0..5 {
for byte in 0..4 {
result[4 * i + byte] = (next[i] >> (24 - 8 * byte)) as u8;
}
}
result
}
fn pad(message: &[u8]) -> Vec<[u8; 64]> {
let mut bytes = message.to_vec();
bytes.push(0x80);
while bytes.len() % 64 != 56 {
bytes.push(0);
}
bytes.extend_from_slice(&(message.len() as u64 * 8).to_be_bytes());
bytes.chunks(64).map(|chunk| chunk.try_into().unwrap()).collect()
}
fn initial_state() -> [u8; 20] {
let mut state = [0u8; 20];
for (index, word) in DEFAULT_STATE.iter().enumerate() {
state[4 * index..4 * index + 4].copy_from_slice(&word.to_be_bytes());
}
state
}
struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
self.0 ^= self.0 << 13;
self.0 ^= self.0 >> 7;
self.0 ^= self.0 << 17;
self.0
}
}
fn hash_all(program: &handwired_rs::TensorProgram, messages: &[Vec<u8>]) -> Vec<[u8; 20]> {
let padded: Vec<Vec<[u8; 64]>> = messages.iter().map(|message| pad(message)).collect();
let mut states = vec![initial_state(); messages.len()];
let longest = padded.iter().map(Vec::len).max().unwrap_or(0);
for position in 0..longest {
let active: Vec<usize> = (0..messages.len()).filter(|&index| padded[index].len() > position).collect();
let mut sources = Vec::new();
for &index in &active {
(states[index], padded[index][position]).encode(&mut sources);
}
let outputs = BatchExecutor::run_parallel(program, &sources, None);
for (&index, digest) in active.iter().zip(outputs.chunks(20)) {
states[index] = std::array::from_fn(|byte| digest[byte] as u8);
}
}
states
}
fn hex(bytes: &[u8]) -> String {
bytes.iter().map(|byte| format!("{byte:02x}")).collect()
}
fn main() {
let start = Instant::now();
let compiled = sha1_block_circuit();
println!(
"sha1_block: {} live operations in {} layers, {} stored weights, built in {:.1?}",
compiled.program.live_operation_count(),
compiled.program.layers.len(),
compiled.program.stored_weight_count(),
start.elapsed()
);
let start = Instant::now();
let report = compiled.verify(64).unwrap();
println!(
"matches the ported Rust function on {} random states and blocks ({:.1?})",
report.checked,
start.elapsed()
);
let vectors: [(&[u8], &str); 4] = [
(b"", "da39a3ee5e6b4b0d3255bfef95601890afd80709"),
(b"abc", "a9993e364706816aba3e25717850c26c9cd0d89d"),
(b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", "84983e441c3bd26ebaae4aa1f95129e5e54670f1"),
(b"The quick brown fox jumps over the lazy dog", "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12"),
];
let messages: Vec<Vec<u8>> = vectors.iter().map(|(message, _)| message.to_vec()).collect();
for ((message, expected), digest) in vectors.iter().zip(hash_all(&compiled.program, &messages)) {
assert_eq!(hex(&digest), *expected);
assert_eq!(digest, sha1_smol::Sha1::from(message).digest().bytes());
println!(" {:<58} {}", format!("{:?}", String::from_utf8_lossy(message)), hex(&digest));
}
let mut rng = Rng(0x5EED_5EED_5EED_5EED);
let messages: Vec<Vec<u8>> = (0..2_000)
.map(|_| {
let length = (rng.next() % 300) as usize;
(0..length).map(|_| rng.next() as u8).collect()
})
.collect();
let blocks: usize = messages.iter().map(|message| pad(message).len()).sum();
let start = Instant::now();
let digests = hash_all(&compiled.program, &messages);
let circuit_time = start.elapsed();
let start = Instant::now();
let library: Vec<[u8; 20]> =
messages.iter().map(|message| sha1_smol::Sha1::from(message).digest().bytes()).collect();
let library_time = start.elapsed();
assert_eq!(digests, library, "the circuit disagrees with sha1_smol");
println!(
"{} random messages of 0..300 bytes ({blocks} blocks): every digest matches sha1_smol \
(circuit {circuit_time:.1?} on the batch executor, sha1_smol {library_time:.1?})",
messages.len()
);
}