Move hash types and LeafVersion to taproot-primitives
What changed, and why it matters
This commit is a routine code reorganization: it moves Taproot hash types (like TapLeafHash, TapNodeHash, TapTweakHash) and the LeafVersion enum from the main bitcoin crate into a new taproot-primitives crate. The bitcoin crate then re-exports these types so existing users see no change. There is no security fix or behavior change visible in the diff.
No security action required. Treat as normal refactoring; verify downstream crates compile and tests pass after the dependency split.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch relocates Taproot tagged-hash newtypes and LeafVersion/InvalidTaprootLeafVersionError definitions from bitcoin/src/taproot/mod.rs to taproot-primitives/src/lib.rs. It adds bitcoin-taproot-primitives as a dependency of the bitcoin crate and wires feature flags (std, serde, arbitrary). The moved code is largely identical, with minor adjustments such as gating TapTweakHash::from_key_and_merkle_root behind the alloc feature and using internals::impl_to_hex_from_lower_hex! instead of a direct import. No cryptographic logic, validation rules, or public APIs are changed.
Changed components
bitcoin/src/taproot/mod.rstaproot-primitives/src/lib.rsbitcoin/Cargo.tomltaproot-primitives/Cargo.tomlInspect captured patch +385 / −314
diff --git a/Cargo-minimal.lock b/Cargo-minimal.lock
index a7c421c1..4c642edd 100644
--- a/Cargo-minimal.lock
+++ b/Cargo-minimal.lock
@@ -60,6 +60,7 @@ dependencies = [
"bitcoin-io",
"bitcoin-network-kind",
"bitcoin-primitives",
+ "bitcoin-taproot-primitives",
"bitcoin-units",
"bitcoin_hashes",
"bitcoinconsensus",
@@ -189,6 +190,14 @@ dependencies = [
[[package]]
name = "bitcoin-taproot-primitives"
version = "0.1.0"
+dependencies = [
+ "arbitrary",
+ "bitcoin-crypto",
+ "bitcoin-internals",
+ "bitcoin_hashes",
+ "secp256k1",
+ "serde",
+]
[[package]]
name = "bitcoin-units"
diff --git a/Cargo-recent.lock b/Cargo-recent.lock
index c4cdf369..71dd952d 100644
--- a/Cargo-recent.lock
+++ b/Cargo-recent.lock
@@ -59,6 +59,7 @@ dependencies = [
"bitcoin-io",
"bitcoin-network-kind",
"bitcoin-primitives",
+ "bitcoin-taproot-primitives",
"bitcoin-units",
"bitcoin_hashes",
"bitcoinconsensus",
@@ -188,6 +189,14 @@ dependencies = [
[[package]]
name = "bitcoin-taproot-primitives"
version = "0.1.0"
+dependencies = [
+ "arbitrary",
+ "bitcoin-crypto",
+ "bitcoin-internals",
+ "bitcoin_hashes",
+ "secp256k1",
+ "serde",
+]
[[package]]
name = "bitcoin-units"
diff --git a/bitcoin/Cargo.toml b/bitcoin/Cargo.toml
index e8ddeb5d..49b93478 100644
--- a/bitcoin/Cargo.toml
+++ b/bitcoin/Cargo.toml
@@ -17,13 +17,13 @@ exclude = ["tests", "contrib"]
# If you change features or optional dependencies in any way please update the "# Cargo features" section in lib.rs as well.
[features]
default = [ "std", "secp-recovery" ]
-std = ["base58/std", "bech32/std", "crypto/std", "encoding/std", "hashes/std", "hex-stable/std", "hex-unstable/std", "internals/std", "io/std", "network/std", "primitives/std", "secp256k1/std", "units/std", "base64?/std", "bitcoinconsensus?/std"]
+std = ["base58/std", "bech32/std", "crypto/std", "encoding/std", "hashes/std", "hex-stable/std", "hex-unstable/std", "internals/std", "io/std", "network/std", "primitives/std", "secp256k1/std", "taproot-primitives/std", "units/std", "base64?/std", "bitcoinconsensus?/std"]
rand = ["secp256k1/rand", "crypto/rand"]
-serde = ["base64", "crypto/serde", "dep:serde", "hashes/serde", "internals/serde", "network/serde", "primitives/serde", "secp256k1/serde", "units/serde"]
+serde = ["base64", "crypto/serde", "dep:serde", "hashes/serde", "internals/serde", "network/serde", "primitives/serde", "secp256k1/serde", "taproot-primitives/serde", "units/serde"]
secp-global-context = ["secp256k1/global-context"]
secp-lowmemory = ["secp256k1/lowmemory"]
secp-recovery = ["secp256k1/recovery"]
-arbitrary = ["crypto/arbitrary", "dep:arbitrary", "units/arbitrary", "primitives/arbitrary", "hashes/arbitrary", "secp256k1/arbitrary", "network/arbitrary"]
+arbitrary = ["crypto/arbitrary", "dep:arbitrary", "units/arbitrary", "primitives/arbitrary", "hashes/arbitrary", "secp256k1/arbitrary", "taproot-primitives/arbitrary", "network/arbitrary"]
[dependencies]
base58 = { package = "base58ck", path = "../base58", version = "0.4.0", default-features = false, features = ["alloc"] }
@@ -38,6 +38,7 @@ io = { package = "bitcoin-io", path = "../io", version = "0.5.0", default-featur
network = { package = "bitcoin-network-kind", path = "../network", version = "0.1.0", default-features = false, features = ["alloc"]}
primitives = { package = "bitcoin-primitives", path = "../primitives", version = "0.102.0", default-features = false, features = ["alloc", "hex"] }
secp256k1 = { version = "0.32.0-beta.2", default-features = false, features = ["alloc"] }
+taproot-primitives = { package = "bitcoin-taproot-primitives", path = "../taproot-primitives", version = "0.1.0", default-features = false, features = ["alloc"] }
units = { package = "bitcoin-units", path = "../units", version = "0.3.0", default-features = false, features = ["alloc"] }
arbitrary = { version = "1.4.1", optional = true }
diff --git a/bitcoin/src/taproot/mod.rs b/bitcoin/src/taproot/mod.rs
index dd300d69..ac46e36e 100644
--- a/bitcoin/src/taproot/mod.rs
+++ b/bitcoin/src/taproot/mod.rs
@@ -7,18 +7,17 @@
pub mod merkle_branch;
use core::cmp::{Ordering, Reverse};
+#[cfg(feature = "serde")]
use core::fmt;
use core::iter::FusedIterator;
#[cfg(feature = "arbitrary")]
use arbitrary::{Arbitrary, Unstructured};
-use hashes::{hash_newtype, sha256t, sha256t_tag, HashEngine};
+use hashes::{sha256t, HashEngine};
use internals::array::ArrayExt;
-use internals::impl_to_hex_from_lower_hex;
#[allow(unused)] // MSRV polyfill
use internals::slice::SliceExt;
use io::Write;
-use secp256k1::Scalar;
use crate::consensus::Encodable;
use crate::crypto::key::{
@@ -35,6 +34,13 @@ pub use crate::crypto::taproot::{SerializedSignature, Signature};
pub use merkle_branch::TaprootMerkleBranch;
#[doc(inline)]
pub use merkle_branch::TaprootMerkleBranchBuf;
+#[doc(inline)]
+pub use taproot_primitives::{
+ FutureLeafVersion, LeafVersion, TapBranchTag, TapLeafHash, TapLeafTag, TapNodeHash,
+ TapTweakHash, TapTweakTag, TAPROOT_ANNEX_PREFIX, TAPROOT_CONTROL_BASE_SIZE,
+ TAPROOT_CONTROL_MAX_NODE_COUNT, TAPROOT_CONTROL_MAX_SIZE, TAPROOT_CONTROL_NODE_SIZE,
+ TAPROOT_LEAF_MASK, TAPROOT_LEAF_TAPSCRIPT,
+};
#[doc(no_inline)]
pub use self::error::{
@@ -49,84 +55,6 @@ pub use crate::XOnlyPublicKey;
type ControlBlockArrayVec = internals::array_vec::ArrayVec<u8, TAPROOT_CONTROL_MAX_SIZE>;
-// Taproot test vectors from BIP-0341 state the hashes without any reversing
-sha256t_tag! {
- pub struct TapLeafTag = hash_str("TapLeaf");
-}
-
-hash_newtype! {
- /// Taproot-tagged hash with tag \"TapLeaf\".
- ///
- /// This is used for computing tapscript script spend hash.
- pub struct TapLeafHash(sha256t::Hash<TapLeafTag>);
-}
-
-hashes::impl_hex_for_newtype!(TapLeafHash);
-#[cfg(feature = "serde")]
-hashes::impl_serde_for_newtype!(TapLeafHash);
-
-sha256t_tag! {
- pub struct TapBranchTag = hash_str("TapBranch");
-}
-
-hash_newtype! {
- /// Tagged hash used in Taproot trees.
- ///
- /// See BIP-0340 for tagging rules.
- #[repr(transparent)]
- pub struct TapNodeHash(sha256t::Hash<TapBranchTag>);
-}
-
-hashes::impl_hex_for_newtype!(TapNodeHash);
-#[cfg(feature = "serde")]
-hashes::impl_serde_for_newtype!(TapNodeHash);
-
-sha256t_tag! {
- pub struct TapTweakTag = hash_str("TapTweak");
-}
-
-hash_newtype! {
- /// Taproot-tagged hash with tag \"TapTweak\".
- ///
- /// This hash type is used while computing the tweaked public key.
- pub struct TapTweakHash(sha256t::Hash<TapTweakTag>);
-}
-
-hashes::impl_hex_for_newtype!(TapTweakHash);
-#[cfg(feature = "serde")]
-hashes::impl_serde_for_newtype!(TapTweakHash);
-
-impl From<TapLeafHash> for TapNodeHash {
- fn from(leaf: TapLeafHash) -> Self { Self::from_byte_array(leaf.to_byte_array()) }
-}
-
-impl TapTweakHash {
- /// Constructs a new BIP-0341 [`TapTweakHash`] from key and Merkle root. Produces `H_taptweak(P||R)` where
- /// `P` is the internal key and `R` is the Merkle root.
- pub fn from_key_and_merkle_root<K: Into<UntweakedPublicKey>>(
- internal_key: K,
- merkle_root: Option<TapNodeHash>,
- ) -> Self {
- let internal_key = internal_key.into();
- let mut eng = sha256t::Hash::<TapTweakTag>::engine();
- // always hash the key
- eng.input(&internal_key.serialize().0);
- if let Some(h) = merkle_root {
- eng.input(h.as_ref());
- } else {
- // nothing to hash
- }
- let inner = sha256t::Hash::<TapTweakTag>::from_engine(eng);
- Self::from_byte_array(inner.to_byte_array())
- }
-
- /// Converts a `TapTweakHash` into a `Scalar` ready for use with key tweaking API.
- pub fn to_scalar(self) -> Scalar {
- // This is statistically extremely unlikely to panic.
- Scalar::from_be_bytes(self.to_byte_array()).expect("hash value greater than curve order")
- }
-}
-
impl From<LeafNode> for TapNodeHash {
fn from(leaf: LeafNode) -> Self { leaf.node_hash() }
}
@@ -192,28 +120,6 @@ fn combine_node_hashes(a: TapNodeHash, b: TapNodeHash) -> (TapNodeHash, bool) {
(TapNodeHash::from_byte_array(inner.to_byte_array()), a < b)
}
-/// Maximum depth of a Taproot tree script spend path.
-// https://github.com/bitcoin/bitcoin/blob/e826b22da252e0599c61d21c98ff89f366b3120f/src/script/interpreter.h#L229
-pub const TAPROOT_CONTROL_MAX_NODE_COUNT: usize = 128;
-/// Size of a Taproot control node.
-// https://github.com/bitcoin/bitcoin/blob/e826b22da252e0599c61d21c98ff89f366b3120f/src/script/interpreter.h#L228
-pub const TAPROOT_CONTROL_NODE_SIZE: usize = 32;
-/// Tapleaf mask for getting the leaf version from first byte of control block.
-// https://github.com/bitcoin/bitcoin/blob/e826b22da252e0599c61d21c98ff89f366b3120f/src/script/interpreter.h#L225
-pub const TAPROOT_LEAF_MASK: u8 = 0xfe;
-/// Tapscript leaf version.
-// https://github.com/bitcoin/bitcoin/blob/e826b22da252e0599c61d21c98ff89f366b3120f/src/script/interpreter.h#L226
-pub const TAPROOT_LEAF_TAPSCRIPT: u8 = 0xc0;
-/// Taproot annex prefix.
-pub const TAPROOT_ANNEX_PREFIX: u8 = 0x50;
-/// Tapscript control base size.
-// https://github.com/bitcoin/bitcoin/blob/e826b22da252e0599c61d21c98ff89f366b3120f/src/script/interpreter.h#L227
-pub const TAPROOT_CONTROL_BASE_SIZE: usize = 33;
-/// Tapscript control max size.
-// https://github.com/bitcoin/bitcoin/blob/e826b22da252e0599c61d21c98ff89f366b3120f/src/script/interpreter.h#L230
-pub const TAPROOT_CONTROL_MAX_SIZE: usize =
- TAPROOT_CONTROL_BASE_SIZE + TAPROOT_CONTROL_NODE_SIZE * TAPROOT_CONTROL_MAX_NODE_COUNT;
-
/// The leaf script with its version.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct LeafScript<S> {
@@ -1223,154 +1129,6 @@ impl<Branch: AsRef<TaprootMerkleBranch> + ?Sized> ControlBlock<Branch> {
}
}
-/// Inner type representing future (non-tapscript) leaf versions. See [`LeafVersion::Future`].
-///
-/// NB: NO PUBLIC CONSTRUCTOR!
-/// The only way to construct this is by converting `u8` to [`LeafVersion`] and then extracting it.
-#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
-pub struct FutureLeafVersion(u8);
-
-impl FutureLeafVersion {
- #[track_caller]
- pub(self) fn from_consensus(version: u8) -> Result<Self, InvalidTaprootLeafVersionError> {
- match version {
- TAPROOT_LEAF_TAPSCRIPT => unreachable!(
- "FutureLeafVersion::from_consensus should never be called for 0xC0 value"
- ),
- TAPROOT_ANNEX_PREFIX => Err(InvalidTaprootLeafVersionError(TAPROOT_ANNEX_PREFIX)),
- odd if odd & 0xFE != odd => Err(InvalidTaprootLeafVersionError(odd)),
- even => Ok(Self(even)),
- }
- }
-
- /// Returns the consensus representation of this [`FutureLeafVersion`].
- #[inline]
- pub fn to_consensus(self) -> u8 { self.0 }
-}
-
-impl fmt::Display for FutureLeafVersion {
- #[inline]
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Display::fmt(&self.0, f) }
-}
-
-impl fmt::LowerHex for FutureLeafVersion {
- #[inline]
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::LowerHex::fmt(&self.0, f) }
-}
-impl_to_hex_from_lower_hex!(FutureLeafVersion, |_| 2);
-
-impl fmt::UpperHex for FutureLeafVersion {
- #[inline]
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::UpperHex::fmt(&self.0, f) }
-}
-
-/// The leaf version for tapleafs.
-#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
-pub enum LeafVersion {
- /// BIP-0342 tapscript.
- TapScript,
-
- /// Future leaf version.
- Future(FutureLeafVersion),
-}
-
-impl LeafVersion {
- /// Constructs a new [`LeafVersion`] from consensus byte representation.
- ///
- /// # Errors
- ///
- /// - If the last bit of the `version` is odd.
- /// - If the `version` is 0x50 ([`TAPROOT_ANNEX_PREFIX`]).
- pub fn from_consensus(version: u8) -> Result<Self, InvalidTaprootLeafVersionError> {
- match version {
- TAPROOT_LEAF_TAPSCRIPT => Ok(Self::TapScript),
- TAPROOT_ANNEX_PREFIX => Err(InvalidTaprootLeafVersionError(TAPROOT_ANNEX_PREFIX)),
- future => FutureLeafVersion::from_consensus(future).map(LeafVersion::Future),
- }
- }
-
- /// Returns the consensus representation of this [`LeafVersion`].
- pub fn to_consensus(self) -> u8 {
- match self {
- Self::TapScript => TAPROOT_LEAF_TAPSCRIPT,
- Self::Future(version) => version.to_consensus(),
- }
- }
-}
-
-impl fmt::Display for LeafVersion {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match (self, f.alternate()) {
- (Self::TapScript, true) => f.write_str("tapscript"),
- (Self::TapScript, false) => fmt::Display::fmt(&TAPROOT_LEAF_TAPSCRIPT, f),
- (Self::Future(version), true) => write!(f, "future_script_{:#02x}", version.0),
- (Self::Future(version), false) => fmt::Display::fmt(version, f),
- }
- }
-}
-
-impl fmt::LowerHex for LeafVersion {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- fmt::LowerHex::fmt(&self.to_consensus(), f)
- }
-}
-impl_to_hex_from_lower_hex!(LeafVersion, |_| 2);
-
-impl fmt::UpperHex for LeafVersion {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- fmt::UpperHex::fmt(&self.to_consensus(), f)
- }
-}
-
-/// Serializes [`LeafVersion`] as a `u8` using consensus encoding.
-#[cfg(feature = "serde")]
-impl serde::Serialize for LeafVersion {
- fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
- where
- S: serde::Serializer,
- {
- serializer.serialize_u8(self.to_consensus())
- }
-}
-
-/// Deserializes [`LeafVersion`] as a `u8` using consensus encoding.
-#[cfg(feature = "serde")]
-impl<'de> serde::Deserialize<'de> for LeafVersion {
- fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
- where
- D: serde::Deserializer<'de>,
- {
- struct U8Visitor;
- impl serde::de::Visitor<'_> for U8Visitor {
- type Value = LeafVersion;
-
- fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
- formatter.write_str("a valid consensus-encoded Taproot leaf version")
- }
-
- fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
- where
- E: serde::de::Error,
- {
- let value = u8::try_from(value).map_err(|_| {
- E::invalid_value(
- serde::de::Unexpected::Unsigned(value),
- &"consensus-encoded leaf version as u8",
- )
- })?;
- LeafVersion::from_consensus(value).map_err(|_| {
- E::invalid_value(
- ::serde::de::Unexpected::Unsigned(value as u64),
- &"consensus-encoded leaf version as u8",
- )
- })
- }
- }
-
- deserializer.deserialize_u8(U8Visitor)
- }
-}
-
/// Error types for taproot.
pub mod error {
use core::convert::Infallible;
@@ -1387,6 +1145,8 @@ pub mod error {
#[rustfmt::skip]
#[doc(inline)]
pub use crate::crypto::taproot::SigFromSliceError;
+ #[doc(no_inline)]
+ pub use taproot_primitives::InvalidTaprootLeafVersionError;
/// Error happening when [`TapTree`] is constructed from a [`TaprootBuilder`]
/// having hidden branches or not being finalized.
@@ -1654,30 +1414,6 @@ pub mod error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None }
}
- /// The last bit of tapleaf version must be zero.
- #[derive(Debug, Clone, PartialEq, Eq)]
- pub struct InvalidTaprootLeafVersionError(pub(super) u8);
-
- impl InvalidTaprootLeafVersionError {
- /// Accessor for the invalid leaf version.
- pub fn invalid_leaf_version(&self) -> u8 { self.0 }
- }
-
- impl From<Infallible> for InvalidTaprootLeafVersionError {
- fn from(never: Infallible) -> Self { match never {} }
- }
-
- impl fmt::Display for InvalidTaprootLeafVersionError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "leaf version({}) must have the least significant bit 0", self.0)
- }
- }
-
- #[cfg(feature = "std")]
- impl std::error::Error for InvalidTaprootLeafVersionError {
- fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None }
- }
-
/// Invalid control block size.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InvalidControlBlockSizeError(pub(super) usize);
@@ -1707,40 +1443,6 @@ pub mod error {
}
}
-#[cfg(feature = "arbitrary")]
-impl<'a> Arbitrary<'a> for TapLeafHash {
- fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
- Ok(Self::from_byte_array(u.arbitrary()?))
- }
-}
-
-#[cfg(feature = "arbitrary")]
-impl<'a> Arbitrary<'a> for TapNodeHash {
- fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
- Ok(Self::from_byte_array(u.arbitrary()?))
- }
-}
-
-#[cfg(feature = "arbitrary")]
-impl<'a> Arbitrary<'a> for FutureLeafVersion {
- fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
- match u8::arbitrary(u)? {
- TAPROOT_LEAF_TAPSCRIPT => Err(arbitrary::Error::IncorrectFormat),
- version => Self::from_consensus(version).map_err(|_| arbitrary::Error::IncorrectFormat),
- }
- }
-}
-
-#[cfg(feature = "arbitrary")]
-impl<'a> Arbitrary<'a> for LeafVersion {
- fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
- match bool::arbitrary(u)? {
- true => Ok(Self::TapScript),
- false => Ok(Self::Future(u.arbitrary()?)),
- }
- }
-}
-
#[cfg(feature = "arbitrary")]
impl<'a> Arbitrary<'a> for LeafNode {
fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
diff --git a/taproot-primitives/CHANGELOG.md b/taproot-primitives/CHANGELOG.md
new file mode 100644
index 00000000..cd72c88c
--- /dev/null
+++ b/taproot-primitives/CHANGELOG.md
@@ -0,0 +1,3 @@
+# 0.1.0 - Initial release
+
+- All the hash types and also the `LeafVersion` enum.
\ No newline at end of file
diff --git a/taproot-primitives/Cargo.toml b/taproot-primitives/Cargo.toml
index 8583f71d..939e5d03 100644
--- a/taproot-primitives/Cargo.toml
+++ b/taproot-primitives/Cargo.toml
@@ -14,10 +14,19 @@ exclude = ["tests", "contrib"]
[features]
default = ["std"]
-std = ["alloc"]
-alloc = []
+std = ["alloc", "crypto/std", "hashes/std", "internals/std", "serde?/std", "secp256k1/std"]
+alloc = ["crypto/alloc", "hashes/alloc", "internals/alloc", "secp256k1/alloc"]
+serde = ["dep:serde", "crypto/serde", "hashes/serde", "internals/serde", "secp256k1/serde"]
+arbitrary = ["crypto/arbitrary", "dep:arbitrary", "hashes/arbitrary", "secp256k1/arbitrary"]
[dependencies]
+crypto = { package = "bitcoin-crypto", path = "../crypto", default-features = false }
+hashes = { package = "bitcoin_hashes", path = "../hashes", version = "0.20.0", default-features = false, features = ["hex"] }
+internals = { package = "bitcoin-internals", path = "../internals", version = "0.5.0" }
+
+arbitrary = { version = "1.4.1", optional = true }
+secp256k1 = { version = "0.32.0-beta.2", default-features = false }
+serde = { version = "1.0.195", default-features = false, features = [ "derive" ], optional = true }
[dev-dependencies]
@@ -27,3 +36,8 @@ rustdoc-args = ["--cfg", "docsrs"]
[lints]
workspace = true
+
+[package.metadata.rbmt.lint]
+allowed_duplicates = [
+ "hex-conservative",
+]
diff --git a/taproot-primitives/src/lib.rs b/taproot-primitives/src/lib.rs
index 6c8fff71..179a3c3f 100644
--- a/taproot-primitives/src/lib.rs
+++ b/taproot-primitives/src/lib.rs
@@ -12,3 +12,336 @@
#![allow(clippy::needless_question_mark)] // https://github.com/rust-bitcoin/rust-bitcoin/pull/2134
#![allow(clippy::manual_range_contains)] // More readable than clippy's format.
#![allow(clippy::uninlined_format_args)] // Allow `format!("{}", x)` instead of enforcing `format!("{x}")`
+
+extern crate alloc;
+
+#[cfg(feature = "std")]
+extern crate std;
+
+#[rustfmt::skip] // Keep pub re-exports separate
+#[doc(no_inline)]
+pub use self::error::InvalidTaprootLeafVersionError;
+
+use core::fmt;
+
+#[cfg(feature = "arbitrary")]
+use arbitrary::{Arbitrary, Unstructured};
+#[cfg(feature = "alloc")]
+use crypto::key::UntweakedPublicKey;
+use hashes::{hash_newtype, sha256t, sha256t_tag};
+#[cfg(feature = "alloc")]
+use hashes::HashEngine;
+use secp256k1::Scalar;
+
+/// Maximum depth of a Taproot tree script spend path.
+// https://github.com/bitcoin/bitcoin/blob/e826b22da252e0599c61d21c98ff89f366b3120f/src/script/interpreter.h#L229
+pub const TAPROOT_CONTROL_MAX_NODE_COUNT: usize = 128;
+/// Size of a Taproot control node.
+// https://github.com/bitcoin/bitcoin/blob/e826b22da252e0599c61d21c98ff89f366b3120f/src/script/interpreter.h#L228
+pub const TAPROOT_CONTROL_NODE_SIZE: usize = 32;
+/// Tapleaf mask for getting the leaf version from first byte of control block.
+// https://github.com/bitcoin/bitcoin/blob/e826b22da252e0599c61d21c98ff89f366b3120f/src/script/interpreter.h#L225
+pub const TAPROOT_LEAF_MASK: u8 = 0xfe;
+/// Tapscript leaf version.
+// https://github.com/bitcoin/bitcoin/blob/e826b22da252e0599c61d21c98ff89f366b3120f/src/script/interpreter.h#L226
+pub const TAPROOT_LEAF_TAPSCRIPT: u8 = 0xc0;
+/// Taproot annex prefix.
+pub const TAPROOT_ANNEX_PREFIX: u8 = 0x50;
+/// Tapscript control base size.
+// https://github.com/bitcoin/bitcoin/blob/e826b22da252e0599c61d21c98ff89f366b3120f/src/script/interpreter.h#L227
+pub const TAPROOT_CONTROL_BASE_SIZE: usize = 33;
+/// Tapscript control max size.
+// https://github.com/bitcoin/bitcoin/blob/e826b22da252e0599c61d21c98ff89f366b3120f/src/script/interpreter.h#L230
+pub const TAPROOT_CONTROL_MAX_SIZE: usize =
+ TAPROOT_CONTROL_BASE_SIZE + TAPROOT_CONTROL_NODE_SIZE * TAPROOT_CONTROL_MAX_NODE_COUNT;
+
+// Taproot test vectors from BIP-0341 state the hashes without any reversing
+sha256t_tag! {
+ pub struct TapLeafTag = hash_str("TapLeaf");
+}
+
+hash_newtype! {
+ /// Taproot-tagged hash with tag \"TapLeaf\".
+ ///
+ /// This is used for computing tapscript script spend hash.
+ pub struct TapLeafHash(sha256t::Hash<TapLeafTag>);
+}
+
+hashes::impl_hex_for_newtype!(TapLeafHash);
+#[cfg(feature = "serde")]
+hashes::impl_serde_for_newtype!(TapLeafHash);
+
+sha256t_tag! {
+ pub struct TapBranchTag = hash_str("TapBranch");
+}
+
+hash_newtype! {
+ /// Tagged hash used in Taproot trees.
+ ///
+ /// See BIP-0340 for tagging rules.
+ #[repr(transparent)]
+ pub struct TapNodeHash(sha256t::Hash<TapBranchTag>);
+}
+
+hashes::impl_hex_for_newtype!(TapNodeHash);
+#[cfg(feature = "serde")]
+hashes::impl_serde_for_newtype!(TapNodeHash);
+
+sha256t_tag! {
+ pub struct TapTweakTag = hash_str("TapTweak");
+}
+
+hash_newtype! {
+ /// Taproot-tagged hash with tag \"TapTweak\".
+ ///
+ /// This hash type is used while computing the tweaked public key.
+ pub struct TapTweakHash(sha256t::Hash<TapTweakTag>);
+}
+
+hashes::impl_hex_for_newtype!(TapTweakHash);
+#[cfg(feature = "serde")]
+hashes::impl_serde_for_newtype!(TapTweakHash);
+
+impl From<TapLeafHash> for TapNodeHash {
+ fn from(leaf: TapLeafHash) -> Self { Self::from_byte_array(leaf.to_byte_array()) }
+}
+
+impl TapTweakHash {
+ /// Constructs a new BIP-0341 [`TapTweakHash`] from key and Merkle root. Produces `H_taptweak(P||R)` where
+ /// `P` is the internal key and `R` is the Merkle root.
+ #[cfg(feature = "alloc")]
+ pub fn from_key_and_merkle_root<K: Into<UntweakedPublicKey>>(
+ internal_key: K,
+ merkle_root: Option<TapNodeHash>,
+ ) -> Self {
+ let internal_key = internal_key.into();
+ let mut eng = sha256t::Hash::<TapTweakTag>::engine();
+ // always hash the key
+ eng.input(&internal_key.serialize().0);
+ if let Some(h) = merkle_root {
+ eng.input(h.as_ref());
+ } else {
+ // nothing to hash
+ }
+ let inner = sha256t::Hash::<TapTweakTag>::from_engine(eng);
+ Self::from_byte_array(inner.to_byte_array())
+ }
+
+ /// Converts a `TapTweakHash` into a `Scalar` ready for use with key tweaking API.
+ pub fn to_scalar(self) -> Scalar {
+ // This is statistically extremely unlikely to panic.
+ Scalar::from_be_bytes(self.to_byte_array()).expect("hash value greater than curve order")
+ }
+}
+
+/// The leaf version for tapleafs.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub enum LeafVersion {
+ /// BIP-0342 tapscript.
+ TapScript,
+
+ /// Future leaf version.
+ Future(FutureLeafVersion),
+}
+
+impl LeafVersion {
+ /// Constructs a new [`LeafVersion`] from consensus byte representation.
+ ///
+ /// # Errors
+ ///
+ /// - If the last bit of the `version` is odd.
+ /// - If the `version` is 0x50 ([`TAPROOT_ANNEX_PREFIX`]).
+ pub fn from_consensus(version: u8) -> Result<Self, InvalidTaprootLeafVersionError> {
+ match version {
+ TAPROOT_LEAF_TAPSCRIPT => Ok(Self::TapScript),
+ TAPROOT_ANNEX_PREFIX => Err(InvalidTaprootLeafVersionError(TAPROOT_ANNEX_PREFIX)),
+ future => FutureLeafVersion::from_consensus(future).map(LeafVersion::Future),
+ }
+ }
+
+ /// Returns the consensus representation of this [`LeafVersion`].
+ pub fn to_consensus(self) -> u8 {
+ match self {
+ Self::TapScript => TAPROOT_LEAF_TAPSCRIPT,
+ Self::Future(version) => version.to_consensus(),
+ }
+ }
+}
+
+impl fmt::Display for LeafVersion {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match (self, f.alternate()) {
+ (Self::TapScript, true) => f.write_str("tapscript"),
+ (Self::TapScript, false) => fmt::Display::fmt(&TAPROOT_LEAF_TAPSCRIPT, f),
+ (Self::Future(version), true) => write!(f, "future_script_{:#02x}", version.0),
+ (Self::Future(version), false) => fmt::Display::fmt(version, f),
+ }
+ }
+}
+
+impl fmt::LowerHex for LeafVersion {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ fmt::LowerHex::fmt(&self.to_consensus(), f)
+ }
+}
+internals::impl_to_hex_from_lower_hex!(LeafVersion, |_| 2);
+
+impl fmt::UpperHex for LeafVersion {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ fmt::UpperHex::fmt(&self.to_consensus(), f)
+ }
+}
+
+/// Serializes [`LeafVersion`] as a `u8` using consensus encoding.
+#[cfg(feature = "serde")]
+impl serde::Serialize for LeafVersion {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ serializer.serialize_u8(self.to_consensus())
+ }
+}
+
+/// Deserializes [`LeafVersion`] as a `u8` using consensus encoding.
+#[cfg(feature = "serde")]
+impl<'de> serde::Deserialize<'de> for LeafVersion {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ struct U8Visitor;
+ impl serde::de::Visitor<'_> for U8Visitor {
+ type Value = LeafVersion;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("a valid consensus-encoded Taproot leaf version")
+ }
+
+ fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
+ where
+ E: serde::de::Error,
+ {
+ let value = u8::try_from(value).map_err(|_| {
+ E::invalid_value(
+ serde::de::Unexpected::Unsigned(value),
+ &"consensus-encoded leaf version as u8",
+ )
+ })?;
+ LeafVersion::from_consensus(value).map_err(|_| {
+ E::invalid_value(
+ ::serde::de::Unexpected::Unsigned(value as u64),
+ &"consensus-encoded leaf version as u8",
+ )
+ })
+ }
+ }
+
+ deserializer.deserialize_u8(U8Visitor)
+ }
+}
+
+/// Inner type representing future (non-tapscript) leaf versions. See [`LeafVersion::Future`].
+///
+/// NB: NO PUBLIC CONSTRUCTOR!
+/// The only way to construct this is by converting `u8` to [`LeafVersion`] and then extracting it.
+#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
+pub struct FutureLeafVersion(u8);
+
+impl FutureLeafVersion {
+ #[track_caller]
+ pub(self) fn from_consensus(version: u8) -> Result<Self, InvalidTaprootLeafVersionError> {
+ match version {
+ TAPROOT_LEAF_TAPSCRIPT => unreachable!(
+ "FutureLeafVersion::from_consensus should never be called for 0xC0 value"
+ ),
+ TAPROOT_ANNEX_PREFIX => Err(InvalidTaprootLeafVersionError(TAPROOT_ANNEX_PREFIX)),
+ odd if odd & 0xFE != odd => Err(InvalidTaprootLeafVersionError(odd)),
+ even => Ok(Self(even)),
+ }
+ }
+
+ /// Returns the consensus representation of this [`FutureLeafVersion`].
+ #[inline]
+ pub fn to_consensus(self) -> u8 { self.0 }
+}
+
+impl fmt::Display for FutureLeafVersion {
+ #[inline]
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Display::fmt(&self.0, f) }
+}
+
+impl fmt::LowerHex for FutureLeafVersion {
+ #[inline]
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::LowerHex::fmt(&self.0, f) }
+}
+internals::impl_to_hex_from_lower_hex!(FutureLeafVersion, |_| 2);
+
+impl fmt::UpperHex for FutureLeafVersion {
+ #[inline]
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::UpperHex::fmt(&self.0, f) }
+}
+
+/// Error types for taproot primitives
+pub mod error {
+ use core::convert::Infallible;
+ use core::fmt;
+
+ /// The last bit of tapleaf version must be zero.
+ #[derive(Debug, Clone, PartialEq, Eq)]
+ pub struct InvalidTaprootLeafVersionError(pub(super) u8);
+
+ impl InvalidTaprootLeafVersionError {
+ /// Accessor for the invalid leaf version.
+ pub fn invalid_leaf_version(&self) -> u8 { self.0 }
+ }
+
+ impl From<Infallible> for InvalidTaprootLeafVersionError {
+ fn from(never: Infallible) -> Self { match never {} }
+ }
+
+ impl fmt::Display for InvalidTaprootLeafVersionError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "leaf version({}) must have the least significant bit 0", self.0)
+ }
+ }
+
+ #[cfg(feature = "std")]
+ impl std::error::Error for InvalidTaprootLeafVersionError {
+ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None }
+ }
+}
+
+#[cfg(feature = "arbitrary")]
+impl<'a> Arbitrary<'a> for TapLeafHash {
+ fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
+ Ok(Self::from_byte_array(u.arbitrary()?))
+ }
+}
+
+#[cfg(feature = "arbitrary")]
+impl<'a> Arbitrary<'a> for TapNodeHash {
+ fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
+ Ok(Self::from_byte_array(u.arbitrary()?))
+ }
+}
+
+#[cfg(feature = "arbitrary")]
+impl<'a> Arbitrary<'a> for FutureLeafVersion {
+ fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
+ match u8::arbitrary(u)? {
+ TAPROOT_LEAF_TAPSCRIPT => Err(arbitrary::Error::IncorrectFormat),
+ version => Self::from_consensus(version).map_err(|_| arbitrary::Error::IncorrectFormat),
+ }
+ }
+}
+
+#[cfg(feature = "arbitrary")]
+impl<'a> Arbitrary<'a> for LeafVersion {
+ fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
+ match bool::arbitrary(u)? {
+ true => Ok(Self::TapScript),
+ false => Ok(Self::Future(u.arbitrary()?)),
+ }
+ }
+}
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.