1use crate::{
20 common::{
21 ActorExecutionError, ActorExecutionErrorReplyReason, DispatchResult, DispatchResultKind,
22 ExecutionError, SystemExecutionError, WasmExecutionContext,
23 },
24 configs::{BlockInfo, ExecutionSettings},
25 ext::{ProcessorContext, ProcessorExternalities},
26};
27use actor_system_error::actor_system_error;
28use alloc::{
29 collections::{BTreeMap, BTreeSet},
30 format,
31 string::{String, ToString},
32 vec::Vec,
33};
34use gear_backend_common::{
35 lazy_pages::{GlobalsAccessConfig, LazyPagesWeights},
36 ActorTerminationReason, BackendExternalities, BackendReport, BackendSyscallError, Environment,
37 EnvironmentError, TerminationReason,
38};
39use gear_core::{
40 code::InstrumentedCode,
41 env::Externalities,
42 gas::{CountersOwner, GasAllowanceCounter, GasCounter, ValueCounter},
43 ids::ProgramId,
44 memory::{AllocationsContext, Memory, MemoryError, PageBuf},
45 message::{
46 ContextSettings, DispatchKind, IncomingDispatch, IncomingMessage, MessageContext,
47 WasmEntryPoint,
48 },
49 pages::{GearPage, PageU32Size, WasmPage},
50 program::Program,
51 reservation::GasReserver,
52};
53use scale_info::{
54 scale::{self, Decode, Encode},
55 TypeInfo,
56};
57
58actor_system_error! {
59 pub type PrepareMemoryError = ActorSystemError<ActorPrepareMemoryError, SystemPrepareMemoryError>;
61}
62
63#[derive(Encode, Decode, TypeInfo, Debug, PartialEq, Eq, PartialOrd, Ord, derive_more::Display)]
65#[codec(crate = scale)]
66pub enum ActorPrepareMemoryError {
67 #[display(fmt = "Stack end page {_0:?} is bigger then WASM static memory size {_1:?}")]
69 StackEndPageBiggerWasmMemSize(WasmPage, WasmPage),
70 #[display(fmt = "Set initial data for stack pages is restricted")]
72 StackPagesHaveInitialData,
73 #[display(fmt = "Stack end addr {_0:#x} must be aligned to WASM page size")]
75 StackIsNotAligned(u32),
76}
77
78#[derive(Debug, Eq, PartialEq, derive_more::Display)]
79pub enum SystemPrepareMemoryError {
80 #[display(fmt = "Mem size less then static pages num")]
82 InsufficientMemorySize,
83 #[display(fmt = "{_0:?} is not allocated for program")]
85 PageIsNotAllocated(GearPage),
86 #[display(fmt = "Cannot read data for {_0:?}: {_1}")]
88 InitialMemoryReadFailed(GearPage, MemoryError),
89 #[display(fmt = "Cannot write initial data for {_0:?}: {_1}")]
91 InitialDataWriteFailed(GearPage, MemoryError),
92 #[display(fmt = "Initial pages data must be empty when execute with lazy pages")]
94 InitialPagesContainsDataInLazyPagesMode,
95}
96
97fn check_memory<'a>(
99 allocations: &BTreeSet<WasmPage>,
100 pages_with_data: impl Iterator<Item = &'a GearPage>,
101 static_pages: WasmPage,
102 memory_size: WasmPage,
103) -> Result<(), SystemPrepareMemoryError> {
104 if memory_size < static_pages {
105 log::error!(
106 "Mem size less then static pages num: mem_size = {:?}, static_pages = {:?}",
107 memory_size,
108 static_pages
109 );
110 return Err(SystemPrepareMemoryError::InsufficientMemorySize);
111 }
112
113 for page in pages_with_data {
115 let wasm_page = page.to_page();
116 if wasm_page >= static_pages && !allocations.contains(&wasm_page) {
117 return Err(SystemPrepareMemoryError::PageIsNotAllocated(*page));
118 }
119 }
120
121 Ok(())
122}
123
124fn lazy_pages_check_initial_data(
125 initial_pages_data: &BTreeMap<GearPage, PageBuf>,
126) -> Result<(), SystemPrepareMemoryError> {
127 initial_pages_data
128 .is_empty()
129 .then_some(())
130 .ok_or(SystemPrepareMemoryError::InitialPagesContainsDataInLazyPagesMode)
131}
132
133fn prepare_memory<ProcessorExt: ProcessorExternalities, EnvMem: Memory>(
135 mem: &mut EnvMem,
136 program_id: ProgramId,
137 pages_data: &mut BTreeMap<GearPage, PageBuf>,
138 static_pages: WasmPage,
139 stack_end: Option<u32>,
140 globals_config: GlobalsAccessConfig,
141 lazy_pages_weights: LazyPagesWeights,
142) -> Result<(), PrepareMemoryError> {
143 let stack_end = if let Some(stack_end) = stack_end {
144 let stack_end = (stack_end % WasmPage::size() == 0)
145 .then_some(WasmPage::from_offset(stack_end))
146 .ok_or(ActorPrepareMemoryError::StackIsNotAligned(stack_end))?;
147
148 if stack_end > static_pages {
149 return Err(ActorPrepareMemoryError::StackEndPageBiggerWasmMemSize(
150 stack_end,
151 static_pages,
152 )
153 .into());
154 }
155
156 Some(stack_end)
157 } else {
158 None
159 };
160
161 for (page, data) in pages_data.iter_mut() {
163 mem.write(page.offset(), data)
164 .map_err(|err| SystemPrepareMemoryError::InitialDataWriteFailed(*page, err))?;
165 }
166
167 if ProcessorExt::LAZY_PAGES_ENABLED {
168 lazy_pages_check_initial_data(pages_data)?;
169
170 ProcessorExt::lazy_pages_init_for_program(
171 mem,
172 program_id,
173 stack_end,
174 globals_config,
175 lazy_pages_weights,
176 );
177 } else {
178 let begin = stack_end.unwrap_or_default();
182
183 if pages_data.keys().any(|&p| p < begin.to_page()) {
184 return Err(ActorPrepareMemoryError::StackPagesHaveInitialData.into());
185 }
186
187 let non_stack_pages = begin.iter_end(static_pages).unwrap_or_else(|err| {
188 unreachable!(
189 "We have already checked that `stack_end` is <= `static_pages`, but get: {}",
190 err
191 )
192 });
193 for page in non_stack_pages.flat_map(|p| p.to_pages_iter()) {
194 if pages_data.contains_key(&page) {
195 continue;
197 }
198 let mut data = PageBuf::new_zeroed();
199 mem.read(page.offset(), &mut data)
200 .map_err(|err| SystemPrepareMemoryError::InitialMemoryReadFailed(page, err))?;
201 pages_data.insert(page, data);
202 }
203 }
204 Ok(())
205}
206
207fn get_pages_to_be_updated<ProcessorExt: ProcessorExternalities>(
209 old_pages_data: BTreeMap<GearPage, PageBuf>,
210 new_pages_data: BTreeMap<GearPage, PageBuf>,
211 static_pages: WasmPage,
212) -> BTreeMap<GearPage, PageBuf> {
213 if ProcessorExt::LAZY_PAGES_ENABLED {
214 new_pages_data.keys().for_each(|page| {
217 log::trace!("{:?} has been write accessed, update it in storage", page)
218 });
219 return new_pages_data;
220 }
221
222 let mut page_update = BTreeMap::new();
223 let mut old_pages_data = old_pages_data;
224 let static_gear_pages = static_pages.to_page();
225 for (page, new_data) in new_pages_data {
226 let initial_data = if let Some(initial_data) = old_pages_data.remove(&page) {
227 initial_data
228 } else {
229 if page < static_gear_pages {
232 continue;
233 }
234
235 PageBuf::new_zeroed()
239 };
240
241 if new_data != initial_data {
242 page_update.insert(page, new_data);
243 log::trace!("{page:?} has been changed - will be updated in storage");
244 }
245 }
246 page_update
247}
248
249pub fn execute_wasm<E>(
251 balance: u128,
252 dispatch: IncomingDispatch,
253 context: WasmExecutionContext,
254 settings: ExecutionSettings,
255 msg_ctx_settings: ContextSettings,
256) -> Result<DispatchResult, ExecutionError>
257where
258 E: Environment,
259 E::Ext: ProcessorExternalities + BackendExternalities + 'static,
260 <E::Ext as Externalities>::UnrecoverableError: BackendSyscallError,
261{
262 let WasmExecutionContext {
263 gas_counter,
264 gas_allowance_counter,
265 gas_reserver,
266 program,
267 mut pages_initial_data,
268 memory_size,
269 } = context;
270
271 let program_id = program.id();
272 let kind = dispatch.kind();
273
274 log::debug!("Executing program {}", program_id);
275 log::debug!("Executing dispatch {:?}", dispatch);
276
277 let static_pages = program.static_pages();
278 let allocations = program.allocations();
279
280 check_memory(
281 allocations,
282 pages_initial_data.keys(),
283 static_pages,
284 memory_size,
285 )
286 .map_err(SystemExecutionError::PrepareMemory)?;
287
288 let allocations_context =
290 AllocationsContext::new(allocations.clone(), static_pages, settings.max_pages);
291
292 let message_context = MessageContext::new(dispatch.clone(), program_id, msg_ctx_settings);
294
295 let value_available = balance.saturating_add(
303 dispatch
304 .context()
305 .is_none()
306 .then(|| dispatch.value())
307 .unwrap_or_default(),
308 );
309 let value_counter = ValueCounter::new(value_available);
310
311 let context = ProcessorContext {
312 gas_counter,
313 gas_allowance_counter,
314 gas_reserver,
315 system_reservation: None,
316 value_counter,
317 allocations_context,
318 message_context,
319 block_info: settings.block_info,
320 max_pages: settings.max_pages,
321 page_costs: settings.page_costs,
322 existential_deposit: settings.existential_deposit,
323 program_id,
324 program_candidates_data: Default::default(),
325 program_rents: Default::default(),
326 host_fn_weights: settings.host_fn_weights,
327 forbidden_funcs: settings.forbidden_funcs,
328 mailbox_threshold: settings.mailbox_threshold,
329 waitlist_cost: settings.waitlist_cost,
330 dispatch_hold_cost: settings.dispatch_hold_cost,
331 reserve_for: settings.reserve_for,
332 reservation: settings.reservation,
333 random_data: settings.random_data,
334 rent_cost: settings.rent_cost,
335 };
336
337 let lazy_pages_weights = context.page_costs.lazy_pages_weights();
338
339 let ext = E::Ext::new(context);
341
342 let execute = || {
344 let env = E::new(
345 ext,
346 program.raw_code(),
347 kind,
348 program.code().exports().clone(),
349 memory_size,
350 )
351 .map_err(EnvironmentError::from_infallible)?;
352 env.execute(|memory, stack_end, globals_config| {
353 prepare_memory::<E::Ext, E::Memory>(
354 memory,
355 program_id,
356 &mut pages_initial_data,
357 static_pages,
358 stack_end,
359 globals_config,
360 lazy_pages_weights,
361 )
362 })
363 };
364 let (termination, memory, ext) = match execute() {
365 Ok(report) => {
366 let BackendReport {
367 termination_reason,
368 memory_wrap: mut memory,
369 ext,
370 } = report;
371
372 let mut termination = match termination_reason {
373 TerminationReason::Actor(reason) => reason,
374 TerminationReason::System(reason) => {
375 return Err(ExecutionError::System(reason.into()))
376 }
377 };
378
379 if E::Ext::LAZY_PAGES_ENABLED {
381 E::Ext::lazy_pages_post_execution_actions(&mut memory);
382
383 if !E::Ext::lazy_pages_status().is_normal() {
384 termination = ext.current_counter_type().into()
385 }
386 }
387
388 (termination, memory, ext)
389 }
390 Err(EnvironmentError::System(e)) => {
391 return Err(ExecutionError::System(SystemExecutionError::Environment(
392 e.to_string(),
393 )))
394 }
395 Err(EnvironmentError::PrepareMemory(gas_amount, PrepareMemoryError::Actor(e))) => {
396 return Err(ExecutionError::Actor(ActorExecutionError {
397 gas_amount,
398 reason: ActorExecutionErrorReplyReason::PrepareMemory(e),
399 }))
400 }
401 Err(EnvironmentError::PrepareMemory(_gas_amount, PrepareMemoryError::System(e))) => {
402 return Err(ExecutionError::System(e.into()));
403 }
404 Err(EnvironmentError::Actor(gas_amount, err)) => {
405 log::trace!("ActorExecutionErrorReplyReason::Environment({err}) occurred");
406 return Err(ExecutionError::Actor(ActorExecutionError {
407 gas_amount,
408 reason: ActorExecutionErrorReplyReason::Environment,
409 }));
410 }
411 };
412
413 log::debug!("Termination reason: {:?}", termination);
414
415 let info = ext
416 .into_ext_info(&memory)
417 .map_err(SystemExecutionError::IntoExtInfo)?;
418
419 if E::Ext::LAZY_PAGES_ENABLED {
420 lazy_pages_check_initial_data(&pages_initial_data)
421 .map_err(SystemExecutionError::PrepareMemory)?;
422 }
423
424 let kind = match termination {
426 ActorTerminationReason::Exit(value_dest) => DispatchResultKind::Exit(value_dest),
427 ActorTerminationReason::Leave | ActorTerminationReason::Success => {
428 DispatchResultKind::Success
429 }
430 ActorTerminationReason::Trap(explanation) => {
431 log::debug!("š„ Trap during execution of {program_id}\nš Explanation: {explanation}");
432 DispatchResultKind::Trap(explanation)
433 }
434 ActorTerminationReason::Wait(duration, waited_type) => {
435 DispatchResultKind::Wait(duration, waited_type)
436 }
437 ActorTerminationReason::GasAllowanceExceeded => DispatchResultKind::GasAllowanceExceed,
438 };
439
440 let page_update =
441 get_pages_to_be_updated::<E::Ext>(pages_initial_data, info.pages_data, static_pages);
442
443 let program_candidates = info.program_candidates_data;
445
446 Ok(DispatchResult {
448 kind,
449 dispatch,
450 program_id,
451 context_store: info.context_store,
452 generated_dispatches: info.generated_dispatches,
453 awakening: info.awakening,
454 reply_deposits: info.reply_deposits,
455 program_candidates,
456 program_rents: info.program_rents,
457 gas_amount: info.gas_amount,
458 gas_reserver: Some(info.gas_reserver),
459 system_reservation_context: info.system_reservation_context,
460 page_update,
461 allocations: info.allocations,
462 })
463}
464
465#[allow(clippy::too_many_arguments)]
467pub fn execute_for_reply<E, EP>(
468 function: EP,
469 instrumented_code: InstrumentedCode,
470 pages_initial_data: Option<BTreeMap<GearPage, PageBuf>>,
471 allocations: Option<BTreeSet<WasmPage>>,
472 program_id: Option<ProgramId>,
473 payload: Vec<u8>,
474 gas_limit: u64,
475 block_info: BlockInfo,
476) -> Result<Vec<u8>, String>
477where
478 E: Environment<EP>,
479 E::Ext: ProcessorExternalities + BackendExternalities + 'static,
480 <E::Ext as Externalities>::UnrecoverableError: BackendSyscallError,
481 EP: WasmEntryPoint,
482{
483 let program = Program::new(program_id.unwrap_or_default(), instrumented_code);
484 let mut pages_initial_data: BTreeMap<GearPage, PageBuf> =
485 pages_initial_data.unwrap_or_default();
486 let static_pages = program.static_pages();
487 let allocations = allocations.unwrap_or_else(|| program.allocations().clone());
488
489 let memory_size = if let Some(page) = allocations.iter().next_back() {
490 page.inc()
491 .map_err(|err| err.to_string())
492 .expect("Memory size overflow, impossible")
493 } else if static_pages != WasmPage::from(0) {
494 static_pages
495 } else {
496 0.into()
497 };
498
499 let context = ProcessorContext {
500 gas_counter: GasCounter::new(gas_limit),
501 gas_allowance_counter: GasAllowanceCounter::new(gas_limit),
502 gas_reserver: GasReserver::new(&Default::default(), Default::default(), Default::default()),
503 value_counter: ValueCounter::new(Default::default()),
504 allocations_context: AllocationsContext::new(allocations, static_pages, 512.into()),
505 message_context: MessageContext::new(
506 IncomingDispatch::new(
507 DispatchKind::Handle,
508 IncomingMessage::new(
509 Default::default(),
510 Default::default(),
511 payload
512 .try_into()
513 .map_err(|e| format!("Failed to create payload: {e:?}"))?,
514 gas_limit,
515 Default::default(),
516 Default::default(),
517 ),
518 None,
519 ),
520 program.id(),
521 ContextSettings::new(0, 0, 0, 0, 0, 0),
522 ),
523 block_info,
524 max_pages: 512.into(),
525 page_costs: Default::default(),
526 existential_deposit: Default::default(),
527 program_id: program.id(),
528 program_candidates_data: Default::default(),
529 program_rents: Default::default(),
530 host_fn_weights: Default::default(),
531 forbidden_funcs: Default::default(),
532 mailbox_threshold: Default::default(),
533 waitlist_cost: Default::default(),
534 dispatch_hold_cost: Default::default(),
535 reserve_for: Default::default(),
536 reservation: Default::default(),
537 random_data: Default::default(),
538 system_reservation: Default::default(),
539 rent_cost: Default::default(),
540 };
541
542 let lazy_pages_weights = context.page_costs.lazy_pages_weights();
543
544 let ext = E::Ext::new(context);
546
547 let f = || {
549 let env = E::new(
550 ext,
551 program.raw_code(),
552 function,
553 program.code().exports().clone(),
554 memory_size,
555 )
556 .map_err(EnvironmentError::from_infallible)?;
557 env.execute(|memory, stack_end, globals_config| {
558 prepare_memory::<E::Ext, E::Memory>(
559 memory,
560 program.id(),
561 &mut pages_initial_data,
562 static_pages,
563 stack_end,
564 globals_config,
565 lazy_pages_weights,
566 )
567 })
568 };
569
570 let (termination, memory, ext) = match f() {
571 Ok(report) => {
572 let BackendReport {
573 termination_reason,
574 memory_wrap,
575 ext,
576 } = report;
577
578 let termination_reason = match termination_reason {
579 TerminationReason::Actor(reason) => reason,
580 TerminationReason::System(reason) => {
581 return Err(format!("Backend error: {reason}"))
582 }
583 };
584
585 (termination_reason, memory_wrap, ext)
586 }
587 Err(e) => return Err(format!("Backend error: {e}")),
588 };
589
590 match termination {
591 ActorTerminationReason::Exit(_)
592 | ActorTerminationReason::Leave
593 | ActorTerminationReason::Wait(_, _) => {
594 return Err("Execution has incorrect termination reason".into())
595 }
596 ActorTerminationReason::Success => (),
597 ActorTerminationReason::Trap(explanation) => {
598 return Err(format!(
599 "Program execution failed with error: {explanation}"
600 ));
601 }
602 ActorTerminationReason::GasAllowanceExceeded => return Err("Unreachable".into()),
603 };
604
605 let info = ext
606 .into_ext_info(&memory)
607 .map_err(|e| format!("Backend postprocessing error: {e:?}"))?;
608
609 log::debug!(
610 "[execute_for_reply] Gas burned: {}",
611 info.gas_amount.burned()
612 );
613
614 for (dispatch, _, _) in info.generated_dispatches {
615 if matches!(dispatch.kind(), DispatchKind::Reply) {
616 return Ok(dispatch.payload_bytes().to_vec());
617 }
618 }
619
620 Err("Reply not found".into())
621}
622
623#[cfg(test)]
624mod tests {
625 use super::*;
626 use alloc::vec::Vec;
627 use gear_backend_common::lazy_pages::Status;
628 use gear_core::{
629 memory::PageBufInner,
630 pages::{PageNumber, WasmPage},
631 };
632
633 struct TestExt;
634 struct LazyTestExt;
635
636 impl ProcessorExternalities for TestExt {
637 const LAZY_PAGES_ENABLED: bool = false;
638 fn new(_context: ProcessorContext) -> Self {
639 Self
640 }
641
642 fn lazy_pages_init_for_program(
643 _mem: &mut impl Memory,
644 _prog_id: ProgramId,
645 _stack_end: Option<WasmPage>,
646 _globals_config: GlobalsAccessConfig,
647 _lazy_pages_weights: LazyPagesWeights,
648 ) {
649 }
650
651 fn lazy_pages_post_execution_actions(_mem: &mut impl Memory) {}
652 fn lazy_pages_status() -> Status {
653 Status::Normal
654 }
655 }
656
657 impl ProcessorExternalities for LazyTestExt {
658 const LAZY_PAGES_ENABLED: bool = true;
659
660 fn new(_context: ProcessorContext) -> Self {
661 Self
662 }
663
664 fn lazy_pages_init_for_program(
665 _mem: &mut impl Memory,
666 _prog_id: ProgramId,
667 _stack_end: Option<WasmPage>,
668 _globals_config: GlobalsAccessConfig,
669 _lazy_pages_weights: LazyPagesWeights,
670 ) {
671 }
672
673 fn lazy_pages_post_execution_actions(_mem: &mut impl Memory) {}
674 fn lazy_pages_status() -> Status {
675 Status::Normal
676 }
677 }
678
679 fn prepare_pages_and_allocs() -> (Vec<GearPage>, BTreeSet<WasmPage>) {
680 let data = [0u16, 1, 2, 8, 18, 25, 27, 28, 93, 146, 240, 518];
681 let pages = data.map(Into::into);
682 (pages.to_vec(), pages.map(|p| p.to_page()).into())
683 }
684
685 fn prepare_pages() -> BTreeMap<GearPage, PageBuf> {
686 let mut pages = BTreeMap::new();
687 for i in 0..=255 {
688 let buffer = PageBufInner::filled_with(i);
689 pages.insert((i as u16).into(), PageBuf::from_inner(buffer));
690 }
691 pages
692 }
693
694 #[test]
695 fn check_memory_insufficient() {
696 let res = check_memory(&[].into(), [].iter(), 8.into(), 4.into());
697 assert_eq!(res, Err(SystemPrepareMemoryError::InsufficientMemorySize));
698 }
699
700 #[test]
701 fn check_memory_not_allocated() {
702 let (pages, mut allocs) = prepare_pages_and_allocs();
703 let last = *allocs.iter().last().unwrap();
704 allocs.remove(&last);
705 let res = check_memory(&allocs, pages.iter(), 2.into(), 4.into());
706 assert_eq!(
707 res,
708 Err(SystemPrepareMemoryError::PageIsNotAllocated(
709 *pages.last().unwrap()
710 ))
711 );
712 }
713
714 #[test]
715 fn check_memory_ok() {
716 let (pages, allocs) = prepare_pages_and_allocs();
717 check_memory(&allocs, pages.iter(), 4.into(), 8.into()).unwrap();
718 }
719
720 #[test]
721 fn lazy_pages_to_update() {
722 let new_pages = prepare_pages();
723 let res =
724 get_pages_to_be_updated::<LazyTestExt>(Default::default(), new_pages.clone(), 0.into());
725 assert_eq!(res, new_pages);
727 }
728
729 #[test]
730 fn no_pages_to_update() {
731 let old_pages = prepare_pages();
732 let mut new_pages = old_pages.clone();
733 let static_pages = 4;
734 let res =
735 get_pages_to_be_updated::<TestExt>(old_pages, new_pages.clone(), static_pages.into());
736 assert_eq!(res, Default::default());
737
738 for i in 0..static_pages {
740 let buffer = PageBufInner::filled_with(42);
741 new_pages.insert(i.into(), PageBuf::from_inner(buffer));
742 }
743 let new_pages = new_pages
745 .into_iter()
746 .take(WasmPage::from(static_pages).to_page::<GearPage>().raw() as _)
747 .collect();
748 let res =
749 get_pages_to_be_updated::<TestExt>(Default::default(), new_pages, static_pages.into());
750 assert_eq!(res, Default::default());
751 }
752
753 #[test]
754 fn pages_to_update() {
755 let old_pages = prepare_pages();
756 let mut new_pages = old_pages.clone();
757
758 let page_with_zero_data = WasmPage::from(30).to_page();
759 let changes: BTreeMap<GearPage, PageBuf> = [
760 (
761 WasmPage::from(1).to_page(),
762 PageBuf::from_inner(PageBufInner::filled_with(42u8)),
763 ),
764 (
765 WasmPage::from(5).to_page(),
766 PageBuf::from_inner(PageBufInner::filled_with(84u8)),
767 ),
768 (page_with_zero_data, PageBuf::new_zeroed()),
769 ]
770 .into_iter()
771 .collect();
772 new_pages.extend(changes.clone().into_iter());
773
774 let static_pages = 4.into();
776 let res = get_pages_to_be_updated::<TestExt>(old_pages, new_pages.clone(), static_pages);
777 assert_eq!(res, changes);
778
779 let res =
781 get_pages_to_be_updated::<TestExt>(Default::default(), new_pages.clone(), static_pages);
782
783 for page in static_pages.to_page::<GearPage>().iter_from_zero() {
785 new_pages.remove(&page);
786 }
787
788 new_pages.remove(&page_with_zero_data);
790
791 assert_eq!(res, new_pages);
792 }
793}