md_codec/bch.rs
1//! BIP 93 codex32 BCH primitives for HRP `"md"` (regular code only).
2//!
3//! Extracted from the v0.x `encoding` module; v0.11 needs only the regular-code
4//! checksum + verify (long code dropped along with v0.x).
5
6/// BCH(93,80,8) generator polynomial coefficients (5 × 65-bit).
7pub const GEN_REGULAR: [u128; 5] = [
8 0x19dc500ce73fde210,
9 0x1bfae00def77fe529,
10 0x1fbd920fffe7bee52,
11 0x1739640bdeee3fdad,
12 0x07729a039cfc75f5a,
13];
14
15/// MD-domain target residue (NUMS-style, top 65 bits of
16/// `SHA-256("shibbolethnums")`).
17pub const MD_REGULAR_CONST: u128 = 0x0815c07747a3392e7;
18
19/// Constellation-internal initial polymod residue, shared byte-for-byte with
20/// mk1 (`mk-codec`'s `string_layer::bch::POLYMOD_INIT`). It is deliberately
21/// **NOT** codex32/BIP-93's initial residue `1` — and that is harmless here:
22/// `md1` is a self-contained code (this same value seeds both
23/// [`bch_create_checksum_regular`] and [`bch_verify_regular`]), so the init's
24/// contribution cancels between create and verify and
25/// `polymod(valid codeword) == MD_REGULAR_CONST` holds at every length, for any
26/// fixed init. Only `ms1` must use `1`, because its checksum has to agree with
27/// the *external* rust-codex32 engine; the reverted ms-codec v0.2.1 bug was a
28/// non-codex32 init *paired with* an empirically-miscalibrated target that
29/// diverged from codex32 across lengths — NOT this value being intrinsically
30/// length-variant. See
31/// `mnemonic-secret/design/BUG_decode_with_correction_length_divergence.md`.
32const POLYMOD_INIT: u128 = 0x23181b3;
33const REGULAR_SHIFT: u32 = 60;
34const REGULAR_MASK: u128 = 0x0fffffffffffffff;
35
36fn polymod_step(residue: u128, value: u128) -> u128 {
37 let b = residue >> REGULAR_SHIFT;
38 let mut new_residue = ((residue & REGULAR_MASK) << 5) ^ value;
39 for (i, &g) in GEN_REGULAR.iter().enumerate() {
40 if (b >> i) & 1 != 0 {
41 new_residue ^= g;
42 }
43 }
44 new_residue
45}
46
47/// Run the BCH polymod over `values` starting from `POLYMOD_INIT`.
48///
49/// Returns the final residue; callers XOR against the per-HRP target
50/// constant (e.g. [`MD_REGULAR_CONST`]) to produce a checksum or to
51/// verify one. Inputs are 5-bit symbols (`u8` in `0..32`); larger
52/// values are reduced modulo 32 by the underlying step.
53pub fn polymod_run(values: &[u8]) -> u128 {
54 let mut residue = POLYMOD_INIT;
55 for &v in values {
56 residue = polymod_step(residue, v as u128);
57 }
58 residue
59}
60
61/// BIP 173-style HRP expansion: `[c >> 5 for c in hrp] ++ [0] ++ [c & 31 for c in hrp]`.
62pub fn hrp_expand(hrp: &str) -> Vec<u8> {
63 let bytes = hrp.as_bytes();
64 let mut out = Vec::with_capacity(bytes.len() * 2 + 1);
65 for &c in bytes {
66 out.push(c >> 5);
67 }
68 out.push(0);
69 for &c in bytes {
70 out.push(c & 31);
71 }
72 out
73}
74
75/// 13-symbol regular-code BCH checksum over `hrp_expand(hrp) || data || [0; 13]`.
76pub fn bch_create_checksum_regular(hrp: &str, data: &[u8]) -> [u8; 13] {
77 let mut input = hrp_expand(hrp);
78 input.extend_from_slice(data);
79 input.extend(std::iter::repeat_n(0, 13));
80 let polymod = polymod_run(&input) ^ MD_REGULAR_CONST;
81 let mut out = [0u8; 13];
82 for (i, slot) in out.iter_mut().enumerate() {
83 *slot = ((polymod >> (5 * (12 - i))) & 0x1F) as u8;
84 }
85 out
86}
87
88/// Verify a regular-code BCH checksum over the data-part-with-checksum.
89pub fn bch_verify_regular(hrp: &str, data_with_checksum: &[u8]) -> bool {
90 if data_with_checksum.len() < 13 {
91 return false;
92 }
93 let mut input = hrp_expand(hrp);
94 input.extend_from_slice(data_with_checksum);
95 polymod_run(&input) == MD_REGULAR_CONST
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101 use bitcoin::hashes::{Hash, sha256};
102
103 /// Drift-guard: `MD_REGULAR_CONST` must reproduce from its documented
104 /// NUMS rule — the top 65 bits of `SHA-256("shibbolethnums")`. Mirrors
105 /// mk-codec's `consts::tests::nums_constants_reproduce_from_domain`
106 /// (and ms-codec's `tests/bch_all_lengths.rs::ms_regular_const_is_secretshare32_packed`).
107 /// Catches accidental drift if the domain string or the constant is
108 /// edited without the other — without this, a silent edit to either would
109 /// break cross-format domain separation undetected.
110 #[test]
111 fn md_regular_const_reproduces_from_nums_domain() {
112 let digest = sha256::Hash::hash(b"shibbolethnums");
113 let bytes = digest.as_byte_array();
114 // Leading 128 bits of the 256-bit digest as a big-endian u128, then
115 // the top 65 bits (shift right by 128 - 65 = 63).
116 let hi = u128::from_be_bytes(bytes[0..16].try_into().unwrap());
117 let derived = hi >> 63;
118 assert_eq!(
119 derived, MD_REGULAR_CONST,
120 "MD_REGULAR_CONST drift from SHA-256(\"shibbolethnums\") top-65-bits",
121 );
122 }
123}