use std::{
fs::File,
io::Write as _,
path::{Path, PathBuf},
};
use commonware_storage::merkle::MAX_PINNED_NODES;
use sha2::{Digest as _, Sha256};
pub const MAGIC: [u8; 4] = *b"PFC1";
pub const FORMAT_REVISION: u8 = 1;
pub const MAX_PAYLOAD_BYTES: u64 = 64 * 1024 * 1024;
const PARTITION_DIGEST_LEN: usize = 32;
const INCARNATION_LEN: usize = 32;
const ROOT_LEN: usize = 32;
const SIGNATURE_LEN: usize = 64;
const SIGNER_LEN: usize = 32;
const PEAK_LEN: usize = 32;
const CHECKSUM_LEN: usize = 32;
const FIXED_PREFIX_LEN: usize = MAGIC.len()
+ 1 + PARTITION_DIGEST_LEN
+ INCARNATION_LEN
+ 8 + 8 + 8 + ROOT_LEN
+ 8 + SIGNATURE_LEN
+ SIGNER_LEN
+ 1 + 8 + 1;
pub const FILE_NAME: &str = "floorcheckpoint";
const TEMP_FILE_NAME: &str = "floorcheckpoint.writing";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FloorCheckpoint {
partition_digest: [u8; PARTITION_DIGEST_LEN],
incarnation: [u8; INCARNATION_LEN],
floor_position: u64,
floor_journal_position: u64,
floor_leaf_count: u64,
covering_root: [u8; ROOT_LEN],
covering_leaf_count: u64,
covering_signature: [u8; SIGNATURE_LEN],
covering_signer: [u8; SIGNER_LEN],
fence: Option<u64>,
pinned_peaks: Vec<[u8; PEAK_LEN]>,
payload: Vec<u8>,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum FloorCheckpointError {
#[error("floor checkpoint i/o failure: {0}")]
Io(#[from] std::io::Error),
#[error("floor checkpoint carries {found} pinned peaks, more than {max}")]
TooManyPinnedPeaks {
found: usize,
max: usize,
},
#[error("floor checkpoint payload is {found} byte(s), more than {max}")]
PayloadTooLarge {
found: u64,
max: u64,
},
#[error("floor checkpoint is {found} byte(s), shorter than the minimum {minimum}")]
TooShort {
minimum: usize,
found: usize,
},
#[error("floor checkpoint declares a length its own byte count does not match")]
LengthMismatch,
#[error("floor checkpoint carries another family's magic")]
WrongMagic,
#[error("floor checkpoint format revision {found} is not {expected}")]
UnsupportedRevision {
expected: u8,
found: u8,
},
#[error("floor checkpoint fails its checksum")]
ChecksumMismatch,
#[error("floor checkpoint was recorded for a different partition")]
WrongPartition,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FloorCheckpointOutcome {
Absent,
Present(Box<FloorCheckpoint>),
}
impl FloorCheckpoint {
#[allow(
clippy::too_many_arguments,
reason = "every field is load-bearing and named"
)]
pub fn try_new(
partition: &str,
incarnation: [u8; INCARNATION_LEN],
floor_position: u64,
floor_journal_position: u64,
floor_leaf_count: u64,
covering_root: [u8; ROOT_LEN],
covering_leaf_count: u64,
covering_signature: [u8; SIGNATURE_LEN],
covering_signer: [u8; SIGNER_LEN],
fence: Option<u64>,
pinned_peaks: Vec<[u8; PEAK_LEN]>,
payload: Vec<u8>,
) -> Result<Self, FloorCheckpointError> {
if pinned_peaks.len() > MAX_PINNED_NODES {
return Err(FloorCheckpointError::TooManyPinnedPeaks {
found: pinned_peaks.len(),
max: MAX_PINNED_NODES,
});
}
let payload_len = u64::try_from(payload.len()).unwrap_or(u64::MAX);
if payload_len > MAX_PAYLOAD_BYTES {
return Err(FloorCheckpointError::PayloadTooLarge {
found: payload_len,
max: MAX_PAYLOAD_BYTES,
});
}
Ok(Self {
partition_digest: digest_partition(partition),
incarnation,
floor_position,
floor_journal_position,
floor_leaf_count,
covering_root,
covering_leaf_count,
covering_signature,
covering_signer,
fence,
pinned_peaks,
payload,
})
}
#[must_use]
pub const fn incarnation(&self) -> [u8; INCARNATION_LEN] {
self.incarnation
}
#[must_use]
pub const fn floor_position(&self) -> u64 {
self.floor_position
}
#[must_use]
pub const fn floor_journal_position(&self) -> u64 {
self.floor_journal_position
}
#[must_use]
pub const fn floor_leaf_count(&self) -> u64 {
self.floor_leaf_count
}
#[must_use]
pub const fn covering_root(&self) -> [u8; ROOT_LEN] {
self.covering_root
}
#[must_use]
pub const fn covering_leaf_count(&self) -> u64 {
self.covering_leaf_count
}
#[must_use]
pub const fn covering_signature(&self) -> [u8; SIGNATURE_LEN] {
self.covering_signature
}
#[must_use]
pub const fn covering_signer(&self) -> [u8; SIGNER_LEN] {
self.covering_signer
}
#[must_use]
pub const fn fence(&self) -> Option<u64> {
self.fence
}
#[must_use]
pub fn pinned_peaks(&self) -> &[[u8; PEAK_LEN]] {
&self.pinned_peaks
}
#[must_use]
pub fn payload(&self) -> &[u8] {
&self.payload
}
fn to_bytes(&self) -> Vec<u8> {
let mut bound = Vec::with_capacity(
FIXED_PREFIX_LEN + self.pinned_peaks.len() * PEAK_LEN + 8 + self.payload.len(),
);
bound.extend_from_slice(&MAGIC);
bound.push(FORMAT_REVISION);
bound.extend_from_slice(&self.partition_digest);
bound.extend_from_slice(&self.incarnation);
bound.extend_from_slice(&self.floor_position.to_be_bytes());
bound.extend_from_slice(&self.floor_journal_position.to_be_bytes());
bound.extend_from_slice(&self.floor_leaf_count.to_be_bytes());
bound.extend_from_slice(&self.covering_root);
bound.extend_from_slice(&self.covering_leaf_count.to_be_bytes());
bound.extend_from_slice(&self.covering_signature);
bound.extend_from_slice(&self.covering_signer);
bound.push(u8::from(self.fence.is_some()));
bound.extend_from_slice(&self.fence.unwrap_or(0).to_be_bytes());
#[allow(
clippy::cast_possible_truncation,
reason = "try_new already refused more than MAX_PINNED_NODES (< 256) peaks"
)]
bound.push(self.pinned_peaks.len() as u8);
for peak in &self.pinned_peaks {
bound.extend_from_slice(peak);
}
let payload_len = u64::try_from(self.payload.len()).unwrap_or(u64::MAX);
bound.extend_from_slice(&payload_len.to_be_bytes());
bound.extend_from_slice(&self.payload);
let checksum = Sha256::digest(&bound);
bound.extend_from_slice(&checksum);
bound
}
#[allow(clippy::too_many_lines, reason = "one linear field-by-field decode")]
fn from_bytes(bytes: &[u8], partition: &str) -> Result<Self, FloorCheckpointError> {
let minimum = FIXED_PREFIX_LEN + 8 + CHECKSUM_LEN;
if bytes.len() < minimum {
return Err(FloorCheckpointError::TooShort {
minimum,
found: bytes.len(),
});
}
let (bound, checksum) = bytes.split_at(bytes.len() - CHECKSUM_LEN);
let computed = Sha256::digest(bound);
if computed.as_slice() != checksum {
return Err(FloorCheckpointError::ChecksumMismatch);
}
if bound.len() < FIXED_PREFIX_LEN {
return Err(FloorCheckpointError::LengthMismatch);
}
let mut at = 0;
if bound[at..at + MAGIC.len()] != MAGIC {
return Err(FloorCheckpointError::WrongMagic);
}
at += MAGIC.len();
let revision = bound[at];
at += 1;
if revision != FORMAT_REVISION {
return Err(FloorCheckpointError::UnsupportedRevision {
expected: FORMAT_REVISION,
found: revision,
});
}
let partition_digest: [u8; PARTITION_DIGEST_LEN] = bound[at..at + PARTITION_DIGEST_LEN]
.try_into()
.expect("slice is exactly PARTITION_DIGEST_LEN bytes");
at += PARTITION_DIGEST_LEN;
if partition_digest != digest_partition(partition) {
return Err(FloorCheckpointError::WrongPartition);
}
let incarnation: [u8; INCARNATION_LEN] = bound[at..at + INCARNATION_LEN]
.try_into()
.expect("slice is exactly INCARNATION_LEN bytes");
at += INCARNATION_LEN;
let floor_position = read_u64(bound, &mut at);
let floor_journal_position = read_u64(bound, &mut at);
let floor_leaf_count = read_u64(bound, &mut at);
let covering_root: [u8; ROOT_LEN] = bound[at..at + ROOT_LEN]
.try_into()
.expect("slice is exactly ROOT_LEN bytes");
at += ROOT_LEN;
let covering_leaf_count = read_u64(bound, &mut at);
let covering_signature: [u8; SIGNATURE_LEN] = bound[at..at + SIGNATURE_LEN]
.try_into()
.expect("slice is exactly SIGNATURE_LEN bytes");
at += SIGNATURE_LEN;
let covering_signer: [u8; SIGNER_LEN] = bound[at..at + SIGNER_LEN]
.try_into()
.expect("slice is exactly SIGNER_LEN bytes");
at += SIGNER_LEN;
let fence_present = bound[at] != 0;
at += 1;
let fence_value = read_u64(bound, &mut at);
let fence = fence_present.then_some(fence_value);
let peak_count = usize::from(bound[at]);
at += 1;
let expected_len = at
.checked_add(peak_count.saturating_mul(PEAK_LEN))
.and_then(|len| len.checked_add(8))
.ok_or(FloorCheckpointError::LengthMismatch)?;
if bound.len() < expected_len {
return Err(FloorCheckpointError::LengthMismatch);
}
let mut pinned_peaks = Vec::with_capacity(peak_count);
for _ in 0..peak_count {
let peak: [u8; PEAK_LEN] = bound[at..at + PEAK_LEN]
.try_into()
.expect("slice is exactly PEAK_LEN bytes");
pinned_peaks.push(peak);
at += PEAK_LEN;
}
let payload_len = read_u64(bound, &mut at);
let payload_len_usize = usize::try_from(payload_len).map_err(|_| {
FloorCheckpointError::LengthMismatch
})?;
if bound.len() != at + payload_len_usize {
return Err(FloorCheckpointError::LengthMismatch);
}
let payload = bound[at..at + payload_len_usize].to_vec();
Ok(Self {
partition_digest,
incarnation,
floor_position,
floor_journal_position,
floor_leaf_count,
covering_root,
covering_leaf_count,
covering_signature,
covering_signer,
fence,
pinned_peaks,
payload,
})
}
pub fn write_atomic(&self, dir: &Path) -> Result<(), FloorCheckpointError> {
std::fs::create_dir_all(dir)?;
let bytes = self.to_bytes();
let temp = dir.join(TEMP_FILE_NAME);
{
let mut file = File::create(&temp)?;
file.write_all(&bytes)?;
file.sync_all()?;
}
std::fs::rename(&temp, dir.join(FILE_NAME))?;
File::open(dir)?.sync_all()?;
Ok(())
}
pub fn read(
dir: &Path,
partition: &str,
) -> Result<FloorCheckpointOutcome, FloorCheckpointError> {
let path: PathBuf = dir.join(FILE_NAME);
let bytes = match std::fs::read(&path) {
Ok(bytes) => bytes,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
return Ok(FloorCheckpointOutcome::Absent);
}
Err(error) => return Err(error.into()),
};
Self::from_bytes(&bytes, partition)
.map(|checkpoint| FloorCheckpointOutcome::Present(Box::new(checkpoint)))
}
pub fn invalidate(dir: &Path) -> Result<(), FloorCheckpointError> {
match std::fs::remove_file(dir.join(FILE_NAME)) {
Ok(()) => {}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
Err(error) => return Err(error.into()),
}
File::open(dir)?.sync_all()?;
Ok(())
}
}
fn read_u64(bound: &[u8], at: &mut usize) -> u64 {
let value = u64::from_be_bytes(
bound[*at..*at + 8]
.try_into()
.expect("slice is exactly 8 bytes"),
);
*at += 8;
value
}
fn digest_partition(partition: &str) -> [u8; PARTITION_DIGEST_LEN] {
Sha256::digest(partition.as_bytes()).into()
}
#[cfg(test)]
mod tests {
use super::*;
fn sample(partition: &str, peaks: usize, payload: &[u8]) -> FloorCheckpoint {
FloorCheckpoint::try_new(
partition,
[7_u8; 32],
42,
45,
40,
[9_u8; 32],
41,
[3_u8; SIGNATURE_LEN],
[4_u8; SIGNER_LEN],
Some(5),
(0..peaks)
.map(|i| [u8::try_from(i).expect("test peak counts stay below 256"); PEAK_LEN])
.collect(),
payload.to_vec(),
)
.expect("a within-bounds checkpoint builds")
}
#[test]
fn a_fresh_directory_reads_as_absent_not_an_error() {
let dir = tempfile::tempdir().expect("temp dir");
let outcome = FloorCheckpoint::read(dir.path(), "conv-1").expect("read");
assert_eq!(outcome, FloorCheckpointOutcome::Absent);
}
#[test]
fn a_written_checkpoint_survives_a_round_trip_with_peaks_and_payload() {
let dir = tempfile::tempdir().expect("temp dir");
let written = sample("conv-1", 5, b"folded-receipts-and-intents");
written.write_atomic(dir.path()).expect("write");
let outcome = FloorCheckpoint::read(dir.path(), "conv-1").expect("read");
assert_eq!(outcome, FloorCheckpointOutcome::Present(Box::new(written)));
}
#[test]
fn empty_peaks_and_empty_payload_round_trip_too() {
let dir = tempfile::tempdir().expect("temp dir");
let written = sample("conv-1", 0, b"");
written.write_atomic(dir.path()).expect("write");
let outcome = FloorCheckpoint::read(dir.path(), "conv-1").expect("read");
assert_eq!(outcome, FloorCheckpointOutcome::Present(Box::new(written)));
}
#[test]
fn a_second_write_replaces_the_first_rather_than_appending() {
let dir = tempfile::tempdir().expect("temp dir");
sample("conv-1", 3, b"first")
.write_atomic(dir.path())
.expect("write");
let second = sample("conv-1", 1, b"second-and-different-length-payload");
second.write_atomic(dir.path()).expect("write");
let outcome = FloorCheckpoint::read(dir.path(), "conv-1").expect("read");
assert_eq!(outcome, FloorCheckpointOutcome::Present(Box::new(second)));
assert!(!dir.path().join(TEMP_FILE_NAME).exists());
}
#[test]
fn too_many_pinned_peaks_refuses_construction() {
let error = FloorCheckpoint::try_new(
"conv-1",
[0; 32],
0,
0,
0,
[0; 32],
0,
[0; SIGNATURE_LEN],
[0; SIGNER_LEN],
None,
vec![[0; PEAK_LEN]; MAX_PINNED_NODES + 1],
vec![],
)
.expect_err("too many peaks must refuse");
assert!(matches!(
error,
FloorCheckpointError::TooManyPinnedPeaks { found, max }
if found == MAX_PINNED_NODES + 1 && max == MAX_PINNED_NODES
));
}
#[test]
fn a_payload_past_the_ceiling_refuses_construction() {
let error = FloorCheckpoint::try_new(
"conv-1",
[0; 32],
0,
0,
0,
[0; 32],
0,
[0; SIGNATURE_LEN],
[0; SIGNER_LEN],
None,
vec![],
vec![0_u8; usize::try_from(MAX_PAYLOAD_BYTES).unwrap() + 1],
)
.expect_err("an oversized payload must refuse");
assert!(matches!(
error,
FloorCheckpointError::PayloadTooLarge { .. }
));
}
#[test]
fn a_truncated_record_is_refused_not_read_as_shorter_or_empty() {
let dir = tempfile::tempdir().expect("temp dir");
sample("conv-1", 2, b"payload")
.write_atomic(dir.path())
.expect("write");
let mut bytes = std::fs::read(dir.path().join(FILE_NAME)).expect("read raw");
bytes.truncate(bytes.len() - 1);
std::fs::write(dir.path().join(FILE_NAME), &bytes).expect("write truncated");
let error = FloorCheckpoint::read(dir.path(), "conv-1").expect_err("must refuse");
assert!(
matches!(error, FloorCheckpointError::ChecksumMismatch),
"a byte dropped from the end changes the checksummed region; got {error}"
);
}
#[test]
fn a_record_shorter_than_the_fixed_prefix_is_refused() {
let dir = tempfile::tempdir().expect("temp dir");
std::fs::create_dir_all(dir.path()).unwrap();
std::fs::write(dir.path().join(FILE_NAME), [0_u8; 4]).expect("write short file");
let error = FloorCheckpoint::read(dir.path(), "conv-1").expect_err("must refuse");
assert!(matches!(error, FloorCheckpointError::TooShort { .. }));
}
#[test]
fn a_flipped_checksum_byte_is_refused() {
let dir = tempfile::tempdir().expect("temp dir");
sample("conv-1", 2, b"payload")
.write_atomic(dir.path())
.expect("write");
let mut bytes = std::fs::read(dir.path().join(FILE_NAME)).expect("read raw");
let last = bytes.len() - 1;
bytes[last] ^= 0xFF;
std::fs::write(dir.path().join(FILE_NAME), &bytes).expect("write flipped");
let error = FloorCheckpoint::read(dir.path(), "conv-1").expect_err("must refuse");
assert!(matches!(error, FloorCheckpointError::ChecksumMismatch));
}
#[test]
fn a_flipped_body_byte_is_refused_by_the_checksum() {
let dir = tempfile::tempdir().expect("temp dir");
sample("conv-1", 2, b"payload")
.write_atomic(dir.path())
.expect("write");
let mut bytes = std::fs::read(dir.path().join(FILE_NAME)).expect("read raw");
bytes[10] ^= 0xFF;
std::fs::write(dir.path().join(FILE_NAME), &bytes).expect("write flipped");
let error = FloorCheckpoint::read(dir.path(), "conv-1").expect_err("must refuse");
assert!(matches!(error, FloorCheckpointError::ChecksumMismatch));
}
#[test]
fn another_familys_magic_is_refused() {
let dir = tempfile::tempdir().expect("temp dir");
sample("conv-1", 0, b"")
.write_atomic(dir.path())
.expect("write");
let mut bytes = std::fs::read(dir.path().join(FILE_NAME)).expect("read raw");
bytes[0] ^= 0xFF;
let checksum = Sha256::digest(&bytes[..bytes.len() - CHECKSUM_LEN]);
let new_len = bytes.len() - CHECKSUM_LEN;
bytes[new_len..].copy_from_slice(&checksum);
std::fs::write(dir.path().join(FILE_NAME), &bytes).expect("write foreign magic");
let error = FloorCheckpoint::read(dir.path(), "conv-1").expect_err("must refuse");
assert!(matches!(error, FloorCheckpointError::WrongMagic));
}
#[test]
fn an_unsupported_revision_is_refused() {
let dir = tempfile::tempdir().expect("temp dir");
sample("conv-1", 0, b"")
.write_atomic(dir.path())
.expect("write");
let mut bytes = std::fs::read(dir.path().join(FILE_NAME)).expect("read raw");
bytes[MAGIC.len()] = FORMAT_REVISION + 1;
let checksum = Sha256::digest(&bytes[..bytes.len() - CHECKSUM_LEN]);
let new_len = bytes.len() - CHECKSUM_LEN;
bytes[new_len..].copy_from_slice(&checksum);
std::fs::write(dir.path().join(FILE_NAME), &bytes).expect("write future revision");
let error = FloorCheckpoint::read(dir.path(), "conv-1").expect_err("must refuse");
assert!(matches!(
error,
FloorCheckpointError::UnsupportedRevision {
found: 2,
expected: 1
}
));
}
#[test]
fn a_valid_artifact_for_a_different_partition_is_refused() {
let dir = tempfile::tempdir().expect("temp dir");
sample("conv-1", 0, b"")
.write_atomic(dir.path())
.expect("write");
let error = FloorCheckpoint::read(dir.path(), "conv-2").expect_err("must refuse");
assert!(matches!(error, FloorCheckpointError::WrongPartition));
}
#[test]
fn an_internally_inconsistent_length_is_refused_not_misread() {
let dir = tempfile::tempdir().expect("temp dir");
let written = sample("conv-1", 2, b"payload");
written.write_atomic(dir.path()).expect("write");
let mut bytes = std::fs::read(dir.path().join(FILE_NAME)).expect("read raw");
let peak_count_offset = FIXED_PREFIX_LEN - 1;
bytes[peak_count_offset] = 60; let checksum = Sha256::digest(&bytes[..bytes.len() - CHECKSUM_LEN]);
let new_len = bytes.len() - CHECKSUM_LEN;
bytes[new_len..].copy_from_slice(&checksum);
std::fs::write(dir.path().join(FILE_NAME), &bytes).expect("write inconsistent length");
let error = FloorCheckpoint::read(dir.path(), "conv-1").expect_err("must refuse");
assert!(matches!(error, FloorCheckpointError::LengthMismatch));
}
#[test]
fn invalidate_removes_a_present_checkpoint_and_is_a_no_op_when_absent() {
let dir = tempfile::tempdir().expect("temp dir");
assert_eq!(
FloorCheckpoint::read(dir.path(), "conv-1").unwrap(),
FloorCheckpointOutcome::Absent
);
FloorCheckpoint::invalidate(dir.path()).expect("invalidate an absent checkpoint");
sample("conv-1", 1, b"x")
.write_atomic(dir.path())
.expect("write");
assert!(matches!(
FloorCheckpoint::read(dir.path(), "conv-1").unwrap(),
FloorCheckpointOutcome::Present(_)
));
FloorCheckpoint::invalidate(dir.path()).expect("invalidate a present checkpoint");
assert_eq!(
FloorCheckpoint::read(dir.path(), "conv-1").unwrap(),
FloorCheckpointOutcome::Absent
);
}
#[test]
fn accessors_return_exactly_what_was_bound() {
let checkpoint = sample("conv-1", 2, b"payload");
assert_eq!(checkpoint.incarnation(), [7_u8; 32]);
assert_eq!(checkpoint.floor_position(), 42);
assert_eq!(checkpoint.floor_journal_position(), 45);
assert_eq!(checkpoint.floor_leaf_count(), 40);
assert_eq!(checkpoint.covering_root(), [9_u8; 32]);
assert_eq!(checkpoint.covering_leaf_count(), 41);
assert_eq!(checkpoint.covering_signature(), [3_u8; SIGNATURE_LEN]);
assert_eq!(checkpoint.covering_signer(), [4_u8; SIGNER_LEN]);
assert_eq!(checkpoint.fence(), Some(5));
assert_eq!(
checkpoint.pinned_peaks(),
&[[0_u8; PEAK_LEN], [1_u8; PEAK_LEN]]
);
assert_eq!(checkpoint.payload(), b"payload");
}
#[test]
fn an_absent_fence_round_trips_as_absent() {
let dir = tempfile::tempdir().expect("temp dir");
let checkpoint = FloorCheckpoint::try_new(
"conv-1",
[1; 32],
1,
1,
1,
[1; 32],
1,
[1; SIGNATURE_LEN],
[1; SIGNER_LEN],
None,
vec![],
vec![],
)
.unwrap();
checkpoint.write_atomic(dir.path()).expect("write");
let FloorCheckpointOutcome::Present(read_back) =
FloorCheckpoint::read(dir.path(), "conv-1").unwrap()
else {
panic!("expected a present checkpoint");
};
assert_eq!(read_back.fence(), None);
}
}