Skip to main content

auths_keri/
tel.rs

1//! Backerless TEL (Transaction Event Log) events for Auths credential status.
2//!
3//! A TEL is the KERI-native revocation registry. A *backerless* (`NB`) registry
4//! derives all of its trust from the issuer's KEL — there is no separate backer
5//! quorum (which would map onto witness infrastructure not run here). Three event
6//! types form the log, all SAID'd under the KERI protocol family (`KERI10JSON…`),
7//! matching keripy 1.3.4's `keri.vdr.eventing` byte-for-byte:
8//!
9//! - [`Vcp`] — registry inception. Self-addressing: `i` (registry SAID) equals
10//!   `d` and both are blanked during SAID-ification (same rule as KEL `icp`/`dip`).
11//!   Carries `c = ["NB"]`, `bt = "0"`, `b = []`, and a nonce `n`.
12//! - [`Iss`] — credential issuance. `i` is the *credential* SAID (an external
13//!   reference, never blanked); `s = "0"`; `ri` links the registry.
14//! - [`Rev`] — credential revocation. `i` is the credential SAID; `s = "1"`;
15//!   `ri` links the registry; `p` back-links the prior `iss` SAID (the chain).
16//!
17//! [`validate_tel`] is a pure function over an ordered event slice that enforces
18//! the `vcp → iss → rev` chain (back-link `p` + monotonic `s`) and returns a
19//! [`TelState`] of issued/revoked credentials, or a typed [`TelError`].
20//!
21//! ## `dt` is informational
22//!
23//! Both `iss` and `rev` carry an ISO-8601 `dt`. Per the clock-injection rule it is
24//! never branched on for correctness — it is preserved on the wire and committed by
25//! the SAID, but [`validate_tel`] does not compare or order by it.
26
27use std::collections::HashMap;
28
29use serde::{Deserialize, Serialize};
30
31use crate::error::TelError;
32use crate::events::KeriSequence;
33use crate::said::{Protocol, compute_said_with_protocol};
34use crate::types::{Prefix, Said};
35
36/// Pinned keripy revision whose TEL event SAID algorithm these types reproduce byte-for-byte.
37pub const TEL_KERIPY_REVISION: &str = "keripy 1.3.4";
38
39/// The backerless registry config trait code (`NoBackers`), as emitted in `vcp.c`.
40pub const TRAIT_NO_BACKERS: &str = "NB";
41
42/// The 17-char placeholder version string used before the two-pass size computation.
43const KERI_VERSION_PLACEHOLDER: &str = "KERI10JSON000000_";
44
45/// The 10-char KERI version-string prefix family (`KERI10JSON…`).
46const KERI_VERSION_PREFIX: &str = "KERI10JSON";
47
48/// Recomputes the `KERI10JSON{size:06x}_` version string for a serializable TEL event.
49///
50/// Two-pass, matching keripy: serialize the body with a zeroed-size placeholder
51/// `v`, measure the byte count, then format the real version string (identical
52/// length, so the size is stable).
53fn recompute_version_string<T: Serialize>(event: &T) -> Result<String, TelError> {
54    let bytes = serde_json::to_vec(event)?;
55    Ok(format!("{KERI_VERSION_PREFIX}{:06x}_", bytes.len()))
56}
57
58/// Registry inception event (`vcp`) for a backerless (`NB`) TEL.
59///
60/// Strict insertion order `{v, t, d, i, ii, s, c, bt, b, n}` matches keripy 1.3.4.
61/// `i` (the registry SAID) is self-addressing: it equals `d`, and both are blanked
62/// during SAID-ification. Construct via [`Vcp::new`] then [`Vcp::saidify`].
63#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
64pub struct Vcp {
65    /// Version string `KERI10JSON{size:06x}_`.
66    pub v: String,
67    /// Event type — always `"vcp"`.
68    pub t: String,
69    /// Registry SAID (Blake3-256, CESR `E…`). Self-addressing: equals `i`.
70    pub d: Said,
71    /// Registry SAID again (self-addressing identifier of the registry).
72    pub i: Said,
73    /// Issuing AID — the issuer's KERI prefix that controls this registry.
74    pub ii: Prefix,
75    /// Sequence number — always `"0"` for the inception event.
76    pub s: KeriSequence,
77    /// Config traits — `["NB"]` for a backerless registry.
78    pub c: Vec<String>,
79    /// Backer threshold — `"0"` for a backerless registry.
80    pub bt: KeriSequence,
81    /// Backer AID list — empty for a backerless registry.
82    pub b: Vec<Prefix>,
83    /// Registry nonce (CESR salt), making each registry SAID unique.
84    pub n: String,
85}
86
87impl Vcp {
88    /// Builds an un-SAID'd backerless `vcp`; call [`Vcp::saidify`] to fill `i`/`d`.
89    ///
90    /// Args:
91    /// * `issuer`: The issuing AID (`ii`) that controls the registry via its KEL.
92    /// * `nonce`: A CESR-encoded nonce (`n`) making the registry SAID unique.
93    ///
94    /// Usage:
95    /// ```ignore
96    /// let vcp = Vcp::new(issuer, nonce).saidify()?;
97    /// ```
98    pub fn new(issuer: Prefix, nonce: String) -> Self {
99        Self {
100            v: KERI_VERSION_PLACEHOLDER.to_string(),
101            t: "vcp".to_string(),
102            d: Said::default(),
103            i: Said::default(),
104            ii: issuer,
105            s: KeriSequence::new(0),
106            c: vec![TRAIT_NO_BACKERS.to_string()],
107            bt: KeriSequence::new(0),
108            b: Vec::new(),
109            n: nonce,
110        }
111    }
112
113    /// Computes the self-addressing registry SAID, filling `d`, `i`, and the sized `v`.
114    ///
115    /// Usage:
116    /// ```ignore
117    /// let vcp = Vcp::new(issuer, nonce).saidify()?;
118    /// assert!(vcp.verify_said().is_ok());
119    /// ```
120    pub fn saidify(mut self) -> Result<Self, TelError> {
121        let body = serde_json::to_value(&self)?;
122        let said = compute_said_with_protocol(&body, Protocol::Keri)?;
123        self.d = said.clone();
124        self.i = said;
125        self.v = recompute_version_string(&self.probe())?;
126        Ok(self)
127    }
128
129    /// A clone with `v` reset to the placeholder, for the two-pass size measurement.
130    fn probe(&self) -> Self {
131        let mut probe = self.clone();
132        probe.v = KERI_VERSION_PLACEHOLDER.to_string();
133        probe
134    }
135
136    /// The registry SAID this inception establishes (the value carried in `iss`/`rev` `ri`).
137    pub fn registry(&self) -> &Said {
138        &self.d
139    }
140
141    /// Verifies the carried `d` (and self-addressing `i`) against a fresh recomputation.
142    ///
143    /// Usage:
144    /// ```ignore
145    /// vcp.verify_said()?; // Err(TelError::SaidMismatch) if tampered.
146    /// ```
147    pub fn verify_said(&self) -> Result<(), TelError> {
148        verify_event_said(self, &self.d, "vcp")?;
149        if self.i != self.d {
150            return Err(TelError::SaidMismatch {
151                event_type: "vcp",
152                computed: self.d.as_str().to_string(),
153                found: self.i.as_str().to_string(),
154            });
155        }
156        Ok(())
157    }
158}
159
160/// Credential issuance event (`iss`).
161///
162/// Strict insertion order `{v, t, d, i, s, ri, dt}` matches keripy 1.3.4. `i` is
163/// the *credential* SAID (an external reference, never blanked); `s` is always
164/// `"0"`; `ri` links the registry SAID. Construct via [`Iss::new`] then [`Iss::saidify`].
165#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
166pub struct Iss {
167    /// Version string `KERI10JSON{size:06x}_`.
168    pub v: String,
169    /// Event type — always `"iss"`.
170    pub t: String,
171    /// Event SAID (Blake3-256, CESR `E…`).
172    pub d: Said,
173    /// Credential SAID being issued.
174    pub i: Said,
175    /// Sequence number — always `"0"` for issuance.
176    pub s: KeriSequence,
177    /// Registry SAID this issuance belongs to.
178    pub ri: Said,
179    /// ISO-8601 issuance datetime (informational; never branched on for correctness).
180    pub dt: String,
181}
182
183impl Iss {
184    /// Builds an un-SAID'd `iss`; call [`Iss::saidify`] to fill `d`.
185    ///
186    /// Args:
187    /// * `credential`: The credential SAID being issued (`i`).
188    /// * `registry`: The registry SAID (`ri`) from a [`Vcp`].
189    /// * `dt`: ISO-8601 issuance datetime (`dt`).
190    ///
191    /// Usage:
192    /// ```ignore
193    /// let iss = Iss::new(credential, registry, dt).saidify()?;
194    /// ```
195    pub fn new(credential: Said, registry: Said, dt: String) -> Self {
196        Self {
197            v: KERI_VERSION_PLACEHOLDER.to_string(),
198            t: "iss".to_string(),
199            d: Said::default(),
200            i: credential,
201            s: KeriSequence::new(0),
202            ri: registry,
203            dt,
204        }
205    }
206
207    /// Computes the event SAID, filling `d` and the sized `v`.
208    pub fn saidify(mut self) -> Result<Self, TelError> {
209        let body = serde_json::to_value(&self)?;
210        self.d = compute_said_with_protocol(&body, Protocol::Keri)?;
211        let mut probe = self.clone();
212        probe.v = KERI_VERSION_PLACEHOLDER.to_string();
213        self.v = recompute_version_string(&probe)?;
214        Ok(self)
215    }
216
217    /// Verifies the carried `d` against a fresh recomputation.
218    pub fn verify_said(&self) -> Result<(), TelError> {
219        verify_event_said(self, &self.d, "iss")
220    }
221}
222
223/// Credential revocation event (`rev`).
224///
225/// Strict insertion order `{v, t, d, i, s, ri, p, dt}` matches keripy 1.3.4. `i`
226/// is the *credential* SAID; `s` is always `"1"`; `ri` links the registry; `p`
227/// back-links the prior `iss` SAID (the chain). Construct via [`Rev::new`] then
228/// [`Rev::saidify`].
229#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
230pub struct Rev {
231    /// Version string `KERI10JSON{size:06x}_`.
232    pub v: String,
233    /// Event type — always `"rev"`.
234    pub t: String,
235    /// Event SAID (Blake3-256, CESR `E…`).
236    pub d: Said,
237    /// Credential SAID being revoked.
238    pub i: Said,
239    /// Sequence number — always `"1"` for revocation.
240    pub s: KeriSequence,
241    /// Registry SAID this revocation belongs to.
242    pub ri: Said,
243    /// Prior event SAID — the `iss` event's `d` (the chain back-link).
244    pub p: Said,
245    /// ISO-8601 revocation datetime (informational; never branched on for correctness).
246    pub dt: String,
247}
248
249impl Rev {
250    /// Builds an un-SAID'd `rev`; call [`Rev::saidify`] to fill `d`.
251    ///
252    /// Args:
253    /// * `credential`: The credential SAID being revoked (`i`).
254    /// * `registry`: The registry SAID (`ri`) from a [`Vcp`].
255    /// * `prior`: The prior `iss` event SAID (`p`, the chain back-link).
256    /// * `dt`: ISO-8601 revocation datetime (`dt`).
257    ///
258    /// Usage:
259    /// ```ignore
260    /// let rev = Rev::new(credential, registry, iss.d.clone(), dt).saidify()?;
261    /// ```
262    pub fn new(credential: Said, registry: Said, prior: Said, dt: String) -> Self {
263        Self {
264            v: KERI_VERSION_PLACEHOLDER.to_string(),
265            t: "rev".to_string(),
266            d: Said::default(),
267            i: credential,
268            s: KeriSequence::new(1),
269            ri: registry,
270            p: prior,
271            dt,
272        }
273    }
274
275    /// Computes the event SAID, filling `d` and the sized `v`.
276    pub fn saidify(mut self) -> Result<Self, TelError> {
277        let body = serde_json::to_value(&self)?;
278        self.d = compute_said_with_protocol(&body, Protocol::Keri)?;
279        let mut probe = self.clone();
280        probe.v = KERI_VERSION_PLACEHOLDER.to_string();
281        self.v = recompute_version_string(&probe)?;
282        Ok(self)
283    }
284
285    /// Verifies the carried `d` against a fresh recomputation.
286    pub fn verify_said(&self) -> Result<(), TelError> {
287        verify_event_said(self, &self.d, "rev")
288    }
289}
290
291/// Recomputes a TEL event's SAID and checks it against the carried `d`.
292fn verify_event_said<T: Serialize>(
293    event: &T,
294    carried: &Said,
295    event_type: &'static str,
296) -> Result<(), TelError> {
297    let body = serde_json::to_value(event)?;
298    let computed = compute_said_with_protocol(&body, Protocol::Keri)?;
299    if &computed != carried {
300        return Err(TelError::SaidMismatch {
301            event_type,
302            computed: computed.into_inner(),
303            found: carried.as_str().to_string(),
304        });
305    }
306    Ok(())
307}
308
309/// The TEL→KEL anchor seal — a key-event seal carried in the issuer KEL `ixn`'s `a[]`.
310///
311/// keripy 1.3.4 anchors a TEL event into the issuer's KEL with a `SealEvent`
312/// (`{i, s, d}`): the registry/credential AID, the TEL event sequence number, and
313/// the TEL event SAID. The verifier (F.5) checks the issuer KEL `ixn` carries this
314/// exact shape. This is the `{i, s, d}` source-seal — not the bare `{s, d}` form.
315///
316/// Usage:
317/// ```ignore
318/// let seal = TelAnchorSeal::for_event(registry.clone(), iss.s, iss.d.clone());
319/// ```
320#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
321pub struct TelAnchorSeal {
322    /// The registry/credential AID the TEL event belongs to.
323    pub i: Prefix,
324    /// The TEL event sequence number (`s`).
325    pub s: KeriSequence,
326    /// The TEL event SAID (`d`).
327    pub d: Said,
328}
329
330impl TelAnchorSeal {
331    /// Builds the `{i, s, d}` anchor seal for a TEL event.
332    ///
333    /// Args:
334    /// * `aid`: The registry/credential AID the TEL event belongs to (`i`).
335    /// * `sequence`: The TEL event sequence number (`s`).
336    /// * `said`: The TEL event SAID (`d`).
337    ///
338    /// Usage:
339    /// ```ignore
340    /// let seal = TelAnchorSeal::for_event(registry, iss.s, iss.d.clone());
341    /// ```
342    pub fn for_event(aid: Prefix, sequence: KeriSequence, said: Said) -> Self {
343        Self {
344            i: aid,
345            s: sequence,
346            d: said,
347        }
348    }
349}
350
351/// The resolved status of a TEL after replaying its events in order.
352///
353/// `issued` holds every credential SAID a valid `iss` introduced; `revoked` holds
354/// those a valid `rev` subsequently revoked. A credential present in `issued` but
355/// absent from `revoked` is *currently valid*.
356#[derive(Debug, Clone, Default, PartialEq, Eq)]
357pub struct TelState {
358    /// Credential SAIDs introduced by an `iss` event.
359    pub issued: Vec<Said>,
360    /// Credential SAIDs revoked by a `rev` event.
361    pub revoked: Vec<Said>,
362}
363
364impl TelState {
365    /// Returns true if `credential` was issued and not subsequently revoked.
366    ///
367    /// Args:
368    /// * `credential`: The credential SAID to check.
369    pub fn is_valid(&self, credential: &Said) -> bool {
370        self.issued.contains(credential) && !self.revoked.contains(credential)
371    }
372}
373
374/// A single backerless TEL event, tagged by its event type.
375///
376/// `validate_tel` consumes an ordered slice of these. Deserializes from the wire by
377/// dispatching on the `t` field — never on byte length or field count. The hand-written
378/// serde impls let a `Vec<TelEvent>` round-trip through serde for the cross-boundary verify
379/// contract: `Serialize` delegates to the inner variant (whose canonical form already carries
380/// `t`), and `Deserialize` peeks `t` then decodes the **full** object into the variant.
381///
382/// A derived internally-`t`-tagged enum cannot be used here (unlike [`crate::Event`]): serde
383/// strips the tag before handing the content to the variant, but `Vcp`/`Iss`/`Rev` each carry
384/// their own `t` field, so the strip would surface as a spurious "missing field `t`".
385#[derive(Debug, Clone, PartialEq, Eq)]
386pub enum TelEvent {
387    /// Registry inception.
388    Vcp(Vcp),
389    /// Credential issuance.
390    Iss(Iss),
391    /// Credential revocation.
392    Rev(Rev),
393}
394
395impl Serialize for TelEvent {
396    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
397        match self {
398            TelEvent::Vcp(e) => e.serialize(serializer),
399            TelEvent::Iss(e) => e.serialize(serializer),
400            TelEvent::Rev(e) => e.serialize(serializer),
401        }
402    }
403}
404
405impl<'de> Deserialize<'de> for TelEvent {
406    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
407        use serde::de::Error as _;
408        let value = serde_json::Value::deserialize(deserializer)?;
409        let event_type = value
410            .get("t")
411            .and_then(|v| v.as_str())
412            .ok_or_else(|| D::Error::missing_field("t"))?;
413        match event_type {
414            "vcp" => serde_json::from_value(value)
415                .map(TelEvent::Vcp)
416                .map_err(D::Error::custom),
417            "iss" => serde_json::from_value(value)
418                .map(TelEvent::Iss)
419                .map_err(D::Error::custom),
420            "rev" => serde_json::from_value(value)
421                .map(TelEvent::Rev)
422                .map_err(D::Error::custom),
423            other => Err(D::Error::custom(format!(
424                "unknown TEL event type '{other}'"
425            ))),
426        }
427    }
428}
429
430impl TelEvent {
431    /// Parses a single TEL event from its wire JSON bytes, dispatching on `t`.
432    ///
433    /// Args:
434    /// * `bytes`: The insertion-order JSON serialization of one TEL event.
435    ///
436    /// Usage:
437    /// ```ignore
438    /// let event = TelEvent::from_wire_bytes(&iss.to_wire_bytes()?)?;
439    /// ```
440    pub fn from_wire_bytes(bytes: &[u8]) -> Result<Self, TelError> {
441        let value: serde_json::Value = serde_json::from_slice(bytes)?;
442        let event_type = value
443            .get("t")
444            .and_then(|v| v.as_str())
445            .ok_or_else(|| TelError::Said("TEL event missing required field 't'".to_string()))?;
446        match event_type {
447            "vcp" => Ok(TelEvent::Vcp(serde_json::from_value(value)?)),
448            "iss" => Ok(TelEvent::Iss(serde_json::from_value(value)?)),
449            "rev" => Ok(TelEvent::Rev(serde_json::from_value(value)?)),
450            other => Err(TelError::BrokenChain {
451                credential: String::new(),
452                detail: format!("unknown TEL event type '{other}'"),
453            }),
454        }
455    }
456}
457
458/// Validates an ordered backerless TEL and resolves its issued/revoked state.
459///
460/// The events must form a valid `vcp → iss… → rev…` log:
461/// - The first event MUST be a `vcp` registry inception.
462/// - Every `iss` MUST name the inceptioned registry (`ri == vcp.d`) and introduce a
463///   credential exactly once (no double-issue).
464/// - Every `rev` MUST reference a previously-issued credential, name the same
465///   registry, back-link the credential's `iss` SAID via `p`, carry a strictly
466///   greater `s` than that `iss`, and revoke exactly once (no double-revoke).
467/// - Every event's carried `d` SAID MUST match a fresh recomputation.
468///
469/// `dt` is informational and is never compared or ordered on (clock-injection rule).
470///
471/// Args:
472/// * `events`: The TEL events in insertion order, starting with the `vcp`.
473///
474/// Usage:
475/// ```ignore
476/// let state = validate_tel(&[TelEvent::Vcp(vcp), TelEvent::Iss(iss)])?;
477/// assert!(state.is_valid(&credential));
478/// ```
479pub fn validate_tel(events: &[TelEvent]) -> Result<TelState, TelError> {
480    let mut iter = events.iter();
481    let registry = match iter.next() {
482        Some(TelEvent::Vcp(vcp)) => {
483            vcp.verify_said()?;
484            vcp.registry().clone()
485        }
486        _ => return Err(TelError::MissingInception),
487    };
488
489    let mut state = TelState::default();
490    let mut issuances: HashMap<String, Issuance> = HashMap::new();
491
492    for event in iter {
493        match event {
494            TelEvent::Vcp(_) => {
495                return Err(TelError::BrokenChain {
496                    credential: registry.as_str().to_string(),
497                    detail: "a second vcp inception is not allowed in one TEL".to_string(),
498                });
499            }
500            TelEvent::Iss(iss) => apply_iss(iss, &registry, &mut state, &mut issuances)?,
501            TelEvent::Rev(rev) => apply_rev(rev, &registry, &mut state, &issuances)?,
502        }
503    }
504
505    Ok(state)
506}
507
508/// The recorded issuance of a credential — its `iss` SAID and sequence number, for
509/// the `rev` chain check (`p` back-link + monotonic `s`).
510struct Issuance {
511    said: Said,
512    sequence: u128,
513}
514
515/// Applies one `iss` event to the running TEL state.
516fn apply_iss(
517    iss: &Iss,
518    registry: &Said,
519    state: &mut TelState,
520    issuances: &mut HashMap<String, Issuance>,
521) -> Result<(), TelError> {
522    iss.verify_said()?;
523    if &iss.ri != registry {
524        return Err(TelError::IssWithoutRegistry {
525            registry: iss.ri.as_str().to_string(),
526        });
527    }
528    if state.issued.contains(&iss.i) {
529        return Err(TelError::DoubleIss {
530            credential: iss.i.as_str().to_string(),
531        });
532    }
533    issuances.insert(
534        iss.i.as_str().to_string(),
535        Issuance {
536            said: iss.d.clone(),
537            sequence: iss.s.value(),
538        },
539    );
540    state.issued.push(iss.i.clone());
541    Ok(())
542}
543
544/// Applies one `rev` event to the running TEL state.
545fn apply_rev(
546    rev: &Rev,
547    registry: &Said,
548    state: &mut TelState,
549    issuances: &HashMap<String, Issuance>,
550) -> Result<(), TelError> {
551    rev.verify_said()?;
552    if &rev.ri != registry {
553        return Err(TelError::IssWithoutRegistry {
554            registry: rev.ri.as_str().to_string(),
555        });
556    }
557    let prior = issuances
558        .get(rev.i.as_str())
559        .ok_or_else(|| TelError::RevWithoutIss {
560            credential: rev.i.as_str().to_string(),
561        })?;
562    if state.revoked.contains(&rev.i) {
563        return Err(TelError::DoubleRev {
564            credential: rev.i.as_str().to_string(),
565        });
566    }
567    if rev.p != prior.said {
568        return Err(TelError::BrokenChain {
569            credential: rev.i.as_str().to_string(),
570            detail: format!(
571                "rev back-link p={} does not match issuance SAID {}",
572                rev.p, prior.said
573            ),
574        });
575    }
576    if rev.s.value() <= prior.sequence {
577        return Err(TelError::BrokenChain {
578            credential: rev.i.as_str().to_string(),
579            detail: format!(
580                "rev sequence {} must exceed the issuance sequence {}",
581                rev.s, prior.sequence
582            ),
583        });
584    }
585    state.revoked.push(rev.i.clone());
586    Ok(())
587}
588
589/// Encodes 16 random bytes as a CESR `Salt_128` (`0A…`) registry nonce (`vcp.n`).
590///
591/// The caller supplies the randomness (the clock/RNG boundary lives above this
592/// pure crate); this only performs the byte-deterministic CESR encoding, so the
593/// same 16 bytes always produce the same nonce — keypy-byte-identical to
594/// `coring.Salter(raw=…).qb64`.
595///
596/// Args:
597/// * `raw`: The 16 random salt bytes.
598///
599/// Usage:
600/// ```ignore
601/// let nonce = encode_nonce(&random_16_bytes)?;
602/// let vcp = Vcp::new(issuer, nonce).saidify()?;
603/// ```
604pub fn encode_nonce(raw: &[u8; 16]) -> Result<String, TelError> {
605    crate::cesr_encode::encode_salt_128(raw)
606        .map_err(|e| TelError::Said(format!("nonce encoding failed: {e}")))
607}
608
609/// Serializes a serializable TEL event to its canonical insertion-order JSON bytes.
610///
611/// Args:
612/// * `event`: Any SAID'd TEL event (`Vcp`/`Iss`/`Rev`).
613///
614/// Usage:
615/// ```ignore
616/// let wire = to_wire_bytes(&iss)?;
617/// ```
618pub fn to_wire_bytes<T: Serialize>(event: &T) -> Result<Vec<u8>, TelError> {
619    Ok(serde_json::to_vec(event)?)
620}