pub mod bandwidth;
pub(crate) mod source;
pub(crate) mod verify;
pub mod testing;
pub mod transport;
pub use bandwidth::BandwidthGovernor;
pub use source::FetchTier;
pub(crate) use source::BlobSource;
use std::{
collections::HashMap,
path::PathBuf,
sync::Arc,
};
use bytes::Bytes;
use tokio::sync::RwLock;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("invalid hash: {0}")]
InvalidHash(String),
#[error("all sources exhausted for {0}")]
FetchFailed(BlakeHash),
#[error("hash mismatch: expected {expected}, got {actual}")]
HashMismatch {
expected: BlakeHash,
actual: BlakeHash,
},
#[error("io: {0}")]
Io(#[from] std::io::Error),
}
pub type Result<T, E = Error> = std::result::Result<T, E>;
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct BlakeHash([u8; 32]);
impl BlakeHash {
pub fn hash(data: &[u8]) -> Self {
Self(*blake3::hash(data).as_bytes())
}
pub fn from_bytes(bytes: [u8; 32]) -> Self {
Self(bytes)
}
pub fn from_hex(s: &str) -> Result<Self> {
let raw =
hex::decode(s).map_err(|_| Error::InvalidHash(s.to_string()))?;
let arr: [u8; 32] = raw
.try_into()
.map_err(|_| Error::InvalidHash(format!("expected 32 bytes: {s}")))?;
Ok(Self(arr))
}
pub fn to_hex(&self) -> String {
hex::encode(self.0)
}
pub fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
pub fn verify(&self, data: &[u8]) -> bool {
*self == Self::hash(data)
}
}
impl std::fmt::Debug for BlakeHash {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "BlakeHash({}…)", &self.to_hex()[..8])
}
}
impl std::fmt::Display for BlakeHash {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.to_hex())
}
}
impl std::str::FromStr for BlakeHash {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
Self::from_hex(s)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SeedRole {
Passive,
Participant,
Permanent,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PeerTier {
Cloud,
Rig,
Workstation,
Mobile,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BwCaps {
pub up_mbit: u32,
pub down_mbit: u32,
}
impl BwCaps {
pub fn passive() -> Self {
Self { up_mbit: 0, down_mbit: 50 }
}
}
#[derive(Debug, Clone, Default)]
pub struct BandwidthPolicy {
roles: HashMap<PeerTier, BwCaps>,
}
impl BandwidthPolicy {
pub fn role(mut self, tier: PeerTier, caps: BwCaps) -> Self {
self.roles.insert(tier, caps);
self
}
pub fn caps_for(&self, tier: PeerTier) -> BwCaps {
self.roles.get(&tier).copied().unwrap_or_else(BwCaps::passive)
}
}
#[derive(Debug, Clone)]
pub struct Discovery {
pub lan: bool,
pub swarm: bool,
pub relays: Vec<String>,
}
impl Default for Discovery {
fn default() -> Self {
Self { lan: true, swarm: true, relays: vec![] }
}
}
impl Discovery {
pub fn with_relays(
mut self,
relays: impl IntoIterator<Item = impl Into<String>>,
) -> Self {
self.relays = relays.into_iter().map(Into::into).collect();
self
}
pub fn lan_only() -> Self {
Self { lan: true, swarm: false, relays: vec![] }
}
pub fn none() -> Self {
Self { lan: false, swarm: false, relays: vec![] }
}
}
pub struct AssetClassConfig {
pub name: &'static str,
pub permanent_seeds: Vec<String>,
pub cdn_fallback: Option<String>,
pub discovery: Discovery,
pub bandwidth: BandwidthPolicy,
pub cache_dir: Option<PathBuf>,
pub cache_budget_bytes: u64,
}
impl Default for AssetClassConfig {
fn default() -> Self {
Self {
name: "default",
permanent_seeds: vec![],
cdn_fallback: None,
discovery: Discovery::default(),
bandwidth: BandwidthPolicy::default(),
cache_dir: None,
cache_budget_bytes: 1024 * 1024 * 1024,
}
}
}
struct ClassInner {
config: AssetClassConfig,
cache: RwLock<HashMap<BlakeHash, Bytes>>,
sources: RwLock<Vec<Arc<dyn BlobSource>>>,
role: RwLock<(SeedRole, PeerTier)>,
governor: BandwidthGovernor,
}
#[derive(Clone)]
pub struct AssetClass(Arc<ClassInner>);
impl AssetClass {
pub async fn register(config: AssetClassConfig) -> Result<Self> {
let cdn: Option<Arc<dyn BlobSource>> = if let Some(url) = &config.cdn_fallback {
match transport::http::HttpFetcher::new(url.as_str()) {
Ok(f) => Some(Arc::new(f)),
Err(e) => {
tracing::warn!(url = %url, "HttpFetcher init failed: {e}");
None
}
}
} else {
None
};
let initial_sources: Vec<Arc<dyn BlobSource>> = cdn.into_iter().collect();
let governor = BandwidthGovernor::new(config.bandwidth.clone());
governor.probe_os();
Ok(Self(Arc::new(ClassInner {
config,
cache: RwLock::new(HashMap::new()),
sources: RwLock::new(initial_sources),
role: RwLock::new((SeedRole::Participant, PeerTier::Workstation)),
governor,
})))
}
pub async fn set_role(&self, role: SeedRole, tier: PeerTier) -> Result<()> {
*self.0.role.write().await = (role, tier);
Ok(())
}
pub fn asset(&self, hash: BlakeHash) -> Asset {
Asset { class: self.clone(), hash }
}
pub fn name(&self) -> &str {
self.0.config.name
}
pub fn governor(&self) -> &BandwidthGovernor {
&self.0.governor
}
pub fn set_battery(&self, on_battery: bool) {
self.0.governor.set_battery(on_battery);
}
pub fn set_metered(&self, metered: bool) {
self.0.governor.set_metered(metered);
}
pub(crate) fn add_source(&self, source: Arc<dyn BlobSource>) {
if let Ok(mut sources) = self.0.sources.try_write() {
sources.push(source);
} else {
tracing::warn!(class = self.name(), "add_source: lock contended, source dropped");
}
}
pub(crate) async fn fetch_bytes(&self, hash: BlakeHash) -> Result<Bytes> {
{
let cache = self.0.cache.read().await;
if let Some(bytes) = cache.get(&hash) {
tracing::trace!(%hash, "cache hit");
return Ok(bytes.clone());
}
}
let sources: Vec<Arc<dyn BlobSource>> =
self.0.sources.read().await.clone();
let chain = source::FetchChain::new(sources);
if let Some((_tier, bytes)) = chain.fetch(&hash).await {
self.0.cache.write().await.insert(hash, bytes.clone());
return Ok(bytes);
}
Err(Error::FetchFailed(hash))
}
}
#[derive(Clone)]
pub struct Asset {
class: AssetClass,
hash: BlakeHash,
}
impl Asset {
pub async fn fetch(&self) -> Result<Bytes> {
self.class.fetch_bytes(self.hash).await
}
pub async fn is_cached(&self) -> bool {
self.class.0.cache.read().await.contains_key(&self.hash)
}
pub fn hash(&self) -> BlakeHash {
self.hash
}
}