lib

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

commit c0da3c5ae7b45a671a0df16c1789d82aa89f7afe
parent 4bf3178cd52c2200e3bbbfc471e0d268b5627c8c
Author: triesap <tyson@radroots.org>
Date:   Tue, 14 Jul 2026 09:21:23 +0000

store: persist canonical raw event wire

- derive store ingest from verified signed event wire

- persist outbox signed and raw JSON from canonical NIP-01 material

- align downstream Nostr, trade, transport, and authority callers

- verify event-store/outbox tests plus contract and hygiene gates

Diffstat:
Mcrates/authority/src/authorization.rs | 200+++++++++++++++++++++++++++----------------------------------------------------
Mcrates/authority/src/error.rs | 4++--
Mcrates/authority/src/local_signer.rs | 14+++-----------
Mcrates/authority/src/signer.rs | 54++++++++++++++++++++++++++++++++++++------------------
Mcrates/event_store/src/error.rs | 8++++++++
Mcrates/event_store/src/model.rs | 33+++++++++++++++++++++++----------
Mcrates/event_store/src/store.rs | 320++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Mcrates/nostr/src/codec_adapters.rs | 5++---
Mcrates/nostr/src/draft_signing.rs | 51++++++++++++++++++++++++++++-----------------------
Mcrates/nostr/src/error.rs | 8++++++++
Mcrates/nostr/src/event_adapters.rs | 9+++------
Mcrates/nostr/src/event_convert.rs | 11+++++------
Mcrates/nostr/src/event_verify.rs | 69++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Mcrates/nostr/src/job_adapter.rs | 4++--
Mcrates/nostr/src/nip17.rs | 14++++++--------
Mcrates/outbox/src/error.rs | 12++++++++++++
Mcrates/outbox/src/store.rs | 138+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Mcrates/trade/src/dvm.rs | 174++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Mcrates/trade/src/listing/mod.rs | 31+++++++++++++++++--------------
Mcrates/trade/src/listing/mutation.rs | 70++++++++++++++++++++++++++++++++++++++--------------------------------
Mcrates/trade/src/listing/validation.rs | 161++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Mcrates/trade/src/order.rs | 85++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mcrates/trade/src/projection.rs | 94+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Mcrates/trade/src/validation_receipt.rs | 127+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Mcrates/transport_nostr/src/fetch.rs | 28+++++++++++++++++++++-------
Mcrates/transport_nostr/src/outbox.rs | 28+++++++---------------------
Mcrates/transport_nostr/src/publish.rs | 130++++++++++++++++++++++++++++++++++++-------------------------------------------
Mcrates/transport_nostr/tests/transport.rs | 237++++++++++++++++++++++++++++++++++++++++---------------------------------------
28 files changed, 1156 insertions(+), 963 deletions(-)

