-
Notifications
You must be signed in to change notification settings - Fork 2
Implement fee savings and privacy metrics for payjoin outcomes #2
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -106,13 +106,14 @@ pub(crate) struct InitiatePayjoinOutcome { | |
| balance_difference: f64, | ||
| /// Fee savings from the payjoin | ||
| fee_savings: Amount, | ||
| // TODO: somekind of privacy gained metric? | ||
| /// Privacy score based on input/output mixing and timing analysis resistance | ||
| privacy_score: f64, | ||
| } | ||
|
|
||
| impl InitiatePayjoinOutcome { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We now have fee_savings and privacy_score calculation in InitiatePayjoinOutcome should this be done for RespondToPayjoinOutcome so scoring is not affected?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, |
||
| /// Batching anxiety should increase and payjoin utility should decrease the closer the deadline is. | ||
| /// This can be modeled as a inverse cubic function of the time left. | ||
| /// TODO: how do we model potential fee savings? Understanding that at most there will be one input and one output added could lead to a simple linear model. | ||
| /// Fee savings are modeled linearly based on the additional input/output structure of payjoins. | ||
| fn score(&self, payjoin_utility_factor: f64) -> ActionScore { | ||
| let points = [ | ||
| (0.0, 0.0), | ||
|
|
@@ -121,8 +122,20 @@ impl InitiatePayjoinOutcome { | |
| ]; | ||
| let utility = piecewise_linear(self.time_left as f64, &points); | ||
|
|
||
| let score = self.balance_difference + (self.amount_handled * utility); | ||
| debug!("InitiatePayjoinEvent score: {:?}", score); | ||
| // Base utility score | ||
| let base_score = self.balance_difference + (self.amount_handled * utility); | ||
|
|
||
| // Add fee savings benefit (convert to float for calculation) | ||
| let fee_benefit = self.fee_savings.to_float_in(bitcoin::Denomination::Satoshi); | ||
|
|
||
| // Add privacy benefit (weighted by utility factor) | ||
| let privacy_benefit = self.privacy_score * payjoin_utility_factor; | ||
|
|
||
| let score = base_score + fee_benefit + privacy_benefit; | ||
| debug!( | ||
| "InitiatePayjoinEvent score: {:?} (base: {:?}, fee: {:?}, privacy: {:?})", | ||
| score, base_score, fee_benefit, privacy_benefit | ||
| ); | ||
| ActionScore(score) | ||
|
Comment on lines
+134
to
139
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This question is for anyone reviewing if maybe a simulation where fee_benefit is meant to matter in. (maybe to see the fee saving benefits of transaction cut through in multi party)
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes. Fee's right now are static. |
||
| } | ||
| } | ||
|
|
@@ -221,6 +234,41 @@ impl WalletView { | |
| } | ||
| } | ||
| } | ||
| /// Calculate fee savings for a payjoin based on the typical structure: | ||
| /// - Payjoin adds one input and one output compared to separate transactions | ||
| /// - Fee savings = (2 separate txs) - (1 combined tx) | ||
| fn calculate_payjoin_fee_savings(amount: f64) -> Amount { | ||
| // Rough estimate: payjoins typically save ~100-200 sats in fees | ||
| // This is a simplified model - in reality it depends on: | ||
| // - Current fee rate | ||
| // - Input/output sizes | ||
| // - Whether batching would have occurred anyway | ||
| let base_savings_sats = 150.0; | ||
|
|
||
| // Larger amounts might justify slightly higher fee savings due to more inputs | ||
| // Cap at 2x for very large amounts | ||
| let amount_factor = (amount / 100000.0).min(2.0); | ||
|
Comment on lines
+248
to
+250
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure I understand this comment. A fee a user would pay is a reflection of their impatience not really a function of the payment amount.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @0xZaddyy I still don't understand why fee savings is a function of the amount not the size of the transaction. |
||
| let total_savings = (base_savings_sats * (1.0 + amount_factor * 0.2)) as u64; | ||
|
|
||
| Amount::from_sat(total_savings) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For context: fees are largely ignored by the simulation right now. If you look in the coin selection code you see that fee rate is hardcoded and the unilateral txs all have the same "weight" so they are paying the same absolute fee. The fee savings for a 2-party payjoin is mainly for the responder / receiver. Where they only need to cover the fees of a input and output contribution and the rest is covered by the sender. Ideally the fee rate a agent choose is a function of their impatience (how close to the deadline they are). This should be a TODO -- I dont think we have this documented anywhere. An easier to place to start is probably representing fee savings to the batched unilateral strategy. Where an agent make 1 tx instead of N when they know about N payment obligation In general fee savings should always reflect how much blockspace you saved. For the payjoin receiver they would compare the absolute fee they would have to pay in a unilateral Tx ( something that we can hardcode for now given that all txs weight and pay the same fee rate) vs the absolute fee they would have to contribute to just adding an input and output. |
||
| } | ||
|
|
||
| /// Calculate privacy score for a payjoin | ||
| /// Higher scores indicate better privacy benefits | ||
| fn calculate_payjoin_privacy_score(amount: f64) -> f64 { | ||
| // Base privacy benefit from transaction structure obfuscation | ||
| let base_privacy = 10.0; | ||
|
|
||
| // Larger amounts get slightly higher privacy scores as they're more valuable to hide | ||
| // Log scaling, capped at 1.0 | ||
| let amount_factor = (amount / 100000.0).ln_1p().min(1.0); | ||
|
|
||
| // Random timing component (simplified - in reality depends on network timing) | ||
| let timing_privacy = 2.0; | ||
|
|
||
| base_privacy + (amount_factor * 5.0) + timing_privacy | ||
|
Comment on lines
+246
to
+269
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. niit : Since some of these are hardcode maybe const world be better also the numbers being multiplied and divided having a const for them with a variable name can help know the usecase better
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you for the review @Mshehu5 |
||
| } | ||
|
|
||
| fn get_payment_obligation_handled_outcome( | ||
| payment_obligation_id: &PaymentObligationId, | ||
| sim: &Simulation, | ||
|
|
@@ -299,7 +347,8 @@ fn simulate_one_action(wallet_handle: &WalletHandleMut, action: &Action) -> Vec< | |
| time_left: po.deadline.0 as i32 - wallet_view.current_timestep.0 as i32, | ||
| amount_handled, | ||
| balance_difference, | ||
| fee_savings: Amount::ZERO, // TODO: implement this | ||
| fee_savings: calculate_payjoin_fee_savings(amount_handled), | ||
| privacy_score: calculate_payjoin_privacy_score(amount_handled), | ||
| })); | ||
| } | ||
|
|
||
|
|
||
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.
Looking back at this privacy score comment now. I think its a bit outdated and should be removed of 2 party payjoins. Eventually we want dense subset score. i.e given I payjoin with this UTXO over others how dense is subtransaction model. Or given I do a mp pj with this participant over others how dense is the subtransaction model.