use std::{
collections::{HashSet, VecDeque},
future,
sync::{Arc, Mutex},
task::{Context, Poll},
};
use futures::{future::BoxFuture, FutureExt};
use tower::{Service, ServiceExt};
use zakura_chain::transaction::UnminedTxId;
use crate::BoxError;
#[cfg(test)]
mod tests;
pub(super) const CACHE_CAPACITY: usize = 20_000;
const VERIFIER_LABEL: &str = "verifier";
const CACHE_HIT: &str = "zakura.consensus.cache.hit";
const CACHE_MISS: &str = "zakura.consensus.cache.miss";
const CACHE_INSERT: &str = "zakura.consensus.cache.insert";
const CACHE_EVICT: &str = "zakura.consensus.cache.evict";
const CACHE_SIZE: &str = "zakura.consensus.cache.size";
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub(super) enum ShieldedPool {
Sapling,
Orchard,
Ironwood,
}
impl From<orchard::ValuePool> for ShieldedPool {
fn from(pool: orchard::ValuePool) -> Self {
match pool {
orchard::ValuePool::Orchard => Self::Orchard,
orchard::ValuePool::Ironwood => Self::Ironwood,
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub(super) struct CacheKey {
tx_id: UnminedTxId,
sighash: [u8; 32],
pool: ShieldedPool,
}
impl CacheKey {
pub(super) fn new(tx_id: UnminedTxId, sighash: [u8; 32], pool: ShieldedPool) -> Self {
Self {
tx_id,
sighash,
pool,
}
}
}
pub(super) trait CachedItem {
fn cache_key(&self) -> Option<CacheKey>;
}
#[derive(Clone, Copy, Debug)]
struct InsertOutcome {
inserted: bool,
evicted: usize,
size: usize,
}
#[derive(Debug)]
struct VerifiedBundles {
keys: HashSet<CacheKey>,
insertion_order: VecDeque<CacheKey>,
capacity: usize,
}
impl VerifiedBundles {
fn new(capacity: usize) -> Self {
Self {
keys: HashSet::with_capacity(capacity),
insertion_order: VecDeque::with_capacity(capacity),
capacity,
}
}
fn contains(&self, key: &CacheKey) -> bool {
self.keys.contains(key)
}
fn insert(&mut self, key: CacheKey) -> InsertOutcome {
if !self.keys.insert(key) {
return InsertOutcome {
inserted: false,
evicted: 0,
size: self.keys.len(),
};
}
let mut evicted = 0;
while self.insertion_order.len() >= self.capacity {
let Some(oldest) = self.insertion_order.pop_front() else {
break;
};
self.keys.remove(&oldest);
evicted += 1;
}
self.insertion_order.push_back(key);
InsertOutcome {
inserted: true,
evicted,
size: self.keys.len(),
}
}
fn clear(&mut self) {
self.keys.clear();
self.insertion_order.clear();
}
}
impl InsertOutcome {
fn report(self, verifier_name: &'static str) {
if !self.inserted {
return;
}
metrics::counter!(CACHE_INSERT, VERIFIER_LABEL => verifier_name).increment(1);
if self.evicted > 0 {
metrics::counter!(CACHE_EVICT, VERIFIER_LABEL => verifier_name)
.increment(self.evicted as u64);
}
metrics::gauge!(CACHE_SIZE, VERIFIER_LABEL => verifier_name).set(self.size as f64);
}
}
pub struct Cached<S> {
inner: S,
verified: Arc<Mutex<VerifiedBundles>>,
verifier_name: &'static str,
#[cfg(test)]
inner_calls: Arc<Mutex<Vec<CacheKey>>>,
}
impl<S: Clone> Clone for Cached<S> {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
verified: self.verified.clone(),
verifier_name: self.verifier_name,
#[cfg(test)]
inner_calls: self.inner_calls.clone(),
}
}
}
impl<S> Cached<S> {
pub(super) fn new(inner: S, capacity: usize, verifier_name: &'static str) -> Self {
Self {
inner,
verified: Arc::new(Mutex::new(VerifiedBundles::new(capacity))),
verifier_name,
#[cfg(test)]
inner_calls: Arc::new(Mutex::new(Vec::new())),
}
}
pub(super) fn inner(&self) -> &S {
&self.inner
}
pub(super) fn clear(&self) {
self.verified
.lock()
.expect("verified bundle cache mutex should not be poisoned")
.clear();
metrics::gauge!(CACHE_SIZE, VERIFIER_LABEL => self.verifier_name).set(0.0);
}
#[cfg(test)]
pub(super) fn inner_calls_for<I: CachedItem>(&self, item: &I) -> usize {
let Some(key) = item.cache_key() else {
return 0;
};
self.inner_calls
.lock()
.expect("inner call record mutex should not be poisoned")
.iter()
.filter(|called| **called == key)
.count()
}
#[cfg(test)]
pub(super) fn with_inner<T>(&self, inner: T) -> Cached<T> {
Cached {
inner,
verified: self.verified.clone(),
verifier_name: self.verifier_name,
inner_calls: self.inner_calls.clone(),
}
}
}
impl<S, I> Service<I> for Cached<S>
where
I: CachedItem + Send + 'static,
S: Service<I, Response = (), Error = BoxError> + Clone + Send + 'static,
S::Future: Send + 'static,
{
type Response = ();
type Error = BoxError;
type Future = BoxFuture<'static, Result<(), BoxError>>;
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, item: I) -> Self::Future {
let key = item.cache_key();
if let Some(key) = key {
if self
.verified
.lock()
.expect("verified bundle cache mutex should not be poisoned")
.contains(&key)
{
metrics::counter!(CACHE_HIT, VERIFIER_LABEL => self.verifier_name).increment(1);
return future::ready(Ok(())).boxed();
}
}
metrics::counter!(CACHE_MISS, VERIFIER_LABEL => self.verifier_name).increment(1);
let verified = self.verified.clone();
let verifier_name = self.verifier_name;
let mut inner = self.inner.clone();
#[cfg(test)]
let inner_calls = self.inner_calls.clone();
async move {
let result = match inner.ready().await {
Ok(inner) => {
#[cfg(test)]
if let Some(key) = key {
inner_calls
.lock()
.expect("inner call record mutex should not be poisoned")
.push(key);
}
inner.call(item).await
}
Err(error) => Err(error),
};
if let (Ok(()), Some(key)) = (&result, key) {
let outcome = verified
.lock()
.expect("verified bundle cache mutex should not be poisoned")
.insert(key);
outcome.report(verifier_name);
}
result
}
.boxed()
}
}