use nostr_sdk::prelude::{Kind, PublicKey, Tag};
use crate::core::constants::CTXVM_MESSAGES_KIND;
use crate::core::error::Result;
use crate::core::types::{JsonRpcMessage, JsonRpcNotification};
use crate::transport::base::BaseTransport;
use super::frame::OversizedFrame;
const SIZING_PROBE_TOKEN: &str = "00000000-0000-0000-0000-000000000000";
const SIZING_PROBE_PROGRESS: u64 = 2;
pub async fn measure_published_event_size(
frame: &JsonRpcNotification,
base: &BaseTransport,
recipient: &PublicKey,
tags: &[Tag],
is_encrypted: bool,
gift_wrap_kind: Kind,
) -> Result<usize> {
let message = JsonRpcMessage::Notification(frame.clone());
let (_event_id, published) = base
.prepare_mcp_message(
&message,
recipient,
CTXVM_MESSAGES_KIND,
tags.to_vec(),
Some(is_encrypted),
Some(gift_wrap_kind.as_u16()),
)
.await?;
Ok(serde_json::to_string(&published)?.len())
}
pub async fn resolve_safe_chunk_size(
desired: usize,
base: &BaseTransport,
recipient: &PublicKey,
tags: &[Tag],
is_encrypted: bool,
gift_wrap_kind: Kind,
max_event_bytes: usize,
) -> Result<usize> {
let mut low: usize = 1;
let mut high: usize = desired.max(1);
let mut best: usize = 1;
while low <= high {
let mid = low + (high - low) / 2;
let probe = OversizedFrame::Chunk {
data: "\\".repeat(mid),
}
.into_progress_notification(SIZING_PROBE_TOKEN, SIZING_PROBE_PROGRESS, None)?;
let fits = match measure_published_event_size(
&probe,
base,
recipient,
tags,
is_encrypted,
gift_wrap_kind,
)
.await
{
Ok(size) => size <= max_event_bytes,
Err(_) => false,
};
if fits {
best = mid;
low = mid + 1;
} else {
high = mid - 1;
}
}
Ok(best)
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use nostr_sdk::prelude::Keys;
use crate::core::types::EncryptionMode;
use crate::relay::mock::MockRelayPool;
use crate::relay::RelayPoolTrait;
fn test_base(mode: EncryptionMode) -> BaseTransport {
BaseTransport {
relay_pool: Arc::new(MockRelayPool::new()) as Arc<dyn RelayPoolTrait>,
encryption_mode: mode,
is_connected: true,
}
}
fn chunk_frame(data_len: usize) -> JsonRpcNotification {
OversizedFrame::Chunk {
data: "x".repeat(data_len),
}
.into_progress_notification("tok", 2, None)
.unwrap()
}
#[tokio::test]
async fn measure_grows_with_data_length() {
let base = test_base(EncryptionMode::Disabled);
let recipient = Keys::generate().public_key();
let tags = BaseTransport::create_recipient_tags(&recipient);
let small = measure_published_event_size(
&chunk_frame(10),
&base,
&recipient,
&tags,
false,
Kind::Custom(crate::core::constants::GIFT_WRAP_KIND),
)
.await
.unwrap();
let large = measure_published_event_size(
&chunk_frame(1000),
&base,
&recipient,
&tags,
false,
Kind::Custom(crate::core::constants::GIFT_WRAP_KIND),
)
.await
.unwrap();
assert!(large > small, "larger data must yield a larger event");
assert!(small > 0);
}
#[tokio::test]
async fn resolve_never_exceeds_desired_and_is_at_least_one() {
let base = test_base(EncryptionMode::Disabled);
let recipient = Keys::generate().public_key();
let tags = BaseTransport::create_recipient_tags(&recipient);
let clamped = resolve_safe_chunk_size(
5000,
&base,
&recipient,
&tags,
false,
Kind::Custom(crate::core::constants::GIFT_WRAP_KIND),
10,
)
.await
.unwrap();
assert_eq!(clamped, 1, "an unsatisfiable ceiling must still return 1");
let capped = resolve_safe_chunk_size(
8,
&base,
&recipient,
&tags,
false,
Kind::Custom(crate::core::constants::GIFT_WRAP_KIND),
10_000_000,
)
.await
.unwrap();
assert_eq!(capped, 8, "a generous ceiling must cap at `desired`");
}
#[tokio::test]
async fn resolve_is_monotonic_in_budget() {
let base = test_base(EncryptionMode::Disabled);
let recipient = Keys::generate().public_key();
let tags = BaseTransport::create_recipient_tags(&recipient);
let gw = Kind::Custom(crate::core::constants::GIFT_WRAP_KIND);
let small_budget =
resolve_safe_chunk_size(48_000, &base, &recipient, &tags, false, gw, 4_000)
.await
.unwrap();
let large_budget =
resolve_safe_chunk_size(48_000, &base, &recipient, &tags, false, gw, 16_000)
.await
.unwrap();
assert!(
large_budget >= small_budget,
"a larger byte ceiling must allow an equal-or-larger chunk ({large_budget} >= {small_budget})"
);
}
#[tokio::test]
async fn encrypted_frames_force_smaller_chunks_than_plaintext() {
let recipient = Keys::generate().public_key();
let tags = BaseTransport::create_recipient_tags(&recipient);
let gw = Kind::Custom(crate::core::constants::GIFT_WRAP_KIND);
let plain = resolve_safe_chunk_size(
48_000,
&test_base(EncryptionMode::Disabled),
&recipient,
&tags,
false,
gw,
8_000,
)
.await
.unwrap();
let encrypted = resolve_safe_chunk_size(
48_000,
&test_base(EncryptionMode::Required),
&recipient,
&tags,
true,
gw,
8_000,
)
.await
.unwrap();
assert!(
encrypted < plain,
"gift-wrap expansion must shrink the safe chunk ({encrypted} < {plain})"
);
}
}