diff --git a/bitcoin/embedded/src/main.rs b/bitcoin/embedded/src/main.rs index 8adbcf4b..c27342c7 100644 --- a/bitcoin/embedded/src/main.rs +++ b/bitcoin/embedded/src/main.rs @@ -43,8 +43,8 @@ fn main() -> ! { let secp = Secp256k1::preallocated_new(&mut buf_ful).unwrap(); // Derive address - let pubkey = pk.public_key(&secp); - let address = Address::p2wpkh(&pubkey, Network::Bitcoin).unwrap(); + let pubkey = pk.public_key(&secp).try_into().unwrap(); + let address = Address::p2wpkh(&pubkey, Network::Bitcoin); hprintln!("Address: {}", address).unwrap(); assert_eq!(address.to_string(), "bc1qpx9t9pzzl4qsydmhyt6ctrxxjd4ep549np9993".to_string()); diff --git a/bitcoin/examples/bip32.rs b/bitcoin/examples/bip32.rs index dc38d5b2..98f19673 100644 --- a/bitcoin/examples/bip32.rs +++ b/bitcoin/examples/bip32.rs @@ -8,7 +8,7 @@ use bitcoin::bip32::{ChildNumber, DerivationPath, Xpriv, Xpub}; use bitcoin::hex::FromHex; use bitcoin::secp256k1::ffi::types::AlignedType; use bitcoin::secp256k1::Secp256k1; -use bitcoin::PublicKey; +use bitcoin::CompressedPublicKey; fn main() { // This example derives root xprv from a 32-byte seed, @@ -53,6 +53,6 @@ fn main() { // manually creating indexes this time let zero = ChildNumber::from_normal_idx(0).unwrap(); let public_key = xpub.derive_pub(&secp, &[zero, zero]).unwrap().public_key; - let address = Address::p2wpkh(&PublicKey::new(public_key), network).unwrap(); + let address = Address::p2wpkh(&CompressedPublicKey(public_key), network); println!("First receiving address: {}", address); } diff --git a/bitcoin/examples/ecdsa-psbt.rs b/bitcoin/examples/ecdsa-psbt.rs index 09e765fb..30f9d32d 100644 --- a/bitcoin/examples/ecdsa-psbt.rs +++ b/bitcoin/examples/ecdsa-psbt.rs @@ -39,7 +39,7 @@ use bitcoin::locktime::absolute; use bitcoin::psbt::{self, Input, Psbt, PsbtSighashType}; use bitcoin::secp256k1::{Secp256k1, Signing, Verification}; use bitcoin::{ - transaction, Address, Amount, Network, OutPoint, PublicKey, ScriptBuf, Sequence, Transaction, + transaction, Address, Amount, Network, OutPoint, CompressedPublicKey, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Witness, }; @@ -201,7 +201,7 @@ impl WatchOnly { let mut input = Input { witness_utxo: Some(previous_output()), ..Default::default() }; let pk = self.input_xpub.to_pub(); - let wpkh = pk.wpubkey_hash().expect("a compressed pubkey"); + let wpkh = pk.wpubkey_hash(); let redeem_script = ScriptBuf::new_p2wpkh(&wpkh); input.redeem_script = Some(redeem_script); @@ -209,7 +209,7 @@ impl WatchOnly { let fingerprint = self.master_fingerprint; let path = input_derivation_path()?; let mut map = BTreeMap::new(); - map.insert(pk.inner, (fingerprint, path)); + map.insert(pk.0, (fingerprint, path)); input.bip32_derivation = map; let ty = PsbtSighashType::from_str("SIGHASH_ALL")?; @@ -251,12 +251,12 @@ impl WatchOnly { fn change_address( &self, secp: &Secp256k1, - ) -> Result<(PublicKey, Address, DerivationPath)> { + ) -> Result<(CompressedPublicKey, Address, DerivationPath)> { let path = [ChildNumber::from_normal_idx(1)?, ChildNumber::from_normal_idx(0)?]; let derived = self.account_0_xpub.derive_pub(secp, &path)?; let pk = derived.to_pub(); - let addr = Address::p2wpkh(&pk, NETWORK)?; + let addr = Address::p2wpkh(&pk, NETWORK); let path = path.into_derivation_path()?; Ok((pk, addr, path)) diff --git a/bitcoin/examples/sighash.rs b/bitcoin/examples/sighash.rs index 6bb9beb2..138e422d 100644 --- a/bitcoin/examples/sighash.rs +++ b/bitcoin/examples/sighash.rs @@ -1,5 +1,5 @@ use bitcoin::hashes::Hash; -use bitcoin::{consensus, ecdsa, sighash, Amount, PublicKey, Script, ScriptBuf, Transaction}; +use bitcoin::{consensus, ecdsa, sighash, Amount, CompressedPublicKey, Script, ScriptBuf, Transaction}; use hex_lit::hex; //These are real blockchain transactions examples of computing sighash for: @@ -35,8 +35,8 @@ fn compute_sighash_p2wpkh(raw_tx: &[u8], inp_idx: usize, value: u64) { //BIP-143: "The item 5 : For P2WPKH witness program, the scriptCode is 0x1976a914{20-byte-pubkey-hash}88ac" //this is nothing but a standard P2PKH script OP_DUP OP_HASH160 OP_EQUALVERIFY OP_CHECKSIG: - let pk = PublicKey::from_slice(pk_bytes).expect("failed to parse pubkey"); - let wpkh = pk.wpubkey_hash().expect("compressed key"); + let pk = CompressedPublicKey::from_slice(pk_bytes).expect("failed to parse pubkey"); + let wpkh = pk.wpubkey_hash(); println!("Script pubkey hash: {:x}", wpkh); let spk = ScriptBuf::new_p2wpkh(&wpkh); @@ -48,7 +48,7 @@ fn compute_sighash_p2wpkh(raw_tx: &[u8], inp_idx: usize, value: u64) { let msg = secp256k1::Message::from_digest(sighash.to_byte_array()); println!("Message is {:x}", msg); let secp = secp256k1::Secp256k1::verification_only(); - secp.verify_ecdsa(&msg, &sig.sig, &pk.inner).unwrap(); + pk.verify(&secp, &msg, &sig).unwrap() } /// Computes sighash for a legacy multisig transaction input that spends either a p2sh or a p2ms output. diff --git a/bitcoin/src/address/mod.rs b/bitcoin/src/address/mod.rs index 362b2c42..336f63b3 100644 --- a/bitcoin/src/address/mod.rs +++ b/bitcoin/src/address/mod.rs @@ -45,7 +45,7 @@ use crate::blockdata::constants::{ use crate::blockdata::script::witness_program::WitnessProgram; use crate::blockdata::script::witness_version::WitnessVersion; use crate::blockdata::script::{self, PushBytesBuf, Script, ScriptBuf, ScriptHash}; -use crate::crypto::key::{self, PubkeyHash, PublicKey, TweakedPublicKey, UntweakedPublicKey}; +use crate::crypto::key::{PubkeyHash, PublicKey, CompressedPublicKey, TweakedPublicKey, UntweakedPublicKey}; use crate::network::Network; use crate::prelude::*; use crate::taproot::TapNodeHash; @@ -441,23 +441,23 @@ impl Address { /// /// This is the native segwit address type for an output redeemable with a single signature. pub fn p2wpkh( - pk: &PublicKey, + pk: &CompressedPublicKey, network: Network, - ) -> Result { - let program = WitnessProgram::p2wpkh(pk)?; - Ok(Address::from_witness_program(program, network)) + ) -> Self { + let program = WitnessProgram::p2wpkh(pk); + Address::from_witness_program(program, network) } /// Creates 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: &PublicKey, + pk: &CompressedPublicKey, network: Network, - ) -> Result { - let builder = script::Builder::new().push_int(0).push_slice(pk.wpubkey_hash()?); + ) -> Self { + let builder = script::Builder::new().push_int(0).push_slice(pk.wpubkey_hash()); let script_hash = builder.as_script().script_hash(); - Ok(Address::p2sh_from_hash(script_hash, network)) + Address::p2sh_from_hash(script_hash, network) } /// Creates a witness pay to script hash address. @@ -838,7 +838,7 @@ mod tests { use secp256k1::XOnlyPublicKey; use super::*; - use crate::crypto::key::{PublicKey, UncompressedPubkeyError}; + use crate::crypto::key::PublicKey; use crate::network::Network::{Bitcoin, Testnet}; fn roundtrips(addr: &Address, network: Network) { @@ -926,17 +926,13 @@ mod tests { #[test] fn test_p2wpkh() { // stolen from Bitcoin transaction: b3c8c2b6cfc335abbcb2c7823a8453f55d64b2b5125a9a61e8737230cdb8ce20 - let mut key = "033bc8c83c52df5712229a2f72206d90192366c36428cb0c12b6af98324d97bfbc" - .parse::() + let key = "033bc8c83c52df5712229a2f72206d90192366c36428cb0c12b6af98324d97bfbc" + .parse::() .unwrap(); - let addr = Address::p2wpkh(&key, Bitcoin).unwrap(); + let addr = Address::p2wpkh(&key, Bitcoin); assert_eq!(&addr.to_string(), "bc1qvzvkjn4q3nszqxrv3nraga2r822xjty3ykvkuw"); assert_eq!(addr.address_type(), Some(AddressType::P2wpkh)); roundtrips(&addr, Bitcoin); - - // Test uncompressed pubkey - key.compressed = false; - assert_eq!(Address::p2wpkh(&key, Bitcoin).unwrap_err(), UncompressedPubkeyError); } #[test] @@ -955,17 +951,13 @@ mod tests { #[test] fn test_p2shwpkh() { // stolen from Bitcoin transaction: ad3fd9c6b52e752ba21425435ff3dd361d6ac271531fc1d2144843a9f550ad01 - let mut key = "026c468be64d22761c30cd2f12cbc7de255d592d7904b1bab07236897cc4c2e766" - .parse::() + let key = "026c468be64d22761c30cd2f12cbc7de255d592d7904b1bab07236897cc4c2e766" + .parse::() .unwrap(); - let addr = Address::p2shwpkh(&key, Bitcoin).unwrap(); + let addr = Address::p2shwpkh(&key, Bitcoin); assert_eq!(&addr.to_string(), "3QBRmWNqqBGme9er7fMkGqtZtp4gjMFxhE"); assert_eq!(addr.address_type(), Some(AddressType::P2sh)); roundtrips(&addr, Bitcoin); - - // Test uncompressed pubkey - key.compressed = false; - assert_eq!(Address::p2wpkh(&key, Bitcoin).unwrap_err(), UncompressedPubkeyError); } #[test] diff --git a/bitcoin/src/bip32.rs b/bitcoin/src/bip32.rs index 0016d77d..ad4dfcf9 100644 --- a/bitcoin/src/bip32.rs +++ b/bitcoin/src/bip32.rs @@ -19,7 +19,7 @@ use secp256k1::{self, Secp256k1, XOnlyPublicKey}; use serde; use crate::base58; -use crate::crypto::key::{self, Keypair, PrivateKey, PublicKey}; +use crate::crypto::key::{self, Keypair, PrivateKey, CompressedPublicKey}; use crate::internal_macros::impl_bytes_newtype; use crate::network::Network; use crate::prelude::*; @@ -708,7 +708,7 @@ impl Xpub { } /// Constructs ECDSA compressed public key matching internal public key representation. - pub fn to_pub(self) -> PublicKey { PublicKey { compressed: true, inner: self.public_key } } + pub fn to_pub(self) -> CompressedPublicKey { CompressedPublicKey(self.public_key) } /// Constructs BIP340 x-only public key for BIP-340 signatures and Taproot use matching /// the internal public key representation. diff --git a/bitcoin/src/blockdata/script/witness_program.rs b/bitcoin/src/blockdata/script/witness_program.rs index 2c4cf1c4..d2b67024 100644 --- a/bitcoin/src/blockdata/script/witness_program.rs +++ b/bitcoin/src/blockdata/script/witness_program.rs @@ -14,7 +14,7 @@ use secp256k1::{Secp256k1, Verification}; use crate::blockdata::script::witness_version::WitnessVersion; use crate::blockdata::script::{PushBytes, PushBytesBuf, PushBytesErrorReport, Script}; -use crate::crypto::key::{self, PublicKey, TapTweak, TweakedPublicKey, UntweakedPublicKey}; +use crate::crypto::key::{CompressedPublicKey, TapTweak, TweakedPublicKey, UntweakedPublicKey}; use crate::taproot::TapNodeHash; /// The segregated witness program. @@ -67,10 +67,9 @@ impl WitnessProgram { } /// Creates a [`WitnessProgram`] from `pk` for a P2WPKH output. - pub fn p2wpkh(pk: &PublicKey) -> Result { - let hash = pk.wpubkey_hash()?; - let program = WitnessProgram::new_p2wpkh(hash.to_byte_array()); - Ok(program) + pub fn p2wpkh(pk: &CompressedPublicKey) -> Self { + let hash = pk.wpubkey_hash(); + WitnessProgram::new_p2wpkh(hash.to_byte_array()) } /// Creates a [`WitnessProgram`] from `script` for a P2WSH output. diff --git a/bitcoin/src/crypto/key.rs b/bitcoin/src/crypto/key.rs index 6b136d9d..d4322acb 100644 --- a/bitcoin/src/crypto/key.rs +++ b/bitcoin/src/crypto/key.rs @@ -261,6 +261,129 @@ impl From<&PublicKey> for PubkeyHash { fn from(key: &PublicKey) -> PubkeyHash { key.pubkey_hash() } } +/// An always-compressed Bitcoin ECDSA public key +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct CompressedPublicKey(pub secp256k1::PublicKey); + +impl CompressedPublicKey { + /// Returns bitcoin 160-bit hash of the public key + pub fn pubkey_hash(&self) -> PubkeyHash { PubkeyHash::hash(&self.to_bytes()) } + + /// Returns bitcoin 160-bit hash of the public key for witness program + pub fn wpubkey_hash(&self) -> WPubkeyHash { + WPubkeyHash::from_byte_array(hash160::Hash::hash(&self.to_bytes()).to_byte_array()) + } + + /// Write the public key into a writer + pub fn write_into(&self, writer: &mut W) -> Result<(), io::Error> { + writer.write_all(&self.to_bytes()) + } + + /// Read the public key from a reader + /// + /// This internally reads the first byte before reading the rest, so + /// use of a `BufReader` is recommended. + pub fn read_from(reader: &mut R) -> Result { + let mut bytes = [0; 33]; + + reader.read_exact(&mut bytes)?; + Self::from_slice(&bytes).map_err(|e| { + // Need a static string for no-std io + #[cfg(feature = "std")] + let reason = e; + #[cfg(not(feature = "std"))] + let reason = "secp256k1 error"; + io::Error::new(io::ErrorKind::InvalidData, reason) + }) + } + + /// Serializes the public key. + /// + /// As the type name suggests, the key is serialzied in compressed format. + /// + /// Note that this can be used as a sort key to get BIP67-compliant sorting. + /// That's why this type doesn't have the `to_sort_key` method - it would duplicate this one. + pub fn to_bytes(&self) -> [u8; 33] { + self.0.serialize() + } + + /// Deserialize a public key from a slice + pub fn from_slice(data: &[u8]) -> Result { + secp256k1::PublicKey::from_slice(data).map(CompressedPublicKey) + } + + /// Computes the public key as supposed to be used with this secret + pub fn from_private_key( + secp: &Secp256k1, + sk: &PrivateKey, + ) -> Result { + sk.public_key(secp).try_into() + } + + /// Checks that `sig` is a valid ECDSA signature for `msg` using this public key. + pub fn verify( + &self, + secp: &Secp256k1, + msg: &secp256k1::Message, + sig: &ecdsa::Signature, + ) -> Result<(), Error> { + Ok(secp.verify_ecdsa(msg, &sig.sig, &self.0)?) + } +} + +impl fmt::Display for CompressedPublicKey { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::LowerHex::fmt(&self.to_bytes().as_hex(), f) + } +} + +impl FromStr for CompressedPublicKey { + type Err = Error; + + fn from_str(s: &str) -> Result { + CompressedPublicKey::from_slice(&<[u8; 33]>::from_hex(s)?).map_err(Into::into) + } +} + +impl TryFrom for CompressedPublicKey { + type Error = UncompressedPubkeyError; + + fn try_from(value: PublicKey) -> Result { + if value.compressed { + Ok(CompressedPublicKey(value.inner)) + } else { + Err(UncompressedPubkeyError) + } + } +} + +impl From for PublicKey { + fn from(value: CompressedPublicKey) -> Self { + PublicKey::new(value.0) + } +} + +impl From for XOnlyPublicKey { + fn from(pk: CompressedPublicKey) -> Self { pk.0.into() } +} + +impl From for PubkeyHash { + fn from(key: CompressedPublicKey) -> Self { key.pubkey_hash() } +} + +impl From<&CompressedPublicKey> for PubkeyHash { + fn from(key: &CompressedPublicKey) -> Self { key.pubkey_hash() } +} + +impl From for WPubkeyHash { + fn from(key: CompressedPublicKey) -> Self { key.wpubkey_hash() } +} + +impl From<&CompressedPublicKey> for WPubkeyHash { + fn from(key: &CompressedPublicKey) -> Self { key.wpubkey_hash() } +} + + /// A Bitcoin ECDSA private key #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct PrivateKey { @@ -484,6 +607,71 @@ impl<'de> serde::Deserialize<'de> for PublicKey { } } +#[cfg(feature = "serde")] +impl serde::Serialize for CompressedPublicKey { + fn serialize(&self, s: S) -> Result { + if s.is_human_readable() { + s.collect_str(self) + } else { + s.serialize_bytes(&self.to_bytes()) + } + } +} + +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for CompressedPublicKey { + fn deserialize>(d: D) -> Result { + if d.is_human_readable() { + struct HexVisitor; + + impl<'de> serde::de::Visitor<'de> for HexVisitor { + type Value = CompressedPublicKey; + + fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result { + formatter.write_str("a 66 digits long ASCII hex string") + } + + fn visit_bytes(self, v: &[u8]) -> Result + where + E: serde::de::Error, + { + if let Ok(hex) = core::str::from_utf8(v) { + CompressedPublicKey::from_str(hex).map_err(E::custom) + } else { + Err(E::invalid_value(::serde::de::Unexpected::Bytes(v), &self)) + } + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + CompressedPublicKey::from_str(v).map_err(E::custom) + } + } + d.deserialize_str(HexVisitor) + } else { + struct BytesVisitor; + + impl<'de> serde::de::Visitor<'de> for BytesVisitor { + type Value = CompressedPublicKey; + + fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result { + formatter.write_str("a bytestring") + } + + fn visit_bytes(self, v: &[u8]) -> Result + where + E: serde::de::Error, + { + CompressedPublicKey::from_slice(v).map_err(E::custom) + } + } + + d.deserialize_bytes(BytesVisitor) + } + } +} /// Untweaked BIP-340 X-coord-only public key pub type UntweakedPublicKey = XOnlyPublicKey; diff --git a/bitcoin/src/lib.rs b/bitcoin/src/lib.rs index d65fe1e6..cf3cc311 100644 --- a/bitcoin/src/lib.rs +++ b/bitcoin/src/lib.rs @@ -130,7 +130,7 @@ pub use crate::{ blockdata::witness::{self, Witness}, consensus::encode::VarInt, crypto::ecdsa, - crypto::key::{self, PrivateKey, PubkeyHash, PublicKey, WPubkeyHash, XOnlyPublicKey}, + crypto::key::{self, PrivateKey, PubkeyHash, PublicKey, CompressedPublicKey, WPubkeyHash, XOnlyPublicKey}, crypto::sighash::{self, LegacySighash, SegwitV0Sighash, TapSighash, TapSighashTag}, merkle_tree::MerkleBlock, network::Network, diff --git a/bitcoin/src/sign_message.rs b/bitcoin/src/sign_message.rs index ca21de32..c543c4ff 100644 --- a/bitcoin/src/sign_message.rs +++ b/bitcoin/src/sign_message.rs @@ -238,22 +238,25 @@ mod tests { assert_eq!(signature.to_base64(), signature.to_string()); let signature2 = super::MessageSignature::from_str(&signature.to_string()).unwrap(); - let pubkey = signature2.recover_pubkey(&secp, msg_hash).unwrap(); - assert!(pubkey.compressed); - assert_eq!(pubkey.inner, secp256k1::PublicKey::from_secret_key(&secp, &privkey)); + let pubkey = signature2.recover_pubkey(&secp, msg_hash) + .unwrap() + .try_into() + .expect("compressed was set to true"); - let p2pkh = Address::p2pkh(pubkey, Network::Bitcoin); - assert_eq!(signature2.is_signed_by_address(&secp, &p2pkh, msg_hash), Ok(true)); - let p2wpkh = Address::p2wpkh(&pubkey, Network::Bitcoin).unwrap(); + let p2wpkh = Address::p2wpkh(&pubkey, Network::Bitcoin); assert_eq!( signature2.is_signed_by_address(&secp, &p2wpkh, msg_hash), Err(MessageSignatureError::UnsupportedAddressType(AddressType::P2wpkh)) ); - let p2shwpkh = Address::p2shwpkh(&pubkey, Network::Bitcoin).unwrap(); + let p2shwpkh = Address::p2shwpkh(&pubkey, Network::Bitcoin); assert_eq!( signature2.is_signed_by_address(&secp, &p2shwpkh, msg_hash), Err(MessageSignatureError::UnsupportedAddressType(AddressType::P2sh)) ); + let p2pkh = Address::p2pkh(pubkey, Network::Bitcoin); + assert_eq!(signature2.is_signed_by_address(&secp, &p2pkh, msg_hash), Ok(true)); + + assert_eq!(pubkey.0, secp256k1::PublicKey::from_secret_key(&secp, &privkey)); } #[test]