use super::path_tree::{MerkleBatch, MerkleNode};
use crate::StorageError;
use redis::aio::ConnectionManager;
use redis::AsyncCommands;
use std::collections::BTreeMap;
use tracing::{debug, instrument};
const MERKLE_HASH_PREFIX: &str = "merkle:hash:";
const MERKLE_CHILDREN_PREFIX: &str = "merkle:children:";
#[derive(Clone)]
pub struct RedisMerkleStore {
conn: ConnectionManager,
prefix: String,
}
impl RedisMerkleStore {
pub fn new(conn: ConnectionManager) -> Self {
Self::with_prefix(conn, None)
}
pub fn with_prefix(conn: ConnectionManager, prefix: Option<&str>) -> Self {
Self {
conn,
prefix: prefix.unwrap_or("").to_string(),
}
}
#[inline]
fn prefixed_key(&self, suffix: &str) -> String {
if self.prefix.is_empty() {
suffix.to_string()
} else {
format!("{}{}", self.prefix, suffix)
}
}
pub fn key_prefix(&self) -> &str {
&self.prefix
}
#[instrument(skip(self))]
pub async fn get_hash(&self, path: &str) -> Result<Option<[u8; 32]>, StorageError> {
let key = self.prefixed_key(&format!("{}{}", MERKLE_HASH_PREFIX, path));
let mut conn = self.conn.clone();
let result: Option<String> = conn.get(&key).await.map_err(|e| {
StorageError::Backend(format!("Failed to get merkle hash: {}", e))
})?;
match result {
Some(hex_str) => {
let bytes = hex::decode(&hex_str).map_err(|e| {
StorageError::Backend(format!("Invalid merkle hash hex: {}", e))
})?;
if bytes.len() != 32 {
return Err(StorageError::Backend(format!(
"Invalid merkle hash length: {}",
bytes.len()
)));
}
let mut hash = [0u8; 32];
hash.copy_from_slice(&bytes);
Ok(Some(hash))
}
None => Ok(None),
}
}
#[instrument(skip(self))]
pub async fn get_children(
&self,
path: &str,
) -> Result<BTreeMap<String, [u8; 32]>, StorageError> {
let key = self.prefixed_key(&format!("{}{}", MERKLE_CHILDREN_PREFIX, path));
let mut conn = self.conn.clone();
let members: Vec<String> = conn.zrange(&key, 0, -1).await.map_err(|e| {
StorageError::Backend(format!("Failed to get merkle children: {}", e))
})?;
let mut children: BTreeMap<String, [u8; 32]> = BTreeMap::new();
for member in &members {
let member_str: &str = member.as_str();
if let Some((segment, hash_hex)) = member_str.split_once(':') {
let bytes = hex::decode(hash_hex).map_err(|e| {
StorageError::Backend(format!("Invalid child hash hex: {}", e))
})?;
if bytes.len() == 32 {
let mut hash = [0u8; 32];
hash.copy_from_slice(&bytes);
children.insert(segment.to_string(), hash);
}
}
}
Ok(children)
}
pub async fn get_node(&self, prefix: &str) -> Result<Option<MerkleNode>, StorageError> {
let hash = self.get_hash(prefix).await?;
match hash {
Some(h) => {
let children: BTreeMap<String, [u8; 32]> = self.get_children(prefix).await?;
Ok(Some(if children.is_empty() {
MerkleNode::leaf(h)
} else {
MerkleNode {
hash: h,
children,
is_leaf: false,
}
}))
}
None => Ok(None),
}
}
#[instrument(skip(self, batch), fields(batch_size = batch.len()))]
pub async fn apply_batch(&self, batch: &MerkleBatch) -> Result<(), StorageError> {
if batch.is_empty() {
return Ok(());
}
let mut conn = self.conn.clone();
let mut pipe = redis::pipe();
pipe.atomic();
for (object_id, maybe_hash) in &batch.leaves {
let hash_key = self.prefixed_key(&format!("{}{}", MERKLE_HASH_PREFIX, object_id));
match maybe_hash {
Some(hash) => {
let hex_str = hex::encode(hash);
pipe.set(&hash_key, &hex_str);
debug!(object_id = %object_id, "Setting leaf hash");
}
None => {
pipe.del(&hash_key);
debug!(object_id = %object_id, "Deleting leaf hash");
}
}
}
pipe.query_async::<()>(&mut conn).await.map_err(|e| {
StorageError::Backend(format!("Failed to apply merkle leaf updates: {}", e))
})?;
let affected_prefixes = batch.affected_prefixes();
for prefix in affected_prefixes {
self.recompute_interior_node(&prefix).await?;
}
Ok(())
}
#[instrument(skip(self))]
async fn recompute_interior_node(&self, prefix: &str) -> Result<(), StorageError> {
let mut conn = self.conn.clone();
let prefix_with_dot = if prefix.is_empty() {
String::new()
} else {
format!("{}.", prefix)
};
let scan_pattern = if prefix.is_empty() {
self.prefixed_key(&format!("{}*", MERKLE_HASH_PREFIX))
} else {
self.prefixed_key(&format!("{}{}.*", MERKLE_HASH_PREFIX, prefix))
};
let full_hash_prefix = self.prefixed_key(MERKLE_HASH_PREFIX);
let mut keys: Vec<String> = Vec::new();
let mut cursor = 0u64;
loop {
let (new_cursor, batch): (u64, Vec<String>) = redis::cmd("SCAN")
.arg(cursor)
.arg("MATCH")
.arg(&scan_pattern)
.arg("COUNT")
.arg(100) .query_async(&mut conn)
.await
.map_err(|e| StorageError::Backend(format!("Failed to scan merkle keys: {}", e)))?;
keys.extend(batch);
cursor = new_cursor;
if cursor == 0 {
break;
}
}
let mut direct_children: Vec<(String, String)> = Vec::new();
for key in &keys {
let path: &str = key.strip_prefix(&full_hash_prefix).unwrap_or(key.as_str());
let suffix: &str = if prefix.is_empty() {
path
} else {
match path.strip_prefix(&prefix_with_dot) {
Some(s) => s,
None => continue,
}
};
if let Some(segment) = suffix.split('.').next() {
if segment == suffix || !suffix.contains('.') {
direct_children.push((segment.to_string(), key.clone()));
}
}
}
if direct_children.is_empty() {
return Ok(());
}
let mut children: BTreeMap<String, [u8; 32]> = BTreeMap::new();
const MGET_CHUNK_SIZE: usize = 1000;
for chunk in direct_children.chunks(MGET_CHUNK_SIZE) {
let keys: Vec<String> = chunk.iter().map(|(_, k)| k.clone()).collect();
let segments: Vec<String> = chunk.iter().map(|(s, _)| s.clone()).collect();
let hex_hashes: Vec<Option<String>> = conn.mget(&keys).await.map_err(|e| {
StorageError::Backend(format!("Failed to batch get merkle hashes: {}", e))
})?;
for (i, maybe_hex) in hex_hashes.into_iter().enumerate() {
if let Some(hex_str) = maybe_hex {
if let Ok(bytes) = hex::decode(&hex_str) {
if bytes.len() == 32 {
let mut hash = [0u8; 32];
hash.copy_from_slice(&bytes);
children.insert(segments[i].clone(), hash);
}
}
}
}
}
if children.is_empty() {
return Ok(());
}
let node = MerkleNode::interior(children.clone());
let hash_hex = hex::encode(node.hash);
let hash_key = self.prefixed_key(&format!("{}{}", MERKLE_HASH_PREFIX, prefix));
let children_key = self.prefixed_key(&format!("{}{}", MERKLE_CHILDREN_PREFIX, prefix));
let mut pipe = redis::pipe();
pipe.atomic();
pipe.set(&hash_key, &hash_hex);
pipe.del(&children_key);
for (segment, hash) in &children {
let member = format!("{}:{}", segment, hex::encode(hash));
pipe.zadd(&children_key, &member, 0i64);
}
pipe.query_async::<()>(&mut conn).await.map_err(|e| {
StorageError::Backend(format!("Failed to update interior node: {}", e))
})?;
debug!(prefix = %prefix, children_count = children.len(), "Recomputed interior node");
Ok(())
}
pub async fn root_hash(&self) -> Result<Option<[u8; 32]>, StorageError> {
self.recompute_interior_node("").await?;
let key = self.prefixed_key(MERKLE_HASH_PREFIX);
let mut conn = self.conn.clone();
let result: Option<String> = conn.get(&key).await.map_err(|e| {
StorageError::Backend(format!("Failed to get root hash: {}", e))
})?;
match result {
Some(hex_str) => {
let bytes = hex::decode(&hex_str).map_err(|e| {
StorageError::Backend(format!("Invalid root hash hex: {}", e))
})?;
if bytes.len() != 32 {
return Err(StorageError::Backend(format!(
"Invalid root hash length: {}",
bytes.len()
)));
}
let mut hash = [0u8; 32];
hash.copy_from_slice(&bytes);
Ok(Some(hash))
}
None => Ok(None),
}
}
#[instrument(skip(self, their_children))]
pub async fn diff_children(
&self,
prefix: &str,
their_children: &BTreeMap<String, [u8; 32]>,
) -> Result<Vec<String>, StorageError> {
let our_children: BTreeMap<String, [u8; 32]> = self.get_children(prefix).await?;
let mut diffs = Vec::new();
let prefix_with_dot = if prefix.is_empty() {
String::new()
} else {
format!("{}.", prefix)
};
for (segment, our_hash) in &our_children {
match their_children.get(segment) {
Some(their_hash) if their_hash != our_hash => {
diffs.push(format!("{}{}", prefix_with_dot, segment));
}
None => {
diffs.push(format!("{}{}", prefix_with_dot, segment));
}
_ => {} }
}
for segment in their_children.keys() {
if !our_children.contains_key(segment) {
diffs.push(format!("{}{}", prefix_with_dot, segment));
}
}
Ok(diffs)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_key_prefixes() {
assert_eq!(
format!("{}{}", MERKLE_HASH_PREFIX, "uk.nhs.patient"),
"merkle:hash:uk.nhs.patient"
);
assert_eq!(
format!("{}{}", MERKLE_CHILDREN_PREFIX, "uk.nhs"),
"merkle:children:uk.nhs"
);
}
}