Skip to content

Psbt: Add finalizer and extractor#119

Merged
apoelstra merged 7 commits intorust-bitcoin:masterfrom
sanket1729:psbt_fixes
Oct 1, 2020
Merged

Psbt: Add finalizer and extractor#119
apoelstra merged 7 commits intorust-bitcoin:masterfrom
sanket1729:psbt_fixes

Conversation

@sanket1729
Copy link
Copy Markdown
Member

@sanket1729 sanket1729 commented Aug 5, 2020

Implemented psbt finalizer and extractor. Fixes #117 .

This PR also has some small refactors.

  • Decouples satisfaction API for reusing most code in psbt input and Tx input
  • Completed psbt finalizer. Miniscript primarily relies on three types of constraints: timelocks, hashlocks and sigs.
  1. Timelocks: There is no special witness required from psbt. The transaction input contains sequence and transaction has the corresponding locktime.
  2. Hashlocks: Awaiting on BIP174: add hash preimage fields to inputs bitcoin/bips#955 and to be merged, psbt support in rust-bitcoin.
  3. Sigs: Partial sigs contain the data required for the finalizer.
  • Once the finalizer fills in the required fields, we double-check everything by running the interpreter and checking that there are no errors.
  • Similarly, before extracting we double-check everything with the interpreter.

The interpreter signature checks rely on sighash calculation from #116. Once the sighash functionality is added this PR can be updated to use it. Currently, this returns true. We can even merge this first and add the check in a followup PR once #116 is in.

