Strengthen the type of `taproot_control_block()`

The type returned by `Witness::taproot_control_block()` was just `&[u8]`
which wasn't very nice since users then had to manually decode it which
so far also required allocation. Thanks to previous improvements to
`ControlBlock` it is now possible to return a `ControlBlock` type
directly.

To avoid expensive checks, this change adds a new type
`SerializedXOnlyPublicKey` which is a wrapper around `[u8; 32]` that is
used in `ControlBlock` if complete checking is undesirable. It is then
used in the `ControlBlock` returned from
`Witness::taproot_control_block`. Users can still conveniently validate
the key using `to_validated` method.

It then uses this type in the recently-added `P2TrSpend` type. As a side
effect this checks more properties of `Witness` when calling unrelated
methods on `Witness`. From correctness perspective this should be OK: a
witness obtained from a verified source will be correct anyway and, if
these checks were done by the caller, they can be removed.

From performance perspective, if the `Witness` was obtained from a
verified source (e.g. using Bitcoin Core RPC) these checks are wasted
CPU time. But they shouldn't be too expensive, we already avoid
`secp256k1` overhead and, given that they always succeed in such case,
they should be easy to branch-predict.
This commit is contained in:
Martin Habovstiak 2025-03-24 15:55:42 +01:00
parent e8a42d5851
commit 492073f288
3 changed files with 102 additions and 22 deletions

View File

