1use crate::{
2 AllocationBootstrap, AllocationDeclaration, AllocationHistory, AllocationLedger,
3 AllocationPolicy, AllocationSlotDescriptor, CborLedgerCodec, DeclarationSnapshot,
4 StableCellLedgerError, StableCellLedgerRecord, StableKey, ValidatedAllocations,
5 registry::{
6 StaticMemoryDeclaration, StaticMemoryDeclarationError, StaticMemoryRangeDeclaration,
7 seal_static_memory_registry, static_memory_declarations, static_memory_range_declarations,
8 },
9 slot::{
10 IC_MEMORY_AUTHORITY_OWNER, IC_MEMORY_AUTHORITY_PURPOSE, IC_MEMORY_LEDGER_LABEL,
11 IC_MEMORY_LEDGER_STABLE_KEY, MEMORY_MANAGER_LEDGER_ID, MemoryManagerAuthorityRecord,
12 MemoryManagerIdRange, MemoryManagerRangeAuthority, MemoryManagerRangeAuthorityError,
13 MemoryManagerRangeMode, MemoryManagerSlotError,
14 },
15};
16use ic_stable_structures::{
17 Cell, DefaultMemoryImpl,
18 memory_manager::{MemoryId, MemoryManager, VirtualMemory},
19};
20use std::{
21 cell::RefCell,
22 collections::BTreeMap,
23 convert::Infallible,
24 sync::{
25 Mutex,
26 atomic::{AtomicBool, Ordering},
27 },
28};
29
30type DefaultLedgerCell = Cell<StableCellLedgerRecord, VirtualMemory<DefaultMemoryImpl>>;
31
32thread_local! {
33 static DEFAULT_MEMORY_MANAGER: MemoryManager<DefaultMemoryImpl> =
34 MemoryManager::init(DefaultMemoryImpl::default());
35 static DEFAULT_LEDGER_CELL: RefCell<Option<DefaultLedgerCell>> = const {
36 RefCell::new(None)
37 };
38}
39
40static EAGER_INIT_HOOKS: Mutex<Vec<fn()>> = Mutex::new(Vec::new());
41static VALIDATED_ALLOCATIONS: Mutex<Option<ValidatedAllocations>> = Mutex::new(None);
42static BOOTSTRAPPED: AtomicBool = AtomicBool::new(false);
43
44#[derive(Debug, thiserror::Error)]
49pub enum RuntimeBootstrapError<P> {
50 #[error(transparent)]
52 Registry(#[from] StaticMemoryDeclarationError),
53 #[error(transparent)]
55 Range(#[from] MemoryManagerRangeAuthorityError),
56 #[error(transparent)]
58 LedgerIntegrity(#[from] crate::LedgerIntegrityError),
59 #[error(transparent)]
61 LedgerCommit(#[from] crate::LedgerCommitError<serde_cbor::Error>),
62 #[error(transparent)]
64 StableCellLedger(#[from] StableCellLedgerError),
65 #[error(transparent)]
67 Validation(#[from] crate::AllocationValidationError<RuntimePolicyError<P>>),
68 #[error(transparent)]
70 Staging(#[from] crate::AllocationStageError),
71 #[error("ic-memory runtime lock poisoned")]
73 RuntimeLockPoisoned,
74}
75
76#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
81pub enum RuntimeOpenError {
82 #[error("ic-memory runtime has not completed bootstrap validation")]
84 NotBootstrapped,
85 #[error("ic-memory runtime lock poisoned")]
87 RuntimeLockPoisoned,
88 #[error(transparent)]
90 StableKey(#[from] crate::StableKeyError),
91 #[error("stable key '{0}' was not validated by ic-memory runtime bootstrap")]
93 StableKeyNotValidated(String),
94 #[error(transparent)]
96 MemoryManagerSlot(#[from] MemoryManagerSlotError),
97 #[error(
99 "stable key '{stable_key}' is validated for MemoryManager ID {validated_id}, not requested ID {requested_id}"
100 )]
101 MemoryIdMismatch {
102 stable_key: String,
104 validated_id: u8,
106 requested_id: u8,
108 },
109}
110
111#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
116pub enum RuntimePolicyError<P> {
117 #[error(transparent)]
119 Range(#[from] MemoryManagerRangeAuthorityError),
120 #[error("runtime declaration metadata is missing for stable key '{0}'")]
122 MissingDeclarationMetadata(String),
123 #[error("stable key '{stable_key}' is reserved to authority '{expected_authority}'")]
125 ReservedStableKeyAuthority {
126 stable_key: String,
128 expected_authority: &'static str,
130 },
131 #[error(transparent)]
133 Custom(P),
134}
135
136pub fn defer_eager_init(f: fn()) {
138 EAGER_INIT_HOOKS
139 .lock()
140 .expect("ic-memory eager-init queue poisoned")
141 .push(f);
142}
143
144#[must_use]
146pub fn is_default_memory_manager_bootstrapped() -> bool {
147 BOOTSTRAPPED.load(Ordering::SeqCst)
148}
149
150pub fn validated_allocations() -> Result<ValidatedAllocations, RuntimeOpenError> {
152 if !is_default_memory_manager_bootstrapped() {
153 return Err(RuntimeOpenError::NotBootstrapped);
154 }
155 VALIDATED_ALLOCATIONS
156 .lock()
157 .map_err(|_| RuntimeOpenError::RuntimeLockPoisoned)?
158 .clone()
159 .ok_or(RuntimeOpenError::NotBootstrapped)
160}
161
162pub fn bootstrap_default_memory_manager()
164-> Result<ValidatedAllocations, RuntimeBootstrapError<Infallible>> {
165 bootstrap_default_memory_manager_with_policy(&NoopPolicy)
166}
167
168pub fn bootstrap_default_memory_manager_with_policy<P: AllocationPolicy>(
183 policy: &P,
184) -> Result<ValidatedAllocations, RuntimeBootstrapError<P::Error>> {
185 if let Ok(validated) = validated_allocations() {
186 return Ok(validated);
187 }
188
189 run_eager_init_hooks();
190
191 let registered_declarations = static_memory_declarations()?;
192 let registered_ranges = static_memory_range_declarations()?;
193 let user_ranges_registered = !registered_ranges.is_empty();
194 let declaration_metadata = declaration_metadata(®istered_declarations);
195 let range_authority = range_authority(registered_ranges)?;
196 let snapshot = declaration_snapshot(registered_declarations)?;
197 seal_static_memory_registry()?;
198 let policy = RuntimeMemoryManagerPolicy {
199 range_authority,
200 user_ranges_registered,
201 declaration_metadata,
202 custom_policy: policy,
203 };
204 let genesis = AllocationLedger::new(
205 crate::CURRENT_LEDGER_SCHEMA_VERSION,
206 crate::CURRENT_PHYSICAL_FORMAT_ID,
207 0,
208 AllocationHistory::default(),
209 )?;
210
211 let validated = with_default_ledger_cell(
212 |cell| -> Result<ValidatedAllocations, RuntimeBootstrapError<P::Error>> {
213 let mut record = cell.get().clone();
214 let mut bootstrap = AllocationBootstrap::new(record.store_mut());
215 let commit = bootstrap
216 .initialize_validate_and_commit(&CborLedgerCodec, &genesis, snapshot, &policy, None)
217 .map_err(runtime_bootstrap_error_from_bootstrap)?;
218 cell.set(record);
219 Ok(commit.validated)
220 },
221 )?;
222
223 publish_validated_allocations(validated.clone())?;
224 BOOTSTRAPPED.store(true, Ordering::SeqCst);
225 Ok(validated)
226}
227
228pub fn open_default_memory_manager_memory(
230 stable_key: &str,
231 id: u8,
232) -> Result<VirtualMemory<DefaultMemoryImpl>, RuntimeOpenError> {
233 let key = StableKey::parse(stable_key)?;
234 let validated = validated_allocations()?;
235 let slot = validated
236 .slot_for(&key)
237 .ok_or_else(|| RuntimeOpenError::StableKeyNotValidated(stable_key.to_string()))?;
238 let validated_id = slot.memory_manager_id()?;
239 if validated_id != id {
240 return Err(RuntimeOpenError::MemoryIdMismatch {
241 stable_key: stable_key.to_string(),
242 validated_id,
243 requested_id: id,
244 });
245 }
246 Ok(default_memory_manager_memory(id))
247}
248
249fn run_eager_init_hooks() {
250 let hooks = {
251 let mut hooks = EAGER_INIT_HOOKS
252 .lock()
253 .expect("ic-memory eager-init queue poisoned");
254 std::mem::take(&mut *hooks)
255 };
256
257 for hook in hooks {
258 hook();
259 }
260}
261
262fn with_default_ledger_cell<P, T>(
263 op: impl FnOnce(&mut DefaultLedgerCell) -> Result<T, RuntimeBootstrapError<P>>,
264) -> Result<T, RuntimeBootstrapError<P>> {
265 DEFAULT_LEDGER_CELL.with(|cell| {
266 let mut cell = cell.borrow_mut();
267 if cell.is_none() {
268 let memory = default_memory_manager_memory(MEMORY_MANAGER_LEDGER_ID);
269 crate::validate_stable_cell_ledger_memory(&memory)?;
270 *cell = Some(Cell::init(memory, StableCellLedgerRecord::default()));
271 }
272 op(cell.as_mut().expect("default ledger cell initialized"))
273 })
274}
275
276fn default_memory_manager_memory(id: u8) -> VirtualMemory<DefaultMemoryImpl> {
277 DEFAULT_MEMORY_MANAGER.with(|manager| manager.get(MemoryId::new(id)))
278}
279
280fn publish_validated_allocations<P>(
281 validated: ValidatedAllocations,
282) -> Result<(), RuntimeBootstrapError<P>> {
283 *VALIDATED_ALLOCATIONS
284 .lock()
285 .map_err(|_| RuntimeBootstrapError::RuntimeLockPoisoned)? = Some(validated);
286 Ok(())
287}
288
289fn declaration_snapshot(
290 registrations: Vec<StaticMemoryDeclaration>,
291) -> Result<DeclarationSnapshot, StaticMemoryDeclarationError> {
292 let mut declarations = Vec::with_capacity(registrations.len() + 1);
293 declarations.push(internal_ledger_declaration()?);
294 declarations.extend(
295 registrations
296 .into_iter()
297 .map(StaticMemoryDeclaration::into_declaration),
298 );
299 DeclarationSnapshot::new(declarations).map_err(StaticMemoryDeclarationError::Declaration)
300}
301
302fn declaration_metadata(registrations: &[StaticMemoryDeclaration]) -> BTreeMap<String, String> {
303 let mut metadata = BTreeMap::new();
304 metadata.insert(
305 IC_MEMORY_LEDGER_STABLE_KEY.to_string(),
306 IC_MEMORY_AUTHORITY_OWNER.to_string(),
307 );
308 for registration in registrations {
309 metadata.insert(
310 registration.declaration().stable_key().as_str().to_string(),
311 registration.declaring_crate().to_string(),
312 );
313 }
314 metadata
315}
316
317fn range_authority(
318 registrations: Vec<StaticMemoryRangeDeclaration>,
319) -> Result<MemoryManagerRangeAuthority, MemoryManagerRangeAuthorityError> {
320 let mut records = Vec::with_capacity(registrations.len() + 1);
321 records.push(internal_ledger_range()?);
322 records.extend(
323 registrations
324 .into_iter()
325 .map(StaticMemoryRangeDeclaration::into_record),
326 );
327 MemoryManagerRangeAuthority::from_records(records)
328}
329
330fn internal_ledger_declaration() -> Result<AllocationDeclaration, crate::DeclarationSnapshotError> {
331 AllocationDeclaration::memory_manager(
332 IC_MEMORY_LEDGER_STABLE_KEY,
333 MEMORY_MANAGER_LEDGER_ID,
334 IC_MEMORY_LEDGER_LABEL,
335 )
336}
337
338fn internal_ledger_range() -> Result<MemoryManagerAuthorityRecord, MemoryManagerRangeAuthorityError>
339{
340 MemoryManagerAuthorityRecord::new(
341 MemoryManagerIdRange::new(
342 MEMORY_MANAGER_LEDGER_ID,
343 crate::MEMORY_MANAGER_GOVERNANCE_MAX_ID,
344 )?,
345 IC_MEMORY_AUTHORITY_OWNER,
346 MemoryManagerRangeMode::Reserved,
347 Some(IC_MEMORY_AUTHORITY_PURPOSE.to_string()),
348 )
349}
350
351fn runtime_bootstrap_error_from_bootstrap<P>(
352 err: crate::BootstrapError<serde_cbor::Error, RuntimePolicyError<P>>,
353) -> RuntimeBootstrapError<P> {
354 match err {
355 crate::BootstrapError::Ledger(err) => RuntimeBootstrapError::LedgerCommit(err),
356 crate::BootstrapError::Validation(err) => RuntimeBootstrapError::Validation(err),
357 crate::BootstrapError::Staging(err) => RuntimeBootstrapError::Staging(err),
358 }
359}
360
361struct RuntimeMemoryManagerPolicy<'a, P> {
362 range_authority: MemoryManagerRangeAuthority,
363 user_ranges_registered: bool,
364 declaration_metadata: BTreeMap<String, String>,
365 custom_policy: &'a P,
366}
367
368impl<P: AllocationPolicy> AllocationPolicy for RuntimeMemoryManagerPolicy<'_, P> {
369 type Error = RuntimePolicyError<P::Error>;
370
371 fn validate_key(&self, key: &StableKey) -> Result<(), Self::Error> {
372 let declaring_crate = self.declaring_crate(key)?;
373 if crate::is_ic_memory_stable_key(key.as_str())
374 && declaring_crate != IC_MEMORY_AUTHORITY_OWNER
375 {
376 return Err(RuntimePolicyError::ReservedStableKeyAuthority {
377 stable_key: key.as_str().to_string(),
378 expected_authority: IC_MEMORY_AUTHORITY_OWNER,
379 });
380 }
381 self.custom_policy
382 .validate_key(key)
383 .map_err(RuntimePolicyError::Custom)
384 }
385
386 fn validate_slot(
387 &self,
388 key: &StableKey,
389 slot: &AllocationSlotDescriptor,
390 ) -> Result<(), Self::Error> {
391 self.validate_runtime_range(key, slot)?;
392 self.custom_policy
393 .validate_slot(key, slot)
394 .map_err(RuntimePolicyError::Custom)
395 }
396
397 fn validate_reserved_slot(
398 &self,
399 key: &StableKey,
400 slot: &AllocationSlotDescriptor,
401 ) -> Result<(), Self::Error> {
402 self.validate_runtime_range(key, slot)?;
403 self.custom_policy
404 .validate_reserved_slot(key, slot)
405 .map_err(RuntimePolicyError::Custom)
406 }
407}
408
409impl<P: AllocationPolicy> RuntimeMemoryManagerPolicy<'_, P> {
410 fn declaring_crate(&self, key: &StableKey) -> Result<&str, RuntimePolicyError<P::Error>> {
411 self.declaration_metadata
412 .get(key.as_str())
413 .map(String::as_str)
414 .ok_or_else(|| RuntimePolicyError::MissingDeclarationMetadata(key.as_str().to_string()))
415 }
416
417 fn validate_runtime_range(
418 &self,
419 key: &StableKey,
420 slot: &AllocationSlotDescriptor,
421 ) -> Result<(), RuntimePolicyError<P::Error>> {
422 let declaring_crate = self.declaring_crate(key)?;
423 if declaring_crate == IC_MEMORY_AUTHORITY_OWNER || self.user_ranges_registered {
429 self.range_authority
430 .validate_slot_authority(slot, declaring_crate)?;
431 return Ok(());
432 }
433
434 let id = slot
435 .memory_manager_id()
436 .map_err(MemoryManagerRangeAuthorityError::Slot)?;
437 if self
438 .range_authority
439 .authority_for_id(id)
440 .map_err(RuntimePolicyError::Range)?
441 .is_some()
442 {
443 self.range_authority
444 .validate_slot_authority(slot, declaring_crate)?;
445 }
446 Ok(())
447 }
448}
449
450struct NoopPolicy;
451
452impl AllocationPolicy for NoopPolicy {
453 type Error = Infallible;
454
455 fn validate_key(&self, _key: &StableKey) -> Result<(), Self::Error> {
456 Ok(())
457 }
458
459 fn validate_slot(
460 &self,
461 _key: &StableKey,
462 _slot: &AllocationSlotDescriptor,
463 ) -> Result<(), Self::Error> {
464 Ok(())
465 }
466
467 fn validate_reserved_slot(
468 &self,
469 _key: &StableKey,
470 _slot: &AllocationSlotDescriptor,
471 ) -> Result<(), Self::Error> {
472 Ok(())
473 }
474}
475
476#[cfg(test)]
477pub(crate) fn reset_for_tests() {
478 crate::registry::reset_static_memory_declarations_for_tests();
479 EAGER_INIT_HOOKS
480 .lock()
481 .expect("ic-memory eager-init queue poisoned")
482 .clear();
483 *VALIDATED_ALLOCATIONS
484 .lock()
485 .expect("ic-memory runtime validation state poisoned") = None;
486 BOOTSTRAPPED.store(false, Ordering::SeqCst);
487 DEFAULT_LEDGER_CELL.with_borrow_mut(|cell| {
488 *cell = None;
489 });
490}
491
492#[cfg(test)]
493mod tests {
494 use super::*;
495 use crate::registry::{
496 TEST_REGISTRY_LOCK, register_static_memory_manager_declaration,
497 register_static_memory_manager_range,
498 };
499 use std::sync::atomic::{AtomicBool, Ordering};
500
501 static EAGER_INIT_RAN: AtomicBool = AtomicBool::new(false);
502
503 fn register_crate_a() {
504 register_static_memory_manager_range(
505 100,
506 109,
507 "crate_a",
508 MemoryManagerRangeMode::Reserved,
509 None,
510 )
511 .expect("crate A range");
512 register_static_memory_manager_declaration(100, "crate_a", "users", "crate_a.users.v1")
513 .expect("crate A memory");
514 }
515
516 fn register_crate_b() {
517 register_static_memory_manager_range(
518 110,
519 119,
520 "crate_b",
521 MemoryManagerRangeMode::Reserved,
522 None,
523 )
524 .expect("crate B range");
525 register_static_memory_manager_declaration(110, "crate_b", "orders", "crate_b.orders.v1")
526 .expect("crate B memory");
527 }
528
529 fn mark_eager_init() {
530 EAGER_INIT_RAN.store(true, Ordering::SeqCst);
531 register_static_memory_manager_declaration(101, "crate_a", "audit", "crate_a.audit.v1")
532 .expect("eager-init declaration");
533 }
534
535 #[test]
536 fn multi_crate_declarations_compose_into_one_bootstrap() {
537 let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
538 reset_for_tests();
539 register_crate_a();
540 register_crate_b();
541
542 let validated = bootstrap_default_memory_manager().expect("bootstrap");
543
544 assert_eq!(validated.declarations().len(), 3);
545 assert!(
546 validated
547 .declarations()
548 .iter()
549 .any(|declaration| declaration.stable_key().as_str() == "crate_a.users.v1")
550 );
551 assert!(
552 validated
553 .declarations()
554 .iter()
555 .any(|declaration| declaration.stable_key().as_str() == "crate_b.orders.v1")
556 );
557 }
558
559 #[test]
560 fn conflicting_ranges_fail() {
561 let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
562 reset_for_tests();
563 register_static_memory_manager_range(
564 100,
565 110,
566 "crate_a",
567 MemoryManagerRangeMode::Reserved,
568 None,
569 )
570 .expect("crate A range");
571 register_static_memory_manager_range(
572 105,
573 119,
574 "crate_b",
575 MemoryManagerRangeMode::Reserved,
576 None,
577 )
578 .expect("crate B range");
579
580 let err = bootstrap_default_memory_manager().expect_err("overlap must fail");
581 assert!(matches!(
582 err,
583 RuntimeBootstrapError::Range(
584 MemoryManagerRangeAuthorityError::OverlappingRanges { .. }
585 )
586 ));
587 }
588
589 #[test]
590 fn duplicate_stable_keys_fail() {
591 let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
592 reset_for_tests();
593 register_static_memory_manager_declaration(100, "crate_a", "users", "app.users.v1")
594 .expect("first declaration");
595 register_static_memory_manager_declaration(101, "crate_b", "users", "app.users.v1")
596 .expect("second declaration");
597
598 let err = bootstrap_default_memory_manager().expect_err("duplicate key must fail");
599 assert!(matches!(
600 err,
601 RuntimeBootstrapError::Registry(StaticMemoryDeclarationError::Declaration(
602 crate::DeclarationSnapshotError::DuplicateStableKey(_)
603 ))
604 ));
605 }
606
607 #[test]
608 fn duplicate_memory_manager_ids_fail() {
609 let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
610 reset_for_tests();
611 register_static_memory_manager_declaration(100, "crate_a", "users", "crate_a.users.v1")
612 .expect("first declaration");
613 register_static_memory_manager_declaration(100, "crate_b", "orders", "crate_b.orders.v1")
614 .expect("second declaration");
615
616 let err = bootstrap_default_memory_manager().expect_err("duplicate slot must fail");
617 assert!(matches!(
618 err,
619 RuntimeBootstrapError::Registry(StaticMemoryDeclarationError::Declaration(
620 crate::DeclarationSnapshotError::DuplicateSlot(_)
621 ))
622 ));
623 }
624
625 #[test]
626 fn out_of_range_memory_declaration_fails_when_ranges_are_declared() {
627 let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
628 reset_for_tests();
629 register_static_memory_manager_range(
630 100,
631 109,
632 "crate_a",
633 MemoryManagerRangeMode::Reserved,
634 None,
635 )
636 .expect("crate A range");
637 register_static_memory_manager_declaration(120, "crate_a", "users", "crate_a.users.v1")
638 .expect("out-of-range declaration");
639
640 let err = bootstrap_default_memory_manager().expect_err("out of range must fail");
641 assert!(matches!(
642 err,
643 RuntimeBootstrapError::Validation(crate::AllocationValidationError::Policy(
644 RuntimePolicyError::Range(MemoryManagerRangeAuthorityError::UnclaimedId {
645 id: 120
646 })
647 ))
648 ));
649 }
650
651 #[test]
652 fn late_registration_after_bootstrap_fails() {
653 let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
654 reset_for_tests();
655 register_static_memory_manager_declaration(100, "crate_a", "users", "crate_a.users.v1")
656 .expect("declaration");
657 bootstrap_default_memory_manager().expect("bootstrap");
658
659 let err = register_static_memory_manager_declaration(
660 101,
661 "crate_a",
662 "orders",
663 "crate_a.orders.v1",
664 )
665 .expect_err("late registration must fail");
666 assert_eq!(err, StaticMemoryDeclarationError::RegistrySealed);
667 }
668
669 #[test]
670 fn eager_init_runs_before_snapshot_seal() {
671 let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
672 reset_for_tests();
673 EAGER_INIT_RAN.store(false, Ordering::SeqCst);
674 register_static_memory_manager_range(
675 100,
676 109,
677 "crate_a",
678 MemoryManagerRangeMode::Reserved,
679 None,
680 )
681 .expect("crate A range");
682 defer_eager_init(mark_eager_init);
683
684 let validated = bootstrap_default_memory_manager().expect("bootstrap");
685
686 assert!(EAGER_INIT_RAN.load(Ordering::SeqCst));
687 assert!(
688 validated
689 .declarations()
690 .iter()
691 .any(|declaration| declaration.stable_key().as_str() == "crate_a.audit.v1")
692 );
693 }
694
695 #[test]
696 fn direct_user_can_bootstrap_and_open_without_canic() {
697 let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
698 reset_for_tests();
699 register_static_memory_manager_range(
700 120,
701 129,
702 "icydb",
703 MemoryManagerRangeMode::Reserved,
704 None,
705 )
706 .expect("icydb range");
707 register_static_memory_manager_declaration(120, "icydb", "users", "icydb.users.data.v1")
708 .expect("icydb declaration");
709
710 bootstrap_default_memory_manager().expect("bootstrap");
711 open_default_memory_manager_memory("icydb.users.data.v1", 120).expect("open memory");
712 }
713}