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    // We check if value is greater than zero to don't provide
290    // no-op journal note.
291    //
292    // We also check if dispatch had context of previous executions:
293    // it's existence shows that we have processed message after
294    // being waken, so the value were already transferred in
295    // execution, where `gr_wait` was called.
296    if dispatch.context().is_none() && value != 0 {
297        // Send back value
298        journal.push(JournalNote::SendValue {
299            from: origin,
300            to: None,
301            value,
302        });
303    }
304
305    if let Some(amount) = system_reservation_ctx.current_reservation {
306        journal.push(JournalNote::SystemReserveGas { message_id, amount });
307    }
308
309    if let ProcessErrorCase::ExecutionFailed(reason) = &case {
310        // TODO: consider to handle error reply and init #3701
311        if system_reservation_ctx.has_any()
312            && !dispatch.is_error_reply()
313            && !matches!(dispatch.kind(), DispatchKind::Signal | DispatchKind::Init)
314        {
315            journal.push(JournalNote::SendSignal {
316                message_id,
317                destination: program_id,
318                code: SignalCode::Execution(reason.as_simple()),
319            });
320        }
321    }
322
323    if system_reservation_ctx.has_any() {
324        journal.push(JournalNote::SystemUnreserveGas { message_id });
325    }
326
327    if !dispatch.is_reply() && dispatch.kind() != DispatchKind::Signal {
328        let err = case.to_reason();
329        let err_payload = case.to_payload();
330
331        // # Safety
332        //
333        // 1. The dispatch.id() has already been checked
334        // 2. This reply message is generated by our system
335        //
336        // So, the message id of this reply message will not be duplicated.
337        let dispatch = ReplyMessage::system(dispatch.id(), err_payload, err).into_dispatch(
338            program_id,
339            dispatch.source(),
340            dispatch.id(),
341        );
342
343        journal.push(JournalNote::SendDispatch {
344            message_id,
345            dispatch,
346            delay: 0,
347            reservation: None,
348        });
349    }
350
351    let outcome = match case {
352        ProcessErrorCase::ExecutionFailed { .. } | ProcessErrorCase::ReinstrumentationFailed => {
353            let err_msg = case.to_string();
354            match dispatch.kind() {
355                DispatchKind::Init => DispatchOutcome::InitFailure {
356                    program_id,
357                    origin,
358                    reason: err_msg,
359                },
360                _ => DispatchOutcome::MessageTrap {
361                    program_id,
362                    trap: err_msg,
363                },
364            }
365        }
366        ProcessErrorCase::ProgramExited { .. }
367        | ProcessErrorCase::FailedInit
368        | ProcessErrorCase::Uninitialized
369        | ProcessErrorCase::CodeNotExists => DispatchOutcome::NoExecution,
370    };
371
372    journal.push(JournalNote::MessageDispatched {
373        message_id,
374        source: origin,
375        outcome,
376    });
377    journal.push(JournalNote::MessageConsumed(message_id));
378
379    journal
380}
381
382/// Helper function for journal creation in trap/error case.
383pub fn process_execution_error(
384    dispatch: IncomingDispatch,
385    program_id: ProgramId,
386    gas_burned: u64,
387    system_reservation_ctx: SystemReservationContext,
388    err: impl Into<ActorExecutionErrorReplyReason>,
389) -> Vec<JournalNote> {
390    process_error(
391        dispatch,
392        program_id,
393        gas_burned,
394        system_reservation_ctx,
395        ProcessErrorCase::ExecutionFailed(err.into()),
396    )
397}
398
399/// Helper function for journal creation in program exited case.
400pub fn process_program_exited(
401    context: ContextChargedForProgram,
402    inheritor: ProgramId,
403) -> Vec<JournalNote> {
404    let ContextChargedForProgram {
405        dispatch,
406        gas_counter,
407        destination_id,
408        ..
409    } = context;
410
411    let system_reservation_ctx = SystemReservationContext::from_dispatch(&dispatch);
412
413    process_error(
414        dispatch,
415        destination_id,
416        gas_counter.burned(),
417        system_reservation_ctx,
418        ProcessErrorCase::ProgramExited { inheritor },
419    )
420}
421
422/// Helper function for journal creation in program failed init case.
423pub fn process_failed_init(context: ContextChargedForProgram) -> Vec<JournalNote> {
424    let ContextChargedForProgram {
425        dispatch,
426        gas_counter,
427        destination_id,
428        ..
429    } = context;
430
431    let system_reservation_ctx = SystemReservationContext::from_dispatch(&dispatch);
432
433    process_error(
434        dispatch,
435        destination_id,
436        gas_counter.burned(),
437        system_reservation_ctx,
438        ProcessErrorCase::FailedInit,
439    )
440}
441
442/// Helper function for journal creation in program uninitialized case.
443pub fn process_uninitialized(context: ContextChargedForProgram) -> Vec<JournalNote> {
444    let ContextChargedForProgram {
445        dispatch,
446        gas_counter,
447        destination_id,
448        ..
449    } = context;
450
451    let system_reservation_ctx = SystemReservationContext::from_dispatch(&dispatch);
452
453    process_error(
454        dispatch,
455        destination_id,
456        gas_counter.burned(),
457        system_reservation_ctx,
458        ProcessErrorCase::Uninitialized,
459    )
460}
461
462/// Helper function for journal creation in code not exists case.
463pub fn process_code_not_exists(context: ContextChargedForProgram) -> Vec<JournalNote> {
464    let ContextChargedForProgram {
465        dispatch,
466        gas_counter,
467        destination_id,
468        ..
469    } = context;
470
471    let system_reservation_ctx = SystemReservationContext::from_dispatch(&dispatch);
472
473    process_error(
474        dispatch,
475        destination_id,
476        gas_counter.burned(),
477        system_reservation_ctx,
478        ProcessErrorCase::CodeNotExists,
479    )
480}
481
482/// Helper function for journal creation in case of re-instrumentation error.
483pub fn process_reinstrumentation_error(
484    context: ContextChargedForInstrumentation,
485) -> Vec<JournalNote> {
486    let dispatch = context.data.dispatch;
487    let program_id = context.data.destination_id;
488    let gas_burned = context.data.gas_counter.burned();
489    let system_reservation_ctx = SystemReservationContext::from_dispatch(&dispatch);
490
491    process_error(
492        dispatch,
493        program_id,
494        gas_burned,
495        system_reservation_ctx,
496        ProcessErrorCase::ReinstrumentationFailed,
497    )
498}
499
500/// Helper function for journal creation in success case
501pub fn process_success(
502    kind: SuccessfulDispatchResultKind,
503    dispatch_result: DispatchResult,
504) -> Vec<JournalNote> {
505    use crate::precharge::SuccessfulDispatchResultKind::*;
506
507    let DispatchResult {
508        dispatch,
509        generated_dispatches,
510        awakening,
511        program_candidates,
512        gas_amount,
513        gas_reserver,
514        system_reservation_context,
515        page_update,
516        program_id,
517        context_store,
518        allocations,
519        reply_deposits,
520        reply_sent,
521        ..
522    } = dispatch_result;
523
524    let mut journal = Vec::new();
525
526    let message_id = dispatch.id();
527    let origin = dispatch.source();
528    let value = dispatch.value();
529
530    journal.push(JournalNote::GasBurned {
531        message_id,
532        amount: gas_amount.burned(),
533    });
534
535    if let Some(gas_reserver) = gas_reserver {
536        journal.extend(gas_reserver.states().iter().flat_map(
537            |(&reservation_id, &state)| match state {
538                GasReservationState::Exists { .. } => None,
539                GasReservationState::Created {
540                    amount, duration, ..
541                } => Some(JournalNote::ReserveGas {
542                    message_id,
543                    reservation_id,
544                    program_id,
545                    amount,
546                    duration,
547                }),
548                GasReservationState::Removed { expiration } => Some(JournalNote::UnreserveGas {
549                    reservation_id,
550                    program_id,
551                    expiration,
552                }),
553            },
554        ));
555
556        journal.push(JournalNote::UpdateGasReservations {
557            program_id,
558            reserver: gas_reserver,
559        });
560    }
561
562    if let Some(amount) = system_reservation_context.current_reservation {
563        journal.push(JournalNote::SystemReserveGas { message_id, amount });
564    }
565
566    // We check if value is greater than zero to don't provide
567    // no-op journal note.
568    //
569    // We also check if dispatch had context of previous executions:
570    // it's existence shows that we have processed message after
571    // being waken, so the value were already transferred in
572    // execution, where `gr_wait` was called.
573    if dispatch.context().is_none() && value != 0 {
574        // Send value further
575        journal.push(JournalNote::SendValue {
576            from: origin,
577            to: Some(program_id),
578            value,
579        });
580    }
581
582    // Must be handled before handling generated dispatches.
583    for (code_id, candidates) in program_candidates {
584        journal.push(JournalNote::StoreNewPrograms {
585            program_id,
586            code_id,
587            candidates,
588        });
589    }
590
591    // Sending auto-generated reply about success execution.
592    if matches!(kind, SuccessfulDispatchResultKind::Success)
593        && !reply_sent
594        && !dispatch.is_reply()
595        && dispatch.kind() != DispatchKind::Signal
596    {
597        let auto_reply = ReplyMessage::auto(dispatch.id()).into_dispatch(
598            program_id,
599            dispatch.source(),
600            dispatch.id(),
601        );
602
603        journal.push(JournalNote::SendDispatch {
604            message_id,
605            dispatch: auto_reply,
606            delay: 0,
607            reservation: None,
608        });
609    }
610
611    for (message_id_sent, amount) in reply_deposits {
612        journal.push(JournalNote::ReplyDeposit {
613            message_id,
614            future_reply_id: MessageId::generate_reply(message_id_sent),
615            amount,
616        });
617    }
618
619    for (dispatch, delay, reservation) in generated_dispatches {
620        journal.push(JournalNote::SendDispatch {
621            message_id,
622            dispatch,
623            delay,
624            reservation,
625        });
626    }
627
628    for (awakening_id, delay) in awakening {
629        journal.push(JournalNote::WakeMessage {
630            message_id,
631            program_id,
632            awakening_id,
633            delay,
634        });
635    }
636
637    for (page_number, data) in page_update {
638        journal.push(JournalNote::UpdatePage {
639            program_id,
640            page_number,
641            data,
642        })
643    }
644
645    if let Some(allocations) = allocations {
646        journal.push(JournalNote::UpdateAllocations {
647            program_id,
648            allocations,
649        });
650    }
651
652    let outcome = match kind {
653        Wait(duration, waited_type) => {
654            journal.push(JournalNote::WaitDispatch {
655                dispatch: dispatch.into_stored(program_id, context_store),
656                duration,
657                waited_type,
658            });
659
660            return journal;
661        }
662        Success => match dispatch.kind() {
663            DispatchKind::Init => DispatchOutcome::InitSuccess { program_id },
664            _ => DispatchOutcome::Success,
665        },
666        Exit(value_destination) => {
667            journal.push(JournalNote::ExitDispatch {
668                id_exited: program_id,
669                value_destination,
670            });
671
672            DispatchOutcome::Exit { program_id }
673        }
674    };
675
676    if system_reservation_context.has_any() {
677        journal.push(JournalNote::SystemUnreserveGas { message_id });
678    }
679
680    journal.push(JournalNote::MessageDispatched {
681        message_id,
682        source: origin,
683        outcome,
684    });
685    journal.push(JournalNote::MessageConsumed(message_id));
686    journal
687}
688
689/// Helper function for journal creation if the block gas allowance has been exceeded.
690pub fn process_allowance_exceed(
691    dispatch: IncomingDispatch,
692    program_id: ProgramId,
693    gas_burned: u64,
694) -> Vec<JournalNote> {
695    let mut journal = Vec::with_capacity(1);
696
697    let (kind, message, opt_context) = dispatch.into_parts();
698
699    let dispatch = StoredDispatch::new(kind, message.into_stored(program_id), opt_context);
700
701    journal.push(JournalNote::StopProcessing {
702        dispatch,
703        gas_burned,
704    });
705
706    journal
707}