Complete implementation of the Synor Storage Layer (L2) for decentralized content storage. This enables permanent, censorship-resistant storage of any file type including Next.js apps, Flutter apps, and arbitrary data. Core modules: - cid.rs: Content addressing with Blake3/SHA256 hashing (synor1... format) - chunker.rs: File chunking for parallel upload/download (1MB chunks) - erasure.rs: Reed-Solomon erasure coding (10+4 shards) for fault tolerance - proof.rs: Storage proofs with Merkle trees for verification - deal.rs: Storage deals and market economics (3 pricing tiers) Infrastructure: - node/: Storage node service with P2P networking and local storage - gateway/: HTTP gateway for browser access with LRU caching - Docker deployment with nginx load balancer Architecture: - Operates as L2 alongside Synor L1 blockchain - Storage proofs verified on-chain for reward distribution - Can lose 4 shards per chunk and still recover data - Gateway URLs: /synor1<cid> for content access All 28 unit tests passing.
321 lines
9.4 KiB
Rust
321 lines
9.4 KiB
Rust
//! Storage Deals - Economic layer for storage
|
|
//!
|
|
//! Users pay storage providers to store their data.
|
|
//! Deals are registered on L1 for enforcement.
|
|
|
|
use crate::cid::ContentId;
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// Storage deal status
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub enum DealStatus {
|
|
/// Awaiting storage node acceptance
|
|
Pending,
|
|
/// Deal active, data being stored
|
|
Active,
|
|
/// Deal completed successfully
|
|
Completed,
|
|
/// Storage node failed proofs
|
|
Failed,
|
|
/// Deal cancelled by user
|
|
Cancelled,
|
|
}
|
|
|
|
/// Storage deal between user and storage provider
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct StorageDeal {
|
|
/// Unique deal ID
|
|
pub deal_id: [u8; 32],
|
|
/// Content being stored
|
|
pub cid: ContentId,
|
|
/// Client address (paying for storage)
|
|
pub client: [u8; 32],
|
|
/// Storage provider node ID
|
|
pub provider: Option<[u8; 32]>,
|
|
/// Content size in bytes
|
|
pub size: u64,
|
|
/// Storage duration (in L1 blocks)
|
|
pub duration: u64,
|
|
/// Price per byte per epoch (in atomic SYNOR units)
|
|
pub price_per_byte_epoch: u64,
|
|
/// Total collateral locked by provider
|
|
pub provider_collateral: u64,
|
|
/// Client deposit
|
|
pub client_deposit: u64,
|
|
/// Deal start block (on L1)
|
|
pub start_block: u64,
|
|
/// Deal end block (on L1)
|
|
pub end_block: u64,
|
|
/// Current status
|
|
pub status: DealStatus,
|
|
/// Replication factor (how many nodes store the data)
|
|
pub replication: u8,
|
|
/// Failed proof count
|
|
pub failed_proofs: u32,
|
|
/// Successful proof count
|
|
pub successful_proofs: u32,
|
|
}
|
|
|
|
impl StorageDeal {
|
|
/// Calculate total cost for the deal
|
|
pub fn total_cost(&self) -> u64 {
|
|
let epochs = self.duration;
|
|
self.size
|
|
.saturating_mul(self.price_per_byte_epoch)
|
|
.saturating_mul(epochs)
|
|
.saturating_mul(self.replication as u64)
|
|
}
|
|
|
|
/// Check if deal is active
|
|
pub fn is_active(&self) -> bool {
|
|
self.status == DealStatus::Active
|
|
}
|
|
|
|
/// Check if deal has expired
|
|
pub fn is_expired(&self, current_block: u64) -> bool {
|
|
current_block >= self.end_block
|
|
}
|
|
|
|
/// Calculate provider reward for successful storage
|
|
pub fn provider_reward(&self) -> u64 {
|
|
// Provider gets paid based on successful proofs
|
|
let total_possible_proofs = self.duration / 100; // Assume proof every 100 blocks
|
|
if total_possible_proofs == 0 {
|
|
return 0;
|
|
}
|
|
|
|
let success_rate = self.successful_proofs as u64 * 100 / total_possible_proofs;
|
|
self.total_cost() * success_rate / 100
|
|
}
|
|
}
|
|
|
|
/// Request to create a new storage deal
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct CreateDealRequest {
|
|
/// Content to store
|
|
pub cid: ContentId,
|
|
/// Content size
|
|
pub size: u64,
|
|
/// Desired duration (blocks)
|
|
pub duration: u64,
|
|
/// Maximum price willing to pay
|
|
pub max_price_per_byte_epoch: u64,
|
|
/// Desired replication factor
|
|
pub replication: u8,
|
|
/// Preferred regions (optional)
|
|
pub preferred_regions: Vec<String>,
|
|
}
|
|
|
|
/// Storage provider offer
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct StorageOffer {
|
|
/// Provider node ID
|
|
pub provider: [u8; 32],
|
|
/// Available capacity (bytes)
|
|
pub available_capacity: u64,
|
|
/// Price per byte per epoch
|
|
pub price_per_byte_epoch: u64,
|
|
/// Minimum deal duration
|
|
pub min_duration: u64,
|
|
/// Maximum deal duration
|
|
pub max_duration: u64,
|
|
/// Regions served
|
|
pub regions: Vec<String>,
|
|
/// Provider stake amount
|
|
pub stake: u64,
|
|
/// Historical uptime percentage
|
|
pub uptime: u8,
|
|
/// Success rate (proofs passed / total)
|
|
pub success_rate: u8,
|
|
}
|
|
|
|
/// Storage market for matching clients with providers
|
|
#[derive(Debug, Default)]
|
|
pub struct StorageMarket {
|
|
/// Active deals
|
|
pub deals: Vec<StorageDeal>,
|
|
/// Available offers from providers
|
|
pub offers: Vec<StorageOffer>,
|
|
}
|
|
|
|
impl StorageMarket {
|
|
/// Create a new storage market
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
/// Find best providers for a deal request
|
|
pub fn find_providers(&self, request: &CreateDealRequest) -> Vec<&StorageOffer> {
|
|
self.offers
|
|
.iter()
|
|
.filter(|offer| {
|
|
offer.available_capacity >= request.size
|
|
&& offer.price_per_byte_epoch <= request.max_price_per_byte_epoch
|
|
&& offer.min_duration <= request.duration
|
|
&& offer.max_duration >= request.duration
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Add a storage offer
|
|
pub fn add_offer(&mut self, offer: StorageOffer) {
|
|
self.offers.push(offer);
|
|
}
|
|
|
|
/// Create a deal from request and accepted offer
|
|
pub fn create_deal(
|
|
&mut self,
|
|
request: CreateDealRequest,
|
|
provider: [u8; 32],
|
|
client: [u8; 32],
|
|
current_block: u64,
|
|
) -> StorageDeal {
|
|
let deal_id = self.generate_deal_id(&request.cid, &client, current_block);
|
|
|
|
let offer = self.offers.iter().find(|o| o.provider == provider);
|
|
let price = offer.map(|o| o.price_per_byte_epoch).unwrap_or(0);
|
|
|
|
let deal = StorageDeal {
|
|
deal_id,
|
|
cid: request.cid,
|
|
client,
|
|
provider: Some(provider),
|
|
size: request.size,
|
|
duration: request.duration,
|
|
price_per_byte_epoch: price,
|
|
provider_collateral: 0, // Set during acceptance
|
|
client_deposit: 0, // Set during creation
|
|
start_block: current_block,
|
|
end_block: current_block + request.duration,
|
|
status: DealStatus::Pending,
|
|
replication: request.replication,
|
|
failed_proofs: 0,
|
|
successful_proofs: 0,
|
|
};
|
|
|
|
self.deals.push(deal.clone());
|
|
deal
|
|
}
|
|
|
|
fn generate_deal_id(&self, cid: &ContentId, client: &[u8; 32], block: u64) -> [u8; 32] {
|
|
let mut input = Vec::new();
|
|
input.extend_from_slice(&cid.digest);
|
|
input.extend_from_slice(client);
|
|
input.extend_from_slice(&block.to_le_bytes());
|
|
*blake3::hash(&input).as_bytes()
|
|
}
|
|
}
|
|
|
|
/// Pricing tiers for storage
|
|
#[derive(Debug, Clone)]
|
|
pub struct PricingTier {
|
|
/// Tier name
|
|
pub name: String,
|
|
/// Price per GB per month (in atomic SYNOR)
|
|
pub price_per_gb_month: u64,
|
|
/// Minimum replication
|
|
pub min_replication: u8,
|
|
/// SLA uptime guarantee in basis points (10000 = 100%)
|
|
pub uptime_sla_bps: u16,
|
|
}
|
|
|
|
impl PricingTier {
|
|
/// Standard tier - basic storage
|
|
pub fn standard() -> Self {
|
|
Self {
|
|
name: "Standard".to_string(),
|
|
price_per_gb_month: 100_000_000, // 0.1 SYNOR per GB/month
|
|
min_replication: 3,
|
|
uptime_sla_bps: 9900, // 99.00%
|
|
}
|
|
}
|
|
|
|
/// Premium tier - higher redundancy
|
|
pub fn premium() -> Self {
|
|
Self {
|
|
name: "Premium".to_string(),
|
|
price_per_gb_month: 250_000_000, // 0.25 SYNOR per GB/month
|
|
min_replication: 5,
|
|
uptime_sla_bps: 9990, // 99.90%
|
|
}
|
|
}
|
|
|
|
/// Permanent tier - one-time payment for ~20 years
|
|
pub fn permanent() -> Self {
|
|
Self {
|
|
name: "Permanent".to_string(),
|
|
price_per_gb_month: 2_400_000_000, // 2.4 SYNOR per GB (one-time, ~20 years equiv)
|
|
min_replication: 10,
|
|
uptime_sla_bps: 9999, // 99.99%
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_deal_cost() {
|
|
let deal = StorageDeal {
|
|
deal_id: [0u8; 32],
|
|
cid: ContentId::from_content(b"test"),
|
|
client: [1u8; 32],
|
|
provider: Some([2u8; 32]),
|
|
size: 1_000_000_000, // 1 GB
|
|
duration: 100,
|
|
price_per_byte_epoch: 1,
|
|
provider_collateral: 0,
|
|
client_deposit: 0,
|
|
start_block: 0,
|
|
end_block: 100,
|
|
status: DealStatus::Active,
|
|
replication: 3,
|
|
failed_proofs: 0,
|
|
successful_proofs: 0,
|
|
};
|
|
|
|
let cost = deal.total_cost();
|
|
assert_eq!(cost, 1_000_000_000 * 100 * 3);
|
|
}
|
|
|
|
#[test]
|
|
fn test_market_find_providers() {
|
|
let mut market = StorageMarket::new();
|
|
|
|
market.add_offer(StorageOffer {
|
|
provider: [1u8; 32],
|
|
available_capacity: 100_000_000_000, // 100 GB
|
|
price_per_byte_epoch: 1,
|
|
min_duration: 10,
|
|
max_duration: 10000,
|
|
regions: vec!["US".to_string()],
|
|
stake: 1000,
|
|
uptime: 99,
|
|
success_rate: 99,
|
|
});
|
|
|
|
let request = CreateDealRequest {
|
|
cid: ContentId::from_content(b"test"),
|
|
size: 1_000_000,
|
|
duration: 100,
|
|
max_price_per_byte_epoch: 2,
|
|
replication: 3,
|
|
preferred_regions: vec![],
|
|
};
|
|
|
|
let providers = market.find_providers(&request);
|
|
assert_eq!(providers.len(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_pricing_tiers() {
|
|
let standard = PricingTier::standard();
|
|
let premium = PricingTier::premium();
|
|
let permanent = PricingTier::permanent();
|
|
|
|
assert!(standard.price_per_gb_month < premium.price_per_gb_month);
|
|
assert!(premium.price_per_gb_month < permanent.price_per_gb_month);
|
|
assert!(standard.min_replication < premium.min_replication);
|
|
}
|
|
}
|