use blake2::{Blake2b512, Digest};
use ligerito_binary_fields::{BinaryElem32, BinaryFieldElement};
use crate::error::ZyncError;
pub const FIELDS_PER_HEADER: usize = 32;
pub const TIP_SENTINEL_SIZE: usize = 24;
#[derive(Clone, Debug, Default)]
pub struct EpochStateRoots {
pub epoch: u32,
pub height: u32,
pub sapling_root: String,
pub orchard_root: String,
pub nullifier_root: [u8; 32],
}
#[derive(Clone, Debug)]
pub struct TraceHeader {
pub height: u32,
pub hash: String,
pub prev_hash: String,
pub bits: String,
}
pub struct HeaderChainTrace {
pub trace: Vec<BinaryElem32>,
pub num_headers: usize,
pub start_height: u32,
pub end_height: u32,
pub initial_commitment: [u8; 32],
pub final_commitment: [u8; 32],
pub initial_state_commitment: [u8; 32],
pub final_state_commitment: [u8; 32],
pub cumulative_difficulty: u64,
pub tip_tree_root: [u8; 32],
pub tip_nullifier_root: [u8; 32],
}
pub fn encode_trace(
headers: &[TraceHeader],
state_roots: &[EpochStateRoots],
initial_commitment: [u8; 32],
initial_state_commitment: [u8; 32],
tip_tree_root: [u8; 32],
tip_nullifier_root: [u8; 32],
final_actions_commitment: [u8; 32],
) -> Result<HeaderChainTrace, ZyncError> {
if headers.is_empty() {
return Err(ZyncError::InvalidData("empty headers".into()));
}
let num_elements = headers.len() * FIELDS_PER_HEADER + TIP_SENTINEL_SIZE;
let trace_size = num_elements.next_power_of_two();
let mut trace = vec![BinaryElem32::zero(); trace_size];
let mut running_commitment = initial_commitment;
let mut state_commitment = initial_state_commitment;
let mut cumulative_difficulty: u64 = 0;
let state_root_map: std::collections::HashMap<u32, &EpochStateRoots> =
state_roots.iter().map(|r| (r.height, r)).collect();
for (i, header) in headers.iter().enumerate() {
let offset = i * FIELDS_PER_HEADER;
let block_hash = hex_to_bytes(&header.hash)?;
let prev_hash = if header.prev_hash.is_empty() {
if header.height != 0 {
return Err(ZyncError::InvalidData(format!(
"block {} has empty prev_hash (only genesis allowed)",
header.height
)));
}
vec![0u8; 32]
} else {
hex_to_bytes(&header.prev_hash)?
};
let nbits = if header.bits.is_empty() {
0u32
} else {
u32::from_str_radix(&header.bits, 16).unwrap_or(0)
};
let block_difficulty = nbits_to_difficulty(nbits);
cumulative_difficulty = cumulative_difficulty.saturating_add(block_difficulty);
trace[offset] = BinaryElem32::from(header.height);
for j in 0..8 {
trace[offset + 1 + j] = bytes_to_field(&block_hash[j * 4..(j + 1) * 4]);
}
for j in 0..8 {
trace[offset + 9 + j] = bytes_to_field(&prev_hash[j * 4..(j + 1) * 4]);
}
trace[offset + 17] = BinaryElem32::from(nbits);
trace[offset + 18] = BinaryElem32::from(cumulative_difficulty as u32);
running_commitment =
update_running_commitment(&running_commitment, &block_hash, &prev_hash, header.height);
trace[offset + 19] = bytes_to_field(&running_commitment[0..4]);
if let Some(roots) = state_root_map.get(&header.height) {
let sapling = if roots.sapling_root.is_empty() {
vec![0u8; 32]
} else {
hex_to_bytes(&roots.sapling_root)?
};
let orchard = if roots.orchard_root.is_empty() {
vec![0u8; 32]
} else {
hex_to_bytes(&roots.orchard_root)?
};
for j in 0..4 {
trace[offset + 20 + j] = bytes_to_field(&sapling[j * 4..(j + 1) * 4]);
}
for j in 0..4 {
trace[offset + 24 + j] = bytes_to_field(&orchard[j * 4..(j + 1) * 4]);
}
let nf_root = &roots.nullifier_root;
trace[offset + 28] = bytes_to_field(&nf_root[0..4]);
trace[offset + 29] = bytes_to_field(&nf_root[4..8]);
state_commitment = update_state_commitment(
&state_commitment,
&sapling,
&orchard,
nf_root,
header.height,
);
trace[offset + 30] = bytes_to_field(&state_commitment[0..4]);
} else {
trace[offset + 30] = bytes_to_field(&state_commitment[0..4]);
}
}
let sentinel_offset = headers.len() * FIELDS_PER_HEADER;
for j in 0..8 {
trace[sentinel_offset + j] = bytes_to_field(&tip_tree_root[j * 4..(j + 1) * 4]);
}
for j in 0..8 {
trace[sentinel_offset + 8 + j] =
bytes_to_field(&tip_nullifier_root[j * 4..(j + 1) * 4]);
}
for j in 0..8 {
trace[sentinel_offset + 16 + j] =
bytes_to_field(&final_actions_commitment[j * 4..(j + 1) * 4]);
}
Ok(HeaderChainTrace {
trace,
num_headers: headers.len(),
start_height: headers[0].height,
end_height: headers.last().unwrap().height,
initial_commitment,
final_commitment: running_commitment,
initial_state_commitment,
final_state_commitment: state_commitment,
cumulative_difficulty,
tip_tree_root,
tip_nullifier_root,
})
}
pub fn bytes_to_field(bytes: &[u8]) -> BinaryElem32 {
assert_eq!(bytes.len(), 4);
let value = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
BinaryElem32::from(value)
}
pub fn hex_to_bytes(hex: &str) -> Result<Vec<u8>, ZyncError> {
hex::decode(hex).map_err(|e| ZyncError::InvalidData(e.to_string()))
}
pub fn update_running_commitment(
prev_commitment: &[u8; 32],
block_hash: &[u8],
prev_hash: &[u8],
height: u32,
) -> [u8; 32] {
let mut hasher = Blake2b512::new();
hasher.update(b"ZIDECAR_header_commitment");
hasher.update(prev_commitment);
hasher.update(block_hash);
hasher.update(prev_hash);
hasher.update(height.to_le_bytes());
let hash = hasher.finalize();
let mut result = [0u8; 32];
result.copy_from_slice(&hash[..32]);
result
}
pub fn update_state_commitment(
prev_commitment: &[u8; 32],
sapling_root: &[u8],
orchard_root: &[u8],
nullifier_root: &[u8],
height: u32,
) -> [u8; 32] {
let mut hasher = Blake2b512::new();
hasher.update(b"ZIDECAR_state_commitment");
hasher.update(prev_commitment);
hasher.update(sapling_root);
hasher.update(orchard_root);
hasher.update(nullifier_root);
hasher.update(height.to_le_bytes());
let hash = hasher.finalize();
let mut result = [0u8; 32];
result.copy_from_slice(&hash[..32]);
result
}
pub fn parse_tree_root_bytes(final_state: &str) -> [u8; 32] {
use sha2::{Digest as Sha2Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(b"ZIDECAR_TREE_ROOT");
hasher.update(final_state.as_bytes());
hasher.finalize().into()
}
pub fn nbits_to_difficulty(nbits: u32) -> u64 {
if nbits == 0 {
return 0;
}
let exponent = (nbits >> 24) as u64;
let mantissa = (nbits & 0x00FFFFFF) as u64;
if mantissa == 0 {
return 0;
}
let shift = exponent.saturating_sub(3);
if shift < 32 {
let base_diff = (1u64 << 32) / mantissa;
base_diff >> (shift * 8).min(63)
} else {
1
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bytes_to_field() {
let bytes = [0x01, 0x02, 0x03, 0x04];
let field = bytes_to_field(&bytes);
assert_eq!(field.poly().value(), 0x04030201); }
#[test]
fn test_hex_to_bytes() {
let bytes = hex_to_bytes("deadbeef").unwrap();
assert_eq!(bytes, vec![0xde, 0xad, 0xbe, 0xef]);
}
#[test]
fn test_running_commitment_deterministic() {
let prev = [0u8; 32];
let block = [1u8; 32];
let prev_hash = [2u8; 32];
let c1 = update_running_commitment(&prev, &block, &prev_hash, 100);
let c2 = update_running_commitment(&prev, &block, &prev_hash, 100);
assert_eq!(c1, c2);
}
#[test]
fn test_nbits_to_difficulty() {
assert_eq!(nbits_to_difficulty(0), 0);
let d = nbits_to_difficulty(0x0400ffff);
assert!(d > 0);
let d2 = nbits_to_difficulty(0x04007fff);
assert_ne!(d, d2);
}
#[test]
fn test_encode_single_header() {
let headers = vec![TraceHeader {
height: 100,
hash: "00".repeat(32),
prev_hash: "00".repeat(32),
bits: "1d00ffff".into(),
}];
let trace = encode_trace(
&headers,
&[],
[0u8; 32],
[0u8; 32],
[0u8; 32],
[0u8; 32],
[0u8; 32],
)
.unwrap();
assert_eq!(trace.num_headers, 1);
assert_eq!(trace.start_height, 100);
assert_eq!(trace.end_height, 100);
assert!(trace.trace.len().is_power_of_two());
assert!(trace.trace.len() >= FIELDS_PER_HEADER + TIP_SENTINEL_SIZE);
}
}