Merge rust-bitcoin/rust-bitcoin#929: Fix TapTree hidden branches bug
c036b0db6f
Unit test for failing TapTree on builder containing hidden nodes. (Dr Maxim Orlovsky)77715311cf
Prevent TapTree from hidden parts (Dr Maxim Orlovsky)b0f3992db1
Rename TaprootBuilder::is_complete into is_finalized (Dr Maxim Orlovsky)efa800fb1f
Make TapTree::from_inner return a proper error type (Dr Maxim Orlovsky)e24c6e23e3
TapTree serialization roundtrip unit test (Dr Maxim Orlovsky)56adfa4527
TaprootBuilder::has_hidden_nodes method (Dr Maxim Orlovsky)e69701e089
Rename taproot `*_hidden` API into `*_hidden_nodes` (Dr Maxim Orlovsky)6add0dd9dc
Track information about hidden leaves in taproot NodeInfo (Dr Maxim Orlovsky) Pull request description: Closes #928 ACKs for top commit: sanket1729: ACKc036b0db6f
. Reviewed the range diff apoelstra: ACKc036b0db6f
Tree-SHA512: 3a8193e6d6dd985da30a2094d1111471b5971f422525870003b77b6ac47cd4ad6e718d46a6d86bbb5e92e5253ac53804badf67edd98bbccbdc11e6383c675663
This commit is contained in:
commit
8ca18f75dd
|
@ -24,7 +24,7 @@ mod input;
|
||||||
mod output;
|
mod output;
|
||||||
|
|
||||||
pub use self::input::{Input, PsbtSighashType};
|
pub use self::input::{Input, PsbtSighashType};
|
||||||
pub use self::output::{Output, TapTree};
|
pub use self::output::{Output, TapTree, IncompleteTapTree};
|
||||||
|
|
||||||
/// A trait that describes a PSBT key-value map.
|
/// A trait that describes a PSBT key-value map.
|
||||||
pub(super) trait Map {
|
pub(super) trait Map {
|
||||||
|
|
|
@ -79,6 +79,39 @@ pub struct Output {
|
||||||
pub unknown: BTreeMap<raw::Key, Vec<u8>>,
|
pub unknown: BTreeMap<raw::Key, Vec<u8>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Error happening when [`TapTree`] is constructed from a [`TaprootBuilder`]
|
||||||
|
/// having hidden branches or not being finalized.
|
||||||
|
#[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug)]
|
||||||
|
pub enum IncompleteTapTree {
|
||||||
|
/// Indicates an attempt to construct a tap tree from a builder containing incomplete branches.
|
||||||
|
NotFinalized(TaprootBuilder),
|
||||||
|
/// Indicates an attempt to construct a tap tree from a builder containing hidden parts.
|
||||||
|
HiddenParts(TaprootBuilder),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IncompleteTapTree {
|
||||||
|
/// Converts error into the original incomplete [`TaprootBuilder`] instance.
|
||||||
|
pub fn into_builder(self) -> TaprootBuilder {
|
||||||
|
match self {
|
||||||
|
IncompleteTapTree::NotFinalized(builder) |
|
||||||
|
IncompleteTapTree::HiddenParts(builder) => builder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl core::fmt::Display for IncompleteTapTree {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
|
f.write_str(match self {
|
||||||
|
IncompleteTapTree::NotFinalized(_) => "an attempt to construct a tap tree from a builder containing incomplete branches.",
|
||||||
|
IncompleteTapTree::HiddenParts(_) => "an attempt to construct a tap tree from a builder containing hidden parts.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
|
||||||
|
impl ::std::error::Error for IncompleteTapTree {}
|
||||||
|
|
||||||
/// Taproot Tree representing a finalized [`TaprootBuilder`] (a complete binary tree).
|
/// Taproot Tree representing a finalized [`TaprootBuilder`] (a complete binary tree).
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
|
@ -104,13 +137,16 @@ impl TapTree {
|
||||||
|
|
||||||
/// Converts a [`TaprootBuilder`] into a tree if it is complete binary tree.
|
/// Converts a [`TaprootBuilder`] into a tree if it is complete binary tree.
|
||||||
///
|
///
|
||||||
/// # Return
|
/// # Returns
|
||||||
/// A `TapTree` iff the `inner` builder is complete, otherwise return the inner as `Err`.
|
/// A [`TapTree`] iff the `inner` builder is complete, otherwise return [`IncompleteTapTree`]
|
||||||
pub fn from_inner(inner: TaprootBuilder) -> Result<Self, TaprootBuilder> {
|
/// error with the content of incomplete builder `inner` instance.
|
||||||
if inner.is_complete() {
|
pub fn from_inner(inner: TaprootBuilder) -> Result<Self, IncompleteTapTree> {
|
||||||
Ok(TapTree(inner))
|
if !inner.is_finalized() {
|
||||||
|
Err(IncompleteTapTree::NotFinalized(inner))
|
||||||
|
} else if inner.has_hidden_nodes() {
|
||||||
|
Err(IncompleteTapTree::HiddenParts(inner))
|
||||||
} else {
|
} else {
|
||||||
Err(inner)
|
Ok(TapTree(inner))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -41,7 +41,7 @@ mod macros;
|
||||||
pub mod serialize;
|
pub mod serialize;
|
||||||
|
|
||||||
mod map;
|
mod map;
|
||||||
pub use self::map::{Input, Output, TapTree, PsbtSighashType};
|
pub use self::map::{Input, Output, TapTree, PsbtSighashType, IncompleteTapTree};
|
||||||
use self::map::Map;
|
use self::map::Map;
|
||||||
|
|
||||||
use util::bip32::{ExtendedPubKey, KeySource};
|
use util::bip32::{ExtendedPubKey, KeySource};
|
||||||
|
|
|
@ -355,7 +355,7 @@ impl Deserialize for TapTree {
|
||||||
builder = builder.add_leaf_with_ver(*depth, script, leaf_version)
|
builder = builder.add_leaf_with_ver(*depth, script, leaf_version)
|
||||||
.map_err(|_| encode::Error::ParseFailed("Tree not in DFS order"))?;
|
.map_err(|_| encode::Error::ParseFailed("Tree not in DFS order"))?;
|
||||||
}
|
}
|
||||||
if builder.is_complete() {
|
if builder.is_finalized() || !builder.has_hidden_nodes() {
|
||||||
Ok(TapTree(builder))
|
Ok(TapTree(builder))
|
||||||
} else {
|
} else {
|
||||||
Err(encode::Error::ParseFailed("Incomplete taproot Tree"))
|
Err(encode::Error::ParseFailed("Incomplete taproot Tree"))
|
||||||
|
@ -370,8 +370,41 @@ fn key_source_len(key_source: &KeySource) -> usize {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use hashes::hex::FromHex;
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
// Composes tree matching a given depth map, filled with dumb script leafs,
|
||||||
|
// each of which consists of a single push-int op code, with int value
|
||||||
|
// increased for each consecutive leaf.
|
||||||
|
pub fn compose_taproot_builder<'map>(opcode: u8, depth_map: impl IntoIterator<Item = &'map u8>) -> TaprootBuilder {
|
||||||
|
let mut val = opcode;
|
||||||
|
let mut builder = TaprootBuilder::new();
|
||||||
|
for depth in depth_map {
|
||||||
|
let script = Script::from_hex(&format!("{:02x}", val)).unwrap();
|
||||||
|
builder = builder.add_leaf(*depth, script).unwrap();
|
||||||
|
let (new_val, _) = val.overflowing_add(1);
|
||||||
|
val = new_val;
|
||||||
|
}
|
||||||
|
builder
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn taptree_hidden() {
|
||||||
|
let mut builder = compose_taproot_builder(0x51, &[2, 2, 2]);
|
||||||
|
builder = builder.add_leaf_with_ver(3, Script::from_hex("b9").unwrap(), LeafVersion::from_consensus(0xC2).unwrap()).unwrap();
|
||||||
|
builder = builder.add_hidden_node(3, sha256::Hash::default()).unwrap();
|
||||||
|
assert!(TapTree::from_inner(builder.clone()).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn taptree_roundtrip() {
|
||||||
|
let mut builder = compose_taproot_builder(0x51, &[2, 2, 2, 3]);
|
||||||
|
builder = builder.add_leaf_with_ver(3, Script::from_hex("b9").unwrap(), LeafVersion::from_consensus(0xC2).unwrap()).unwrap();
|
||||||
|
let tree = TapTree::from_inner(builder).unwrap();
|
||||||
|
let tree_prime = TapTree::deserialize(&tree.serialize()).unwrap();
|
||||||
|
assert_eq!(tree, tree_prime);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn can_deserialize_non_standard_psbt_sighash_type() {
|
fn can_deserialize_non_standard_psbt_sighash_type() {
|
||||||
let non_standard_sighash = [222u8, 0u8, 0u8, 0u8]; // 32 byte value.
|
let non_standard_sighash = [222u8, 0u8, 0u8, 0u8]; // 32 byte value.
|
||||||
|
|
|
@ -456,16 +456,28 @@ impl TaprootBuilder {
|
||||||
|
|
||||||
/// Adds a hidden/omitted node at `depth` to the builder. Errors if the leaves are not provided
|
/// Adds a hidden/omitted node at `depth` to the builder. Errors if the leaves are not provided
|
||||||
/// in DFS walk order. The depth of the root node is 0.
|
/// in DFS walk order. The depth of the root node is 0.
|
||||||
pub fn add_hidden(self, depth: u8, hash: sha256::Hash) -> Result<Self, TaprootBuilderError> {
|
pub fn add_hidden_node(self, depth: u8, hash: sha256::Hash) -> Result<Self, TaprootBuilderError> {
|
||||||
let node = NodeInfo::new_hidden(hash);
|
let node = NodeInfo::new_hidden_node(hash);
|
||||||
self.insert(node, depth)
|
self.insert(node, depth)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks if the builder is a complete tree.
|
/// Checks if the builder has finalized building a tree.
|
||||||
pub fn is_complete(&self) -> bool {
|
pub fn is_finalized(&self) -> bool {
|
||||||
self.branch.len() == 1 && self.branch[0].is_some()
|
self.branch.len() == 1 && self.branch[0].is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks if the builder has hidden nodes.
|
||||||
|
pub fn has_hidden_nodes(&self) -> bool {
|
||||||
|
for node in &self.branch {
|
||||||
|
if let Some(node) = node {
|
||||||
|
if node.has_hidden_nodes {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
/// Creates a [`TaprootSpendInfo`] with the given internal key.
|
/// Creates a [`TaprootSpendInfo`] with the given internal key.
|
||||||
pub fn finalize<C: secp256k1::Verification>(
|
pub fn finalize<C: secp256k1::Verification>(
|
||||||
mut self,
|
mut self,
|
||||||
|
@ -549,14 +561,17 @@ pub struct NodeInfo {
|
||||||
pub(crate) hash: sha256::Hash,
|
pub(crate) hash: sha256::Hash,
|
||||||
/// Information about leaves inside this node.
|
/// Information about leaves inside this node.
|
||||||
pub(crate) leaves: Vec<LeafInfo>,
|
pub(crate) leaves: Vec<LeafInfo>,
|
||||||
|
/// Tracks information on hidden nodes below this node.
|
||||||
|
pub(crate) has_hidden_nodes: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NodeInfo {
|
impl NodeInfo {
|
||||||
/// Creates a new [`NodeInfo`] with omitted/hidden info.
|
/// Creates a new [`NodeInfo`] with omitted/hidden info.
|
||||||
pub fn new_hidden(hash: sha256::Hash) -> Self {
|
pub fn new_hidden_node(hash: sha256::Hash) -> Self {
|
||||||
Self {
|
Self {
|
||||||
hash: hash,
|
hash: hash,
|
||||||
leaves: vec![],
|
leaves: vec![],
|
||||||
|
has_hidden_nodes: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -566,6 +581,7 @@ impl NodeInfo {
|
||||||
Self {
|
Self {
|
||||||
hash: leaf.hash(),
|
hash: leaf.hash(),
|
||||||
leaves: vec![leaf],
|
leaves: vec![leaf],
|
||||||
|
has_hidden_nodes: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -584,6 +600,7 @@ impl NodeInfo {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
hash: sha256::Hash::from_inner(hash.into_inner()),
|
hash: sha256::Hash::from_inner(hash.into_inner()),
|
||||||
leaves: all_leaves,
|
leaves: all_leaves,
|
||||||
|
has_hidden_nodes: a.has_hidden_nodes || b.has_hidden_nodes
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue