Skip to content

Add DescriptorSecretKey and Descriptor::parse_secret()#116

Merged
apoelstra merged 5 commits intorust-bitcoin:masterfrom
afilini:sgeisler-descriptor-key
Nov 11, 2020
Merged

Add DescriptorSecretKey and Descriptor::parse_secret()#116
apoelstra merged 5 commits intorust-bitcoin:masterfrom
afilini:sgeisler-descriptor-key

Conversation

@afilini
Copy link
Copy Markdown
Contributor

@afilini afilini commented Jul 24, 2020

I'm opening this right PR now mostly to gather feedback, since many things are still missing and need a lot of polishing.

This PR builds on top of #93 (here's the diff with it), so it incorporates the DescriptorKey concept from @sgeisler and extends it to also support parsing of secrets (WIF or xprv) in Descriptors.

It implements pretty much what @apoelstra described in #93, which is having a way to parse a descriptor with secrets, but immediately separate them out and store them in a map, and only have public types in the descriptor. This is achieved with the SplitSecret trait that can be implemented on types that are MiniscriptKey that may contain secrets, and it adds a method split_secret() that when called returns a tuple of, roughly speaking, (PublicType, Option<SecretType>).

This PR also adds the Signer trait which at the moment is very simple but again, I wanted to hear some feedback on how to move forward with it, and especially how to make it interact in a nice way with the Satisfier trait.

Also, I don't particularly like the name I chose for some structs, like DescriptorKeyWithSecrets or DescriptorAndSigners so if you have better ideas I'd love to hear them!

@afilini
Copy link
Copy Markdown
Contributor Author

afilini commented Jul 28, 2020

I pushed some experiments I've been doing with the Signer trait. I still don't feel 100% confident about the interface, specifically if it's better to take a &mut psbt::PartiallySignedTransaction and apply the signatures inside the signer (what's currently implemented) as opposed to taking a normal reference, returning the signature and letting the caller handle everything else.

I initially liked the first approach better because it hides the logic of pushing the signature in the partial_sigs map from the caller, but that actually forces me to clone the psbt somewhere, which is not optimal.

I guess the &mut approach is better if we expect people to directly use the signers outside of the code provided by this crate, while the other one is less "user-friendly" but it allows for some minor optimizations inside this code.

The other big question is whether I should add an explicit flag to ask for a BIP143 signature or try to automatically determine it by looking at the witness_utxo/non_witness_utxo fields like I'm doing now. Considering the recent issue with Trezors and the fact that they decided to require non_witness_utxos for segwit psbts too, I think it might be necessary to have an explicit flag to avoid issues down the road.

@sanket1729 sanket1729 self-requested a review July 29, 2020 08:55
@sanket1729
Copy link
Copy Markdown
Member

The other big question is whether I should add an explicit flag to ask for a BIP143 signature or try to automatically determine it by looking at the witness_utxo/non_witness_utxo fields like I'm doing now. Considering the recent issue with Trezors and the fact that they decided to require non_witness_utxos for segwit psbts too, I think it might be necessary to have an explicit flag to avoid issues down the road.

I don't completely follow this, what is the issue to determine by looking at witness_utxo field? Can you elaborate how this might lead to more issues ahead?

