Implement `CompressedPublicKey`

P2WPKH requires keys to be compressed which introduces error handling
even in cases when it's statically known that a key is compressed. To
avoid it, this change introduces `CompressedPublicKey` which is similar
to `PublicKey` except it's statically known to be compressed.

This also changes relevant code to use `CompressedPublicKey` instead of
`PublicKey`.
This commit is contained in:
Martin Habovstiak 2023-12-11 21:10:32 +01:00
parent 6fe073b324
commit a92d49fe33
10 changed files with 234 additions and 52 deletions

View File

@ -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());

View File

@ -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);
}

View File

@ -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<C: Verification>(
&self,
secp: &Secp256k1<C>,
) -> 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))

View File

@ -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 <pubKeyHash> 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.

View File

@ -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<Address, key::UncompressedPubkeyError> {
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<Address, key::UncompressedPubkeyError> {
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::<PublicKey>()
let key = "033bc8c83c52df5712229a2f72206d90192366c36428cb0c12b6af98324d97bfbc"
.parse::<CompressedPublicKey>()
.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::<PublicKey>()
let key = "026c468be64d22761c30cd2f12cbc7de255d592d7904b1bab07236897cc4c2e766"
.parse::<CompressedPublicKey>()
.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]

View File

@ -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.

View File

@ -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<Self, key::UncompressedPubkeyError> {
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.

View File

@ -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<W: io::Write + ?Sized>(&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<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, io::Error> {
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<Self, secp256k1::Error> {
secp256k1::PublicKey::from_slice(data).map(CompressedPublicKey)
}
/// Computes the public key as supposed to be used with this secret
pub fn from_private_key<C: secp256k1::Signing>(
secp: &Secp256k1<C>,
sk: &PrivateKey,
) -> Result<Self, UncompressedPubkeyError> {
sk.public_key(secp).try_into()
}
/// Checks that `sig` is a valid ECDSA signature for `msg` using this public key.
pub fn verify<C: secp256k1::Verification>(
&self,
secp: &Secp256k1<C>,
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<Self, Self::Err> {
CompressedPublicKey::from_slice(&<[u8; 33]>::from_hex(s)?).map_err(Into::into)
}
}
impl TryFrom<PublicKey> for CompressedPublicKey {
type Error = UncompressedPubkeyError;
fn try_from(value: PublicKey) -> Result<Self, Self::Error> {
if value.compressed {
Ok(CompressedPublicKey(value.inner))
} else {
Err(UncompressedPubkeyError)
}
}
}
impl From<CompressedPublicKey> for PublicKey {
fn from(value: CompressedPublicKey) -> Self {
PublicKey::new(value.0)
}
}
impl From<CompressedPublicKey> for XOnlyPublicKey {
fn from(pk: CompressedPublicKey) -> Self { pk.0.into() }
}
impl From<CompressedPublicKey> 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<CompressedPublicKey> 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<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
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: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
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<E>(self, v: &[u8]) -> Result<Self::Value, E>
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<E>(self, v: &str) -> Result<Self::Value, E>
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<E>(self, v: &[u8]) -> Result<Self::Value, E>
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;

View File

@ -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,

View File

@ -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]