From 385a6579743bdeb0abeb31253e81ba17a3e257fb Mon Sep 17 00:00:00 2001 From: Steven Roose Date: Wed, 24 Apr 2019 15:17:43 +0100 Subject: [PATCH 01/11] Refactor Address - use AddressError instead of encode::Error - replace using bech32-bitcoin with Payload::WitnessProgram variant --- Cargo.toml | 2 +- src/consensus/encode.rs | 13 -- src/lib.rs | 2 +- src/util/address.rs | 256 +++++++++++++++++++++++++++++----------- 4 files changed, 187 insertions(+), 86 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 10f614bf..94e92096 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ unstable = [] use-serde = ["serde", "bitcoin_hashes/serde"] [dependencies] -bitcoin-bech32 = "0.9.0" +bech32 = "0.7.1" byteorder = "1.2" bitcoin_hashes = "0.7" bitcoinconsensus = { version = "0.16", optional = true } diff --git a/src/consensus/encode.rs b/src/consensus/encode.rs index 7f6ecdcc..cf4c4d52 100644 --- a/src/consensus/encode.rs +++ b/src/consensus/encode.rs @@ -38,7 +38,6 @@ use std::io::{Cursor, Read, Write}; use byteorder::{LittleEndian, WriteBytesExt, ReadBytesExt}; use hex::encode as hex_encode; -use bitcoin_bech32; use bitcoin_hashes::{sha256d, Hash as HashTrait}; use secp256k1; @@ -56,8 +55,6 @@ pub enum Error { Io(io::Error), /// Base58 encoding error Base58(base58::Error), - /// Bech32 encoding error - Bech32(bitcoin_bech32::Error), /// Error from the `byteorder` crate ByteOrder(io::Error), /// secp-related error @@ -104,7 +101,6 @@ impl fmt::Display for Error { match *self { Error::Io(ref e) => fmt::Display::fmt(e, f), Error::Base58(ref e) => fmt::Display::fmt(e, f), - Error::Bech32(ref e) => fmt::Display::fmt(e, f), Error::ByteOrder(ref e) => fmt::Display::fmt(e, f), Error::Secp256k1(ref e) => fmt::Display::fmt(e, f), Error::Psbt(ref e) => fmt::Display::fmt(e, f), @@ -126,7 +122,6 @@ impl error::Error for Error { match *self { Error::Io(ref e) => Some(e), Error::Base58(ref e) => Some(e), - Error::Bech32(ref e) => Some(e), Error::ByteOrder(ref e) => Some(e), Error::Secp256k1(ref e) => Some(e), Error::Psbt(ref e) => Some(e), @@ -146,7 +141,6 @@ impl error::Error for Error { match *self { Error::Io(ref e) => e.description(), Error::Base58(ref e) => e.description(), - Error::Bech32(ref e) => e.description(), Error::ByteOrder(ref e) => e.description(), Error::Secp256k1(ref e) => e.description(), Error::Psbt(ref e) => e.description(), @@ -170,13 +164,6 @@ impl From for Error { } } -#[doc(hidden)] -impl From for Error { - fn from(e: bitcoin_bech32::Error) -> Error { - Error::Bech32(e) - } -} - #[doc(hidden)] impl From for Error { fn from(e: secp256k1::Error) -> Error { diff --git a/src/lib.rs b/src/lib.rs index 1a20fa6a..d235717e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -44,7 +44,7 @@ #![forbid(unsafe_code)] -extern crate bitcoin_bech32; +extern crate bech32; extern crate bitcoin_hashes; extern crate byteorder; extern crate hex; diff --git a/src/util/address.rs b/src/util/address.rs index 74c58748..66b701c2 100644 --- a/src/util/address.rs +++ b/src/util/address.rs @@ -43,16 +43,85 @@ use std::fmt::{self, Display, Formatter}; use std::str::FromStr; -use bitcoin_bech32::{self, WitnessProgram, u5}; +use bech32::{self, u5, FromBase32, ToBase32}; use bitcoin_hashes::{hash160, Hash}; use blockdata::opcodes; use blockdata::script; use network::constants::Network; -use consensus::encode; use util::base58; use util::key; +/// Address error. +#[derive(Debug, PartialEq)] +pub enum Error { + /// Base58 encoding error + Base58(base58::Error), + /// Bech32 encoding error + Bech32(bech32::Error), + /// The bech32 payload was empty + EmptyBech32Payload, + /// Script version must be 0 to 16 inclusive + 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), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let desc = ::std::error::Error::description(self); + match *self { + Error::Base58(ref e) => write!(f, "{}: {}", desc, e), + Error::Bech32(ref e) => write!(f, "{}: {}", desc, e), + Error::InvalidWitnessVersion(v) => write!(f, "{}: {}", desc, v), + Error::InvalidWitnessProgramLength(l) => write!(f, "{}: {}", desc, l), + Error::InvalidSegwitV0ProgramLength(l) => write!(f, "{}: {}", desc, l), + _ => f.write_str(desc), + } + } +} + +impl ::std::error::Error for Error { + fn cause(&self) -> Option<&::std::error::Error> { + match *self { + Error::Base58(ref e) => Some(e), + Error::Bech32(ref e) => Some(e), + _ => None, + } + } + + fn description(&self) -> &str { + match *self { + Error::Base58(..) => "base58 error", + Error::Bech32(..) => "bech32 error", + Error::EmptyBech32Payload => "the bech32 payload was empty", + Error::InvalidWitnessVersion(..) => "invalid witness script version", + Error::InvalidWitnessProgramLength(..) => { + "the witness program must be between 2 and 40 bytes in length" + }, + Error::InvalidSegwitV0ProgramLength(..) => { + "a v0 witness program must be either of length 20 or 32" + }, + } + } +} + +#[doc(hidden)] +impl From for Error { + fn from(e: base58::Error) -> Error { + Error::Base58(e) + } +} + +#[doc(hidden)] +impl From for Error { + fn from(e: bech32::Error) -> Error { + Error::Bech32(e) + } +} + /// The method used to produce an address #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum Payload { @@ -61,7 +130,12 @@ pub enum Payload { /// P2SH address ScriptHash(hash160::Hash), /// Segwit address - WitnessProgram(WitnessProgram), + WitnessProgram { + /// The witness program version + version: u5, + /// The witness program + program: Vec, + }, } #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -106,11 +180,10 @@ impl Address { Address { network: network, - payload: Payload::WitnessProgram( - // unwrap is safe as witness program is known to be correct as above - WitnessProgram::new(u5::try_from_u8(0).expect("0<32"), - hash160::Hash::from_engine(hash_engine)[..].to_vec(), - Address::bech_network(network)).unwrap()) + payload: Payload::WitnessProgram { + version: u5::try_from_u8(0).expect("0<32"), + program: hash160::Hash::from_engine(hash_engine)[..].to_vec(), + }, } } @@ -138,14 +211,10 @@ impl Address { Address { network: network, - payload: Payload::WitnessProgram( - // unwrap is safe as witness program is known to be correct as above - WitnessProgram::new( - u5::try_from_u8(0).expect("0<32"), - sha256::Hash::hash(&script[..])[..].to_vec(), - Address::bech_network(network) - ).unwrap() - ), + payload: Payload::WitnessProgram { + version: u5::try_from_u8(0).expect("0<32"), + program: sha256::Hash::hash(&script[..])[..].to_vec(), + }, } } @@ -164,39 +233,44 @@ impl Address { } } - #[inline] - /// convert Network to bech32 network (this should go away soon) - fn bech_network (network: Network) -> bitcoin_bech32::constants::Network { - match network { - Network::Bitcoin => bitcoin_bech32::constants::Network::Bitcoin, - Network::Testnet => bitcoin_bech32::constants::Network::Testnet, - Network::Regtest => bitcoin_bech32::constants::Network::Regtest, + /// Check whether or not the address is following Bitcoin + /// standardness rules. + /// + /// Segwit addresses with unassigned witness versions or non-standard + /// program sizes are considered non-standard. + pub fn is_standard(&self) -> bool { + match self.payload { + Payload::WitnessProgram { + version: ver, + program: ref prog, + } => { + // BIP-141 p2wpkh or p2wsh addresses. + ver.to_u8() == 0 && (prog.len() == 20 || prog.len() == 32) + } + Payload::PubkeyHash(_) => true, + Payload::ScriptHash(_) => true, } } /// Generates a script pubkey spending to this address pub fn script_pubkey(&self) -> script::Script { match self.payload { - Payload::PubkeyHash(ref hash) => { - script::Builder::new() - .push_opcode(opcodes::all::OP_DUP) - .push_opcode(opcodes::all::OP_HASH160) - .push_slice(&hash[..]) - .push_opcode(opcodes::all::OP_EQUALVERIFY) - .push_opcode(opcodes::all::OP_CHECKSIG) - }, - Payload::ScriptHash(ref hash) => { - script::Builder::new() - .push_opcode(opcodes::all::OP_HASH160) - .push_slice(&hash[..]) - .push_opcode(opcodes::all::OP_EQUAL) - }, - Payload::WitnessProgram(ref witprog) => { - script::Builder::new() - .push_int(witprog.version().to_u8() as i64) - .push_slice(witprog.program()) - } - }.into_script() + Payload::PubkeyHash(ref hash) => script::Builder::new() + .push_opcode(opcodes::all::OP_DUP) + .push_opcode(opcodes::all::OP_HASH160) + .push_slice(&hash[..]) + .push_opcode(opcodes::all::OP_EQUALVERIFY) + .push_opcode(opcodes::all::OP_CHECKSIG), + Payload::ScriptHash(ref hash) => script::Builder::new() + .push_opcode(opcodes::all::OP_HASH160) + .push_slice(&hash[..]) + .push_opcode(opcodes::all::OP_EQUAL), + Payload::WitnessProgram { + version: ver, + program: ref prog, + } => script::Builder::new().push_int(ver.to_u8() as i64).push_slice(&prog), + } + .into_script() } } @@ -220,48 +294,88 @@ impl Display for Address { }; prefixed[1..].copy_from_slice(&hash[..]); base58::check_encode_slice_to_fmt(fmt, &prefixed[..]) - }, - Payload::WitnessProgram(ref witprog) => { - fmt.write_str(&witprog.to_address()) - }, + } + Payload::WitnessProgram { + version: ver, + program: ref prog, + } => { + let mut b32_data = vec![ver]; + b32_data.extend_from_slice(&prog.to_base32()); + let hrp = match self.network { + Network::Bitcoin => "bc", + Network::Testnet => "tb", + Network::Regtest => "bcrt", + }; + bech32::encode_to_fmt(fmt, &hrp, &b32_data).expect("only errors on invalid HRP") + } } } } -impl FromStr for Address { - type Err = encode::Error; +/// Extract the bech32 prefix. +/// Returns the same slice when no prefix is found. +fn find_bech32_prefix(bech32: &str) -> &str { + // Split at the last occurrence of the separator character '1'. + match bech32.rfind("1") { + None => bech32, + Some(sep) => bech32.split_at(sep).0, + } +} - fn from_str(s: &str) -> Result { - // bech32 (note that upper or lowercase is allowed but NOT mixed case) - if s.starts_with("bc1") || s.starts_with("BC1") || - s.starts_with("tb1") || s.starts_with("TB1") || - s.starts_with("bcrt1") || s.starts_with("BCRT1") - { - let witprog = WitnessProgram::from_address(s)?; - let network = match witprog.network() { - bitcoin_bech32::constants::Network::Bitcoin => Network::Bitcoin, - bitcoin_bech32::constants::Network::Testnet => Network::Testnet, - bitcoin_bech32::constants::Network::Regtest => Network::Regtest, - _ => panic!("unknown network") - }; - if witprog.version().to_u8() != 0 { - return Err(encode::Error::UnsupportedWitnessVersion(witprog.version().to_u8())); +impl FromStr for Address { + type Err = Error; + + fn from_str(s: &str) -> Result { + // try bech32 + let bech32_network = match find_bech32_prefix(s) { + // note that upper or lowercase is allowed but NOT mixed case + "bc" | "BC" => Some(Network::Bitcoin), + "tb" | "TB" => Some(Network::Testnet), + "bcrt" | "BCRT" => Some(Network::Regtest), + _ => None, + }; + if let Some(network) = bech32_network { + // decode as bech32 + let (_, payload) = bech32::decode(s)?; + if payload.len() == 0 { + return Err(Error::EmptyBech32Payload); } + + // Get the script version and program (converted from 5-bit to 8-bit) + let (version, program) = { + let (v, p5) = payload.split_at(1); + (v[0], Vec::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) { + return Err(Error::InvalidSegwitV0ProgramLength(program.len())); + } + return Ok(Address { + payload: Payload::WitnessProgram { + version: version, + program: program, + }, network: network, - payload: Payload::WitnessProgram(witprog), }); } + // Base58 if s.len() > 50 { - return Err(encode::Error::Base58(base58::Error::InvalidLength(s.len() * 11 / 15))); + return Err(Error::Base58(base58::Error::InvalidLength(s.len() * 11 / 15))); } - - // Base 58 let data = base58::from_check(s)?; - if data.len() != 21 { - return Err(encode::Error::Base58(base58::Error::InvalidLength(data.len()))); + return Err(Error::Base58(base58::Error::InvalidLength(data.len()))); } let (network, payload) = match data[0] { @@ -281,7 +395,7 @@ impl FromStr for Address { Network::Testnet, Payload::ScriptHash(hash160::Hash::from_slice(&data[1..]).unwrap()) ), - x => return Err(encode::Error::Base58(base58::Error::InvalidVersion(vec![x]))) + x => return Err(Error::Base58(base58::Error::InvalidVersion(vec![x]))), }; Ok(Address { From 7e0d997150c5ee88c0826973eda5cb7c4fea8cca Mon Sep 17 00:00:00 2001 From: Steven Roose Date: Wed, 24 Apr 2019 15:21:33 +0100 Subject: [PATCH 02/11] Remove unused encode::Error variant --- src/consensus/encode.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/consensus/encode.rs b/src/consensus/encode.rs index cf4c4d52..5c3d5899 100644 --- a/src/consensus/encode.rs +++ b/src/consensus/encode.rs @@ -86,8 +86,6 @@ pub enum Error { UnknownNetworkMagic(u32), /// Parsing error ParseFailed(&'static str), - /// Unsupported witness version - UnsupportedWitnessVersion(u8), /// Unsupported Segwit flag UnsupportedSegwitFlag(u8), /// Unrecognized network command @@ -109,7 +107,6 @@ impl fmt::Display for Error { Error::InvalidChecksum { expected: ref e, actual: ref a } => write!(f, "{}: expected {}, actual {}", error::Error::description(self), hex_encode(e), hex_encode(a)), Error::UnknownNetworkMagic(ref m) => write!(f, "{}: {}", error::Error::description(self), m), Error::ParseFailed(ref e) => write!(f, "{}: {}", error::Error::description(self), e), - Error::UnsupportedWitnessVersion(ref wver) => write!(f, "{}: {}", error::Error::description(self), wver), Error::UnsupportedSegwitFlag(ref swflag) => write!(f, "{}: {}", error::Error::description(self), swflag), Error::UnrecognizedNetworkCommand(ref nwcmd) => write!(f, "{}: {}", error::Error::description(self), nwcmd), Error::UnexpectedHexDigit(ref d) => write!(f, "{}: {}", error::Error::description(self), d), @@ -130,7 +127,6 @@ impl error::Error for Error { | Error::InvalidChecksum { .. } | Error::UnknownNetworkMagic(..) | Error::ParseFailed(..) - | Error::UnsupportedWitnessVersion(..) | Error::UnsupportedSegwitFlag(..) | Error::UnrecognizedNetworkCommand(..) | Error::UnexpectedHexDigit(..) => None, @@ -149,7 +145,6 @@ impl error::Error for Error { Error::InvalidChecksum { .. } => "invalid checksum", Error::UnknownNetworkMagic(..) => "unknown network magic", Error::ParseFailed(..) => "parse failed", - Error::UnsupportedWitnessVersion(..) => "unsupported witness version", Error::UnsupportedSegwitFlag(..) => "unsupported segwit version", Error::UnrecognizedNetworkCommand(..) => "unrecognized network command", Error::UnexpectedHexDigit(..) => "unexpected hex digit", From 8804a41f1f999ef2bec86a80a614c5555c5cbe3b Mon Sep 17 00:00:00 2001 From: Steven Roose Date: Tue, 16 Jul 2019 22:31:49 +0200 Subject: [PATCH 03/11] Add AddressType enum --- src/lib.rs | 1 + src/util/address.rs | 84 ++++++++++++++++++++++++++++++++++++--------- 2 files changed, 69 insertions(+), 16 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index d235717e..058d9e15 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -82,6 +82,7 @@ pub use consensus::encode::VarInt; pub use network::constants::Network; pub use util::Error; pub use util::address::Address; +pub use util::address::AddressType; pub use util::amount::Amount; pub use util::amount::SignedAmount; pub use util::hash::BitcoinHash; diff --git a/src/util/address.rs b/src/util/address.rs index 66b701c2..0dd47a7f 100644 --- a/src/util/address.rs +++ b/src/util/address.rs @@ -44,7 +44,7 @@ use std::fmt::{self, Display, Formatter}; use std::str::FromStr; use bech32::{self, u5, FromBase32, ToBase32}; -use bitcoin_hashes::{hash160, Hash}; +use bitcoin_hashes::{hash160, Hash, sha256}; use blockdata::opcodes; use blockdata::script; @@ -122,6 +122,43 @@ impl From for Error { } } +/// The different types of addresses. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum AddressType { + /// pay-to-pubkey-hash + P2pkh, + /// pay-to-script-hash + P2sh, + /// pay-to-witness-pubkey-hash + P2wpkh, + /// pay-to-witness-script-hash + P2wsh, +} + +impl fmt::Display for AddressType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(match *self { + AddressType::P2pkh => "p2pkh", + AddressType::P2sh => "p2sh", + AddressType::P2wpkh => "p2wpkh", + AddressType::P2wsh => "p2wsh", + }) + } +} + +impl FromStr for AddressType { + type Err = (); + fn from_str(s: &str) -> Result { + match s { + "p2pkh" => Ok(AddressType::P2pkh), + "p2sh" => Ok(AddressType::P2sh), + "p2wpkh" => Ok(AddressType::P2wpkh), + "p2wsh" => Ok(AddressType::P2wsh), + _ => Err(()), + } + } +} + /// The method used to produce an address #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum Payload { @@ -207,8 +244,6 @@ impl Address { /// Create a witness pay to script hash address pub fn p2wsh (script: &script::Script, network: Network) -> Address { - use bitcoin_hashes::sha256; - Address { network: network, payload: Payload::WitnessProgram { @@ -221,8 +256,6 @@ impl Address { /// Create a pay to script address that embeds a witness pay to script hash address /// This is a segwit address type that looks familiar (as p2sh) to legacy clients pub fn p2shwsh (script: &script::Script, network: Network) -> Address { - use bitcoin_hashes::sha256; - let ws = script::Builder::new().push_int(0) .push_slice(&sha256::Hash::hash(&script[..])[..]) .into_script(); @@ -233,23 +266,36 @@ impl Address { } } + /// Get the address type of the address. + /// None if unknown or non-standard. + pub fn address_type(&self) -> Option { + match self.payload { + Payload::PubkeyHash(_) => Some(AddressType::P2pkh), + Payload::ScriptHash(_) => Some(AddressType::P2sh), + Payload::WitnessProgram { + version: ver, + program: ref prog, + } => { + // BIP-141 p2wpkh or p2wsh addresses. + match ver.to_u8() { + 0 => match prog.len() { + 20 => Some(AddressType::P2wpkh), + 32 => Some(AddressType::P2wsh), + _ => None, + }, + _ => None, + } + }, + } + } + /// Check whether or not the address is following Bitcoin /// standardness rules. /// /// Segwit addresses with unassigned witness versions or non-standard /// program sizes are considered non-standard. pub fn is_standard(&self) -> bool { - match self.payload { - Payload::WitnessProgram { - version: ver, - program: ref prog, - } => { - // BIP-141 p2wpkh or p2wsh addresses. - ver.to_u8() == 0 && (prog.len() == 20 || prog.len() == 32) - } - Payload::PubkeyHash(_) => true, - Payload::ScriptHash(_) => true, - } + self.address_type().is_some() } /// Generates a script pubkey spending to this address @@ -442,6 +488,7 @@ mod tests { assert_eq!(addr.script_pubkey(), hex_script!("76a914162c5ea71c0b23f5b9022ef047c4a86470a5b07088ac")); assert_eq!(&addr.to_string(), "132F25rTsvBdp9JzLLBHP5mvGY66i1xdiM"); assert_eq!(Address::from_str("132F25rTsvBdp9JzLLBHP5mvGY66i1xdiM").unwrap(), addr); + assert_eq!(addr.address_type(), Some(AddressType::P2pkh)); } #[test] @@ -453,6 +500,7 @@ mod tests { let key = hex_key!(&"03df154ebfcf29d29cc10d5c2565018bce2d9edbab267c31d2caf44a63056cf99f"); let addr = Address::p2pkh(&key, Testnet); assert_eq!(&addr.to_string(), "mqkhEMH6NCeYjFybv7pvFC22MFeaNT9AQC"); + assert_eq!(addr.address_type(), Some(AddressType::P2pkh)); } #[test] @@ -467,6 +515,7 @@ mod tests { assert_eq!(addr.script_pubkey(), hex_script!("a914162c5ea71c0b23f5b9022ef047c4a86470a5b07087")); assert_eq!(&addr.to_string(), "33iFwdLuRpW1uK1RTRqsoi8rR4NpDzk66k"); assert_eq!(Address::from_str("33iFwdLuRpW1uK1RTRqsoi8rR4NpDzk66k").unwrap(), addr); + assert_eq!(addr.address_type(), Some(AddressType::P2sh)); } #[test] @@ -476,6 +525,7 @@ mod tests { assert_eq!(&addr.to_string(), "2N3zXjbwdTcPsJiy8sUK9FhWJhqQCxA8Jjr"); assert_eq!(Address::from_str("2N3zXjbwdTcPsJiy8sUK9FhWJhqQCxA8Jjr").unwrap(), addr); + assert_eq!(addr.address_type(), Some(AddressType::P2sh)); } #[test] @@ -484,6 +534,7 @@ mod tests { let key = hex_key!("033bc8c83c52df5712229a2f72206d90192366c36428cb0c12b6af98324d97bfbc"); let addr = Address::p2wpkh(&key, Bitcoin); assert_eq!(&addr.to_string(), "bc1qvzvkjn4q3nszqxrv3nraga2r822xjty3ykvkuw"); + assert_eq!(addr.address_type(), Some(AddressType::P2wpkh)); } #[test] @@ -492,6 +543,7 @@ mod tests { let script = hex_script!("52210375e00eb72e29da82b89367947f29ef34afb75e8654f6ea368e0acdfd92976b7c2103a1b26313f430c4b15bb1fdce663207659d8cac749a0e53d70eff01874496feff2103c96d495bfdd5ba4145e3e046fee45e84a8a48ad05bd8dbb395c011a32cf9f88053ae"); let addr = Address::p2wsh(&script, Bitcoin); assert_eq!(&addr.to_string(), "bc1qwqdg6squsna38e46795at95yu9atm8azzmyvckulcc7kytlcckxswvvzej"); + assert_eq!(addr.address_type(), Some(AddressType::P2wsh)); } #[test] From 33e8ba3c7e08f522e6ada3363b7fcbfed658494f Mon Sep 17 00:00:00 2001 From: Steven Roose Date: Wed, 24 Apr 2019 07:55:51 +0100 Subject: [PATCH 04/11] Extract roundtrip method from Address tests --- src/util/address.rs | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/util/address.rs b/src/util/address.rs index 0dd47a7f..9b956de8 100644 --- a/src/util/address.rs +++ b/src/util/address.rs @@ -476,6 +476,14 @@ mod tests { macro_rules! hex_script (($hex:expr) => (Script::from(hex!($hex)))); macro_rules! hex_hash160 (($hex:expr) => (hash160::Hash::from_slice(&hex!($hex)).unwrap())); + fn roundtrips(addr: &Address) { + assert_eq!( + Address::from_str(&addr.to_string()).unwrap(), *addr, + "string round-trip failed for {}", addr, + ); + //TODO: add serde roundtrip after no-strason PR + } + #[test] fn test_p2pkh_address_58() { let addr = Address { @@ -487,8 +495,8 @@ mod tests { assert_eq!(addr.script_pubkey(), hex_script!("76a914162c5ea71c0b23f5b9022ef047c4a86470a5b07088ac")); assert_eq!(&addr.to_string(), "132F25rTsvBdp9JzLLBHP5mvGY66i1xdiM"); - assert_eq!(Address::from_str("132F25rTsvBdp9JzLLBHP5mvGY66i1xdiM").unwrap(), addr); assert_eq!(addr.address_type(), Some(AddressType::P2pkh)); + roundtrips(&addr); } #[test] @@ -501,6 +509,7 @@ mod tests { let addr = Address::p2pkh(&key, Testnet); assert_eq!(&addr.to_string(), "mqkhEMH6NCeYjFybv7pvFC22MFeaNT9AQC"); assert_eq!(addr.address_type(), Some(AddressType::P2pkh)); + roundtrips(&addr); } #[test] @@ -514,8 +523,8 @@ mod tests { assert_eq!(addr.script_pubkey(), hex_script!("a914162c5ea71c0b23f5b9022ef047c4a86470a5b07087")); assert_eq!(&addr.to_string(), "33iFwdLuRpW1uK1RTRqsoi8rR4NpDzk66k"); - assert_eq!(Address::from_str("33iFwdLuRpW1uK1RTRqsoi8rR4NpDzk66k").unwrap(), addr); assert_eq!(addr.address_type(), Some(AddressType::P2sh)); + roundtrips(&addr); } #[test] @@ -524,8 +533,8 @@ mod tests { let addr = Address::p2sh(&script, Testnet); assert_eq!(&addr.to_string(), "2N3zXjbwdTcPsJiy8sUK9FhWJhqQCxA8Jjr"); - assert_eq!(Address::from_str("2N3zXjbwdTcPsJiy8sUK9FhWJhqQCxA8Jjr").unwrap(), addr); assert_eq!(addr.address_type(), Some(AddressType::P2sh)); + roundtrips(&addr); } #[test] @@ -535,6 +544,7 @@ mod tests { let addr = Address::p2wpkh(&key, Bitcoin); assert_eq!(&addr.to_string(), "bc1qvzvkjn4q3nszqxrv3nraga2r822xjty3ykvkuw"); assert_eq!(addr.address_type(), Some(AddressType::P2wpkh)); + roundtrips(&addr); } #[test] @@ -544,6 +554,7 @@ mod tests { let addr = Address::p2wsh(&script, Bitcoin); assert_eq!(&addr.to_string(), "bc1qwqdg6squsna38e46795at95yu9atm8azzmyvckulcc7kytlcckxswvvzej"); assert_eq!(addr.address_type(), Some(AddressType::P2wsh)); + roundtrips(&addr); } #[test] @@ -559,18 +570,21 @@ mod tests { assert_eq!(addr.network, Testnet); assert_eq!(addr.script_pubkey(), hex_script!("00201863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262")); assert_eq!(addr.to_string(), addrstr); + roundtrips(&addr); let addrstr = "tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy"; let addr = Address::from_str(addrstr).unwrap(); assert_eq!(addr.network, Testnet); assert_eq!(addr.script_pubkey(), hex_script!("0020000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433")); assert_eq!(addr.to_string(), addrstr); + roundtrips(&addr); let addrstr = "bcrt1q2nfxmhd4n3c8834pj72xagvyr9gl57n5r94fsl"; let addr = Address::from_str(addrstr).unwrap(); assert_eq!(addr.network, Regtest); assert_eq!(addr.script_pubkey(), hex_script!("001454d26dddb59c7073c6a197946ea1841951fa7a74")); assert_eq!(addr.to_string(), addrstr); + roundtrips(&addr); // bad vectors let addrstr = "tc1qw508d6qejxtdg4y5r3zarvary0c5xw7kg3g4ty"; // invalid hrp From bfdcfee28e509759cb6041af5d71d8afee1e716e Mon Sep 17 00:00:00 2001 From: Steven Roose Date: Wed, 24 Apr 2019 08:02:05 +0100 Subject: [PATCH 05/11] Add Address::from_script constructor --- src/blockdata/script.rs | 20 ++++++++ src/util/address.rs | 101 +++++++++++++++++++++++++++++++++------- 2 files changed, 104 insertions(+), 17 deletions(-) diff --git a/src/blockdata/script.rs b/src/blockdata/script.rs index a87cb17c..36939e84 100644 --- a/src/blockdata/script.rs +++ b/src/blockdata/script.rs @@ -282,6 +282,26 @@ impl Script { self.0[34] == opcodes::all::OP_CHECKSIG.into_u8()) } + /// Checks whether a script pubkey is a Segregated Witness (segwit) program. + #[inline] + pub fn is_witness_program(&self) -> bool { + // A scriptPubKey (or redeemScript as defined in BIP16/P2SH) that consists of a 1-byte + // 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() + // Check that the rest of the script has the correct size + && self.0.len() - 2 == self.0[1] as usize + } + /// Checks whether a script pubkey is a p2wsh output #[inline] pub fn is_v0_p2wsh(&self) -> bool { diff --git a/src/util/address.rs b/src/util/address.rs index 9b956de8..baa6dfc3 100644 --- a/src/util/address.rs +++ b/src/util/address.rs @@ -175,6 +175,62 @@ pub enum Payload { }, } +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(Hash::from_slice(&script.as_bytes()[3..23]).unwrap()) + } else if script.is_p2sh() { + Payload::ScriptHash(Hash::from_slice(&script.as_bytes()[2..22]).unwrap()) + } 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(), + } + } else { + return None; + }) + } + + /// Generates a script pubkey spending to this [Payload]. + pub fn script_pubkey(&self) -> script::Script { + match *self { + Payload::PubkeyHash(ref hash) => script::Builder::new() + .push_opcode(opcodes::all::OP_DUP) + .push_opcode(opcodes::all::OP_HASH160) + .push_slice(&hash[..]) + .push_opcode(opcodes::all::OP_EQUALVERIFY) + .push_opcode(opcodes::all::OP_CHECKSIG), + Payload::ScriptHash(ref hash) => script::Builder::new() + .push_opcode(opcodes::all::OP_HASH160) + .push_slice(&hash[..]) + .push_opcode(opcodes::all::OP_EQUAL), + Payload::WitnessProgram { + version: ver, + program: ref prog, + } => { + assert!(ver.to_u8() <= 16); + let mut verop = ver.to_u8(); + if verop > 0 { + verop = 0x50 + verop; + } + script::Builder::new().push_opcode(verop.into()).push_slice(&prog) + } + } + .into_script() + } +} + #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] /// A Bitcoin address pub struct Address { @@ -298,25 +354,17 @@ impl Address { self.address_type().is_some() } + /// Get an [Address] from an output script (scriptPubkey). + pub fn from_script(script: &script::Script, network: Network) -> Option
{ + Some(Address { + payload: Payload::from_script(script)?, + network: network, + }) + } + /// Generates a script pubkey spending to this address pub fn script_pubkey(&self) -> script::Script { - match self.payload { - Payload::PubkeyHash(ref hash) => script::Builder::new() - .push_opcode(opcodes::all::OP_DUP) - .push_opcode(opcodes::all::OP_HASH160) - .push_slice(&hash[..]) - .push_opcode(opcodes::all::OP_EQUALVERIFY) - .push_opcode(opcodes::all::OP_CHECKSIG), - Payload::ScriptHash(ref hash) => script::Builder::new() - .push_opcode(opcodes::all::OP_HASH160) - .push_slice(&hash[..]) - .push_opcode(opcodes::all::OP_EQUAL), - Payload::WitnessProgram { - version: ver, - program: ref prog, - } => script::Builder::new().push_int(ver.to_u8() as i64).push_slice(&prog), - } - .into_script() + self.payload.script_pubkey() } } @@ -481,6 +529,10 @@ mod tests { Address::from_str(&addr.to_string()).unwrap(), *addr, "string round-trip failed for {}", addr, ); + assert_eq!( + Address::from_script(&addr.script_pubkey(), addr.network).as_ref(), Some(addr), + "script round-trip failed for {}", addr, + ); //TODO: add serde roundtrip after no-strason PR } @@ -557,6 +609,21 @@ mod tests { roundtrips(&addr); } + #[test] + fn test_non_existent_segwit_version() { + let version = 13; + // 40-byte program + let program = hex!("654f6ea368e0acdfd92976b7c2103a1b26313f430654f6ea368e0acdfd92976b7c2103a1b26313f4"); + let addr = Address { + payload: Payload::WitnessProgram { + version: u5::try_from_u8(version).expect("0<32"), + program: program, + }, + network: Network::Bitcoin, + }; + roundtrips(&addr); + } + #[test] fn test_bip173_vectors() { let addrstr = "BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4"; From e469fec8392bc44cc567e4e9a6404c2fa50116df Mon Sep 17 00:00:00 2001 From: Steven Roose Date: Fri, 19 Jul 2019 11:43:31 +0200 Subject: [PATCH 06/11] address: Drop error::Error::description impl --- src/util/address.rs | 36 ++++++++++++++++-------------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/src/util/address.rs b/src/util/address.rs index baa6dfc3..21a6df25 100644 --- a/src/util/address.rs +++ b/src/util/address.rs @@ -71,14 +71,21 @@ pub enum Error { impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let desc = ::std::error::Error::description(self); match *self { - Error::Base58(ref e) => write!(f, "{}: {}", desc, e), - Error::Bech32(ref e) => write!(f, "{}: {}", desc, e), - Error::InvalidWitnessVersion(v) => write!(f, "{}: {}", desc, v), - Error::InvalidWitnessProgramLength(l) => write!(f, "{}: {}", desc, l), - Error::InvalidSegwitV0ProgramLength(l) => write!(f, "{}: {}", desc, l), - _ => f.write_str(desc), + Error::Base58(ref e) => write!(f, "base58: {}", e), + 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::InvalidWitnessProgramLength(l) => write!( + f, + "the witness program must be between 2 and 40 bytes in length: lengh={}", + l + ), + Error::InvalidSegwitV0ProgramLength(l) => write!( + f, + "a v0 witness program must be either of length 20 or 32 bytes: length={}", + l + ), } } } @@ -92,19 +99,8 @@ impl ::std::error::Error for Error { } } - fn description(&self) -> &str { - match *self { - Error::Base58(..) => "base58 error", - Error::Bech32(..) => "bech32 error", - Error::EmptyBech32Payload => "the bech32 payload was empty", - Error::InvalidWitnessVersion(..) => "invalid witness script version", - Error::InvalidWitnessProgramLength(..) => { - "the witness program must be between 2 and 40 bytes in length" - }, - Error::InvalidSegwitV0ProgramLength(..) => { - "a v0 witness program must be either of length 20 or 32" - }, - } + fn description(&self) -> &'static str { + "std::error::Error::description is deprecated" } } From 8de13a3915c86a6a4f28946c22e20db9df63051d Mon Sep 17 00:00:00 2001 From: Steven Roose Date: Fri, 26 Jul 2019 18:48:43 +0200 Subject: [PATCH 07/11] Redo the BIP-173 test vectors Before, non-version-zero segwit addresses were not included. --- src/util/address.rs | 86 +++++++++++++++------------------------------ 1 file changed, 29 insertions(+), 57 deletions(-) diff --git a/src/util/address.rs b/src/util/address.rs index 21a6df25..e82174a9 100644 --- a/src/util/address.rs +++ b/src/util/address.rs @@ -507,7 +507,7 @@ mod tests { use std::string::ToString; use bitcoin_hashes::{hash160, Hash}; - use hex::decode as hex_decode; + use hex::{decode as hex_decode, encode as hex_encode}; use blockdata::script::Script; use network::constants::Network::{Bitcoin, Testnet, Regtest}; @@ -622,63 +622,35 @@ mod tests { #[test] fn test_bip173_vectors() { - let addrstr = "BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4"; - let addr = Address::from_str(addrstr).unwrap(); - assert_eq!(addr.network, Bitcoin); - assert_eq!(addr.script_pubkey(), hex_script!("0014751e76e8199196d454941c45d1b3a323f1433bd6")); - // skip round-trip because we'll serialize to lowercase which won't match + let valid_vectors = [ + ("BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4", "0014751e76e8199196d454941c45d1b3a323f1433bd6"), + ("tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7", "00201863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262"), + ("bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx", "5128751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6"), + ("BC1SW50QA3JX3S", "6002751e"), + ("bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj", "5210751e76e8199196d454941c45d1b3a323"), + ("tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy", "0020000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433"), + ]; + for vector in &valid_vectors { + let addr: Address = vector.0.parse().unwrap(); + assert_eq!(&hex_encode(addr.script_pubkey().as_bytes()), vector.1); + roundtrips(&addr); + } - let addrstr = "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7"; - let addr = Address::from_str(addrstr).unwrap(); - assert_eq!(addr.network, Testnet); - assert_eq!(addr.script_pubkey(), hex_script!("00201863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262")); - assert_eq!(addr.to_string(), addrstr); - roundtrips(&addr); - - let addrstr = "tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy"; - let addr = Address::from_str(addrstr).unwrap(); - assert_eq!(addr.network, Testnet); - assert_eq!(addr.script_pubkey(), hex_script!("0020000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433")); - assert_eq!(addr.to_string(), addrstr); - roundtrips(&addr); - - let addrstr = "bcrt1q2nfxmhd4n3c8834pj72xagvyr9gl57n5r94fsl"; - let addr = Address::from_str(addrstr).unwrap(); - assert_eq!(addr.network, Regtest); - assert_eq!(addr.script_pubkey(), hex_script!("001454d26dddb59c7073c6a197946ea1841951fa7a74")); - assert_eq!(addr.to_string(), addrstr); - roundtrips(&addr); - - // bad vectors - let addrstr = "tc1qw508d6qejxtdg4y5r3zarvary0c5xw7kg3g4ty"; // invalid hrp - assert!(Address::from_str(addrstr).is_err()); - - let addrstr = "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t5"; // invalid checksum - assert!(Address::from_str(addrstr).is_err()); - - let addrstr = "BC13W508D6QEJXTDG4Y5R3ZARVARY0C5XW7KN40WF2"; // invalid witness version - assert!(Address::from_str(addrstr).is_err()); - - let addrstr = "bc1rw5uspcuh"; // invalid program length - assert!(Address::from_str(addrstr).is_err()); - - let addrstr = "bc10w508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kw5rljs90"; // invalid program length - assert!(Address::from_str(addrstr).is_err()); - - let addrstr = "BC1QR508D6QEJXTDG4Y5R3ZARVARYV98GJ9P"; // invalid program length for wit v0 - assert!(Address::from_str(addrstr).is_err()); - - let addrstr = "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sL5k7"; // mixed case - assert!(Address::from_str(addrstr).is_err()); - - let addrstr = "bc1zw508d6qejxtdg4y5r3zarvaryvqyzf3du"; // zero padding of more than 4 bits - assert!(Address::from_str(addrstr).is_err()); - - let addrstr = "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3pjxtptv"; // nonzero padding - assert!(Address::from_str(addrstr).is_err()); - - let addrstr = "bc1gmk9yu"; // empty data section - assert!(Address::from_str(addrstr).is_err()); + let invalid_vectors = [ + "tc1qw508d6qejxtdg4y5r3zarvary0c5xw7kg3g4ty", + "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t5", + "BC13W508D6QEJXTDG4Y5R3ZARVARY0C5XW7KN40WF2", + "bc1rw5uspcuh", + "bc10w508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kw5rljs90", + "BC1QR508D6QEJXTDG4Y5R3ZARVARYV98GJ9P", + "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sL5k7", + "bc1zw508d6qejxtdg4y5r3zarvaryvqyzf3du", + "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3pjxtptv", + "bc1gmk9yu", + ]; + for vector in &invalid_vectors { + assert!(vector.parse::
().is_err()); + } } #[test] From 3a93f8522c7ea5fb68a67b5dac8e1812b57932f0 Mon Sep 17 00:00:00 2001 From: Steven Roose Date: Fri, 26 Jul 2019 18:49:21 +0200 Subject: [PATCH 08/11] Format address module --- src/util/address.rs | 110 +++++++++++++++++++++++++++----------------- 1 file changed, 68 insertions(+), 42 deletions(-) diff --git a/src/util/address.rs b/src/util/address.rs index e82174a9..b5f24cef 100644 --- a/src/util/address.rs +++ b/src/util/address.rs @@ -44,7 +44,7 @@ use std::fmt::{self, Display, Formatter}; use std::str::FromStr; use bech32::{self, u5, FromBase32, ToBase32}; -use bitcoin_hashes::{hash160, Hash, sha256}; +use bitcoin_hashes::{hash160, sha256, Hash}; use blockdata::opcodes; use blockdata::script; @@ -247,7 +247,7 @@ impl Address { Address { network: network, - payload: Payload::PubkeyHash(hash160::Hash::from_engine(hash_engine)) + payload: Payload::PubkeyHash(hash160::Hash::from_engine(hash_engine)), } } @@ -257,13 +257,13 @@ impl Address { pub fn p2sh(script: &script::Script, network: Network) -> Address { Address { network: network, - payload: Payload::ScriptHash(hash160::Hash::hash(&script[..])) + payload: Payload::ScriptHash(hash160::Hash::hash(&script[..])), } } /// Create a witness pay to public key address from a public key /// This is the native segwit address type for an output redeemable with a single signature - pub fn p2wpkh (pk: &key::PublicKey, network: Network) -> Address { + pub fn p2wpkh(pk: &key::PublicKey, network: Network) -> Address { let mut hash_engine = hash160::Hash::engine(); pk.write_into(&mut hash_engine); @@ -278,7 +278,7 @@ impl Address { /// Create a pay to script address that embeds a witness pay to public key /// This is a segwit address type that looks familiar (as p2sh) to legacy clients - pub fn p2shwpkh (pk: &key::PublicKey, network: Network) -> Address { + pub fn p2shwpkh(pk: &key::PublicKey, network: Network) -> Address { let mut hash_engine = hash160::Hash::engine(); pk.write_into(&mut hash_engine); @@ -288,14 +288,12 @@ impl Address { Address { network: network, - payload: Payload::ScriptHash( - hash160::Hash::hash(builder.into_script().as_bytes()) - ) + payload: Payload::ScriptHash(hash160::Hash::hash(builder.into_script().as_bytes())), } } /// Create a witness pay to script hash address - pub fn p2wsh (script: &script::Script, network: Network) -> Address { + pub fn p2wsh(script: &script::Script, network: Network) -> Address { Address { network: network, payload: Payload::WitnessProgram { @@ -307,14 +305,15 @@ impl Address { /// Create a pay to script address that embeds a witness pay to script hash address /// This is a segwit address type that looks familiar (as p2sh) to legacy clients - pub fn p2shwsh (script: &script::Script, network: Network) -> Address { - let ws = script::Builder::new().push_int(0) - .push_slice(&sha256::Hash::hash(&script[..])[..]) - .into_script(); + pub fn p2shwsh(script: &script::Script, network: Network) -> Address { + let ws = script::Builder::new() + .push_int(0) + .push_slice(&sha256::Hash::hash(&script[..])[..]) + .into_script(); Address { network: network, - payload: Payload::ScriptHash(hash160::Hash::hash(&ws[..])) + payload: Payload::ScriptHash(hash160::Hash::hash(&ws[..])), } } @@ -337,7 +336,7 @@ impl Address { }, _ => None, } - }, + } } } @@ -375,7 +374,7 @@ impl Display for Address { }; prefixed[1..].copy_from_slice(&hash[..]); base58::check_encode_slice_to_fmt(fmt, &prefixed[..]) - }, + } Payload::ScriptHash(ref hash) => { let mut prefixed = [0; 21]; prefixed[0] = match self.network { @@ -471,19 +470,19 @@ impl FromStr for Address { let (network, payload) = match data[0] { 0 => ( Network::Bitcoin, - Payload::PubkeyHash(hash160::Hash::from_slice(&data[1..]).unwrap()) + Payload::PubkeyHash(hash160::Hash::from_slice(&data[1..]).unwrap()), ), 5 => ( Network::Bitcoin, - Payload::ScriptHash(hash160::Hash::from_slice(&data[1..]).unwrap()) + Payload::ScriptHash(hash160::Hash::from_slice(&data[1..]).unwrap()), ), 111 => ( Network::Testnet, - Payload::PubkeyHash(hash160::Hash::from_slice(&data[1..]).unwrap()) + Payload::PubkeyHash(hash160::Hash::from_slice(&data[1..]).unwrap()), ), 196 => ( Network::Testnet, - Payload::ScriptHash(hash160::Hash::from_slice(&data[1..]).unwrap()) + Payload::ScriptHash(hash160::Hash::from_slice(&data[1..]).unwrap()), ), x => return Err(Error::Base58(base58::Error::InvalidVersion(vec![x]))), }; @@ -510,7 +509,7 @@ mod tests { use hex::{decode as hex_decode, encode as hex_encode}; use blockdata::script::Script; - use network::constants::Network::{Bitcoin, Testnet, Regtest}; + use network::constants::Network::{Bitcoin, Testnet}; use util::key::PublicKey; use super::*; @@ -522,12 +521,16 @@ mod tests { fn roundtrips(addr: &Address) { assert_eq!( - Address::from_str(&addr.to_string()).unwrap(), *addr, - "string round-trip failed for {}", addr, + Address::from_str(&addr.to_string()).unwrap(), + *addr, + "string round-trip failed for {}", + addr, ); assert_eq!( - Address::from_script(&addr.script_pubkey(), addr.network).as_ref(), Some(addr), - "script round-trip failed for {}", addr, + Address::from_script(&addr.script_pubkey(), addr.network).as_ref(), + Some(addr), + "script round-trip failed for {}", + addr, ); //TODO: add serde roundtrip after no-strason PR } @@ -536,12 +539,13 @@ mod tests { fn test_p2pkh_address_58() { let addr = Address { network: Bitcoin, - payload: Payload::PubkeyHash( - hex_hash160!("162c5ea71c0b23f5b9022ef047c4a86470a5b070") - ) + payload: Payload::PubkeyHash(hex_hash160!("162c5ea71c0b23f5b9022ef047c4a86470a5b070")), }; - assert_eq!(addr.script_pubkey(), hex_script!("76a914162c5ea71c0b23f5b9022ef047c4a86470a5b07088ac")); + assert_eq!( + addr.script_pubkey(), + hex_script!("76a914162c5ea71c0b23f5b9022ef047c4a86470a5b07088ac") + ); assert_eq!(&addr.to_string(), "132F25rTsvBdp9JzLLBHP5mvGY66i1xdiM"); assert_eq!(addr.address_type(), Some(AddressType::P2pkh)); roundtrips(&addr); @@ -564,12 +568,13 @@ mod tests { fn test_p2sh_address_58() { let addr = Address { network: Bitcoin, - payload: Payload::ScriptHash( - hex_hash160!("162c5ea71c0b23f5b9022ef047c4a86470a5b070") - ) + payload: Payload::ScriptHash(hex_hash160!("162c5ea71c0b23f5b9022ef047c4a86470a5b070")), }; - assert_eq!(addr.script_pubkey(), hex_script!("a914162c5ea71c0b23f5b9022ef047c4a86470a5b07087")); + assert_eq!( + addr.script_pubkey(), + hex_script!("a914162c5ea71c0b23f5b9022ef047c4a86470a5b07087") + ); assert_eq!(&addr.to_string(), "33iFwdLuRpW1uK1RTRqsoi8rR4NpDzk66k"); assert_eq!(addr.address_type(), Some(AddressType::P2sh)); roundtrips(&addr); @@ -586,7 +591,7 @@ mod tests { } #[test] - fn test_p2wpkh () { + fn test_p2wpkh() { // stolen from Bitcoin transaction: b3c8c2b6cfc335abbcb2c7823a8453f55d64b2b5125a9a61e8737230cdb8ce20 let key = hex_key!("033bc8c83c52df5712229a2f72206d90192366c36428cb0c12b6af98324d97bfbc"); let addr = Address::p2wpkh(&key, Bitcoin); @@ -596,11 +601,14 @@ mod tests { } #[test] - fn test_p2wsh () { + fn test_p2wsh() { // stolen from Bitcoin transaction 5df912fda4becb1c29e928bec8d64d93e9ba8efa9b5b405bd683c86fd2c65667 let script = hex_script!("52210375e00eb72e29da82b89367947f29ef34afb75e8654f6ea368e0acdfd92976b7c2103a1b26313f430c4b15bb1fdce663207659d8cac749a0e53d70eff01874496feff2103c96d495bfdd5ba4145e3e046fee45e84a8a48ad05bd8dbb395c011a32cf9f88053ae"); let addr = Address::p2wsh(&script, Bitcoin); - assert_eq!(&addr.to_string(), "bc1qwqdg6squsna38e46795at95yu9atm8azzmyvckulcc7kytlcckxswvvzej"); + assert_eq!( + &addr.to_string(), + "bc1qwqdg6squsna38e46795at95yu9atm8azzmyvckulcc7kytlcckxswvvzej" + ); assert_eq!(addr.address_type(), Some(AddressType::P2wsh)); roundtrips(&addr); } @@ -609,7 +617,9 @@ mod tests { fn test_non_existent_segwit_version() { let version = 13; // 40-byte program - let program = hex!("654f6ea368e0acdfd92976b7c2103a1b26313f430654f6ea368e0acdfd92976b7c2103a1b26313f4"); + let program = hex!( + "654f6ea368e0acdfd92976b7c2103a1b26313f430654f6ea368e0acdfd92976b7c2103a1b26313f4" + ); let addr = Address { payload: Payload::WitnessProgram { version: u5::try_from_u8(version).expect("0<32"), @@ -660,7 +670,10 @@ mod tests { let addr = Address::from_str("132F25rTsvBdp9JzLLBHP5mvGY66i1xdiM").unwrap(); let json = serde_json::to_value(&addr).unwrap(); - assert_eq!(json, serde_json::Value::String("132F25rTsvBdp9JzLLBHP5mvGY66i1xdiM".to_owned())); + assert_eq!( + json, + serde_json::Value::String("132F25rTsvBdp9JzLLBHP5mvGY66i1xdiM".to_owned()) + ); let into: Address = serde_json::from_value(json).unwrap(); assert_eq!(addr.to_string(), into.to_string()); assert_eq!( @@ -670,7 +683,10 @@ mod tests { let addr = Address::from_str("33iFwdLuRpW1uK1RTRqsoi8rR4NpDzk66k").unwrap(); let json = serde_json::to_value(&addr).unwrap(); - assert_eq!(json, serde_json::Value::String("33iFwdLuRpW1uK1RTRqsoi8rR4NpDzk66k".to_owned())); + assert_eq!( + json, + serde_json::Value::String("33iFwdLuRpW1uK1RTRqsoi8rR4NpDzk66k".to_owned()) + ); let into: Address = serde_json::from_value(json).unwrap(); assert_eq!(addr.to_string(), into.to_string()); assert_eq!( @@ -678,9 +694,16 @@ mod tests { hex_script!("a914162c5ea71c0b23f5b9022ef047c4a86470a5b07087") ); - let addr = Address::from_str("tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7").unwrap(); + let addr = + Address::from_str("tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7") + .unwrap(); let json = serde_json::to_value(&addr).unwrap(); - assert_eq!(json, serde_json::Value::String("tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7".to_owned())); + assert_eq!( + json, + serde_json::Value::String( + "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7".to_owned() + ) + ); let into: Address = serde_json::from_value(json).unwrap(); assert_eq!(addr.to_string(), into.to_string()); assert_eq!( @@ -690,7 +713,10 @@ mod tests { let addr = Address::from_str("bcrt1q2nfxmhd4n3c8834pj72xagvyr9gl57n5r94fsl").unwrap(); let json = serde_json::to_value(&addr).unwrap(); - assert_eq!(json, serde_json::Value::String("bcrt1q2nfxmhd4n3c8834pj72xagvyr9gl57n5r94fsl".to_owned())); + assert_eq!( + json, + serde_json::Value::String("bcrt1q2nfxmhd4n3c8834pj72xagvyr9gl57n5r94fsl".to_owned()) + ); let into: Address = serde_json::from_value(json).unwrap(); assert_eq!(addr.to_string(), into.to_string()); assert_eq!( From 170abaa82ff77f013b5e068c5ea4a485409302b6 Mon Sep 17 00:00:00 2001 From: Steven Roose Date: Tue, 13 Aug 2019 08:58:38 +0100 Subject: [PATCH 09/11] Use the new bech32 functions --- src/util/address.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/util/address.rs b/src/util/address.rs index b5f24cef..9ba699ac 100644 --- a/src/util/address.rs +++ b/src/util/address.rs @@ -43,7 +43,7 @@ use std::fmt::{self, Display, Formatter}; use std::str::FromStr; -use bech32::{self, u5, FromBase32, ToBase32}; +use bech32; use bitcoin_hashes::{hash160, sha256, Hash}; use blockdata::opcodes; @@ -165,7 +165,7 @@ pub enum Payload { /// Segwit address WitnessProgram { /// The witness program version - version: u5, + version: bech32::u5, /// The witness program program: Vec, }, @@ -270,7 +270,7 @@ impl Address { Address { network: network, payload: Payload::WitnessProgram { - version: u5::try_from_u8(0).expect("0<32"), + version: bech32::u5::try_from_u8(0).expect("0<32"), program: hash160::Hash::from_engine(hash_engine)[..].to_vec(), }, } @@ -297,7 +297,7 @@ impl Address { Address { network: network, payload: Payload::WitnessProgram { - version: u5::try_from_u8(0).expect("0<32"), + version: bech32::u5::try_from_u8(0).expect("0<32"), program: sha256::Hash::hash(&script[..])[..].to_vec(), }, } @@ -388,14 +388,14 @@ impl Display for Address { version: ver, program: ref prog, } => { - let mut b32_data = vec![ver]; - b32_data.extend_from_slice(&prog.to_base32()); let hrp = match self.network { Network::Bitcoin => "bc", Network::Testnet => "tb", Network::Regtest => "bcrt", }; - bech32::encode_to_fmt(fmt, &hrp, &b32_data).expect("only errors on invalid HRP") + let mut bech32_writer = bech32::Bech32Writer::new(hrp, fmt)?; + bech32::WriteBase32::write_u5(&mut bech32_writer, ver)?; + bech32::ToBase32::write_base32(&prog, &mut bech32_writer) } } } @@ -431,9 +431,9 @@ impl FromStr for Address { } // Get the script version and program (converted from 5-bit to 8-bit) - let (version, program) = { + let (version, program): (bech32::u5, Vec) = { let (v, p5) = payload.split_at(1); - (v[0], Vec::from_base32(p5)?) + (v[0], bech32::FromBase32::from_base32(p5)?) }; // Generic segwit checks. @@ -622,7 +622,7 @@ mod tests { ); let addr = Address { payload: Payload::WitnessProgram { - version: u5::try_from_u8(version).expect("0<32"), + version: bech32::u5::try_from_u8(version).expect("0<32"), program: program, }, network: Network::Bitcoin, From 09a65023a2cbd244035fca64b5ea68914d586a7d Mon Sep 17 00:00:00 2001 From: Steven Roose Date: Tue, 13 Aug 2019 13:00:42 +0100 Subject: [PATCH 10/11] Add tests for p2shwpkh and p2shwsh --- src/util/address.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/util/address.rs b/src/util/address.rs index 9ba699ac..34cdd5b5 100644 --- a/src/util/address.rs +++ b/src/util/address.rs @@ -613,6 +613,26 @@ mod tests { roundtrips(&addr); } + #[test] + fn test_p2shwpkh() { + // stolen from Bitcoin transaction: ad3fd9c6b52e752ba21425435ff3dd361d6ac271531fc1d2144843a9f550ad01 + let key = hex_key!("026c468be64d22761c30cd2f12cbc7de255d592d7904b1bab07236897cc4c2e766"); + let addr = Address::p2shwpkh(&key, Bitcoin); + assert_eq!(&addr.to_string(), "3QBRmWNqqBGme9er7fMkGqtZtp4gjMFxhE"); + assert_eq!(addr.address_type(), Some(AddressType::P2sh)); + roundtrips(&addr); + } + + #[test] + fn test_p2shwsh() { + // stolen from Bitcoin transaction f9ee2be4df05041d0e0a35d7caa3157495ca4f93b233234c9967b6901dacf7a9 + let script = hex_script!("522103e5529d8eaa3d559903adb2e881eb06c86ac2574ffa503c45f4e942e2a693b33e2102e5f10fcdcdbab211e0af6a481f5532536ec61a5fdbf7183770cf8680fe729d8152ae"); + let addr = Address::p2shwsh(&script, Bitcoin); + assert_eq!(&addr.to_string(), "36EqgNnsWW94SreZgBWc1ANC6wpFZwirHr"); + assert_eq!(addr.address_type(), Some(AddressType::P2sh)); + roundtrips(&addr); + } + #[test] fn test_non_existent_segwit_version() { let version = 13; From 3c390ceb93e853e3f2744e7b3a735d41ab15f726 Mon Sep 17 00:00:00 2001 From: Steven Roose Date: Thu, 15 Aug 2019 21:52:10 +0100 Subject: [PATCH 11/11] Add fuzzer for Address::from_script --- fuzz/fuzz_targets/deserialize_script.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/fuzz/fuzz_targets/deserialize_script.rs b/fuzz/fuzz_targets/deserialize_script.rs index fb8fd43d..e7c36de0 100644 --- a/fuzz/fuzz_targets/deserialize_script.rs +++ b/fuzz/fuzz_targets/deserialize_script.rs @@ -1,5 +1,7 @@ extern crate bitcoin; +use bitcoin::util::address::Address; +use bitcoin::network::constants::Network; use bitcoin::blockdata::script; use bitcoin::consensus::encode; @@ -32,6 +34,11 @@ fn do_test(data: &[u8]) { } assert_eq!(b.into_script(), script); assert_eq!(data, &encode::serialize(&script)[..]); + + // Check if valid address and if that address roundtrips. + if let Some(addr) = Address::from_script(&script, Network::Bitcoin) { + assert_eq!(addr.script_pubkey(), script); + } } }