1use crate::api::{compute_hash_from_hex, get_record_by_hash};
2use crate::error::{ProvableError, Result};
3use crate::lightnet::{
4 get_merkle_proof, get_record_by_data_item, verify_hash_batch, verify_hash_existence,
5};
6use crate::merkle_proof::normalize_merkle_proof;
7use crate::types::{
8 BatchExistenceCheckResult, ComputeHashRequest, HashBatchRequest, HashExistenceRequest,
9 LevelCheckResult, NormalizedKayrosRecord, NormalizedMerkleProof,
10 VerifyMerkleProofWithDetailsRequest, VerifyMerkleProofWithDetailsResult, VerifyRequest,
11 VerifyResult, VerifyResultDetails, VerifyWithInclusionRequest,
12};
13use crate::util::{
14 is_zero_hash, normalize_hex_string, normalize_level_counts, timeuuid_hex_to_timestamp,
15 uuid_string_to_hex,
16};
17use sha2::{Digest, Sha256};
18use sha3::Sha3_256;
19
20const ZERO_HASH_32: &str = "0000000000000000000000000000000000000000000000000000000000000000";
21const DEFAULT_LEVELS_HASH_TYPE: &str = "sha3-256";
22
23#[derive(Clone)]
24struct VerifyCoreState {
25 request: CanonicalVerifyRequest,
26 record: NormalizedKayrosRecord,
27 details: VerifyResultDetails,
28}
29
30#[derive(Clone, Default)]
31struct CanonicalVerifyRequest {
32 data_type: String,
33 data_item: Option<String>,
34 kayros_hash: Option<String>,
35 api_key: Option<String>,
36}
37
38pub fn verify(request: VerifyRequest) -> VerifyResult {
39 match verify_record_core(request) {
40 Ok((result, _)) => result,
41 Err(result) => result,
42 }
43}
44
45pub fn verify_with_inclusion(request: VerifyWithInclusionRequest) -> VerifyResult {
46 let (_, state) = match verify_record_core(request.verify_request) {
47 Ok(pair) => pair,
48 Err(result) => return result,
49 };
50 let Some(mut state) = state else {
51 return VerifyResult {
52 valid: true,
53 error: None,
54 details: None,
55 };
56 };
57
58 let levels_hash_type = match normalize_levels_hash_type(request.levels_hash_type.as_deref()) {
59 Ok(value) => value,
60 Err(error) => return invalid_result(state.details, &error.0),
61 };
62 state.details.levels_hash_type = Some(levels_hash_type.clone());
63
64 let proof_response = match get_merkle_proof(
65 &state.request.data_type,
66 Some(&state.record.kayros_hash),
67 None,
68 state.request.api_key.as_deref(),
69 ) {
70 Ok(response) => response,
71 Err(error) => {
72 return invalid_result(
73 state.details,
74 &format!("Failed to fetch merkle proof: {}", error.0),
75 )
76 }
77 };
78
79 let proof = match normalize_merkle_proof(proof_response) {
80 Ok(proof) => proof,
81 Err(error) => return invalid_result(state.details, &error.0),
82 };
83
84 state.details.proof_fetched = Some(true);
85 state.details.proof = Some(proof.clone());
86 state.details.proof_data_type_match = Some(
87 proof.data_type == state.request.data_type
88 || utf8_hex(&proof.data_type) == utf8_hex(&state.request.data_type),
89 );
90 state.details.proof_hash_item_match = Some(proof.hash_item == state.record.kayros_hash);
91 if state.details.proof_data_type_match != Some(true) {
92 return invalid_result(
93 state.details,
94 &format!(
95 "Proof data_type mismatch: expected={} proof={}",
96 state.request.data_type, proof.data_type
97 ),
98 );
99 }
100 if state.details.proof_hash_item_match != Some(true) {
101 return invalid_result(
102 state.details,
103 &format!(
104 "Proof hash_item mismatch: expected={} proof={}",
105 state.record.kayros_hash, proof.hash_item
106 ),
107 );
108 }
109
110 let level_counts =
111 match normalize_level_counts(&proof.level_counts, proof.levels, proof.proof.len()) {
112 Ok(counts) => counts,
113 Err(error) => return invalid_result(state.details, &error.0),
114 };
115 let (pending, max_level, max_level_position, max_level_hash) =
116 proof_inclusion_meta(&proof, &level_counts);
117 state.details.pending = Some(pending);
118 state.details.max_level = Some(max_level);
119 state.details.max_level_position = Some(max_level_position);
120 state.details.max_level_hash = Some(max_level_hash.clone());
121
122 if let Err(error) =
123 verify_proof_target_position(&proof, &state.record.kayros_hash, &level_counts)
124 {
125 state.details.target_position_match = Some(false);
126 return invalid_result(state.details, &error.0);
127 }
128 state.details.target_position_match = Some(true);
129
130 if !pending {
131 match verify_proof_path(&proof, &level_counts, &levels_hash_type) {
132 Ok(root_hash) => {
133 state.details.proof_path_match = Some(true);
134 state.details.local_root_hash = Some(root_hash);
135 }
136 Err(error) => {
137 state.details.proof_path_match = Some(false);
138 return invalid_result(state.details, &error.0);
139 }
140 }
141 }
142
143 if let Some(trusted_root_hash) = request
144 .trusted_root_hash
145 .as_deref()
146 .and_then(normalize_hex_string)
147 {
148 if !pending {
149 state.details.trusted_root_match = Some(proof.root == trusted_root_hash);
150 if state.details.trusted_root_match != Some(true) {
151 return invalid_result(
152 state.details,
153 &format!(
154 "Root hash mismatch: proof={} trusted={}",
155 proof.root, trusted_root_hash
156 ),
157 );
158 }
159 }
160 }
161
162 if let (Some(trusted_level), Some(trusted_position)) =
163 (request.trusted_level, request.trusted_position)
164 {
165 let expected_hash = request
166 .trusted_root_hash
167 .as_deref()
168 .and_then(normalize_hex_string)
169 .or_else(|| {
170 proof_hash_at_level_position(&proof, &level_counts, trusted_level, trusted_position)
171 });
172 let Some(expected_hash) = expected_hash else {
173 return invalid_result(
174 state.details,
175 &format!(
176 "Missing proof hash at level={} position={}",
177 trusted_level, trusted_position
178 ),
179 );
180 };
181 match verify_hash_existence(
182 &HashExistenceRequest {
183 data_type: state.request.data_type.clone(),
184 level: trusted_level,
185 position: trusted_position,
186 hash: expected_hash.clone(),
187 },
188 state.request.api_key.as_deref(),
189 ) {
190 Ok(response) => {
191 let valid = response.exists
192 && response
193 .found_hash
194 .as_deref()
195 .and_then(normalize_hex_string)
196 .map(|hash| hash == expected_hash)
197 .unwrap_or(true);
198 state.details.trusted_level_match = Some(valid);
199 if !valid {
200 return invalid_result(
201 state.details,
202 response
203 .message
204 .as_deref()
205 .unwrap_or("Trusted level check failed"),
206 );
207 }
208 }
209 Err(error) => {
210 return invalid_result(
211 state.details,
212 &format!("Trusted level check failed: {}", error.0),
213 )
214 }
215 }
216 }
217
218 if !request.level_checks.is_empty() {
219 let mut results = Vec::new();
220 for check in request.level_checks {
221 let expected_hash = check
222 .hash
223 .as_deref()
224 .and_then(normalize_hex_string)
225 .or_else(|| {
226 proof_hash_at_level_position(&proof, &level_counts, check.level, check.position)
227 });
228 let Some(expected_hash) = expected_hash else {
229 return invalid_result(
230 state.details,
231 &format!(
232 "Missing proof hash at level={} position={}",
233 check.level, check.position
234 ),
235 );
236 };
237 match verify_hash_existence(
238 &HashExistenceRequest {
239 data_type: state.request.data_type.clone(),
240 level: check.level,
241 position: check.position,
242 hash: expected_hash.clone(),
243 },
244 state.request.api_key.as_deref(),
245 ) {
246 Ok(response) => {
247 let valid = response.exists
248 && response
249 .found_hash
250 .as_deref()
251 .and_then(normalize_hex_string)
252 .map(|hash| hash == expected_hash)
253 .unwrap_or(true);
254 results.push(LevelCheckResult {
255 level: check.level,
256 position: check.position,
257 hash: expected_hash.clone(),
258 valid,
259 exists: Some(response.exists),
260 found_hash: response
261 .found_hash
262 .as_deref()
263 .and_then(normalize_hex_string),
264 message: response.message.clone(),
265 });
266 if !valid {
267 state.details.level_checks = Some(results);
268 return invalid_result(
269 state.details,
270 response.message.as_deref().unwrap_or("Level check failed"),
271 );
272 }
273 }
274 Err(error) => {
275 results.push(LevelCheckResult {
276 level: check.level,
277 position: check.position,
278 hash: expected_hash.clone(),
279 valid: false,
280 exists: None,
281 found_hash: None,
282 message: Some(error.0.clone()),
283 });
284 state.details.level_checks = Some(results);
285 return invalid_result(
286 state.details,
287 &format!("Level check failed: {}", error.0),
288 );
289 }
290 }
291 }
292 state.details.level_checks = Some(results);
293 }
294
295 if request.verify_batch_existence {
296 let mut batch_checks = Vec::new();
297 let mut offset = 0usize;
298 for (level, count) in level_counts.iter().enumerate() {
299 let hashes = proof.proof[offset..offset + count].to_vec();
300 let start = proof.level_starts.get(level).copied().unwrap_or(0);
301 match verify_hash_batch(
302 &HashBatchRequest {
303 data_type: state.request.data_type.clone(),
304 level,
305 start,
306 hashes: hashes.clone(),
307 },
308 state.request.api_key.as_deref(),
309 ) {
310 Ok(response) => {
311 let valid = response.mismatches == 0
312 && response.results.iter().all(|result| *result == 1);
313 batch_checks.push(BatchExistenceCheckResult {
314 level,
315 start,
316 hashes,
317 valid,
318 results: response.results,
319 matches: response.matches,
320 mismatches: response.mismatches,
321 });
322 if !valid {
323 state.details.batch_checks = Some(batch_checks);
324 state.details.batch_existence_match = Some(false);
325 return invalid_result(
326 state.details,
327 &format!("Batch existence check failed at level={}", level),
328 );
329 }
330 }
331 Err(error) => {
332 batch_checks.push(BatchExistenceCheckResult {
333 level,
334 start,
335 hashes,
336 valid: false,
337 results: vec![],
338 matches: 0,
339 mismatches: *count,
340 });
341 state.details.batch_checks = Some(batch_checks);
342 state.details.batch_existence_match = Some(false);
343 return invalid_result(
344 state.details,
345 &format!("Batch existence check failed: {}", error.0),
346 );
347 }
348 }
349 offset += count;
350 }
351 state.details.batch_checks = Some(batch_checks);
352 state.details.batch_existence_match = Some(true);
353 }
354
355 VerifyResult {
356 valid: true,
357 error: None,
358 details: Some(state.details),
359 }
360}
361
362pub fn verify_merkle_proof(
363 request: VerifyMerkleProofWithDetailsRequest,
364) -> VerifyMerkleProofWithDetailsResult {
365 let levels_hash_type = match normalize_levels_hash_type(request.levels_hash_type.as_deref()) {
366 Ok(value) => value,
367 Err(error) => return invalid_merkle_proof_result(&error.0, None),
368 };
369 let proof = match normalize_merkle_proof(request.proof) {
370 Ok(proof) => proof,
371 Err(error) => {
372 return invalid_merkle_proof_result(
373 &error.0,
374 Some((&levels_hash_type, None, None, -1, -1, "", None)),
375 )
376 }
377 };
378 let level_counts =
379 match normalize_level_counts(&proof.level_counts, proof.levels, proof.proof.len()) {
380 Ok(counts) => counts,
381 Err(error) => {
382 return invalid_merkle_proof_result(
383 &error.0,
384 Some((
385 &levels_hash_type,
386 Some(proof.clone()),
387 None,
388 -1,
389 -1,
390 "",
391 None,
392 )),
393 )
394 }
395 };
396 let position_path = build_position_path(proof.position, level_counts.len());
397 let (pending, max_level, max_level_position, max_level_hash) =
398 proof_inclusion_meta(&proof, &level_counts);
399 if pending {
400 return VerifyMerkleProofWithDetailsResult {
401 valid: false,
402 pending: true,
403 status: "pending".to_string(),
404 message: pending_merkle_proof_message(&proof, &level_counts, &position_path),
405 error: None,
406 details: pending_merkle_proof_details(&proof, &level_counts),
407 position_path,
408 levels_hash_type,
409 computed_root: None,
410 max_level: max_level as i64,
411 max_level_position,
412 max_level_hash,
413 proof: Some(proof),
414 };
415 }
416
417 let mut details = Vec::new();
418 let mut offset = 0usize;
419 for level in 0..level_counts.len().saturating_sub(1) {
420 let count = level_counts[level];
421 let level_hashes = proof.proof[offset..offset + count].to_vec();
422 let level_start = proof.level_starts.get(level).copied().unwrap_or(0);
423 let next_level_hashes = proof_level_hashes(&proof.proof, &level_counts, level + 1);
424 let next_level_start = proof.level_starts.get(level + 1).copied().unwrap_or(0);
425 let next_level_position = position_path[level + 1];
426 let next_level_index = (next_level_position - next_level_start) as isize;
427 let computed_rollup = match hash_hex_concat(&level_hashes, &levels_hash_type) {
428 Ok(hash) => hash,
429 Err(error) => {
430 return invalid_merkle_proof_result(
431 &error.0,
432 Some((
433 &levels_hash_type,
434 Some(proof.clone()),
435 Some(position_path.clone()),
436 max_level as i64,
437 max_level_position,
438 &max_level_hash,
439 None,
440 )),
441 )
442 }
443 };
444 let label = display_levels_hash_type(&levels_hash_type);
445 if next_level_index < 0 || next_level_index as usize >= next_level_hashes.len() {
446 details.push(format!(
447 "L{}[{}..{}] -> {} -> L{}[pos {}, idx {}]: pending",
448 level,
449 level_start,
450 level_start + count as i64 - 1,
451 label,
452 level + 1,
453 next_level_position,
454 next_level_index,
455 ));
456 let mut pending_details = details;
457 pending_details.extend(higher_level_pending_details(
458 level + 1,
459 next_level_position,
460 next_level_index as i64,
461 ));
462 return VerifyMerkleProofWithDetailsResult {
463 valid: true,
464 pending: false,
465 status: "valid".to_string(),
466 message: format!(
467 "Proof verified for existing levels ({} levels). Higher-level rollup pending.",
468 level + 1
469 ),
470 error: None,
471 details: pending_details,
472 position_path,
473 levels_hash_type,
474 computed_root: Some(computed_rollup),
475 max_level: max_level as i64,
476 max_level_position,
477 max_level_hash,
478 proof: Some(proof),
479 };
480 }
481 let expected_hash = &next_level_hashes[next_level_index as usize];
482 let matches = computed_rollup == *expected_hash;
483 details.push(format!(
484 "L{}[{}..{}] -> {} -> L{}[pos {}, idx {}]: {}",
485 level,
486 level_start,
487 level_start + count as i64 - 1,
488 label,
489 level + 1,
490 next_level_position,
491 next_level_index,
492 if matches { "✓" } else { "✗" }
493 ));
494 if !matches {
495 return VerifyMerkleProofWithDetailsResult {
496 valid: false,
497 pending: false,
498 status: "invalid".to_string(),
499 message: format!(
500 "Level {} rollup mismatch at level {} position {}.",
501 level,
502 level + 1,
503 next_level_position
504 ),
505 error: Some(format!(
506 "Computed {} but expected {}",
507 computed_rollup, expected_hash
508 )),
509 details,
510 position_path,
511 levels_hash_type,
512 computed_root: Some(computed_rollup),
513 max_level: max_level as i64,
514 max_level_position,
515 max_level_hash,
516 proof: Some(proof),
517 };
518 }
519 offset += count;
520 }
521
522 let final_level_hashes =
523 proof_level_hashes(&proof.proof, &level_counts, level_counts.len() - 1);
524 if final_level_hashes.is_empty() {
525 return invalid_merkle_proof_result(
526 "Missing final proof level",
527 Some((
528 &levels_hash_type,
529 Some(proof),
530 Some(position_path),
531 max_level as i64,
532 max_level_position,
533 &max_level_hash,
534 None,
535 )),
536 );
537 }
538 let computed_root = if final_level_hashes.len() == 1 {
539 final_level_hashes[0].clone()
540 } else {
541 match hash_hex_concat(&final_level_hashes, &levels_hash_type) {
542 Ok(hash) => hash,
543 Err(error) => {
544 return invalid_merkle_proof_result(
545 &error.0,
546 Some((
547 &levels_hash_type,
548 Some(proof),
549 Some(position_path),
550 max_level as i64,
551 max_level_position,
552 &max_level_hash,
553 None,
554 )),
555 )
556 }
557 }
558 };
559 if proof.root.is_empty() {
560 details.push("Root pending: final rollup not yet recorded in proof.root.".to_string());
561 return VerifyMerkleProofWithDetailsResult {
562 valid: true,
563 pending: false,
564 status: "valid".to_string(),
565 message: format!(
566 "Proof verified for existing levels ({} levels). Root pending.",
567 level_counts.len()
568 ),
569 error: None,
570 details,
571 position_path,
572 levels_hash_type,
573 computed_root: Some(computed_root),
574 max_level: max_level as i64,
575 max_level_position,
576 max_level_hash,
577 proof: Some(proof),
578 };
579 }
580 let root_matches = computed_root == proof.root;
581 details.push(format!(
582 "Root: {} ({}...)",
583 if root_matches { "✓" } else { "✗" },
584 &proof.root[..proof.root.len().min(16)]
585 ));
586 if !root_matches {
587 return VerifyMerkleProofWithDetailsResult {
588 valid: false,
589 pending: false,
590 status: "invalid".to_string(),
591 message: "Root hash mismatch.".to_string(),
592 error: Some(format!(
593 "Expected {} but computed {}",
594 proof.root, computed_root
595 )),
596 details,
597 position_path,
598 levels_hash_type,
599 computed_root: Some(computed_root),
600 max_level: max_level as i64,
601 max_level_position,
602 max_level_hash,
603 proof: Some(proof),
604 };
605 }
606 VerifyMerkleProofWithDetailsResult {
607 valid: true,
608 pending: false,
609 status: "valid".to_string(),
610 message: format!(
611 "Proof verified! {} levels, {} hashes.",
612 level_counts.len(),
613 proof.proof.len()
614 ),
615 error: None,
616 details,
617 position_path,
618 levels_hash_type,
619 computed_root: Some(computed_root),
620 max_level: max_level as i64,
621 max_level_position,
622 max_level_hash,
623 proof: Some(proof),
624 }
625}
626
627fn verify_record_core(
628 request: VerifyRequest,
629) -> std::result::Result<(VerifyResult, Option<VerifyCoreState>), VerifyResult> {
630 let request = CanonicalVerifyRequest {
631 data_type: request.data_type.unwrap_or_default(),
632 data_item: request.data_item,
633 kayros_hash: request.kayros_hash,
634 api_key: request.api_key,
635 };
636 let mut details = VerifyResultDetails {
637 lookup_mode: if request.kayros_hash.is_some() {
638 "kayros_hash".into()
639 } else {
640 "data_item".into()
641 },
642 record_found: false,
643 ..Default::default()
644 };
645 if request.data_type.is_empty() {
646 return Err(invalid_result(details, "Missing data_type"));
647 }
648 if request.data_item.is_none() && request.kayros_hash.is_none() {
649 return Err(invalid_result(
650 details,
651 "Either data_item or kayros_hash is required",
652 ));
653 }
654
655 let record = if let Some(kayros_hash) = &request.kayros_hash {
656 get_record_by_hash(kayros_hash, Some(&request.data_type))
657 .and_then(normalize_record)
658 .map_err(|error| {
659 invalid_result(
660 details.clone(),
661 &format!("Failed to fetch record: {}", error.0),
662 )
663 })?
664 } else {
665 fetch_record_by_data_item(
666 &request.data_type,
667 request.data_item.as_deref().unwrap_or_default(),
668 request.api_key.as_deref(),
669 )
670 .map_err(|error| {
671 invalid_result(
672 details.clone(),
673 &format!("Failed to fetch record: {}", error.0),
674 )
675 })?
676 };
677
678 details.record_found = true;
679 details.record = Some(record.clone());
680 details.data_type_match = Some(
681 record.data_type == request.data_type
682 || record.data_type_hex == utf8_hex(&request.data_type),
683 );
684 if details.data_type_match != Some(true) {
685 return Err(invalid_result(
686 details,
687 &format!(
688 "Record data_type mismatch: expected={} record={}",
689 request.data_type, record.data_type
690 ),
691 ));
692 }
693
694 if let Some(data_item) = &request.data_item {
695 let normalized = normalize_hex_string(data_item);
696 details.data_item_match = Some(normalized.as_deref() == Some(&record.data_item));
697 if details.data_item_match != Some(true) {
698 return Err(invalid_result(
699 details,
700 &format!(
701 "Record data_item mismatch: expected={} record={}",
702 normalized.unwrap_or_else(|| data_item.clone()),
703 record.data_item
704 ),
705 ));
706 }
707 }
708 if let Some(kayros_hash) = &request.kayros_hash {
709 let normalized = normalize_hex_string(kayros_hash);
710 details.kayros_hash_match = Some(normalized.as_deref() == Some(&record.kayros_hash));
711 if details.kayros_hash_match != Some(true) {
712 return Err(invalid_result(
713 details,
714 &format!(
715 "Record hash mismatch: expected={} record={}",
716 normalized.unwrap_or_else(|| kayros_hash.clone()),
717 record.kayros_hash
718 ),
719 ));
720 }
721 }
722
723 let previous_record = if let Some(prev_hash) = &record.prev_hash {
724 if !is_zero_hash(prev_hash) {
725 Some(
726 get_record_by_hash(prev_hash, Some(&record.data_type))
727 .and_then(normalize_record)
728 .map_err(|error| {
729 invalid_result(
730 details.clone(),
731 &format!("Failed to fetch previous record: {}", error.0),
732 )
733 })?,
734 )
735 } else {
736 None
737 }
738 } else {
739 None
740 };
741 details.previous_record = previous_record.clone();
742 details.chain_link_match = Some(
743 previous_record
744 .as_ref()
745 .map(|previous| {
746 previous.data_type == record.data_type
747 && previous.kayros_hash == record.prev_hash.clone().unwrap_or_default()
748 })
749 .unwrap_or(true),
750 );
751 if details.chain_link_match != Some(true) {
752 return Err(invalid_result(
753 details,
754 "Previous record chain link mismatch",
755 ));
756 }
757
758 let compute_request = ComputeHashRequest {
759 prev_hash: Some(
760 record
761 .prev_hash
762 .clone()
763 .unwrap_or_else(|| ZERO_HASH_32.to_string()),
764 ),
765 data_type: record.data_type.clone(),
766 data_item: record.data_item.clone(),
767 timeuuid: record.uuid.clone(),
768 hash_type: record.hash_type.clone(),
769 };
770 let computed =
771 compute_hash_from_hex(&compute_request, request.api_key.as_deref()).map_err(|error| {
772 invalid_result(
773 details.clone(),
774 &format!("Failed to recompute Kayros hash: {}", error.0),
775 )
776 })?;
777 details.computed_record_hash = normalize_hex_string(&computed.hash);
778 details.record_hash_match =
779 Some(details.computed_record_hash.as_deref() == Some(&record.kayros_hash));
780 if details.record_hash_match != Some(true) {
781 let computed_record_hash = details.computed_record_hash.clone().unwrap_or_default();
782 return Err(invalid_result(
783 details,
784 &format!(
785 "Kayros hash mismatch: computed={} record={}",
786 computed_record_hash, record.kayros_hash
787 ),
788 ));
789 }
790
791 details.uuid_timestamp_match = Some(!record.timestamp.is_empty());
792 if details.uuid_timestamp_match != Some(true) {
793 return Err(invalid_result(details, "Invalid record UUID timestamp"));
794 }
795
796 let result = VerifyResult {
797 valid: true,
798 error: None,
799 details: Some(details.clone()),
800 };
801 Ok((
802 result,
803 Some(VerifyCoreState {
804 request,
805 record,
806 details,
807 }),
808 ))
809}
810
811fn fetch_record_by_data_item(
812 data_type: &str,
813 data_item: &str,
814 api_key: Option<&str>,
815) -> Result<NormalizedKayrosRecord> {
816 let response = get_record_by_data_item(data_type, data_item, api_key, None)?;
817 if response.records.is_empty() {
818 return Err(ProvableError::new("Record not found"));
819 }
820 if response.records.len() > 1 {
821 return Err(ProvableError::new(format!(
822 "Multiple records found for data_item; provide kayros_hash (count={})",
823 response.records.len()
824 )));
825 }
826 normalize_record(response.records[0].clone())
827}
828
829pub(crate) fn normalize_record(
830 raw: crate::types::GetRecordResponse,
831) -> Result<NormalizedKayrosRecord> {
832 let data_item = raw
833 .data_item_hex
834 .as_deref()
835 .and_then(normalize_hex_string)
836 .or_else(|| normalize_hex_string(&raw.data_item))
837 .ok_or_else(|| ProvableError::new("Invalid remote record structure"))?;
838 let kayros_hash = raw
839 .hash_item_hex
840 .as_deref()
841 .and_then(normalize_hex_string)
842 .or_else(|| normalize_hex_string(&raw.hash_item))
843 .ok_or_else(|| ProvableError::new("Invalid remote record structure"))?;
844 let prev_hash = raw
845 .prev_hash_hex
846 .as_deref()
847 .and_then(normalize_hex_string)
848 .or_else(|| raw.prev_hash.as_deref().and_then(normalize_hex_string));
849 let uuid = uuid_string_to_hex(raw.uuid_hex.as_deref().unwrap_or(&raw.ts));
850 let timestamp = timeuuid_hex_to_timestamp(&uuid);
851 if raw.data_type.is_empty()
852 || raw.hash_type.is_empty()
853 || uuid.is_empty()
854 || timestamp.is_empty()
855 {
856 return Err(ProvableError::new("Invalid remote record structure"));
857 }
858 Ok(NormalizedKayrosRecord {
859 data_type_hex: utf8_hex(&raw.data_type),
860 data_type: raw.data_type.clone(),
861 data_item,
862 kayros_hash,
863 prev_hash,
864 hash_type: raw.hash_type.clone(),
865 uuid,
866 timestamp,
867 position: raw.position,
868 raw,
869 })
870}
871
872fn verify_proof_target_position(
873 proof: &NormalizedMerkleProof,
874 target_hash: &str,
875 level_counts: &[usize],
876) -> Result<()> {
877 if level_counts.is_empty() || level_counts[0] == 0 {
878 return Err(ProvableError::new("invalid level count"));
879 }
880 let index = level_index_for_position(0, proof.position, level_counts[0], &proof.level_starts)?;
881 if proof.proof.get(index).map(|value| value.as_str()) != Some(target_hash) {
882 return Err(ProvableError::new(format!(
883 "target hash not found at expected position index={} expected={} got={}",
884 index,
885 target_hash,
886 proof.proof.get(index).cloned().unwrap_or_default()
887 )));
888 }
889 Ok(())
890}
891
892fn verify_proof_path(
893 proof: &NormalizedMerkleProof,
894 level_counts: &[usize],
895 levels_hash_type: &str,
896) -> Result<String> {
897 let mut offset = 0usize;
898 let mut previous_rollup = String::new();
899 let mut last_rollup = String::new();
900 let mut current_position = proof.position;
901 for (level, count) in level_counts.iter().enumerate() {
902 if *count == 0 {
903 return Err(ProvableError::new("invalid level count"));
904 }
905 if offset + count > proof.proof.len() {
906 return Err(ProvableError::new("proof length mismatch"));
907 }
908 let level_hashes = &proof.proof[offset..offset + count];
909 if !previous_rollup.is_empty() {
910 let index =
911 level_index_for_position(level, current_position, *count, &proof.level_starts)?;
912 if level_hashes.get(index).map(|value| value.as_str()) != Some(previous_rollup.as_str())
913 {
914 return Err(ProvableError::new(format!(
915 "level hash mismatch level={} index={} expected={} got={}",
916 level,
917 index,
918 previous_rollup,
919 level_hashes.get(index).cloned().unwrap_or_default()
920 )));
921 }
922 }
923 let is_last = level == level_counts.len() - 1;
924 if is_last && *count == 1 {
925 last_rollup = level_hashes[0].clone();
926 } else {
927 previous_rollup = hash_hex_concat(level_hashes, levels_hash_type)?;
928 if is_last {
929 last_rollup = previous_rollup.clone();
930 }
931 }
932 offset += count;
933 current_position /= 256;
934 }
935 if last_rollup.is_empty() {
936 return Err(ProvableError::new("missing final hash"));
937 }
938 if !proof.root.is_empty() && last_rollup != proof.root {
939 return Err(ProvableError::new(format!(
940 "root hash mismatch computed={} root={}",
941 last_rollup, proof.root
942 )));
943 }
944 Ok(last_rollup)
945}
946
947fn proof_inclusion_meta(
948 proof: &NormalizedMerkleProof,
949 level_counts: &[usize],
950) -> (bool, usize, i64, String) {
951 if proof.level_counts.is_empty() || level_counts.is_empty() {
952 return (true, 0, -1, String::new());
953 }
954 let positions = build_position_path(proof.position, level_counts.len());
955 let max_level = level_counts.len() - 1;
956 let max_level_position = positions[max_level];
957 let max_level_hash = if proof.root.is_empty() {
958 let level_hashes = proof_level_hashes(&proof.proof, level_counts, max_level);
959 let level_start = proof.level_starts.get(max_level).copied().unwrap_or(0);
960 let index = (max_level_position - level_start) as usize;
961 level_hashes.get(index).cloned().unwrap_or_default()
962 } else {
963 proof.root.clone()
964 };
965 let pending = if level_counts.len() < 2 {
966 true
967 } else {
968 let level_start = proof.level_starts.get(1).copied().unwrap_or(0);
969 let level_index = positions[1] - level_start;
970 level_index < 0 || level_index as usize >= level_counts[1]
971 };
972 (pending, max_level, max_level_position, max_level_hash)
973}
974
975fn proof_hash_at_level_position(
976 proof: &NormalizedMerkleProof,
977 level_counts: &[usize],
978 level: usize,
979 position: i64,
980) -> Option<String> {
981 let level_hashes = proof_level_hashes(&proof.proof, level_counts, level);
982 if level_hashes.is_empty() {
983 return None;
984 }
985 level_index_for_position(level, position, level_hashes.len(), &proof.level_starts)
986 .ok()
987 .and_then(|index| level_hashes.get(index).cloned())
988}
989
990fn proof_level_hashes(all_hashes: &[String], level_counts: &[usize], level: usize) -> Vec<String> {
991 if level >= level_counts.len() {
992 return vec![];
993 }
994 let offset = level_counts.iter().take(level).sum::<usize>();
995 all_hashes[offset..offset + level_counts[level]].to_vec()
996}
997
998fn level_index_for_position(
999 level: usize,
1000 current_position: i64,
1001 count: usize,
1002 level_starts: &[i64],
1003) -> Result<usize> {
1004 if count == 0 {
1005 return Err(ProvableError::new("invalid level count"));
1006 }
1007 let start = level_starts
1008 .get(level)
1009 .copied()
1010 .unwrap_or_else(|| (current_position / count as i64) * count as i64);
1011 let index = current_position - start;
1012 if index < 0 || index as usize >= count {
1013 return Err(ProvableError::new("proof index out of range"));
1014 }
1015 Ok(index as usize)
1016}
1017
1018fn normalize_levels_hash_type(input: Option<&str>) -> Result<String> {
1019 match input.map(|value| value.trim().to_lowercase().replace('_', "-")) {
1020 None => Ok(DEFAULT_LEVELS_HASH_TYPE.to_string()),
1021 Some(value) if value.is_empty() => Ok(DEFAULT_LEVELS_HASH_TYPE.to_string()),
1022 Some(value) if value == "sha3" || value == "sha3-256" => Ok("sha3-256".to_string()),
1023 Some(value) if value == "sha256" || value == "sha-256" => Ok("sha256".to_string()),
1024 Some(value) => Err(ProvableError::new(format!(
1025 "Unsupported levels_hash_type: {}",
1026 value
1027 ))),
1028 }
1029}
1030
1031fn hash_hex_concat(hashes: &[String], levels_hash_type: &str) -> Result<String> {
1032 let bytes = hashes
1033 .iter()
1034 .map(|hash| hex::decode(hash).map_err(|error| ProvableError::new(error.to_string())))
1035 .collect::<Result<Vec<_>>>()?
1036 .concat();
1037 match levels_hash_type {
1038 "sha256" => {
1039 let mut hasher = Sha256::new();
1040 hasher.update(bytes);
1041 Ok(hex::encode(hasher.finalize()))
1042 }
1043 "sha3-256" => {
1044 let mut hasher = Sha3_256::new();
1045 hasher.update(bytes);
1046 Ok(hex::encode(hasher.finalize()))
1047 }
1048 _ => Err(ProvableError::new(format!(
1049 "Unsupported levels_hash_type: {}",
1050 levels_hash_type
1051 ))),
1052 }
1053}
1054
1055fn utf8_hex(value: &str) -> String {
1056 hex::encode(value.as_bytes())
1057}
1058
1059fn invalid_result(mut details: VerifyResultDetails, error: &str) -> VerifyResult {
1060 VerifyResult {
1061 valid: false,
1062 error: Some(error.to_string()),
1063 details: Some({
1064 if details.lookup_mode.is_empty() {
1065 details.lookup_mode = "data_item".to_string();
1066 }
1067 details
1068 }),
1069 }
1070}
1071
1072fn build_position_path(position: i64, levels: usize) -> Vec<i64> {
1073 if levels == 0 {
1074 return vec![];
1075 }
1076 let mut path = vec![position];
1077 let mut current = position;
1078 for _ in 1..levels {
1079 current /= 256;
1080 path.push(current);
1081 }
1082 path
1083}
1084
1085fn pending_merkle_proof_message(
1086 proof: &NormalizedMerkleProof,
1087 level_counts: &[usize],
1088 position_path: &[i64],
1089) -> String {
1090 let level0_count = level_counts.first().copied().unwrap_or(0);
1091 if level_counts.len() < 2 {
1092 return format!(
1093 "Proof pending: L0 group has {} hashes and no L1 rollup yet.",
1094 level0_count
1095 );
1096 }
1097 let level1_position = position_path[1];
1098 let level1_start = proof.level_starts.get(1).copied().unwrap_or(0);
1099 let level1_index = level1_position - level1_start;
1100 format!(
1101 "Proof pending: L1[pos {}, idx {}] has not been generated yet.",
1102 level1_position, level1_index
1103 )
1104}
1105
1106fn pending_merkle_proof_details(
1107 proof: &NormalizedMerkleProof,
1108 level_counts: &[usize],
1109) -> Vec<String> {
1110 let mut details = Vec::new();
1111 let level0_start = proof.level_starts.first().copied().unwrap_or(0);
1112 let level0_count = level_counts.first().copied().unwrap_or(0);
1113 if level0_count > 0 {
1114 details.push(format!(
1115 "L0[{}..{}] partial group",
1116 level0_start,
1117 level0_start + level0_count as i64 - 1
1118 ));
1119 }
1120 let missing = level_counts
1121 .iter()
1122 .map(|count| 256usize.saturating_sub(*count))
1123 .collect::<Vec<_>>();
1124 let missing_l0 = missing.first().copied().unwrap_or(0);
1125 if missing_l0 > 0 {
1126 details.push(format!(
1127 "Need {} more L0 records to complete current L0 group.",
1128 missing_l0
1129 ));
1130 }
1131 let last = level_counts.len().saturating_sub(1);
1132 if last > 0 && missing.get(last).copied().unwrap_or(0) > 0 {
1133 let mut needed = missing_l0;
1134 for (level, miss) in missing.iter().enumerate().skip(1).take(last) {
1135 if *miss > 0 {
1136 needed += miss.saturating_sub(1) * 256usize.pow(level as u32);
1137 }
1138 }
1139 if needed > 0 {
1140 details.push(format!(
1141 "~{} more L0 records to complete L{} group (to get next-level rollup).",
1142 needed, last
1143 ));
1144 }
1145 }
1146 details
1147}
1148
1149fn higher_level_pending_details(level: usize, position: i64, index: i64) -> Vec<String> {
1150 vec![format!(
1151 "Higher-level rollup pending at L{}[pos {}, idx {}].",
1152 level, position, index
1153 )]
1154}
1155
1156fn display_levels_hash_type(levels_hash_type: &str) -> &str {
1157 match levels_hash_type {
1158 "sha256" => "SHA-256",
1159 "sha3-256" => "SHA3-256",
1160 other => other,
1161 }
1162}
1163
1164fn invalid_merkle_proof_result(
1165 message: &str,
1166 options: Option<(
1167 &str,
1168 Option<NormalizedMerkleProof>,
1169 Option<Vec<i64>>,
1170 i64,
1171 i64,
1172 &str,
1173 Option<String>,
1174 )>,
1175) -> VerifyMerkleProofWithDetailsResult {
1176 let (
1177 levels_hash_type,
1178 proof,
1179 position_path,
1180 max_level,
1181 max_level_position,
1182 max_level_hash,
1183 computed_root,
1184 ) = options.unwrap_or((DEFAULT_LEVELS_HASH_TYPE, None, None, -1, -1, "", None));
1185 VerifyMerkleProofWithDetailsResult {
1186 valid: false,
1187 pending: false,
1188 status: "invalid".to_string(),
1189 message: message.to_string(),
1190 error: Some(message.to_string()),
1191 details: vec![],
1192 position_path: position_path.unwrap_or_default(),
1193 levels_hash_type: levels_hash_type.to_string(),
1194 computed_root,
1195 max_level,
1196 max_level_position,
1197 max_level_hash: max_level_hash.to_string(),
1198 proof,
1199 }
1200}