1use crate::error::{Error, Result};
30use crate::merkle::{HASH_LEN, Hash, merkle_tree_hash};
31
32pub const TILE_WIDTH: u16 = 256;
34
35pub const MAX_LEVEL: u8 = 63;
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
45pub struct Tile {
46 level: u8,
47 index: u64,
48 width: u16,
49}
50
51impl Tile {
52 pub fn new(level: u8, index: u64, width: u16) -> Result<Self> {
58 if level > MAX_LEVEL {
59 return Err(Error::MalformedTile(format!(
60 "level {level} exceeds maximum {MAX_LEVEL}"
61 )));
62 }
63 if width == 0 || width > TILE_WIDTH {
64 return Err(Error::MalformedTile(format!(
65 "tile width {width} not in 1..=256"
66 )));
67 }
68 Ok(Self {
69 level,
70 index,
71 width,
72 })
73 }
74
75 #[must_use]
77 pub fn level(&self) -> u8 {
78 self.level
79 }
80
81 #[must_use]
83 pub fn index(&self) -> u64 {
84 self.index
85 }
86
87 #[must_use]
89 pub fn width(&self) -> u16 {
90 self.width
91 }
92
93 #[must_use]
95 pub fn is_partial(&self) -> bool {
96 self.width < TILE_WIDTH
97 }
98
99 #[must_use]
102 pub fn byte_len(&self) -> usize {
103 self.width as usize * HASH_LEN
104 }
105
106 #[must_use]
112 pub fn path(&self) -> String {
113 let mut p = format!("tile/{}/{}", self.level, encode_index(self.index));
114 if self.is_partial() {
115 p.push_str(&format!(".p/{}", self.width));
116 }
117 p
118 }
119
120 #[must_use]
124 pub fn entries_path(&self) -> String {
125 let mut p = format!("tile/entries/{}", encode_index(self.index));
126 if self.is_partial() {
127 p.push_str(&format!(".p/{}", self.width));
128 }
129 p
130 }
131
132 pub fn parse_path(path: &str) -> Result<Self> {
139 let rest = path
140 .strip_prefix("tile/")
141 .ok_or_else(|| Error::MalformedTile(format!("missing 'tile/' prefix: {path:?}")))?;
142
143 let (coords, width_override) = match rest.split_once(".p/") {
145 Some((coords, w)) => {
146 let w = parse_decimal_u16(w)?;
147 if !(1..TILE_WIDTH).contains(&w) {
148 return Err(Error::MalformedTile(format!(
149 "partial tile width {w} not in 1..=255"
150 )));
151 }
152 (coords, Some(w))
153 }
154 None => (rest, None),
155 };
156
157 let (level_str, index_str) = coords
158 .split_once('/')
159 .ok_or_else(|| Error::MalformedTile(format!("missing level/index: {path:?}")))?;
160
161 let level = parse_decimal_u8(level_str)?;
162 let index = decode_index(index_str)?;
163
164 let width = match width_override {
165 Some(w) => w,
166 None => TILE_WIDTH,
167 };
168 Self::new(level, index, width)
169 }
170
171 pub fn hashes(&self, bytes: &[u8]) -> Result<Vec<Hash>> {
177 if bytes.len() != self.byte_len() {
178 return Err(Error::MalformedTile(format!(
179 "tile byte length {} does not match width {} ({} bytes)",
180 bytes.len(),
181 self.width,
182 self.byte_len()
183 )));
184 }
185 Ok(bytes
186 .chunks_exact(HASH_LEN)
187 .map(|c| {
188 let mut h = [0u8; HASH_LEN];
189 h.copy_from_slice(c);
190 h
191 })
192 .collect())
193 }
194}
195
196#[must_use]
201pub fn partial_width(level: u8, size: u64) -> u16 {
202 if level >= 8 {
206 return 0;
207 }
208 let span = 256u128.pow(u32::from(level));
209 ((u128::from(size) / span) % 256) as u16
210}
211
212#[must_use]
222pub fn tiles_for_size(size: u64) -> Vec<Tile> {
223 let mut tiles = Vec::new();
224 if size == 0 {
225 return tiles;
226 }
227 for level in 0..=MAX_LEVEL {
228 let span = 256u128.pow(u32::from(level));
229 if u128::from(size) < span {
230 break;
233 }
234 let full = (u128::from(size) / (span * 256)) as u64;
235 for n in 0..full {
236 tiles.push(Tile {
237 level,
238 index: n,
239 width: TILE_WIDTH,
240 });
241 }
242 let partial = partial_width(level, size);
243 if partial > 0 {
244 tiles.push(Tile {
245 level,
246 index: full,
247 width: partial,
248 });
249 }
250 }
251 tiles
252}
253
254#[must_use]
262pub fn recompute_root(leaf_hashes: &[Hash]) -> Hash {
263 merkle_tree_hash(leaf_hashes)
264}
265
266pub fn parent_hash(tile_hashes: &[Hash]) -> Result<Hash> {
274 if tile_hashes.len() != TILE_WIDTH as usize {
275 return Err(Error::MalformedTile(format!(
276 "parent_hash requires a full 256-hash tile, got {}",
277 tile_hashes.len()
278 )));
279 }
280 Ok(merkle_tree_hash(tile_hashes))
281}
282
283fn encode_index(n: u64) -> String {
285 let mut parts = vec![format!("{:03}", n % 1000)];
286 let mut m = n / 1000;
287 while m > 0 {
288 parts.push(format!("x{:03}", m % 1000));
289 m /= 1000;
290 }
291 parts.reverse();
292 parts.join("/")
293}
294
295fn decode_index(s: &str) -> Result<u64> {
297 let parts: Vec<&str> = s.split('/').collect();
298 let malformed = || Error::MalformedTile(format!("malformed tile index path: {s:?}"));
299 let mut value: u64 = 0;
300 for (i, part) in parts.iter().enumerate() {
301 let is_last = i == parts.len() - 1;
302 let digits = if is_last {
303 part
304 } else {
305 part.strip_prefix('x').ok_or_else(malformed)?
306 };
307 if digits.len() != 3 || !digits.bytes().all(|b| b.is_ascii_digit()) {
308 return Err(malformed());
309 }
310 let group: u64 = digits.parse().map_err(|_| malformed())?;
311 value = value
312 .checked_mul(1000)
313 .and_then(|v| v.checked_add(group))
314 .ok_or_else(malformed)?;
315 }
316 Ok(value)
317}
318
319fn parse_decimal_u8(s: &str) -> Result<u8> {
321 check_no_leading_zero(s)?;
322 s.parse::<u8>()
323 .map_err(|_| Error::MalformedTile(format!("invalid decimal byte: {s:?}")))
324}
325
326fn parse_decimal_u16(s: &str) -> Result<u16> {
328 check_no_leading_zero(s)?;
329 s.parse::<u16>()
330 .map_err(|_| Error::MalformedTile(format!("invalid decimal: {s:?}")))
331}
332
333fn check_no_leading_zero(s: &str) -> Result<()> {
335 if s.is_empty() || !s.bytes().all(|b| b.is_ascii_digit()) {
336 return Err(Error::MalformedTile(format!(
337 "not a decimal integer: {s:?}"
338 )));
339 }
340 if s.len() > 1 && s.starts_with('0') {
341 return Err(Error::MalformedTile(format!(
342 "leading zero not allowed: {s:?}"
343 )));
344 }
345 Ok(())
346}
347
348#[cfg(all(test, not(target_arch = "wasm32")))]
349mod tests {
350 use super::*;
351 use crate::merkle::{MerkleTree, hash_leaf};
352 use proptest::prelude::*;
353
354 #[test]
355 fn index_encoding_spec_example() {
356 assert_eq!(encode_index(1_234_067), "x001/x234/067");
358 assert_eq!(encode_index(0), "000");
359 assert_eq!(encode_index(67), "067");
360 assert_eq!(encode_index(1234), "x001/234");
361 }
362
363 #[test]
364 fn path_round_trip_full_and_partial() {
365 let full = Tile::new(2, 1_234_067, TILE_WIDTH).unwrap();
366 assert_eq!(full.path(), "tile/2/x001/x234/067");
367 assert_eq!(Tile::parse_path(&full.path()).unwrap(), full);
368
369 let partial = Tile::new(1, 5, 17).unwrap();
370 assert_eq!(partial.path(), "tile/1/005.p/17");
371 assert_eq!(Tile::parse_path(&partial.path()).unwrap(), partial);
372 }
373
374 #[test]
375 fn parse_rejects_bad_paths() {
376 assert!(Tile::parse_path("tile/64/000").is_err()); assert!(Tile::parse_path("tile/00/000").is_err()); assert!(Tile::parse_path("tile/0/00").is_err()); assert!(Tile::parse_path("tile/0/000.p/256").is_err()); assert!(Tile::parse_path("tile/0/000.p/0").is_err()); assert!(Tile::parse_path("tile/0/1234/067").is_err()); assert!(Tile::parse_path("checkpoint").is_err());
383 }
384
385 #[test]
386 fn partial_width_does_not_overflow_for_high_levels() {
387 assert_eq!(partial_width(8, u64::MAX), 0);
389 assert_eq!(partial_width(MAX_LEVEL, u64::MAX), 0);
390 }
391
392 #[test]
393 fn partial_width_spec_example() {
394 let size = 70_000;
396 assert_eq!(partial_width(0, size), 112); assert_eq!(partial_width(1, size), 17); assert_eq!(partial_width(2, size), 1); }
400
401 #[test]
402 fn tiles_for_size_70000_matches_spec() {
403 let tiles = tiles_for_size(70_000);
406 let l0: Vec<_> = tiles.iter().filter(|t| t.level == 0).collect();
407 let l1: Vec<_> = tiles.iter().filter(|t| t.level == 1).collect();
408 let l2: Vec<_> = tiles.iter().filter(|t| t.level == 2).collect();
409
410 assert_eq!(l0.iter().filter(|t| !t.is_partial()).count(), 273);
411 assert_eq!(l0.iter().filter(|t| t.is_partial()).count(), 1);
412 assert_eq!(l0.last().unwrap().width, 112);
413
414 assert_eq!(l1.iter().filter(|t| !t.is_partial()).count(), 1);
415 assert_eq!(l1.last().unwrap().width, 17);
416
417 assert_eq!(l2.len(), 1);
418 assert_eq!(l2[0].width, 1);
419 }
420
421 #[test]
422 fn tree_of_size_256_has_partial_level1_width_1() {
423 let tiles = tiles_for_size(256);
426 assert_eq!(tiles.len(), 2);
427 assert_eq!((tiles[0].level, tiles[0].width), (0, 256));
428 assert_eq!((tiles[1].level, tiles[1].width), (1, 1));
429 }
430
431 #[test]
432 fn parent_hash_matches_oracle_subtree_root() {
433 let mut tree = MerkleTree::new();
436 let leaves: Vec<Hash> = (0u32..256).map(|i| hash_leaf(&i.to_be_bytes())).collect();
437 for h in &leaves {
438 tree.push_leaf_hash(*h);
439 }
440 let oracle_root = tree.root();
441 assert_eq!(parent_hash(&leaves).unwrap(), oracle_root);
442 assert!(parent_hash(&leaves[..255]).is_err());
443 }
444
445 #[test]
446 fn recompute_root_matches_oracle() {
447 let mut tree = MerkleTree::new();
448 let mut leaf_hashes = Vec::new();
449 for i in 0u32..1000 {
450 let h = hash_leaf(&i.to_be_bytes());
451 leaf_hashes.push(h);
452 tree.push_leaf_hash(h);
453 }
454 assert_eq!(recompute_root(&leaf_hashes), tree.root());
455 }
456
457 #[test]
458 fn hashes_validates_length() {
459 let t = Tile::new(0, 0, 2).unwrap();
460 assert!(t.hashes(&[0u8; 64]).is_ok());
461 assert!(t.hashes(&[0u8; 63]).is_err());
462 assert!(t.hashes(&[0u8; 96]).is_err());
463 }
464
465 proptest! {
466 #[test]
467 fn index_round_trip(n in 0u64..100_000_000) {
468 prop_assert_eq!(decode_index(&encode_index(n)).unwrap(), n);
469 }
470
471 #[test]
472 fn tile_path_round_trip(
473 level in 0u8..=MAX_LEVEL,
474 index in 0u64..10_000_000,
475 width in 1u16..=TILE_WIDTH,
476 ) {
477 let t = Tile::new(level, index, width).unwrap();
478 prop_assert_eq!(Tile::parse_path(&t.path()).unwrap(), t);
479 }
480 }
481}