-
Notifications
You must be signed in to change notification settings - Fork 130
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Update Psbt APIs #305
Update Psbt APIs #305
Conversation
0c27a44
to
5072177
Compare
afdc2c7
to
27679de
Compare
src/psbt/mod.rs
Outdated
/// Additional operations for miniscript descriptors for various psbt roles. | ||
/// Note that these APIs would generally error when used on scripts that are not | ||
/// miniscripts. | ||
pub trait PbstOps { |
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.
In 27679de:
I think this should be called PsbtExt
|
||
/// Sighash message(signing data) for a given psbt transaction input. | ||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] | ||
pub enum PsbtSigHashMsg { |
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.
In 27679de:
I think these variants should also hold the sighash type. Thoughts?
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.
Why do you think this might be useful? The sighash type is directly available in psbt APIs by rust-bitcoin/rust-bitcoin#847 . The signer only needs to know the message in order to sign with keys
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.
Ah, good point. I forgot about the other accessor function.
src/psbt/mod.rs
Outdated
} | ||
|
||
impl PbstOps for Psbt { | ||
/// See the triat level documentation for details |
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.
In 27679de:
Typo triat
in several places
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.
Actually I don't think any of these doccomments are needed at all. rustdoc by default will show the trait-level documentation.
src/psbt/mod.rs
Outdated
.map_err(|_e| UxtoUpdateError::MissingInputUxto)?; | ||
let secp = secp256k1::Secp256k1::verification_only(); | ||
|
||
if !(range.start < range.end) { |
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.
In 27679de:
I'm confused about the purpose of this if-clause. If the user provides an empty range shouldn't we just do nothing?
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.
Ohh, is the issue that if you try to iterate on a bad range it'll run (effectively) forever? I think you should comment this, and also change the if condition to if range.start > range.end
and also I continue to think this should be empty, rather than deriving the 0 index. Users who explicitly want 0 can provide 0..0
.
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.
If the descriptor is already is derived and contains no wildcards, we still need to process it. Will add a comment 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.
I wanted to use the range.is_empty()
API, but that some weird behavior in rust 1.29. Specifically, I can't select candidate 1 amongst the following:
511 | if Range::<u32>::is_empty(&range) { // required for MSRV
| ^^^^^^^^^^^^^^^^^^^^^^ multiple `is_empty` found
|
= note: candidate #1 is defined in an impl for the type `std::ops::Range<_>`
= note: candidate #2 is defined in an impl of the trait `std::iter::ExactSizeIterator`
I have tried a lot of ways to do this and rust IRC/discord could not help 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.
I think it might make more sense to take an Option<Range>
?
Done reviewing 27679de. No serious comments. This is spectacular, it's what we imagined years ago when PSBT and Miniscript were first coming into focus. It should probably have more extensive testing but I'm not gonna hold up the PR over that. I'm sure we'll have some API iteration to do when people start using this in the wild. |
looks pretty good! One comment: It would be great if we had a couple more APIs released:
|
@JeremyRubin the original intention of the +1 to |
Hmmmmm.... I think it makes more sense to rearchitect Satisfier to be trait Satisfier : ECDSASigner + SchnorrSigner + Preimage.... {} becuase it seems weird to have all these "baggage" methods when really you just want to provide a HWI that can sign a schnorr thing. + the methods for create_sig / update_sig need to take PSBT since otherwise we can't verify on HWI what we are signing? |
All the methods on |
ah ok... so i guess you'd do something like struct HWI<'a> {
psbt: &'a Psbt
usb_device: Path
}
impl Satisfier for HWI {
fn lookup_sig(&self, _: &Pk) -> Option<BitcoinSig> {
todo!();
}
} it still seems off, since in theory our HWI can analyze, generate sigs, finalize inputs, etc, whereas Satisfier is really for pluggin the data into our own Psbt solver. |
utACK 27679de |
@JeremyRubin I don't understand why a HWI would be used for finalization, when this requires only public data and any choices made could be overwritten by a host PC anyway. But I agree it would be nice if the |
My point is more that the correct API to have with a HWI is not to "lookup signatures" from it for a given Pk, but rather to ask it "can you do anything to this PSBT". W.r.t. finalizing, they might do that if one of the inputs has some sort of proprietary script type that is specific to that HWW. |
I think the API you're looking for would make more sense as part of a HWI object (in a HWI library) rather than as part of the rust-miniscript PSBT API. |
For example, on thing I'm tinkering with is a virtual HWI. This is an application which runs on the same computer, but you have to e.g. copy/paste PSBTs into it to get them signed. It's good for putting the key somewhere else (imagine a core wallet where you watchonly the VHWI xpub instead of having core actually have the key). I would like to send a PSBT with rust-miniscript to such a VHWI, and it makes sense to me that the rust-miniscript library would provide a Basically similar to the logic I have here: https://github.com/sapio-lang/sapio/blob/072b8835dcf4ba6f8f00f3a5d9034ef8e021e0a7/ctv_emulators/src/servers/hd.rs#L63, but this is a special case of CTV oracle where it's safe to sign without parsing. I think this is the sort of thing miniscript should cover, v.s. external signers having to write their own logic for it since it makes the library a bit more self contained "this is how you should sign a miniscript". And you shouldn't sign for things you don't understand, so the thing you understand is the miniscript (hence making it this libraries responsibility kind-of?). |
p.s. this totally could be a new crate, rust-miniscript-psbttools, but then I'd suggest extracting all the psbt code into such a target anyways (why should PSBT finalization stuff need priveledged access to the miniscript internals? if it does, then we need a better abstracted API). |
Can you propose a You may be correct that your proposal belongs in a different crate (I'm pretty sure none of this PSBT stuff needs access to minscript internals). But the support that we have here is pretty powerful and general, and I'd like to keep it in this crate even if it were redundant with a more advanced library. |
f5efd00
to
815d012
Compare
src/psbt/finalizer.rs
Outdated
if !allow_mall { | ||
desc.get_satisfaction(PsbtInputSatisfier::new(&psbt, index)) | ||
} else { | ||
desc.get_satisfaction_mall(PsbtInputSatisfier::new(&psbt, index)) |
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.
In 815d012:
Can't these use sat
as well rather than creating new input satisfiers?
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.
seems like it should work, no borrowchecker issues i can think of.
Before I get too far into the gritty details, does something like this seem to make sense for a diff --git a/src/psbt/finalizer.rs b/src/psbt/finalizer.rs
index 66466ed..94adc99 100644
--- a/src/psbt/finalizer.rs
+++ b/src/psbt/finalizer.rs
@@ -27,6 +27,7 @@ use super::{Error, InputError, PsbtInputSatisfier};
use bitcoin::blockdata::witness::Witness;
use bitcoin::secp256k1::{self, Secp256k1};
use bitcoin::util::key::XOnlyPublicKey;
+use bitcoin::util::bip32::ExtendedPrivKey;
use bitcoin::util::taproot::LeafVersion;
use bitcoin::{self, PublicKey, Script};
use descriptor::DescriptorTrait;
@@ -369,8 +370,86 @@ pub fn finalize_helper<C: secp256k1::Verification>(
Ok(())
}
+/// Trait which handles correctly the BIP-174 defined Signer role.
+pub trait SignerRole {
+ /// sign the provided PSBT in place.
+ /// we use a concrete type to allow singers to be represented as trait objects.
+ fn sign_mut(&self, psbt: &mut Psbt, secp: &Secp256k1<secp256k1::All>) -> Result<(), super::Error>;
+ /// sign the provided PSBT and put the data into a copy that we return.
+ /// The returned PSBT should be combine with the input.
+ ///
+ /// we use a concrete type to allow singers to be represented as trait objects.
+ /// Implementors may wish to optimize this implementation to write to a new PSBT which can be
+ /// combined.
+ fn sign(&self, psbt: &Psbt, secp: &Secp256k1<secp256k1::All>) -> Result<Psbt, super::Error> {
+ let mut ret = psbt.clone();
+ self.sign_mut(&mut ret, secp)?;
+ Ok(ret)
+ }
+}
+
+impl SignerRole for ExtendedPrivKey {
+ fn sign_mut(&self, psbt: &mut Psbt, secp: &Secp256k1<secp256k1::All>) -> Result<(), super::Error> {
+ for (index, input) in psbt.inputs.iter_mut().enumerate() {
+ if let Some(non_witness_utxo) = input.non_witness_utxo {
+ let txid_expected = psbt.unsigned_tx.input.get(index).map(|t|t.previous_output.txid);
+ if Some(non_witness_utxo.txid()) == txid_expected {
+ if let Some(redeem_script) = input.redeem_script {
+ // TODO: fix indexing to .at
+ let p2sh_expected = non_witness_utxo.output[psbt.unsigned_tx.input[index].previous_output.vout as usize].script_pubkey;
+ if p2sh_expected == redeem_script.to_p2sh() {
+ // sign_non_witness(redeemScript, i)
+ } else {
+ return Err(Error::InputError(InputError::InvalidRedeemScript { redeem:redeem_script:, p2sh_expected}, index));
+ }
+ } else {
+ // sign_non_witness(non_witness_utxo.vout[psbt.tx.input[i].prevout.n].scriptPubKey, i)
+ }
+ } else {
+ if let Some(txid_expected) = txid_expected {
+ return Err(Error::InputError(InputError::InvalidUTXO{txid_expected,
+ tx_provided: non_witness_utxo }, index));
+ } else {
+ return Err(Error::InputError(InputError::InvalidUnsignedTx{tx_provided: non_witness_utxo}, index));
+ }
+ }
+ } else if let Some(witness_utxo) = input.witness_utxo {
+ let script = if let Some(redeem_script) = input.redeem_script {
+ if witness_utxo.script_pubkey != redeem_script.to_p2sh() {
+ Err(InputError::InvalidRedeemScript{redeem: redeem_script, p2sh_expected: witness_utxo.script_pubkey})
+ } else {
+ Ok(redeem_script)
+ }
+ } else {
+ Ok(witness_utxo.script_pubkey)
+ }.map_err(|e| Error::InputError(e, index))?;
+ if script.is_v0_p2wpkh() {
+ // sign_witness(script[2:22], index)
+ } else if script.is_v0_p2wsh() {
+ if let Some(witness_script) = input.witness_script {
+ if script == witness_script.to_v0_p2wsh() {
+ // sign_witness(witnessScript, i)
+ } else {
+ return Err(Error::InputError(InputError::InvalidWitnessScript
+ {
+ witness_script: witness_script.clone(),
+ p2wsh_expected: script.clone(),
+ }, index));
+ }
+ } else {
+ return Err(Error::InputError(InputError::MissingWitnessScript, index));
+ }
+ } else if script.is_v1_p2tr() {
+ // sign Tr
+ }
+ }
+ }
+ Ok(())
+ }
+}
+
diff --git a/src/psbt/mod.rs b/src/psbt/mod.rs
index 52b9e90..2265b4f 100644
--- a/src/psbt/mod.rs
+++ b/src/psbt/mod.rs
@@ -79,6 +79,13 @@ pub enum InputError {
/// The (incorrect) signature
sig: Vec<u8>,
},
+ InvalidUTXO {
+ tx_provided: bitcoin::Transaction,
+ txid_expected: bitcoin::Txid,
+ },
+ InvalidUnsignedTx {
+ tx_provided: bitcoin::Transaction,
+ },
/// Pass through the underlying errors in miniscript
MiniscriptError(super::Error),
/// Missing redeem script for p2sh
@@ -132,6 +139,13 @@ pub enum Error {
impl fmt::Display for InputError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
+ InputError::InvalidUTXO {
+ ref tx_provided,
+ ref txid_expected,
+ } => write!(f, "PSBT: Expected txid {:x} got txn {:?}", txid_expected, tx_provided),
+ InputError::InvalidUnsignedTX {
+ ref tx_provided,
+ } => write!(f, "PSBT: incorrect unsigned txn {:?}", tx_provided),
InputError::InvalidSignature {
ref pubkey,
ref sig,
|
@JeremyRubin it makes sense to me but it's not clear that it actually uses Miniscript -- could you just as easily propose this for rust-bitcoin? |
815d012
to
d331bf6
Compare
In principle, the signers need not understand anything about miniscript. As @apoelstra said, I think this is better suited for rust-bitcoin directly as the method you suggest really does not require knowledge of descriptors or miniscript. In the psbt role workflow, at minimum, only updaters and finalizers need to understand miniscript. For completeness, I will list my understanding of psbt workflow should fit in rust-bitcoin ecosystem.
|
d331bf6
to
9d995a4
Compare
b08938c
to
6eefb03
Compare
Adds support for finalizing individual inputs and an additional AP that does not mutuate the original Psbt, but returns a new one
6eefb03
to
472f803
Compare
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 472f803
I'm not sure that _mut
is the correct way to name this sort of method variant, but I'm not sure it's not, so I'm ok with it.
The APIs unsigned_script_sig give a single script element push <elem>. But what Psbt redeem script, we need the script without the prefixing the op pushbytes opcode
@apoelstra , sorry for revoking your ACK. I added two more commits to this PR: One is API modification and another is a bug fix caught by integration tests(in a final upcoming PR). |
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 b3f4c18
secp: &secp256k1::Secp256k1<C>, | ||
) -> Result<(), Vec<Error>>; | ||
|
||
/// Same as [`PsbtExt::finalize_mut`], but does not mutate the input psbt and |
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.
doc issue: it does mutate the input, it owns it?
what's different here is that it's clear that unless you're the owner of the psbt, you don't clobber it and return the psbt either in the Ok or Err.
Based on #301