Skip to content

Commit b0508d8

Browse files
committed
checksum: clean up errors and error paths
This creates a new checksum::Error type. For now it sticks it into the giant global error enum, but we will fold it into an expression-parsing enum in the following commits. This eliminates several instances of the Error::BadDescriptor variant, which is a meaningless stringly-typed variant that we'd like to eliminate.
1 parent ae8f450 commit b0508d8

File tree

3 files changed

+125
-42
lines changed

3 files changed

+125
-42
lines changed

src/descriptor/checksum.rs

Lines changed: 110 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -16,44 +16,116 @@ use bech32::{Checksum, Fe32};
1616

1717
pub use crate::expression::VALID_CHARS;
1818
use crate::prelude::*;
19-
use crate::Error;
2019

2120
const CHECKSUM_LENGTH: usize = 8;
2221
const CODE_LENGTH: usize = 32767;
2322

24-
/// Compute the checksum of a descriptor.
23+
/// Map of valid characters in descriptor strings.
2524
///
26-
/// Note that this function does not check if the descriptor string is
27-
/// syntactically correct or not. This only computes the checksum.
28-
pub fn desc_checksum(desc: &str) -> Result<String, Error> {
29-
let mut eng = Engine::new();
30-
eng.input(desc)?;
31-
Ok(eng.checksum())
25+
/// The map starts at 32 (space) and runs up to 126 (tilde).
26+
#[rustfmt::skip]
27+
const CHAR_MAP: [u8; 95] = [
28+
94, 59, 92, 91, 28, 29, 50, 15, 10, 11, 17, 51, 14, 52, 53, 16,
29+
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 27, 54, 55, 56, 57, 58,
30+
26, 82, 83, 84, 85, 86, 87, 88, 89, 32, 33, 34, 35, 36, 37, 38,
31+
39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 12, 93, 13, 60, 61,
32+
90, 18, 19, 20, 21, 22, 23, 24, 25, 64, 65, 66, 67, 68, 69, 70,
33+
71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 30, 62, 31, 63,
34+
];
35+
36+
/// Error validating descriptor checksum.
37+
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
38+
pub enum Error {
39+
/// Character outside of descriptor charset.
40+
InvalidCharacter {
41+
/// The character in question.
42+
ch: char,
43+
/// Its position in the string.
44+
pos: usize,
45+
},
46+
/// Checksum had the incorrect length.
47+
InvalidChecksumLength {
48+
/// The length of the checksum in the string.
49+
actual: usize,
50+
/// The length of a valid descriptor checksum.
51+
expected: usize,
52+
},
53+
/// Checksum was invalid.
54+
InvalidChecksum {
55+
/// The checksum in the string.
56+
actual: [char; CHECKSUM_LENGTH],
57+
/// The checksum that should have been there, assuming the string is valid.
58+
expected: [char; CHECKSUM_LENGTH],
59+
},
60+
}
61+
62+
impl fmt::Display for Error {
63+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
64+
match *self {
65+
Error::InvalidCharacter { ch, pos } => {
66+
write!(f, "invalid character '{}' (position {})", ch, pos)
67+
}
68+
Error::InvalidChecksumLength { actual, expected } => {
69+
write!(f, "invalid checksum (length {}, expected {})", actual, expected)
70+
}
71+
Error::InvalidChecksum { actual, expected } => {
72+
f.write_str("invalid checksum ")?;
73+
for ch in actual {
74+
ch.fmt(f)?;
75+
}
76+
f.write_str("; expected ")?;
77+
for ch in expected {
78+
ch.fmt(f)?;
79+
}
80+
Ok(())
81+
}
82+
}
83+
}
84+
}
85+
86+
#[cfg(feature = "std")]
87+
impl std::error::Error for Error {
88+
fn cause(&self) -> Option<&dyn std::error::Error> { None }
3289
}
3390

3491
/// Helper function for `FromStr` for various descriptor types.
3592
///
3693
/// Checks and verifies the checksum if it is present and returns the descriptor
3794
/// string without the checksum.
38-
pub(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));
95+
pub fn verify_checksum(s: &str) -> Result<&str, Error> {
96+
let mut last_hash_pos = s.len();
97+
for (pos, ch) in s.char_indices() {
98+
if !(32..127).contains(&u32::from(ch)) {
99+
return Err(Error::InvalidCharacter { ch, pos });
100+
} else if ch == '#' {
101+
last_hash_pos = pos;
42102
}
43103
}
104+
// After this point we know we have ASCII and can stop using character methods.
105+
106+
if last_hash_pos < s.len() {
107+
let checksum_str = &s[last_hash_pos + 1..];
108+
if checksum_str.len() != CHECKSUM_LENGTH {
109+
return Err(Error::InvalidChecksumLength {
110+
actual: checksum_str.len(),
111+
expected: CHECKSUM_LENGTH,
112+
});
113+
}
114+
115+
let mut eng = Engine::new();
116+
eng.input_unchecked(s[..last_hash_pos].as_bytes());
44117

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-
)));
118+
let expected = eng.checksum_chars();
119+
let mut actual = ['_'; CHECKSUM_LENGTH];
120+
for (act, ch) in actual.iter_mut().zip(checksum_str.chars()) {
121+
*act = ch;
122+
}
123+
124+
if expected != actual {
125+
return Err(Error::InvalidChecksum { actual, expected });
54126
}
55127
}
56-
Ok(desc_str)
128+
Ok(&s[..last_hash_pos])
57129
}
58130

