lib

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

commit e4107f7185a6ad72b6ca6a47ae7c661531b3566f
parent 445c2b27f9613df72d0e13c79221caf29c242530
Author: triesap <tyson@radroots.org>
Date:   Mon, 27 Jul 2026 18:52:38 +0000

event_codec: eliminate repeated publication artifact copies

- validate sealed artifact state without canonical reload reparsing
- serialize draft and media payload fields through borrowed views
- replace repeated removal with sort-adjacent conflict and dedup logic
- reuse strict reload validation across allowlist and readiness boundaries

Diffstat:
Mcrates/event_codec/src/wire/publication.rs | 294+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Mcrates/event_codec/src/wire/publication/allowlist.rs | 8+++++++-
Mcrates/event_codec/src/wire/publication/media_readiness.rs | 13+++++++------
3 files changed, 218 insertions(+), 97 deletions(-)

diff --git a/crates/event_codec/src/wire/publication.rs b/crates/event_codec/src/wire/publication.rs @@ -30,7 +30,7 @@ use alloc::{ vec, vec::Vec, }; -use core::fmt; +use core::{cmp::Ordering, fmt}; use radroots_blossom::{ RADROOTS_BLOSSOM_PUBLICATION_RASTER_MAX_BYTES, @@ -62,7 +62,7 @@ use radroots_event::{ compute_canonical_nip01_event_id, }, }; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Serialize, ser::SerializeSeq}; use sha2::{Digest, Sha256}; use crate::{ @@ -272,7 +272,9 @@ impl RadrootsPhase1PublicationMediaReference { } } - fn from_wire(wire: MediaReferenceWire) -> Result<Self, RadrootsPhase1PublicationArtifactError> { + fn from_wire( + wire: &MediaReferenceWire, + ) -> Result<Self, RadrootsPhase1PublicationArtifactError> { let parsed_url = RadrootsBlossomBlobUrl::parse(&wire.url) .and_then(RadrootsBlossomBlobUrl::approve) .map_err(|_| RadrootsPhase1PublicationArtifactError::InvalidMediaReference)?; @@ -300,15 +302,6 @@ impl RadrootsPhase1PublicationMediaReference { Ok(reference) }) } - - fn to_wire(&self) -> MediaReferenceWire { - MediaReferenceWire { - url: self.url.as_str().to_string(), - sha256: self.sha256.to_hex(), - size: self.size, - media_type: self.media_type.as_str().to_string(), - } - } } /// Domain-separated SHA-256 identity for the canonical artifact payload. @@ -584,25 +577,12 @@ impl RadrootsPhase1PublicationArtifact { }, ); } - let original_media_wire = wire.media_references; - let mut media_references = original_media_wire - .iter() - .cloned() - .map(RadrootsPhase1PublicationMediaReference::from_wire) - .collect::<Result<Vec<_>, _>>()?; - canonicalize_media_references(&mut media_references)?; - if media_references - .iter() - .map(RadrootsPhase1PublicationMediaReference::to_wire) - .ne(original_media_wire.iter().cloned()) - { - return Err(RadrootsPhase1PublicationArtifactError::NonCanonicalMediaInventory); - } + let media_references = parse_canonical_media_references(&wire.media_references)?; validate_phase1_publication_profile(variant, &draft, &media_references)?; let artifact_digest = RadrootsPhase1PublicationArtifactDigest::parse(&wire.artifact_digest)?; - let computed = compute_artifact_digest( + let (computed, canonical_json) = build_canonical_artifact( variant, &expected_author, &draft, @@ -612,14 +592,6 @@ impl RadrootsPhase1PublicationArtifact { if computed != artifact_digest { return Err(RadrootsPhase1PublicationArtifactError::DigestMismatch); } - let canonical_json = serialize_artifact( - variant, - &expected_author, - &draft, - &expected_event_id, - &media_references, - artifact_digest, - )?; if canonical_json != bytes { return Err(RadrootsPhase1PublicationArtifactError::NonCanonicalJson); } @@ -677,20 +649,12 @@ impl RadrootsPhase1PublicationArtifact { }; validate_signed_event_wire_size(&expected_author, &draft, &expected_event_id)?; validate_phase1_publication_profile(variant, &draft, &media_references)?; - let artifact_digest = compute_artifact_digest( - variant, - &expected_author, - &draft, - &expected_event_id, - &media_references, - )?; - let canonical_json = serialize_artifact( + let (artifact_digest, canonical_json) = build_canonical_artifact( variant, &expected_author, &draft, &expected_event_id, &media_references, - artifact_digest, )?; if canonical_json.len() > RADROOTS_PHASE1_PUBLICATION_ARTIFACT_MAX_BYTES { return Err(RadrootsPhase1PublicationArtifactError::ArtifactTooLarge { @@ -710,14 +674,38 @@ impl RadrootsPhase1PublicationArtifact { } } -/// Revalidates a sealed artifact through the same strict persisted-byte path -/// used by later allowlist and outbox consumers. +/// Revalidates a sealed artifact without cloning and reparsing its persisted +/// representation. +/// +/// All private typed fields, the digest, and the canonical serialization are +/// recomputed before the artifact may cross a later trusted boundary. pub fn validate_phase1_publication_artifact( artifact: &RadrootsPhase1PublicationArtifact, ) -> Result<(), RadrootsPhase1PublicationArtifactError> { - let canonical_json = artifact.to_canonical_json(); - let reloaded = RadrootsPhase1PublicationArtifact::from_canonical_json(&canonical_json)?; - if &reloaded != artifact { + validate_draft_identifier( + &artifact.expected_author, + &artifact.draft, + &artifact.expected_event_id, + )?; + validate_signed_event_wire_size( + &artifact.expected_author, + &artifact.draft, + &artifact.expected_event_id, + )?; + validate_canonical_media_references(&artifact.media_references)?; + validate_phase1_publication_profile( + artifact.semantic_variant, + &artifact.draft, + &artifact.media_references, + )?; + let (digest, canonical_json) = build_canonical_artifact( + artifact.semantic_variant, + &artifact.expected_author, + &artifact.draft, + &artifact.expected_event_id, + &artifact.media_references, + )?; + if digest != artifact.artifact_digest || canonical_json != artifact.canonical_json { return Err(RadrootsPhase1PublicationArtifactError::ArtifactStateMismatch); } Ok(()) @@ -902,7 +890,10 @@ fn signed_event_wire_size( draft: &RadrootsPhase1PublicationDraft, expected_event_id: &RadrootsEventId, ) -> Result<usize, RadrootsPhase1PublicationArtifactError> { - let signature = "0".repeat(128); + const ZERO_SIGNATURE_HEX: &str = concat!( + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + ); Ok(serde_json::to_vec(&SignedEventSizeWire { id: expected_event_id.as_str(), pubkey: author.as_str(), @@ -910,7 +901,7 @@ fn signed_event_wire_size( kind: draft.kind, tags: &draft.tags, content: &draft.content, - sig: &signature, + sig: ZERO_SIGNATURE_HEX, }) .map_err(|_| RadrootsPhase1PublicationArtifactError::Serialization)? .len()) @@ -951,15 +942,61 @@ fn canonicalize_media_references( media: &mut Vec<RadrootsPhase1PublicationMediaReference>, ) -> Result<(), RadrootsPhase1PublicationArtifactError> { media.sort_by(|left, right| left.url.as_str().cmp(right.url.as_str())); - let mut index = 1usize; - while index < media.len() { - if media[index - 1].url.as_str() == media[index].url.as_str() { - if media[index - 1] != media[index] { + if media + .windows(2) + .any(|pair| pair[0].url.as_str() == pair[1].url.as_str() && pair[0] != pair[1]) + { + return Err(RadrootsPhase1PublicationArtifactError::ConflictingMediaReference); + } + media.dedup_by(|right, left| right.url.as_str() == left.url.as_str()); + Ok(()) +} + +fn parse_canonical_media_references( + wire: &[MediaReferenceWire], +) -> Result<Vec<RadrootsPhase1PublicationMediaReference>, RadrootsPhase1PublicationArtifactError> { + let mut media = Vec::new(); + media + .try_reserve_exact(wire.len()) + .map_err(|_| RadrootsPhase1PublicationArtifactError::Serialization)?; + for item in wire { + let reference = RadrootsPhase1PublicationMediaReference::from_wire(item)?; + media.push(reference); + } + let strictly_ordered = media + .windows(2) + .all(|pair| pair[0].url.as_str() < pair[1].url.as_str()); + let original_len = media.len(); + canonicalize_media_references(&mut media)?; + if !strictly_ordered || media.len() != original_len { + return Err(RadrootsPhase1PublicationArtifactError::NonCanonicalMediaInventory); + } + Ok(media) +} + +fn validate_canonical_media_references( + media: &[RadrootsPhase1PublicationMediaReference], +) -> Result<(), RadrootsPhase1PublicationArtifactError> { + if media.len() > RADROOTS_PHASE1_PUBLICATION_MEDIA_MAX_COUNT { + return Err( + RadrootsPhase1PublicationArtifactError::TooManyMediaReferences { + max: RADROOTS_PHASE1_PUBLICATION_MEDIA_MAX_COUNT, + actual: media.len(), + }, + ); + } + for reference in media { + validate_media_reference_envelope(reference)?; + } + for pair in media.windows(2) { + match pair[0].url.as_str().cmp(pair[1].url.as_str()) { + Ordering::Less => {} + Ordering::Equal if pair[0] != pair[1] => { return Err(RadrootsPhase1PublicationArtifactError::ConflictingMediaReference); } - media.remove(index); - } else { - index += 1; + Ordering::Equal | Ordering::Greater => { + return Err(RadrootsPhase1PublicationArtifactError::NonCanonicalMediaInventory); + } } } Ok(()) @@ -1449,23 +1486,59 @@ struct ArtifactWire { } #[derive(Serialize)] +struct DraftPayloadWire<'a> { + created_at: u64, + kind: u32, + tags: &'a [Vec<String>], + content: &'a str, +} + +#[derive(Serialize)] struct ArtifactPayloadWire<'a> { schema_version: u32, semantic_variant: &'a str, authored_operation_id: &'a str, event_contract_id: &'a str, expected_author: &'a str, - draft: DraftWire, + draft: DraftPayloadWire<'a>, expected_event_id: &'a str, - media_references: Vec<MediaReferenceWire>, + media_references: MediaReferencesPayloadWire<'a>, +} + +#[derive(Serialize)] +struct MediaReferencePayloadWire<'a> { + url: &'a str, + sha256: RadrootsBlossomSha256, + size: u64, + media_type: &'a str, +} + +struct MediaReferencesPayloadWire<'a>(&'a [RadrootsPhase1PublicationMediaReference]); + +impl Serialize for MediaReferencesPayloadWire<'_> { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + let mut sequence = serializer.serialize_seq(Some(self.0.len()))?; + for reference in self.0 { + sequence.serialize_element(&MediaReferencePayloadWire { + url: reference.url.as_str(), + sha256: reference.sha256, + size: reference.size, + media_type: reference.media_type.as_str(), + })?; + } + sequence.end() + } } fn artifact_payload_wire<'a>( variant: RadrootsPhase1PublicationSemanticVariant, author: &'a RadrootsPublicKey, - draft: &RadrootsPhase1PublicationDraft, + draft: &'a RadrootsPhase1PublicationDraft, expected_event_id: &'a RadrootsEventId, - media: &[RadrootsPhase1PublicationMediaReference], + media: &'a [RadrootsPhase1PublicationMediaReference], ) -> ArtifactPayloadWire<'a> { ArtifactPayloadWire { schema_version: RADROOTS_PHASE1_PUBLICATION_ARTIFACT_SCHEMA_VERSION, @@ -1473,20 +1546,18 @@ fn artifact_payload_wire<'a>( authored_operation_id: variant.authored_operation_id(), event_contract_id: variant.event_contract_id(), expected_author: author.as_str(), - draft: DraftWire { + draft: DraftPayloadWire { created_at: draft.created_at, kind: draft.kind, - tags: draft.tags.clone(), - content: draft.content.clone(), + tags: &draft.tags, + content: &draft.content, }, expected_event_id: expected_event_id.as_str(), - media_references: media - .iter() - .map(RadrootsPhase1PublicationMediaReference::to_wire) - .collect(), + media_references: MediaReferencesPayloadWire(media), } } +#[cfg(test)] fn compute_artifact_digest( variant: RadrootsPhase1PublicationSemanticVariant, author: &RadrootsPublicKey, @@ -1494,44 +1565,58 @@ fn compute_artifact_digest( expected_event_id: &RadrootsEventId, media: &[RadrootsPhase1PublicationMediaReference], ) -> Result<RadrootsPhase1PublicationArtifactDigest, RadrootsPhase1PublicationArtifactError> { - let payload = serde_json::to_vec(&artifact_payload_wire( + let payload = serialize_artifact_payload(variant, author, draft, expected_event_id, media)?; + Ok(digest_artifact_payload(&payload)) +} + +fn serialize_artifact_payload( + variant: RadrootsPhase1PublicationSemanticVariant, + author: &RadrootsPublicKey, + draft: &RadrootsPhase1PublicationDraft, + expected_event_id: &RadrootsEventId, + media: &[RadrootsPhase1PublicationMediaReference], +) -> Result<Vec<u8>, RadrootsPhase1PublicationArtifactError> { + serde_json::to_vec(&artifact_payload_wire( variant, author, draft, expected_event_id, media, )) - .map_err(|_| RadrootsPhase1PublicationArtifactError::Serialization)?; + .map_err(|_| RadrootsPhase1PublicationArtifactError::Serialization) +} + +fn digest_artifact_payload(payload: &[u8]) -> RadrootsPhase1PublicationArtifactDigest { let mut hasher = Sha256::new(); hasher.update(ARTIFACT_DIGEST_DOMAIN); hasher.update(ARTIFACT_DIGEST_DOMAIN_TERMINATOR); hasher.update(payload); - Ok(RadrootsPhase1PublicationArtifactDigest( - hasher.finalize().into(), - )) + RadrootsPhase1PublicationArtifactDigest(hasher.finalize().into()) } -fn serialize_artifact( +fn build_canonical_artifact( variant: RadrootsPhase1PublicationSemanticVariant, author: &RadrootsPublicKey, draft: &RadrootsPhase1PublicationDraft, expected_event_id: &RadrootsEventId, media: &[RadrootsPhase1PublicationMediaReference], - digest: RadrootsPhase1PublicationArtifactDigest, -) -> Result<Vec<u8>, RadrootsPhase1PublicationArtifactError> { - let payload = artifact_payload_wire(variant, author, draft, expected_event_id, media); - serde_json::to_vec(&ArtifactWire { - schema_version: payload.schema_version, - semantic_variant: payload.semantic_variant.to_string(), - authored_operation_id: payload.authored_operation_id.to_string(), - event_contract_id: payload.event_contract_id.to_string(), - expected_author: payload.expected_author.to_string(), - draft: payload.draft, - expected_event_id: payload.expected_event_id.to_string(), - media_references: payload.media_references, - artifact_digest: digest.to_hex(), - }) - .map_err(|_| RadrootsPhase1PublicationArtifactError::Serialization) +) -> Result< + (RadrootsPhase1PublicationArtifactDigest, Vec<u8>), + RadrootsPhase1PublicationArtifactError, +> { + let mut canonical_json = + serialize_artifact_payload(variant, author, draft, expected_event_id, media)?; + let digest = digest_artifact_payload(&canonical_json); + if canonical_json.pop() != Some(b'}') { + return Err(RadrootsPhase1PublicationArtifactError::Serialization); + } + canonical_json + .try_reserve_exact(86) + .map_err(|_| RadrootsPhase1PublicationArtifactError::Serialization)?; + canonical_json.extend_from_slice(br#","artifact_digest":""#); + canonical_json.extend_from_slice(digest.to_hex().as_bytes()); + canonical_json.extend_from_slice(br#""}"#); + Ok((digest, canonical_json)) } #[cfg(test)] @@ -1619,6 +1704,35 @@ mod tests { } } + #[test] + fn canonical_media_reload_rejects_duplicates_and_conflicts_after_adjacent_sort() { + const HASH: &str = "1111111111111111111111111111111111111111111111111111111111111111"; + let first = MediaReferenceWire { + url: format!("https://a.example/{HASH}.png"), + sha256: HASH.to_string(), + size: 1, + media_type: "image/png".to_string(), + }; + let middle = MediaReferenceWire { + url: format!("https://b.example/{HASH}.png"), + sha256: HASH.to_string(), + size: 1, + media_type: "image/png".to_string(), + }; + + assert_eq!( + parse_canonical_media_references(&[first.clone(), middle.clone(), first.clone()]), + Err(RadrootsPhase1PublicationArtifactError::NonCanonicalMediaInventory), + ); + + let mut conflicting = first.clone(); + conflicting.size += 1; + assert_eq!( + parse_canonical_media_references(&[first, middle, conflicting]), + Err(RadrootsPhase1PublicationArtifactError::ConflictingMediaReference), + ); + } + fn event_id( author: &RadrootsPublicKey, draft: &RadrootsPhase1PublicationDraft, diff --git a/crates/event_codec/src/wire/publication/allowlist.rs b/crates/event_codec/src/wire/publication/allowlist.rs @@ -151,6 +151,12 @@ pub fn allow_phase1_publication_artifact( validate_phase1_publication_artifact(&artifact) .map_err(RadrootsPhase1PublicationAllowlistError::ArtifactInvalid)?; + allow_validated_phase1_publication_artifact(artifact) +} + +fn allow_validated_phase1_publication_artifact( + artifact: RadrootsPhase1PublicationArtifact, +) -> Result<RadrootsPhase1AllowlistedPublicationArtifact, RadrootsPhase1PublicationAllowlistError> { let claimed = RadrootsPhase1PublicationLeaf::from_semantic_variant(artifact.semantic_variant()); let projected = reproject_publication_leaf(&artifact)?; if claimed != projected { @@ -177,7 +183,7 @@ pub fn allow_phase1_publication_canonical_json( ) -> Result<RadrootsPhase1AllowlistedPublicationArtifact, RadrootsPhase1PublicationAllowlistError> { let artifact = RadrootsPhase1PublicationArtifact::from_canonical_json(bytes) .map_err(RadrootsPhase1PublicationAllowlistError::ArtifactInvalid)?; - allow_phase1_publication_artifact(artifact) + allow_validated_phase1_publication_artifact(artifact) } fn reproject_publication_leaf( diff --git a/crates/event_codec/src/wire/publication/media_readiness.rs b/crates/event_codec/src/wire/publication/media_readiness.rs @@ -239,15 +239,16 @@ where build_media_readiness_binding(allowlisted_artifact, bounded) } -/// Revalidates an in-memory media-ready typestate through canonical reload. +/// Revalidates an in-memory media-ready typestate without cloning the +/// allowlisted artifact or reparsing the binding bytes. pub fn validate_phase1_publication_media_readiness( ready: &RadrootsPhase1MediaReadyPublicationArtifact, ) -> Result<(), RadrootsPhase1PublicationMediaReadinessError> { - let reloaded = RadrootsPhase1MediaReadyPublicationArtifact::from_canonical_json( - ready.allowlisted_artifact.clone(), - &ready.canonical_json, - )?; - if &reloaded != ready { + let artifact = ready.allowlisted_artifact.artifact(); + validate_evidence_parity(artifact, &ready.evidence)?; + let binding_digest = compute_binding_digest(artifact, &ready.evidence); + let canonical_json = serialize_binding(artifact, &ready.evidence, binding_digest)?; + if binding_digest != ready.binding_digest || canonical_json != ready.canonical_json { return Err(RadrootsPhase1PublicationMediaReadinessError::StateMismatch); } Ok(())