Skip to content

Commit

Permalink
Refactor gas tracking in builtin actors
Browse files Browse the repository at this point in the history
  • Loading branch information
ekovalev committed Dec 10, 2024
1 parent fde9730 commit 4ab1f6e
Show file tree
Hide file tree
Showing 11 changed files with 280 additions and 424 deletions.
424 changes: 131 additions & 293 deletions pallets/gear-builtin/src/bls12_381.rs

Large diffs are not rendered by default.

135 changes: 83 additions & 52 deletions pallets/gear-builtin/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ use core_processor::{
};
use frame_support::dispatch::extract_actual_weight;
use gear_core::{
gas::GasCounter,
gas::{ChargeResult, GasAllowanceCounter, GasAmount, GasCounter},
ids::{hash, ProgramId},
message::{
ContextOutcomeDrain, DispatchKind, MessageContext, Payload, ReplyPacket, StoredDispatch,
Expand All @@ -81,7 +81,7 @@ pub type GasAllowanceOf<T> = <<T as Config>::BlockLimiter as BlockLimiter>::GasA

const LOG_TARGET: &str = "gear::builtin";

pub type ActorErrorHandleFn = HandleFn<BuiltinActorError>;
pub type ActorErrorHandleFn = HandleFn<BuiltinContext, BuiltinActorError>;

/// Built-in actor error type
#[derive(Encode, Decode, Clone, Debug, PartialEq, Eq, derive_more::Display)]
Expand Down Expand Up @@ -120,14 +120,53 @@ impl From<BuiltinActorError> for ActorExecutionErrorReplyReason {
}
}

/// TODO:
#[derive(Debug)]
pub struct BuiltinContext {
pub(crate) gas_counter: GasCounter,
pub(crate) gas_allowance_counter: GasAllowanceCounter,
}

impl BuiltinContext {
// Tries to charge the gas amount from the gas counters.
pub fn try_charge_gas(&mut self, amount: u64) -> Result<(), BuiltinActorError> {
if self.gas_counter.charge_if_enough(amount) == ChargeResult::NotEnough {
return Err(BuiltinActorError::InsufficientGas);
}

if self.gas_allowance_counter.charge_if_enough(amount) == ChargeResult::NotEnough {
return Err(BuiltinActorError::GasAllowanceExceeded);
}

Ok(())
}

// Checks if an amount of gas can be charged without actually modifying the inner counters.
pub fn can_charge_gas(&self, amount: u64) -> Result<(), BuiltinActorError> {
if self.gas_counter.left() < amount {
return Err(BuiltinActorError::InsufficientGas);
}

if self.gas_allowance_counter.left() < amount {
return Err(BuiltinActorError::GasAllowanceExceeded);
}

Ok(())
}

fn to_gas_amount(&self) -> GasAmount {
self.gas_counter.to_amount()
}
}

/// A trait representing an interface of a builtin actor that can handle a message
/// from message queue (a `StoredDispatch`) to produce an outcome and gas spent.
pub trait BuiltinActor {
/// Handles a message and returns a result and the actual gas spent.
fn handle(
dispatch: &StoredDispatch,
gas_limit: u64,
) -> (Result<Payload, BuiltinActorError>, u64);
context: &mut BuiltinContext,
) -> Result<Payload, BuiltinActorError>;

/// Returns the maximum gas that can be spent by the actor.
fn max_gas() -> u64;
Expand Down Expand Up @@ -234,49 +273,41 @@ pub mod pallet {
pub(crate) fn dispatch_call(
origin: ProgramId,
call: CallOf<T>,
gas_limit: u64,
) -> (Result<(), BuiltinActorError>, u64)
context: &mut BuiltinContext,
) -> Result<(), BuiltinActorError>
where
T::AccountId: Origin,
{
let call_info = call.get_dispatch_info();

// Necessary upfront gas sufficiency checks
let gas_cost = call_info.weight.ref_time();
if gas_limit < gas_cost {
return (Err(BuiltinActorError::InsufficientGas), 0_u64);
}
if GasAllowanceOf::<T>::get() < gas_cost {
return (Err(BuiltinActorError::GasAllowanceExceeded), 0_u64);
}
context.can_charge_gas(gas_cost)?;

// Execute call
let res = call.dispatch(frame_system::RawOrigin::Signed(origin.cast()).into());
let actual_gas = extract_actual_weight(&res, &call_info).ref_time();

match res {
Ok(_post_info) => {
log::debug!(
target: LOG_TARGET,
"Call dispatched successfully",
);
(Ok(()), actual_gas)
}
Err(e) => {
log::debug!(target: LOG_TARGET, "Error dispatching call: {:?}", e);
(
Err(BuiltinActorError::Custom(LimitedStr::from_small_str(
e.into(),
))),
actual_gas,
)
}
}
// Now actually charge the gas
context.try_charge_gas(actual_gas)?;

res.map(|_| {
log::debug!(
target: LOG_TARGET,
"Call dispatched successfully",
);
})
.map_err(|e| {
log::debug!(target: LOG_TARGET, "Error dispatching call: {:?}", e);

BuiltinActorError::Custom(LimitedStr::from_small_str(e.into()))
})
}
}
}

