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
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "saorsa-node"
version = "0.3.1"
version = "0.3.2"
edition = "2021"
authors = ["David Irvine <david.irvine@maidsafe.net>"]
description = "Pure quantum-proof network node for the Saorsa decentralized network"
Expand Down Expand Up @@ -79,7 +79,7 @@ flate2 = "1"
tar = "0.4"

# Protocol serialization
bincode = "1"
postcard = { version = "1.1.3", features = ["use-std"] }

[dev-dependencies]
tokio-test = "0.4"
Expand Down
65 changes: 44 additions & 21 deletions src/ant_protocol/chunk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@
//! is the SHA256 hash of the content. Maximum size is 4MB.
//!
//! This module defines the wire protocol messages for chunk operations
//! using bincode serialization for compact, fast encoding.
//! using postcard serialization for compact, fast encoding.

use bincode::Options;
use serde::{Deserialize, Serialize};

/// Protocol identifier for chunk operations.
Expand All @@ -18,13 +17,12 @@ pub const PROTOCOL_VERSION: u16 = 1;
/// Maximum chunk size in bytes (4MB).
pub const MAX_CHUNK_SIZE: usize = 4 * 1024 * 1024;

/// Maximum wire message size for deserialization.
/// Maximum wire message size in bytes (5MB).
///
/// Set to `MAX_CHUNK_SIZE` + 1 MB headroom for the envelope (`request_id`,
/// enum discriminants, address, payment proof, etc.). This prevents a
/// malicious peer from sending a length-prefixed `Vec` that causes an
/// unbounded allocation.
const MAX_WIRE_MESSAGE_SIZE: u64 = (MAX_CHUNK_SIZE + 1024 * 1024) as u64;
/// Limits the input buffer accepted by [`ChunkMessage::decode`] to prevent
/// unbounded allocation from malicious or corrupted payloads. Set slightly
/// above [`MAX_CHUNK_SIZE`] to accommodate message envelope overhead.
pub const MAX_WIRE_MESSAGE_SIZE: usize = 5 * 1024 * 1024;

/// Data type identifier for chunks.
pub const DATA_TYPE_CHUNK: u32 = 0;
Expand Down Expand Up @@ -66,30 +64,33 @@ pub struct ChunkMessage {
}

impl ChunkMessage {
/// Encode the message to bytes using bincode.
/// Encode the message to bytes using postcard.
///
/// # Errors
///
/// Returns an error if serialization fails.
pub fn encode(&self) -> Result<Vec<u8>, ProtocolError> {
bincode::DefaultOptions::new()
.with_limit(MAX_WIRE_MESSAGE_SIZE)
.allow_trailing_bytes()
.serialize(self)
.map_err(|e| ProtocolError::SerializationFailed(e.to_string()))
postcard::to_stdvec(self).map_err(|e| ProtocolError::SerializationFailed(e.to_string()))
}

/// Decode a message from bytes using bincode.
/// Decode a message from bytes using postcard.
///
/// Rejects payloads larger than [`MAX_WIRE_MESSAGE_SIZE`] before
/// attempting deserialization.
///
/// # Errors
///
/// Returns an error if deserialization fails.
/// Returns [`ProtocolError::MessageTooLarge`] if the input exceeds the
/// size limit, or [`ProtocolError::DeserializationFailed`] if postcard
/// cannot parse the data.
pub fn decode(data: &[u8]) -> Result<Self, ProtocolError> {
bincode::DefaultOptions::new()
.with_limit(MAX_WIRE_MESSAGE_SIZE)
.allow_trailing_bytes()
.deserialize(data)
.map_err(|e| ProtocolError::DeserializationFailed(e.to_string()))
if data.len() > MAX_WIRE_MESSAGE_SIZE {
return Err(ProtocolError::MessageTooLarge {
size: data.len(),
max_size: MAX_WIRE_MESSAGE_SIZE,
});
}
postcard::from_bytes(data).map_err(|e| ProtocolError::DeserializationFailed(e.to_string()))
}
}

