gear_core_processor/
precharge.rs

1// Copyright (C) 2023 Gear Technologies Inc.
2// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with this program. If not, see <https://www.gnu.org/licenses/>.
16
17use crate::{
18    common::{
19        ActorExecutionErrorReplyReason, DispatchResult, ExecutableActorData, JournalNote,
20        PrechargedDispatch,
21    },
22    configs::{BlockConfig, PageCosts},
23    context::{ContextChargedForCodeLength, ContextChargedForMemory, ContextData},
24    processing::{
25        process_allowance_exceed, process_error, process_non_executable, process_success,
26    },
27    ContextChargedForCode, ContextChargedForInstrumentation,
28};
29use alloc::{collections::BTreeSet, vec::Vec};
30use gear_backend_common::SystemReservationContext;
31use gear_core::{
32    gas::{ChargeResult, GasAllowanceCounter, GasCounter},
33    ids::ProgramId,
34    message::{DispatchKind, IncomingDispatch, MessageWaitedType},
35    pages::{PageU32Size, WasmPage},
36};
37use scale_info::{
38    scale::{self, Decode, Encode},
39    TypeInfo,
40};
41
42/// Operation related to gas charging.
43#[derive(Encode, Decode, TypeInfo, Debug, PartialEq, Eq, PartialOrd, Ord, derive_more::Display)]
44#[codec(crate = scale)]
45pub enum PreChargeGasOperation {
46    /// Handle memory static pages.
47    #[display(fmt = "handle memory static pages")]
48    StaticPages,
49    /// Handle program data.
50    #[display(fmt = "handle program data")]
51    ProgramData,
52    /// Obtain code length.
53    #[display(fmt = "obtain program code length")]
54    ProgramCodeLen,
55    /// Handle program code.
56    #[display(fmt = "handle program code")]
57    ProgramCode,
58    /// Instantiate Wasm module.
59    #[display(fmt = "instantiate Wasm module")]
60    ModuleInstantiation,
61    /// Instrument Wasm module.
62    #[display(fmt = "instrument Wasm module")]
63    ModuleInstrumentation,
64}
65
66#[derive(Debug, Eq, PartialEq)]
67enum PrechargeError {
68    BlockGasExceeded,
69    GasExceeded(PreChargeGasOperation),
70}
71
72struct GasPrecharger<'a> {
73    counter: &'a mut GasCounter,
74    allowance_counter: &'a mut GasAllowanceCounter,
75}
76
77impl<'a> GasPrecharger<'a> {
78    pub fn new(
79        counter: &'a mut GasCounter,
80        allowance_counter: &'a mut GasAllowanceCounter,
81    ) -> Self {
82        Self {
83            counter,
84            allowance_counter,
85        }
86    }
87
88    fn charge_gas(
89        &mut self,
90        operation: PreChargeGasOperation,
91        amount: u64,
92    ) -> Result<(), PrechargeError> {
93        if self.allowance_counter.charge_if_enough(amount) != ChargeResult::Enough {
94            return Err(PrechargeError::BlockGasExceeded);
95        }
96        if self.counter.charge_if_enough(amount) != ChargeResult::Enough {
97            return Err(PrechargeError::GasExceeded(operation));
98        }
99
100        Ok(())
101    }
102
103    pub fn charge_gas_for_program_data(
104        &mut self,
105        read_cost: u64,
106        per_byte_cost: u64,
107    ) -> Result<(), PrechargeError> {
108        self.charge_gas(
109            PreChargeGasOperation::ProgramData,
110            calculate_gas_for_program(read_cost, per_byte_cost),
111        )
112    }
113
114    pub fn charge_gas_for_program_code_len(
115        &mut self,
116        read_cost: u64,
117    ) -> Result<(), PrechargeError> {
118        self.charge_gas(PreChargeGasOperation::ProgramCodeLen, read_cost)
119    }
120
121    pub fn charge_gas_for_program_code(
122        &mut self,
123        read_cost: u64,
124        per_byte_cost: u64,
125        code_len_bytes: u32,
126    ) -> Result<(), PrechargeError> {
127        self.charge_gas(
128            PreChargeGasOperation::ProgramCode,
129            calculate_gas_for_code(read_cost, per_byte_cost, code_len_bytes.into()),
130        )
131    }
132
133    pub fn charge_gas_for_instantiation(
134        &mut self,
135        gas_per_byte: u64,
136        code_length: u32,
137    ) -> Result<(), PrechargeError> {
138        let amount = gas_per_byte.saturating_mul(code_length as u64);
139        self.charge_gas(PreChargeGasOperation::ModuleInstantiation, amount)
140    }
141
142    pub fn charge_gas_for_instrumentation(
143        &mut self,
144        instrumentation_cost: u64,
145        instrumentation_byte_cost: u64,
146        original_code_len_bytes: u32,
147    ) -> Result<(), PrechargeError> {
148        let amount = instrumentation_cost.saturating_add(
149            instrumentation_byte_cost.saturating_mul(original_code_len_bytes.into()),
150        );
151        self.charge_gas(PreChargeGasOperation::ModuleInstrumentation, amount)
152    }
153
154    /// Charge gas for pages and checks that there is enough gas for that.
155    /// Returns size of wasm memory buffer which must be created in execution environment.
156    pub fn charge_gas_for_pages(
157        &mut self,
158        costs: &PageCosts,
159        allocations: &BTreeSet<WasmPage>,
160        static_pages: WasmPage,
161    ) -> Result<WasmPage, PrechargeError> {
162        // Charging gas for static pages.
163        let amount = costs.static_page.calc(static_pages);
164        self.charge_gas(PreChargeGasOperation::StaticPages, amount)?;
165
166        if let Some(page) = allocations.iter().next_back() {
167            // It means we somehow violated some constraints:
168            // 1. one of allocated pages > MAX_WASM_PAGE_COUNT
169            // 2. static pages > MAX_WASM_PAGE_COUNT
170            Ok(page
171                .inc()
172                .unwrap_or_else(|_| unreachable!("WASM memory size is too big")))
173        } else {
174            Ok(static_pages)
175        }
176    }
177}
178
179/// Calculates gas amount required to charge for program loading.
180pub fn calculate_gas_for_program(read_cost: u64, _per_byte_cost: u64) -> u64 {
181    read_cost
182}
183
184/// Calculates gas amount required to charge for code loading.
185pub fn calculate_gas_for_code(read_cost: u64, per_byte_cost: u64, code_len_bytes: u64) -> u64 {
186    read_cost.saturating_add(code_len_bytes.saturating_mul(per_byte_cost))
187}
188
189#[derive(Debug)]
190pub enum SuccessfulDispatchResultKind {
191    Exit(ProgramId),
192    Wait(Option<u32>, MessageWaitedType),
193    Success,
194}
195
196/// Defines result variants of the precharge functions.
197pub type PrechargeResult<T> = Result<T, Vec<JournalNote>>;
198
199/// Charge a message for program data beforehand.
200pub fn precharge_for_program(
201    block_config: &BlockConfig,
202    gas_allowance: u64,
203    dispatch: IncomingDispatch,
204    destination_id: ProgramId,
205) -> PrechargeResult<PrechargedDispatch> {
206    let read_per_byte_cost = block_config.read_per_byte_cost;
207    let read_cost = block_config.read_cost;
208
209    let mut gas_counter = GasCounter::new(dispatch.gas_limit());
210    let mut gas_allowance_counter = GasAllowanceCounter::new(gas_allowance);
211    let mut charger = GasPrecharger::new(&mut gas_counter, &mut gas_allowance_counter);
212
213    match charger.charge_gas_for_program_data(read_cost, read_per_byte_cost) {
214        Ok(()) => Ok((dispatch, gas_counter, gas_allowance_counter).into()),
215        Err(PrechargeError::BlockGasExceeded) => {
216            let gas_burned = gas_counter.burned();
217            Err(process_allowance_exceed(
218                dispatch,
219                destination_id,
220                gas_burned,
221            ))
222        }
223        Err(PrechargeError::GasExceeded(op)) => {
224            let gas_burned = gas_counter.burned();
225            let system_reservation_ctx = SystemReservationContext::from_dispatch(&dispatch);
226            Err(process_error(
227                dispatch,
228                destination_id,
229                gas_burned,
230                system_reservation_ctx,
231                ActorExecutionErrorReplyReason::PreChargeGasLimitExceeded(op),
232                false,
233            ))
234        }
235    }
236}
237
238fn check_is_executable(
239    executable_data: Option<ExecutableActorData>,
240    dispatch: &IncomingDispatch,
241) -> Option<ExecutableActorData> {
242    executable_data
243        .filter(|data| !(data.initialized && matches!(dispatch.kind(), DispatchKind::Init)))
244}
245
246/// Charge a message for fetching the actual length of the binary code
247/// from a storage. The updated value of binary code length
248/// should be kept in standalone storage. The caller has to call this
249/// function to charge gas-counters accordingly before fetching the value.
250///
251/// The function also performs several additional checks:
252/// - if an actor is executable
253/// - if a required dispatch method is exported.
254pub fn precharge_for_code_length(
255    block_config: &BlockConfig,
256    dispatch: PrechargedDispatch,
257    destination_id: ProgramId,
258    executable_data: Option<ExecutableActorData>,
259) -> PrechargeResult<ContextChargedForCodeLength> {
260    let read_cost = block_config.read_cost;
261
262    let (dispatch, mut gas_counter, mut gas_allowance_counter) = dispatch.into_parts();
263
264    let Some(actor_data) = check_is_executable(executable_data, &dispatch) else {
265        let system_reservation_ctx = SystemReservationContext::from_dispatch(&dispatch);
266        return Err(process_non_executable(
267            dispatch,
268            destination_id,
269            system_reservation_ctx,
270        ));
271    };
272
273    if !actor_data.code_exports.contains(&dispatch.kind()) {
274        return Err(process_success(
275            SuccessfulDispatchResultKind::Success,
276            DispatchResult::success(dispatch, destination_id, gas_counter.to_amount()),
277        ));
278    }
279
280    let mut charger = GasPrecharger::new(&mut gas_counter, &mut gas_allowance_counter);
281    match charger.charge_gas_for_program_code_len(read_cost) {
282        Ok(()) => Ok(ContextChargedForCodeLength {
283            data: ContextData {
284                gas_counter,
285                gas_allowance_counter,
286                dispatch,
287                destination_id,
288                actor_data,
289            },
290        }),
291        Err(PrechargeError::BlockGasExceeded) => Err(process_allowance_exceed(
292            dispatch,
293            destination_id,
294            gas_counter.burned(),
295        )),
296        Err(PrechargeError::GasExceeded(op)) => {
297            let system_reservation_ctx = SystemReservationContext::from_dispatch(&dispatch);
298            Err(process_error(
299                dispatch,
300                destination_id,
301                gas_counter.burned(),
302                system_reservation_ctx,
303                ActorExecutionErrorReplyReason::PreChargeGasLimitExceeded(op),
304                false,
305            ))
306        }
307    }
308}
309
310/// Charge a message for the program binary code beforehand.
311pub fn precharge_for_code(
312    block_config: &BlockConfig,
313    mut context: ContextChargedForCodeLength,
314    code_len_bytes: u32,
315) -> PrechargeResult<ContextChargedForCode> {
316    let read_per_byte_cost = block_config.read_per_byte_cost;
317    let read_cost = block_config.read_cost;
318
319    let mut charger = GasPrecharger::new(
320        &mut context.data.gas_counter,
321        &mut context.data.gas_allowance_counter,
322    );
323
324    match charger.charge_gas_for_program_code(read_cost, read_per_byte_cost, code_len_bytes) {
325        Ok(()) => Ok((context, code_len_bytes).into()),
326        Err(PrechargeError::BlockGasExceeded) => Err(process_allowance_exceed(
327            context.data.dispatch,
328            context.data.destination_id,
329            context.data.gas_counter.burned(),
330        )),
331        Err(PrechargeError::GasExceeded(op)) => {
332            let system_reservation_ctx =
333                SystemReservationContext::from_dispatch(&context.data.dispatch);
334            Err(process_error(
335                context.data.dispatch,
336                context.data.destination_id,
337                context.data.gas_counter.burned(),
338                system_reservation_ctx,
339                ActorExecutionErrorReplyReason::PreChargeGasLimitExceeded(op),
340                false,
341            ))
342        }
343    }
344}
345
346/// Charge a message for instrumentation of the binary code beforehand.
347pub fn precharge_for_instrumentation(
348    block_config: &BlockConfig,
349    mut context: ContextChargedForCode,
350    original_code_len_bytes: u32,
351) -> PrechargeResult<ContextChargedForInstrumentation> {
352    let cost_base = block_config.code_instrumentation_cost;
353    let cost_per_byte = block_config.code_instrumentation_byte_cost;
354
355    let mut charger = GasPrecharger::new(
356        &mut context.data.gas_counter,
357        &mut context.data.gas_allowance_counter,
358    );
359
360    match charger.charge_gas_for_instrumentation(cost_base, cost_per_byte, original_code_len_bytes)
361    {
362        Ok(()) => Ok(context.into()),
363        Err(PrechargeError::BlockGasExceeded) => Err(process_allowance_exceed(
364            context.data.dispatch,
365            context.data.destination_id,
366            context.data.gas_counter.burned(),
367        )),
368        Err(PrechargeError::GasExceeded(op)) => {
369            let system_reservation_ctx =
370                SystemReservationContext::from_dispatch(&context.data.dispatch);
371            Err(process_error(
372                context.data.dispatch,
373                context.data.destination_id,
374                context.data.gas_counter.burned(),
375                system_reservation_ctx,
376                ActorExecutionErrorReplyReason::PreChargeGasLimitExceeded(op),
377                false,
378            ))
379        }
380    }
381}
382
383/// Charge a message for program memory and module instantiation beforehand.
384pub fn precharge_for_memory(
385    block_config: &BlockConfig,
386    mut context: ContextChargedForInstrumentation,
387) -> PrechargeResult<ContextChargedForMemory> {
388    let ContextChargedForInstrumentation {
389        data:
390            ContextData {
391                gas_counter,
392                gas_allowance_counter,
393                actor_data,
394                ..
395            },
396        code_len_bytes,
397    } = &mut context;
398
399    let mut f = || {
400        let mut charger = GasPrecharger::new(gas_counter, gas_allowance_counter);
401
402        let memory_size = charger.charge_gas_for_pages(
403            &block_config.page_costs,
404            &actor_data.allocations,
405            actor_data.static_pages,
406        )?;
407
408        charger.charge_gas_for_instantiation(
409            block_config.module_instantiation_byte_cost,
410            *code_len_bytes,
411        )?;
412
413        Ok(memory_size)
414    };
415
416    let memory_size = match f() {
417        Ok(size) => {
418            log::debug!("Charged for module instantiation and memory pages. Size: {size:?}");
419            size
420        }
421        Err(err) => {
422            log::debug!("Failed to charge for module instantiation or memory pages: {err:?}");
423            let reason = match err {
424                PrechargeError::BlockGasExceeded => {
425                    return Err(process_allowance_exceed(
426                        context.data.dispatch,
427                        context.data.destination_id,
428                        context.data.gas_counter.burned(),
429                    ));
430                }
431                PrechargeError::GasExceeded(op) => {
432                    ActorExecutionErrorReplyReason::PreChargeGasLimitExceeded(op)
433                }
434            };
435
436            let system_reservation_ctx =
437                SystemReservationContext::from_dispatch(&context.data.dispatch);
438            return Err(process_error(
439                context.data.dispatch,
440                context.data.destination_id,
441                context.data.gas_counter.burned(),
442                system_reservation_ctx,
443                reason,
444                false,
445            ));
446        }
447    };
448
449    Ok(ContextChargedForMemory {
450        data: context.data,
451        max_reservations: block_config.max_reservations,
452        memory_size,
453    })
454}
455
456#[cfg(test)]
457mod tests {
458    use super::*;
459    use gear_backend_common::assert_ok;
460
461    fn prepare_gas_counters() -> (GasCounter, GasAllowanceCounter) {
462        (
463            GasCounter::new(1_000_000),
464            GasAllowanceCounter::new(4_000_000),
465        )
466    }
467
468    #[test]
469    fn gas_for_static_pages() {
470        let costs = PageCosts::new_for_tests();
471        let (mut gas_counter, mut gas_allowance_counter) = prepare_gas_counters();
472        let mut charger = GasPrecharger::new(&mut gas_counter, &mut gas_allowance_counter);
473        let static_pages = 4.into();
474        let res = charger.charge_gas_for_pages(&costs, &Default::default(), static_pages);
475        // Result is static pages count
476        assert_ok!(res, static_pages);
477        // Charging for static pages initialization
478        let charge = costs.static_page.calc(static_pages);
479        assert_eq!(charger.counter.left(), 1_000_000 - charge);
480        assert_eq!(charger.allowance_counter.left(), 4_000_000 - charge);
481    }
482}