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::{
31    format,
32    string::{String, ToString},
33    vec::Vec,
34};
35use gear_core::{
36    env::Externalities,
37    ids::{prelude::*, MessageId, ProgramId},
38    message::{ContextSettings, DispatchKind, IncomingDispatch, ReplyMessage, StoredDispatch},
39    reservation::GasReservationState,
40};
41use gear_core_backend::{
42    error::{BackendAllocSyscallError, BackendSyscallError, RunFallibleError},
43    BackendExternalities,
44};
45use gear_core_errors::{ErrorReplyReason, SignalCode};
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    /// Message is not executable error.
203    NonExecutable,
204    /// Error is considered as an execution failure.
205    ExecutionFailed(ActorExecutionErrorReplyReason),
206    /// Message is executable, but it's execution failed due to re-instrumentation.
207    ReinstrumentationFailed,
208}
209
210impl ProcessErrorCase {
211    pub fn to_reason_and_payload(&self) -> (ErrorReplyReason, String) {
212        match self {
213            ProcessErrorCase::NonExecutable => {
214                let reason = ErrorReplyReason::InactiveActor;
215                (reason, reason.to_string())
216            }
217            ProcessErrorCase::ExecutionFailed(reason) => {
218                (reason.as_simple().into(), reason.to_string())
219            }
220            ProcessErrorCase::ReinstrumentationFailed => {
221                let err = ErrorReplyReason::ReinstrumentationFailure;
222                (err, err.to_string())
223            }
224        }
225    }
226}
227
228fn process_error(
229    dispatch: IncomingDispatch,
230    program_id: ProgramId,
231    gas_burned: u64,
232    system_reservation_ctx: SystemReservationContext,
233    case: ProcessErrorCase,
234) -> Vec<JournalNote> {
235    let mut journal = Vec::new();
236
237    let message_id = dispatch.id();
238    let origin = dispatch.source();
239    let value = dispatch.value();
240
241    journal.push(JournalNote::GasBurned {
242        message_id,
243        amount: gas_burned,
244    });
245
246    // We check if value is greater than zero to don't provide
247    // no-op journal note.
248    //
249    // We also check if dispatch had context of previous executions:
250    // it's existence shows that we have processed message after
251    // being waken, so the value were already transferred in
252    // execution, where `gr_wait` was called.
253    if dispatch.context().is_none() && value != 0 {
254        // Send back value
255        journal.push(JournalNote::SendValue {
256            from: origin,
257            to: None,
258            value,
259        });
260    }
261
262    if let Some(amount) = system_reservation_ctx.current_reservation {
263        journal.push(JournalNote::SystemReserveGas { message_id, amount });
264    }
265
266    if let ProcessErrorCase::ExecutionFailed(reason) = &case {
267        // TODO: consider to handle error reply and init #3701
268        if system_reservation_ctx.has_any()
269            && !dispatch.is_error_reply()
270            && !matches!(dispatch.kind(), DispatchKind::Signal | DispatchKind::Init)
271        {
272            journal.push(JournalNote::SendSignal {
273                message_id,
274                destination: program_id,
275                code: SignalCode::Execution(reason.as_simple()),
276            });
277        }
278    }
279
280    if system_reservation_ctx.has_any() {
281        journal.push(JournalNote::SystemUnreserveGas { message_id });
282    }
283
284    if !dispatch.is_reply() && dispatch.kind() != DispatchKind::Signal {
285        let (err, err_payload) = case.to_reason_and_payload();
286
287        // Panic is impossible, unless error message is too large or [Payload] max size is too small.
288        let err_payload = err_payload.into_bytes().try_into().unwrap_or_else(|_| {
289            let (_, err_payload) = case.to_reason_and_payload();
290            let err_msg =
291                format!("process_error: Error message is too big. Message id - {message_id}, error payload - {err_payload}",
292            );
293
294            log::error!("{err_msg}");
295            unreachable!("{err_msg}")
296        });
297
298        // # Safety
299        //
300        // 1. The dispatch.id() has already been checked
301        // 2. This reply message is generated by our system
302        //
303        // So, the message id of this reply message will not be duplicated.
304        let dispatch = ReplyMessage::system(dispatch.id(), err_payload, err).into_dispatch(
305            program_id,
306            dispatch.source(),
307            dispatch.id(),
308        );
309
310        journal.push(JournalNote::SendDispatch {
311            message_id,
312            dispatch,
313            delay: 0,
314            reservation: None,
315        });
316    }
317
318    let outcome = match case {
319        ProcessErrorCase::ExecutionFailed { .. } | ProcessErrorCase::ReinstrumentationFailed => {
320            let (_, err_payload) = case.to_reason_and_payload();
321            match dispatch.kind() {
322                DispatchKind::Init => DispatchOutcome::InitFailure {
323                    program_id,
324                    origin,
325                    reason: err_payload,
326                },
327                _ => DispatchOutcome::MessageTrap {
328                    program_id,
329                    trap: err_payload,
330                },
331            }
332        }
333        ProcessErrorCase::NonExecutable => DispatchOutcome::NoExecution,
334    };
335
336    journal.push(JournalNote::MessageDispatched {
337        message_id,
338        source: origin,
339        outcome,
340    });
341    journal.push(JournalNote::MessageConsumed(message_id));
342
343    journal
344}
345
346/// Helper function for journal creation in trap/error case.
347pub fn process_execution_error(
348    dispatch: IncomingDispatch,
349    program_id: ProgramId,
350    gas_burned: u64,
351    system_reservation_ctx: SystemReservationContext,
352    err: impl Into<ActorExecutionErrorReplyReason>,
353) -> Vec<JournalNote> {
354    process_error(
355        dispatch,
356        program_id,
357        gas_burned,
358        system_reservation_ctx,
359        ProcessErrorCase::ExecutionFailed(err.into()),
360    )
361}
362
363/// Helper function for journal creation in case of re-instrumentation error.
364pub fn process_reinstrumentation_error(
365    context: ContextChargedForInstrumentation,
366) -> Vec<JournalNote> {
367    let dispatch = context.data.dispatch;
368    let program_id = context.data.destination_id;
369    let gas_burned = context.data.gas_counter.burned();
370    let system_reservation_ctx = SystemReservationContext::from_dispatch(&dispatch);
371
372    process_error(
373        dispatch,
374        program_id,
375        gas_burned,
376        system_reservation_ctx,
377        ProcessErrorCase::ReinstrumentationFailed,
378    )
379}
380
381/// Helper function for journal creation in message no execution case.
382pub fn process_non_executable(context: ContextChargedForProgram) -> Vec<JournalNote> {
383    let ContextChargedForProgram {
384        dispatch,
385        gas_counter,
386        destination_id,
387        ..
388    } = context;
389
390    let system_reservation_ctx = SystemReservationContext::from_dispatch(&dispatch);
391
392    process_error(
393        dispatch,
394        destination_id,
395        gas_counter.burned(),
396        system_reservation_ctx,
397        ProcessErrorCase::NonExecutable,
398    )
399}
400
401/// Helper function for journal creation in success case
402pub fn process_success(
403    kind: SuccessfulDispatchResultKind,
404    dispatch_result: DispatchResult,
405) -> Vec<JournalNote> {
406    use crate::precharge::SuccessfulDispatchResultKind::*;
407
408    let DispatchResult {
409        dispatch,
410        generated_dispatches,
411        awakening,
412        program_candidates,
413        gas_amount,
414        gas_reserver,
415        system_reservation_context,
416        page_update,
417        program_id,
418        context_store,
419        allocations,
420        reply_deposits,
421        reply_sent,
422        ..
423    } = dispatch_result;
424
425    let mut journal = Vec::new();
426
427    let message_id = dispatch.id();
428    let origin = dispatch.source();
429    let value = dispatch.value();
430
431    journal.push(JournalNote::GasBurned {
432        message_id,
433        amount: gas_amount.burned(),
434    });
435
436    if let Some(gas_reserver) = gas_reserver {
437        journal.extend(gas_reserver.states().iter().flat_map(
438            |(&reservation_id, &state)| match state {
439                GasReservationState::Exists { .. } => None,
440                GasReservationState::Created {
441                    amount, duration, ..
442                } => Some(JournalNote::ReserveGas {
443                    message_id,
444                    reservation_id,
445                    program_id,
446                    amount,
447                    duration,
448                }),
449                GasReservationState::Removed { expiration } => Some(JournalNote::UnreserveGas {
450                    reservation_id,
451                    program_id,
452                    expiration,
453                }),
454            },
455        ));
456
457        journal.push(JournalNote::UpdateGasReservations {
458            program_id,
459            reserver: gas_reserver,
460        });
461    }
462
463    if let Some(amount) = system_reservation_context.current_reservation {
464        journal.push(JournalNote::SystemReserveGas { message_id, amount });
465    }
466
467    // We check if value is greater than zero to don't provide
468    // no-op journal note.
469    //
470    // We also check if dispatch had context of previous executions:
471    // it's existence shows that we have processed message after
472    // being waken, so the value were already transferred in
473    // execution, where `gr_wait` was called.
474    if dispatch.context().is_none() && value != 0 {
475        // Send value further
476        journal.push(JournalNote::SendValue {
477            from: origin,
478            to: Some(program_id),
479            value,
480        });
481    }
482
483    // Must be handled before handling generated dispatches.
484    for (code_id, candidates) in program_candidates {
485        journal.push(JournalNote::StoreNewPrograms {
486            program_id,
487            code_id,
488            candidates,
489        });
490    }
491
492    // Sending auto-generated reply about success execution.
493    if matches!(kind, SuccessfulDispatchResultKind::Success)
494        && !reply_sent
495        && !dispatch.is_reply()
496        && dispatch.kind() != DispatchKind::Signal
497    {
498        let auto_reply = ReplyMessage::auto(dispatch.id()).into_dispatch(
499            program_id,
500            dispatch.source(),
501            dispatch.id(),
502        );
503
504        journal.push(JournalNote::SendDispatch {
505            message_id,
506            dispatch: auto_reply,
507            delay: 0,
508            reservation: None,
509        });
510    }
511
512    for (message_id_sent, amount) in reply_deposits {
513        journal.push(JournalNote::ReplyDeposit {
514            message_id,
515            future_reply_id: MessageId::generate_reply(message_id_sent),
516            amount,
517        });
518    }
519
520    for (dispatch, delay, reservation) in generated_dispatches {
521        journal.push(JournalNote::SendDispatch {
522            message_id,
523            dispatch,
524            delay,
525            reservation,
526        });
527    }
528
529    for (awakening_id, delay) in awakening {
530        journal.push(JournalNote::WakeMessage {
531            message_id,
532            program_id,
533            awakening_id,
534            delay,
535        });
536    }
537
538    for (page_number, data) in page_update {
539        journal.push(JournalNote::UpdatePage {
540            program_id,
541            page_number,
542            data,
543        })
544    }
545
546    if let Some(allocations) = allocations {
547        journal.push(JournalNote::UpdateAllocations {
548            program_id,
549            allocations,
550        });
551    }
552
553    let outcome = match kind {
554        Wait(duration, waited_type) => {
555            journal.push(JournalNote::WaitDispatch {
556                dispatch: dispatch.into_stored(program_id, context_store),
557                duration,
558                waited_type,
559            });
560
561            return journal;
562        }
563        Success => match dispatch.kind() {
564            DispatchKind::Init => DispatchOutcome::InitSuccess { program_id },
565            _ => DispatchOutcome::Success,
566        },
567        Exit(value_destination) => {
568            journal.push(JournalNote::ExitDispatch {
569                id_exited: program_id,
570                value_destination,
571            });
572
573            DispatchOutcome::Exit { program_id }
574        }
575    };
576
577    if system_reservation_context.has_any() {
578        journal.push(JournalNote::SystemUnreserveGas { message_id });
579    }
580
581    journal.push(JournalNote::MessageDispatched {
582        message_id,
583        source: origin,
584        outcome,
585    });
586    journal.push(JournalNote::MessageConsumed(message_id));
587    journal
588}
589
590/// Helper function for journal creation if the block gas allowance has been exceeded.
591pub fn process_allowance_exceed(
592    dispatch: IncomingDispatch,
593    program_id: ProgramId,
594    gas_burned: u64,
595) -> Vec<JournalNote> {
596    let mut journal = Vec::with_capacity(1);
597
598    let (kind, message, opt_context) = dispatch.into_parts();
599
600    let dispatch = StoredDispatch::new(kind, message.into_stored(program_id), opt_context);
601
602    journal.push(JournalNote::StopProcessing {
603        dispatch,
604        gas_burned,
605    });
606
607    journal
608}