use alloc::vec::Vec;
use alloy_core::rlp;
use alloy_trie::{
hash_builder::{HashBuilderValue, HashBuilderValueRef},
nodes::RlpNode,
HashBuilder, Nibbles, TrieMask,
};
use codec::{Decode, Encode};
use sp_core::H256;
const LOG_TARGET: &str = "runtime::revive::hash_builder";
pub struct IncrementalHashBuilder {
hash_builder: HashBuilder,
index: u64,
first_value: Option<Vec<u8>>,
#[cfg(test)]
stats: Option<HashBuilderStats>,
}
impl Default for IncrementalHashBuilder {
fn default() -> Self {
Self {
index: 1,
hash_builder: HashBuilder::default(),
first_value: None,
#[cfg(test)]
stats: None,
}
}
}
#[cfg(test)]
#[derive(Debug, Clone)]
pub struct HashBuilderStats {
pub total_data_size: usize,
pub hb_current_size: usize,
pub hb_max_size: (usize, u64),
pub largest_data: (usize, u64),
}
#[cfg(test)]
impl core::fmt::Display for HashBuilderStats {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
writeln!(
f,
" Total data processed: {} bytes ({:.2} MB)",
self.total_data_size,
self.total_data_size as f64 / 1_048_576.0
)?;
writeln!(
f,
" Current HB size: {} bytes ({:.2} KB)",
self.hb_current_size,
self.hb_current_size as f64 / 1024.0
)?;
writeln!(
f,
" Max HB size: {:?} ({:.2} KB at index {})",
self.hb_max_size,
self.hb_max_size.0 as f64 / 1024.0,
self.hb_max_size.1
)?;
writeln!(f, " Largest data item: {:?}", self.largest_data)?;
write!(
f,
" Memory efficiency: {:.4}% (current size vs total data)",
(self.hb_current_size as f64 / self.total_data_size as f64) * 100.0
)
}
}
impl IncrementalHashBuilder {
pub fn from_ir(serialized: IncrementalHashBuilderIR) -> Self {
let value = match serialized.value_type {
0 => {
let mut value = HashBuilderValue::new();
value.set_bytes_owned(serialized.builder_value);
value
},
1 => {
let buffer: alloy_core::primitives::B256 = serialized.builder_value[..]
.try_into()
.expect("The buffer was serialized properly; qed");
let value_ref = HashBuilderValueRef::Hash(&buffer);
let mut value = HashBuilderValue::new();
value.set_from_ref(value_ref);
value
},
_ => panic!("Value type was serialized properly; qed"),
};
let hash_builder = HashBuilder {
key: Nibbles::from_nibbles(serialized.key),
value,
stack: serialized
.stack
.into_iter()
.map(|raw| RlpNode::from_raw(&raw).expect("RlpNode was encoded properly; qed"))
.collect(),
state_masks: serialized
.state_masks
.into_iter()
.map(|mask| TrieMask::new(mask))
.collect(),
tree_masks: serialized.tree_masks.into_iter().map(|mask| TrieMask::new(mask)).collect(),
hash_masks: serialized.hash_masks.into_iter().map(|mask| TrieMask::new(mask)).collect(),
stored_in_database: serialized.stored_in_database,
updated_branch_nodes: None,
proof_retainer: None,
rlp_buf: serialized.rlp_buf,
};
IncrementalHashBuilder {
hash_builder,
index: serialized.index,
first_value: None,
#[cfg(test)]
stats: None,
}
}
pub fn to_ir(self) -> IncrementalHashBuilderIR {
IncrementalHashBuilderIR {
key: self.hash_builder.key.to_vec(),
value_type: match self.hash_builder.value.as_ref() {
HashBuilderValueRef::Bytes(_) => 0,
HashBuilderValueRef::Hash(_) => 1,
},
builder_value: self.hash_builder.value.as_slice().to_vec(),
stack: self.hash_builder.stack.into_iter().map(|n| n.as_slice().to_vec()).collect(),
state_masks: self.hash_builder.state_masks.into_iter().map(|mask| mask.get()).collect(),
tree_masks: self.hash_builder.tree_masks.into_iter().map(|mask| mask.get()).collect(),
hash_masks: self.hash_builder.hash_masks.into_iter().map(|mask| mask.get()).collect(),
stored_in_database: self.hash_builder.stored_in_database,
rlp_buf: self.hash_builder.rlp_buf,
index: self.index,
}
}
pub fn add_value(&mut self, value: Vec<u8>) {
let rlp_index = rlp::encode_fixed_size(&self.index);
self.hash_builder.add_leaf(Nibbles::unpack(&rlp_index), &value);
#[cfg(test)]
self.process_stats(value.len(), self.index);
if self.index == 0x7f {
let encoded_value = self
.first_value
.take()
.expect("First value must be set when processing index 127; qed");
log::trace!(target: LOG_TARGET, "Adding first value at index 0 while processing index 127");
let rlp_index = rlp::encode_fixed_size(&0usize);
self.hash_builder.add_leaf(Nibbles::unpack(&rlp_index), &encoded_value);
#[cfg(test)]
self.process_stats(value.len(), 0);
}
self.index = self.index.saturating_add(1);
}
pub fn set_first_value(&mut self, value: Vec<u8>) {
self.first_value = Some(value);
}
pub fn needs_first_value(&self, phase: BuilderPhase) -> bool {
match phase {
BuilderPhase::ProcessingValue => self.index == 0x7f,
BuilderPhase::Build => self.index < 0x7f,
}
}
pub fn finish(&mut self) -> H256 {
if let Some(encoded_value) = self.first_value.take() {
log::trace!(target: LOG_TARGET, "Adding first value at index 0 while building the trie");
let rlp_index = rlp::encode_fixed_size(&0usize);
self.hash_builder.add_leaf(Nibbles::unpack(&rlp_index), &encoded_value);
#[cfg(test)]
self.process_stats(encoded_value.len(), 0);
}
self.hash_builder.root().0.into()
}
#[cfg(test)]
fn calculate_current_size(&self) -> usize {
let masks_len = (self.hash_builder.state_masks.len() +
self.hash_builder.tree_masks.len() +
self.hash_builder.hash_masks.len()) *
2;
self.hash_builder.key.len() +
self.hash_builder.value.as_slice().len() +
self.hash_builder.stack.len() * 33 +
masks_len + self.hash_builder.rlp_buf.len()
}
#[cfg(test)]
fn process_stats(&mut self, data_len: usize, index: u64) {
if self.stats.is_none() {
return
}
let hb_current_size = self.calculate_current_size();
let stats = self.stats.as_mut().unwrap();
stats.total_data_size += data_len;
stats.hb_current_size = hb_current_size;
if hb_current_size > stats.hb_max_size.0 {
stats.hb_max_size = (hb_current_size, index);
}
if data_len > stats.largest_data.0 {
stats.largest_data = (data_len, index);
}
}
#[cfg(test)]
pub fn enable_stats(&mut self) {
let initial_size = self.calculate_current_size();
self.stats = Some(HashBuilderStats {
total_data_size: 0,
hb_current_size: initial_size,
hb_max_size: (initial_size, 0),
largest_data: (0, 0),
});
}
#[cfg(test)]
pub fn get_stats(&self) -> Option<&HashBuilderStats> {
self.stats.as_ref()
}
}
pub enum BuilderPhase {
ProcessingValue,
Build,
}
#[derive(Encode, Decode, scale_info::TypeInfo, Clone, PartialEq, Eq, Debug)]
pub struct IncrementalHashBuilderIR {
pub key: Vec<u8>,
pub value_type: u8,
pub builder_value: Vec<u8>,
pub stack: Vec<Vec<u8>>,
pub state_masks: Vec<u16>,
pub tree_masks: Vec<u16>,
pub hash_masks: Vec<u16>,
pub stored_in_database: bool,
pub rlp_buf: Vec<u8>,
pub index: u64,
}
impl IncrementalHashBuilderIR {
#[cfg(test)]
pub fn calculate_size(&self) -> usize {
let fixed_size = core::mem::size_of::<u64>() + core::mem::size_of::<u8>() + core::mem::size_of::<bool>();
let key_size = self.key.len();
let builder_value_size = self.builder_value.len();
let stack_size: usize = self.stack.iter().map(|item| item.len()).sum();
let state_masks_size = self.state_masks.len() * core::mem::size_of::<u16>();
let tree_masks_size = self.tree_masks.len() * core::mem::size_of::<u16>();
let hash_masks_size = self.hash_masks.len() * core::mem::size_of::<u16>();
let rlp_buf_size = self.rlp_buf.len();
let vec_overhead = 8 * core::mem::size_of::<usize>();
fixed_size +
key_size + builder_value_size +
stack_size +
state_masks_size +
tree_masks_size +
hash_masks_size +
rlp_buf_size +
vec_overhead
}
}
impl Default for IncrementalHashBuilderIR {
fn default() -> Self {
Self {
index: 1,
key: Vec::new(),
value_type: 0,
builder_value: Vec::new(),
stack: Vec::new(),
state_masks: Vec::new(),
tree_masks: Vec::new(),
hash_masks: Vec::new(),
stored_in_database: false,
rlp_buf: Vec::new(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hash_builder_stats() {
let mut builder = IncrementalHashBuilder::default();
builder.enable_stats();
let stats = builder.get_stats().expect("Stats should be enabled");
assert_eq!(stats.total_data_size, 0);
let initial_size = stats.hb_current_size;
assert_eq!(stats.hb_max_size, (initial_size, 0));
assert_eq!(stats.largest_data, (0, 0));
let _ = stats;
let test_data1 = vec![10; 500]; let test_data2 = vec![20; 700]; let test_data3 = vec![30; 300];
builder.set_first_value(test_data1.clone());
builder.add_value(test_data2.clone());
builder.add_value(test_data3.clone());
let _root = builder.finish();
let stats = builder.get_stats().expect("Stats should be enabled");
assert_eq!(stats.total_data_size, 1500);
assert_eq!(stats.hb_max_size.1, 2);
assert_eq!(stats.largest_data, (700, 1)); }
#[test]
fn test_hash_builder_without_stats() {
let mut builder = IncrementalHashBuilder::default();
assert!(builder.get_stats().is_none());
builder.add_value(vec![1, 2, 3]);
assert!(builder.get_stats().is_none());
}
#[test]
#[ignore]
fn test_stats_item_count_and_sizes() {
for (item_count, item_size) in [
(100 * 1024, 1024),
(1024, 1024 * 1024),
(5, 512),
] {
println!("\n=== Testing Hash Builder with {item_count} items ===");
let mut builder = IncrementalHashBuilder::default();
builder.enable_stats();
let initial_stats = builder.get_stats().unwrap();
println!("Initial size: {} bytes", initial_stats.hb_current_size);
let test_data = vec![42u8; item_size];
println!(
"Adding {} items of {} bytes ({:.2} KB) each...",
item_count,
item_size,
item_size as f64 / 1024.0
);
builder.set_first_value(test_data.clone());
for _ in 0..(item_count - 1) {
builder.add_value(test_data.clone());
}
let final_stats = builder.get_stats().unwrap().clone();
println!("\nFinal Stats - {item_count} Items of {item_size} bytes each:");
println!("{}", final_stats);
let builder_ir = builder.to_ir();
let ir_size = builder_ir.calculate_size();
println!(" Builder IR size: {ir_size} bytes ({} KB)", ir_size as f64 / 1024.0);
let expected_data_size = if item_count > 128 {
item_count * item_size
} else {
(item_count - 1) * item_size
};
assert_eq!(final_stats.total_data_size, expected_data_size);
assert!(final_stats.hb_current_size < final_stats.total_data_size);
}
}
#[test]
fn test_ir_size_calculation() {
println!("\n=== Testing IncrementalHashBuilderIR Size Calculation ===");
let mut builder = IncrementalHashBuilder::default();
builder.enable_stats();
println!("Initial builder state");
let initial_ir = builder.to_ir();
let initial_size = initial_ir.calculate_size();
println!("Initial IR size: {} bytes", initial_size);
builder = IncrementalHashBuilder::from_ir(initial_ir);
let test_data_small = vec![42u8; 100];
let test_data_large = vec![99u8; 2048];
builder.set_first_value(test_data_small.clone());
let ir_after_first = builder.to_ir();
let size_after_first = ir_after_first.calculate_size();
println!("IR size after first value: {} bytes", size_after_first);
builder = IncrementalHashBuilder::from_ir(ir_after_first.clone());
builder.add_value(test_data_large.clone());
let ir_after_add = builder.to_ir();
let size_after_add = ir_after_add.calculate_size();
println!("IR size after adding large value: {} bytes", size_after_add);
let restored_builder = IncrementalHashBuilder::from_ir(ir_after_add.clone());
let restored_ir = restored_builder.to_ir();
let restored_size = restored_ir.calculate_size();
println!("IR size after round-trip: {} bytes", restored_size);
assert!(size_after_add > size_after_first);
assert!(size_after_add > initial_size);
assert_eq!(size_after_add, restored_size);
assert!(restored_size > 0);
}
}