1use alloc::string::{String, ToString};
28use alloc::vec::Vec;
29use core::fmt;
30use core::str::FromStr;
31
32use bech32::primitives::decode::CheckedHrpstring;
33use bech32::{Bech32m, Hrp};
34use serde::{Deserialize, Serialize};
35
36#[non_exhaustive]
40#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
41pub enum AddressError {
42 #[error("address is not lowercase (the canonical form is lowercase-only)")]
47 NotLowercase,
48 #[error("address is not valid bech32m: {reason}")]
52 InvalidBech32m {
53 reason: String,
55 },
56 #[error("address has human-readable part '{found}', expected '{expected}'")]
58 HrpMismatch {
59 found: String,
61 expected: &'static str,
63 },
64 #[error("address payload is {len} bytes, expected {expected}")]
66 PayloadLength {
67 len: usize,
69 expected: usize,
71 },
72 #[error("address version byte is {found:#04x}, expected {expected:#04x}")]
74 VersionMismatch {
75 found: u8,
77 expected: u8,
79 },
80 #[error("address is a non-canonical encoding of its payload")]
87 NonCanonical,
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
111pub struct Address {
112 hash: [u8; Address::HASH_LEN],
113}
114
115impl Address {
116 pub const HRP_STR: &'static str = "off";
118
119 pub const VERSION: u8 = 0x01;
121
122 pub const HASH_LEN: usize = 20;
148
149 pub const PAYLOAD_LEN: usize = 1 + Self::HASH_LEN;
151
152 pub const ENCODED_LEN: usize = 44;
158
159 const HRP: Hrp = Hrp::parse_unchecked(Self::HRP_STR);
165
166 pub fn from_hash_bytes(hash: [u8; Self::HASH_LEN]) -> Self {
170 Self { hash }
171 }
172
173 pub fn from_payload(payload: &[u8]) -> Result<Self, AddressError> {
175 if payload.len() != Self::PAYLOAD_LEN {
176 return Err(AddressError::PayloadLength {
177 len: payload.len(),
178 expected: Self::PAYLOAD_LEN,
179 });
180 }
181 if payload[0] != Self::VERSION {
182 return Err(AddressError::VersionMismatch {
183 found: payload[0],
184 expected: Self::VERSION,
185 });
186 }
187 let mut hash = [0u8; Self::HASH_LEN];
188 hash.copy_from_slice(&payload[1..]);
189 Ok(Self { hash })
190 }
191
192 pub fn hash_bytes(&self) -> &[u8; Self::HASH_LEN] {
194 &self.hash
195 }
196
197 pub fn payload(&self) -> [u8; Self::PAYLOAD_LEN] {
199 let mut payload = [0u8; Self::PAYLOAD_LEN];
200 payload[0] = Self::VERSION;
201 payload[1..].copy_from_slice(&self.hash);
202 payload
203 }
204
205 pub fn version(&self) -> u8 {
207 Self::VERSION
208 }
209}
210
211impl fmt::Display for Address {
212 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213 bech32::encode_lower_to_fmt::<Bech32m, _>(f, Self::HRP, &self.payload())
217 .map_err(|_| fmt::Error)
218 }
219}
220
221impl FromStr for Address {
222 type Err = AddressError;
223
224 fn from_str(s: &str) -> Result<Self, Self::Err> {
225 if s.bytes().any(|b| b.is_ascii_uppercase()) {
228 return Err(AddressError::NotLowercase);
229 }
230
231 let checked =
235 CheckedHrpstring::new::<Bech32m>(s).map_err(|e| AddressError::InvalidBech32m {
236 reason: e.to_string(),
237 })?;
238
239 if checked.hrp() != Self::HRP {
240 return Err(AddressError::HrpMismatch {
241 found: checked.hrp().to_string(),
242 expected: Self::HRP_STR,
243 });
244 }
245
246 let payload: Vec<u8> = checked.byte_iter().collect();
247 let address = Self::from_payload(&payload)?;
248
249 if address.to_string() != s {
255 return Err(AddressError::NonCanonical);
256 }
257
258 Ok(address)
259 }
260}
261
262impl Serialize for Address {
263 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
264 serializer.collect_str(self)
265 }
266}
267
268impl<'de> Deserialize<'de> for Address {
269 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
270 let s = String::deserialize(deserializer)?;
271 s.parse().map_err(serde::de::Error::custom)
272 }
273}
274
275#[cfg(all(test, feature = "std"))]
276mod tests {
277 use super::*;
278
279 const RFC8032_TV1_HASH: [u8; 20] = [
290 0x21, 0xfe, 0x31, 0xdf, 0xa1, 0x54, 0xa2, 0x61, 0x62, 0x6b, 0xf8, 0x54, 0x04, 0x6f, 0xd2,
291 0x27, 0x1b, 0x7b, 0xed, 0x4b,
292 ];
293 const RFC8032_TV1_ADDR: &str = "off1qysluvwl5922yctzd0u9gpr06gn3k7ldfvgtwgvn";
294
295 const ZEROS_HASH: [u8; 20] = [0u8; 20];
296 const ZEROS_ADDR: &str = "off1qyqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqn8antf";
297
298 const FF_HASH: [u8; 20] = [0xffu8; 20];
299 const FF_ADDR: &str = "off1q8llllllllllllllllllllllllllllllluvweh4p";
300
301 const RAMP_HASH: [u8; 20] = [
302 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
303 ];
304 const RAMP_ADDR: &str = "off1qyqqzqsrqszsvpcgpy9qkrqdpc83qygjzvpgkpr5";
305
306 fn vectors() -> [([u8; 20], &'static str); 4] {
307 [
308 (RFC8032_TV1_HASH, RFC8032_TV1_ADDR),
309 (ZEROS_HASH, ZEROS_ADDR),
310 (FF_HASH, FF_ADDR),
311 (RAMP_HASH, RAMP_ADDR),
312 ]
313 }
314
315 #[test]
316 fn encodes_reference_vectors() {
317 for (hash, expected) in vectors() {
318 let rendered = Address::from_hash_bytes(hash).to_string();
319 assert_eq!(rendered, expected);
320 assert_eq!(rendered.len(), Address::ENCODED_LEN);
321 }
322 }
323
324 #[test]
325 fn decodes_reference_vectors() {
326 for (hash, encoded) in vectors() {
327 let address: Address = encoded.parse().expect("reference vector must parse");
328 assert_eq!(address.hash_bytes(), &hash);
329 assert_eq!(address.version(), Address::VERSION);
330 }
331 }
332
333 #[test]
334 fn reencoding_a_parsed_vector_is_byte_identical() {
335 for (_, encoded) in vectors() {
336 let address: Address = encoded.parse().expect("reference vector must parse");
337 assert_eq!(address.to_string(), encoded);
338 }
339 }
340
341 #[test]
342 fn roundtrips_arbitrary_hashes() {
343 let mut state: u64 = 0x2545_F491_4F6C_DD1D;
345 for _ in 0..256 {
346 let mut hash = [0u8; Address::HASH_LEN];
347 for byte in hash.iter_mut() {
348 state = state
349 .wrapping_mul(6364136223846793005)
350 .wrapping_add(1442695040888963407);
351 *byte = (state >> 33) as u8;
352 }
353 let address = Address::from_hash_bytes(hash);
354 let encoded = address.to_string();
355 assert_eq!(encoded.len(), Address::ENCODED_LEN);
356 assert_eq!(encoded.parse::<Address>().expect("roundtrip"), address);
357 }
358 }
359
360 #[test]
361 fn payload_roundtrips() {
362 let address = Address::from_hash_bytes(RAMP_HASH);
363 let payload = address.payload();
364 assert_eq!(payload[0], Address::VERSION);
365 assert_eq!(&payload[1..], &RAMP_HASH);
366 assert_eq!(Address::from_payload(&payload), Ok(address));
367 }
368
369 #[test]
370 fn hrp_const_matches_parsed() {
371 let parsed = Hrp::parse(Address::HRP_STR).expect("hrp must be valid");
374 assert_eq!(parsed, Address::HRP);
375 }
376
377 #[test]
378 fn serde_uses_the_canonical_string() {
379 let address = Address::from_hash_bytes(RFC8032_TV1_HASH);
380 let json = serde_json::to_string(&address).expect("serialize");
381 assert_eq!(json, format!("\"{RFC8032_TV1_ADDR}\""));
382 let back: Address = serde_json::from_str(&json).expect("deserialize");
383 assert_eq!(back, address);
384 }
385
386 #[test]
387 fn serde_rejects_non_canonical_forms() {
388 for bad in [
389 ZEROS_ADDR.to_uppercase(),
390 "off1qyqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqra5g99k".to_string(),
392 ] {
393 let json = format!("\"{bad}\"");
394 assert!(serde_json::from_str::<Address>(&json).is_err(), "{bad}");
395 }
396 }
397
398 #[test]
399 fn ord_follows_hash_bytes_not_the_rendered_string() {
400 let low = Address::from_hash_bytes(ZEROS_HASH);
401 let high = Address::from_hash_bytes(FF_HASH);
402 assert!(low < high);
403 assert_eq!(low.hash_bytes() < high.hash_bytes(), low < high);
404
405 let mut a = ZEROS_HASH;
409 a[0] = 0x20; let mut b = ZEROS_HASH;
411 b[0] = 0x28; let (a, b) = (Address::from_hash_bytes(a), Address::from_hash_bytes(b));
413 assert!(a < b, "byte order");
414 assert!(a.to_string() > b.to_string(), "string order inverts");
415 }
416
417 #[test]
418 fn rejects_uppercase_and_mixed_case() {
419 assert_eq!(
422 ZEROS_ADDR.to_uppercase().parse::<Address>(),
423 Err(AddressError::NotLowercase)
424 );
425 let mixed = format!("{}N8antf", &ZEROS_ADDR[..ZEROS_ADDR.len() - 6]);
426 assert_eq!(mixed.parse::<Address>(), Err(AddressError::NotLowercase));
427 }
428
429 #[test]
430 fn rejects_wrong_hrp() {
431 let other = "ofx1qyqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq2e32t6";
433 assert_eq!(
434 other.parse::<Address>(),
435 Err(AddressError::HrpMismatch {
436 found: "ofx".to_string(),
437 expected: Address::HRP_STR,
438 })
439 );
440 }
441
442 #[test]
443 fn rejects_the_original_bech32_checksum() {
444 let bech32_variant = "off1qyqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqxmdlwt";
448 assert!(matches!(
449 bech32_variant.parse::<Address>(),
450 Err(AddressError::InvalidBech32m { .. })
451 ));
452 }
453
454 #[test]
455 fn rejects_corrupted_characters() {
456 let mut corrupted = ZEROS_ADDR.to_string();
457 corrupted.pop();
458 corrupted.push('p'); assert!(matches!(
460 corrupted.parse::<Address>(),
461 Err(AddressError::InvalidBech32m { .. })
462 ));
463
464 for bad_char in ['b', 'i', 'o'] {
466 let mut off_charset = ZEROS_ADDR.to_string();
467 off_charset.pop();
468 off_charset.push(bad_char);
469 assert!(
470 matches!(
471 off_charset.parse::<Address>(),
472 Err(AddressError::InvalidBech32m { .. })
473 ),
474 "{bad_char}"
475 );
476 }
477 }
478
479 #[test]
480 fn rejects_unsupported_version_bytes() {
481 for (encoded, expected_version) in [
482 ("off1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqkynkkv", 0x00u8),
483 ("off1qgqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqquz0u9x", 0x02),
484 ("off1luqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqn347dq", 0xff),
485 ] {
486 assert_eq!(
487 encoded.parse::<Address>(),
488 Err(AddressError::VersionMismatch {
489 found: expected_version,
490 expected: Address::VERSION,
491 })
492 );
493 }
494 }
495
496 #[test]
497 fn rejects_wrong_payload_lengths() {
498 for (encoded, len) in [
501 ("off1qyqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqxfx3pw", 20usize),
502 ("off1qyqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqkev2nx", 22),
503 ] {
504 assert_eq!(
505 encoded.parse::<Address>(),
506 Err(AddressError::PayloadLength {
507 len,
508 expected: Address::PAYLOAD_LEN,
509 })
510 );
511 }
512 }
513
514 #[test]
515 fn rejects_non_zero_padding_bits() {
516 for mutant in [
523 "off1qyqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpw3fxkm",
524 "off1qyqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzqzuscy",
525 "off1qyqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqra5g99k",
526 ] {
527 assert_eq!(
528 mutant.parse::<Address>(),
529 Err(AddressError::NonCanonical),
530 "{mutant}"
531 );
532 assert!(ZEROS_ADDR.parse::<Address>().is_ok());
535 }
536 }
537
538 #[test]
539 fn rejects_over_long_data_parts() {
540 for mutant in [
544 "off1qyqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqj7v8n5",
545 "off1qyqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqp0gcjwx",
546 "off1qyqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqlcjhrel",
547 ] {
548 assert_eq!(
549 mutant.parse::<Address>(),
550 Err(AddressError::NonCanonical),
551 "{mutant}"
552 );
553 }
554 }
555
556 #[test]
557 fn rejects_structural_garbage() {
558 for bad in [
559 "",
560 "off",
561 "off1",
562 "off1qqq",
563 "1qyqqqq",
564 ZEROS_ADDR.trim_end_matches('f'),
565 ] {
566 assert!(
567 matches!(
568 bad.parse::<Address>(),
569 Err(AddressError::InvalidBech32m { .. })
570 ),
571 "{bad:?}"
572 );
573 }
574 }
575
576 #[test]
577 fn from_payload_rejects_bad_input() {
578 assert_eq!(
579 Address::from_payload(&[]),
580 Err(AddressError::PayloadLength {
581 len: 0,
582 expected: Address::PAYLOAD_LEN,
583 })
584 );
585 let mut payload = [0u8; Address::PAYLOAD_LEN];
586 payload[0] = 0x02;
587 assert_eq!(
588 Address::from_payload(&payload),
589 Err(AddressError::VersionMismatch {
590 found: 0x02,
591 expected: Address::VERSION,
592 })
593 );
594 }
595}
596
597#[cfg(all(test, feature = "std"))]
609mod spec_vectors {
610 use super::*;
611
612 const VECTORS: &str = include_str!("../tests/data/address-v1.vectors.json");
613
614 fn vectors() -> serde_json::Value {
615 serde_json::from_str(VECTORS).expect("the vector file is JSON")
616 }
617
618 fn hash_from(hex: &str) -> [u8; Address::HASH_LEN] {
619 let bytes: Vec<u8> = (0..hex.len())
620 .step_by(2)
621 .map(|i| u8::from_str_radix(&hex[i..i + 2], 16).expect("hex digit"))
622 .collect();
623 bytes.try_into().expect("20 bytes")
624 }
625
626 #[test]
629 fn the_vector_file_is_the_size_it_was() {
630 let v = vectors();
631 assert_eq!(v["hrp"], "off");
632 assert_eq!(v["version_byte"], 1);
633 assert_eq!(v["hash_len"], Address::HASH_LEN);
634 assert_eq!(v["encoded_len"], Address::ENCODED_LEN);
635 assert_eq!(v["encode"].as_array().expect("encode").len(), 4);
636 assert_eq!(v["reject"].as_array().expect("reject").len(), 5);
637 assert!(
638 !v["ordering"].as_array().expect("ordering").is_empty(),
639 "the ordering cases are the reason this file exists"
640 );
641 }
642
643 #[test]
644 fn every_hash_renders_to_its_vector() {
645 for case in vectors()["encode"].as_array().expect("encode") {
646 let name = case["name"].as_str().expect("a name");
647 let hash = hash_from(case["hash_hex"].as_str().expect("a hash"));
648 assert_eq!(
649 Address::from_hash_bytes(hash).to_string(),
650 case["address"].as_str().expect("an address"),
651 "[{name}] rendered differently than the chapter specifies"
652 );
653 }
654 }
655
656 #[test]
657 fn every_vector_parses_back_into_its_hash() {
658 for case in vectors()["encode"].as_array().expect("encode") {
659 let name = case["name"].as_str().expect("a name");
660 let text = case["address"].as_str().expect("an address");
661 let parsed: Address = text.parse().unwrap_or_else(|e| panic!("[{name}]: {e:?}"));
662 assert_eq!(
663 parsed.hash_bytes(),
664 &hash_from(case["hash_hex"].as_str().expect("a hash")),
665 "[{name}] parsed to a different hash"
666 );
667 assert_eq!(
668 parsed.to_string(),
669 text,
670 "[{name}] did not re-encode to the string it came from"
671 );
672 }
673 }
674
675 #[test]
676 fn reject_vectors_are_refused() {
677 for case in vectors()["reject"].as_array().expect("reject") {
678 let name = case["name"].as_str().expect("a name");
679 let reason = case["reason"].as_str().expect("a reason");
680 let input = case["input"].as_str().expect("an input");
681 assert!(
682 input.parse::<Address>().is_err(),
683 "[{name}] was accepted, but the chapter refuses it: {reason}"
684 );
685 }
686 }
687
688 #[test]
696 fn the_two_orderings_disagree_where_the_vectors_say_they_do() {
697 for case in vectors()["ordering"].as_array().expect("ordering") {
698 let a: Address = case["a"].as_str().expect("a").parse().expect("a parses");
699 let b: Address = case["b"].as_str().expect("b").parse().expect("b parses");
700
701 let by_hash = if a <= b { a } else { b };
702 assert_eq!(
703 by_hash.to_string(),
704 case["first_by_hash_bytes"].as_str().expect("hash order"),
705 "the derived Ord no longer compares hash bytes"
706 );
707
708 let by_string = core::cmp::min(a.to_string(), b.to_string());
709 assert_eq!(
710 by_string,
711 case["first_by_rendered_string"]
712 .as_str()
713 .expect("string order"),
714 "string order moved"
715 );
716
717 assert_ne!(
718 by_hash.to_string(),
719 by_string,
720 "this pair no longer disagrees, so it can no longer catch a \
721 tiebreaker that swapped orders"
722 );
723 }
724 }
725
726 fn chapter() -> Option<String> {
729 let path =
730 std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../docs/spec/identity.md");
731 std::fs::read_to_string(&path).ok().or_else(|| {
732 eprintln!("spec tree not present, skipping the identity chapter drift checks");
733 None
734 })
735 }
736
737 #[test]
738 fn the_chapter_states_the_constants_the_code_uses() {
739 let Some(text) = chapter() else { return };
740
741 assert!(
742 text.contains(&format!("truncated to {} bytes", Address::HASH_LEN)),
743 "the chapter does not state the {}-byte truncation",
744 Address::HASH_LEN
745 );
746 assert!(
747 text.contains(&format!("exactly {} characters", Address::ENCODED_LEN)),
748 "the chapter does not state the {}-character canonical rendering",
749 Address::ENCODED_LEN
750 );
751 assert!(
752 text.contains("bech32m"),
753 "the chapter no longer names bech32m, and bech32 is a different \
754 checksum constant that this parser refuses"
755 );
756 }
757}