From eb3f8fe528cd255f0b5cf9c2fb1384f5eee506f0 Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Thu, 5 Aug 2021 22:49:47 +0000 Subject: [PATCH] introduce blech32m format --- src/address.rs | 118 +++++++++++++++++++++++++++++++++++++++---------- src/blech32.rs | 68 +++++++++++++++++++++------- src/script.rs | 10 +++++ 3 files changed, 156 insertions(+), 40 deletions(-) diff --git a/src/address.rs b/src/address.rs index 529e4ff0..34777017 100644 --- a/src/address.rs +++ b/src/address.rs @@ -48,15 +48,18 @@ pub enum AddressError { /// Was unable to parse the address. InvalidAddress(String), /// Script version must be 0 to 16 inclusive - InvalidWitnessVersion, - /// Unsupported witness version - UnsupportedWitnessVersion(u8), + InvalidWitnessVersion(u8), + /// The witness program must be between 2 and 40 bytes in length. + InvalidWitnessProgramLength(usize), + /// A v0 witness program must be either of length 20 or 32. + InvalidSegwitV0ProgramLength(usize), + /// A v1+ witness program must use b(l)ech32m not b(l)ech32 + InvalidWitnessEncoding, + /// A v0 witness program must use b(l)ech32 not b(l)ech32m + InvalidSegwitV0Encoding, + /// An invalid blinding pubkey was encountered. InvalidBlindingPubKey(secp256k1_zkp::UpstreamError), - /// Given the program version, the length is invalid - /// - /// Version 0 scripts must be either 20 or 32 bytes - InvalidWitnessProgramLength, } impl fmt::Display for AddressError { @@ -68,16 +71,24 @@ impl fmt::Display for AddressError { AddressError::InvalidAddress(ref a) => { write!(f, "was unable to parse the address: {}", a) } - AddressError::UnsupportedWitnessVersion(ref wver) => { - write!(f, "unsupported witness version: {}", wver) + AddressError::InvalidWitnessVersion(ref wver) => { + write!(f, "invalid witness script version: {}", wver) + } + AddressError::InvalidWitnessProgramLength(ref len) => { + write!(f, "the witness program must be between 2 and 40 bytes in length, not {}", len) + } + AddressError::InvalidSegwitV0ProgramLength(ref len) => { + write!(f, "a v0 witness program must be length 20 or 32, not {}", len) } AddressError::InvalidBlindingPubKey(ref e) => { write!(f, "an invalid blinding pubkey was encountered: {}", e) } - AddressError::InvalidWitnessProgramLength => { - write!(f, "program length incompatible with version") + AddressError::InvalidWitnessEncoding => { + write!(f, "v1+ witness program must use b(l)ech32m not b(l)ech32") + } + AddressError::InvalidSegwitV0Encoding => { + write!(f, "v0 witness program must use b(l)ech32 not b(l)ech32m") } - AddressError::InvalidWitnessVersion => write!(f, "invalid witness script version"), } } } @@ -299,6 +310,11 @@ impl Address { version: u5::try_from_u8(0).expect("0<32"), program: script.as_bytes()[2..34].to_vec(), } + } else if script.is_v1plus_p2witprog() { + Payload::WitnessProgram { + version: u5::try_from_u8(script.as_bytes()[0] - 0x50).expect("0<32"), + program: script.as_bytes()[2..].to_vec(), + } } else { return None; }, @@ -351,10 +367,12 @@ impl Address { blinded: bool, params: &'static AddressParams, ) -> Result { - let payload = if !blinded { - bech32::decode(s).map_err(AddressError::Bech32)?.1 + let (payload, is_bech32m) = if !blinded { + let (_, payload, variant) = bech32::decode(s).map_err(AddressError::Bech32)?; + (payload, variant == bech32::Variant::Bech32m) } else { - blech32::decode(s).map_err(AddressError::Blech32)?.1 + let (_, payload, variant) = blech32::decode(s).map_err(AddressError::Blech32)?; + (payload, variant == blech32::Variant::Blech32m) }; if payload.is_empty() { @@ -373,19 +391,27 @@ impl Address { } (v[0], data_res.unwrap()) }; + + // Generic segwit checks. if version.to_u8() > 16 { - return Err(AddressError::InvalidWitnessVersion); + return Err(AddressError::InvalidWitnessVersion(version.to_u8())); } - - // Segwit version specific checks. - if version.to_u8() != 0 { - return Err(AddressError::UnsupportedWitnessVersion(version.to_u8())); + if data.len() < 2 || data.len() > 40 + if blinded { 33 } else { 0 } { + return Err(AddressError::InvalidWitnessProgramLength(data.len() - if blinded { 33 } else { 0 })); } + + // Specific segwit v0 check. if !blinded && version.to_u8() == 0 && data.len() != 20 && data.len() != 32 { - return Err(AddressError::InvalidWitnessProgramLength); + return Err(AddressError::InvalidSegwitV0ProgramLength(data.len())); } if blinded && version.to_u8() == 0 && data.len() != 53 && data.len() != 65 { - return Err(AddressError::InvalidWitnessProgramLength); + return Err(AddressError::InvalidSegwitV0ProgramLength(data.len() - 33)); + } + + if version.to_u8() == 0 && is_bech32m { + return Err(AddressError::InvalidSegwitV0Encoding); + } else if version.to_u8() > 0 && !is_bech32m { + return Err(AddressError::InvalidWitnessEncoding); } let (blinding_pubkey, program) = match blinded { @@ -530,9 +556,14 @@ impl fmt::Display for Address { data.extend_from_slice(&witprog); let mut b32_data = vec![witver]; b32_data.extend_from_slice(&data.to_base32()); - blech32::encode_to_fmt(fmt, &hrp, &b32_data) + if witver.to_u8() == 0 { + blech32::encode_to_fmt(fmt, &hrp, &b32_data, blech32::Variant::Blech32) + } else { + blech32::encode_to_fmt(fmt, &hrp, &b32_data, blech32::Variant::Blech32m) + } } else { - let mut bech32_writer = bech32::Bech32Writer::new(hrp, bech32::Variant::Bech32, fmt)?; + let var = if witver.to_u8() == 0 { bech32::Variant::Bech32 } else { bech32::Variant::Bech32m }; + let mut bech32_writer = bech32::Bech32Writer::new(hrp, var, fmt)?; bech32::WriteBase32::write_u5(&mut bech32_writer, witver)?; bech32::ToBase32::write_base32(&witprog, &mut bech32_writer) } @@ -769,4 +800,43 @@ mod test { roundtrips(&addr); } } + + #[test] + fn test_blech32_vectors() { + // taken from Elements test/functional/rpc_invalid_address_message.py + let address: Result = "el1qq0umk3pez693jrrlxz9ndlkuwne93gdu9g83mhhzuyf46e3mdzfpva0w48gqgzgrklncnm0k5zeyw8my2ypfsmxh4xcjh2rse".parse(); + assert!(address.is_ok()); + + let address: Result = "el1pq0umk3pez693jrrlxz9ndlkuwne93gdu9g83mhhzuyf46e3mdzfpva0w48gqgzgrklncnm0k5zeyw8my2ypfsxguu9nrdg2pc".parse(); + assert_eq!( + address.err().unwrap().to_string(), + "v1+ witness program must use b(l)ech32m not b(l)ech32", + ); + + let address: Result = "el1qq0umk3pez693jrrlxz9ndlkuwne93gdu9g83mhhzuyf46e3mdzfpva0w48gqgzgrklncnm0k5zeyw8my2ypfsnnmzrstzt7de".parse(); + assert_eq!( + address.err().unwrap().to_string(), + "v0 witness program must use b(l)ech32 not b(l)ech32m", + ); + + let address: Result = "ert130xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqqu2tys".parse(); + assert_eq!( + address.err().unwrap().to_string(), + "invalid witness script version: 17", + ); + + let address: Result = "el1pq0umk3pez693jrrlxz9ndlkuwne93gdu9g83mhhzuyf46e3mdzfpva0w48gqgzgrklncnm0k5zeyw8my2ypfsqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpe9jfn0gypaj".parse(); + assert_eq!( + address.err().unwrap().to_string(), + "the witness program must be between 2 and 40 bytes in length, not 41", + ); + + // "invalid prefix" gives a weird error message because we do + // a dumb prefix check before even attempting bech32 decoding + let address: Result = "rrr1qq0umk3pez693jrrlxz9ndlkuwne93gdu9g83mhhzuyf46e3mdzfpva0w48gqgzgrklncnm0k5zeyw8my2ypfs2d9rp7meq4kg".parse(); + assert_eq!( + address.err().unwrap().to_string(), + "base58 error: invalid base58 character 0x30", + ); + } } diff --git a/src/blech32.rs b/src/blech32.rs index fd35bcca..7536b325 100644 --- a/src/blech32.rs +++ b/src/blech32.rs @@ -48,10 +48,45 @@ use std::ascii::AsciiExt; use bitcoin::bech32::{u5, Error}; +/// Used for encode/decode operations for the two variants of Blech32 +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +pub enum Variant { + /// The original Blech32 + Blech32, + /// The improved Blech32m + Blech32m, +} + +const BLECH32_CONST: u64 = 1; +const BLECH32M_CONST: u64 = 0x455972a3350f7a1; + +impl Variant { + // Produce the variant based on the remainder of the polymod operation + fn from_remainder(c: u64) -> Option { + match c { + BLECH32_CONST => Some(Variant::Blech32), + BLECH32M_CONST => Some(Variant::Blech32m), + _ => None, + } + } + + fn constant(self) -> u64 { + match self { + Variant::Blech32 => BLECH32_CONST, + Variant::Blech32m => BLECH32M_CONST, + } + } +} + /// Encode a bech32 payload to an [fmt::Formatter]. -pub fn encode_to_fmt>(fmt: &mut fmt::Formatter, hrp: &str, data: T) -> fmt::Result { +pub fn encode_to_fmt>( + fmt: &mut dyn fmt::Write, + hrp: &str, + data: T, + variant: Variant, +) -> fmt::Result { let hrp_bytes: &[u8] = hrp.as_bytes(); - let checksum = create_checksum(hrp_bytes, data.as_ref()); + let checksum = create_checksum(hrp_bytes, data.as_ref(), variant); let data_part = data.as_ref().iter().chain(checksum.iter()); write!( @@ -68,7 +103,7 @@ pub fn encode_to_fmt>(fmt: &mut fmt::Formatter, hrp: &str, data: /// Decode a bech32 string into the raw HRP and the data bytes. /// The HRP is returned as it was found in the original string, /// so it can be either lower or upper case. -pub fn decode(s: &str) -> Result<(&str, Vec), Error> { +pub fn decode(s: &str) -> Result<(&str, Vec, Variant), Error> { // Ensure overall length is within bounds let len: usize = s.len(); // ELEMENTS: 8->14 @@ -144,23 +179,24 @@ pub fn decode(s: &str) -> Result<(&str, Vec), Error> { } // Ensure checksum - if !verify_checksum(&hrp_bytes, &data) { - return Err(Error::InvalidChecksum); - } + match verify_checksum(&raw_hrp.as_bytes(), &data) { + Some(variant) => { + // Remove checksum from data payload + let dbl: usize = data.len(); + data.truncate(dbl - 12); - // Remove checksum from data payload - let dbl: usize = data.len(); - data.truncate(dbl - 12); // ELEMENTS: 6->12 - - Ok((raw_hrp, data)) + Ok((raw_hrp, data, variant)) + } + None => Err(Error::InvalidChecksum), + } } -fn create_checksum(hrp: &[u8], data: &[u5]) -> Vec { +fn create_checksum(hrp: &[u8], data: &[u5], variant: Variant) -> Vec { let mut values: Vec = hrp_expand(hrp); values.extend_from_slice(data); // Pad with 12 zeros values.extend_from_slice(&[u5::try_from_u8(0).unwrap(); 12]); // ELEMENTS: 6->12 - let plm: u64 = polymod(&values) ^ 1; + let plm: u64 = polymod(&values) ^ variant.constant(); let mut checksum: Vec = Vec::new(); // ELEMENTS: 6->12 for p in 0..12 { @@ -169,10 +205,10 @@ fn create_checksum(hrp: &[u8], data: &[u5]) -> Vec { checksum } -fn verify_checksum(hrp: &[u8], data: &[u5]) -> bool { +fn verify_checksum(hrp: &[u8], data: &[u5]) -> Option { let mut exp = hrp_expand(hrp); exp.extend_from_slice(data); - polymod(&exp) == 1u64 + Variant::from_remainder(polymod(&exp)) } fn hrp_expand(hrp: &[u8]) -> Vec { @@ -256,7 +292,7 @@ mod test { #[test] fn test_checksum() { let data = vec![7,2,3,4,5,6,7,8,9,234,123,213,16]; - let cs = create_checksum(b"lq", &data.to_base32()); + let cs = create_checksum(b"lq", &data.to_base32(), Variant::Blech32); let expected_cs = vec![22,13,13,5,4,4,23,7,28,21,30,12]; for i in 0..expected_cs.len() { assert_eq!(expected_cs[i], *cs[i].as_ref()); diff --git a/src/script.rs b/src/script.rs index 507c413c..a4fb14da 100644 --- a/src/script.rs +++ b/src/script.rs @@ -386,6 +386,16 @@ impl Script { self.0[1] == opcodes::all::OP_PUSHBYTES_32.into_u8() } + /// Checks whether a script pubkey is a p2wsh output + #[inline] + pub fn is_v1plus_p2witprog(&self) -> bool { + self.0.len() > 1 && + self.0.len() == self.0[1] as usize + 2 && + self.0[0] >= opcodes::all::OP_PUSHNUM_1.into_u8() && + self.0[0] <= opcodes::all::OP_PUSHNUM_16.into_u8() && + self.0[1] <= opcodes::all::OP_PUSHBYTES_40.into_u8() + } + /// Checks whether a script pubkey is a p2wpkh output #[inline] pub fn is_v0_p2wpkh(&self) -> bool {