From d1042382f4223085188c1a4da29178ae7153ecd5 Mon Sep 17 00:00:00 2001 From: Victor Lopez Date: Thu, 28 Sep 2023 17:51:01 +0200 Subject: [PATCH 1/4] 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. --- constants.json | 12 +- .../sov-bank/src/lib.rs | 29 +++- .../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 | 127 +++++++----------- .../sov-modules-macros/src/module_info.rs | 52 ++++++- 9 files changed, 269 insertions(+), 87 deletions(-) create mode 100644 module-system/sov-modules-api/src/gas.rs diff --git a/constants.json b/constants.json index 96620c6e7..4bb5296db 100644 --- a/constants.json +++ b/constants.json @@ -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] + } + } } diff --git a/module-system/module-implementations/sov-bank/src/lib.rs b/module-system/module-implementations/sov-bank/src/lib.rs index cd664fd3c..fb391fed7 100644 --- a/module-system/module-implementations/sov-bank/src/lib.rs +++ b/module-system/module-implementations/sov-bank/src/lib.rs @@ -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}; @@ -38,6 +38,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. @@ -49,6 +68,9 @@ pub struct Bank { #[address] pub(crate) address: C::Address, + #[gas] + pub(crate) gas: BankGasConfig, + /// A mapping of addresses to tokens in the sov-bank. #[state] pub(crate) tokens: sov_modules_api::StateMap>, @@ -79,6 +101,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, @@ -92,10 +115,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)?) } @@ -103,11 +128,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/src/default_context.rs b/module-system/sov-modules-api/src/default_context.rs index a07f6b735..38698c25b 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<1>; + 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..85afd2ab5 --- /dev/null +++ b/module-system/sov-modules-api/src/gas.rs @@ -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 = [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 b5e73d056..1446d0543 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; #[cfg(feature = "macros")] @@ -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")] @@ -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; @@ -305,6 +310,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 @@ -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. diff --git a/module-system/sov-modules-api/src/state/scratchpad.rs b/module-system/sov-modules-api/src/state/scratchpad.rs index 0f24ee058..7e15e4bf2 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, @@ -103,6 +104,7 @@ impl StateCheckpoint { delta: RevertableWriter::new(self.delta), accessory_delta: RevertableWriter::new(self.accessory_delta), events: Default::default(), + gas_meter: GasMeter::default(), } } @@ -186,6 +188,7 @@ pub struct WorkingSet { delta: RevertableWriter>, accessory_delta: RevertableWriter>, events: Vec, + gas_meter: GasMeter, } impl StateReaderAndWriter for WorkingSet { @@ -352,6 +355,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) + } } pub(crate) trait StateReaderAndWriter { diff --git a/module-system/sov-modules-macros/src/lib.rs b/module-system/sov-modules-macros/src/lib.rs index 4db6d96af..40a942af5 100644 --- a/module-system/sov-modules-macros/src/lib.rs +++ b/module-system/sov-modules-macros/src/lib.rs @@ -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); diff --git a/module-system/sov-modules-macros/src/manifest.rs b/module-system/sov-modules-macros/src/manifest.rs index 601010fce..d540f2940 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, &self.parent, "manifest is not an object"))?; + + let root = map .get("gas") .ok_or_else(|| { Self::err( &self.path, - parent, + &self.parent, "manifest does not contain a `gas` attribute", ) })? @@ -143,20 +145,20 @@ impl Manifest { .ok_or_else(|| { Self::err( &self.path, - parent, - format!("`gas` attribute of `{}` is not an object", parent), + &self.parent, + format!("`gas` attribute of `{}` is not an object", &self.parent), ) })?; - 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, + &self.parent, format!( "matching constants entry `{}` is not an object", - &parent.to_string() + self.parent.to_string() ), )) } @@ -164,12 +166,11 @@ impl Manifest { }; 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, + &self.parent, format!("failed to parse key attribyte `{}`: {}", k, e), ) })?; @@ -179,7 +180,7 @@ impl Manifest { .ok_or_else(|| { Self::err( &self.path, - parent, + &self.parent, format!("`{}` attribute is not an array", k), ) })? @@ -188,42 +189,29 @@ impl Manifest { v.as_u64().ok_or_else(|| { Self::err( &self.path, - parent, + &self.parent, format!("`{}` attribute is not an array of integers", k), ) }) }) .collect::, _>>()?; - let n = v.len(); - fields.push(quote::quote!(pub #k: [u64; #n])); - field_values.push(quote::quote!(#k: [#(#v,)*])); + 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 { + path.segments + .last_mut() + .map(|p| 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 @@ -267,6 +255,7 @@ fn fetch_manifest_works() { fn parse_gas_config_works() { let input = r#"{ "comment": "Sovereign SDK constants", + "gas_unit": "TupleGasUnit<5>", "gas": { "complex_math_operation": [1, 2, 3], "some_other_operation": [4, 5, 6] @@ -274,42 +263,22 @@ 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(), - quote::quote!( - FooGasConfig - ) - .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() - ); - #[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, 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() ); 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), )) } } From 85d21824134530061b91979e1457196f48cae66a Mon Sep 17 00:00:00 2001 From: Victor Lopez Date: Thu, 28 Sep 2023 17:57:56 +0200 Subject: [PATCH 2/4] fix lint fmt --- module-system/sov-modules-api/src/gas.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/module-system/sov-modules-api/src/gas.rs b/module-system/sov-modules-api/src/gas.rs index 85afd2ab5..071af99fe 100644 --- a/module-system/sov-modules-api/src/gas.rs +++ b/module-system/sov-modules-api/src/gas.rs @@ -1,7 +1,7 @@ -use anyhow::Result; - 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. From 48bc321e66df886bfe83e41b2720099cb817c3c6 Mon Sep 17 00:00:00 2001 From: Victor Lopez Date: Thu, 28 Sep 2023 18:15:58 +0200 Subject: [PATCH 3/4] fix ci test expected error string --- .../sov-modules-macros/src/manifest.rs | 27 +++++++++---------- .../field_missing_attribute.stderr | 2 +- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/module-system/sov-modules-macros/src/manifest.rs b/module-system/sov-modules-macros/src/manifest.rs index d540f2940..c8be16d07 100644 --- a/module-system/sov-modules-macros/src/manifest.rs +++ b/module-system/sov-modules-macros/src/manifest.rs @@ -130,14 +130,14 @@ impl<'a> Manifest<'a> { let map = self .value .as_object() - .ok_or_else(|| Self::err(&self.path, &self.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, - &self.parent, + field, "manifest does not contain a `gas` attribute", ) })? @@ -145,8 +145,8 @@ impl<'a> Manifest<'a> { .ok_or_else(|| { Self::err( &self.path, - &self.parent, - format!("`gas` attribute of `{}` is not an object", &self.parent), + field, + format!("`gas` attribute of `{}` is not an object", field), ) })?; @@ -155,11 +155,8 @@ impl<'a> Manifest<'a> { Some(_) => { return Err(Self::err( &self.path, - &self.parent, - format!( - "matching constants entry `{}` is not an object", - self.parent.to_string() - ), + field, + format!("matching constants entry `{}` is not an object", field), )) } None => root, @@ -170,7 +167,7 @@ impl<'a> Manifest<'a> { let k: Ident = syn::parse_str(k).map_err(|e| { Self::err( &self.path, - &self.parent, + field, format!("failed to parse key attribyte `{}`: {}", k, e), ) })?; @@ -180,7 +177,7 @@ impl<'a> Manifest<'a> { .ok_or_else(|| { Self::err( &self.path, - &self.parent, + field, format!("`{}` attribute is not an array", k), ) })? @@ -189,7 +186,7 @@ impl<'a> Manifest<'a> { v.as_u64().ok_or_else(|| { Self::err( &self.path, - &self.parent, + field, format!("`{}` attribute is not an array of integers", k), ) }) @@ -202,9 +199,9 @@ impl<'a> Manifest<'a> { // remove generics, if any let mut ty = ty.clone(); if let Type::Path(TypePath { path, .. }) = &mut ty { - path.segments - .last_mut() - .map(|p| p.arguments = PathArguments::None); + if let Some(p) = path.segments.last_mut() { + p.arguments = PathArguments::None; + } } Ok(quote::quote! { 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, From 006663682b2c0b648b7d49f3a42da73a2d71ca83 Mon Sep 17 00:00:00 2001 From: Victor Lopez Date: Fri, 29 Sep 2023 12:34:28 +0200 Subject: [PATCH 4/4] update default context to 2 dimensions --- constants.json | 10 +++++----- module-system/README.md | 10 ++++------ module-system/sov-modules-api/src/default_context.rs | 2 +- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/constants.json b/constants.json index 6bf96689a..9beb309f4 100644 --- a/constants.json +++ b/constants.json @@ -2,11 +2,11 @@ "comment": "Sovereign SDK constants", "gas": { "Bank": { - "create_token": 4, - "transfer": 5, - "burn": 2, - "mint": 2, - "freeze": 1 + "create_token": [4, 4], + "transfer": [5, 5], + "burn": [2, 2], + "mint": [2, 2], + "freeze": [1, 1] } } } diff --git a/module-system/README.md b/module-system/README.md index 69a0eceee..9812c4b1c 100644 --- a/module-system/README.md +++ b/module-system/README.md @@ -75,9 +75,7 @@ pub struct BankGasConfig { } ``` -The `GasUnit` generic type will be defined by the runtime `Context`. For `DefaultContext`, we use `TupleGasUnit<1>` - that is, a gas unit with a single dimension; in other words, a raw `u64`. For `ZkDefaultContext`, we use `TupleGasUnit<2>` - that is, a gas unit with two dimensions; one for the ZK context, the other for the native context. - -Here is an example of a `constants.json` file, specific to the `Bank` module: +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 { @@ -94,7 +92,7 @@ Here is an example of a `constants.json` file, specific to the `Bank` module: } ``` -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: if the setup above is executed under `DefaultContext` (i.e. only one dimension), the effective config will be: +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 { @@ -128,9 +126,9 @@ fn call( } ``` -On the example above, we charge the configured unit from the working set. Concretely, we will charge a unit of `[4]` from a `DefaultContext`, and `[4, 19]` from a `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. +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 `DefaultContext`, and `[3, 2] · [4, 19] = 3 × 4 + 2 × 19 = 50` from `ZkDefaultContext`. This approach is intended to unlock [Dynamic Pricing](https://arxiv.org/abs/2208.07919). +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: diff --git a/module-system/sov-modules-api/src/default_context.rs b/module-system/sov-modules-api/src/default_context.rs index 38698c25b..8fccbcc41 100644 --- a/module-system/sov-modules-api/src/default_context.rs +++ b/module-system/sov-modules-api/src/default_context.rs @@ -31,7 +31,7 @@ impl Spec for DefaultContext { #[cfg(feature = "native")] impl Context for DefaultContext { - type GasUnit = TupleGasUnit<1>; + type GasUnit = TupleGasUnit<2>; fn sender(&self) -> &Self::Address { &self.sender