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    ContextCharged, ForCodeMetadata, ForInstrumentedCode, ForProgram,
21    common::{
22        ActorExecutionErrorReplyReason, DispatchOutcome, DispatchResult, DispatchResultKind,
23        ExecutionError, JournalNote, SuccessfulDispatchResultKind, SystemExecutionError,
24        WasmExecutionContext,
25    },
26    configs::{BlockConfig, ExecutionSettings},
27    context::*,
28    executor,
29    ext::ProcessorExternalities,
30};
31use alloc::{string::ToString, vec::Vec};
32use core::{fmt, fmt::Formatter};
33use gear_core::{
34    buffer::Payload,
35    env::Externalities,
36    ids::{ActorId, MessageId, prelude::*},
37    limited::LimitedVecError,
38    message::{ContextSettings, DispatchKind, IncomingDispatch, ReplyMessage, StoredDispatch},
39    reservation::GasReservationState,
40};
41use gear_core_backend::{
42    BackendExternalities,
43    error::{BackendAllocSyscallError, BackendSyscallError, RunFallibleError, TrapExplanation},
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::common::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.db.write.cost_for(2.into()),
117        scheduled_sending_fee: costs.db.write.cost_for(4.into()),
118        waiting_fee: costs.db.write.cost_for(3.into()),
119        waking_fee: costs.db.write.cost_for(2.into()),
120        reservation_fee: costs.db.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!(
161                        res.gas_reserver
162                            .as_ref()
163                            .map(|reserver| initial_reservations_amount <= reserver.states().len())
164                            .unwrap_or(true)
165                    );
166                }
167                // reservation does not change in case of failure
168                _ => (),
169            }
170            Ok(match res.kind {
171                DispatchResultKind::Trap(reason) => process_execution_error(
172                    dispatch,
173                    program_id,
174                    res.gas_amount.burned(),
175                    res.system_reservation_context,
176                    ActorExecutionErrorReplyReason::Trap(reason),
177                ),
178
179                DispatchResultKind::Success => process_success(Success, res, dispatch),
180                DispatchResultKind::Wait(duration, ref waited_type) => {
181                    process_success(Wait(duration, waited_type.clone()), res, dispatch)
182                }
183                DispatchResultKind::Exit(value_destination) => {
184                    process_success(Exit(value_destination), res, dispatch)
185                }
186                DispatchResultKind::GasAllowanceExceed => {
187                    process_allowance_exceed(dispatch, program_id, res.gas_amount.burned())
188                }
189            })
190        }
191        Err(ExecutionError::Actor(e)) => Ok(process_execution_error(
192            dispatch,
193            program_id,
194            e.gas_amount.burned(),
195            system_reservation_ctx,
196            e.reason,
197        )),
198        Err(ExecutionError::System(e)) => Err(e),
199    }
200}
201
202enum ProcessErrorCase {
203    /// Program exited.
204    ProgramExited {
205        /// Inheritor of an exited program.
206        inheritor: ActorId,
207    },
208    /// Program failed during init.
209    FailedInit,
210    /// Program is not initialized yet.
211    Uninitialized,
212    /// Given code id for program creation doesn't exist.
213    CodeNotExists,
214    /// Message is executable, but its execution failed due to re-instrumentation.
215    ReinstrumentationFailed,
216    /// Error is considered as an execution failure.
217    ExecutionFailed(ActorExecutionErrorReplyReason),
218}
219
220impl fmt::Display for ProcessErrorCase {
221    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
222        match self {
223            ProcessErrorCase::ExecutionFailed(reason) => fmt::Display::fmt(reason, f),
224            this => fmt::Display::fmt(&this.to_reason(), f),
225        }
226    }
227}
228
229impl ProcessErrorCase {
230    fn to_reason(&self) -> ErrorReplyReason {
231        match self {
232            ProcessErrorCase::ProgramExited { .. } => {
233                ErrorReplyReason::UnavailableActor(SimpleUnavailableActorError::ProgramExited)
234            }
235            ProcessErrorCase::FailedInit => ErrorReplyReason::UnavailableActor(
236                SimpleUnavailableActorError::InitializationFailure,
237            ),
238            ProcessErrorCase::Uninitialized => {
239                ErrorReplyReason::UnavailableActor(SimpleUnavailableActorError::Uninitialized)
240            }
241            ProcessErrorCase::CodeNotExists => {
242                ErrorReplyReason::UnavailableActor(SimpleUnavailableActorError::ProgramNotCreated)
243            }
244            ProcessErrorCase::ReinstrumentationFailed => ErrorReplyReason::UnavailableActor(
245                SimpleUnavailableActorError::ReinstrumentationFailure,
246            ),
247            ProcessErrorCase::ExecutionFailed(reason) => reason.as_simple().into(),
248        }
249    }
250
251    // TODO: consider to convert `self` into `Payload` to avoid `PanicBuffer` cloning (#4594)
252    fn to_payload(&self) -> Payload {
253        match self {
254            ProcessErrorCase::ProgramExited { inheritor } => {
255                const _: () = assert!(size_of::<ActorId>() <= Payload::MAX_LEN);
256                inheritor
257                    .into_bytes()
258                    .to_vec()
259                    .try_into()
260                    .unwrap_or_else(|LimitedVecError| {
261                        unreachable!("`ActorId` is always smaller than maximum payload size")
262                    })
263            }
264            ProcessErrorCase::ExecutionFailed(ActorExecutionErrorReplyReason::Trap(
265                TrapExplanation::Panic(buf),
266            )) => buf.inner().clone(),
267            _ => Payload::default(),
268        }
269    }
270}
271
272fn process_error(
273    dispatch: IncomingDispatch,
274    program_id: ActorId,
275    gas_burned: u64,
276    system_reservation_ctx: SystemReservationContext,
277    case: ProcessErrorCase,
278) -> Vec<JournalNote> {
279    let mut journal = Vec::new();
280
281    let message_id = dispatch.id();
282    let origin = dispatch.source();
283    let value = dispatch.value();
284
285    journal.push(JournalNote::GasBurned {
286        message_id,
287        amount: gas_burned,
288        is_panic: matches!(
289            case,
290            ProcessErrorCase::ExecutionFailed(ActorExecutionErrorReplyReason::Trap(
291                TrapExplanation::Panic(_)
292            ))
293        ),
294    });
295
296    let to_send_reply = !matches!(dispatch.kind(), DispatchKind::Reply | DispatchKind::Signal);
297
298    // We check if value is greater than zero to don't provide
299    // no-op journal note.
300    //
301    // We also check if dispatch had context of previous executions:
302    // it's existence shows that we have processed message after
303    // being waken, so the value were already transferred in
304    // execution, where `gr_wait` was called.
305    if dispatch.context().is_none() && value != 0 {
306        // Value on error is always delivered to the program, but may return with error reply.
307        journal.push(JournalNote::SendValue {
308            from: origin,
309            to: program_id,
310            value,
311            // in case of upcoming error reply, we want to send locked value,
312            // instead of deposit, to avoid ED manipulations.
313            locked: to_send_reply,
314        });
315    }
316
317    if let Some(amount) = system_reservation_ctx.current_reservation {
318        journal.push(JournalNote::SystemReserveGas { message_id, amount });
319    }
320
321    if let ProcessErrorCase::ExecutionFailed(reason) = &case {
322        // TODO: consider to handle error reply and init #3701
323        if system_reservation_ctx.has_any()
324            && !dispatch.is_error_reply()
325            && !matches!(dispatch.kind(), DispatchKind::Signal | DispatchKind::Init)
326        {
327            journal.push(JournalNote::SendSignal {
328                message_id,
329                destination: program_id,
330                code: SignalCode::Execution(reason.as_simple()),
331            });
332        }
333    }
334
335    if system_reservation_ctx.has_any() {
336        journal.push(JournalNote::SystemUnreserveGas { message_id });
337    }
338
339    if to_send_reply {
340        let err = case.to_reason();
341        let err_payload = case.to_payload();
342
343        let value = if dispatch.context().is_none() {
344            value
345        } else {
346            0
347        };
348
349        // # Safety
350        //
351        // 1. The dispatch.id() has already been checked
352        // 2. This reply message is generated by our system
353        //
354        // So, the message id of this reply message will not be duplicated.
355        let dispatch = ReplyMessage::system(dispatch.id(), err_payload, value, err).into_dispatch(
356            program_id,
357            dispatch.source(),
358            dispatch.id(),
359        );
360
361        journal.push(JournalNote::SendDispatch {
362            message_id,
363            dispatch,
364            delay: 0,
365            reservation: None,
366        });
367    }
368
369    let outcome = match case {
370        ProcessErrorCase::ExecutionFailed { .. } | ProcessErrorCase::ReinstrumentationFailed => {
371            let err_msg = case.to_string();
372            match dispatch.kind() {
373                DispatchKind::Init => DispatchOutcome::InitFailure {
374                    program_id,
375                    origin,
376                    reason: err_msg,
377                },
378                _ => DispatchOutcome::MessageTrap {
379                    program_id,
380                    trap: err_msg,
381                },
382            }
383        }
384        ProcessErrorCase::ProgramExited { .. }
385        | ProcessErrorCase::FailedInit
386        | ProcessErrorCase::Uninitialized
387        | ProcessErrorCase::CodeNotExists => DispatchOutcome::NoExecution,
388    };
389
390    journal.push(JournalNote::MessageDispatched {
391        message_id,
392        source: origin,
393        outcome,
394    });
395    journal.push(JournalNote::MessageConsumed(message_id));
396
397    journal
398}
399
400/// Helper function for journal creation in trap/error case.
401pub fn process_execution_error(
402    dispatch: IncomingDispatch,
403    program_id: ActorId,
404    gas_burned: u64,
405    system_reservation_ctx: SystemReservationContext,
406    err: impl Into<ActorExecutionErrorReplyReason>,
407) -> Vec<JournalNote> {
408    process_error(
409        dispatch,
410        program_id,
411        gas_burned,
412        system_reservation_ctx,
413        ProcessErrorCase::ExecutionFailed(err.into()),
414    )
415}
416
417/// Helper function for journal creation in program exited case.
418pub fn process_program_exited(
419    context: ContextCharged<ForProgram>,
420    inheritor: ActorId,
421) -> Vec<JournalNote> {
422    let (destination_id, dispatch, gas_counter, _) = context.into_parts();
423
424    let system_reservation_ctx = SystemReservationContext::from_dispatch(&dispatch);
425
426    process_error(
427        dispatch,
428        destination_id,
429        gas_counter.burned(),
430        system_reservation_ctx,
431        ProcessErrorCase::ProgramExited { inheritor },
432    )
433}
434
435/// Helper function for journal creation in program failed init case.
436pub fn process_failed_init(context: ContextCharged<ForProgram>) -> Vec<JournalNote> {
437    let (destination_id, dispatch, gas_counter, _) = context.into_parts();
438
439    let system_reservation_ctx = SystemReservationContext::from_dispatch(&dispatch);
440
441    process_error(
442        dispatch,
443        destination_id,
444        gas_counter.burned(),
445        system_reservation_ctx,
446        ProcessErrorCase::FailedInit,
447    )
448}
449
450/// Helper function for journal creation in program uninitialized case.
451pub fn process_uninitialized(context: ContextCharged<ForProgram>) -> Vec<JournalNote> {
452    let (destination_id, dispatch, gas_counter, _) = context.into_parts();
453
454    let system_reservation_ctx = SystemReservationContext::from_dispatch(&dispatch);
455
456    process_error(
457        dispatch,
458        destination_id,
459        gas_counter.burned(),
460        system_reservation_ctx,
461        ProcessErrorCase::Uninitialized,
462    )
463}
464
465/// Helper function for journal creation in code not exists case.
466pub fn process_code_not_exists(context: ContextCharged<ForProgram>) -> Vec<JournalNote> {
467    let (destination_id, dispatch, gas_counter, _) = context.into_parts();
468
469    let system_reservation_ctx = SystemReservationContext::from_dispatch(&dispatch);
470
471    process_error(
472        dispatch,
473        destination_id,
474        gas_counter.burned(),
475        system_reservation_ctx,
476        ProcessErrorCase::CodeNotExists,
477    )
478}
479
480/// Helper function for journal creation in case of re-instrumentation error.
481pub fn process_reinstrumentation_error(
482    context: ContextCharged<ForInstrumentedCode>,
483) -> Vec<JournalNote> {
484    let (destination_id, dispatch, gas_counter, _) = context.into_parts();
485
486    let gas_burned = gas_counter.burned();
487    let system_reservation_ctx = SystemReservationContext::from_dispatch(&dispatch);
488
489    process_error(
490        dispatch,
491        destination_id,
492        gas_burned,
493        system_reservation_ctx,
494        ProcessErrorCase::ReinstrumentationFailed,
495    )
496}
497
498/// Helper function for journal creation in case of instrumentation failure.
499pub fn process_instrumentation_failed(
500    context: ContextCharged<ForCodeMetadata>,
501) -> Vec<JournalNote> {
502    let (destination_id, dispatch, gas_counter, _) = context.into_parts();
503
504    let gas_burned = gas_counter.burned();
505    let system_reservation_ctx = SystemReservationContext::from_dispatch(&dispatch);
506
507    process_error(
508        dispatch,
509        destination_id,
510        gas_burned,
511        system_reservation_ctx,
512        ProcessErrorCase::ReinstrumentationFailed,
513    )
514}
515
516/// Helper function for journal creation in success case
517pub fn process_success(
518    kind: SuccessfulDispatchResultKind,
519    dispatch_result: DispatchResult,
520    dispatch: IncomingDispatch,
521) -> Vec<JournalNote> {
522    use crate::common::SuccessfulDispatchResultKind::*;
523
524    let DispatchResult {
525        generated_dispatches,
526        awakening,
527        program_candidates,
528        gas_amount,
529        gas_reserver,
530        system_reservation_context,
531        page_update,
532        program_id,
533        context_store,
534        allocations,
535        reply_deposits,
536        reply_sent,
537        ..
538    } = dispatch_result;
539
540    let mut journal = Vec::new();
541
542    let message_id = dispatch.id();
543    let origin = dispatch.source();
544    let value = dispatch.value();
545
546    journal.push(JournalNote::GasBurned {
547        message_id,
548        amount: gas_amount.burned(),
549        is_panic: false,
550    });
551
552    if let Some(gas_reserver) = gas_reserver {
553        journal.extend(gas_reserver.states().iter().flat_map(
554            |(&reservation_id, &state)| match state {
555                GasReservationState::Exists { .. } => None,
556                GasReservationState::Created {
557                    amount, duration, ..
558                } => Some(JournalNote::ReserveGas {
559                    message_id,
560                    reservation_id,
561                    program_id,
562                    amount,
563                    duration,
564                }),
565                GasReservationState::Removed { expiration } => Some(JournalNote::UnreserveGas {
566                    reservation_id,
567                    program_id,
568                    expiration,
569                }),
570            },
571        ));
572
573        journal.push(JournalNote::UpdateGasReservations {
574            program_id,
575            reserver: gas_reserver,
576        });
577    }
578
579    if let Some(amount) = system_reservation_context.current_reservation {
580        journal.push(JournalNote::SystemReserveGas { message_id, amount });
581    }
582
583    // We check if value is greater than zero to don't provide
584    // no-op journal note.
585    //
586    // We also check if dispatch had context of previous executions:
587    // it's existence shows that we have processed message after
588    // being waken, so the value were already transferred in
589    // execution, where `gr_wait` was called.
590    if dispatch.context().is_none() && value != 0 {
591        // Send value further
592        journal.push(JournalNote::SendValue {
593            from: origin,
594            to: program_id,
595            value,
596            locked: false,
597        });
598    }
599
600    // Must be handled before handling generated dispatches.
601    for (code_id, candidates) in program_candidates {
602        journal.push(JournalNote::StoreNewPrograms {
603            program_id,
604            code_id,
605            candidates,
606        });
607    }
608
609    // Sending auto-generated reply about success execution.
610    if !matches!(kind, SuccessfulDispatchResultKind::Wait(_, _))
611        && !matches!(dispatch.kind(), DispatchKind::Reply | DispatchKind::Signal)
612        && !reply_sent
613    {
614        let auto_reply = ReplyMessage::auto(dispatch.id()).into_dispatch(
615            program_id,
616            dispatch.source(),
617            dispatch.id(),
618        );
619
620        journal.push(JournalNote::SendDispatch {
621            message_id,
622            dispatch: auto_reply,
623            delay: 0,
624            reservation: None,
625        });
626    }
627
628    for (message_id_sent, amount) in reply_deposits {
629        journal.push(JournalNote::ReplyDeposit {
630            message_id,
631            future_reply_id: MessageId::generate_reply(message_id_sent),
632            amount,
633        });
634    }
635
636    for (dispatch, delay, reservation) in generated_dispatches {
637        journal.push(JournalNote::SendDispatch {
638            message_id,
639            dispatch,
640            delay,
641            reservation,
642        });
643    }
644
645    for (awakening_id, delay) in awakening {
646        journal.push(JournalNote::WakeMessage {
647            message_id,
648            program_id,
649            awakening_id,
650            delay,
651        });
652    }
653
654    for (page_number, data) in page_update {
655        journal.push(JournalNote::UpdatePage {
656            program_id,
657            page_number,
658            data,
659        })
660    }
661
662    if let Some(allocations) = allocations {
663        journal.push(JournalNote::UpdateAllocations {
664            program_id,
665            allocations,
666        });
667    }
668
669    let outcome = match kind {
670        Wait(duration, waited_type) => {
671            journal.push(JournalNote::WaitDispatch {
672                dispatch: dispatch.into_stored(program_id, context_store),
673                duration,
674                waited_type,
675            });
676
677            return journal;
678        }
679        Success => match dispatch.kind() {
680            DispatchKind::Init => DispatchOutcome::InitSuccess { program_id },
681            _ => DispatchOutcome::Success,
682        },
683        Exit(value_destination) => {
684            journal.push(JournalNote::ExitDispatch {
685                id_exited: program_id,
686                value_destination,
687            });
688
689            DispatchOutcome::Exit { program_id }
690        }
691    };
692
693    if system_reservation_context.has_any() {
694        journal.push(JournalNote::SystemUnreserveGas { message_id });
695    }
696
697    journal.push(JournalNote::MessageDispatched {
698        message_id,
699        source: origin,
700        outcome,
701    });
702    journal.push(JournalNote::MessageConsumed(message_id));
703    journal
704}
705
706/// Helper function for journal creation if the block gas allowance has been exceeded.
707pub fn process_allowance_exceed(
708    dispatch: IncomingDispatch,
709    program_id: ActorId,
710    gas_burned: u64,
711) -> Vec<JournalNote> {
712    let mut journal = Vec::with_capacity(1);
713
714    let (kind, message, opt_context) = dispatch.into_parts();
715
716    let dispatch = StoredDispatch::new(kind, message.into_stored(program_id), opt_context);
717
718    journal.push(JournalNote::StopProcessing {
719        dispatch,
720        gas_burned,
721    });
722
723    journal
724}