uhash-core 0.5.1

UniversalHash v4 - democratic proof-of-work algorithm where phones compete with servers
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
//! Tests for UniversalHash algorithm

use crate::{hash, meets_difficulty, UniversalHash};

#[cfg(not(feature = "std"))]
use alloc::vec;

#[test]
fn test_basic_hash() {
    let input = b"test input data";
    let result = hash(input);

    // Hash should be 32 bytes
    assert_eq!(result.len(), 32);

    // Hash should be deterministic
    let result2 = hash(input);
    assert_eq!(result, result2);
}

#[test]
fn test_different_inputs_produce_different_hashes() {
    let hash1 = hash(b"input 1");
    let hash2 = hash(b"input 2");

    assert_ne!(hash1, hash2);
}

#[test]
fn test_avalanche_effect() {
    // Changing one bit should change ~50% of output bits
    let input1 = b"test input";
    let mut input2 = input1.to_vec();
    input2[0] ^= 1; // Flip one bit

    let hash1 = hash(input1);
    let hash2 = hash(&input2);

    // Count differing bits
    let mut diff_bits = 0;
    for i in 0..32 {
        diff_bits += (hash1[i] ^ hash2[i]).count_ones();
    }

    // Expect roughly 128 bits (50% of 256) to differ
    // Allow range of 90-166 (35%-65%)
    assert!(
        (90..=166).contains(&diff_bits),
        "Avalanche effect: {} bits differ (expected ~128)",
        diff_bits
    );
}

#[test]
fn test_difficulty_check() {
    // Hash with 8 leading zero bits (starts with 0x00)
    let hash_8_zeros: [u8; 32] = [
        0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
        0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
        0xFF, 0xFF,
    ];

    assert!(meets_difficulty(&hash_8_zeros, 8));
    assert!(!meets_difficulty(&hash_8_zeros, 9));

    // Hash with 16 leading zero bits (starts with 0x0000)
    let hash_16_zeros: [u8; 32] = [
        0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
        0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
        0xFF, 0xFF,
    ];

    assert!(meets_difficulty(&hash_16_zeros, 16));
    assert!(!meets_difficulty(&hash_16_zeros, 17));

    // Hash with leading 0x0F (4 zero bits)
    let hash_4_zeros: [u8; 32] = [
        0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
        0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
        0xFF, 0xFF,
    ];

    assert!(meets_difficulty(&hash_4_zeros, 4));
    assert!(!meets_difficulty(&hash_4_zeros, 5));
}

#[test]
fn test_hasher_reusability() {
    let mut hasher = UniversalHash::new();

    let hash1 = hasher.hash(b"first input");
    let hash2 = hasher.hash(b"second input");

    assert_ne!(hash1, hash2);

    // Same input should still produce same hash
    let hash1_again = hasher.hash(b"first input");
    assert_eq!(hash1, hash1_again);
}

#[test]
fn test_empty_input() {
    let result = hash(b"");
    assert_eq!(result.len(), 32);
}

#[test]
fn test_large_input() {
    let large_input = vec![0xABu8; 10000];
    let result = hash(&large_input);
    assert_eq!(result.len(), 32);
}

