-
Notifications
You must be signed in to change notification settings - Fork 36
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
Refactor output substitution #277
Refactor output substitution #277
Conversation
This refactor makes sense to me to prevent a consumer from shooting themselves in the foot. It throws an error if output substitution is disabled when I have one suggestion, that we allow output substitution for any old script instead of just known address types. If you're up for it, consider what an interface that enables transaction cut-through would look like. To replace an output with transaction cut-through outputs, a list of outputs and their amounts would need to be passed and validated such that they spend no more than the output they replace minus additional fee costs. It would be easier for downstream to handle a single API change after deliberation here rather than multiple changes. Getting transaction construction right is at the core of the utility PDK can provide. What do you think? Output substitution is a similar interface to input contribution methods 1 2 just because it modifies the PSBt. Though I think input contribution is a bit more difficult since PSBTv1 separates Input information in both the unsigned_tx and input maps. FYI, within the context of It's a critical part of the interface to get right whether it ends up being used for simple output substitution, [transaction cut-through], or dropped in payjoin-cli. |
Agreed on allowing any output script type. Supporting cut-through would be amazing. Should we always require an output being replaced though? A receiver might want to just add new outputs for another purpose, and should be able to as long as they also contributed a large enough input to cover the total output amounts + fees. If this is true, then |
I'm not sure what the best API is yet but I think a simple one would just completely replace the Original PSBT of Output(s) paying the receiver. Then it's just a single method to maintain. pub fn substitute_outputs(&mut self, receiver_outputs: impl IntoIterator<Script>) {
// ...
} I'm trying to imagine a stable PDK 1.0 receiver interface and am taking a stab at getting your opinion on how we might get this blurry vision into focus. An interface with complete outputs would allow us to make a state machine to guide receiver implementations through one step at a time. Substitution could be a mandatory step in the state machine with this interface as an Option; to skip substitution, just pass impl WantsOutputs {
// ...
fn try_substitute_outputs(self, outputs: Option<impl IntoIterator<Script>> )
-> Result<WantsInputs>
} I haven't thought through how the receiver might pay for fees and make change for inputs in excess of what the sender is willing to pay in additional contribution out of their change output. A receiver might also need a change output that can be reduced by subsequent fees depending on the input, like in a typical UI transaction construction interface. If lots of inputs and outputs get added then the sender's additional fee contribution is likely insufficient to cover all additional costs, but the receiver has incentive to add them since the marginal cost for inputs/outputs is still less batched versus in their own transaction. A state machine with a completely defined output list can also have a simple input contribution interface follow at the next step. Right now impl WantsInputs {
// ...
fn try_preserving_privacy(self, inputs: Option<impl IntoIterator<psbt_v2::v2::Input>) -> Result<WantsInputs> As a side note, Using the PSBTv2 There's a lot to think about and a lot that can be simplified in the receiver feature API compared to what we have. Whatever step this change takes should look at the long term context. We're aiming at to get to a stable, capable, and delightful version 1.0 |
75122c3
to
7bffb4b
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.
tACK this change is working as intended but I found a couple issues that ought to be addressed in follow ups
- V2 requests are being sent with v=1 default send parameter and thus defaulting to disable_output_substitution = false. v1 requests over v2 directories are in the unsecured payment server category of BIP 78, so any v1 request has output substitution disabled. V2 requests are mistakenly being interpreted as v1 requests wrt Params.
- The error nesting doesn't make a whole lot of sense. The warning produces "[2024-06-26T00:03:44Z WARN payjoin_cli::app::v2] Failed to substitute output: Internal Server Error: Output substitution is disabled." Internal Server error is intended to be returned as a response, it probably doesn't make sense here inside the "Failed to substitute output" context. I think
crate::receive::Error
is being abused in many more spots other than this commit and the receive crate needs an error audit. If there's an easy way to make the error of this addition nesting make sense It'd be nice to get pretty before merge but it's not a show stopper.
I've tested this and am prepared to merge if you are.
payjoin/src/receive/v2.rs
Outdated
pub fn is_output_substitution_disabled(&self) -> bool { | ||
self.inner.is_output_substitution_disabled() | ||
} |
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 you sure that consumers won't want to check to avoid output substitution if they knew in advance it is disabled? Seems like the only way to check now is by actually calling try_substitute_receiver_output
. Makes sense to me but I want to make this decision consciously.
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.
Good point, I wasn't thinking about consumers other than payjoin-cli when I removed it but I think it would be useful to leave it exposed.
payjoin-cli/src/app/v2.rs
Outdated
_ = provisional_payjoin | ||
.try_substitute_receiver_output(|| { | ||
Ok(bitcoind | ||
.get_new_address(None, None) | ||
.map_err(|e| Error::Server(e.into()))? | ||
.require_network(network) | ||
.map_err(|e| Error::Server(e.into()))? | ||
.script_pubkey()) | ||
}) | ||
.map_err(|e| log::warn!("Failed to substitute output: {}", 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.
V1 receivers are long running hosted servers and only output a single bip21 per run, it made sense to me to have them substitute outputs by default.
V2 receivers on the other hand are always session based and serve only one sender. It makes less sense to me for them to try to substitute outputs. I understand that your change is unrelated but imo v2 receivers should be changed not to substitute addresses by default in a follow up.
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.
Removed in v2 receiver
Always error if `disable_output_substitution` is true.
7bffb4b
to
a480294
Compare
Agreed. I haven't come up with a satisfying solution so far, that would encompass any error type thrown by |
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.
utACK
let substitute_script = generate_script()?; | ||
self.payjoin_psbt.unsigned_tx.output[self.owned_vouts[0]].script_pubkey = substitute_script; |
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.
ultranit: two lines doesn't save length in this instance so this is borderline unnecessary assignment.
As per discussion in #239 (comment), never allow output substitution if
disable_output_substitution
is true.Consider whether there should be a custom
SubstituteOutputError
type, similar toSelectionError
.