@ -10,15 +10,15 @@ use io::{BufRead, Write};
use crate::consensus::encode::{self, Error, ReadExt, WriteExt, MAX_VEC_SIZE}; use crate::consensus::encode::{self, Error, ReadExt, WriteExt, MAX_VEC_SIZE};
use crate::consensus::{Decodable, Encodable}; use crate::consensus::{Decodable, Encodable};
use crate::crypto::ecdsa; use crate::crypto::ecdsa;
use crate::crypto::key::SerializedXOnlyPublicKey;
use crate::prelude::Vec; use crate::prelude::Vec;
#[cfg(doc)] #[cfg(doc)]
use crate::script::ScriptExt as _; use crate::script::ScriptExt as _;
use crate::taproot::{ use crate::taproot::{self, ControlBlock, LeafScript, TAPROOT_ANNEX_PREFIX, TaprootMerkleBranch};
self, ControlBlock, LeafScript, LeafVersion, TAPROOT_ANNEX_PREFIX, TAPROOT_CONTROL_BASE_SIZE,
TAPROOT_LEAF_MASK, TaprootMerkleBranch,
};
use crate::Script; use crate::Script;
type BorrowedControlBlock<'a> = ControlBlock<&'a TaprootMerkleBranch, &'a SerializedXOnlyPublicKey>;
#[rustfmt::skip] // Keep public re-exports separate. #[rustfmt::skip] // Keep public re-exports separate.
#[doc(inline)] #[doc(inline)]
pub use primitives::witness::{Iter, Witness}; pub use primitives::witness::{Iter, Witness};
@ -176,9 +176,8 @@ crate::internal_macros::define_extension_trait! {
/// version. /// version.
fn taproot_leaf_script(&self) -> Option<LeafScript<&Script>> { fn taproot_leaf_script(&self) -> Option<LeafScript<&Script>> {
match P2TrSpend::from_witness(self) { match P2TrSpend::from_witness(self) {
Some(P2TrSpend::Script { leaf_script, control_block, .. }) if control_block.len() >= TAPROOT_CONTROL_BASE_SIZE => { Some(P2TrSpend::Script { leaf_script, control_block, .. }) => {
let version = LeafVersion::from_consensus(control_block[0] & TAPROOT_LEAF_MASK).ok()?; Some(LeafScript { version: control_block.leaf_version, script: leaf_script, })
Some(LeafScript { version, script: leaf_script, })
}, },
_ => None, _ => None,
} }
@ -191,7 +190,7 @@ crate::internal_macros::define_extension_trait! {
/// byte of the last element being equal to 0x50. /// byte of the last element being equal to 0x50.
/// ///
/// See [`Script::is_p2tr`] to check whether this is actually a Taproot witness. /// See [`Script::is_p2tr`] to check whether this is actually a Taproot witness.
fn taproot_control_block(&self) -> Option<&[u8]> { fn taproot_control_block(&self) -> Option<BorrowedControlBlock<'_>> {
match P2TrSpend::from_witness(self) { match P2TrSpend::from_witness(self) {
Some(P2TrSpend::Script { control_block, .. }) => Some(control_block), Some(P2TrSpend::Script { control_block, .. }) => Some(control_block),
_ => None, _ => None,
@ -236,7 +235,7 @@ enum P2TrSpend<'a> {
}, },
Script { Script {
leaf_script: &'a Script, leaf_script: &'a Script,
control_block: &'a [u8], control_block: BorrowedControlBlock<'a>,
annex: Option<&'a [u8]>, annex: Option<&'a [u8]>,
}, },
} }
@ -275,17 +274,21 @@ impl<'a> P2TrSpend<'a> {
// last one does NOT start with TAPROOT_ANNEX_PREFIX. This is handled in the catchall // last one does NOT start with TAPROOT_ANNEX_PREFIX. This is handled in the catchall
// arm. // arm.
3.. if witness.last().expect("len > 0").starts_with(&[TAPROOT_ANNEX_PREFIX]) => { 3.. if witness.last().expect("len > 0").starts_with(&[TAPROOT_ANNEX_PREFIX]) => {
let control_block = witness.get_back(1).expect("len > 1");
let control_block = BorrowedControlBlock::decode_borrowed(control_block).ok()?;
let spend = P2TrSpend::Script { let spend = P2TrSpend::Script {
leaf_script: Script::from_bytes(witness.get_back(2).expect("len > 2")), leaf_script: Script::from_bytes(witness.get_back(2).expect("len > 2")),
control_block: witness.get_back(1).expect("len > 1"), control_block,
annex: witness.last(), annex: witness.last(),
}; };
Some(spend) Some(spend)
} }
_ => { _ => {
let control_block = witness.last().expect("len > 0");
let control_block = BorrowedControlBlock::decode_borrowed(control_block).ok()?;
let spend = P2TrSpend::Script { let spend = P2TrSpend::Script {
leaf_script: Script::from_bytes(witness.get_back(1).expect("len > 1")), leaf_script: Script::from_bytes(witness.get_back(1).expect("len > 1")),
control_block: witness.last().expect("len > 0"), control_block,
annex: None, annex: None,
}; };
Some(spend) Some(spend)
@ -324,6 +327,7 @@ mod test {
use crate::consensus::{deserialize, encode, serialize}; use crate::consensus::{deserialize, encode, serialize};
use crate::hex::DisplayHex; use crate::hex::DisplayHex;
use crate::sighash::EcdsaSighashType; use crate::sighash::EcdsaSighashType;
use crate::taproot::LeafVersion;
use crate::Transaction; use crate::Transaction;
#[test] #[test]
@ -383,7 +387,7 @@ mod test {
#[test] #[test]
fn get_tapscript() { fn get_tapscript() {
let tapscript = hex!("deadbeef"); let tapscript = hex!("deadbeef");
let control_block = hex!("02"); let control_block = hex!("c0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
// annex starting with 0x50 causes the branching logic. // annex starting with 0x50 causes the branching logic.
let annex = hex!("50"); let annex = hex!("50");
@ -449,7 +453,8 @@ mod test {
#[test] #[test]
fn get_control_block() { fn get_control_block() {
let tapscript = hex!("deadbeef"); let tapscript = hex!("deadbeef");
let control_block = hex!("02"); let control_block = hex!("c0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
let expected_control_block = BorrowedControlBlock::decode_borrowed(&control_block).unwrap();
// annex starting with 0x50 causes the branching logic. // annex starting with 0x50 causes the branching logic.
let annex = hex!("50"); let annex = hex!("50");
let signature = vec![0xff; 64]; let signature = vec![0xff; 64];
@ -468,15 +473,15 @@ mod test {
deserialize::<Witness>(&witness_serialized_key_spend_annex[..]).unwrap(); deserialize::<Witness>(&witness_serialized_key_spend_annex[..]).unwrap();
// With or without annex, the tapscript should be returned. // With or without annex, the tapscript should be returned.
assert_eq!(witness.taproot_control_block(), Some(&control_block[..])); assert_eq!(witness.taproot_control_block().unwrap(), expected_control_block);
assert_eq!(witness_annex.taproot_control_block(), Some(&control_block[..])); assert_eq!(witness_annex.taproot_control_block().unwrap(), expected_control_block);
assert!(witness_key_spend_annex.taproot_control_block().is_none()) assert!(witness_key_spend_annex.taproot_control_block().is_none())
} }
#[test] #[test]
fn get_annex() { fn get_annex() {
let tapscript = hex!("deadbeef"); let tapscript = hex!("deadbeef");
let control_block = hex!("02"); let control_block = hex!("c0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
// annex starting with 0x50 causes the branching logic. // annex starting with 0x50 causes the branching logic.
let annex = hex!("50"); let annex = hex!("50");

View File

@ -26,6 +26,7 @@ use crate::taproot::{TapNodeHash, TapTweakHash};
#[rustfmt::skip] // Keep public re-exports separate. #[rustfmt::skip] // Keep public re-exports separate.
pub use secp256k1::{constants, Keypair, Parity, Secp256k1, Verification, XOnlyPublicKey}; pub use secp256k1::{constants, Keypair, Parity, Secp256k1, Verification, XOnlyPublicKey};
pub use serialized_x_only::SerializedXOnlyPublicKey;
#[cfg(feature = "rand-std")] #[cfg(feature = "rand-std")]
pub use secp256k1::rand; pub use secp256k1::rand;
@ -1208,6 +1209,63 @@ impl fmt::Display for InvalidWifCompressionFlagError {
#[cfg(feature = "std")] #[cfg(feature = "std")]
impl std::error::Error for InvalidWifCompressionFlagError {} impl std::error::Error for InvalidWifCompressionFlagError {}
mod serialized_x_only {
internals::transparent_newtype! {
/// An array of bytes that's semantically an x-only public but was **not** validated.
///
/// This can be useful when validation is not desired but semantics of the bytes should be
/// preserved. The validation can still happen using `to_validated()` method.
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct SerializedXOnlyPublicKey([u8; 32]);
impl SerializedXOnlyPublicKey {
pub(crate) fn from_bytes_ref(bytes: &_) -> Self;
}
}
impl SerializedXOnlyPublicKey {
/// Marks the supplied bytes as a serialized x-only public key.
pub const fn from_byte_array(bytes: [u8; 32]) -> Self {
Self(bytes)
}
/// Returns the raw bytes.
pub const fn to_byte_array(self) -> [u8; 32] {
self.0
}
/// Returns a reference to the raw bytes.
pub const fn as_byte_array(&self) -> &[u8; 32] {
&self.0
}
}
}
impl SerializedXOnlyPublicKey {
/// Returns `XOnlyPublicKey` if the bytes are valid.
pub fn to_validated(self) -> Result<XOnlyPublicKey, secp256k1::Error> {
XOnlyPublicKey::from_byte_array(self.as_byte_array())
}
}
impl AsRef<[u8; 32]> for SerializedXOnlyPublicKey {
fn as_ref(&self) -> &[u8; 32] {
self.as_byte_array()
}
}
impl From<&SerializedXOnlyPublicKey> for SerializedXOnlyPublicKey {
fn from(borrowed: &SerializedXOnlyPublicKey) -> Self {
*borrowed
}
}
impl fmt::Debug for SerializedXOnlyPublicKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(&self.as_byte_array().as_hex(), f)
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -22,7 +22,7 @@ use io::Write;
use secp256k1::{Scalar, Secp256k1}; use secp256k1::{Scalar, Secp256k1};
use crate::consensus::Encodable; use crate::consensus::Encodable;
use crate::crypto::key::{TapTweak, TweakedPublicKey, UntweakedPublicKey, XOnlyPublicKey}; use crate::crypto::key::{SerializedXOnlyPublicKey, TapTweak, TweakedPublicKey, UntweakedPublicKey, XOnlyPublicKey};
use crate::prelude::{BTreeMap, BTreeSet, BinaryHeap, Vec}; use crate::prelude::{BTreeMap, BTreeSet, BinaryHeap, Vec};
use crate::{Script, ScriptBuf}; use crate::{Script, ScriptBuf};
@ -1141,13 +1141,13 @@ impl<'leaf> ScriptLeaf<'leaf> {
/// Control block data structure used in Tapscript satisfaction. /// Control block data structure used in Tapscript satisfaction.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct ControlBlock<Branch = TaprootMerkleBranchBuf> where Branch: ?Sized { pub struct ControlBlock<Branch = TaprootMerkleBranchBuf, Key = UntweakedPublicKey> where Branch: ?Sized {
/// The tapleaf version. /// The tapleaf version.
pub leaf_version: LeafVersion, pub leaf_version: LeafVersion,
/// The parity of the output key (NOT THE INTERNAL KEY WHICH IS ALWAYS XONLY). /// The parity of the output key (NOT THE INTERNAL KEY WHICH IS ALWAYS XONLY).
pub output_key_parity: secp256k1::Parity, pub output_key_parity: secp256k1::Parity,
/// The internal key. /// The internal key.
pub internal_key: UntweakedPublicKey, pub internal_key: Key,
/// The Merkle proof of a script associated with this leaf. /// The Merkle proof of a script associated with this leaf.
pub merkle_branch: Branch, pub merkle_branch: Branch,
} }
@ -1166,6 +1166,24 @@ impl ControlBlock {
/// - [`TaprootError::InvalidInternalKey`] if internal key is invalid (first 32 bytes after the parity byte). /// - [`TaprootError::InvalidInternalKey`] if internal key is invalid (first 32 bytes after the parity byte).
/// - [`TaprootError::InvalidMerkleTreeDepth`] if Merkle tree is too deep (more than 128 levels). /// - [`TaprootError::InvalidMerkleTreeDepth`] if Merkle tree is too deep (more than 128 levels).
pub fn decode(sl: &[u8]) -> Result<ControlBlock, TaprootError> { pub fn decode(sl: &[u8]) -> Result<ControlBlock, TaprootError> {
use alloc::borrow::ToOwned;
let ControlBlock {
leaf_version,
output_key_parity,
internal_key,
merkle_branch,
} = ControlBlock::<&TaprootMerkleBranch, &SerializedXOnlyPublicKey>::decode_borrowed(sl)?;
let internal_key = internal_key.to_validated().map_err(TaprootError::InvalidInternalKey)?;
let merkle_branch = merkle_branch.to_owned();
Ok(ControlBlock { leaf_version, output_key_parity, internal_key, merkle_branch })
}
}
impl<B, K> ControlBlock<B, K> {
pub(crate) fn decode_borrowed<'a>(sl: &'a [u8]) -> Result<Self, TaprootError> where B: From<&'a TaprootMerkleBranch>, K: From<&'a SerializedXOnlyPublicKey> {
let (base, merkle_branch) = sl.split_first_chunk::<TAPROOT_CONTROL_BASE_SIZE>() let (base, merkle_branch) = sl.split_first_chunk::<TAPROOT_CONTROL_BASE_SIZE>()
.ok_or(InvalidControlBlockSizeError(sl.len()))?; .ok_or(InvalidControlBlockSizeError(sl.len()))?;
@ -1177,9 +1195,8 @@ impl ControlBlock {
}; };
let leaf_version = LeafVersion::from_consensus(first & TAPROOT_LEAF_MASK)?; let leaf_version = LeafVersion::from_consensus(first & TAPROOT_LEAF_MASK)?;
let internal_key = UntweakedPublicKey::from_byte_array(internal_key) let internal_key = SerializedXOnlyPublicKey::from_bytes_ref(internal_key).into();
.map_err(TaprootError::InvalidInternalKey)?; let merkle_branch = TaprootMerkleBranch::decode(merkle_branch)?.into();
let merkle_branch = TaprootMerkleBranchBuf::decode(merkle_branch)?;
Ok(ControlBlock { leaf_version, output_key_parity, internal_key, merkle_branch }) Ok(ControlBlock { leaf_version, output_key_parity, internal_key, merkle_branch })
} }
} }