commit 06aac3d5f9b573c8923ee094eb6efe2e3ad52a11
parent cf75f8b9d49ca9c0a25c1477ccc16cd26b457679
Author: triesap <tyson@radroots.org>
Date: Mon, 27 Jul 2026 07:08:38 +0000
transport: enforce foundational resource bounds
- define the exact Phase 1 transport resource maxima
- reject oversized targets and required-target collections before set work
- bound request identifiers and every foundational payload variant
- cover exact and one-over behavior across constructors and adapters
Diffstat:
8 files changed, 391 insertions(+), 24 deletions(-)
diff --git a/crates/transport/src/delivery.rs b/crates/transport/src/delivery.rs
@@ -1,4 +1,5 @@
use crate::{
+ RADROOTS_TRANSPORT_IDENTIFIER_MAX_BYTES, RADROOTS_TRANSPORT_TARGET_MAX_COUNT,
RadrootsTransportDeliveryTargetStatus, RadrootsTransportError, RadrootsTransportOutcome,
RadrootsTransportOutcomeKind, RadrootsTransportPayload, RadrootsTransportTarget,
RadrootsTransportTargetFingerprint, RadrootsTransportTargetSet,
@@ -7,7 +8,8 @@ use alloc::collections::{BTreeMap, BTreeSet};
use alloc::string::String;
use alloc::vec::Vec;
-pub const RADROOTS_TRANSPORT_DELIVERY_REQUEST_ID_MAX_BYTES: usize = 256;
+pub const RADROOTS_TRANSPORT_DELIVERY_REQUEST_ID_MAX_BYTES: usize =
+ RADROOTS_TRANSPORT_IDENTIFIER_MAX_BYTES;
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
@@ -295,6 +297,11 @@ fn validate_required_targets(
if targets.is_empty() {
return Err(RadrootsTransportError::EmptyRequiredTargetSet);
}
+ crate::limits::ensure_resource_limit(
+ "required_target_count",
+ targets.len(),
+ RADROOTS_TRANSPORT_TARGET_MAX_COUNT,
+ )?;
let mut fingerprints = BTreeSet::new();
for target in targets {
if !fingerprints.insert(target.as_str()) {
@@ -365,10 +372,12 @@ fn validate_delivery_request_id(value: &str) -> Result<(), RadrootsTransportErro
if value.is_empty() {
return Err(RadrootsTransportError::EmptyDeliveryRequestId);
}
- if value != value.trim()
- || value.chars().any(char::is_control)
- || value.len() > RADROOTS_TRANSPORT_DELIVERY_REQUEST_ID_MAX_BYTES
- {
+ crate::limits::ensure_resource_limit(
+ "delivery_request_id",
+ value.len(),
+ RADROOTS_TRANSPORT_DELIVERY_REQUEST_ID_MAX_BYTES,
+ )?;
+ if value != value.trim() || value.chars().any(char::is_control) {
return Err(RadrootsTransportError::InvalidDeliveryRequestId);
}
Ok(())
diff --git a/crates/transport/src/error.rs b/crates/transport/src/error.rs
@@ -38,6 +38,11 @@ pub enum RadrootsTransportError {
InvalidPayloadSignature,
InvalidPayloadDigest,
PayloadDigestMismatch,
+ ResourceLimitExceeded {
+ field: &'static str,
+ max: usize,
+ actual: usize,
+ },
}
impl fmt::Display for RadrootsTransportError {
@@ -111,6 +116,10 @@ impl fmt::Display for RadrootsTransportError {
Self::PayloadDigestMismatch => {
f.write_str("transport payload digest does not match payload bytes")
}
+ Self::ResourceLimitExceeded { field, max, actual } => write!(
+ f,
+ "transport resource `{field}` exceeds maximum {max}: observed {actual}"
+ ),
}
}
}
diff --git a/crates/transport/src/lib.rs b/crates/transport/src/lib.rs
@@ -7,6 +7,7 @@ extern crate alloc;
mod delivery;
mod error;
mod kind;
+mod limits;
mod message;
mod payload;
mod reticulum;
@@ -24,6 +25,16 @@ pub use kind::{
RadrootsTransportCapabilityAvailability, RadrootsTransportCapabilityMaturity,
RadrootsTransportImplementationState, RadrootsTransportKind,
};
+pub use limits::{
+ RADROOTS_TRANSPORT_DIAGNOSTIC_MAX_BYTES, RADROOTS_TRANSPORT_ENDPOINT_URI_MAX_BYTES,
+ RADROOTS_TRANSPORT_FETCH_ADMITTED_EVENT_MAX_COUNT, RADROOTS_TRANSPORT_FETCH_FILTER_MAX_BYTES,
+ RADROOTS_TRANSPORT_FETCH_FILTER_MAX_COUNT, RADROOTS_TRANSPORT_FETCH_FILTERS_MAX_BYTES,
+ RADROOTS_TRANSPORT_FETCH_RAW_ITEM_MAX_COUNT, RADROOTS_TRANSPORT_FETCH_RAW_JSON_MAX_BYTES,
+ RADROOTS_TRANSPORT_IDENTIFIER_MAX_BYTES, RADROOTS_TRANSPORT_OPAQUE_PAYLOAD_MAX_BYTES,
+ RADROOTS_TRANSPORT_RETICULUM_PAYLOAD_MAX_BYTES, RADROOTS_TRANSPORT_SIGNED_EVENT_JSON_MAX_BYTES,
+ RADROOTS_TRANSPORT_TARGET_LABEL_MAX_BYTES, RADROOTS_TRANSPORT_TARGET_MAX_COUNT,
+ RADROOTS_TRANSPORT_TARGET_SCOPE_MAX_BYTES, RADROOTS_TRANSPORT_TOTAL_DEADLINE_MAX_MS,
+};
pub use message::{
RADROOTS_RETICULUM_ENDPOINT_URI, RADROOTS_RETICULUM_SCOPE_ID,
RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE,
diff --git a/crates/transport/src/limits.rs b/crates/transport/src/limits.rs
@@ -0,0 +1,30 @@
+pub const RADROOTS_TRANSPORT_SIGNED_EVENT_JSON_MAX_BYTES: usize = 256 * 1024;
+pub const RADROOTS_TRANSPORT_RETICULUM_PAYLOAD_MAX_BYTES: usize = 64 * 1024;
+pub const RADROOTS_TRANSPORT_OPAQUE_PAYLOAD_MAX_BYTES: usize =
+ RADROOTS_TRANSPORT_RETICULUM_PAYLOAD_MAX_BYTES;
+pub const RADROOTS_TRANSPORT_ENDPOINT_URI_MAX_BYTES: usize = 2_048;
+pub const RADROOTS_TRANSPORT_IDENTIFIER_MAX_BYTES: usize = 256;
+pub const RADROOTS_TRANSPORT_TARGET_SCOPE_MAX_BYTES: usize =
+ RADROOTS_TRANSPORT_IDENTIFIER_MAX_BYTES;
+pub const RADROOTS_TRANSPORT_TARGET_LABEL_MAX_BYTES: usize =
+ RADROOTS_TRANSPORT_IDENTIFIER_MAX_BYTES;
+pub const RADROOTS_TRANSPORT_TARGET_MAX_COUNT: usize = 16;
+pub const RADROOTS_TRANSPORT_FETCH_FILTER_MAX_COUNT: usize = 16;
+pub const RADROOTS_TRANSPORT_FETCH_FILTER_MAX_BYTES: usize = 64 * 1024;
+pub const RADROOTS_TRANSPORT_FETCH_FILTERS_MAX_BYTES: usize = 1024 * 1024;
+pub const RADROOTS_TRANSPORT_FETCH_ADMITTED_EVENT_MAX_COUNT: usize = 1_000;
+pub const RADROOTS_TRANSPORT_FETCH_RAW_ITEM_MAX_COUNT: usize = 4_096;
+pub const RADROOTS_TRANSPORT_FETCH_RAW_JSON_MAX_BYTES: usize = 64 * 1024 * 1024;
+pub const RADROOTS_TRANSPORT_DIAGNOSTIC_MAX_BYTES: usize = 4_096;
+pub const RADROOTS_TRANSPORT_TOTAL_DEADLINE_MAX_MS: u64 = 30_000;
+
+pub(crate) fn ensure_resource_limit(
+ field: &'static str,
+ actual: usize,
+ max: usize,
+) -> Result<(), crate::RadrootsTransportError> {
+ if actual > max {
+ return Err(crate::RadrootsTransportError::ResourceLimitExceeded { field, max, actual });
+ }
+ Ok(())
+}
diff --git a/crates/transport/src/payload.rs b/crates/transport/src/payload.rs
@@ -1,4 +1,8 @@
-use crate::RadrootsTransportError;
+use crate::{
+ RADROOTS_TRANSPORT_IDENTIFIER_MAX_BYTES, RADROOTS_TRANSPORT_OPAQUE_PAYLOAD_MAX_BYTES,
+ RADROOTS_TRANSPORT_RETICULUM_PAYLOAD_MAX_BYTES, RADROOTS_TRANSPORT_SIGNED_EVENT_JSON_MAX_BYTES,
+ RadrootsTransportError, limits::ensure_resource_limit,
+};
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use sha2::{Digest, Sha256};
@@ -72,7 +76,11 @@ impl RadrootsTransportPayload {
bytes: &[u8],
) -> Result<Self, RadrootsTransportError> {
let message_id = validate_token_id(message_id)?;
- let bytes = validate_bytes(bytes)?;
+ let bytes = validate_bytes(
+ "mesh_frame_cbor_bytes",
+ bytes,
+ RADROOTS_TRANSPORT_RETICULUM_PAYLOAD_MAX_BYTES,
+ )?;
let digest = sha256_hex(bytes.as_slice());
Ok(Self::MeshFrameCbor {
message_id,
@@ -112,7 +120,11 @@ impl RadrootsTransportPayload {
fn validated_opaque_bytes(label: &str, bytes: &[u8]) -> Result<Self, RadrootsTransportError> {
let label = validate_label(label)?;
- let bytes = validate_bytes(bytes)?;
+ let bytes = validate_bytes(
+ "opaque_payload_bytes",
+ bytes,
+ RADROOTS_TRANSPORT_OPAQUE_PAYLOAD_MAX_BYTES,
+ )?;
let digest = sha256_hex(bytes.as_slice());
Ok(Self::OpaqueBytes {
label,
@@ -256,6 +268,11 @@ fn validate_token_id(raw: &str) -> Result<String, RadrootsTransportError> {
if raw.is_empty() {
return Err(RadrootsTransportError::EmptyPayloadId);
}
+ ensure_resource_limit(
+ "payload_id",
+ raw.len(),
+ RADROOTS_TRANSPORT_IDENTIFIER_MAX_BYTES,
+ )?;
if raw != raw.trim()
|| raw
.chars()
@@ -271,7 +288,12 @@ fn validate_label(raw: &str) -> Result<String, RadrootsTransportError> {
if trimmed.is_empty() {
return Err(RadrootsTransportError::EmptyPayloadLabel);
}
- if trimmed.chars().any(char::is_control) {
+ ensure_resource_limit(
+ "payload_label",
+ raw.len(),
+ RADROOTS_TRANSPORT_IDENTIFIER_MAX_BYTES,
+ )?;
+ if raw != trimmed || trimmed.chars().any(char::is_control) {
return Err(RadrootsTransportError::InvalidPayloadLabel);
}
Ok(trimmed.to_string())
@@ -281,6 +303,11 @@ fn validate_raw_json(raw: &str) -> Result<String, RadrootsTransportError> {
if raw.is_empty() {
return Err(RadrootsTransportError::EmptyPayloadBytes);
}
+ ensure_resource_limit(
+ "signed_event_json_bytes",
+ raw.len(),
+ RADROOTS_TRANSPORT_SIGNED_EVENT_JSON_MAX_BYTES,
+ )?;
if raw != raw.trim()
|| raw.chars().any(char::is_control)
|| !raw.starts_with('{')
@@ -291,10 +318,15 @@ fn validate_raw_json(raw: &str) -> Result<String, RadrootsTransportError> {
Ok(raw.to_string())
}
-fn validate_bytes(raw: &[u8]) -> Result<Vec<u8>, RadrootsTransportError> {
+fn validate_bytes(
+ field: &'static str,
+ raw: &[u8],
+ max: usize,
+) -> Result<Vec<u8>, RadrootsTransportError> {
if raw.is_empty() {
return Err(RadrootsTransportError::EmptyPayloadBytes);
}
+ ensure_resource_limit(field, raw.len(), max)?;
Ok(raw.to_vec())
}
diff --git a/crates/transport/src/target.rs b/crates/transport/src/target.rs
@@ -1,6 +1,8 @@
use crate::{
- RADROOTS_RETICULUM_ENDPOINT_URI, RADROOTS_RETICULUM_SCOPE_ID, RadrootsTransportError,
- RadrootsTransportKind,
+ RADROOTS_RETICULUM_ENDPOINT_URI, RADROOTS_RETICULUM_SCOPE_ID,
+ RADROOTS_TRANSPORT_ENDPOINT_URI_MAX_BYTES, RADROOTS_TRANSPORT_TARGET_LABEL_MAX_BYTES,
+ RADROOTS_TRANSPORT_TARGET_MAX_COUNT, RADROOTS_TRANSPORT_TARGET_SCOPE_MAX_BYTES,
+ RadrootsTransportError, RadrootsTransportKind, limits::ensure_resource_limit,
};
use alloc::collections::BTreeSet;
use alloc::format;
@@ -15,11 +17,21 @@ pub struct RadrootsTransportTargetUri(String);
impl RadrootsTransportTargetUri {
pub fn parse(raw: impl AsRef<str>) -> Result<Self, RadrootsTransportError> {
+ ensure_resource_limit(
+ "target_uri",
+ raw.as_ref().len(),
+ RADROOTS_TRANSPORT_ENDPOINT_URI_MAX_BYTES,
+ )?;
let canonical = canonicalize_uri(raw.as_ref())?;
Ok(Self(canonical))
}
fn parse_nostr_relay(raw: impl AsRef<str>) -> Result<Self, RadrootsTransportError> {
+ ensure_resource_limit(
+ "target_uri",
+ raw.as_ref().len(),
+ RADROOTS_TRANSPORT_ENDPOINT_URI_MAX_BYTES,
+ )?;
let canonical = canonicalize_nostr_relay_uri(raw.as_ref())?;
Ok(Self(canonical))
}
@@ -62,6 +74,11 @@ impl RadrootsTransportMeshScopeId {
if value.is_empty() {
return Err(RadrootsTransportError::EmptyTargetScope);
}
+ ensure_resource_limit(
+ "target_scope",
+ value.len(),
+ RADROOTS_TRANSPORT_TARGET_SCOPE_MAX_BYTES,
+ )?;
if value != value.trim()
|| value
.chars()
@@ -104,11 +121,17 @@ pub struct RadrootsTransportTargetLabel(String);
impl RadrootsTransportTargetLabel {
pub fn parse(raw: impl AsRef<str>) -> Result<Self, RadrootsTransportError> {
- let trimmed = raw.as_ref().trim();
+ let raw = raw.as_ref();
+ let trimmed = raw.trim();
if trimmed.is_empty() {
return Err(RadrootsTransportError::EmptyTargetLabel);
}
- if trimmed.chars().any(char::is_control) {
+ ensure_resource_limit(
+ "target_label",
+ raw.len(),
+ RADROOTS_TRANSPORT_TARGET_LABEL_MAX_BYTES,
+ )?;
+ if raw != trimmed || trimmed.chars().any(char::is_control) {
return Err(RadrootsTransportError::InvalidTargetLabel);
}
Ok(Self(trimmed.to_string()))
@@ -383,6 +406,11 @@ impl RadrootsTransportTargetSet {
if targets.is_empty() {
return Err(RadrootsTransportError::EmptyTargetSet);
}
+ ensure_resource_limit(
+ "target_count",
+ targets.len(),
+ RADROOTS_TRANSPORT_TARGET_MAX_COUNT,
+ )?;
let mut fingerprints = BTreeSet::new();
for target in &targets {
if !fingerprints.insert(target.fingerprint.as_str().to_string()) {
diff --git a/crates/transport/tests/transport.rs b/crates/transport/tests/transport.rs
@@ -1,13 +1,16 @@
use radroots_transport::{
RADROOTS_RETICULUM_ENDPOINT_URI, RADROOTS_RETICULUM_SCOPE_ID,
- RADROOTS_TRANSPORT_DELIVERY_REQUEST_ID_MAX_BYTES, RadrootsTransport,
- RadrootsTransportCapabilities, RadrootsTransportCapabilityAvailability,
- RadrootsTransportCapabilityMaturity, RadrootsTransportDeliveryReceipt,
- RadrootsTransportDeliveryRequest, RadrootsTransportDeliveryTargetStatus,
- RadrootsTransportError, RadrootsTransportFetchReceipt, RadrootsTransportFetchRequest,
- RadrootsTransportFuture, RadrootsTransportImplementationState, RadrootsTransportKind,
- RadrootsTransportMeshScopeId, RadrootsTransportOutcome, RadrootsTransportOutcomeKind,
- RadrootsTransportPayload, RadrootsTransportSatisfactionClass,
+ RADROOTS_TRANSPORT_DELIVERY_REQUEST_ID_MAX_BYTES, RADROOTS_TRANSPORT_ENDPOINT_URI_MAX_BYTES,
+ RADROOTS_TRANSPORT_IDENTIFIER_MAX_BYTES, RADROOTS_TRANSPORT_OPAQUE_PAYLOAD_MAX_BYTES,
+ RADROOTS_TRANSPORT_RETICULUM_PAYLOAD_MAX_BYTES, RADROOTS_TRANSPORT_SIGNED_EVENT_JSON_MAX_BYTES,
+ RADROOTS_TRANSPORT_TARGET_LABEL_MAX_BYTES, RADROOTS_TRANSPORT_TARGET_MAX_COUNT,
+ RADROOTS_TRANSPORT_TARGET_SCOPE_MAX_BYTES, RadrootsTransport, RadrootsTransportCapabilities,
+ RadrootsTransportCapabilityAvailability, RadrootsTransportCapabilityMaturity,
+ RadrootsTransportDeliveryReceipt, RadrootsTransportDeliveryRequest,
+ RadrootsTransportDeliveryTargetStatus, RadrootsTransportError, RadrootsTransportFetchReceipt,
+ RadrootsTransportFetchRequest, RadrootsTransportFuture, RadrootsTransportImplementationState,
+ RadrootsTransportKind, RadrootsTransportMeshScopeId, RadrootsTransportOutcome,
+ RadrootsTransportOutcomeKind, RadrootsTransportPayload, RadrootsTransportSatisfactionClass,
RadrootsTransportSatisfactionPolicy, RadrootsTransportStatus, RadrootsTransportTarget,
RadrootsTransportTargetFingerprint, RadrootsTransportTargetLabel,
RadrootsTransportTargetReceipt, RadrootsTransportTargetSet, RadrootsTransportTargetUri,
@@ -18,6 +21,7 @@ use radroots_transport::{
use serde_json::Value;
use std::borrow::ToOwned;
use std::boxed::Box;
+use std::format;
use std::string::{String, ToString};
use std::vec;
use std::vec::Vec;
@@ -1734,7 +1738,7 @@ fn payload_contract_covers_all_validation_boundaries() {
let signed = RadrootsTransportPayload::unchecked_signed_event_json("a".repeat(64), "{}")
.expect("signed");
let mesh = RadrootsTransportPayload::mesh_frame_cbor("mesh", [1]).expect("mesh");
- let opaque = RadrootsTransportPayload::opaque_bytes(" label ", [2]).expect("opaque");
+ let opaque = RadrootsTransportPayload::opaque_bytes("label", [2]).expect("opaque");
assert_eq!(signed.payload_kind(), "signed_event_json");
assert_eq!(mesh.payload_kind(), "mesh_frame_cbor");
assert_eq!(opaque.payload_kind(), "opaque_bytes");
@@ -1979,10 +1983,14 @@ fn target_contract_covers_parser_and_authority_boundaries() {
);
}
- let label = RadrootsTransportTargetLabel::parse(" Relay One ").expect("label");
+ let label = RadrootsTransportTargetLabel::parse("Relay One").expect("label");
assert_eq!(label.as_str(), "Relay One");
assert_eq!(label.to_string(), "Relay One");
assert_eq!(
+ RadrootsTransportTargetLabel::parse(" Relay One ").expect_err("noncanonical label"),
+ RadrootsTransportError::InvalidTargetLabel
+ );
+ assert_eq!(
RadrootsTransportTargetLabel::parse("\u{7f}").expect_err("control label"),
RadrootsTransportError::InvalidTargetLabel
);
@@ -2065,6 +2073,232 @@ fn target_contract_covers_parser_and_authority_boundaries() {
}
#[test]
+fn transport_bounds_targets_enforce_exact_and_one_over_before_set_work() {
+ let exact_uri = "a".repeat(RADROOTS_TRANSPORT_ENDPOINT_URI_MAX_BYTES);
+ assert_eq!(
+ RadrootsTransportTargetUri::parse(exact_uri)
+ .expect("exact URI")
+ .as_str()
+ .len(),
+ RADROOTS_TRANSPORT_ENDPOINT_URI_MAX_BYTES
+ );
+ assert_eq!(
+ RadrootsTransportTargetUri::parse(
+ "a".repeat(RADROOTS_TRANSPORT_ENDPOINT_URI_MAX_BYTES + 1)
+ )
+ .expect_err("one-over URI"),
+ RadrootsTransportError::ResourceLimitExceeded {
+ field: "target_uri",
+ max: RADROOTS_TRANSPORT_ENDPOINT_URI_MAX_BYTES,
+ actual: RADROOTS_TRANSPORT_ENDPOINT_URI_MAX_BYTES + 1,
+ }
+ );
+
+ assert_eq!(
+ RadrootsTransportMeshScopeId::parse("a".repeat(RADROOTS_TRANSPORT_TARGET_SCOPE_MAX_BYTES))
+ .expect("exact scope")
+ .as_str()
+ .len(),
+ RADROOTS_TRANSPORT_TARGET_SCOPE_MAX_BYTES
+ );
+ assert_eq!(
+ RadrootsTransportMeshScopeId::parse(
+ "a".repeat(RADROOTS_TRANSPORT_TARGET_SCOPE_MAX_BYTES + 1)
+ )
+ .expect_err("one-over scope"),
+ RadrootsTransportError::ResourceLimitExceeded {
+ field: "target_scope",
+ max: RADROOTS_TRANSPORT_TARGET_SCOPE_MAX_BYTES,
+ actual: RADROOTS_TRANSPORT_TARGET_SCOPE_MAX_BYTES + 1,
+ }
+ );
+
+ assert_eq!(
+ RadrootsTransportTargetLabel::parse("a".repeat(RADROOTS_TRANSPORT_TARGET_LABEL_MAX_BYTES))
+ .expect("exact label")
+ .as_str()
+ .len(),
+ RADROOTS_TRANSPORT_TARGET_LABEL_MAX_BYTES
+ );
+ assert_eq!(
+ RadrootsTransportTargetLabel::parse(
+ "a".repeat(RADROOTS_TRANSPORT_TARGET_LABEL_MAX_BYTES + 1)
+ )
+ .expect_err("one-over label"),
+ RadrootsTransportError::ResourceLimitExceeded {
+ field: "target_label",
+ max: RADROOTS_TRANSPORT_TARGET_LABEL_MAX_BYTES,
+ actual: RADROOTS_TRANSPORT_TARGET_LABEL_MAX_BYTES + 1,
+ }
+ );
+
+ let exact_targets = (0..RADROOTS_TRANSPORT_TARGET_MAX_COUNT)
+ .map(|index| {
+ RadrootsTransportTarget::local(format!("local:target-{index}")).expect("bounded target")
+ })
+ .collect::<Vec<_>>();
+ assert_eq!(
+ RadrootsTransportTargetSet::new(exact_targets.clone())
+ .expect("exact target set")
+ .len(),
+ RADROOTS_TRANSPORT_TARGET_MAX_COUNT
+ );
+ let mut one_over_targets = exact_targets;
+ one_over_targets
+ .push(RadrootsTransportTarget::local("local:target-one-over").expect("one-over target"));
+ assert_eq!(
+ RadrootsTransportTargetSet::new(one_over_targets).expect_err("one-over target set"),
+ RadrootsTransportError::ResourceLimitExceeded {
+ field: "target_count",
+ max: RADROOTS_TRANSPORT_TARGET_MAX_COUNT,
+ actual: RADROOTS_TRANSPORT_TARGET_MAX_COUNT + 1,
+ }
+ );
+
+ let required_targets = (0..RADROOTS_TRANSPORT_TARGET_MAX_COUNT)
+ .map(|index| {
+ RadrootsTransportTargetFingerprint::parse(format!("{index:064x}"))
+ .expect("bounded fingerprint")
+ })
+ .collect::<Vec<_>>();
+ RadrootsTransportSatisfactionPolicy::required_targets(
+ RadrootsTransportSatisfactionClass::Accepted,
+ required_targets.clone(),
+ )
+ .expect("exact required-target set");
+ let mut one_over_required_targets = required_targets;
+ one_over_required_targets.push(
+ RadrootsTransportTargetFingerprint::parse(format!(
+ "{:064x}",
+ RADROOTS_TRANSPORT_TARGET_MAX_COUNT
+ ))
+ .expect("one-over fingerprint"),
+ );
+ assert_eq!(
+ RadrootsTransportSatisfactionPolicy::required_targets(
+ RadrootsTransportSatisfactionClass::Accepted,
+ one_over_required_targets,
+ )
+ .expect_err("one-over required-target set"),
+ RadrootsTransportError::ResourceLimitExceeded {
+ field: "required_target_count",
+ max: RADROOTS_TRANSPORT_TARGET_MAX_COUNT,
+ actual: RADROOTS_TRANSPORT_TARGET_MAX_COUNT + 1,
+ }
+ );
+}
+
+#[test]
+fn transport_bounds_payloads_enforce_exact_and_one_over_before_copying() {
+ let exact_json = format!(
+ "{{{}}}",
+ "a".repeat(RADROOTS_TRANSPORT_SIGNED_EVENT_JSON_MAX_BYTES - 2)
+ );
+ RadrootsTransportPayload::unchecked_signed_event_json("a".repeat(64), &exact_json)
+ .expect("exact signed event JSON");
+ let one_over_json = format!(
+ "{{{}}}",
+ "a".repeat(RADROOTS_TRANSPORT_SIGNED_EVENT_JSON_MAX_BYTES - 1)
+ );
+ assert_eq!(
+ RadrootsTransportPayload::unchecked_signed_event_json("a".repeat(64), &one_over_json)
+ .expect_err("one-over signed event JSON"),
+ RadrootsTransportError::ResourceLimitExceeded {
+ field: "signed_event_json_bytes",
+ max: RADROOTS_TRANSPORT_SIGNED_EVENT_JSON_MAX_BYTES,
+ actual: RADROOTS_TRANSPORT_SIGNED_EVENT_JSON_MAX_BYTES + 1,
+ }
+ );
+
+ RadrootsTransportPayload::mesh_frame_cbor(
+ "a".repeat(RADROOTS_TRANSPORT_IDENTIFIER_MAX_BYTES),
+ vec![0; RADROOTS_TRANSPORT_RETICULUM_PAYLOAD_MAX_BYTES],
+ )
+ .expect("exact mesh payload");
+ assert_eq!(
+ RadrootsTransportPayload::mesh_frame_cbor(
+ "mesh",
+ vec![0; RADROOTS_TRANSPORT_RETICULUM_PAYLOAD_MAX_BYTES + 1],
+ )
+ .expect_err("one-over mesh payload"),
+ RadrootsTransportError::ResourceLimitExceeded {
+ field: "mesh_frame_cbor_bytes",
+ max: RADROOTS_TRANSPORT_RETICULUM_PAYLOAD_MAX_BYTES,
+ actual: RADROOTS_TRANSPORT_RETICULUM_PAYLOAD_MAX_BYTES + 1,
+ }
+ );
+ assert_eq!(
+ RadrootsTransportPayload::mesh_frame_cbor(
+ "a".repeat(RADROOTS_TRANSPORT_IDENTIFIER_MAX_BYTES + 1),
+ [1],
+ )
+ .expect_err("one-over payload id"),
+ RadrootsTransportError::ResourceLimitExceeded {
+ field: "payload_id",
+ max: RADROOTS_TRANSPORT_IDENTIFIER_MAX_BYTES,
+ actual: RADROOTS_TRANSPORT_IDENTIFIER_MAX_BYTES + 1,
+ }
+ );
+
+ RadrootsTransportPayload::opaque_bytes(
+ "a".repeat(RADROOTS_TRANSPORT_IDENTIFIER_MAX_BYTES),
+ vec![0; RADROOTS_TRANSPORT_OPAQUE_PAYLOAD_MAX_BYTES],
+ )
+ .expect("exact opaque payload");
+ assert_eq!(
+ RadrootsTransportPayload::opaque_bytes(
+ "a".repeat(RADROOTS_TRANSPORT_IDENTIFIER_MAX_BYTES + 1),
+ [1],
+ )
+ .expect_err("one-over payload label"),
+ RadrootsTransportError::ResourceLimitExceeded {
+ field: "payload_label",
+ max: RADROOTS_TRANSPORT_IDENTIFIER_MAX_BYTES,
+ actual: RADROOTS_TRANSPORT_IDENTIFIER_MAX_BYTES + 1,
+ }
+ );
+ assert_eq!(
+ RadrootsTransportPayload::opaque_bytes(
+ "label",
+ vec![0; RADROOTS_TRANSPORT_OPAQUE_PAYLOAD_MAX_BYTES + 1],
+ )
+ .expect_err("one-over opaque payload"),
+ RadrootsTransportError::ResourceLimitExceeded {
+ field: "opaque_payload_bytes",
+ max: RADROOTS_TRANSPORT_OPAQUE_PAYLOAD_MAX_BYTES,
+ actual: RADROOTS_TRANSPORT_OPAQUE_PAYLOAD_MAX_BYTES + 1,
+ }
+ );
+
+ let target_set = RadrootsTransportTargetSet::new(vec![
+ RadrootsTransportTarget::local("local:bounded-request").expect("request target"),
+ ])
+ .expect("request target set");
+ let payload = RadrootsTransportPayload::opaque_bytes("bounded", [1]).expect("request payload");
+ RadrootsTransportDeliveryRequest::new(
+ "a".repeat(RADROOTS_TRANSPORT_DELIVERY_REQUEST_ID_MAX_BYTES),
+ payload.clone(),
+ target_set.clone(),
+ RadrootsTransportSatisfactionPolicy::no_wait(),
+ )
+ .expect("exact request id");
+ assert_eq!(
+ RadrootsTransportDeliveryRequest::new(
+ "a".repeat(RADROOTS_TRANSPORT_DELIVERY_REQUEST_ID_MAX_BYTES + 1),
+ payload,
+ target_set,
+ RadrootsTransportSatisfactionPolicy::no_wait(),
+ )
+ .expect_err("one-over request id"),
+ RadrootsTransportError::ResourceLimitExceeded {
+ field: "delivery_request_id",
+ max: RADROOTS_TRANSPORT_DELIVERY_REQUEST_ID_MAX_BYTES,
+ actual: RADROOTS_TRANSPORT_DELIVERY_REQUEST_ID_MAX_BYTES + 1,
+ }
+ );
+}
+
+#[test]
fn every_transport_error_has_a_stable_display_message() {
let remaining = [
RadrootsTransportError::RequiredTargetNotRequested,
@@ -2087,6 +2321,11 @@ fn every_transport_error_has_a_stable_display_message() {
RadrootsTransportError::InvalidPayloadBytes,
RadrootsTransportError::InvalidPayloadDigest,
RadrootsTransportError::PayloadDigestMismatch,
+ RadrootsTransportError::ResourceLimitExceeded {
+ field: "fixture",
+ max: 1,
+ actual: 2,
+ },
];
for error in remaining {
assert!(!error.to_string().is_empty());
diff --git a/crates/transport_nostr/src/outbox.rs b/crates/transport_nostr/src/outbox.rs
@@ -835,6 +835,7 @@ fn transport_error_to_relay_error(error: RadrootsTransportError) -> RadrootsRela
}
RadrootsTransportError::EmptyTargetUri
| RadrootsTransportError::InvalidTargetUri
+ | RadrootsTransportError::ResourceLimitExceeded { .. }
| RadrootsTransportError::EmptyTargetSet
| RadrootsTransportError::DuplicateTargetFingerprint
| RadrootsTransportError::InvalidTargetFingerprint
@@ -1528,6 +1529,14 @@ mod tests {
RadrootsRelayTransportError::TransportContract(_)
));
}
+ assert!(matches!(
+ transport_error_to_relay_error(RadrootsTransportError::ResourceLimitExceeded {
+ field: "fixture",
+ max: 1,
+ actual: 2,
+ }),
+ RadrootsRelayTransportError::TransportContract(_)
+ ));
let payload_errors = [
RadrootsTransportError::EmptyPayloadId,
RadrootsTransportError::InvalidPayloadId,