commit f2f14a5901d602adf08526cf0777ec1cafb641df
parent c0da3c5ae7b45a671a0df16c1789d82aa89f7afe
Author: triesap <tyson@radroots.org>
Date: Tue, 14 Jul 2026 09:51:27 +0000
transport: require raw publish event JSON
- replace transport publish event structs with raw JSON ingress
- remove stale event kind narrowing from publish protocol surfaces
- align replica sync with sealed event envelopes and u64 event-head timestamps
- update transport fetch expectations for ID-verified raw wire ingestion
Diffstat:
6 files changed, 237 insertions(+), 250 deletions(-)
diff --git a/crates/replica_schema/src/models/nostr_event_head.rs b/crates/replica_schema/src/models/nostr_event_head.rs
@@ -12,7 +12,7 @@ pub struct NostrEventHead {
pub pubkey: String,
pub d_tag: String,
pub last_event_id: String,
- pub last_created_at: u32,
+ pub last_created_at: u64,
pub content_hash: String,
}
@@ -23,7 +23,7 @@ pub struct INostrEventHeadFields {
pub pubkey: String,
pub d_tag: String,
pub last_event_id: String,
- pub last_created_at: u32,
+ pub last_created_at: u64,
pub content_hash: String,
}
@@ -48,7 +48,7 @@ pub struct INostrEventHeadFieldsFilter {
pub pubkey: Option<String>,
pub d_tag: Option<String>,
pub last_event_id: Option<String>,
- pub last_created_at: Option<u32>,
+ pub last_created_at: Option<u64>,
pub content_hash: Option<String>,
}
diff --git a/crates/replica_sync/src/emit.rs b/crates/replica_sync/src/emit.rs
@@ -18,11 +18,11 @@ use radroots_event::profile::{
RADROOTS_PROFILE_TYPE_TAG_KEY, RadrootsProfile, RadrootsProfileType,
radroots_profile_type_from_tag_value, radroots_profile_type_tag_value,
};
+use radroots_event::wire::RadrootsNip01EventWireParts;
use radroots_event_codec::farm::encode as farm_encode;
use radroots_event_codec::farm::list_sets as farm_list_sets;
use radroots_event_codec::list_set::encode as list_set_encode;
use radroots_event_codec::plot::encode as plot_encode;
-use radroots_event_codec::wire::WireEventParts;
use radroots_replica_schema::farm::{
Farm, IFarmFieldsFilter, IFarmFindMany, IFarmFindOne, IFarmFindOneArgs,
};
@@ -185,7 +185,7 @@ pub fn radroots_replica_farm_event(
};
let tags = farm_encode::farm_build_tags(&farm_event)?;
let content = canonical_json_string(&farm_event)?;
- let parts = WireEventParts {
+ let parts = RadrootsNip01EventWireParts {
kind: KIND_FARM,
content,
tags,
@@ -215,7 +215,7 @@ pub fn radroots_replica_plot_events(
};
let tags = plot_encode::plot_build_tags(&plot_event)?;
let content = canonical_json_string(&plot_event)?;
- let parts = WireEventParts {
+ let parts = RadrootsNip01EventWireParts {
kind: KIND_PLOT,
content,
tags,
@@ -659,7 +659,7 @@ fn compare_relation_rows(a: &RelationRow, b: &RelationRow) -> core::cmp::Orderin
fn list_set_to_wire_parts(
list_set: &radroots_event::list_set::RadrootsListSet,
-) -> Result<WireEventParts, RadrootsReplicaEventsError> {
+) -> Result<RadrootsNip01EventWireParts, RadrootsReplicaEventsError> {
#[cfg(test)]
if failpoints::take_list_set_to_wire_error() {
return Err(RadrootsReplicaEventsError::InvalidData(
@@ -907,7 +907,7 @@ fn load_member_claims_for_member(
Ok(result.results)
}
-fn parts_to_draft(author: &str, parts: WireEventParts) -> RadrootsReplicaEventDraft {
+fn parts_to_draft(author: &str, parts: RadrootsNip01EventWireParts) -> RadrootsReplicaEventDraft {
RadrootsReplicaEventDraft {
kind: parts.kind,
author: author.to_string(),
diff --git a/crates/replica_sync/src/ingest.rs b/crates/replica_sync/src/ingest.rs
@@ -13,6 +13,8 @@ use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use radroots_core::RadrootsCoreDecimal;
use radroots_event::RadrootsEventEnvelope;
+#[cfg(test)]
+use radroots_event::RadrootsEventEnvelopeParts;
use radroots_event::event_head::{
RadrootsCurrentEventHead, RadrootsEventHeadCandidateResult, RadrootsEventHeadCoordinate,
RadrootsEventHeadDecision as ProtocolEventHeadDecision, event_head_candidate_for_event,
@@ -192,7 +194,7 @@ fn ingest_event_inner(
event: &RadrootsEventEnvelope,
factory: &dyn RadrootsReplicaIdFactory,
) -> Result<RadrootsReplicaIngestOutcome, RadrootsReplicaEventsError> {
- match event.kind {
+ match event.kind_u32() {
KIND_PROFILE => ingest_profile_event(exec, event),
KIND_FARM => ingest_farm_event(exec, event, factory),
KIND_PLOT => ingest_plot_event(exec, event, factory),
@@ -200,7 +202,7 @@ fn ingest_event_inner(
kind if is_nip51_list_set_kind(kind) => ingest_list_set_event(exec, event),
_ => Err(RadrootsReplicaEventsError::InvalidData(format!(
"unsupported kind {}",
- event.kind
+ event.kind_u32()
))),
}
}
@@ -210,12 +212,12 @@ fn ingest_profile_event(
event: &RadrootsEventEnvelope,
) -> Result<RadrootsReplicaIngestOutcome, RadrootsReplicaEventsError> {
let data_result = profile_decode::data_from_event(
- event.id.clone(),
- event.author.clone(),
- event.created_at,
- event.kind,
- event.content.clone(),
- event.tags.clone(),
+ event.id_str().to_owned(),
+ event.author_str().to_owned(),
+ event.created_at_u64(),
+ event.kind_u32(),
+ event.content().to_owned(),
+ event.tags_as_vec(),
);
let data = data_result?;
let profile_type = match data.data.profile_type {
@@ -301,7 +303,8 @@ fn ingest_farm_event(
event: &RadrootsEventEnvelope,
_factory: &dyn RadrootsReplicaIdFactory,
) -> Result<RadrootsReplicaIngestOutcome, RadrootsReplicaEventsError> {
- let farm = farm_decode::farm_from_event(event.kind, &event.tags, &event.content)?;
+ let farm =
+ farm_decode::farm_from_event(event.kind_u32(), &event.tags_as_vec(), event.content())?;
let decision = event_head_decision(exec, event)?;
if !decision.apply {
return Ok(RadrootsReplicaIngestOutcome::Skipped);
@@ -312,7 +315,7 @@ fn ingest_farm_event(
created_at: None,
updated_at: None,
d_tag: Some(farm.d_tag.clone()),
- pubkey: Some(event.author.clone()),
+ pubkey: Some(event.author_str().to_owned()),
name: None,
about: None,
website: None,
@@ -336,7 +339,7 @@ fn ingest_farm_event(
let farm_id = if let Some(row) = existing.results.first() {
let fields = IFarmFieldsPartial {
d_tag: Some(Value::from(farm.d_tag.clone())),
- pubkey: Some(Value::from(event.author.clone())),
+ pubkey: Some(Value::from(event.author_str().to_owned())),
name: Some(Value::from(farm.name.clone())),
about: to_value_opt(farm.about.clone()),
website: to_value_opt(farm.website.clone()),
@@ -359,7 +362,7 @@ fn ingest_farm_event(
} else {
let fields = IFarmFields {
d_tag: farm.d_tag.clone(),
- pubkey: event.author.clone(),
+ pubkey: event.author_str().to_owned(),
name: farm.name.clone(),
about: farm.about.clone(),
website: farm.website.clone(),
@@ -384,7 +387,8 @@ fn ingest_plot_event(
event: &RadrootsEventEnvelope,
factory: &dyn RadrootsReplicaIdFactory,
) -> Result<RadrootsReplicaIngestOutcome, RadrootsReplicaEventsError> {
- let plot = plot_decode::plot_from_event(event.kind, &event.tags, &event.content)?;
+ let plot =
+ plot_decode::plot_from_event(event.kind_u32(), &event.tags_as_vec(), event.content())?;
let decision = event_head_decision(exec, event)?;
if !decision.apply {
return Ok(RadrootsReplicaIngestOutcome::Skipped);
@@ -459,7 +463,11 @@ fn ingest_listing_event(
exec: &dyn SqlExecutor,
event: &RadrootsEventEnvelope,
) -> Result<RadrootsReplicaIngestOutcome, RadrootsReplicaEventsError> {
- let listing = listing_decode::listing_from_event(event.kind, &event.tags, &event.content)?;
+ let listing = listing_decode::listing_from_event(
+ event.kind_u32(),
+ &event.tags_as_vec(),
+ event.content(),
+ )?;
let decision = event_head_decision(exec, event)?;
if !decision.apply {
return Ok(RadrootsReplicaIngestOutcome::Skipped);
@@ -481,11 +489,14 @@ fn ingest_list_set_event(
exec: &dyn SqlExecutor,
event: &RadrootsEventEnvelope,
) -> Result<RadrootsReplicaIngestOutcome, RadrootsReplicaEventsError> {
- if event.kind != radroots_event::kinds::KIND_LIST_SET_GENERIC {
+ if event.kind_u32() != radroots_event::kinds::KIND_LIST_SET_GENERIC {
return Ok(RadrootsReplicaIngestOutcome::Skipped);
}
- let list_set =
- list_set_decode::list_set_from_tags(event.kind, event.content.clone(), &event.tags)?;
+ let list_set = list_set_decode::list_set_from_tags(
+ event.kind_u32(),
+ event.content().to_owned(),
+ &event.tags_as_vec(),
+ )?;
let metadata_count = usize::from(list_set.title.is_some())
+ usize::from(list_set.description.is_some())
@@ -509,7 +520,7 @@ fn ingest_list_set_event(
if !decision.apply {
return Ok(RadrootsReplicaIngestOutcome::Skipped);
}
- upsert_member_claims(exec, &event.author, &list_set)?;
+ upsert_member_claims(exec, event.author_str(), &list_set)?;
upsert_event_head(exec, &decision)?;
return Ok(RadrootsReplicaIngestOutcome::Applied);
}
@@ -529,7 +540,7 @@ fn ingest_list_set_event(
if !decision.apply {
return Ok(RadrootsReplicaIngestOutcome::Skipped);
}
- let farm = find_farm_by_ref(exec, &event.author, &farm_d_tag)?;
+ let farm = find_farm_by_ref(exec, event.author_str(), &farm_d_tag)?;
upsert_farm_members(exec, &farm.id, role, &list_set)?;
upsert_event_head(exec, &decision)?;
return Ok(RadrootsReplicaIngestOutcome::Applied);
@@ -541,7 +552,12 @@ fn ingest_list_set_event(
}
fn listing_event_addr(event: &RadrootsEventEnvelope, listing: &RadrootsListing) -> String {
- format!("{}:{}:{}", event.kind, event.author, listing.d_tag)
+ format!(
+ "{}:{}:{}",
+ event.kind_u32(),
+ event.author_str(),
+ listing.d_tag
+ )
}
fn listing_is_orderable(listing: &RadrootsListing) -> bool {
@@ -904,11 +920,11 @@ fn event_head_decision(
return Ok(EventHeadDecision {
apply: false,
key: String::new(),
- kind: event.kind,
- pubkey: event.author.clone(),
+ kind: event.kind_u32(),
+ pubkey: event.author_str().to_owned(),
d_tag: String::new(),
- last_event_id: event.id.clone(),
- last_created_at: event.created_at,
+ last_event_id: event.id_str().to_owned(),
+ last_created_at: event.created_at_u64(),
content_hash: String::new(),
});
}
@@ -920,9 +936,9 @@ fn event_head_decision(
};
let (key, kind, pubkey, d_tag) = event_head_coordinate_fields(&candidate.coordinate);
#[cfg(test)]
- let content_hash = event_content_hash(&event.content, &event.tags)?;
+ let content_hash = event_content_hash(event.content(), &event.tags_as_vec())?;
#[cfg(not(test))]
- let content_hash = event_content_hash(&event.content, &event.tags);
+ let content_hash = event_content_hash(event.content(), &event.tags_as_vec());
let existing_result = nostr_event_head::find_one(
exec,
&INostrEventHeadFindOne::On(INostrEventHeadFindOneArgs {
@@ -954,8 +970,8 @@ fn event_head_decision(
kind,
pubkey,
d_tag,
- last_event_id: event.id.clone(),
- last_created_at: event.created_at,
+ last_event_id: event.id_str().to_owned(),
+ last_created_at: event.created_at_u64(),
content_hash,
})
}
@@ -1497,7 +1513,7 @@ struct EventHeadDecision {
pubkey: String,
d_tag: String,
last_event_id: String,
- last_created_at: u32,
+ last_created_at: u64,
content_hash: String,
}
@@ -1542,6 +1558,51 @@ mod tests {
};
use radroots_sql_core::{ExecOutcome, SqlExecutor, SqliteExecutor};
+ fn test_event_envelope(
+ id: u64,
+ author: &str,
+ created_at: u64,
+ kind: u32,
+ tags: Vec<Vec<String>>,
+ content: String,
+ ) -> RadrootsEventEnvelope {
+ RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts {
+ id: format!("{id:064x}"),
+ author: author.to_string(),
+ created_at,
+ kind,
+ tags,
+ content,
+ sig: "f".repeat(128),
+ })
+ .expect("test event envelope")
+ }
+
+ fn test_event_with_parts(
+ event: &RadrootsEventEnvelope,
+ kind: u32,
+ tags: Vec<Vec<String>>,
+ content: 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,
+ content,
+ sig: event.sig_str().to_owned(),
+ })
+ .expect("test event envelope parts")
+ }
+
+ fn test_event_with_content(
+ event: &RadrootsEventEnvelope,
+ content: String,
+ ) -> RadrootsEventEnvelope {
+ test_event_with_parts(event, event.kind_u32(), event.tags_as_vec(), content)
+ }
+
struct FixedFactory;
impl RadrootsReplicaIdFactory for FixedFactory {
@@ -1747,15 +1808,14 @@ mod tests {
radroots_profile_type_tag_value(profile_type).to_string(),
]);
}
- RadrootsEventEnvelope {
- id: format!("{id:064x}"),
- author: author.to_string(),
- created_at,
- kind: KIND_PROFILE,
+ test_event_envelope(
+ id,
+ author,
+ u64::from(created_at),
+ KIND_PROFILE,
tags,
- content: serde_json::to_string(&profile).expect("profile json"),
- sig: "f".repeat(128),
- }
+ serde_json::to_string(&profile).expect("profile json"),
+ )
}
fn farm_event(
@@ -1778,15 +1838,14 @@ mod tests {
tags,
};
let tags = farm_encode::farm_build_tags(&farm).expect("farm tags");
- RadrootsEventEnvelope {
- id: format!("{id:064x}"),
- author: author.to_string(),
- created_at,
- kind: KIND_FARM,
+ test_event_envelope(
+ id,
+ author,
+ u64::from(created_at),
+ KIND_FARM,
tags,
- content: serde_json::to_string(&farm).expect("farm json"),
- sig: "f".repeat(128),
- }
+ serde_json::to_string(&farm).expect("farm json"),
+ )
}
#[allow(clippy::too_many_arguments)]
@@ -1809,15 +1868,14 @@ mod tests {
tags,
};
let tags = plot_encode::plot_build_tags(&plot).expect("plot tags");
- RadrootsEventEnvelope {
- id: format!("{id:064x}"),
- author: author.to_string(),
- created_at,
- kind: KIND_PLOT,
+ test_event_envelope(
+ id,
+ author,
+ u64::from(created_at),
+ KIND_PLOT,
tags,
- content: serde_json::to_string(&plot).expect("plot json"),
- sig: "f".repeat(128),
- }
+ serde_json::to_string(&plot).expect("plot json"),
+ )
}
fn list_set_event(
@@ -1828,15 +1886,14 @@ mod tests {
list_set: &RadrootsListSet,
) -> RadrootsEventEnvelope {
let parts = list_set_encode::to_wire_parts_with_kind(list_set, kind).expect("list set");
- RadrootsEventEnvelope {
- id: format!("{id:064x}"),
- author: author.to_string(),
- created_at,
+ test_event_envelope(
+ id,
+ author,
+ u64::from(created_at),
kind,
- tags: parts.tags,
- content: parts.content,
- sig: "f".repeat(128),
- }
+ parts.tags,
+ parts.content,
+ )
}
fn listing_event(
@@ -1848,12 +1905,12 @@ mod tests {
title: &str,
) -> RadrootsEventEnvelope {
let farm_d_tag = "AAAAAAAAAAAAAAAAAAAAAA";
- RadrootsEventEnvelope {
- id: format!("{id:064x}"),
- author: author.to_string(),
- created_at,
- kind: KIND_LISTING,
- tags: vec![
+ test_event_envelope(
+ id,
+ author,
+ u64::from(created_at),
+ KIND_LISTING,
+ vec![
vec!["d".to_string(), d_tag.to_string()],
vec![
"a".to_string(),
@@ -1891,9 +1948,8 @@ mod tests {
vec!["inventory".to_string(), "5".to_string()],
vec!["status".to_string(), status.to_string()],
],
- content: format!("# {title}"),
- sig: "f".repeat(128),
- }
+ format!("# {title}"),
+ )
}
fn listing_decimal(raw: &str) -> RadrootsCoreDecimal {
@@ -2133,15 +2189,14 @@ mod tests {
commit_err: None,
rollback_count: Arc::new(AtomicUsize::new(0)),
};
- let event = RadrootsEventEnvelope {
- id: format!("{:064x}", 1u64),
- author: "a".repeat(64),
- created_at: 1,
- kind: KIND_LIST_SET_FOLLOW,
- tags: Vec::new(),
- content: String::new(),
- sig: "f".repeat(128),
- };
+ let event = test_event_envelope(
+ 1,
+ &"a".repeat(64),
+ 1,
+ KIND_LIST_SET_FOLLOW,
+ Vec::new(),
+ String::new(),
+ );
let begin_err =
radroots_replica_ingest_event_with_factory(&begin_executor, &event, &FixedFactory)
.expect_err("begin");
@@ -2181,15 +2236,7 @@ mod tests {
commit_err: None,
rollback_count: Arc::new(AtomicUsize::new(0)),
};
- let unsupported = RadrootsEventEnvelope {
- id: format!("{:064x}", 2u64),
- author: "a".repeat(64),
- created_at: 2,
- kind: 42,
- tags: Vec::new(),
- content: String::new(),
- sig: "f".repeat(128),
- };
+ let unsupported = test_event_envelope(2, &"a".repeat(64), 2, 42, Vec::new(), String::new());
let err = radroots_replica_ingest_event_with_factory(
&rollback_executor,
&unsupported,
@@ -2550,7 +2597,8 @@ mod tests {
"active",
"Pasture Eggs",
);
- active.tags.push(vec![
+ let mut active_tags = active.tags_as_vec();
+ active_tags.push(vec![
"radroots:discount".to_string(),
serde_json::json!({
"scope": "bin",
@@ -2565,6 +2613,12 @@ mod tests {
})
.to_string(),
]);
+ active = test_event_with_parts(
+ &active,
+ active.kind_u32(),
+ active_tags,
+ active.content().to_owned(),
+ );
assert_eq!(
radroots_replica_ingest_event(&exec, &active).expect("active ingest"),
RadrootsReplicaIngestOutcome::Applied
@@ -2666,7 +2720,7 @@ mod tests {
.expect("event state")
.result
.expect("state row");
- assert_eq!(state.last_event_id, archived.id);
+ assert_eq!(state.last_event_id, archived.id_str());
let stale_active = listing_event(
499,
@@ -2708,7 +2762,8 @@ mod tests {
"active",
"Half Gram Greens",
);
- for tag in &mut active.tags {
+ let mut active_tags = active.tags_as_vec();
+ for tag in &mut active_tags {
if tag.first().is_some_and(|name| name == "radroots:bin") {
tag[2] = "0.5".to_string();
tag[3] = "g".to_string();
@@ -2725,6 +2780,12 @@ mod tests {
tag[7] = "g".to_string();
}
}
+ active = test_event_with_parts(
+ &active,
+ active.kind_u32(),
+ active_tags,
+ active.content().to_owned(),
+ );
assert_eq!(
radroots_replica_ingest_event(&exec, &active).expect("fractional active ingest"),
@@ -3426,8 +3487,7 @@ mod tests {
Some(RadrootsProfileType::Individual),
"profile-base",
);
- let mut profile_bad_content = profile.clone();
- profile_bad_content.content = "{".to_string();
+ let profile_bad_content = test_event_with_content(&profile, "{".to_string());
assert!(ingest_profile_event(&exec, &profile_bad_content).is_err());
let profile_query_fail = QueryFailExecutor {
@@ -3503,8 +3563,7 @@ mod tests {
RadrootsReplicaIngestOutcome::Applied
);
- let mut farm_bad_content = farm_seed.clone();
- farm_bad_content.content = "{".to_string();
+ let farm_bad_content = test_event_with_content(&farm_seed, "{".to_string());
assert!(ingest_farm_event(&exec, &farm_bad_content, &FixedFactory).is_err());
let farm_query_fail = QueryFailExecutor {
@@ -3703,8 +3762,7 @@ mod tests {
RadrootsReplicaIngestOutcome::Applied
);
- let mut plot_bad_content = plot_seed.clone();
- plot_bad_content.content = "{".to_string();
+ let plot_bad_content = test_event_with_content(&plot_seed, "{".to_string());
assert!(ingest_plot_event(&exec, &plot_bad_content, &FixedFactory).is_err());
let plot_query_fail = QueryFailExecutor {
@@ -3859,16 +3917,19 @@ mod tests {
);
assert!(ingest_plot_event(&plot_state_fail, &plot_state_event, &FixedFactory).is_err());
- let mut list_decode_fail = profile_event(
+ let list_decode_fail = profile_event(
830,
&farm_pubkey,
108,
Some(RadrootsProfileType::Farm),
"unused",
);
- list_decode_fail.kind = KIND_LIST_SET_GENERIC;
- list_decode_fail.content = "{".to_string();
- list_decode_fail.tags = Vec::new();
+ let list_decode_fail = test_event_with_parts(
+ &list_decode_fail,
+ KIND_LIST_SET_GENERIC,
+ Vec::new(),
+ "{".to_string(),
+ );
assert!(ingest_list_set_event(&exec, &list_decode_fail).is_err());
let members_list = farm_list_sets::farm_members_list_set(farm_d_tag, vec!["6".repeat(64)])
diff --git a/crates/replica_sync/tests/ingest_roundtrip.rs b/crates/replica_sync/tests/ingest_roundtrip.rs
@@ -1,4 +1,3 @@
-use radroots_event::RadrootsEventEnvelope;
use radroots_event::farm::{RadrootsFarm, RadrootsFarmPublicLocation, RadrootsFarmRef};
use radroots_event::gcs::{RadrootsGcsLocation, RadrootsGeoJsonPoint, RadrootsGeoJsonPolygon};
use radroots_event::kinds::{
@@ -11,6 +10,7 @@ use radroots_event::profile::{
RADROOTS_PROFILE_TYPE_TAG_KEY, RadrootsProfile, RadrootsProfileType,
radroots_profile_type_tag_value,
};
+use radroots_event::{RadrootsEventEnvelope, RadrootsEventEnvelopeParts};
use radroots_event_codec::error::{EventEncodeError, EventParseError};
use radroots_event_codec::farm::encode as farm_encode;
use radroots_event_codec::farm::list_sets as farm_list_sets;
@@ -180,15 +180,14 @@ fn unwrap_sql_panics_on_error() {
}
fn draft_to_event(draft: &RadrootsReplicaEventDraft, index: u32) -> RadrootsEventEnvelope {
- RadrootsEventEnvelope {
- id: format!("{:064x}", index as u64 + 1),
- author: draft.author.clone(),
- created_at: 1_720_000_000 + index,
- kind: draft.kind,
- tags: draft.tags.clone(),
- content: draft.content.clone(),
- sig: "f".repeat(128),
- }
+ event_with_parts(
+ u64::from(index) + 1,
+ draft.author.as_str(),
+ 1_720_000_000 + index,
+ draft.kind,
+ draft.content.clone(),
+ draft.tags.clone(),
+ )
}
fn seed_source(
@@ -547,15 +546,14 @@ fn sync_all_selector_and_options_paths_are_supported() {
fn ingest_rejects_unsupported_kind() {
let exec = SqliteExecutor::open_memory().expect("db");
migrations::run_all_up(&exec).expect("migrations");
- let event = RadrootsEventEnvelope {
- id: format!("{:064x}", 1u64),
- author: "a".repeat(64),
- created_at: 1_720_000_001,
- kind: 42,
- tags: Vec::new(),
- content: String::new(),
- sig: "f".repeat(128),
- };
+ let event = event_with_parts(
+ 1,
+ &"a".repeat(64),
+ 1_720_000_001,
+ 42,
+ String::new(),
+ Vec::new(),
+ );
let err = radroots_replica_ingest_event(&exec, &event).expect_err("unsupported kind");
assert!(err.to_string().contains("unsupported kind"));
}
@@ -637,7 +635,7 @@ fn ingest_reports_parse_and_state_error_paths_for_all_kinds() {
11,
KIND_PROFILE,
"{".to_string(),
- profile_ok.tags.clone(),
+ profile_ok.tags_as_vec(),
);
assert!(radroots_replica_ingest_event(&exec, &profile_parse_error).is_err());
@@ -663,7 +661,7 @@ fn ingest_reports_parse_and_state_error_paths_for_all_kinds() {
13,
KIND_FARM,
"{".to_string(),
- farm_seed.tags.clone(),
+ farm_seed.tags_as_vec(),
);
assert!(radroots_replica_ingest_event(&exec, &farm_parse_error).is_err());
@@ -686,7 +684,7 @@ fn ingest_reports_parse_and_state_error_paths_for_all_kinds() {
15,
KIND_PLOT,
"{".to_string(),
- plot_ok.tags.clone(),
+ plot_ok.tags_as_vec(),
);
assert!(radroots_replica_ingest_event(&exec, &plot_parse_error).is_err());
@@ -919,15 +917,16 @@ fn event_with_parts(
content: String,
tags: Vec<Vec<String>>,
) -> RadrootsEventEnvelope {
- RadrootsEventEnvelope {
+ RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts {
id: format!("{id:064x}"),
author: author.to_string(),
- created_at,
+ created_at: u64::from(created_at),
kind,
tags,
content,
sig: "f".repeat(128),
- }
+ })
+ .expect("test event envelope")
}
fn sample_point(lat: f64, lng: f64) -> RadrootsGeoJsonPoint {
diff --git a/crates/transport_nostr/tests/transport.rs b/crates/transport_nostr/tests/transport.rs
@@ -1166,10 +1166,10 @@ async fn fetch_ingests_events_and_records_transport_observations() {
.await
.expect("fetch ingest");
- assert_eq!(receipt.inserted_count, 3);
+ assert_eq!(receipt.inserted_count, 2);
assert_eq!(receipt.duplicate_count, 1);
assert_eq!(receipt.unsupported_count, 1);
- assert_eq!(receipt.malformed_count, 1);
+ assert_eq!(receipt.malformed_count, 2);
assert_eq!(receipt.eose_count, 1);
assert_eq!(receipt.closed_count, 2);
assert_eq!(receipt.notice_count, 1);
@@ -1218,10 +1218,7 @@ async fn fetch_ingests_events_and_records_transport_observations() {
Some(RadrootsEventVerificationStatus::Verified.as_str())
);
assert!(!receipt.events[2].projection_eligible);
- assert_eq!(
- receipt.events[3].verification_status.as_deref(),
- Some(RadrootsEventVerificationStatus::IdMismatch.as_str())
- );
+ assert_eq!(receipt.events[3].verification_status, None);
assert!(!receipt.events[3].projection_eligible);
assert_eq!(receipt.events[4].verification_status, None);
assert!(!receipt.events[4].projection_eligible);
diff --git a/crates/transport_publish_protocol/src/lib.rs b/crates/transport_publish_protocol/src/lib.rs
@@ -16,7 +16,7 @@ use radroots_transport::{
RadrootsTransportTarget, RadrootsTransportTargetFingerprint, RadrootsTransportTargetLabel,
};
-pub const API_VERSION: &str = "radrootsd.transport_publish.v4";
+pub const API_VERSION: &str = "radrootsd.transport_publish.v5";
pub const DAEMON_NAME: &str = "radrootsd";
pub const METHOD_CAPABILITIES: &str = "transport.publish.capabilities";
pub const METHOD_EVENT: &str = "transport.publish.event";
@@ -29,7 +29,7 @@ pub enum TransportPublishProtocolError {
field: &'static str,
expected_len: usize,
},
- InvalidKind(u32),
+ EmptyRawEventJson,
EmptyTag {
index: usize,
},
@@ -130,9 +130,7 @@ impl fmt::Display for TransportPublishProtocolError {
field,
expected_len,
} => write!(f, "{field} must be {expected_len} lowercase hex characters"),
- Self::InvalidKind(kind) => {
- write!(f, "event kind {kind} exceeds transport publish range")
- }
+ Self::EmptyRawEventJson => f.write_str("raw_event_json must not be empty"),
Self::EmptyTag { index } => write!(f, "tag {index} must not be empty"),
Self::EmptyIdempotencyKey => f.write_str("idempotency key must not be empty"),
Self::EmptyTransportKind { index } => {
@@ -255,36 +253,6 @@ impl fmt::Display for TransportPublishProtocolError {
impl std::error::Error for TransportPublishProtocolError {}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
-#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct SignedEventWire {
- pub id: String,
- pub pubkey: String,
- pub created_at: u64,
- pub kind: u32,
- pub tags: Vec<Vec<String>>,
- pub content: String,
- pub sig: String,
-}
-
-impl SignedEventWire {
- pub fn validate(&self) -> Result<(), TransportPublishProtocolError> {
- validate_lower_hex("id", self.id.as_str(), 64)?;
- validate_lower_hex("pubkey", self.pubkey.as_str(), 64)?;
- validate_lower_hex("sig", self.sig.as_str(), 128)?;
- if self.kind > u16::MAX as u32 {
- return Err(TransportPublishProtocolError::InvalidKind(self.kind));
- }
- for (index, tag) in self.tags.iter().enumerate() {
- if tag.is_empty() {
- return Err(TransportPublishProtocolError::EmptyTag { index });
- }
- }
- Ok(())
- }
-}
-
-#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TransportPublishPreviewBehavior {
@@ -651,7 +619,7 @@ fn validate_required_target_fingerprints(
#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TransportPublishEventRequest {
- pub event: SignedEventWire,
+ pub raw_event_json: String,
pub target_policy: TransportPublishTargetPolicy,
pub delivery_policy: TransportPublishDeliveryPolicy,
#[cfg_attr(
@@ -668,7 +636,9 @@ pub struct TransportPublishEventRequest {
impl TransportPublishEventRequest {
pub fn validate(&self, max_targets: usize) -> Result<(), TransportPublishProtocolError> {
- self.event.validate()?;
+ if self.raw_event_json.is_empty() {
+ return Err(TransportPublishProtocolError::EmptyRawEventJson);
+ }
self.target_policy.validate(max_targets)?;
self.delivery_policy.validate()?;
if let TransportPublishTargetPolicy::ExplicitTargets { targets } = &self.target_policy {
@@ -855,9 +825,6 @@ impl TransportPublishJobView {
}
validate_lower_hex("event_id", self.event_id.as_str(), 64)?;
validate_lower_hex("pubkey", self.pubkey.as_str(), 64)?;
- if self.event_kind > u16::MAX as u32 {
- return Err(TransportPublishProtocolError::InvalidKind(self.event_kind));
- }
self.target_policy.validate(usize::MAX)?;
self.delivery_policy.validate()?;
if self.terminal != job_status_is_terminal(self.status) {
@@ -961,7 +928,7 @@ pub struct TransportPublishCapabilities {
}
impl TransportPublishCapabilities {
- pub fn v4(max_event_bytes: usize, max_targets_per_request: usize) -> Self {
+ pub fn v5(max_event_bytes: usize, max_targets_per_request: usize) -> Self {
Self {
daemon: DAEMON_NAME.to_owned(),
api_version: API_VERSION.to_owned(),
@@ -976,7 +943,7 @@ impl TransportPublishCapabilities {
mode: "scoped_bearer_token".to_owned(),
},
publish: TransportPublishSurfaceCapabilities {
- signed_event_ingress: true,
+ raw_event_json_ingress: true,
server_side_user_signing: false,
max_event_bytes,
max_targets_per_request,
@@ -1034,7 +1001,7 @@ pub struct TransportPublishAuthCapabilities {
#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TransportPublishSurfaceCapabilities {
- pub signed_event_ingress: bool,
+ pub raw_event_json_ingress: bool,
pub server_side_user_signing: bool,
pub max_event_bytes: usize,
pub max_targets_per_request: usize,
@@ -1410,16 +1377,13 @@ fn validate_job_status_state(
mod tests {
use super::*;
- fn event() -> SignedEventWire {
- SignedEventWire {
- id: "0".repeat(64),
- pubkey: "1".repeat(64),
- created_at: 1_700_000_000,
- kind: 30_402,
- tags: vec![vec!["d".to_owned(), "listing-1".to_owned()]],
- content: "{}".to_owned(),
- sig: "2".repeat(128),
- }
+ fn raw_event_json() -> String {
+ format!(
+ r#"{{"id":"{}","pubkey":"{}","created_at":1700000000,"kind":30402,"tags":[["d","listing-1"]],"content":"{{}}","sig":"{}"}}"#,
+ "0".repeat(64),
+ "1".repeat(64),
+ "2".repeat(128)
+ )
}
fn nostr_outcome(outcome_kind: TransportPublishOutcomeKind) -> TransportPublishTargetOutcome {
@@ -1540,10 +1504,10 @@ mod tests {
}
#[test]
- fn transport_publish_capabilities_match_v4_surface() {
- let capabilities = TransportPublishCapabilities::v4(1024, 10);
+ fn transport_publish_capabilities_match_v5_surface() {
+ let capabilities = TransportPublishCapabilities::v5(1024, 10);
- assert_eq!(capabilities.api_version, "radrootsd.transport_publish.v4");
+ assert_eq!(capabilities.api_version, "radrootsd.transport_publish.v5");
assert_eq!(
capabilities.methods,
vec![
@@ -1596,7 +1560,7 @@ mod tests {
#[test]
fn request_validation_covers_targets_and_policy() {
let request = TransportPublishEventRequest {
- event: event(),
+ raw_event_json: raw_event_json(),
target_policy: TransportPublishTargetPolicy::explicit_targets(vec![
TransportPublishTarget::nostr("wss://relay.example.com"),
]),
@@ -2179,7 +2143,7 @@ mod tests {
#[test]
fn serde_round_trip_preserves_preview_target() {
let request = TransportPublishEventRequest {
- event: event(),
+ raw_event_json: raw_event_json(),
target_policy: TransportPublishTargetPolicy::explicit_targets(vec![
TransportPublishTarget::reticulum_preview(
TransportPublishPreviewBehavior::DeferDeliveryPlans,
@@ -2200,7 +2164,7 @@ mod tests {
#[test]
fn serde_round_trip_preserves_target_metadata() {
let request = TransportPublishEventRequest {
- event: event(),
+ raw_event_json: raw_event_json(),
target_policy: TransportPublishTargetPolicy::explicit_targets(vec![
TransportPublishTarget::nostr("wss://relay.example.com")
.with_scope("farm.local")
@@ -2229,8 +2193,8 @@ mod tests {
"id must be 64 lowercase hex characters",
),
(
- TransportPublishProtocolError::InvalidKind(70_000),
- "event kind 70000 exceeds transport publish range",
+ TransportPublishProtocolError::EmptyRawEventJson,
+ "raw_event_json must not be empty",
),
(
TransportPublishProtocolError::EmptyTag { index: 2 },
@@ -2320,45 +2284,20 @@ mod tests {
}
#[test]
- fn signed_event_validation_rejects_each_invalid_event_shape() {
- let mut invalid_id = event();
- invalid_id.id = "A".repeat(64);
- assert!(matches!(
- invalid_id.validate(),
- Err(TransportPublishProtocolError::InvalidHexField { field: "id", .. })
- ));
-
- let mut invalid_pubkey = event();
- invalid_pubkey.pubkey = "g".repeat(64);
- assert!(matches!(
- invalid_pubkey.validate(),
- Err(TransportPublishProtocolError::InvalidHexField {
- field: "pubkey",
- ..
- })
- ));
-
- let mut invalid_sig = event();
- invalid_sig.sig = "2".repeat(127);
- assert!(matches!(
- invalid_sig.validate(),
- Err(TransportPublishProtocolError::InvalidHexField { field: "sig", .. })
- ));
-
- let mut invalid_kind = event();
- invalid_kind.kind = u16::MAX as u32 + 1;
- assert_eq!(
- invalid_kind.validate(),
- Err(TransportPublishProtocolError::InvalidKind(
- u16::MAX as u32 + 1
- ))
- );
-
- let mut empty_tag = event();
- empty_tag.tags.push(Vec::new());
+ fn publish_request_validation_rejects_empty_raw_event_json() {
+ let empty_raw_event = TransportPublishEventRequest {
+ raw_event_json: String::new(),
+ target_policy: TransportPublishTargetPolicy::nostr(
+ NostrPublishTargetSourcePolicy::DaemonDefaultOnly,
+ Vec::new(),
+ ),
+ delivery_policy: TransportPublishDeliveryPolicy::Any,
+ idempotency_key: None,
+ timeout_ms: None,
+ };
assert_eq!(
- empty_tag.validate(),
- Err(TransportPublishProtocolError::EmptyTag { index: 1 })
+ empty_raw_event.validate(10),
+ Err(TransportPublishProtocolError::EmptyRawEventJson)
);
}
@@ -2382,7 +2321,7 @@ mod tests {
assert_eq!(nostr.request_target_count(), 1);
let mut empty_targets = TransportPublishEventRequest {
- event: event(),
+ raw_event_json: raw_event_json(),
target_policy: TransportPublishTargetPolicy::explicit_targets(Vec::new()),
delivery_policy: TransportPublishDeliveryPolicy::Any,
idempotency_key: None,
@@ -2462,7 +2401,7 @@ mod tests {
.expect("required policy");
assert_eq!(required_policy.required_target_count(3), 1);
let required_request = TransportPublishEventRequest {
- event: event(),
+ raw_event_json: raw_event_json(),
target_policy: TransportPublishTargetPolicy::explicit_targets(vec![
required_target.clone(),
TransportPublishTarget::nostr("wss://relay.example").with_scope("farm.remote"),
@@ -2586,15 +2525,6 @@ mod tests {
Err(TransportPublishProtocolError::EmptyJobId)
);
- let mut invalid_kind = base.clone();
- invalid_kind.event_kind = u16::MAX as u32 + 1;
- assert_eq!(
- invalid_kind.validate(),
- Err(TransportPublishProtocolError::InvalidKind(
- u16::MAX as u32 + 1
- ))
- );
-
let mut invalid_pubkey = base.clone();
invalid_pubkey.pubkey = "x".repeat(64);
assert!(matches!(