miniscript/descriptor/
checksum.rs1use core::convert::TryFrom;
11use core::fmt;
12use core::iter::FromIterator;
13
14use bech32::primitives::checksum::PackedFe32;
15use bech32::{Checksum, Fe32};
16
17pub use crate::expression::VALID_CHARS;
18use crate::prelude::*;
19use crate::Error;
20
21const CHECKSUM_LENGTH: usize = 8;
22const CODE_LENGTH: usize = 32767;
23
24pub fn desc_checksum(desc: &str) -> Result<String, Error> {
29 let mut eng = Engine::new();
30 eng.input(desc)?;
31 Ok(eng.checksum())
32}
33
34pub(super) fn verify_checksum(s: &str) -> Result<&str, Error> {
39 for ch in s.as_bytes() {
40 if *ch < 20 || *ch > 127 {
41 return Err(Error::Unprintable(*ch));
42 }
43 }
44
45 let mut parts = s.splitn(2, '#');
46 let desc_str = parts.next().unwrap();
47 if let Some(checksum_str) = parts.next() {
48 let expected_sum = desc_checksum(desc_str)?;
49 if checksum_str != expected_sum {
50 return Err(Error::BadDescriptor(format!(
51 "Invalid checksum '{}', expected '{}'",
52 checksum_str, expected_sum
53 )));
54 }
55 }
56 Ok(desc_str)
57}
58
59pub struct Engine {
61 inner: bech32::primitives::checksum::Engine<DescriptorChecksum>,
62 cls: u64,
63 clscount: u64,
64}
65
66impl Default for Engine {
67 fn default() -> Engine { Engine::new() }
68}
69
70impl Engine {
71 pub fn new() -> Self {
73 Engine { inner: bech32::primitives::checksum::Engine::new(), cls: 0, clscount: 0 }
74 }
75
76 pub fn input(&mut self, s: &str) -> Result<(), Error> {
81 for ch in s.chars() {
82 let pos = VALID_CHARS
83 .get(ch as usize)
84 .ok_or_else(|| {
85 Error::BadDescriptor(format!("Invalid character in checksum: '{}'", ch))
86 })?
87 .ok_or_else(|| {
88 Error::BadDescriptor(format!("Invalid character in checksum: '{}'", ch))
89 })? as u64;
90
91 let fe = Fe32::try_from(pos & 31).expect("pos is valid because of the mask");
92 self.inner.input_fe(fe);
93
94 self.cls = self.cls * 3 + (pos >> 5);
95 self.clscount += 1;
96 if self.clscount == 3 {
97 let fe = Fe32::try_from(self.cls).expect("cls is valid");
98 self.inner.input_fe(fe);
99 self.cls = 0;
100 self.clscount = 0;
101 }
102 }
103 Ok(())
104 }
105
106 pub fn checksum_chars(&mut self) -> [char; CHECKSUM_LENGTH] {
109 if self.clscount > 0 {
110 let fe = Fe32::try_from(self.cls).expect("cls is valid");
111 self.inner.input_fe(fe);
112 }
113 self.inner.input_target_residue();
114
115 let mut chars = [0 as char; CHECKSUM_LENGTH];
116 let mut checksum_remaining = CHECKSUM_LENGTH;
117
118 for checksum_ch in &mut chars {
119 checksum_remaining -= 1;
120 let unpacked = self.inner.residue().unpack(checksum_remaining);
121 let fe = Fe32::try_from(unpacked).expect("5 bits fits in an fe32");
122 *checksum_ch = fe.to_char();
123 }
124 chars
125 }
126
127 pub fn checksum(&mut self) -> String {
129 String::from_iter(self.checksum_chars().iter().copied())
130 }
131}
132
133#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
137enum DescriptorChecksum {}
138
139#[rustfmt::skip]
141const GEN: [u64; 5] = [0xf5dee51989, 0xa9fdca3312, 0x1bab10e32d, 0x3706b1677a, 0x644d626ffd];
142
143impl Checksum for DescriptorChecksum {
144 type MidstateRepr = u64; const CHECKSUM_LENGTH: usize = CHECKSUM_LENGTH;
146 const CODE_LENGTH: usize = CODE_LENGTH;
147 const GENERATOR_SH: [u64; 5] = GEN;
148 const TARGET_RESIDUE: u64 = 1;
149}
150
151pub struct Formatter<'f, 'a> {
153 fmt: &'f mut fmt::Formatter<'a>,
154 eng: Engine,
155}
156
157impl<'f, 'a> Formatter<'f, 'a> {
158 pub fn new(f: &'f mut fmt::Formatter<'a>) -> Self { Formatter { fmt: f, eng: Engine::new() } }
160
161 pub fn write_checksum(&mut self) -> fmt::Result {
163 use fmt::Write;
164 self.fmt.write_char('#')?;
165 for ch in self.eng.checksum_chars().iter().copied() {
166 self.fmt.write_char(ch)?;
167 }
168 Ok(())
169 }
170
171 pub fn write_checksum_if_not_alt(&mut self) -> fmt::Result {
173 if !self.fmt.alternate() {
174 self.write_checksum()?;
175 }
176 Ok(())
177 }
178}
179
180impl<'f, 'a> fmt::Write for Formatter<'f, 'a> {
181 fn write_str(&mut self, s: &str) -> fmt::Result {
182 self.fmt.write_str(s)?;
183 self.eng.input(s).map_err(|_| fmt::Error)
184 }
185}
186
187#[cfg(test)]
188mod test {
189 use core::str;
190
191 use super::*;
192
193 macro_rules! check_expected {
194 ($desc: expr, $checksum: expr) => {
195 assert_eq!(desc_checksum($desc).unwrap(), $checksum);
196 };
197 }
198
199 #[test]
200 fn test_valid_descriptor_checksum() {
201 check_expected!(
202 "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)",
203 "tqz0nc62"
204 );
205 check_expected!(
206 "pkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/44'/1'/0'/0/*)",
207 "lasegmfs"
208 );
209
210 check_expected!(
212 "sh(multi(2,[00000000/111'/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))",
213 "ggrsrxfy"
214 );
215 check_expected!(
216 "sh(multi(2,[00000000/111'/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))",
217 "tjg09x5t"
218 );
219 }
220
221 #[test]
222 fn test_desc_checksum_invalid_character() {
223 let sparkle_heart = vec![240, 159, 146, 150];
224 let sparkle_heart = str::from_utf8(&sparkle_heart)
225 .unwrap()
226 .chars()
227 .next()
228 .unwrap();
229 let invalid_desc = format!("wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcL{}fjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)", sparkle_heart);
230
231 assert_eq!(
232 desc_checksum(&invalid_desc).err().unwrap().to_string(),
233 format!("Invalid descriptor: Invalid character in checksum: '{}'", sparkle_heart)
234 );
235 }
236
237 #[test]
238 fn bip_380_test_vectors_checksum_and_character_set_valid() {
239 let tcs = vec![
240 "raw(deadbeef)#89f8spxm", "raw(deadbeef)", ];
243 for tc in tcs {
244 if verify_checksum(tc).is_err() {
245 panic!("false negative: {}", tc)
246 }
247 }
248 }
249
250 #[test]
251 fn bip_380_test_vectors_checksum_and_character_set_invalid() {
252 let tcs = vec![
253 "raw(deadbeef)#", "raw(deadbeef)#89f8spxmx", "raw(deadbeef)#89f8spx", "raw(dedbeef)#89f8spxm", "raw(deadbeef)##9f8spxm", "raw(Ü)#00000000", ];
260 for tc in tcs {
261 if verify_checksum(tc).is_ok() {
262 panic!("false positive: {}", tc)
263 }
264 }
265 }
266}