lib

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

commit bc61676668cfb1b684be39542009c3f96d6e4e79
parent 0b180bd81b69d5f6e39418808eed39f8937aa524
Author: triesap <tyson@radroots.org>
Date:   Mon, 27 Jul 2026 19:37:04 +0000

identity: migrate canonical public keys out of event IDs

- make identity PublicKey the canonical event author and key type
- add explicit wire and DTO conversions and remove text-wrapper accessors
- migrate key fixtures and NIP-01 vectors to curve-valid values
- verify host, all-feature, WASM, docs, and boundary gates

Diffstat:
MCargo.lock | 1+
Mcontracts/conformance/vectors/event/nip01_wire.v1.json | 14+++++++-------
Mcrates/event/Cargo.toml | 10++++++++--
Mcrates/event/src/calendar.rs | 30++++++++++++++++--------------
Mcrates/event/src/comment.rs | 71++++++++++++++++++++++++++++++++++++++---------------------------------
Mcrates/event/src/contract/registry_v7.rs | 18+++++++++---------
Mcrates/event/src/contract/registry_v7/tests.rs | 52++++++++++++++++++++++++++--------------------------
Mcrates/event/src/deletion.rs | 28++++++++++++++++------------
Mcrates/event/src/draft.rs | 82++++++++++++++++++++++++++++++++++---------------------------------------------
Mcrates/event/src/envelope.rs | 24++++++++++--------------
Mcrates/event/src/event_head/v1.rs | 12++++++------
Mcrates/event/src/event_head/v1/tests.rs | 19++++++++++---------
Mcrates/event/src/ids.rs | 57++++++++++++++++++++++++++++++++++++++-------------------
Mcrates/event/src/knowledge.rs | 10+++++-----
Mcrates/event/src/lib.rs | 25+++++++++++++++++++++++++
Mcrates/event/src/order.rs | 37+++++++++++++++++--------------------
Mcrates/event/src/reply.rs | 34+++++++++++++++++++---------------
Mcrates/event/src/repost.rs | 4++--
Mcrates/event/src/social.rs | 2+-
Mcrates/event/src/trade.rs | 27++++++++++++++-------------
Mcrates/event/src/wire/v1.rs | 12+++++-------
Mcrates/event/src/wire/v1/tests.rs | 2+-
Mcrates/event/tests/source_boundary.rs | 7++++++-
23 files changed, 315 insertions(+), 263 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -4535,6 +4535,7 @@ dependencies = [ "jiff-tzdb", "radroots_blossom", "radroots_core", + "radroots_identity", "secp256k1", "serde", "serde_json", diff --git a/contracts/conformance/vectors/event/nip01_wire.v1.json b/contracts/conformance/vectors/event/nip01_wire.v1.json @@ -6,11 +6,11 @@ "id": "nip01_wire_profile_minimal_001", "kind": "event.nip01_wire.valid", "input": { - "raw_json": "{\"id\":\"2a15e33622a155ae231b28bebe390869e67a0e228f77ecfcd652b1ce180a9dde\",\"pubkey\":\"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",\"created_at\":1700000001,\"kind\":0,\"tags\":[],\"content\":\"{}\",\"sig\":\"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd\",\"client\":\"radroots-test\"}" + "raw_json": "{\"id\":\"56bfc78223bb2221bad82b539efdec1ade0f56d0eb0e1f592fd387df4b2ceee0\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1700000001,\"kind\":0,\"tags\":[],\"content\":\"{}\",\"sig\":\"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd\",\"client\":\"radroots-test\"}" }, "expected": { - "canonical_id_preimage": "[0,\"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",1700000001,0,[],\"{}\"]", - "computed_event_id": "2a15e33622a155ae231b28bebe390869e67a0e228f77ecfcd652b1ce180a9dde", + "canonical_id_preimage": "[0,\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",1700000001,0,[],\"{}\"]", + "computed_event_id": "56bfc78223bb2221bad82b539efdec1ade0f56d0eb0e1f592fd387df4b2ceee0", "extra_fields": [ "client" ] @@ -31,7 +31,7 @@ "id": "nip01_wire_id_mismatch_003", "kind": "event.nip01_wire.invalid", "input": { - "raw_json": "{\"id\":\"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\",\"pubkey\":\"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",\"created_at\":1700000001,\"kind\":0,\"tags\":[],\"content\":\"{}\",\"sig\":\"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd\"}" + "raw_json": "{\"id\":\"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1700000001,\"kind\":0,\"tags\":[],\"content\":\"{}\",\"sig\":\"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd\"}" }, "expected": { "error": "event_id_mismatch" @@ -51,7 +51,7 @@ "id": "nip01_wire_missing_content_005", "kind": "event.nip01_wire.invalid", "input": { - "raw_json": "{\"id\":\"2a15e33622a155ae231b28bebe390869e67a0e228f77ecfcd652b1ce180a9dde\",\"pubkey\":\"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",\"created_at\":1700000001,\"kind\":0,\"tags\":[],\"sig\":\"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd\"}" + "raw_json": "{\"id\":\"56bfc78223bb2221bad82b539efdec1ade0f56d0eb0e1f592fd387df4b2ceee0\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1700000001,\"kind\":0,\"tags\":[],\"sig\":\"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd\"}" }, "expected": { "error": "missing_content" @@ -61,7 +61,7 @@ "id": "nip01_wire_empty_tag_006", "kind": "event.nip01_wire.invalid", "input": { - "raw_json": "{\"id\":\"2a15e33622a155ae231b28bebe390869e67a0e228f77ecfcd652b1ce180a9dde\",\"pubkey\":\"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",\"created_at\":1700000001,\"kind\":0,\"tags\":[[]],\"content\":\"{}\",\"sig\":\"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd\"}" + "raw_json": "{\"id\":\"56bfc78223bb2221bad82b539efdec1ade0f56d0eb0e1f592fd387df4b2ceee0\",\"pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"created_at\":1700000001,\"kind\":0,\"tags\":[[]],\"content\":\"{}\",\"sig\":\"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd\"}" }, "expected": { "error": "empty_tag" @@ -71,7 +71,7 @@ "id": "nip01_wire_noncanonical_pubkey_007", "kind": "event.nip01_wire.invalid", "input": { - "raw_json": "{\"id\":\"2a15e33622a155ae231b28bebe390869e67a0e228f77ecfcd652b1ce180a9dde\",\"pubkey\":\"CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC\",\"created_at\":1700000001,\"kind\":0,\"tags\":[],\"content\":\"{}\",\"sig\":\"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd\"}" + "raw_json": "{\"id\":\"56bfc78223bb2221bad82b539efdec1ade0f56d0eb0e1f592fd387df4b2ceee0\",\"pubkey\":\"585591529DA0BAB31B3B1B1F986611CF5F435DCA84F978C89EE8A40CCA7103DF\",\"created_at\":1700000001,\"kind\":0,\"tags\":[],\"content\":\"{}\",\"sig\":\"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd\"}" }, "expected": { "error": "noncanonical_pubkey" diff --git a/crates/event/Cargo.toml b/crates/event/Cargo.toml @@ -21,13 +21,19 @@ dto-bindgen = ["std", "serde", "knowledge-nip54", "dep:dto_bindgen"] knowledge = [] knowledge-nip54 = ["knowledge"] signature = ["dep:secp256k1"] -std = ["radroots_blossom/std", "radroots_core/std", "url_nostd/std"] -serde = ["dep:serde", "radroots_core/serde"] +std = [ + "radroots_blossom/std", + "radroots_core/std", + "radroots_identity/std", + "url_nostd/std", +] +serde = ["dep:serde", "radroots_core/serde", "radroots_identity/serde"] [dependencies] dto_bindgen = { workspace = true, optional = true } radroots_blossom = { workspace = true, default-features = false } radroots_core = { workspace = true, default-features = false } +radroots_identity = { workspace = true, default-features = false } hex = { version = "0.4", default-features = false, features = ["alloc"] } jiff-tzdb = { workspace = true } serde = { workspace = true, default-features = false, features = [ diff --git a/crates/event/src/calendar.rs b/crates/event/src/calendar.rs @@ -8,8 +8,8 @@ use alloc::{ use core::{fmt, ops::RangeInclusive, str::FromStr}; use crate::ids::{ - RadrootsAddressableCoordinate, RadrootsAddressableCoordinateParts, RadrootsDTag, - RadrootsEventId, RadrootsPublicKey, RadrootsRelayUrl, + PublicKey, RadrootsAddressableCoordinate, RadrootsAddressableCoordinateParts, RadrootsDTag, + RadrootsEventId, RadrootsRelayUrl, parse_public_key, }; use crate::media::RadrootsAuthoredImage; use crate::wire::v1::{ @@ -281,7 +281,7 @@ impl serde::Serialize for RadrootsCalendarUid { pub struct RadrootsCalendarEventReference { coordinate: RadrootsAddressableCoordinate, kind: u32, - author: RadrootsPublicKey, + author: PublicKey, d_tag: RadrootsDTag, relay: Option<String>, } @@ -322,7 +322,7 @@ impl RadrootsCalendarEventReference { self.kind } - pub fn author(&self) -> &RadrootsPublicKey { + pub fn author(&self) -> &PublicKey { &self.author } @@ -400,7 +400,7 @@ impl RadrootsCalendarEventRevisionReference { #[derive(Clone, Debug, PartialEq, Eq)] pub struct RadrootsCalendarEventAuthorReference { raw_pubkey: String, - pubkey: RadrootsPublicKey, + pubkey: PublicKey, relay: Option<String>, } @@ -410,7 +410,7 @@ impl RadrootsCalendarEventAuthorReference { relay: Option<&str>, ) -> Result<Self, RadrootsCalendarEventError> { let raw_pubkey = pubkey.as_ref(); - let pubkey = RadrootsPublicKey::parse(raw_pubkey) + let pubkey = parse_public_key(raw_pubkey) .map_err(|_| RadrootsCalendarEventError::InvalidAuthorReference)?; let relay = parse_calendar_reference_relay( relay, @@ -427,7 +427,7 @@ impl RadrootsCalendarEventAuthorReference { &self.raw_pubkey } - pub fn pubkey(&self) -> &RadrootsPublicKey { + pub fn pubkey(&self) -> &PublicKey { &self.pubkey } @@ -437,7 +437,7 @@ impl RadrootsCalendarEventAuthorReference { pub fn is_canonical(&self) -> bool { // Relay hints use strict Radroots syntax; their raw URL spelling is not normalized. - self.raw_pubkey == self.pubkey.as_str() + self.raw_pubkey == self.pubkey.to_hex() && self .relay() .is_none_or(|relay| RadrootsRelayUrl::parse(relay).is_ok()) @@ -2065,7 +2065,7 @@ fn validate_inbound_calendar_participants( participants: &[RadrootsCalendarParticipant], ) -> Result<(), RadrootsCalendarEventError> { for (index, participant) in participants.iter().enumerate() { - if RadrootsPublicKey::parse(&participant.pubkey).is_err() + if parse_public_key(&participant.pubkey).is_err() || participant .relay .as_deref() @@ -2092,9 +2092,9 @@ fn validate_authored_calendar_participants( } validate_inbound_calendar_participants(participants)?; for (index, participant) in participants.iter().enumerate() { - let pubkey = RadrootsPublicKey::parse(&participant.pubkey) + let pubkey = parse_public_key(&participant.pubkey) .map_err(|_| RadrootsCalendarEventError::InvalidParticipant { index })?; - if pubkey.as_str() != participant.pubkey + if pubkey.to_hex() != participant.pubkey || participant .relay .as_deref() @@ -2973,7 +2973,7 @@ mod tests { event.coordinate().as_str(), format!("031923:{uppercase_author}:wash-pack") ); - assert_eq!(event.author().as_str(), "a".repeat(64)); + assert_eq!(event.author().to_hex(), "a".repeat(64)); assert_eq!(event.relay(), Some("WSS://Relay.Example/events")); assert!(!event.is_canonical()); @@ -2992,7 +2992,7 @@ mod tests { ) .unwrap(); assert_eq!(author.raw_pubkey(), uppercase_author); - assert_eq!(author.pubkey().as_str(), "a".repeat(64)); + assert_eq!(author.pubkey().to_hex(), "a".repeat(64)); assert!(!author.is_canonical()); assert_eq!( @@ -3211,7 +3211,9 @@ mod tests { fn rsvp_author_hint_must_match_event_coordinate_in_strict_layers() { let uid = RadrootsCalendarUid::parse("AAAAAAAAAAAAAAAAAAAAAQ").unwrap(); let event = canonical_event_reference("wash-pack"); - let mismatched = RadrootsCalendarEventAuthorReference::parse("b".repeat(64), None).unwrap(); + let mismatched = + RadrootsCalendarEventAuthorReference::parse(crate::test_valid_hex_64('b'), None) + .unwrap(); assert_eq!( RadrootsAuthoredCalendarEventRsvp::new( uid.clone(), diff --git a/crates/event/src/comment.rs b/crates/event/src/comment.rs @@ -7,8 +7,8 @@ use core::fmt; use crate::{ ids::{ - RadrootsAddressableCoordinate, RadrootsAddressableCoordinateParts, RadrootsEventId, - RadrootsIdParseError, RadrootsPublicKey, + PublicKey, RadrootsAddressableCoordinate, RadrootsAddressableCoordinateParts, + RadrootsEventId, RadrootsIdParseError, parse_public_key, }, kinds::{ KIND_CALENDAR_DATE_EVENT, KIND_CALENDAR_TIME_EVENT, KIND_CLASSIFIED_LISTING, KIND_COMMENT, @@ -225,7 +225,7 @@ impl TryFrom<u32> for RadrootsNip22CommentRootKind { #[derive(Clone, Debug, PartialEq, Eq)] pub struct RadrootsNip22EventRootReference { event_id: RadrootsEventId, - author: RadrootsPublicKey, + author: PublicKey, kind: RadrootsNip22CommentRootKind, relay: Option<RadrootsNostrRelayHint>, } @@ -233,7 +233,7 @@ pub struct RadrootsNip22EventRootReference { impl RadrootsNip22EventRootReference { pub fn new( event_id: RadrootsEventId, - author: RadrootsPublicKey, + author: PublicKey, kind: RadrootsNip22CommentRootKind, relay: Option<RadrootsNostrRelayHint>, ) -> Result<Self, RadrootsNip22CommentError> { @@ -255,8 +255,7 @@ impl RadrootsNip22EventRootReference { Self::new( RadrootsEventId::parse(event_id) .map_err(RadrootsNip22CommentError::RootEventIdInvalid)?, - RadrootsPublicKey::parse(author) - .map_err(RadrootsNip22CommentError::RootAuthorInvalid)?, + parse_public_key(author).map_err(RadrootsNip22CommentError::RootAuthorInvalid)?, RadrootsNip22CommentRootKind::parse(kind)?, parse_optional_relay(relay)?, ) @@ -266,7 +265,7 @@ impl RadrootsNip22EventRootReference { &self.event_id } - pub const fn author(&self) -> &RadrootsPublicKey { + pub const fn author(&self) -> &PublicKey { &self.author } @@ -291,7 +290,7 @@ impl RadrootsNip22EventRootReference { #[derive(Clone, Debug, PartialEq, Eq)] pub struct RadrootsNip22AddressRootReference { coordinate: RadrootsAddressableCoordinate, - author: RadrootsPublicKey, + author: PublicKey, kind: RadrootsNip22CommentRootKind, relay: Option<RadrootsNostrRelayHint>, } @@ -336,7 +335,7 @@ impl RadrootsNip22AddressRootReference { &self.coordinate } - pub const fn author(&self) -> &RadrootsPublicKey { + pub const fn author(&self) -> &PublicKey { &self.author } @@ -361,14 +360,14 @@ impl RadrootsNip22AddressRootReference { #[derive(Clone, Debug, PartialEq, Eq)] pub struct RadrootsNip22CommentParentReference { event_id: RadrootsEventId, - author: RadrootsPublicKey, + author: PublicKey, relay: Option<RadrootsNostrRelayHint>, } impl RadrootsNip22CommentParentReference { pub fn new( event_id: RadrootsEventId, - author: RadrootsPublicKey, + author: PublicKey, relay: Option<RadrootsNostrRelayHint>, ) -> Result<Self, RadrootsNip22CommentError> { validate_optional_relay(&relay)?; @@ -387,8 +386,7 @@ impl RadrootsNip22CommentParentReference { Self::new( RadrootsEventId::parse(event_id) .map_err(RadrootsNip22CommentError::ParentEventIdInvalid)?, - RadrootsPublicKey::parse(author) - .map_err(RadrootsNip22CommentError::ParentAuthorInvalid)?, + parse_public_key(author).map_err(RadrootsNip22CommentError::ParentAuthorInvalid)?, parse_optional_relay(relay)?, ) } @@ -397,7 +395,7 @@ impl RadrootsNip22CommentParentReference { &self.event_id } - pub const fn author(&self) -> &RadrootsPublicKey { + pub const fn author(&self) -> &PublicKey { &self.author } @@ -424,7 +422,7 @@ impl RadrootsNip22CommentRoot { } } - pub const fn author(&self) -> &RadrootsPublicKey { + pub const fn author(&self) -> &PublicKey { match self { Self::Event(reference) => reference.author(), Self::Address(reference) => reference.author(), @@ -631,7 +629,7 @@ fn validate_authored_comment_wire_size( "E", reference.event_id().as_str(), reference.relay_or_empty(), - reference.author().as_str(), + reference.author().to_hex().as_str(), ], ); 8usize + usize::from(reference.relay().is_some()) @@ -659,7 +657,7 @@ fn validate_authored_comment_wire_size( &mut tags_json_bytes, &mut tag_count, "P", - root.author().as_str(), + root.author().to_hex().as_str(), root.relay(), ); @@ -676,7 +674,7 @@ fn validate_authored_comment_wire_size( "e", reference.event_id().as_str(), reference.relay_or_empty(), - reference.author().as_str(), + reference.author().to_hex().as_str(), ], ); add_tag( @@ -690,7 +688,7 @@ fn validate_authored_comment_wire_size( &mut tags_json_bytes, &mut tag_count, "p", - reference.author().as_str(), + reference.author().to_hex().as_str(), reference.relay(), ); 8usize + usize::from(reference.relay().is_some()) @@ -726,7 +724,7 @@ fn validate_authored_comment_wire_size( &mut tags_json_bytes, &mut tag_count, "p", - reference.author().as_str(), + reference.author().to_hex().as_str(), reference.relay(), ); 8usize + 3 * usize::from(reference.relay().is_some()) @@ -740,7 +738,7 @@ fn validate_authored_comment_wire_size( "e", parent.event_id().as_str(), parent.relay_or_empty(), - parent.author().as_str(), + parent.author().to_hex().as_str(), ], ); add_tag( @@ -754,7 +752,7 @@ fn validate_authored_comment_wire_size( &mut tags_json_bytes, &mut tag_count, "p", - parent.author().as_str(), + parent.author().to_hex().as_str(), parent.relay(), ); 8usize + usize::from(parent.relay().is_some()) @@ -851,7 +849,7 @@ mod tests { fn event_root(kind: u32) -> RadrootsNip22EventRootReference { RadrootsNip22EventRootReference::parse( "a".repeat(64), - "b".repeat(64), + crate::test_valid_hex_64('b'), kind, Some("wss://relay.example"), ) @@ -860,7 +858,7 @@ mod tests { fn address_root(kind: u32) -> RadrootsNip22AddressRootReference { RadrootsNip22AddressRootReference::parse( - format!("{kind}:{}:victoria-market", "b".repeat(64)), + format!("{kind}:{}:victoria-market", crate::test_valid_hex_64('b')), Some("wss://relay.example"), ) .expect("address root") @@ -903,7 +901,7 @@ mod tests { assert!(matches!( RadrootsNip22EventRootReference::parse( "a".repeat(64), - "b".repeat(64), + crate::test_valid_hex_64('b'), kind, None ), @@ -927,15 +925,18 @@ mod tests { #[test] fn canonicalizes_address_author_and_requires_current_revision() { let root = RadrootsNip22AddressRootReference::parse( - format!("30402:{}:listing", "B".repeat(64)), + format!("30402:{}:listing", crate::test_valid_hex_64('B')), None, ) .expect("address root"); assert_eq!( root.coordinate().as_str(), - format!("30402:{}:listing", "b".repeat(64)) + format!("30402:{}:listing", crate::test_valid_hex_64('b')) + ); + assert_eq!( + root.author().to_hex().as_str(), + crate::test_valid_hex_64('b') ); - assert_eq!(root.author().as_str(), "b".repeat(64)); let comment = RadrootsAuthoredNip22Comment::parse_top_level_address("Comment", root, "E".repeat(64)) @@ -951,13 +952,17 @@ mod tests { fn address_root_rechecks_the_canonical_coordinate_tag_element() { let maximum_d_tag = "x".repeat(512); let root = RadrootsNip22AddressRootReference::parse( - format!("30402:{}:{maximum_d_tag}", "b".repeat(64)), + format!("30402:{}:{maximum_d_tag}", crate::test_valid_hex_64('b')), None, ) .expect("maximum public d tag fits the tag-element budget"); assert!(root.coordinate().as_str().len() <= RADROOTS_NIP22_COMMENT_TAG_ELEMENT_MAX_BYTES); - let oversized = format!("30402:{}:{}", "b".repeat(64), "x".repeat(513)); + let oversized = format!( + "30402:{}:{}", + crate::test_valid_hex_64('b'), + "x".repeat(513) + ); assert!(matches!( RadrootsNip22AddressRootReference::parse(oversized, None), Err(RadrootsNip22CommentError::RootCoordinateInvalid( @@ -975,7 +980,7 @@ mod tests { assert!(matches!( RadrootsNip22EventRootReference::parse( "a".repeat(64), - "b".repeat(64), + crate::test_valid_hex_64('b'), KIND_CLASSIFIED_LISTING, Some(&oversized_noncanonical_relay), ), @@ -1010,7 +1015,7 @@ mod tests { ); RadrootsNip22EventRootReference::parse( "a".repeat(64), - "b".repeat(64), + crate::test_valid_hex_64('b'), KIND_CLASSIFIED_LISTING, Some(&exact_relay), ) @@ -1019,7 +1024,7 @@ mod tests { assert!(matches!( RadrootsNip22EventRootReference::parse( "a".repeat(64), - "b".repeat(64), + crate::test_valid_hex_64('b'), KIND_CLASSIFIED_LISTING, Some(&overflow) ), diff --git a/crates/event/src/contract/registry_v7.rs b/crates/event/src/contract/registry_v7.rs @@ -17,7 +17,7 @@ use crate::{ envelope::RadrootsEventEnvelope, ids::{ RadrootsAddressableCoordinate, RadrootsDTag, RadrootsEventId, RadrootsNip01Coordinate, - RadrootsPublicKey, relay_url_is_valid, + parse_public_key, relay_url_is_valid, }, kinds::*, }; @@ -4175,7 +4175,7 @@ fn tag_value_is_valid_in_registry( RadrootsTagValueType::IanaTimeZoneId => RadrootsIanaTimeZoneId::parse(value).is_ok(), RadrootsTagValueType::Kind => value.parse::<u32>().is_ok(), RadrootsTagValueType::Nip01Coordinate => RadrootsNip01Coordinate::parse(value).is_ok(), - RadrootsTagValueType::PublicKey => RadrootsPublicKey::parse(value).is_ok(), + RadrootsTagValueType::PublicKey => parse_public_key(value).is_ok(), RadrootsTagValueType::RelayUrl => relay_url_is_valid(value), RadrootsTagValueType::Text => visible_text_is_valid(value), RadrootsTagValueType::UnixTimestamp => value.parse::<u64>().is_ok(), @@ -4195,7 +4195,7 @@ fn event_pointer_tag_is_valid(tag: &[String]) -> bool { let kind = tag[3].as_str(); let d_tag = tag[4].as_str(); RadrootsEventId::parse(id).is_ok() - && RadrootsPublicKey::parse(author).is_ok() + && parse_public_key(author).is_ok() && kind.parse::<u32>().is_ok() && (d_tag.is_empty() || RadrootsDTag::parse(d_tag).is_ok()) && tag @@ -4335,9 +4335,9 @@ fn validate_calendar_rsvp_contract( }) .map(|parts| parts.pubkey); if let Some(author_hint) = tag_value(tags, "p") { - let hint = RadrootsPublicKey::parse(author_hint).ok(); + let hint = parse_public_key(author_hint).ok(); if hint.as_ref() != event_author.as_ref() - || hint.as_ref().is_none_or(|key| key.as_str() != author_hint) + || hint.as_ref().is_none_or(|key| key.to_hex() != author_hint) { return Err(calendar_tag_mismatch( contract, @@ -4390,7 +4390,7 @@ fn validate_calendar_rsvp_pointer_tag( if event_id { RadrootsEventId::parse(value).is_ok_and(|parsed| parsed.as_str() == value) } else { - RadrootsPublicKey::parse(value).is_ok_and(|parsed| parsed.as_str() == value) + parse_public_key(value).is_ok_and(|parsed| parsed.to_hex() == *value) } }); let relay_is_valid = tag @@ -4605,7 +4605,7 @@ fn validate_calendar_participant_tags( .filter(|tag| tag.first().map(String::as_str) == Some("p")) { let pubkey_is_canonical = tag.get(1).is_some_and(|value| { - RadrootsPublicKey::parse(value).is_ok_and(|pubkey| pubkey.as_str() == value.as_str()) + parse_public_key(value).is_ok_and(|pubkey| pubkey.to_hex() == *value) }); let relay_is_valid = tag .get(2) @@ -4669,7 +4669,7 @@ fn canonical_calendar_coordinate_is_valid(value: &str) -> bool { let Ok(parts) = crate::ids::RadrootsAddressableCoordinateParts::parse(value) else { return false; }; - kind == "31924" && pubkey == parts.pubkey.as_str() && d_tag == parts.d_tag.as_str() + kind == "31924" && pubkey == parts.pubkey.to_hex() && d_tag == parts.d_tag.as_str() } fn canonical_calendar_event_coordinate_is_valid(value: &str) -> bool { @@ -4686,7 +4686,7 @@ fn canonical_calendar_event_coordinate_is_valid(value: &str) -> bool { parts.kind, KIND_CALENDAR_DATE_EVENT | KIND_CALENDAR_TIME_EVENT ) && matches!(kind, "31922" | "31923") - && pubkey == parts.pubkey.as_str() + && pubkey == parts.pubkey.to_hex() && d_tag == parts.d_tag.as_str() } diff --git a/crates/event/src/contract/registry_v7/tests.rs b/crates/event/src/contract/registry_v7/tests.rs @@ -116,7 +116,7 @@ fn synthetic_kind_contract(kind: u32) -> RadrootsKindContract { fn unsigned_event(kind: u32, tags: Vec<Vec<&str>>, content: &str) -> RadrootsEventEnvelope { RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts { id: "0".repeat(64), - author: "1".repeat(64), + author: crate::test_valid_hex_64('1'), created_at: 1_700_000_000, kind, tags: tags @@ -132,7 +132,7 @@ fn unsigned_event(kind: u32, tags: Vec<Vec<&str>>, content: &str) -> RadrootsEve fn unsigned_event_owned(kind: u32, tags: Vec<Vec<String>>, content: &str) -> RadrootsEventEnvelope { RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts { id: "0".repeat(64), - author: "1".repeat(64), + author: crate::test_valid_hex_64('1'), created_at: 1_700_000_000, kind, tags, @@ -143,7 +143,7 @@ fn unsigned_event_owned(kind: u32, tags: Vec<Vec<String>>, content: &str) -> Rad } fn hex_64(character: char) -> String { - core::iter::repeat_n(character, 64).collect() + crate::test_valid_hex_64(character) } fn event_ref_tag(name: &str, event_id: &str, author: &str, kind: u32) -> Vec<String> { @@ -1281,16 +1281,19 @@ fn rejects_custom_knowledge_missing_schema_version() { #[test] fn rejects_authoritative_knowledge_status_fields() { - let event = unsigned_event( + let event = unsigned_event_owned( KIND_KNOWLEDGE_REVIEW, vec![ - vec!["contract", "radroots.knowledge.review.v1"], vec![ - "review_target", - "0000000000000000000000000000000000000000000000000000000000000000", - "1111111111111111111111111111111111111111111111111111111111111111", - "30818", - "soil", + "contract".to_owned(), + "radroots.knowledge.review.v1".to_owned(), + ], + vec![ + "review_target".to_owned(), + "0".repeat(64), + crate::test_valid_hex_64('1'), + "30818".to_owned(), + "soil".to_owned(), ], ], r#"{"schema":"radroots.knowledge.review.v1","schema_version":1,"canon_status":"approved"}"#, @@ -1532,18 +1535,15 @@ fn validate_contract_tags_enforces_declared_value_types() { }) ); - let invalid_event_id = unsigned_event( + let invalid_event_id = unsigned_event_owned( KIND_WIKI_MERGE_REQUEST, vec![ vec![ - "a", - "30818:0000000000000000000000000000000000000000000000000000000000000000:soil", + "a".to_owned(), + format!("30818:{}:soil", crate::test_valid_hex_64('0')), ], - vec![ - "p", - "1111111111111111111111111111111111111111111111111111111111111111", - ], - vec!["e", "not-hex"], + vec!["p".to_owned(), crate::test_valid_hex_64('1')], + vec!["e".to_owned(), "not-hex".to_owned()], ], "", ); @@ -1861,13 +1861,13 @@ fn validate_custom_knowledge_contract_rejects_missing_schema_and_bad_version() { #[test] fn validates_nip54_empty_redirect_content() { - let event = unsigned_event( + let event = unsigned_event_owned( KIND_WIKI_REDIRECT, vec![ - vec!["d", "soil"], + vec!["d".to_owned(), "soil".to_owned()], vec![ - "a", - "30818:0000000000000000000000000000000000000000000000000000000000000000:soil", + "a".to_owned(), + format!("30818:{}:soil", crate::test_valid_hex_64('0')), ], ], "", @@ -1878,13 +1878,13 @@ fn validates_nip54_empty_redirect_content() { Ok(()) ); - let invalid = unsigned_event( + let invalid = unsigned_event_owned( KIND_WIKI_REDIRECT, vec![ - vec!["d", "soil"], + vec!["d".to_owned(), "soil".to_owned()], vec![ - "a", - "30818:0000000000000000000000000000000000000000000000000000000000000000:soil", + "a".to_owned(), + format!("30818:{}:soil", crate::test_valid_hex_64('0')), ], ], "{}", diff --git a/crates/event/src/deletion.rs b/crates/event/src/deletion.rs @@ -510,7 +510,7 @@ mod tests { ) -> RadrootsNip09DeletionAddressTarget { RadrootsNip09DeletionAddressTarget::parse(format!( "{kind}:{}:{identifier}", - character.to_string().repeat(64) + crate::test_valid_hex_64(character) )) .expect("address target") } @@ -519,7 +519,7 @@ mod tests { total_bytes: usize, index: usize, ) -> RadrootsNip09DeletionAddressTarget { - let prefix = format!("30000:{}:", "a".repeat(64)); + let prefix = format!("30000:{}:", crate::test_valid_hex_64('a')); let suffix = format!("{index:04x}"); assert!(prefix.len() + suffix.len() <= total_bytes); RadrootsNip09DeletionAddressTarget::parse(format!( @@ -559,12 +559,14 @@ mod tests { } ); - let address = - RadrootsNip09DeletionAddressTarget::parse(format!("30000:{}:", "B".repeat(64))) - .expect("address target"); + let address = RadrootsNip09DeletionAddressTarget::parse(format!( + "30000:{}:", + crate::test_valid_hex_64('B') + )) + .expect("address target"); assert_eq!( address.coordinate().as_str(), - format!("30000:{}:", "b".repeat(64)) + format!("30000:{}:", crate::test_valid_hex_64('b')) ); assert_eq!(address.kind_hint(), 30_000); @@ -632,11 +634,11 @@ mod tests { ); assert_eq!( request.address_targets()[0].coordinate().as_str(), - format!("30402:{}:produce", "b".repeat(64)) + format!("30402:{}:produce", crate::test_valid_hex_64('b')) ); assert_eq!( request.address_targets()[1].coordinate().as_str(), - format!("31923:{}:harvest", "e".repeat(64)) + format!("31923:{}:harvest", crate::test_valid_hex_64('e')) ); assert_eq!(request.kind_hints(), &[1, 30_402, 31_923]); } @@ -655,9 +657,11 @@ mod tests { } ); - let uppercase_address = - RadrootsNip09DeletionAddressTarget::parse(format!("030402:{}:produce", "A".repeat(64))) - .expect("uppercase address"); + let uppercase_address = RadrootsNip09DeletionAddressTarget::parse(format!( + "030402:{}:produce", + crate::test_valid_hex_64('A') + )) + .expect("uppercase address"); assert_eq!( RadrootsAuthoredNip09DeletionRequest::new( "", @@ -666,7 +670,7 @@ mod tests { ) .unwrap_err(), RadrootsNip09DeletionError::DuplicateAddressTarget { - coordinate: format!("30402:{}:produce", "a".repeat(64)) + coordinate: format!("30402:{}:produce", crate::test_valid_hex_64('a')) } ); } diff --git a/crates/event/src/draft.rs b/crates/event/src/draft.rs @@ -12,7 +12,7 @@ use crate::contract::registry_v7::{ validate_event_contract_parts, }; use crate::ids::{ - RadrootsEventId, RadrootsEventSignature, RadrootsIdParseError, RadrootsPublicKey, + PublicKey, RadrootsEventId, RadrootsEventSignature, RadrootsIdParseError, parse_public_key, }; use crate::wire::v1::{ RadrootsCanonicalEventIdError, RadrootsEventWireError, RadrootsNip01EventWire, @@ -211,7 +211,7 @@ pub struct RadrootsEventDraft { created_at: RadrootsEventTimestamp, tags: RadrootsEventTags, content: String, - expected_pubkey: RadrootsPublicKey, + expected_pubkey: PublicKey, expected_event_id: RadrootsEventId, } @@ -237,7 +237,7 @@ impl RadrootsEventDraft { }); } ensure_generic_draft_authorable(contract)?; - let expected_pubkey = RadrootsPublicKey::parse(expected_pubkey.as_ref())?; + let expected_pubkey = parse_public_key(expected_pubkey.as_ref())?; let content = content.into(); validate_event_contract_parts(kind, &tags, content.as_str(), contract.id).map_err( |error| RadrootsDraftError::ContractShape { @@ -246,8 +246,9 @@ impl RadrootsEventDraft { }, )?; let typed_tags = RadrootsEventTags::new(tags)?; + let expected_pubkey_hex = expected_pubkey.to_hex(); let expected_event_id = compute_nip01_event_id_for_valid_pubkey( - expected_pubkey.as_str(), + expected_pubkey_hex.as_str(), created_at, kind, &typed_tags.to_vec(), @@ -266,8 +267,9 @@ impl RadrootsEventDraft { } pub fn nip01_preimage(&self) -> Result<String, RadrootsDraftError> { + let expected_pubkey = self.expected_pubkey.to_hex(); Ok(nip01_event_id_preimage_for_valid_pubkey( - self.expected_pubkey.as_str(), + expected_pubkey.as_str(), self.created_at.as_u64(), self.kind.as_u32(), &self.tags.to_vec(), @@ -306,8 +308,9 @@ impl RadrootsEventDraft { contract_id: contract.id.to_owned(), error, })?; + let expected_pubkey = self.expected_pubkey.to_hex(); let actual_event_id = compute_nip01_event_id_for_valid_pubkey( - self.expected_pubkey_str(), + expected_pubkey.as_str(), self.created_at_u64(), self.kind_u32(), &self.tags_as_vec(), @@ -367,16 +370,11 @@ impl RadrootsEventDraft { } #[inline] - pub fn expected_pubkey(&self) -> &RadrootsPublicKey { + pub fn expected_pubkey(&self) -> &PublicKey { &self.expected_pubkey } #[inline] - pub fn expected_pubkey_str(&self) -> &str { - self.expected_pubkey.as_str() - } - - #[inline] pub fn expected_event_id(&self) -> &RadrootsEventId { &self.expected_event_id } @@ -413,7 +411,7 @@ impl<'de> serde::Deserialize<'de> for RadrootsEventDraft { created_at: RadrootsEventTimestamp, tags: RadrootsEventTags, content: String, - expected_pubkey: RadrootsPublicKey, + expected_pubkey: PublicKey, expected_event_id: RadrootsEventId, } @@ -432,7 +430,7 @@ impl<'de> serde::Deserialize<'de> for RadrootsEventDraft { value.created_at.as_u64(), value.tags.to_vec(), value.content, - value.expected_pubkey.as_str(), + value.expected_pubkey.to_hex(), ) .map_err(serde::de::Error::custom)?; if draft.expected_event_id != value.expected_event_id { @@ -580,9 +578,9 @@ impl RadrootsSignedEvent { let id = RadrootsEventId::parse(parts.id) .map_err(RadrootsEventEnvelopeError::InvalidId)? .into_string(); - let pubkey = RadrootsPublicKey::parse(parts.pubkey) + let pubkey = parse_public_key(parts.pubkey) .map_err(RadrootsEventEnvelopeError::InvalidAuthor)? - .into_string(); + .to_hex(); let sig = RadrootsEventSignature::parse(parts.sig) .map_err(RadrootsEventEnvelopeError::InvalidSignature)? .into_string(); @@ -663,16 +661,11 @@ impl RadrootsSignedEvent { } #[inline] - pub fn pubkey(&self) -> &RadrootsPublicKey { + pub fn pubkey(&self) -> &PublicKey { self.envelope.author() } #[inline] - pub fn pubkey_str(&self) -> &str { - self.envelope.author_str() - } - - #[inline] pub fn created_at(&self) -> u64 { self.envelope.created_at_u64() } @@ -728,10 +721,10 @@ pub fn validate_signed_nostr_event_matches_draft( draft: &RadrootsEventDraft, ) -> Result<(), RadrootsDraftError> { draft.validate_for_signing()?; - if signed_event.pubkey_str() != draft.expected_pubkey_str() { + if signed_event.pubkey() != draft.expected_pubkey() { return Err(RadrootsDraftError::SignedEventPubkeyMismatch { - expected_pubkey: draft.expected_pubkey_str().to_owned(), - actual_pubkey: signed_event.pubkey_str().to_owned(), + expected_pubkey: draft.expected_pubkey().to_hex(), + actual_pubkey: signed_event.pubkey().to_hex(), }); } if signed_event.created_at() != draft.created_at_u64() { @@ -766,8 +759,9 @@ pub fn validate_signed_nostr_event_matches_draft( actual_event_id: signed_event.id_str().to_owned(), }); } + let signed_pubkey = signed_event.pubkey().to_hex(); let computed_event_id = compute_nip01_event_id_for_valid_pubkey( - signed_event.pubkey_str(), + signed_pubkey.as_str(), draft.created_at_u64(), signed_event.kind(), &signed_tags, @@ -792,9 +786,7 @@ fn verify_bip340_signature( let mut event_id = [0u8; 32]; hex::decode_to_slice(signed_event.id_str(), &mut event_id) .map_err(|_| RadrootsSignatureVerificationError::InvalidEventId)?; - let mut pubkey = [0u8; 32]; - hex::decode_to_slice(signed_event.pubkey_str(), &mut pubkey) - .map_err(|_| RadrootsSignatureVerificationError::InvalidPubkey)?; + let pubkey = signed_event.pubkey().into_bytes(); let mut sig = [0u8; 64]; hex::decode_to_slice(signed_event.sig_str(), &mut sig) .map_err(|_| RadrootsSignatureVerificationError::InvalidSignature)?; @@ -815,7 +807,7 @@ pub fn compute_nip01_event_id( tags: &[Vec<String>], content: &str, ) -> Result<RadrootsEventId, RadrootsDraftError> { - RadrootsPublicKey::parse(pubkey)?; + parse_public_key(pubkey)?; Ok(compute_nip01_event_id_for_valid_pubkey( pubkey, created_at, kind, tags, content, )) @@ -828,7 +820,7 @@ pub fn nip01_event_id_preimage( tags: &[Vec<String>], content: &str, ) -> Result<String, RadrootsDraftError> { - RadrootsPublicKey::parse(pubkey)?; + parse_public_key(pubkey)?; Ok(nip01_event_id_preimage_for_valid_pubkey( pubkey, created_at, kind, tags, content, )) @@ -867,7 +859,7 @@ mod tests { }; fn hex_64(character: char) -> String { - core::iter::repeat_n(character, 64).collect() + crate::test_valid_hex_64(character) } fn hex_128(character: char) -> String { @@ -939,7 +931,7 @@ mod tests { fn signed_event_for_draft(draft: &RadrootsEventDraft) -> RadrootsSignedEvent { let wire = verified_wire( - draft.expected_pubkey_str().to_string(), + draft.expected_pubkey().to_hex(), draft.created_at_u64(), draft.kind_u32(), draft.tags_as_vec(), @@ -1031,7 +1023,7 @@ mod tests { assert_eq!( event_id.as_str(), - "2a15e33622a155ae231b28bebe390869e67a0e228f77ecfcd652b1ce180a9dde" + "679ee570a933961d4f5ee95ed60cbc34d85c9c24a8da5e92f6036462ee0fc852" ); } @@ -1285,8 +1277,7 @@ mod tests { assert_eq!(decoded.envelope().id_str(), decoded.id_str()); assert_eq!(decoded.wire().id, decoded.id_str()); assert_eq!(decoded.id().as_str(), decoded.id_str()); - assert_eq!(decoded.pubkey().as_str(), decoded.pubkey_str()); - assert_eq!(decoded.pubkey_str(), hex_64('e')); + assert_eq!(decoded.pubkey().to_hex(), hex_64('e')); assert_eq!(decoded.created_at(), 10); assert_eq!(decoded.kind(), KIND_POST); assert_eq!(decoded.tags_as_vec(), wire.tags); @@ -1310,7 +1301,7 @@ mod tests { .expect("signed event"); assert_eq!(signed.id_str(), wire.id); - assert_eq!(signed.pubkey_str(), hex_64('2')); + assert_eq!(signed.pubkey().to_hex(), hex_64('2')); assert_eq!(signed.sig_str(), hex_128('3')); assert_eq!(signed.raw_json(), raw_json); @@ -1405,7 +1396,7 @@ mod tests { let signed = RadrootsSignedEvent::from_wire_unchecked( unchecked_wire( hex_64('d'), - draft.expected_pubkey_str().to_string(), + draft.expected_pubkey().to_hex(), draft.created_at_u64(), draft.kind_u32(), draft.tags_as_vec(), @@ -1425,7 +1416,7 @@ mod tests { let signed = RadrootsSignedEvent::from_wire_unchecked( unchecked_wire( draft.expected_event_id_str().to_string(), - draft.expected_pubkey_str().to_string(), + draft.expected_pubkey().to_hex(), draft.created_at_u64() + 1, draft.kind_u32(), draft.tags_as_vec(), @@ -1445,7 +1436,7 @@ mod tests { let signed = RadrootsSignedEvent::from_wire_unchecked( unchecked_wire( draft.expected_event_id_str().to_string(), - draft.expected_pubkey_str().to_string(), + draft.expected_pubkey().to_hex(), draft.created_at_u64(), KIND_PROFILE, draft.tags_as_vec(), @@ -1467,7 +1458,7 @@ mod tests { let signed = RadrootsSignedEvent::from_wire_unchecked( unchecked_wire( draft.expected_event_id_str().to_string(), - draft.expected_pubkey_str().to_string(), + draft.expected_pubkey().to_hex(), draft.created_at_u64(), draft.kind_u32(), tags, @@ -1487,7 +1478,7 @@ mod tests { let signed = RadrootsSignedEvent::from_wire_unchecked( unchecked_wire( draft.expected_event_id_str().to_string(), - draft.expected_pubkey_str().to_string(), + draft.expected_pubkey().to_hex(), draft.created_at_u64(), draft.kind_u32(), draft.tags_as_vec(), @@ -1610,10 +1601,7 @@ mod tests { assert_eq!(draft.kind().as_u32(), draft.kind_u32()); assert_eq!(draft.created_at().as_u64(), draft.created_at_u64()); assert_eq!(draft.tags().to_vec(), draft.tags_as_vec()); - assert_eq!( - draft.expected_pubkey().as_str(), - draft.expected_pubkey_str() - ); + assert_eq!(draft.expected_pubkey().to_hex(), hex_64('a')); assert_eq!( draft.expected_event_id().as_str(), draft.expected_event_id_str() @@ -1623,7 +1611,7 @@ mod tests { assert_eq!(signed.envelope().id_str(), signed.id_str()); assert_eq!(signed.wire().id, signed.id_str()); assert_eq!(signed.id().as_str(), signed.id_str()); - assert_eq!(signed.pubkey().as_str(), signed.pubkey_str()); + assert_eq!(signed.pubkey(), draft.expected_pubkey()); assert_eq!(signed.sig().as_str(), signed.sig_str()); for error in [ diff --git a/crates/event/src/envelope.rs b/crates/event/src/envelope.rs @@ -7,7 +7,7 @@ use alloc::{string::String, string::ToString, vec::Vec}; use std::{string::String, vec::Vec}; use crate::ids::{ - RadrootsEventId, RadrootsEventSignature, RadrootsIdParseError, RadrootsPublicKey, + PublicKey, RadrootsEventId, RadrootsEventSignature, RadrootsIdParseError, parse_public_key, }; use crate::wire::v1::{ DEFAULT_CONTENT_MAX_BYTES, DEFAULT_TAG_ELEMENT_MAX_BYTES, DEFAULT_TAG_MAX_COUNT, @@ -432,7 +432,8 @@ pub struct RadrootsEventEnvelopeParts { #[derive(Clone, Debug, PartialEq, Eq)] pub struct RadrootsEventEnvelope { id: RadrootsEventId, - author: RadrootsPublicKey, + #[cfg_attr(feature = "dto-bindgen", dto(as = "string"))] + author: PublicKey, #[cfg_attr(feature = "dto-bindgen", dto(int = "json_number"))] created_at: RadrootsEventTimestamp, kind: RadrootsEventKind, @@ -455,9 +456,9 @@ impl RadrootsEventEnvelope { if id.as_str() != parts.id.as_str() { return Err(RadrootsEventEnvelopeError::NonCanonicalId); } - let author = RadrootsPublicKey::parse(parts.author.as_str()) + let author = parse_public_key(parts.author.as_str()) .map_err(RadrootsEventEnvelopeError::InvalidAuthor)?; - if author.as_str() != parts.author.as_str() { + if author.to_hex() != parts.author { return Err(RadrootsEventEnvelopeError::NonCanonicalAuthor); } let sig = RadrootsEventSignature::parse(parts.sig.as_str()) @@ -495,16 +496,11 @@ impl RadrootsEventEnvelope { } #[inline] - pub fn author(&self) -> &RadrootsPublicKey { + pub fn author(&self) -> &PublicKey { &self.author } #[inline] - pub fn author_str(&self) -> &str { - self.author.as_str() - } - - #[inline] pub fn created_at(&self) -> RadrootsEventTimestamp { self.created_at } @@ -561,7 +557,7 @@ impl RadrootsEventEnvelope { pub fn to_nip01_wire(&self) -> RadrootsNip01EventWire { RadrootsNip01EventWire { id: self.id.as_str().to_string(), - pubkey: self.author.as_str().to_string(), + pubkey: self.author.to_hex(), created_at: self.created_at.as_u64(), kind: self.kind.as_u32(), tags: self.tags.to_vec(), @@ -611,7 +607,7 @@ mod tests { use super::*; fn hex_64(character: char) -> String { - core::iter::repeat_n(character, 64).collect() + crate::test_valid_hex_64(character) } fn hex_128(character: char) -> String { @@ -635,7 +631,7 @@ mod tests { let envelope = RadrootsEventEnvelope::new(event_parts()).expect("envelope"); assert_eq!(envelope.id_str(), hex_64('1')); - assert_eq!(envelope.author_str(), hex_64('a')); + assert_eq!(envelope.author().to_hex(), hex_64('a')); assert_eq!(envelope.created_at_u64(), u64::from(u32::MAX) + 1); assert_eq!(envelope.kind_u32(), 30_023); assert_eq!(envelope.kind_class(), RadrootsEventKindClass::Addressable); @@ -937,7 +933,7 @@ mod tests { let envelope = RadrootsEventEnvelope::new(event_parts()).expect("envelope"); assert_eq!(envelope.id().as_str(), envelope.id_str()); - assert_eq!(envelope.author().as_str(), envelope.author_str()); + assert_eq!(envelope.author().to_hex(), hex_64('a')); assert_eq!(envelope.created_at().as_u64(), envelope.created_at_u64()); assert_eq!(envelope.kind().as_u32(), envelope.kind_u32()); assert_eq!(envelope.tags().to_vec(), envelope.tags_as_vec()); diff --git a/crates/event/src/event_head/v1.rs b/crates/event/src/event_head/v1.rs @@ -9,18 +9,18 @@ use crate::contract::registry_v7::{ RadrootsContractMatchError, RadrootsEventClass, RadrootsEventContract, identify_event_contract, }; use crate::envelope::{RadrootsEventEnvelope, RadrootsEventKindClass, RadrootsEventTag}; -use crate::ids::{RadrootsDTag, RadrootsEventId, RadrootsIdParseError, RadrootsPublicKey}; +use crate::ids::{PublicKey, RadrootsDTag, RadrootsEventId, RadrootsIdParseError}; use crate::tags::TAG_D; #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum RadrootsEventHeadCoordinate { Replaceable { kind: u32, - pubkey: RadrootsPublicKey, + pubkey: PublicKey, }, Addressable { kind: u32, - pubkey: RadrootsPublicKey, + pubkey: PublicKey, d_tag: String, }, } @@ -83,7 +83,7 @@ pub fn event_head_candidate_for_class( RadrootsEventClass::Ephemeral => RadrootsEventHeadCandidateResult::NotPersisted, RadrootsEventClass::Replaceable | RadrootsEventClass::Addressable => { let event_id = event.id().clone(); - let pubkey = event.author().clone(); + let pubkey = *event.author(); let coordinate = if class == RadrootsEventClass::Replaceable { RadrootsEventHeadCoordinate::Replaceable { kind: event.kind_u32(), @@ -142,11 +142,11 @@ pub fn event_head_candidate_for_nip01_event_v1( } RadrootsEventKindClass::Replaceable => RadrootsEventHeadCoordinate::Replaceable { kind: event.kind_u32(), - pubkey: event.author().clone(), + pubkey: *event.author(), }, RadrootsEventKindClass::Addressable => RadrootsEventHeadCoordinate::Addressable { kind: event.kind_u32(), - pubkey: event.author().clone(), + pubkey: *event.author(), d_tag: String::from(first_tag_value(event.tag_slices(), TAG_D).unwrap_or("")), }, }; diff --git a/crates/event/src/event_head/v1/tests.rs b/crates/event/src/event_head/v1/tests.rs @@ -1,12 +1,13 @@ use super::*; use crate::RadrootsEventEnvelopeParts; use crate::contract::RadrootsContractMatchError; +use crate::ids::parse_public_key; use crate::kinds::{ KIND_FOLLOW, KIND_LIST_SET_GENERIC, KIND_POST, KIND_PROFILE, KIND_TRADE_PROPOSAL, }; fn hex_64(character: char) -> String { - core::iter::repeat_n(character, 64).collect() + crate::test_valid_hex_64(character) } fn hex_128(character: char) -> String { @@ -90,7 +91,7 @@ fn replaceable_events_use_kind_and_pubkey_coordinates() { candidate.coordinate, RadrootsEventHeadCoordinate::Replaceable { kind: 10002, - pubkey: RadrootsPublicKey::parse(hex_64('a')).unwrap() + pubkey: parse_public_key(hex_64('a')).unwrap() } ); assert_eq!(candidate.created_at, 5); @@ -113,7 +114,7 @@ fn addressable_events_use_kind_pubkey_and_d_tag_coordinates() { candidate.coordinate, RadrootsEventHeadCoordinate::Addressable { kind: 30023, - pubkey: RadrootsPublicKey::parse(hex_64('b')).unwrap(), + pubkey: parse_public_key(hex_64('b')).unwrap(), d_tag: "article-1".to_owned() } ); @@ -198,7 +199,7 @@ fn contract_bridge_uses_replaceable_event_classes() { candidate.coordinate, RadrootsEventHeadCoordinate::Replaceable { kind: KIND_FOLLOW, - pubkey: RadrootsPublicKey::parse(hex_64('a')).unwrap() + pubkey: parse_public_key(hex_64('a')).unwrap() } ); } @@ -211,7 +212,7 @@ fn raw_nip01_bridge_uses_numeric_kind_classes_without_contract_identification() replaceable.coordinate, RadrootsEventHeadCoordinate::Replaceable { kind: 19_999, - pubkey: RadrootsPublicKey::parse(hex_64('a')).unwrap(), + pubkey: parse_public_key(hex_64('a')).unwrap(), } ); @@ -227,7 +228,7 @@ fn raw_nip01_bridge_uses_numeric_kind_classes_without_contract_identification() addressable.coordinate, RadrootsEventHeadCoordinate::Addressable { kind: 39_999, - pubkey: RadrootsPublicKey::parse(hex_64('b')).unwrap(), + pubkey: parse_public_key(hex_64('b')).unwrap(), d_tag: "unsupported".to_owned(), } ); @@ -273,7 +274,7 @@ fn raw_nip01_addressable_coordinates_treat_d_as_opaque_protocol_data() { candidate.coordinate, RadrootsEventHeadCoordinate::Addressable { kind: 39_999, - pubkey: RadrootsPublicKey::parse(hex_64('b')).unwrap(), + pubkey: parse_public_key(hex_64('b')).unwrap(), d_tag: expected.to_owned(), } ); @@ -309,7 +310,7 @@ fn contract_bridge_uses_addressable_event_classes() { candidate.coordinate, RadrootsEventHeadCoordinate::Addressable { kind: KIND_LIST_SET_GENERIC, - pubkey: RadrootsPublicKey::parse(hex_64('b')).unwrap(), + pubkey: parse_public_key(hex_64('b')).unwrap(), d_tag: "member_of.farms".to_owned() } ); @@ -331,7 +332,7 @@ fn contract_bridge_uses_profile_replaceable_heads() { candidate.coordinate, RadrootsEventHeadCoordinate::Replaceable { kind: KIND_PROFILE, - pubkey: RadrootsPublicKey::parse(hex_64('c')).unwrap() + pubkey: parse_public_key(hex_64('c')).unwrap() } ); } diff --git a/crates/event/src/ids.rs b/crates/event/src/ids.rs @@ -7,6 +7,7 @@ use alloc::{format, string::String, string::ToString, vec::Vec}; use std::{string::String, vec::Vec}; use core::{borrow::Borrow, fmt, ops::Deref, str::FromStr}; +pub(crate) use radroots_identity::PublicKey; use url_nostd::Url; use crate::kinds::KIND_CLASSIFIED_LISTING; @@ -17,6 +18,7 @@ pub enum RadrootsIdParseError { InvalidFormat, InvalidLength { expected: usize, actual: usize }, InvalidCharacter, + InvalidPublicKey, UnexpectedKind { expected: u32, actual: u32 }, TooLong { max: usize, actual: usize }, } @@ -33,6 +35,9 @@ impl fmt::Display for RadrootsIdParseError { ) } Self::InvalidCharacter => write!(f, "identifier contains an invalid character"), + Self::InvalidPublicKey => { + write!(f, "identifier is not a valid secp256k1 x-only public key") + } Self::UnexpectedKind { expected, actual } => { write!( f, @@ -183,7 +188,6 @@ macro_rules! validated_string_id { }; } -validated_string_id!(RadrootsPublicKey, validate_hex_64); validated_string_id!(RadrootsEventId, validate_hex_64); validated_string_id!(RadrootsEventSignature, validate_hex_128); validated_string_id!(RadrootsTradeId, validate_hex_32); @@ -205,6 +209,19 @@ validated_string_id!(RadrootsEconomicsDigest, validate_economics_digest); validated_string_id!(RadrootsEventPointer, validate_hex_64); validated_string_id!(RadrootsRelayUrl, validate_relay_url); +pub(crate) fn parse_public_key(value: impl AsRef<str>) -> Result<PublicKey, RadrootsIdParseError> { + PublicKey::from_hex(value.as_ref()).map_err(|error| match error { + radroots_identity::Error::InvalidHexLength { expected, actual } + | radroots_identity::Error::InvalidByteLength { expected, actual } => { + RadrootsIdParseError::InvalidLength { expected, actual } + } + radroots_identity::Error::InvalidHexCharacter { .. } => { + RadrootsIdParseError::InvalidCharacter + } + _ => RadrootsIdParseError::InvalidPublicKey, + }) +} + /// Radroots tag-element policy for a NIP-01 coordinate. /// /// NIP-01 does not define this resource limit. @@ -259,7 +276,7 @@ impl std::error::Error for RadrootsNip01CoordinateParseError { #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct RadrootsNip01CoordinateParts { pub kind: u32, - pub pubkey: RadrootsPublicKey, + pub pubkey: PublicKey, pub identifier: String, } @@ -277,7 +294,7 @@ impl RadrootsNip01CoordinateParts { pub struct RadrootsNip01Coordinate { canonical: String, kind: u32, - pubkey: RadrootsPublicKey, + pubkey: PublicKey, identifier: String, } @@ -311,8 +328,7 @@ impl RadrootsNip01Coordinate { return Err(RadrootsNip01CoordinateParseError::IdentifierMustBeEmpty { kind }); } - let pubkey = - RadrootsPublicKey::parse(pubkey).map_err(RadrootsNip01CoordinateParseError::Pubkey)?; + let pubkey = parse_public_key(pubkey).map_err(RadrootsNip01CoordinateParseError::Pubkey)?; let identifier = identifier.to_string(); let canonical = format!("{kind}:{pubkey}:{identifier}"); Ok(Self { @@ -334,7 +350,7 @@ impl RadrootsNip01Coordinate { } #[inline] - pub const fn pubkey(&self) -> &RadrootsPublicKey { + pub const fn pubkey(&self) -> &PublicKey { &self.pubkey } @@ -347,7 +363,7 @@ impl RadrootsNip01Coordinate { pub fn parts(&self) -> RadrootsNip01CoordinateParts { RadrootsNip01CoordinateParts { kind: self.kind, - pubkey: self.pubkey.clone(), + pubkey: self.pubkey, identifier: self.identifier.clone(), } } @@ -451,7 +467,7 @@ impl<'de> serde::Deserialize<'de> for RadrootsNip01Coordinate { #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct RadrootsAddressableCoordinateParts { pub kind: u32, - pub pubkey: RadrootsPublicKey, + pub pubkey: PublicKey, pub d_tag: RadrootsDTag, } @@ -565,7 +581,7 @@ fn parse_addressable_coordinate_parts( let kind = kind .parse::<u32>() .map_err(|_| RadrootsIdParseError::InvalidFormat)?; - let pubkey = RadrootsPublicKey::parse(pubkey)?; + let pubkey = parse_public_key(pubkey)?; let d_tag = RadrootsDTag::parse(d_tag)?; Ok(RadrootsAddressableCoordinateParts { kind, @@ -683,7 +699,7 @@ mod tests { } fn hex_64(character: char) -> String { - core::iter::repeat_n(character, 64).collect() + crate::test_valid_hex_64(character) } fn hex_32(character: char) -> String { @@ -696,9 +712,12 @@ mod tests { #[test] fn public_keys_and_event_ids_require_64_hex_chars() { - let upper = "A".repeat(64); - let public_key = RadrootsPublicKey::parse(&upper).expect("public key"); - assert_eq!(public_key.as_str(), "a".repeat(64)); + let upper = "585591529DA0BAB31B3B1B1F986611CF5F435DCA84F978C89EE8A40CCA7103DF"; + let public_key = parse_public_key(upper).expect("public key"); + assert_eq!( + public_key.to_hex(), + "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df" + ); let event_id = RadrootsEventId::parse(hex_64('f')).expect("event id"); assert_eq!(event_id.as_str(), hex_64('f')); @@ -725,6 +744,7 @@ mod tests { actual: 7, }, RadrootsIdParseError::InvalidCharacter, + RadrootsIdParseError::InvalidPublicKey, RadrootsIdParseError::UnexpectedKind { expected: KIND_CLASSIFIED_LISTING, actual: 30023, @@ -856,7 +876,7 @@ mod tests { let addr = format!("30402:{}:farm:farm-1:members", hex_64('A')); let parts = RadrootsAddressableCoordinateParts::parse(&addr).expect("coordinate parts"); assert_eq!(parts.kind, 30402); - assert_eq!(parts.pubkey.as_str(), hex_64('a')); + assert_eq!(parts.pubkey.to_hex(), hex_64('a')); assert_eq!(parts.d_tag.as_str(), "farm:farm-1:members"); } @@ -866,7 +886,7 @@ mod tests { let coordinate = RadrootsNip01Coordinate::parse(format!("+0{kind}:{}:", hex_64('A'))) .expect("replaceable coordinate"); assert_eq!(coordinate.kind(), kind); - assert_eq!(coordinate.pubkey().as_str(), hex_64('a')); + assert_eq!(coordinate.pubkey().to_hex(), hex_64('a')); assert_eq!(coordinate.identifier(), ""); assert_eq!(coordinate.as_str(), format!("{kind}:{}:", hex_64('a'))); } @@ -887,7 +907,7 @@ mod tests { .expect("opaque addressable coordinate"); assert_eq!(coordinate.kind(), 30_402); - assert_eq!(coordinate.pubkey().as_str(), hex_64('a')); + assert_eq!(coordinate.pubkey().to_hex(), hex_64('a')); assert_eq!(coordinate.identifier().as_bytes(), identifier.as_bytes()); assert_eq!( coordinate.as_str().as_bytes(), @@ -895,7 +915,7 @@ mod tests { ); let parts = coordinate.parts(); assert_eq!(parts.kind, 30_402); - assert_eq!(parts.pubkey.as_str(), hex_64('a')); + assert_eq!(parts.pubkey.to_hex(), hex_64('a')); assert_eq!(parts.identifier.as_bytes(), identifier.as_bytes()); assert_eq!( RadrootsNip01CoordinateParts::parse(coordinate.as_str()).expect("parts"), @@ -1085,7 +1105,6 @@ mod tests { fn validated_identifier_wrappers_expose_consistent_traits() { let addressable = format!("30402:{}:listing-1", hex_64('0')); - assert_identifier_impls!(RadrootsPublicKey, hex_64('a').as_str()); assert_identifier_impls!(RadrootsEventId, hex_64('b').as_str()); assert_identifier_impls!(RadrootsEventSignature, hex_128('c').as_str()); assert_identifier_impls!(RadrootsDTag, "listing-1"); @@ -1239,7 +1258,7 @@ mod tests { #[allow(dead_code)] #[derive(Debug, serde::Deserialize)] struct MissingPublicKey { - value: RadrootsPublicKey, + value: PublicKey, } #[allow(dead_code)] #[derive(Debug, serde::Deserialize)] diff --git a/crates/event/src/knowledge.rs b/crates/event/src/knowledge.rs @@ -7,8 +7,8 @@ use core::fmt; use crate::RadrootsEventRef; use crate::ids::{ - RadrootsAddressableCoordinate, RadrootsDTag, RadrootsEventId, RadrootsPublicKey, - RadrootsRelayUrl, + RadrootsAddressableCoordinate, RadrootsDTag, RadrootsEventId, RadrootsRelayUrl, + parse_public_key, }; use crate::kinds::KIND_WIKI_ARTICLE; @@ -185,7 +185,7 @@ fn validate_pubkey( if value.trim().is_empty() { return Err(RadrootsKnowledgeValidationError::EmptyField(field)); } - RadrootsPublicKey::parse(value) + parse_public_key(value) .map(|_| ()) .map_err(|_| RadrootsKnowledgeValidationError::InvalidField(field)) } @@ -915,7 +915,7 @@ mod tests { use super::*; fn hex_64(character: char) -> String { - core::iter::repeat_n(character, 64).collect() + crate::test_valid_hex_64(character) } fn event_ref() -> RadrootsEventRef { @@ -925,7 +925,7 @@ mod tests { fn event_ref_with_kind(kind: u32) -> RadrootsEventRef { RadrootsEventRef { id: "0".repeat(64), - author: "1".repeat(64), + author: crate::test_valid_hex_64('1'), kind, d_tag: None, relays: None, diff --git a/crates/event/src/lib.rs b/crates/event/src/lib.rs @@ -9,6 +9,31 @@ use alloc::{string::String, vec::Vec}; pub const RADROOTS_EVENT_VERSION: &str = env!("CARGO_PKG_VERSION"); +#[cfg(test)] +/// Returns deterministic 64-character fixtures that are also valid secp256k1 +/// x-only public keys; labels without a curve point are remapped. +pub(crate) fn test_valid_hex_64(character: char) -> String { + if matches!(character, 'b' | 'B') { + let value = "2f8bde4d1a07209355b4a7250a5c5128e88b84bddc619ab7cba8d569b240efe4"; + return if character.is_ascii_uppercase() { + value.to_ascii_uppercase() + } else { + value.to_owned() + }; + } + let character = match character { + '0' => '7', + '1' => '8', + '5' => 'd', + '6' => 'e', + '9' => 'a', + 'c' | 'C' => '3', + 'f' | 'F' => '4', + other => other, + }; + core::iter::repeat_n(character, 64).collect() +} + pub mod account; pub mod app_data; pub mod article; diff --git a/crates/event/src/order.rs b/crates/event/src/order.rs @@ -9,7 +9,7 @@ use alloc::{ #[cfg(test)] use crate::ids::RadrootsOrderQuoteId; use crate::ids::{ - RadrootsClassifiedListingAddress, RadrootsInventoryBinId, RadrootsOrderId, RadrootsPublicKey, + PublicKey, RadrootsClassifiedListingAddress, RadrootsInventoryBinId, RadrootsOrderId, }; use crate::kinds::*; #[cfg(test)] @@ -142,8 +142,10 @@ impl RadrootsOrderEconomics { pub struct RadrootsOrderRequest { pub order_id: RadrootsOrderId, pub listing_addr: RadrootsClassifiedListingAddress, - pub buyer_pubkey: RadrootsPublicKey, - pub seller_pubkey: RadrootsPublicKey, + #[cfg_attr(feature = "dto-bindgen", dto(as = "string"))] + pub buyer_pubkey: PublicKey, + #[cfg_attr(feature = "dto-bindgen", dto(as = "string"))] + pub seller_pubkey: PublicKey, pub items: Vec<RadrootsOrderItem>, pub economics: RadrootsOrderEconomics, } @@ -152,8 +154,6 @@ impl RadrootsOrderRequest { pub fn validate(&self) -> Result<(), RadrootsOrderPayloadError> { validate_required_field(&self.order_id, "order_id")?; validate_required_field(&self.listing_addr, "listing_addr")?; - validate_required_field(&self.buyer_pubkey, "buyer_pubkey")?; - validate_required_field(&self.seller_pubkey, "seller_pubkey")?; validate_order_items(&self.items)?; self.economics.validate()?; validate_order_economics_binding(&self.items, &self.economics) @@ -222,8 +222,10 @@ impl RadrootsOrderDecisionOutcome { pub struct RadrootsOrderDecision { pub order_id: RadrootsOrderId, pub listing_addr: RadrootsClassifiedListingAddress, - pub buyer_pubkey: RadrootsPublicKey, - pub seller_pubkey: RadrootsPublicKey, + #[cfg_attr(feature = "dto-bindgen", dto(as = "string"))] + pub buyer_pubkey: PublicKey, + #[cfg_attr(feature = "dto-bindgen", dto(as = "string"))] + pub seller_pubkey: PublicKey, pub decision: RadrootsOrderDecisionOutcome, } @@ -231,8 +233,6 @@ impl RadrootsOrderDecision { pub fn validate(&self) -> Result<(), RadrootsOrderPayloadError> { validate_required_field(&self.order_id, "order_id")?; validate_required_field(&self.listing_addr, "listing_addr")?; - validate_required_field(&self.buyer_pubkey, "buyer_pubkey")?; - validate_required_field(&self.seller_pubkey, "seller_pubkey")?; self.decision.validate() } } @@ -248,8 +248,10 @@ impl RadrootsOrderDecision { pub struct RadrootsOrderCancellation { pub order_id: RadrootsOrderId, pub listing_addr: RadrootsClassifiedListingAddress, - pub buyer_pubkey: RadrootsPublicKey, - pub seller_pubkey: RadrootsPublicKey, + #[cfg_attr(feature = "dto-bindgen", dto(as = "string"))] + pub buyer_pubkey: PublicKey, + #[cfg_attr(feature = "dto-bindgen", dto(as = "string"))] + pub seller_pubkey: PublicKey, pub reason: String, } @@ -257,8 +259,6 @@ impl RadrootsOrderCancellation { pub fn validate(&self) -> Result<(), RadrootsOrderPayloadError> { validate_required_field(&self.order_id, "order_id")?; validate_required_field(&self.listing_addr, "listing_addr")?; - validate_required_field(&self.buyer_pubkey, "buyer_pubkey")?; - validate_required_field(&self.seller_pubkey, "seller_pubkey")?; validate_required_field(&self.reason, "reason") } } @@ -749,18 +749,15 @@ mod tests { use super::*; use radroots_core::{Currency, Decimal, Money, Unit}; - fn pubkey(character: char) -> RadrootsPublicKey { - core::iter::repeat_n(character, 64) - .collect::<String>() - .parse() - .unwrap() + fn pubkey(character: char) -> PublicKey { + crate::test_valid_hex_64(character).parse().unwrap() } - fn buyer_pubkey() -> RadrootsPublicKey { + fn buyer_pubkey() -> PublicKey { pubkey('b') } - fn seller_pubkey() -> RadrootsPublicKey { + fn seller_pubkey() -> PublicKey { pubkey('a') } diff --git a/crates/event/src/reply.rs b/crates/event/src/reply.rs @@ -3,7 +3,7 @@ use alloc::string::String; use core::fmt; use crate::{ - ids::{RadrootsEventId, RadrootsIdParseError, RadrootsPublicKey}, + ids::{PublicKey, RadrootsEventId, RadrootsIdParseError, parse_public_key}, post::{ RADROOTS_POST_CONTENT_MAX_BYTES, RADROOTS_POST_EVENT_WIRE_MAX_BYTES, RADROOTS_POST_TAG_ELEMENT_MAX_BYTES, RADROOTS_POST_TAG_TOTAL_MAX_BYTES, @@ -116,14 +116,14 @@ impl std::error::Error for RadrootsNip10ReplyError { #[derive(Clone, Debug, PartialEq, Eq)] pub struct RadrootsNip10ReplyReference { event_id: RadrootsEventId, - author: RadrootsPublicKey, + author: PublicKey, relay: Option<RadrootsNostrRelayHint>, } impl RadrootsNip10ReplyReference { pub fn new( event_id: RadrootsEventId, - author: RadrootsPublicKey, + author: PublicKey, relay: Option<RadrootsNostrRelayHint>, ) -> Result<Self, RadrootsNip10ReplyError> { if let Some(relay) = &relay { @@ -143,8 +143,7 @@ impl RadrootsNip10ReplyReference { ) -> Result<Self, RadrootsNip10ReplyError> { let event_id = RadrootsEventId::parse(event_id).map_err(RadrootsNip10ReplyError::EventIdInvalid)?; - let author = - RadrootsPublicKey::parse(author).map_err(RadrootsNip10ReplyError::AuthorInvalid)?; + let author = parse_public_key(author).map_err(RadrootsNip10ReplyError::AuthorInvalid)?; let relay = match relay { None | Some("") => None, Some(relay) => Some( @@ -159,7 +158,7 @@ impl RadrootsNip10ReplyReference { &self.event_id } - pub const fn author(&self) -> &RadrootsPublicKey { + pub const fn author(&self) -> &PublicKey { &self.author } @@ -296,14 +295,14 @@ fn validate_authored_reply_wire_size( &mut tag_bytes, &mut tags_json_bytes, &mut tag_count, - &["p", root.author.as_str()], + &["p", root.author.to_hex().as_str()], ); if let Some(parent) = parent.filter(|parent| parent.author != root.author) { add_tag( &mut tag_bytes, &mut tags_json_bytes, &mut tag_count, - &["p", parent.author.as_str()], + &["p", parent.author.to_hex().as_str()], ); } @@ -362,7 +361,7 @@ mod tests { fn reference(event: char, author: char) -> RadrootsNip10ReplyReference { RadrootsNip10ReplyReference::parse( event.to_string().repeat(64), - author.to_string().repeat(64), + crate::test_valid_hex_64(author), Some("wss://relay.example"), ) .expect("reference") @@ -404,19 +403,20 @@ mod tests { fn parses_and_canonicalizes_reference_identifiers() { let reference = RadrootsNip10ReplyReference::parse( "A".repeat(64), - "B".repeat(64), + crate::test_valid_hex_64('B'), Some("wss://relay.example"), ) .expect("reference"); assert_eq!(reference.event_id().as_str(), "a".repeat(64)); - assert_eq!(reference.author().as_str(), "b".repeat(64)); + assert_eq!(reference.author().to_hex(), crate::test_valid_hex_64('b')); assert_eq!( reference.relay().expect("relay").as_str(), "wss://relay.example" ); let error = - RadrootsNip10ReplyReference::parse("not-an-id", "b".repeat(64), None).unwrap_err(); + RadrootsNip10ReplyReference::parse("not-an-id", crate::test_valid_hex_64('b'), None) + .unwrap_err(); assert_eq!(error.code(), "reply_event_id_invalid"); } @@ -441,13 +441,17 @@ mod tests { "{prefix}{}", "a".repeat(RADROOTS_POST_TAG_ELEMENT_MAX_BYTES - prefix.len()) ); - RadrootsNip10ReplyReference::parse("a".repeat(64), "b".repeat(64), Some(&exact_relay)) - .expect("exact tag-element limit"); + RadrootsNip10ReplyReference::parse( + "a".repeat(64), + crate::test_valid_hex_64('b'), + Some(&exact_relay), + ) + .expect("exact tag-element limit"); let overflow_relay = format!("{exact_relay}a"); assert!(matches!( RadrootsNip10ReplyReference::parse( "a".repeat(64), - "b".repeat(64), + crate::test_valid_hex_64('b'), Some(&overflow_relay), ), Err(RadrootsNip10ReplyError::TagElementTooLarge { diff --git a/crates/event/src/repost.rs b/crates/event/src/repost.rs @@ -40,13 +40,13 @@ mod tests { fn repost_models_represent_note_and_generic_targets() { let note_target = RadrootsSocialTarget::Event { id: "a".repeat(64), - author: Some("b".repeat(64)), + author: Some(crate::test_valid_hex_64('b')), event_kind: Some(1), relays: None, }; let article_target = RadrootsSocialTarget::Address { address: "30023:pubkey:article".to_string(), - author: Some("b".repeat(64)), + author: Some(crate::test_valid_hex_64('b')), event_kind: Some(30023), relays: Some(vec!["wss://relay.example".to_string()]), }; diff --git a/crates/event/src/social.rs b/crates/event/src/social.rs @@ -149,7 +149,7 @@ mod tests { fn supports_nip22_target_shapes() { let event = RadrootsSocialTarget::Event { id: "a".repeat(64), - author: Some("b".repeat(64)), + author: Some(crate::test_valid_hex_64('b')), event_kind: Some(30023), relays: Some(vec!["wss://relay.example".to_string()]), }; diff --git a/crates/event/src/trade.rs b/crates/event/src/trade.rs @@ -12,8 +12,8 @@ use std::{string::String, vec::Vec}; use core::fmt; use crate::ids::{ - RadrootsClassifiedListingAddress, RadrootsDTag, RadrootsEventId, RadrootsIdParseError, - RadrootsInventoryBinId, RadrootsPublicKey, RadrootsTradeCandidateId, RadrootsTradeId, + PublicKey, RadrootsClassifiedListingAddress, RadrootsDTag, RadrootsEventId, + RadrootsIdParseError, RadrootsInventoryBinId, RadrootsTradeCandidateId, RadrootsTradeId, RadrootsTradeMutationId, }; use crate::kinds::{ @@ -94,12 +94,12 @@ pub struct RadrootsTradeMutationEnvelopeV1 { pub schema_version: u16, pub trade_id: RadrootsTradeId, pub root_mutation_id: Option<RadrootsTradeMutationId>, - pub buyer_pubkey: RadrootsPublicKey, - pub seller_pubkey: RadrootsPublicKey, + pub buyer_pubkey: PublicKey, + pub seller_pubkey: PublicKey, pub farm_id: RadrootsDTag, pub parent_mutation_ids: Vec<RadrootsTradeMutationId>, - pub author_pubkey: RadrootsPublicKey, - pub counterparty_pubkey: RadrootsPublicKey, + pub author_pubkey: PublicKey, + pub counterparty_pubkey: PublicKey, pub authored_at_unix_s: u64, pub body: RadrootsTradeMutationBodyV1, } @@ -217,8 +217,8 @@ pub struct RadrootsTradeCandidateTermsV1 { pub schema_version: u16, pub base_candidate_id: Option<RadrootsTradeCandidateId>, pub supersession_intent: Option<String>, - pub buyer_pubkey: RadrootsPublicKey, - pub seller_pubkey: RadrootsPublicKey, + pub buyer_pubkey: PublicKey, + pub seller_pubkey: PublicKey, pub farm_id: RadrootsDTag, pub lines: Vec<RadrootsTradeCandidateLineV1>, pub line_tombstones: Vec<RadrootsTradeLineTombstoneV1>, @@ -451,7 +451,7 @@ impl RadrootsTradePrivateTermsRefV1 { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct RadrootsSellerReservationAssertionV1 { pub reservation_id: RadrootsDTag, - pub inventory_authority_id: RadrootsPublicKey, + pub inventory_authority_id: PublicKey, pub inventory_epoch: u64, pub candidate_id: RadrootsTradeCandidateId, pub commitments: Vec<RadrootsSellerReservationLineV1>, @@ -1076,6 +1076,7 @@ impl<'de> Visitor<'de> for NoDuplicateJsonValueVisitor { #[cfg(all(test, feature = "serde"))] mod tests { use super::*; + use crate::ids::parse_public_key; fn hex_64(character: char) -> String { core::iter::repeat_n(character, 64).collect() @@ -1085,8 +1086,8 @@ mod tests { core::iter::repeat_n(character, 32).collect() } - fn pubkey(character: char) -> RadrootsPublicKey { - RadrootsPublicKey::parse(hex_64(character)).unwrap() + fn pubkey(character: char) -> PublicKey { + parse_public_key(crate::test_valid_hex_64(character)).unwrap() } fn event_id(character: char) -> RadrootsEventId { @@ -1118,7 +1119,7 @@ mod tests { line_id: RadrootsDTag::parse("line-1").unwrap(), listing_addr: RadrootsClassifiedListingAddress::parse(format!( "30402:{}:listing-1", - hex_64('b') + crate::test_valid_hex_64('b') )) .unwrap(), listing_event_id: event_id('c'), @@ -1559,7 +1560,7 @@ mod tests { fn trade_candidate_deserialization_rejects_non_classified_listing_coordinates() { let mut value = serde_json::to_value(candidate()).expect("candidate json"); value["lines"][0]["listing_addr"] = - serde_json::json!(format!("30023:{}:listing-1", hex_64('b'))); + serde_json::json!(format!("30023:{}:listing-1", crate::test_valid_hex_64('b'))); assert!(serde_json::from_value::<RadrootsTradeCandidateTermsV1>(value).is_err()); } diff --git a/crates/event/src/wire/v1.rs b/crates/event/src/wire/v1.rs @@ -15,9 +15,7 @@ use std::{collections::BTreeMap, string::String, vec::Vec}; use crate::envelope::{ RadrootsEventEnvelope, RadrootsEventEnvelopeError, RadrootsEventEnvelopeParts, }; -use crate::ids::{ - RadrootsEventId, RadrootsEventSignature, RadrootsIdParseError, RadrootsPublicKey, -}; +use crate::ids::{RadrootsEventId, RadrootsEventSignature, RadrootsIdParseError, parse_public_key}; use core::fmt; use serde_json::{Map, Value}; use sha2::{Digest, Sha256}; @@ -376,8 +374,8 @@ pub fn canonical_nip01_event_id_preimage_v1( tags: &[Vec<String>], content: &str, ) -> Result<String, RadrootsCanonicalEventIdError> { - let pubkey = - RadrootsPublicKey::parse(pubkey).map_err(RadrootsCanonicalEventIdError::InvalidPubkey)?; + let pubkey = parse_public_key(pubkey).map_err(RadrootsCanonicalEventIdError::InvalidPubkey)?; + let pubkey = pubkey.to_hex(); let mut preimage = String::new(); preimage.push_str("[0,"); push_canonical_json_string(&mut preimage, pubkey.as_str()); @@ -453,13 +451,13 @@ fn take_canonical_pubkey( object: &mut Map<String, Value>, ) -> Result<String, RadrootsEventWireError> { let raw = take_string(object, "pubkey")?; - let parsed = RadrootsPublicKey::parse(raw.as_str()).map_err(|error| { + let parsed = parse_public_key(raw.as_str()).map_err(|error| { RadrootsEventWireError::InvalidIdentifier { field: "pubkey", error, } })?; - canonical_identifier_string("pubkey", raw, parsed.into_string()) + canonical_identifier_string("pubkey", raw, parsed.to_hex()) } fn take_canonical_signature( diff --git a/crates/event/src/wire/v1/tests.rs b/crates/event/src/wire/v1/tests.rs @@ -2,7 +2,7 @@ use super::*; use serde_json::json; fn hex_64(character: char) -> String { - core::iter::repeat_n(character, 64).collect() + crate::test_valid_hex_64(character) } fn hex_128(character: char) -> String { diff --git a/crates/event/tests/source_boundary.rs b/crates/event/tests/source_boundary.rs @@ -8,6 +8,10 @@ struct ForbiddenEventName { const FORBIDDEN_EVENT_NAMES: &[ForbiddenEventName] = &[ ForbiddenEventName { + pattern: "RadrootsPublicKey", + reason: "public author keys must use radroots_identity::PublicKey", + }, + ForbiddenEventName { pattern: "pub fn from_wire_unchecked", reason: "unchecked signed-event construction must not be public API", }, @@ -113,7 +117,8 @@ fn collect_source_boundary_guard_files(root: &Path, paths: &mut Vec<PathBuf>) { } fn forbidden_event_name_allowed(relative_path: &str, pattern: &str) -> bool { - pattern == "RadrootsNostrEvent" && is_nostr_protocol_context(relative_path) + (pattern == "RadrootsPublicKey" && !relative_path.starts_with("crates/event/")) + || (pattern == "RadrootsNostrEvent" && is_nostr_protocol_context(relative_path)) } fn is_nostr_protocol_context(relative_path: &str) -> bool {