prikk_store/
state_root.rs1use std::collections::BTreeSet;
4
5use prikk_error::{PrikkError, Result};
6use prikk_hash::sha256;
7use prikk_object::{MerkleRoot, NodeId, NodeKind, ObjectId};
8
9use crate::node_lifecycle::{NodeContent, NodeLifecycleState};
10use crate::path::{RepoPath, validate_no_path_collisions};
11
12const LEAF_DOMAIN: &[u8] = b"PRIKK-STATE-LEAF-v2";
13const NODE_DOMAIN: &[u8] = b"PRIKK-STATE-NODE-v2";
14const ROOT_DOMAIN: &[u8] = b"PRIKK-STATE-ROOT-v2";
15const REGULAR_MODE: u32 = 0o100644;
16const EXECUTABLE_MODE: u32 = 0o100755;
17
18#[derive(Debug, Clone, PartialEq, Eq)]
20pub enum StateRootContent {
21 Blob(ObjectId),
23 Symlink(String),
25}
26
27#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct StateRootEntry {
30 pub path: RepoPath,
32 pub node_id: NodeId,
34 pub kind: NodeKind,
36 pub mode: u32,
38 pub content: StateRootContent,
40}
41
42pub fn state_leaf_preimage(entry: &StateRootEntry) -> Result<Vec<u8>> {
44 validate_entry(entry)?;
45 let path = entry.path.as_str().as_bytes();
46 let path_len = u32::try_from(path.len())
47 .map_err(|_| PrikkError::Integrity("state-root path length exceeds u32".to_string()))?;
48 let content = content_bytes(entry);
49 let content_len = u64::try_from(content.len())
50 .map_err(|_| PrikkError::Integrity("state-root content length exceeds u64".to_string()))?;
51 let mut preimage =
52 Vec::with_capacity(LEAF_DOMAIN.len() + 4 + path.len() + 32 + 2 + 4 + 8 + content.len());
53 preimage.extend_from_slice(LEAF_DOMAIN);
54 preimage.extend_from_slice(&path_len.to_be_bytes());
55 preimage.extend_from_slice(path);
56 preimage.extend_from_slice(entry.node_id.as_bytes());
57 preimage.extend_from_slice(&entry.kind.code().to_be_bytes());
58 preimage.extend_from_slice(&entry.mode.to_be_bytes());
59 preimage.extend_from_slice(&content_len.to_be_bytes());
60 preimage.extend_from_slice(content);
61 Ok(preimage)
62}
63
64pub fn state_leaf_hash(entry: &StateRootEntry) -> Result<[u8; 32]> {
66 Ok(sha256(&state_leaf_preimage(entry)?))
67}
68
69pub fn compute_state_root(entries: &[StateRootEntry]) -> Result<MerkleRoot> {
71 validate_entries(entries)?;
72 let count = u64::try_from(entries.len())
73 .map_err(|_| PrikkError::Integrity("state-root entry count exceeds u64".to_string()))?;
74 if entries.is_empty() {
75 let mut preimage = Vec::with_capacity(ROOT_DOMAIN.len() + 8);
76 preimage.extend_from_slice(ROOT_DOMAIN);
77 preimage.extend_from_slice(&count.to_be_bytes());
78 return Ok(MerkleRoot(sha256(&preimage)));
79 }
80 let mut level = entries
81 .iter()
82 .map(state_leaf_hash)
83 .collect::<Result<Vec<_>>>()?;
84 while level.len() > 1 {
85 let mut next = Vec::with_capacity(level.len().div_ceil(2));
86 for pair in level.chunks(2) {
87 match pair {
88 [left, right] => {
89 let mut preimage = Vec::with_capacity(NODE_DOMAIN.len() + 64);
90 preimage.extend_from_slice(NODE_DOMAIN);
91 preimage.extend_from_slice(left);
92 preimage.extend_from_slice(right);
93 next.push(sha256(&preimage));
94 }
95 [single] => next.push(*single),
96 _ => {}
97 }
98 }
99 level = next;
100 }
101 let top = level.first().ok_or_else(|| {
102 PrikkError::Integrity("non-empty state-root reduction produced no hash".to_string())
103 })?;
104 let mut preimage = Vec::with_capacity(ROOT_DOMAIN.len() + 8 + 32);
105 preimage.extend_from_slice(ROOT_DOMAIN);
106 preimage.extend_from_slice(&count.to_be_bytes());
107 preimage.extend_from_slice(top);
108 Ok(MerkleRoot(sha256(&preimage)))
109}
110
111pub(crate) fn entries_from_state(state: &NodeLifecycleState) -> Result<Vec<StateRootEntry>> {
112 let mut entries = state
113 .live_nodes()
114 .map(|(node_id, node)| {
115 let (mode, content) = match &node.content {
116 NodeContent::File { blob_id, mode } => (*mode, StateRootContent::Blob(*blob_id)),
117 NodeContent::Symlink { target } => (0, StateRootContent::Symlink(target.clone())),
118 };
119 StateRootEntry {
120 path: node.path.clone(),
121 node_id: *node_id,
122 kind: node.kind,
123 mode,
124 content,
125 }
126 })
127 .collect::<Vec<_>>();
128 entries.sort_by(|left, right| {
129 left.path
130 .as_str()
131 .as_bytes()
132 .cmp(right.path.as_str().as_bytes())
133 });
134 validate_entries(&entries)?;
135 Ok(entries)
136}
137
138fn validate_entries(entries: &[StateRootEntry]) -> Result<()> {
139 let paths = entries
140 .iter()
141 .map(|entry| entry.path.clone())
142 .collect::<Vec<_>>();
143 validate_no_path_collisions(&paths)?;
144 if !entries.windows(2).all(
145 |pair| matches!(pair, [left, right] if left.path.as_str().as_bytes() < right.path.as_str().as_bytes()),
146 ) {
147 return Err(PrikkError::Integrity(
148 "state-root entries are not in strict canonical path order".to_string(),
149 ));
150 }
151 let mut node_ids = BTreeSet::new();
152 for entry in entries {
153 validate_entry(entry)?;
154 if !node_ids.insert(entry.node_id) {
155 return Err(PrikkError::Integrity(
156 "state-root entries contain a duplicate node_id".to_string(),
157 ));
158 }
159 }
160 Ok(())
161}
162
163fn validate_entry(entry: &StateRootEntry) -> Result<()> {
164 if entry.node_id.is_zero() {
165 return Err(PrikkError::Integrity(
166 "state-root entry node_id must be nonzero".to_string(),
167 ));
168 }
169 match (&entry.kind, &entry.content, entry.mode) {
170 (
171 NodeKind::TextFile | NodeKind::BinaryFile,
172 StateRootContent::Blob(_),
173 REGULAR_MODE | EXECUTABLE_MODE,
174 ) => Ok(()),
175 (NodeKind::Symlink, StateRootContent::Symlink(_), 0) => Ok(()),
176 _ => Err(PrikkError::Integrity(
177 "state-root entry kind, content, or normalized mode is invalid".to_string(),
178 )),
179 }
180}
181
182fn content_bytes(entry: &StateRootEntry) -> &[u8] {
183 match &entry.content {
184 StateRootContent::Blob(blob_id) => blob_id.as_bytes(),
185 StateRootContent::Symlink(target) => target.as_bytes(),
186 }
187}
188
189#[cfg(test)]
190mod tests;