test: add MLS and MeshEnvelope size measurement tests

- measure_mls_wire_sizes: KeyPackage, Welcome, Commit, AppMessage sizes
- measure_mls_wire_sizes_hybrid: same with post-quantum mode
- measure_mesh_envelope_overhead: MeshEnvelope overhead for various payloads

These tests print actual byte sizes to inform constrained link
feasibility planning (LoRa SF12, MLS-Lite design).
This commit is contained in:
2026-03-30 23:45:07 +02:00
parent db49d83fda
commit 3f81837112
2 changed files with 151 additions and 0 deletions

View File

@@ -1079,4 +1079,96 @@ mod tests {
"send_message before join must return an error"
);
}
/// Measure actual MLS artifact sizes for mesh planning.
/// These numbers inform the MLS-Lite design and constrained link feasibility.
#[test]
fn measure_mls_wire_sizes() {
let creator_id = Arc::new(IdentityKeypair::generate());
let joiner_id = Arc::new(IdentityKeypair::generate());
let mut creator = GroupMember::new(Arc::clone(&creator_id));
let mut joiner = GroupMember::new(Arc::clone(&joiner_id));
// 1. KeyPackage size
let kp_bytes = joiner.generate_key_package().expect("generate KP");
println!("=== MLS Wire Format Sizes ===");
println!("KeyPackage: {} bytes", kp_bytes.len());
// 2. Create group (no wire message, just local state)
creator.create_group(b"size-test").expect("create group");
// 3. Add member -> Commit + Welcome
let (commit_bytes, welcome_bytes) = creator.add_member(&kp_bytes).expect("add member");
println!("Commit (add): {} bytes", commit_bytes.len());
println!("Welcome: {} bytes", welcome_bytes.len());
// Join the group
joiner.join_group(&welcome_bytes).expect("join");
// 4. Application message (short payload)
let short_msg = creator.send_message(b"hello").expect("short msg");
println!("AppMessage (5B): {} bytes", short_msg.len());
// 5. Application message (medium payload ~100 bytes)
let medium_payload = vec![0x42u8; 100];
let medium_msg = creator.send_message(&medium_payload).expect("medium msg");
println!("AppMessage (100B): {} bytes", medium_msg.len());
// 6. Self-update proposal
let update_proposal = creator.propose_self_update().expect("update proposal");
println!("UpdateProposal: {} bytes", update_proposal.len());
// Joiner processes the proposal
joiner.receive_message(&update_proposal).expect("recv proposal");
// 7. Commit (update only, no welcome)
let (update_commit, _) = joiner.commit_pending_proposals().expect("commit update");
println!("Commit (update): {} bytes", update_commit.len());
// Summary for LoRa feasibility
println!("\n=== LoRa Feasibility (SF12/BW125, MTU=51 bytes) ===");
println!("KeyPackage: {} fragments ({:.0}s at 1% duty)",
(kp_bytes.len() + 50) / 51,
(kp_bytes.len() as f64 / 51.0).ceil() * 36.0 / 60.0);
println!("Welcome: {} fragments ({:.0}s at 1% duty)",
(welcome_bytes.len() + 50) / 51,
(welcome_bytes.len() as f64 / 51.0).ceil() * 36.0 / 60.0);
println!("AppMessage (5B): {} fragments",
(short_msg.len() + 50) / 51);
// Assertions to catch regressions / validate estimates
assert!(kp_bytes.len() < 1000, "KeyPackage should be under 1KB");
assert!(welcome_bytes.len() < 3000, "Welcome should be under 3KB");
assert!(short_msg.len() < 300, "Short AppMessage should be under 300B");
}
/// Measure MLS sizes with hybrid (post-quantum) mode enabled.
#[test]
fn measure_mls_wire_sizes_hybrid() {
let creator_id = Arc::new(IdentityKeypair::generate());
let joiner_id = Arc::new(IdentityKeypair::generate());
let mut creator = GroupMember::new_hybrid(Arc::clone(&creator_id));
let mut joiner = GroupMember::new_hybrid(Arc::clone(&joiner_id));
// KeyPackage with hybrid (X25519 + ML-KEM-768) init key
let kp_bytes = joiner.generate_key_package().expect("generate hybrid KP");
println!("=== MLS Wire Format Sizes (Hybrid PQ Mode) ===");
println!("KeyPackage (PQ): {} bytes", kp_bytes.len());
creator.create_group(b"hybrid-size-test").expect("create group");
let (commit_bytes, welcome_bytes) = creator.add_member(&kp_bytes).expect("add member");
println!("Commit (add, PQ): {} bytes", commit_bytes.len());
println!("Welcome (PQ): {} bytes", welcome_bytes.len());
joiner.join_group(&welcome_bytes).expect("join");
let short_msg = creator.send_message(b"hello").expect("short msg");
println!("AppMessage (PQ): {} bytes", short_msg.len());
// PQ KeyPackages are larger due to ML-KEM-768 public key (1184 bytes)
assert!(kp_bytes.len() > 1000, "Hybrid KeyPackage should be >1KB due to ML-KEM");
assert!(kp_bytes.len() < 3000, "Hybrid KeyPackage should be <3KB");
}
}

