lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

commit 74735443d1437b946fd541216b6a646c91237d2c
parent fcdfbe7afa29d55c9974ba9b6576b827cfa36aa4
Author: triesap <tyson@radroots.org>
Date:   Wed, 29 Apr 2026 20:42:30 +0000

trade: harden inventory accounting arithmetic

Diffstat:
Mcrates/trade/src/order.rs | 136+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
1 file changed, 119 insertions(+), 17 deletions(-)

diff --git a/crates/trade/src/order.rs b/crates/trade/src/order.rs @@ -129,6 +129,10 @@ pub enum RadrootsListingInventoryAccountingIssue { order_id: String, event_ids: Vec<String>, }, + ArithmeticOverflow { + bin_id: String, + event_ids: Vec<String>, + }, UnknownInventoryBin { bin_id: String, event_ids: Vec<String>, @@ -246,7 +250,7 @@ where J: IntoIterator<Item = RadrootsActiveOrderRequestRecord>, K: IntoIterator<Item = RadrootsActiveOrderDecisionRecord>, { - let mut bins = normalized_listing_inventory_bins(bins); + let (mut bins, mut issues) = normalized_listing_inventory_bins(bins); let requests = unique_request_records(requests) .into_iter() .filter(|request| request.payload.listing_addr.trim() == listing_addr) @@ -258,7 +262,6 @@ where let mut order_ids = listing_order_ids(&requests, &decisions); let mut declined_order_ids = Vec::new(); let mut invalid_event_ids = Vec::new(); - let mut issues = Vec::new(); for order_id in order_ids.drain(..) { let order_requests = requests @@ -494,11 +497,17 @@ where unique } -fn normalized_listing_inventory_bins<I>(bins: I) -> Vec<RadrootsListingInventoryBinAccounting> +fn normalized_listing_inventory_bins<I>( + bins: I, +) -> ( + Vec<RadrootsListingInventoryBinAccounting>, + Vec<RadrootsListingInventoryAccountingIssue>, +) where I: IntoIterator<Item = RadrootsListingInventoryBinAvailability>, { let mut normalized: Vec<RadrootsListingInventoryBinAccounting> = Vec::new(); + let mut issues = Vec::new(); for bin in bins { let bin_id = bin.bin_id.trim(); if bin_id.is_empty() { @@ -508,7 +517,19 @@ where .iter_mut() .find(|existing| existing.bin_id == bin_id) { - existing.available_count += bin.available_count; + if let Some(next_count) = existing.available_count.checked_add(bin.available_count) { + existing.available_count = next_count; + existing.remaining_count = next_count; + } else { + existing.available_count = u64::MAX; + existing.remaining_count = u64::MAX; + issues.push( + RadrootsListingInventoryAccountingIssue::ArithmeticOverflow { + bin_id: existing.bin_id.clone(), + event_ids: Vec::new(), + }, + ); + } } else { normalized.push(RadrootsListingInventoryBinAccounting { bin_id: bin_id.to_string(), @@ -521,7 +542,7 @@ where } } normalized.sort_by(|left, right| left.bin_id.cmp(&right.bin_id)); - normalized + (normalized, issues) } fn listing_order_ids( @@ -566,13 +587,7 @@ fn add_accepted_inventory_reservations( }; for commitment in commitments { if let Some(bin) = bins.iter_mut().find(|bin| bin.bin_id == commitment.bin_id) { - bin.accepted_reserved_count += commitment.bin_count; - bin.accepted_orders - .push(RadrootsListingInventoryOrderReservation { - order_id: order_id.to_string(), - decision_event_id: decision.event_id.clone(), - bin_count: commitment.bin_count, - }); + add_inventory_reservation(bin, order_id, decision, commitment.bin_count, issues); } else { issues.push( RadrootsListingInventoryAccountingIssue::UnknownInventoryBin { @@ -584,6 +599,31 @@ fn add_accepted_inventory_reservations( } } +fn add_inventory_reservation( + bin: &mut RadrootsListingInventoryBinAccounting, + order_id: &str, + decision: &RadrootsActiveOrderDecisionRecord, + bin_count: u64, + issues: &mut Vec<RadrootsListingInventoryAccountingIssue>, +) { + if let Some(next_count) = bin.accepted_reserved_count.checked_add(bin_count) { + bin.accepted_reserved_count = next_count; + bin.accepted_orders + .push(RadrootsListingInventoryOrderReservation { + order_id: order_id.to_string(), + decision_event_id: decision.event_id.clone(), + bin_count, + }); + } else { + issues.push( + RadrootsListingInventoryAccountingIssue::ArithmeticOverflow { + bin_id: bin.bin_id.clone(), + event_ids: vec![decision.event_id.clone()], + }, + ); + } +} + fn finish_inventory_accounting_bins( bins: &mut [RadrootsListingInventoryBinAccounting], issues: &mut Vec<RadrootsListingInventoryAccountingIssue>, @@ -669,15 +709,17 @@ fn inventory_issue_sort_key( fn inventory_issue_rank(issue: &RadrootsListingInventoryAccountingIssue) -> u8 { match issue { RadrootsListingInventoryAccountingIssue::InvalidActiveOrder { .. } => 0, - RadrootsListingInventoryAccountingIssue::UnknownInventoryBin { .. } => 1, - RadrootsListingInventoryAccountingIssue::OverReserved { .. } => 2, + RadrootsListingInventoryAccountingIssue::ArithmeticOverflow { .. } => 1, + RadrootsListingInventoryAccountingIssue::UnknownInventoryBin { .. } => 2, + RadrootsListingInventoryAccountingIssue::OverReserved { .. } => 3, } } fn inventory_issue_id(issue: &RadrootsListingInventoryAccountingIssue) -> &str { match issue { RadrootsListingInventoryAccountingIssue::InvalidActiveOrder { order_id, .. } => order_id, - RadrootsListingInventoryAccountingIssue::UnknownInventoryBin { bin_id, .. } + RadrootsListingInventoryAccountingIssue::ArithmeticOverflow { bin_id, .. } + | RadrootsListingInventoryAccountingIssue::UnknownInventoryBin { bin_id, .. } | RadrootsListingInventoryAccountingIssue::OverReserved { bin_id, .. } => bin_id, } } @@ -685,6 +727,7 @@ fn inventory_issue_id(issue: &RadrootsListingInventoryAccountingIssue) -> &str { fn inventory_issue_event_ids(issue: &RadrootsListingInventoryAccountingIssue) -> &[String] { match issue { RadrootsListingInventoryAccountingIssue::InvalidActiveOrder { event_ids, .. } + | RadrootsListingInventoryAccountingIssue::ArithmeticOverflow { event_ids, .. } | RadrootsListingInventoryAccountingIssue::UnknownInventoryBin { event_ids, .. } | RadrootsListingInventoryAccountingIssue::OverReserved { event_ids, .. } => event_ids, } @@ -1026,7 +1069,7 @@ fn push_normalized_inventory_count( return None; } if let Some(existing) = counts.iter_mut().find(|count| count.bin_id == bin_id) { - existing.bin_count += u64::from(bin_count); + existing.bin_count = existing.bin_count.checked_add(u64::from(bin_count))?; } else { counts.push(NormalizedInventoryCount { bin_id: bin_id.to_string(), @@ -1061,7 +1104,8 @@ mod tests { RadrootsActiveOrderRequestRecord, RadrootsActiveOrderStatus, RadrootsListingInventoryAccountingIssue, RadrootsListingInventoryBinAccounting, RadrootsListingInventoryBinAvailability, RadrootsListingInventoryOrderReservation, - RadrootsTradeOrderCanonicalizationError, canonicalize_active_order_decision_for_signer, + RadrootsTradeOrderCanonicalizationError, add_inventory_reservation, + canonicalize_active_order_decision_for_signer, canonicalize_active_order_request_for_signer, canonicalize_order_request_for_signer, reduce_active_order_events, reduce_listing_inventory_accounting, }; @@ -1472,6 +1516,64 @@ mod tests { } #[test] + fn reduce_listing_inventory_accounting_reports_duplicate_availability_overflow() { + let projection = reduce_listing_inventory_accounting( + &listing_addr(), + "listing-event-1", + [ + RadrootsListingInventoryBinAvailability { + bin_id: "bin-1".to_string(), + available_count: u64::MAX, + }, + inventory_bin(1), + ], + Vec::<RadrootsActiveOrderRequestRecord>::new(), + Vec::<RadrootsActiveOrderDecisionRecord>::new(), + ); + + assert_eq!(projection.bins[0].available_count, u64::MAX); + assert_eq!(projection.bins[0].accepted_reserved_count, 0); + assert_eq!(projection.bins[0].remaining_count, u64::MAX); + assert_eq!( + projection.issues, + vec![ + RadrootsListingInventoryAccountingIssue::ArithmeticOverflow { + bin_id: "bin-1".to_string(), + event_ids: Vec::new(), + } + ] + ); + } + + #[test] + fn add_inventory_reservation_reports_reservation_overflow() { + let mut bin = RadrootsListingInventoryBinAccounting { + bin_id: "bin-1".to_string(), + available_count: u64::MAX, + accepted_reserved_count: u64::MAX, + remaining_count: 0, + over_reserved: false, + accepted_orders: Vec::new(), + }; + let decision = accepted_decision_record("decision-overflow"); + let mut issues = Vec::new(); + + add_inventory_reservation(&mut bin, "order-overflow", &decision, 1, &mut issues); + + assert_eq!(bin.accepted_reserved_count, u64::MAX); + assert!(bin.accepted_orders.is_empty()); + assert_eq!( + issues, + vec![ + RadrootsListingInventoryAccountingIssue::ArithmeticOverflow { + bin_id: "bin-1".to_string(), + event_ids: vec!["decision-overflow".to_string()], + } + ] + ); + } + + #[test] fn reduce_active_order_events_rejects_invalid_decision_actor() { let mut decision = accepted_decision_record("decision-1"); decision.author_pubkey = BUYER.to_string();