commit 2f2023f54cf2e7b0e0fb98b8a0e380a68c7f26d3
parent ddbb7a450c86156fb735c7dfa7260254c15a0dbc
Author: triesap <tyson@radroots.org>
Date: Tue, 28 Apr 2026 18:23:01 +0000
trade: make order reducer deterministic
Diffstat:
1 file changed, 71 insertions(+), 4 deletions(-)
diff --git a/crates/trade/src/order.rs b/crates/trade/src/order.rs
@@ -309,7 +309,9 @@ where
I: IntoIterator<Item = RadrootsActiveOrderRequestRecord>,
{
let mut unique = Vec::new();
- for request in requests {
+ let mut records = requests.into_iter().collect::<Vec<_>>();
+ records.sort_by(|left, right| left.event_id.cmp(&right.event_id));
+ for request in records {
if unique
.iter()
.all(|existing: &RadrootsActiveOrderRequestRecord| {
@@ -327,7 +329,9 @@ where
I: IntoIterator<Item = RadrootsActiveOrderDecisionRecord>,
{
let mut unique = Vec::new();
- for decision in decisions {
+ let mut records = decisions.into_iter().collect::<Vec<_>>();
+ records.sort_by(|left, right| left.event_id.cmp(&right.event_id));
+ for decision in records {
if unique
.iter()
.all(|existing: &RadrootsActiveOrderDecisionRecord| {
@@ -695,14 +699,18 @@ mod tests {
}
}
- fn request_record() -> RadrootsActiveOrderRequestRecord {
+ fn request_record_with_event_id(event_id: &str) -> RadrootsActiveOrderRequestRecord {
RadrootsActiveOrderRequestRecord {
- event_id: "request-1".to_string(),
+ event_id: event_id.to_string(),
author_pubkey: BUYER.to_string(),
payload: clean_request_payload(),
}
}
+ fn request_record() -> RadrootsActiveOrderRequestRecord {
+ request_record_with_event_id("request-1")
+ }
+
fn decision_payload(decision: RadrootsTradeOrderDecision) -> RadrootsTradeOrderDecisionEvent {
RadrootsTradeOrderDecisionEvent {
order_id: "order-1".to_string(),
@@ -968,4 +976,63 @@ mod tests {
}]
);
}
+
+ #[test]
+ fn reduce_active_order_events_reports_multiple_requests_deterministically() {
+ let projection = reduce_active_order_events(
+ "order-1",
+ [
+ request_record_with_event_id("request-2"),
+ request_record_with_event_id("request-1"),
+ ],
+ [],
+ );
+ let reversed = reduce_active_order_events(
+ "order-1",
+ [
+ request_record_with_event_id("request-1"),
+ request_record_with_event_id("request-2"),
+ ],
+ [],
+ );
+
+ assert_eq!(projection, reversed);
+ assert_eq!(projection.status, RadrootsActiveOrderStatus::Invalid);
+ assert_eq!(projection.request_event_id.as_deref(), Some("request-1"));
+ assert_eq!(
+ projection.issues,
+ vec![RadrootsActiveOrderReducerIssue::MultipleRequests {
+ event_ids: vec!["request-1".to_string(), "request-2".to_string()]
+ }]
+ );
+ }
+
+ #[test]
+ fn reduce_active_order_events_reports_conflicting_decisions_deterministically() {
+ let projection = reduce_active_order_events(
+ "order-1",
+ [request_record()],
+ [
+ accepted_decision_record("decision-2"),
+ declined_decision_record("decision-1"),
+ ],
+ );
+ let reversed = reduce_active_order_events(
+ "order-1",
+ [request_record()],
+ [
+ declined_decision_record("decision-1"),
+ accepted_decision_record("decision-2"),
+ ],
+ );
+
+ assert_eq!(projection, reversed);
+ assert_eq!(projection.status, RadrootsActiveOrderStatus::Invalid);
+ assert_eq!(
+ projection.issues,
+ vec![RadrootsActiveOrderReducerIssue::ConflictingDecisions {
+ event_ids: vec!["decision-1".to_string(), "decision-2".to_string()]
+ }]
+ );
+ }
}