59131
/// An engine to compute a checksum from a string.
@@ -78,16 +150,18 @@ impl Engine {
78150
/// If this function returns an error, the `Engine` will be left in an indeterminate
79151
/// state! It is safe to continue feeding it data but the result will not be meaningful.
80152
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;
153+
for (pos, ch) in s.char_indices() {
154+
if !(32..127).contains(&u32::from(ch)) {
155+
return Err(Error::InvalidCharacter { ch, pos });
156+
}
157+
}
158+
self.input_unchecked(s.as_bytes());
159+
Ok(())
160+
}
90161

162+
fn input_unchecked(&mut self, s: &[u8]) {
163+
for ch in s {
164+
let pos = u64::from(CHAR_MAP[usize::from(*ch) - 32]);
91165
let fe = Fe32::try_from(pos & 31).expect("pos is valid because of the mask");
92166
self.inner.input_fe(fe);
93167

@@ -100,7 +174,6 @@ impl Engine {
100174
self.clscount = 0;
101175
}
102176
}
103-
Ok(())
104177
}
105178

106179
/// Obtains the checksum characters of all the data thus-far fed to the
@@ -192,7 +265,9 @@ mod test {
192265

193266
macro_rules! check_expected {
194267
($desc: expr, $checksum: expr) => {
195-
assert_eq!(desc_checksum($desc).unwrap(), $checksum);
268+
let mut eng = Engine::new();
269+
eng.input_unchecked($desc.as_bytes());
270+
assert_eq!(eng.checksum(), $checksum);
196271
};
197272
}
198273

@@ -229,8 +304,8 @@ mod test {
229304
let invalid_desc = format!("wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcL{}fjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)", sparkle_heart);
230305

231306
assert_eq!(
232-
desc_checksum(&invalid_desc).err().unwrap().to_string(),
233-
format!("Invalid descriptor: Invalid character in checksum: '{}'", sparkle_heart)
307+
verify_checksum(&invalid_desc).err().unwrap().to_string(),
308+
format!("invalid character '{}' (position 85)", sparkle_heart)
234309
);
235310
}
236311

src/descriptor/mod.rs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1053,8 +1053,7 @@ mod tests {
10531053
use bitcoin::sighash::EcdsaSighashType;
10541054
use bitcoin::{bip32, PublicKey, Sequence};
10551055

1056-
use super::checksum::desc_checksum;
1057-
use super::*;
1056+
use super::{checksum, *};
10581057
use crate::hex_script;
10591058
#[cfg(feature = "compiler")]
10601059
use crate::policy;
@@ -1066,10 +1065,10 @@ mod tests {
10661065
let desc = Descriptor::<String>::from_str(s).unwrap();
10671066
let output = desc.to_string();
10681067
let normalize_aliases = s.replace("c:pk_k(", "pk(").replace("c:pk_h(", "pkh(");
1069-
assert_eq!(
1070-
format!("{}#{}", &normalize_aliases, desc_checksum(&normalize_aliases).unwrap()),
1071-
output
1072-
);
1068+
1069+
let mut checksum_eng = checksum::Engine::new();
1070+
checksum_eng.input(&normalize_aliases).unwrap();
1071+
assert_eq!(format!("{}#{}", &normalize_aliases, checksum_eng.checksum()), output);
10731072
}
10741073

10751074
#[test]
@@ -1841,7 +1840,7 @@ mod tests {
18411840
($secp: ident,$($desc: expr),*) => {
18421841
$(
18431842
match Descriptor::parse_descriptor($secp, $desc) {
1844-
Err(Error::BadDescriptor(_)) => {},
1843+
Err(Error::Checksum(_)) => {},
18451844
Err(e) => panic!("Expected bad checksum for {}, got '{}'", $desc, e),
18461845
_ => panic!("Invalid checksum treated as valid: {}", $desc),
18471846
};

src/lib.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,8 @@ pub enum Error {
492492
Threshold(ThresholdError),
493493
/// Invalid threshold.
494494
ParseThreshold(ParseThresholdError),
495+
/// Checksum error parsing a descriptor.
496+
Checksum(descriptor::checksum::Error),
495497
}
496498

497499
// https://github.com/sipa/miniscript/pull/5 for discussion on this number
@@ -553,6 +555,7 @@ impl fmt::Display for Error {
553555
Error::RelativeLockTime(ref e) => e.fmt(f),
554556
Error::Threshold(ref e) => e.fmt(f),
555557
Error::ParseThreshold(ref e) => e.fmt(f),
558+
Error::Checksum(ref e) => e.fmt(f),
556559
}
557560
}
558561
}
@@ -603,6 +606,7 @@ impl error::Error for Error {
603606
RelativeLockTime(e) => Some(e),
604607
Threshold(e) => Some(e),
605608
ParseThreshold(e) => Some(e),
609+
Checksum(e) => Some(e),
606610
}
607611
}
608612
}
@@ -642,6 +646,11 @@ impl From<bitcoin::address::P2shError> for Error {
642646
fn from(e: bitcoin::address::P2shError) -> Error { Error::AddrP2shError(e) }
643647
}
644648

649+
#[doc(hidden)]
650+
impl From<descriptor::checksum::Error> for Error {
651+
fn from(e: descriptor::checksum::Error) -> Error { Error::Checksum(e) }
652+
}
653+
645654
#[doc(hidden)]
646655
#[cfg(feature = "compiler")]
647656
impl From<crate::policy::compiler::CompilerError> for Error {

0 commit comments

Comments
 (0)