diff --git a/crates/authority/src/authorization.rs b/crates/authority/src/authorization.rs @@ -2,12 +2,12 @@ use crate::{RadrootsActorContext, RadrootsAuthorityError, RadrootsEventSigner}; use radroots_event::contract::{RadrootsEventContract, event_contract}; -#[cfg(test)] -use radroots_event::draft::RadrootsSignedEventParts; use radroots_event::draft::{ RadrootsDraftError, RadrootsEventDraft, RadrootsSignedEvent, validate_signed_nostr_event_matches_draft, }; +#[cfg(test)] +use radroots_event::wire::RadrootsNip01EventWire; #[cfg(not(feature = "std"))] use alloc::{borrow::ToOwned, string::ToString}; @@ -32,22 +32,22 @@ pub fn authorize_actor_for_draft( actor: &RadrootsActorContext, draft: &RadrootsEventDraft, ) -> Result<&'static RadrootsEventContract, RadrootsAuthorityError> { - let contract = event_contract(draft.contract_id.as_str()).ok_or_else(|| { + let contract = event_contract(draft.contract_id()).ok_or_else(|| { RadrootsAuthorityError::UnknownContract { - contract_id: draft.contract_id.clone(), + contract_id: draft.contract_id().to_owned(), } })?; - if contract.kind != draft.kind { + if contract.kind != draft.kind_u32() { return Err(RadrootsAuthorityError::DraftKindMismatch { - contract_id: draft.contract_id.clone(), + contract_id: draft.contract_id().to_owned(), expected_kind: contract.kind, - actual_kind: draft.kind, + actual_kind: draft.kind_u32(), }); } authorize_actor_for_contract(actor, contract)?; - if actor.pubkey().as_str() != draft.expected_pubkey.as_str() { + if actor.pubkey().as_str() != draft.expected_pubkey_str() { return Err(RadrootsAuthorityError::ActorPubkeyMismatch { - expected_pubkey: draft.expected_pubkey.clone(), + expected_pubkey: draft.expected_pubkey_str().to_owned(), actor_pubkey: actor.pubkey().as_str().to_owned(), }); } @@ -61,11 +61,11 @@ pub fn authorize_signer_for_draft<S>( where S: RadrootsEventSigner + ?Sized, { - if signer.pubkey().as_str() == draft.expected_pubkey.as_str() { + if signer.pubkey().as_str() == draft.expected_pubkey_str() { Ok(()) } else { Err(RadrootsAuthorityError::SignerPubkeyMismatch { - expected_pubkey: draft.expected_pubkey.clone(), + expected_pubkey: draft.expected_pubkey_str().to_owned(), signer_pubkey: signer.pubkey().as_str().to_owned(), }) } @@ -190,7 +190,7 @@ mod tests { #[derive(Default)] struct SignedEventOverrides { event_id: Option<String>, - created_at: Option<u32>, + created_at: Option<u64>, kind: Option<u32>, tags: Option<Vec<Vec<String>>>, content: Option<String>, @@ -236,46 +236,69 @@ mod tests { &self, draft: &RadrootsEventDraft, ) -> Result<RadrootsSignedEvent, RadrootsSignerError> { - RadrootsSignedEvent::new(RadrootsSignedEventParts { + let wire = RadrootsNip01EventWire { id: self .overrides .event_id .as_deref() - .unwrap_or(draft.expected_event_id.as_str()) + .unwrap_or(draft.expected_event_id_str()) .to_owned(), pubkey: self.pubkey.to_string(), - created_at: self.overrides.created_at.unwrap_or(draft.created_at), - kind: self.overrides.kind.unwrap_or(draft.kind), + created_at: self.overrides.created_at.unwrap_or(draft.created_at_u64()), + kind: self.overrides.kind.unwrap_or(draft.kind_u32()), tags: self .overrides .tags .clone() - .unwrap_or_else(|| draft.tags.clone()), + .unwrap_or_else(|| draft.tags_as_vec()), content: self .overrides .content .clone() - .unwrap_or_else(|| draft.content.clone()), + .unwrap_or_else(|| draft.content().to_owned()), sig: hex_128('f'), - raw_json: "{}".to_owned(), - }) - .map_err(|error| RadrootsSignerError::SigningFailed { - message: error.to_string(), + extra: Default::default(), + }; + RadrootsSignedEvent::from_wire_unchecked(wire, "{}").map_err(|error| { + RadrootsSignerError::SigningFailed { + message: error.to_string(), + } }) } } fn signed_event_from_draft(draft: &RadrootsEventDraft) -> RadrootsSignedEvent { - RadrootsSignedEvent::new(RadrootsSignedEventParts { - id: draft.expected_event_id.clone(), - pubkey: draft.expected_pubkey.clone(), - created_at: draft.created_at, - kind: draft.kind, - tags: draft.tags.clone(), - content: draft.content.clone(), - sig: hex_128('f'), - raw_json: "{}".to_owned(), - }) + signed_event_from_parts( + draft.expected_event_id_str().to_owned(), + draft.expected_pubkey_str().to_owned(), + draft.created_at_u64(), + draft.kind_u32(), + draft.tags_as_vec(), + draft.content().to_owned(), + ) + } + + fn signed_event_from_parts( + id: String, + pubkey: String, + created_at: u64, + kind: u32, + tags: Vec<Vec<String>>, + content: String, + ) -> RadrootsSignedEvent { + RadrootsSignedEvent::from_wire_unchecked( + RadrootsNip01EventWire { + id, + pubkey, + created_at, + kind, + tags, + content, + sig: hex_128('f'), + extra: Default::default(), + }, + "{}", + ) .expect("signed event") } @@ -344,44 +367,6 @@ mod tests { } #[test] - fn unknown_contract_and_kind_mismatch_fail() { - let actor = seller_actor(hex_64('a').as_str()); - let unknown = RadrootsEventDraft { - contract_id: "radroots.unknown.v1".to_owned(), - contract_registry_version: 1, - kind: KIND_LISTING, - created_at: 1_700_000_000, - tags: Vec::new(), - content: "{}".to_owned(), - expected_pubkey: hex_64('a'), - expected_event_id: hex_64('e'), - }; - assert!(matches!( - authorize_actor_for_draft(&actor, &unknown), - Err(RadrootsAuthorityError::UnknownContract { .. }) - )); - - let wrong_kind = RadrootsEventDraft { - contract_id: "radroots.listing.published.v1".to_owned(), - contract_registry_version: 1, - kind: KIND_POST, - created_at: 1_700_000_000, - tags: Vec::new(), - content: "{}".to_owned(), - expected_pubkey: hex_64('a'), - expected_event_id: hex_64('e'), - }; - assert!(matches!( - authorize_actor_for_draft(&actor, &wrong_kind), - Err(RadrootsAuthorityError::DraftKindMismatch { - expected_kind: KIND_LISTING, - actual_kind: KIND_POST, - .. - }) - )); - } - - #[test] fn signed_event_id_mismatch_fails() { let pubkey = hex_64('a'); let draft = listing_draft(pubkey.as_str()); @@ -402,7 +387,7 @@ mod tests { let signer = StaticSigner::with_overrides( pubkey.as_str(), SignedEventOverrides { - created_at: Some(draft.created_at + 1), + created_at: Some(draft.created_at_u64() + 1), ..SignedEventOverrides::default() }, ); @@ -500,17 +485,14 @@ mod tests { fn signed_event_pubkey_mismatch_fails() { let pubkey = hex_64('a'); let draft = listing_draft(pubkey.as_str()); - let signed = RadrootsSignedEvent::new(RadrootsSignedEventParts { - id: draft.expected_event_id.clone(), - pubkey: hex_64('b'), - created_at: draft.created_at, - kind: draft.kind, - tags: draft.tags.clone(), - content: draft.content.clone(), - sig: hex_128('f'), - raw_json: "{}".to_owned(), - }) - .expect("signed event"); + let signed = signed_event_from_parts( + draft.expected_event_id_str().to_owned(), + hex_64('b'), + draft.created_at_u64(), + draft.kind_u32(), + draft.tags_as_vec(), + draft.content().to_owned(), + ); assert!(matches!( validate_signed_event_matches_draft(&signed, &draft), @@ -531,54 +513,10 @@ mod tests { } #[test] - fn signed_event_computed_id_mismatch_fails() { - let pubkey = hex_64('a'); - let inconsistent_draft = RadrootsEventDraft { - contract_id: "radroots.listing.published.v1".to_owned(), - contract_registry_version: 1, - kind: KIND_LISTING, - created_at: 1_700_000_000, - tags: vec![vec!["d".to_owned(), "listing-a".to_owned()]], - content: "{}".to_owned(), - expected_pubkey: pubkey, - expected_event_id: hex_64('e'), - }; - let signed = signed_event_from_draft(&inconsistent_draft); - - assert!(matches!( - validate_signed_event_matches_draft(&signed, &inconsistent_draft), - Err(RadrootsAuthorityError::SignedEventComputedIdMismatch { .. }) - )); - } - - #[test] - fn sign_authorized_draft_calls_full_integrity_check() { - let pubkey = hex_64('a'); - let inconsistent_draft = RadrootsEventDraft { - contract_id: "radroots.listing.published.v1".to_owned(), - contract_registry_version: 1, - kind: KIND_LISTING, - created_at: 1_700_000_000, - tags: vec![vec!["d".to_owned(), "listing-a".to_owned()]], - content: "{}".to_owned(), - expected_pubkey: pubkey.clone(), - expected_event_id: hex_64('e'), - }; - let actor = seller_actor(pubkey.as_str()); - let signer = StaticSigner::new(pubkey.as_str()); - - assert!(matches!( - sign_authorized_draft(&actor, &signer, &inconsistent_draft), - Err(RadrootsAuthorityError::SignedEventComputedIdMismatch { .. }) - )); - } - - #[test] fn static_signer_maps_invalid_signed_event_parts() { let pubkey = hex_64('a'); - let mut draft = listing_draft(pubkey.as_str()); - draft.expected_event_id = "bad-id".to_owned(); - let signer = StaticSigner::new(pubkey.as_str()); + let draft = listing_draft(pubkey.as_str()); + let signer = StaticSigner::with_event_id(pubkey.as_str(), "bad-id".to_owned()); assert!(matches!( signer.sign_frozen_draft(&draft), @@ -595,9 +533,9 @@ mod tests { let signed = sign_authorized_draft(&actor, &signer, &draft).expect("signed"); - assert_eq!(signed.id, draft.expected_event_id); - assert_eq!(signed.pubkey, draft.expected_pubkey); - assert_eq!(signed.kind, KIND_LISTING); + assert_eq!(signed.id_str(), draft.expected_event_id_str()); + assert_eq!(signed.pubkey_str(), draft.expected_pubkey_str()); + assert_eq!(signed.kind(), KIND_LISTING); } #[test] diff --git a/crates/authority/src/error.rs b/crates/authority/src/error.rs @@ -59,8 +59,8 @@ pub enum RadrootsAuthorityError { }, SignedEventCreatedAtMismatch { - expected_created_at: u32, - actual_created_at: u32, + expected_created_at: u64, + actual_created_at: u64, }, SignedEventKindMismatch { diff --git a/crates/authority/src/local_signer.rs b/crates/authority/src/local_signer.rs @@ -68,15 +68,7 @@ mod tests { } fn verification_event(signed: &RadrootsSignedEvent) -> RadrootsEventEnvelope { - RadrootsEventEnvelope { - id: signed.id.clone(), - author: signed.pubkey.clone(), - created_at: signed.created_at, - kind: signed.kind, - tags: signed.tags.clone(), - content: signed.content.clone(), - sig: signed.sig.clone(), - } + signed.envelope().clone() } #[test] @@ -93,8 +85,8 @@ mod tests { let signed = signer.sign_frozen_draft(&draft).expect("signed"); - assert_eq!(signed.id, draft.expected_event_id); - assert_eq!(signed.pubkey, draft.expected_pubkey); + assert_eq!(signed.id_str(), draft.expected_event_id_str()); + assert_eq!(signed.pubkey_str(), draft.expected_pubkey_str()); assert_eq!( radroots_nostr_verify_event(&verification_event(&signed)), RadrootsNostrEventVerification::Verified diff --git a/crates/authority/src/signer.rs b/crates/authority/src/signer.rs @@ -1,10 +1,10 @@ #![forbid(unsafe_code)] use crate::{RadrootsAuthorityError, RadrootsSignerError}; -#[cfg(test)] -use radroots_event::draft::RadrootsSignedEventParts; use radroots_event::draft::{RadrootsEventDraft, RadrootsSignedEvent}; use radroots_event::ids::RadrootsPublicKey; +#[cfg(test)] +use radroots_event::wire::RadrootsNip01EventWire; #[derive(Clone, Debug, PartialEq, Eq)] pub struct RadrootsSignerIdentity { @@ -60,6 +60,7 @@ mod tests { struct MockSigner { pubkey: RadrootsPublicKey, failure: Option<RadrootsSignerError>, + event_id: Option<String>, } impl MockSigner { @@ -67,6 +68,7 @@ mod tests { Self { pubkey: RadrootsPublicKey::parse(pubkey).expect("pubkey"), failure: None, + event_id: None, } } @@ -74,6 +76,15 @@ mod tests { Self { pubkey: RadrootsPublicKey::parse(pubkey).expect("pubkey"), failure: Some(failure), + event_id: None, + } + } + + fn with_event_id(pubkey: &str, event_id: String) -> Self { + Self { + pubkey: RadrootsPublicKey::parse(pubkey).expect("pubkey"), + failure: None, + event_id: Some(event_id), } } } @@ -98,16 +109,24 @@ mod tests { } }); } - RadrootsSignedEvent::new(RadrootsSignedEventParts { - id: draft.expected_event_id.to_string(), - pubkey: self.pubkey.to_string(), - created_at: draft.created_at, - kind: draft.kind, - tags: draft.tags.clone(), - content: draft.content.clone(), - sig: hex_128('f'), - raw_json: "{}".to_owned(), - }) + let id = self + .event_id + .as_deref() + .unwrap_or(draft.expected_event_id_str()) + .to_owned(); + RadrootsSignedEvent::from_wire_unchecked( + RadrootsNip01EventWire { + id, + pubkey: self.pubkey.to_string(), + created_at: draft.created_at_u64(), + kind: draft.kind_u32(), + tags: draft.tags_as_vec(), + content: draft.content().to_owned(), + sig: hex_128('f'), + extra: Default::default(), + }, + "{}", + ) .map_err(|error| RadrootsSignerError::SigningFailed { message: error.to_string(), }) @@ -142,9 +161,9 @@ mod tests { let signed = signer.sign_frozen_draft(&draft).expect("signed"); - assert_eq!(signed.id, draft.expected_event_id); - assert_eq!(signed.pubkey, pubkey); - assert_eq!(signed.kind, KIND_POST); + assert_eq!(signed.id_str(), draft.expected_event_id_str()); + assert_eq!(signed.pubkey_str(), pubkey); + assert_eq!(signed.kind(), KIND_POST); } #[test] @@ -175,9 +194,8 @@ mod tests { #[test] fn mock_signer_maps_invalid_signed_event_parts() { let pubkey = hex_64('a'); - let signer = MockSigner::new(pubkey.as_str()); - let mut draft = draft_for(pubkey.as_str()); - draft.expected_event_id = "bad-id".to_string(); + let signer = MockSigner::with_event_id(pubkey.as_str(), "bad-id".to_string()); + let draft = draft_for(pubkey.as_str()); let err = signer.sign_frozen_draft(&draft).expect_err("failure"); diff --git a/crates/event_store/src/error.rs b/crates/event_store/src/error.rs @@ -1,6 +1,8 @@ use radroots_event::contract::RadrootsContractMatchError; +use radroots_event::draft::RadrootsSignedEventError; use radroots_event::event_head::RadrootsEventHeadMalformed; use radroots_event::ids::RadrootsIdParseError; +use radroots_event::wire::RadrootsEventWireError; use radroots_transport::RadrootsTransportError; #[derive(Debug, thiserror::Error)] @@ -15,6 +17,10 @@ pub enum RadrootsEventStoreError { EventHeadMalformed(RadrootsEventHeadMalformed), #[error("identifier parse error: {0}")] IdParse(#[from] RadrootsIdParseError), + #[error("event wire error: {0}")] + EventWire(#[from] RadrootsEventWireError), + #[error("signed event error: {0}")] + SignedEvent(#[from] RadrootsSignedEventError), #[error("transport contract error: {0}")] Transport(RadrootsTransportError), #[error("stored event `{0}` was not found")] @@ -40,6 +46,8 @@ pub enum RadrootsEventStoreError { }, #[error("integer value `{value}` is outside {field} range")] IntegerRange { field: &'static str, value: i64 }, + #[error("unsigned integer value `{value}` is outside {field} range")] + UnsignedIntegerRange { field: &'static str, value: u64 }, } impl From<RadrootsTransportError> for RadrootsEventStoreError { diff --git a/crates/event_store/src/model.rs b/crates/event_store/src/model.rs @@ -3,7 +3,9 @@ use radroots_event::RadrootsEventEnvelope; use radroots_event::contract::{ RadrootsContractMatchError, RadrootsEventClass, RadrootsTagSemantic, RadrootsTagValueType, }; +use radroots_event::draft::RadrootsSignedEvent; use radroots_event::event_head::RadrootsEventHeadDecision; +use radroots_event::wire::RadrootsNip01EventWire; use radroots_transport::{ RadrootsTransportKind, RadrootsTransportTargetFingerprint, RadrootsTransportTargetUri, }; @@ -215,31 +217,42 @@ impl RadrootsTransportObservation { #[derive(Clone, Debug, PartialEq, Eq)] pub struct RadrootsEventIngest { - pub event: RadrootsEventEnvelope, - pub raw_json: Option<String>, + pub signed_event: RadrootsSignedEvent, pub observed_at_ms: i64, pub transport_observation: Option<RadrootsTransportObservation>, } impl RadrootsEventIngest { - pub fn new(event: RadrootsEventEnvelope, observed_at_ms: i64) -> Self { + pub fn new(signed_event: RadrootsSignedEvent, observed_at_ms: i64) -> Self { Self { - event, - raw_json: None, + signed_event, observed_at_ms, transport_observation: None, } } - pub fn with_raw_json(mut self, raw_json: impl Into<String>) -> Self { - self.raw_json = Some(raw_json.into()); - self + pub fn from_raw_json( + raw_json: impl Into<String>, + observed_at_ms: i64, + ) -> Result<Self, RadrootsEventStoreError> { + let raw_json = raw_json.into(); + let wire = RadrootsNip01EventWire::parse_json(raw_json.as_str())?; + let signed_event = RadrootsSignedEvent::from_wire_verified_id(wire, raw_json)?; + Ok(Self::new(signed_event, observed_at_ms)) } pub fn with_observation(mut self, observation: RadrootsTransportObservation) -> Self { self.transport_observation = Some(observation); self } + + pub fn event(&self) -> &RadrootsEventEnvelope { + self.signed_event.envelope() + } + + pub fn raw_json(&self) -> &str { + self.signed_event.raw_json() + } } #[derive(Clone, Debug, PartialEq, Eq)] @@ -295,7 +308,7 @@ pub struct RadrootsStoredEvent { pub seq: i64, pub event_id: String, pub pubkey: String, - pub created_at: u32, + pub created_at: u64, pub kind: u32, pub tags_json: String, pub content: String, @@ -329,7 +342,7 @@ pub struct RadrootsStoredEventHead { pub pubkey: String, pub d_tag: Option<String>, pub event_id: String, - pub created_at: u32, + pub created_at: u64, pub updated_at_ms: i64, } diff --git a/crates/event_store/src/store.rs b/crates/event_store/src/store.rs @@ -16,7 +16,7 @@ use radroots_event::event_head::{ RadrootsEventHeadCoordinate, RadrootsEventHeadDecision, event_head_candidate_for_contract, select_event_head, }; -use radroots_event::ids::{RadrootsEventId, RadrootsEventSignature, RadrootsPublicKey}; +use radroots_event::ids::{RadrootsEventId, RadrootsPublicKey}; use radroots_nostr::prelude::{RadrootsNostrEventVerification, radroots_nostr_verify_event}; use radroots_transport::{ RadrootsTransportKind, RadrootsTransportTargetFingerprint, RadrootsTransportTargetUri, @@ -105,22 +105,20 @@ impl RadrootsEventStore { &self, ingest: RadrootsEventIngest, ) -> Result<RadrootsEventIngestReceipt, RadrootsEventStoreError> { - validate_event_identity(&ingest.event)?; - let verification_status = verify_event(&ingest.event); - let classification = classify_event(&ingest.event); - let raw_json = ingest - .raw_json - .clone() - .map(Ok) - .unwrap_or_else(|| serde_json::to_string(&ingest.event))?; - let tags_json = serde_json::to_string(&ingest.event.tags)?; + let event = ingest.event(); + validate_event_identity(event)?; + let verification_status = verify_event(event); + let classification = classify_event(event); + let tags = event.tags_as_vec(); + let tags_json = serde_json::to_string(&tags)?; + let event_id = event.id_str().to_owned(); let mut tx = self.pool.begin().await?; let insert = insert_raw_event( &mut tx, &ingest, &classification, verification_status, - raw_json.as_str(), + ingest.raw_json(), tags_json.as_str(), ) .await?; @@ -129,12 +127,11 @@ impl RadrootsEventStore { let mut projection_eligible = classification.base_projection_eligible(verification_status); if inserted { - insert_tags(&mut tx, &ingest.event, classification.contract).await?; + insert_tags(&mut tx, event, classification.contract).await?; if let Some(contract) = classification.contract { if projection_eligible { let head = - apply_event_head(&mut tx, &ingest.event, contract, ingest.observed_at_ms) - .await?; + apply_event_head(&mut tx, event, contract, ingest.observed_at_ms).await?; projection_eligible = head.projection_eligible; head_decision = head.decision; sqlx::query( @@ -142,7 +139,7 @@ impl RadrootsEventStore { ) .bind(bool_i64(projection_eligible)) .bind(ingest.observed_at_ms) - .bind(ingest.event.id.as_str()) + .bind(event_id.as_str()) .execute(&mut *tx) .await?; } else { @@ -155,14 +152,14 @@ impl RadrootsEventStore { } if let Some(observation) = ingest.transport_observation.as_ref() { - upsert_observation(&mut tx, ingest.event.id.as_str(), observation).await?; + upsert_observation(&mut tx, event_id.as_str(), observation).await?; } tx.commit().await?; Ok(RadrootsEventIngestReceipt { seq: insert.seq, - event_id: ingest.event.id, + event_id, inserted, verification_status, contract_status: classification.contract_status, @@ -450,14 +447,14 @@ async fn query_string(pool: &SqlitePool, sql: &str) -> Result<String, RadrootsEv } fn validate_event_identity(event: &RadrootsEventEnvelope) -> Result<(), RadrootsEventStoreError> { - RadrootsEventId::parse(event.id.as_str())?; - RadrootsPublicKey::parse(event.author.as_str())?; - RadrootsEventSignature::parse(event.sig.as_str())?; + RadrootsEventId::parse(event.id_str())?; + RadrootsPublicKey::parse(event.author_str())?; Ok(()) } fn classify_event(event: &RadrootsEventEnvelope) -> EventClassification { - match identify_event_contract(event.kind, &event.tags, &event.content) { + let tags = event.tags_as_vec(); + match identify_event_contract(event.kind_u32(), &tags, event.content()) { Ok(contract) => EventClassification { contract_status: RadrootsEventContractStatus::Supported, contract: Some(contract), @@ -497,7 +494,7 @@ async fn insert_raw_event( raw_json: &str, tags_json: &str, ) -> Result<InsertRawEventResult, RadrootsEventStoreError> { - let event = &ingest.event; + let event = ingest.event(); let contract_id = classification.contract.map(|contract| contract.id); let event_class = classification .contract @@ -506,13 +503,13 @@ async fn insert_raw_event( let result = sqlx::query( "INSERT OR IGNORE INTO event_envelopes(event_id, pubkey, created_at, kind, tags_json, content, sig, raw_json, verification_status, contract_status, contract_id, event_class, projection_eligible, inserted_at_ms, updated_at_ms) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", ) - .bind(event.id.as_str()) - .bind(event.author.as_str()) - .bind(i64::from(event.created_at)) - .bind(i64::from(event.kind)) + .bind(event.id_str()) + .bind(event.author_str()) + .bind(i64_from_u64("created_at", event.created_at_u64())?) + .bind(i64::from(event.kind_u32())) .bind(tags_json) - .bind(event.content.as_str()) - .bind(event.sig.as_str()) + .bind(event.content()) + .bind(event.sig_str()) .bind(raw_json) .bind(verification_status.as_str()) .bind(classification.contract_status.as_str()) @@ -524,7 +521,7 @@ async fn insert_raw_event( .execute(&mut **tx) .await?; let inserted = result.rows_affected() > 0; - let seq = event_seq(tx, event.id.as_str()).await?; + let seq = event_seq(tx, event.id_str()).await?; Ok(InsertRawEventResult { inserted, seq }) } @@ -546,10 +543,11 @@ async fn insert_tags( event: &RadrootsEventEnvelope, contract: Option<&'static RadrootsEventContract>, ) -> Result<(), RadrootsEventStoreError> { - for (index, tag) in event.tags.iter().enumerate() { - let tag_name = tag.first().map(String::as_str).unwrap_or(""); - let tag_value = tag.get(1).map(String::as_str); - let tag_json = serde_json::to_string(tag)?; + for (index, tag) in event.tag_slices().iter().enumerate() { + let tag_values = tag.as_slice(); + let tag_name = tag_values.first().map(String::as_str).unwrap_or(""); + let tag_value = tag_values.get(1).map(String::as_str); + let tag_json = serde_json::to_string(tag_values)?; let tag_contract = contract.and_then(|contract| { contract .tags @@ -562,7 +560,7 @@ async fn insert_tags( sqlx::query( "INSERT INTO event_envelope_tags(event_id, tag_index, tag_name, tag_value, tag_json, contract_semantic, contract_value_type, relay_indexed) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", ) - .bind(event.id.as_str()) + .bind(event.id_str()) .bind(i64::try_from(index).map_err(|_| RadrootsEventStoreError::IntegerRange { field: "tag_index", value: i64::MAX, @@ -675,7 +673,7 @@ async fn current_event_head( Ok(RadrootsCurrentEventHead { coordinate: coordinate.clone(), event_id: RadrootsEventId::parse(event_id)?, - created_at: u32_from_i64("created_at", created_at)?, + created_at: u64_from_i64("created_at", created_at)?, }) }) .transpose() @@ -703,7 +701,7 @@ async fn upsert_head( .bind(i64::from(*kind)) .bind(pubkey.as_str()) .bind(candidate.event_id.as_str()) - .bind(i64::from(candidate.created_at)) + .bind(i64_from_u64("created_at", candidate.created_at)?) .bind(updated_at_ms) .execute(&mut **tx) .await?; @@ -728,7 +726,7 @@ async fn upsert_head( .bind(pubkey.as_str()) .bind(d_tag.as_str()) .bind(candidate.event_id.as_str()) - .bind(i64::from(candidate.created_at)) + .bind(i64_from_u64("created_at", candidate.created_at)?) .bind(updated_at_ms) .execute(&mut **tx) .await?; @@ -742,7 +740,7 @@ fn stored_event_from_row( row: sqlx::sqlite::SqliteRow, ) -> Result<RadrootsStoredEvent, RadrootsEventStoreError> { let kind = u32_from_i64("kind", row.try_get("kind")?)?; - let created_at = u32_from_i64("created_at", row.try_get("created_at")?)?; + let created_at = u64_from_i64("created_at", row.try_get("created_at")?)?; let verification_status = RadrootsEventVerificationStatus::parse(row.try_get("verification_status")?)?; let contract_status = @@ -798,7 +796,7 @@ fn stored_head_from_row( pubkey: row.try_get("pubkey")?, d_tag: row.try_get("d_tag")?, event_id: row.try_get("event_id")?, - created_at: u32_from_i64("created_at", row.try_get("created_at")?)?, + created_at: u64_from_i64("created_at", row.try_get("created_at")?)?, updated_at_ms: row.try_get("updated_at_ms")?, }) } @@ -859,6 +857,16 @@ fn u32_from_i64(field: &'static str, value: i64) -> Result<u32, RadrootsEventSto u32::try_from(value).map_err(|_| RadrootsEventStoreError::IntegerRange { field, value }) } +#[cfg_attr(coverage_nightly, coverage(off))] +fn u64_from_i64(field: &'static str, value: i64) -> Result<u64, RadrootsEventStoreError> { + u64::try_from(value).map_err(|_| RadrootsEventStoreError::IntegerRange { field, value }) +} + +#[cfg_attr(coverage_nightly, coverage(off))] +fn i64_from_u64(field: &'static str, value: u64) -> Result<i64, RadrootsEventStoreError> { + i64::try_from(value).map_err(|_| RadrootsEventStoreError::UnsignedIntegerRange { field, value }) +} + fn bool_i64(value: bool) -> i64 { if value { 1 } else { 0 } } @@ -900,13 +908,15 @@ where #[cfg(test)] mod tests { use super::*; + use radroots_event::draft::RadrootsSignedEvent; use radroots_event::event_head::event_head_candidate_for_event; use radroots_event::kinds::{ KIND_GEOCHAT, KIND_LISTING, KIND_ORDER_REQUEST, KIND_POST, KIND_PROFILE, }; + use radroots_event::wire::{RadrootsNip01EventWire, compute_canonical_nip01_event_id}; use radroots_nostr::prelude::{ RadrootsNostrKeys, RadrootsNostrSecretKey, RadrootsNostrTimestamp, - radroots_event_from_nostr, radroots_nostr_build_event, + radroots_nostr_build_event, }; const FIXTURE_ALICE_SECRET_KEY_HEX: &str = @@ -929,27 +939,72 @@ mod tests { created_at: u32, tags: Vec<Vec<String>>, content: &str, - ) -> RadrootsEventEnvelope { + ) -> RadrootsSignedEvent { let raw_event = radroots_nostr_build_event(kind, content, tags) .expect("builder") .custom_created_at(RadrootsNostrTimestamp::from_secs(u64::from(created_at))) .sign_with_keys(&fixture_keys()) .expect("signed event"); - radroots_event_from_nostr(&raw_event) + signed_event_from_raw_json(serde_json::to_string(&raw_event).expect("raw json")) + } + + fn signed_event_from_raw_json(raw_json: String) -> RadrootsSignedEvent { + let wire = RadrootsNip01EventWire::parse_json(raw_json.as_str()).expect("wire"); + RadrootsSignedEvent::from_wire_verified_id(wire, raw_json).expect("signed event") + } + + fn synthetic_signed_event( + kind: u32, + created_at: u64, + tags: Vec<Vec<String>>, + content: &str, + ) -> RadrootsSignedEvent { + let pubkey = FIXTURE_ALICE_PUBLIC_KEY_HEX.to_owned(); + let content = content.to_owned(); + let id = compute_canonical_nip01_event_id( + pubkey.as_str(), + created_at, + kind, + &tags, + content.as_str(), + ) + .expect("event id") + .into_string(); + let wire = RadrootsNip01EventWire { + id, + pubkey, + created_at, + kind, + tags, + content, + sig: event_id('f').repeat(2), + extra: Default::default(), + }; + let raw_json = serde_json::to_string(&wire).expect("raw json"); + RadrootsSignedEvent::from_wire_verified_id(wire, raw_json).expect("signed event") } - fn tamper_signature(event: &mut RadrootsEventEnvelope) { - let replacement = if event.sig.starts_with('0') { "1" } else { "0" }; - event.sig.replace_range(0..1, replacement); + fn tamper_signature(event: &RadrootsSignedEvent) -> RadrootsSignedEvent { + let mut wire = event.wire().clone(); + let replacement = if wire.sig.starts_with('0') { "1" } else { "0" }; + wire.sig.replace_range(0..1, replacement); + let raw_json = serde_json::to_string(&wire).expect("raw json"); + RadrootsSignedEvent::from_wire_verified_id(wire, raw_json).expect("signed event") + } + + fn tampered_content_raw_json(event: &RadrootsSignedEvent, content: &str) -> String { + let mut wire = event.wire().clone(); + wire.content = content.to_owned(); + serde_json::to_string(&wire).expect("raw json") } fn listing_tags(d_tag: &str) -> Vec<Vec<String>> { vec![vec!["d".to_owned(), d_tag.to_owned()]] } - fn head_coordinate_for_event(event: &RadrootsEventEnvelope) -> RadrootsEventHeadCoordinate { + fn head_coordinate_for_event(event: &RadrootsSignedEvent) -> RadrootsEventHeadCoordinate { let RadrootsEventHeadCandidateResult::Candidate(candidate) = - event_head_candidate_for_event(event).expect("head candidate") + event_head_candidate_for_event(event.envelope()).expect("head candidate") else { panic!("event should select a head"); }; @@ -1153,8 +1208,7 @@ mod tests { vec![vec!["t".to_owned(), "soil".to_owned()]], "hello", ); - let ingest = - RadrootsEventIngest::new(event.clone(), 1_000).with_raw_json("{\"fixture\":true}"); + let ingest = RadrootsEventIngest::new(event.clone(), 1_000); let first = store .ingest_event(ingest.clone()) @@ -1162,7 +1216,7 @@ mod tests { .expect("first ingest"); let second = store.ingest_event(ingest).await.expect("second ingest"); let stored = store - .get_event(event.id.as_str()) + .get_event(event.id_str()) .await .expect("get") .expect("stored"); @@ -1179,7 +1233,7 @@ mod tests { RadrootsEventVerificationStatus::Verified ); assert_eq!(stored.seq, first.seq); - assert_eq!(stored.raw_json, "{\"fixture\":true}"); + assert_eq!(stored.raw_json, event.raw_json()); assert_eq!(stored.content, "hello"); assert_eq!(stored.tags_json, "[[\"t\",\"soil\"]]"); assert_eq!( @@ -1189,7 +1243,7 @@ mod tests { assert!(stored.projection_eligible); assert_eq!( store - .tags_for_event(event.id.as_str()) + .tags_for_event(event.id_str()) .await .expect("tags") .len(), @@ -1198,6 +1252,17 @@ mod tests { } #[tokio::test] + async fn wrapper_json_is_rejected_as_event_authority() { + let event = signed_event(KIND_POST, 10, Vec::new(), "hello"); + let wrapper_json = serde_json::to_string(&event).expect("wrapper json"); + + let error = RadrootsEventIngest::from_raw_json(wrapper_json, 1_000) + .expect_err("wrapper json should not parse as event wire"); + + assert!(matches!(error, RadrootsEventStoreError::EventWire(_))); + } + + #[tokio::test] async fn unsupported_verified_events_are_stored_but_not_projected() { let store = RadrootsEventStore::open_memory().await.expect("open"); let event = signed_event(999, 11, Vec::new(), "unsupported"); @@ -1206,7 +1271,7 @@ mod tests { .await .expect("ingest"); let stored = store - .get_event(event.id.as_str()) + .get_event(event.id_str()) .await .expect("get") .expect("stored"); @@ -1234,15 +1299,15 @@ mod tests { #[test] fn test_helpers_cover_signature_and_non_head_branches() { - let mut zero_sig = signed_event(KIND_POST, 12, Vec::new(), "zero"); - zero_sig.sig.replace_range(0..1, "0"); - tamper_signature(&mut zero_sig); - assert!(zero_sig.sig.starts_with('1')); - - let mut nonzero_sig = signed_event(KIND_POST, 12, Vec::new(), "nonzero"); - nonzero_sig.sig.replace_range(0..1, "1"); - tamper_signature(&mut nonzero_sig); - assert!(nonzero_sig.sig.starts_with('0')); + let zero_sig = synthetic_signed_event(KIND_POST, 12, Vec::new(), "zero"); + let zero_sig = tamper_signature(&zero_sig); + assert!(zero_sig.sig_str().starts_with('0')); + + let nonzero_sig = tamper_signature(&signed_event(KIND_POST, 12, Vec::new(), "nonzero")); + assert_ne!( + nonzero_sig.sig_str(), + signed_event(KIND_POST, 12, Vec::new(), "nonzero").sig_str() + ); } #[test] @@ -1253,33 +1318,14 @@ mod tests { } #[tokio::test] - async fn id_mismatch_events_are_stored_but_not_projected() { + async fn id_mismatch_raw_json_is_rejected_before_storage() { let store = RadrootsEventStore::open_memory().await.expect("open"); - let mut event = signed_event(KIND_POST, 12, Vec::new(), "hello"); - event.content = "tampered".to_owned(); - let receipt = store - .ingest_event(RadrootsEventIngest::new(event.clone(), 2_100)) - .await - .expect("ingest"); - let stored = store - .get_event(event.id.as_str()) - .await - .expect("get") - .expect("stored"); + let event = signed_event(KIND_POST, 12, Vec::new(), "hello"); + let raw_json = tampered_content_raw_json(&event, "tampered"); - assert_eq!( - receipt.contract_status, - RadrootsEventContractStatus::Supported - ); - assert_eq!( - receipt.verification_status, - RadrootsEventVerificationStatus::IdMismatch - ); - assert_eq!( - stored.verification_status, - RadrootsEventVerificationStatus::IdMismatch - ); - assert!(!stored.projection_eligible); + let error = RadrootsEventIngest::from_raw_json(raw_json, 2_100).expect_err("id mismatch"); + + assert!(matches!(error, RadrootsEventStoreError::EventWire(_))); assert!( store .events_since_cursor("social", 10) @@ -1292,14 +1338,13 @@ mod tests { #[tokio::test] async fn signature_invalid_events_are_stored_but_not_projected() { let store = RadrootsEventStore::open_memory().await.expect("open"); - let mut event = signed_event(KIND_POST, 13, Vec::new(), "hello"); - tamper_signature(&mut event); + let event = tamper_signature(&signed_event(KIND_POST, 13, Vec::new(), "hello")); let receipt = store .ingest_event(RadrootsEventIngest::new(event.clone(), 2_200)) .await .expect("ingest"); let stored = store - .get_event(event.id.as_str()) + .get_event(event.id_str()) .await .expect("get") .expect("stored"); @@ -1325,15 +1370,14 @@ mod tests { #[tokio::test] async fn malformed_envelope_events_are_stored_but_not_projected() { let store = RadrootsEventStore::open_memory().await.expect("open"); - let mut event = signed_event(KIND_POST, 13, Vec::new(), "hello"); - event.kind = u32::from(u16::MAX) + 1; + let event = synthetic_signed_event(u32::from(u16::MAX) + 1, 13, Vec::new(), "hello"); let receipt = store .ingest_event(RadrootsEventIngest::new(event.clone(), 2_250)) .await .expect("ingest"); let stored = store - .get_event(event.id.as_str()) + .get_event(event.id_str()) .await .expect("get") .expect("stored"); @@ -1359,7 +1403,7 @@ mod tests { .await .expect("ingest"); let stored = store - .get_event(event.id.as_str()) + .get_event(event.id_str()) .await .expect("get") .expect("stored"); @@ -1380,11 +1424,11 @@ mod tests { async fn event_head_helper_maps_not_persisted_candidates() { let store = RadrootsEventStore::open_memory().await.expect("open"); let event = signed_event(KIND_GEOCHAT, 17, Vec::new(), "hello"); - let classification = classify_event(&event); + let classification = classify_event(event.envelope()); let contract = classification.contract.expect("contract"); let mut tx = store.pool.begin().await.expect("tx"); - let head = apply_event_head(&mut tx, &event, contract, 2_280) + let head = apply_event_head(&mut tx, event.envelope(), contract, 2_280) .await .expect("head"); @@ -1402,7 +1446,7 @@ mod tests { .await .expect("ingest"); let stored = store - .get_event(event.id.as_str()) + .get_event(event.id_str()) .await .expect("get") .expect("stored"); @@ -1420,7 +1464,7 @@ mod tests { } #[tokio::test] - async fn id_mismatch_addressable_events_do_not_update_heads() { + async fn id_mismatch_addressable_raw_json_does_not_update_heads() { let store = RadrootsEventStore::open_memory().await.expect("open"); let original = signed_event(KIND_LISTING, 17, listing_tags("listing-1"), "{}"); let first = store @@ -1428,18 +1472,9 @@ mod tests { .await .expect("first"); let coordinate = head_coordinate_for_event(&original); - let mut invalid = signed_event(KIND_LISTING, 18, listing_tags("listing-1"), "{}"); - invalid.content = "{\"tampered\":true}".to_owned(); - - let receipt = store - .ingest_event(RadrootsEventIngest::new(invalid.clone(), 2_400)) - .await - .expect("invalid"); - let stored = store - .get_event(invalid.id.as_str()) - .await - .expect("get") - .expect("stored"); + let invalid = signed_event(KIND_LISTING, 18, listing_tags("listing-1"), "{}"); + let raw_json = tampered_content_raw_json(&invalid, "{\"tampered\":true}"); + let error = RadrootsEventIngest::from_raw_json(raw_json, 2_400).expect_err("id mismatch"); let head = store .event_head(&coordinate) .await @@ -1447,17 +1482,8 @@ mod tests { .expect("stored head"); assert_eq!(first.head_decision, RadrootsEventHeadStoreDecision::Applied); - assert_eq!( - receipt.verification_status, - RadrootsEventVerificationStatus::IdMismatch - ); - assert_eq!( - receipt.head_decision, - RadrootsEventHeadStoreDecision::NotProjectionEligible - ); - assert!(!receipt.projection_eligible); - assert!(!stored.projection_eligible); - assert_eq!(head.event_id, original.id); + assert!(matches!(error, RadrootsEventStoreError::EventWire(_))); + assert_eq!(head.event_id, original.id_str()); } #[tokio::test] @@ -1469,8 +1495,12 @@ mod tests { .await .expect("first"); let coordinate = head_coordinate_for_event(&original); - let mut invalid = signed_event(KIND_LISTING, 20, listing_tags("listing-2"), "{}"); - tamper_signature(&mut invalid); + let invalid = tamper_signature(&signed_event( + KIND_LISTING, + 20, + listing_tags("listing-2"), + "{}", + )); let receipt = store .ingest_event(RadrootsEventIngest::new(invalid.clone(), 2_600)) @@ -1491,7 +1521,7 @@ mod tests { RadrootsEventHeadStoreDecision::NotProjectionEligible ); assert!(!receipt.projection_eligible); - assert_eq!(head.event_id, original.id); + assert_eq!(head.event_id, original.id_str()); } #[tokio::test] @@ -1503,8 +1533,12 @@ mod tests { .await .expect("original"); let coordinate = head_coordinate_for_event(&original); - let mut invalid = signed_event(KIND_LISTING, 22, listing_tags("listing-3"), "{}"); - invalid.content = "{\"tampered\":true}".to_owned(); + let invalid = tamper_signature(&signed_event( + KIND_LISTING, + 22, + listing_tags("listing-3"), + "{}", + )); let first_invalid = store .ingest_event(RadrootsEventIngest::new(invalid.clone(), 2_800)) @@ -1531,7 +1565,7 @@ mod tests { second_invalid.head_decision, RadrootsEventHeadStoreDecision::SkippedDuplicate ); - assert_eq!(head.event_id, original.id); + assert_eq!(head.event_id, original.id_str()); } #[tokio::test] @@ -1562,7 +1596,7 @@ mod tests { second.head_decision, RadrootsEventHeadStoreDecision::SkippedDuplicate ); - assert_eq!(head.event_id, event.id); + assert_eq!(head.event_id, event.id_str()); } #[tokio::test] @@ -1575,7 +1609,7 @@ mod tests { .await .expect("ingest"); let stored = store - .get_event(event.id.as_str()) + .get_event(event.id_str()) .await .expect("get") .expect("stored"); @@ -1651,8 +1685,8 @@ mod tests { .await .expect("tag query"); assert_eq!(events.len(), 2); - assert_eq!(events[0].event_id, high_created_at.id); - assert_eq!(events[1].event_id, low_created_at.id); + assert_eq!(events[0].event_id, high_created_at.id_str()); + assert_eq!(events[1].event_id, low_created_at.id_str()); assert!(events.iter().all(|event| event.projection_eligible)); let limited = store @@ -1660,7 +1694,7 @@ mod tests { .await .expect("limited tag query"); assert_eq!(limited.len(), 1); - assert_eq!(limited[0].event_id, high_created_at.id); + assert_eq!(limited[0].event_id, high_created_at.id_str()); } #[tokio::test] @@ -1746,7 +1780,7 @@ mod tests { .await .expect("contract tag query"); assert_eq!(events.len(), 1); - assert_eq!(events[0].event_id, matching_order.id); + assert_eq!(events[0].event_id, matching_order.id_str()); assert_eq!( events[0].contract_id.as_deref(), Some("radroots.order.request.v1") @@ -1771,7 +1805,7 @@ mod tests { .ingest_event(RadrootsEventIngest::new(event.clone(), 3_000)) .await .expect("ingest"); - let tags = store.tags_for_event(event.id.as_str()).await.expect("tags"); + let tags = store.tags_for_event(event.id_str()).await.expect("tags"); assert_eq!(tags[0].tag_index, 0); assert_eq!(tags[0].tag_name, "p"); @@ -1811,7 +1845,7 @@ mod tests { .ingest_event(RadrootsEventIngest::new(event.clone(), 3_100)) .await .expect("ingest"); - let tags = store.tags_for_event(event.id.as_str()).await.expect("tags"); + let tags = store.tags_for_event(event.id_str()).await.expect("tags"); let listing_tag = tags .iter() .find(|tag| tag.tag_name == "listing_event") @@ -1864,7 +1898,7 @@ mod tests { store.ingest_event(ingest).await.expect("older duplicate"); let observations = store - .observations_for_event(event.id.as_str()) + .observations_for_event(event.id_str()) .await .expect("stale duplicate observations"); assert_eq!(observations.len(), 1); @@ -1912,7 +1946,7 @@ mod tests { .expect("newer duplicate without message"); let observations = store - .observations_for_event(event.id.as_str()) + .observations_for_event(event.id_str()) .await .expect("observations"); assert_eq!(observations.len(), 1); @@ -1943,7 +1977,7 @@ mod tests { signed_event(KIND_PROFILE, 20, Vec::new(), "{\"name\":\"a\"}"), signed_event(KIND_PROFILE, 20, Vec::new(), "{\"name\":\"b\"}"), ]; - events.sort_by(|left, right| left.id.cmp(&right.id)); + events.sort_by(|left, right| left.id_str().cmp(right.id_str())); let lower = events[0].clone(); let higher = events[1].clone(); @@ -1967,7 +2001,7 @@ mod tests { second.head_decision, RadrootsEventHeadStoreDecision::Applied ); - assert_eq!(head.event_id, lower.id); + assert_eq!(head.event_id, lower.id_str()); let store = RadrootsEventStore::open_memory().await.expect("open"); store @@ -1988,7 +2022,7 @@ mod tests { second.head_decision, RadrootsEventHeadStoreDecision::SkippedSameTimestampHigherEventId ); - assert_eq!(head.event_id, lower.id); + assert_eq!(head.event_id, lower.id_str()); } #[tokio::test] @@ -2011,8 +2045,8 @@ mod tests { .await .expect("initial replay"); assert_eq!(replay.len(), 2); - assert_eq!(replay[0].event_id, first.id); - assert_eq!(replay[1].event_id, second.id); + assert_eq!(replay[0].event_id, first.id_str()); + assert_eq!(replay[1].event_id, second.id_str()); store .update_projection_cursor(&RadrootsProjectionCursor { projection_id: "social".to_owned(), @@ -2027,7 +2061,7 @@ mod tests { .await .expect("next replay"); assert_eq!(replay.len(), 1); - assert_eq!(replay[0].event_id, second.id); + assert_eq!(replay[0].event_id, second.id_str()); } #[tokio::test] diff --git a/crates/nostr/src/codec_adapters.rs b/crates/nostr/src/codec_adapters.rs @@ -3,7 +3,6 @@ use alloc::{string::String, vec::Vec}; use crate::types::RadrootsNostrEvent; -use crate::util::created_at_u32_saturating; use radroots_event::{ job_feedback::RadrootsJobFeedback, job_request::RadrootsJobRequest, job_result::RadrootsJobResult, @@ -22,8 +21,8 @@ fn author(e: &RadrootsNostrEvent) -> String { e.pubkey.to_hex() } -fn published_at(e: &RadrootsNostrEvent) -> u32 { - created_at_u32_saturating(e.created_at) +fn published_at(e: &RadrootsNostrEvent) -> u64 { + e.created_at.as_secs() } fn kind_u32(e: &RadrootsNostrEvent) -> u32 { diff --git a/crates/nostr/src/draft_signing.rs b/crates/nostr/src/draft_signing.rs @@ -1,39 +1,42 @@ #![forbid(unsafe_code)] use crate::error::RadrootsNostrError; -use crate::event_convert::radroots_event_from_nostr; use crate::events::radroots_nostr_build_event; use crate::types::{RadrootsNostrKeys, RadrootsNostrTimestamp}; use nostr::JsonUtil; use radroots_event::draft::{RadrootsEventDraft, RadrootsSignedEvent}; +use radroots_event::wire::RadrootsNip01EventWire; pub fn radroots_nostr_sign_frozen_draft( keys: &RadrootsNostrKeys, draft: &RadrootsEventDraft, ) -> Result<RadrootsSignedEvent, RadrootsNostrError> { let actual_pubkey = keys.public_key().to_hex(); - if actual_pubkey != draft.expected_pubkey { + if actual_pubkey != draft.expected_pubkey_str() { return Err(RadrootsNostrError::FrozenDraftPubkeyMismatch { - expected_pubkey: draft.expected_pubkey.clone(), + expected_pubkey: draft.expected_pubkey_str().to_owned(), actual_pubkey, }); } - let event = radroots_nostr_build_event(draft.kind, draft.content.clone(), draft.tags.clone())? - .custom_created_at(RadrootsNostrTimestamp::from_secs(u64::from( - draft.created_at, - ))) - .sign_with_keys(keys)?; + let event = radroots_nostr_build_event( + draft.kind_u32(), + draft.content().to_owned(), + draft.tags_as_vec(), + )? + .custom_created_at(RadrootsNostrTimestamp::from_secs(draft.created_at_u64())) + .sign_with_keys(keys)?; let actual_event_id = event.id.to_hex(); - if actual_event_id != draft.expected_event_id { + if actual_event_id != draft.expected_event_id_str() { return Err(RadrootsNostrError::FrozenDraftEventIdMismatch { - expected_event_id: draft.expected_event_id.clone(), + expected_event_id: draft.expected_event_id_str().to_owned(), actual_event_id, }); } let raw_json = event.as_json(); - RadrootsSignedEvent::from_event(radroots_event_from_nostr(&event), raw_json).map_err(Into::into) + let wire = RadrootsNip01EventWire::parse_json(raw_json.as_str())?; + RadrootsSignedEvent::from_wire_verified_id(wire, raw_json).map_err(Into::into) } #[cfg(test)] @@ -69,17 +72,17 @@ mod tests { let draft = post_draft(FIXTURE_ALICE.public_key_hex); let signed = radroots_nostr_sign_frozen_draft(&keys, &draft).expect("signed event"); - assert_eq!(signed.id, draft.expected_event_id); - assert_eq!(signed.pubkey, draft.expected_pubkey); - assert_eq!(signed.created_at, draft.created_at); - assert_eq!(signed.kind, draft.kind); - assert_eq!(signed.tags, draft.tags); - assert_eq!(signed.content, draft.content); + assert_eq!(signed.id_str(), draft.expected_event_id_str()); + assert_eq!(signed.pubkey_str(), draft.expected_pubkey_str()); + assert_eq!(signed.created_at(), draft.created_at_u64()); + assert_eq!(signed.kind(), draft.kind_u32()); + assert_eq!(signed.tags_as_vec(), draft.tags_as_vec()); + assert_eq!(signed.content(), draft.content()); - let raw_event = crate::types::RadrootsNostrEvent::from_json(signed.raw_json.as_str()) - .expect("raw json"); - assert_eq!(raw_event.id.to_hex(), signed.id); - assert_eq!(raw_event.created_at.as_secs(), u64::from(draft.created_at)); + let raw_event = + crate::types::RadrootsNostrEvent::from_json(signed.raw_json()).expect("raw json"); + assert_eq!(raw_event.id.to_hex(), signed.id_str()); + assert_eq!(raw_event.created_at.as_secs(), draft.created_at_u64()); } #[test] @@ -97,8 +100,10 @@ mod tests { #[test] fn sign_frozen_draft_rejects_event_id_mismatch() { let keys = fixture_keys(FIXTURE_ALICE.secret_key_hex); - let mut draft = post_draft(FIXTURE_ALICE.public_key_hex); - draft.expected_event_id = "f".repeat(64); + let draft = post_draft(FIXTURE_ALICE.public_key_hex); + let mut raw = serde_json::to_value(&draft).expect("draft json"); + raw["expected_event_id"] = serde_json::Value::String("f".repeat(64)); + let draft: RadrootsEventDraft = serde_json::from_value(raw).expect("draft"); let error = radroots_nostr_sign_frozen_draft(&keys, &draft).expect_err("id mismatch"); assert!(matches!( diff --git a/crates/nostr/src/error.rs b/crates/nostr/src/error.rs @@ -28,6 +28,14 @@ pub enum RadrootsNostrError { DraftError(#[from] radroots_event::draft::RadrootsDraftError), #[cfg(feature = "events")] + #[error("Event wire error: {0}")] + EventWire(#[from] radroots_event::wire::RadrootsEventWireError), + + #[cfg(feature = "events")] + #[error("Signed event error: {0}")] + SignedEvent(#[from] radroots_event::draft::RadrootsSignedEventError), + + #[cfg(feature = "events")] #[error( "Frozen draft signer public key mismatch: expected {expected_pubkey}, got {actual_pubkey}" )] diff --git a/crates/nostr/src/event_adapters.rs b/crates/nostr/src/event_adapters.rs @@ -13,14 +13,11 @@ use radroots_event_codec::profile::RadrootsProfileData; use crate::types::{RadrootsNostrEvent, RadrootsNostrMetadata}; #[cfg(feature = "events")] -use crate::util::created_at_u32_saturating; - -#[cfg(feature = "events")] pub fn to_post_event_metadata(e: &RadrootsNostrEvent) -> RadrootsParsedData<RadrootsPost> { RadrootsParsedData::new( e.id.to_string(), e.pubkey.to_string(), - created_at_u32_saturating(e.created_at), + e.created_at.as_secs(), e.kind.as_u16() as u32, RadrootsPost { content: e.content.clone(), @@ -56,7 +53,7 @@ pub fn to_profile_event_metadata( return Some(RadrootsParsedData::new( e.id.to_string(), e.pubkey.to_string(), - created_at_u32_saturating(e.created_at), + e.created_at.as_secs(), e.kind.as_u16() as u32, RadrootsProfileData { profile_type, @@ -81,7 +78,7 @@ pub fn to_profile_event_metadata( return Some(RadrootsParsedData::new( e.id.to_string(), e.pubkey.to_string(), - created_at_u32_saturating(e.created_at), + e.created_at.as_secs(), e.kind.as_u16() as u32, RadrootsProfileData { profile_type, diff --git a/crates/nostr/src/event_convert.rs b/crates/nostr/src/event_convert.rs @@ -1,20 +1,19 @@ #![forbid(unsafe_code)] use crate::types::RadrootsNostrEvent as RadrootsNostrRawEvent; -use radroots_event::{RadrootsEventEnvelope, RadrootsEventPtr}; - -use crate::util::event_created_at_u32_saturating; +use radroots_event::{RadrootsEventEnvelope, RadrootsEventEnvelopeParts, RadrootsEventPtr}; pub fn radroots_event_from_nostr(event: &RadrootsNostrRawEvent) -> RadrootsEventEnvelope { - RadrootsEventEnvelope { + RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts { id: event.id.to_string(), author: event.pubkey.to_string(), - created_at: event_created_at_u32_saturating(event), + created_at: event.created_at.as_secs(), kind: event.kind.as_u16() as u32, tags: event.tags.iter().map(|t| t.as_slice().to_vec()).collect(), content: event.content.clone(), sig: event.sig.to_string(), - } + }) + .expect("nostr event is canonical") } pub fn radroots_event_ptr_from_nostr(event: &RadrootsNostrRawEvent) -> RadrootsEventPtr { diff --git a/crates/nostr/src/event_verify.rs b/crates/nostr/src/event_verify.rs @@ -48,21 +48,21 @@ pub fn radroots_nostr_verify_event_id( } fn raw_event_from_radroots(event: &RadrootsEventEnvelope) -> Option<RadrootsNostrRawEvent> { - let id = RadrootsNostrEventId::from_hex(event.id.as_str()).ok()?; - let public_key = RadrootsNostrPublicKey::from_hex(event.author.as_str()).ok()?; - let kind_u16 = u16::try_from(event.kind).ok()?; - let mut tags = Vec::with_capacity(event.tags.len()); - for tag in event.tags.iter().cloned() { - tags.push(RadrootsNostrTag::parse(tag).ok()?); + let id = RadrootsNostrEventId::from_hex(event.id_str()).ok()?; + let public_key = RadrootsNostrPublicKey::from_hex(event.author_str()).ok()?; + let kind_u16 = u16::try_from(event.kind_u32()).ok()?; + let mut tags = Vec::with_capacity(event.tag_slices().len()); + for tag in event.tag_slices() { + tags.push(RadrootsNostrTag::parse(tag.as_slice().to_vec()).ok()?); } - let sig = Signature::from_str(event.sig.as_str()).ok()?; + let sig = Signature::from_str(event.sig_str()).ok()?; Some(RadrootsNostrRawEvent::new( id, public_key, - RadrootsNostrTimestamp::from_secs(u64::from(event.created_at)), + RadrootsNostrTimestamp::from_secs(event.created_at_u64()), RadrootsNostrKind::Custom(kind_u16), tags, - event.content.clone(), + event.content().to_owned(), sig, )) } @@ -74,7 +74,7 @@ mod tests { use crate::events::radroots_nostr_build_event; use crate::test_fixtures::FIXTURE_ALICE; use crate::types::{RadrootsNostrKeys, RadrootsNostrSecretKey}; - use radroots_event::kinds::KIND_POST; + use radroots_event::{RadrootsEventEnvelopeParts, kinds::KIND_POST}; fn fixture_keys() -> RadrootsNostrKeys { let secret_key = @@ -95,6 +95,24 @@ mod tests { radroots_event_from_nostr(&raw_event) } + fn envelope_with( + event: &RadrootsEventEnvelope, + content: String, + kind: u32, + sig: String, + ) -> RadrootsEventEnvelope { + RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts { + id: event.id_str().to_owned(), + author: event.author_str().to_owned(), + created_at: event.created_at_u64(), + kind, + tags: event.tags_as_vec(), + content, + sig, + }) + .expect("envelope") + } + #[test] fn verifies_signed_event_id_and_signature() { let event = signed_event(); @@ -111,8 +129,13 @@ mod tests { #[test] fn reports_id_mismatch_before_signature_checks() { - let mut event = signed_event(); - event.content = "tampered".to_owned(); + let original = signed_event(); + let event = envelope_with( + &original, + "tampered".to_owned(), + original.kind_u32(), + original.sig_str().to_owned(), + ); assert_eq!( radroots_nostr_verify_event(&event), @@ -122,9 +145,16 @@ mod tests { #[test] fn reports_signature_invalid_for_valid_id_with_wrong_signature() { - let mut event = signed_event(); - let replacement = if event.sig.starts_with('0') { "1" } else { "0" }; - event.sig.replace_range(0..1, replacement); + let original = signed_event(); + let mut sig = original.sig_str().to_owned(); + let replacement = if sig.starts_with('0') { "1" } else { "0" }; + sig.replace_range(0..1, replacement); + let event = envelope_with( + &original, + original.content().to_owned(), + original.kind_u32(), + sig, + ); assert_eq!( radroots_nostr_verify_event(&event), @@ -134,8 +164,13 @@ mod tests { #[test] fn reports_malformed_envelope_for_unparseable_wire_fields() { - let mut event = signed_event(); - event.kind = u32::from(u16::MAX) + 1; + let original = signed_event(); + let event = envelope_with( + &original, + original.content().to_owned(), + u32::from(u16::MAX) + 1, + original.sig_str().to_owned(), + ); assert_eq!( radroots_nostr_verify_event(&event), diff --git a/crates/nostr/src/job_adapter.rs b/crates/nostr/src/job_adapter.rs @@ -60,8 +60,8 @@ impl JobEventLike for RadrootsNostrEventAdapter<'_> { fn raw_author(&self) -> String { self.author_hex.clone() } - fn raw_published_at(&self) -> u32 { - self.evt.created_at.as_secs() as u32 + fn raw_published_at(&self) -> u64 { + self.evt.created_at.as_secs() } fn raw_kind(&self) -> u32 { match self.evt.kind { diff --git a/crates/nostr/src/nip17.rs b/crates/nostr/src/nip17.rs @@ -13,15 +13,13 @@ use thiserror::Error; use radroots_event::kinds::{KIND_MESSAGE, KIND_MESSAGE_FILE}; use radroots_event::message::RadrootsMessage; use radroots_event::message_file::RadrootsMessageFile; +use radroots_event::wire::RadrootsNip01EventWireParts; use radroots_event_codec::error::{EventEncodeError, EventParseError}; use radroots_event_codec::message::decode as message_decode; use radroots_event_codec::message::encode as message_encode; use radroots_event_codec::message_file::decode as message_file_decode; use radroots_event_codec::message_file::encode as message_file_encode; use radroots_event_codec::parsed::RadrootsParsedData; -use radroots_event_codec::wire::WireEventParts; - -use crate::util::created_at_u32_saturating; #[derive(Debug, Error)] pub enum RadrootsNip17Error { @@ -50,7 +48,7 @@ pub enum RadrootsNip17Rumor { #[derive(Clone, Debug)] pub struct RadrootsNip17WrapOptions { pub include_sender: bool, - pub rumor_created_at: Option<u32>, + pub rumor_created_at: Option<u64>, pub gift_wrap_tags: Vec<Vec<String>>, } @@ -78,13 +76,13 @@ fn tags_from_slices(tag_slices: &[Vec<String>]) -> Vec<Tag> { } fn rumor_from_parts( - parts: WireEventParts, + parts: RadrootsNip01EventWireParts, author: PublicKey, - created_at: Option<u32>, + created_at: Option<u64>, ) -> UnsignedEvent { let tags = tags_from_slices(&parts.tags); let timestamp = match created_at { - Some(ts) => Timestamp::from_secs(ts as u64), + Some(ts) => Timestamp::from_secs(ts), None => Timestamp::now(), }; let mut rumor = UnsignedEvent::new( @@ -180,7 +178,7 @@ where let mut rumor = unwrapped.rumor; let id = rumor.id().to_string(); let author = rumor.pubkey.to_string(); - let published_at = created_at_u32_saturating(rumor.created_at); + let published_at = rumor.created_at.as_secs(); let kind = rumor.kind.as_u16() as u32; let tags: Vec<Vec<String>> = rumor .tags diff --git a/crates/outbox/src/error.rs b/crates/outbox/src/error.rs @@ -17,6 +17,12 @@ pub enum RadrootsOutboxError { #[error("Signed event does not match frozen draft: {0}")] SignedEventDraftMismatch(#[from] radroots_event::draft::RadrootsDraftError), + #[error("Event wire error: {0}")] + EventWire(#[from] radroots_event::wire::RadrootsEventWireError), + + #[error("Signed event error: {0}")] + SignedEvent(#[from] radroots_event::draft::RadrootsSignedEventError), + #[error("delivery targets cannot be empty")] EmptyDeliveryTargets, @@ -65,6 +71,12 @@ pub enum RadrootsOutboxError { #[error("Signed event missing for outbox event {0}")] MissingSignedEvent(i64), + #[error("Stored signed event JSON missing raw event JSON for outbox event {0}")] + StoredSignedEventMissingRawJson(i64), + + #[error("Stored raw event JSON missing signed event JSON for outbox event {0}")] + StoredRawEventMissingSignedEvent(i64), + #[error("Signed event ID mismatch: expected {expected_event_id}, got {actual_event_id}")] SignedEventIdMismatch { expected_event_id: String, diff --git a/crates/outbox/src/store.rs b/crates/outbox/src/store.rs @@ -13,10 +13,10 @@ use crate::model::{ RadrootsOutboxReticulumPreviewBehavior, RadrootsOutboxReticulumPreviewEventRecord, RadrootsOutboxSignedOperationInput, RadrootsOutboxStatusSummary, }; -use radroots_event::RadrootsEventEnvelope; use radroots_event::draft::{ RadrootsEventDraft, RadrootsSignedEvent, validate_signed_nostr_event_matches_draft, }; +use radroots_event::wire::RadrootsNip01EventWire; use radroots_event_store::{ RadrootsEventIngest, RadrootsEventStore, RadrootsTransportObservation, RadrootsTransportObservationType, @@ -134,10 +134,11 @@ impl RadrootsOutbox { input: &RadrootsOutboxSignedOperationInput, ) -> Result<RadrootsOutboxIdempotencyPreflight, RadrootsOutboxError> { validate_signed_nostr_event_matches_draft(&input.signed_event, &input.draft)?; - let prepared = prepare_delivery_plan(&input.draft.expected_event_id, &input.delivery_plan)?; + let prepared = + prepare_delivery_plan(input.draft.expected_event_id_str(), &input.delivery_plan)?; let operation_digest = operation_idempotency_digest( input.operation_kind.as_str(), - input.draft.expected_pubkey.as_str(), + input.draft.expected_pubkey_str(), &input.draft, ); @@ -145,7 +146,7 @@ impl RadrootsOutbox { && let Some(existing) = existing_idempotent_operation_for_pool( &self.pool, input.operation_kind.as_str(), - input.draft.expected_pubkey.as_str(), + input.draft.expected_pubkey_str(), idempotency_key, ) .await? @@ -153,7 +154,7 @@ impl RadrootsOutbox { { return Err(RadrootsOutboxError::IdempotencyConflict { operation_kind: input.operation_kind.clone(), - expected_pubkey: input.draft.expected_pubkey.clone(), + expected_pubkey: input.draft.expected_pubkey_str().to_owned(), idempotency_key: idempotency_key.to_owned(), existing_digest: existing.operation_idempotency_digest, new_digest: operation_digest, @@ -170,10 +171,11 @@ impl RadrootsOutbox { &self, input: RadrootsOutboxOperationInput, ) -> Result<RadrootsOutboxEnqueueReceipt, RadrootsOutboxError> { - let prepared = prepare_delivery_plan(&input.draft.expected_event_id, &input.delivery_plan)?; + let prepared = + prepare_delivery_plan(input.draft.expected_event_id_str(), &input.delivery_plan)?; let operation_digest = operation_idempotency_digest( input.operation_kind.as_str(), - input.draft.expected_pubkey.as_str(), + input.draft.expected_pubkey_str(), &input.draft, ); let mut tx = self.pool.begin().await?; @@ -182,7 +184,7 @@ impl RadrootsOutbox { && let Some(existing) = existing_idempotent_operation( &mut tx, input.operation_kind.as_str(), - input.draft.expected_pubkey.as_str(), + input.draft.expected_pubkey_str(), idempotency_key, ) .await? @@ -190,7 +192,7 @@ impl RadrootsOutbox { if existing.operation_idempotency_digest != operation_digest { return Err(RadrootsOutboxError::IdempotencyConflict { operation_kind: input.operation_kind, - expected_pubkey: input.draft.expected_pubkey, + expected_pubkey: input.draft.expected_pubkey_str().to_owned(), idempotency_key: idempotency_key.to_owned(), existing_digest: existing.operation_idempotency_digest, new_digest: operation_digest, @@ -223,7 +225,7 @@ impl RadrootsOutbox { "INSERT INTO outbox_operations(operation_kind, expected_pubkey, idempotency_key, operation_idempotency_digest, status, created_at_ms, updated_at_ms) VALUES (?, ?, ?, ?, ?, ?, ?)", ) .bind(input.operation_kind.as_str()) - .bind(input.draft.expected_pubkey.as_str()) + .bind(input.draft.expected_pubkey_str()) .bind(input.idempotency_key.as_deref()) .bind(operation_digest.as_str()) .bind(RadrootsOutboxOperationStatus::Queued.as_str()) @@ -237,8 +239,8 @@ impl RadrootsOutbox { "INSERT INTO outbox_event(operation_id, event_id, expected_pubkey, draft_json, state, attempt_count, next_attempt_after_ms, event_store_ingested, event_store_inserted, created_at_ms, updated_at_ms) VALUES (?, ?, ?, ?, ?, 0, ?, 0, 0, ?, ?)", ) .bind(operation_id) - .bind(input.draft.expected_event_id.as_str()) - .bind(input.draft.expected_pubkey.as_str()) + .bind(input.draft.expected_event_id_str()) + .bind(input.draft.expected_pubkey_str()) .bind(draft_json.as_str()) .bind(RadrootsOutboxEventState::DraftQueued.as_str()) .bind(input.created_at_ms) @@ -256,7 +258,7 @@ impl RadrootsOutbox { operation_id, outbox_event_id, delivery_plan_id: plan.delivery_plan_id, - expected_event_id: input.draft.expected_event_id, + expected_event_id: input.draft.expected_event_id_str().to_owned(), operation_idempotency_digest: operation_digest, delivery_plan_idempotency_digest: prepared.delivery_plan_idempotency_digest, }) @@ -267,10 +269,11 @@ impl RadrootsOutbox { input: RadrootsOutboxSignedOperationInput, ) -> Result<RadrootsOutboxEnqueueReceipt, RadrootsOutboxError> { validate_signed_nostr_event_matches_draft(&input.signed_event, &input.draft)?; - let prepared = prepare_delivery_plan(&input.draft.expected_event_id, &input.delivery_plan)?; + let prepared = + prepare_delivery_plan(input.draft.expected_event_id_str(), &input.delivery_plan)?; let operation_digest = operation_idempotency_digest( input.operation_kind.as_str(), - input.draft.expected_pubkey.as_str(), + input.draft.expected_pubkey_str(), &input.draft, ); let mut tx = self.pool.begin().await?; @@ -279,7 +282,7 @@ impl RadrootsOutbox { && let Some(existing) = existing_idempotent_operation( &mut tx, input.operation_kind.as_str(), - input.draft.expected_pubkey.as_str(), + input.draft.expected_pubkey_str(), idempotency_key, ) .await? @@ -287,7 +290,7 @@ impl RadrootsOutbox { if existing.operation_idempotency_digest != operation_digest { return Err(RadrootsOutboxError::IdempotencyConflict { operation_kind: input.operation_kind, - expected_pubkey: input.draft.expected_pubkey, + expected_pubkey: input.draft.expected_pubkey_str().to_owned(), idempotency_key: idempotency_key.to_owned(), existing_digest: existing.operation_idempotency_digest, new_digest: operation_digest, @@ -326,7 +329,7 @@ impl RadrootsOutbox { "INSERT INTO outbox_operations(operation_kind, expected_pubkey, idempotency_key, operation_idempotency_digest, status, created_at_ms, updated_at_ms) VALUES (?, ?, ?, ?, ?, ?, ?)", ) .bind(input.operation_kind.as_str()) - .bind(input.draft.expected_pubkey.as_str()) + .bind(input.draft.expected_pubkey_str()) .bind(input.idempotency_key.as_deref()) .bind(operation_digest.as_str()) .bind(RadrootsOutboxOperationStatus::Queued.as_str()) @@ -336,16 +339,16 @@ impl RadrootsOutbox { .await?; let operation_id = operation.last_insert_rowid(); let draft_json = serde_json::to_string(&input.draft)?; - let signed_event_json = serde_json::to_string(&input.signed_event)?; + let signed_event_json = signed_event_wire_json(&input.signed_event)?; let event = sqlx::query( "INSERT INTO outbox_event(operation_id, event_id, expected_pubkey, draft_json, signed_event_json, raw_event_json, state, attempt_count, next_attempt_after_ms, event_store_ingested, event_store_inserted, event_store_ingested_at_ms, created_at_ms, updated_at_ms) VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?, 1, ?, ?, ?, ?)", ) .bind(operation_id) - .bind(input.draft.expected_event_id.as_str()) - .bind(input.draft.expected_pubkey.as_str()) + .bind(input.draft.expected_event_id_str()) + .bind(input.draft.expected_pubkey_str()) .bind(draft_json.as_str()) .bind(signed_event_json.as_str()) - .bind(input.signed_event.raw_json.as_str()) + .bind(input.signed_event.raw_json()) .bind(RadrootsOutboxEventState::Signed.as_str()) .bind(input.created_at_ms) .bind(bool_i64(input.event_store_inserted)) @@ -365,7 +368,7 @@ impl RadrootsOutbox { operation_id, outbox_event_id, delivery_plan_id: plan.delivery_plan_id, - expected_event_id: input.draft.expected_event_id, + expected_event_id: input.draft.expected_event_id_str().to_owned(), operation_idempotency_digest: operation_digest, delivery_plan_idempotency_digest: prepared.delivery_plan_idempotency_digest, }) @@ -622,18 +625,18 @@ impl RadrootsOutbox { if stored != Some(claim_token) { return Err(RadrootsOutboxError::ClaimTokenMismatch { outbox_event_id }); } - if signed_event.id != record.event_id { + if signed_event.id_str() != record.event_id { return Err(RadrootsOutboxError::SignedEventIdMismatch { expected_event_id: record.event_id, - actual_event_id: signed_event.id, + actual_event_id: signed_event.id_str().to_owned(), }); } - let signed_event_json = serde_json::to_string(&signed_event)?; + let signed_event_json = signed_event_wire_json(&signed_event)?; let changed = sqlx::query( "UPDATE outbox_event SET signed_event_json = ?, raw_event_json = ?, state = ?, claim_token = NULL, claim_owner = NULL, claim_expires_at_ms = NULL, active_delivery_plan_id = NULL, last_error = NULL, updated_at_ms = ? WHERE outbox_event_id = ? AND claim_token = ?", ) .bind(signed_event_json.as_str()) - .bind(signed_event.raw_json.as_str()) + .bind(signed_event.raw_json()) .bind(RadrootsOutboxEventState::Signed.as_str()) .bind(now_ms) .bind(outbox_event_id) @@ -724,15 +727,13 @@ impl RadrootsOutbox { let signed_event = record .signed_event .ok_or(RadrootsOutboxError::MissingSignedEvent(outbox_event_id))?; - let event = event_from_signed(&signed_event); let observation = RadrootsTransportObservation::new( RadrootsTransportKind::Local, "local:outbox", RadrootsTransportObservationType::LocalImport, observed_at_ms, )?; - let ingest = RadrootsEventIngest::new(event, observed_at_ms) - .with_raw_json(signed_event.raw_json.clone()) + let ingest = RadrootsEventIngest::new(signed_event.clone(), observed_at_ms) .with_observation(observation); let receipt = event_store.ingest_event(ingest).await?; let changed = sqlx::query( @@ -1794,12 +1795,12 @@ async fn ensure_event_signed( event_store_inserted: bool, event_store_ingested_at_ms: i64, ) -> Result<(), RadrootsOutboxError> { - let signed_event_json = serde_json::to_string(signed_event)?; + let signed_event_json = signed_event_wire_json(signed_event)?; sqlx::query( "UPDATE outbox_event SET signed_event_json = ?, raw_event_json = ?, state = CASE WHEN state IN ('draft_queued', 'sign_retryable', 'signing') THEN ? ELSE state END, event_store_ingested = 1, event_store_inserted = ?, event_store_ingested_at_ms = ? WHERE outbox_event_id = ? AND signed_event_json IS NULL", ) .bind(signed_event_json.as_str()) - .bind(signed_event.raw_json.as_str()) + .bind(signed_event.raw_json()) .bind(RadrootsOutboxEventState::Signed.as_str()) .bind(bool_i64(event_store_inserted)) .bind(event_store_ingested_at_ms) @@ -2226,17 +2227,28 @@ fn operation_from_row( fn event_from_row( row: sqlx::sqlite::SqliteRow, ) -> Result<RadrootsOutboxEventRecord, RadrootsOutboxError> { + let outbox_event_id = row.try_get("outbox_event_id")?; + let event_id = row.try_get::<String, _>("event_id")?; let draft: RadrootsEventDraft = serde_json::from_str(row.try_get::<String, _>("draft_json")?.as_str())?; - let signed_event = row - .try_get::<Option<String>, _>("signed_event_json")? - .map(|json| serde_json::from_str(json.as_str())) - .transpose()?; + let signed_event = signed_event_from_storage( + outbox_event_id, + row.try_get("signed_event_json")?, + row.try_get("raw_event_json")?, + )?; + if let Some(signed_event) = signed_event.as_ref() + && signed_event.id_str() != event_id + { + return Err(RadrootsOutboxError::SignedEventIdMismatch { + expected_event_id: event_id, + actual_event_id: signed_event.id_str().to_owned(), + }); + } let state = RadrootsOutboxEventState::parse(row.try_get::<String, _>("state")?.as_str())?; Ok(RadrootsOutboxEventRecord { - outbox_event_id: row.try_get("outbox_event_id")?, + outbox_event_id, operation_id: row.try_get("operation_id")?, - event_id: row.try_get("event_id")?, + event_id, expected_pubkey: row.try_get("expected_pubkey")?, draft, signed_event, @@ -2257,6 +2269,34 @@ fn event_from_row( }) } +fn signed_event_from_storage( + outbox_event_id: i64, + signed_event_json: Option<String>, + raw_event_json: Option<String>, +) -> Result<Option<RadrootsSignedEvent>, RadrootsOutboxError> { + match (signed_event_json, raw_event_json) { + (None, None) => Ok(None), + (Some(_), None) => Err(RadrootsOutboxError::StoredSignedEventMissingRawJson( + outbox_event_id, + )), + (None, Some(_)) => Err(RadrootsOutboxError::StoredRawEventMissingSignedEvent( + outbox_event_id, + )), + (Some(signed_json), Some(raw_json)) => { + let wire = RadrootsNip01EventWire::parse_json(signed_json.as_str())?; + RadrootsSignedEvent::from_wire_verified_id(wire, raw_json) + .map(Some) + .map_err(Into::into) + } + } +} + +fn signed_event_wire_json( + signed_event: &RadrootsSignedEvent, +) -> Result<String, RadrootsOutboxError> { + serde_json::to_string(signed_event.wire()).map_err(Into::into) +} + fn delivery_plan_from_row( row: sqlx::sqlite::SqliteRow, ) -> Result<RadrootsOutboxDeliveryPlanRecord, RadrootsOutboxError> { @@ -2427,18 +2467,6 @@ fn parse_transport_outcome_kind( } } -fn event_from_signed(signed_event: &RadrootsSignedEvent) -> RadrootsEventEnvelope { - RadrootsEventEnvelope { - id: signed_event.id.clone(), - author: signed_event.pubkey.clone(), - created_at: signed_event.created_at, - kind: signed_event.kind, - tags: signed_event.tags.clone(), - content: signed_event.content.clone(), - sig: signed_event.sig.clone(), - } -} - #[derive(Serialize)] struct OperationDigestInput<'a> { operation_kind: &'a str, @@ -3253,7 +3281,7 @@ mod tests { assert_ne!(scoped.fingerprint, rescaled.fingerprint); let first_prepared = prepare_delivery_plan( - draft.expected_event_id.as_str(), + draft.expected_event_id_str(), &RadrootsOutboxDeliveryPlanInput::new( "transport.nostr.local", 1, @@ -3263,7 +3291,7 @@ mod tests { ) .expect("first plan"); let relabeled_prepared = prepare_delivery_plan( - draft.expected_event_id.as_str(), + draft.expected_event_id_str(), &RadrootsOutboxDeliveryPlanInput::new( "transport.nostr.local", 1, @@ -3273,7 +3301,7 @@ mod tests { ) .expect("relabeled plan"); let rescaled_prepared = prepare_delivery_plan( - draft.expected_event_id.as_str(), + draft.expected_event_id_str(), &RadrootsOutboxDeliveryPlanInput::new( "transport.nostr.local", 1, @@ -5323,7 +5351,7 @@ mod tests { .ingest_signed_event_local(&event_store, receipt.outbox_event_id, "claim-b", 2_200) .await .expect("first ingest"); - assert_eq!(first.event_id, signed.id); + assert_eq!(first.event_id, signed.id_str()); assert!(!first.already_ingested); let second = outbox @@ -5332,7 +5360,7 @@ mod tests { .expect("second ingest"); assert!(second.already_ingested); let observations = event_store - .observations_for_event(signed.id.as_str()) + .observations_for_event(signed.id_str()) .await .expect("observations"); assert_eq!(observations.len(), 1); diff --git a/crates/trade/src/dvm.rs b/crates/trade/src/dvm.rs @@ -401,15 +401,16 @@ pub fn build_transition_proof_request_tags( pub fn parse_transition_proof_request_event( event: &RadrootsEventEnvelope, ) -> Result<RadrootsTradeTransitionProofRequestEnvelope, RadrootsTradeDvmError> { - if event.kind != KIND_TRADE_TRANSITION_PROOF_REQUEST { + if event.kind_u32() != KIND_TRADE_TRANSITION_PROOF_REQUEST { return Err(RadrootsTradeDvmError::UnsupportedKind { expected: KIND_TRADE_TRANSITION_PROOF_REQUEST, - actual: event.kind, + actual: event.kind_u32(), }); } - let tags = parse_transition_proof_request_tags(&event.tags)?; + let event_tags = event.tags_as_vec(); + let tags = parse_transition_proof_request_tags(&event_tags)?; let content: RadrootsTradeTransitionProofRequestV1 = - serde_json::from_str(&event.content).map_err(RadrootsTradeDvmError::InvalidContent)?; + serde_json::from_str(event.content()).map_err(RadrootsTradeDvmError::InvalidContent)?; validate_transition_proof_request_binding(&tags, &content)?; validate_transition_proof_request_content(&content)?; Ok(RadrootsTradeTransitionProofRequestEnvelope { tags, content }) @@ -437,13 +438,7 @@ pub fn build_transition_proof_result_tags( inputs: &[RadrootsTradeDvmInputTag], binding: &RadrootsTradeTransitionProofResultBinding, ) -> Result<Vec<Vec<String>>, RadrootsTradeDvmError> { - let request_event_id = RadrootsEventId::parse(request_event.id.as_str()).map_err(|source| { - RadrootsTradeDvmError::InvalidTag { - tag: TAG_E, - value: request_event.id.clone(), - source, - } - })?; + let request_event_id = request_event.id().clone(); let request_json = serialize_request_event(request_event)?; let mut tags = vec![ vec![RADROOTS_DVM_TAG_REQUEST.to_string(), request_json], @@ -514,13 +509,13 @@ pub fn parse_transition_proof_result_tags( let request_event_json = required_tag_value(tags, RADROOTS_DVM_TAG_REQUEST)?; let request_event: RadrootsEventEnvelope = serde_json::from_str(request_event_json) .map_err(RadrootsTradeDvmError::InvalidRequestEvent)?; - if request_event.kind != KIND_TRADE_TRANSITION_PROOF_REQUEST { + if request_event.kind_u32() != KIND_TRADE_TRANSITION_PROOF_REQUEST { return Err(RadrootsTradeDvmError::RequestEventKind { - kind: request_event.kind, + kind: request_event.kind_u32(), }); } let request_event_id = parse_event_id_tag(TAG_E, required_tag_value(tags, TAG_E)?)?; - if request_event.id != request_event_id.as_str() { + if request_event.id() != &request_event_id { return Err(RadrootsTradeDvmError::RequestEventIdMismatch); } let customer_pubkey = parse_pubkey_tag(TAG_P, required_tag_value(tags, TAG_P)?)?; @@ -860,15 +855,66 @@ mod tests { } fn request_event(content: &RadrootsTradeTransitionProofRequestV1) -> RadrootsEventEnvelope { - RadrootsEventEnvelope { - id: event_id(10).into_string(), + request_event_with_parts( + event_id(10), + KIND_TRADE_TRANSITION_PROOF_REQUEST, + build_transition_proof_request_tags(&public_key(WORKER), content), + serde_json::to_string(content).expect("content"), + ) + } + + fn request_event_with_parts( + id: RadrootsEventId, + kind: u32, + tags: Vec<Vec<String>>, + content: String, + ) -> RadrootsEventEnvelope { + RadrootsEventEnvelope::new(radroots_event::RadrootsEventEnvelopeParts { + id: id.into_string(), author: BUYER.to_string(), created_at: 1, - kind: KIND_TRADE_TRANSITION_PROOF_REQUEST, - tags: build_transition_proof_request_tags(&public_key(WORKER), content), - content: serde_json::to_string(content).expect("content"), - sig: "sig".to_string(), - } + kind, + tags, + content, + sig: "f".repeat(128), + }) + .expect("request event") + } + + fn request_event_with_tags( + content: &RadrootsTradeTransitionProofRequestV1, + tags: Vec<Vec<String>>, + ) -> RadrootsEventEnvelope { + request_event_with_parts( + event_id(10), + KIND_TRADE_TRANSITION_PROOF_REQUEST, + tags, + serde_json::to_string(content).expect("content"), + ) + } + + fn request_event_with_kind( + content: &RadrootsTradeTransitionProofRequestV1, + kind: u32, + ) -> RadrootsEventEnvelope { + request_event_with_parts( + event_id(10), + kind, + build_transition_proof_request_tags(&public_key(WORKER), content), + serde_json::to_string(content).expect("content"), + ) + } + + fn request_event_with_content( + content: &RadrootsTradeTransitionProofRequestV1, + event_content: String, + ) -> RadrootsEventEnvelope { + request_event_with_parts( + event_id(10), + KIND_TRADE_TRANSITION_PROOF_REQUEST, + build_transition_proof_request_tags(&public_key(WORKER), content), + event_content, + ) } #[test] @@ -887,8 +933,8 @@ mod tests { fn transition_proof_result_tags_bind_stringified_request() { let content = request_content(); let request_event = request_event(&content); - let request_tags = - parse_transition_proof_request_tags(&request_event.tags).expect("request tags"); + let request_tags = parse_transition_proof_request_tags(&request_event.tags_as_vec()) + .expect("request tags"); let binding = RadrootsTradeTransitionProofResultBinding { listing_event_id: content.listing_event_id.clone(), root_event_id: content.request_event_id.clone(), @@ -937,10 +983,9 @@ mod tests { #[test] fn transition_proof_request_rejects_missing_input_tags() { let content = request_content(); - let mut event = request_event(&content); - event - .tags - .retain(|tag| tag.first().map(String::as_str) != Some(TAG_I)); + let mut tags = request_event(&content).tags_as_vec(); + tags.retain(|tag| tag.first().map(String::as_str) != Some(TAG_I)); + let event = request_event_with_tags(&content, tags); assert!(matches!( parse_transition_proof_request_event(&event), @@ -1013,8 +1058,7 @@ mod tests { #[test] fn transition_proof_request_parser_rejects_kind_tag_content_and_hash_edges() { let content = request_content(); - let mut event = request_event(&content); - event.kind = KIND_PROFILE; + let event = request_event_with_kind(&content, KIND_PROFILE); assert!(matches!( parse_transition_proof_request_event(&event), Err(RadrootsTradeDvmError::UnsupportedKind { @@ -1023,41 +1067,43 @@ mod tests { }) )); - let mut event = request_event(&content); - event.content = "{".to_string(); + let event = request_event_with_content(&content, "{".to_string()); assert!(matches!( parse_transition_proof_request_event(&event), Err(RadrootsTradeDvmError::InvalidContent(_)) )); - let mut event = request_event(&content); - event.tags.iter_mut().for_each(|tag| { + let mut tags = request_event(&content).tags_as_vec(); + tags.iter_mut().for_each(|tag| { if tag.first().map(String::as_str) == Some(TAG_P) { tag[1] = "bad".to_string(); } }); + let event = request_event_with_tags(&content, tags); assert!(matches!( parse_transition_proof_request_event(&event), Err(RadrootsTradeDvmError::InvalidTag { tag: TAG_P, .. }) )); - let mut event = request_event(&content); - event.tags.iter_mut().for_each(|tag| { + let mut tags = request_event(&content).tags_as_vec(); + tags.iter_mut().for_each(|tag| { if tag.first().map(String::as_str) == Some(TAG_A) { tag[1] = "bad".to_string(); } }); + let event = request_event_with_tags(&content, tags); assert!(matches!( parse_transition_proof_request_event(&event), Err(RadrootsTradeDvmError::InvalidTag { tag: TAG_A, .. }) )); - let mut event = request_event(&content); - event.tags.iter_mut().for_each(|tag| { + let mut tags = request_event(&content).tags_as_vec(); + tags.iter_mut().for_each(|tag| { if tag.first().map(String::as_str) == Some(TAG_A) { tag[1] = format!("{KIND_LISTING}:{SELLER}:BBBBBBBBBBBBBBBBBBBBBg"); } }); + let event = request_event_with_tags(&content, tags); assert!(matches!( parse_transition_proof_request_event(&event), Err(RadrootsTradeDvmError::ContentMismatch { @@ -1065,8 +1111,8 @@ mod tests { }) )); - let mut event = request_event(&content); - event.tags.iter_mut().for_each(|tag| { + let mut tags = request_event(&content).tags_as_vec(); + tags.iter_mut().for_each(|tag| { if tag.first().map(String::as_str) == Some(TAG_I) && tag.get(3).map(String::as_str) == Some(RadrootsTradeDvmInputRole::Listing.as_str()) @@ -1074,6 +1120,7 @@ mod tests { tag[1] = event_id(99).into_string(); } }); + let event = request_event_with_tags(&content, tags); assert!(matches!( parse_transition_proof_request_event(&event), Err(RadrootsTradeDvmError::ContentMismatch { @@ -1142,43 +1189,47 @@ mod tests { )); } - let mut event = request_event(&content); - event.tags.push(vec![TAG_I.to_string()]); + let mut tags = request_event(&content).tags_as_vec(); + tags.push(vec![TAG_I.to_string()]); + let event = request_event_with_tags(&content, tags); assert!(matches!( parse_transition_proof_request_event(&event), Err(RadrootsTradeDvmError::MissingTag { tag: TAG_I }) )); - let mut event = request_event(&content); - event.tags.push(vec![ + let mut tags = request_event(&content).tags_as_vec(); + tags.push(vec![ TAG_I.to_string(), event_id(44).into_string(), "url".to_string(), RadrootsTradeDvmInputRole::Listing.as_str().to_string(), ]); + let event = request_event_with_tags(&content, tags); assert!(matches!( parse_transition_proof_request_event(&event), Err(RadrootsTradeDvmError::InvalidTag { tag: TAG_I, .. }) )); - let mut event = request_event(&content); - event.tags.push(vec![ + let mut tags = request_event(&content).tags_as_vec(); + tags.push(vec![ TAG_I.to_string(), event_id(44).into_string(), RADROOTS_DVM_INPUT_TYPE_EVENT.to_string(), ]); + let event = request_event_with_tags(&content, tags); assert!(matches!( parse_transition_proof_request_event(&event), Err(RadrootsTradeDvmError::MissingTag { tag: TAG_I }) )); - let mut event = request_event(&content); - event.tags.push(vec![ + let mut tags = request_event(&content).tags_as_vec(); + tags.push(vec![ TAG_I.to_string(), event_id(44).into_string(), RADROOTS_DVM_INPUT_TYPE_EVENT.to_string(), "radroots:legacy_event".to_string(), ]); + let event = request_event_with_tags(&content, tags); assert!(matches!( parse_transition_proof_request_event(&event), Err(RadrootsTradeDvmError::InvalidInputRole { .. }) @@ -1214,27 +1265,29 @@ mod tests { event_id(97), ), ] { - let mut event = request_event(&content); - event.tags.iter_mut().for_each(|tag| { + let mut tags = request_event(&content).tags_as_vec(); + tags.iter_mut().for_each(|tag| { if tag.first().map(String::as_str) == Some(TAG_I) && tag.get(3).map(String::as_str) == Some(role.as_str()) { tag[1] = event_id.clone().into_string(); } }); + let event = request_event_with_tags(&content, tags); assert!(matches!( parse_transition_proof_request_event(&event), Err(RadrootsTradeDvmError::ContentMismatch { field: got }) if got == field )); } - let mut event = request_event(&content); - event.tags.push(vec![ + let mut tags = request_event(&content).tags_as_vec(); + tags.push(vec![ TAG_I.to_string(), "bad".to_string(), RADROOTS_DVM_INPUT_TYPE_EVENT.to_string(), RadrootsTradeDvmInputRole::Listing.as_str().to_string(), ]); + let event = request_event_with_tags(&content, tags); assert!(matches!( parse_transition_proof_request_event(&event), Err(RadrootsTradeDvmError::InvalidTag { tag: TAG_I, .. }) @@ -1254,8 +1307,8 @@ mod tests { fn transition_proof_result_and_feedback_parsers_cover_error_edges() { let content = request_content(); let request_event = request_event(&content); - let request_tags = - parse_transition_proof_request_tags(&request_event.tags).expect("request tags"); + let request_tags = parse_transition_proof_request_tags(&request_event.tags_as_vec()) + .expect("request tags"); let binding = RadrootsTradeTransitionProofResultBinding { listing_event_id: content.listing_event_id.clone(), root_event_id: content.request_event_id.clone(), @@ -1300,18 +1353,6 @@ mod tests { }) )); - let mut bad_request_event = request_event.clone(); - bad_request_event.id = "bad".to_string(); - assert!(matches!( - build_transition_proof_result_tags( - &bad_request_event, - &public_key(BUYER), - &request_tags.inputs, - &binding, - ), - Err(RadrootsTradeDvmError::InvalidTag { tag: TAG_E, .. }) - )); - let mut invalid_json = tags.clone(); invalid_json[0][1] = "{".to_string(); assert!(matches!( @@ -1319,8 +1360,7 @@ mod tests { Err(RadrootsTradeDvmError::InvalidRequestEvent(_)) )); - let mut wrong_kind_request = request_event.clone(); - wrong_kind_request.kind = KIND_PROFILE; + let wrong_kind_request = request_event_with_kind(&content, KIND_PROFILE); let mut wrong_kind = tags.clone(); wrong_kind[0][1] = serde_json::to_string(&wrong_kind_request).unwrap(); assert!(matches!( diff --git a/crates/trade/src/listing/mod.rs b/crates/trade/src/listing/mod.rs @@ -129,10 +129,11 @@ fn ensure_public_listing_kind(kind: u32) -> Result<(), RadrootsPublicListingAddr pub fn parse_listing_event( event: &RadrootsEventEnvelope, ) -> Result<RadrootsListing, ListingParseError> { - if !is_listing_kind(event.kind) { - return Err(ListingParseError::InvalidKind(event.kind)); + if !is_listing_kind(event.kind_u32()) { + return Err(ListingParseError::InvalidKind(event.kind_u32())); } - self::codec::listing_from_event_parts(&event.tags, &event.content) + let tags = event.tags_as_vec(); + self::codec::listing_from_event_parts(&tags, event.content()) } #[cfg(test)] @@ -143,7 +144,7 @@ mod tests { parse_public_listing_address, }; use radroots_event::{ - RadrootsEventEnvelope, + RadrootsEventEnvelope, RadrootsEventEnvelopeParts, ids::RadrootsListingAddress, kinds::{KIND_LISTING, KIND_LISTING_DRAFT, KIND_PROFILE}, order::RadrootsListingParseError, @@ -152,9 +153,9 @@ mod tests { const SELLER: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; fn listing_event() -> RadrootsEventEnvelope { - RadrootsEventEnvelope { - id: "event-1".into(), - author: SELLER.into(), + RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts { + id: "9".repeat(64), + author: SELLER.to_string(), created_at: 1, kind: KIND_LISTING, tags: vec![ @@ -182,21 +183,23 @@ mod tests { ], ], content: String::new(), - sig: String::new(), - } + sig: "f".repeat(128), + }) + .expect("listing event") } #[test] fn parse_listing_event_rejects_non_listing_kind() { - let event = RadrootsEventEnvelope { - id: "event-1".into(), - author: "seller".into(), + let event = RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts { + id: "8".repeat(64), + author: SELLER.to_string(), created_at: 1, kind: KIND_PROFILE, tags: vec![], content: String::new(), - sig: String::new(), - }; + sig: "f".repeat(128), + }) + .expect("profile event"); assert!(matches!( parse_listing_event(&event), diff --git a/crates/trade/src/listing/mutation.rs b/crates/trade/src/listing/mutation.rs @@ -21,7 +21,7 @@ use radroots_event::{ kinds::{KIND_LISTING, KIND_LISTING_DRAFT}, }; #[cfg(feature = "serde_json")] -use radroots_event_codec::{listing::encode::to_json_wire_parts_with_kind, wire::to_frozen_draft}; +use radroots_event_codec::listing::encode::to_json_wire_parts_with_kind; use thiserror::Error; use crate::listing::draft::RadrootsCanonicalListingDraft; @@ -120,7 +120,7 @@ impl RadrootsListingMutation { #[cfg(feature = "serde_json")] pub fn build_listing_mutation_draft( mutation: &RadrootsListingMutation, - created_at: u32, + created_at: u64, ) -> Result<RadrootsEventDraft, RadrootsListingMutationError> { let (draft, kind, contract_id) = match mutation { RadrootsListingMutation::Publish { draft } | RadrootsListingMutation::Update { draft } => { @@ -135,11 +135,13 @@ pub fn build_listing_mutation_draft( }; let parts = to_json_wire_parts_with_kind(draft.listing(), kind) .map_err(|error| RadrootsListingMutationError::EncodeListing(error.to_string()))?; - to_frozen_draft( - parts, + RadrootsEventDraft::new( contract_id, - draft.seller_pubkey().as_str(), + parts.kind, created_at, + parts.tags, + parts.content, + draft.seller_pubkey().as_str(), ) .map_err(RadrootsListingMutationError::FrozenDraft) } @@ -151,7 +153,7 @@ mod tests { RadrootsCoreQuantityPrice, RadrootsCoreUnit, }; use radroots_event::{ - RadrootsEventEnvelope, + RadrootsEventEnvelope, RadrootsEventEnvelopeParts, farm::RadrootsFarmRef, ids::{RadrootsDTag, RadrootsInventoryBinId, RadrootsListingAddress, RadrootsPublicKey}, kinds::{KIND_LISTING, KIND_LISTING_DRAFT}, @@ -364,16 +366,16 @@ mod tests { let publish_draft = build_listing_mutation_draft(&publish, 1_700_000_000).expect("draft"); let update_draft = build_listing_mutation_draft(&update, 1_700_000_000).expect("draft"); - assert_eq!(publish_draft.kind, KIND_LISTING); - assert_eq!(publish_draft.contract_id, LISTING_PUBLISHED_CONTRACT_ID); - assert_eq!(publish_draft.expected_pubkey, SELLER); - assert_eq!(publish_draft.created_at, 1_700_000_000); + assert_eq!(publish_draft.kind_u32(), KIND_LISTING); + assert_eq!(publish_draft.contract_id(), LISTING_PUBLISHED_CONTRACT_ID); + assert_eq!(publish_draft.expected_pubkey_str(), SELLER); + assert_eq!(publish_draft.created_at_u64(), 1_700_000_000); let published_content: RadrootsListing = - serde_json::from_str(&publish_draft.content).expect("listing json"); + serde_json::from_str(publish_draft.content()).expect("listing json"); assert_eq!(published_content.d_tag.as_str(), "AAAAAAAAAAAAAAAAAAAAAg"); - assert_eq!(update_draft.kind, KIND_LISTING); - assert_eq!(update_draft.contract_id, LISTING_PUBLISHED_CONTRACT_ID); - assert_eq!(update_draft.expected_pubkey, SELLER); + assert_eq!(update_draft.kind_u32(), KIND_LISTING); + assert_eq!(update_draft.contract_id(), LISTING_PUBLISHED_CONTRACT_ID); + assert_eq!(update_draft.expected_pubkey_str(), SELLER); } #[test] @@ -382,12 +384,12 @@ mod tests { let draft = build_listing_mutation_draft(&save_draft, 1_700_000_000).expect("draft"); - assert_eq!(draft.kind, KIND_LISTING_DRAFT); - assert_eq!(draft.contract_id, LISTING_DRAFT_CONTRACT_ID); - assert_eq!(draft.expected_pubkey, SELLER); - assert_eq!(draft.created_at, 1_700_000_000); + assert_eq!(draft.kind_u32(), KIND_LISTING_DRAFT); + assert_eq!(draft.contract_id(), LISTING_DRAFT_CONTRACT_ID); + assert_eq!(draft.expected_pubkey_str(), SELLER); + assert_eq!(draft.created_at_u64(), 1_700_000_000); let draft_content: RadrootsListing = - serde_json::from_str(&draft.content).expect("listing json"); + serde_json::from_str(draft.content()).expect("listing json"); assert_eq!(draft_content.d_tag.as_str(), "AAAAAAAAAAAAAAAAAAAAAg"); } @@ -435,10 +437,13 @@ mod tests { let first = build_listing_mutation_draft(&publish, 1_700_000_000).expect("draft"); let second = build_listing_mutation_draft(&publish, 1_700_000_000).expect("draft"); - assert_eq!(first.expected_event_id, second.expected_event_id); - assert_eq!(first.expected_event_id.len(), 64); - assert_eq!(first.tags, second.tags); - assert_eq!(first.content, second.content); + assert_eq!( + first.expected_event_id_str(), + second.expected_event_id_str() + ); + assert_eq!(first.expected_event_id_str().len(), 64); + assert_eq!(first.tags_as_vec(), second.tags_as_vec()); + assert_eq!(first.content(), second.content()); } #[test] @@ -446,15 +451,16 @@ mod tests { let publish = RadrootsListingMutation::publish(canonical_draft()); let draft = build_listing_mutation_draft(&publish, 1_700_000_000).expect("draft"); - let event = RadrootsEventEnvelope { - id: String::new(), - author: draft.expected_pubkey.clone(), - created_at: draft.created_at, - kind: draft.kind, - tags: draft.tags, - content: draft.content, - sig: String::new(), - }; + let event = RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts { + id: draft.expected_event_id_str().to_owned(), + author: draft.expected_pubkey_str().to_owned(), + created_at: draft.created_at_u64(), + kind: draft.kind_u32(), + tags: draft.tags_as_vec(), + content: draft.content().to_owned(), + sig: "f".repeat(128), + }) + .expect("listing event"); let validated = validate_listing_event(&event).expect("validated listing"); assert_eq!(validated.seller_pubkey, SELLER); diff --git a/crates/trade/src/listing/validation.rs b/crates/trade/src/listing/validation.rs @@ -44,19 +44,22 @@ pub struct RadrootsTradeListing { pub fn validate_listing_event( event: &RadrootsEventEnvelope, ) -> Result<RadrootsTradeListing, TradeListingValidationError> { - if !is_listing_kind(event.kind) { - return Err(TradeListingValidationError::InvalidKind { kind: event.kind }); + if !is_listing_kind(event.kind_u32()) { + return Err(TradeListingValidationError::InvalidKind { + kind: event.kind_u32(), + }); } - let listing = listing_from_event_parts(&event.tags, &event.content) + let tags = event.tags_as_vec(); + let listing = listing_from_event_parts(&tags, event.content()) .map_err(|error| TradeListingValidationError::ParseError { error })?; let listing_id = listing.d_tag.trim().to_string(); - let seller_pubkey = event.author.clone(); + let seller_pubkey = event.author_str().to_owned(); if listing.farm.pubkey != seller_pubkey { return Err(TradeListingValidationError::InvalidSeller); } - let listing_addr_raw = format!("{}:{}:{}", event.kind, seller_pubkey, listing_id); + let listing_addr_raw = format!("{}:{}:{}", event.kind_u32(), seller_pubkey, listing_id); let listing_addr = RadrootsListingAddress::parse(&listing_addr_raw) .map_err(|_| TradeListingValidationError::ParseError { error: RadrootsListingParseError::InvalidTag("listing_addr".to_string()), @@ -271,12 +274,10 @@ mod tests { } fn base_event(listing: &RadrootsListing) -> RadrootsEventEnvelope { - RadrootsEventEnvelope { - id: "evt".into(), - author: SELLER.into(), - created_at: 0, - kind: KIND_LISTING, - tags: vec![ + event_with_parts( + SELLER, + KIND_LISTING, + vec![ vec!["d".into(), listing.d_tag.to_string()], vec!["p".into(), listing.farm.pubkey.clone()], vec![ @@ -284,9 +285,26 @@ mod tests { format!("30340:{}:{}", listing.farm.pubkey, listing.farm.d_tag), ], ], - content: serde_json::to_string(listing).unwrap(), - sig: "sig".into(), - } + serde_json::to_string(listing).unwrap(), + ) + } + + fn event_with_parts( + author: &str, + kind: u32, + tags: Vec<Vec<String>>, + content: String, + ) -> RadrootsEventEnvelope { + RadrootsEventEnvelope::new(radroots_event::RadrootsEventEnvelopeParts { + id: "9".repeat(64), + author: author.to_string(), + created_at: 0, + kind, + tags, + content, + sig: "f".repeat(128), + }) + .expect("event") } fn assert_validation_err(listing: RadrootsListing, expected: TradeListingValidationError) { @@ -305,8 +323,12 @@ mod tests { #[test] fn validate_draft_listing_ok() { let listing = base_listing(); - let mut event = base_event(&listing); - event.kind = KIND_LISTING_DRAFT; + let event = event_with_parts( + SELLER, + KIND_LISTING_DRAFT, + base_event(&listing).tags_as_vec(), + serde_json::to_string(&listing).unwrap(), + ); let validated = validate_listing_event(&event).expect("draft listing"); assert_eq!( validated.listing_addr, @@ -317,8 +339,12 @@ mod tests { #[test] fn validate_listing_rejects_missing_d_tag() { let listing = base_listing(); - let mut event = base_event(&listing); - event.tags.clear(); + let event = event_with_parts( + SELLER, + KIND_LISTING, + Vec::new(), + serde_json::to_string(&listing).unwrap(), + ); let err = validate_listing_event(&event).unwrap_err(); assert_eq!( err, @@ -330,39 +356,42 @@ mod tests { #[test] fn validate_listing_rejects_invalid_currency() { - let mut event = base_event(&base_listing()); - event.content = String::new(); - event.tags = vec![ - vec!["d".into(), "AAAAAAAAAAAAAAAAAAAAAg".into()], - vec!["p".into(), SELLER.into()], - vec!["a".into(), format!("30340:{SELLER}:AAAAAAAAAAAAAAAAAAAAAA")], - vec!["key".into(), "coffee".into()], - vec!["title".into(), "Coffee".into()], - vec!["category".into(), "coffee".into()], - vec!["summary".into(), "Single origin".into()], - vec![ - "quantity".into(), - "1".into(), - "lb".into(), - "bag".into(), - "5".into(), - ], + let event = event_with_parts( + SELLER, + KIND_LISTING, vec![ - "price".into(), - "20".into(), - "US".into(), - "1".into(), - "lb".into(), - ], - vec![ - "location".into(), - "Farm".into(), - "Town".into(), - "Region".into(), + vec!["d".into(), "AAAAAAAAAAAAAAAAAAAAAg".into()], + vec!["p".into(), SELLER.into()], + vec!["a".into(), format!("30340:{SELLER}:AAAAAAAAAAAAAAAAAAAAAA")], + vec!["key".into(), "coffee".into()], + vec!["title".into(), "Coffee".into()], + vec!["category".into(), "coffee".into()], + vec!["summary".into(), "Single origin".into()], + vec![ + "quantity".into(), + "1".into(), + "lb".into(), + "bag".into(), + "5".into(), + ], + vec![ + "price".into(), + "20".into(), + "US".into(), + "1".into(), + "lb".into(), + ], + vec![ + "location".into(), + "Farm".into(), + "Town".into(), + "Region".into(), + ], + vec!["status".into(), "active".into()], + vec!["delivery".into(), "pickup".into()], ], - vec!["status".into(), "active".into()], - vec!["delivery".into(), "pickup".into()], - ]; + String::new(), + ); let err = validate_listing_event(&event).unwrap_err(); assert!(format!("{err:?}").starts_with("ParseError")); } @@ -370,31 +399,17 @@ mod tests { #[test] fn validate_listing_rejects_mismatched_seller() { let listing = base_listing(); - let mut event = base_event(&listing); - event.author = OTHER_SELLER.into(); + let event = event_with_parts( + OTHER_SELLER, + KIND_LISTING, + base_event(&listing).tags_as_vec(), + serde_json::to_string(&listing).unwrap(), + ); let err = validate_listing_event(&event).unwrap_err(); assert_eq!(err, TradeListingValidationError::InvalidSeller); } #[test] - fn validate_listing_rejects_invalid_listing_address_parts() { - let mut listing = base_listing(); - listing.farm.pubkey = "not-a-pubkey".into(); - let mut event = base_event(&listing); - event.author = "not-a-pubkey".into(); - let err = validate_listing_event(&event).unwrap_err(); - - assert_eq!( - err, - TradeListingValidationError::ParseError { - error: crate::listing::codec::ListingParseError::InvalidTag( - "listing_addr".to_string() - ) - } - ); - } - - #[test] fn validate_listing_rejects_missing_inventory() { let mut listing = base_listing(); listing.inventory_available = None; @@ -406,8 +421,12 @@ mod tests { #[test] fn validate_listing_rejects_invalid_kind() { let listing = base_listing(); - let mut event = base_event(&listing); - event.kind = 0; + let event = event_with_parts( + SELLER, + 0, + base_event(&listing).tags_as_vec(), + serde_json::to_string(&listing).unwrap(), + ); let err = validate_listing_event(&event).unwrap_err(); assert_eq!(err, TradeListingValidationError::InvalidKind { kind: 0 }); } diff --git a/crates/trade/src/order.rs b/crates/trade/src/order.rs @@ -8,6 +8,8 @@ use alloc::{ #[cfg(feature = "serde_json")] use radroots_event::RadrootsEventEnvelope; +#[cfg(feature = "event_store")] +use radroots_event::RadrootsEventEnvelopeParts; use radroots_event::ids::{ RadrootsEventId, RadrootsIdParseError, RadrootsInventoryBinId, RadrootsListingAddress, RadrootsOrderId, RadrootsPublicKey, @@ -169,13 +171,15 @@ pub enum RadrootsOrderEventDecodeError { pub fn order_event_record_from_event( event: &RadrootsEventEnvelope, ) -> Result<RadrootsOrderEventRecord, RadrootsOrderEventDecodeError> { - let message_type = RadrootsOrderEventType::from_kind(event.kind) - .ok_or(RadrootsOrderEventDecodeError::UnsupportedKind { kind: event.kind })?; - let context = order_event_context_from_tags(message_type, &event.tags)?; - let event_id = - RadrootsEventId::parse(&event.id).map_err(RadrootsOrderEventDecodeError::InvalidEventId)?; - let author_pubkey = RadrootsPublicKey::parse(&event.author) - .map_err(RadrootsOrderEventDecodeError::InvalidAuthor)?; + let message_type = RadrootsOrderEventType::from_kind(event.kind_u32()).ok_or( + RadrootsOrderEventDecodeError::UnsupportedKind { + kind: event.kind_u32(), + }, + )?; + let tags = event.tags_as_vec(); + let context = order_event_context_from_tags(message_type, &tags)?; + let event_id = event.id().clone(); + let author_pubkey = event.author().clone(); match message_type { RadrootsOrderEventType::OrderRequested => { @@ -255,6 +259,11 @@ pub enum RadrootsOrderStoreQueryError { event_id: String, source: serde_json::Error, }, + #[error("stored order event {event_id} contains invalid envelope fields: {source}")] + InvalidStoredEnvelope { + event_id: String, + source: radroots_event::RadrootsEventEnvelopeError, + }, #[error("stored order event {event_id} could not decode as an order record: {source}")] Decode { event_id: String, @@ -364,7 +373,7 @@ fn stored_order_event_to_nostr_event( source, } })?; - Ok(RadrootsEventEnvelope { + RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts { id: stored_event.event_id.clone(), author: stored_event.pubkey.clone(), created_at: stored_event.created_at, @@ -373,6 +382,12 @@ fn stored_order_event_to_nostr_event( content: stored_event.content.clone(), sig: stored_event.sig.clone(), }) + .map_err( + |source| RadrootsOrderStoreQueryError::InvalidStoredEnvelope { + event_id: stored_event.event_id.clone(), + source, + }, + ) } #[cfg(feature = "serde_json")] @@ -2666,7 +2681,7 @@ mod tests { RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreUnit, }; use radroots_event::{ - RadrootsEventEnvelope, RadrootsEventPtr, + RadrootsEventEnvelope, RadrootsEventEnvelopeParts, RadrootsEventPtr, ids::{ RadrootsEventId, RadrootsInventoryBinId, RadrootsListingAddress, RadrootsOrderId, RadrootsOrderQuoteId, RadrootsOrderRevisionId, RadrootsPublicKey, @@ -2679,14 +2694,12 @@ mod tests { RadrootsOrderRevisionDecision, RadrootsOrderRevisionOutcome, RadrootsOrderRevisionProposal, }, + wire::RadrootsNip01EventWireParts, }; #[cfg(feature = "serde_json")] - use radroots_event_codec::{ - order::{ - order_cancellation_event_build, order_decision_event_build, order_request_event_build, - order_revision_decision_event_build, order_revision_proposal_event_build, - }, - wire::WireEventParts, + use radroots_event_codec::order::{ + order_cancellation_event_build, order_decision_event_build, order_request_event_build, + order_revision_decision_event_build, order_revision_proposal_event_build, }; const BUYER: &str = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; @@ -2743,16 +2756,21 @@ mod tests { } #[cfg(feature = "serde_json")] - fn event_from_parts(raw_id: u8, author: &str, parts: WireEventParts) -> RadrootsEventEnvelope { - RadrootsEventEnvelope { + fn event_from_parts( + raw_id: u8, + author: &str, + parts: RadrootsNip01EventWireParts, + ) -> RadrootsEventEnvelope { + RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts { id: event_id(raw_id).into_string(), - author: author.into(), + author: author.to_string(), created_at: 1, kind: parts.kind, tags: parts.tags, content: parts.content, - sig: "sig".into(), - } + sig: "f".repeat(128), + }) + .expect("event") } fn economics(bin_count: u32) -> RadrootsOrderEconomics { @@ -3356,37 +3374,20 @@ mod tests { && record.payload.reason == "changed plans" )); - let unsupported = RadrootsEventEnvelope { + let unsupported = RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts { id: event_id(16).into_string(), - author: BUYER.into(), + author: BUYER.to_string(), created_at: 1, kind: 1, tags: Vec::new(), content: "{}".into(), - sig: "sig".into(), - }; + sig: "f".repeat(128), + }) + .expect("unsupported event"); assert!(matches!( super::order_event_record_from_event(&unsupported), Err(super::RadrootsOrderEventDecodeError::UnsupportedKind { kind: 1 }) )); - - let request_parts = - order_request_event_build(&listing_event_ptr(), &request.payload).unwrap(); - let mut invalid_id_event = event_from_parts(17, BUYER, request_parts); - invalid_id_event.id = "not-an-event-id".into(); - assert!(matches!( - super::order_event_record_from_event(&invalid_id_event), - Err(super::RadrootsOrderEventDecodeError::InvalidEventId(_)) - )); - - let request_parts = - order_request_event_build(&listing_event_ptr(), &request.payload).unwrap(); - let mut invalid_author_event = event_from_parts(18, BUYER, request_parts); - invalid_author_event.author = "not-a-pubkey".into(); - assert!(matches!( - super::order_event_record_from_event(&invalid_author_event), - Err(super::RadrootsOrderEventDecodeError::InvalidAuthor(_)) - )); } #[cfg(feature = "serde_json")] diff --git a/crates/trade/src/projection.rs b/crates/trade/src/projection.rs @@ -3,7 +3,7 @@ use std::collections::{BTreeMap, BTreeSet}; use radroots_event::{ - RadrootsEventEnvelope, + RadrootsEventEnvelope, RadrootsEventEnvelopeError, RadrootsEventEnvelopeParts, ids::{RadrootsEventId, RadrootsIdParseError, RadrootsListingAddress, RadrootsOrderId}, kinds::{KIND_TRADE_VALIDATION_RECEIPT, is_listing_kind, is_order_event_kind}, listing::{RadrootsListingAvailability, RadrootsListingDeliveryMethod, RadrootsListingStatus}, @@ -60,6 +60,13 @@ pub enum RadrootsTradeProjectionError { event_id: String, source: serde_json::Error, }, + #[error("stored event {event_id} contains invalid envelope data: {source}")] + InvalidStoredEnvelope { + event_id: String, + source: RadrootsEventEnvelopeError, + }, + #[error("stored event {event_id} created_at {created_at} exceeds sqlite integer range")] + StoredCreatedAtRange { event_id: String, created_at: u64 }, #[error("stored listing event {event_id} failed validation: {source}")] ListingValidation { event_id: String, @@ -379,7 +386,12 @@ async fn upsert_listing_projection( .bind(location.geohash.as_str()) .bind(listing_json) .bind(stored_event.seq) - .bind(i64::from(stored_event.created_at)) + .bind(i64::try_from(stored_event.created_at).map_err(|_| { + RadrootsTradeProjectionError::StoredCreatedAtRange { + event_id: stored_event.event_id.clone(), + created_at: stored_event.created_at, + } + })?) .bind(updated_at_ms) .execute(store.pool()) .await?; @@ -709,18 +721,18 @@ fn push_order_record( fn request_listing_event_id( event: &RadrootsEventEnvelope, ) -> Result<Option<RadrootsEventId>, RadrootsTradeProjectionError> { - let context = - order_event_context_from_tags(RadrootsOrderEventType::OrderRequested, &event.tags) - .map_err(|source| RadrootsTradeProjectionError::OrderContext { - event_id: event.id.clone(), - source, - })?; + let tags = event.tags_as_vec(); + let context = order_event_context_from_tags(RadrootsOrderEventType::OrderRequested, &tags) + .map_err(|source| RadrootsTradeProjectionError::OrderContext { + event_id: event.id_str().to_owned(), + source, + })?; context .listing_event .map(|listing_event| { RadrootsEventId::parse(listing_event.id.as_str()).map_err(|source| { RadrootsTradeProjectionError::ValidationReceiptEventId { - event_id: event.id.clone(), + event_id: event.id_str().to_owned(), source, } }) @@ -757,7 +769,7 @@ fn stored_event_to_nostr_event( source, } })?; - Ok(RadrootsEventEnvelope { + RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts { id: stored_event.event_id.clone(), author: stored_event.pubkey.clone(), created_at: stored_event.created_at, @@ -766,6 +778,12 @@ fn stored_event_to_nostr_event( content: stored_event.content.clone(), sig: stored_event.sig.clone(), }) + .map_err( + |source| RadrootsTradeProjectionError::InvalidStoredEnvelope { + event_id: stored_event.event_id.clone(), + source, + }, + ) } async fn transport_observation_count_for_events( @@ -908,6 +926,7 @@ mod tests { }; use radroots_event::{ RadrootsEventPtr, + draft::RadrootsSignedEvent, farm::RadrootsFarmRef, ids::RadrootsOrderQuoteId, kinds::KIND_LISTING, @@ -920,6 +939,7 @@ mod tests { RadrootsOrderEconomics, RadrootsOrderInventoryCommitment, RadrootsOrderItem, RadrootsOrderPricingBasis, RadrootsOrderRequest, }, + wire::RadrootsNip01EventWire, }; use radroots_event_codec::order::{order_decision_event_build, order_request_event_build}; use radroots_event_store::{ @@ -927,7 +947,7 @@ mod tests { }; use radroots_nostr::prelude::{ RadrootsNostrKeys, RadrootsNostrSecretKey, RadrootsNostrTimestamp, - radroots_event_from_nostr, radroots_nostr_build_event, + radroots_nostr_build_event, }; use radroots_transport::RadrootsTransportKind; @@ -1003,7 +1023,7 @@ mod tests { } } - fn signed_listing_event() -> RadrootsEventEnvelope { + fn signed_listing_event() -> RadrootsSignedEvent { let parts = radroots_event_codec::listing::encode::to_wire_parts(&listing()) .expect("listing parts"); sign_parts( @@ -1015,11 +1035,11 @@ mod tests { ) } - fn listing_addr(event: &RadrootsEventEnvelope) -> RadrootsListingAddress { + fn listing_addr(event: &RadrootsSignedEvent) -> RadrootsListingAddress { RadrootsListingAddress::parse(format!( "{}:{}:{}", KIND_LISTING, - event.author, + event.pubkey_str(), listing().d_tag )) .expect("listing address") @@ -1054,7 +1074,7 @@ mod tests { } } - fn order_request(listing_event: &RadrootsEventEnvelope) -> RadrootsOrderRequest { + fn order_request(listing_event: &RadrootsSignedEvent) -> RadrootsOrderRequest { RadrootsOrderRequest { order_id: order_id(), listing_addr: listing_addr(listing_event), @@ -1068,17 +1088,17 @@ mod tests { } } - fn signed_order_request_event(listing_event: &RadrootsEventEnvelope) -> RadrootsEventEnvelope { + fn signed_order_request_event(listing_event: &RadrootsSignedEvent) -> RadrootsSignedEvent { signed_order_request_event_at(listing_event, 1_700_000_010) } fn signed_order_request_event_at( - listing_event: &RadrootsEventEnvelope, + listing_event: &RadrootsSignedEvent, created_at: u32, - ) -> RadrootsEventEnvelope { + ) -> RadrootsSignedEvent { let parts = order_request_event_build( &RadrootsEventPtr { - id: listing_event.id.clone(), + id: listing_event.id_str().to_owned(), relays: Some("wss://relay.example.test".to_owned()), }, &order_request(listing_event), @@ -1094,9 +1114,9 @@ mod tests { } fn signed_order_decision_event( - request: &RadrootsEventEnvelope, - listing_event: &RadrootsEventEnvelope, - ) -> RadrootsEventEnvelope { + request: &RadrootsSignedEvent, + listing_event: &RadrootsSignedEvent, + ) -> RadrootsSignedEvent { let decision = RadrootsOrderDecision { order_id: order_id(), listing_addr: listing_addr(listing_event), @@ -1109,7 +1129,7 @@ mod tests { }], }, }; - let root = RadrootsEventId::parse(request.id.as_str()).expect("root"); + let root = request.id().clone(); let parts = order_decision_event_build(&root, &root, &decision).expect("decision parts"); sign_parts( parts.kind, @@ -1121,14 +1141,14 @@ mod tests { } fn signed_receipt_event( - listing_event: &RadrootsEventEnvelope, - request: &RadrootsEventEnvelope, - decision: &RadrootsEventEnvelope, + listing_event: &RadrootsSignedEvent, + request: &RadrootsSignedEvent, + decision: &RadrootsSignedEvent, result: RadrootsValidationReceiptResult, - ) -> RadrootsEventEnvelope { - let request_id = RadrootsEventId::parse(request.id.as_str()).expect("request"); - let listing_event_id = RadrootsEventId::parse(listing_event.id.as_str()).expect("listing"); - let decision_id = RadrootsEventId::parse(decision.id.as_str()).expect("decision"); + ) -> RadrootsSignedEvent { + let request_id = request.id().clone(); + let listing_event_id = listing_event.id().clone(); + let decision_id = decision.id().clone(); let receipt = RadrootsTradeValidationReceipt { changed_records_root: hash32('a'), domain: "radroots.receipt".to_owned(), @@ -1180,13 +1200,15 @@ mod tests { tags: Vec<Vec<String>>, created_at: u32, keys: &RadrootsNostrKeys, - ) -> RadrootsEventEnvelope { + ) -> RadrootsSignedEvent { let raw_event = radroots_nostr_build_event(kind, content, tags) .expect("builder") .custom_created_at(RadrootsNostrTimestamp::from_secs(u64::from(created_at))) .sign_with_keys(keys) .expect("signed"); - radroots_event_from_nostr(&raw_event) + let raw_json = serde_json::to_string(&raw_event).expect("raw event json"); + let wire = RadrootsNip01EventWire::parse_json(raw_json.as_str()).expect("wire"); + RadrootsSignedEvent::from_wire_verified_id(wire, raw_json).expect("signed event") } fn hash32(character: char) -> String { @@ -1265,10 +1287,10 @@ mod tests { ); assert_eq!( status.projection.validation_receipt_event_id, - Some(RadrootsEventId::parse(receipt_event.id).expect("receipt")) + Some(receipt_event.id().clone()) ); - let root_event_id = RadrootsEventId::parse(request_event.id).expect("request"); + let root_event_id = request_event.id().clone(); let trade_row = sqlx::query( "SELECT root_event_id, projection_version, status, rhi_state, transport_observation_count, source_event_count, evidence_hash FROM trade_projection WHERE order_id = ? AND root_event_id = ? AND projection_version = ?", ) @@ -1319,8 +1341,8 @@ mod tests { let listing_event = signed_listing_event(); let first_request_event = signed_order_request_event_at(&listing_event, 1_700_000_010); let second_request_event = signed_order_request_event_at(&listing_event, 1_700_000_011); - let first_root = RadrootsEventId::parse(first_request_event.id.clone()).expect("first"); - let second_root = RadrootsEventId::parse(second_request_event.id.clone()).expect("second"); + let first_root = first_request_event.id().clone(); + let second_root = second_request_event.id().clone(); store .ingest_event(RadrootsEventIngest::new(listing_event.clone(), 10)) diff --git a/crates/trade/src/validation_receipt.rs b/crates/trade/src/validation_receipt.rs @@ -10,9 +10,8 @@ use alloc::{ use base64::Engine as _; use radroots_event::{ RadrootsEventEnvelope, ids::RadrootsPublicKey, kinds::KIND_TRADE_VALIDATION_RECEIPT, - tags::TAG_D, + tags::TAG_D, wire::RadrootsNip01EventWireParts, }; -use radroots_event_codec::wire::WireEventParts; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use thiserror::Error; @@ -566,8 +565,8 @@ pub fn validation_receipt_tags_from_tags( pub fn validation_receipt_event_build( order_id: &str, receipt: &RadrootsTradeValidationReceipt, -) -> Result<WireEventParts, RadrootsValidationReceiptError> { - Ok(WireEventParts { +) -> Result<RadrootsNip01EventWireParts, RadrootsValidationReceiptError> { + Ok(RadrootsNip01EventWireParts { kind: KIND_TRADE_VALIDATION_RECEIPT, content: validation_receipt_canonical_content(receipt)?, tags: validation_receipt_tags(order_id, receipt)?, @@ -584,15 +583,16 @@ pub fn verify_validation_receipt_event( event: &RadrootsEventEnvelope, expected: RadrootsValidationReceiptExpectedBinding<'_>, ) -> Result<RadrootsVerifiedValidationReceipt, RadrootsValidationReceiptError> { - if event.kind != KIND_TRADE_VALIDATION_RECEIPT { + if event.kind_u32() != KIND_TRADE_VALIDATION_RECEIPT { return Err(RadrootsValidationReceiptError::InvalidKind { expected: KIND_TRADE_VALIDATION_RECEIPT, - got: event.kind, + got: event.kind_u32(), }); } - let receipt = validation_receipt_content_from_str(&event.content)?; - let tags = validation_receipt_tags_from_tags(&event.tags)?; + let receipt = validation_receipt_content_from_str(event.content())?; + let event_tags = event.tags_as_vec(); + let tags = validation_receipt_tags_from_tags(&event_tags)?; if tags.listing_event_id != receipt.statement.listing_event_id { return Err(RadrootsValidationReceiptError::TagMismatch( @@ -864,8 +864,8 @@ mod tests { verify_validation_receipt_event, }; use radroots_event::{ - RadrootsEventEnvelope, ids::RadrootsPublicKey, kinds::KIND_TRADE_VALIDATION_RECEIPT, - tags::TAG_D, + RadrootsEventEnvelope, RadrootsEventEnvelopeParts, ids::RadrootsPublicKey, + kinds::KIND_TRADE_VALIDATION_RECEIPT, tags::TAG_D, }; fn hash32(c: char) -> String { @@ -923,15 +923,30 @@ mod tests { fn sample_validation_receipt_event() -> RadrootsEventEnvelope { let receipt = sample_validation_receipt(); let parts = validation_receipt_event_build("order-1", &receipt).expect("event parts"); - RadrootsEventEnvelope { + validation_receipt_event_with_parts(parts.kind, parts.tags, parts.content) + } + + fn validation_receipt_event_with_parts( + kind: u32, + tags: Vec<Vec<String>>, + content: String, + ) -> RadrootsEventEnvelope { + RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts { id: event_id('9'), author: event_id('a'), created_at: 1, - kind: parts.kind, - tags: parts.tags, - content: parts.content, - sig: "signature".to_string(), - } + kind, + tags, + content, + sig: "f".repeat(128), + }) + .expect("receipt event") + } + + fn validation_receipt_event_with_tags(tags: Vec<Vec<String>>) -> RadrootsEventEnvelope { + let receipt = sample_validation_receipt(); + let parts = validation_receipt_event_build("order-1", &receipt).expect("event parts"); + validation_receipt_event_with_parts(parts.kind, tags, parts.content) } #[test] @@ -1449,8 +1464,9 @@ mod tests { #[test] fn validation_receipt_verifier_rejects_each_tag_mismatch() { - let mut event = sample_validation_receipt_event(); - event.tags[1][1] = event_id('3'); + let mut tags = sample_validation_receipt_event().tags_as_vec(); + tags[1][1] = event_id('3'); + let event = validation_receipt_event_with_tags(tags); assert_eq!( validation_receipt_from_event(&event), Err(RadrootsValidationReceiptError::TagMismatch( @@ -1458,15 +1474,17 @@ mod tests { )) ); - let mut event = sample_validation_receipt_event(); - event.tags[2][1] = event_id('3'); + let mut tags = sample_validation_receipt_event().tags_as_vec(); + tags[2][1] = event_id('3'); + let event = validation_receipt_event_with_tags(tags); assert_eq!( validation_receipt_from_event(&event), Err(RadrootsValidationReceiptError::TagMismatch("root_event_id")) ); - let mut event = sample_validation_receipt_event(); - event.tags[3][1] = event_id('3'); + let mut tags = sample_validation_receipt_event().tags_as_vec(); + tags[3][1] = event_id('3'); + let event = validation_receipt_event_with_tags(tags); assert_eq!( validation_receipt_from_event(&event), Err(RadrootsValidationReceiptError::TagMismatch( @@ -1474,8 +1492,9 @@ mod tests { )) ); - let mut event = sample_validation_receipt_event(); - event.tags[4][1] = hash32('d'); + let mut tags = sample_validation_receipt_event().tags_as_vec(); + tags[4][1] = hash32('d'); + let event = validation_receipt_event_with_tags(tags); assert_eq!( validation_receipt_from_event(&event), Err(RadrootsValidationReceiptError::TagMismatch( @@ -1483,8 +1502,9 @@ mod tests { )) ); - let mut event = sample_validation_receipt_event(); - event.tags[5][1] = hash32('d'); + let mut tags = sample_validation_receipt_event().tags_as_vec(); + tags[5][1] = hash32('d'); + let event = validation_receipt_event_with_tags(tags); assert_eq!( validation_receipt_from_event(&event), Err(RadrootsValidationReceiptError::TagMismatch( @@ -1492,8 +1512,9 @@ mod tests { )) ); - let mut event = sample_validation_receipt_event(); - event.tags[6][1] = hash32('d'); + let mut tags = sample_validation_receipt_event().tags_as_vec(); + tags[6][1] = hash32('d'); + let event = validation_receipt_event_with_tags(tags); assert_eq!( validation_receipt_from_event(&event), Err(RadrootsValidationReceiptError::TagMismatch( @@ -1501,15 +1522,17 @@ mod tests { )) ); - let mut event = sample_validation_receipt_event(); - event.tags[7][1] = "sp1_core".to_string(); + let mut tags = sample_validation_receipt_event().tags_as_vec(); + tags[7][1] = "sp1_core".to_string(); + let event = validation_receipt_event_with_tags(tags); assert_eq!( validation_receipt_from_event(&event), Err(RadrootsValidationReceiptError::TagMismatch("proof_system")) ); - let mut event = sample_validation_receipt_event(); - event.tags[8][1] = "listing_validation".to_string(); + let mut tags = sample_validation_receipt_event().tags_as_vec(); + tags[8][1] = "listing_validation".to_string(); + let event = validation_receipt_event_with_tags(tags); assert_eq!( validation_receipt_from_event(&event), Err(RadrootsValidationReceiptError::TagMismatch("receipt_type")) @@ -1659,7 +1682,7 @@ mod tests { ); let event = sample_validation_receipt_event(); - assert_eq!(event.kind, KIND_TRADE_VALIDATION_RECEIPT); + assert_eq!(event.kind_u32(), KIND_TRADE_VALIDATION_RECEIPT); let verified = validation_receipt_from_event(&event).expect("verified receipt"); assert_eq!(verified.tags.order_id, "order-1"); assert_eq!(verified.tags.listing_event_id, event_id('0')); @@ -1813,8 +1836,12 @@ mod tests { #[test] fn validation_receipt_verifier_rejects_non_validation_receipt_kind() { - let mut event = sample_validation_receipt_event(); - event.kind = 3434; + let sample = sample_validation_receipt_event(); + let event = validation_receipt_event_with_parts( + 3434, + sample.tags_as_vec(), + sample.content().to_owned(), + ); assert_eq!( validation_receipt_from_event(&event), Err(RadrootsValidationReceiptError::InvalidKind { @@ -1840,22 +1867,23 @@ mod tests { )) ); - let mut missing_event_set = event.clone(); - missing_event_set - .tags - .retain(|tag| tag.first().map(|value| value.as_str()) != Some("event_set_root")); + let mut tags = event.tags_as_vec(); + tags.retain(|tag| tag.first().map(|value| value.as_str()) != Some("event_set_root")); + let missing_event_set = + validation_receipt_event_with_parts(event.kind_u32(), tags, event.content().to_owned()); assert_eq!( validation_receipt_from_event(&missing_event_set), Err(RadrootsValidationReceiptError::MissingTag("event_set_root")) ); - let mut wrong_reducer_output = event.clone(); - let reducer_tag = wrong_reducer_output - .tags + let mut tags = event.tags_as_vec(); + let reducer_tag = tags .iter_mut() .find(|tag| tag.first().map(|value| value.as_str()) == Some("reducer_output_root")) .expect("reducer output tag"); reducer_tag[1] = hash32('8'); + let wrong_reducer_output = + validation_receipt_event_with_parts(event.kind_u32(), tags, event.content().to_owned()); assert_eq!( validation_receipt_from_event(&wrong_reducer_output), Err(RadrootsValidationReceiptError::TagMismatch( @@ -1863,13 +1891,14 @@ mod tests { )) ); - let mut wrong_public_values = event.clone(); - let public_values_tag = wrong_public_values - .tags + let mut tags = event.tags_as_vec(); + let public_values_tag = tags .iter_mut() .find(|tag| tag.first().map(|value| value.as_str()) == Some("public_values_hash")) .expect("public values tag"); public_values_tag[1] = hash32('b'); + let wrong_public_values = + validation_receipt_event_with_parts(event.kind_u32(), tags, event.content().to_owned()); assert_eq!( validation_receipt_from_event(&wrong_public_values), Err(RadrootsValidationReceiptError::TagMismatch( @@ -1898,9 +1927,7 @@ mod tests { receipt.proof.proof_reference = Some(format!("radroots-proof://sha256/{}", "1".repeat(64))); let parts = validation_receipt_event_build("order-1", &receipt).expect("sp1 event parts"); - let mut event = sample_validation_receipt_event(); - event.content = parts.content; - event.tags = parts.tags; + let event = validation_receipt_event_with_parts(parts.kind, parts.tags, parts.content); let verified = verify_validation_receipt_event( &event, RadrootsValidationReceiptExpectedBinding { @@ -2036,9 +2063,7 @@ mod tests { fn validation_receipt_expected_binding_enforces_sp1_identity() { let receipt = sample_sp1_reference_receipt(); let parts = validation_receipt_event_build("order-1", &receipt).expect("sp1 event parts"); - let mut event = sample_validation_receipt_event(); - event.content = parts.content; - event.tags = parts.tags; + let event = validation_receipt_event_with_parts(parts.kind, parts.tags, parts.content); verify_validation_receipt_event( &event, diff --git a/crates/transport_nostr/src/fetch.rs b/crates/transport_nostr/src/fetch.rs @@ -8,9 +8,7 @@ use radroots_event_store::{ RadrootsEventContractStatus, RadrootsEventIngest, RadrootsEventStore, RadrootsTransportObservation, RadrootsTransportObservationType, }; -use radroots_nostr::prelude::{ - RadrootsNostrClient, RadrootsNostrEvent, RadrootsNostrFilter, radroots_event_from_nostr, -}; +use radroots_nostr::prelude::{RadrootsNostrClient, RadrootsNostrEvent, RadrootsNostrFilter}; use radroots_transport::RadrootsTransportKind; use serde::{Deserialize, Serialize}; use std::sync::{Arc, Mutex, PoisonError}; @@ -360,7 +358,6 @@ where raw_json, observed_at_ms, }) => { - let event = radroots_event_from_nostr(&raw_event); let observation_type = match mode { RadrootsRelayFetchMode::Fetch => RadrootsTransportObservationType::Fetch, RadrootsRelayFetchMode::Subscription => { @@ -373,9 +370,26 @@ where observation_type, observed_at_ms, )?; - let ingest = RadrootsEventIngest::new(event, observed_at_ms) - .with_raw_json(raw_json) - .with_observation(observation); + let ingest = match RadrootsEventIngest::from_raw_json(raw_json, observed_at_ms) { + Ok(ingest) => ingest.with_observation(observation), + Err(error) => { + receipt.malformed_count += 1; + receipt.events.push(RadrootsRelayFetchEventReceipt { + relay_url, + event_id: Some(raw_event.id.to_hex()), + inserted: false, + duplicate: false, + unsupported: false, + malformed: true, + out_of_filter: false, + skipped_over_limit: false, + projection_eligible: false, + verification_status: None, + message: Some(error.to_string()), + }); + continue; + } + }; match event_store.ingest_event(ingest).await { Ok(store_receipt) => { let unsupported = diff --git a/crates/transport_nostr/src/outbox.rs b/crates/transport_nostr/src/outbox.rs @@ -6,7 +6,6 @@ use crate::{ RadrootsRelayTransportError, RadrootsRelayUrlPolicy, publish_signed_event, verified_signed_event_payload, }; -use radroots_event::RadrootsEventEnvelope; use radroots_event::draft::RadrootsSignedEvent; use radroots_event_store::{ RadrootsEventIngest, RadrootsEventStore, RadrootsTransportObservation, @@ -113,7 +112,7 @@ where .await?; return Ok(RadrootsOutboxPublishReceipt { local_ingest, - event_id: signed_event.id, + event_id: signed_event.id_str().to_owned(), attempted_count: 0, accepted_count: publishable.accepted_count, retryable_count: 0, @@ -144,13 +143,13 @@ where .with_idempotency_key(outbox_publish_idempotency_key( claimed.outbox_event_id, claimed.attempt_count, - signed_event.id.as_str(), + signed_event.id_str(), active_delivery_plan_id, )); let publish = match publish_signed_event(adapter, request).await { Ok(receipt) => receipt, Err(RadrootsRelayTransportError::Transport(message)) => adapter_transport_failure_receipt( - signed_event.id.clone(), + signed_event.id_str().to_owned(), target_strings, publishable.remaining_satisfaction_count, message, @@ -259,7 +258,7 @@ where .await?; return Ok(RadrootsOutboxPublishReceipt { local_ingest, - event_id: signed_event.id, + event_id: signed_event.id_str().to_owned(), attempted_count: 0, accepted_count: publishable.accepted_count, retryable_count: 0, @@ -283,7 +282,7 @@ where let request_id = outbox_publish_idempotency_key( claimed.outbox_event_id, claimed.attempt_count, - signed_event.id.as_str(), + signed_event.id_str(), publishable.active_delivery_plan_id, ); let payload = @@ -337,7 +336,7 @@ where Ok(RadrootsOutboxPublishReceipt { local_ingest, - event_id: signed_event.id, + event_id: signed_event.id_str().to_owned(), attempted_count: target_receipts .iter() .filter(|receipt| receipt.attempted) @@ -1030,25 +1029,12 @@ async fn ingest_publish_observation( if let Some(message) = message { observation = observation.with_redacted_message(message); } - let ingest = RadrootsEventIngest::new(event_from_signed(signed_event), observed_at_ms) - .with_raw_json(signed_event.raw_json.clone()) + let ingest = RadrootsEventIngest::new(signed_event.clone(), observed_at_ms) .with_observation(observation); event_store.ingest_event(ingest).await?; Ok(()) } -fn event_from_signed(signed_event: &RadrootsSignedEvent) -> RadrootsEventEnvelope { - RadrootsEventEnvelope { - id: signed_event.id.clone(), - author: signed_event.pubkey.clone(), - created_at: signed_event.created_at, - kind: signed_event.kind, - tags: signed_event.tags.clone(), - content: signed_event.content.clone(), - sig: signed_event.sig.clone(), - } -} - #[cfg(test)] mod tests { use super::{ diff --git a/crates/transport_nostr/src/publish.rs b/crates/transport_nostr/src/publish.rs @@ -4,7 +4,7 @@ use crate::{RadrootsRelayOutcome, RadrootsRelayTargetSet, RadrootsRelayTransport #[cfg(feature = "client")] use core::time::Duration; use futures::future::BoxFuture; -use radroots_event::draft::{RadrootsSignedEvent, RadrootsSignedEventParts}; +use radroots_event::{draft::RadrootsSignedEvent, wire::RadrootsNip01EventWire}; use radroots_transport::{ RadrootsTransport, RadrootsTransportCapabilities, RadrootsTransportDeliveryReceipt, RadrootsTransportDeliveryRequest, RadrootsTransportError, RadrootsTransportFetchReceipt, @@ -114,26 +114,20 @@ pub fn verified_signed_event_payload( ) -> Result<RadrootsTransportPayload, RadrootsTransportError> { verify_signed_event_raw_json_matches_event(signed_event)?; RadrootsTransportPayload::unchecked_signed_event_json( - signed_event.id.as_str(), - signed_event.raw_json.as_str(), + signed_event.id_str(), + signed_event.raw_json(), ) } fn verify_signed_event_raw_json_matches_event( signed_event: &RadrootsSignedEvent, ) -> Result<(), RadrootsTransportError> { - let wire: SignedEventJsonWire = serde_json::from_str(signed_event.raw_json.as_str()) + let wire = RadrootsNip01EventWire::parse_json(signed_event.raw_json()) .map_err(|_| RadrootsTransportError::InvalidPayloadBytes)?; - if wire.id != signed_event.id { + if wire.id != signed_event.id_str() { return Err(RadrootsTransportError::InvalidPayloadId); } - if wire.pubkey != signed_event.pubkey - || wire.created_at != signed_event.created_at - || wire.kind != signed_event.kind - || wire.tags != signed_event.tags - || wire.content != signed_event.content - || wire.sig != signed_event.sig - { + if &wire != signed_event.wire() { return Err(RadrootsTransportError::InvalidPayloadBytes); } Ok(()) @@ -272,17 +266,6 @@ fn nostr_error_to_transport_error(error: RadrootsRelayTransportError) -> Radroot } } -#[derive(Deserialize)] -struct SignedEventJsonWire { - id: String, - pubkey: String, - created_at: u32, - kind: u32, - tags: Vec<Vec<String>>, - content: String, - sig: String, -} - fn signed_event_from_transport_payload( payload: &RadrootsTransportPayload, ) -> Result<RadrootsSignedEvent, RadrootsTransportError> { @@ -292,22 +275,13 @@ fn signed_event_from_transport_payload( else { return Err(RadrootsTransportError::InvalidPayloadBytes); }; - let wire: SignedEventJsonWire = - serde_json::from_str(raw_json).map_err(|_| RadrootsTransportError::InvalidPayloadBytes)?; + let wire = RadrootsNip01EventWire::parse_json(raw_json) + .map_err(|_| RadrootsTransportError::InvalidPayloadBytes)?; if wire.id != *event_id { return Err(RadrootsTransportError::InvalidPayloadId); } - RadrootsSignedEvent::new(RadrootsSignedEventParts { - id: wire.id, - pubkey: wire.pubkey, - created_at: wire.created_at, - kind: wire.kind, - tags: wire.tags, - content: wire.content, - sig: wire.sig, - raw_json: raw_json.clone(), - }) - .map_err(|_| RadrootsTransportError::InvalidPayloadBytes) + RadrootsSignedEvent::from_wire_verified_id(wire, raw_json.clone()) + .map_err(|_| RadrootsTransportError::InvalidPayloadBytes) } fn relay_targets_from_transport_targets( @@ -376,7 +350,7 @@ pub async fn publish_signed_event<A>( where A: RadrootsRelayPublishAdapter, { - let event_id = request.signed_event.id.clone(); + let event_id = request.signed_event.id_str().to_owned(); let satisfaction_policy = request.satisfaction_policy.clone(); let target_count = request.targets.len(); let quorum = satisfaction_policy.required_target_count(target_count)?; @@ -497,7 +471,7 @@ impl RadrootsRelayPublishAdapter for RadrootsMockRelayPublishAdapter { self.captured_raw_events .lock() .map_err(captured_raw_event_lock_error)? - .push(request.signed_event.raw_json.clone()); + .push(request.signed_event.raw_json().to_owned()); Ok(request .targets .relays() @@ -543,7 +517,7 @@ impl RadrootsRelayPublishAdapter for RadrootsNostrClientPublishAdapter { ) -> BoxFuture<'a, Result<Vec<RadrootsRelayPublishRelayReceipt>, RadrootsRelayTransportError>> { Box::pin(async move { - let event = RadrootsNostrEvent::from_json(request.signed_event.raw_json.as_str()) + let event = RadrootsNostrEvent::from_json(request.signed_event.raw_json()) .map_err(|error| RadrootsRelayTransportError::NostrEventJson(error.to_string()))?; ensure_raw_event_matches_signed_event(&event, &request.signed_event)?; let target_strings = request.targets.relay_strings(); @@ -661,24 +635,32 @@ fn ensure_raw_event_matches_signed_event( signed_event: &RadrootsSignedEvent, ) -> Result<(), RadrootsRelayTransportError> { let mismatches = [ - ("id", event.id.to_hex(), signed_event.id.clone()), - ("pubkey", event.pubkey.to_hex(), signed_event.pubkey.clone()), + ("id", event.id.to_hex(), signed_event.id_str().to_owned()), + ( + "pubkey", + event.pubkey.to_hex(), + signed_event.pubkey_str().to_owned(), + ), ( "created_at", event.created_at.as_secs().to_string(), - signed_event.created_at.to_string(), + signed_event.created_at().to_string(), ), ( "kind", (event.kind.as_u16() as u32).to_string(), - signed_event.kind.to_string(), + signed_event.kind().to_string(), ), ( "content", event.content.clone(), - signed_event.content.clone(), + signed_event.content().to_owned(), + ), + ( + "sig", + event.sig.to_string(), + signed_event.sig_str().to_owned(), ), - ("sig", event.sig.to_string(), signed_event.sig.clone()), ]; for (field, raw, wrapped) in mismatches { if raw != wrapped { @@ -692,7 +674,7 @@ fn ensure_raw_event_matches_signed_event( .iter() .map(|tag| tag.as_slice().to_vec()) .collect::<Vec<_>>(); - if raw_tags != signed_event.tags { + if raw_tags != signed_event.tags_as_vec() { return Err(RadrootsRelayTransportError::NostrEventJson( "raw event JSON tags do not match signed event tags".to_owned(), )); @@ -706,6 +688,7 @@ mod tests { use nostr::JsonUtil; use radroots_event::draft::{RadrootsEventDraft, RadrootsSignedEvent}; use radroots_event::kinds::KIND_POST; + use radroots_event::wire::RadrootsNip01EventWire; use radroots_nostr::prelude::{ RadrootsNostrKeys, RadrootsNostrSecretKey, radroots_nostr_sign_frozen_draft, }; @@ -729,8 +712,7 @@ mod tests { ) .expect("draft"); let signed_event = radroots_nostr_sign_frozen_draft(&keys, &draft).expect("signed event"); - let raw_event = - RadrootsNostrEvent::from_json(signed_event.raw_json.as_str()).expect("raw event"); + let raw_event = RadrootsNostrEvent::from_json(signed_event.raw_json()).expect("raw event"); (raw_event, signed_event) } @@ -738,39 +720,45 @@ mod tests { assert!(ensure_raw_event_matches_signed_event(raw_event, &signed_event).is_err()); } + fn signed_event_with_wire( + original: &RadrootsSignedEvent, + wire: RadrootsNip01EventWire, + ) -> RadrootsSignedEvent { + RadrootsSignedEvent::from_wire_unchecked(wire, original.raw_json().to_owned()) + .expect("signed event") + } + #[test] fn raw_event_match_guard_accepts_exact_event_and_rejects_field_mismatches() { let (raw_event, signed_event) = signed_post("matched"); ensure_raw_event_matches_signed_event(&raw_event, &signed_event).expect("matching event"); - let mut mismatched = signed_event.clone(); - mismatched.id = "00".repeat(32); - assert_mismatch(&raw_event, mismatched); + let mut wire = signed_event.wire().clone(); + wire.id = "00".repeat(32); + assert_mismatch(&raw_event, signed_event_with_wire(&signed_event, wire)); - let mut mismatched = signed_event.clone(); - mismatched.pubkey = "11".repeat(32); - assert_mismatch(&raw_event, mismatched); + let mut wire = signed_event.wire().clone(); + wire.pubkey = "11".repeat(32); + assert_mismatch(&raw_event, signed_event_with_wire(&signed_event, wire)); - let mut mismatched = signed_event.clone(); - mismatched.created_at += 1; - assert_mismatch(&raw_event, mismatched); + let mut wire = signed_event.wire().clone(); + wire.created_at += 1; + assert_mismatch(&raw_event, signed_event_with_wire(&signed_event, wire)); - let mut mismatched = signed_event.clone(); - mismatched.kind += 1; - assert_mismatch(&raw_event, mismatched); + let mut wire = signed_event.wire().clone(); + wire.kind += 1; + assert_mismatch(&raw_event, signed_event_with_wire(&signed_event, wire)); - let mut mismatched = signed_event.clone(); - mismatched.content.push_str(" changed"); - assert_mismatch(&raw_event, mismatched); + let mut wire = signed_event.wire().clone(); + wire.content.push_str(" changed"); + assert_mismatch(&raw_event, signed_event_with_wire(&signed_event, wire)); - let mut mismatched = signed_event.clone(); - mismatched.sig = "22".repeat(64); - assert_mismatch(&raw_event, mismatched); + let mut wire = signed_event.wire().clone(); + wire.sig = "22".repeat(64); + assert_mismatch(&raw_event, signed_event_with_wire(&signed_event, wire)); - let mut mismatched = signed_event; - mismatched - .tags - .push(vec!["t".to_owned(), "compost".to_owned()]); - assert_mismatch(&raw_event, mismatched); + let mut wire = signed_event.wire().clone(); + wire.tags.push(vec!["t".to_owned(), "compost".to_owned()]); + assert_mismatch(&raw_event, signed_event_with_wire(&signed_event, wire)); } } diff --git a/crates/transport_nostr/tests/transport.rs b/crates/transport_nostr/tests/transport.rs @@ -329,8 +329,7 @@ fn post_relay_fetch_request(observed_at_ms: i64, max_events: usize) -> RadrootsR fn tampered_raw_event() -> String { let signed = signed_post("trusted"); - let mut value = - serde_json::from_str::<serde_json::Value>(signed.raw_json.as_str()).expect("raw json"); + let mut value = serde_json::from_str::<serde_json::Value>(signed.raw_json()).expect("raw json"); value["content"] = serde_json::Value::String("tampered".to_owned()); serde_json::to_string(&value).expect("tampered json") } @@ -712,7 +711,10 @@ async fn mock_publish_preserves_exact_raw_json_and_counts_outcomes() { .await .expect("publish"); - assert_eq!(adapter.captured_raw_events(), vec![signed.raw_json]); + assert_eq!( + adapter.captured_raw_events(), + vec![signed.raw_json().to_owned()] + ); assert_eq!(receipt.attempted_count, 3); assert_eq!(receipt.accepted_count, 2); assert_eq!(receipt.retryable_count, 1); @@ -729,8 +731,8 @@ async fn nostr_transport_facade_delivers_signed_event_payloads() { let request = RadrootsTransportDeliveryRequest::new( "facade-request-1", RadrootsTransportPayload::unchecked_signed_event_json( - signed.id.clone(), - signed.raw_json.clone(), + signed.id_str().to_owned(), + signed.raw_json().to_owned(), ) .expect("payload"), RadrootsTransportTargetSet::new(vec![target.clone()]).expect("targets"), @@ -740,7 +742,10 @@ async fn nostr_transport_facade_delivers_signed_event_payloads() { let receipt = transport.deliver(request).await.expect("delivery"); let status = transport.status().await.expect("status"); - assert_eq!(adapter.captured_raw_events(), vec![signed.raw_json]); + assert_eq!( + adapter.captured_raw_events(), + vec![signed.raw_json().to_owned()] + ); assert!(status.capabilities.deliver); assert!(!status.capabilities.fetch); assert_eq!(receipt.request_id, "facade-request-1"); @@ -770,12 +775,12 @@ fn verified_signed_event_payload_preserves_transport_payload_identity() { panic!("signed event payload expected"); }; - assert_eq!(event_id, signed.id); - assert_eq!(raw_json, signed.raw_json); + assert_eq!(event_id, signed.id_str()); + assert_eq!(raw_json, signed.raw_json().to_owned()); assert_eq!(digest.len(), 64); - let mut mismatched = signed; - mismatched.raw_json = "{}".to_owned(); + let mismatched = + RadrootsSignedEvent::from_wire_unchecked(signed.wire().clone(), "{}").expect("mismatch"); assert_eq!( verified_signed_event_payload(&mismatched).expect_err("mismatched raw json"), RadrootsTransportError::InvalidPayloadBytes @@ -820,8 +825,8 @@ async fn nostr_transport_facade_rejects_unsupported_payloads_and_targets() { .deliver(RadrootsTransportDeliveryRequest::new( "facade-request-target", RadrootsTransportPayload::unchecked_signed_event_json( - signed.id.clone(), - signed.raw_json.clone(), + signed.id_str().to_owned(), + signed.raw_json().to_owned(), ) .expect("payload"), RadrootsTransportTargetSet::new(vec![non_nostr_target]).expect("targets"), @@ -846,8 +851,8 @@ async fn nostr_transport_facade_matches_canonical_equivalent_relay_receipts() { .deliver(RadrootsTransportDeliveryRequest::new( "facade-canonical-receipt", RadrootsTransportPayload::unchecked_signed_event_json( - signed.id.clone(), - signed.raw_json.clone(), + signed.id_str().to_owned(), + signed.raw_json().to_owned(), ) .expect("payload"), RadrootsTransportTargetSet::new(vec![target.clone()]).expect("target set"), @@ -894,8 +899,8 @@ async fn nostr_transport_facade_preserves_scoped_duplicate_target_metadata() { let request = RadrootsTransportDeliveryRequest::new( "facade-request-scoped", RadrootsTransportPayload::unchecked_signed_event_json( - signed.id.clone(), - signed.raw_json.clone(), + signed.id_str().to_owned(), + signed.raw_json().to_owned(), ) .expect("payload"), RadrootsTransportTargetSet::new(vec![first.clone(), second.clone()]).expect("targets"), @@ -932,7 +937,7 @@ async fn publish_receipts_track_terminal_skipped_and_adapter_errors() { .await .expect("publish"); - assert_eq!(receipt.event_id, signed.id); + assert_eq!(receipt.event_id, signed.id_str()); assert_eq!(receipt.attempted_count, 2); assert_eq!(receipt.accepted_count, 1); assert_eq!(receipt.retryable_count, 0); @@ -1089,11 +1094,11 @@ fn fetch_requests_reject_zero_limits_and_timeouts() { #[test] fn fetch_blocking_facade_runs_mock_adapter() { let signed = signed_post("blocking fetch"); - let accepted_id = signed.id.clone(); + let accepted_id = signed.id_str().to_owned(); let adapter = RadrootsMockRelayFetchAdapter::new(vec![ RadrootsRelayFetchItem::Event { relay_url: RELAY_PRIMARY_WSS.to_owned(), - raw_json: signed.raw_json, + raw_json: signed.raw_json().to_owned(), observed_at_ms: 1_090, }, RadrootsRelayFetchItem::Eose { @@ -1116,12 +1121,12 @@ async fn fetch_ingests_events_and_records_transport_observations() { let adapter = RadrootsMockRelayFetchAdapter::new(vec![ RadrootsRelayFetchItem::Event { relay_url: RELAY_PRIMARY_WSS.to_owned(), - raw_json: signed.raw_json.clone(), + raw_json: signed.raw_json().to_owned(), observed_at_ms: 1_000, }, RadrootsRelayFetchItem::Event { relay_url: RELAY_PRIMARY_WSS.to_owned(), - raw_json: signed.raw_json.clone(), + raw_json: signed.raw_json().to_owned(), observed_at_ms: 1_001, }, RadrootsRelayFetchItem::Event { @@ -1222,7 +1227,7 @@ async fn fetch_ingests_events_and_records_transport_observations() { assert!(!receipt.events[4].projection_eligible); let observations = store - .observations_for_event(signed.id.as_str()) + .observations_for_event(signed.id_str()) .await .expect("observations"); assert_eq!(observations.len(), 1); @@ -1245,12 +1250,12 @@ async fn fetch_rejects_out_of_filter_events_before_store_mutation() { let adapter = RadrootsMockRelayFetchAdapter::new(vec![ RadrootsRelayFetchItem::Event { relay_url: RELAY_PRIMARY_WSS.to_owned(), - raw_json: wrong_tag.raw_json.clone(), + raw_json: wrong_tag.raw_json().to_owned(), observed_at_ms: 1_005, }, RadrootsRelayFetchItem::Event { relay_url: RELAY_PRIMARY_WSS.to_owned(), - raw_json: accepted.raw_json.clone(), + raw_json: accepted.raw_json().to_owned(), observed_at_ms: 1_006, }, RadrootsRelayFetchItem::Event { @@ -1289,14 +1294,14 @@ async fn fetch_rejects_out_of_filter_events_before_store_mutation() { assert!(receipt.events[2].out_of_filter); assert!( store - .get_event(accepted.id.as_str()) + .get_event(accepted.id_str()) .await .expect("accepted lookup") .is_some() ); assert!( store - .get_event(wrong_tag.id.as_str()) + .get_event(wrong_tag.id_str()) .await .expect("wrong tag lookup") .is_none() @@ -1315,9 +1320,9 @@ async fn fetch_event_cap_counts_accepted_in_filter_events_and_preserves_later_co let accepted = signed_post("accepted capped event"); let skipped = signed_post("skipped capped event"); let wrong_tag = signed_event_with_kind_and_hashtag("wrong capped tag", KIND_POST, "compost"); - let accepted_id = accepted.id.clone(); - let skipped_id = skipped.id.clone(); - let wrong_tag_id = wrong_tag.id.clone(); + let accepted_id = accepted.id_str().to_owned(); + let skipped_id = skipped.id_str().to_owned(); + let wrong_tag_id = wrong_tag.id_str().to_owned(); let store = RadrootsEventStore::open_memory().await.expect("store"); let adapter = RadrootsMockRelayFetchAdapter::new(vec![ RadrootsRelayFetchItem::Event { @@ -1327,17 +1332,17 @@ async fn fetch_event_cap_counts_accepted_in_filter_events_and_preserves_later_co }, RadrootsRelayFetchItem::Event { relay_url: RELAY_PRIMARY_WSS.to_owned(), - raw_json: wrong_tag.raw_json, + raw_json: wrong_tag.raw_json().to_owned(), observed_at_ms: 1_100, }, RadrootsRelayFetchItem::Event { relay_url: RELAY_PRIMARY_WSS.to_owned(), - raw_json: accepted.raw_json.clone(), + raw_json: accepted.raw_json().to_owned(), observed_at_ms: 1_101, }, RadrootsRelayFetchItem::Event { relay_url: RELAY_PRIMARY_WSS.to_owned(), - raw_json: skipped.raw_json, + raw_json: skipped.raw_json().to_owned(), observed_at_ms: 1_102, }, RadrootsRelayFetchItem::Eose { @@ -1426,7 +1431,7 @@ async fn fetch_relay_events_applies_shared_filter_limit_and_outcome_evidence() { vec!["soil".to_owned()], ) .expect("filter"); - let accepted_id = accepted.id.clone(); + let accepted_id = accepted.id_str().to_owned(); let adapter = RadrootsMockRelayFetchAdapter::new(vec![ RadrootsRelayFetchItem::Event { relay_url: RELAY_PRIMARY_WSS.to_owned(), @@ -1435,17 +1440,17 @@ async fn fetch_relay_events_applies_shared_filter_limit_and_outcome_evidence() { }, RadrootsRelayFetchItem::Event { relay_url: RELAY_PRIMARY_WSS.to_owned(), - raw_json: wrong_tag.raw_json, + raw_json: wrong_tag.raw_json().to_owned(), observed_at_ms: 2_101, }, RadrootsRelayFetchItem::Event { relay_url: RELAY_PRIMARY_WSS.to_owned(), - raw_json: accepted.raw_json.clone(), + raw_json: accepted.raw_json().to_owned(), observed_at_ms: 2_102, }, RadrootsRelayFetchItem::Event { relay_url: RELAY_PRIMARY_WSS.to_owned(), - raw_json: skipped.raw_json, + raw_json: skipped.raw_json().to_owned(), observed_at_ms: 2_103, }, RadrootsRelayFetchItem::Eose { @@ -1496,7 +1501,7 @@ async fn fetch_relay_events_applies_shared_filter_limit_and_outcome_evidence() { async fn fetch_raw_scan_limit_bounds_noisy_adapter_output() { let accepted = signed_post("raw scan accepted event"); let wrong_tag = signed_event_with_kind_and_hashtag("raw scan wrong tag", KIND_POST, "compost"); - let accepted_id = accepted.id.clone(); + let accepted_id = accepted.id_str().to_owned(); let store = RadrootsEventStore::open_memory().await.expect("store"); let adapter = RadrootsMockRelayFetchAdapter::new(vec![ RadrootsRelayFetchItem::Event { @@ -1506,12 +1511,12 @@ async fn fetch_raw_scan_limit_bounds_noisy_adapter_output() { }, RadrootsRelayFetchItem::Event { relay_url: RELAY_PRIMARY_WSS.to_owned(), - raw_json: wrong_tag.raw_json, + raw_json: wrong_tag.raw_json().to_owned(), observed_at_ms: 1_131, }, RadrootsRelayFetchItem::Event { relay_url: RELAY_PRIMARY_WSS.to_owned(), - raw_json: accepted.raw_json, + raw_json: accepted.raw_json().to_owned(), observed_at_ms: 1_132, }, RadrootsRelayFetchItem::Eose { @@ -1550,7 +1555,7 @@ async fn fetch_subscription_mode_and_store_errors_are_reported() { let store = RadrootsEventStore::open_memory().await.expect("store"); let adapter = RadrootsMockRelayFetchAdapter::new(vec![RadrootsRelayFetchItem::Event { relay_url: RELAY_PRIMARY_WSS.to_owned(), - raw_json: signed.raw_json.clone(), + raw_json: signed.raw_json().to_owned(), observed_at_ms: 1_200, }]); @@ -1565,7 +1570,7 @@ async fn fetch_subscription_mode_and_store_errors_are_reported() { assert_eq!(receipt.inserted_count, 1); let observations = store - .observations_for_event(signed.id.as_str()) + .observations_for_event(signed.id_str()) .await .expect("observations"); assert_eq!(observations.len(), 1); @@ -1578,7 +1583,7 @@ async fn fetch_subscription_mode_and_store_errors_are_reported() { closed_store.pool().close().await; let adapter = RadrootsMockRelayFetchAdapter::new(vec![RadrootsRelayFetchItem::Event { relay_url: RELAY_PRIMARY_WSS.to_owned(), - raw_json: signed.raw_json, + raw_json: signed.raw_json().to_owned(), observed_at_ms: 1_210, }]); let receipt = @@ -1598,7 +1603,7 @@ async fn fetch_ingest_rejects_invalid_observation_endpoint() { let store = RadrootsEventStore::open_memory().await.expect("store"); let adapter = RadrootsMockRelayFetchAdapter::new(vec![RadrootsRelayFetchItem::Event { relay_url: " ".to_owned(), - raw_json: signed.raw_json, + raw_json: signed.raw_json().to_owned(), observed_at_ms: 1_300, }]); @@ -1625,10 +1630,10 @@ async fn outbox_publish_persists_partial_success_and_skips_accepted_retry() { let draft = RadrootsEventDraft::new( "radroots.social.post.v1", KIND_POST, - signed.created_at, - signed.tags.clone(), - signed.content.clone(), - signed.pubkey.as_str(), + signed.created_at(), + signed.tags_as_vec(), + signed.content().to_owned(), + signed.pubkey_str(), ) .expect("draft"); let receipt = outbox @@ -1733,7 +1738,7 @@ async fn outbox_publish_persists_partial_success_and_skips_accepted_retry() { .await .expect("retry publish"); - assert_eq!(second.local_ingest.event_id, signed.id); + assert_eq!(second.local_ingest.event_id, signed.id_str()); assert_eq!(second.attempted_count, 1); assert_eq!(retry_adapter.captured_raw_events().len(), 1); @@ -1751,7 +1756,7 @@ async fn outbox_publish_persists_partial_success_and_skips_accepted_retry() { assert_eq!(operation.status, RadrootsOutboxOperationStatus::Complete); let observations = store - .observations_for_event(signed.id.as_str()) + .observations_for_event(signed.id_str()) .await .expect("observations"); assert_outbox_publish_observations(&observations, 3); @@ -1765,10 +1770,10 @@ async fn outbox_publish_fans_out_endpoint_receipts_to_scoped_logical_targets() { let draft = RadrootsEventDraft::new( "radroots.social.post.v1", KIND_POST, - signed.created_at, - signed.tags.clone(), - signed.content.clone(), - signed.pubkey.as_str(), + signed.created_at(), + signed.tags_as_vec(), + signed.content().to_owned(), + signed.pubkey_str(), ) .expect("draft"); let receipt = outbox @@ -1813,8 +1818,8 @@ async fn outbox_publish_fans_out_endpoint_receipts_to_scoped_logical_targets() { .await .expect("publish"); - assert_eq!(published.local_ingest.event_id, signed.id); - assert_eq!(published.event_id, signed.id); + assert_eq!(published.local_ingest.event_id, signed.id_str()); + assert_eq!(published.event_id, signed.id_str()); assert_eq!(published.attempted_count, 2); assert_eq!(published.accepted_count, 2); assert_eq!(published.retryable_count, 0); @@ -1865,7 +1870,7 @@ async fn outbox_publish_fans_out_endpoint_receipts_to_scoped_logical_targets() { && target.target_label.as_ref().map(|label| label.as_str()) == Some("East foodshed") })); let observations = store - .observations_for_event(signed.id.as_str()) + .observations_for_event(signed.id_str()) .await .expect("observations"); assert_outbox_publish_observations(&observations, 1); @@ -1879,10 +1884,10 @@ async fn outbox_publish_required_target_failure_is_not_satisfied_by_optional_suc let draft = RadrootsEventDraft::new( "radroots.social.post.v1", KIND_POST, - signed.created_at, - signed.tags.clone(), - signed.content.clone(), - signed.pubkey.as_str(), + signed.created_at(), + signed.tags_as_vec(), + signed.content().to_owned(), + signed.pubkey_str(), ) .expect("draft"); let optional = nostr_target(RELAY_PRIMARY_WSS); @@ -1982,10 +1987,10 @@ async fn outbox_publish_required_target_success_is_not_blocked_by_optional_retry let draft = RadrootsEventDraft::new( "radroots.social.post.v1", KIND_POST, - signed.created_at, - signed.tags.clone(), - signed.content.clone(), - signed.pubkey.as_str(), + signed.created_at(), + signed.tags_as_vec(), + signed.content().to_owned(), + signed.pubkey_str(), ) .expect("draft"); let optional = nostr_target(RELAY_PRIMARY_WSS); @@ -2049,7 +2054,7 @@ async fn outbox_publish_required_target_success_is_not_blocked_by_optional_retry .await .expect("publish"); - assert_eq!(published.local_ingest.event_id, signed.id); + assert_eq!(published.local_ingest.event_id, signed.id_str()); assert_eq!(published.attempted_count, 1); assert_eq!(published.accepted_count, 1); assert_eq!(published.retryable_count, 0); @@ -2074,7 +2079,7 @@ async fn outbox_publish_required_target_success_is_not_blocked_by_optional_retry && target.status == RadrootsOutboxDeliveryTargetStatus::Accepted })); let observations = store - .observations_for_event(signed.id.as_str()) + .observations_for_event(signed.id_str()) .await .expect("observations"); assert_outbox_publish_observations(&observations, 1); @@ -2088,10 +2093,10 @@ async fn outbox_publish_required_targets_fan_out_same_endpoint_scoped_receipts() let draft = RadrootsEventDraft::new( "radroots.social.post.v1", KIND_POST, - signed.created_at, - signed.tags.clone(), - signed.content.clone(), - signed.pubkey.as_str(), + signed.created_at(), + signed.tags_as_vec(), + signed.content().to_owned(), + signed.pubkey_str(), ) .expect("draft"); let required = scoped_nostr_target(RELAY_PRIMARY_WSS, "foodshed.west", "West foodshed"); @@ -2178,10 +2183,10 @@ async fn outbox_transport_publish_failure_releases_retryable_claim() { let draft = RadrootsEventDraft::new( "radroots.social.post.v1", KIND_POST, - signed.created_at, - signed.tags.clone(), - signed.content.clone(), - signed.pubkey.as_str(), + signed.created_at(), + signed.tags_as_vec(), + signed.content().to_owned(), + signed.pubkey_str(), ) .expect("draft"); let receipt = outbox @@ -2269,10 +2274,10 @@ async fn outbox_publish_marks_published_without_adapter_when_all_relays_already_ let draft = RadrootsEventDraft::new( "radroots.social.post.v1", KIND_POST, - signed.created_at, - signed.tags.clone(), - signed.content.clone(), - signed.pubkey.as_str(), + signed.created_at(), + signed.tags_as_vec(), + signed.content().to_owned(), + signed.pubkey_str(), ) .expect("draft"); let receipt = outbox @@ -2325,8 +2330,8 @@ async fn outbox_publish_marks_published_without_adapter_when_all_relays_already_ .await .expect("publish"); - assert_eq!(published.local_ingest.event_id, signed.id); - assert_eq!(published.event_id, signed.id); + assert_eq!(published.local_ingest.event_id, signed.id_str()); + assert_eq!(published.event_id, signed.id_str()); assert_eq!(published.attempted_count, 0); assert_eq!(published.accepted_count, 2); assert_eq!(published.quorum, 0); @@ -2358,10 +2363,10 @@ async fn outbox_publish_ignores_unknown_adapter_receipts() { let draft = RadrootsEventDraft::new( "radroots.social.post.v1", KIND_POST, - signed.created_at, - signed.tags.clone(), - signed.content.clone(), - signed.pubkey.as_str(), + signed.created_at(), + signed.tags_as_vec(), + signed.content().to_owned(), + signed.pubkey_str(), ) .expect("draft"); let receipt = outbox @@ -2406,7 +2411,7 @@ async fn outbox_publish_ignores_unknown_adapter_receipts() { .expect("event"); assert_eq!(event.state, RadrootsOutboxEventState::Published); let observations = store - .observations_for_event(signed.id.as_str()) + .observations_for_event(signed.id_str()) .await .expect("observations"); assert_outbox_publish_observations(&observations, 1); @@ -2424,10 +2429,10 @@ async fn outbox_publish_skips_non_nostr_targets() { let draft = RadrootsEventDraft::new( "radroots.social.post.v1", KIND_POST, - signed.created_at, - signed.tags.clone(), - signed.content.clone(), - signed.pubkey.as_str(), + signed.created_at(), + signed.tags_as_vec(), + signed.content().to_owned(), + signed.pubkey_str(), ) .expect("draft"); let receipt = outbox @@ -2498,10 +2503,10 @@ async fn outbox_publish_marks_published_when_delivery_plan_satisfaction_is_met_w let draft = RadrootsEventDraft::new( "radroots.social.post.v1", KIND_POST, - signed.created_at, - signed.tags.clone(), - signed.content.clone(), - signed.pubkey.as_str(), + signed.created_at(), + signed.tags_as_vec(), + signed.content().to_owned(), + signed.pubkey_str(), ) .expect("draft"); let receipt = outbox @@ -2589,7 +2594,7 @@ async fn outbox_publish_marks_published_when_delivery_plan_satisfaction_is_met_w ); let observations = store - .observations_for_event(signed.id.as_str()) + .observations_for_event(signed.id_str()) .await .expect("observations"); assert_outbox_publish_observations(&observations, 2); @@ -2603,10 +2608,10 @@ async fn outbox_publish_republishes_accepted_relays_when_policy_requests_it() { let draft = RadrootsEventDraft::new( "radroots.social.post.v1", KIND_POST, - signed.created_at, - signed.tags.clone(), - signed.content.clone(), - signed.pubkey.as_str(), + signed.created_at(), + signed.tags_as_vec(), + signed.content().to_owned(), + signed.pubkey_str(), ) .expect("draft"); let receipt = outbox @@ -2654,7 +2659,7 @@ async fn outbox_publish_republishes_accepted_relays_when_policy_requests_it() { .await .expect("publish"); - assert_eq!(published.local_ingest.event_id, signed.id); + assert_eq!(published.local_ingest.event_id, signed.id_str()); assert_eq!(published.attempted_count, 2); assert_eq!(published.accepted_count, 2); assert_eq!(published.quorum, 1); @@ -2686,10 +2691,10 @@ async fn outbox_publish_republish_policy_keeps_terminal_targets_excluded() { let draft = RadrootsEventDraft::new( "radroots.social.post.v1", KIND_POST, - signed.created_at, - signed.tags.clone(), - signed.content.clone(), - signed.pubkey.as_str(), + signed.created_at(), + signed.tags_as_vec(), + signed.content().to_owned(), + signed.pubkey_str(), ) .expect("draft"); let receipt = outbox @@ -2766,10 +2771,10 @@ async fn outbox_publish_requires_claimed_signed_event() { let draft = RadrootsEventDraft::new( "radroots.social.post.v1", KIND_POST, - signed.created_at, - signed.tags, - signed.content, - signed.pubkey.as_str(), + signed.created_at(), + signed.tags_as_vec(), + signed.content().to_owned(), + signed.pubkey_str(), ) .expect("draft"); let receipt = outbox @@ -2813,10 +2818,10 @@ async fn outbox_publish_propagates_non_transport_adapter_errors_after_target_fil let draft = RadrootsEventDraft::new( "radroots.social.post.v1", KIND_POST, - signed.created_at, - signed.tags, - signed.content, - signed.pubkey.as_str(), + signed.created_at(), + signed.tags_as_vec(), + signed.content().to_owned(), + signed.pubkey_str(), ) .expect("draft"); let receipt = outbox @@ -2870,10 +2875,10 @@ async fn outbox_publish_rejects_invalid_relay_target_uri_before_adapter_publish( let draft = RadrootsEventDraft::new( "radroots.social.post.v1", KIND_POST, - signed.created_at, - signed.tags, - signed.content, - signed.pubkey.as_str(), + signed.created_at(), + signed.tags_as_vec(), + signed.content().to_owned(), + signed.pubkey_str(), ) .expect("draft"); let receipt = outbox @@ -2933,7 +2938,7 @@ async fn smoke_relay_fetch_processes_one_thousand_event_receipts() { }; items.push(RadrootsRelayFetchItem::Event { relay_url: relay_url.to_owned(), - raw_json: signed.raw_json, + raw_json: signed.raw_json().to_owned(), observed_at_ms: 10_000 + index, }); }