Skip to content

Commit 8efc9a1

Browse files
committed
Merge rust-bitcoin/rust-bitcoin#929: Fix TapTree hidden branches bug
c036b0d Unit test for failing TapTree on builder containing hidden nodes. (Dr Maxim Orlovsky) 7771531 Prevent TapTree from hidden parts (Dr Maxim Orlovsky) b0f3992 Rename TaprootBuilder::is_complete into is_finalized (Dr Maxim Orlovsky) efa800f Make TapTree::from_inner return a proper error type (Dr Maxim Orlovsky) e24c6e2 TapTree serialization roundtrip unit test (Dr Maxim Orlovsky) 56adfa4 TaprootBuilder::has_hidden_nodes method (Dr Maxim Orlovsky) e69701e Rename taproot `*_hidden` API into `*_hidden_nodes` (Dr Maxim Orlovsky) 6add0dd Track information about hidden leaves in taproot NodeInfo (Dr Maxim Orlovsky) Pull request description: Closes #928 ACKs for top commit: sanket1729: ACK c036b0d. Reviewed the range diff apoelstra: ACK c036b0d Tree-SHA512: 3a8193e6d6dd985da30a2094d1111471b5971f422525870003b77b6ac47cd4ad6e718d46a6d86bbb5e92e5253ac53804badf67edd98bbccbdc11e6383c675663
2 parents f409fe4 + b77d27f commit 8efc9a1

File tree

5 files changed

+100
-14
lines changed

5 files changed

+100
-14
lines changed

src/util/psbt/map/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ mod input;
2424
mod output;
2525

2626
pub use self::input::{Input, PsbtSighashType};
27-
pub use self::output::{Output, TapTree};
27+
pub use self::output::{Output, TapTree, IncompleteTapTree};
2828

