1use serde::{Deserialize, Serialize};
59use sha2::{Digest, Sha256};
60use std::path::{Path, PathBuf};
61
62use crate::wasm_op::WasmOp;
63
64pub const SAFE_ACCESSES_SCHEMA: &str = "scry/safe-accesses/v1";
66
67pub const ELISION_ATTESTATION_SCHEMA: &str = "synth-proven-safe-elisions-v1";
69
70#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
76pub struct SafeSite {
77 pub func: u32,
80 pub pc: u32,
82 #[serde(default)]
86 pub op: String,
87 pub width: u32,
89}
90
91#[derive(Debug, Clone, Deserialize)]
96struct RawDocument {
97 schema: String,
98 #[serde(default)]
99 scry_version: String,
100 module_sha256: String,
101 memory_min_bytes: u64,
102 #[serde(default)]
103 proven_safe: Vec<SafeSite>,
104}
105
106#[derive(Debug, Clone, PartialEq, Eq, Default)]
119pub struct ProvenSafeIngest {
120 pub accepted: bool,
123 pub refusal: Option<String>,
125 pub scry_version: String,
128 pub declared_module_sha256: String,
131 pub actual_module_sha256: String,
133 pub declared_memory_min_bytes: u64,
135 pub offered: Vec<SafeSite>,
138 pub diagnostics: Vec<String>,
140}
141
142impl ProvenSafeIngest {
143 pub fn refused(reason: impl Into<String>) -> Self {
149 Self::refuse(reason)
150 }
151
152 fn refuse(reason: impl Into<String>) -> Self {
154 let reason = reason.into();
155 Self {
156 accepted: false,
157 diagnostics: vec![reason.clone()],
158 refusal: Some(reason),
159 ..Self::default()
160 }
161 }
162
163 pub fn offered_for_func(&self, func: u32) -> Vec<&SafeSite> {
165 let mut v: Vec<&SafeSite> = self.offered.iter().filter(|s| s.func == func).collect();
166 v.sort_by_key(|s| s.pc);
167 v
168 }
169
170 pub fn validate_function(
185 &self,
186 func: u32,
187 ops: &[WasmOp],
188 notes: &mut Vec<String>,
189 ) -> Vec<usize> {
190 let mut marks = Vec::new();
191 for site in self.offered_for_func(func) {
192 let idx = site.pc as usize;
193 let Some(op) = ops.get(idx) else {
194 notes.push(format!(
195 "func {func} pc {} — out of range (function has {} operators); \
196 entry DROPPED, guard retained. If the producer emitted wasm BYTE \
197 OFFSETS, the key space is wrong: `pc` is the 0-based OPERATOR index",
198 site.pc,
199 ops.len()
200 ));
201 continue;
202 };
203 let Some(actual_width) = access_width(op) else {
204 notes.push(format!(
205 "func {func} pc {} — the operator there is {op:?}, not a linear-memory \
206 access; entry DROPPED, guard retained (claimed op '{}')",
207 site.pc, site.op
208 ));
209 continue;
210 };
211 if actual_width != site.width {
212 notes.push(format!(
213 "func {func} pc {} — declared width {} B disagrees with the decoded \
214 operator {op:?} ({actual_width} B); entry DROPPED, guard retained",
215 site.pc, site.width
216 ));
217 continue;
218 }
219 marks.push(idx);
220 }
221 marks.sort_unstable();
222 marks.dedup();
223 marks
224 }
225}
226
227pub fn access_width(op: &WasmOp) -> Option<u32> {
233 Some(match op {
234 WasmOp::I32Load8S { .. }
235 | WasmOp::I32Load8U { .. }
236 | WasmOp::I32Store8 { .. }
237 | WasmOp::I64Load8S { .. }
238 | WasmOp::I64Load8U { .. }
239 | WasmOp::I64Store8 { .. } => 1,
240 WasmOp::I32Load16S { .. }
241 | WasmOp::I32Load16U { .. }
242 | WasmOp::I32Store16 { .. }
243 | WasmOp::I64Load16S { .. }
244 | WasmOp::I64Load16U { .. }
245 | WasmOp::I64Store16 { .. } => 2,
246 WasmOp::I32Load { .. }
247 | WasmOp::I32Store { .. }
248 | WasmOp::I64Load32S { .. }
249 | WasmOp::I64Load32U { .. }
250 | WasmOp::I64Store32 { .. }
251 | WasmOp::F32Load { .. }
252 | WasmOp::F32Store { .. } => 4,
253 WasmOp::I64Load { .. }
254 | WasmOp::I64Store { .. }
255 | WasmOp::F64Load { .. }
256 | WasmOp::F64Store { .. } => 8,
257 WasmOp::V128Load { .. } | WasmOp::V128Store { .. } => 16,
258 _ => return None,
259 })
260}
261
262pub fn ingest(path: &Path, module_bytes: &[u8], memory_min_bytes: u32) -> ProvenSafeIngest {
270 let actual = hex_sha256(module_bytes);
271
272 let text = match std::fs::read_to_string(path) {
273 Ok(t) => t,
274 Err(e) => {
275 return ProvenSafeIngest {
276 actual_module_sha256: actual,
277 ..ProvenSafeIngest::refuse(format!(
278 "--proven-safe {}: cannot read the file ({e}); NO bounds guard is \
279 elided (fail closed) and the compile continues unchanged",
280 path.display()
281 ))
282 };
283 }
284 };
285
286 let doc: RawDocument = match serde_json::from_str(&text) {
287 Ok(d) => d,
288 Err(e) => {
289 return ProvenSafeIngest {
290 actual_module_sha256: actual,
291 ..ProvenSafeIngest::refuse(format!(
292 "--proven-safe {}: not a well-formed `{SAFE_ACCESSES_SCHEMA}` document \
293 ({e}); NO bounds guard is elided (fail closed) and the compile \
294 continues unchanged",
295 path.display()
296 ))
297 };
298 }
299 };
300
301 if doc.schema != SAFE_ACCESSES_SCHEMA {
302 return ProvenSafeIngest {
303 actual_module_sha256: actual,
304 scry_version: doc.scry_version.clone(),
305 declared_module_sha256: doc.module_sha256.clone(),
306 declared_memory_min_bytes: doc.memory_min_bytes,
307 ..ProvenSafeIngest::refuse(format!(
308 "--proven-safe {}: schema is '{}', expected '{SAFE_ACCESSES_SCHEMA}'; \
309 NO bounds guard is elided (fail closed)",
310 path.display(),
311 doc.schema
312 ))
313 };
314 }
315
316 if !doc.module_sha256.eq_ignore_ascii_case(&actual) {
319 return ProvenSafeIngest {
320 actual_module_sha256: actual.clone(),
321 scry_version: doc.scry_version.clone(),
322 declared_module_sha256: doc.module_sha256.clone(),
323 declared_memory_min_bytes: doc.memory_min_bytes,
324 ..ProvenSafeIngest::refuse(format!(
325 "--proven-safe {}: REFUSED — module_sha256 mismatch. The file was produced \
326 for {}, but this compile's module hashes to {actual}. Eliding a bounds \
327 check on a stale analysis is a memory-safety hole, not a stale \
328 optimization, so NO guard is elided. (The hash covers the bytes handed to \
329 the decoder — after .wat parsing, after loom, and after the #418 \
330 arena-bind rewrite — so a module rewrite that shifts operator indices \
331 lands here too.)",
332 path.display(),
333 if doc.module_sha256.is_empty() {
334 "<empty>"
335 } else {
336 &doc.module_sha256
337 }
338 ))
339 };
340 }
341
342 if doc.memory_min_bytes != u64::from(memory_min_bytes) {
346 return ProvenSafeIngest {
347 actual_module_sha256: actual,
348 scry_version: doc.scry_version.clone(),
349 declared_module_sha256: doc.module_sha256.clone(),
350 declared_memory_min_bytes: doc.memory_min_bytes,
351 ..ProvenSafeIngest::refuse(format!(
352 "--proven-safe {}: REFUSED — memory_min_bytes disagreement. The verdicts \
353 were proven against a {} B floor; synth's declared linear-memory minimum \
354 for this module is {memory_min_bytes} B. The module_sha256 MATCHED, so \
355 these should be equal — a disagreement means the producer is broken, and \
356 verdicts from a broken prover are not trusted. NO guard is elided.",
357 path.display(),
358 doc.memory_min_bytes
359 ))
360 };
361 }
362
363 let mut diagnostics = Vec::new();
364 if doc.proven_safe.is_empty() {
365 diagnostics.push(format!(
366 "--proven-safe {}: accepted, but the document proves ZERO access sites — \
367 nothing to elide (every guard is retained)",
368 path.display()
369 ));
370 }
371
372 ProvenSafeIngest {
373 accepted: true,
374 refusal: None,
375 scry_version: doc.scry_version,
376 declared_module_sha256: doc.module_sha256,
377 actual_module_sha256: actual,
378 declared_memory_min_bytes: doc.memory_min_bytes,
379 offered: doc.proven_safe,
380 diagnostics,
381 }
382}
383
384pub fn hex_sha256(bytes: &[u8]) -> String {
386 let digest = Sha256::digest(bytes);
387 let mut s = String::with_capacity(64);
388 for b in digest {
389 s.push_str(&format!("{b:02x}"));
390 }
391 s
392}
393
394#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
400pub struct AttestedElision {
401 pub func: u32,
402 pub pc: u32,
404 pub op: String,
405 pub width: u32,
406 pub authority: String,
410}
411
412#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
419pub struct ElisionAttestation {
420 pub schema: String,
421 pub synth_version: String,
423 pub scry_version: String,
425 pub module_sha256: String,
428 pub declared_module_sha256: String,
429 pub memory_min_bytes: Option<u32>,
437 pub declared_memory_min_bytes: u64,
438 pub safety_bounds: String,
442 pub accepted: bool,
444 #[serde(skip_serializing_if = "Option::is_none")]
446 pub refusal: Option<String>,
447 pub sites_offered: usize,
449 pub sites_elided: usize,
451 pub sites_not_elided: usize,
454 pub elisions: Vec<AttestedElision>,
456 pub diagnostics: Vec<String>,
458}
459
460impl ElisionAttestation {
461 pub fn to_json(&self) -> String {
463 serde_json::to_string_pretty(self).expect("ElisionAttestation serializes")
464 }
465
466 pub fn sidecar_path(elf_path: &Path) -> PathBuf {
469 let mut p = elf_path.to_path_buf();
470 let stem = elf_path
471 .file_stem()
472 .map(|s| s.to_string_lossy().to_string())
473 .unwrap_or_else(|| "out".to_string());
474 p.set_file_name(format!("{stem}.proven-safe-elisions.json"));
475 p
476 }
477}
478
479#[cfg(test)]
480mod tests {
481 use super::*;
482 use std::io::Write;
483
484 fn write_tmp(name: &str, body: &str) -> PathBuf {
485 let dir = std::env::temp_dir().join("proven_safe_901_unit");
486 std::fs::create_dir_all(&dir).expect("mk tempdir");
487 let p = dir.join(name);
488 let mut f = std::fs::File::create(&p).expect("create");
489 f.write_all(body.as_bytes()).expect("write");
490 p
491 }
492
493 const MODULE: &[u8] = b"\0asm\x01\0\0\0 pretend this is a module";
494
495 fn doc(hash: &str, min: u64, sites: &str) -> String {
496 format!(
497 r#"{{ "schema": "scry/safe-accesses/v1", "scry_version": "3.2.4",
498 "module_sha256": "{hash}", "memory_min_bytes": {min},
499 "premises": {{ "bounded_memory": true }},
500 "counts": {{ "access_sites": 9, "proven_safe": 1 }},
501 "proven_safe": [{sites}] }}"#
502 )
503 }
504
505 #[test]
508 fn hash_mismatch_refuses_everything_901() {
509 let stale = "0".repeat(64);
510 let p = write_tmp(
511 "stale.json",
512 &doc(
513 &stale,
514 65536,
515 r#"{"func":0,"pc":1,"op":"i32.load","width":4}"#,
516 ),
517 );
518 let r = ingest(&p, MODULE, 65536);
519 assert!(!r.accepted, "a stale analysis must never be accepted");
520 assert!(
521 r.offered.is_empty(),
522 "a refused document is not trusted piecemeal"
523 );
524 let why = r.refusal.expect("a refusal names its reason");
525 assert!(why.contains("module_sha256 mismatch"), "{why}");
526 assert!(why.contains(&stale), "{why}");
528 assert!(why.contains(&hex_sha256(MODULE)), "{why}");
529 }
530
531 #[test]
532 fn matching_hash_is_accepted_and_case_insensitive_901() {
533 let h = hex_sha256(MODULE).to_uppercase();
534 let p = write_tmp(
535 "good.json",
536 &doc(&h, 65536, r#"{"func":0,"pc":1,"op":"i32.load","width":4}"#),
537 );
538 let r = ingest(&p, MODULE, 65536);
539 assert!(r.accepted, "{:?}", r.refusal);
540 assert_eq!(r.offered.len(), 1);
541 assert_eq!(r.scry_version, "3.2.4");
542 }
543
544 #[test]
547 fn one_flipped_module_byte_refuses_901() {
548 let p = write_tmp(
549 "flip.json",
550 &doc(
551 &hex_sha256(MODULE),
552 65536,
553 r#"{"func":0,"pc":1,"op":"i32.load","width":4}"#,
554 ),
555 );
556 let mut rewritten = MODULE.to_vec();
557 rewritten.push(0x00);
558 let r = ingest(&p, &rewritten, 65536);
559 assert!(!r.accepted);
560 assert!(r.refusal.unwrap().contains("module_sha256 mismatch"));
561 }
562
563 #[test]
566 fn memory_min_bytes_disagreement_refuses_901() {
567 let p = write_tmp(
568 "floor.json",
569 &doc(
570 &hex_sha256(MODULE),
571 131072,
572 r#"{"func":0,"pc":1,"op":"i32.load","width":4}"#,
573 ),
574 );
575 let r = ingest(&p, MODULE, 65536);
576 assert!(!r.accepted);
577 let why = r.refusal.unwrap();
578 assert!(why.contains("memory_min_bytes disagreement"), "{why}");
579 assert!(why.contains("131072") && why.contains("65536"), "{why}");
580 }
581
582 #[test]
585 fn malformed_missing_and_wrong_schema_all_refuse_without_erroring_901() {
586 let h = hex_sha256(MODULE);
587 let cases = vec![
588 ("missing", None),
589 ("garbage.json", Some("this is not json {{{".to_string())),
590 ("empty.json", Some(String::new())),
591 (
592 "wrongschema.json",
593 Some(doc(&h, 65536, "").replace(SAFE_ACCESSES_SCHEMA, "scry/safe-accesses/v2")),
594 ),
595 (
596 "nohash.json",
597 Some(r#"{"schema":"scry/safe-accesses/v1","memory_min_bytes":65536}"#.to_string()),
598 ),
599 (
600 "sitegarbage.json",
601 Some(doc(&h, 65536, r#"{"func":"four","pc":1,"width":4}"#)),
602 ),
603 ];
604 for (name, body) in cases {
605 let p = match body {
606 Some(b) => write_tmp(name, &b),
607 None => std::env::temp_dir().join("proven_safe_901_unit/definitely-absent.json"),
608 };
609 let r = ingest(&p, MODULE, 65536);
610 assert!(!r.accepted, "'{name}' must not be accepted");
611 assert!(r.offered.is_empty(), "'{name}' offered sites");
612 assert!(r.refusal.is_some(), "'{name}' refused without a reason");
613 assert!(!r.diagnostics.is_empty(), "'{name}' refused silently");
614 }
615 }
616
617 #[test]
618 fn unknown_fields_are_tolerated_901() {
619 let p = write_tmp(
621 "future.json",
622 &format!(
623 r#"{{ "schema": "scry/safe-accesses/v1", "scry_version": "9.9.9",
624 "module_sha256": "{}", "memory_min_bytes": 65536,
625 "brand_new_field": {{ "nested": [1,2,3] }},
626 "proven_safe": [{{"func":0,"pc":1,"op":"i32.load","width":4,
627 "confidence":"high"}}] }}"#,
628 hex_sha256(MODULE)
629 ),
630 );
631 let r = ingest(&p, MODULE, 65536);
632 assert!(r.accepted, "{:?}", r.refusal);
633 assert_eq!(r.offered.len(), 1);
634 }
635
636 #[test]
637 fn accepted_but_empty_is_diagnosed_901() {
638 let p = write_tmp("none.json", &doc(&hex_sha256(MODULE), 65536, ""));
639 let r = ingest(&p, MODULE, 65536);
640 assert!(r.accepted);
641 assert!(r.offered.is_empty());
642 assert!(
643 r.diagnostics
644 .iter()
645 .any(|d| d.contains("ZERO access sites")),
646 "an accepted-but-vacuous document must say so: {:?}",
647 r.diagnostics
648 );
649 }
650
651 fn ops() -> Vec<WasmOp> {
654 vec![
655 WasmOp::LocalGet(0), WasmOp::I32Load {
657 offset: 0,
658 align: 2,
659 }, WasmOp::LocalGet(0), WasmOp::I32Load8U {
662 offset: 1,
663 align: 0,
664 }, WasmOp::I32Add, WasmOp::I64Store {
667 offset: 8,
668 align: 3,
669 }, ]
671 }
672
673 #[test]
674 fn valid_sites_become_marks_901() {
675 let p = write_tmp(
676 "marks.json",
677 &doc(
678 &hex_sha256(MODULE),
679 65536,
680 r#"{"func":0,"pc":5,"op":"i64.store","width":8},
681 {"func":0,"pc":1,"op":"i32.load","width":4},
682 {"func":0,"pc":3,"op":"i32.load8_u","width":1}"#,
683 ),
684 );
685 let r = ingest(&p, MODULE, 65536);
686 let mut notes = Vec::new();
687 assert_eq!(r.validate_function(0, &ops(), &mut notes), vec![1, 3, 5]);
688 assert!(notes.is_empty(), "{notes:?}");
689 }
690
691 #[test]
695 fn byte_offsets_instead_of_op_indices_elide_nothing_loudly_901() {
696 let p = write_tmp(
697 "byteoffsets.json",
698 &doc(
699 &hex_sha256(MODULE),
700 65536,
701 r#"{"func":0,"pc":41,"op":"i32.load","width":4},
702 {"func":0,"pc":137,"op":"i32.load8_u","width":1}"#,
703 ),
704 );
705 let r = ingest(&p, MODULE, 65536);
706 assert!(
707 r.accepted,
708 "the FILE is well formed — only the keys are wrong"
709 );
710 let mut notes = Vec::new();
711 assert_eq!(
712 r.validate_function(0, &ops(), &mut notes),
713 Vec::<usize>::new()
714 );
715 assert_eq!(notes.len(), 2);
716 assert!(
717 notes.iter().all(|n| n.contains("out of range")),
718 "{notes:?}"
719 );
720 assert!(notes[0].contains("OPERATOR index"), "{notes:?}");
721 }
722
723 #[test]
724 fn non_access_operator_is_dropped_901() {
725 let p = write_tmp(
726 "nonaccess.json",
727 &doc(
728 &hex_sha256(MODULE),
729 65536,
730 r#"{"func":0,"pc":4,"op":"i32.load","width":4}"#,
731 ),
732 );
733 let mut notes = Vec::new();
734 let marks = ingest(&p, MODULE, 65536).validate_function(0, &ops(), &mut notes);
735 assert_eq!(marks, Vec::<usize>::new());
736 assert!(notes[0].contains("not a linear-memory access"), "{notes:?}");
737 }
738
739 #[test]
740 fn width_disagreement_is_dropped_901() {
741 let p = write_tmp(
744 "width.json",
745 &doc(
746 &hex_sha256(MODULE),
747 65536,
748 r#"{"func":0,"pc":3,"op":"i32.load","width":4},
749 {"func":0,"pc":1,"op":"i32.load","width":4}"#,
750 ),
751 );
752 let mut notes = Vec::new();
753 let marks = ingest(&p, MODULE, 65536).validate_function(0, &ops(), &mut notes);
754 assert_eq!(
755 marks,
756 vec![1],
757 "the sound entry survives, the skewed one does not"
758 );
759 assert_eq!(notes.len(), 1);
760 assert!(
761 notes[0].contains("disagrees with the decoded operator"),
762 "{notes:?}"
763 );
764 }
765
766 #[test]
767 fn sites_are_keyed_per_function_901() {
768 let p = write_tmp(
769 "perfunc.json",
770 &doc(
771 &hex_sha256(MODULE),
772 65536,
773 r#"{"func":7,"pc":1,"op":"i32.load","width":4}"#,
774 ),
775 );
776 let r = ingest(&p, MODULE, 65536);
777 let mut notes = Vec::new();
778 assert_eq!(
780 r.validate_function(0, &ops(), &mut notes),
781 Vec::<usize>::new()
782 );
783 assert!(notes.is_empty());
784 assert_eq!(r.validate_function(7, &ops(), &mut notes), vec![1]);
785 }
786
787 #[test]
790 fn access_width_covers_the_bytes_touched_not_the_value_width_901() {
791 assert_eq!(
792 access_width(&WasmOp::I64Load32U {
793 offset: 0,
794 align: 2
795 }),
796 Some(4)
797 );
798 assert_eq!(
799 access_width(&WasmOp::I64Store8 {
800 offset: 0,
801 align: 0
802 }),
803 Some(1)
804 );
805 assert_eq!(
806 access_width(&WasmOp::I32Load16S {
807 offset: 0,
808 align: 1
809 }),
810 Some(2)
811 );
812 assert_eq!(
813 access_width(&WasmOp::F64Load {
814 offset: 0,
815 align: 3
816 }),
817 Some(8)
818 );
819 assert_eq!(access_width(&WasmOp::I32Add), None);
820 assert_eq!(access_width(&WasmOp::LocalGet(0)), None);
821 }
822
823 #[test]
826 fn attestation_sidecar_path_mirrors_the_safety_manifest_901() {
827 assert_eq!(
828 ElisionAttestation::sidecar_path(Path::new("/tmp/foo.elf")),
829 PathBuf::from("/tmp/foo.proven-safe-elisions.json")
830 );
831 assert_eq!(
832 ElisionAttestation::sidecar_path(Path::new("out")),
833 PathBuf::from("out.proven-safe-elisions.json")
834 );
835 }
836
837 #[test]
838 fn refusal_is_attested_not_hidden_901() {
839 let a = ElisionAttestation {
840 schema: ELISION_ATTESTATION_SCHEMA.to_string(),
841 synth_version: "0.55.0".to_string(),
842 scry_version: "3.2.4".to_string(),
843 module_sha256: "aa".repeat(32),
844 declared_module_sha256: "bb".repeat(32),
845 memory_min_bytes: Some(65536),
846 declared_memory_min_bytes: 65536,
847 safety_bounds: "software".to_string(),
848 accepted: false,
849 refusal: Some("module_sha256 mismatch".to_string()),
850 sites_offered: 8,
851 sites_elided: 0,
852 sites_not_elided: 8,
853 elisions: Vec::new(),
854 diagnostics: vec!["refused".to_string()],
855 };
856 let json = a.to_json();
857 assert!(json.contains("\"accepted\": false"));
859 assert!(json.contains("module_sha256 mismatch"));
860 assert!(json.contains("\"sites_offered\": 8"));
861 assert!(json.contains("\"sites_elided\": 0"));
862 let back: ElisionAttestation = serde_json::from_str(&json).expect("round-trips");
863 assert_eq!(back, a);
864 }
865
866 #[test]
873 fn no_floor_attests_null_not_zero_rq57() {
874 let a = ElisionAttestation {
875 schema: ELISION_ATTESTATION_SCHEMA.to_string(),
876 synth_version: "0.56.1".to_string(),
877 scry_version: "3.2.4".to_string(),
878 module_sha256: "aa".repeat(32),
879 declared_module_sha256: "aa".repeat(32),
880 memory_min_bytes: None,
881 declared_memory_min_bytes: 65536,
882 safety_bounds: "software".to_string(),
883 accepted: false,
884 refusal: Some("no memory floor can be established".to_string()),
885 sites_offered: 1,
886 sites_elided: 0,
887 sites_not_elided: 1,
888 elisions: Vec::new(),
889 diagnostics: Vec::new(),
890 };
891 let json = a.to_json();
892 assert!(
893 json.contains("\"memory_min_bytes\": null"),
894 "absence must be an explicit null, got:\n{json}"
895 );
896 assert!(
897 !json.contains("\"memory_min_bytes\": 0"),
898 "the invented-0 floor must be unrepresentable, got:\n{json}"
899 );
900 let back: ElisionAttestation = serde_json::from_str(&json).expect("round-trips");
901 assert_eq!(back, a);
902 }
903}