@sanket1729 sanket1729 force-pushed the psbt_fixes branch 2 times, most recently from 5dd1e1f to 1b8fbd2 Compare August 5, 2020 18:40
@sanket1729 sanket1729 changed the title Psbt fixes Psbt: Add finalizer and extractor Aug 5, 2020
pub fn bitcoinsig_from_rawsig(rawsig: &[u8]) -> Result<BitcoinSig, Error> {
let (flag, sig) = rawsig.split_last().unwrap();
let flag = bitcoin::SigHashType::from_u32(*flag as u32);
let sig = match secp256k1::Signature::from_der(sig) {
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.

Will

let sig = secp256k1::Signature::from_der(sig)?;

work?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yup, that should be cleaner. We already have a From implementation from the Secp error

src/psbt/mod.rs Outdated
#[derive(Debug)]
pub enum InputError {
/// Public key parsing error
InvalidPubkey { pubkey: Vec<u8> },
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 should retain the secp256k1::Error as well here

src/psbt/mod.rs Outdated
/// Public key parsing error
InvalidPubkey { pubkey: Vec<u8> },
/// Redeem script does not match the p2sh hash
InvalidRedeemScript,
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 should retain the expected and actual hashes here

src/psbt/mod.rs Outdated
/// Redeem script does not match the p2sh hash
InvalidRedeemScript,
/// Witness script does not match the p2wsh hash
InvalidWitnessScript,
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 should retain the expected and actual hashes here

src/psbt/mod.rs Outdated
InputError::MissingWitness => write!(f, "PSBT is missing witness"),
InputError::MissingRedeemScript => write!(f, "PSBT is Redeem script"),
InputError::MissingUtxo => {
write!(f, "PSBT is missing both witness and non-witnes UTXO")
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.

"witness" spelled wrong

use Terminal;

/// Type alias for a signature/hashtype pair
pub type BitcoinSig = (secp256k1::Signature, bitcoin::SigHashType);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Shouldn't BitcoinSig type and bitcoinsig_from_rawsig function belong to rust-bitcoin, and not miniscript lib?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Conceptually yes. But I think along with that we would also want to add new APIs to use this type instead.

/// Helper function to create BitcoinSig from Rawsig
/// Useful for downstream when implementing Satisfier.
/// Returns underlying secp if the Signature is not of correct formart
pub fn bitcoinsig_from_rawsig(rawsig: &[u8]) -> Result<BitcoinSig, Error> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It's a pity we do not have TryFrom in rust 1.22... Otherwise impl TryFrom<&[u8]> for BitcoinSig would look more native.

Ok(())
}

pub fn finalize(psbt: &mut Psbt) -> Result<(), super::Error> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Mutably borrowing the PSBT will leave it in a potentially invalid state in case of an error. I'd rather consume and return the PSBT.

Copy link
Copy Markdown
Member Author

@sanket1729 sanket1729 Sep 12, 2020

Choose a reason for hiding this comment

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

We do check the validity of the psbt before returning.

Somehow, it seems that allocating a new psbt when we are only adding two fields per input(final sciptsig and final witness) seems wrong.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

While working on PSBTs in rust-bitcoin, I am not getting why finalize shouldn't be part of PartiallySignedTransaction structure with access to self
https://github.com/rust-bitcoin/rust-bitcoin/pull/472/files#diff-77e0d8cfeb8d4f84f6f901f6fc14139fR337

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I think the method finalize could belong there, but rust-miniscript offers a way to construct the final witness and final scriptsig in a generic way by the satisfier trait.

We don't want to introduce a rust-miniscript dependancy to rust-bitcoin.

Maybe one way might be to have the method at both places, the generic finalizer in rust-miniscript and one method in rust-bitcoin that only fills in the required fields.

Copy link
Copy Markdown
Member Author

@sanket1729 sanket1729 Sep 14, 2020

Choose a reason for hiding this comment

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

The more I think about this, I think there definitely should be a finalize method in rust-bitcoin that works for all scripts. Miniscript is more of helper to the finalizer. The job of a finalizer is to fill in the final values for witness and script_sig even if they are not miniscripts.

I think the API should be as follows:

rust-miniscript:


struct finalized_witness((Script, Vec<Vec<u8>>));

fn finalize_miniscript(&psb, S: Satisfier) -> Vec<finalized_witness> 
// Final script_sig and witness
// Only works when all scripts used are miniscripts

This should also address @sgeisler concern about taking in mutable reference.
rust-bitcoin:

fn finalize(&mut self, final_witness:  Vec<finalized_witness>)
// This fills in the values in psbt.

cc @apoelstra

Copy link
Copy Markdown
Contributor

@sgeisler sgeisler Sep 14, 2020

Choose a reason for hiding this comment

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

fn finalize(&mut self, witness: Vec<u8>, script_sig: Script)

Do I understand it correctly that this finalize method would work on per-input basis? Otherwise the witness/script sig had to be matched to an input which would make the finalization at least O(n^2). This also means that miniscript finalize had to return multiple (Script, Vec<u8>) tuples (one for each input).

EDIT: and isn't the witness Vec<Vec<u8>>?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yup, my bad. Edited the original post. We can discuss the API even further. Maybe it should only work on a per input basis, but I wanted to share my opinion on how to the split between rust-miniscript's role and rust-bitcoin's role on finalizing.

}
None => {
return Err(InputError::MissingPubkey);
}
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.

more idiomatic (and readable) to move the return out of the match so it is clear there are no non-returning branches

return match partial_sig_contains_pk {
    Some((pk, _sig)) => Ok(Descriptor::Pkh(pk.to_owned())),
    None => Err(InputError::MissingPubkey),
};

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.

also every single return in this function could be removed since they appear at the end of the block (you need to change a couple ifs to else ifs). This would also be clearer since it emphasizes that this giant if statement is the only thing going on.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Removing all returns as it is the last statement

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Rather, should I keep the short circuit return paths? Maybe those are more clear

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 only saw one that I'd consider a "short-circuit return path", which is the one I complained about below because it was immediately followed by an .unwrap().

if inp.redeem_script.is_none() {
return Err(InputError::MissingRedeemScript);
}
let redeem_script = inp.redeem_script.as_ref().unwrap();
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_none followed by unwrap should be replaced with a match (which will add an indentation level but will eliminate a panic case)

let script_pubkey =
get_scriptpubkey(psbt, index).map_err(|e| Error::InputError(e, index))?;
let empty_script_sig = Script::new();
let empty_witness = Vec::new();
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: i would drop both of these variables, they are only used once and both have longer names than the expressions they replace :P

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I think I need a let binding for the following?

fn_name(&Script::new())

@sanket1729 sanket1729 force-pushed the psbt_fixes branch 2 times, most recently from 2c20d3d to 1a3db30 Compare October 1, 2020 20:02
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.

@apoelstra apoelstra merged commit eae1634 into rust-bitcoin:master Oct 1, 2020
@sanket1729 sanket1729 mentioned this pull request Oct 1, 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.

Fix Psbt finalizing for Legacy inputs

4 participants