Skip to main content

uhash_core/
lithium.rs

1//! Lithium v1 adapter over UniversalHash engine.
2//!
3//! Canonical flow:
4//! 1) `lithium_header = SHA256(miner_address_utf8 || block_hash_32 || cyberlinks_merkle_32)`
5//! 2) `pow_hash = UniversalHash(lithium_header || nonce_le_u64)`
6
7use sha2::{Digest, Sha256};
8
9use crate::hash;
10
11#[cfg(not(feature = "std"))]
12use alloc::vec::Vec;
13#[cfg(feature = "std")]
14use std::vec::Vec;
15
16/// Fixed block hash length for lithium v1 preimage.
17pub const LITHIUM_BLOCK_HASH_LEN: usize = 32;
18/// Fixed cyberlinks merkle length for lithium v1 preimage.
19pub const LITHIUM_CYBERLINKS_MERKLE_LEN: usize = 32;
20
21/// Canonical lithium-v1 vector tuple shared across crates/tests.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub struct LithiumCanonicalVector {
24    pub miner_address: &'static str,
25    pub nonce: u64,
26    pub block_hash_hex: &'static str,
27    pub cyberlinks_merkle_hex: &'static str,
28    pub hash_hex: &'static str,
29}
30
31/// Shared canonical vectors used by core/web/consumer parity tests.
32pub const LITHIUM_CANONICAL_VECTORS: [LithiumCanonicalVector; 2] = [
33    LithiumCanonicalVector {
34        miner_address: "bostrom1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqwxyzab",
35        nonce: 1,
36        block_hash_hex: "0000000000000000000000000000000000000000000000000000000000000000",
37        cyberlinks_merkle_hex: "1111111111111111111111111111111111111111111111111111111111111111",
38        hash_hex: "c90e35884ae2dc15327121aa0009fac30afa77eb4a0c1b3b5127c5bdbd60fe4d",
39    },
40    LithiumCanonicalVector {
41        miner_address: "bostrom1s7fuy43h8v6hzjtulx9gxyp30rl9t5cz3z56mk",
42        nonce: 226_943,
43        block_hash_hex: "45ee333a09e4f32dbfb4ca2b22287d10758e7dc2e9ca68f45fcec66ac31b055e",
44        cyberlinks_merkle_hex: "f9b24273b7b5a399b394945aff3c9fe08a126b957516b9f357c7f1e868f8378c",
45        hash_hex: "b4c98eb2752f240247266380e870fce04914abfa7199fa78c80acb80200192e0",
46    },
47];
48
49/// Build canonical lithium header bytes:
50/// `SHA256(miner_address_utf8 || block_hash_32 || cyberlinks_merkle_32)`.
51pub fn lithium_header(
52    miner_address: &str,
53    block_hash: &[u8; LITHIUM_BLOCK_HASH_LEN],
54    cyberlinks_merkle: &[u8; LITHIUM_CYBERLINKS_MERKLE_LEN],
55) -> [u8; 32] {
56    let mut hasher = Sha256::new();
57    hasher.update(miner_address.as_bytes());
58    hasher.update(block_hash);
59    hasher.update(cyberlinks_merkle);
60    hasher.finalize().into()
61}
62
63/// Build canonical lithium PoW input bytes:
64/// `lithium_header || nonce_le_u64`.
65pub fn lithium_preimage(
66    miner_address: &str,
67    nonce: u64,
68    block_hash: &[u8; LITHIUM_BLOCK_HASH_LEN],
69    cyberlinks_merkle: &[u8; LITHIUM_CYBERLINKS_MERKLE_LEN],
70) -> Vec<u8> {
71    let header = lithium_header(miner_address, block_hash, cyberlinks_merkle);
72    let mut input = Vec::with_capacity(32usize + core::mem::size_of::<u64>());
73    input.extend_from_slice(&header);
74    input.extend_from_slice(&nonce.to_le_bytes());
75    input
76}
77
78/// Compute lithium v1 PoW hash using UniversalHash engine:
79/// `UniversalHash(SHA256(miner_address || block_hash || cyberlinks_merkle) || nonce_le)`.
80pub fn lithium_hash(
81    miner_address: &str,
82    nonce: u64,
83    block_hash: &[u8; LITHIUM_BLOCK_HASH_LEN],
84    cyberlinks_merkle: &[u8; LITHIUM_CYBERLINKS_MERKLE_LEN],
85) -> [u8; 32] {
86    let input = lithium_preimage(miner_address, nonce, block_hash, cyberlinks_merkle);
87    hash(&input)
88}
89
90#[cfg(test)]
91mod tests {
92    use super::{lithium_hash, lithium_header, lithium_preimage, LITHIUM_CANONICAL_VECTORS};
93    use hex::decode;
94
95    #[test]
96    fn lithium_preimage_layout_is_stable() {
97        let miner = "bostrom1minerxyz";
98        let nonce = 0x0102_0304_0506_0708u64;
99        let block = [0x11u8; 32];
100        let merkle = [0x22u8; 32];
101
102        let preimage = lithium_preimage(miner, nonce, &block, &merkle);
103        let expected_len = 32 + 8;
104        assert_eq!(preimage.len(), expected_len);
105
106        let header = lithium_header(miner, &block, &merkle);
107        assert_eq!(&preimage[..32], &header);
108        assert_eq!(&preimage[32..], &nonce.to_le_bytes());
109    }
110
111    #[test]
112    fn lithium_hash_changes_when_nonce_changes() {
113        let miner = "bostrom1minerxyz";
114        let block = [0xABu8; 32];
115        let merkle = [0xCDu8; 32];
116
117        let h1 = lithium_hash(miner, 1, &block, &merkle);
118        let h2 = lithium_hash(miner, 2, &block, &merkle);
119        assert_ne!(h1, h2);
120    }
121
122    #[test]
123    fn lithium_hash_changes_when_address_changes() {
124        let block = [0xABu8; 32];
125        let merkle = [0xCDu8; 32];
126
127        let h1 = lithium_hash("bostrom1a", 7, &block, &merkle);
128        let h2 = lithium_hash("bostrom1b", 7, &block, &merkle);
129        assert_ne!(h1, h2);
130    }
131
132    #[test]
133    fn lithium_hash_matches_canonical_vectors() {
134        for vector in LITHIUM_CANONICAL_VECTORS {
135            let block: [u8; 32] = decode(vector.block_hash_hex)
136                .expect("valid hex")
137                .try_into()
138                .expect("32-byte block hash");
139            let merkle: [u8; 32] = decode(vector.cyberlinks_merkle_hex)
140                .expect("valid hex")
141                .try_into()
142                .expect("32-byte merkle");
143            let out = lithium_hash(vector.miner_address, vector.nonce, &block, &merkle);
144            let expected = decode(vector.hash_hex).expect("valid hex");
145            assert_eq!(&out[..], &expected[..], "vector failed: {:?}", vector);
146        }
147    }
148}