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, ®istry, &mut state, &mut issuances)?,
501 TelEvent::Rev(rev) => apply_rev(rev, ®istry, &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}