Skip to content

Commit

Permalink
feat: add gas meter to working set
Browse files Browse the repository at this point in the history
This commit introduces `GasMeter`, encapsulated by `WorkingSet`.

It will allow the user to consume scalar gas from the working set, and
define arbitrary price parsed from a constants.json manifest file at
compilation. At each compilation, the `ModuleInfo` derive macro will
parse such file, and set the gas price configuration.
  • Loading branch information
vlopes11 committed Sep 28, 2023
1 parent bcb02f7 commit d104238
Show file tree
Hide file tree
Showing 9 changed files with 269 additions and 87 deletions.
12 changes: 11 additions & 1 deletion constants.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
{
"comment": "Sovereign SDK constants"
"comment": "Sovereign SDK constants",
"gas": {
"multiplier": [1],
"Bank": {
"create_token": [1],
"transfer": [1],
"burn": [1],
"mint": [1],
"freeze": [1]
}
}
}
29 changes: 28 additions & 1 deletion module-system/module-implementations/sov-bank/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ pub mod utils;

/// Specifies the call methods using in that module.
pub use call::CallMessage;
use sov_modules_api::{CallResponse, Error, ModuleInfo, WorkingSet};
use sov_modules_api::{CallResponse, Error, GasUnit, ModuleInfo, WorkingSet};
use token::Token;
/// Specifies an interface to interact with tokens.
pub use token::{Amount, Coins};
Expand All @@ -38,6 +38,25 @@ pub struct BankConfig<C: sov_modules_api::Context> {
pub tokens: Vec<TokenConfig<C>>,
}

/// Gas configuration for the bank module
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct BankGasConfig<GU: GasUnit> {
/// Gas price multiplier for the create token operation
pub create_token: GU,

/// Gas price multiplier for the transfer operation
pub transfer: GU,

/// Gas price multiplier for the burn operation
pub burn: GU,

/// Gas price multiplier for the mint operation
pub mint: GU,

/// Gas price multiplier for the freeze operation
pub freeze: GU,
}