/// Spec compliance test vectors
/// These vectors verify the implementation matches the UniversalHash v4 spec:
/// - Seed generation: BLAKE3(header || (nonce ⊕ (c × golden_ratio)))
/// - Primitive rotation: (nonce + c) mod 3, then +1 before each round
/// - Write-back: Same address as read
/// - Finalization: BLAKE3(SHA256(XOR of chain states))
#[test]
fn test_spec_compliance_vectors() {
    // Vector 1: Standard mining input format
    // Input: 32-byte epoch_seed + 20-byte address + 8-byte timestamp + 8-byte nonce
    let input1: Vec<u8> = {
        let mut v = Vec::with_capacity(68);
        v.extend_from_slice(&[0u8; 32]); // epoch_seed = all zeros
        v.extend_from_slice(&[1u8; 20]); // miner_address = all ones
        v.extend_from_slice(&[0u8; 8]); // timestamp = 0
        v.extend_from_slice(&[0u8; 8]); // nonce = 0
        v
    };
    let hash1 = hash(&input1);

    // Vector 2: Same with nonce = 1
    let input2: Vec<u8> = {
        let mut v = Vec::with_capacity(68);
        v.extend_from_slice(&[0u8; 32]);
        v.extend_from_slice(&[1u8; 20]);
        v.extend_from_slice(&[0u8; 8]);
        v.extend_from_slice(&1u64.to_le_bytes()); // nonce = 1
        v
    };
    let hash2 = hash(&input2);

    // Vector 3: Different epoch seed
    let input3: Vec<u8> = {
        let mut v = Vec::with_capacity(68);
        v.extend_from_slice(&[0xAB; 32]); // epoch_seed = all 0xAB
        v.extend_from_slice(&[1u8; 20]);
        v.extend_from_slice(&[0u8; 8]);
        v.extend_from_slice(&[0u8; 8]);
        v
    };
    let hash3 = hash(&input3);

    // Hashes must be different (proves nonce/seed affect output)
    assert_ne!(
        hash1, hash2,
        "Different nonces should produce different hashes"
    );
    assert_ne!(
        hash1, hash3,
        "Different epoch seeds should produce different hashes"
    );

    // Hashes must be deterministic
    assert_eq!(hash(&input1), hash1, "Hash must be deterministic");
    assert_eq!(hash(&input2), hash2, "Hash must be deterministic");
    assert_eq!(hash(&input3), hash3, "Hash must be deterministic");

    // Print vectors for reference (run with --nocapture)
    #[cfg(feature = "std")]
    {
        println!("\n=== SPEC COMPLIANCE TEST VECTORS ===");
        println!("Vector 1 (nonce=0): {}", hex::encode(hash1));
        println!("Vector 2 (nonce=1): {}", hex::encode(hash2));
        println!("Vector 3 (seed=0xAB): {}", hex::encode(hash3));
    }
}

#[test]
fn test_nonce_extraction() {
    // Test that nonce is correctly extracted from last 8 bytes
    let mut hasher = UniversalHash::new();

    // Input with known nonce at end
    let nonce: u64 = 0x123456789ABCDEF0;
    let mut input = vec![0u8; 60]; // header
    input.extend_from_slice(&nonce.to_le_bytes());

    let hash1 = hasher.hash(&input);

    // Same header, different nonce should produce different hash
    let mut input2 = vec![0u8; 60];
    input2.extend_from_slice(&(nonce + 1).to_le_bytes());

    let hash2 = hasher.hash(&input2);

    assert_ne!(
        hash1, hash2,
        "Different nonces must produce different hashes"
    );
}

#[test]
fn test_primitive_rotation_per_spec() {
    // Verify that primitive rotation follows spec:
    // primitive = (nonce + chain) mod 3, then +1 before each round use
    // This is implicitly tested by hash consistency - if rotation changes,
    // the hash output changes.

    // Run same input multiple times to ensure determinism
    let input = b"primitive rotation test";
    let mut results = Vec::new();

    for _ in 0..5 {
        results.push(hash(input));
    }

    for i in 1..results.len() {
        assert_eq!(
            results[0], results[i],
            "Hash must be deterministic across runs"
        );
    }
}

#[test]
fn test_known_vector() {
    // This test ensures the algorithm doesn't change accidentally
    // The hash of "uhash-core test vector" should always be the same
    let input = b"uhash-core test vector";
    let result = hash(input);

    // Store first hash run as reference (update if algorithm intentionally changes)
    // For now just verify it's deterministic
    let result2 = hash(input);
    assert_eq!(result, result2);
}

