core_processor/
processing.rs

1// This file is part of Gear.
2
3// Copyright (C) 2021-2025 Gear Technologies Inc.
4// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
5
6// This program is free software: you can redistribute it and/or modify
7// it under the terms of the GNU General Public License as published by
8// the Free Software Foundation, either version 3 of the License, or
9// (at your option) any later version.
10
11// This program is distributed in the hope that it will be useful,
12// but WITHOUT ANY WARRANTY; without even the implied warranty of
13// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14// GNU General Public License for more details.
15
16// You should have received a copy of the GNU General Public License
17// along with this program. If not, see <https://www.gnu.org/licenses/>.
18
19use crate::{
20    common::{
21        ActorExecutionErrorReplyReason, DispatchOutcome, DispatchResult, DispatchResultKind,
22        ExecutionError, JournalNote, SystemExecutionError, WasmExecutionContext,
23    },
24    configs::{BlockConfig, ExecutionSettings},
25    context::*,
26    executor,
27    ext::ProcessorExternalities,
28    precharge::SuccessfulDispatchResultKind,
29};
30use alloc::{string::ToString, vec::Vec};
31use core::{fmt, fmt::Formatter};
32use gear_core::{
33    env::Externalities,
34    ids::{prelude::*, MessageId, ProgramId},
35    message::{
36        ContextSettings, DispatchKind, IncomingDispatch, Payload, PayloadSizeError, ReplyMessage,
37        StoredDispatch,
38    },
39    reservation::GasReservationState,
40};
41use gear_core_backend::{
42    error::{BackendAllocSyscallError, BackendSyscallError, RunFallibleError, TrapExplanation},
43    BackendExternalities,
44};
45use gear_core_errors::{ErrorReplyReason, SignalCode, SimpleUnavailableActorError};
46
47/// Process program & dispatch for it and return journal for updates.
48pub fn process<Ext>(
49    block_config: &BlockConfig,
50    execution_context: ProcessExecutionContext,
51    random_data: (Vec<u8>, u32),
52) -> Result<Vec<JournalNote>, SystemExecutionError>
53where
54    Ext: ProcessorExternalities + BackendExternalities + Send + 'static,
55    <Ext as Externalities>::AllocError:
56        BackendAllocSyscallError<ExtError = Ext::UnrecoverableError>,
57    RunFallibleError: From<Ext::FallibleError>,
58    <Ext as Externalities>::UnrecoverableError: BackendSyscallError,
59{
60    use crate::precharge::SuccessfulDispatchResultKind::*;
61
62    let BlockConfig {
63        block_info,
64        performance_multiplier,
65        forbidden_funcs,
66        reserve_for,
67        gas_multiplier,
68        costs,
69        existential_deposit,
70        mailbox_threshold,
71        max_pages,
72        outgoing_limit,
73        outgoing_bytes_limit,
74        ..
75    } = block_config.clone();
76
77    let execution_settings = ExecutionSettings {
78        block_info,
79        performance_multiplier,
80        existential_deposit,
81        mailbox_threshold,
82        max_pages,
83        ext_costs: costs.ext,
84        lazy_pages_costs: costs.lazy_pages,
85        forbidden_funcs,
86        reserve_for,
87        random_data,
88        gas_multiplier,
89    };
90
91    let dispatch = execution_context.dispatch;
92    let balance = execution_context.balance;
93    let program_id = execution_context.program.id;
94    let initial_reservations_amount = execution_context.gas_reserver.states().len();
95
96    let execution_context = WasmExecutionContext {
97        gas_counter: execution_context.gas_counter,
98        gas_allowance_counter: execution_context.gas_allowance_counter,
99        gas_reserver: execution_context.gas_reserver,
100        program: execution_context.program,
101        memory_size: execution_context.memory_size,
102    };
103
104    // Sending fee: double write cost for addition and removal some time soon
105    // from queue.
106    //
107    // Scheduled sending fee: double write cost for addition and removal some time soon
108    // from queue and double write cost (addition and removal) for dispatch stash.
109    //
110    // Waiting fee: triple write cost for addition and removal some time soon
111    // from waitlist and enqueuing / sending error reply afterward.
112    //
113    // Waking fee: double write cost for removal from waitlist
114    // and further enqueueing.
115    let msg_ctx_settings = ContextSettings {
116        sending_fee: costs.write.cost_for(2.into()),
117        scheduled_sending_fee: costs.write.cost_for(4.into()),
118        waiting_fee: costs.write.cost_for(3.into()),
119        waking_fee: costs.write.cost_for(2.into()),
120        reservation_fee: costs.write.cost_for(2.into()),
121        outgoing_limit,
122        outgoing_bytes_limit,
123    };
124
125    // TODO: add tests that system reservation is successfully unreserved after
126    // actor execution error #3756.
127
128    // Get system reservation context in order to use it if actor execution error occurs.
129    let system_reservation_ctx = SystemReservationContext::from_dispatch(&dispatch);
130
131    let exec_result = executor::execute_wasm::<Ext>(
132        balance,
133        dispatch.clone(),
134        execution_context,
135        execution_settings,
136        msg_ctx_settings,
137    )
138    .map_err(|err| {
139        log::debug!("Wasm execution error: {}", err);
140        err
141    });
142
143    match exec_result {
144        Ok(res) => {
145            match res.kind {
146                DispatchResultKind::Success
147                | DispatchResultKind::Wait(_, _)
148                | DispatchResultKind::Exit(_) => {
149                    // assert that after processing the initial reservation is less or equal to the current one.
150                    // during execution reservation amount might increase due to `system_reserve_gas` calls
151                    // thus making initial reservation less than current one.
152                    debug_assert!(
153                        res.context_store.system_reservation()
154                            >= system_reservation_ctx.previous_reservation
155                    );
156                    debug_assert!(
157                        system_reservation_ctx.previous_reservation
158                            == res.system_reservation_context.previous_reservation
159                    );
160                    debug_assert!(res
161                        .gas_reserver
162                        .as_ref()
163                        .map(|reserver| initial_reservations_amount <= reserver.states().len())
164                        .unwrap_or(true));
165                }
166                // reservation does not change in case of failure
167                _ => (),
168            }
169            Ok(match res.kind {
170                DispatchResultKind::Trap(reason) => process_execution_error(
171                    res.dispatch,
172                    program_id,
173                    res.gas_amount.burned(),
174                    res.system_reservation_context,
175                    ActorExecutionErrorReplyReason::Trap(reason),
176                ),
177
178                DispatchResultKind::Success => process_success(Success, res),
179                DispatchResultKind::Wait(duration, ref waited_type) => {
180                    process_success(Wait(duration, waited_type.clone()), res)
181                }
182                DispatchResultKind::Exit(value_destination) => {
183                    process_success(Exit(value_destination), res)
184                }
185                DispatchResultKind::GasAllowanceExceed => {
186                    process_allowance_exceed(dispatch, program_id, res.gas_amount.burned())
187                }
188            })
189        }
190        Err(ExecutionError::Actor(e)) => Ok(process_execution_error(
191            dispatch,
192            program_id,
193            e.gas_amount.burned(),
194            system_reservation_ctx,
195            e.reason,
196        )),
197        Err(ExecutionError::System(e)) => Err(e),
198    }
199}
200
201enum ProcessErrorCase {
202    /// Program exited.
203    ProgramExited {
204        /// Inheritor of an exited program.
205        inheritor: ProgramId,
206    },
207    /// Program failed during init.
208    FailedInit,
209    /// Program is not initialized yet.
210    Uninitialized,
211    /// Given code id for program creation doesn't exist.
212    CodeNotExists,
213    /// Message is executable, but its execution failed due to re-instrumentation.
214    ReinstrumentationFailed,
215    /// Error is considered as an execution failure.
216    ExecutionFailed(ActorExecutionErrorReplyReason),
217}
218
219impl fmt::Display for ProcessErrorCase {
220    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
221        match self {
222            ProcessErrorCase::ExecutionFailed(reason) => fmt::Display::fmt(reason, f),
223            this => fmt::Display::fmt(&this.to_reason(), f),
224        }
225    }
226}
227
228impl ProcessErrorCase {
229    fn to_reason(&self) -> ErrorReplyReason {
230        match self {
231            ProcessErrorCase::ProgramExited { .. } => {
232                ErrorReplyReason::UnavailableActor(SimpleUnavailableActorError::ProgramExited)
233            }
234            ProcessErrorCase::FailedInit => ErrorReplyReason::UnavailableActor(
235                SimpleUnavailableActorError::InitializationFailure,
236            ),
237            ProcessErrorCase::Uninitialized => {
238                ErrorReplyReason::UnavailableActor(SimpleUnavailableActorError::Uninitialized)
239            }
240            ProcessErrorCase::CodeNotExists => {
241                ErrorReplyReason::UnavailableActor(SimpleUnavailableActorError::ProgramNotCreated)
242            }
243            ProcessErrorCase::ReinstrumentationFailed => ErrorReplyReason::UnavailableActor(
244                SimpleUnavailableActorError::ReinstrumentationFailure,
245            ),
246            ProcessErrorCase::ExecutionFailed(reason) => reason.as_simple().into(),
247        }
248    }
249
250    // TODO: consider to convert `self` into `Payload` to avoid `PanicBuffer` cloning (#4594)
251    fn to_payload(&self) -> Payload {
252        match self {
253            ProcessErrorCase::ProgramExited { inheritor } => {
254                const _: () = assert!(size_of::<ProgramId>() <= Payload::MAX_LEN);
255                inheritor
256                    .into_bytes()
257                    .to_vec()
258                    .try_into()
259                    .unwrap_or_else(|PayloadSizeError| {
260                        unreachable!("`ProgramId` is always smaller than maximum payload size")
261                    })
262            }
263            ProcessErrorCase::ExecutionFailed(ActorExecutionErrorReplyReason::Trap(
264                TrapExplanation::Panic(buf),
265            )) => buf.inner().clone(),
266            _ => Payload::default(),
267        }
268    }
269}
270
271fn process_error(
272    dispatch: IncomingDispatch,
273    program_id: ProgramId,
274    gas_burned: u64,
275    system_reservation_ctx: SystemReservationContext,
276    case: ProcessErrorCase,
277) -> Vec<JournalNote> {
278    let mut journal = Vec::new();
279
280    let message_id = dispatch.id();
281    let origin = dispatch.source();
282    let value = dispatch.value();
283
284    journal.push(JournalNote::GasBurned {
285        message_id,
286        amount: gas_burned,
287    });
288
289    let to_send_reply = !matches!(dispatch.kind(), DispatchKind::Reply | DispatchKind::Signal);
290
291    // We check if value is greater than zero to don't provide
292    // no-op journal note.
293    //
294    // We also check if dispatch had context of previous executions:
295    // it's existence shows that we have processed message after
296    // being waken, so the value were already transferred in
297    // execution, where `gr_wait` was called.
298    if dispatch.context().is_none() && value != 0 {
299        // Value on error is always delivered to the program, but may return with error reply.
300        journal.push(JournalNote::SendValue {
301            from: origin,
302            to: program_id,
303            value,
304            // in case of upcoming error reply, we want to send locked value,
305            // instead of deposit, to avoid ED manipulations.
306            locked: to_send_reply,
307        });
308    }
309
310    if let Some(amount) = system_reservation_ctx.current_reservation {
311        journal.push(JournalNote::SystemReserveGas { message_id, amount });
312    }
313
314    if let ProcessErrorCase::ExecutionFailed(reason) = &case {
315        // TODO: consider to handle error reply and init #3701
316        if system_reservation_ctx.has_any()
317            && !dispatch.is_error_reply()
318            && !matches!(dispatch.kind(), DispatchKind::Signal | DispatchKind::Init)
319        {
320            journal.push(JournalNote::SendSignal {
321                message_id,
322                destination: program_id,
323                code: SignalCode::Execution(reason.as_simple()),
324            });
325        }
326    }
327
328    if system_reservation_ctx.has_any() {
329        journal.push(JournalNote::SystemUnreserveGas { message_id });
330    }
331
332    if to_send_reply {
333        let err = case.to_reason();
334        let err_payload = case.to_payload();
335
336        let value = if dispatch.context().is_none() {
337            value
338        } else {
339            0
340        };
341
342        // # Safety
343        //
344        // 1. The dispatch.id() has already been checked
345        // 2. This reply message is generated by our system
346        //
347        // So, the message id of this reply message will not be duplicated.
348        let dispatch = ReplyMessage::system(dispatch.id(), err_payload, value, err).into_dispatch(
349            program_id,
350            dispatch.source(),
351            dispatch.id(),
352        );
353
354        journal.push(JournalNote::SendDispatch {
355            message_id,
356            dispatch,
357            delay: 0,
358            reservation: None,
359        });
360    }
361
362    let outcome = match case {
363        ProcessErrorCase::ExecutionFailed { .. } | ProcessErrorCase::ReinstrumentationFailed => {
364            let err_msg = case.to_string();
365            match dispatch.kind() {
366                DispatchKind::Init => DispatchOutcome::InitFailure {
367                    program_id,
368                    origin,
369                    reason: err_msg,
370                },
371                _ => DispatchOutcome::MessageTrap {
372                    program_id,
373                    trap: err_msg,
374                },
375            }
376        }
377        ProcessErrorCase::ProgramExited { .. }
378        | ProcessErrorCase::FailedInit
379        | ProcessErrorCase::Uninitialized
380        | ProcessErrorCase::CodeNotExists => DispatchOutcome::NoExecution,
381    };
382
383    journal.push(JournalNote::MessageDispatched {
384        message_id,
385        source: origin,
386        outcome,
387    });
388    journal.push(JournalNote::MessageConsumed(message_id));
389
390    journal
391}
392
393/// Helper function for journal creation in trap/error case.
394pub fn process_execution_error(
395    dispatch: IncomingDispatch,
396    program_id: ProgramId,
397    gas_burned: u64,
398    system_reservation_ctx: SystemReservationContext,
399    err: impl Into<ActorExecutionErrorReplyReason>,
400) -> Vec<JournalNote> {
401    process_error(
402        dispatch,
403        program_id,
404        gas_burned,
405        system_reservation_ctx,
406        ProcessErrorCase::ExecutionFailed(err.into()),
407    )
408}
409
410/// Helper function for journal creation in program exited case.
411pub fn process_program_exited(
412    context: ContextChargedForProgram,
413    inheritor: ProgramId,
414) -> Vec<JournalNote> {
415    let ContextChargedForProgram {
416        dispatch,
417        gas_counter,
418        destination_id,
419        ..
420    } = context;
421
422    let system_reservation_ctx = SystemReservationContext::from_dispatch(&dispatch);
423
424    process_error(
425        dispatch,
426        destination_id,
427        gas_counter.burned(),
428        system_reservation_ctx,
429        ProcessErrorCase::ProgramExited { inheritor },
430    )
431}
432
433/// Helper function for journal creation in program failed init case.
434pub fn process_failed_init(context: ContextChargedForProgram) -> Vec<JournalNote> {
435    let ContextChargedForProgram {
436        dispatch,
437        gas_counter,
438        destination_id,
439        ..
440    } = context;
441
442    let system_reservation_ctx = SystemReservationContext::from_dispatch(&dispatch);
443
444    process_error(
445        dispatch,
446        destination_id,
447        gas_counter.burned(),
448        system_reservation_ctx,
449        ProcessErrorCase::FailedInit,
450    )
451}
452
453/// Helper function for journal creation in program uninitialized case.
454pub fn process_uninitialized(context: ContextChargedForProgram) -> Vec<JournalNote> {
455    let ContextChargedForProgram {
456        dispatch,
457        gas_counter,
458        destination_id,
459        ..
460    } = context;
461
462    let system_reservation_ctx = SystemReservationContext::from_dispatch(&dispatch);
463
464    process_error(
465        dispatch,
466        destination_id,
467        gas_counter.burned(),
468        system_reservation_ctx,
469        ProcessErrorCase::Uninitialized,
470    )
471}
472
473/// Helper function for journal creation in code not exists case.
474pub fn process_code_not_exists(context: ContextChargedForProgram) -> Vec<JournalNote> {
475    let ContextChargedForProgram {
476        dispatch,
477        gas_counter,
478        destination_id,
479        ..
480    } = context;
481
482    let system_reservation_ctx = SystemReservationContext::from_dispatch(&dispatch);
483
484    process_error(
485        dispatch,
486        destination_id,
487        gas_counter.burned(),
488        system_reservation_ctx,
489        ProcessErrorCase::CodeNotExists,
490    )
491}
492
493/// Helper function for journal creation in case of re-instrumentation error.
494pub fn process_reinstrumentation_error(
495    context: ContextChargedForInstrumentation,
496) -> Vec<JournalNote> {
497    let dispatch = context.data.dispatch;
498    let program_id = context.data.destination_id;
499    let gas_burned = context.data.gas_counter.burned();
500    let system_reservation_ctx = SystemReservationContext::from_dispatch(&dispatch);
501
502    process_error(
503        dispatch,
504        program_id,
505        gas_burned,
506        system_reservation_ctx,
507        ProcessErrorCase::ReinstrumentationFailed,
508    )
509}
510
511/// Helper function for journal creation in success case
512pub fn process_success(
513    kind: SuccessfulDispatchResultKind,
514    dispatch_result: DispatchResult,
515) -> Vec<JournalNote> {
516    use crate::precharge::SuccessfulDispatchResultKind::*;
517
518    let DispatchResult {
519        dispatch,
520        generated_dispatches,
521        awakening,
522        program_candidates,
523        gas_amount,
524        gas_reserver,
525        system_reservation_context,
526        page_update,
527        program_id,
528        context_store,
529        allocations,
530        reply_deposits,
531        reply_sent,
532        ..
533    } = dispatch_result;
534
535    let mut journal = Vec::new();
536
537    let message_id = dispatch.id();
538    let origin = dispatch.source();
539    let value = dispatch.value();
540
541    journal.push(JournalNote::GasBurned {
542        message_id,
543        amount: gas_amount.burned(),
544    });
545
546    if let Some(gas_reserver) = gas_reserver {
547        journal.extend(gas_reserver.states().iter().flat_map(
548            |(&reservation_id, &state)| match state {
549                GasReservationState::Exists { .. } => None,
550                GasReservationState::Created {
551                    amount, duration, ..
552                } => Some(JournalNote::ReserveGas {
553                    message_id,
554                    reservation_id,
555                    program_id,
556                    amount,
557                    duration,
558                }),
559                GasReservationState::Removed { expiration } => Some(JournalNote::UnreserveGas {
560                    reservation_id,
561                    program_id,
562                    expiration,
563                }),
564            },
565        ));
566
567        journal.push(JournalNote::UpdateGasReservations {
568            program_id,
569            reserver: gas_reserver,
570        });
571    }
572
573    if let Some(amount) = system_reservation_context.current_reservation {
574        journal.push(JournalNote::SystemReserveGas { message_id, amount });
575    }
576
577    // We check if value is greater than zero to don't provide
578    // no-op journal note.
579    //
580    // We also check if dispatch had context of previous executions:
581    // it's existence shows that we have processed message after
582    // being waken, so the value were already transferred in
583    // execution, where `gr_wait` was called.
584    if dispatch.context().is_none() && value != 0 {
585        // Send value further
586        journal.push(JournalNote::SendValue {
587            from: origin,
588            to: program_id,
589            value,
590            locked: false,
591        });
592    }
593
594    // Must be handled before handling generated dispatches.
595    for (code_id, candidates) in program_candidates {
596        journal.push(JournalNote::StoreNewPrograms {
597            program_id,
598            code_id,
599            candidates,
600        });
601    }
602
603    // Sending auto-generated reply about success execution.
604    if !matches!(kind, SuccessfulDispatchResultKind::Wait(_, _))
605        && !matches!(dispatch.kind(), DispatchKind::Reply | DispatchKind::Signal)
606        && !reply_sent
607    {
608        let auto_reply = ReplyMessage::auto(dispatch.id()).into_dispatch(
609            program_id,
610            dispatch.source(),
611            dispatch.id(),
612        );
613
614        journal.push(JournalNote::SendDispatch {
615            message_id,
616            dispatch: auto_reply,
617            delay: 0,
618            reservation: None,
619        });
620    }
621
622    for (message_id_sent, amount) in reply_deposits {
623        journal.push(JournalNote::ReplyDeposit {
624            message_id,
625            future_reply_id: MessageId::generate_reply(message_id_sent),
626            amount,
627        });
628    }
629
630    for (dispatch, delay, reservation) in generated_dispatches {
631        journal.push(JournalNote::SendDispatch {
632            message_id,
633            dispatch,
634            delay,
635            reservation,
636        });
637    }
638
639    for (awakening_id, delay) in awakening {
640        journal.push(JournalNote::WakeMessage {
641            message_id,
642            program_id,
643            awakening_id,
644            delay,
645        });
646    }
647
648    for (page_number, data) in page_update {
649        journal.push(JournalNote::UpdatePage {
650            program_id,
651            page_number,
652            data,
653        })
654    }
655
656    if let Some(allocations) = allocations {
657        journal.push(JournalNote::UpdateAllocations {
658            program_id,
659            allocations,
660        });
661    }
662
663    let outcome = match kind {
664        Wait(duration, waited_type) => {
665            journal.push(JournalNote::WaitDispatch {
666                dispatch: dispatch.into_stored(program_id, context_store),
667                duration,
668                waited_type,
669            });
670
671            return journal;
672        }
673        Success => match dispatch.kind() {
674            DispatchKind::Init => DispatchOutcome::InitSuccess { program_id },
675            _ => DispatchOutcome::Success,
676        },
677        Exit(value_destination) => {
678            journal.push(JournalNote::ExitDispatch {
679                id_exited: program_id,
680                value_destination,
681            });
682
683            DispatchOutcome::Exit { program_id }
684        }
685    };
686
687    if system_reservation_context.has_any() {
688        journal.push(JournalNote::SystemUnreserveGas { message_id });
689    }
690
691    journal.push(JournalNote::MessageDispatched {
692        message_id,
693        source: origin,
694        outcome,
695    });
696    journal.push(JournalNote::MessageConsumed(message_id));
697    journal
698}
699
700/// Helper function for journal creation if the block gas allowance has been exceeded.
701pub fn process_allowance_exceed(
702    dispatch: IncomingDispatch,
703    program_id: ProgramId,
704    gas_burned: u64,
705) -> Vec<JournalNote> {
706    let mut journal = Vec::with_capacity(1);
707
708    let (kind, message, opt_context) = dispatch.into_parts();
709
710    let dispatch = StoredDispatch::new(kind, message.into_stored(program_id), opt_context);
711
712    journal.push(JournalNote::StopProcessing {
713        dispatch,
714        gas_burned,
715    });
716
717    journal
718}