Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/gateway/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ mod tests {
bootstrap_relay_urls: None,
publish_relay_list: true,
profile_metadata: None,
oversized_transfer: Default::default(),
};

let config = GatewayConfig { nostr_config };
Expand Down
1 change: 1 addition & 0 deletions src/proxy/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ mod tests {
timeout: Duration::from_secs(60),
discovery_relay_urls: None,
fallback_operational_relay_urls: None,
oversized_transfer: Default::default(),
};

let config = ProxyConfig { nostr_config };
Expand Down
62 changes: 62 additions & 0 deletions src/transport/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ use crate::encryption;
use crate::relay::{RelayPool, RelayPoolTrait};
use crate::transport::base::BaseTransport;
use crate::transport::discovery_tags::{parse_discovered_peer_capabilities, PeerCapabilities};
use crate::transport::oversized_transfer::OversizedTransferConfig;

const LOG_TARGET: &str = "contextvm_sdk::transport::client";

Expand Down Expand Up @@ -59,6 +60,8 @@ pub struct NostrClientTransportConfig {
pub discovery_relay_urls: Option<Vec<String>>,
/// Non-authoritative operational relays probed in parallel with CEP-17 discovery.
pub fallback_operational_relay_urls: Option<Vec<String>>,
/// CEP-22 oversized payload transfer configuration. Disabled by default.
pub oversized_transfer: OversizedTransferConfig,
}

impl Default for NostrClientTransportConfig {
Expand All @@ -72,6 +75,7 @@ impl Default for NostrClientTransportConfig {
timeout: Duration::from_secs(30),
discovery_relay_urls: None,
fallback_operational_relay_urls: None,
oversized_transfer: OversizedTransferConfig::default(),
}
}
}
Expand Down Expand Up @@ -117,6 +121,16 @@ impl NostrClientTransportConfig {
self.fallback_operational_relay_urls = Some(urls);
self
}
/// Set the full CEP-22 oversized payload transfer configuration.
pub fn with_oversized_transfer(mut self, config: OversizedTransferConfig) -> Self {
self.oversized_transfer = config;
self
}
/// Enable or disable CEP-22 oversized payload transfer, leaving other knobs at default.
pub fn with_oversized_enabled(mut self, enabled: bool) -> Self {
self.oversized_transfer.enabled = enabled;
self
}
}

/// Client-side Nostr transport for sending MCP requests and receiving responses.
Expand Down Expand Up @@ -599,6 +613,13 @@ impl NostrClientTransport {
));
}
}
// CEP-22: advertise oversized-transfer support when enabled.
if self.config.oversized_transfer.enabled {
tags.push(Tag::custom(
TagKind::Custom(tags::SUPPORT_OVERSIZED_TRANSFER.into()),
Vec::<String>::new(),
));
}
tags
}

Expand Down Expand Up @@ -1220,6 +1241,47 @@ mod tests {
assert_eq!(names, vec!["support_encryption"]);
}

#[test]
fn client_capability_tags_oversized_disabled_by_default() {
let t = make_transport_for_tags(EncryptionMode::Optional, GiftWrapMode::Optional);
assert!(!t.config.oversized_transfer.enabled);
let names = tag_names(&t.get_client_capability_tags());
assert!(
!names.contains(&"support_oversized_transfer".to_string()),
"oversized tag must not be advertised when disabled"
);
}

#[test]
fn client_capability_tags_oversized_enabled() {
let mut t = make_transport_for_tags(EncryptionMode::Optional, GiftWrapMode::Optional);
t.config.oversized_transfer.enabled = true;
let names = tag_names(&t.get_client_capability_tags());
assert!(
names.contains(&"support_oversized_transfer".to_string()),
"oversized tag must be advertised when enabled"
);
}

#[test]
fn client_capability_tags_oversized_enabled_without_encryption() {
// Tag is emitted independently of the encryption capability tags.
let mut t = make_transport_for_tags(EncryptionMode::Disabled, GiftWrapMode::Optional);
t.config.oversized_transfer.enabled = true;
let names = tag_names(&t.get_client_capability_tags());
assert_eq!(names, vec!["support_oversized_transfer"]);
}

