crypto: Change to_public_key to return FullPublicKey
What changed, and why it matters
This commit is a routine API cleanup in the rust-bitcoin library. It changes several methods named to_public_key so they return a modern compressed public key type (FullPublicKey) instead of an older type that could be either compressed or uncompressed (LegacyPublicKey). The old behavior is preserved under new to_legacy_public_key methods. The change is about encouraging safer modern key handling, not about fixing an active security bug.
Treat as a normal API-breaking change. Downstream users should update calls to to_public_key where they expect LegacyPublicKey, and use to_legacy_public_key if uncompressed-key behavior is still required. No urgent security patch is indicated by the commit content.
Security signals we found
API behavior change: to_public_key now returns compressed FullPublicKey instead of optionally-uncompressed LegacyPublicKey
Encourages use of compressed/X-only public keys, which is considered modern best practice
Legacy uncompressed behavior explicitly moved to to_legacy_public_key
No memory-safety, cryptographic, or authorization flaw visible in the diff
Evidence from the diff
The patch renames/redirects to_public_key methods across PrivateKey, Keypair, and XOnlyPublicKey to return FullPublicKey (always compressed for PrivateKey). It introduces to_legacy_public_key methods to retain the previous LegacyPublicKey behavior. FullPublicKey::from_private_key no longer errors on uncompressed keys because the new to_public_key path always produces a compressed FullPublicKey. Existing callers in examples and tests are updated accordingly. This is a breaking API change aimed at type-system hygiene, not a vulnerability fix.
Changed components
bitcoin/src/crypto/key.rscrypto/src/key.rsbitcoin/examples/sign-tx-segwit-v0.rsbitcoin/src/sign_message.rsInspect captured patch +38 / −21
diff --git a/bitcoin/examples/sign-tx-segwit-v0.rs b/bitcoin/examples/sign-tx-segwit-v0.rs
index 36f24fd7..6163d9f2 100644
--- a/bitcoin/examples/sign-tx-segwit-v0.rs
+++ b/bitcoin/examples/sign-tx-segwit-v0.rs
@@ -69,7 +69,7 @@ fn main() {
let signature = ecdsa::Signature { signature: sk.raw_ecdsa_sign(sighash), sighash_type };
// Update the witness stack.
- let pk = sk.to_public_key().force_compressed();
+ let pk = sk.to_public_key();
*sighasher.witness_mut(input_index).unwrap() = Witness::p2wpkh(signature, pk);
// Get the signed transaction.
@@ -85,7 +85,7 @@ fn main() {
fn senders_keys() -> (PrivateKey, WPubkeyHash) {
let sk = PrivateKey::generate();
let pk = sk.to_public_key();
- let wpkh = pk.wpubkey_hash().expect("key is compressed");
+ let wpkh = pk.wpubkey_hash();
(sk, wpkh)
}
diff --git a/bitcoin/src/crypto/key.rs b/bitcoin/src/crypto/key.rs
index 1a35f55d..3bcd92ba 100644
--- a/bitcoin/src/crypto/key.rs
+++ b/bitcoin/src/crypto/key.rs
@@ -231,7 +231,7 @@ mod tests {
assert!(!sk.private_key.compressed());
assert_eq!(&sk.to_wif(), "5JYkZjmN7PVMjJUfJWfRFwtuXTGB439XV6faajeHPAM9Z2PT2R3");
- let mut pk = sk.private_key.to_public_key();
+ let mut pk = sk.private_key.to_legacy_public_key();
assert!(!pk.compressed());
assert_eq!(&pk.to_string(), "042e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd1af191923a2964c177f5b5923ae500fca49e99492d534aa3759d6b25a8bc971b133");
assert_eq!(pk, "042e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd1af191923a2964c177f5b5923ae500fca49e99492d534aa3759d6b25a8bc971b133"
diff --git a/bitcoin/src/sign_message.rs b/bitcoin/src/sign_message.rs
index 418454f6..9e8c2d83 100644
--- a/bitcoin/src/sign_message.rs
+++ b/bitcoin/src/sign_message.rs
@@ -284,7 +284,7 @@ mod tests {
let p2pkh = Address::p2pkh(pubkey, Network::Bitcoin);
assert_eq!(signature2.is_signed_by_address(&p2pkh, msg_hash), Ok(true));
- assert_eq!(pubkey, FullPublicKey::from_private_key(&privkey).unwrap());
+ assert_eq!(pubkey, FullPublicKey::from_private_key(&privkey));
let signature_base64 = signature.to_base64();
let signature_round_trip =
super::MessageSignature::from_base64(&signature_base64).expect("message signature");
diff --git a/crypto/src/key.rs b/crypto/src/key.rs
index 9cb2ab7d..63f05510 100644
--- a/crypto/src/key.rs
+++ b/crypto/src/key.rs
@@ -352,11 +352,15 @@ impl XOnlyPublicKey {
/// Converts this x-only public key to a full public key.
///
+ /// The [`FullPublicKey`] is constructed using the parity in this x-only public key.
+ #[inline]
+ pub fn to_public_key(self) -> FullPublicKey { self.as_inner().public_key(self.parity()).into() }
+
+ /// Converts this x-only public key to a legacy public key.
+ ///
/// The [`LegacyPublicKey`] is constructed using the parity in this x-only public key.
#[inline]
- pub fn to_public_key(self) -> LegacyPublicKey {
- self.as_inner().public_key(self.parity()).into()
- }
+ pub fn to_legacy_public_key(self) -> LegacyPublicKey { self.to_public_key().into() }
/// Verifies that a tweak produced by [`XOnlyPublicKey::add_tweak`] was computed correctly.
///
@@ -481,11 +485,17 @@ impl Keypair {
self.as_inner().to_secret_bytes()
}
+ /// Returns the [`FullPublicKey`] for this [`Keypair`].
+ ///
+ /// This is equivalent to using [`FullPublicKey::from_keypair`].
+ #[inline]
+ pub fn to_public_key(&self) -> FullPublicKey { FullPublicKey::from_keypair(self) }
+
/// Returns the [`LegacyPublicKey`] for this [`Keypair`].
///
/// This is equivalent to using [`LegacyPublicKey::from_keypair`].
#[inline]
- pub fn to_public_key(&self) -> LegacyPublicKey { LegacyPublicKey::from_keypair(self) }
+ pub fn to_legacy_public_key(&self) -> LegacyPublicKey { LegacyPublicKey::from_keypair(self) }
/// Returns the [`XOnlyPublicKey`] for this [`Keypair`].
///
@@ -733,7 +743,7 @@ impl LegacyPublicKey {
}
/// Computes the public key as supposed to be used with this secret.
- pub fn from_private_key(sk: &PrivateKey) -> Self { sk.to_public_key() }
+ pub fn from_private_key(sk: &PrivateKey) -> Self { sk.to_legacy_public_key() }
/// Extracts the public key from a Keypair
pub fn from_keypair(pair: &Keypair) -> Self { FullPublicKey::from_keypair(pair).into() }
@@ -863,13 +873,7 @@ impl FullPublicKey {
}
/// Computes the public key as supposed to be used with this secret.
- ///
- /// # Errors
- ///
- /// Errors if the private key is not compressed.
- pub fn from_private_key(sk: &PrivateKey) -> Result<Self, UncompressedPublicKeyError> {
- sk.to_public_key().try_into()
- }
+ pub fn from_private_key(sk: &PrivateKey) -> Self { sk.to_public_key() }
/// Extracts the public key from a Keypair
pub fn from_keypair(pair: &Keypair) -> Self {
@@ -962,7 +966,20 @@ impl PrivateKey {
}
/// Constructs a new public key from this private key.
- pub fn to_public_key(&self) -> LegacyPublicKey {
+ ///
+ /// The returned key is always in compressed form. Use [`to_legacy_public_key`] to get a key
+ /// that respects the `compressed` field of this private key.
+ ///
+ /// [`to_legacy_public_key`]: PrivateKey::to_legacy_public_key
+ pub fn to_public_key(&self) -> FullPublicKey {
+ FullPublicKey::from_secp(secp256k1::PublicKey::from_secret_key(self.as_inner()))
+ }
+
+ /// Constructs a new legacy public key from this private key.
+ ///
+ /// The `compressed` field of this private key determines whether the returned key is in
+ /// compressed or uncompressed form.
+ pub fn to_legacy_public_key(&self) -> LegacyPublicKey {
match self.compressed() {
true =>
LegacyPublicKey::from_secp(secp256k1::PublicKey::from_secret_key(self.as_inner())),
@@ -973,8 +990,8 @@ impl PrivateKey {
}
/// Constructs a new public key from this private key.
- #[deprecated(since = "TBD", note = "use `to_public_key` instead")]
- pub fn public_key(&self) -> LegacyPublicKey { self.to_public_key() }
+ #[deprecated(since = "TBD", note = "use `to_legacy_public_key` instead")]
+ pub fn public_key(&self) -> LegacyPublicKey { self.to_legacy_public_key() }
/// Serializes the private key to bytes.
#[deprecated(since = "TBD", note = "use to_secret_vec instead")]
@@ -2127,7 +2144,7 @@ mod tests {
#[cfg(feature = "rand")]
#[cfg(feature = "std")]
fn public_key_secp_roundtrip() {
- let bitcoin_key = Keypair::generate().to_public_key();
+ let bitcoin_key = Keypair::generate().to_legacy_public_key();
let secp_key =
secp256k1::PublicKey::from_byte_array_compressed(bitcoin_key.serialize_compressed())
.unwrap();
@@ -2166,7 +2183,7 @@ mod tests {
#[cfg(feature = "rand")]
#[cfg(feature = "std")]
fn serialized_legacy_public_key_roundtrip() {
- let key = Keypair::generate().to_public_key();
+ let key = Keypair::generate().to_legacy_public_key();
assert!(key.compressed());
let serialized = &key.to_bytes();
assert_eq!(serialized.len(), 33);
Why this scored 19/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.