From 261e6cb4beeaf259d20325f43b3c14e7bac1297e Mon Sep 17 00:00:00 2001 From: Victor Lopes Date: Fri, 29 Sep 2023 13:17:48 +0200 Subject: [PATCH] feature: gas meter (#795) * feat: add gas meter to working set 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. * fix lint fmt * fix ci test expected error string * update default context to 2 dimensions --- constants.json | 11 +- .../methods/guest-celestia/Cargo.lock | 5 - .../demo-prover/methods/guest-mock/Cargo.lock | 5 - module-system/README.md | 105 ++++++++++ .../sov-bank/src/lib.rs | 30 ++- module-system/sov-modules-api/README.md | 6 + .../sov-modules-api/src/default_context.rs | 6 +- module-system/sov-modules-api/src/gas.rs | 92 ++++++++ module-system/sov-modules-api/src/lib.rs | 17 ++ .../sov-modules-api/src/state/scratchpad.rs | 19 ++ module-system/sov-modules-macros/src/lib.rs | 2 +- .../sov-modules-macros/src/manifest.rs | 196 ++++++++++-------- .../sov-modules-macros/src/module_info.rs | 52 ++++- .../field_missing_attribute.stderr | 2 +- 14 files changed, 442 insertions(+), 106 deletions(-) create mode 100644 module-system/sov-modules-api/src/gas.rs diff --git a/constants.json b/constants.json index 96620c6e7..9beb309f4 100644 --- a/constants.json +++ b/constants.json @@ -1,3 +1,12 @@ { - "comment": "Sovereign SDK constants" + "comment": "Sovereign SDK constants", + "gas": { + "Bank": { + "create_token": [4, 4], + "transfer": [5, 5], + "burn": [2, 2], + "mint": [2, 2], + "freeze": [1, 1] + } + } } diff --git a/examples/demo-prover/methods/guest-celestia/Cargo.lock b/examples/demo-prover/methods/guest-celestia/Cargo.lock index da5807d78..b4574b135 100644 --- a/examples/demo-prover/methods/guest-celestia/Cargo.lock +++ b/examples/demo-prover/methods/guest-celestia/Cargo.lock @@ -1854,8 +1854,3 @@ dependencies = [ "quote", "syn 2.0.37", ] - -[[patch.unused]] -name = "cc" -version = "1.0.79" -source = "git+https://github.com/rust-lang/cc-rs?rev=e5bbdfa#e5bbdfa1fa468c028cb38fee6c35a3cf2e5a2736" diff --git a/examples/demo-prover/methods/guest-mock/Cargo.lock b/examples/demo-prover/methods/guest-mock/Cargo.lock index db5de049c..d4302d1f0 100644 --- a/examples/demo-prover/methods/guest-mock/Cargo.lock +++ b/examples/demo-prover/methods/guest-mock/Cargo.lock @@ -1272,8 +1272,3 @@ name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - -[[patch.unused]] -name = "cc" -version = "1.0.79" -source = "git+https://github.com/rust-lang/cc-rs?rev=e5bbdfa#e5bbdfa1fa468c028cb38fee6c35a3cf2e5a2736" diff --git a/module-system/README.md b/module-system/README.md index 4d8bf8d43..9812c4b1c 100644 --- a/module-system/README.md +++ b/module-system/README.md @@ -44,6 +44,111 @@ This has several consequences. First, it means that modules are always cheap to always yields the same result as calling `MyModule::new()`. Finally, it means that every method of the module which reads or modifies state needs to take a `WorkingSet` as an argument. +### Gas configuration + +The module might contain a field for the gas configuration. If annotated with `#[gas]` under a struct that derives `ModuleInfo`, it will attempt to read a `constants.json` file from the root of the project, and inject it into the `Default::default()` implementation of the module. + +Here is an example `constants.json` file: + +```json +{ + "gas": { + "create_token": 4, + "transfer": 5, + "burn": 2, + "mint": 2, + "freeze": 1 + } +} +``` + +The `ModuleInfo` macro will look for a `gas` field inside the JSON, that must be an object, and will look for the name of the module inside of the `gas` object. If present, it will parse that object as gas configuration; otherwise, it will parse the `gas` object directly. On the example above, it will attempt to parse a structure that looks like this: + +```rust +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct BankGasConfig { + pub create_token: GU, + pub transfer: GU, + pub burn: GU, + pub mint: GU, + pub freeze: GU, +} +``` + +The `GasUnit` generic type will be defined by the runtime `Context`. For `DefaultContext`, we use `TupleGasUnit<2>` - that is, a gas unit with a two dimensions. The same setup is defined for `ZkDefaultContext`. Here is an example of a `constants.json` file, specific to the `Bank` module: + +```json +{ + "gas": { + "comment": "this field will be ignored, as there is a matching module field", + "Bank": { + "create_token": [4, 19], + "transfer": [5, 25], + "burn": [2, 7], + "mint": [2, 6], + "freeze": [1, 4] + } + } +} +``` + +As you can see above, the fields can be either array, numeric, or boolean. If boolean, it will be converted to either `0` or `1`. If array, each element is expected to be either a numeric or boolean. The example above will create a gas unit of two dimensions. If the `Context` requires less dimensions than available, it will pick the first ones of relevance, and ignore the rest. That is: with a `Context` of one dimension, , the effective config will be expanded to: + +```rust +BankGasConfig { + create_token: [4], + transfer: [5], + burn: [2], + mint: [2], + freeze: [1], +} +``` + +In order to charge gas from the working set, the function `charge_gas` can be used. + +```rust +fn call( + &self, + msg: Self::CallMessage, + context: &Self::Context, + working_set: &mut WorkingSet, +) -> Result { + match msg { + call::CallMessage::CreateToken { + salt, + token_name, + initial_balance, + minter_address, + authorized_minters, + } => { + self.charge_gas(working_set, &self.gas.create_token)?; + // Implementation elided... +} +``` + +On the example above, we charge the configured unit from the working set. Concretely, we will charge a unit of `[4, 19]` from both `DefaultContext` and `ZkDefaultContext`. The working set will be the responsible to perform a scalar conversion from the dimensions to a single funds value. It will perform an inner product of the loaded price, with the provided unit. + +Let's assume we have a working set with the loaded price `[3, 2]`. The charged gas of the operation above will be `[3] · [4] = 3 × 4 = 12` for a single dimension context, and `[3, 2] · [4, 19] = 3 × 4 + 2 × 19 = 50` for both `DefaultContext` and `ZkDefaultContext`. This approach is intended to unlock [Dynamic Pricing](https://arxiv.org/abs/2208.07919). + +The aforementioned `Bank` struct, with the gas configuration, will look like this: + +```rust +#[derive(ModuleInfo)] +pub struct Bank { + /// The address of the bank module. + #[address] + pub(crate) address: C::Address, + + /// The gas configuration of the sov-bank module. + #[gas] + pub(crate) gas: BankGasConfig, + + /// A mapping of addresses to tokens in the bank. + #[state] + pub(crate) tokens: sov_state::StateMap>, +} +``` + ### Public Functions: The Module-to-Module Interface The first interface that modules expose is defined by the public methods from the rollup's `impl`. These methods are diff --git a/module-system/module-implementations/sov-bank/src/lib.rs b/module-system/module-implementations/sov-bank/src/lib.rs index e8d3425e4..7f3cece42 100644 --- a/module-system/module-implementations/sov-bank/src/lib.rs +++ b/module-system/module-implementations/sov-bank/src/lib.rs @@ -13,7 +13,7 @@ pub mod utils; /// Specifies the call methods using in that module. pub use call::CallMessage; use serde::{Deserialize, Serialize}; -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}; @@ -41,6 +41,25 @@ pub struct BankConfig { pub tokens: Vec>, } +/// Gas configuration for the bank module +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct BankGasConfig { + /// 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. @@ -52,6 +71,10 @@ pub struct Bank { #[address] pub(crate) address: C::Address, + /// The gas configuration of the sov-bank module. + #[gas] + pub(crate) gas: BankGasConfig, + /// A mapping of addresses to tokens in the sov-bank. #[state] pub(crate) tokens: sov_modules_api::StateMap>, @@ -82,6 +105,7 @@ impl sov_modules_api::Module for Bank { minter_address, authorized_minters, } => { + self.charge_gas(working_set, &self.gas.create_token)?; self.create_token( token_name, salt, @@ -95,10 +119,12 @@ impl sov_modules_api::Module for Bank { } 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)?) } @@ -106,11 +132,13 @@ impl sov_modules_api::Module for Bank { 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)?) } } diff --git a/module-system/sov-modules-api/README.md b/module-system/sov-modules-api/README.md index 5753e7eb5..9db055ed7 100644 --- a/module-system/sov-modules-api/README.md +++ b/module-system/sov-modules-api/README.md @@ -14,6 +14,9 @@ crate: - Interaction with user messages: The module must define the `call` method and the `CallMessage` type, which handle user messages. These messages typically result in changes to the module's state. + - Gas configuration: The module may use a `GasConfig` type, annotated by `#[gas]`, that will be loaded from the + constants manifest configuration. + 1. The `ModuleInfo` trait: Provides additional information related to a module. This trait is automatically derived. 1. The `Spec` trait: It defines all the types that modules are generic over. This separation allows the module logic to @@ -29,3 +32,6 @@ crate: 1. The `DispatchCall` trait: Defines how messages are forwarded to the appropriate module and how the call message is executed. The implementation of this trait can be generated automatically using a macro. + +1. The `GasUnit` trait: Defines how the scalar gas value is deducted from the working set. This is implemented for + `[u64; N]`, and can be customized by the user. diff --git a/module-system/sov-modules-api/src/default_context.rs b/module-system/sov-modules-api/src/default_context.rs index a07f6b735..8fccbcc41 100644 --- a/module-system/sov-modules-api/src/default_context.rs +++ b/module-system/sov-modules-api/src/default_context.rs @@ -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))] @@ -31,6 +31,8 @@ impl Spec for DefaultContext { #[cfg(feature = "native")] impl Context for DefaultContext { + type GasUnit = TupleGasUnit<2>; + fn sender(&self) -> &Self::Address { &self.sender } @@ -58,6 +60,8 @@ impl Spec for ZkDefaultContext { } impl Context for ZkDefaultContext { + type GasUnit = TupleGasUnit<2>; + fn sender(&self) -> &Self::Address { &self.sender } diff --git a/module-system/sov-modules-api/src/gas.rs b/module-system/sov-modules-api/src/gas.rs new file mode 100644 index 000000000..071af99fe --- /dev/null +++ b/module-system/sov-modules-api/src/gas.rs @@ -0,0 +1,92 @@ +use core::fmt; + +use anyhow::Result; + +/// 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 = [u64; N]; + +impl GasUnit for TupleGasUnit { + 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 +where + GU: GasUnit, +{ + remaining_funds: u64, + gas_price: GU, +} + +impl Default for GasMeter +where + GU: GasUnit, +{ + fn default() -> Self { + Self { + remaining_funds: 0, + gas_price: GU::ZEROED, + } + } +} + +impl GasMeter +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(()) + } +} diff --git a/module-system/sov-modules-api/src/lib.rs b/module-system/sov-modules-api/src/lib.rs index 053fdbd47..38ec31cfd 100644 --- a/module-system/sov-modules-api/src/lib.rs +++ b/module-system/sov-modules-api/src/lib.rs @@ -9,6 +9,7 @@ pub mod default_signature; mod dispatch; mod encode; mod error; +mod gas; pub mod hooks; mod pub_key_hex; @@ -46,6 +47,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")] @@ -256,6 +258,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; @@ -311,6 +316,17 @@ pub trait Module { ) -> Result { 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, + gas: &::GasUnit, + ) -> anyhow::Result<()> { + working_set.charge_gas(gas) + } } /// A [`Module`] that has a well-defined and known [JSON @@ -327,6 +343,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. diff --git a/module-system/sov-modules-api/src/state/scratchpad.rs b/module-system/sov-modules-api/src/state/scratchpad.rs index 11c32f281..62070c9d0 100644 --- a/module-system/sov-modules-api/src/state/scratchpad.rs +++ b/module-system/sov-modules-api/src/state/scratchpad.rs @@ -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, @@ -145,6 +146,7 @@ impl StateCheckpoint { delta: RevertableWriter::new(self.delta), accessory_delta: RevertableWriter::new(self.accessory_delta), events: Default::default(), + gas_meter: GasMeter::default(), } } @@ -181,6 +183,7 @@ pub struct WorkingSet { delta: RevertableWriter>, accessory_delta: RevertableWriter>, events: Vec, + gas_meter: GasMeter, } impl WorkingSet { @@ -249,6 +252,22 @@ impl WorkingSet { pub fn backing(&self) -> &::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) + } } impl StateReaderAndWriter for WorkingSet { diff --git a/module-system/sov-modules-macros/src/lib.rs b/module-system/sov-modules-macros/src/lib.rs index 4a52beeb5..c5bd5d1ca 100644 --- a/module-system/sov-modules-macros/src/lib.rs +++ b/module-system/sov-modules-macros/src/lib.rs @@ -36,7 +36,7 @@ use proc_macro::TokenStream; use rpc::ExposeRpcMacro; use syn::{parse_macro_input, DeriveInput, ItemFn}; -#[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); diff --git a/module-system/sov-modules-macros/src/manifest.rs b/module-system/sov-modules-macros/src/manifest.rs index 601010fce..5f778620f 100644 --- a/module-system/sov-modules-macros/src/manifest.rs +++ b/module-system/sov-modules-macros/src/manifest.rs @@ -1,19 +1,18 @@ -// TODO remove once consumed -#![allow(dead_code)] - use std::path::{Path, PathBuf}; use std::{env, fmt, fs, ops}; use proc_macro2::{Ident, TokenStream}; use serde_json::Value; +use syn::{PathArguments, Type, TypePath}; #[derive(Debug, Clone)] -pub struct Manifest { +pub struct Manifest<'a> { + parent: &'a Ident, path: PathBuf, value: Value, } -impl ops::Deref for Manifest { +impl<'a> ops::Deref for Manifest<'a> { type Target = Value; fn deref(&self) -> &Self::Target { @@ -21,20 +20,24 @@ impl ops::Deref for Manifest { } } -impl Manifest { +impl<'a> Manifest<'a> { /// Parse a manifest file from a string. /// /// The provided path will be used to feedback error to the user, if any. /// /// The `parent` is used to report the errors to the correct span location. - pub fn read_str(manifest: S, path: PathBuf, parent: &Ident) -> Result + pub fn read_str(manifest: S, path: PathBuf, parent: &'a Ident) -> Result where S: AsRef, { let value = serde_json::from_str(manifest.as_ref()) .map_err(|e| Self::err(&path, parent, format!("failed to parse manifest: {e}")))?; - Ok(Self { path, value }) + Ok(Self { + parent, + path, + value, + }) } /// Reads a `constants.json` manifest file, recursing from the target directory that builds the @@ -43,7 +46,7 @@ impl Manifest { /// If the environment variable `CONSTANTS_MANIFEST` is set, it will use that instead. /// /// The `parent` is used to report the errors to the correct span location. - pub fn read_constants(parent: &Ident) -> Result { + pub fn read_constants(parent: &'a Ident) -> Result { let manifest = "constants.json"; let initial_path = match env::var("CONSTANTS_MANIFEST") { Ok(p) => PathBuf::from(&p).canonicalize().map_err(|e| { @@ -123,19 +126,18 @@ impl Manifest { /// /// The `gas` field resolution will first attempt to query `gas.parent`, and then fallback to /// `gas`. They must be objects with arrays of integers as fields. - pub fn parse_gas_config( - &self, - parent: &Ident, - ) -> Result<(Ident, TokenStream, TokenStream), syn::Error> { - let root = self + pub fn parse_gas_config(&self, ty: &Type, field: &Ident) -> Result { + let map = self .value .as_object() - .ok_or_else(|| Self::err(&self.path, parent, "manifest is not an object"))? + .ok_or_else(|| Self::err(&self.path, field, "manifest is not an object"))?; + + let root = map .get("gas") .ok_or_else(|| { Self::err( &self.path, - parent, + field, "manifest does not contain a `gas` attribute", ) })? @@ -143,87 +145,95 @@ impl Manifest { .ok_or_else(|| { Self::err( &self.path, - parent, - format!("`gas` attribute of `{}` is not an object", parent), + field, + format!("`gas` attribute of `{}` is not an object", field), ) })?; - let root = match root.get(&parent.to_string()) { + let root = match root.get(&self.parent.to_string()) { Some(Value::Object(m)) => m, Some(_) => { return Err(Self::err( &self.path, - parent, - format!( - "matching constants entry `{}` is not an object", - &parent.to_string() - ), + field, + format!("matching constants entry `{}` is not an object", field), )) } None => root, }; let mut field_values = vec![]; - let mut fields = vec![]; for (k, v) in root { let k: Ident = syn::parse_str(k).map_err(|e| { Self::err( &self.path, - parent, + field, format!("failed to parse key attribyte `{}`: {}", k, e), ) })?; - let v = v - .as_array() - .ok_or_else(|| { - Self::err( - &self.path, - parent, - format!("`{}` attribute is not an array", k), - ) - })? - .iter() - .map(|v| { - v.as_u64().ok_or_else(|| { + let v = match v { + Value::Array(a) => a + .iter() + .map(|v| match v { + Value::Bool(b) => Ok(*b as u64), + Value::Number(n) => n.as_u64().ok_or_else(|| { + Self::err( + &self.path, + field, + format!( + "the value of the field `{k}` must be an array of valid `u64`" + ), + ) + }), + _ => Err(Self::err( + &self.path, + field, + format!( + "the value of the field `{k}` must be an array of numbers, or booleans" + ), + )), + }) + .collect::>()?, + Value::Number(n) => n + .as_u64() + .ok_or_else(|| { Self::err( &self.path, - parent, - format!("`{}` attribute is not an array of integers", k), + field, + format!("the value of the field `{k}` must be a `u64`"), ) }) - }) - .collect::, _>>()?; + .map(|n| vec![n])?, + Value::Bool(b) => vec![*b as u64], - let n = v.len(); - fields.push(quote::quote!(pub #k: [u64; #n])); - field_values.push(quote::quote!(#k: [#(#v,)*])); + _ => { + return Err(Self::err( + &self.path, + field, + format!( + "the value of the field `{k}` must be an array, number, or boolean" + ), + )) + } + }; + + field_values.push(quote::quote!(#k: <<::Context as ::sov_modules_api::Context>::GasUnit as ::sov_modules_api::GasUnit>::from_arbitrary_dimensions(&[#(#v,)*]))); } - let ty = format!("{parent}GasConfig"); - let ty = syn::parse_str(&ty).map_err(|e| { - Self::err( - &self.path, - parent, - format!("failed to parse type name `{}`: {}", ty, e), - ) - })?; - - let def = quote::quote! { - #[allow(missing_docs)] - #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] - pub struct #ty { - #(#fields,)* + // remove generics, if any + let mut ty = ty.clone(); + if let Type::Path(TypePath { path, .. }) = &mut ty { + if let Some(p) = path.segments.last_mut() { + p.arguments = PathArguments::None; } - }; + } - let decl = quote::quote! { - #ty { + Ok(quote::quote! { + let #field = #ty { #(#field_values,)* - } - }; - - Ok((ty, def, decl)) + }; + }) } fn err(path: P, ident: &syn::Ident, msg: T) -> syn::Error @@ -274,42 +284,54 @@ fn parse_gas_config_works() { }"#; let parent = Ident::new("Foo", proc_macro2::Span::call_site()); - let (ty, def, decl) = Manifest::read_str(input, PathBuf::from("foo.json"), &parent) + let gas_config: Type = syn::parse_str("FooGasConfig").unwrap(); + let field: Ident = syn::parse_str("foo_gas_config").unwrap(); + + let decl = Manifest::read_str(input, PathBuf::from("foo.json"), &parent) .unwrap() - .parse_gas_config(&parent) + .parse_gas_config(&gas_config, &field) .unwrap(); #[rustfmt::skip] assert_eq!( - ty.to_string(), + decl.to_string(), quote::quote!( - FooGasConfig + let foo_gas_config = FooGasConfig { + complex_math_operation: <<::Context as ::sov_modules_api::Context>::GasUnit as ::sov_modules_api::GasUnit>::from_arbitrary_dimensions(&[1u64, 2u64, 3u64, ]), + some_other_operation: <<::Context as ::sov_modules_api::Context>::GasUnit as ::sov_modules_api::GasUnit>::from_arbitrary_dimensions(&[4u64, 5u64, 6u64, ]), + }; ) .to_string() ); +} - #[rustfmt::skip] - assert_eq!( - def.to_string(), - quote::quote!( - #[allow(missing_docs)] - #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] - pub struct FooGasConfig { - pub complex_math_operation: [u64; 3usize], - pub some_other_operation: [u64; 3usize], - } - ) - .to_string() - ); +#[test] +fn parse_gas_config_single_dimension_works() { + let input = r#"{ + "comment": "Sovereign SDK constants", + "gas": { + "complex_math_operation": 1, + "some_other_operation": 2 + } + }"#; + + let parent = Ident::new("Foo", proc_macro2::Span::call_site()); + let gas_config: Type = syn::parse_str("FooGasConfig").unwrap(); + let field: Ident = syn::parse_str("foo_gas_config").unwrap(); + + let decl = Manifest::read_str(input, PathBuf::from("foo.json"), &parent) + .unwrap() + .parse_gas_config(&gas_config, &field) + .unwrap(); #[rustfmt::skip] assert_eq!( decl.to_string(), quote::quote!( - FooGasConfig { - complex_math_operation: [1u64, 2u64, 3u64, ], - some_other_operation: [4u64, 5u64, 6u64, ], - } + let foo_gas_config = FooGasConfig { + complex_math_operation: <<::Context as ::sov_modules_api::Context>::GasUnit as ::sov_modules_api::GasUnit>::from_arbitrary_dimensions(&[1u64, ]), + some_other_operation: <<::Context as ::sov_modules_api::Context>::GasUnit as ::sov_modules_api::GasUnit>::from_arbitrary_dimensions(&[2u64, ]), + }; ) .to_string() ); diff --git a/module-system/sov-modules-macros/src/module_info.rs b/module-system/sov-modules-macros/src/module_info.rs index 833947824..c199fbc00 100644 --- a/module-system/sov-modules-macros/src/module_info.rs +++ b/module-system/sov-modules-macros/src/module_info.rs @@ -5,6 +5,7 @@ use syn::{ use self::parsing::{ModuleField, ModuleFieldAttribute, StructDef}; use crate::common::get_generics_type_param; +use crate::manifest::Manifest; pub(crate) fn derive_module_info( input: DeriveInput, @@ -84,6 +85,10 @@ fn impl_module_info(struct_def: &StructDef) -> Result { + impl_self_init.push(make_init_gas_config(ident, field)?); + impl_self_body.push(&field.ident); + } }; } @@ -93,7 +98,6 @@ fn impl_module_info(struct_def: &StructDef) -> Result Self { #(#impl_self_init)* @@ -213,6 +217,16 @@ fn make_init_module(field: &ModuleField) -> Result Result { + let field_ident = &field.ident; + let ty = &field.ty; + + Manifest::read_constants(parent)?.parse_gas_config(ty, field_ident) +} + fn make_module_prefix_fn(struct_ident: &Ident) -> proc_macro2::TokenStream { let body = make_module_prefix_fn_body(struct_ident); quote::quote! { @@ -270,6 +284,7 @@ pub mod parsing { let (impl_generics, type_generics, where_clause) = input.generics.split_for_impl(); let fields = parse_module_fields(&input.data)?; check_exactly_one_address(&fields)?; + check_zero_or_one_gas(&fields)?; Ok(StructDef { ident, @@ -301,6 +316,7 @@ pub mod parsing { Module, State { codec_builder: Option }, Address, + Gas, } impl ModuleFieldAttribute { @@ -327,6 +343,16 @@ pub mod parsing { } } "state" => parse_state_attr(attr), + "gas" => { + if attr.tokens.is_empty() { + Ok(Self::Gas) + } else { + Err(syn::Error::new_spanned( + attr, + "The `#[gas]` attribute does not accept any arguments.", + )) + } + } _ => unreachable!("attribute names were validated already; this is a bug"), } } @@ -407,6 +433,24 @@ pub mod parsing { } } + fn check_zero_or_one_gas(fields: &[ModuleField]) -> syn::Result<()> { + let gas_fields = fields + .iter() + .filter(|field| matches!(field.attr, ModuleFieldAttribute::Gas)) + .collect::>(); + + match gas_fields.len() { + 0 | 1 => Ok(()), + _ => Err(syn::Error::new_spanned( + gas_fields[1].ident.clone(), + format!( + "The `gas` attribute is defined more than once, revisit field: {}", + gas_fields[1].ident, + ), + )), + } + } + fn data_to_struct(data: &syn::Data) -> syn::Result<&DataStruct> { match data { syn::Data::Struct(data_struct) => Ok(data_struct), @@ -433,9 +477,9 @@ pub mod parsing { let mut attr = None; for a in field.attrs.iter() { match a.path.segments[0].ident.to_string().as_str() { - "state" | "module" | "address" => { + "state" | "module" | "address" | "gas" => { if attr.is_some() { - return Err(syn::Error::new_spanned(ident, "Only one attribute out of `#[module]`, `#[state]` and `#[address]` is allowed per field.")); + return Err(syn::Error::new_spanned(ident, "Only one attribute out of `#[module]`, `#[state]`, `#[address]`, and #[gas] is allowed per field.")); } else { attr = Some(a); } @@ -449,7 +493,7 @@ pub mod parsing { } else { Err(syn::Error::new_spanned( ident, - format!("The field `{}` is missing an attribute: add `#[module]`, `#[state]` or `#[address]`.", ident), + format!("The field `{}` is missing an attribute: add `#[module]`, `#[state]`, `#[address]`, or #[gas].", ident), )) } } diff --git a/module-system/sov-modules-macros/tests/module_info/field_missing_attribute.stderr b/module-system/sov-modules-macros/tests/module_info/field_missing_attribute.stderr index 4cdbb45b5..6045d7148 100644 --- a/module-system/sov-modules-macros/tests/module_info/field_missing_attribute.stderr +++ b/module-system/sov-modules-macros/tests/module_info/field_missing_attribute.stderr @@ -1,4 +1,4 @@ -error: The field `test_state1` is missing an attribute: add `#[module]`, `#[state]` or `#[address]`. +error: The field `test_state1` is missing an attribute: add `#[module]`, `#[state]`, `#[address]`, or #[gas]. --> tests/module_info/field_missing_attribute.rs:8:5 | 8 | test_state1: StateMap,