#[test]
fn client_config_oversized_builders() {
let cfg = NostrClientTransportConfig::default().with_oversized_enabled(true);
assert!(cfg.oversized_transfer.enabled);
let cfg = NostrClientTransportConfig::default()
.with_oversized_transfer(OversizedTransferConfig::enabled().with_chunk_size(1024));
assert!(cfg.oversized_transfer.enabled);
assert_eq!(cfg.oversized_transfer.chunk_size, 1024);
}

#[test]
fn client_discovery_tags_sent_once() {
let t = make_transport_for_tags(EncryptionMode::Optional, GiftWrapMode::Optional);
Expand Down
6 changes: 3 additions & 3 deletions src/transport/oversized_transfer/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ pub const DEFAULT_MAX_CONCURRENT_TRANSFERS: usize = 64;

/// Default hard timeout for an in-flight transfer (milliseconds).
///
/// Not enforced by the PR-1 pure engine (no timers yet); wired into the
/// transport watchdog in a later PR.
/// Not enforced by the pure engine (no timers yet); wired into the
/// transport watchdog once transport integration lands.
pub const DEFAULT_TRANSFER_TIMEOUT_MS: u64 = 5 * 60 * 1000;

/// Default maximum forward gap between the next expected chunk and an
Expand All @@ -46,7 +46,7 @@ pub const DEFAULT_MAX_OUT_OF_ORDER_CHUNKS: usize = 42;

/// Default timeout a sender waits for an `accept` frame before giving up (milliseconds).
///
/// Used by the sender handshake in a later PR; defined here for parity.
/// Used by the sender handshake once transport integration lands; defined here for parity.
pub const DEFAULT_ACCEPT_TIMEOUT_MS: u64 = 30_000;

/// Canonical progress slot for the `start` frame.
Expand Down
129 changes: 127 additions & 2 deletions src/transport/oversized_transfer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
//!
//! This module is the **pure engine**: building frames ([`codec`]) and
//! reassembling them ([`receiver`]). It carries no transport, I/O, or timers —
//! those are wired in by the client and server transports in later PRs. Until
//! then the module is intentionally unused by the rest of the crate.
//! those are wired in by the client and server transports once transport
//! integration lands. Until then the module is intentionally unused by the
//! rest of the crate.
//!
//! ```
//! use contextvm_sdk::transport::oversized_transfer::{
Expand Down Expand Up @@ -55,3 +56,127 @@ pub use constants::*;
pub use errors::OversizedTransferError;
pub use frame::{CompletionMode, OversizedFrame};
pub use receiver::{OversizedTransferReceiver, TransferPolicy};

/// CEP-22 oversized-transfer configuration shared by both transports.
///
/// Bundles the capability gate plus the sender/receiver tuning knobs (D6) so the
/// nine numeric defaults don't clutter the flat transport configs. Attached to
/// [`NostrServerTransportConfig`](crate::transport::NostrServerTransportConfig)
/// and [`NostrClientTransportConfig`](crate::transport::NostrClientTransportConfig)
/// via their `with_oversized_transfer` / `with_oversized_enabled` builders.
///
/// **Disabled by default** — until a peer opts in, no `support_oversized_transfer`
/// capability is advertised and the server never learns or activates the feature.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct OversizedTransferConfig {
/// Master gate. When `false` (default) the capability is neither advertised
/// nor activated, and the server does not learn a client's flag.
pub enabled: bool,
/// Serialized byte length at or above which the sender switches to oversized transfer.
pub threshold: usize,
/// Per-chunk data size (bytes).
pub chunk_size: usize,
/// Upper bound on the total reassembled payload a receiver will accept (bytes).
pub max_transfer_bytes: u64,
/// Upper bound on the number of chunks a receiver will accept.
pub max_transfer_chunks: u64,
/// Upper bound on concurrently active receiver-side transfers.
pub max_concurrent_transfers: usize,
/// Hard timeout for an in-flight transfer (milliseconds).
pub transfer_timeout_ms: u64,
/// Maximum forward gap between the next expected chunk and an out-of-order
/// chunk that will still be buffered.
pub max_out_of_order_window: u64,
/// Maximum number of buffered out-of-order chunks.
pub max_out_of_order_chunks: usize,
/// Timeout a sender waits for an `accept` frame before giving up (milliseconds).
pub accept_timeout_ms: u64,
}

