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
6 changes: 4 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ heed = "0.22"
# Migration: Decrypt ant-node data (read-only)
aes-gcm-siv = "0.11"
hkdf = "0.12"
sha2 = "0.10"

# Hashing (aligned with saorsa-core)
blake3 = "1"

# Async runtime
tokio = { version = "1.35", features = ["full", "signal"] }
Expand Down Expand Up @@ -176,4 +178,4 @@ assets = [
]

[package.metadata.generate-rpm.requires]
# Runtime dependencies auto-detected
# Runtime dependencies auto-detected
Binary file added saorsa-transport-overview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions src/ant_protocol/chunk.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Chunk message types for the ANT protocol.
//!
//! Chunks are immutable, content-addressed data blocks where the address
//! is the SHA256 hash of the content. Maximum size is 4MB.
//! is the BLAKE3 hash of the content. Maximum size is 4MB.
//!
//! This module defines the wire protocol messages for chunk operations
//! using postcard serialization for compact, fast encoding.
Expand Down Expand Up @@ -101,7 +101,7 @@ impl ChunkMessage {
/// Request to store a chunk.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChunkPutRequest {
/// The content-addressed identifier (SHA256 of content).
/// The content-addressed identifier (BLAKE3 of content).
pub address: XorName,
/// The chunk data.
pub content: Vec<u8>,
Expand Down
6 changes: 2 additions & 4 deletions src/client/chunk_protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,7 @@ pub async fn send_and_await_chunk_response<T, E>(
// Subscribe before sending so we don't miss the response
let mut events = node.subscribe_events();

let target_peer_id = *target_peer;

node.send_message(&target_peer_id, CHUNK_PROTOCOL_ID, message_bytes)
node.send_message(target_peer, CHUNK_PROTOCOL_ID, message_bytes)
.await
.map_err(|e| send_error(e.to_string()))?;

Expand All @@ -56,7 +54,7 @@ pub async fn send_and_await_chunk_response<T, E>(
topic,
source: Some(source),
data,
})) if topic == CHUNK_PROTOCOL_ID && source == target_peer_id => {
})) if topic == CHUNK_PROTOCOL_ID && source == *target_peer => {
let response = match ChunkMessage::decode(&data) {
Ok(r) => r,
Err(e) => {
Expand Down
31 changes: 12 additions & 19 deletions src/client/data_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,13 @@
//!
//! This module provides the core data types for content-addressed chunk storage
//! on the saorsa network. Chunks are immutable, content-addressed blobs where
//! the address is the SHA256 hash of the content.
//! the address is the BLAKE3 hash of the content.

use bytes::Bytes;
use sha2::{Digest, Sha256};

/// Compute the content address (SHA256 hash) for the given data.
/// Compute the content address (BLAKE3 hash) for the given data.
#[must_use]
pub fn compute_address(content: &[u8]) -> XorName {
let mut hasher = Sha256::new();
hasher.update(content);
let result = hasher.finalize();
let mut address = [0u8; 32];
address.copy_from_slice(&result);
address
*blake3::hash(content).as_bytes()
}
Comment thread
mickvandijke marked this conversation as resolved.

/// Compute the XOR distance between two 32-byte addresses.
Expand Down Expand Up @@ -46,19 +39,19 @@ pub fn peer_id_to_xor_name(peer_id: &str) -> Option<XorName> {

/// A content-addressed identifier (32 bytes).
///
/// The address is computed as SHA256(content) for chunks,
/// The address is computed as BLAKE3(content) for chunks,
/// ensuring content-addressed storage.
pub type XorName = [u8; 32];

/// A chunk of data with its content-addressed identifier.
///
/// Chunks are the fundamental storage unit in saorsa. They are:
/// - **Immutable**: Content cannot be changed after storage
/// - **Content-addressed**: Address = SHA256(content)
/// - **Content-addressed**: Address = BLAKE3(content)
/// - **Paid**: Storage requires EVM payment on Arbitrum
#[derive(Debug, Clone)]
pub struct DataChunk {
/// The content-addressed identifier (SHA256 of content).
/// The content-addressed identifier (BLAKE3 of content).
pub address: XorName,
/// The raw data content.
pub content: Bytes,
Expand All @@ -67,7 +60,7 @@ pub struct DataChunk {
impl DataChunk {
/// Create a new data chunk.
///
/// Note: This does NOT verify that address == SHA256(content).
/// Note: This does NOT verify that address == BLAKE3(content).
/// Use `from_content` for automatic address computation.
#[must_use]
pub fn new(address: XorName, content: Bytes) -> Self {
Expand All @@ -87,7 +80,7 @@ impl DataChunk {
self.content.len()
}

/// Verify that the address matches SHA256(content).
/// Verify that the address matches BLAKE3(content).
#[must_use]
pub fn verify(&self) -> bool {
self.address == compute_address(&self.content)
Expand Down Expand Up @@ -132,11 +125,11 @@ mod tests {
let content = Bytes::from("hello world");
let chunk = DataChunk::from_content(content.clone());

// SHA256 of "hello world"
// BLAKE3 of "hello world"
let expected: [u8; 32] = [
0xb9, 0x4d, 0x27, 0xb9, 0x93, 0x4d, 0x3e, 0x08, 0xa5, 0x2e, 0x52, 0xd7, 0xda, 0x7d,
0xab, 0xfa, 0xc4, 0x84, 0xef, 0xe3, 0x7a, 0x53, 0x80, 0xee, 0x90, 0x88, 0xf7, 0xac,
0xe2, 0xef, 0xcd, 0xe9,
0xd7, 0x49, 0x81, 0xef, 0xa7, 0x0a, 0x0c, 0x88, 0x0b, 0x8d, 0x8c, 0x19, 0x85, 0xd0,
0x75, 0xdb, 0xcb, 0xf6, 0x79, 0xb9, 0x9a, 0x5f, 0x99, 0x14, 0xe5, 0xaa, 0xf9, 0x6b,
0x83, 0x1a, 0x9e, 0x24,
];

assert_eq!(chunk.address, expected);
Expand Down
2 changes: 1 addition & 1 deletion src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
//!
//! The chunk client provides:
//!
//! 1. **Content-addressed storage**: Chunk address = SHA256(content)
//! 1. **Content-addressed storage**: Chunk address = BLAKE3(content)
//! 2. **PQC security**: All data uses ML-KEM-768 and ML-DSA-65
//! 3. **EVM payment**: Chunks are paid for on Arbitrum network
//!
Expand Down
6 changes: 3 additions & 3 deletions src/client/quantum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
//! ## Data Model
//!
//! Chunks are the only data type supported:
//! - **Content-addressed**: Address = SHA256(content)
//! - **Content-addressed**: Address = BLAKE3(content)
//! - **Immutable**: Once stored, content cannot change
//! - **Paid**: Storage requires EVM payment on Arbitrum when a wallet is configured;
//! devnets with EVM disabled accept unpaid puts
Expand Down Expand Up @@ -77,7 +77,7 @@ impl Default for QuantumConfig {
///
/// ## Chunk Storage Model
///
/// Chunks are content-addressed: the address is the SHA256 hash of the content.
/// Chunks are content-addressed: the address is the BLAKE3 hash of the content.
/// This ensures data integrity - if the content matches the address, the data
/// is authentic. When a wallet is configured, chunk storage requires EVM payment
/// on Arbitrum. Without a wallet, chunks can be stored on devnets with EVM disabled.
Expand Down Expand Up @@ -128,7 +128,7 @@ impl QuantumClient {
///
/// # Arguments
///
/// * `address` - The `XorName` address of the chunk (SHA256 of content)
/// * `address` - The `XorName` address of the chunk (BLAKE3 of content)
///
/// # Returns
///
Expand Down
26 changes: 19 additions & 7 deletions src/devnet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use ant_evm::RewardsAddress;
use evmlib::Network as EvmNetwork;
use rand::Rng;
use saorsa_core::identity::NodeIdentity;
use saorsa_core::{NodeConfig as CoreNodeConfig, P2PEvent, P2PNode};
use saorsa_core::{NodeConfig as CoreNodeConfig, P2PEvent, P2PNode, PeerId};
use serde::{Deserialize, Serialize};
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::path::PathBuf;
Expand Down Expand Up @@ -291,8 +291,8 @@ pub enum NodeState {
#[allow(dead_code)]
pub struct DevnetNode {
index: usize,
node_id: String,
peer_id: String,
label: String,
peer_id: PeerId,
port: u16,
address: SocketAddr,
data_dir: PathBuf,
Expand Down Expand Up @@ -531,9 +531,13 @@ impl Devnet {
// Generate identity first so we can use peer_id as the directory name
let identity = NodeIdentity::generate()
.map_err(|e| DevnetError::Core(format!("Failed to generate node identity: {e}")))?;
let peer_id = identity.peer_id().to_hex();
let node_id = format!("devnet_node_{index}");
let data_dir = self.config.data_dir.join(NODES_SUBDIR).join(&peer_id);
let peer_id = *identity.peer_id();
let label = format!("devnet_node_{index}");
let data_dir = self
.config
.data_dir
.join(NODES_SUBDIR)
.join(peer_id.to_hex());

tokio::fs::create_dir_all(&data_dir).await?;

Expand All @@ -546,7 +550,7 @@ impl Devnet {

Ok(DevnetNode {
index,
node_id,
label,
peer_id,
port,
address,
Expand Down Expand Up @@ -620,6 +624,14 @@ impl Devnet {
let mut core_config = CoreNodeConfig::new()
.map_err(|e| DevnetError::Core(format!("Failed to create core config: {e}")))?;

// Load the node identity for app-level message signing
let identity = NodeIdentity::load_from_file(
&node.data_dir.join(crate::config::NODE_IDENTITY_FILENAME),
)
.await
.map_err(|e| DevnetError::Core(format!("Failed to load node identity: {e}")))?;

core_config.node_identity = Some(Arc::new(identity));
core_config.listen_addr = node.address;
core_config.listen_addrs = vec![node.address];
core_config.enable_ipv6 = false;
Expand Down
2 changes: 1 addition & 1 deletion src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ impl NodeBuilder {
// Enable IPv6 if configured
core_config.enable_ipv6 = matches!(config.ip_version, IpVersion::Ipv6 | IpVersion::Dual);

// Add bootstrap peers
// Add bootstrap peers.
core_config.bootstrap_peers.clone_from(&config.bootstrap);

// Forward max_message_size to the transport layer.
Expand Down
2 changes: 1 addition & 1 deletion src/storage/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ impl AntProtocol {
});
}

// 2. Verify content address matches SHA256(content)
// 2. Verify content address matches BLAKE3(content)
let computed = crate::client::compute_address(&request.content);
if computed != address {
return ChunkPutResponse::Error(ProtocolError::AddressMismatch {
Expand Down
8 changes: 4 additions & 4 deletions src/storage/lmdb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ impl LmdbStorage {
///
/// # Arguments
///
/// * `address` - Content address (should be SHA256 of content)
/// * `address` - Content address (should be BLAKE3 of content)
/// * `content` - Chunk data
///
/// # Returns
Expand Down Expand Up @@ -397,7 +397,7 @@ impl LmdbStorage {
Ok(entries as u64)
}

/// Compute content address (SHA256 hash).
/// Compute content address (BLAKE3 hash).
#[must_use]
pub fn compute_address(content: &[u8]) -> XorName {
crate::client::compute_address(content)
Expand Down Expand Up @@ -551,11 +551,11 @@ mod tests {

#[test]
fn test_compute_address() {
// Known SHA256 hash of "hello world"
// Known BLAKE3 hash of "hello world"
let content = b"hello world";
let address = LmdbStorage::compute_address(content);

let expected_hex = "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9";
let expected_hex = "d74981efa70a0c880b8d8c1985d075dbcbf679b99a5f9914e5aaf96b831a9e24";
assert_eq!(hex::encode(address), expected_hex);
}

Expand Down
28 changes: 11 additions & 17 deletions src/upgrade/rollout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
//! - Nodes are evenly distributed across the rollout window
//! - The same node always upgrades at the same point in the window

use sha2::{Digest, Sha256};
use std::time::Duration;
use tracing::debug;

Expand All @@ -45,12 +44,7 @@ impl StagedRollout {
/// * `max_delay_hours` - Maximum rollout window (default: 24 hours)
#[must_use]
pub fn new(node_id: &[u8], max_delay_hours: u64) -> Self {
let mut hasher = Sha256::new();
hasher.update(node_id);
let hash_result = hasher.finalize();

let mut node_id_hash = [0u8; 32];
node_id_hash.copy_from_slice(&hash_result);
let node_id_hash = *blake3::hash(node_id).as_bytes();

Self {
max_delay_hours,
Expand Down Expand Up @@ -133,20 +127,20 @@ impl StagedRollout {
}

// Include version in the hash for version-specific delays
let mut hasher = Sha256::new();
hasher.update(self.node_id_hash);
let mut hasher = blake3::Hasher::new();
hasher.update(&self.node_id_hash);
hasher.update(version.to_string().as_bytes());
let hash_result = hasher.finalize();

let hash_value = u64::from_le_bytes([
hash_result[0],
hash_result[1],
hash_result[2],
hash_result[3],
hash_result[4],
hash_result[5],
hash_result[6],
hash_result[7],
hash_result.as_bytes()[0],
hash_result.as_bytes()[1],
hash_result.as_bytes()[2],
hash_result.as_bytes()[3],
hash_result.as_bytes()[4],
hash_result.as_bytes()[5],
hash_result.as_bytes()[6],
hash_result.as_bytes()[7],
]);

let max_delay_secs = self.max_delay_hours * 3600;
Expand Down
10 changes: 5 additions & 5 deletions tests/e2e/data_types/chunk.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Chunk data type E2E tests.
//!
//! Chunks are immutable, content-addressed data blocks (up to 4MB).
//! The address is derived from the content hash (SHA256 -> `XorName`).
//! The address is derived from the content hash (BLAKE3 -> `XorName`).
//!
//! ## Test Coverage
//!
Expand Down Expand Up @@ -50,7 +50,7 @@ impl ChunkTestFixture {
}
}

/// Compute content address for data (SHA256 hash).
/// Compute content address for data (BLAKE3 hash).
#[must_use]
pub fn compute_address(data: &[u8]) -> [u8; 32] {
saorsa_node::compute_address(data)
Expand Down Expand Up @@ -101,10 +101,10 @@ mod tests {
#[test]
fn test_empty_data_address() {
let addr = ChunkTestFixture::compute_address(&[]);
// SHA256 of empty string is well-known
// BLAKE3 of empty string is well-known
assert_eq!(
hex::encode(addr),
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
"af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262"
);
}

Expand Down Expand Up @@ -152,7 +152,7 @@ mod tests {
.await
.expect("Failed to store chunk");

// Verify the address is a valid SHA256 hash
// Verify the address is a valid BLAKE3 hash
let expected_address = ChunkTestFixture::compute_address(&fixture.small);
assert_eq!(
address, expected_address,
Expand Down
7 changes: 2 additions & 5 deletions tests/e2e/data_types/graph_entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,17 +85,14 @@ impl GraphEntryTestFixture {
/// Compute graph entry address from owner and content.
#[must_use]
pub fn compute_address(owner: &[u8; 32], content: &[u8], parents: &[[u8; 32]]) -> [u8; 32] {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
let mut hasher = blake3::Hasher::new();
hasher.update(b"graph_entry:");
hasher.update(owner);
hasher.update(content);
for parent in parents {
hasher.update(parent);
}
let hash = hasher.finalize();
let mut address = [0u8; 32];
address.copy_from_slice(&hash);
let address = *hasher.finalize().as_bytes();
address
}
}
Expand Down
Loading