/// The sov-bank module manages user balances. It provides functionality for:
/// - Token creation.
/// - Token transfers.
Expand All @@ -49,6 +68,9 @@ pub struct Bank<C: sov_modules_api::Context> {
#[address]
pub(crate) address: C::Address,

#[gas]
pub(crate) gas: BankGasConfig<C::GasUnit>,

/// A mapping of addresses to tokens in the sov-bank.
#[state]
pub(crate) tokens: sov_modules_api::StateMap<C::Address, Token<C>>,
Expand Down Expand Up @@ -79,6 +101,7 @@ impl<C: sov_modules_api::Context> sov_modules_api::Module for Bank<C> {
minter_address,
authorized_minters,
} => {
self.charge_gas(working_set, &self.gas.create_token)?;
self.create_token(
token_name,
salt,
Expand All @@ -92,22 +115,26 @@ impl<C: sov_modules_api::Context> sov_modules_api::Module for Bank<C> {
}

call::CallMessage::Transfer { to, coins } => {
self.charge_gas(working_set, &self.gas.create_token)?;
Ok(self.transfer(to, coins, context, working_set)?)
}

call::CallMessage::Burn { coins } => {
self.charge_gas(working_set, &self.gas.burn)?;
Ok(self.burn_from_eoa(coins, context, working_set)?)
}

call::CallMessage::Mint {
coins,
minter_address,
} => {
self.charge_gas(working_set, &self.gas.mint)?;
self.mint_from_eoa(&coins, &minter_address, context, working_set)?;
Ok(CallResponse::default())
}

call::CallMessage::Freeze { token_address } => {
self.charge_gas(working_set, &self.gas.freeze)?;
Ok(self.freeze(token_address, context, working_set)?)
}
}
Expand Down
6 changes: 5 additions & 1 deletion module-system/sov-modules-api/src/default_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use sov_state::{ArrayWitness, DefaultStorageSpec, ZkStorage};
#[cfg(feature = "native")]
use crate::default_signature::private_key::DefaultPrivateKey;
use crate::default_signature::{DefaultPublicKey, DefaultSignature};
use crate::{Address, Context, PublicKey, Spec};
use crate::{Address, Context, PublicKey, Spec, TupleGasUnit};

#[cfg(feature = "native")]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
Expand All @@ -31,6 +31,8 @@ impl Spec for DefaultContext {

#[cfg(feature = "native")]
impl Context for DefaultContext {
type GasUnit = TupleGasUnit<1>;

fn sender(&self) -> &Self::Address {
&self.sender
}
Expand Down Expand Up @@ -58,6 +60,8 @@ impl Spec for ZkDefaultContext {
}

impl Context for ZkDefaultContext {
type GasUnit = TupleGasUnit<2>;

fn sender(&self) -> &Self::Address {
&self.sender
}
Expand Down
92 changes: 92 additions & 0 deletions module-system/sov-modules-api/src/gas.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
use anyhow::Result;

use core::fmt;

/// A gas unit that provides scalar conversion from complex, multi-dimensional types.
pub trait GasUnit: fmt::Debug + Clone {
/// A zeroed instance of the unit.
const ZEROED: Self;

/// Creates a unit from a multi-dimensional unit with arbitrary dimension.
fn from_arbitrary_dimensions(dimensions: &[u64]) -> Self;

/// Converts the unit into a scalar value, given a price.
fn value(&self, price: &Self) -> u64;
}

/// A multi-dimensional gas unit.
pub type TupleGasUnit<const N: usize> = [u64; N];

impl<const N: usize> GasUnit for TupleGasUnit<N> {
const ZEROED: Self = [0; N];

fn from_arbitrary_dimensions(dimensions: &[u64]) -> Self {
// as demonstrated on the link below, the compiler can easily optimize the conversion as if
// it is a transparent type.
//
// https://rust.godbolt.org/z/rPhaxnPEY
let mut unit = Self::ZEROED;
unit.iter_mut()
.zip(dimensions.iter().copied())
.for_each(|(a, b)| *a = b);
unit
}

fn value(&self, price: &Self) -> u64 {
self.iter()
.zip(price.iter().copied())
.map(|(a, b)| a.saturating_mul(b))
.fold(0, |a, b| a.saturating_add(b))
}
}

/// A gas meter.
pub struct GasMeter<GU>
where
GU: GasUnit,
{
remaining_funds: u64,
gas_price: GU,
}

impl<GU> Default for GasMeter<GU>
where
GU: GasUnit,
{
fn default() -> Self {
Self {
remaining_funds: 0,
gas_price: GU::ZEROED,
}
}
}

impl<GU> GasMeter<GU>
where
GU: GasUnit,
{
/// Creates a new instance of the gas meter with the provided price.
pub fn new(remaining_funds: u64, gas_price: GU) -> Self {
Self {
remaining_funds,
gas_price,
}
}

/// Returns the remaining gas funds.
pub const fn remaining_funds(&self) -> u64 {
self.remaining_funds
}

/// Deducts the provided gas unit from the remaining funds, computing the scalar value of the
/// funds from the price of the instance.
pub fn charge_gas(&mut self, gas: &GU) -> Result<()> {
let gas = gas.value(&self.gas_price);
self.remaining_funds = self
.remaining_funds
.checked_sub(gas)
.ok_or_else(|| anyhow::anyhow!("Not enough gas"))?;

Ok(())
}
}
17 changes: 17 additions & 0 deletions module-system/sov-modules-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub mod default_signature;
mod dispatch;
mod encode;
mod error;
mod gas;
pub mod hooks;

#[cfg(feature = "macros")]
Expand Down Expand Up @@ -45,6 +46,7 @@ use digest::Digest;
pub use dispatch::CliWallet;
pub use dispatch::{DispatchCall, EncodeCall, Genesis};
pub use error::Error;
pub use gas::{GasUnit, TupleGasUnit};
pub use prefix::Prefix;
pub use response::CallResponse;
#[cfg(feature = "native")]
Expand Down Expand Up @@ -250,6 +252,9 @@ pub trait Spec {
/// instance of the state transition function. By making modules generic over a `Context`, developers
/// can easily update their cryptography to conform to the needs of different zk-proof systems.
pub trait Context: Spec + Clone + Debug + PartialEq + 'static {
/// Gas unit for the gas price computation.
type GasUnit: GasUnit;

/// Sender of the transaction.
fn sender(&self) -> &Self::Address;

Expand Down Expand Up @@ -305,6 +310,17 @@ pub trait Module {
) -> Result<CallResponse, Error> {
unreachable!()
}

/// Attempts to charge the provided amount of gas from the working set.
///
/// The scalar gas value will be computed from the price defined on the working set.
fn charge_gas(
&self,
working_set: &mut WorkingSet<Self::Context>,
gas: &<Self::Context as Context>::GasUnit,
) -> anyhow::Result<()> {
working_set.charge_gas(gas)
}
}

/// A [`Module`] that has a well-defined and known [JSON
Expand All @@ -321,6 +337,7 @@ pub trait ModuleCallJsonSchema: Module {

/// Every module has to implement this trait.
pub trait ModuleInfo {
/// Execution context.
type Context: Context;

/// Returns address of the module.
Expand Down
19 changes: 19 additions & 0 deletions module-system/sov-modules-api/src/state/scratchpad.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use sov_state::codec::{EncodeKeyLike, StateCodec, StateValueCodec};
use sov_state::storage::{Storage, StorageKey, StorageValue};
use sov_state::{OrderedReadsAndWrites, Prefix, StorageInternalCache};

use crate::gas::GasMeter;
use crate::{Context, Spec};

/// A working set accumulates reads and writes on top of the underlying DB,
Expand Down Expand Up @@ -103,6 +104,7 @@ impl<C: Context> StateCheckpoint<C> {
delta: RevertableWriter::new(self.delta),
accessory_delta: RevertableWriter::new(self.accessory_delta),
events: Default::default(),
gas_meter: GasMeter::default(),
}
}

Expand Down Expand Up @@ -186,6 +188,7 @@ pub struct WorkingSet<C: Context> {
delta: RevertableWriter<Delta<C::Storage>>,
accessory_delta: RevertableWriter<AccessoryDelta<C::Storage>>,
events: Vec<Event>,
gas_meter: GasMeter<C::GasUnit>,
}

impl<C: Context> StateReaderAndWriter for WorkingSet<C> {
Expand Down Expand Up @@ -352,6 +355,22 @@ impl<C: Context> WorkingSet<C> {
pub fn backing(&self) -> &<C as Spec>::Storage {
&self.delta.inner.inner
}

/// Returns the remaining gas funds.
pub const fn gas_remaining_funds(&self) -> u64 {
self.gas_meter.remaining_funds()
}

/// Overrides the current gas settings with the provided values.
pub fn set_gas(&mut self, funds: u64, gas_price: C::GasUnit) {
self.gas_meter = GasMeter::new(funds, gas_price);
}

/// Attempts to charge the provided gas unit from the gas meter, using the internal price to
/// compute the scalar value.
pub fn charge_gas(&mut self, gas: &C::GasUnit) -> anyhow::Result<()> {
self.gas_meter.charge_gas(gas)
}
}

pub(crate) trait StateReaderAndWriter {
Expand Down
2 changes: 1 addition & 1 deletion module-system/sov-modules-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ use proc_macro::TokenStream;
use rpc::ExposeRpcMacro;
use syn::{parse_macro_input, DeriveInput};

#[proc_macro_derive(ModuleInfo, attributes(state, module, address))]
#[proc_macro_derive(ModuleInfo, attributes(state, module, address, gas))]
pub fn module_info(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input);

Expand Down
Loading

0 comments on commit d104238

Please sign in to comment.