impl Default for OversizedTransferConfig {
fn default() -> Self {
Self {
enabled: false,
threshold: DEFAULT_OVERSIZED_THRESHOLD,
chunk_size: DEFAULT_CHUNK_SIZE,
max_transfer_bytes: DEFAULT_MAX_TRANSFER_BYTES,
max_transfer_chunks: DEFAULT_MAX_TRANSFER_CHUNKS,
max_concurrent_transfers: DEFAULT_MAX_CONCURRENT_TRANSFERS,
transfer_timeout_ms: DEFAULT_TRANSFER_TIMEOUT_MS,
max_out_of_order_window: DEFAULT_MAX_OUT_OF_ORDER_WINDOW,
max_out_of_order_chunks: DEFAULT_MAX_OUT_OF_ORDER_CHUNKS,
accept_timeout_ms: DEFAULT_ACCEPT_TIMEOUT_MS,
}
}
}

impl OversizedTransferConfig {
/// An explicitly enabled config with all other knobs at their defaults.
pub fn enabled() -> Self {
Self {
enabled: true,
..Self::default()
}
}

/// Enable or disable oversized transfer.
pub fn with_enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}

/// Set the serialized-byte threshold at which the sender fragments.
pub fn with_threshold(mut self, threshold: usize) -> Self {
self.threshold = threshold;
self
}

/// Set the per-chunk data size (bytes).
pub fn with_chunk_size(mut self, chunk_size: usize) -> Self {
self.chunk_size = chunk_size;
self
}

/// Set the upper bound on the total reassembled payload (bytes).
pub fn with_max_transfer_bytes(mut self, max: u64) -> Self {
self.max_transfer_bytes = max;
self
}

/// Set the upper bound on the number of chunks a receiver will accept.
pub fn with_max_transfer_chunks(mut self, max: u64) -> Self {
self.max_transfer_chunks = max;
self
}

/// Set the upper bound on concurrently active receiver-side transfers.
pub fn with_max_concurrent_transfers(mut self, max: usize) -> Self {
self.max_concurrent_transfers = max;
self
}

/// Set the hard per-transfer timeout (milliseconds).
pub fn with_transfer_timeout_ms(mut self, ms: u64) -> Self {
self.transfer_timeout_ms = ms;
self
}

/// Set the maximum forward gap for buffering out-of-order chunks.
pub fn with_max_out_of_order_window(mut self, window: u64) -> Self {
self.max_out_of_order_window = window;
self
}

/// Set the maximum number of buffered out-of-order chunks.
pub fn with_max_out_of_order_chunks(mut self, max: usize) -> Self {
self.max_out_of_order_chunks = max;
self
}

/// Set the sender's `accept`-frame wait timeout (milliseconds).
pub fn with_accept_timeout_ms(mut self, ms: u64) -> Self {
self.accept_timeout_ms = ms;
self
}
}
4 changes: 2 additions & 2 deletions src/transport/oversized_transfer/receiver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
//! and returns the reassembled [`JsonRpcMessage`] once a transfer completes and
//! passes byte-length, SHA-256, and JSON-RPC validation.
//!
//! This PR-1 engine is **pure and synchronous**: it owns no timers. The hard
//! This engine is **pure and synchronous**: it owns no timers. The hard
//! per-transfer watchdog (`transfer_timeout_ms`) and the sender-side
//! accept-waiter are added when the engine is wired into the transport.

Expand Down Expand Up @@ -42,7 +42,7 @@ pub struct TransferPolicy {
/// Maximum number of buffered out-of-order chunks.
pub max_out_of_order_chunks: usize,
/// Hard per-transfer timeout (milliseconds). Reserved for the transport
/// watchdog in a later PR; the pure engine does not enforce it.
/// watchdog; the pure engine does not enforce it.
pub transfer_timeout_ms: u64,
}

