commit a891f09aedb6f219ed544184edcbf3e896e9e5dd
parent 06aac3d5f9b573c8923ee094eb6efe2e3ad52a11
Author: triesap <tyson@radroots.org>
Date: Mon, 27 Jul 2026 07:14:13 +0000
transport: seal bounded payload state
- hide payload variant fields behind validated constructors and accessors
- preserve the established serialized payload representation
- reject oversized strings and byte sequences through bounded visitors
- migrate runtime and Nostr consumers to the sealed payload API
Diffstat:
8 files changed, 404 insertions(+), 94 deletions(-)
diff --git a/crates/runtime/src/transport.rs b/crates/runtime/src/transport.rs
@@ -686,10 +686,9 @@ mod tests {
RadrootsTransportDeliveryRequest, RadrootsTransportDeliveryTargetStatus,
RadrootsTransportError, RadrootsTransportFetchReceipt, RadrootsTransportFetchRequest,
RadrootsTransportFuture, RadrootsTransportImplementationState, RadrootsTransportKind,
- RadrootsTransportOutcome, RadrootsTransportOutcomeKind, RadrootsTransportPayload,
- RadrootsTransportSatisfactionClass, RadrootsTransportSatisfactionPolicy,
- RadrootsTransportStatus, RadrootsTransportTarget, RadrootsTransportTargetReceipt,
- RadrootsTransportTargetSet,
+ RadrootsTransportOutcome, RadrootsTransportOutcomeKind, RadrootsTransportSatisfactionClass,
+ RadrootsTransportSatisfactionPolicy, RadrootsTransportStatus, RadrootsTransportTarget,
+ RadrootsTransportTargetReceipt, RadrootsTransportTargetSet,
};
#[cfg(feature = "transport-workers")]
use std::sync::{Arc, Mutex};
@@ -912,12 +911,9 @@ mod tests {
let via_variant = RadrootsRuntimeTransportPayload::signed_event(event.clone())
.transport_payload()
.expect("transport payload");
- let RadrootsTransportPayload::SignedEventJson {
- event_id, raw_json, ..
- } = payload.clone()
- else {
- panic!("signed event payload expected");
- };
+ let (event_id, raw_json) = payload
+ .signed_event_json_parts()
+ .expect("signed event payload");
assert_eq!(payload, via_variant);
assert_eq!(event_id, event.signed_event().id_str());
diff --git a/crates/transport/src/lib.rs b/crates/transport/src/lib.rs
@@ -11,6 +11,8 @@ mod limits;
mod message;
mod payload;
mod reticulum;
+#[cfg(feature = "serde")]
+mod serde_bounds;
mod status;
mod target;
mod transport;
diff --git a/crates/transport/src/payload.rs b/crates/transport/src/payload.rs
@@ -7,9 +7,14 @@ use alloc::string::{String, ToString};
use alloc::vec::Vec;
use sha2::{Digest, Sha256};
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct RadrootsTransportPayload {
+ body: RadrootsTransportPayloadBody,
+}
+
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[derive(Clone, Debug, PartialEq, Eq)]
-pub enum RadrootsTransportPayload {
+enum RadrootsTransportPayloadBody {
SignedEventJson {
event_id: String,
raw_json: String,
@@ -39,10 +44,12 @@ impl RadrootsTransportPayload {
let event_id = validate_hex_id(event_id)?;
let raw_json = validate_raw_json(raw_json)?;
let digest = sha256_hex(raw_json.as_bytes());
- Ok(Self::SignedEventJson {
- event_id,
- raw_json,
- digest,
+ Ok(Self {
+ body: RadrootsTransportPayloadBody::SignedEventJson {
+ event_id,
+ raw_json,
+ digest,
+ },
})
}
@@ -82,10 +89,12 @@ impl RadrootsTransportPayload {
RADROOTS_TRANSPORT_RETICULUM_PAYLOAD_MAX_BYTES,
)?;
let digest = sha256_hex(bytes.as_slice());
- Ok(Self::MeshFrameCbor {
- message_id,
- bytes,
- digest,
+ Ok(Self {
+ body: RadrootsTransportPayloadBody::MeshFrameCbor {
+ message_id,
+ bytes,
+ digest,
+ },
})
}
@@ -126,10 +135,12 @@ impl RadrootsTransportPayload {
RADROOTS_TRANSPORT_OPAQUE_PAYLOAD_MAX_BYTES,
)?;
let digest = sha256_hex(bytes.as_slice());
- Ok(Self::OpaqueBytes {
- label,
- bytes,
- digest,
+ Ok(Self {
+ body: RadrootsTransportPayloadBody::OpaqueBytes {
+ label,
+ bytes,
+ digest,
+ },
})
}
@@ -152,34 +163,62 @@ impl RadrootsTransportPayload {
}
pub fn digest(&self) -> &str {
- match self {
- Self::SignedEventJson { digest, .. }
- | Self::MeshFrameCbor { digest, .. }
- | Self::OpaqueBytes { digest, .. } => digest.as_str(),
+ match &self.body {
+ RadrootsTransportPayloadBody::SignedEventJson { digest, .. }
+ | RadrootsTransportPayloadBody::MeshFrameCbor { digest, .. }
+ | RadrootsTransportPayloadBody::OpaqueBytes { digest, .. } => digest.as_str(),
}
}
pub fn payload_kind(&self) -> &'static str {
- match self {
- Self::SignedEventJson { .. } => "signed_event_json",
- Self::MeshFrameCbor { .. } => "mesh_frame_cbor",
- Self::OpaqueBytes { .. } => "opaque_bytes",
+ match &self.body {
+ RadrootsTransportPayloadBody::SignedEventJson { .. } => "signed_event_json",
+ RadrootsTransportPayloadBody::MeshFrameCbor { .. } => "mesh_frame_cbor",
+ RadrootsTransportPayloadBody::OpaqueBytes { .. } => "opaque_bytes",
+ }
+ }
+
+ pub fn signed_event_json_parts(&self) -> Option<(&str, &str)> {
+ match &self.body {
+ RadrootsTransportPayloadBody::SignedEventJson {
+ event_id, raw_json, ..
+ } => Some((event_id, raw_json)),
+ RadrootsTransportPayloadBody::MeshFrameCbor { .. }
+ | RadrootsTransportPayloadBody::OpaqueBytes { .. } => None,
+ }
+ }
+
+ pub fn mesh_frame_cbor_parts(&self) -> Option<(&str, &[u8])> {
+ match &self.body {
+ RadrootsTransportPayloadBody::MeshFrameCbor {
+ message_id, bytes, ..
+ } => Some((message_id, bytes)),
+ RadrootsTransportPayloadBody::SignedEventJson { .. }
+ | RadrootsTransportPayloadBody::OpaqueBytes { .. } => None,
+ }
+ }
+
+ pub fn opaque_bytes_parts(&self) -> Option<(&str, &[u8])> {
+ match &self.body {
+ RadrootsTransportPayloadBody::OpaqueBytes { label, bytes, .. } => Some((label, bytes)),
+ RadrootsTransportPayloadBody::SignedEventJson { .. }
+ | RadrootsTransportPayloadBody::MeshFrameCbor { .. } => None,
}
}
pub fn validate(&self) -> Result<(), RadrootsTransportError> {
- let canonical = match self {
- Self::SignedEventJson {
+ let canonical = match &self.body {
+ RadrootsTransportPayloadBody::SignedEventJson {
event_id,
raw_json,
digest,
} => Self::signed_event_json_with_digest(event_id, raw_json, digest)?,
- Self::MeshFrameCbor {
+ RadrootsTransportPayloadBody::MeshFrameCbor {
message_id,
bytes,
digest,
} => Self::validated_mesh_frame_cbor_with_digest(message_id, bytes, digest)?,
- Self::OpaqueBytes {
+ RadrootsTransportPayloadBody::OpaqueBytes {
label,
bytes,
digest,
@@ -193,21 +232,41 @@ impl RadrootsTransportPayload {
}
#[cfg(feature = "serde")]
+impl serde::Serialize for RadrootsTransportPayload {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ self.body.serialize(serializer)
+ }
+}
+
+#[cfg(feature = "serde")]
#[derive(serde::Deserialize)]
+#[serde(deny_unknown_fields)]
enum RadrootsTransportPayloadWire {
SignedEventJson {
+ #[serde(deserialize_with = "deserialize_payload_event_id")]
event_id: String,
+ #[serde(deserialize_with = "deserialize_signed_event_json")]
raw_json: String,
+ #[serde(deserialize_with = "deserialize_payload_digest")]
digest: String,
},
MeshFrameCbor {
+ #[serde(deserialize_with = "deserialize_payload_identifier")]
message_id: String,
+ #[serde(deserialize_with = "deserialize_reticulum_payload_bytes")]
bytes: Vec<u8>,
+ #[serde(deserialize_with = "deserialize_payload_digest")]
digest: String,
},
OpaqueBytes {
+ #[serde(deserialize_with = "deserialize_payload_identifier")]
label: String,
+ #[serde(deserialize_with = "deserialize_opaque_payload_bytes")]
bytes: Vec<u8>,
+ #[serde(deserialize_with = "deserialize_payload_digest")]
digest: String,
},
}
@@ -219,40 +278,91 @@ impl<'de> serde::Deserialize<'de> for RadrootsTransportPayload {
D: serde::Deserializer<'de>,
{
let wire = RadrootsTransportPayloadWire::deserialize(deserializer)?;
- let payload = match wire {
+ match wire {
RadrootsTransportPayloadWire::SignedEventJson {
event_id,
raw_json,
digest,
- } => Self::SignedEventJson {
- event_id,
- raw_json,
- digest,
- },
+ } => Self::signed_event_json_with_digest(&event_id, &raw_json, &digest),
RadrootsTransportPayloadWire::MeshFrameCbor {
message_id,
bytes,
digest,
- } => Self::MeshFrameCbor {
- message_id,
- bytes,
- digest,
- },
+ } => Self::validated_mesh_frame_cbor_with_digest(&message_id, &bytes, &digest),
RadrootsTransportPayloadWire::OpaqueBytes {
label,
bytes,
digest,
- } => Self::OpaqueBytes {
- label,
- bytes,
- digest,
- },
- };
- payload.validate().map_err(serde::de::Error::custom)?;
- Ok(payload)
+ } => Self::validated_opaque_bytes_with_digest(&label, &bytes, &digest),
+ }
+ .map_err(serde::de::Error::custom)
}
}
+#[cfg(feature = "serde")]
+fn deserialize_payload_event_id<'de, D>(deserializer: D) -> Result<String, D::Error>
+where
+ D: serde::Deserializer<'de>,
+{
+ crate::serde_bounds::deserialize_string(deserializer, "payload_id", 64)
+}
+
+#[cfg(feature = "serde")]
+fn deserialize_payload_identifier<'de, D>(deserializer: D) -> Result<String, D::Error>
+where
+ D: serde::Deserializer<'de>,
+{
+ crate::serde_bounds::deserialize_string(
+ deserializer,
+ "payload_id",
+ RADROOTS_TRANSPORT_IDENTIFIER_MAX_BYTES,
+ )
+}
+
+#[cfg(feature = "serde")]
+fn deserialize_signed_event_json<'de, D>(deserializer: D) -> Result<String, D::Error>
+where
+ D: serde::Deserializer<'de>,
+{
+ crate::serde_bounds::deserialize_string(
+ deserializer,
+ "signed_event_json_bytes",
+ RADROOTS_TRANSPORT_SIGNED_EVENT_JSON_MAX_BYTES,
+ )
+}
+
+#[cfg(feature = "serde")]
+fn deserialize_payload_digest<'de, D>(deserializer: D) -> Result<String, D::Error>
+where
+ D: serde::Deserializer<'de>,
+{
+ crate::serde_bounds::deserialize_string(deserializer, "digest", 64)
+}
+
+#[cfg(feature = "serde")]
+fn deserialize_reticulum_payload_bytes<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
+where
+ D: serde::Deserializer<'de>,
+{
+ crate::serde_bounds::deserialize_vec(
+ deserializer,
+ "mesh_frame_cbor_bytes",
+ RADROOTS_TRANSPORT_RETICULUM_PAYLOAD_MAX_BYTES,
+ )
+}
+
+#[cfg(feature = "serde")]
+fn deserialize_opaque_payload_bytes<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
+where
+ D: serde::Deserializer<'de>,
+{
+ crate::serde_bounds::deserialize_vec(
+ deserializer,
+ "opaque_payload_bytes",
+ RADROOTS_TRANSPORT_OPAQUE_PAYLOAD_MAX_BYTES,
+ )
+}
+
fn validate_hex_id(raw: &str) -> Result<String, RadrootsTransportError> {
if raw.len() != 64 || !raw.bytes().all(|byte| byte.is_ascii_hexdigit()) {
return Err(RadrootsTransportError::InvalidPayloadId);
diff --git a/crates/transport/src/serde_bounds.rs b/crates/transport/src/serde_bounds.rs
@@ -0,0 +1,136 @@
+use crate::RadrootsTransportError;
+use alloc::string::String;
+use alloc::vec::Vec;
+use core::fmt;
+use core::marker::PhantomData;
+use serde::de::{Error as _, SeqAccess, Visitor};
+
+pub(crate) fn deserialize_string<'de, D>(
+ deserializer: D,
+ field: &'static str,
+ max: usize,
+) -> Result<String, D::Error>
+where
+ D: serde::Deserializer<'de>,
+{
+ deserializer.deserialize_string(BoundedStringVisitor { field, max })
+}
+
+pub(crate) fn deserialize_vec<'de, D, T>(
+ deserializer: D,
+ field: &'static str,
+ max: usize,
+) -> Result<Vec<T>, D::Error>
+where
+ D: serde::Deserializer<'de>,
+ T: serde::Deserialize<'de>,
+{
+ deserializer.deserialize_seq(BoundedVecVisitor {
+ field,
+ max,
+ marker: PhantomData,
+ })
+}
+
+struct BoundedStringVisitor {
+ field: &'static str,
+ max: usize,
+}
+
+impl BoundedStringVisitor {
+ fn validate<E>(self, value: &str) -> Result<String, E>
+ where
+ E: serde::de::Error,
+ {
+ if value.len() > self.max {
+ return Err(E::custom(RadrootsTransportError::ResourceLimitExceeded {
+ field: self.field,
+ max: self.max,
+ actual: value.len(),
+ }));
+ }
+ Ok(value.into())
+ }
+}
+
+impl Visitor<'_> for BoundedStringVisitor {
+ type Value = String;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(formatter, "a string of at most {} UTF-8 bytes", self.max)
+ }
+
+ fn visit_borrowed_str<E>(self, value: &str) -> Result<Self::Value, E>
+ where
+ E: serde::de::Error,
+ {
+ self.validate(value)
+ }
+
+ fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
+ where
+ E: serde::de::Error,
+ {
+ self.validate(value)
+ }
+
+ fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
+ where
+ E: serde::de::Error,
+ {
+ if value.len() > self.max {
+ return Err(E::custom(RadrootsTransportError::ResourceLimitExceeded {
+ field: self.field,
+ max: self.max,
+ actual: value.len(),
+ }));
+ }
+ Ok(value)
+ }
+}
+
+struct BoundedVecVisitor<T> {
+ field: &'static str,
+ max: usize,
+ marker: PhantomData<T>,
+}
+
+impl<'de, T> Visitor<'de> for BoundedVecVisitor<T>
+where
+ T: serde::Deserialize<'de>,
+{
+ type Value = Vec<T>;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(formatter, "a sequence of at most {} items", self.max)
+ }
+
+ fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
+ where
+ A: SeqAccess<'de>,
+ {
+ if let Some(actual) = sequence.size_hint().filter(|actual| *actual > self.max) {
+ return Err(A::Error::custom(
+ RadrootsTransportError::ResourceLimitExceeded {
+ field: self.field,
+ max: self.max,
+ actual,
+ },
+ ));
+ }
+ let mut values = Vec::with_capacity(sequence.size_hint().unwrap_or(0).min(self.max));
+ while let Some(value) = sequence.next_element()? {
+ if values.len() == self.max {
+ return Err(A::Error::custom(
+ RadrootsTransportError::ResourceLimitExceeded {
+ field: self.field,
+ max: self.max,
+ actual: self.max.saturating_add(1),
+ },
+ ));
+ }
+ values.push(value);
+ }
+ Ok(values)
+ }
+}
diff --git a/crates/transport/tests/transport.rs b/crates/transport/tests/transport.rs
@@ -1458,11 +1458,7 @@ fn delivery_requests_and_receipts_reject_forged_identity_and_cardinality() {
.expect_err("empty request id"),
RadrootsTransportError::EmptyDeliveryRequestId
);
- for request_id in [
- " request".to_owned(),
- "request\n".to_owned(),
- "x".repeat(RADROOTS_TRANSPORT_DELIVERY_REQUEST_ID_MAX_BYTES + 1),
- ] {
+ for request_id in [" request".to_owned(), "request\n".to_owned()] {
assert_eq!(
RadrootsTransportDeliveryRequest::new(
request_id,
@@ -1491,22 +1487,6 @@ fn delivery_requests_and_receipts_reject_forged_identity_and_cardinality() {
RadrootsTransportError::RequiredTargetNotRequested
);
- let forged_payload = RadrootsTransportPayload::OpaqueBytes {
- label: "opaque".to_owned(),
- bytes: vec![1],
- digest: "0".repeat(64),
- };
- assert_eq!(
- RadrootsTransportDeliveryRequest::new(
- "request",
- forged_payload,
- targets.clone(),
- RadrootsTransportSatisfactionPolicy::all_accepted(),
- )
- .expect_err("forged payload"),
- RadrootsTransportError::PayloadDigestMismatch
- );
-
let request = RadrootsTransportDeliveryRequest::new(
"request",
payload,
@@ -2298,6 +2278,100 @@ fn transport_bounds_payloads_enforce_exact_and_one_over_before_copying() {
);
}
+#[cfg(feature = "serde")]
+#[test]
+fn transport_bounds_payload_wire_is_strict_and_bounded_for_every_variant() {
+ let exact_signed_json = format!(
+ "{{{}}}",
+ "a".repeat(RADROOTS_TRANSPORT_SIGNED_EVENT_JSON_MAX_BYTES - 2)
+ );
+ let exact_signed =
+ RadrootsTransportPayload::unchecked_signed_event_json("a".repeat(64), &exact_signed_json)
+ .expect("exact signed payload");
+ assert_eq!(
+ serde_json::from_value::<RadrootsTransportPayload>(
+ serde_json::to_value(&exact_signed).expect("serialize signed payload")
+ )
+ .expect("decode exact signed payload"),
+ exact_signed
+ );
+
+ let one_over_signed_json = format!(
+ "{{{}}}",
+ "a".repeat(RADROOTS_TRANSPORT_SIGNED_EVENT_JSON_MAX_BYTES - 1)
+ );
+ let one_over_signed_wire = serde_json::json!({
+ "SignedEventJson": {
+ "event_id": "a".repeat(64),
+ "raw_json": one_over_signed_json,
+ "digest": "0".repeat(64),
+ }
+ });
+ assert!(
+ serde_json::from_value::<RadrootsTransportPayload>(one_over_signed_wire)
+ .expect_err("reject one-over signed wire")
+ .to_string()
+ .contains("signed_event_json_bytes")
+ );
+
+ for exact in [
+ RadrootsTransportPayload::mesh_frame_cbor(
+ "mesh",
+ vec![0; RADROOTS_TRANSPORT_RETICULUM_PAYLOAD_MAX_BYTES],
+ )
+ .expect("exact mesh payload"),
+ RadrootsTransportPayload::opaque_bytes(
+ "opaque",
+ vec![0; RADROOTS_TRANSPORT_OPAQUE_PAYLOAD_MAX_BYTES],
+ )
+ .expect("exact opaque payload"),
+ ] {
+ assert_eq!(
+ serde_json::from_value::<RadrootsTransportPayload>(
+ serde_json::to_value(&exact).expect("serialize byte payload")
+ )
+ .expect("decode exact byte payload"),
+ exact
+ );
+ }
+
+ for (variant, field) in [
+ ("MeshFrameCbor", "mesh_frame_cbor_bytes"),
+ ("OpaqueBytes", "opaque_payload_bytes"),
+ ] {
+ let id_field = if variant == "MeshFrameCbor" {
+ "message_id"
+ } else {
+ "label"
+ };
+ let mut body = serde_json::Map::new();
+ body.insert(id_field.to_owned(), Value::String("bounded".to_owned()));
+ body.insert(
+ "bytes".to_owned(),
+ Value::Array(
+ core::iter::repeat_n(
+ Value::from(0),
+ RADROOTS_TRANSPORT_RETICULUM_PAYLOAD_MAX_BYTES + 1,
+ )
+ .collect(),
+ ),
+ );
+ body.insert("digest".to_owned(), Value::String("0".repeat(64)));
+ let mut wire = serde_json::Map::new();
+ wire.insert(variant.to_owned(), Value::Object(body));
+ assert!(
+ serde_json::from_value::<RadrootsTransportPayload>(Value::Object(wire))
+ .expect_err("reject one-over byte wire")
+ .to_string()
+ .contains(field)
+ );
+ }
+
+ let mut unknown = serde_json::to_value(opaque_payload()).expect("payload wire");
+ unknown["OpaqueBytes"]["unknown"] = Value::Bool(true);
+ assert!(serde_json::from_value::<RadrootsTransportPayload>(unknown).is_err());
+}
+
#[test]
fn every_transport_error_has_a_stable_display_message() {
let remaining = [
diff --git a/crates/transport_nostr/src/publish.rs b/crates/transport_nostr/src/publish.rs
@@ -570,18 +570,15 @@ mod contract_tests {
fn signed_event_from_transport_payload(
payload: &RadrootsTransportPayload,
) -> Result<RadrootsVerifiedSignedEvent, RadrootsTransportError> {
- let RadrootsTransportPayload::SignedEventJson {
- event_id, raw_json, ..
- } = payload
- else {
+ let Some((event_id, raw_json)) = payload.signed_event_json_parts() else {
return Err(RadrootsTransportError::InvalidPayloadBytes);
};
let wire = RadrootsNip01EventWire::parse_json(raw_json)
.map_err(|_| RadrootsTransportError::InvalidPayloadBytes)?;
- if wire.id != *event_id {
+ if wire.id != event_id {
return Err(RadrootsTransportError::InvalidPayloadId);
}
- RadrootsSignedEvent::from_wire_verified_id(wire, raw_json.clone())
+ RadrootsSignedEvent::from_wire_verified_id(wire, raw_json.to_owned())
.map_err(|_| RadrootsTransportError::InvalidPayloadBytes)?
.verify_signature()
.map_err(|_| RadrootsTransportError::InvalidPayloadSignature)
diff --git a/crates/transport_nostr/tests/phase1_outbox_publication.rs b/crates/transport_nostr/tests/phase1_outbox_publication.rs
@@ -245,9 +245,9 @@ async fn outbox_publication_all_seven_leaves_reuse_exact_bytes_and_dispatch_iden
}
fn assert_payload_exact(payload: &RadrootsTransportPayload, expected_raw: &str) {
- let RadrootsTransportPayload::SignedEventJson { raw_json, .. } = payload else {
- panic!("Phase 1 Nostr dispatch must carry signed event JSON");
- };
+ let (_, raw_json) = payload
+ .signed_event_json_parts()
+ .expect("Phase 1 Nostr dispatch must carry signed event JSON");
assert_eq!(raw_json.as_bytes(), expected_raw.as_bytes());
}
diff --git a/crates/transport_nostr/tests/transport.rs b/crates/transport_nostr/tests/transport.rs
@@ -1218,18 +1218,13 @@ fn verified_signed_event_payload_preserves_transport_payload_identity() {
let signed = signed_post("verified payload");
let payload = verified_signed_event_payload(&verified_signed_event(signed.clone()))
.expect("verified payload");
- let RadrootsTransportPayload::SignedEventJson {
- event_id,
- raw_json,
- digest,
- } = payload
- else {
- panic!("signed event payload expected");
- };
+ let (event_id, raw_json) = payload
+ .signed_event_json_parts()
+ .expect("signed event payload");
assert_eq!(event_id, signed.id_str());
- assert_eq!(raw_json, signed.raw_json().to_owned());
- assert_eq!(digest.len(), 64);
+ assert_eq!(raw_json, signed.raw_json());
+ assert_eq!(payload.digest().len(), 64);
}
#[tokio::test]