-
Notifications
You must be signed in to change notification settings - Fork 142
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
Remove the DescriptorTrait
#386
Conversation
spk
method
spk
methodspk
method
13e3892
to
9dd0371
Compare
Now that the trait methods are infallible I wonder if we should just remove the specialized ones. |
That was one direction I thought would come out of review, I'll explore it. |
spk
methodDescriptorTrait
This PR has been totally re-written, description and title have been changed to reflect the new PR content. |
I was hoping to re-name the |
I don't know about this approach of pulling individual methods into infallible traits. It means that you can't write code which is generic over all descriptor types, and still be able to compute It also requires author of generic code to provide several more explicit trait bounds. On the other hand, going forward we expect users to mostly use Taproot in which case they can ignore the @sanket1729 what do you think? |
hmmm, I'm not confident that I can mentally model consuming these generic APIs, I seem to be only thinking at the level of the code on the sceen in In a nutshell the [perceived] problem this PR is trying to solve is that pre-taproot and taproot descriptors are not being treated as equivalent as is apparent by the need for the
I agree with you, this is definitely bad.
I'm out of my depth here.
Thanks in advance @sanket1729 :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for tagging me @tcharding, I'll add my 2c :)
- In general, smaller, focused traits are better because they encourage composability. i.e. I can write code that only abstracts over
ToAddress
and compose that with code that abstracts overToAddress + ScriptCode
. If I can only abstract over a single trait, the individual units of code tend to be bigger. - It is still possible to provide a general
DescriptorTrait
that forces to implement all other traits and provide a blanket impl to reduce noise inwhere
clauses. I would consider this to be somewhat of an anti-pattern though.
I've skimmed the codebase and we don't seem to have a single piece of code that says:
fn generic_code<D: DescriptorTrait>(desc: D) { .. }
I'd like to challenge whether this trait should even exist if we never abstract over it?
In my opinion:
- Trying to guess what other uses may want abstract over is a fallacy. They can create their own traits if they need to abstract over multiple descriptors.
- Aligning APIs is not enough to justify a trait. Several, concrete descriptors can have the same API without forcing the user to import a trait to use them. This should resolve the fallible vs. infallible dilemma.
- The main API seems to be the
Descriptor
enum. The functions on that one can just live within a regularimpl
block and delegate to all known variants. No trait needed for that.
src/descriptor/mod.rs
Outdated
} | ||
|
||
/// A trait for getting the `scriptCode` of a transaction output. | ||
pub trait ScriptCode<Pk: MiniscriptKey + ToPublicKey> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would avoid trait-bounds in trait definitions (and struct definitions as well). They don't add any value unless you need to access associated types. Keep trait bounds to where
clauses in impl
blocks where (pun intended) the trait functionality is actually needed.
That will result in the least amount of friction.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This definition also doesn't seem to even use Pk
. Why is it there? I think you should be able to completely omit these Pk
type parameters.
src/descriptor/mod.rs
Outdated
/// A trait for getting the "witness script" for a descriptor. | ||
pub trait ExplicitScript<Pk: MiniscriptKey + ToPublicKey> { | ||
/// Computes the "witness script" of the descriptor, i.e. the underlying | ||
/// script before any hashing is done. For `Bare`, `Pkh` and `Wpkh` this | ||
/// is the scriptPubkey; for `ShWpkh` and `Sh` this is the redeemScript; | ||
/// for the others it is the witness script. | ||
fn explicit_script(&self) -> Script; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are talking a lot about "witness script" in the docs but the trait is named ExplicitScript
. Sure that is the best name? :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is on purpose, FTR my new version does not use this short form of the comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, I think it's the comment that should be changed :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cheers, fixed. The code comment I left in may need further improvements.
On this: The problem the Instead of splitting the descriptors into two enums, we can use another type parameter to encode in the type system, which APIs are available. For example (names subject to bike-shedding): /// Marker type to encode that we only support descriptors pre-taproot.
enum PreTaprootOnly { }
/// Marker type to encode that we support all kinds of descriptors, including taproot.
enum AllKinds { }
struct Descriptor<Pk, TaprootMarker> {
pub fn parse_pre_taproot<Pk>(desc: &str) -> Result<Descriptor<Pk, PreTaprootOnly>, Error> {}
pub fn parse<Pk>(desc: &str) -> Result<Descriptor<Pk, AllKinds>, Error> {}
} Within struct Descriptor<Pk, PreTaprootOnly> {
// all sorts of functions in an infallible-flavor because we are guaranteed to never have the `Tr` variant
}
struct Descriptor<Pk, AllKinds> {
// all sorts of functions in a fallible-flavor because we may also hold a `Tr` variant
}
struct Descriptor<Pk, TaprootMarker> {
// all functions which don't care about taproot
} This design relies on the fact that nothing else other than the Coming back to the initial goal: In case a user statically knows that they will never need taproot scripts, they can use the specialised constructor and gain access to a range of infallible APIs which would otherwise not exist. If that cannot be guaranteed statically, they need to use the other I am unsure whether this complexity is worth it though. |
I think before we tackle the reform of descriptor trait, we should
We also clubbed other minor functionality descriptor trait with signing-related utilities. For example:
None of these require functions like sanity check that should be removed and not be a part of trait API. This should be enforced(I think it already is, but I will double-check) at descriptor creation time. I tried to do document something similar a while ago: https://gist.github.com/sanket1729/b1ed8ed776c7ac798d5e33e40f5d996d.
I agree with @thomaseizinger, I think this is the way to go. AFAIR, @afilini has a downstream implementation of
This is the major criticism of the idea. I agree that this is bad, but I think people implementing custom descriptor traits are already advanced users of the library that I don't mind making things hard of them. This simplifies the codebase for us and the majority of people whom we expect will use the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for the delay. I was away last weekend, will catch up with notifications across the rust-bitcoin stack
src/descriptor/bare.rs
Outdated
@@ -20,7 +20,7 @@ | |||
|
|||
use std::{fmt, str::FromStr}; | |||
|
|||
use bitcoin::{self, blockdata::script, Script}; | |||
use bitcoin::{self, blockdata::script, Address, Network, Script}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You might find this throughout the codebase. I think I (probably Andrew too), always used bitcoin::DataStructure
throughout the codebase.
I have no opposition to changing this, just because I want to be consistent with the new code that I push going forward.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was not sure what to do about this. I saw, as you say, this is done a bunch. I get it for PublicKey
and PrivateKey
but I don't get it for Network
/Address
etc. since this is a bitcoin library we all know what these things are. I was also unsure because of uniformity in this crate vs uniformity across the stack, I'm not exactly sure but I think it makes sense to try for uniformity across the stack?
Is there some perspective on using bitcoin::Address
that I'm missing. Its not the first time I've debated removing paths from identifiers so I know I tend to err on the side of too terse :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do this because I work a lot on rust-elements (which has its own fork of miniscript which uses the new opcodes) so it's easy to distinguish elements::Address
from bitcoin::Address
. For Address
in particular it also helps me distinguish a "bitcoin address" from a "network address" or "memory address" etc.
I agree that within rust-miniscript itself, this serves little purpose. I mildly prefer things the way they are but don't have strong feelings either way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not exactly sure but I think it makes sense to try for uniformity across the stack
Yep, I would like to be consistent throughout the stack. I think with all the new contributors and throughout the lifetime of the project, this seems difficult to enforce/guarantee.
I agree that using Address
makes sense (although there is also IP address in the network module). I always have had the personal convention to external_crate::DataStructure
or have exactly one thing before the ::
in the path. I don't have a strong rationte to do this, just a habit I subconsciously formed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do this because I work a lot on rust-elements (which has its own fork of miniscript which uses the new opcodes) so it's easy to distinguish elements::Address from bitcoin::Address
For the purposes of having minimal conflicts when porting to elements-miniscript, it might be better to Addresss
instead of bitcoin::Address
. Considering this, if you don't have any preference between the two I would rather it be Address
than bitcoin::Address
as it would save me some time when porting changes from here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok cool, good feedback. Let's go with Address
(and Network
) then.
No sweat, good to have you back :) |
Thanks for your input @thomaseizinger, I appreciate the effort you went to. |
I agree with @thomaseizinger and @sanket1729. Lets just drop our own People who want their own "descriptor trait" are doing advanced and probably-custom things and can roll their own traits. Thanks for your detailed analysis, and for reverse-engineering so much of our API @thomaseizinger !! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Skimmed the PR again and made a few comments, looks very good overall :)
Implemented suggestions from @thomaseizinger, squashed it all down to two patches, rebased, and re-wrote the PR description. |
Yeah kinda, we have some traits that we implement on I haven't read the code or anything, just wanted to say that we were planning to abstract over generic That said, it's true that we already have our own custom traits that you probably would have to implement as well in the scenario I described, so it's not the end of the world to reimplement in BDK the equivalent of |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ACK dbdcc81
It's a big diff and I'm not sure I fully grokked all the API changes. But I tried to read it carefully and this all looks good to me. One minor nit is that I'd prefer the term "ec-signature" in the comments to be changed to "ECDSA siganture".
This sentence in the 2nd commit could be worded better IMO:
Maybe say: This makes usage of the |
DescriptorTrait
DescriptorTrait
I converted to draft. Now we have some agreement on the way forward I'll put some effort into making the patches more reviewable. Probably should have been draft this whole time, my bad. |
Using the `bitcoin::` path is necessary for keys but for other well known types, like `Address`, using the path does not add to the readability of the code. (This is subjective.)
In preparation for removing the `DescriptorTrait` remove the pretaproot module. This is possible because removing the trait will resolve the error return issues that the taproot module exists to solve.
In preparation for removing the `DescriptorTrait` move the `sanity_check` trait methods onto each individual descriptor.
In preparation for removing the `DescriptorTrait` move the `address` trait methods onto each individual descriptor. Replaces the `addr` method.
In preparation for removing the `DescriptorTrait` move the `script_pubkey` trait methods onto each individual descriptor. Replaces the `spk` method. Note that once upon a time `spk` was an infallible version of `script_pubkey` but now `script_pubkey` is itself infallible so this patch just renames the `spk` method and removes the trait method.
In preparation for removing the `DescriptorTrait` move the `unsigned_script_sig` trait methods onto each individual descriptor as required (`Sh` only). All the other trait method implementations just return `Script::new()`, just call `new` directly from the implantation of `unsigned_script_sig` on `Descriptor`.
In preparation for removing the `DescriptorTrait` remove the `explicit_script` trait method. Implement the same logic on `Descriptor` directly but call through to `inner_script` or `script_pubkey` for each descriptor as required.
In preparation for removing the `DescriptorTrait` move the `script_code` trait methods onto each individual descriptor. Fix docs on related methods also (e.g. `inner_scipt`). Implement `script_code` directly on `Descriptor`.
We never abstract over the `DescriptorTrait`, it is therefore questionable for it to exist. We have recently removed a number of trait methods out of the trait and implemented them directly on the respective descriptors. We can now do the same with the remaining 'satisfaction' trait methods. Implement the various satisfaction methods on each descriptor as required. Implement the methods directly on `Descriptor`. Delete the `DescriptorTrait`.
Split up into smaller patches, re-wrote PR description. Its still a bit to review, thanks in advance for the time! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ACK 32a74af
It is much easier to read this way, thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work.
ACK 32a74af
Pk: ToPublicKey, | ||
{ | ||
/// | ||
/// Some descriptors like pk() don't have any address. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit:
/// Some descriptors like pk() don't have any address. | |
/// Some descriptors like pk() don't have an address. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree, this is a silght improvement
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ACK 32a74af. This is a very good improvement.
@@ -706,7 +705,7 @@ impl fmt::Display for Error { | |||
write!(f, "MultiA too many keys {}", k) | |||
} | |||
Error::TaprootSpendInfoUnavialable => { | |||
write!(f, "Taproot Spend Info not computed. Hint: Did you call `compute_spend_info` before calling methods from DescriptorTrait") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for catching this.
a84a238 Fix grammar 'any' -> 'an' (Tobin C. Harding) Pull request description: Do minor grammar improvement. This is a nit from review of #386 (review) ACKs for top commit: sanket1729: ACK a84a238 Tree-SHA512: a9b08fb7a28a10958b8d2ecdf103fa4e7e6240d9217b3b5468aff423f7d6e6cae7e2980469f206f4316a91b29d1cfc8282d8dc563acce299b3925f3f2296a5c7
We never abstract over the
DescriptorTrait
so it is questionable if it should exist at all. Removing theDescriptorTrait
allows us to also delete thepretaproot
module because all the fallible vs infallible issues disolve.Delete the
DescriptorTrait
, implement the trait methods directly onDescriptor
(fallible methods). Implement infallible versions of the trait methods on each descriptor as required.Done as individual patches to ease review. Although the work is only really meaningful as a block each patch is discreet enough to merge as is.