impl<T: Config> BuiltinDispatcherFactory for Pallet<T> {
type Context = BuiltinContext;
type Error = BuiltinActorError;

type Output = BuiltinRegistry<T>;
Expand Down Expand Up @@ -306,20 +337,21 @@ impl<T: Config> BuiltinRegistry<T> {
}

impl<T: Config> BuiltinDispatcher for BuiltinRegistry<T> {
type Context = BuiltinContext;
type Error = BuiltinActorError;

fn lookup<'a>(&'a self, id: &ProgramId) -> Option<BuiltinInfo<'a, Self::Error>> {
fn lookup<'a>(&'a self, id: &ProgramId) -> Option<BuiltinInfo<'a, Self::Context, Self::Error>> {
self.registry
.get(id)
.map(|(f, g)| BuiltinInfo::<'a, Self::Error> {
.map(|(f, g)| BuiltinInfo::<'a, Self::Context, Self::Error> {
handle: &**f,
max_gas: &**g,
})
}

fn run(
&self,
context: BuiltinInfo<Self::Error>,
context: BuiltinInfo<Self::Context, Self::Error>,
dispatch: StoredDispatch,
gas_limit: u64,
) -> Vec<JournalNote> {
Expand Down Expand Up @@ -350,34 +382,27 @@ impl<T: Config> BuiltinDispatcher for BuiltinRegistry<T> {
return process_allowance_exceed(dispatch.into_incoming(gas_limit), actor_id, 0);
}

// Creating a gas counter to track gas usage.
let mut gas_counter = GasCounter::new(gas_limit);

// TODO: Refactor the BuiltinActor::handle() signature to take a [mutable] gas counter
// and gas allowance counter as inputs. This way `handle` would return just a `Result`,
// with all gas-counting related information contained in the gas counters.
// Setting up the context to track gas usage.
let mut context = BuiltinContext {
gas_counter: GasCounter::new(gas_limit),
gas_allowance_counter: GasAllowanceCounter::new(current_gas_allowance),
};

// Actual call to the builtin actor
let (res, gas_spent) = handle(&dispatch, gas_limit);

// We rely on a builtin actor to perform the check for gas limit consistency before
// executing a message and report an error if the `gas_limit` was to have been exceeded.
// However, to avoid gas tree corruption error, we must not report as spent more gas than
// the amount reserved in gas tree (that is, `gas_limit`). Hence (just in case):
let gas_spent = gas_spent.min(gas_limit);

// Let the `gas_counter` know how much gas was spent.
let _ = gas_counter.charge(gas_spent);
let res = handle(&dispatch, &mut context);

let dispatch = dispatch.into_incoming(gas_limit);

// Consume the context and extract the amount of gas spent.
let gas_amount = context.to_gas_amount();

match res {
Ok(response_payload) => {
// Builtin actor call was successful and returned some payload.
log::debug!(target: LOG_TARGET, "Builtin call dispatched successfully");

let mut dispatch_result =
DispatchResult::success(dispatch.clone(), actor_id, gas_counter.to_amount());
DispatchResult::success(dispatch.clone(), actor_id, gas_amount);

// Create an artificial `MessageContext` object that will help us to generate
// a reply from the builtin actor.
Expand Down Expand Up @@ -411,14 +436,20 @@ impl<T: Config> BuiltinDispatcher for BuiltinRegistry<T> {
process_success(SuccessfulDispatchResultKind::Success, dispatch_result)
}
Err(BuiltinActorError::GasAllowanceExceeded) => {
process_allowance_exceed(dispatch, actor_id, gas_spent)
process_allowance_exceed(dispatch, actor_id, gas_amount.burned())
}
Err(err) => {
// Builtin actor call failed.
log::debug!(target: LOG_TARGET, "Builtin actor error: {:?}", err);
let system_reservation_ctx = SystemReservationContext::from_dispatch(&dispatch);
// The core processor will take care of creating necessary `JournalNote`'s.
process_execution_error(dispatch, actor_id, gas_spent, system_reservation_ctx, err)
process_execution_error(
dispatch,
actor_id,
gas_amount.burned(),
system_reservation_ctx,
err,
)
}
}
}
Expand Down
28 changes: 16 additions & 12 deletions pallets/gear-builtin/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

use crate::{
self as pallet_gear_builtin, bls12_381, proxy, ActorWithId, BuiltinActor, BuiltinActorError,
GasAllowanceOf,
BuiltinContext, GasAllowanceOf,
};
use common::{storage::Limiter, GasProvider, GasTree};
use core::cell::RefCell;
Expand Down Expand Up @@ -237,8 +237,8 @@ pub struct SuccessBuiltinActor {}
impl BuiltinActor for SuccessBuiltinActor {
fn handle(
dispatch: &StoredDispatch,
_gas_limit: u64,
) -> (Result<Payload, BuiltinActorError>, u64) {
context: &mut BuiltinContext,
) -> Result<Payload, BuiltinActorError> {
if !in_transaction() {
DEBUG_EXECUTION_TRACE.with(|d| {
d.borrow_mut().push(ExecutionTraceFrame {
Expand All @@ -252,8 +252,9 @@ impl BuiltinActor for SuccessBuiltinActor {

// Build the reply message
let payload = b"Success".to_vec().try_into().expect("Small vector");
context.try_charge_gas(1_000_000_u64)?;

(Ok(payload), 1_000_000_u64)
Ok(payload)
}

fn max_gas() -> u64 {
Expand All @@ -266,8 +267,8 @@ pub struct ErrorBuiltinActor {}
impl BuiltinActor for ErrorBuiltinActor {
fn handle(
dispatch: &StoredDispatch,
_gas_limit: u64,
) -> (Result<Payload, BuiltinActorError>, u64) {
context: &mut BuiltinContext,
) -> Result<Payload, BuiltinActorError> {
if !in_transaction() {
DEBUG_EXECUTION_TRACE.with(|d| {
d.borrow_mut().push(ExecutionTraceFrame {
Expand All @@ -278,7 +279,8 @@ impl BuiltinActor for ErrorBuiltinActor {
})
});
}
(Err(BuiltinActorError::InsufficientGas), 100_000_u64)
context.try_charge_gas(100_000_u64)?;
Err(BuiltinActorError::InsufficientGas)
}

fn max_gas() -> u64 {
Expand All @@ -291,9 +293,9 @@ pub struct HonestBuiltinActor {}
impl BuiltinActor for HonestBuiltinActor {
fn handle(
dispatch: &StoredDispatch,
gas_limit: u64,
) -> (Result<Payload, BuiltinActorError>, u64) {
let is_error = gas_limit < 500_000_u64;
context: &mut BuiltinContext,
) -> Result<Payload, BuiltinActorError> {
let is_error = context.to_gas_amount().left() < 500_000_u64;

if !in_transaction() {
DEBUG_EXECUTION_TRACE.with(|d| {
Expand All @@ -307,13 +309,15 @@ impl BuiltinActor for HonestBuiltinActor {
}

if is_error {
return (Err(BuiltinActorError::InsufficientGas), 100_000_u64);
context.try_charge_gas(100_000_u64)?;
return Err(BuiltinActorError::InsufficientGas);
}

// Build the reply message
let payload = b"Success".to_vec().try_into().expect("Small vector");
context.try_charge_gas(500_000_u64)?;

(Ok(payload), 500_000_u64)
Ok(payload)
}

fn max_gas() -> u64 {
Expand Down
18 changes: 6 additions & 12 deletions pallets/gear-builtin/src/proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,21 +77,15 @@ where
{
fn handle(
dispatch: &StoredDispatch,
gas_limit: u64,
) -> (Result<Payload, BuiltinActorError>, u64) {
let Ok(request) = Request::decode(&mut dispatch.payload_bytes()) else {
return (Err(BuiltinActorError::DecodingError), 0);
};
context: &mut BuiltinContext,
) -> Result<Payload, BuiltinActorError> {
let request = Request::decode(&mut dispatch.payload_bytes())
.map_err(|_| BuiltinActorError::DecodingError)?;

let origin = dispatch.source();

match Self::cast(request) {
Ok(call) => {
let (result, actual_gas) = Pallet::<T>::dispatch_call(origin, call, gas_limit);
(result.map(|_| Default::default()), actual_gas)
}
Err(e) => (Err(e), gas_limit),
}
let call = Self::cast(request)?;
Pallet::<T>::dispatch_call(origin, call, context).map(|_| Default::default())
}

fn max_gas() -> u64 {
Expand Down
15 changes: 6 additions & 9 deletions pallets/gear-builtin/src/staking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,8 @@ where
{
fn handle(
dispatch: &StoredDispatch,
gas_limit: u64,
) -> (Result<Payload, BuiltinActorError>, u64) {
context: &mut BuiltinContext,
) -> Result<Payload, BuiltinActorError> {
let message = dispatch.message();
let origin = dispatch.source();
let mut payload = message.payload_bytes();
Expand All @@ -122,19 +122,16 @@ where
* 11
/ 10;
if payload.len() > max_payload_size {
return (Err(BuiltinActorError::DecodingError), 0);
return Err(BuiltinActorError::DecodingError);
}

// Decode the message payload to derive the desired action
let Ok(request) = Request::decode(&mut payload) else {
return (Err(BuiltinActorError::DecodingError), 0);
};
let request =
Request::decode(&mut payload).map_err(|_| BuiltinActorError::DecodingError)?;

// Handle staking requests
let call = Self::cast(request);
let (result, gas_spent) = Pallet::<T>::dispatch_call(origin, call, gas_limit);

(result.map(|_| Default::default()), gas_spent)
Pallet::<T>::dispatch_call(origin, call, context).map(|_| Default::default())
}

fn max_gas() -> u64 {
Expand Down
Loading

0 comments on commit 4ab1f6e

Please sign in to comment.