use crate::{HashOutput, ScannerImpl, log::ScannerLogInner};
use luct_core::{
store::{AsyncOrderedStoreRead, AsyncStoreRead, Hashable, MemoryStore, StoreBase},
tiling::{TileId, TilingError},
tree::{Node, NodeKey, ProofValidationError, Tree, TreeHead},
v1::{MerkleTreeLeaf, SignedCertificateTimestamp, SignedTreeHead},
};
use luct_store::LruCacheStore;
use std::{
fmt::{self, Debug},
sync::{
Arc,
atomic::{AtomicU64, Ordering},
},
};
pub(crate) struct TileFetcher<S: ScannerImpl>(
#[allow(clippy::type_complexity)]
Tree<LruCacheStore<TileFetchStore<S>>, MemoryStore<u64, SignedCertificateTimestamp>>,
);
impl<S: ScannerImpl> Debug for TileFetcher<S> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("TileFetcher").field(&self.0).finish()
}
}
impl<S: ScannerImpl> TileFetcher<S> {
pub(crate) fn new(log: &Arc<ScannerLogInner<S>>) -> Self {
Self(Tree::new(
LruCacheStore::new(TileFetchStore::new(log.clone()), 1000),
MemoryStore::default(),
))
}
}
impl<S: ScannerImpl> TileFetcher<S> {
#[tracing::instrument(level = "trace")]
pub(crate) async fn check_sct_inclusion(
&self,
sct: &SignedCertificateTimestamp,
sth: &SignedTreeHead,
leaf: &MerkleTreeLeaf,
) -> Result<u64, TilingError> {
let Some(leaf_index) = sct.leaf_index() else {
return Err(TilingError::LeafIndexMissing);
};
let tree_head = TreeHead::from(sth);
tracing::debug!(
"Fetching audit proof for leaf index {:?} for tree size {}",
leaf_index,
tree_head.tree_size()
);
self.0.nodes().set_tree_size(tree_head.tree_size());
let audit_proof = self
.0
.get_audit_proof_async(&tree_head, *leaf_index)
.await
.map_err(TilingError::AuditProofGenerationError)?;
audit_proof
.validate(&tree_head, leaf)
.map_err(TilingError::AuditProofError)?;
Ok(audit_proof.index())
}
pub(crate) async fn check_sth_consistency(
&self,
old_sth: &SignedTreeHead,
new_sth: &SignedTreeHead,
) -> Result<(), TilingError> {
if old_sth.tree_size() > new_sth.tree_size() {
return Err(TilingError::ConsistencyProofError(
ProofValidationError::InvalidTreeSize {
expected: old_sth.tree_size(),
received: new_sth.tree_size(),
},
));
}
if old_sth.tree_size() == new_sth.tree_size() {
if old_sth.sha256_root_hash() == new_sth.sha256_root_hash() {
return Ok(());
} else {
return Err(TilingError::ConsistencyProofError(
ProofValidationError::HashMismatch,
));
}
}
let old_tree_head = TreeHead::from(old_sth);
let new_tree_head = TreeHead::from(new_sth);
tracing::debug!(
"Fetching extension proof from tree size {} to {}",
old_tree_head.tree_size(),
new_tree_head.tree_size()
);
self.0.nodes().set_tree_size(new_tree_head.tree_size());
let consistency_proof = self
.0
.get_consistency_proof_async(&old_tree_head, &new_tree_head)
.await
.map_err(TilingError::ConsistencyProofGenerationError)?;
consistency_proof
.validate(&old_tree_head, &new_tree_head)
.map_err(TilingError::ConsistencyProofError)?;
Ok(())
}
}
pub(crate) struct TileFetchStore<S: ScannerImpl> {
log: Arc<ScannerLogInner<S>>,
tree_size: AtomicU64,
}
impl<S: ScannerImpl> fmt::Debug for TileFetchStore<S> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TileFetchStore").finish()
}
}
impl<S: ScannerImpl> TileFetchStore<S> {
fn new(log: Arc<ScannerLogInner<S>>) -> Self {
Self {
log,
tree_size: AtomicU64::new(0),
}
}
fn set_tree_size(&self, tree_size: u64) {
self.tree_size.store(tree_size, Ordering::Release);
}
}
impl<S: ScannerImpl> StoreBase for TileFetchStore<S> {
type Key = NodeKey;
type Value = HashOutput;
}
impl<S: ScannerImpl> AsyncStoreRead for TileFetchStore<S> {
#[tracing::instrument(level = "trace")]
async fn get(&self, key: NodeKey) -> Option<HashOutput> {
let tree_size = self.tree_size.load(Ordering::Acquire);
if tree_size == 0 {
tracing::error!(
"Failed to retrieve STH for log {}. Initialize STH before checking inclusions",
self.log.name
);
return None;
}
tracing::trace!("Fetching key {:?} against tree size {}", key, tree_size);
let nodes = self.fetch_unbalanced_keys(&key, tree_size).await?;
let result = nodes
.iter()
.find(|(nk, _)| nk == &key)
.map(|(_, hash)| *hash)
.expect("Node was not included in result. This is a bug");
Some(result)
}
async fn len(&self) -> usize {
self.log
.sth_store
.last()
.await
.map(|last| last.1.tree_size() as usize)
.unwrap_or(0)
}
}
impl<S: ScannerImpl> TileFetchStore<S> {
async fn fetch_unbalanced_keys(
&self,
key: &NodeKey,
tree_size: u64,
) -> Option<Vec<(NodeKey, [u8; 32])>> {
let nodes = if key.is_balanced() {
tracing::trace!("Fetching balanced key: {:?}", key);
self.fetch_balanced_keys(key, tree_size).await?
} else {
let (left, right) = key.split();
tracing::trace!("Fetching balanced key: {:?}", left);
tracing::trace!("Fetching unbalanced key: {:?}", right);
let (left_nodes, right_nodes) = futures::join!(
self.fetch_balanced_keys(&left, tree_size),
Box::pin(self.fetch_unbalanced_keys(&right, tree_size)),
);
let mut left_nodes = left_nodes?;
let mut right_nodes = right_nodes?;
let left_hash = left_nodes.iter().find(|(key, _)| key == &left)?.1;
let right_hash = right_nodes.iter().find(|(key, _)| key == &right)?.1;
let hash = Node {
left: left_hash,
right: right_hash,
}
.hash();
left_nodes.append(&mut right_nodes);
left_nodes.push((key.clone(), hash));
tracing::trace!("Fetched unbalanced key: {:?}", key);
left_nodes
};
tracing::trace!("Fetched {} nodes", nodes.len());
Some(nodes)
}
async fn fetch_balanced_keys(
&self,
key: &NodeKey,
tree_size: u64,
) -> Option<Vec<(NodeKey, [u8; 32])>> {
let tile_id = TileId::from_node_key(key, tree_size)?;
let tile = self.log.client.get_tile(tile_id.clone()).await;
if tile.is_err() {
tracing::error!("Failed to fetch tile {:?}, reason: {:?}", tile_id, tile);
}
let tile = tile.ok()?;
let nodes = tile.recompute_node_keys();
tracing::trace!("Fetched balanced key: {:?}", key);
Some(nodes)
}
}