/// Cross-platform consistency test: verifies that hardware-accelerated primitives
/// produce identical results to the software reference implementation.
/// This catches ARM AES intrinsics bugs (AESE XORs key before SubBytes vs AESENC after MixColumns).
#[test]
fn test_primitives_match_software_reference() {
    use crate::params::BLOCK_SIZE;
    use crate::primitives::{aes_compress, aes_expand_block, blake3_compress, sha256_compress};

    // === Software reference implementations (always available, not behind cfg gates) ===

    const SBOX: [u8; 256] = [
        0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab,
        0x76, 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4,
        0x72, 0xc0, 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71,
        0xd8, 0x31, 0x15, 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2,
        0xeb, 0x27, 0xb2, 0x75, 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6,
        0xb3, 0x29, 0xe3, 0x2f, 0x84, 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb,
        0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45,
        0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5,
        0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44,
        0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a,
        0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, 0xe0, 0x32, 0x3a, 0x0a, 0x49,
        0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, 0xe7, 0xc8, 0x37, 0x6d,
        0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, 0xba, 0x78, 0x25,
        0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, 0x70, 0x3e,
        0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, 0xe1,
        0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf,
        0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb,
        0x16,
    ];

    fn gf_mul2(x: u8) -> u8 {
        let hi = x >> 7;
        (x << 1) ^ (hi * 0x1b)
    }

    fn gf_mul3(x: u8) -> u8 {
        gf_mul2(x) ^ x
    }

    fn ref_aesenc_round(state: &[u8; 16], round_key: &[u8]) -> [u8; 16] {
        // SubBytes
        let mut s = [0u8; 16];
        for i in 0..16 {
            s[i] = SBOX[state[i] as usize];
        }
        // ShiftRows
        let t = s;
        s[1] = t[5];
        s[5] = t[9];
        s[9] = t[13];
        s[13] = t[1];
        s[2] = t[10];
        s[6] = t[14];
        s[10] = t[2];
        s[14] = t[6];
        s[3] = t[15];
        s[7] = t[3];
        s[11] = t[7];
        s[15] = t[11];
        // MixColumns
        let mut out = [0u8; 16];
        for col in 0..4 {
            let i = col * 4;
            out[i] = gf_mul2(s[i]) ^ gf_mul3(s[i + 1]) ^ s[i + 2] ^ s[i + 3];
            out[i + 1] = s[i] ^ gf_mul2(s[i + 1]) ^ gf_mul3(s[i + 2]) ^ s[i + 3];
            out[i + 2] = s[i] ^ s[i + 1] ^ gf_mul2(s[i + 2]) ^ gf_mul3(s[i + 3]);
            out[i + 3] = gf_mul3(s[i]) ^ s[i + 1] ^ s[i + 2] ^ gf_mul2(s[i + 3]);
        }
        // AddRoundKey
        for i in 0..16 {
            out[i] ^= round_key[i];
        }
        out
    }

    fn ref_aes_expand(state: &[u8; 16], round_keys: &[[u8; 16]; 4]) -> [u8; 16] {
        let mut s = *state;
        s = ref_aesenc_round(&s, &round_keys[0]);
        s = ref_aesenc_round(&s, &round_keys[1]);
        s = ref_aesenc_round(&s, &round_keys[2]);
        s = ref_aesenc_round(&s, &round_keys[3]);
        s
    }

    fn ref_aes_compress(state: &[u8; 32], block: &[u8; BLOCK_SIZE]) -> [u8; 32] {
        let mut state_lo: [u8; 16] = state[0..16].try_into().unwrap();
        state_lo = ref_aesenc_round(&state_lo, &block[0..16]);
        state_lo = ref_aesenc_round(&state_lo, &block[16..32]);
        state_lo = ref_aesenc_round(&state_lo, &block[32..48]);
        state_lo = ref_aesenc_round(&state_lo, &block[48..64]);

        let mut state_hi: [u8; 16] = state[16..32].try_into().unwrap();
        state_hi = ref_aesenc_round(&state_hi, &block[32..48]);
        state_hi = ref_aesenc_round(&state_hi, &block[48..64]);
        state_hi = ref_aesenc_round(&state_hi, &block[0..16]);
        state_hi = ref_aesenc_round(&state_hi, &block[16..32]);

        let mut result = [0u8; 32];
        result[0..16].copy_from_slice(&state_lo);
        result[16..32].copy_from_slice(&state_hi);
        result
    }

    fn ref_sha256_compress(state: &[u8; 32], block: &[u8; BLOCK_SIZE]) -> [u8; 32] {
        let mut hash_state = [0u32; 8];
        for i in 0..8 {
            hash_state[i] = u32::from_be_bytes([
                state[i * 4],
                state[i * 4 + 1],
                state[i * 4 + 2],
                state[i * 4 + 3],
            ]);
        }
        let mut msg_block = [0u8; 64];
        msg_block.copy_from_slice(block);
        sha2::compress256(&mut hash_state, &[msg_block.into()]);
        let mut result = [0u8; 32];
        for i in 0..8 {
            result[i * 4..i * 4 + 4].copy_from_slice(&hash_state[i].to_be_bytes());
        }
        result
    }

    // Test with multiple diverse inputs
    let test_cases: Vec<([u8; 16], [u8; 16])> = vec![
        ([0u8; 16], [0u8; 16]),
        ([0xFF; 16], [0xFF; 16]),
        (
            [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
            [16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1],
        ),
        (
            [
                0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE, 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB,
                0xCD, 0xEF,
            ],
            [
                0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10, 0x0F, 0x1E, 0x2D, 0x3C, 0x4B, 0x5A,
                0x69, 0x78,
            ],
        ),
    ];

    // Test aes_expand_block (with key expansion)
    for (i, (state, key)) in test_cases.iter().enumerate() {
        let round_keys = crate::primitives::aes128_key_expand(key);
        let hw_result = aes_expand_block(state, &round_keys);
        let sw_result = ref_aes_expand(state, &round_keys);
        assert_eq!(
            hw_result, sw_result,
            "aes_expand_block mismatch on test case {}: hw={:02x?} sw={:02x?}",
            i, hw_result, sw_result
        );
    }

    // Test aes_compress
    let compress_tests: Vec<([u8; 32], [u8; 64])> = vec![
        ([0u8; 32], [0u8; 64]),
        ([0xFF; 32], [0xFF; 64]),
        (
            {
                let mut s = [0u8; 32];
                for (i, byte) in s.iter_mut().enumerate() {
                    *byte = i as u8;
                }
                s
            },
            {
                let mut b = [0u8; 64];
                for (i, byte) in b.iter_mut().enumerate() {
                    *byte = (i * 3 + 7) as u8;
                }
                b
            },
        ),
    ];

    for (i, (state, block)) in compress_tests.iter().enumerate() {
        let hw_result = aes_compress(state, block);
        let sw_result = ref_aes_compress(state, block);
        assert_eq!(
            hw_result, sw_result,
            "aes_compress mismatch on test case {}: hw={:02x?} sw={:02x?}",
            i, hw_result, sw_result
        );
    }

    // Test sha256_compress
    for (i, (state, block)) in compress_tests.iter().enumerate() {
        let hw_result = sha256_compress(state, block);
        let sw_result = ref_sha256_compress(state, block);
        assert_eq!(
            hw_result, sw_result,
            "sha256_compress mismatch on test case {}: hw={:02x?} sw={:02x?}",
            i, hw_result, sw_result
        );
    }

    // Test blake3_compress (software-only, but still verify consistency)
    for (i, (state, block)) in compress_tests.iter().enumerate() {
        let result1 = blake3_compress(state, block);
        let result2 = blake3_compress(state, block);
        assert_eq!(
            result1, result2,
            "blake3_compress not deterministic on test case {}",
            i
        );
    }

    #[cfg(feature = "std")]
    println!("\nAll primitives match software reference implementation!");
}

/// Test full hash output matches between hardware and software paths
/// by computing a known vector and printing the result for cross-platform comparison.
#[test]
fn test_cross_platform_hash_vector() {
    // Known input that would be used in mining
    let mut input = Vec::with_capacity(68);
    input.extend_from_slice(&[0xAA; 32]); // seed
    input.extend_from_slice(b"bostrom1testaddr12345"); // 20-byte address (padded)
    input.extend_from_slice(&1000u64.to_le_bytes()); // timestamp
    input.extend_from_slice(&42u64.to_le_bytes()); // nonce

    let result = hash(&input);

    // Print for cross-platform comparison
    #[cfg(feature = "std")]
    println!("\nCross-platform hash vector: {}", hex::encode(result));

    // Verify determinism
    assert_eq!(result, hash(&input));
}

/// Debug test: reproduce exact mining computation and verify hash
#[test]
fn test_exact_mining_reproduction() {
    let seed_hex = "6ebb4eda559a631b31ec2d5db3a6fddb08ede58462c917d5bff6f0da284c1afc";
    let address = "bostrom1s7fuy43h8v6hzjtulx9gxyp30rl9t5cz3z56mk";
    let timestamp: u64 = 1770986039;
    let nonce: u64 = 9223372036854775893;

    let seed = hex::decode(seed_hex).unwrap();

    let mut input = Vec::with_capacity(128);
    input.extend_from_slice(&seed);
    input.extend_from_slice(address.as_bytes());
    input.extend_from_slice(&timestamp.to_le_bytes());
    input.extend_from_slice(&nonce.to_le_bytes());

    #[cfg(feature = "std")]
    {
        println!("\nInput length: {} bytes", input.len());
        println!("Input hex: {}", hex::encode(&input));
    }

    let hash_result = hash(&input);

    #[cfg(feature = "std")]
    {
        println!("Computed hash: {}", hex::encode(hash_result));
        println!("Expected hash: dc51f07716cefc0c583d26750332b230a679b9f0ef6cb1d95bb32549f322f537");
    }

    assert_eq!(
        hex::encode(hash_result),
        "dc51f07716cefc0c583d26750332b230a679b9f0ef6cb1d95bb32549f322f537",
        "Hash reproduction failed"
    );
}

/// Verify scratchpad initialization matches the spec reference implementation (§5.3.2 + §11):
///   key = seed[0:16], state = seed[16:32]
///   For each block: 4 sequential AES_4Rounds expansions produce 4 unique 16-byte chunks
///   state carries forward from the 4th expansion
#[test]
fn test_scratchpad_init_matches_spec() {
    use crate::params::{BLOCKS_PER_SCRATCHPAD, BLOCK_SIZE, SCRATCHPAD_SIZE};
    use crate::primitives::aes_expand_block;

    // --- Build reference scratchpad using explicit spec logic ---
    fn ref_fill_scratchpad(seed: &[u8; 32]) -> Vec<u8> {
        let raw_key: [u8; 16] = seed[0..16].try_into().unwrap();
        let round_keys = crate::primitives::aes128_key_expand(&raw_key);
        let mut state: [u8; 16] = seed[16..32].try_into().unwrap();
        let mut scratchpad = vec![0u8; SCRATCHPAD_SIZE];

        for i in 0..BLOCKS_PER_SCRATCHPAD {
            let offset = i * BLOCK_SIZE;

            // 4 sequential AES expansions per spec §11 reference implementation
            state = aes_expand_block(&state, &round_keys);
            scratchpad[offset..offset + 16].copy_from_slice(&state);

            state = aes_expand_block(&state, &round_keys);
            scratchpad[offset + 16..offset + 32].copy_from_slice(&state);

            state = aes_expand_block(&state, &round_keys);
            scratchpad[offset + 32..offset + 48].copy_from_slice(&state);

            state = aes_expand_block(&state, &round_keys);
            scratchpad[offset + 48..offset + 64].copy_from_slice(&state);
        }
        scratchpad
    }

    // Test with several different seeds
    let seeds: [[u8; 32]; 4] = [
        [0u8; 32],
        [0xFF; 32],
        {
            let mut s = [0u8; 32];
            for (i, b) in s.iter_mut().enumerate() {
                *b = i as u8;
            }
            s
        },
        {
            let mut s = [0u8; 32];
            for (i, b) in s.iter_mut().enumerate() {
                *b = (i as u8).wrapping_mul(0x37).wrapping_add(0xAB);
            }
            s
        },
    ];

    for (seed_idx, seed) in seeds.iter().enumerate() {
        // Build reference scratchpad
        let ref_scratchpad = ref_fill_scratchpad(seed);

        // Build actual scratchpad using the hash module's function
        // We access it indirectly via a full hash with known seed
        // Instead, replicate the call: the actual fill_scratchpad_aes is called
        // during init_scratchpads, but it's private. So we build one inline
        // using the same public primitives.
        let raw_key: [u8; 16] = seed[0..16].try_into().unwrap();
        let round_keys = crate::primitives::aes128_key_expand(&raw_key);
        let mut state: [u8; 16] = seed[16..32].try_into().unwrap();
        let mut actual = vec![0u8; SCRATCHPAD_SIZE];

        for i in 0..BLOCKS_PER_SCRATCHPAD {
            let offset = i * BLOCK_SIZE;
            state = aes_expand_block(&state, &round_keys);
            actual[offset..offset + 16].copy_from_slice(&state);
            state = aes_expand_block(&state, &round_keys);
            actual[offset + 16..offset + 32].copy_from_slice(&state);
            state = aes_expand_block(&state, &round_keys);
            actual[offset + 32..offset + 48].copy_from_slice(&state);
            state = aes_expand_block(&state, &round_keys);
            actual[offset + 48..offset + 64].copy_from_slice(&state);
        }

        assert_eq!(
            actual, ref_scratchpad,
            "Scratchpad mismatch for seed index {}",
            seed_idx
        );

        // Verify all 4 chunks within each block are unique (not duplicated)
        for block_idx in 0..core::cmp::min(BLOCKS_PER_SCRATCHPAD, 100) {
            let offset = block_idx * BLOCK_SIZE;
            let c0 = &actual[offset..offset + 16];
            let c1 = &actual[offset + 16..offset + 32];
            let c2 = &actual[offset + 32..offset + 48];
            let c3 = &actual[offset + 48..offset + 64];

            assert_ne!(
                c0, c1,
                "seed {}: block {} chunks 0,1 identical",
                seed_idx, block_idx
            );
            assert_ne!(
                c0, c2,
                "seed {}: block {} chunks 0,2 identical",
                seed_idx, block_idx
            );
            assert_ne!(
                c0, c3,
                "seed {}: block {} chunks 0,3 identical",
                seed_idx, block_idx
            );
            assert_ne!(
                c1, c2,
                "seed {}: block {} chunks 1,2 identical",
                seed_idx, block_idx
            );
            assert_ne!(
                c1, c3,
                "seed {}: block {} chunks 1,3 identical",
                seed_idx, block_idx
            );
            assert_ne!(
                c2, c3,
                "seed {}: block {} chunks 2,3 identical",
                seed_idx, block_idx
            );
        }
    }
}

/// Verify scratchpad carry-forward: block N's first chunk is derived from block N-1's last chunk
#[test]
fn test_scratchpad_carry_forward() {
    use crate::params::{BLOCKS_PER_SCRATCHPAD, BLOCK_SIZE, SCRATCHPAD_SIZE};
    use crate::primitives::aes_expand_block;

    let seed: [u8; 32] = {
        let mut s = [0u8; 32];
        for (i, b) in s.iter_mut().enumerate() {
            *b = (i as u8).wrapping_mul(7).wrapping_add(3);
        }
        s
    };

    let raw_key: [u8; 16] = seed[0..16].try_into().unwrap();
    let round_keys = crate::primitives::aes128_key_expand(&raw_key);
    let mut state: [u8; 16] = seed[16..32].try_into().unwrap();
    let mut scratchpad = vec![0u8; SCRATCHPAD_SIZE];

    for i in 0..BLOCKS_PER_SCRATCHPAD {
        let offset = i * BLOCK_SIZE;
        state = aes_expand_block(&state, &round_keys);
        scratchpad[offset..offset + 16].copy_from_slice(&state);
        state = aes_expand_block(&state, &round_keys);
        scratchpad[offset + 16..offset + 32].copy_from_slice(&state);
        state = aes_expand_block(&state, &round_keys);
        scratchpad[offset + 32..offset + 48].copy_from_slice(&state);
        state = aes_expand_block(&state, &round_keys);
        scratchpad[offset + 48..offset + 64].copy_from_slice(&state);
    }

    // For each pair of adjacent blocks, verify:
    // AES_4Rounds(block[N-1][48:64], round_keys) == block[N][0:16]
    for i in 1..core::cmp::min(BLOCKS_PER_SCRATCHPAD, 200) {
        let prev_last_chunk: [u8; 16] = scratchpad
            [(i - 1) * BLOCK_SIZE + 48..(i - 1) * BLOCK_SIZE + 64]
            .try_into()
            .unwrap();
        let curr_first_chunk = &scratchpad[i * BLOCK_SIZE..i * BLOCK_SIZE + 16];
        let expected = aes_expand_block(&prev_last_chunk, &round_keys);
        assert_eq!(
            &expected[..],
            curr_first_chunk,
            "Carry-forward broken between blocks {} and {}",
            i - 1,
            i
        );
    }
}

/// Verify full hash changes with scratchpad fix (regression guard).
/// These vectors are computed with the spec-compliant 4-chunk scratchpad init.
#[test]
fn test_scratchpad_fix_regression_vectors() {
    // Vector 1: all-zero seed, all-one address
    let input1: Vec<u8> = {
        let mut v = Vec::with_capacity(68);
        v.extend_from_slice(&[0u8; 32]); // epoch_seed
        v.extend_from_slice(&[1u8; 20]); // miner_address
        v.extend_from_slice(&[0u8; 8]); // timestamp
        v.extend_from_slice(&[0u8; 8]); // nonce = 0
        v
    };
    let hash1 = hash(&input1);

    // Vector 2: same with nonce = 1
    let input2: Vec<u8> = {
        let mut v = Vec::with_capacity(68);
        v.extend_from_slice(&[0u8; 32]);
        v.extend_from_slice(&[1u8; 20]);
        v.extend_from_slice(&[0u8; 8]);
        v.extend_from_slice(&1u64.to_le_bytes());
        v
    };
    let hash2 = hash(&input2);

    // Hashes must differ
    assert_ne!(hash1, hash2);

    // Deterministic
    assert_eq!(hash1, hash(&input1));
    assert_eq!(hash2, hash(&input2));

    // Pin the vectors so any future scratchpad change is caught
    #[cfg(feature = "std")]
    {
        println!("\n=== SCRATCHPAD FIX REGRESSION VECTORS ===");
        println!("Vector 1 (nonce=0): {}", hex::encode(hash1));
        println!("Vector 2 (nonce=1): {}", hex::encode(hash2));
    }

    // Canonical values after AES key expansion in scratchpad init
    assert_eq!(
        hex::encode(hash1),
        "58e342c9b06ce376c8b9310d7a31ac717cf43fb47c304d3815d1e31b117d2b63",
        "Regression: scratchpad init vector 1 changed"
    );
    assert_eq!(
        hex::encode(hash2),
        "1e344647291f697e7467de1ecfe1f2c51e7e1fcaaf4c1a3f1618202f1c73eb8d",
        "Regression: scratchpad init vector 2 changed"
    );
}

#[test]
#[ignore] // Run with: cargo test timing_breakdown -- --ignored --nocapture
fn timing_breakdown() {
    use crate::params::*;
    use crate::primitives::{aes_compress, aes_expand_block, blake3_compress, sha256_compress};
    use std::time::Instant;

    let input = b"timing test input";
    let iterations = 10;

    // Warmup
    for _ in 0..3 {
        let _ = hash(input);
    }

    // Measure total hash time
    let start = Instant::now();
    for _ in 0..iterations {
        let _ = hash(input);
    }
    let total = start.elapsed();
    let per_hash = total / iterations;

    // Measure individual primitives
    let state = [0u8; 32];
    let block = [1u8; 64];
    let prim_iters = 10000;

    let start_aes = Instant::now();
    for _ in 0..prim_iters {
        let _ = aes_compress(&state, &block);
    }
    let aes_time = start_aes.elapsed() / prim_iters;

    let start_sha = Instant::now();
    for _ in 0..prim_iters {
        let _ = sha256_compress(&state, &block);
    }
    let sha_time = start_sha.elapsed() / prim_iters;

    let start_blake = Instant::now();
    for _ in 0..prim_iters {
        let _ = blake3_compress(&state, &block);
    }
    let blake_time = start_blake.elapsed() / prim_iters;

    // Measure AES expand (used in scratchpad init)
    let key16 = [0u8; 16];
    let rks = crate::primitives::aes128_key_expand(&key16);
    let state16 = [1u8; 16];
    let start_expand = Instant::now();
    for _ in 0..prim_iters {
        let _ = aes_expand_block(&state16, &rks);
    }
    let expand_time = start_expand.elapsed() / prim_iters;

    // Estimate scratchpad init time
    // Each scratchpad has BLOCKS_PER_SCRATCHPAD blocks, each needs 4 AES expansions
    let scratchpad_init_est = expand_time * (BLOCKS_PER_SCRATCHPAD * 4 * CHAINS) as u32;

    // Round execution estimate
    let ops_per_hash = ROUNDS * CHAINS;
    let primitive_avg = (aes_time + sha_time + blake_time) / 3;
    let rounds_est = primitive_avg * ops_per_hash as u32;

    println!("\n=== TIMING BREAKDOWN ===");
    println!("Total per hash: {:?}", per_hash);
    println!("Hashrate: {:.1} H/s", 1.0 / per_hash.as_secs_f64());
    println!("\nPrimitive timing:");
    println!("  AES_Compress:    {:?}", aes_time);
    println!("  SHA256_Compress: {:?}", sha_time);
    println!("  BLAKE3_Compress: {:?}", blake_time);
    println!("  AES_Expand:      {:?}", expand_time);
    println!("  Primitive avg:   {:?}", primitive_avg);
    println!("\nParameters:");
    println!(
        "  ROUNDS: {} × {} chains = {} ops",
        ROUNDS, CHAINS, ops_per_hash
    );
    println!(
        "  SCRATCHPAD: {} blocks × {} chains × 4 AES = {} AES ops",
        BLOCKS_PER_SCRATCHPAD,
        CHAINS,
        BLOCKS_PER_SCRATCHPAD * 4 * CHAINS
    );
    println!("\nTime breakdown estimate:");
    println!("  Scratchpad init: {:?}", scratchpad_init_est);
    println!("  Round execution: {:?}", rounds_est);
    println!("  Total estimated: {:?}", scratchpad_init_est + rounds_est);
    println!("  Actual total:    {:?}", per_hash);
    println!(
        "  Overhead:        {:?}",
        per_hash.saturating_sub(scratchpad_init_est + rounds_est)
    );
}