freenet_stdlib/delegate_manifest.rs
1//! The delegate manifest: what a delegate asks the node for, declared inside
2//! its own WASM.
3//!
4//! A delegate that wants more than the request/response model it has always
5//! had (today: lifecycle events; later: wake-ups, notifications) says so in a
6//! manifest. The `#[delegate(manifest(...))]` attribute writes it into a WASM
7//! custom section named [`MANIFEST_SECTION_NAME`]. The node reads that section
8//! when the delegate is registered, without running any delegate code.
9//!
10//! # Why a manifest at all
11//!
12//! Two jobs, both of which need the node to know what a delegate wants
13//! *before* it runs:
14//!
15//! 1. **Backward compatibility of new inbound messages.** An
16//! [`InboundDelegateMsg`](crate::prelude::InboundDelegateMsg) variant added
17//! in a later stdlib is a hard decode error in a delegate built against an
18//! earlier one (see `WIRE-FORMAT.md`). So the node sends a new kind of
19//! inbound message — [`LifecycleEvent`] today — **only** to a delegate
20//! whose manifest lists it. A delegate without a manifest never receives
21//! one, and behaves exactly as it did before manifests existed.
22//! 2. **Consent.** Some capabilities are gated by the node on the user's
23//! permission ("run in the background"). The node reads the capabilities
24//! from the manifest and asks the user once, at registration time, for the
25//! ones not yet granted.
26//!
27//! # Why it cannot be swapped
28//!
29//! The section is part of the WASM module, and a delegate's key is derived
30//! from the hash of that module. Changing the manifest changes the delegate
31//! key, exactly as changing any line of code does.
32//!
33//! # Encoding: JSON, deliberately
34//!
35//! The payload is UTF-8 JSON, not bincode. The manifest is the one piece of
36//! delegate metadata the node must read from delegates built against *any*
37//! stdlib, including ones newer than the node. bincode is positional and
38//! cannot skip what it does not know (`WIRE-FORMAT.md`), so a newer stdlib
39//! adding a field or a capability would make older nodes reject the whole
40//! manifest. JSON names its fields, so an older reader ignores fields it does
41//! not know, and unknown capability or lifecycle names decode as
42//! [`Capability::Unknown`] / [`LifecycleKind::Unknown`] and are ignored rather
43//! than rejecting the manifest. (`#[serde(other)]` is safe here because JSON
44//! is self-describing; `WIRE-FORMAT.md`'s warning against it is about
45//! bincode.)
46//!
47//! # Emitting a manifest adds a section, and so changes the delegate key
48//!
49//! Only delegates that write `manifest(...)` get the section. Upgrading stdlib
50//! does not add one to delegates that do not ask for it.
51
52use serde::{Deserialize, Serialize};
53
54/// Name of the WASM custom section holding the manifest.
55pub const MANIFEST_SECTION_NAME: &str = "freenet-manifest";
56
57/// The manifest format version this stdlib writes.
58///
59/// Informational only. Readers accept any version `>= 1` and never gate on it,
60/// because a reader that refused newer versions would drop every capability it
61/// does understand the moment one it does not is added. That works only under
62/// two rules, which are permanent:
63///
64/// - the meaning of an existing field or name never changes; a changed meaning
65/// gets a new field or a new name;
66/// - `lifecycle` and `capabilities` entries are what a reader looks up by
67/// name. A later format that needs parameters for a capability adds a new
68/// top-level field for them. (A reader still tolerates a non-string entry:
69/// it decodes as `Unknown`, see [`DelegateManifest::from_bytes`].)
70pub const MANIFEST_VERSION: u16 = 1;
71
72/// Largest manifest payload a reader accepts, in bytes. A manifest is a
73/// handful of short names; anything bigger is not a manifest.
74pub const MAX_MANIFEST_BYTES: usize = 4096;
75
76/// What a delegate declares it wants from the node.
77///
78/// `#[non_exhaustive]` so fields can be added without a source break; build one
79/// with [`DelegateManifest::new`].
80#[non_exhaustive]
81#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
82pub struct DelegateManifest {
83 /// Format version; see [`MANIFEST_VERSION`].
84 pub manifest_version: u16,
85 /// Lifecycle events the delegate wants delivered. The node never sends a
86 /// [`LifecycleEvent`] of a kind that is not listed here.
87 #[serde(default, deserialize_with = "lenient_list")]
88 pub lifecycle: Vec<LifecycleKind>,
89 /// Node-enforced capabilities the delegate asks the user for.
90 #[serde(default, deserialize_with = "lenient_list")]
91 pub capabilities: Vec<Capability>,
92}
93
94/// A kind of [`LifecycleEvent`] a delegate can ask to receive.
95///
96/// Adding a kind later means adding a variant here **and** a matching
97/// [`LifecycleEvent`] variant. A delegate built before the addition cannot
98/// list the new kind, so it never receives the new event — which is what makes
99/// appending a `LifecycleEvent` variant safe for already-deployed delegates.
100#[non_exhaustive]
101#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
102#[serde(rename_all = "snake_case")]
103pub enum LifecycleKind {
104 /// [`LifecycleEvent::Installed`].
105 Installed,
106 /// [`LifecycleEvent::NodeStarted`].
107 NodeStarted,
108 /// A name this stdlib does not know, written by a newer one. Readers
109 /// ignore it. The macro never writes it; re-serializing a manifest read
110 /// from a newer stdlib does (as `"unknown"`).
111 #[serde(other)]
112 Unknown,
113}
114
115/// A node-enforced capability, granted by the user once per app.
116///
117/// A node that implements capabilities refuses one until the user has granted
118/// it, and remembers the answer per app, so the user is asked once. How a node
119/// identifies an app is the node's business, not part of this format.
120#[non_exhaustive]
121#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
122#[serde(rename_all = "snake_case")]
123pub enum Capability {
124 /// Run without an open app: receive lifecycle events. Required by any
125 /// manifest that lists a lifecycle kind (the macro enforces it).
126 Background,
127 /// A name this stdlib does not know, written by a newer one. Readers
128 /// ignore it. The macro never writes it; re-serializing a manifest read
129 /// from a newer stdlib does (as `"unknown"`).
130 #[serde(other)]
131 Unknown,
132}
133
134/// A lifecycle event, delivered as
135/// [`InboundDelegateMsg::Lifecycle`](crate::prelude::InboundDelegateMsg::Lifecycle).
136///
137/// Only sent to a delegate whose manifest lists the matching
138/// [`LifecycleKind`]. A node that implements delivery also requires the user's
139/// [`Capability::Background`] grant for the delegate's app. The run gets the
140/// delegate's registered parameters and no origin.
141///
142/// # Wire format
143///
144/// bincode, nested inside `InboundDelegateMsg`. Variants are appended, never
145/// inserted or reordered (pinned by `lifecycle_event_tags_are_pinned`). A
146/// variant's fields are frozen once released: `WIRE-FORMAT.md` rule 1 forbids
147/// appending a field to a struct already on the wire, so new information
148/// arrives as a new variant.
149#[non_exhaustive]
150#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
151pub enum LifecycleEvent {
152 /// This delegate was installed on this node: its first registration here,
153 /// or the first time its app was granted [`Capability::Background`] after
154 /// that. Delivered at most once per delegate key per node.
155 ///
156 /// A delegate typically uses it to subscribe to the contracts it watches
157 /// and to do any one-time setup.
158 Installed,
159 /// The node started. Delivered once per node start, after the node has
160 /// finished whatever restore work it does at start-up.
161 ///
162 /// Contract notifications that arrived while the node was down were not
163 /// delivered and are never replayed, so a delegate should re-read any
164 /// contract it depends on rather than assume it saw every change.
165 NodeStarted {
166 /// When the node was last known to be running (milliseconds since the
167 /// Unix epoch), if it knows. Everything between this and now was
168 /// missed. `None` means the node does not know, not that nothing was
169 /// missed.
170 down_since_ms: Option<u64>,
171 },
172}
173
174impl LifecycleEvent {
175 /// The manifest kind that must be listed for this event to be delivered.
176 pub fn kind(&self) -> LifecycleKind {
177 match self {
178 LifecycleEvent::Installed => LifecycleKind::Installed,
179 LifecycleEvent::NodeStarted { .. } => LifecycleKind::NodeStarted,
180 }
181 }
182}
183
184/// Why a manifest could not be read.
185#[derive(Debug, thiserror::Error, PartialEq, Eq)]
186#[non_exhaustive]
187pub enum ManifestError {
188 #[error("not a WASM module (bad magic or version)")]
189 NotWasm,
190 #[error("truncated or malformed WASM section structure")]
191 Malformed,
192 #[error("more than one `{MANIFEST_SECTION_NAME}` custom section")]
193 Duplicate,
194 #[error("manifest is {0} bytes, over the {MAX_MANIFEST_BYTES}-byte limit")]
195 TooLarge(usize),
196 #[error("manifest is not valid JSON for this schema: {0}")]
197 Decode(String),
198 #[error("manifest_version 0 is not a valid version")]
199 BadVersion,
200}
201
202impl DelegateManifest {
203 /// A manifest at the current [`MANIFEST_VERSION`].
204 pub fn new(lifecycle: Vec<LifecycleKind>, capabilities: Vec<Capability>) -> Self {
205 Self {
206 manifest_version: MANIFEST_VERSION,
207 lifecycle,
208 capabilities,
209 }
210 }
211
212 /// Whether the manifest asks for lifecycle events of this kind.
213 pub fn wants_lifecycle(&self, kind: LifecycleKind) -> bool {
214 kind != LifecycleKind::Unknown && self.lifecycle.contains(&kind)
215 }
216
217 /// Whether the manifest asks for this capability.
218 pub fn wants_capability(&self, cap: Capability) -> bool {
219 cap != Capability::Unknown && self.capabilities.contains(&cap)
220 }
221
222 /// Known capabilities this manifest asks for, deduplicated, in declaration
223 /// order. Unknown names are dropped.
224 pub fn known_capabilities(&self) -> Vec<Capability> {
225 let mut out = Vec::new();
226 for c in &self.capabilities {
227 if *c != Capability::Unknown && !out.contains(c) {
228 out.push(*c);
229 }
230 }
231 out
232 }
233
234 /// Serialize to the section payload.
235 pub fn to_bytes(&self) -> Vec<u8> {
236 serde_json::to_vec(self).expect("a manifest always serializes")
237 }
238
239 /// Parse a section payload.
240 ///
241 /// Tolerant of manifests written by newer stdlibs: unknown fields are
242 /// ignored, a list field that is not an array (`null`, or any other shape)
243 /// reads as empty, and a list entry that is not a
244 /// name this reader knows (an unknown name, or a non-string value) reads as
245 /// `Unknown` rather than failing the manifest, so the known entries next
246 /// to it still count.
247 pub fn from_bytes(bytes: &[u8]) -> Result<Self, ManifestError> {
248 if bytes.len() > MAX_MANIFEST_BYTES {
249 return Err(ManifestError::TooLarge(bytes.len()));
250 }
251 let m: DelegateManifest =
252 serde_json::from_slice(bytes).map_err(|e| ManifestError::Decode(e.to_string()))?;
253 if m.manifest_version == 0 {
254 return Err(ManifestError::BadVersion);
255 }
256 Ok(m)
257 }
258
259 /// Read the manifest from a raw WASM module (no version prefix).
260 ///
261 /// `Ok(None)` means the module has no manifest section: a delegate that
262 /// asked for nothing, which is every delegate built before manifests
263 /// existed. Only the section headers are walked; nothing is executed or
264 /// validated beyond what locating the section needs.
265 pub fn from_wasm(module: &[u8]) -> Result<Option<Self>, ManifestError> {
266 let mut found: Option<&[u8]> = None;
267 for section in custom_sections(module)? {
268 let (name, payload) = section?;
269 if name == MANIFEST_SECTION_NAME.as_bytes() {
270 if found.is_some() {
271 return Err(ManifestError::Duplicate);
272 }
273 found = Some(payload);
274 }
275 }
276 found.map(Self::from_bytes).transpose()
277 }
278}
279
280/// Decode a list whose entries are enum names, mapping any entry this reader
281/// cannot decode to the enum's `#[serde(other)]` variant.
282fn lenient_list<'de, D, T>(d: D) -> Result<Vec<T>, D::Error>
283where
284 D: serde::Deserializer<'de>,
285 T: serde::de::DeserializeOwned + Unknownable,
286{
287 // Anything but an array (`null`, or a shape a later format might use)
288 // reads as an empty list: asking for nothing is the safe direction.
289 let serde_json::Value::Array(raw) = serde_json::Value::deserialize(d)? else {
290 return Ok(Vec::new());
291 };
292 Ok(raw
293 .into_iter()
294 .map(|v| serde_json::from_value(v).unwrap_or_else(|_| T::unknown()))
295 .collect())
296}
297
298trait Unknownable {
299 fn unknown() -> Self;
300}
301impl Unknownable for LifecycleKind {
302 fn unknown() -> Self {
303 LifecycleKind::Unknown
304 }
305}
306impl Unknownable for Capability {
307 fn unknown() -> Self {
308 Capability::Unknown
309 }
310}
311
312/// Used by `#[delegate(manifest(...))]` to check, at compile time, that the
313/// section name and version it writes are the ones this stdlib reads. Not
314/// part of the public API.
315#[doc(hidden)]
316pub const fn __manifest_macro_agrees(section: &str, version: u16) -> bool {
317 let (a, b) = (section.as_bytes(), MANIFEST_SECTION_NAME.as_bytes());
318 if a.len() != b.len() || version != MANIFEST_VERSION {
319 return false;
320 }
321 let mut i = 0;
322 while i < a.len() {
323 if a[i] != b[i] {
324 return false;
325 }
326 i += 1;
327 }
328 true
329}
330
331/// A custom section's `(name, payload)`.
332type CustomSection<'a> = (&'a [u8], &'a [u8]);
333
334/// Iterate over a WASM module's custom sections.
335fn custom_sections(
336 module: &[u8],
337) -> Result<impl Iterator<Item = Result<CustomSection<'_>, ManifestError>>, ManifestError> {
338 const HEADER: [u8; 8] = [0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00];
339 if module.len() < HEADER.len() || module[..HEADER.len()] != HEADER {
340 return Err(ManifestError::NotWasm);
341 }
342 let mut pos = HEADER.len();
343 let mut failed = false;
344 Ok(std::iter::from_fn(move || loop {
345 if failed || pos >= module.len() {
346 return None;
347 }
348 let parsed = (|| {
349 let id = module[pos];
350 let mut p = pos + 1;
351 let size = read_leb_u32(module, &mut p)? as usize;
352 let end = p.checked_add(size).ok_or(ManifestError::Malformed)?;
353 if end > module.len() {
354 return Err(ManifestError::Malformed);
355 }
356 let custom = if id == 0 {
357 let name_len = read_leb_u32(module, &mut p)? as usize;
358 let name_end = p.checked_add(name_len).ok_or(ManifestError::Malformed)?;
359 if name_end > end {
360 return Err(ManifestError::Malformed);
361 }
362 Some((&module[p..name_end], &module[name_end..end]))
363 } else {
364 None
365 };
366 Ok((end, custom))
367 })();
368 match parsed {
369 Ok((end, custom)) => {
370 pos = end;
371 if let Some(c) = custom {
372 return Some(Ok(c));
373 }
374 }
375 Err(e) => {
376 failed = true;
377 return Some(Err(e));
378 }
379 }
380 }))
381}
382
383fn read_leb_u32(buf: &[u8], pos: &mut usize) -> Result<u32, ManifestError> {
384 let mut result: u32 = 0;
385 for i in 0..5 {
386 let byte = *buf.get(*pos).ok_or(ManifestError::Malformed)?;
387 *pos += 1;
388 if i == 4 && byte & 0xf0 != 0 {
389 return Err(ManifestError::Malformed);
390 }
391 result |= u32::from(byte & 0x7f) << (7 * i);
392 if byte & 0x80 == 0 {
393 return Ok(result);
394 }
395 }
396 Err(ManifestError::Malformed)
397}
398
399#[cfg(test)]
400mod tests {
401 use super::*;
402
403 fn leb(mut v: u32) -> Vec<u8> {
404 let mut out = Vec::new();
405 loop {
406 let mut b = (v & 0x7f) as u8;
407 v >>= 7;
408 if v != 0 {
409 b |= 0x80;
410 }
411 out.push(b);
412 if v == 0 {
413 return out;
414 }
415 }
416 }
417
418 fn custom_section(name: &str, payload: &[u8]) -> Vec<u8> {
419 let mut body = leb(name.len() as u32);
420 body.extend_from_slice(name.as_bytes());
421 body.extend_from_slice(payload);
422 let mut out = vec![0u8];
423 out.extend(leb(body.len() as u32));
424 out.extend(body);
425 out
426 }
427
428 fn module(sections: &[Vec<u8>]) -> Vec<u8> {
429 let mut m = vec![0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00];
430 // A non-custom section first (type section, empty vec), so the walker
431 // has to step over a section it does not care about.
432 m.extend([0x01, 0x01, 0x00]);
433 for s in sections {
434 m.extend_from_slice(s);
435 }
436 m
437 }
438
439 fn sample() -> DelegateManifest {
440 DelegateManifest::new(
441 vec![LifecycleKind::Installed, LifecycleKind::NodeStarted],
442 vec![Capability::Background],
443 )
444 }
445
446 #[test]
447 fn round_trips_through_a_wasm_custom_section() {
448 let m = module(&[
449 custom_section("name", b"whatever"),
450 custom_section(MANIFEST_SECTION_NAME, &sample().to_bytes()),
451 ]);
452 assert_eq!(DelegateManifest::from_wasm(&m).unwrap(), Some(sample()));
453 }
454
455 #[test]
456 fn a_module_without_the_section_has_no_manifest() {
457 let m = module(&[custom_section("producers", b"rustc")]);
458 assert_eq!(DelegateManifest::from_wasm(&m).unwrap(), None);
459 }
460
461 /// The exact JSON the macro writes. If this changes, every delegate that
462 /// declares a manifest re-keys on its next build, and older nodes must
463 /// still read it.
464 #[test]
465 fn json_shape_is_pinned() {
466 assert_eq!(
467 String::from_utf8(sample().to_bytes()).unwrap(),
468 r#"{"manifest_version":1,"lifecycle":["installed","node_started"],"capabilities":["background"]}"#
469 );
470 }
471
472 /// A manifest from a newer stdlib — an unknown field, an unknown
473 /// capability, an unknown lifecycle kind — must still be read, keeping
474 /// what this reader knows. Otherwise one new capability name would strip
475 /// every older node of the kinds it does understand.
476 #[test]
477 fn a_newer_manifest_is_read_keeping_known_entries() {
478 let json = br#"{"manifest_version":3,"lifecycle":["installed","woke_up"],
479 "capabilities":["background","teleport"],"brand_new_field":{"x":1}}"#;
480 let m = DelegateManifest::from_bytes(json).unwrap();
481 assert!(m.wants_lifecycle(LifecycleKind::Installed));
482 assert!(!m.wants_lifecycle(LifecycleKind::NodeStarted));
483 assert!(!m.wants_lifecycle(LifecycleKind::Unknown));
484 assert!(!m.wants_capability(Capability::Unknown));
485 assert_eq!(m.known_capabilities(), vec![Capability::Background]);
486 }
487
488 #[test]
489 fn missing_lists_default_to_empty() {
490 let m = DelegateManifest::from_bytes(br#"{"manifest_version":1}"#).unwrap();
491 assert!(m.lifecycle.is_empty() && m.capabilities.is_empty());
492 let m = DelegateManifest::from_bytes(
493 br#"{"manifest_version":1,"lifecycle":null,"capabilities":null}"#,
494 )
495 .unwrap();
496 assert!(m.lifecycle.is_empty() && m.capabilities.is_empty());
497 let m = DelegateManifest::from_bytes(
498 br#"{"manifest_version":1,"lifecycle":"installed","capabilities":{"background":{}}}"#,
499 )
500 .unwrap();
501 assert!(m.lifecycle.is_empty() && m.capabilities.is_empty());
502 }
503
504 /// A later format might write an entry that is not a bare name. That entry
505 /// is unknown to this reader; the known ones next to it must still count.
506 #[test]
507 fn a_non_string_entry_reads_as_unknown_not_as_an_error() {
508 let json = br#"{"manifest_version":2,
509 "lifecycle":[{"woke_up":{"every_s":60}},"node_started",7],
510 "capabilities":[{"notify":{"max_per_hour":4}},"background",null]}"#;
511 let m = DelegateManifest::from_bytes(json).unwrap();
512 assert_eq!(
513 m.lifecycle,
514 vec![
515 LifecycleKind::Unknown,
516 LifecycleKind::NodeStarted,
517 LifecycleKind::Unknown
518 ]
519 );
520 assert_eq!(m.known_capabilities(), vec![Capability::Background]);
521 }
522
523 #[test]
524 fn macro_agreement_check() {
525 assert!(__manifest_macro_agrees(
526 MANIFEST_SECTION_NAME,
527 MANIFEST_VERSION
528 ));
529 assert!(!__manifest_macro_agrees(
530 "freenet-manifesT",
531 MANIFEST_VERSION
532 ));
533 assert!(!__manifest_macro_agrees(
534 "freenet-manifest2",
535 MANIFEST_VERSION
536 ));
537 assert!(!__manifest_macro_agrees(
538 MANIFEST_SECTION_NAME,
539 MANIFEST_VERSION + 1
540 ));
541 }
542
543 #[test]
544 fn rejects_version_zero_oversize_and_garbage() {
545 assert_eq!(
546 DelegateManifest::from_bytes(br#"{"manifest_version":0}"#),
547 Err(ManifestError::BadVersion)
548 );
549 let big = vec![b' '; MAX_MANIFEST_BYTES + 1];
550 assert_eq!(
551 DelegateManifest::from_bytes(&big),
552 Err(ManifestError::TooLarge(MAX_MANIFEST_BYTES + 1))
553 );
554 assert!(matches!(
555 DelegateManifest::from_bytes(b"not json"),
556 Err(ManifestError::Decode(_))
557 ));
558 }
559
560 /// Two sections would be ambiguous. It also catches two crates in one
561 /// build each emitting a manifest: the linker would normally concatenate
562 /// same-named sections into one, which fails to decode instead, but a
563 /// post-link tool could leave them separate.
564 #[test]
565 fn rejects_a_duplicate_section() {
566 let payload = sample().to_bytes();
567 let m = module(&[
568 custom_section(MANIFEST_SECTION_NAME, &payload),
569 custom_section(MANIFEST_SECTION_NAME, &payload),
570 ]);
571 assert_eq!(
572 DelegateManifest::from_wasm(&m),
573 Err(ManifestError::Duplicate)
574 );
575 }
576
577 #[test]
578 fn concatenated_manifests_fail_to_decode() {
579 let mut payload = sample().to_bytes();
580 payload.extend(sample().to_bytes());
581 let m = module(&[custom_section(MANIFEST_SECTION_NAME, &payload)]);
582 assert!(matches!(
583 DelegateManifest::from_wasm(&m),
584 Err(ManifestError::Decode(_))
585 ));
586 }
587
588 #[test]
589 fn rejects_non_wasm_and_truncated_modules() {
590 assert_eq!(
591 DelegateManifest::from_wasm(b"\0asm"),
592 Err(ManifestError::NotWasm)
593 );
594 assert_eq!(
595 DelegateManifest::from_wasm(b"hello world, not wasm"),
596 Err(ManifestError::NotWasm)
597 );
598 let mut m = module(&[custom_section(MANIFEST_SECTION_NAME, &sample().to_bytes())]);
599 m.truncate(m.len() - 3);
600 assert_eq!(
601 DelegateManifest::from_wasm(&m),
602 Err(ManifestError::Malformed)
603 );
604 // A section whose declared size runs past the end.
605 let mut m = module(&[]);
606 m.extend([0x00, 0xff, 0xff, 0x03]);
607 assert_eq!(
608 DelegateManifest::from_wasm(&m),
609 Err(ManifestError::Malformed)
610 );
611 // An over-long LEB128 (more than 5 bytes).
612 let mut m = module(&[]);
613 m.extend([0x00, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00]);
614 assert_eq!(
615 DelegateManifest::from_wasm(&m),
616 Err(ManifestError::Malformed)
617 );
618 }
619
620 /// Wire pin for the nested enum. Appending a variant is fine; reordering
621 /// or inserting one silently reinterprets deployed delegates' input.
622 #[test]
623 fn lifecycle_event_tags_are_pinned() {
624 fn tag(e: &LifecycleEvent) -> u32 {
625 match e {
626 LifecycleEvent::Installed => 0,
627 LifecycleEvent::NodeStarted { .. } => 1,
628 }
629 }
630 let all = [
631 LifecycleEvent::Installed,
632 LifecycleEvent::NodeStarted {
633 down_since_ms: Some(0x0102_0304_0506_0708),
634 },
635 ];
636 for e in &all {
637 let enc = bincode::serialize(e).unwrap();
638 assert_eq!(u32::from_le_bytes(enc[..4].try_into().unwrap()), tag(e));
639 assert_eq!(&bincode::deserialize::<LifecycleEvent>(&enc).unwrap(), e);
640 }
641 // Full byte layout of NodeStarted: tag 1, Option tag 1, u64 LE.
642 assert_eq!(
643 bincode::serialize(&all[1]).unwrap(),
644 vec![1, 0, 0, 0, 1, 8, 7, 6, 5, 4, 3, 2, 1]
645 );
646 // `kind()` agrees with the manifest kinds.
647 assert_eq!(all[0].kind(), LifecycleKind::Installed);
648 assert_eq!(all[1].kind(), LifecycleKind::NodeStarted);
649 }
650}