From 64c1ec0b764e034ba0d5dbae02fc9576d81c1467 Mon Sep 17 00:00:00 2001 From: Dr Maxim Orlovsky Date: Sat, 30 Jan 2021 20:50:04 +0100 Subject: [PATCH 1/2] WitnessVersion type --- src/blockdata/script.rs | 14 +-- src/util/address.rs | 252 +++++++++++++++++++++++++++++++++------- 2 files changed, 215 insertions(+), 51 deletions(-) diff --git a/src/blockdata/script.rs b/src/blockdata/script.rs index 18e2ea12..b7693905 100644 --- a/src/blockdata/script.rs +++ b/src/blockdata/script.rs @@ -41,6 +41,7 @@ use policy::DUST_RELAY_TX_FEE; #[cfg(feature="bitcoinconsensus")] use OutPoint; use util::ecdsa::PublicKey; +use util::address::WitnessVersion; #[derive(Clone, Default, PartialOrd, Ord, PartialEq, Eq, Hash)] /// A Bitcoin script @@ -268,23 +269,18 @@ impl Script { /// Generates P2WPKH-type of scriptPubkey pub fn new_v0_wpkh(pubkey_hash: &WPubkeyHash) -> Script { - Script::new_witness_program(::bech32::u5::try_from_u8(0).unwrap(), &pubkey_hash.to_vec()) + Script::new_witness_program(WitnessVersion::V0, &pubkey_hash.to_vec()) } /// Generates P2WSH-type of scriptPubkey with a given hash of the redeem script pub fn new_v0_wsh(script_hash: &WScriptHash) -> Script { - Script::new_witness_program(::bech32::u5::try_from_u8(0).unwrap(), &script_hash.to_vec()) + Script::new_witness_program(WitnessVersion::V0, &script_hash.to_vec()) } /// Generates P2WSH-type of scriptPubkey with a given hash of the redeem script - pub fn new_witness_program(ver: ::bech32::u5, program: &[u8]) -> Script { - let mut verop = ver.to_u8(); - assert!(verop <= 16, "incorrect witness version provided: {}", verop); - if verop > 0 { - verop = 0x50 + verop; - } + pub fn new_witness_program(version: WitnessVersion, program: &[u8]) -> Script { Builder::new() - .push_opcode(verop.into()) + .push_opcode(version.into()) .push_slice(&program) .into_script() } diff --git a/src/util/address.rs b/src/util/address.rs index 4c639a64..51e653e1 100644 --- a/src/util/address.rs +++ b/src/util/address.rs @@ -36,20 +36,22 @@ use prelude::*; use core::fmt; +use core::num::ParseIntError; use core::str::FromStr; #[cfg(feature = "std")] use std::error; use bech32; use hashes::Hash; use hash_types::{PubkeyHash, WPubkeyHash, ScriptHash, WScriptHash}; -use blockdata::script; +use blockdata::{script, opcodes}; use blockdata::constants::{PUBKEY_ADDRESS_PREFIX_MAIN, SCRIPT_ADDRESS_PREFIX_MAIN, PUBKEY_ADDRESS_PREFIX_TEST, SCRIPT_ADDRESS_PREFIX_TEST}; use network::constants::Network; use util::base58; use util::ecdsa; +use blockdata::script::Instruction; /// Address error. -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)] +#[derive(Debug, PartialEq, Eq, Clone)] pub enum Error { /// Base58 encoding error Base58(base58::Error), @@ -59,6 +61,10 @@ pub enum Error { EmptyBech32Payload, /// Script version must be 0 to 16 inclusive InvalidWitnessVersion(u8), + /// Unable to parse witness version from string + UnparsableWitnessVersion(ParseIntError), + /// Bitcoin script opcode does not match any known witness version, the script is malformed + MalformedWitnessVersion, /// 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. @@ -74,6 +80,8 @@ impl fmt::Display for Error { Error::Bech32(ref e) => write!(f, "bech32: {}", e), Error::EmptyBech32Payload => write!(f, "the bech32 payload was empty"), Error::InvalidWitnessVersion(v) => write!(f, "invalid witness script version: {}", v), + Error::UnparsableWitnessVersion(ref e) => write!(f, "Incorrect format of a witness version byte: {}", e), + Error::MalformedWitnessVersion => f.write_str("bitcoin script opcode does not match any known witness version, the script is malformed"), Error::InvalidWitnessProgramLength(l) => write!(f, "the witness program must be between 2 and 40 bytes in length: length={}", l, ), @@ -93,6 +101,7 @@ impl ::std::error::Error for Error { match *self { Error::Base58(ref e) => Some(e), Error::Bech32(ref e) => Some(e), + Error::UnparsableWitnessVersion(ref e) => Some(e), _ => None, } } @@ -149,6 +158,176 @@ impl FromStr for AddressType { } } +/// Version of the WitnessProgram: first byte of `scriptPubkey` in +/// transaction output for transactions starting with opcodes ranging from 0 +/// to 16 (inclusive). +/// +/// Structure helps to limit possible version of the witness according to the +/// specification; if a plain `u8` type was be used instead it would mean that +/// version may be > 16, which is incorrect. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +#[repr(u8)] +pub enum WitnessVersion { + /// Initial version of Witness Program. Used for P2WPKH and P2WPK outputs + V0 = 0, + /// Version of Witness Program used for Taproot P2TR outputs + V1 = 1, + /// Future (unsupported) version of Witness Program + V2 = 2, + /// Future (unsupported) version of Witness Program + V3 = 3, + /// Future (unsupported) version of Witness Program + V4 = 4, + /// Future (unsupported) version of Witness Program + V5 = 5, + /// Future (unsupported) version of Witness Program + V6 = 6, + /// Future (unsupported) version of Witness Program + V7 = 7, + /// Future (unsupported) version of Witness Program + V8 = 8, + /// Future (unsupported) version of Witness Program + V9 = 9, + /// Future (unsupported) version of Witness Program + V10 = 10, + /// Future (unsupported) version of Witness Program + V11 = 11, + /// Future (unsupported) version of Witness Program + V12 = 12, + /// Future (unsupported) version of Witness Program + V13 = 13, + /// Future (unsupported) version of Witness Program + V14 = 14, + /// Future (unsupported) version of Witness Program + V15 = 15, + /// Future (unsupported) version of Witness Program + V16 = 16, +} + +/// Prints [`WitnessVersion`] number (from 0 to 16) as integer, without +/// any prefix or suffix. +impl fmt::Display for WitnessVersion { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", *self as u8) + } +} + + +impl FromStr for WitnessVersion { + type Err = Error; + + fn from_str(s: &str) -> Result { + let version = s.parse().map_err(|err| Error::UnparsableWitnessVersion(err))?; + WitnessVersion::from_num(version) + } +} + +impl WitnessVersion { + /// Converts 5-bit unsigned integer value matching single symbol from Bech32(m) address encoding + /// ([`bech32::u5`]) into [`WitnessVersion`] variant. + /// + /// # Returns + /// Version of the Witness program + /// + /// # Errors + /// If the integer does not corresponds to any witness version, errors with + /// [`Error::InvalidWitnessVersion`] + pub fn from_u5(value: ::bech32::u5) -> Result { + WitnessVersion::from_num(value.to_u8()) + } + + /// Converts 8-bit unsigned integer value into [`WitnessVersion`] variant. + /// + /// # Returns + /// Version of the Witness program + /// + /// # Errors + /// If the integer does not corresponds to any witness version, errors with + /// [`Error::InvalidWitnessVersion`] + pub fn from_num(no: u8) -> Result { + Ok(match no { + 0 => WitnessVersion::V0, + 1 => WitnessVersion::V1, + 2 => WitnessVersion::V2, + 3 => WitnessVersion::V3, + 4 => WitnessVersion::V4, + 5 => WitnessVersion::V5, + 6 => WitnessVersion::V6, + 7 => WitnessVersion::V7, + 8 => WitnessVersion::V8, + 9 => WitnessVersion::V9, + 10 => WitnessVersion::V10, + 11 => WitnessVersion::V11, + 12 => WitnessVersion::V12, + 13 => WitnessVersion::V13, + 14 => WitnessVersion::V14, + 15 => WitnessVersion::V15, + 16 => WitnessVersion::V16, + wrong => Err(Error::InvalidWitnessVersion(wrong))?, + }) + } + + /// Converts bitcoin script opcode into [`WitnessVersion`] variant. + /// + /// # Returns + /// Version of the Witness program (for opcodes in range of `OP_0`..`OP_16`) + /// + /// # Errors + /// If the opcode does not correspond to any witness version, errors with + /// [`Error::UnparsableWitnessVersion`] + pub fn from_opcode(opcode: opcodes::All) -> Result { + match opcode.into_u8() { + 0 => Ok(WitnessVersion::V0), + version if version >= opcodes::all::OP_PUSHNUM_1.into_u8() && version <= opcodes::all::OP_PUSHNUM_16.into_u8() => + WitnessVersion::from_num(version - opcodes::all::OP_PUSHNUM_1.into_u8() + 1), + _ => Err(Error::MalformedWitnessVersion) + } + } + + /// Converts bitcoin script [`Instruction`] (parsed opcode) into [`WitnessVersion`] variant. + /// + /// # Returns + /// Version of the Witness program for [`Instruction::Op`] and [`Instruction::PushBytes`] with + /// byte value within `1..=16` range. + /// + /// # Errors + /// If the opcode does not correspond to any witness version, errors with + /// [`Error::UnparsableWitnessVersion`] for the rest of opcodes + pub fn from_instruction(instruction: Instruction) -> Result { + match instruction { + Instruction::Op(op) => WitnessVersion::from_opcode(op), + Instruction::PushBytes(bytes) if bytes.len() == 0 => Ok(WitnessVersion::V0), + Instruction::PushBytes(_) => Err(Error::MalformedWitnessVersion), + } + } + + /// Returns integer version number representation for a given [`WitnessVersion`] value. + /// + /// NB: this is not the same as an integer representation of the opcode signifying witness + /// version in bitcoin script. Thus, there is no function to directly convert witness version + /// into a byte since the conversion requires context (bitcoin script or just a version number) + pub fn into_num(self) -> u8 { + self as u8 + } +} + +impl From for ::bech32::u5 { + /// Converts [`WitnessVersion`] instance into corresponding Bech32(m) u5-value ([`bech32::u5`]) + fn from(version: WitnessVersion) -> Self { + ::bech32::u5::try_from_u8(version.into_num()).expect("WitnessVersion must be 0..=16") + } +} + +impl From for opcodes::All { + /// Converts [`WitnessVersion`] instance into corresponding Bitcoin scriptopcode (`OP_0`..`OP_16`) + fn from(version: WitnessVersion) -> opcodes::All { + match version { + WitnessVersion::V0 => opcodes::all::OP_PUSHBYTES_0, + no => opcodes::All::from(opcodes::all::OP_PUSHNUM_1.into_u8() + no.into_num() - 1) + } + } +} + /// The method used to produce an address #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum Payload { @@ -159,7 +338,7 @@ pub enum Payload { /// Segwit addresses WitnessProgram { /// The witness program version - version: bech32::u5, + version: WitnessVersion, /// The witness program program: Vec, }, @@ -169,23 +348,17 @@ impl Payload { /// Get a [Payload] from an output script (scriptPubkey). pub fn from_script(script: &script::Script) -> Option { Some(if script.is_p2pkh() { - Payload::PubkeyHash(PubkeyHash::from_slice(&script.as_bytes()[3..23]).unwrap()) + let mut hash_inner = [0u8; 20]; + hash_inner.copy_from_slice(&script.as_bytes()[3..23]); + Payload::PubkeyHash(PubkeyHash::from_inner(hash_inner)) } else if script.is_p2sh() { - Payload::ScriptHash(ScriptHash::from_slice(&script.as_bytes()[2..22]).unwrap()) + let mut hash_inner = [0u8; 20]; + hash_inner.copy_from_slice(&script.as_bytes()[2..22]); + Payload::ScriptHash(ScriptHash::from_inner(hash_inner)) } else if script.is_witness_program() { - // We can unwrap the u5 check and assume script length - // because [Script::is_witness_program] makes sure of this. Payload::WitnessProgram { - version: { - // Since we passed the [is_witness_program] check, - // the first byte is either 0x00 or 0x50 + version. - let mut verop = script.as_bytes()[0]; - if verop > 0x50 { - verop -= 0x50; - } - bech32::u5::try_from_u8(verop).expect("checked before") - }, - program: script.as_bytes()[2..].to_vec(), + version: WitnessVersion::from_opcode(opcodes::All::from(script[0])).ok()?, + program: script[2..].to_vec(), } } else { return None; @@ -200,9 +373,9 @@ impl Payload { Payload::ScriptHash(ref hash) => script::Script::new_p2sh(hash), Payload::WitnessProgram { - version: ver, + version, program: ref prog, - } => script::Script::new_witness_program(ver, prog) + } => script::Script::new_witness_program(version, prog) } } } @@ -256,7 +429,7 @@ impl Address { Ok(Address { network: network, payload: Payload::WitnessProgram { - version: bech32::u5::try_from_u8(0).expect("0<32"), + version: WitnessVersion::V0, program: WPubkeyHash::from_engine(hash_engine)[..].to_vec(), }, }) @@ -289,7 +462,7 @@ impl Address { Address { network: network, payload: Payload::WitnessProgram { - version: bech32::u5::try_from_u8(0).expect("0<32"), + version: WitnessVersion::V0, program: WScriptHash::hash(&script[..])[..].to_vec(), }, } @@ -310,18 +483,18 @@ impl Address { } /// Get the address type of the address. - /// None if unknown or non-standard. + /// None if unknown, non-standard or related to the future witness version. pub fn address_type(&self) -> Option { match self.payload { Payload::PubkeyHash(_) => Some(AddressType::P2pkh), Payload::ScriptHash(_) => Some(AddressType::P2sh), Payload::WitnessProgram { - version: ver, + version, program: ref prog, } => { // BIP-141 p2wpkh or p2wsh addresses. - match ver.to_u8() { - 0 => match prog.len() { + match version { + WitnessVersion::V0 => match prog.len() { 20 => Some(AddressType::P2wpkh), 32 => Some(AddressType::P2wsh), _ => None, @@ -335,7 +508,7 @@ impl Address { /// Check whether or not the address is following Bitcoin /// standardness rules. /// - /// Segwit addresses with unassigned witness versions or non-standard + /// SegWit addresses with unassigned witness versions or non-standard /// program sizes are considered non-standard. pub fn is_standard(&self) -> bool { self.address_type().is_some() @@ -428,7 +601,7 @@ impl fmt::Display for Address { base58::check_encode_slice_to_fmt(fmt, &prefixed[..]) } Payload::WitnessProgram { - version: ver, + version, program: ref prog, } => { let hrp = match self.network { @@ -436,7 +609,7 @@ impl fmt::Display for Address { Network::Testnet | Network::Signet => "tb", Network::Regtest => "bcrt", }; - let bech_ver = if ver.to_u8() > 0 { bech32::Variant::Bech32m } else { bech32::Variant::Bech32 }; + let bech_ver = if version.into_num() > 0 { bech32::Variant::Bech32m } else { bech32::Variant::Bech32 }; let mut upper_writer; let writer = if fmt.alternate() { upper_writer = UpperWriter(fmt); @@ -445,7 +618,7 @@ impl fmt::Display for Address { fmt as &mut dyn fmt::Write }; let mut bech32_writer = bech32::Bech32Writer::new(hrp, bech_ver, writer)?; - bech32::WriteBase32::write_u5(&mut bech32_writer, ver)?; + bech32::WriteBase32::write_u5(&mut bech32_writer, version.into())?; bech32::ToBase32::write_base32(&prog, &mut bech32_writer) } } @@ -493,28 +666,24 @@ impl FromStr for Address { } // Get the script version and program (converted from 5-bit to 8-bit) - let (version, program): (bech32::u5, Vec) = { + let (version, program): (WitnessVersion, Vec) = { let (v, p5) = payload.split_at(1); - (v[0], bech32::FromBase32::from_base32(p5)?) + (WitnessVersion::from_u5(v[0])?, bech32::FromBase32::from_base32(p5)?) }; - // Generic segwit checks. - if version.to_u8() > 16 { - return Err(Error::InvalidWitnessVersion(version.to_u8())); - } if program.len() < 2 || program.len() > 40 { return Err(Error::InvalidWitnessProgramLength(program.len())); } // Specific segwit v0 check. - if version.to_u8() == 0 && (program.len() != 20 && program.len() != 32) { + if version == WitnessVersion::V0 && (program.len() != 20 && program.len() != 32) { return Err(Error::InvalidSegwitV0ProgramLength(program.len())); } // Bech32 encoding check - if (version.to_u8() > 0 && variant != bech32::Variant::Bech32m) || - (version.to_u8() == 0 && variant != bech32::Variant::Bech32) { - return Err(Error::InvalidWitnessVersion(version.to_u8())) + if (version.into_num() > 0 && variant != bech32::Variant::Bech32m) || + (version.into_num() == 0 && variant != bech32::Variant::Bech32) { + return Err(Error::InvalidWitnessVersion(version.into_num())) } return Ok(Address { @@ -710,14 +879,13 @@ mod tests { #[test] fn test_non_existent_segwit_version() { - let version = 13; // 40-byte program let program = hex!( "654f6ea368e0acdfd92976b7c2103a1b26313f430654f6ea368e0acdfd92976b7c2103a1b26313f4" ); let addr = Address { payload: Payload::WitnessProgram { - version: bech32::u5::try_from_u8(version).expect("0<32"), + version: WitnessVersion::V13, program: program, }, network: Network::Bitcoin, @@ -887,7 +1055,7 @@ mod tests { ]; let segwit_payload = (0..=16).map(|version| { Payload::WitnessProgram { - version: bech32::u5::try_from_u8(version).unwrap(), + version: WitnessVersion::from_num(version).unwrap(), program: vec![] } }).collect::>(); From ecc400826c5eebf4c8bf3f28219af348a1ba0191 Mon Sep 17 00:00:00 2001 From: Dr Maxim Orlovsky Date: Sun, 1 Aug 2021 20:37:32 +0200 Subject: [PATCH 2/2] Updating Script::is_witness_program to use new WitnessVersion --- src/blockdata/script.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/blockdata/script.rs b/src/blockdata/script.rs index b7693905..4ea23d4a 100644 --- a/src/blockdata/script.rs +++ b/src/blockdata/script.rs @@ -367,17 +367,17 @@ impl Script { // push opcode (for 0 to 16) followed by a data push between 2 and 40 bytes gets a new // special meaning. The value of the first push is called the "version byte". The following // byte vector pushed is called the "witness program". - let min_vernum: u8 = opcodes::all::OP_PUSHNUM_1.into_u8(); - let max_vernum: u8 = opcodes::all::OP_PUSHNUM_16.into_u8(); - self.0.len() >= 4 - && self.0.len() <= 42 - // Version 0 or PUSHNUM_1-PUSHNUM_16 - && (self.0[0] == 0 || self.0[0] >= min_vernum && self.0[0] <= max_vernum) - // Second byte push opcode 2-40 bytes - && self.0[1] >= opcodes::all::OP_PUSHBYTES_2.into_u8() - && self.0[1] <= opcodes::all::OP_PUSHBYTES_40.into_u8() + let script_len = self.0.len(); + if script_len < 4 || script_len > 42 { + return false + } + let ver_opcode = opcodes::All::from(self.0[0]); // Version 0 or PUSHNUM_1-PUSHNUM_16 + let push_opbyte = self.0[1]; // Second byte push opcode 2-40 bytes + WitnessVersion::from_opcode(ver_opcode).is_ok() + && push_opbyte >= opcodes::all::OP_PUSHBYTES_2.into_u8() + && push_opbyte <= opcodes::all::OP_PUSHBYTES_40.into_u8() // Check that the rest of the script has the correct size - && self.0.len() - 2 == self.0[1] as usize + && script_len - 2 == push_opbyte as usize } /// Checks whether a script pubkey is a p2wsh output