Skip to content
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

PSBT responsibilities #456

Closed
wants to merge 1 commit into from
Closed

PSBT responsibilities #456

wants to merge 1 commit into from

Conversation

dr-orlovsky
Copy link
Collaborator

Implements #455

The discussion happens under the original issue (at least until concept Ack)

/// PSBT signing verification error
#[derive(Debug)]
pub enum VerificationError {
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Are the variants ignored for the purpose of WIP it's meant to be uninhabited?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This whole PR right now is just a mockup to agree on the planned API, so no actual data here

}
impl Display for VerificationError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
Ok(())
Copy link
Collaborator

Choose a reason for hiding this comment

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

If it's uninhabited then match *self {} expresses the intent cleanly.

type SigningError: Error + From<Self::VerificationError>;

/// Verifies PSBT structure against BIp-174 rules
fn verify(&self) -> Result<(), Self::VerificationError> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Would it make sense to take it by value and return it wrapped as Verified<Self> to aid static checking?

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think that would be too useful as the verification is part of signing afaik. So it's just a convenience function so that different signer impls can share it. Verified PSBTs aren't supposed to be passed around much afaik.

Ok(())
}

/// Signs all PSBT inputs
Copy link
Collaborator

Choose a reason for hiding this comment

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

If it signs all the inputs, shouldn't it return SignedTransaction instead of PartiallySignedTransaction? If not then the doc should be updated.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is done in accordance to BIP-174 responsibilities; and at this stage we are not producing the final Transaction. Not sure what you mean by SignedTransaction

Copy link
Contributor

Choose a reason for hiding this comment

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

The problem is that we might not be able to sign all inputs in a multisig setup, so it would take multiple sign phases. That's why I think modelling the PSBT workflow in the type system is not really worth it.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Fair, in that case the doc should be worded as "Sign all required PSBT inputs" or something similar.

{
psbt: PartiallySignedTransaction,
/// External signing function
pub sign_fn: Option<F>,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why Option?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Otherwise it's just impossible to do impl Responsibility for DefaultSigner - you will fail at struct initialization in from_psbt

psbt: PartiallySignedTransaction,
/// External signing function
pub sign_fn: Option<F>,
_err: PhantomData<E>,
Copy link
Collaborator

Choose a reason for hiding this comment

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

AFAIK the correct way to write it for functions returning E is PhantomData<fn() -> E>.

/// External signing function
pub sign_fn: Option<F>,
_err: PhantomData<E>,
_si: PhantomData<SigningInfo>,
Copy link
Collaborator

Choose a reason for hiding this comment

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

AFAIK the correct way to write it is PhantomData<fn(SginingInfo)>

Or maybe it's possible to put them together:

_marker: PhantomData<fn(SigningInfo) -> E>

Copy link
Contributor

@sgeisler sgeisler left a comment

Choose a reason for hiding this comment

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

I'm unconvinced that having a Responsibility trait is useful. The API seems overly cumbersome. A signer should be an object that can do something with a PSBT imo and not the PSBT itself that now has some more features. That might make sense if you are coming from a more traditional OO background but it really doesn't in rust. I'll try to code up an alternative approach soon.

If it turns out having a responsibility trait is actually useful in some case I'd prefer it to define a transformation of a PSBT:

trait Responsibility {
    type Error;

    fn transform(&self, psbt: PartiallySignedTransaction) -> Result<PartiallySignedTransaction, Self::Error>;
}

where F: Fn(SigningInfo, Fingerprint, DerivationPath) -> Result<Option<Signature>, E>,
E: Error
{
psbt: PartiallySignedTransaction,
Copy link
Contributor

Choose a reason for hiding this comment

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

Why would you want the signer to own the PSBT?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Responsibilities add state machine over PBST; meaning that PBST can be only is a particular responsibility state, not their mix. That is why I found decorator/facade more useful

Copy link
Contributor

@sgeisler sgeisler Aug 11, 2020

Choose a reason for hiding this comment

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

Does explicitly modeling the state machine give us any big advantages? I'm all for enforcing invariants through the type system, but the PSBT process isn't all that complicated and at least in the signing stage the guarantees it can give are limited (multiple signing rounds).

If we really want to go this way of enforcing the correct life cycle of a PSBT through the type system I'd build it using newtypes (kinda like you did), but in the following way:

  • Every state a PSBT can be is has a newtype with
    • a constructor that checks the invariants and returns a Result
    • a function to turn it back into a plain PSBT
    • side note: e.g. a PSBT that can be signed is in the state updated and stays there until it's finalized
  • Trivial state transitions that don't require external data (combiner, finalizer, extractor) can be modeled using member functions of the newtypes that represent the source state and transforms it into the target state, possibly returning an error
  • Nontrivial state transitions (update, sign, maybe create?) that need external data should be modeled as traits that define the state transition as a function consuming the source state newtype and returning a result with the target state newtype
    • we may supply a default impl, e.g. an extended private key or any type implementing Fn with a certain signature is a signer or an updater based on bitcoincore-rpc (if that makes sense)
    • but we always leave the door open for other implementations and allow abstracting over them (e.g. this function needs an argument that can act as a PSBT signer, we don't care how it works inside)

This would add a lot of complexity, but might be worth it. One annoying thing with this is that external implementors of these state transition trait's can't be allowed to directly create state newtypes (to guarantee the invariants). They'll have to go through the fallible constructors and probably unwrap if they know they did everything correctly.

EDIT: diagram of the state transitions as I imagine them
psbt_flow

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hm, this is exactly what I am trying to do, I do not see where we are different that much (agree with constructor that checks invariants). I just drafted a single state related to signing, but just to align on APIs, before implementing the rest.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

EDIT: diagram of the state transitions as I imagine them

My intent was to implement them according to the spec BIP-174, where they are quite different

Copy link
Contributor

Choose a reason for hiding this comment

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

My intent was to implement them according to the spec BIP-174, where they are quite different

That might be where our disagreement stems from, how do you interpret BIP-174? I see a few points where my model doesn't cover all possible workflows (multiple/out of order updates), but if we take that into account will we still gain any safety from modeling a workflow? When you look at this example from the BIP:

PSBT example workflow

There's a giant state in which arbitrary updates/signatures can happen and there's no sense in modeling two independent states. You'd be left with the created and finalized state. But who cares about the finalized state anyway? All we want is the extracted BTC tx at that point. That's part of the reason why I'm doubtful that we should implement these states in the type system.

use super::PartiallySignedTransaction;

/// Trait for a generic PSBT responsibility (decorator+facade)
pub trait Responsibility where Self: Sized {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this responsibility abstraction actually useful? What's the intended use case?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

There might be functions taking PSBT from a responsibility and internally extracting from it - or packing into it.

@dr-orlovsky
Copy link
Collaborator Author

Closed in favor of #472

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
PSBT
Done
Development

Successfully merging this pull request may close these issues.

None yet

3 participants