commit d6a9fb446ca526617c0ddf1764b92bf11a4aeffa
parent 4e001164f4ef8ef0d3920902f18ed81466417c5d
Author: triesap <tyson@radroots.org>
Date: Tue, 26 May 2026 04:35:02 +0000
sqlite: require usable order request evidence
Diffstat:
2 files changed, 280 insertions(+), 25 deletions(-)
diff --git a/crates/launchers/desktop/src/runtime.rs b/crates/launchers/desktop/src/runtime.rs
@@ -4565,6 +4565,7 @@ impl DesktopAppRuntimeState {
radroots_sdk::trade::RadrootsActiveTradeMessageType::TradeOrderRequested
.kind(),
))
+ || !signed_order_request_evidence_record_is_usable(&record)
{
continue;
}
@@ -8328,6 +8329,24 @@ fn order_sync_payload(
.to_string()
}
+fn signed_order_request_evidence_record_is_usable(record: &LocalEventRecord) -> bool {
+ if record.status != LocalRecordStatus::Published
+ || matches!(
+ record.outbox_status,
+ PublishOutboxStatus::Pending | PublishOutboxStatus::Failed
+ )
+ {
+ return false;
+ }
+ let Some(relay_delivery_json) = record.relay_delivery_json.as_ref() else {
+ return false;
+ };
+ let Ok(relay_delivery) = RelayDeliveryEvidence::from_json_value(relay_delivery_json) else {
+ return false;
+ };
+ matches!(relay_delivery.state.as_str(), "acknowledged" | "observed")
+}
+
#[cfg(test)]
mod tests {
use std::{
@@ -8394,7 +8413,8 @@ mod tests {
use radroots_local_events::{
BUYER_ORDER_REQUEST_LOCAL_WORK_RECORD_KIND, CANONICAL_RELAY_SET_FINGERPRINT_VERSION,
LocalEventRecord, LocalEventRecordInput, LocalEventsStore, LocalRecordFamily,
- LocalRecordStatus, PublishOutboxStatus, SourceRuntime, canonical_relay_set_fingerprint,
+ LocalRecordStatus, PublishOutboxStatus, RelayDeliveryEvidence, SourceRuntime,
+ canonical_relay_set_fingerprint,
};
use radroots_nostr_accounts::prelude::{
RadrootsNostrAccountsManager, RadrootsNostrFileAccountStore,
@@ -8406,7 +8426,7 @@ mod tests {
RadrootsTradeOrderDecision, RadrootsTradeOrderEconomicItem, RadrootsTradeOrderEconomics,
RadrootsTradeOrderItem, RadrootsTradeOrderRequested, RadrootsTradePricingBasis,
};
- use radroots_sql_core::SqliteExecutor;
+ use radroots_sql_core::{SqlExecutor, SqliteExecutor};
use serde_json::json;
use tokio::net::TcpListener;
use tokio::sync::oneshot;
@@ -13005,6 +13025,28 @@ mod tests {
}
#[test]
+ fn runtime_rejects_seller_order_decision_with_unusable_request_evidence() {
+ let relay = ThreadedAckRelay::spawn();
+ let (runtime, paths, order_id, _product_id, _seller_pubkey, _buyer_pubkey) =
+ seller_order_decision_runtime("seller_order_unusable_request_evidence", 6, 2);
+ configure_runtime_relay_ingest(&runtime, &relay);
+ mark_shared_seller_order_request_evidence_pending(&paths);
+
+ let error = runtime
+ .prepare_order_accept(order_id)
+ .expect_err("seller order decision should require usable request evidence");
+
+ assert!(matches!(
+ error,
+ AppSqliteError::InvalidProjection {
+ reason: "seller order decision requires signed order request evidence"
+ }
+ ));
+
+ cleanup_bootstrapped_runtime_paths(&paths);
+ }
+
+ #[test]
fn runtime_refreshes_configured_relay_before_seller_order_decision_signing() {
let relay = ThreadedAckRelay::spawn();
let (runtime, paths, order_id, product_id, seller_pubkey, buyer_pubkey) =
@@ -17193,6 +17235,8 @@ mod tests {
relay_set_fingerprint: Some("relay-set".to_owned()),
relay_delivery_json: Some(json!({
"state": "acknowledged",
+ "target_relays": ["wss://relay.example"],
+ "connected_relays": ["wss://relay.example"],
"acknowledged_relays": ["wss://relay.example"]
})),
})
@@ -17268,6 +17312,15 @@ mod tests {
.into_wire_parts();
let record_id = format!("app:signed_event:order-request:{trade_order_id}");
let event_id = format!("event-{record_id}");
+ let relay_delivery_json = RelayDeliveryEvidence::acknowledged(
+ ["wss://relay.example"],
+ ["wss://relay.example"],
+ ["wss://relay.example"],
+ Vec::new(),
+ )
+ .expect("acknowledged relay delivery evidence")
+ .to_json_value()
+ .expect("acknowledged relay delivery json");
store
.append_record(&LocalEventRecordInput {
record_id,
@@ -17296,14 +17349,30 @@ mod tests {
})),
outbox_status: PublishOutboxStatus::Acknowledged,
relay_set_fingerprint: Some("relay-set".to_owned()),
- relay_delivery_json: Some(json!({
- "state": "acknowledged",
- "acknowledged_relays": ["wss://relay.example"]
- })),
+ relay_delivery_json: Some(relay_delivery_json),
})
.expect("append signed order request");
}
+ fn mark_shared_seller_order_request_evidence_pending(paths: &AppDesktopRuntimePaths) {
+ let database_path = paths
+ .shared_local_events_database_path()
+ .expect("shared local events path");
+ let executor =
+ SqliteExecutor::open(database_path.as_path()).expect("open shared local events db");
+ executor
+ .exec(
+ "UPDATE local_event_record
+ SET status = 'pending_publish',
+ outbox_status = 'pending',
+ relay_set_fingerprint = NULL,
+ relay_delivery_json = NULL
+ WHERE record_id = 'app:signed_event:order-request:seller-order-decision-1'",
+ "[]",
+ )
+ .expect("mark shared order request evidence pending");
+ }
+
fn append_unrelated_signed_event_records(paths: &AppDesktopRuntimePaths, count: usize) {
let database_path = paths
.shared_local_events_database_path()
diff --git a/crates/shared/sqlite/src/local_interop.rs b/crates/shared/sqlite/src/local_interop.rs
@@ -219,6 +219,9 @@ impl<'a> AppLocalInteropRepository<'a> {
"SELECT
event_id,
event_kind,
+ local_status,
+ outbox_status,
+ relay_delivery_json,
event_pubkey,
event_created_at,
event_tags_json,
@@ -226,6 +229,7 @@ impl<'a> AppLocalInteropRepository<'a> {
event_sig
FROM local_interop_imports
WHERE record_family = 'signed_event'
+ AND local_status = 'published'
AND event_kind = ?1
ORDER BY local_seq ASC, record_id ASC",
)
@@ -238,11 +242,14 @@ impl<'a> AppLocalInteropRepository<'a> {
Ok(StoredLocalInteropSignedEventEvidence {
event_id: row.get(0)?,
event_kind: row.get(1)?,
- event_pubkey: row.get(2)?,
- event_created_at: row.get(3)?,
- event_tags_json: row.get(4)?,
- event_content: row.get(5)?,
- event_sig: row.get(6)?,
+ local_status: row.get(2)?,
+ outbox_status: row.get(3)?,
+ relay_delivery_json: row.get(4)?,
+ event_pubkey: row.get(5)?,
+ event_created_at: row.get(6)?,
+ event_tags_json: row.get(7)?,
+ event_content: row.get(8)?,
+ event_sig: row.get(9)?,
})
})
.map_err(|source| AppSqliteError::Query {
@@ -255,6 +262,9 @@ impl<'a> AppLocalInteropRepository<'a> {
operation: "read local interop signed event evidence row",
source,
})?;
+ if !signed_event_local_interop_evidence_is_usable(&evidence) {
+ continue;
+ }
if let Some(event) = signed_event_from_local_interop_evidence(&evidence)? {
events.push(event);
}
@@ -1606,6 +1616,9 @@ struct StoredSignedEventDuplicate {
struct StoredLocalInteropSignedEventEvidence {
event_id: Option<String>,
event_kind: Option<i64>,
+ local_status: String,
+ outbox_status: String,
+ relay_delivery_json: Option<String>,
event_pubkey: Option<String>,
event_created_at: Option<i64>,
event_tags_json: Option<String>,
@@ -1791,6 +1804,29 @@ fn signed_event_from_record(
}))
}
+fn signed_event_local_interop_evidence_is_usable(
+ evidence: &StoredLocalInteropSignedEventEvidence,
+) -> bool {
+ if evidence.local_status != LocalRecordStatus::Published.as_str()
+ || matches!(evidence.outbox_status.as_str(), "pending" | "failed")
+ {
+ return false;
+ }
+ let Some(relay_delivery_json) = evidence.relay_delivery_json.as_deref() else {
+ return false;
+ };
+ let Ok(relay_delivery_value) = serde_json::from_str::<Value>(relay_delivery_json) else {
+ return false;
+ };
+ let Ok(relay_delivery) = RelayDeliveryEvidence::from_json_value(&relay_delivery_value) else {
+ return false;
+ };
+ matches!(
+ relay_delivery.state,
+ RelayDeliveryState::Acknowledged | RelayDeliveryState::Observed
+ )
+}
+
fn signed_event_from_local_interop_evidence(
evidence: &StoredLocalInteropSignedEventEvidence,
) -> Result<Option<RadrootsNostrEvent>, AppSqliteError> {
@@ -1833,15 +1869,11 @@ fn signed_event_from_local_interop_evidence(
let Some(tags_json) = evidence.event_tags_json.as_deref() else {
return Ok(None);
};
- let tags_value = serde_json::from_str::<Value>(tags_json).map_err(|_| {
- AppSqliteError::InvalidProjection {
- reason: "local interop signed event tags must decode",
- }
- })?;
+ let Ok(tags_value) = serde_json::from_str::<Value>(tags_json) else {
+ return Ok(None);
+ };
let Some(tags) = tags_from_json(&tags_value) else {
- return Err(AppSqliteError::InvalidProjection {
- reason: "local interop signed event tags must be an array",
- });
+ return Ok(None);
};
Ok(Some(RadrootsNostrEvent {
id: id.to_owned(),
@@ -2318,7 +2350,7 @@ mod tests {
};
use radroots_local_events::{
LocalEventRecordInput, LocalEventRecordUpdate, LocalEventsStore, LocalRecordFamily,
- LocalRecordStatus, PublishOutboxStatus, SourceRuntime,
+ LocalRecordStatus, PublishOutboxStatus, RelayDeliveryEvidence, SourceRuntime,
};
use radroots_sql_core::SqliteExecutor;
use rusqlite::params;
@@ -2416,7 +2448,9 @@ mod tests {
relay_set_fingerprint: Some("relay-set".to_owned()),
relay_delivery_json: Some(json!({
"state": "acknowledged",
- "acknowledged_relays": ["ws://127.0.0.1:1234/"]
+ "target_relays": ["ws://127.0.0.1:1234"],
+ "connected_relays": ["ws://127.0.0.1:1234"],
+ "acknowledged_relays": ["ws://127.0.0.1:1234"]
})),
}
}
@@ -2803,6 +2837,15 @@ mod tests {
source_runtime: SourceRuntime,
owner_account_id: Option<&str>,
) -> LocalEventRecordInput {
+ let relay_delivery_json = RelayDeliveryEvidence::acknowledged(
+ ["ws://127.0.0.1:1234"],
+ ["ws://127.0.0.1:1234"],
+ ["ws://127.0.0.1:1234"],
+ Vec::new(),
+ )
+ .expect("acknowledged relay evidence")
+ .to_json_value()
+ .expect("acknowledged relay evidence json");
LocalEventRecordInput {
record_id: record_id.to_owned(),
family: LocalRecordFamily::SignedEvent,
@@ -2833,10 +2876,7 @@ mod tests {
})),
outbox_status: PublishOutboxStatus::Acknowledged,
relay_set_fingerprint: Some("relay-set".to_owned()),
- relay_delivery_json: Some(json!({
- "state": "acknowledged",
- "acknowledged_relays": ["ws://127.0.0.1:1234/"]
- })),
+ relay_delivery_json: Some(relay_delivery_json),
}
}
@@ -2947,6 +2987,152 @@ mod tests {
}
#[test]
+ fn local_interop_order_request_evidence_requires_usable_delivery_state() {
+ let app_store =
+ AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store");
+ let events = local_events_store();
+ let listing_addr = "30402:seller-pubkey:AAAAAAAAAAAAAAAAAAAAAg";
+ let buyer_pubkey = "buyer-pubkey";
+ let seller_pubkey = "seller-pubkey";
+ let relay_url = "ws://127.0.0.1:1234";
+ let build_event = |event_id: &str, order_id_raw: &str| {
+ let payload =
+ order_request_payload(order_id_raw, listing_addr, buyer_pubkey, seller_pubkey);
+ let parts = active_trade_order_request_event_build(
+ &listing_event_ptr("listing-event-1"),
+ &payload,
+ )
+ .expect("build order request event");
+ event_from_parts(event_id, buyer_pubkey, parts)
+ };
+ let acknowledged_event = build_event("order-request-evidence-ack", "usable-ack");
+ events
+ .append_record(&signed_order_event_record(
+ "cli:signed_event:order-request:evidence-ack",
+ &acknowledged_event,
+ listing_addr,
+ SourceRuntime::Cli,
+ None,
+ ))
+ .expect("append acknowledged order request evidence");
+
+ let observed_event = build_event("order-request-evidence-observed", "usable-observed");
+ let mut observed_record = signed_order_event_record(
+ "cli:signed_event:order-request:evidence-observed",
+ &observed_event,
+ listing_addr,
+ SourceRuntime::Cli,
+ None,
+ );
+ observed_record.outbox_status = PublishOutboxStatus::None;
+ observed_record.relay_delivery_json = Some(
+ RelayDeliveryEvidence::observed([relay_url], [relay_url], [relay_url], Vec::new())
+ .expect("observed relay evidence")
+ .to_json_value()
+ .expect("observed relay evidence json"),
+ );
+ events
+ .append_record(&observed_record)
+ .expect("append observed order request evidence");
+
+ let pending_event = build_event("order-request-evidence-pending", "pending");
+ let mut pending_record = signed_order_event_record(
+ "cli:signed_event:order-request:evidence-pending",
+ &pending_event,
+ listing_addr,
+ SourceRuntime::Cli,
+ None,
+ );
+ pending_record.status = LocalRecordStatus::PendingPublish;
+ pending_record.outbox_status = PublishOutboxStatus::Pending;
+ pending_record.relay_delivery_json = Some(
+ RelayDeliveryEvidence::pending([relay_url])
+ .expect("pending relay evidence")
+ .to_json_value()
+ .expect("pending relay evidence json"),
+ );
+ events
+ .append_record(&pending_record)
+ .expect("append pending order request evidence");
+
+ let failed_event = build_event("order-request-evidence-failed", "failed");
+ let mut failed_record = signed_order_event_record(
+ "cli:signed_event:order-request:evidence-failed",
+ &failed_event,
+ listing_addr,
+ SourceRuntime::Cli,
+ None,
+ );
+ failed_record.outbox_status = PublishOutboxStatus::Failed;
+ failed_record.relay_delivery_json = Some(json!({
+ "state": "failed",
+ "target_relays": [relay_url],
+ "connected_relays": [relay_url],
+ "acknowledged_relays": [],
+ "failed_relays": [{"relay_url": relay_url, "error": "relay rejected event"}]
+ }));
+ events
+ .append_record(&failed_record)
+ .expect("append failed order request evidence");
+
+ let local_only_event = build_event("order-request-evidence-local-only", "local-only");
+ let mut local_only_record = signed_order_event_record(
+ "cli:signed_event:order-request:evidence-local-only",
+ &local_only_event,
+ listing_addr,
+ SourceRuntime::Cli,
+ None,
+ );
+ local_only_record.outbox_status = PublishOutboxStatus::None;
+ local_only_record.relay_set_fingerprint = None;
+ local_only_record.relay_delivery_json = None;
+ events
+ .append_record(&local_only_record)
+ .expect("append local-only order request evidence");
+
+ let malformed_delivery_event = build_event(
+ "order-request-evidence-malformed-delivery",
+ "malformed-delivery",
+ );
+ let mut malformed_delivery_record = signed_order_event_record(
+ "cli:signed_event:order-request:evidence-malformed-delivery",
+ &malformed_delivery_event,
+ listing_addr,
+ SourceRuntime::Cli,
+ None,
+ );
+ malformed_delivery_record.relay_delivery_json = Some(json!({
+ "state": "acknowledged"
+ }));
+ events
+ .append_record(&malformed_delivery_record)
+ .expect("append malformed delivery order request evidence");
+
+ let malformed_event =
+ build_event("order-request-evidence-malformed-event", "malformed-event");
+ let mut malformed_record = signed_order_event_record(
+ "cli:signed_event:order-request:evidence-malformed-event",
+ &malformed_event,
+ listing_addr,
+ SourceRuntime::Cli,
+ None,
+ );
+ malformed_record.event_tags_json = Some(json!({"invalid": "tags"}));
+ events
+ .append_record(&malformed_record)
+ .expect("append malformed order request evidence");
+
+ app_store
+ .import_shared_local_events_from_store(&events)
+ .expect("import signed evidence records");
+ let signed_evidence = app_store
+ .load_local_interop_signed_events_by_kind(KIND_ORDER_REQUEST)
+ .expect("load filtered signed event evidence");
+
+ assert_eq!(signed_evidence, vec![acknowledged_event, observed_event]);
+ }
+
+ #[test]
fn app_origin_signed_order_request_and_decision_project_to_buyer_orders() {
let app_store =
AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store");