Return error from wpubkey_hash

Calling `wpubkey_hash` on a key that is uncompressed is flat out an
error, really it is a programmer error at build time because a segwit
key should never be compressed, however, for historical reasons we do
not enforce this in the type system. As a step towards clarity make it
an error to call `wpubkey_hash` on a an uncompressed pubkey. This adds
documentation and potentially might assist debugging for newer devs.
This commit is contained in:
Tobin C. Harding 2023-11-29 13:00:58 +11:00
parent f7ab253ce4
commit 3490433618
No known key found for this signature in database
GPG Key ID: 40BF9E4C269D6607
3 changed files with 34 additions and 23 deletions

View File

@ -6,6 +6,7 @@ use internals::write_err;
use crate::address::{Address, NetworkUnchecked}; use crate::address::{Address, NetworkUnchecked};
use crate::blockdata::script::{witness_program, witness_version}; use crate::blockdata::script::{witness_program, witness_version};
use crate::crypto::key;
use crate::prelude::String; use crate::prelude::String;
use crate::{base58, Network}; use crate::{base58, Network};
@ -18,7 +19,7 @@ pub enum Error {
/// A witness program error. /// A witness program error.
WitnessProgram(witness_program::Error), WitnessProgram(witness_program::Error),
/// An uncompressed pubkey was used where it is not allowed. /// An uncompressed pubkey was used where it is not allowed.
UncompressedPubkey, UncompressedPubkey(key::UncompressedPubkeyError),
/// Address size more than 520 bytes is not allowed. /// Address size more than 520 bytes is not allowed.
ExcessiveScriptSize, ExcessiveScriptSize,
/// Script is not a p2pkh, p2sh or witness program. /// Script is not a p2pkh, p2sh or witness program.
@ -41,8 +42,7 @@ impl fmt::Display for Error {
match *self { match *self {
WitnessVersion(ref e) => write_err!(f, "witness version construction error"; e), WitnessVersion(ref e) => write_err!(f, "witness version construction error"; e),
WitnessProgram(ref e) => write_err!(f, "witness program error"; e), WitnessProgram(ref e) => write_err!(f, "witness program error"; e),
UncompressedPubkey => UncompressedPubkey(ref e) => write_err!(f, "uncompressed pubkey"; e),
write!(f, "an uncompressed pubkey was used where it is not allowed"),
ExcessiveScriptSize => write!(f, "script size exceed 520 bytes"), ExcessiveScriptSize => write!(f, "script size exceed 520 bytes"),
UnrecognizedScript => write!(f, "script is not a p2pkh, p2sh or witness program"), UnrecognizedScript => write!(f, "script is not a p2pkh, p2sh or witness program"),
NetworkValidation { required, found, ref address } => { NetworkValidation { required, found, ref address } => {
@ -66,14 +66,16 @@ impl std::error::Error for Error {
match self { match self {
WitnessVersion(e) => Some(e), WitnessVersion(e) => Some(e),
WitnessProgram(e) => Some(e), WitnessProgram(e) => Some(e),
UncompressedPubkey UncompressedPubkey(e) => Some(e),
| ExcessiveScriptSize ExcessiveScriptSize | UnrecognizedScript | NetworkValidation { .. } => None,
| UnrecognizedScript
| NetworkValidation { .. } => None,
} }
} }
} }
impl From<key::UncompressedPubkeyError> for Error {
fn from(e: key::UncompressedPubkeyError) -> Self { Self::UncompressedPubkey(e) }
}
impl From<witness_version::TryFromError> for Error { impl From<witness_version::TryFromError> for Error {
fn from(e: witness_version::TryFromError) -> Error { Error::WitnessVersion(e) } fn from(e: witness_version::TryFromError) -> Error { Error::WitnessVersion(e) }
} }

View File

@ -424,10 +424,7 @@ impl Address {
/// # Errors /// # Errors
/// Will only return an error if an uncompressed public key is provided. /// Will only return an error if an uncompressed public key is provided.
pub fn p2wpkh(pk: &PublicKey, network: Network) -> Result<Address, Error> { pub fn p2wpkh(pk: &PublicKey, network: Network) -> Result<Address, Error> {
let prog = WitnessProgram::new( let prog = WitnessProgram::new(WitnessVersion::V0, pk.wpubkey_hash()?)?;
WitnessVersion::V0,
pk.wpubkey_hash().ok_or(Error::UncompressedPubkey)?,
)?;
let payload = Payload::WitnessProgram(prog); let payload = Payload::WitnessProgram(prog);
Ok(Address(AddressInner { network, payload }, PhantomData)) Ok(Address(AddressInner { network, payload }, PhantomData))
} }
@ -439,9 +436,7 @@ impl Address {
/// # Errors /// # Errors
/// Will only return an Error if an uncompressed public key is provided. /// Will only return an Error if an uncompressed public key is provided.
pub fn p2shwpkh(pk: &PublicKey, network: Network) -> Result<Address, Error> { pub fn p2shwpkh(pk: &PublicKey, network: Network) -> Result<Address, Error> {
let builder = script::Builder::new() let builder = script::Builder::new().push_int(0).push_slice(pk.wpubkey_hash()?);
.push_int(0)
.push_slice(pk.wpubkey_hash().ok_or(Error::UncompressedPubkey)?);
let payload = Payload::ScriptHash(builder.into_script().script_hash()); let payload = Payload::ScriptHash(builder.into_script().script_hash());
Ok(Address(AddressInner { network, payload }, PhantomData)) Ok(Address(AddressInner { network, payload }, PhantomData))
@ -802,7 +797,7 @@ mod tests {
use secp256k1::XOnlyPublicKey; use secp256k1::XOnlyPublicKey;
use super::*; use super::*;
use crate::crypto::key::PublicKey; use crate::crypto::key::{PublicKey, UncompressedPubkeyError};
use crate::network::Network::{Bitcoin, Testnet}; use crate::network::Network::{Bitcoin, Testnet};
fn roundtrips(addr: &Address) { fn roundtrips(addr: &Address) {
@ -903,7 +898,7 @@ mod tests {
// Test uncompressed pubkey // Test uncompressed pubkey
key.compressed = false; key.compressed = false;
assert_eq!(Address::p2wpkh(&key, Bitcoin), Err(Error::UncompressedPubkey)); assert_eq!(Address::p2wpkh(&key, Bitcoin).unwrap_err(), UncompressedPubkeyError.into());
} }
#[test] #[test]
@ -932,7 +927,7 @@ mod tests {
// Test uncompressed pubkey // Test uncompressed pubkey
key.compressed = false; key.compressed = false;
assert_eq!(Address::p2wpkh(&key, Bitcoin), Err(Error::UncompressedPubkey)); assert_eq!(Address::p2wpkh(&key, Bitcoin).unwrap_err(), UncompressedPubkeyError.into());
} }
#[test] #[test]

View File

@ -58,15 +58,13 @@ impl PublicKey {
pub fn pubkey_hash(&self) -> PubkeyHash { self.with_serialized(PubkeyHash::hash) } pub fn pubkey_hash(&self) -> PubkeyHash { self.with_serialized(PubkeyHash::hash) }
/// Returns bitcoin 160-bit hash of the public key for witness program /// Returns bitcoin 160-bit hash of the public key for witness program
pub fn wpubkey_hash(&self) -> Option<WPubkeyHash> { pub fn wpubkey_hash(&self) -> Result<WPubkeyHash, UncompressedPubkeyError> {
if self.compressed { if self.compressed {
Some(WPubkeyHash::from_byte_array( Ok(WPubkeyHash::from_byte_array(
hash160::Hash::hash(&self.inner.serialize()).to_byte_array(), hash160::Hash::hash(&self.inner.serialize()).to_byte_array(),
)) ))
} else { } else {
// We can't create witness pubkey hashes for an uncompressed Err(UncompressedPubkeyError)
// public keys
None
} }
} }
@ -746,6 +744,22 @@ impl From<hex::HexToArrayError> for Error {
fn from(e: hex::HexToArrayError) -> Self { Error::Hex(e) } fn from(e: hex::HexToArrayError) -> Self { Error::Hex(e) }
} }
/// Segwit public keys must always be compressed.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct UncompressedPubkeyError;
impl fmt::Display for UncompressedPubkeyError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("segwit public keys must always be compressed")
}
}
#[cfg(feature = "std")]
impl std::error::Error for UncompressedPubkeyError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None }
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::str::FromStr; use std::str::FromStr;
@ -826,7 +840,7 @@ mod tests {
pk.wpubkey_hash().unwrap().to_string(), pk.wpubkey_hash().unwrap().to_string(),
"9511aa27ef39bbfa4e4f3dd15f4d66ea57f475b4" "9511aa27ef39bbfa4e4f3dd15f4d66ea57f475b4"
); );
assert_eq!(upk.wpubkey_hash(), None); assert!(upk.wpubkey_hash().is_err());
} }
#[cfg(feature = "serde")] #[cfg(feature = "serde")]