Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 54 additions & 5 deletions src/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/// 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),
Expand All @@ -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)
}
}
Expand Down Expand Up @@ -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
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@0xZaddyy I still dont understand this and it is not getting addressed.
The fee saving logic should be simple. First lets consider it in teh unilateral case. I think I made this comemnt somewhere else in this PR.

i.e how much block space can I conserve by batching my txs vs doing them unilaterally? This should be simpler starting point than a 2p payjoin.

let total_savings = (base_savings_sats * (1.0 + amount_factor * 0.2)) as u64;

Amount::from_sat(total_savings)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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
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.

niit : Since some of these are hardcode maybe const world be better
e.g

+const BASE_PRIVACY_SCORE: f64 = 10.0;
+const TIMING_PRIVACY_BONUS: f64 = 2.0;

also the numbers being multiplied and divided having a const for them with a variable name can help know the usecase better

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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,
Expand Down Expand Up @@ -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),
}));
}

Expand Down