As for the role of miniscript in PSBT stack, we originally thought that miniscript should only be required for finalizing psbt. Other roles must fill in the partial sigs as required. I read BIP32, BIP 174 a couple days ago, so maybe I am wrong with these. We only considered the following workflow

  1. The creator creates a psbt with unsigned tx and passes it on for signing.
  2. The signers can analyze the Miniscript, infer which keys are in the miniscript; sign the tx with appropriate sighash flag and add the partial sigs to the pbst.
  3. The finalizer finally looks at the partial sigs and each inputs and creates the transaction. The role of satisfier triat is to construct the final pbst as implemented here.
    pub fn finalize(psbt: &mut Psbt) -> Result<(), super::Error> {

Please note that the pbst implementation is incomplete and not tested.

My understanding that the overall flow should be as follows: (cc @apoelstra )

fn main() {
    // get descriptor and signers
    let descriptor, signers = .....

    /// Create unsigned initial psbt
    let psbt: bitcoin::util::psbt::PartiallySignedTransaction = .......

    // Note that this could be a distributed process
    // Sign each input and
    for input in psbt.inputs{
    // Add sig for each input. Again could be distributed process.
        for signer in signers{
        //Add partial sig here.
        signer.sign(&mut psbt, input_index)    
        }
    }
    // Pass on to the finalizer. The Satisfier trait is already implemented for psbt input.
    pbst::finalize(& mut psbt);
}

I did not look at all of the code yet, but most of it looks good.
A couple of questions that I am still unclear about:
Is there any special need for DescriptorWithSigners struct to implement FromTree. I think that this only requires FromStr and then the struct MiniscriptWithSigners should no longer be required.

@afilini
Copy link
Copy Markdown
Contributor Author

afilini commented Jul 30, 2020

I don't completely follow this, what is the issue to determine by looking at witness_utxo field? Can you elaborate how this might lead to more issues ahead?

I don't know how realistic this is, I was just thinking that maybe some wallets could decide in the future to always include non_witness_utxos and potentially remove witness_utxos entirely to save some space, because after all you can find everyting you need from the non_witness_utxo. If that was the case, PSBTs built by these wallets would be mistaken for non-segwit with the logic I have implemented currently.

As for the role of miniscript in PSBT stack, we originally thought that miniscript should only be required for finalizing psbt. Other roles must fill in the partial sigs as required.

If you feel like the signers are out of scope I can totally remove them. There was #93 (comment) that said "and have our [signers] map use the Satisfier trait to allow users to produce signatures etc with it." so I interpreted that as "implement Satisfier on SignersContaner so that it can produce signatures on-demand", but I might have just misunderstood something.

I think that this only requires FromStr and then the struct MiniscriptWithSigners should no longer be required.

Yeah, I think you are right. I was trying to keep the structs "parallel" (Miniscript -> MiniscriptWithSigners, Descriptor -> DescriptorWithSigners) but now that I think about it, the signers pretty much only make sense with descriptors, so I could remove it MiniscriptWithSigners. I'll try working on that today.

@apoelstra
Copy link
Copy Markdown
Member

I agree that PSBT signing is out of scope ... if only because it is so simple. As Sanket says, you just compute the sighash (which rust-bitcoin has functionality for), produce a signature (i.e. call sign in rust-secp) and attach it to the PSBT (and the PSBT struct exposes a BTreeMap for you to insert the signature in). Given this, I'm not sure I see the purpose of the Signer trait.

As for implementing Satisfier on a map from public keys to private keys (and from Pk::Hash to private keys), I think this seems totally reasonable. And I agree that the way to do this is to create something like the PSBTSigningContext structure which contains a reference to a PSBT, a reference to a map, and an input index. But the implementation in this PR seems overly complicated. I will do a detailed review with more comments.

@apoelstra
Copy link
Copy Markdown
Member

Some more thoughts:

  • I think DescriptorWithSigners and and MiniscriptWithSigners and DescriptorKeyWithSecrets is too clever by half. I think it'd be better to have two concrete types, DescriptorPublicKey and DescriptorSecretKey, which are basically the same except that one has secret-key variants of the other.
  • Then I think rather than having a separate type DescriptorWithSigners where we impl FromStr on it, we should just add a method to Descriptor called parse_secret or something, which returns a Descriptor<DescriptorPublicKey> as well as a HashMap<DescriptorPublicKey, DescriptorPrivateKey>. I think it's fine to wrap the HashMap in a new type, maybe call it KeyMap
  • Then you can implement a method on KeyMap which takes an input index and a mutable reference to a PSBT, which computes a signature and inserts it in the right place. (Notice that this does not actually involve Miniscript or descriptors at all, it's just a convenience function...I am ambivalent about whether we ought to have it in this library at all.)
  • Then you can implement Satisfier directly on Psbt, which takes signatures from the input map, scripts from the input map, and outputs a valid witness.

@apoelstra
Copy link
Copy Markdown
Member

Regarding my comment in 93 about "using Satisfier to produce signatures", I was just wrong. The Satisfier trait's job is to produce a witness, given signatures, not to produce signatures (which it cannot do because the API provides nowhere to input a sighash nor anywhere to output a signature). If we add such a function, and I don't necessarily think we should, it should be a standalone function.

fn sighash(
psbt: &psbt::PartiallySignedTransaction,
input_index: usize,
) -> Result<(SigHash, SigHashType), SignerError>;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a cool idea, putting this into ScriptContext. I think it should be changed though as

fn sighash(tx: &bitcoin::Transaction, input_index: usize, sighash_type: SigHashType) -> SigHash;

Then the function is more generic than just PSBT, and also it can't fail.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The existing functionality I think is still worth including in the library, but as a standalone function which takes a PSBT and input index, does the appropriate lookups (which may fail) then calls ScriptContext::sighash to do the actual sighash computation.

@afilini
Copy link
Copy Markdown
Contributor Author

afilini commented Aug 4, 2020

The reason why I kinda liked the idea of connecting the signing flow with the Satisfier trait is that it seemed nice to be able to only generate the signatures you require and nothing more, on-demand, but that really makes a difference only when somebody has the ability to provide multiple signatures in the same script, which is kind of a weird situation and probably doesn't justify all the complexity that signing carries: it's much easier to just sign with every key you have (probably just one in most cases) and then the satisfier will only pick the ones it needs.

So here's what I'll try to do for this PR:

  • Remove the Signer trait, PSBTSigningContext, etc
  • Rename DescriptorKey to DescriptorPublicKey
  • Rename DescriptorKeyWithSecrets to DescriptorSecretKey, but don't make it MiniscriptKey since we won't care about parsing it directly in Descriptor::from_str (which also means removing all the custom implementation for Eq, Ord, etc since we won't need them anymore)
  • Add Descriptor::parse_secret that parses the string as Descriptor<String> internally first, then parses and maps every individual key using translate_pk() and at the same time populates the KeyMap with secrets, if they keys contain any
  • Modify ScriptContext::sighash to use transactions instead of PSBTs and move the PSBT stuff in a separate, standalone function

Hope I understood everything right, thanks for the feedback!

@sgeisler
Copy link
Copy Markdown
Contributor

sgeisler commented Aug 4, 2020

Just so we are on the same page: Descriptor::parse_secret would return Result<(Descriptor<DescriptorPublicKey>, HashMap<DescriptorPublicKey, DescriptorSecretKey>), _>?

Another thing I noticed is that we might want to add a Hash variant to the DescriptorPublicKey enum. It's not pretty as some methods will need to panic or we'll need to sacrifice the ToPublicKey impl. But not being able to build a pkh(<hash>) descriptor is very annoying because one doesn't even know the pubkey.

@apoelstra
Copy link
Copy Markdown
Member

That's a neat idea to generate only the actually-used signatures. Unfortunately it doesn't really fit into the workflows we imagined or the API of the Satisfier trait, and I don't think it's worth the added complexity to try to support it.

If you wanted to do this, it is possible to do outside the library, by writing your own type that knows how to sign (I guess it would include a sighash or means to compute this) and implementing Satisfier on it. But my feeling is that this is too niche to be worth including in the library.

Everything else you say @afilini sounds good to me! @sgeisler that does sound like the right method signature.

@apoelstra
Copy link
Copy Markdown
Member

@sgeisler regarding a Hash variant, can you bring this up on IRC and ping me and sipa? Even with PSBT, I don't think there should be any situations where you don't know a public key but still want to create a descriptor.

@sanket1729
Copy link
Copy Markdown
Member

Even I don't know why but there is an addr descriptor in bitcoin-core that is roughly equivalent to Hash. But, even I am curious about use-cases where one might need Hash descriptor.

Also, the current implementation of PSBT finalizer is incomplete. I hope to have PR for it tomorrow.

@dr-orlovsky
Copy link
Copy Markdown
Contributor

@afilini what do you think about extending xpub/xpriv derivation paths with arbitrary tweaks, which can be useful for situations like in RGB?

@afilini
Copy link
Copy Markdown
Contributor Author

afilini commented Aug 9, 2020

It's probably out of scope in my opinion, considering that you can very easily define your own MiniscriptKey types outside of this library. It's trivial to wrap DescriptorPublicKey and store whatever you need in your custom struct.

@darosior
Copy link
Copy Markdown
Contributor

darosior commented Sep 10, 2020

Hi @afilini, i'd be pretty interested to see this moving forward (at least the xpub part).
I'd happily help you with it, thought about making a PR for the DescriptorPublicKey of your above list mainly based on #93. What do you think ?

@afilini
Copy link
Copy Markdown
Contributor Author

afilini commented Sep 11, 2020

I think I will have some time this weekend to come back to this and finish it up. I think most of the code is already there, it needs cleanup and docs. I was also planning to add commit d9fa945 from @sgeisler in this branch since I think it could be useful.

The only thing that's missing with the "refactoring" I've done is the sighash computation part, which I was trying to rewrite as suggested to take a transaction instead of a PSBT, but for whatever reason I got kinda stuck (IIRC the interface was awful and I couldn't figure out a way to improve it) and never ended up finishing it. But since this PR doesn't do signing anymore maybe we could also go ahead without sighash and open a separate PR if we really want it.

The only thing (other than me rebasing and finishing the code) that could block merging this PR is the fact that it needs a git version of rust-bitcoin, since I don't think there has been a release yet since the required changes have been merged. I don't know the release schedule of rust-bitcoin, but I think it's been a while since the last release, so hopefully that's coming soon.

As a side note, I've been using this branch in some of my projects for the past few weeks and I don't think I've noticed any particular issue/regression with it, which is nice I guess.

@darosior
Copy link
Copy Markdown
Contributor

The only thing that's missing with the "refactoring" I've done is the sighash computation part, which I was trying to rewrite as suggested to take a transaction instead of a PSBT, but for whatever reason I got kinda stuck (IIRC the interface was awful and I couldn't figure out a way to improve it) and never ended up finishing it. But since this PR doesn't do signing anymore maybe we could also go ahead without sighash and open a separate PR if we really want it.

Yeah, @sanket1729 has been doing some PSBT stuff related to sighash too lately if i'm not mistaking.

The only thing (other than me rebasing and finishing the code) that could block merging this PR is the fact that it needs a git version of rust-bitcoin, since I don't think there has been a release yet since the required changes have been merged. I don't know the release schedule of rust-bitcoin, but I think it's been a while since the last release, so hopefully that's coming soon.

It has just been bumped, and my cleaned up / improved version of #93 is rebased on top of #130 can be found here https://github.com/darosior/rust-miniscript/commits/descriptor_publickey (currently WIP, need some more tests squashed and raw pubkey with origin implementation: working on it locally).

@afilini afilini force-pushed the sgeisler-descriptor-key branch 4 times, most recently from c6862fe to f1061c3 Compare October 4, 2020 16:51
@afilini afilini marked this pull request as ready for review October 4, 2020 17:03
@afilini afilini force-pushed the sgeisler-descriptor-key branch 3 times, most recently from 37a88b3 to 807d99c Compare October 9, 2020 09:57
Copy link
Copy Markdown
Member

@sanket1729 sanket1729 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left some minor nits. It would be great if we can use Keysource from bitcoin 0.25 wherever possible.


#[derive(Debug)]
pub struct DescriptorSinglePriv {
pub origin: Option<(bip32::Fingerprint, bip32::DerivationPath)>,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you rebase to latest master which has rust-bitcoin 0.25 we can use

type KeySource = (Fingerprint, DerivationPath)

.expect("Translation fn can't fail.")
}

pub fn parse_secret(s: &str) -> Result<(Descriptor<DescriptorPublicKey>, KeyMap), Error> {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Doccomment for a public API. More important for this one because of its return type.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be renamed to parse_descriptor

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still not thrilled but I can't think of anything better :). parse_descriptor_with_keymap is probably too long for such an important function.

In any case, I'm not going to hold up the PR for this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it sounds a bit weird to me as well, but I also don't have any better ideas unfortunately..

@afilini afilini force-pushed the sgeisler-descriptor-key branch 2 times, most recently from e01b6c6 to 2ba85b9 Compare November 2, 2020 10:38
@afilini
Copy link
Copy Markdown
Contributor Author

afilini commented Nov 2, 2020

Just rebased this on master, it's ready to be reviewed :)


impl InnerXKey for bip32::ExtendedPrivKey {
fn xkey_fingerprint(&self) -> bip32::Fingerprint {
self.fingerprint(&secp256k1::Secp256k1::signing_only())
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

☠️ we definitely should not be creating a one-off secp context for this

/// # }
/// # body().unwrap()
/// ```
pub fn matches(&self, keysource: &bip32::KeySource) -> Option<bip32::DerivationPath> {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think most all of the clones in this function can be avoided

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, not the ones in the assignment to path_excluding_wildcard, since that result gets returned and needs to be owned.

}

impl DescriptorSecretKey {
pub fn as_public(&self) -> Result<DescriptorPublicKey, DescriptorKeyParseError> {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs a doccomment, and should mention that it will drop some of the derivation path if this involves hardened derivations. (Maybe it should be renamed to reflect this, but I'm not sure what a better name would be.)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you rebase so that the doccomment was there originally? Otherwise the individual commits do not compile.

You can check whether the commits compile by running git rebase -x 'cargo test && cargo +1.29.0 test' master on your branch.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be good now..

Ok((descriptor, keymap_pk))
}

pub fn to_string_with_secret(&self, key_map: &KeyMap) -> String {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could also be done with far fewer allocations, but we can leave it to a followup PR

@afilini afilini force-pushed the sgeisler-descriptor-key branch from 2ba85b9 to f7e992e Compare November 9, 2020 14:55
@afilini
Copy link
Copy Markdown
Contributor Author

afilini commented Nov 9, 2020

Thanks, sorry it took me a while to fix this!

I think most of the comments should have been addressed: everything public is now documented and I removed pretty much all the ephemeral secp contexts except for one in Descriptor::parse_secret, mostly because I think it's not a critical method in terms of performance, and in general I think it keeps the public API a bit more "clean". But if you think this should also be removed and taken as argument I'm happy to change it, I don't have any strong opinion on that and it was just my personal preference.

Other than that, I don't think I see what you mean with your comment on Descriptor::to_string_with_secret: I mean, I can see that it does many allocations and it clones the public keys needlessly, but I don't think &String is a MiniscriptKey, so I don't think you can avoid cloning them, unless you completely change approach. If it's good for you to do that in a separate PR I could try to work on it, I guess I just need a bit of help!

@apoelstra
Copy link
Copy Markdown
Member

What I was thinking with to_string_with_secret was that you'd make a type like

struct MapKey<'a, 'b> {
    map: &'a KeyMap,
    key: &'b DescriptorPublicKey,
}

then impl fmt::Display on this in a way that did the lookup and then either wrote the secret key or public key to the fmt::Formatter without allocating.

Unfortunately you still have to translate the input descriptor to a descriptor which uses this weird type as keys, so you can't avoid the allocations inherent in that.

@afilini afilini force-pushed the sgeisler-descriptor-key branch from 95d826c to 4a1e94c Compare November 11, 2020 10:12
Copy link
Copy Markdown
Member

@apoelstra apoelstra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ack 4a1e94c

There are still a few clones that I think we could eliminate, but we can deal with them in a later PR. We should probably allocate everything because we've added a lot of new APIs recently.

@apoelstra
Copy link
Copy Markdown
Member

Lol, I meant "audit" not "allocate"

@apoelstra apoelstra merged commit 283ab2b into rust-bitcoin:master Nov 11, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants