Implement support for Hidden nodes in taproot trees and Fix taproot serde bugs#1533
Conversation
|
Based on #1523 |
tcharding
left a comment
There was a problem hiding this comment.
ACK 7cf04aa0070e45c6dd322a695239dd43dc0f7d68
bitcoin/src/taproot.rs
Outdated
There was a problem hiding this comment.
If you're implementing this manually (presumably to improve error handling) I suggest to use size hint both to pre-allocate the vec and to return early.
And we should use Error::invalid_length instead of custom.
There was a problem hiding this comment.
Implementing it automatically with #[serde(into = "Vec<sha256::Hash>")]
bitcoin/src/taproot.rs
Outdated
There was a problem hiding this comment.
I think the optimizer will see you're comparing twice and remove the duplicate, so the commit message is not accurate, the only cost is returning the extra bool. Or if you want to be explicit assign a < b to a variable and use it at both places.
bitcoin/src/taproot.rs
Outdated
There was a problem hiding this comment.
Seeing these panics, why are we even storing TaprootBuilder and not NodeInfo and script leaves? Make invalid states unrepresentable...
There was a problem hiding this comment.
Indeed, I already am working on this locally. Raised this PR initially to move things forward.
38aab00 to
174092d
Compare
|
Crap, you pushed in the middle of me reviewing. Do you intend to make more changes? |
|
@Kixunil, yes. Converting this to draft. I have a few other changes to address hidden nodes cleanly. I should be done by end of day today. |
8819d3d to
7472f29
Compare
|
I plan to squash the last 4 commits into one after I get access to my dev machine. This should be ready for review otherwise. There are still rough edges, which I can fix after we have #1479 in. I will spend time on it first |
7472f29 to
0cac06b
Compare
|
I squashed the last 4 commits into one. It might be a big commit to review, but could not easily split them while maintaining modularity and making sure each commit passes tests. |
bitcoin/src/taproot.rs
Outdated
There was a problem hiding this comment.
Since serialize accepts &self this is forced to clone. I think into shouldn't be needed since serde treats newtypes as transparent.
There was a problem hiding this comment.
It does not. It is still serialized as a new struct. Removing this line results in
thread 'taproot::test::test_merkle_branch_serde' panicked at 'expected Token::Seq { len: Some(2) } but serialized as NewtypeStruct { name: "TaprootMerkleBranch", }
There was a problem hiding this comment.
Oh, yeah, serializers should treat it as transaparent but they don't have to. Maybe #[serde(transparent)] would work? I'm not sure if it'd clash with try_from though.
There was a problem hiding this comment.
Yeah, #[serde(transparent)] conflicts with try_from
bitcoin/src/taproot.rs
Outdated
There was a problem hiding this comment.
Why these two delegate to a different field? It looks foot-gunny since they are supposed to be related.
There was a problem hiding this comment.
Perhaps, I should add some documentation to this. For taproot trees, because the leaves get sorted by hash while constructing the root, a tree with root A, left child B and right child C is the same as root A with left child B and right child A.
We had Eq reflect this property in TapTree, so I thought Hash should reflect this property. The Eq property is really handy while round-tripping psbts.
There was a problem hiding this comment.
If Eq returns true then comparing hashes must also return true.
I'm confused by this explanation:
a tree with root A, left child B and right child C is the same as root A with left child B and right child A.
Maybe you meant to say this?
a tree with root A, left child B and right child C is the same as root A with left child C and right child B.
There was a problem hiding this comment.
Sorry, that is what I meant to say. This is reason for custom implementation of Eq and Hash in NodeInfo. But TapTree now auto drives it.
bitcoin/src/taproot.rs
Outdated
bitcoin/src/taproot.rs
Outdated
bitcoin/src/taproot.rs
Outdated
There was a problem hiding this comment.
Would it make sense to have fn with_capacity(capacity: usize) -> TaprootBuilder and call it here? I suppose we would need to internally allocate Vec with log2(capacity + 1) (size_of::<usize>() * 8 - (capacity + 1).leading_zeros() if I'm not mistaken)
There was a problem hiding this comment.
I did not follow the formula, but conceptually I don't think it is possible to estimate the height of a tree from the number of leaves.
For example, there can be 128 depth tree with 129 leaves. One leaf at each level and 2 leaves at level 128.
There was a problem hiding this comment.
I don't think we have to optimize for unoptimal trees - they will have bigger problems than a vec reallocating too many times.
There was a problem hiding this comment.
Ah right. capacity is just a good enough guess, does not have to be strict upper bound.
There was a problem hiding this comment.
Done. I don't think there is a +1 here. Used size_of::<usize>() * 8 - (capacity).leading_zeros() instead of capacity + 1
bitcoin/src/taproot.rs
Outdated
There was a problem hiding this comment.
Looks like outside code could break invariants when this is pub?
There was a problem hiding this comment.
Indeed, it might break invariants. I remember thinking about this while adding pub, but I was mistaken :(
bitcoin/src/taproot.rs
Outdated
There was a problem hiding this comment.
If the leaf is not known we already know the hash and should return it?
There was a problem hiding this comment.
I thought it might be confusing because the hidden node might not necessarily be a leaf. It could be a hidden tree.
There was a problem hiding this comment.
I had a note for adding node_hash() method that returns the TapNodeHash
There was a problem hiding this comment.
Ah, that makes sense. Agree with adding node_hash.
4686241 to
0b4ac5e
Compare
sanket1729
left a comment
There was a problem hiding this comment.
I have a few fixes planned for the PR. Basically, there will be two iterators.
- Iterator from
NodeInfothat returnsScriptLeaves. - Iterator from
TapTreethat returns a tuple (depth:u8, ver: LeafVersion, script: &Script)
I am also not thrilled with the name ScriptLeaf because it also now has hidden nodes, but couldn't think of anything better
Is there any problem with |
Nodes might represent an internal node too. We are only dealing with tree leaves over here. But they may not be Scripts. So, |
|
My understanding is that hidden node could potentially hide subtree rather than a single leaf so then "Leaf" depend on whether you define it as "leaf of the whole tree" or "leaf of the known part of the tree". But I admit I don't see a good way to express the latter.
|
3902bdc to
af0b2d0
Compare
|
Sorry for the delay. This is ready for review again. |
|
If there are no significant changes required to get this one in, we should try to get this in 0.30 |
a5544b9
61d8fe9 to
b621465
Compare
|
@Kixunil addressed all the comments. This is ready for review. @apoelstra Great that we have the test scripts to test these kinds of silent merge failures in these kinds of long-range PRs. |
tcharding
left a comment
There was a problem hiding this comment.
ACK b6214651cb7579a5773f3da062b620b92d8f74e6
Kixunil
left a comment
There was a problem hiding this comment.
Found some bugs unfortunately.
bitcoin/src/psbt/serialize.rs
Outdated
There was a problem hiding this comment.
This doesn't look correct, the value needs to be number of bytes but len() is returning number of items.
bitcoin/src/taproot.rs
Outdated
There was a problem hiding this comment.
Perhaps unreachable! rather than silently ignoring potential bug?
There was a problem hiding this comment.
This is not a bug. TaprootSpendInfo is a cached data structure for storing information about spending leaves. As of now, we don't cache anything for hidden leaves.
There was a problem hiding this comment.
Perhaps this is cleaner:
pub fn from_node_info<C: secp256k1::Verification>(
secp: &Secp256k1<C>,
internal_key: UntweakedPublicKey,
node: NodeInfo,
) -> TaprootSpendInfo {
// Create as if it is a key spend path with the given merkle root
let root_hash = Some(node.hash);
let mut info = TaprootSpendInfo::new_key_spend(secp, internal_key, root_hash);
for leaves in node.leaves {
match leaves.leaf {
TapLeaf::Hidden(_) => {
// We don't store any information about hidden nodes in TaprootSpendInfo.
}
TapLeaf::Script(script, ver) => {
let key = (script, ver);
let value = leaves.merkle_branch;
match info.script_map.get_mut(&key) {
None => {
let mut set = BTreeSet::new();
set.insert(value);
info.script_map.insert(key, set);
},
Some(set) => {
set.insert(value);
}
}
},
}
}
info
}
bitcoin/src/taproot.rs
Outdated
There was a problem hiding this comment.
I think this needs to be multiplied by 2 since there are two calls of serialize_element.
bitcoin/src/taproot.rs
Outdated
There was a problem hiding this comment.
x needs to be divided by 2.
bitcoin/src/taproot.rs
Outdated
There was a problem hiding this comment.
Oh, just realized this is wrong because it has to forward size_hint correctly. This method also doesn't need to be implemented.
bitcoin/src/taproot.rs
Outdated
bitcoin/src/taproot.rs
Outdated
There was a problem hiding this comment.
Other traits for consideration here and above: FusedIterator, DoubleEndedIterator
31b283d to
82e4f5b
Compare
@Kixunil, good eye and great work on catching those bugs on |
|
Ouch, that was a hard review to read @Kixunil after my ack. You are a good bloke to have on the team. |
tcharding
left a comment
There was a problem hiding this comment.
I tried to put a bit more review effort in, many suggestions are only mildly better, feel free to ignore.
And just to be clear, I'm not a domain expert on taproot, take my review as code/style only please.
bitcoin/src/taproot.rs
Outdated
There was a problem hiding this comment.
Perhaps this is cleaner:
pub fn from_node_info<C: secp256k1::Verification>(
secp: &Secp256k1<C>,
internal_key: UntweakedPublicKey,
node: NodeInfo,
) -> TaprootSpendInfo {
// Create as if it is a key spend path with the given merkle root
let root_hash = Some(node.hash);
let mut info = TaprootSpendInfo::new_key_spend(secp, internal_key, root_hash);
for leaves in node.leaves {
match leaves.leaf {
TapLeaf::Hidden(_) => {
// We don't store any information about hidden nodes in TaprootSpendInfo.
}
TapLeaf::Script(script, ver) => {
let key = (script, ver);
let value = leaves.merkle_branch;
match info.script_map.get_mut(&key) {
None => {
let mut set = BTreeSet::new();
set.insert(value);
info.script_map.insert(key, set);
},
Some(set) => {
set.insert(value);
}
}
},
}
}
info
}
bitcoin/src/taproot.rs
Outdated
There was a problem hiding this comment.
OT: These use self::<ErrorType>::* statements are sprinkled around the codebase because I was coding sometime with only half my brain connected, the self:: is totally useless.
bitcoin/src/psbt/serialize.rs
Outdated
There was a problem hiding this comment.
Why the * 32 when we push just the length here?
There was a problem hiding this comment.
another good catch. I really need to think before I am typing :(
bitcoin/src/taproot.rs
Outdated
There was a problem hiding this comment.
Its not important right now but just FYI; we don't need the : here.
bitcoin/src/taproot.rs
Outdated
bitcoin/src/taproot.rs
Outdated
There was a problem hiding this comment.
Any reason next get inlined but not size_hint?
Cleanly separate `TapTree` and `NodeInfo`. Fix serde not respecting invariants for several data structures Repurpose some tests from removed taproot builder for taptree
This was incorrect and not needed. Users should not be able to create only tree leaves directly without going through the tree construction in rust-bitcoin
Implementing this for spendinfo is really complicated because it contains some cached data without retaining the components that are used to compute them. Users should serde the 1) NodeInfo and 2) internal key and reconstruct TaprootSpendInfo from it.
ScriptLeaf feels like leaf has to be a script, but it can a hidden subtree. LeafNode conveys this better
82e4f5b to
74022ba
Compare
|
@tcharding, thanks for the detailed review. Force pushed addressing your comments. |
|
Review needed on this one please crew, this is the last big PR to go in before we can consider release (other than formatting changes and the hashes/hex debate). Thanks. |
|
Sorry @tcharding I am ready to ACK this, but I spent all day yesterday trying to test it using my new nix setup, and running into unimplemented stuff that I had to fix (with pretty slow iteration times, and a long time spent trawling through cargo source code). You can see my efforts on the crate2nix repo PR list. |
This PR changes/removes the serde implementation for the following types
TaprootSpendInfois constructed from a tree, but loses information about the tree structure and maintains handy information likescript_control_block_map, cached tweaked key, Merkle root etc.TapTree::script_leaves()Data structure changes:
LeafNode: Supports Hidden leaves. Has serde implementedTapTreeandNodeInfo:TapTreeis a full BIP370 compatible tree with no hidden nodes.NodeInfois a tree that can potentially contain hidden leaves.NodeInfo::leaf_nodes: Iterator that iterates over known and hidden leaves ofNodeInfo.TapTree::script_leaves: Iterator that is guaranteed to output known leaves.