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:
@@ -1079,4 +1079,96 @@ mod tests {
|
|||||||
"send_message before join must return an error"
|
"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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -375,4 +375,63 @@ mod tests {
|
|||||||
let result = MeshEnvelope::from_wire(&garbage);
|
let result = MeshEnvelope::from_wire(&garbage);
|
||||||
assert!(result.is_err(), "garbage input must return Err, not panic");
|
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)");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user