use std::{
collections::HashMap,
sync::{atomic::AtomicU64, Arc},
time::Duration,
};
use bimap::BiHashMap;
use chrono::{DateTime, TimeDelta, Utc};
use lazy_static::lazy_static;
use snops_common::state::{LatestBlockInfo, NodeKey};
use crate::state::GlobalState;
lazy_static! {
static ref TX_COUNTER: AtomicU64 = AtomicU64::new(0);
}
pub type ABlockHash = Arc<str>;
pub type ATransactionId = Arc<str>;
pub const MAX_BLOCK_RANGE: u32 = 10;
pub const MAX_CULL_AGE: TimeDelta = TimeDelta::seconds(3 * 60);
pub async fn invalidation_task(state: Arc<GlobalState>) {
loop {
for mut cache in state.env_network_cache.iter_mut() {
for hash in cache.stale_blocks() {
cache.remove_block(&hash);
}
}
tokio::time::sleep(Duration::from_secs(60)).await;
}
}
#[derive(Default)]
pub struct NetworkCache {
pub height_and_hash: BiHashMap<u32, ABlockHash>,
pub block_to_transaction: HashMap<ABlockHash, TransactionCache>,
pub transaction_to_block_hash: HashMap<ATransactionId, ABlockHash>,
pub blocks: HashMap<ABlockHash, LatestBlockInfo>,
pub external_peer_infos: HashMap<NodeKey, LatestBlockInfo>,
pub external_peer_record: HashMap<NodeKey, ResponsiveRecord>,
pub latest: Option<LatestBlockInfo>,
}
#[derive(Default)]
pub struct TransactionCache {
pub create_time: DateTime<Utc>,
pub entries: Vec<ATransactionId>,
}
impl NetworkCache {
pub fn update_latest_info(&mut self, info: &LatestBlockInfo) -> bool {
match &self.latest {
Some(prev) if prev.block_timestamp < info.block_timestamp => {
self.latest.replace(info.clone());
true
}
None => {
self.latest = Some(info.clone());
true
}
_ => false,
}
}
pub fn update_peer_req(&mut self, key: &NodeKey, success: bool) {
let record = self
.external_peer_record
.entry(key.clone())
.or_insert(ResponsiveRecord {
failed_attempts: 0,
total_successes: 0,
last_attempt: Utc::now(),
last_success: Utc::now(),
});
if success {
record.reward();
} else {
record.punish();
}
}
pub fn is_peer_penalized(&self, key: &NodeKey) -> bool {
self.external_peer_record
.get(key)
.map_or(false, ResponsiveRecord::has_penalty)
}
pub fn update_peer_info_for_hash(&mut self, key: &NodeKey, hash: &str) {
let Some(info) = self.blocks.get(hash) else {
return;
};
if self
.external_peer_infos
.get(key)
.is_some_and(|i| i.block_hash == hash)
{
return;
}
let mut info = info.clone();
info.update_time = Utc::now();
self.update_peer_info(key.clone(), info);
}
pub fn update_peer_info(&mut self, key: NodeKey, info: LatestBlockInfo) {
self.external_peer_infos.insert(key, info);
}
pub fn stale_blocks(&self) -> Vec<ABlockHash> {
let now = Utc::now();
self.blocks
.iter()
.filter_map(|(hash, info)| {
(info.update_time + MAX_CULL_AGE < now).then_some(Arc::clone(hash))
})
.collect()
}
pub fn add_block(&mut self, block_info: LatestBlockInfo, txs: Vec<ATransactionId>) {
let hash = Arc::from(block_info.block_hash.as_ref());
self.height_and_hash
.insert(block_info.height, Arc::clone(&hash));
for tx in &txs {
self.transaction_to_block_hash
.insert(Arc::clone(tx), Arc::clone(&hash));
}
self.block_to_transaction.insert(
Arc::clone(&hash),
TransactionCache {
create_time: block_info.update_time,
entries: txs,
},
);
self.blocks.insert(Arc::clone(&hash), block_info);
}
pub fn has_transactions_for_block(&self, block_hash: &str) -> bool {
self.block_to_transaction.contains_key(block_hash)
}
pub fn has_transaction(&self, tx_id: &str) -> bool {
self.transaction_to_block_hash.contains_key(tx_id)
}
pub fn is_recent_block(&self, height: u32) -> bool {
self.latest
.as_ref()
.is_some_and(|i| i.height.saturating_sub(MAX_BLOCK_RANGE) < height)
}
pub fn remove_block(&mut self, block_hash: &ABlockHash) {
self.height_and_hash.retain(|_, v| v != block_hash);
self.block_to_transaction.remove(block_hash);
self.transaction_to_block_hash
.retain(|_, v| v != block_hash);
self.blocks.remove(block_hash);
}
}
pub struct ResponsiveRecord {
pub failed_attempts: u32,
pub total_successes: u32,
pub last_attempt: DateTime<Utc>,
pub last_success: DateTime<Utc>,
}
impl ResponsiveRecord {
pub const MAX_PENALTY: u32 = 60 * 60;
pub fn penalty(&self) -> Option<TimeDelta> {
if self.failed_attempts == 0 {
return None;
}
Some(
(self.last_success - self.last_attempt)
.min(TimeDelta::seconds(Self::MAX_PENALTY as i64)),
)
}
pub fn has_penalty(&self) -> bool {
let Some(penalty) = self.penalty() else {
return false;
};
self.last_attempt + penalty > Utc::now()
}
pub fn punish(&mut self) {
self.failed_attempts += 1;
self.last_attempt = Utc::now();
}
pub fn reward(&mut self) {
self.total_successes += 1;
self.failed_attempts = 0;
self.last_success = Utc::now();
self.last_attempt = Utc::now();
}
}