Skip to content

Commit

Permalink
Merge pull request #89 from ElementsProject/2021-08--blech32m
Browse files Browse the repository at this point in the history
introduce blech32m format
  • Loading branch information
stevenroose authored Dec 16, 2021
2 parents f2cae61 + eb3f8fe commit d988bd7
Show file tree
Hide file tree
Showing 3 changed files with 156 additions and 40 deletions.
118 changes: 94 additions & 24 deletions src/address.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,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 {
Expand All @@ -64,16 +67,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"),
}
}
}
Expand Down Expand Up @@ -295,6 +306,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;
},
Expand Down Expand Up @@ -347,10 +363,12 @@ impl Address {
blinded: bool,
params: &'static AddressParams,
) -> Result<Address, AddressError> {
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() {
Expand All @@ -369,19 +387,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 {
Expand Down Expand Up @@ -526,9 +552,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)
}
Expand Down Expand Up @@ -765,4 +796,43 @@ mod test {
roundtrips(&addr);
}
}

#[test]
fn test_blech32_vectors() {
// taken from Elements test/functional/rpc_invalid_address_message.py
let address: Result<Address, _> = "el1qq0umk3pez693jrrlxz9ndlkuwne93gdu9g83mhhzuyf46e3mdzfpva0w48gqgzgrklncnm0k5zeyw8my2ypfsmxh4xcjh2rse".parse();
assert!(address.is_ok());

let address: Result<Address, _> = "el1pq0umk3pez693jrrlxz9ndlkuwne93gdu9g83mhhzuyf46e3mdzfpva0w48gqgzgrklncnm0k5zeyw8my2ypfsxguu9nrdg2pc".parse();
assert_eq!(
address.err().unwrap().to_string(),
"v1+ witness program must use b(l)ech32m not b(l)ech32",
);

let address: Result<Address, _> = "el1qq0umk3pez693jrrlxz9ndlkuwne93gdu9g83mhhzuyf46e3mdzfpva0w48gqgzgrklncnm0k5zeyw8my2ypfsnnmzrstzt7de".parse();
assert_eq!(
address.err().unwrap().to_string(),
"v0 witness program must use b(l)ech32 not b(l)ech32m",
);

let address: Result<Address, _> = "ert130xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqqu2tys".parse();
assert_eq!(
address.err().unwrap().to_string(),
"invalid witness script version: 17",
);

let address: Result<Address, _> = "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<Address, _> = "rrr1qq0umk3pez693jrrlxz9ndlkuwne93gdu9g83mhhzuyf46e3mdzfpva0w48gqgzgrklncnm0k5zeyw8my2ypfs2d9rp7meq4kg".parse();
assert_eq!(
address.err().unwrap().to_string(),
"base58 error: invalid base58 character 0x30",
);
}
}
68 changes: 52 additions & 16 deletions src/blech32.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,45 @@ use std::fmt;

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<Self> {
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<T: AsRef<[u5]>>(fmt: &mut fmt::Formatter, hrp: &str, data: T) -> fmt::Result {
pub fn encode_to_fmt<T: AsRef<[u5]>>(
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!(
Expand All @@ -64,7 +99,7 @@ pub fn encode_to_fmt<T: AsRef<[u5]>>(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<u5>), Error> {
pub fn decode(s: &str) -> Result<(&str, Vec<u5>, Variant), Error> {
// Ensure overall length is within bounds
let len: usize = s.len();
// ELEMENTS: 8->14
Expand Down Expand Up @@ -140,23 +175,24 @@ pub fn decode(s: &str) -> Result<(&str, Vec<u5>), 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<u5> {
fn create_checksum(hrp: &[u8], data: &[u5], variant: Variant) -> Vec<u5> {
let mut values: Vec<u5> = 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<u5> = Vec::new();
// ELEMENTS: 6->12
for p in 0..12 {
Expand All @@ -165,10 +201,10 @@ fn create_checksum(hrp: &[u8], data: &[u5]) -> Vec<u5> {
checksum
}

fn verify_checksum(hrp: &[u8], data: &[u5]) -> bool {
fn verify_checksum(hrp: &[u8], data: &[u5]) -> Option<Variant> {
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<u5> {
Expand Down Expand Up @@ -252,7 +288,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());
Expand Down
10 changes: 10 additions & 0 deletions src/script.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit d988bd7

Please sign in to comment.