2929
/// A trait that describes a PSBT key-value map.
3030
pub(super) trait Map {

src/util/psbt/map/output.rs

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,39 @@ pub struct Output {
7979
pub unknown: BTreeMap<raw::Key, Vec<u8>>,
8080
}
8181

82+
/// Error happening when [`TapTree`] is constructed from a [`TaprootBuilder`]
83+
/// having hidden branches or not being finalized.
84+
#[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug)]
85+
pub enum IncompleteTapTree {
86+
/// Indicates an attempt to construct a tap tree from a builder containing incomplete branches.
87+
NotFinalized(TaprootBuilder),
88+
/// Indicates an attempt to construct a tap tree from a builder containing hidden parts.
89+
HiddenParts(TaprootBuilder),
90+
}
91+
92+
impl IncompleteTapTree {
93+
/// Converts error into the original incomplete [`TaprootBuilder`] instance.
94+
pub fn into_builder(self) -> TaprootBuilder {
95+
match self {
96+
IncompleteTapTree::NotFinalized(builder) |
97+
IncompleteTapTree::HiddenParts(builder) => builder
98+
}
99+
}
100+
}
101+
102+
impl core::fmt::Display for IncompleteTapTree {
103+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
104+
f.write_str(match self {
105+
IncompleteTapTree::NotFinalized(_) => "an attempt to construct a tap tree from a builder containing incomplete branches.",
106+
IncompleteTapTree::HiddenParts(_) => "an attempt to construct a tap tree from a builder containing hidden parts.",
107+
})
108+
}
109+
}
110+
111+
#[cfg(feature = "std")]
112+
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
113+
impl ::std::error::Error for IncompleteTapTree {}
114+
82115
/// Taproot Tree representing a finalized [`TaprootBuilder`] (a complete binary tree).
83116
#[derive(Clone, Debug)]
84117
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
@@ -104,13 +137,16 @@ impl TapTree {
104137

105138
/// Converts a [`TaprootBuilder`] into a tree if it is complete binary tree.
106139
///
107-
/// # Return
108-
/// A `TapTree` iff the `inner` builder is complete, otherwise return the inner as `Err`.
109-
pub fn from_inner(inner: TaprootBuilder) -> Result<Self, TaprootBuilder> {
110-
if inner.is_complete() {
111-
Ok(TapTree(inner))
140+
/// # Returns
141+
/// A [`TapTree`] iff the `inner` builder is complete, otherwise return [`IncompleteTapTree`]
142+
/// error with the content of incomplete builder `inner` instance.
143+
pub fn from_inner(inner: TaprootBuilder) -> Result<Self, IncompleteTapTree> {
144+
if !inner.is_finalized() {
145+
Err(IncompleteTapTree::NotFinalized(inner))
146+
} else if inner.has_hidden_nodes() {
147+
Err(IncompleteTapTree::HiddenParts(inner))
112148
} else {
113-
Err(inner)
149+
Ok(TapTree(inner))
114150
}
115151
}
116152

src/util/psbt/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ mod macros;
4141
pub mod serialize;
4242

4343
mod map;
44-
pub use self::map::{Input, Output, TapTree, PsbtSighashType};
44+
pub use self::map::{Input, Output, TapTree, PsbtSighashType, IncompleteTapTree};
4545
use self::map::Map;
4646

4747
use util::bip32::{ExtendedPubKey, KeySource};

src/util/psbt/serialize.rs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,7 @@ impl Deserialize for TapTree {
355355
builder = builder.add_leaf_with_ver(*depth, script, leaf_version)
356356
.map_err(|_| encode::Error::ParseFailed("Tree not in DFS order"))?;
357357
}
358-
if builder.is_complete() {
358+
if builder.is_finalized() || !builder.has_hidden_nodes() {
359359
Ok(TapTree(builder))
360360
} else {
361361
Err(encode::Error::ParseFailed("Incomplete taproot Tree"))
@@ -370,8 +370,41 @@ fn key_source_len(key_source: &KeySource) -> usize {
370370

371371
#[cfg(test)]
372372
mod tests {
373+
use hashes::hex::FromHex;
373374
use super::*;
374375

376+
// Composes tree matching a given depth map, filled with dumb script leafs,
377+
// each of which consists of a single push-int op code, with int value
378+
// increased for each consecutive leaf.
379+
pub fn compose_taproot_builder<'map>(opcode: u8, depth_map: impl IntoIterator<Item = &'map u8>) -> TaprootBuilder {
380+
let mut val = opcode;
381+
let mut builder = TaprootBuilder::new();
382+
for depth in depth_map {
383+
let script = Script::from_hex(&format!("{:02x}", val)).unwrap();
384+
builder = builder.add_leaf(*depth, script).unwrap();
385+
let (new_val, _) = val.overflowing_add(1);
386+
val = new_val;
387+
}
388+
builder
389+
}
390+
391+
#[test]
392+
fn taptree_hidden() {
393+
let mut builder = compose_taproot_builder(0x51, &[2, 2, 2]);
394+
builder = builder.add_leaf_with_ver(3, Script::from_hex("b9").unwrap(), LeafVersion::from_consensus(0xC2).unwrap()).unwrap();
395+
builder = builder.add_hidden_node(3, sha256::Hash::default()).unwrap();
396+
assert!(TapTree::from_inner(builder.clone()).is_err());
397+
}
398+
399+
#[test]
400+
fn taptree_roundtrip() {
401+
let mut builder = compose_taproot_builder(0x51, &[2, 2, 2, 3]);
402+
builder = builder.add_leaf_with_ver(3, Script::from_hex("b9").unwrap(), LeafVersion::from_consensus(0xC2).unwrap()).unwrap();
403+
let tree = TapTree::from_inner(builder).unwrap();
404+
let tree_prime = TapTree::deserialize(&tree.serialize()).unwrap();
405+
assert_eq!(tree, tree_prime);
406+
}
407+
375408
#[test]
376409
fn can_deserialize_non_standard_psbt_sighash_type() {
377410
let non_standard_sighash = [222u8, 0u8, 0u8, 0u8]; // 32 byte value.

src/util/taproot.rs

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -456,16 +456,28 @@ impl TaprootBuilder {
456456

457457
/// Adds a hidden/omitted node at `depth` to the builder. Errors if the leaves are not provided
458458
/// in DFS walk order. The depth of the root node is 0.
459-
pub fn add_hidden(self, depth: u8, hash: sha256::Hash) -> Result<Self, TaprootBuilderError> {
460-
let node = NodeInfo::new_hidden(hash);
459+
pub fn add_hidden_node(self, depth: u8, hash: sha256::Hash) -> Result<Self, TaprootBuilderError> {
460+
let node = NodeInfo::new_hidden_node(hash);
461461
self.insert(node, depth)
462462
}
463463

464-
/// Checks if the builder is a complete tree.
465-
pub fn is_complete(&self) -> bool {
464+
/// Checks if the builder has finalized building a tree.
465+
pub fn is_finalized(&self) -> bool {
466466
self.branch.len() == 1 && self.branch[0].is_some()
467467
}
468468

469+
/// Checks if the builder has hidden nodes.
470+
pub fn has_hidden_nodes(&self) -> bool {
471+
for node in &self.branch {
472+
if let Some(node) = node {
473+
if node.has_hidden_nodes {
474+
return true
475+
}
476+
}
477+
}
478+
false
479+
}
480+
469481
/// Creates a [`TaprootSpendInfo`] with the given internal key.
470482
pub fn finalize<C: secp256k1::Verification>(
471483
mut self,
@@ -549,14 +561,17 @@ pub struct NodeInfo {
549561
pub(crate) hash: sha256::Hash,
550562
/// Information about leaves inside this node.
551563
pub(crate) leaves: Vec<LeafInfo>,
564+
/// Tracks information on hidden nodes below this node.
565+
pub(crate) has_hidden_nodes: bool,
552566
}
553567

554568
impl NodeInfo {
555569
/// Creates a new [`NodeInfo`] with omitted/hidden info.
556-
pub fn new_hidden(hash: sha256::Hash) -> Self {
570+
pub fn new_hidden_node(hash: sha256::Hash) -> Self {
557571
Self {
558572
hash: hash,
559573
leaves: vec![],
574+
has_hidden_nodes: true
560575
}
561576
}
562577

@@ -566,6 +581,7 @@ impl NodeInfo {
566581
Self {
567582
hash: leaf.hash(),
568583
leaves: vec![leaf],
584+
has_hidden_nodes: false,
569585
}
570586
}
571587

@@ -584,6 +600,7 @@ impl NodeInfo {
584600
Ok(Self {
585601
hash: sha256::Hash::from_inner(hash.into_inner()),
586602
leaves: all_leaves,
603+
has_hidden_nodes: a.has_hidden_nodes || b.has_hidden_nodes
587604
})
588605
}
589606
}

0 commit comments

Comments
 (0)