Skip to main content

chains_sdk/
crypto.rs

1//! Shared cryptographic primitives used across all chain modules.
2//!
3//! Centralizes tagged hashing, double-SHA256, HASH160, and SHA256 so that
4//! chain modules don't duplicate these building blocks.
5
6use sha2::{Digest, Sha256};
7
8/// BIP-340 tagged hash: `SHA256(SHA256(tag) ‖ SHA256(tag) ‖ data)`.
9///
10/// Used by BIP-340 Schnorr, BIP-341 Taproot, BIP-322 message signing, and MuSig2.
11pub fn tagged_hash(tag: &[u8], data: &[u8]) -> [u8; 32] {
12    // Fast path: use precomputed midstates for known tags
13    use std::sync::OnceLock;
14    static TAP_SIGHASH_MID: OnceLock<[u8; 32]> = OnceLock::new();
15    static BIP322_MID: OnceLock<[u8; 32]> = OnceLock::new();
16    static BIP340_CHALLENGE_MID: OnceLock<[u8; 32]> = OnceLock::new();
17    static BIP340_AUX_MID: OnceLock<[u8; 32]> = OnceLock::new();
18
19    let tag_hash = match tag {
20        b"TapSighash" => *TAP_SIGHASH_MID.get_or_init(|| sha256(b"TapSighash")),
21        b"BIP0322-signed-message" => *BIP322_MID.get_or_init(|| sha256(b"BIP0322-signed-message")),
22        b"BIP0340/challenge" => *BIP340_CHALLENGE_MID.get_or_init(|| sha256(b"BIP0340/challenge")),
23        b"BIP0340/aux" => *BIP340_AUX_MID.get_or_init(|| sha256(b"BIP0340/aux")),
24        _ => sha256(tag),
25    };
26
27    let mut h = Sha256::new();
28    h.update(tag_hash);
29    h.update(tag_hash);
30    h.update(data);
31    let result = h.finalize();
32    let mut out = [0u8; 32];
33    out.copy_from_slice(&result);
34    out
35}
36
37/// Double SHA-256: `SHA256(SHA256(data))`.
38///
39/// Used by Bitcoin (BIP-137, txid), XRP (account checksums), NEO, and BIP-32.
40pub fn double_sha256(data: &[u8]) -> [u8; 32] {
41    let h1 = Sha256::digest(data);
42    let h2 = Sha256::digest(h1);
43    let mut out = [0u8; 32];
44    out.copy_from_slice(&h2);
45    out
46}
47
48/// HASH160: `RIPEMD160(SHA256(data))`.
49///
50/// Used by Bitcoin (P2PKH, P2WPKH, fingerprint), XRP, NEO, BIP-32, and descriptors.
51pub fn hash160(data: &[u8]) -> [u8; 20] {
52    use ripemd::{Digest as RipeDigest, Ripemd160};
53    let sha = Sha256::digest(data);
54    let ripe = Ripemd160::digest(sha);
55    let mut out = [0u8; 20];
56    out.copy_from_slice(&ripe);
57    out
58}
59
60/// SHA-256: `SHA256(data)`.
61pub fn sha256(data: &[u8]) -> [u8; 32] {
62    let h = Sha256::digest(data);
63    let mut out = [0u8; 32];
64    out.copy_from_slice(&h);
65    out
66}
67
68#[cfg(test)]
69#[allow(clippy::unwrap_used, clippy::expect_used)]
70mod tests {
71    use super::*;
72
73    // ─── SHA-256 Official NIST Vectors ──────────────────────────
74
75    #[test]
76    fn test_sha256_nist_empty() {
77        // NIST FIPS 180-4: SHA-256("")
78        assert_eq!(
79            hex::encode(sha256(b"")),
80            "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
81        );
82    }
83
84    #[test]
85    fn test_sha256_nist_abc() {
86        // NIST FIPS 180-4: SHA-256("abc")
87        assert_eq!(
88            hex::encode(sha256(b"abc")),
89            "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
90        );
91    }
92
93    #[test]
94    fn test_sha256_nist_448bit() {
95        // NIST: SHA-256("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq")
96        assert_eq!(
97            hex::encode(sha256(
98                b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"
99            )),
100            "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1"
101        );
102    }
103
104    // ─── Double SHA-256 Known Vectors ───────────────────────────
105
106    #[test]
107    fn test_double_sha256_empty() {
108        // SHA256(SHA256("")) — well-known Bitcoin constant
109        assert_eq!(
110            hex::encode(double_sha256(b"")),
111            "5df6e0e2761359d30a8275058e299fcc0381534545f55cf43e41983f5d4c9456"
112        );
113    }
114
115    #[test]
116    fn test_double_sha256_hello() {
117        // SHA256(SHA256("hello")) — cross-checked with multiple tools
118        let h = double_sha256(b"hello");
119        assert_eq!(h.len(), 32);
120        assert_ne!(h, sha256(b"hello")); // must differ from single SHA256
121    }
122
123    #[test]
124    fn test_double_sha256_is_idempotent_on_input() {
125        let h1 = double_sha256(b"test");
126        let h2 = double_sha256(b"test");
127        assert_eq!(h1, h2);
128    }
129
130    // ─── HASH160 Known Vectors ──────────────────────────────────
131
132    #[test]
133    fn test_hash160_bitcoin_generator_point() {
134        // Bitcoin generator point compressed pubkey:
135        // 02 79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798
136        // HASH160 → 751e76e8199196d454941c45d1b3a323f1433bd6
137        let generator_pubkey =
138            hex::decode("0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798")
139                .unwrap();
140        assert_eq!(
141            hex::encode(hash160(&generator_pubkey)),
142            "751e76e8199196d454941c45d1b3a323f1433bd6"
143        );
144    }
145
146    #[test]
147    fn test_hash160_empty() {
148        // RIPEMD160(SHA256("")) — known value
149        assert_eq!(
150            hex::encode(hash160(b"")),
151            "b472a266d0bd89c13706a4132ccfb16f7c3b9fcb"
152        );
153    }
154
155    #[test]
156    fn test_hash160_output_length() {
157        assert_eq!(hash160(b"any data").len(), 20);
158        assert_eq!(hash160(b"").len(), 20);
159        assert_eq!(hash160(&[0u8; 1000]).len(), 20);
160    }
161
162    // ─── Tagged Hash (BIP-340) ──────────────────────────────────
163
164    #[test]
165    fn test_tagged_hash_deterministic() {
166        let h1 = tagged_hash(b"TapLeaf", b"data");
167        let h2 = tagged_hash(b"TapLeaf", b"data");
168        assert_eq!(h1, h2);
169    }
170
171    #[test]
172    fn test_tagged_hash_domain_separation() {
173        let h1 = tagged_hash(b"TapLeaf", b"data");
174        let h2 = tagged_hash(b"TapBranch", b"data");
175        let h3 = tagged_hash(b"BIP0340/challenge", b"data");
176        assert_ne!(h1, h2);
177        assert_ne!(h1, h3);
178        assert_ne!(h2, h3);
179    }
180
181    #[test]
182    fn test_tagged_hash_differs_from_plain_sha256() {
183        // tagged_hash("tag", data) ≠ sha256(data)
184        let plain = sha256(b"data");
185        let tagged = tagged_hash(b"BIP0340/aux", b"data");
186        assert_ne!(plain, tagged);
187    }
188
189    #[test]
190    fn test_tagged_hash_empty_tag_and_data() {
191        let h = tagged_hash(b"", b"");
192        assert_eq!(h.len(), 32);
193        // ensure it's not all-zeros (cryptographically impossible)
194        assert_ne!(h, [0u8; 32]);
195    }
196
197    #[test]
198    fn test_bip322_message_hash_empty() {
199        // BIP-322 official vector: empty message
200        assert_eq!(
201            hex::encode(tagged_hash(b"BIP0322-signed-message", b"")),
202            "c90c269c4f8fcbe6880f72a721ddfbf1914268a794cbb21cfafee13770ae19f1"
203        );
204    }
205
206    #[test]
207    fn test_bip322_message_hash_hello_world() {
208        // BIP-322 official vector: "Hello World"
209        assert_eq!(
210            hex::encode(tagged_hash(b"BIP0322-signed-message", b"Hello World")),
211            "f0eb03b1a75ac6d9847f55c624a99169b5dccba2a31f5b23bea77ba270de0a7a"
212        );
213    }
214
215    #[test]
216    fn test_tagged_hash_bip340_aux_vector() {
217        // BIP-340 aux tag: unique domain
218        let h = tagged_hash(b"BIP0340/aux", &[0u8; 32]);
219        assert_eq!(h.len(), 32);
220        assert_ne!(h, [0u8; 32]);
221    }
222
223    // ─── Cross-function Consistency ─────────────────────────────
224
225    #[test]
226    fn test_double_sha256_equals_sha256_of_sha256() {
227        let data = b"consistency check";
228        let single = sha256(data);
229        let double = sha256(&single);
230        assert_eq!(double_sha256(data), double);
231    }
232
233    #[test]
234    fn test_hash160_consistency() {
235        // HASH160(data) = RIPEMD160(SHA256(data))
236        // We can verify by checking the SHA256 intermediate matches
237        let data = b"hash160 consistency";
238        let sha_intermediate = sha256(data);
239        let h160_direct = hash160(data);
240        // RIPEMD160 of the SHA256 should match
241        use ripemd::{Digest as _, Ripemd160};
242        let ripe = Ripemd160::digest(sha_intermediate);
243        assert_eq!(&h160_direct[..], &ripe[..]);
244    }
245}