Expand Down Expand Up @@ -241,6 +242,13 @@ pub enum ProtocolError {
SerializationFailed(String),
/// Message deserialization failed.
DeserializationFailed(String),
/// Wire message exceeds the maximum allowed size.
MessageTooLarge {
/// Actual size of the message in bytes.
size: usize,
/// Maximum allowed size.
max_size: usize,
},
/// Chunk exceeds maximum size.
ChunkTooLarge {
/// Size of the chunk in bytes.
Expand Down Expand Up @@ -270,6 +278,9 @@ impl std::fmt::Display for ProtocolError {
match self {
Self::SerializationFailed(msg) => write!(f, "serialization failed: {msg}"),
Self::DeserializationFailed(msg) => write!(f, "deserialization failed: {msg}"),
Self::MessageTooLarge { size, max_size } => {
write!(f, "message size {size} exceeds maximum {max_size}")
}
Self::ChunkTooLarge { size, max_size } => {
write!(f, "chunk size {size} exceeds maximum {max_size}")
}
Expand Down Expand Up @@ -434,6 +445,18 @@ mod tests {
assert!(display.contains("address mismatch"));
}

#[test]
fn test_decode_rejects_oversized_payload() {
let oversized = vec![0u8; MAX_WIRE_MESSAGE_SIZE + 1];
let result = ChunkMessage::decode(&oversized);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
matches!(err, ProtocolError::MessageTooLarge { .. }),
"expected MessageTooLarge, got {err:?}"
);
}

#[test]
fn test_invalid_decode() {
let invalid_data = vec![0xFF, 0xFF, 0xFF];
Expand Down
2 changes: 1 addition & 1 deletion src/ant_protocol/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
//!
//! # Protocol Overview
//!
//! The protocol uses bincode serialization for compact, fast encoding.
//! The protocol uses postcard serialization for compact, fast encoding.
//! Each data type has its own message types for PUT/GET operations.
//!
//! ## Chunk Messages
Expand Down
8 changes: 6 additions & 2 deletions src/client/chunk_protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
use crate::ant_protocol::{ChunkMessage, ChunkMessageBody, CHUNK_PROTOCOL_ID};
use saorsa_core::{P2PEvent, P2PNode};
use std::time::Duration;
use tokio::sync::broadcast::error::RecvError;
use tokio::time::Instant;
use tracing::warn;
use tracing::{debug, warn};

/// Send a chunk-protocol message to `target_peer` and await a matching response.
///
Expand Down Expand Up @@ -70,7 +71,10 @@ pub async fn send_and_await_chunk_response<T, E>(
}
}
Ok(Ok(_)) => {}
Ok(Err(_)) | Err(_) => break,
Ok(Err(RecvError::Lagged(skipped))) => {
debug!("Chunk protocol events lagged by {skipped} messages, continuing");
}
Ok(Err(RecvError::Closed)) | Err(_) => break,
}
}

Expand Down
41 changes: 34 additions & 7 deletions src/client/quantum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ use saorsa_core::P2PNode;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tracing::{debug, info};
use tracing::{debug, info, warn};

/// Default timeout for network operations in seconds.
const DEFAULT_TIMEOUT_SECS: u64 = 30;
Expand Down Expand Up @@ -154,12 +154,39 @@ impl QuantumClient {
address: addr,
content,
}) => {
debug!(
"Found chunk {} on saorsa network ({} bytes)",
hex::encode(addr),
content.len()
);
Some(Ok(Some(DataChunk::new(addr, Bytes::from(content)))))
if addr == *address {
let computed = crate::client::compute_address(&content);
if computed == addr {
debug!(
"Found chunk {} on saorsa network ({} bytes)",
hex::encode(addr),
content.len()
);
Some(Ok(Some(DataChunk::new(addr, Bytes::from(content)))))
} else {
warn!(
"Peer returned chunk {} with invalid content hash {}",
addr_hex,
hex::encode(computed)
);
Some(Err(Error::InvalidChunk(format!(
"Invalid chunk content: expected hash {}, got {}",
addr_hex,
hex::encode(computed)
))))
}
} else {
warn!(
"Peer returned chunk {} but we requested {}",
hex::encode(addr),
addr_hex
);
Some(Err(Error::InvalidChunk(format!(
"Mismatched chunk address: expected {}, got {}",
addr_hex,
hex::encode(addr)
))))
}
}
ChunkMessageBody::GetResponse(ChunkGetResponse::NotFound { .. }) => {
debug!("Chunk {} not found on saorsa network", addr_hex);
Expand Down
Loading
Loading