deletion.rs (30067B)
1 #![forbid(unsafe_code)] 2 3 #[cfg(not(feature = "std"))] 4 use alloc::{ 5 string::{String, ToString}, 6 vec::Vec, 7 }; 8 #[cfg(feature = "std")] 9 use std::{string::String, vec::Vec}; 10 11 use core::fmt; 12 13 use crate::{ 14 id::{EventId, Nip01Coordinate, Nip01CoordinateParseError, ParseError}, 15 wire::{ 16 DEFAULT_CONTENT_MAX_BYTES, DEFAULT_RAW_JSON_MAX_BYTES, DEFAULT_TAG_ELEMENT_MAX_BYTES, 17 DEFAULT_TAG_MAX_COUNT, DEFAULT_TAG_TOTAL_ELEMENT_MAX_COUNT, DEFAULT_TAG_TOTAL_MAX_BYTES, 18 }, 19 }; 20 21 pub const RADROOTS_NIP09_DELETION_CONTENT_MAX_BYTES: usize = DEFAULT_CONTENT_MAX_BYTES; 22 pub const RADROOTS_NIP09_DELETION_TAG_MAX_COUNT: usize = DEFAULT_TAG_MAX_COUNT; 23 pub const RADROOTS_NIP09_DELETION_TAG_TOTAL_ELEMENT_MAX_COUNT: usize = 24 DEFAULT_TAG_TOTAL_ELEMENT_MAX_COUNT; 25 pub const RADROOTS_NIP09_DELETION_TAG_ELEMENT_MAX_BYTES: usize = DEFAULT_TAG_ELEMENT_MAX_BYTES; 26 pub const RADROOTS_NIP09_DELETION_TAG_TOTAL_MAX_BYTES: usize = DEFAULT_TAG_TOTAL_MAX_BYTES; 27 pub const RADROOTS_NIP09_DELETION_EVENT_WIRE_MAX_BYTES: usize = DEFAULT_RAW_JSON_MAX_BYTES; 28 pub const RADROOTS_NIP09_DELETION_TARGET_KIND_MAX: u32 = u16::MAX as u32; 29 30 const RADROOTS_NIP09_DELETION_SIGNED_EVENT_FIXED_MAX_BYTES: usize = "{\"id\":\"".len() 31 + 64 32 + "\",\"pubkey\":\"".len() 33 + 64 34 + "\",\"created_at\":".len() 35 + 20 36 + ",\"kind\":5,\"tags\":".len() 37 + ",\"content\":".len() 38 + ",\"sig\":\"".len() 39 + 128 40 + "\"}".len(); 41 42 #[non_exhaustive] 43 #[derive(Clone, Debug, PartialEq, Eq)] 44 pub enum Nip09DeletionError { 45 ContentTooLarge { max: usize, actual: usize }, 46 EventIdInvalid(ParseError), 47 CoordinateInvalid(Nip01CoordinateParseError), 48 TargetKindOutOfRange { max: u32, actual: u32 }, 49 DuplicateEventTarget { event_id: String }, 50 DuplicateAddressTarget { coordinate: String }, 51 TargetMissing, 52 TagCountExceeded { max: usize, actual: usize }, 53 TagElementTooLarge { max: usize, actual: usize }, 54 TagBytesExceeded { max: usize, actual: usize }, 55 EventWireTooLarge { max: usize, actual: usize }, 56 } 57 58 impl Nip09DeletionError { 59 pub const fn code(&self) -> &'static str { 60 match self { 61 Self::ContentTooLarge { .. } => "deletion_content_too_large", 62 Self::EventIdInvalid(_) | Self::TargetKindOutOfRange { .. } => { 63 "deletion_event_target_invalid" 64 } 65 Self::CoordinateInvalid(_) => "deletion_address_target_invalid", 66 Self::DuplicateEventTarget { .. } => "deletion_event_target_duplicate", 67 Self::DuplicateAddressTarget { .. } => "deletion_address_target_duplicate", 68 Self::TargetMissing => "deletion_target_missing", 69 Self::TagCountExceeded { .. } => "deletion_tag_count_exceeded", 70 Self::TagElementTooLarge { .. } => "deletion_tag_element_too_large", 71 Self::TagBytesExceeded { .. } => "deletion_tag_bytes_exceeded", 72 Self::EventWireTooLarge { .. } => "deletion_event_wire_too_large", 73 } 74 } 75 } 76 77 impl fmt::Display for Nip09DeletionError { 78 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 79 match self { 80 Self::ContentTooLarge { max, actual } => write!( 81 formatter, 82 "authored NIP-09 deletion content is {actual} bytes; max is {max}" 83 ), 84 Self::EventIdInvalid(error) => { 85 write!( 86 formatter, 87 "NIP-09 deletion event target is invalid: {error}" 88 ) 89 } 90 Self::CoordinateInvalid(error) => { 91 write!( 92 formatter, 93 "NIP-09 deletion address target is invalid: {error}" 94 ) 95 } 96 Self::TargetKindOutOfRange { max, actual } => write!( 97 formatter, 98 "NIP-09 deletion event target kind {actual} exceeds {max}" 99 ), 100 Self::DuplicateEventTarget { event_id } => write!( 101 formatter, 102 "NIP-09 deletion event target `{event_id}` is duplicated" 103 ), 104 Self::DuplicateAddressTarget { coordinate } => write!( 105 formatter, 106 "NIP-09 deletion address target {coordinate:?} is duplicated" 107 ), 108 Self::TargetMissing => { 109 formatter.write_str("authored NIP-09 deletion requires an event or address target") 110 } 111 Self::TagCountExceeded { max, actual } => write!( 112 formatter, 113 "authored NIP-09 deletion has {actual} tags; max is {max}" 114 ), 115 Self::TagElementTooLarge { max, actual } => write!( 116 formatter, 117 "authored NIP-09 deletion tag element is {actual} bytes; max is {max}" 118 ), 119 Self::TagBytesExceeded { max, actual } => write!( 120 formatter, 121 "authored NIP-09 deletion tag bytes are {actual}; max is {max}" 122 ), 123 Self::EventWireTooLarge { max, actual } => write!( 124 formatter, 125 "authored NIP-09 deletion maximum canonical signed event size is {actual} bytes; max is {max}" 126 ), 127 } 128 } 129 } 130 131 #[cfg(feature = "std")] 132 impl std::error::Error for Nip09DeletionError { 133 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 134 match self { 135 Self::EventIdInvalid(error) => Some(error), 136 Self::CoordinateInvalid(error) => Some(error), 137 _ => None, 138 } 139 } 140 } 141 142 /// One event-id target with caller-asserted target-kind metadata. 143 /// 144 /// The kind hint is required for canonical authored `k` tags. This type does 145 /// not prove that the target exists or actually has the asserted kind. 146 #[derive(Clone, Debug, PartialEq, Eq)] 147 pub struct Nip09DeletionEventTarget { 148 event_id: EventId, 149 kind_hint: u32, 150 } 151 152 impl Nip09DeletionEventTarget { 153 pub fn new(event_id: EventId, kind_hint: u32) -> Result<Self, Nip09DeletionError> { 154 if kind_hint > RADROOTS_NIP09_DELETION_TARGET_KIND_MAX { 155 return Err(Nip09DeletionError::TargetKindOutOfRange { 156 max: RADROOTS_NIP09_DELETION_TARGET_KIND_MAX, 157 actual: kind_hint, 158 }); 159 } 160 Ok(Self { 161 event_id, 162 kind_hint, 163 }) 164 } 165 166 pub fn parse(event_id: impl AsRef<str>, kind_hint: u32) -> Result<Self, Nip09DeletionError> { 167 Self::new( 168 EventId::parse(event_id).map_err(Nip09DeletionError::EventIdInvalid)?, 169 kind_hint, 170 ) 171 } 172 173 #[inline] 174 pub const fn event_id(&self) -> &EventId { 175 &self.event_id 176 } 177 178 #[inline] 179 pub const fn kind_hint(&self) -> u32 { 180 self.kind_hint 181 } 182 } 183 184 /// One NIP-01 replaceable or addressable coordinate target. 185 #[derive(Clone, Debug, PartialEq, Eq)] 186 pub struct Nip09DeletionAddressTarget { 187 coordinate: Nip01Coordinate, 188 } 189 190 impl Nip09DeletionAddressTarget { 191 pub const fn new(coordinate: Nip01Coordinate) -> Self { 192 Self { coordinate } 193 } 194 195 pub fn parse(coordinate: impl AsRef<str>) -> Result<Self, Nip09DeletionError> { 196 let coordinate = coordinate.as_ref(); 197 if coordinate.len() > RADROOTS_NIP09_DELETION_TAG_ELEMENT_MAX_BYTES { 198 return Err(Nip09DeletionError::TagElementTooLarge { 199 max: RADROOTS_NIP09_DELETION_TAG_ELEMENT_MAX_BYTES, 200 actual: coordinate.len(), 201 }); 202 } 203 Nip01Coordinate::parse(coordinate) 204 .map(Self::new) 205 .map_err(Nip09DeletionError::CoordinateInvalid) 206 } 207 208 #[inline] 209 pub const fn coordinate(&self) -> &Nip01Coordinate { 210 &self.coordinate 211 } 212 213 #[inline] 214 pub const fn kind_hint(&self) -> u32 { 215 self.coordinate.kind() 216 } 217 } 218 219 /// Strict authored kind-5 NIP-09 deletion request. 220 /// 221 /// Targets are canonicalized at construction. Event targets sort by event ID, 222 /// address targets sort by coordinate, and derived kind hints are unique and 223 /// ascending. The request remains an immutable protocol statement; it performs 224 /// no target-author authorization or deletion effect. 225 /// 226 /// This type is opaque and has no Serde construction path. 227 /// 228 /// ```compile_fail 229 /// let _: radroots_event::post::deletion::AuthoredNip09DeletionRequest = 230 /// serde_json::from_str( 231 /// r#"{"content":"","event_targets":[],"address_targets":[]}"# 232 /// ).unwrap(); 233 /// ``` 234 #[derive(Clone, Debug, PartialEq, Eq)] 235 pub struct AuthoredNip09DeletionRequest { 236 content: String, 237 event_targets: Vec<Nip09DeletionEventTarget>, 238 address_targets: Vec<Nip09DeletionAddressTarget>, 239 kind_hints: Vec<u32>, 240 maximum_signed_event_wire_bytes: usize, 241 } 242 243 impl AuthoredNip09DeletionRequest { 244 pub fn new( 245 content: impl Into<String>, 246 mut event_targets: Vec<Nip09DeletionEventTarget>, 247 mut address_targets: Vec<Nip09DeletionAddressTarget>, 248 ) -> Result<Self, Nip09DeletionError> { 249 let content = content.into(); 250 if content.len() > RADROOTS_NIP09_DELETION_CONTENT_MAX_BYTES { 251 return Err(Nip09DeletionError::ContentTooLarge { 252 max: RADROOTS_NIP09_DELETION_CONTENT_MAX_BYTES, 253 actual: content.len(), 254 }); 255 } 256 if event_targets.is_empty() && address_targets.is_empty() { 257 return Err(Nip09DeletionError::TargetMissing); 258 } 259 260 let kind_hints = collect_authored_deletion_kind_hints(&event_targets, &address_targets)?; 261 262 let maximum_signed_event_wire_bytes = validate_authored_deletion_wire_size( 263 &content, 264 &event_targets, 265 &address_targets, 266 &kind_hints, 267 )?; 268 269 event_targets.sort_by_key(|target| target.event_id); 270 if let Some(duplicates) = event_targets 271 .windows(2) 272 .find(|targets| targets[0].event_id == targets[1].event_id) 273 { 274 return Err(Nip09DeletionError::DuplicateEventTarget { 275 event_id: duplicates[0].event_id.to_string(), 276 }); 277 } 278 279 address_targets.sort_by(|left, right| left.coordinate.cmp(&right.coordinate)); 280 if let Some(duplicates) = address_targets 281 .windows(2) 282 .find(|targets| targets[0].coordinate == targets[1].coordinate) 283 { 284 return Err(Nip09DeletionError::DuplicateAddressTarget { 285 coordinate: duplicates[0].coordinate.to_string(), 286 }); 287 } 288 289 Ok(Self { 290 content, 291 event_targets, 292 address_targets, 293 kind_hints, 294 maximum_signed_event_wire_bytes, 295 }) 296 } 297 298 #[inline] 299 pub fn content(&self) -> &str { 300 self.content.as_str() 301 } 302 303 #[inline] 304 pub fn event_targets(&self) -> &[Nip09DeletionEventTarget] { 305 self.event_targets.as_slice() 306 } 307 308 #[inline] 309 pub fn address_targets(&self) -> &[Nip09DeletionAddressTarget] { 310 self.address_targets.as_slice() 311 } 312 313 #[inline] 314 pub fn kind_hints(&self) -> &[u32] { 315 self.kind_hints.as_slice() 316 } 317 318 #[inline] 319 pub fn target_count(&self) -> usize { 320 self.event_targets 321 .len() 322 .saturating_add(self.address_targets.len()) 323 } 324 325 /// Compact canonical signed-event size using `u64::MAX` for `created_at`. 326 #[inline] 327 pub const fn maximum_signed_event_wire_bytes(&self) -> usize { 328 self.maximum_signed_event_wire_bytes 329 } 330 } 331 332 fn collect_authored_deletion_kind_hints( 333 event_targets: &[Nip09DeletionEventTarget], 334 address_targets: &[Nip09DeletionAddressTarget], 335 ) -> Result<Vec<u32>, Nip09DeletionError> { 336 const WORD_BITS: usize = u64::BITS as usize; 337 const WORD_COUNT: usize = (RADROOTS_NIP09_DELETION_TARGET_KIND_MAX as usize + 1) / WORD_BITS; 338 339 let mut seen = [0_u64; WORD_COUNT]; 340 let mut unique_kind_count = 0usize; 341 for kind in event_targets 342 .iter() 343 .map(|target| target.kind_hint()) 344 .chain(address_targets.iter().map(|target| target.kind_hint())) 345 { 346 let kind = kind as usize; 347 let word = kind / WORD_BITS; 348 let mask = 1_u64 << (kind % WORD_BITS); 349 if seen[word] & mask == 0 { 350 seen[word] |= mask; 351 unique_kind_count = unique_kind_count.saturating_add(1); 352 } 353 } 354 355 let target_count = event_targets.len().saturating_add(address_targets.len()); 356 let tag_count = target_count.saturating_add(unique_kind_count); 357 if tag_count > RADROOTS_NIP09_DELETION_TAG_MAX_COUNT { 358 return Err(Nip09DeletionError::TagCountExceeded { 359 max: RADROOTS_NIP09_DELETION_TAG_MAX_COUNT, 360 actual: tag_count, 361 }); 362 } 363 364 let mut kind_hints = Vec::with_capacity(unique_kind_count); 365 for (word_index, word) in seen.into_iter().enumerate() { 366 let mut remaining = word; 367 while remaining != 0 { 368 let bit = remaining.trailing_zeros() as usize; 369 kind_hints.push((word_index * WORD_BITS + bit) as u32); 370 remaining &= remaining - 1; 371 } 372 } 373 Ok(kind_hints) 374 } 375 376 fn validate_authored_deletion_wire_size( 377 content: &str, 378 event_targets: &[Nip09DeletionEventTarget], 379 address_targets: &[Nip09DeletionAddressTarget], 380 kind_hints: &[u32], 381 ) -> Result<usize, Nip09DeletionError> { 382 let tag_count = event_targets 383 .len() 384 .saturating_add(address_targets.len()) 385 .saturating_add(kind_hints.len()); 386 if tag_count > RADROOTS_NIP09_DELETION_TAG_MAX_COUNT { 387 return Err(Nip09DeletionError::TagCountExceeded { 388 max: RADROOTS_NIP09_DELETION_TAG_MAX_COUNT, 389 actual: tag_count, 390 }); 391 } 392 393 let mut tag_bytes = 0usize; 394 let mut tags_json_bytes = 2usize; 395 let mut visited_tags = 0usize; 396 for target in event_targets { 397 let event_id = target.event_id().to_hex(); 398 add_tag_size( 399 &mut tag_bytes, 400 &mut tags_json_bytes, 401 &mut visited_tags, 402 "e", 403 event_id.as_str(), 404 )?; 405 } 406 for target in address_targets { 407 add_tag_size( 408 &mut tag_bytes, 409 &mut tags_json_bytes, 410 &mut visited_tags, 411 "a", 412 target.coordinate().as_str(), 413 )?; 414 } 415 for kind in kind_hints { 416 let kind = kind.to_string(); 417 add_tag_size( 418 &mut tag_bytes, 419 &mut tags_json_bytes, 420 &mut visited_tags, 421 "k", 422 kind.as_str(), 423 )?; 424 } 425 426 if tag_bytes > RADROOTS_NIP09_DELETION_TAG_TOTAL_MAX_BYTES { 427 return Err(Nip09DeletionError::TagBytesExceeded { 428 max: RADROOTS_NIP09_DELETION_TAG_TOTAL_MAX_BYTES, 429 actual: tag_bytes, 430 }); 431 } 432 433 let actual = RADROOTS_NIP09_DELETION_SIGNED_EVENT_FIXED_MAX_BYTES 434 .saturating_add(tags_json_bytes) 435 .saturating_add(canonical_json_string_bytes(content)); 436 if actual > RADROOTS_NIP09_DELETION_EVENT_WIRE_MAX_BYTES { 437 return Err(Nip09DeletionError::EventWireTooLarge { 438 max: RADROOTS_NIP09_DELETION_EVENT_WIRE_MAX_BYTES, 439 actual, 440 }); 441 } 442 Ok(actual) 443 } 444 445 fn add_tag_size( 446 tag_bytes: &mut usize, 447 tags_json_bytes: &mut usize, 448 tag_count: &mut usize, 449 name: &str, 450 value: &str, 451 ) -> Result<(), Nip09DeletionError> { 452 for element in [name, value] { 453 if element.len() > RADROOTS_NIP09_DELETION_TAG_ELEMENT_MAX_BYTES { 454 return Err(Nip09DeletionError::TagElementTooLarge { 455 max: RADROOTS_NIP09_DELETION_TAG_ELEMENT_MAX_BYTES, 456 actual: element.len(), 457 }); 458 } 459 *tag_bytes = tag_bytes.saturating_add(element.len()); 460 } 461 if *tag_count > 0 { 462 *tags_json_bytes = tags_json_bytes.saturating_add(1); 463 } 464 *tags_json_bytes = tags_json_bytes 465 .saturating_add(2) 466 .saturating_add(canonical_json_string_bytes(name)) 467 .saturating_add(1) 468 .saturating_add(canonical_json_string_bytes(value)); 469 *tag_count = tag_count.saturating_add(1); 470 Ok(()) 471 } 472 473 fn canonical_json_string_bytes(value: &str) -> usize { 474 value.chars().fold(2usize, |total, character| { 475 total.saturating_add(match character { 476 '"' | '\\' | '\u{0008}' | '\t' | '\n' | '\u{000c}' | '\r' => 2, 477 '\u{0000}'..='\u{001f}' => 6, 478 _ => character.len_utf8(), 479 }) 480 }) 481 } 482 483 #[cfg(test)] 484 #[cfg_attr(coverage_nightly, coverage(off))] 485 mod tests { 486 use super::*; 487 use crate::id::RADROOTS_NIP01_COORDINATE_MAX_BYTES; 488 489 fn event_target(character: char, kind: u32) -> Nip09DeletionEventTarget { 490 Nip09DeletionEventTarget::parse(character.to_string().repeat(64), kind) 491 .expect("event target") 492 } 493 494 fn numeric_event_target(index: usize, kind: u32) -> Nip09DeletionEventTarget { 495 Nip09DeletionEventTarget::parse(format!("{index:064x}"), kind) 496 .expect("numeric event target") 497 } 498 499 fn address_target(kind: u32, character: char, identifier: &str) -> Nip09DeletionAddressTarget { 500 Nip09DeletionAddressTarget::parse(format!( 501 "{kind}:{}:{identifier}", 502 crate::test_valid_hex_64(character) 503 )) 504 .expect("address target") 505 } 506 507 fn address_target_with_total_bytes( 508 total_bytes: usize, 509 index: usize, 510 ) -> Nip09DeletionAddressTarget { 511 let prefix = format!("30000:{}:", crate::test_valid_hex_64('a')); 512 let suffix = format!("{index:04x}"); 513 assert!(prefix.len() + suffix.len() <= total_bytes); 514 Nip09DeletionAddressTarget::parse(format!( 515 "{prefix}{}{suffix}", 516 "x".repeat(total_bytes - prefix.len() - suffix.len()) 517 )) 518 .expect("fixed-size address target") 519 } 520 521 #[test] 522 fn target_constructors_validate_identity_and_kind_hint() { 523 let target = 524 Nip09DeletionEventTarget::parse("A".repeat(64), u16::MAX as u32).expect("event target"); 525 assert_eq!(target.event_id().to_hex(), "a".repeat(64)); 526 assert_eq!(target.kind_hint(), u16::MAX as u32); 527 assert_eq!( 528 Nip09DeletionEventTarget::parse("5".repeat(64), 5) 529 .expect("kind-5 target") 530 .kind_hint(), 531 5 532 ); 533 assert!(matches!( 534 Nip09DeletionEventTarget::parse("not-an-id", 1), 535 Err(Nip09DeletionError::EventIdInvalid( 536 ParseError::InvalidLength { 537 expected: 64, 538 actual: 9 539 } 540 )) 541 )); 542 assert_eq!( 543 Nip09DeletionEventTarget::parse("a".repeat(64), u16::MAX as u32 + 1).unwrap_err(), 544 Nip09DeletionError::TargetKindOutOfRange { 545 max: u16::MAX as u32, 546 actual: u16::MAX as u32 + 1, 547 } 548 ); 549 550 let address = 551 Nip09DeletionAddressTarget::parse(format!("30000:{}:", crate::test_valid_hex_64('B'))) 552 .expect("address target"); 553 assert_eq!( 554 address.coordinate().as_str(), 555 format!("30000:{}:", crate::test_valid_hex_64('b')) 556 ); 557 assert_eq!(address.kind_hint(), 30_000); 558 559 let coordinate_prefix = format!("30000:{}:", "a".repeat(64)); 560 let oversized_coordinate = format!( 561 "{coordinate_prefix}{}", 562 "x".repeat(RADROOTS_NIP09_DELETION_TAG_ELEMENT_MAX_BYTES + 1 - coordinate_prefix.len()) 563 ); 564 assert_eq!( 565 Nip09DeletionAddressTarget::parse(oversized_coordinate), 566 Err(Nip09DeletionError::TagElementTooLarge { 567 max: RADROOTS_NIP09_DELETION_TAG_ELEMENT_MAX_BYTES, 568 actual: RADROOTS_NIP09_DELETION_TAG_ELEMENT_MAX_BYTES + 1, 569 }) 570 ); 571 } 572 573 #[test] 574 fn authored_request_allows_event_address_and_mixed_batches() { 575 let event_only = 576 AuthoredNip09DeletionRequest::new("", vec![event_target('a', 1)], Vec::new()) 577 .expect("event-only request"); 578 assert_eq!(event_only.target_count(), 1); 579 assert_eq!(event_only.kind_hints(), &[1]); 580 581 let address_only = AuthoredNip09DeletionRequest::new( 582 "withdrawn", 583 Vec::new(), 584 vec![address_target(30_402, 'b', "victoria-kale")], 585 ) 586 .expect("address-only request"); 587 assert_eq!(address_only.target_count(), 1); 588 assert_eq!(address_only.kind_hints(), &[30_402]); 589 590 let mixed = AuthoredNip09DeletionRequest::new( 591 "\tæ€å ð±\n", 592 vec![event_target('c', 31_922)], 593 vec![address_target(30_402, 'd', "victoria-carrots")], 594 ) 595 .expect("mixed request"); 596 assert_eq!(mixed.content(), "\tæ€å ð±\n"); 597 assert_eq!(mixed.target_count(), 2); 598 assert_eq!(mixed.kind_hints(), &[30_402, 31_922]); 599 } 600 601 #[test] 602 fn authored_request_sorts_targets_and_deduplicates_kind_hints() { 603 let request = AuthoredNip09DeletionRequest::new( 604 "duplicate crop listing", 605 vec![event_target('f', 30_402), event_target('a', 1)], 606 vec![ 607 address_target(31_923, 'e', "harvest"), 608 address_target(30_402, 'b', "produce"), 609 ], 610 ) 611 .expect("canonical request"); 612 613 assert_eq!( 614 request.event_targets()[0].event_id().to_hex(), 615 "a".repeat(64) 616 ); 617 assert_eq!( 618 request.event_targets()[1].event_id().to_hex(), 619 "f".repeat(64) 620 ); 621 assert_eq!( 622 request.address_targets()[0].coordinate().as_str(), 623 format!("30402:{}:produce", crate::test_valid_hex_64('b')) 624 ); 625 assert_eq!( 626 request.address_targets()[1].coordinate().as_str(), 627 format!("31923:{}:harvest", crate::test_valid_hex_64('e')) 628 ); 629 assert_eq!(request.kind_hints(), &[1, 30_402, 31_923]); 630 } 631 632 #[test] 633 fn authored_request_rejects_canonical_duplicate_targets() { 634 let uppercase = 635 Nip09DeletionEventTarget::parse("A".repeat(64), 1).expect("uppercase event"); 636 let lowercase = 637 Nip09DeletionEventTarget::parse("a".repeat(64), 31_922).expect("lowercase event"); 638 assert_eq!( 639 AuthoredNip09DeletionRequest::new("", vec![uppercase, lowercase], Vec::new(),) 640 .unwrap_err(), 641 Nip09DeletionError::DuplicateEventTarget { 642 event_id: "a".repeat(64) 643 } 644 ); 645 646 let uppercase_address = Nip09DeletionAddressTarget::parse(format!( 647 "030402:{}:produce", 648 crate::test_valid_hex_64('A') 649 )) 650 .expect("uppercase address"); 651 assert_eq!( 652 AuthoredNip09DeletionRequest::new( 653 "", 654 Vec::new(), 655 vec![uppercase_address, address_target(30_402, 'a', "produce"),], 656 ) 657 .unwrap_err(), 658 Nip09DeletionError::DuplicateAddressTarget { 659 coordinate: format!("30402:{}:produce", crate::test_valid_hex_64('a')) 660 } 661 ); 662 } 663 664 #[test] 665 fn authored_request_requires_target_union_and_caps_content() { 666 assert_eq!( 667 AuthoredNip09DeletionRequest::new("", Vec::new(), Vec::new()).unwrap_err(), 668 Nip09DeletionError::TargetMissing 669 ); 670 AuthoredNip09DeletionRequest::new( 671 "x".repeat(RADROOTS_NIP09_DELETION_CONTENT_MAX_BYTES), 672 vec![event_target('a', 1)], 673 Vec::new(), 674 ) 675 .expect("exact content byte limit"); 676 assert_eq!( 677 AuthoredNip09DeletionRequest::new( 678 "x".repeat(RADROOTS_NIP09_DELETION_CONTENT_MAX_BYTES + 1), 679 vec![event_target('a', 1)], 680 Vec::new(), 681 ) 682 .unwrap_err(), 683 Nip09DeletionError::ContentTooLarge { 684 max: RADROOTS_NIP09_DELETION_CONTENT_MAX_BYTES, 685 actual: RADROOTS_NIP09_DELETION_CONTENT_MAX_BYTES + 1, 686 } 687 ); 688 } 689 690 #[test] 691 fn authored_request_enforces_tag_count_budget() { 692 let exact_targets = (0..RADROOTS_NIP09_DELETION_TAG_MAX_COUNT - 1) 693 .map(|index| numeric_event_target(index, 1)) 694 .collect(); 695 AuthoredNip09DeletionRequest::new("", exact_targets, Vec::new()) 696 .expect("1023 targets plus one kind tag"); 697 698 let overflow_targets = (0..RADROOTS_NIP09_DELETION_TAG_MAX_COUNT) 699 .map(|index| numeric_event_target(index, 1)) 700 .collect(); 701 assert_eq!( 702 AuthoredNip09DeletionRequest::new("", overflow_targets, Vec::new()).unwrap_err(), 703 Nip09DeletionError::TagCountExceeded { 704 max: RADROOTS_NIP09_DELETION_TAG_MAX_COUNT, 705 actual: RADROOTS_NIP09_DELETION_TAG_MAX_COUNT + 1, 706 } 707 ); 708 709 let duplicate_overflow = vec![event_target('a', 1); RADROOTS_NIP09_DELETION_TAG_MAX_COUNT]; 710 assert_eq!( 711 AuthoredNip09DeletionRequest::new("", duplicate_overflow, Vec::new()).unwrap_err(), 712 Nip09DeletionError::TagCountExceeded { 713 max: RADROOTS_NIP09_DELETION_TAG_MAX_COUNT, 714 actual: RADROOTS_NIP09_DELETION_TAG_MAX_COUNT + 1, 715 } 716 ); 717 } 718 719 #[test] 720 fn duplicate_address_error_escapes_opaque_identifier_controls() { 721 let target = address_target(30_000, 'b', "line\nbreak"); 722 let error = AuthoredNip09DeletionRequest::new("", Vec::new(), vec![target.clone(), target]) 723 .unwrap_err(); 724 let rendered = error.to_string(); 725 assert!(rendered.contains("\\n")); 726 assert!(!rendered.contains('\n')); 727 } 728 729 #[test] 730 fn authored_request_enforces_exact_aggregate_tag_byte_budget() { 731 let mut exact_targets = (0..31) 732 .map(|index| { 733 address_target_with_total_bytes(RADROOTS_NIP01_COORDINATE_MAX_BYTES, index) 734 }) 735 .collect::<Vec<_>>(); 736 exact_targets.push(address_target_with_total_bytes(4_058, 31)); 737 let exact_tag_bytes = exact_targets 738 .iter() 739 .map(|target| 1 + target.coordinate().as_str().len()) 740 .sum::<usize>() 741 + "k".len() 742 + "30000".len(); 743 assert_eq!(exact_tag_bytes, RADROOTS_NIP09_DELETION_TAG_TOTAL_MAX_BYTES); 744 AuthoredNip09DeletionRequest::new("", Vec::new(), exact_targets) 745 .expect("exact aggregate tag byte limit"); 746 747 let mut overflow_targets = (0..31) 748 .map(|index| { 749 address_target_with_total_bytes(RADROOTS_NIP01_COORDINATE_MAX_BYTES, index) 750 }) 751 .collect::<Vec<_>>(); 752 overflow_targets.push(address_target_with_total_bytes(4_059, 31)); 753 assert_eq!( 754 AuthoredNip09DeletionRequest::new("", Vec::new(), overflow_targets).unwrap_err(), 755 Nip09DeletionError::TagBytesExceeded { 756 max: RADROOTS_NIP09_DELETION_TAG_TOTAL_MAX_BYTES, 757 actual: RADROOTS_NIP09_DELETION_TAG_TOTAL_MAX_BYTES + 1, 758 } 759 ); 760 } 761 762 #[test] 763 fn authored_request_counts_json_escaping_in_wire_budget() { 764 let mut lower = 0usize; 765 let mut upper = RADROOTS_NIP09_DELETION_CONTENT_MAX_BYTES; 766 while lower < upper { 767 let candidate = lower + (upper - lower).div_ceil(2); 768 if AuthoredNip09DeletionRequest::new( 769 "\u{0001}".repeat(candidate), 770 vec![event_target('a', 1)], 771 Vec::new(), 772 ) 773 .is_ok() 774 { 775 lower = candidate; 776 } else { 777 upper = candidate - 1; 778 } 779 } 780 AuthoredNip09DeletionRequest::new( 781 "\u{0001}".repeat(lower), 782 vec![event_target('a', 1)], 783 Vec::new(), 784 ) 785 .expect("largest escaped content fitting the wire budget"); 786 assert!(matches!( 787 AuthoredNip09DeletionRequest::new( 788 "\u{0001}".repeat(lower + 1), 789 vec![event_target('a', 1)], 790 Vec::new(), 791 ), 792 Err(Nip09DeletionError::EventWireTooLarge { max, .. }) 793 if max == RADROOTS_NIP09_DELETION_EVENT_WIRE_MAX_BYTES 794 )); 795 } 796 797 #[test] 798 fn deletion_errors_expose_stable_codes_and_messages() { 799 let errors = [ 800 Nip09DeletionError::ContentTooLarge { max: 1, actual: 2 }, 801 Nip09DeletionError::EventIdInvalid(ParseError::InvalidFormat), 802 Nip09DeletionError::TargetKindOutOfRange { max: 1, actual: 2 }, 803 Nip09DeletionError::CoordinateInvalid(Nip01CoordinateParseError::InvalidFormat), 804 Nip09DeletionError::DuplicateEventTarget { 805 event_id: "a".repeat(64), 806 }, 807 Nip09DeletionError::DuplicateAddressTarget { 808 coordinate: format!("30000:{}:", "a".repeat(64)), 809 }, 810 Nip09DeletionError::TargetMissing, 811 Nip09DeletionError::TagCountExceeded { max: 1, actual: 2 }, 812 Nip09DeletionError::TagElementTooLarge { max: 1, actual: 2 }, 813 Nip09DeletionError::TagBytesExceeded { max: 1, actual: 2 }, 814 Nip09DeletionError::EventWireTooLarge { max: 1, actual: 2 }, 815 ]; 816 let expected = [ 817 "deletion_content_too_large", 818 "deletion_event_target_invalid", 819 "deletion_event_target_invalid", 820 "deletion_address_target_invalid", 821 "deletion_event_target_duplicate", 822 "deletion_address_target_duplicate", 823 "deletion_target_missing", 824 "deletion_tag_count_exceeded", 825 "deletion_tag_element_too_large", 826 "deletion_tag_bytes_exceeded", 827 "deletion_event_wire_too_large", 828 ]; 829 for (error, expected) in errors.into_iter().zip(expected) { 830 assert_eq!(error.code(), expected); 831 assert!(!error.to_string().is_empty()); 832 } 833 } 834 }