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:
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 {