Skip to main content

rns_core/
announce.rs

1use alloc::vec::Vec;
2use core::fmt;
3
4use rns_crypto::identity::Identity;
5use rns_crypto::sha256::Sha256;
6
7use crate::constants;
8use crate::hash;
9use crate::stamp::{stamp_valid, stamp_value, stamp_workblock, StampCost, StampValue};
10
11#[derive(Debug)]
12pub enum AnnounceError {
13    DataTooShort,
14    InvalidSignature,
15    DestinationMismatch,
16    SigningError,
17}
18
19impl fmt::Display for AnnounceError {
20    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21        match self {
22            AnnounceError::DataTooShort => write!(f, "Announce data too short"),
23            AnnounceError::InvalidSignature => write!(f, "Invalid announce signature"),
24            AnnounceError::DestinationMismatch => write!(f, "Destination hash mismatch"),
25            AnnounceError::SigningError => write!(f, "Could not sign announce"),
26        }
27    }
28}
29
30/// Parsed announce data.
31///
32/// Layout without ratchet:
33/// ```text
34/// [public_key:64][name_hash:10][random_hash:10][signature:64][app_data:*]
35/// ```
36///
37/// Layout with ratchet (context_flag == FLAG_SET):
38/// ```text
39/// [public_key:64][name_hash:10][random_hash:10][ratchet:32][signature:64][app_data:*]
40/// ```
41#[derive(Debug, Clone)]
42pub struct AnnounceData {
43    pub public_key: [u8; 64],
44    pub name_hash: [u8; 10],
45    pub random_hash: [u8; 10],
46    pub ratchet: Option<[u8; 32]>,
47    pub signature: [u8; 64],
48    pub app_data: Option<Vec<u8>>,
49}
50
51/// Result of a successfully validated announce.
52#[derive(Debug)]
53pub struct ValidatedAnnounce {
54    pub identity_hash: [u8; 16],
55    pub public_key: [u8; 64],
56    pub name_hash: [u8; 10],
57    pub random_hash: [u8; 10],
58    pub ratchet: Option<[u8; 32]>,
59    pub app_data: Option<Vec<u8>>,
60    /// Detached destination stamp parsed from the announce, if present.
61    pub stamp: Option<[u8; constants::STAMP_LENGTH]>,
62    /// Fractional value of the parsed stamp, including one below the
63    /// required cost (upstream 0cc6f5ae), or zero when no stamp is present.
64    pub stamp_value: StampValue,
65    /// Link request stamp cost announced in the extension's last two bytes,
66    /// if present.
67    pub lr_cost: Option<StampCost>,
68}
69
70/// Outcome of checking an announce's optional stamp extension.
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub enum AnnounceStampStatus {
73    /// No stamp extension was present.
74    NotStamped,
75    /// A stamp extension was present and met the required cost.
76    Valid,
77    /// A stamp extension was present but was below the required cost.
78    Invalid,
79}
80
81impl ValidatedAnnounce {
82    /// Parse and validate an announce stamp extension in-place.
83    ///
84    /// Mirrors upstream `Identity.validate_announce`: only a valid stamp is
85    /// stripped from `app_data`. A present-but-invalid stamp stays in
86    /// `app_data` and still records its value (upstream 0cc6f5ae), while
87    /// enforcement decides whether the announce is accepted.
88    pub fn apply_stamp(
89        &mut self,
90        destination_hash: &[u8; 16],
91        required_value: StampCost,
92    ) -> AnnounceStampStatus {
93        if !constants::STAMPS_ENABLED {
94            return AnnounceStampStatus::NotStamped;
95        }
96        let Some(app_data) = self.app_data.as_ref() else {
97            return AnnounceStampStatus::NotStamped;
98        };
99        if app_data.len() < constants::EXT_PAYLOAD_LENGTH {
100            return AnnounceStampStatus::NotStamped;
101        }
102
103        let ext = &app_data[app_data.len() - constants::EXT_PAYLOAD_LENGTH..];
104        if ext[0] != constants::EXT_PAYLOAD_MARKER {
105            return AnnounceStampStatus::NotStamped;
106        }
107
108        // The trailing anchor carries the whole part of the announced link
109        // request cost and the byte before it the 1/256 fraction (796ef2d4).
110        // A set anchor high bit marks a newer, longer extension format that
111        // this version must not interpret.
112        let Some(lr_cost) = crate::stamp::link_cost_from_ext_bytes(
113            ext[constants::EXT_PAYLOAD_LENGTH - 2],
114            ext[constants::EXT_PAYLOAD_LENGTH - 1],
115        ) else {
116            return AnnounceStampStatus::NotStamped;
117        };
118
119        let mut stamp = [0u8; constants::STAMP_LENGTH];
120        stamp.copy_from_slice(
121            &ext[constants::EXT_HEADER_LENGTH
122                ..constants::EXT_HEADER_LENGTH + constants::STAMP_LENGTH],
123        );
124        self.stamp = Some(stamp);
125
126        let mut material = Vec::with_capacity(16 + 64 + 10);
127        material.extend_from_slice(destination_hash);
128        material.extend_from_slice(&self.public_key);
129        material.extend_from_slice(&self.name_hash);
130        let workblock = stamp_workblock(&material, 0);
131        self.stamp_value = stamp_value(&workblock, &stamp);
132
133        if stamp_valid(&stamp, required_value, &workblock) {
134            let stripped_len = app_data.len() - constants::EXT_PAYLOAD_LENGTH;
135            self.app_data = if stripped_len == 0 {
136                None
137            } else {
138                Some(app_data[..stripped_len].to_vec())
139            };
140            self.lr_cost = Some(lr_cost);
141            AnnounceStampStatus::Valid
142        } else {
143            AnnounceStampStatus::Invalid
144        }
145    }
146}
147
148impl AnnounceData {
149    /// Pack announce fields into bytes, signing with the provided identity.
150    ///
151    /// Returns (announce_data_bytes, has_ratchet).
152    pub fn pack(
153        identity: &Identity,
154        destination_hash: &[u8; 16],
155        name_hash: &[u8; 10],
156        random_hash: &[u8; 10],
157        ratchet: Option<&[u8; 32]>,
158        app_data: Option<&[u8]>,
159    ) -> Result<(Vec<u8>, bool), AnnounceError> {
160        let public_key = identity
161            .get_public_key()
162            .ok_or(AnnounceError::SigningError)?;
163
164        // Build signed data: destination_hash + public_key + name_hash + random_hash + ratchet + app_data
165        let mut signed_data = Vec::new();
166        signed_data.extend_from_slice(destination_hash);
167        signed_data.extend_from_slice(&public_key);
168        signed_data.extend_from_slice(name_hash);
169        signed_data.extend_from_slice(random_hash);
170
171        let has_ratchet = ratchet.is_some();
172        if let Some(r) = ratchet {
173            signed_data.extend_from_slice(r);
174        }
175        if let Some(ad) = app_data {
176            signed_data.extend_from_slice(ad);
177        }
178
179        let signature = identity
180            .sign(&signed_data)
181            .map_err(|_| AnnounceError::SigningError)?;
182
183        // Build announce data: public_key + name_hash + random_hash + [ratchet] + signature + [app_data]
184        let mut announce_data = Vec::new();
185        announce_data.extend_from_slice(&public_key);
186        announce_data.extend_from_slice(name_hash);
187        announce_data.extend_from_slice(random_hash);
188        if let Some(r) = ratchet {
189            announce_data.extend_from_slice(r);
190        }
191        announce_data.extend_from_slice(&signature);
192        if let Some(ad) = app_data {
193            announce_data.extend_from_slice(ad);
194        }
195
196        Ok((announce_data, has_ratchet))
197    }
198
199    /// Parse announce bytes into structured data.
200    ///
201    /// `has_ratchet` corresponds to context_flag == FLAG_SET.
202    pub fn unpack(data: &[u8], has_ratchet: bool) -> Result<Self, AnnounceError> {
203        let keysize = constants::KEYSIZE / 8; // 64
204        let name_hash_len = constants::NAME_HASH_LENGTH / 8; // 10
205        let sig_len = constants::SIGLENGTH / 8; // 64
206        let ratchet_size = constants::RATCHETSIZE / 8; // 32
207
208        let min_len = if has_ratchet {
209            keysize + name_hash_len + 10 + ratchet_size + sig_len
210        } else {
211            keysize + name_hash_len + 10 + sig_len
212        };
213
214        if data.len() < min_len {
215            return Err(AnnounceError::DataTooShort);
216        }
217
218        let mut public_key = [0u8; 64];
219        public_key.copy_from_slice(&data[..keysize]);
220
221        let mut name_hash = [0u8; 10];
222        name_hash.copy_from_slice(&data[keysize..keysize + name_hash_len]);
223
224        let mut random_hash = [0u8; 10];
225        random_hash.copy_from_slice(&data[keysize + name_hash_len..keysize + name_hash_len + 10]);
226
227        let (ratchet, sig_start) = if has_ratchet {
228            let mut ratchet = [0u8; 32];
229            ratchet.copy_from_slice(
230                &data[keysize + name_hash_len + 10..keysize + name_hash_len + 10 + ratchet_size],
231            );
232            (Some(ratchet), keysize + name_hash_len + 10 + ratchet_size)
233        } else {
234            (None, keysize + name_hash_len + 10)
235        };
236
237        let mut signature = [0u8; 64];
238        signature.copy_from_slice(&data[sig_start..sig_start + sig_len]);
239
240        // Determine app_data
241        // From Python: app_data is present if len(data) > keysize + name_hash_len + 10 + sig_len [+ ratchetsize]
242        let app_data_start = sig_start + sig_len;
243        let app_data = if data.len() > app_data_start {
244            Some(data[app_data_start..].to_vec())
245        } else {
246            None
247        };
248
249        Ok(AnnounceData {
250            public_key,
251            name_hash,
252            random_hash,
253            ratchet,
254            signature,
255            app_data,
256        })
257    }
258
259    /// Feed each part of the signed data, in order, to `f`:
260    /// destination_hash + public_key + name_hash + random_hash + ratchet + app_data.
261    ///
262    /// Python builds signed_data with `app_data = b""` when there is no app data,
263    /// so absent optional fields contribute nothing.
264    fn for_each_signed_part(&self, destination_hash: &[u8; 16], mut f: impl FnMut(&[u8])) {
265        f(destination_hash);
266        f(&self.public_key);
267        f(&self.name_hash);
268        f(&self.random_hash);
269        if let Some(ref ratchet) = self.ratchet {
270            f(ratchet);
271        }
272        if let Some(ref ad) = self.app_data {
273            f(ad);
274        }
275    }
276
277    /// The bytes covered by the announce signature.
278    fn signed_data(&self, destination_hash: &[u8; 16]) -> Vec<u8> {
279        let mut signed_data = Vec::new();
280        self.for_each_signed_part(destination_hash, |part| signed_data.extend_from_slice(part));
281        signed_data
282    }
283
284    /// Key for the announce signature cache: SHA-256 over the signed data
285    /// followed by the signature.
286    ///
287    /// Two announces share a key only when every signed field and the
288    /// signature are identical, so a cached verification result applies to
289    /// exactly the announce it was computed for.
290    pub fn signature_cache_key(&self, destination_hash: &[u8; 16]) -> [u8; 32] {
291        let mut hasher = Sha256::new();
292        self.for_each_signed_part(destination_hash, |part| hasher.update(part));
293        hasher.update(&self.signature);
294        hasher.digest()
295    }
296
297    /// Check that `destination_hash == truncated_hash(name_hash || identity_hash)`.
298    fn check_destination_binding(
299        &self,
300        destination_hash: &[u8; 16],
301        identity_hash: &[u8; 16],
302    ) -> Result<(), AnnounceError> {
303        let mut hash_material = [0u8; 26];
304        hash_material[..10].copy_from_slice(&self.name_hash);
305        hash_material[10..].copy_from_slice(identity_hash);
306
307        let expected_hash = hash::truncated_hash(&hash_material);
308
309        if &expected_hash != destination_hash {
310            return Err(AnnounceError::DestinationMismatch);
311        }
312        Ok(())
313    }
314
315    fn to_validated(&self, identity_hash: [u8; 16]) -> ValidatedAnnounce {
316        ValidatedAnnounce {
317            identity_hash,
318            public_key: self.public_key,
319            name_hash: self.name_hash,
320            random_hash: self.random_hash,
321            ratchet: self.ratchet,
322            app_data: self.app_data.clone(),
323            stamp: None,
324            stamp_value: StampValue::ZERO,
325            lr_cost: None,
326        }
327    }
328
329    /// Validate an announce: verify signature and check destination hash.
330    ///
331    /// Follows Python Identity.validate_announce():
332    /// 1. Create Identity from public_key
333    /// 2. Build signed_data = destination_hash + public_key + name_hash + random_hash + ratchet + app_data
334    /// 3. Verify signature over signed_data
335    /// 4. Compute identity_hash = truncated_hash(public_key)
336    /// 5. expected_hash = truncated_hash(name_hash || identity_hash)
337    /// 6. Verify expected_hash == destination_hash
338    pub fn validate(
339        &self,
340        destination_hash: &[u8; 16],
341    ) -> Result<ValidatedAnnounce, AnnounceError> {
342        // Create identity from public key. Key bytes that do not decode to a
343        // valid Ed25519 point cannot carry a valid signature, so the announce
344        // is rejected the same way (Python returns False from validate_announce).
345        let announced_identity = Identity::from_public_key(&self.public_key)
346            .map_err(|_| AnnounceError::InvalidSignature)?;
347
348        // Verify signature
349        let signed_data = self.signed_data(destination_hash);
350        if !announced_identity.verify(&self.signature, &signed_data) {
351            return Err(AnnounceError::InvalidSignature);
352        }
353
354        // Verify destination hash
355        let identity_hash = *announced_identity.hash();
356        self.check_destination_binding(destination_hash, &identity_hash)?;
357
358        Ok(self.to_validated(identity_hash))
359    }
360
361    /// Build a `ValidatedAnnounce` for an announce whose signature cache key
362    /// (see [`Self::signature_cache_key`]) is already in the signature cache.
363    ///
364    /// The signature was verified for an identical announce earlier; the
365    /// structural checks are repeated here: the identity hash is recomputed
366    /// from `public_key` and the destination hash binding is checked. The
367    /// public key decoded successfully when that earlier announce was
368    /// verified, since the cache key covers the key bytes.
369    pub fn to_validated_with_cached_signature(
370        &self,
371        destination_hash: &[u8; 16],
372    ) -> Result<ValidatedAnnounce, AnnounceError> {
373        let identity_hash = hash::truncated_hash(&self.public_key);
374        self.check_destination_binding(destination_hash, &identity_hash)?;
375        Ok(self.to_validated(identity_hash))
376    }
377}
378
379#[cfg(test)]
380mod tests {
381    use super::*;
382    use crate::destination;
383
384    #[test]
385    fn test_pack_unpack_roundtrip_no_ratchet() {
386        let identity = Identity::from_private_key(&[0x42; 64]);
387        let id_hash = *identity.hash();
388
389        let nh = destination::name_hash("testapp", &["aspect"]);
390        let dh = destination::destination_hash("testapp", &["aspect"], Some(&id_hash));
391        let random = [0xAA; 10];
392
393        let (data, has_ratchet) =
394            AnnounceData::pack(&identity, &dh, &nh, &random, None, None).unwrap();
395        assert!(!has_ratchet);
396
397        let parsed = AnnounceData::unpack(&data, false).unwrap();
398        assert_eq!(parsed.public_key, identity.get_public_key().unwrap());
399        assert_eq!(parsed.name_hash, nh);
400        assert_eq!(parsed.random_hash, random);
401        assert!(parsed.ratchet.is_none());
402        assert!(parsed.app_data.is_none());
403    }
404
405    #[test]
406    fn test_pack_unpack_roundtrip_with_ratchet() {
407        let identity = Identity::from_private_key(&[0x42; 64]);
408        let id_hash = *identity.hash();
409
410        let nh = destination::name_hash("testapp", &["aspect"]);
411        let dh = destination::destination_hash("testapp", &["aspect"], Some(&id_hash));
412        let random = [0xBB; 10];
413        let ratchet = [0xCC; 32];
414
415        let (data, has_ratchet) =
416            AnnounceData::pack(&identity, &dh, &nh, &random, Some(&ratchet), None).unwrap();
417        assert!(has_ratchet);
418
419        let parsed = AnnounceData::unpack(&data, true).unwrap();
420        assert_eq!(parsed.ratchet.unwrap(), ratchet);
421    }
422
423    #[test]
424    fn test_pack_unpack_with_app_data() {
425        let identity = Identity::from_private_key(&[0x42; 64]);
426        let id_hash = *identity.hash();
427
428        let nh = destination::name_hash("testapp", &["aspect"]);
429        let dh = destination::destination_hash("testapp", &["aspect"], Some(&id_hash));
430        let random = [0xDD; 10];
431        let app_data = b"hello app data";
432
433        let (data, _) =
434            AnnounceData::pack(&identity, &dh, &nh, &random, None, Some(app_data)).unwrap();
435
436        let parsed = AnnounceData::unpack(&data, false).unwrap();
437        assert_eq!(parsed.app_data.as_deref(), Some(app_data.as_slice()));
438    }
439
440    #[test]
441    fn test_validate_valid_announce() {
442        let identity = Identity::from_private_key(&[0x42; 64]);
443        let id_hash = *identity.hash();
444
445        let nh = destination::name_hash("testapp", &["aspect"]);
446        let dh = destination::destination_hash("testapp", &["aspect"], Some(&id_hash));
447        let random = [0xEE; 10];
448
449        let (data, _) =
450            AnnounceData::pack(&identity, &dh, &nh, &random, None, Some(b"data")).unwrap();
451
452        let parsed = AnnounceData::unpack(&data, false).unwrap();
453        let validated = parsed.validate(&dh).unwrap();
454
455        assert_eq!(validated.identity_hash, id_hash);
456        assert_eq!(validated.name_hash, nh);
457    }
458
459    #[test]
460    fn test_validate_tampered_signature() {
461        let identity = Identity::from_private_key(&[0x42; 64]);
462        let id_hash = *identity.hash();
463
464        let nh = destination::name_hash("testapp", &["aspect"]);
465        let dh = destination::destination_hash("testapp", &["aspect"], Some(&id_hash));
466        let random = [0xFF; 10];
467
468        let (mut data, _) = AnnounceData::pack(&identity, &dh, &nh, &random, None, None).unwrap();
469
470        // Tamper with the signature (located at offset 84 = 64 + 10 + 10)
471        data[84] ^= 0xFF;
472
473        let parsed = AnnounceData::unpack(&data, false).unwrap();
474        assert!(parsed.validate(&dh).is_err());
475    }
476
477    #[test]
478    fn test_validate_rejects_announce_with_invalid_signing_key() {
479        let identity = Identity::from_private_key(&[0x42; 64]);
480        let id_hash = *identity.hash();
481
482        let nh = destination::name_hash("testapp", &["aspect"]);
483        let dh = destination::destination_hash("testapp", &["aspect"], Some(&id_hash));
484        let random = [0x22; 10];
485
486        let (mut data, _) = AnnounceData::pack(&identity, &dh, &nh, &random, None, None).unwrap();
487
488        // Replace the Ed25519 half of the public key (bytes 32..64) with
489        // y = 2, which is not a valid compressed point encoding.
490        data[32..64].copy_from_slice(&[0; 32]);
491        data[32] = 2;
492
493        let parsed = AnnounceData::unpack(&data, false).unwrap();
494        assert!(matches!(
495            parsed.validate(&dh),
496            Err(AnnounceError::InvalidSignature)
497        ));
498    }
499
500    #[test]
501    fn test_validate_wrong_destination_hash() {
502        let identity = Identity::from_private_key(&[0x42; 64]);
503        let id_hash = *identity.hash();
504
505        let nh = destination::name_hash("testapp", &["aspect"]);
506        let dh = destination::destination_hash("testapp", &["aspect"], Some(&id_hash));
507        let random = [0x11; 10];
508
509        let (data, _) = AnnounceData::pack(&identity, &dh, &nh, &random, None, None).unwrap();
510
511        let parsed = AnnounceData::unpack(&data, false).unwrap();
512        let wrong_hash = [0x00; 16];
513        assert!(parsed.validate(&wrong_hash).is_err());
514    }
515
516    #[test]
517    fn test_to_validated_with_cached_signature_matches_validate() {
518        let identity = Identity::from_private_key(&[0x42; 64]);
519        let id_hash = *identity.hash();
520
521        let nh = destination::name_hash("testapp", &["aspect"]);
522        let dh = destination::destination_hash("testapp", &["aspect"], Some(&id_hash));
523        let random = [0xAB; 10];
524
525        let (data, _) =
526            AnnounceData::pack(&identity, &dh, &nh, &random, None, Some(b"appdata")).unwrap();
527
528        let parsed = AnnounceData::unpack(&data, false).unwrap();
529        let validated = parsed.validate(&dh).unwrap();
530        let unchecked = parsed.to_validated_with_cached_signature(&dh).unwrap();
531
532        assert_eq!(validated.identity_hash, unchecked.identity_hash);
533        assert_eq!(validated.public_key, unchecked.public_key);
534        assert_eq!(validated.name_hash, unchecked.name_hash);
535        assert_eq!(validated.random_hash, unchecked.random_hash);
536        assert_eq!(validated.ratchet, unchecked.ratchet);
537        assert_eq!(validated.app_data, unchecked.app_data);
538    }
539    fn signed_announce(
540        app_data: Option<&[u8]>,
541        ratchet: Option<&[u8; 32]>,
542    ) -> (AnnounceData, [u8; 16]) {
543        let identity = Identity::from_private_key(&[0x42; 64]);
544        let id_hash = *identity.hash();
545        let nh = destination::name_hash("testapp", &["aspect"]);
546        let dh = destination::destination_hash("testapp", &["aspect"], Some(&id_hash));
547        let (data, has_ratchet) =
548            AnnounceData::pack(&identity, &dh, &nh, &[0x31; 10], ratchet, app_data).unwrap();
549        (AnnounceData::unpack(&data, has_ratchet).unwrap(), dh)
550    }
551
552    #[test]
553    fn test_cached_signature_path_checks_destination_binding() {
554        let (parsed, dh) = signed_announce(None, None);
555        let mut other_dh = dh;
556        other_dh[0] ^= 0x01;
557        assert!(matches!(
558            parsed.to_validated_with_cached_signature(&other_dh),
559            Err(AnnounceError::DestinationMismatch)
560        ));
561
562        let other = Identity::from_private_key(&[0x43; 64]);
563        let mut swapped = parsed.clone();
564        swapped.public_key = other.get_public_key().unwrap();
565        assert!(matches!(
566            swapped.to_validated_with_cached_signature(&dh),
567            Err(AnnounceError::DestinationMismatch)
568        ));
569    }
570
571    #[test]
572    fn test_signature_cache_key_covers_every_signed_field() {
573        let ratchet = [0x44; 32];
574        let (base, dh) = signed_announce(Some(b"app"), Some(&ratchet));
575        let key = base.signature_cache_key(&dh);
576        assert_eq!(key, base.clone().signature_cache_key(&dh));
577
578        let mut other_dh = dh;
579        other_dh[15] ^= 0x01;
580        assert_ne!(key, base.signature_cache_key(&other_dh));
581
582        let mut variants = Vec::new();
583        let mut v = base.clone();
584        v.public_key[0] ^= 0x01;
585        variants.push(v);
586        let mut v = base.clone();
587        v.name_hash[0] ^= 0x01;
588        variants.push(v);
589        let mut v = base.clone();
590        v.random_hash[0] ^= 0x01;
591        variants.push(v);
592        let mut v = base.clone();
593        v.ratchet = None;
594        variants.push(v);
595        let mut v = base.clone();
596        v.ratchet.as_mut().unwrap()[0] ^= 0x01;
597        variants.push(v);
598        let mut v = base.clone();
599        v.app_data = None;
600        variants.push(v);
601        let mut v = base.clone();
602        v.app_data = Some(b"apq".to_vec());
603        variants.push(v);
604        let mut v = base.clone();
605        v.signature[0] ^= 0x01;
606        variants.push(v);
607
608        for variant in &variants {
609            assert_ne!(key, variant.signature_cache_key(&dh));
610        }
611    }
612
613    #[test]
614    fn test_signature_cache_key_is_hash_of_signed_data_and_signature() {
615        let (parsed, dh) = signed_announce(Some(b"payload"), None);
616        let mut material = parsed.signed_data(&dh);
617        material.extend_from_slice(&parsed.signature);
618        assert_eq!(parsed.signature_cache_key(&dh), hash::full_hash(&material));
619    }
620
621    /// Link request cost carried in the test announces' extension anchor.
622    const LINK_COST: StampCost = StampCost::from_fp(11 * 256 + 64);
623
624    fn stamped_announce(
625        app_data: Option<&[u8]>,
626        cost: StampCost,
627    ) -> (
628        AnnounceData,
629        [u8; 16],
630        [u8; constants::STAMP_LENGTH],
631        StampValue,
632    ) {
633        use rns_crypto::{OsRng, Rng};
634
635        let identity = Identity::from_private_key(&[0x42; 64]);
636        let id_hash = *identity.hash();
637        let nh = destination::name_hash("testapp", &["aspect"]);
638        let dh = destination::destination_hash("testapp", &["aspect"], Some(&id_hash));
639        let public_key = identity.get_public_key().unwrap();
640
641        let mut material = Vec::new();
642        material.extend_from_slice(&dh);
643        material.extend_from_slice(&public_key);
644        material.extend_from_slice(&nh);
645        let workblock = stamp_workblock(&material, 0);
646
647        let mut rng = OsRng;
648        let mut stamp = [0u8; constants::STAMP_LENGTH];
649        loop {
650            rng.fill_bytes(&mut stamp);
651            if stamp_valid(&stamp, cost, &workblock) {
652                break;
653            }
654        }
655        let value = stamp_value(&workblock, &stamp);
656
657        let mut wire = app_data.map(|data| data.to_vec()).unwrap_or_default();
658        wire.push(constants::EXT_PAYLOAD_MARKER);
659        wire.push(0);
660        wire.extend_from_slice(&stamp);
661        wire.extend_from_slice(&crate::stamp::link_cost_ext_bytes(LINK_COST));
662
663        let (data, _) =
664            AnnounceData::pack(&identity, &dh, &nh, &[0x31; 10], None, Some(&wire)).unwrap();
665        (
666            AnnounceData::unpack(&data, false).unwrap(),
667            dh,
668            stamp,
669            value,
670        )
671    }
672
673    #[test]
674    fn test_announce_stamp_valid_is_stripped_and_valued() {
675        let (parsed, dh, stamp, value) =
676            stamped_announce(Some(b"app-data"), StampCost::from_whole(4));
677        let mut validated = parsed.validate(&dh).unwrap();
678        assert_eq!(
679            validated.apply_stamp(&dh, StampCost::from_whole(4)),
680            AnnounceStampStatus::Valid
681        );
682        assert_eq!(validated.stamp, Some(stamp));
683        assert_eq!(validated.stamp_value, value);
684        assert_eq!(validated.lr_cost, Some(LINK_COST));
685        assert_eq!(validated.app_data.as_deref(), Some(b"app-data".as_slice()));
686    }
687
688    #[test]
689    fn test_announce_stamp_valid_without_app_data_strips_to_none() {
690        let (parsed, dh, _, _) = stamped_announce(None, StampCost::from_whole(4));
691        let mut validated = parsed.validate(&dh).unwrap();
692        assert_eq!(
693            validated.apply_stamp(&dh, StampCost::from_whole(4)),
694            AnnounceStampStatus::Valid
695        );
696        assert_eq!(validated.app_data, None);
697    }
698
699    #[test]
700    fn test_announce_stamp_below_required_value_is_kept_invalid_and_valued() {
701        let (parsed, dh, stamp, value) =
702            stamped_announce(Some(b"app-data"), StampCost::from_whole(4));
703        let mut validated = parsed.validate(&dh).unwrap();
704        assert_eq!(
705            validated.apply_stamp(&dh, StampCost::from_whole(200)),
706            AnnounceStampStatus::Invalid
707        );
708        assert_eq!(validated.stamp, Some(stamp));
709        assert!(value > StampValue::ZERO);
710        assert_eq!(validated.stamp_value, value);
711        assert_eq!(validated.lr_cost, None);
712        assert_eq!(
713            validated.app_data.as_ref().map(|data| data.len()),
714            Some(b"app-data".len() + constants::EXT_PAYLOAD_LENGTH)
715        );
716    }
717
718    #[test]
719    fn test_announce_without_stamp_is_not_stamped() {
720        let (parsed, dh) = signed_announce(Some(b"app-data"), None);
721        let mut validated = parsed.validate(&dh).unwrap();
722        assert_eq!(
723            validated.apply_stamp(&dh, StampCost::from_whole(4)),
724            AnnounceStampStatus::NotStamped
725        );
726        assert_eq!(validated.stamp, None);
727        assert_eq!(validated.stamp_value, StampValue::ZERO);
728        assert_eq!(validated.lr_cost, None);
729        assert_eq!(validated.app_data.as_deref(), Some(b"app-data".as_slice()));
730    }
731
732    #[test]
733    fn test_announce_stamp_with_one_byte_link_cost_is_not_stamped() {
734        // Releases before upstream 796ef2d4 end the extension with a single
735        // cost byte. Read as the two-byte format, its 18 trailing bytes start
736        // inside the application data, so it is left there unparsed.
737        let (parsed, dh, _, _) = stamped_announce(Some(b"app-data"), StampCost::from_whole(4));
738        let mut app_data = parsed.app_data.clone().unwrap();
739        let fraction = app_data.len() - 2;
740        app_data.remove(fraction);
741        let identity = Identity::from_private_key(&[0x42; 64]);
742        let (data, _) = AnnounceData::pack(
743            &identity,
744            &dh,
745            &parsed.name_hash,
746            &parsed.random_hash,
747            None,
748            Some(&app_data),
749        )
750        .unwrap();
751        let mut validated = AnnounceData::unpack(&data, false)
752            .unwrap()
753            .validate(&dh)
754            .unwrap();
755        assert_eq!(
756            validated.apply_stamp(&dh, StampCost::from_whole(4)),
757            AnnounceStampStatus::NotStamped
758        );
759        assert_eq!(validated.lr_cost, None);
760        assert_eq!(validated.app_data, Some(app_data));
761    }
762
763    #[test]
764    fn test_announce_stamp_extended_format_is_not_interpreted() {
765        let (parsed, dh, _, _) = stamped_announce(Some(b"app-data"), StampCost::from_whole(4));
766        let mut announce = parsed.clone();
767        // Flip the anchor's high bit: this marks a newer, longer extension.
768        let app_data = announce.app_data.as_mut().unwrap();
769        let last = app_data.len() - 1;
770        app_data[last] |= 0x80;
771
772        // Re-sign because the extension is covered by the announce signature.
773        let identity = Identity::from_private_key(&[0x42; 64]);
774        let (data, _) = AnnounceData::pack(
775            &identity,
776            &dh,
777            &announce.name_hash,
778            &announce.random_hash,
779            None,
780            Some(app_data),
781        )
782        .unwrap();
783        let reparsed = AnnounceData::unpack(&data, false).unwrap();
784        let mut validated = reparsed.validate(&dh).unwrap();
785        assert_eq!(
786            validated.apply_stamp(&dh, StampCost::from_whole(4)),
787            AnnounceStampStatus::NotStamped
788        );
789        assert_eq!(validated.stamp, None);
790        assert_eq!(validated.lr_cost, None);
791    }
792}