use crate::messages::{Message, WantEntry as MessageWantEntry};
use crate::peer_manager::{ConcurrentPeerManager, PeerId, PeerScoringConfig, SelectionStrategy};
use crate::want_list::{ConcurrentWantList, WantEntry, WantListConfig};
use ipfrs_core::{Block, Cid, Result};
use ipfrs_storage::traits::BlockStore;
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use std::time::{Duration, Instant};
#[derive(Debug, Clone)]
pub struct BitswapConfig {
pub want_list: WantListConfig,
pub peer_scoring: PeerScoringConfig,
pub max_peers_per_request: usize,
pub selection_strategy: SelectionStrategy,
pub auto_cleanup: bool,
pub cleanup_interval: Duration,
}
impl Default for BitswapConfig {
fn default() -> Self {
Self {
want_list: WantListConfig::default(),
peer_scoring: PeerScoringConfig::default(),
max_peers_per_request: 3,
selection_strategy: SelectionStrategy::BestScore,
auto_cleanup: true,
cleanup_interval: Duration::from_secs(10),
}
}
}
type PendingRequestMap = Arc<RwLock<HashMap<Cid, (Vec<PeerId>, Instant)>>>;
pub struct BitswapExchange<S: BlockStore> {
store: Arc<S>,
want_list: ConcurrentWantList,
peer_manager: ConcurrentPeerManager,
pending_requests: PendingRequestMap,
config: BitswapConfig,
}
impl<S: BlockStore> BitswapExchange<S> {
pub fn new(store: Arc<S>, config: BitswapConfig) -> Result<Self> {
Ok(Self {
store,
want_list: ConcurrentWantList::new(config.want_list.clone()),
peer_manager: ConcurrentPeerManager::new(config.peer_scoring.clone()),
pending_requests: Arc::new(RwLock::new(HashMap::new())),
config,
})
}
pub fn with_defaults(store: Arc<S>) -> Result<Self> {
Self::new(store, BitswapConfig::default())
}
pub fn want(&self, cid: Cid, priority: i32) -> Result<()> {
if !self.want_list.add_simple(cid, priority) {
if self.want_list.contains(&cid) {
self.want_list.update_priority(&cid, priority);
return Ok(());
}
return Err(ipfrs_core::Error::Internal("Want list full".to_string()));
}
Ok(())
}
pub fn want_with_entry(&self, entry: WantEntry) -> Result<()> {
if !self.want_list.add(entry) {
return Err(ipfrs_core::Error::Internal(
"Want list full or duplicate".to_string(),
));
}
Ok(())
}
pub fn cancel_want(&self, cid: &Cid) -> Result<()> {
self.want_list.remove(cid);
self.pending_requests
.write()
.unwrap_or_else(|e| e.into_inner())
.remove(cid);
Ok(())
}
pub fn is_wanted(&self, cid: &Cid) -> bool {
self.want_list.contains(cid)
}
pub fn next_want(&self) -> Option<Cid> {
self.want_list.pop().map(|e| e.cid)
}
pub fn get_want_list(&self) -> Vec<MessageWantEntry> {
self.want_list
.cids()
.into_iter()
.map(MessageWantEntry::new)
.collect()
}
pub fn get_wanted_cids(&self) -> Vec<Cid> {
self.want_list.cids()
}
pub fn update_priority(&self, cid: &Cid, priority: i32) -> bool {
self.want_list.update_priority(cid, priority)
}
pub fn boost_deadline_priorities(&self) {
self.want_list.boost_deadline_priorities()
}
pub fn add_peer(&self, peer_id: PeerId) {
self.peer_manager.add_peer(peer_id);
}
pub fn remove_peer(&self, peer_id: &PeerId) {
self.peer_manager.remove_peer(peer_id);
}
pub fn select_peers_for_request(&self, cid: &Cid) -> Vec<PeerId> {
let providers = self
.peer_manager
.select_providers(cid, self.config.max_peers_per_request);
if !providers.is_empty() {
return providers;
}
self.peer_manager.select_peers(
cid,
self.config.max_peers_per_request,
self.config.selection_strategy,
)
}
pub async fn receive_block(&self, peer_id: &PeerId, block: Block) -> Result<()> {
let cid = *block.cid();
let size = block.size();
let latency = {
let pending = self
.pending_requests
.read()
.unwrap_or_else(|e| e.into_inner());
pending.get(&cid).map(|(_, start)| start.elapsed())
};
self.store.put(&block).await?;
if let Some(latency) = latency {
self.peer_manager.record_success(peer_id, size, latency);
} else {
self.peer_manager
.record_success(peer_id, size, Duration::from_millis(100));
}
self.peer_manager.record_has(peer_id, cid);
self.cancel_want(&cid)?;
Ok(())
}
pub fn record_failure(&self, peer_id: &PeerId, cid: &Cid) {
self.peer_manager.record_failure(peer_id);
self.want_list.mark_attempted(cid);
}
pub fn record_have(&self, peer_id: &PeerId, cid: Cid) {
self.peer_manager.record_has(peer_id, cid);
}
pub fn record_dont_have(&self, peer_id: &PeerId, cid: Cid) {
self.peer_manager.record_doesnt_have(peer_id, cid);
}
pub async fn send_block(&self, _peer_id: &PeerId, cid: &Cid) -> Result<Option<Message>> {
let block = match self.store.get(cid).await? {
Some(b) => b,
None => {
return Ok(Some(Message::dont_have(*cid)));
}
};
Ok(Some(Message::block(*cid, block.data().to_vec())))
}
pub fn mark_request_sent(&self, cid: Cid, peers: Vec<PeerId>) {
for peer in &peers {
self.peer_manager.mark_request_sent(peer);
}
self.pending_requests
.write()
.unwrap_or_else(|e| e.into_inner())
.insert(cid, (peers, Instant::now()));
}
pub fn cleanup(&self) {
let expired = self.want_list.cleanup_expired();
let mut pending = self
.pending_requests
.write()
.unwrap_or_else(|e| e.into_inner());
for entry in expired {
pending.remove(&entry.cid);
}
}
pub fn get_peer_scores(&self) -> HashMap<PeerId, f64> {
self.peer_manager.get_scores()
}
pub fn is_peer_blacklisted(&self, peer_id: &PeerId) -> bool {
self.peer_manager.is_blacklisted(peer_id)
}
pub fn peer_manager(&self) -> &ConcurrentPeerManager {
&self.peer_manager
}
pub fn want_list_manager(&self) -> &ConcurrentWantList {
&self.want_list
}
pub fn stats(&self) -> BitswapStats {
let peer_stats = self.peer_manager.stats();
BitswapStats {
want_list_size: self.want_list.len(),
pending_requests: self
.pending_requests
.read()
.unwrap_or_else(|e| e.into_inner())
.len(),
num_peers: peer_stats.total_peers,
connected_peers: peer_stats.connected_peers,
blacklisted_peers: peer_stats.blacklisted_peers,
total_bytes_sent: 0, total_bytes_recv: 0, total_requests: peer_stats.total_requests,
completed_requests: peer_stats.total_completed,
failed_requests: peer_stats.total_failed,
avg_peer_score: peer_stats.avg_score,
avg_latency_ms: peer_stats.avg_latency_ms,
}
}
}
#[derive(Debug, Clone)]
pub struct BitswapStats {
pub want_list_size: usize,
pub pending_requests: usize,
pub num_peers: usize,
pub connected_peers: usize,
pub blacklisted_peers: usize,
pub total_bytes_sent: u64,
pub total_bytes_recv: u64,
pub total_requests: u64,
pub completed_requests: u64,
pub failed_requests: u64,
pub avg_peer_score: f64,
pub avg_latency_ms: f64,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::peer_manager::BlacklistReason;
use ipfrs_storage::{BlockStoreConfig, SledBlockStore};
#[tokio::test]
async fn test_bitswap_want_list() {
let config = BlockStoreConfig {
path: std::env::temp_dir().join("ipfrs-test-bitswap-want"),
cache_size: 100 * 1024 * 1024,
};
let _ = std::fs::remove_dir_all(&config.path);
let store = Arc::new(SledBlockStore::new(config).expect("test: create block store"));
let bitswap = BitswapExchange::with_defaults(store).expect("test: create bitswap");
let cid = "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"
.parse::<Cid>()
.expect("test: parse cid");
bitswap.want(cid, 10).expect("test: add want");
assert!(bitswap.is_wanted(&cid));
bitswap.cancel_want(&cid).expect("test: cancel want");
assert!(!bitswap.is_wanted(&cid));
}
#[tokio::test]
async fn test_peer_management() {
let config = BlockStoreConfig {
path: std::env::temp_dir().join("ipfrs-test-bitswap-peer"),
cache_size: 100 * 1024 * 1024,
};
let _ = std::fs::remove_dir_all(&config.path);
let store = Arc::new(SledBlockStore::new(config).expect("test: create block store"));
let bitswap = BitswapExchange::with_defaults(store).expect("test: create bitswap");
let peer_id = "peer1".to_string();
bitswap.add_peer(peer_id.clone());
let cid = "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"
.parse::<Cid>()
.expect("test: parse cid");
bitswap.record_have(&peer_id, cid);
let providers = bitswap.select_peers_for_request(&cid);
assert_eq!(providers.len(), 1);
assert_eq!(providers[0], peer_id);
let stats = bitswap.stats();
assert_eq!(stats.num_peers, 1);
}
#[tokio::test]
async fn test_receive_block_updates_peer() {
let config = BlockStoreConfig {
path: std::env::temp_dir().join("ipfrs-test-bitswap-recv"),
cache_size: 100 * 1024 * 1024,
};
let _ = std::fs::remove_dir_all(&config.path);
let store = Arc::new(SledBlockStore::new(config).expect("test: create block store"));
let bitswap = BitswapExchange::with_defaults(store).expect("test: create bitswap");
let peer_id = "peer1".to_string();
bitswap.add_peer(peer_id.clone());
let block = Block::new(vec![1, 2, 3, 4].into()).expect("test: create block");
let cid = *block.cid();
bitswap.want(cid, 10).expect("test: add want");
assert!(bitswap.is_wanted(&cid));
bitswap
.receive_block(&peer_id, block)
.await
.expect("test: receive block");
assert!(!bitswap.is_wanted(&cid));
let stats = bitswap.stats();
assert_eq!(stats.completed_requests, 1);
}
#[tokio::test]
async fn test_blacklist() {
let config = BlockStoreConfig {
path: std::env::temp_dir().join("ipfrs-test-bitswap-blacklist"),
cache_size: 100 * 1024 * 1024,
};
let _ = std::fs::remove_dir_all(&config.path);
let store = Arc::new(SledBlockStore::new(config).expect("test: create block store"));
let bitswap = BitswapExchange::with_defaults(store).expect("test: create bitswap");
let peer_id = "bad_peer".to_string();
bitswap.add_peer(peer_id.clone());
bitswap
.peer_manager()
.blacklist_peer(peer_id.clone(), BlacklistReason::Manual, None);
assert!(bitswap.is_peer_blacklisted(&peer_id));
let stats = bitswap.stats();
assert_eq!(stats.blacklisted_peers, 1);
}
#[tokio::test]
async fn test_priority_update() {
let config = BlockStoreConfig {
path: std::env::temp_dir().join("ipfrs-test-bitswap-priority"),
cache_size: 100 * 1024 * 1024,
};
let _ = std::fs::remove_dir_all(&config.path);
let store = Arc::new(SledBlockStore::new(config).expect("test: create block store"));
let bitswap = BitswapExchange::with_defaults(store).expect("test: create bitswap");
let cid = "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"
.parse::<Cid>()
.expect("test: parse cid");
bitswap.want(cid, 10).expect("test: add want");
assert!(bitswap.update_priority(&cid, 100));
}
#[tokio::test]
async fn test_multiple_concurrent_wants() {
let config = BlockStoreConfig {
path: std::env::temp_dir().join("ipfrs-test-bitswap-multi-want"),
cache_size: 100 * 1024 * 1024,
};
let _ = std::fs::remove_dir_all(&config.path);
let store = Arc::new(SledBlockStore::new(config).expect("test: create block store"));
let bitswap = BitswapExchange::with_defaults(store).expect("test: create bitswap");
let cid1 = "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"
.parse::<Cid>()
.expect("test: parse cid");
let cid2 = "bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku"
.parse::<Cid>()
.expect("test: parse cid");
bitswap.want(cid1, 100).expect("test: add want");
bitswap.want(cid2, 50).expect("test: add want");
let wanted_cids = bitswap.get_wanted_cids();
assert_eq!(wanted_cids.len(), 2);
assert!(wanted_cids.contains(&cid1));
assert!(wanted_cids.contains(&cid2));
bitswap.cancel_want(&cid1).expect("test: cancel want");
assert!(!bitswap.is_wanted(&cid1));
assert!(bitswap.is_wanted(&cid2));
}
#[tokio::test]
async fn test_send_block_exists() {
let config = BlockStoreConfig {
path: std::env::temp_dir().join("ipfrs-test-bitswap-send-exists"),
cache_size: 100 * 1024 * 1024,
};
let _ = std::fs::remove_dir_all(&config.path);
let store = Arc::new(SledBlockStore::new(config).expect("test: create block store"));
let block = Block::new(vec![1, 2, 3, 4].into()).expect("test: create block");
let cid = *block.cid();
store.put(&block).await.expect("test: put block");
let bitswap = BitswapExchange::with_defaults(store).expect("test: create bitswap");
let peer_id = "peer1".to_string();
let msg = bitswap
.send_block(&peer_id, &cid)
.await
.expect("test: send block");
assert!(msg.is_some());
match msg.expect("test: message should be Some") {
Message::Block(block_msg) => {
assert_eq!(block_msg.cid, cid);
assert_eq!(block_msg.data.len(), 4);
}
_ => panic!("Expected Block message"),
}
}
#[tokio::test]
async fn test_send_block_not_found() {
let config = BlockStoreConfig {
path: std::env::temp_dir().join("ipfrs-test-bitswap-send-notfound"),
cache_size: 100 * 1024 * 1024,
};
let _ = std::fs::remove_dir_all(&config.path);
let store = Arc::new(SledBlockStore::new(config).expect("test: create block store"));
let bitswap = BitswapExchange::with_defaults(store).expect("test: create bitswap");
let cid = "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"
.parse::<Cid>()
.expect("test: parse cid");
let peer_id = "peer1".to_string();
let msg = bitswap
.send_block(&peer_id, &cid)
.await
.expect("test: send block");
assert!(msg.is_some());
match msg.expect("test: message should be Some") {
Message::DontHave(dont_have) => {
assert_eq!(dont_have.cid, cid);
}
_ => panic!("Expected DontHave message"),
}
}
#[tokio::test]
async fn test_cleanup_stale_wants() {
let config = BlockStoreConfig {
path: std::env::temp_dir().join("ipfrs-test-bitswap-cleanup"),
cache_size: 100 * 1024 * 1024,
};
let _ = std::fs::remove_dir_all(&config.path);
let store = Arc::new(SledBlockStore::new(config).expect("test: create block store"));
let mut bitswap_config = BitswapConfig::default();
bitswap_config.want_list.default_timeout = Duration::from_millis(1);
let bitswap =
BitswapExchange::new(store, bitswap_config).expect("test: create bitswap with config");
let cid = "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"
.parse::<Cid>()
.expect("test: parse cid");
bitswap.want(cid, 10).expect("test: add want");
assert!(bitswap.is_wanted(&cid));
tokio::time::sleep(Duration::from_millis(10)).await;
bitswap.cleanup();
assert!(!bitswap.is_wanted(&cid));
}
#[tokio::test]
async fn test_mark_request_sent() {
let config = BlockStoreConfig {
path: std::env::temp_dir().join("ipfrs-test-bitswap-mark-sent"),
cache_size: 100 * 1024 * 1024,
};
let _ = std::fs::remove_dir_all(&config.path);
let store = Arc::new(SledBlockStore::new(config).expect("test: create block store"));
let bitswap = BitswapExchange::with_defaults(store).expect("test: create bitswap");
let cid = "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"
.parse::<Cid>()
.expect("test: parse cid");
let peers = vec!["peer1".to_string(), "peer2".to_string()];
bitswap.mark_request_sent(cid, peers.clone());
let stats = bitswap.stats();
assert_eq!(stats.pending_requests, 1);
}
#[tokio::test]
async fn test_record_failure() {
let config = BlockStoreConfig {
path: std::env::temp_dir().join("ipfrs-test-bitswap-failure"),
cache_size: 100 * 1024 * 1024,
};
let _ = std::fs::remove_dir_all(&config.path);
let store = Arc::new(SledBlockStore::new(config).expect("test: create block store"));
let bitswap = BitswapExchange::with_defaults(store).expect("test: create bitswap");
let peer_id = "peer1".to_string();
bitswap.add_peer(peer_id.clone());
let cid = "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"
.parse::<Cid>()
.expect("test: parse cid");
bitswap.want(cid, 10).expect("test: add want");
bitswap.record_failure(&peer_id, &cid);
let stats = bitswap.stats();
assert!(stats.failed_requests >= 1);
}
#[tokio::test]
async fn test_multiple_peers_provider_selection() {
let config = BlockStoreConfig {
path: std::env::temp_dir().join("ipfrs-test-bitswap-multi-peers"),
cache_size: 100 * 1024 * 1024,
};
let _ = std::fs::remove_dir_all(&config.path);
let store = Arc::new(SledBlockStore::new(config).expect("test: create block store"));
let bitswap = BitswapExchange::with_defaults(store).expect("test: create bitswap");
let cid = "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"
.parse::<Cid>()
.expect("test: parse cid");
bitswap.add_peer("peer1".to_string());
bitswap.add_peer("peer2".to_string());
bitswap.add_peer("peer3".to_string());
bitswap.record_have(&"peer2".to_string(), cid);
let providers = bitswap.select_peers_for_request(&cid);
assert!(providers.contains(&"peer2".to_string()));
}
#[tokio::test]
async fn test_record_dont_have() {
let config = BlockStoreConfig {
path: std::env::temp_dir().join("ipfrs-test-bitswap-dont-have"),
cache_size: 100 * 1024 * 1024,
};
let _ = std::fs::remove_dir_all(&config.path);
let store = Arc::new(SledBlockStore::new(config).expect("test: create block store"));
let bitswap = BitswapExchange::with_defaults(store).expect("test: create bitswap");
let peer_id = "peer1".to_string();
bitswap.add_peer(peer_id.clone());
let cid = "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"
.parse::<Cid>()
.expect("test: parse cid");
bitswap.record_dont_have(&peer_id, cid);
let providers = bitswap.select_peers_for_request(&cid);
assert!(!providers.contains(&peer_id) || providers.is_empty());
}
#[tokio::test]
async fn test_get_want_list_message() {
let config = BlockStoreConfig {
path: std::env::temp_dir().join("ipfrs-test-bitswap-want-msg"),
cache_size: 100 * 1024 * 1024,
};
let _ = std::fs::remove_dir_all(&config.path);
let store = Arc::new(SledBlockStore::new(config).expect("test: create block store"));
let bitswap = BitswapExchange::with_defaults(store).expect("test: create bitswap");
let cid1 = "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"
.parse::<Cid>()
.expect("test: parse cid");
let cid2 = "bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku"
.parse::<Cid>()
.expect("test: parse cid");
bitswap.want(cid1, 100).expect("test: add want");
bitswap.want(cid2, 50).expect("test: add want");
let want_list = bitswap.get_want_list();
assert_eq!(want_list.len(), 2);
assert!(want_list.iter().any(|e| e.cid == cid1));
assert!(want_list.iter().any(|e| e.cid == cid2));
}
#[tokio::test]
async fn test_duplicate_want() {
let config = BlockStoreConfig {
path: std::env::temp_dir().join("ipfrs-test-bitswap-dup-want"),
cache_size: 100 * 1024 * 1024,
};
let _ = std::fs::remove_dir_all(&config.path);
let store = Arc::new(SledBlockStore::new(config).expect("test: create block store"));
let bitswap = BitswapExchange::with_defaults(store).expect("test: create bitswap");
let cid = "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"
.parse::<Cid>()
.expect("test: parse cid");
bitswap.want(cid, 10).expect("test: add want");
bitswap.want(cid, 20).expect("test: add want");
assert!(bitswap.is_wanted(&cid));
assert_eq!(bitswap.get_wanted_cids().len(), 1);
}
#[tokio::test]
async fn test_peer_removal() {
let config = BlockStoreConfig {
path: std::env::temp_dir().join("ipfrs-test-bitswap-peer-remove"),
cache_size: 100 * 1024 * 1024,
};
let _ = std::fs::remove_dir_all(&config.path);
let store = Arc::new(SledBlockStore::new(config).expect("test: create block store"));
let bitswap = BitswapExchange::with_defaults(store).expect("test: create bitswap");
let peer_id = "peer1".to_string();
bitswap.add_peer(peer_id.clone());
let stats = bitswap.stats();
assert_eq!(stats.num_peers, 1);
bitswap.remove_peer(&peer_id);
let stats = bitswap.stats();
assert_eq!(stats.num_peers, 0);
}
#[tokio::test]
async fn test_peer_scores() {
let config = BlockStoreConfig {
path: std::env::temp_dir().join("ipfrs-test-bitswap-scores"),
cache_size: 100 * 1024 * 1024,
};
let _ = std::fs::remove_dir_all(&config.path);
let store = Arc::new(SledBlockStore::new(config).expect("test: create block store"));
let bitswap = BitswapExchange::with_defaults(store).expect("test: create bitswap");
let peer_id = "peer1".to_string();
bitswap.add_peer(peer_id.clone());
let scores = bitswap.get_peer_scores();
assert!(scores.contains_key(&peer_id));
}
#[tokio::test]
async fn test_empty_want_list() {
let config = BlockStoreConfig {
path: std::env::temp_dir().join("ipfrs-test-bitswap-empty"),
cache_size: 100 * 1024 * 1024,
};
let _ = std::fs::remove_dir_all(&config.path);
let store = Arc::new(SledBlockStore::new(config).expect("test: create block store"));
let bitswap = BitswapExchange::with_defaults(store).expect("test: create bitswap");
let wanted_cids = bitswap.get_wanted_cids();
assert_eq!(wanted_cids.len(), 0);
let next = bitswap.next_want();
assert!(next.is_none());
let stats = bitswap.stats();
assert_eq!(stats.want_list_size, 0);
}
#[tokio::test]
async fn test_boost_deadline_priorities() {
let config = BlockStoreConfig {
path: std::env::temp_dir().join("ipfrs-test-bitswap-deadline"),
cache_size: 100 * 1024 * 1024,
};
let _ = std::fs::remove_dir_all(&config.path);
let store = Arc::new(SledBlockStore::new(config).expect("test: create block store"));
let bitswap = BitswapExchange::with_defaults(store).expect("test: create bitswap");
let cid = "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"
.parse::<Cid>()
.expect("test: parse cid");
bitswap.want(cid, 10).expect("test: add want");
bitswap.boost_deadline_priorities();
assert!(bitswap.is_wanted(&cid));
}
}