Skip to main content

provable_sdk/
verify.rs

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}