use crate::error::{Error, Result};
use crate::merkle::{consistency_proof, inclusion_proof, Hash, Roots};
use std::fs::{File, OpenOptions};
use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
const FILE_NAME: &str = "merkle.spine";
const MAGIC: [u8; 4] = *b"QMKL";
const VERSION: u8 = 1;
const HEADER: usize = MAGIC.len() + 1;
const ENTRY: usize = 32;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InclusionProof {
pub leaf_index: u64,
pub tree_size: u64,
pub leaf: Hash,
pub path: Vec<Hash>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConsistencyProof {
pub first_size: u64,
pub second_size: u64,
pub path: Vec<Hash>,
}
pub struct MerkleLog {
path: PathBuf,
writer: BufWriter<File>,
roots: Roots,
}
impl MerkleLog {
pub fn open(dir: &Path) -> Result<Self> {
let path = dir.join(FILE_NAME);
let leaves = load(&path)?;
let roots = Roots::from_leaves(&leaves);
let valid_len = (HEADER + leaves.len() * ENTRY) as u64;
let file = OpenOptions::new()
.create(true)
.truncate(false)
.read(true)
.write(true)
.open(&path)?;
if file.metadata()?.len() != valid_len {
if valid_len <= HEADER as u64 {
file.set_len(0)?;
let mut w = BufWriter::new(file);
w.write_all(&MAGIC)?;
w.write_all(&[VERSION])?;
w.flush()?;
let file = w.into_inner().map_err(|e| Error::Io(e.into_error()))?;
let mut writer = BufWriter::new(file);
writer.seek(SeekFrom::Start(HEADER as u64))?;
return Ok(Self {
path,
writer,
roots,
});
}
file.set_len(valid_len)?;
}
let mut writer = BufWriter::new(file);
writer.seek(SeekFrom::Start(valid_len))?;
Ok(Self {
path,
writer,
roots,
})
}
pub fn size(&self) -> u64 {
self.roots.size()
}
pub fn root(&self) -> Hash {
self.roots.root()
}
pub fn append(&mut self, leaf: Hash) -> Result<()> {
self.writer.write_all(&leaf)?;
self.roots.push(leaf);
Ok(())
}
pub fn flush(&mut self) -> Result<()> {
self.writer.flush()?;
Ok(())
}
pub fn leaves(&mut self) -> Result<Vec<Hash>> {
self.flush()?;
load(&self.path)
}
pub fn truncate(&mut self, new_size: u64) -> Result<()> {
self.flush()?;
let leaves = load(&self.path)?;
let n = leaves.len() as u64;
if new_size > n {
return Err(Error::Encode(format!(
"cannot truncate spine to {new_size}: only {n} leaves present"
)));
}
if new_size == n {
return Ok(());
}
let valid_len = HEADER as u64 + new_size * ENTRY as u64;
self.writer.flush()?;
self.writer.get_ref().set_len(valid_len)?;
self.writer.seek(SeekFrom::Start(valid_len))?;
self.roots = Roots::from_leaves(&leaves[..new_size as usize]);
Ok(())
}
pub fn sync(&mut self) -> Result<()> {
self.writer.flush()?;
self.writer.get_ref().sync_data()?;
Ok(())
}
pub fn prove_inclusion(&mut self, leaf_index: u64) -> Result<InclusionProof> {
self.flush()?;
let leaves = load(&self.path)?;
let n = leaves.len() as u64;
if leaf_index >= n {
return Err(Error::Encode(format!(
"leaf index {leaf_index} out of range for tree size {n}"
)));
}
let path = inclusion_proof(&leaves, leaf_index as usize);
Ok(InclusionProof {
leaf_index,
tree_size: n,
leaf: leaves[leaf_index as usize],
path,
})
}
pub fn prove_consistency(&mut self, first_size: u64) -> Result<ConsistencyProof> {
self.flush()?;
let leaves = load(&self.path)?;
let n = leaves.len() as u64;
if first_size > n {
return Err(Error::Encode(format!(
"prior size {first_size} exceeds tree size {n}"
)));
}
let path = consistency_proof(&leaves, first_size as usize);
Ok(ConsistencyProof {
first_size,
second_size: n,
path,
})
}
pub fn verify(&mut self) -> Result<Hash> {
self.flush()?;
let leaves = load(&self.path)?;
let on_disk = Roots::from_leaves(&leaves).root();
let live = self.roots.root();
if on_disk != live {
return Err(Error::Corrupt {
segment: self.path.display().to_string(),
offset: 0,
reason: "merkle spine root does not match its leaves — the spine was edited".into(),
});
}
Ok(live)
}
pub fn path(&self) -> &Path {
&self.path
}
}
fn load(path: &Path) -> Result<Vec<Hash>> {
let file = match File::open(path) {
Ok(f) => f,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(e) => return Err(e.into()),
};
let total = file.metadata()?.len();
if total < HEADER as u64 {
return Ok(Vec::new());
}
let mut reader = BufReader::new(file);
let mut head = [0u8; HEADER];
reader.read_exact(&mut head)?;
if head[0..4] != MAGIC {
return Err(Error::Corrupt {
segment: path.display().to_string(),
offset: 0,
reason: "bad magic (not a merkle spine file)".into(),
});
}
if head[4] != VERSION {
return Err(Error::Corrupt {
segment: path.display().to_string(),
offset: 0,
reason: format!(
"unsupported merkle spine version {} (this build reads {VERSION})",
head[4]
),
});
}
let count = (total - HEADER as u64) / ENTRY as u64;
let mut leaves = Vec::with_capacity(count as usize);
let mut buf = [0u8; ENTRY];
for _ in 0..count {
reader.read_exact(&mut buf)?;
leaves.push(buf);
}
Ok(leaves)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::merkle::{leaf_hash, mth, verify_consistency, verify_inclusion};
fn leaf(i: usize) -> Hash {
leaf_hash(format!("event-{i}").as_bytes())
}
#[test]
fn root_matches_reference_and_survives_reopen() {
let dir = tempfile::tempdir().unwrap();
let expected;
{
let mut log = MerkleLog::open(dir.path()).unwrap();
for i in 0..100 {
log.append(leaf(i)).unwrap();
}
log.sync().unwrap();
let leaves: Vec<Hash> = (0..100).map(leaf).collect();
expected = mth(&leaves);
assert_eq!(log.root(), expected);
assert_eq!(log.size(), 100);
}
let log = MerkleLog::open(dir.path()).unwrap();
assert_eq!(log.size(), 100);
assert_eq!(log.root(), expected);
}
#[test]
fn proofs_from_disk_verify_standalone() {
let dir = tempfile::tempdir().unwrap();
let mut log = MerkleLog::open(dir.path()).unwrap();
for i in 0..73 {
log.append(leaf(i)).unwrap();
}
log.sync().unwrap();
let root = log.root();
for i in 0..73u64 {
let p = log.prove_inclusion(i).unwrap();
assert!(verify_inclusion(
&p.leaf,
p.leaf_index as usize,
p.tree_size as usize,
&p.path,
&root
));
}
let earlier_leaves: Vec<Hash> = (0..40).map(leaf).collect();
let earlier_root = mth(&earlier_leaves);
let c = log.prove_consistency(40).unwrap();
assert!(verify_consistency(
c.first_size as usize,
c.second_size as usize,
&earlier_root,
&root,
&c.path
));
}
#[test]
fn torn_tail_is_recovered() {
let dir = tempfile::tempdir().unwrap();
{
let mut log = MerkleLog::open(dir.path()).unwrap();
for i in 0..10 {
log.append(leaf(i)).unwrap();
}
log.sync().unwrap();
}
{
let mut f = OpenOptions::new()
.append(true)
.open(dir.path().join(FILE_NAME))
.unwrap();
f.write_all(&[1, 2, 3, 4, 5]).unwrap();
}
let mut log = MerkleLog::open(dir.path()).unwrap();
assert_eq!(log.size(), 10);
log.append(leaf(10)).unwrap();
log.sync().unwrap();
let leaves: Vec<Hash> = (0..11).map(leaf).collect();
assert_eq!(log.root(), mth(&leaves));
}
#[test]
fn in_place_edit_of_spine_is_detected() {
let dir = tempfile::tempdir().unwrap();
let mut log = MerkleLog::open(dir.path()).unwrap();
for i in 0..20 {
log.append(leaf(i)).unwrap();
}
log.sync().unwrap();
assert!(log.verify().is_ok());
{
let mut f = OpenOptions::new()
.write(true)
.open(dir.path().join(FILE_NAME))
.unwrap();
f.seek(SeekFrom::Start((HEADER + 2 * ENTRY + 7) as u64))
.unwrap();
f.write_all(&[0xaa]).unwrap();
}
let tampered = MerkleLog::open(dir.path()).unwrap();
assert_ne!(tampered.root(), log.root());
assert!(log.verify().is_err());
}
}