View File

@@ -375,4 +375,63 @@ mod tests {
let result = MeshEnvelope::from_wire(&garbage);
assert!(result.is_err(), "garbage input must return Err, not panic");
}
/// Measure MeshEnvelope overhead for various payload sizes.
/// This informs constrained link feasibility planning.
#[test]
fn measure_mesh_envelope_overhead() {
let id = test_identity();
let recipient = [0xAAu8; 32];
println!("=== MeshEnvelope Wire Overhead (CBOR) ===");
// Empty payload
let env_empty = MeshEnvelope::new(&id, &recipient, vec![], 3600, 5);
let wire_empty = env_empty.to_wire();
println!("Payload 0B: wire {} bytes (overhead: {} bytes)", wire_empty.len(), wire_empty.len());
let base_overhead = wire_empty.len();
// 1-byte payload
let env_1 = MeshEnvelope::new(&id, &recipient, vec![0x42], 3600, 5);
let wire_1 = env_1.to_wire();
println!("Payload 1B: wire {} bytes (overhead: {} bytes)", wire_1.len(), wire_1.len() - 1);
// 10-byte payload ("hello mesh")
let env_10 = MeshEnvelope::new(&id, &recipient, b"hello mesh".to_vec(), 3600, 5);
let wire_10 = env_10.to_wire();
println!("Payload 10B: wire {} bytes (overhead: {} bytes)", wire_10.len(), wire_10.len() - 10);
// 50-byte payload
let env_50 = MeshEnvelope::new(&id, &recipient, vec![0x42; 50], 3600, 5);
let wire_50 = env_50.to_wire();
println!("Payload 50B: wire {} bytes (overhead: {} bytes)", wire_50.len(), wire_50.len() - 50);
// 100-byte payload (typical short message)
let env_100 = MeshEnvelope::new(&id, &recipient, vec![0x42; 100], 3600, 5);
let wire_100 = env_100.to_wire();
println!("Payload 100B: wire {} bytes (overhead: {} bytes)", wire_100.len(), wire_100.len() - 100);
// Broadcast (empty recipient) - saves 32 bytes
let env_bc = MeshEnvelope::new(&id, &[], b"broadcast".to_vec(), 3600, 5);
let wire_bc = env_bc.to_wire();
println!("Broadcast 9B: wire {} bytes (no recipient)", wire_bc.len());
println!("\n=== LoRa Feasibility (SF12/BW125, MTU=51 bytes) ===");
println!("Empty envelope: {} fragments", (wire_empty.len() + 50) / 51);
println!("10B payload: {} fragments", (wire_10.len() + 50) / 51);
println!("100B payload: {} fragments", (wire_100.len() + 50) / 51);
// Baseline overhead is fixed fields:
// - id: 32 bytes
// - sender_key: 32 bytes
// - recipient_key: 32 bytes (or 0 for broadcast)
// - signature: 64 bytes
// - ttl_secs: 4 bytes
// - hop_count: 1 byte
// - max_hops: 1 byte
// - timestamp: 8 bytes
// Total fixed: ~174 bytes raw, CBOR adds ~5-10% overhead
assert!(base_overhead < 200, "Base overhead should be under 200 bytes");
assert!(base_overhead > 150, "Base overhead should be over 150 bytes (sanity check)");
}
}