-
Notifications
You must be signed in to change notification settings - Fork 620
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
Conversation
/// PSBT signing verification error | ||
#[derive(Debug)] | ||
pub enum VerificationError { | ||
} |
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.
Are the variants ignored for the purpose of WIP it's meant to be uninhabited?
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 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(()) |
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 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> { |
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.
Would it make sense to take it by value and return it wrapped as Verified<Self>
to aid static checking?
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 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 |
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 it signs all the inputs, shouldn't it return SignedTransaction
instead of PartiallySignedTransaction
? If not then the doc should be updated.
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 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
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.
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.
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.
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>, |
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 Option
?
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.
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>, |
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.
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>, |
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.
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>
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 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, |
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 would you want the signer to own the PSBT?
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.
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
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.
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
- a constructor that checks the invariants and returns a
- 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)
- we may supply a default impl, e.g. an extended private key or any type implementing
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.
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.
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.
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.
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
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.
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:
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 { |
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.
Is this responsibility abstraction actually useful? What's the intended use case?
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.
There might be functions taking PSBT from a responsibility and internally extracting from it - or packing into it.
Closed in favor of #472 |
Implements #455
The discussion happens under the original issue (at least until concept Ack)