Skip to content
Open
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
29 changes: 28 additions & 1 deletion crates/tempo-common/src/payment/session/store/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,12 @@ fn save_channel_in_conn(
grace_ready_at = excluded.grace_ready_at,
created_at = channels.created_at,
last_used_at = excluded.last_used_at,
server_spent = excluded.server_spent",
server_spent = CASE
WHEN LENGTH(channels.server_spent) > LENGTH(excluded.server_spent) THEN channels.server_spent
WHEN LENGTH(channels.server_spent) < LENGTH(excluded.server_spent) THEN excluded.server_spent
WHEN channels.server_spent >= excluded.server_spent THEN channels.server_spent
ELSE excluded.server_spent
END",
Comment on lines +298 to +303
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve lower reconciled server spend

When a later receipt reconciles a reservation down (for example an earlier receipt reported spent=150 but the final successful receipt reports spent=120 with a higher/equal accepted cumulative), persist_session loads the existing record and calls set_server_spent(120) because server spend is explicitly latest-value, not monotonic. This conflict handler then keeps the old larger value, so ChannelRecord::close_amount() will overcharge during cooperative close instead of using the server’s latest actual spend.

Useful? React with 👍 / 👎.

params![
record.channel_id_hex(),
record.version,
Expand Down Expand Up @@ -791,6 +796,28 @@ mod tests {
assert_eq!(loaded.cumulative_amount_u128(), 150);
}

#[test]
fn stale_save_does_not_regress_server_spent() {
let temp = tempdir().unwrap();
let db_path = temp.path().join("channels.db");
let conn = open_db_at(&db_path).unwrap();

let channel_id = alloy::primitives::B256::from([0x34; 32]);
let mut latest = sample_record(channel_id, 2, ChannelStatus::Active);
latest.server_spent = 150;
save_channel_in_conn(&conn, &latest).unwrap();

let mut stale = latest;
stale.server_spent = 120;
stale.last_used_at = 3;
save_channel_in_conn(&conn, &stale).unwrap();

let loaded = load_channel_in_conn(&conn, &format!("{channel_id:#x}"))
.unwrap()
.expect("channel should load");
assert_eq!(loaded.server_spent, 150);
}

#[test]
fn load_channels_by_origin_returns_all_matching_records_ordered() {
let temp = tempdir().unwrap();
Expand Down