Wrap secp256k1::XOnlyPublicKey to improve error handling

This commit creates a wrapper type for XOnlyPublicKey instead of
directly re-exporting it from the secp256k1 library.
This commit is contained in:
Erick Cestari 2025-04-28 10:23:33 -03:00
parent f9bc0f517d
commit 2a518d62e6
No known key found for this signature in database
GPG Key ID: D7D17E26F2FC3F3C
13 changed files with 179 additions and 35 deletions

View File

@ -26,7 +26,7 @@ fn main() {
// Get an unspent output that is locked to the key above that we control.
// In a real application these would come from the chain.
let (dummy_out_point, dummy_utxo) = dummy_unspent_transaction_output(&secp, internal_key);
let (dummy_out_point, dummy_utxo) = dummy_unspent_transaction_output(&secp, internal_key.into());
// Get an address to send to.
let address = receivers_address();
@ -45,7 +45,7 @@ fn main() {
// The change output is locked to a key controlled by us.
let change = TxOut {
value: CHANGE_AMOUNT,
script_pubkey: ScriptBuf::new_p2tr(&secp, internal_key, None), // Change comes back to us.
script_pubkey: ScriptBuf::new_p2tr(&secp, internal_key.into(), None), // Change comes back to us.
};
// The transaction we want to sign and broadcast.

View File

@ -151,12 +151,12 @@ fn main() {
// Get the Tap Key Origins
// Map of tap root X-only keys to origin info and leaf hashes contained in it.
let origin_input_1 = get_tap_key_origin(
pk_input_1,
pk_input_1.into(),
MASTER_FINGERPRINT.parse::<Fingerprint>().unwrap(),
"m/86'/0'/0'/0/0".parse::<DerivationPath>().unwrap(),
);
let origin_input_2 = get_tap_key_origin(
pk_input_2,
pk_input_2.into(),
MASTER_FINGERPRINT.parse::<Fingerprint>().unwrap(),
"m/86'/0'/0'/1/0".parse::<DerivationPath>().unwrap(),
);
@ -187,7 +187,7 @@ fn main() {
// The change output is locked to a key controlled by us.
let change = TxOut {
value: CHANGE_AMOUNT,
script_pubkey: ScriptBuf::new_p2tr(&secp, pk_change, None), // Change comes back to us.
script_pubkey: ScriptBuf::new_p2tr(&secp, pk_change.into(), None), // Change comes back to us.
};
// The transaction we want to sign and broadcast.
@ -210,14 +210,14 @@ fn main() {
Input {
witness_utxo: Some(utxos[0].clone()),
tap_key_origins: origins[0].clone(),
tap_internal_key: Some(pk_input_1),
tap_internal_key: Some(pk_input_1.into()),
sighash_type: Some(ty),
..Default::default()
},
Input {
witness_utxo: Some(utxos[1].clone()),
tap_key_origins: origins[1].clone(),
tap_internal_key: Some(pk_input_2),
tap_internal_key: Some(pk_input_2.into()),
sighash_type: Some(ty),
..Default::default()
},

View File

@ -404,7 +404,7 @@ impl BenefactorWallet {
let taproot_spend_info = TaprootBuilder::new()
.add_leaf(0, script.clone())?
.finalize(&self.secp, internal_keypair.x_only_public_key().0)
.finalize(&self.secp, internal_keypair.x_only_public_key().0.into())
.expect("should be finalizable");
self.current_spend_info = Some(taproot_spend_info.clone());
let script_pubkey = ScriptBuf::new_p2tr(
@ -442,7 +442,7 @@ impl BenefactorWallet {
(vec![leaf_hash], (self.beneficiary_xpub.fingerprint(), derivation_path.clone())),
);
origins.insert(
internal_keypair.x_only_public_key().0,
internal_keypair.x_only_public_key().0.into(),
(vec![], (self.master_xpriv.fingerprint(&self.secp), derivation_path)),
);
let ty = "SIGHASH_ALL".parse::<PsbtSighashType>()?;
@ -457,7 +457,7 @@ impl BenefactorWallet {
tap_key_origins: origins,
tap_merkle_root: taproot_spend_info.merkle_root(),
sighash_type: Some(ty),
tap_internal_key: Some(internal_keypair.x_only_public_key().0),
tap_internal_key: Some(internal_keypair.x_only_public_key().0.into()),
tap_scripts,
..Default::default()
};
@ -502,7 +502,7 @@ impl BenefactorWallet {
let taproot_spend_info = TaprootBuilder::new()
.add_leaf(0, script.clone())?
.finalize(&self.secp, new_internal_keypair.x_only_public_key().0)
.finalize(&self.secp, new_internal_keypair.x_only_public_key().0.into())
.expect("should be finalizable");
self.current_spend_info = Some(taproot_spend_info.clone());
let prevout_script_pubkey = input.witness_utxo.as_ref().unwrap().script_pubkey.clone();
@ -608,7 +608,7 @@ impl BenefactorWallet {
tap_key_origins: origins,
tap_merkle_root: taproot_spend_info.merkle_root(),
sighash_type: Some(ty),
tap_internal_key: Some(new_internal_keypair.x_only_public_key().0),
tap_internal_key: Some(new_internal_keypair.x_only_public_key().0.into()),
tap_scripts,
..Default::default()
};

View File

@ -50,7 +50,7 @@ use bech32::primitives::gf32::Fe32;
use bech32::primitives::hrp::Hrp;
use hashes::{hash160, HashEngine};
use internals::array::ArrayExt;
use secp256k1::{Secp256k1, Verification, XOnlyPublicKey};
use secp256k1::{Secp256k1, Verification};
use crate::address::script_pubkey::ScriptBufExt as _;
use crate::constants::{
@ -69,6 +69,7 @@ use crate::script::{
WitnessScriptSizeError,
};
use crate::taproot::TapNodeHash;
use crate::XOnlyPublicKey;
#[rustfmt::skip] // Keep public re-exports separate.
#[doc(inline)]

View File

@ -13,12 +13,13 @@ use core::{fmt, slice};
use hashes::{hash160, hash_newtype, sha512, Hash, HashEngine, Hmac, HmacEngine};
use internals::array::ArrayExt;
use internals::write_err;
use secp256k1::{Secp256k1, XOnlyPublicKey};
use secp256k1::Secp256k1;
use crate::crypto::key::{CompressedPublicKey, Keypair, PrivateKey};
use crate::internal_macros::{impl_array_newtype, impl_array_newtype_stringify};
use crate::network::NetworkKind;
use crate::prelude::{String, Vec};
use crate::XOnlyPublicKey;
/// Version bytes for extended public keys on the Bitcoin network.
const VERSION_BYTES_MAINNET_PUBLIC: [u8; 4] = [0x04, 0x88, 0xB2, 0x1E];

View File

@ -25,11 +25,110 @@ use crate::script::{self, ScriptBuf};
use crate::taproot::{TapNodeHash, TapTweakHash};
#[rustfmt::skip] // Keep public re-exports separate.
pub use secp256k1::{constants, Keypair, Parity, Secp256k1, Verification, XOnlyPublicKey};
pub use secp256k1::{constants, Keypair, Parity, Secp256k1, Verification};
#[cfg(feature = "rand-std")]
pub use secp256k1::rand;
pub use serialized_x_only::SerializedXOnlyPublicKey;
/// A Bitcoin Schnorr X-only public key used for BIP340 signatures.
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct XOnlyPublicKey(secp256k1::XOnlyPublicKey);
impl XOnlyPublicKey {
/// Constructs a new x-only public key from the provided generic Secp256k1 x-only public key.
pub fn new(key: impl Into<secp256k1::XOnlyPublicKey>) -> XOnlyPublicKey {
XOnlyPublicKey(key.into())
}
/// Creates an x-only public key from a keypair.
///
/// Returns the x-only public key and the parity of the full public key.
#[inline]
pub fn from_keypair(keypair: &Keypair) -> (XOnlyPublicKey, Parity) {
let (xonly, parity) = secp256k1::XOnlyPublicKey::from_keypair(keypair);
(XOnlyPublicKey::new(xonly), parity)
}
/// Creates an x-only public key from a 32-byte x-coordinate.
///
/// Returns an error if the provided bytes don't represent a valid secp256k1 point x-coordinate.
#[inline]
pub fn from_byte_array(
data: &[u8; constants::SCHNORR_PUBLIC_KEY_SIZE],
) -> Result<XOnlyPublicKey, ParseXOnlyPublicKeyError> {
secp256k1::XOnlyPublicKey::from_byte_array(data)
.map(XOnlyPublicKey::new)
.map_err(|_| ParseXOnlyPublicKeyError::InvalidXCoordinate)
}
/// Serializes the x-only public key as a byte-encoded x coordinate value (32 bytes).
#[inline]
pub fn serialize(&self) -> [u8; constants::SCHNORR_PUBLIC_KEY_SIZE] { self.0.serialize() }
/// Converts this x-only public key to a full public key given the parity.
#[inline]
pub fn public_key(&self, parity: Parity) -> PublicKey { self.0.public_key(parity).into() }
/// Verifies that a tweak produced by [`XOnlyPublicKey::add_tweak`] was computed correctly.
///
/// Should be called on the original untweaked key. Takes the tweaked key and output parity from
/// [`XOnlyPublicKey::add_tweak`] as input.
#[inline]
pub fn tweak_add_check<V: Verification>(
&self,
secp: &Secp256k1<V>,
tweaked_key: &Self,
tweaked_parity: Parity,
tweak: secp256k1::Scalar,
) -> bool {
self.0.tweak_add_check(secp, &tweaked_key.0, tweaked_parity, tweak)
}
/// Tweaks an [`XOnlyPublicKey`] by adding the generator multiplied with the given tweak to it.
///
/// # Returns
///
/// The newly tweaked key plus an opaque type representing the parity of the tweaked key, this
/// should be provided to `tweak_add_check` which can be used to verify a tweak more efficiently
/// than regenerating it and checking equality.
///
/// # Errors
///
/// If the resulting key would be invalid.
#[inline]
pub fn add_tweak<V: Verification>(
&self,
secp: &Secp256k1<V>,
tweak: &secp256k1::Scalar,
) -> Result<(XOnlyPublicKey, Parity), TweakXOnlyPublicKeyError> {
match self.0.add_tweak(secp, tweak) {
Ok((xonly, parity)) => Ok((XOnlyPublicKey(xonly), parity)),
Err(secp256k1::Error::InvalidTweak) => Err(TweakXOnlyPublicKeyError::BadTweak),
Err(secp256k1::Error::InvalidParityValue(_)) =>
Err(TweakXOnlyPublicKeyError::ParityError),
Err(_) => Err(TweakXOnlyPublicKeyError::ResultKeyInvalid),
}
}
}
impl FromStr for XOnlyPublicKey {
type Err = ParseXOnlyPublicKeyError;
fn from_str(s: &str) -> Result<XOnlyPublicKey, ParseXOnlyPublicKeyError> {
secp256k1::XOnlyPublicKey::from_str(s)
.map(XOnlyPublicKey::from)
.map_err(|_| ParseXOnlyPublicKeyError::InvalidXCoordinate)
}
}
impl From<secp256k1::XOnlyPublicKey> for XOnlyPublicKey {
fn from(pk: secp256k1::XOnlyPublicKey) -> XOnlyPublicKey { XOnlyPublicKey::new(pk) }
}
impl From<secp256k1::PublicKey> for XOnlyPublicKey {
fn from(pk: secp256k1::PublicKey) -> XOnlyPublicKey { XOnlyPublicKey::new(pk) }
}
/// A Bitcoin ECDSA public key.
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct PublicKey {
@ -222,7 +321,7 @@ impl From<secp256k1::PublicKey> for PublicKey {
}
impl From<PublicKey> for XOnlyPublicKey {
fn from(pk: PublicKey) -> XOnlyPublicKey { pk.inner.into() }
fn from(pk: PublicKey) -> XOnlyPublicKey { XOnlyPublicKey::new(pk.inner) }
}
/// An opaque return type for PublicKey::to_sort_key.
@ -743,13 +842,13 @@ pub type UntweakedPublicKey = XOnlyPublicKey;
pub struct TweakedPublicKey(XOnlyPublicKey);
impl fmt::LowerHex for TweakedPublicKey {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::LowerHex::fmt(&self.0, f) }
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::LowerHex::fmt(&self.0 .0, f) }
}
// Allocate for serialized size
impl_to_hex_from_lower_hex!(TweakedPublicKey, |_| constants::SCHNORR_PUBLIC_KEY_SIZE * 2);
impl fmt::Display for TweakedPublicKey {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(&self.0, f) }
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(&self.0 .0, f) }
}
/// Untweaked BIP-340 key pair.
@ -874,7 +973,7 @@ impl TweakedPublicKey {
#[inline]
pub fn from_keypair(keypair: TweakedKeypair) -> Self {
let (xonly, _parity) = keypair.0.x_only_public_key();
TweakedPublicKey(xonly)
TweakedPublicKey(xonly.into())
}
/// Constructs a new [`TweakedPublicKey`] from a [`XOnlyPublicKey`]. No tweak is applied, consider
@ -914,7 +1013,7 @@ impl TweakedKeypair {
#[inline]
pub fn public_parts(&self) -> (TweakedPublicKey, Parity) {
let (xonly, parity) = self.0.x_only_public_key();
(TweakedPublicKey(xonly), parity)
(TweakedPublicKey(xonly.into()), parity)
}
}
@ -1246,7 +1345,7 @@ mod serialized_x_only {
impl SerializedXOnlyPublicKey {
/// Returns `XOnlyPublicKey` if the bytes are valid.
pub fn to_validated(self) -> Result<XOnlyPublicKey, secp256k1::Error> {
pub fn to_validated(self) -> Result<XOnlyPublicKey, ParseXOnlyPublicKeyError> {
XOnlyPublicKey::from_byte_array(self.as_byte_array())
}
}
@ -1265,6 +1364,48 @@ impl fmt::Debug for SerializedXOnlyPublicKey {
}
}
/// Error that can occur when parsing an [`XOnlyPublicKey`] from bytes.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParseXOnlyPublicKeyError {
/// The provided bytes do not represent a valid secp256k1 point x-coordinate.
InvalidXCoordinate,
}
impl fmt::Display for ParseXOnlyPublicKeyError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::InvalidXCoordinate => write!(f, "Invalid X coordinate for secp256k1 point"),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for ParseXOnlyPublicKeyError {}
/// Error that can occur when tweaking an [`XOnlyPublicKey`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TweakXOnlyPublicKeyError {
/// The tweak value was invalid.
BadTweak,
/// The resulting public key would be invalid.
ResultKeyInvalid,
/// Invalid parity value encountered during the operation.
ParityError,
}
impl fmt::Display for TweakXOnlyPublicKeyError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::BadTweak => write!(f, "Invalid tweak value"),
Self::ResultKeyInvalid => write!(f, "Resulting public key would be invalid"),
Self::ParityError => write!(f, "Invalid parity value encountered"),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for TweakXOnlyPublicKeyError {}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -1878,10 +1878,11 @@ mod tests {
})
}
use secp256k1::{SecretKey, XOnlyPublicKey};
use secp256k1::SecretKey;
use crate::consensus::serde as con_serde;
use crate::taproot::{TapNodeHash, TapTweakHash};
use crate::XOnlyPublicKey;
#[derive(serde::Deserialize)]
struct UtxoSpent {

View File

@ -4,7 +4,6 @@ use core::fmt;
use core::str::FromStr;
use hashes::{hash160, ripemd160, sha256, sha256d};
use secp256k1::XOnlyPublicKey;
use crate::bip32::KeySource;
use crate::crypto::key::PublicKey;
@ -21,6 +20,7 @@ use crate::sighash::{
use crate::taproot::{ControlBlock, LeafVersion, TapLeafHash, TapNodeHash};
use crate::transaction::{Transaction, TxOut};
use crate::witness::Witness;
use crate::XOnlyPublicKey;
/// Type: Non-Witness UTXO PSBT_IN_NON_WITNESS_UTXO = 0x00
const PSBT_IN_NON_WITNESS_UTXO: u64 = 0x00;

View File

@ -1,13 +1,12 @@
// SPDX-License-Identifier: CC0-1.0
use secp256k1::XOnlyPublicKey;
use crate::bip32::KeySource;
use crate::prelude::{btree_map, BTreeMap, Vec};
use crate::psbt::map::Map;
use crate::psbt::{raw, Error};
use crate::script::ScriptBuf;
use crate::taproot::{TapLeafHash, TapTree};
use crate::XOnlyPublicKey;
/// Type: Redeem ScriptBuf PSBT_OUT_REDEEM_SCRIPT = 0x00
const PSBT_OUT_REDEEM_SCRIPT: u64 = 0x00;

View File

@ -850,14 +850,14 @@ impl GetKey for $map<PublicKey, PrivateKey> {
match key_request {
KeyRequest::Pubkey(pk) => Ok(self.get(&pk).cloned()),
KeyRequest::XOnlyPubkey(xonly) => {
let pubkey_even = PublicKey::new(xonly.public_key(secp256k1::Parity::Even));
let pubkey_even = xonly.public_key(secp256k1::Parity::Even);
let key = self.get(&pubkey_even).cloned();
if key.is_some() {
return Ok(key);
}
let pubkey_odd = PublicKey::new(xonly.public_key(secp256k1::Parity::Odd));
let pubkey_odd = xonly.public_key(secp256k1::Parity::Odd);
if let Some(priv_key) = self.get(&pubkey_odd).copied() {
let negated_priv_key = priv_key.negate();
return Ok(Some(negated_priv_key));
@ -2257,7 +2257,7 @@ mod tests {
pubkey_map.insert(pk, priv_key);
let req_result = pubkey_map.get_key(&KeyRequest::XOnlyPubkey(xonly), &secp).unwrap();
let req_result = pubkey_map.get_key(&KeyRequest::XOnlyPubkey(xonly.into()), &secp).unwrap();
let retrieved_key = req_result.unwrap();

View File

@ -9,7 +9,6 @@ use hashes::{hash160, ripemd160, sha256, sha256d};
use internals::compact_size;
#[allow(unused)] // MSRV polyfill
use internals::slice::SliceExt;
use secp256k1::XOnlyPublicKey;
use super::map::{Input, Map, Output, PsbtSighashType};
use crate::bip32::{ChildNumber, Fingerprint, KeySource};
@ -25,6 +24,7 @@ use crate::taproot::{
};
use crate::transaction::{Transaction, TxOut};
use crate::witness::Witness;
use crate::XOnlyPublicKey;
/// A trait for serializing a value as raw data for insertion into PSBT
/// key-value maps.

View File

@ -25,6 +25,7 @@ use crate::consensus::Encodable;
use crate::crypto::key::{
SerializedXOnlyPublicKey, TapTweak, TweakedPublicKey, UntweakedPublicKey,
};
use crate::key::ParseXOnlyPublicKeyError;
use crate::prelude::{BTreeMap, BTreeSet, BinaryHeap, Vec};
use crate::{Script, ScriptBuf};
@ -1523,7 +1524,7 @@ pub enum TaprootError {
/// Invalid control block size.
InvalidControlBlockSize(InvalidControlBlockSizeError),
/// Invalid Taproot internal key.
InvalidInternalKey(secp256k1::Error),
InvalidInternalKey(ParseXOnlyPublicKeyError),
/// Invalid control block hex
InvalidControlBlockHex(HexToBytesError),
}

View File

@ -11,9 +11,9 @@ use bitcoin::taproot::{LeafVersion, TaprootBuilder, TaprootSpendInfo};
use bitcoin::transaction::Version;
use bitcoin::{
absolute, script, Address, Network, OutPoint, PrivateKey, Psbt, ScriptBuf, Sequence,
Transaction, TxIn, TxOut, Witness,
Transaction, TxIn, TxOut, Witness, XOnlyPublicKey,
};
use secp256k1::{Keypair, Secp256k1, Signing, XOnlyPublicKey};
use secp256k1::{Keypair, Secp256k1, Signing};
use units::Amount;
#[test]
@ -66,7 +66,7 @@ fn psbt_sign_taproot() {
let internal_key = kp.x_only_public_key().0; // Ignore the parity.
let tree =
create_taproot_tree(secp, script1.clone(), script2.clone(), script3.clone(), internal_key);
create_taproot_tree(secp, script1.clone(), script2.clone(), script3.clone(), internal_key.into());
let address = create_p2tr_address(tree.clone());
assert_eq!(
@ -131,7 +131,7 @@ fn psbt_sign_taproot() {
address,
to_address,
tree.clone(),
x_only_pubkey,
x_only_pubkey.into(),
signing_key_path,
script2.clone(),
);
@ -146,7 +146,7 @@ fn psbt_sign_taproot() {
sig,
psbt_script_path_spend.inputs[0]
.tap_script_sigs
.get(&(x_only_pubkey, script2.clone().tapscript_leaf_hash()))
.get(&(x_only_pubkey.into(), script2.clone().tapscript_leaf_hash()))
.unwrap()
.signature
.to_string()