Expand Down
23 changes: 14 additions & 9 deletions src/transport/server/announcement_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ pub(crate) struct AnnouncementManager {
gift_wrap_mode: GiftWrapMode,
/// User-provided extra tags (e.g. PMI discovery for CEP-8).
extra_common_tags: Vec<Tag>,
/// Transport-owned internal tags (future CEP-22 oversized support signal).
/// Transport-owned internal tags (e.g. CEP-22 oversized support signal).
internal_common_tags: Vec<Tag>,
/// CEP-8 pricing tags for capability list responses.
pricing_tags: Vec<Tag>,
Expand Down Expand Up @@ -203,8 +203,10 @@ impl AnnouncementManager {
tags
}

/// Build capability tags based on encryption and gift-wrap mode.
pub fn get_capability_tags(&self) -> Vec<Tag> {
/// Build the **encryption** capability tags based on encryption and gift-wrap
/// mode. Returns encryption capability tags only; CEP-22 oversized transfer and
/// other capabilities are contributed separately via `internal_common_tags`.
pub fn get_encryption_capability_tags(&self) -> Vec<Tag> {
let mut tags = Vec::new();
if self.encryption_mode != EncryptionMode::Disabled {
tags.push(Tag::custom(
Expand Down Expand Up @@ -234,7 +236,7 @@ impl AnnouncementManager {
return cached.clone();
}
let mut tags = self.get_server_info_tags();
tags.extend(self.get_capability_tags());
tags.extend(self.get_encryption_capability_tags());
tags.extend(self.extra_common_tags.iter().cloned());
tags.extend(self.internal_common_tags.iter().cloned());
*cache = Some(tags.clone());
Expand Down Expand Up @@ -277,7 +279,6 @@ impl AnnouncementManager {
/// Set transport-owned internal common tags (e.g. CEP-22 oversized support).
///
/// Invalidates the common tags cache.
#[allow(dead_code)] // API reserved for CEP-22 oversized transfer integration
pub fn set_internal_common_tags(&mut self, tags: Vec<Tag>) {
self.internal_common_tags = tags;
*self
Expand Down Expand Up @@ -645,6 +646,7 @@ impl AnnouncementManager {
CommonTagsSnapshot {
server_info: self.server_info.clone(),
extra_common_tags: self.extra_common_tags.clone(),
internal_common_tags: self.internal_common_tags.clone(),
encryption_mode: self.encryption_mode,
gift_wrap_mode: self.gift_wrap_mode,
}
Expand Down Expand Up @@ -882,6 +884,8 @@ pub(crate) struct CommonTagsSnapshot {
pub server_info: Option<ServerInfo>,
/// User-provided extra common tags.
pub extra_common_tags: Vec<Tag>,
/// Transport-owned internal common tags (e.g. CEP-22 oversized support).
pub internal_common_tags: Vec<Tag>,
/// Encryption mode for capability tag decisions.
pub encryption_mode: EncryptionMode,
/// Gift-wrap mode for ephemeral tag decisions.
Expand Down Expand Up @@ -915,6 +919,7 @@ impl CommonTagsSnapshot {
}
}
tags.extend(self.extra_common_tags.iter().cloned());
tags.extend(self.internal_common_tags.iter().cloned());
}
}

Expand Down Expand Up @@ -987,23 +992,23 @@ mod tests {
#[test]
fn capability_tags_encryption_enabled() {
let mgr = make_manager(EncryptionMode::Optional, GiftWrapMode::Persistent, None);
let tags = mgr.get_capability_tags();
let tags = mgr.get_encryption_capability_tags();
let names: Vec<String> = tags.iter().map(tag_name).collect();
assert!(names.contains(&tags::SUPPORT_ENCRYPTION.to_string()));
}

#[test]
fn capability_tags_ephemeral_enabled() {
let mgr = make_manager(EncryptionMode::Optional, GiftWrapMode::Optional, None);
let tags = mgr.get_capability_tags();
let tags = mgr.get_encryption_capability_tags();
let names: Vec<String> = tags.iter().map(tag_name).collect();
assert!(names.contains(&tags::SUPPORT_ENCRYPTION_EPHEMERAL.to_string()));
}

#[test]
fn capability_tags_ephemeral_excluded() {
let mgr = make_manager(EncryptionMode::Optional, GiftWrapMode::Persistent, None);
let tags = mgr.get_capability_tags();
let tags = mgr.get_encryption_capability_tags();
let names: Vec<String> = tags.iter().map(tag_name).collect();
assert!(
!names.contains(&tags::SUPPORT_ENCRYPTION_EPHEMERAL.to_string()),
Expand All @@ -1014,7 +1019,7 @@ mod tests {
#[test]
fn capability_tags_encryption_disabled() {
let mgr = make_manager(EncryptionMode::Disabled, GiftWrapMode::Optional, None);
let tags = mgr.get_capability_tags();
let tags = mgr.get_encryption_capability_tags();
assert!(
tags.is_empty(),
"Disabled encryption should produce no capability tags"
Expand Down
Loading
Loading