From bfdcfee28e509759cb6041af5d71d8afee1e716e Mon Sep 17 00:00:00 2001 From: Steven Roose Date: Wed, 24 Apr 2019 08:02:05 +0100 Subject: [PATCH] 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";